UNPKG

@ylide/everscale

Version:

Ylide Protocol SDK implementation for EverScale blockchain

174 lines (153 loc) 5.3 kB
import type * as nt from 'nekoton-wasm'; import { GqlSocketParams } from 'everscale-standalone-client'; import { GqlSocket } from 'everscale-standalone-client/client/ConnectionController/gql'; import { IEverscaleMessage } from './types'; import { getContractMessagesQuery } from './gqlQueries'; type Endpoint = ReturnType<typeof GqlSocket['expandAddress']>; export class GqlSender implements nt.IGqlSender { private readonly params: GqlSocketParams; private readonly latencyDetectionInterval: number; private readonly endpoints: Endpoint[]; private nextLatencyDetectionTime: number = 0; private currentEndpoint?: Endpoint; private resolutionPromise?: Promise<Endpoint>; constructor(params: GqlSocketParams) { this.params = params; this.latencyDetectionInterval = params.latencyDetectionInterval || 60000; this.endpoints = params.endpoints.map(e => GqlSocket.expandAddress(e)); if (this.endpoints.length === 1) { this.currentEndpoint = this.endpoints[0]; this.nextLatencyDetectionTime = Number.MAX_VALUE; } } isLocal(): boolean { return !!this.params.local; } async send(data: string) { const now = Date.now(); try { let endpoint: Endpoint; if (this.currentEndpoint != null && now < this.nextLatencyDetectionTime) { // Default route endpoint = this.currentEndpoint; } else if (this.resolutionPromise != null) { // Already resolving endpoint = await this.resolutionPromise; delete this.resolutionPromise; } else { delete this.currentEndpoint; // Start resolving (current endpoint is null, or it is time to refresh) this.resolutionPromise = this._selectQueryingEndpoint().then(_endpoint => { this.currentEndpoint = _endpoint; this.nextLatencyDetectionTime = Date.now() + this.latencyDetectionInterval; return _endpoint; }); endpoint = await this.resolutionPromise; delete this.resolutionPromise; } return fetch(endpoint.url, { method: 'post', headers: { 'Content-Type': 'application/json', }, body: data, }).then(response => response.json()); } catch (e: any) { throw e; } } private async _selectQueryingEndpoint(): Promise<Endpoint> { const maxLatency = this.params.maxLatency || 60000; const endpointCount = this.endpoints.length; for (let retryCount = 0; retryCount < 5; ++retryCount) { let handlers: { resolve: (endpoint: Endpoint) => void; reject: () => void }; const promise = new Promise<Endpoint>((resolve, reject) => { handlers = { resolve: (endpoint: Endpoint) => resolve(endpoint), reject: () => reject(undefined), }; }); let checkedEndpoints = 0; let lastLatency: { endpoint: Endpoint; latency: number | undefined } | undefined; for (const endpoint of this.endpoints) { GqlSocket.checkLatency(endpoint).then(latency => { ++checkedEndpoints; if (latency !== undefined && latency <= maxLatency) { return handlers.resolve(endpoint); } if ( lastLatency === undefined || lastLatency.latency === undefined || (latency !== undefined && latency < lastLatency.latency) ) { lastLatency = { endpoint, latency }; } if (checkedEndpoints >= endpointCount) { if (lastLatency?.latency !== undefined) { handlers.resolve(lastLatency.endpoint); } else { handlers.reject(); } } }); } try { return await promise; } catch (e: any) { let resolveDelay: () => void; const delayPromise = new Promise<void>(resolve => { resolveDelay = () => resolve(); }); setTimeout(() => resolveDelay(), Math.min(100 * retryCount, 5000)); await delayPromise; } } throw new Error('Not available endpoint found'); } async query(query: string, variables: Record<string, any> = {}) { return this.send( JSON.stringify({ query, variables, }), ); } async queryContractMessages(dst: string, contractAddress: string, limit?: number): Promise<IEverscaleMessage[]> { const query = getContractMessagesQuery(dst, contractAddress, limit); return await this.queryMessages(query); } async queryMessage(query: string, variables: Record<string, any> = {}) { const data = await this.query(query, variables); if (!data || !data.data || !data.data.blockchain || !data.data.blockchain.message) { return null; } const m = data.data.blockchain.message; return { ...m, id: m.id.startsWith('message/') ? m.id.split('message/')[1] : m.id, cursor: null, } as IEverscaleMessage; } async queryMessages(query: string, variables: Record<string, any> = {}) { const data = await this.query(query, variables); if ( !data || !data.data || !data.data.blockchain || !data.data.blockchain.account || !data.data.blockchain.account.messages || !data.data.blockchain.account.messages.edges || !Array.isArray(data.data.blockchain.account.messages.edges) || !data.data.blockchain.account.messages.edges.length ) { return []; } const msgs = data.data.blockchain.account.messages.edges.map((e: any) => ({ ...e.node, id: e.node.id.startsWith('message/') ? e.node.id.split('message/')[1] : e.node.id, cursor: e.cursor, })) as IEverscaleMessage[]; msgs.sort((a, b) => b.created_at - a.created_at); return msgs; } }