@ton3/liteclient
Version:
TON Blockchain LiteClient
152 lines (115 loc) • 4.11 kB
text/typescript
import EventEmitter from 'events'
import { ADNLClientTCP, ADNLClientWS } from 'adnl'
import { BlockchainClient } from './blockchain'
import { LiteApi } from './liteapi'
import { hrtime } from 'process'
const PING_INTERVAL = 1000
const SYNC_INTERVAL = 2000
const MEASUREMENT_WINDOW = 60
interface LiteClientStats {
roundTimeTrip: number
outOfSync: number
head: number
}
class LiteClient {
private _isReady = false
public adnl: ADNLClientTCP
public api: BlockchainClient
public roundTimeTrips: number[] = []
public outOfSyncs: number[] = []
public utime: number
public seqno: number
public readonly events = new EventEmitter();
private constructor(
private readonly _url: string,
private readonly _publicKey: string | Uint8Array
) {
const adnlClient = this._url.toLowerCase().startsWith('tcp://')
? new ADNLClientTCP(this._url, this.publicKey)
: new ADNLClientWS(this._url, this.publicKey)
adnlClient.on('error', this.onError.bind(this))
adnlClient.on('ready', this.onReady.bind(this))
this.adnl = adnlClient
this.api = new BlockchainClient(new LiteApi(adnlClient))
adnlClient.connect()
}
private onReady (): void {
this._isReady = true
this.loop(this.ping.bind(this), PING_INTERVAL)
this.loop(this.sync.bind(this))
}
private onError (err: any): void {
// ...
}
public static create (url: string, publicKey: string | Uint8Array): LiteClient {
const liteServer = new LiteClient(url, publicKey)
return liteServer
}
private async ping (): Promise<void> {
if (!this.isReady) return Promise.resolve()
const start = hrtime()
await this.api.getTime()
const [ sec, nano ] = hrtime(start)
const rtt = Math.round((sec * 1e9 + nano) / 1e6)
if (this.roundTimeTrips.unshift(rtt) > MEASUREMENT_WINDOW) {
this.roundTimeTrips.pop()
}
}
private async sync (): Promise<void> {
if (!this.isReady) return Promise.resolve()
const wait = this.seqno ? { seqno: this.seqno + 1 } : undefined
const last = await this.api.getMasterchainInfo(wait)
const { info } = await this.api.getBlock(last)
const outOfSync = Math.floor(Date.now() / 1000) - info.gen_utime
this.events.emit('block', last)
this.seqno = info.seq_no
this.utime = info.gen_utime
if (this.outOfSyncs.unshift(outOfSync) > MEASUREMENT_WINDOW) {
this.outOfSyncs.pop()
}
}
private async delay (timeout: number): Promise<null> {
return new Promise(res => setTimeout(() => res(null), timeout))
}
private async loop (action: () => Promise<void>, sleep?: number): Promise<void> {
while (this.isReady) {
try {
await action()
if (!sleep) {
continue
}
await this.delay(sleep)
} catch (err) {
// TODO: handle error
}
}
}
public get url (): string {
return this._url
}
public get publicKey (): string | Uint8Array {
return this._publicKey
}
public get stats (): LiteClientStats {
const roundTimeTrip = this.roundTimeTrips.reduce((acc, el) => acc + el, 0) / this.roundTimeTrips.length
const outOfSync = this.outOfSyncs.reduce((acc, el) => acc + el, 0) / this.outOfSyncs.length
return {
roundTimeTrip: Math.round(roundTimeTrip),
outOfSync: Math.round(outOfSync),
head: this.seqno
}
}
public get isReady (): boolean {
return this._isReady
}
public get isSynced (): boolean {
if (!this.isReady || !this.utime) return false
const now = Math.round(Date.now() / 1000)
const diff = now - this.utime
return diff <= 30
}
public get isAlive (): boolean {
return this.isReady && this.isSynced
}
}
export { LiteClient }