UNPKG

0xweb

Version:

Contract package manager and other web3 tools

1,117 lines (973 loc) 40.4 kB
import alot from 'alot'; import memd from 'memd'; import { $date } from '@dequanto/utils/$date'; import { $number } from '@dequanto/utils/$number'; import { $require } from '@dequanto/utils/$require'; import { $contract } from '@dequanto/utils/$contract'; import { $logger, l } from '@dequanto/utils/$logger'; import { $promise } from '@dequanto/utils/$promise'; import { $array } from '@dequanto/utils/$array'; import { PromiseEventWrap } from './model/PromiEventWrap'; import { IWeb3ClientStatus } from './interfaces/IWeb3ClientStatus'; import { ClientStatus } from './model/ClientStatus'; import { ClientPoolTrace, ClientPoolTraceError, ErrorCode } from './ClientPoolStats'; import { class_Dfr } from 'atma-utils'; import { ClientErrorUtil } from './utils/ClientErrorUtil'; import { IWeb3ClientOptions } from './interfaces/IWeb3Client'; import { RateLimitGuard } from './handlers/RateLimitGuard'; import { Web3BatchRequests } from './Web3BatchRequests'; import { Rpc, RpcTypes } from '@dequanto/rpc/Rpc'; import { PromiseEvent } from '@dequanto/class/PromiseEvent'; import { TTransport } from '@dequanto/rpc/transports/ITransport'; import { TRpc } from '@dequanto/rpc/RpcBase'; import { TEth } from '@dequanto/models/TEth'; import { $rpc } from '@dequanto/rpc/$rpc'; import { DataLike } from '@dequanto/utils/types'; export interface IRpcConfig { url?: string options?: TTransport.Options.Http | TTransport.Options.Ws /** Will be a preferred node for submitting transactions */ safe?: boolean distinct?: boolean wallet?: boolean web3?: TTransport.Transport | Promise<TTransport.Transport> name?: string /** Will be used only if manually requested with .getWeb3, or .getNodeUrl */ manual?: boolean /** max block range per request when getting for example the past logs*/ fetchableBlockRange?: number /** True if the node supports traceTransaction calls */ traceable?: boolean // requestCount/timeSpan: e.g. 100/1min rateLimit?: string // the max block range to fetch per single request when getting Logs blockRangeLimit?: string | number // maximum of requests to be batched, otherwise batches will be paginated batchLimit?: number // Will be used only for the specified methods methods?: { exclude?: string[] only?: string[] } } export interface IPoolWeb3Request { ws?: boolean preferSafe?: boolean /** Request wallet node to submit unsigned transactions */ wallet?: boolean distinct?: boolean name?: string /** HTTP Web3 Client traces */ trace?: ClientPoolTrace /** Supported node features */ node?: { // For specific url url?: string /** Supports traceTransaction */ traceable?: boolean } /** RPC method - will find the node that has the configuration */ method?: string // Amount of batch requests to be performed, sothat we can choose the appropriate wClient and handle the rate limits correctly batchRequestCount?: number // By fetching logs, specifies the desired block range to query blockRangeCount?: number } export class ClientPool { private discoveredPartial = false; private discoveredFull = false; private clients: WClient[]; private ws: WClient; /** Minimum rate limit range to assume the RPC is alive */ MINIMUM_BLOCK_RANGE = 200 constructor(config: IWeb3ClientOptions) { if (config.endpoints != null && config.endpoints.length > 0) { this.clients = config.endpoints.map(cfg => new WClient(cfg)) } else if (config.web3 || config.provider) { this.clients = [new WClient({ web3: config.web3 ?? config.provider })]; } else { console.dir(config, { depth: null }); throw new Error(`Neither Node endpoints nor Web3 instance`) } if (this.clients.length < 2) { this.discoveredPartial = true; this.discoveredFull = true; } } // callSync<TResult>(fn: (web3: Web3) => TResult): TResult { // let arr = this.clients.filter(x => x.status === 'ok'); // let wClient = arr[$number.randomInt(0, arr.length)]; // let { status, result } = wClient.callSync(fn); // if (status == ClientStatus.Ok) { // return result; // } // throw result; // } async callBatched(data: { requests: (rpc: Rpc) => Promise<Web3BatchRequests.IRpcRequest[]> map?: (results: any[]) => any }, opts?: IPoolWeb3Request) { return this.call(async (wClient) => { let requests = await data.requests(wClient.rpc); let results = await wClient.callBatched(requests); let mapped = data?.map?.(results) ?? results; return mapped; }, { ...(opts ?? {}), /** * web3@1.6.0 has a bug with batch request via websockets, as the callback can stuck if a single response contains multiple IDs, as only the first one will be taken * https://github.com/web3/web3.js/blob/9238e106294784b4a6a20af020765973f0437022/packages/web3-providers-ws/src/index.js#L128 */ ws: false }); } async call<TResult>(fn: (wClient: WClient) => Promise<TResult>, opts?: IPoolWeb3Request): Promise<TResult> { // Client - Retries let used = new Map<WClient, number>(); let errors = []; while (true) { let wClient = await this.next(used, opts); if (wClient == null) { let error = errors.pop(); if (error == null) { let urls = this .clients .map(x => ` ${x.config.url}`) .join('\n'); error = new Error(`Live clients not found in \n${urls}`); } throw ClientPoolTraceError.create(error, opts?.trace, ErrorCode.NO_LIVE_CLIENT); } let wClientUsage = used.get(wClient); let { status, result, error, time } = await wClient.call(fn, opts); if (error != null && error.data != null) { error.data = $contract.decodeCustomError(error.data, []); } opts ?.trace ?.onComplete({ status, error, time, url: wClient.config.url }) if (wClientUsage == null) { // per default NO_RETRIES used.set(wClient, 0); } else { // decrease retry count used.set(wClient, wClientUsage - 1); } errors.push(error ?? result); if (status == ClientStatus.Ok) { return result; } if (status == ClientStatus.RateLimited) { if (wClientUsage == null) { const RETRIES = 5; used.set(wClient, RETRIES); } } if (status === ClientStatus.CallError) { let error = ClientPoolTraceError.create(errors.pop(), opts?.trace, ErrorCode.CALL); throw error; return result; } // if not the CallError, process the while loop to check another NodeProvider } } async getRpc(options?: IPoolWeb3Request) { let wClient = await this.getWrappedWeb3(options); return wClient?.rpc; } async getWrappedWeb3(options?: IPoolWeb3Request) { let wClient = await this.next(null, options, { manual: true }); if (wClient == null) { throw new Error(`No client found in ${this.clients.length} Clients with options: ${JSON.stringify(options)}`); } return wClient; } async getNodeURL(options?: IPoolWeb3Request) { let wClient = await this.next(null, options, { manual: true }); if (wClient == null) { let stats = await this.getNodeStats(); let info = stats.map(x => ` ${x.url}. ERR: ${x.fail}; OK: ${x.success}; Ping: ${x.ping}`).join('\n'); let requirements = JSON.stringify(options); throw new Error(`No alive node for ${requirements} found. \n ${info}`); } return wClient?.config.url; } async releaseWeb3() { } getOptionForFetchableRange(blockRangeLimits?: WClient['blockRangeLimits']): number { const DEFAULT = blockRangeLimits?.blocks; let max = alot(this.clients).max(x => x.config?.fetchableBlockRange ?? 0); if (max === 0) { return DEFAULT; } if (typeof DEFAULT === 'number') { return Math.min(DEFAULT, max); } return max; } wrappedPromiEvent <T> (asyncFactory: () => Promise<T>): PromiseEvent<T> { let promiEvent = new PromiseEventWrap(); asyncFactory().then( (result) => { promiEvent.resolve(result); }, (error) => { promiEvent.reject(error); } ); return promiEvent as any as PromiseEvent<T>; } callPromiEvent<TResult extends PromiseEvent<any>>( fn: (web3: WClient) => TResult , opts?: { preferSafe?: boolean, parallel?: number, silent?: boolean, distinct?: boolean, wallet?: boolean } , used: Map<WClient, number> = new Map<WClient, number>() , errors = [] , root?: PromiseEventWrap ): TResult { root = root ?? new PromiseEventWrap(); (async () => { let wClient = await this.next(used, opts); if (wClient == null) { if (opts?.silent) { return root; } setTimeout(() => { let urls = this .clients .map(x => ` ${x.config.url}`) .join('\n'); let error = new Error(`Live clients not found in \n${urls}`); root.emit('error', error); root.reject(error); }); return root as any as TResult; } let promiseEvent = wClient.callPromiEvent(fn); root.bind(promiseEvent); promiseEvent.on('error', async error => { error.message += ` (RPC: ${wClient.config.url})`; if (ClientErrorUtil.isConnectionFailed(error)) { this.callPromiEvent( fn, opts, used, errors, root ); return; } if (ClientErrorUtil.isAlreadyKnown(error)) { $logger.log(`TxWriter ERROR ${error.message}. Check pending...`); let rpc = await this.getRpc(); let txs = await rpc.eth_pendingTransactions(); $logger.log('PENDING ', txs?.map(x => x.hash)); // throw anyway } root.emit('error', error); root.reject(error); }); used.set(wClient, 1); if (typeof opts?.parallel === 'number') { while (--opts.parallel > 0) { this.callPromiEvent( fn , { ...opts, distinct: true, parallel: null, silent: true } , used , errors , root ); } } })(); return root as any as TResult; } // getEventStream (address: TAddress, abi: TAbiItem[], event: string) { // if (this.ws == null) { // this.ws = this.clients.find(x => x.config.url?.startsWith('ws')); // } // let stream = this.ws.getEventStream(address, abi, event); // return stream; // } getNodeStats() { return this .clients .filter(client => client.getRequestCount() > 0) .map(client => { return { url: client.config.url, ...client.requests }; }); } async getNodeInfos(options?: { timeout?: number calls?: ('net_peerCount' | 'eth_blockNumber' | 'eth_syncing' | 'net_version')[] }): Promise<IWeb3ClientStatus[]> { const Calls = { async net_peerCount(wClient: WClient) { /** @TODO Public nodes smt. do not allow net_peerCount methods. Allow to switch this on/off on node-url-config level */ try { return await wClient.rpc.net_peerCount(); } catch (error) { return `ERROR: ${error.message}`; } }, async eth_syncing(wClient: WClient): Promise<IWeb3ClientStatus['syncing']> { let syncing = await wClient.rpc.eth_syncing(); if (syncing == null || typeof syncing === 'boolean') { return null; } return syncing as any; }, async net_version(wClient: WClient): Promise<IWeb3ClientStatus['node']> { return await wClient.rpc.net_version() }, async eth_blockNumber(wClient: WClient): Promise<IWeb3ClientStatus['blockNumber']> { return await wClient.rpc.eth_blockNumber() } } as const; async function query<TReturn extends Promise<any>>(wClient: WClient, method: (wClient: WClient) => TReturn): Promise<TReturn> { let methodName = method.name; let shouldCall = methodName === 'eth_blockNumber' || options?.calls == null || options?.calls.includes(methodName as any); if (shouldCall === false) { return null; } let promise = method(wClient); return options?.timeout == null ? promise : $promise.timeout(promise, options.timeout, `${methodName} in ${ wClient.config?.url }`); } let nodes = await alot(this.clients).mapAsync(async (wClient, idx) => { let url = wClient.config.url; try { let start = Date.now(); let [syncing, blockNumberUint, peers, node] = await Promise.all([ query(wClient, Calls.eth_syncing), query(wClient, Calls.eth_blockNumber), query(wClient, Calls.net_peerCount), query(wClient, Calls.net_version), ]); let blockNumber = Number(blockNumberUint); let requestsCount = options?.calls?.length ?? 4 let ping = Math.round((Date.now() - start) / requestsCount); let blockNumberBehind = 0; if (syncing?.currentBlock && syncing.currentBlock < blockNumber) { blockNumberBehind = blockNumber - Number(syncing.currentBlock); blockNumber = Number(syncing.currentBlock); } return <IWeb3ClientStatus>{ url: url, status: syncing ? 'sync' : (isNaN(blockNumber) ? 'off': 'live'), syncing: syncing, blockNumber: blockNumber, blockNumberBehind: blockNumberBehind, peers: Number(peers), pingMs: ping, node: node, i: idx, }; } catch (error) { return <IWeb3ClientStatus>{ url, status: 'error', error: error, peers: 0, i: idx, }; } }).toArrayAsync(); let max = alot(nodes).max(x => x.syncing?.highestBlock ?? (x.syncing as any)?.HighestBlock ?? x.blockNumber); nodes.forEach(node => { node.blockNumberBehind = node.blockNumber - max; }); return nodes; } private async next(used?: Map<WClient, number>, opts?: IPoolWeb3Request, params?: { manual?: boolean }): Promise<WClient> { let clients = this.clients; if (params?.manual !== true) { clients = clients.filter(x => x.config.manual !== true); } if (opts?.method != null) { let clientsWithSupportedMethod = clients.filter(x => { let methods = x.config.methods; if (methods == null) { return false; } if (methods.exclude?.includes(opts.method) === true) { return false; } if (methods.only != null) { return methods.only.includes(opts.method) === true; } return true; }); if (clientsWithSupportedMethod.length > 0) { // if there are no clients with manually specified method, use all clients clients = clientsWithSupportedMethod; } } else { // Unspecified method, so exclude clients with specific methods clients = clients.filter(x => x.config.methods == null); } if (opts?.ws === true) { if (this.ws == null) { this.ws = clients.find(x => x.config.url?.startsWith('ws')); } if (this.ws == null) { this.ws = clients.find(x => x.config.web3 != null); } return this.ws; } if (opts?.ws === false) { // filter out all WS providers (important for batched requests, as web3js has issues submitting multiple batch requests and handle response IDs) clients = clients.filter(x => x.config.url?.startsWith('http') || x.config.url == null); } if (opts?.node?.traceable === true) { clients = clients.filter(x => x.config.traceable === true); } if (opts?.node?.url != null) { clients = clients.filter(x => x.config.url === opts.node.url); } if (this.discoveredPartial === false) { await this.discoverLive().ready; this.discoveredPartial = true; } // we check OK clients first let okClients = clients.filter(x => x.status === 'ok'); if (okClients.length === 0) { // then switch to at least not off let notOffClients = clients.filter(x => x.status !== 'off'); if (notOffClients) { clients = notOffClients; } } else { clients = okClients; } let available = used == null ? clients : clients.filter(x => used.has(x) === false || used.get(x) > 0); if (available.length === 0) { if (this.discoveredFull === false) { await this.discoverLive().completed; this.discoveredFull = true; return this.next(used, opts); } return null; } let healthy = available.filter(x => x.healthy()); if (opts?.preferSafe === true) { let safe = healthy.filter(x => x.config.safe === true); if (safe.length > 0) { healthy = safe; } } if (opts?.distinct === true) { let safe = healthy.filter(x => x.config.distinct === true); if (safe.length > 0) { healthy = safe; } } if (opts?.wallet === true) { healthy = healthy.filter(x => x.config.wallet === true); } let arr = healthy.length > 0 ? healthy : available; if (opts?.blockRangeCount != null) { let upperThreshold = alot(arr).max(x => x.blockRangeLimits.blocks) * .5; arr = arr.filter(x => { let blocks = x.blockRangeLimits.blocks; if (blocks == null) { // Block limit is unknown yet return true; } if (blocks < this.MINIMUM_BLOCK_RANGE) { // Was not possible to load a minimum amount of blocks return false; } // Get if higher than 50% of max supported limit return x.blockRangeLimits.blocks >= upperThreshold }); } return await this.getClientWithLowestWaitTime(arr); } private async getClientWithLowestWaitTime(clients: WClient[]): Promise<WClient> { if (clients.length === 0) { return null; } clients = $array.shuffle(clients); let minWait = Infinity; let minClient: WClient = null; for (let i = 0; i < clients.length; i++) { let client = clients[i]; let waitMs = client.getRateLimitGuardTime(); if (waitMs === 0) { return client; } if (minWait > waitMs) { minWait = waitMs; minClient = client; } } const MAX_WAIT = 60_000; if (minWait > MAX_WAIT) { throw new Error(`rate limit overflows. Waiting ${minWait}ms`); } return minClient; } /** * We may have tens of Nodes to communicate with. Discover LIVE and operating nodes. * Resolves when first 3 active nodes are discovered, to prevent waiting for all of them. * @returns * - Ready Promise - in case 3 clients look good * - Complete Promise - when all clients are resolved */ @memd.deco.memoize({ perInstance: true }) private discoverLive(): { ready: class_Dfr, completed: class_Dfr } { this.clients.forEach(x => x.status = 'ping'); let ready = new class_Dfr(); let completed = new class_Dfr(); let clientInfos = [] as TClientInfo[]; let isReady = false as boolean; let isCompleted = false; let clients = this.clients; type TClientInfo = { i: number error?: Error status?: WClient['status'], blockNumberBehind: number blockNumber: number }; (async () => { let nodeInfosAsync = clients.map(async (wClient, idx) => { try { let clientInfo = <TClientInfo>{ i: idx, error: null, status: null, blockNumberBehind: 0, blockNumber: await $promise.timeout(wClient.rpc.eth_blockNumber(), 20_000) }; onIntermediateSuccess(clientInfo); return clientInfo; } catch (error) { return <TClientInfo>{ i: idx, error: error, status: 'off', blockNumberBehind: 0, blockNumber: 0 }; } }); let nodeInfos = await Promise.all(nodeInfosAsync); let hasLive = nodeInfos.some(x => x.status === 'ok'); if (hasLive === false) { let messages = nodeInfos.map(x => { let url = clients[x.i]?.config.url; let message = x.error?.message; console.log(`Stack`, x.error?.stack); return ` ${url}: ${message}`; }).join('\n'); let fullMessage = `No live nodes found: \n ${messages}`; let error = new Error(fullMessage); completed.reject(error); ready.reject(error); return; } const blockLatest = alot(nodeInfos).max(x => x.blockNumber); nodeInfos.forEach(info => { info.blockNumberBehind = info.blockNumber - blockLatest; }); nodeInfos.forEach(info => { this.clients[info.i].status = isOk(info); }); isCompleted = true; completed.resolve(); if (isReady !== true) { isReady = true; ready.resolve(); } })(); function isOk(info: TClientInfo): WClient['status'] { if (info.error) { return 'off'; } if (info.blockNumber == null || info.blockNumberBehind < -200n) { return 'off'; } return 'ok'; } function onIntermediateSuccess(info: TClientInfo) { const TOLERATE_BLOCK_COUNT = 5n; const WAIT_POOL_OK = Math.min(3, clientInfos.length); const count = clientInfos.push(info); if (isReady === true) { return; } if (count < WAIT_POOL_OK) { return; } let maxBlockNumber = alot(clientInfos).max(x => x.blockNumber); let ok = [] as TClientInfo[]; for (let info of clientInfos) { let diff = Math.abs(info.blockNumber - maxBlockNumber); if (diff <= TOLERATE_BLOCK_COUNT) { ok.push(info) } } if (ok.length >= WAIT_POOL_OK) { ok.forEach(info => { clients[info.i].status = info.status = 'ok'; }); isReady = true; ready.resolve(); } } return { ready, completed }; } } export class WClient { lastStatus = 0; lastDate = new Date(2000).getTime(); status: 'ok' | 'off' | 'ping' = 'ok' requests = { success: 0, fail: 0, ping: 0 }; rpc: Rpc config: IRpcConfig rateLimitGuard: RateLimitGuard /** For getLogs method, as some providers limit the range request or page result value */ blockRangeLimits: { blocks?: number results?: number } // Max requests per single Web3 Batch request batchLimit?: number healthy() { if (this.getRequestCount() === 0) { return true; } if (this.requests.fail === 0) { return true; } let health = this.requests.fail / this.getRequestCount(); if (health > .5) { return true; } if (Date.now() - this.lastDate > $date.parseTimespan('10m')) { return true; } return false; } private updateRateLimitInfo(info: ReturnType<typeof RateLimitGuard['extractRateLimitFromError']>) { if (this.rateLimitGuard == null) { this.rateLimitGuard = new RateLimitGuard({ id: this.config.url ?? 'web3', rates: [] }); } this.rateLimitGuard.updateRateLimitInfo(info); } public updateBlockRangeInfo(info: WClient['blockRangeLimits']) { if (this.blockRangeLimits == null) { this.blockRangeLimits = {}; } if (info.blocks != null) { this.blockRangeLimits.blocks = info.blocks; } if (info.results != null) { this.blockRangeLimits.results = info.results; } } constructor(mix: IRpcConfig) { const hasUrl = 'url' in mix && typeof mix.url === 'string'; const hasWeb3 = 'web3' in mix && typeof mix.web3 != null; if (hasUrl || hasWeb3) { this.config = mix; let transport = mix as TTransport.Options.Any; this.rpc = new Rpc(transport); } else { throw new Error(`Neither Node URL nor Web3 Instance in argument`); } this.blockRangeLimits = { blocks: Infinity }; if (mix.rateLimit) { let rates = RateLimitGuard.parseRateLimit(mix.rateLimit); this.rateLimitGuard = new RateLimitGuard({ id: this.config?.url ?? 'web3', rates: rates }) } if (mix.blockRangeLimit) { this.updateBlockRangeInfo({ blocks: $number.parse(mix.blockRangeLimit) }) } if (mix.batchLimit) { this.batchLimit = $number.parse(mix.batchLimit); } if (mix.fetchableBlockRange) { this.updateBlockRangeInfo({ blocks: $number.parse(mix.fetchableBlockRange) }); } } // @memd.deco.memoize() // private createWebSocketClient(url: string) { // let { options } = this.config; // // options = obj_extendDefaults(options ?? {}, { clientConfig: {} }); // // obj_extendDefaults((options as TTransport.Options.Ws).clientConfig, { // // // default frame size is too small // // maxReceivedFrameSize: 50_000_000, // // maxReceivedMessageSize: 50_000_000, // // }); // let transport = new WsTransport({ url, ...options }); // // transport.on('close', ev => this.websocket.code = ev.code); // // transport.on('connect', _ => this.websocket.code = WS_STATE.CONNECTED); // return transport; // } async send<TResult>(fn: (web3: WClient) => PromiseEvent<TResult>): Promise<{ status: ClientStatus, error?, result?: PromiseEvent<TResult> }> { return new Promise((resolve, reject) => { let result = fn(this); result.then( _ => { this.lastStatus = ClientStatus.Ok; this.requests.success++; resolve({ status: ClientStatus.Ok, result }) }, error => { if (ClientErrorUtil.isConnectionFailed(error)) { this.lastStatus = ClientStatus.NetworkError; this.requests.fail++; resolve({ status: ClientStatus.NetworkError, error }); } return resolve({ status: ClientStatus.CallError, result: error }); } ); }); } sendSignedTransaction (tx: TEth.Hex) { let promise = new PromiseEvent<TEth.TxReceipt>(); this .rpc .eth_sendRawTransaction(tx) .then(async hash => { promise.emit('transactionHash', hash); let receipt = await $rpc.waitForReceipt(this.rpc, hash); promise.resolve(receipt); }, error => { promise.reject(error); }); return promise; } sendTransaction (tx) { let promise = new PromiseEvent<TEth.TxReceipt>(); this .rpc .eth_sendTransaction(tx) .then(async hash => { promise.emit('transactionHash', hash); let receipt = await $rpc.waitForReceipt(this.rpc, hash); promise.resolve(receipt); }, error => { promise.reject(error); }); return promise; } sign (address: TEth.Address, message: string): Promise<string> { return this.rpc.eth_sign(address, message); } signTypedData (address: TEth.Address, typedData: DataLike<RpcTypes.TypedData>): Promise<TEth.Hex> { return this .rpc .eth_signTypedData_v4(address, typedData); } async callBatched<TResult = any>(requests: TRpc.IRpcAction[]): Promise<TResult[]> { let total = requests.length; let spanLimit = this.getSpanLimit(requests.length); let output = [] as TResult[]; let errors = []; let pageIdx = 0; while (requests.length > 0) { ++pageIdx; let page = requests.splice(0, spanLimit); if (requests.length > 0 || pageIdx > 1) { $logger.throttled(`Sending ${page.length} batched requests. Loaded ${output.length}/${total}`); } let { status, error, result: pageResult } = await this.call(async (client) => { let batch = new Web3BatchRequests.BatchRequest(client.rpc, page); let results = await batch.execute(); return results; }); if (status === ClientStatus.Ok) { output.push(...pageResult); continue; } if (status === ClientStatus.RateLimited) { spanLimit = this.getSpanLimit(requests.length); } errors.push(error); if (errors.length > 2) { throw error; } requests.unshift(...page); } return output; } async call<TResult extends PromiseLike<any>>(fn: (wClient: WClient) => TResult, options?: { // For the rate limit guard, to make sure we wait enough time to proceed with batch request for example batchRequestCount?: number }): Promise<{ status: ClientStatus, error?, result?: Awaited<TResult>, time: number }> { let now = Date.now(); await this.rateLimitGuard?.wait(options?.batchRequestCount ?? 1, now); let connectionError = await this.ensureConnected(); if (connectionError) { return { status: ClientStatus.NetworkError, result: null, error: connectionError, time: Date.now() - now }; } return new Promise((resolve, reject) => { let start = Date.now(); let result = fn(this); result.then( result => { let time = Date.now() - start; let status = ClientStatus.Ok; this.onComplete(status, time); this.rateLimitGuard?.onComplete(now); resolve({ status, result, time }) }, error => { let time = Date.now() - start; let status = ClientStatus.CallError; if (RateLimitGuard.isRateLimited(error)) { status = ClientStatus.RateLimited; let rateLimitInfo = RateLimitGuard.extractRateLimitFromError(error); if (rateLimitInfo == null) { l`RateLimit not extracted: ${error.message}`; } this.updateRateLimitInfo(rateLimitInfo); } else if (RateLimitGuard.isBatchLimit(error)) { status = ClientStatus.RateLimited; let limit = RateLimitGuard.extractBatchLimitFromError(error); if (limit !== this.batchLimit) { l`yellow<New BatchLimit> for "${this.config.url}" bold<${limit}>`; this.batchLimit = limit; } } else if (ClientErrorUtil.isConnectionFailed(error)) { status = ClientStatus.NetworkError; } this.onComplete(status, time); resolve({ status, error, time }) } ); }); } callPromiEvent<TResult extends PromiseEvent<any>>(fn: (web3: WClient) => TResult): TResult { let result = fn(this); result.on('error', error => { if (ClientErrorUtil.isConnectionFailed(error)) { this.lastStatus = ClientStatus.NetworkError; this.requests.fail++; } }); result.on('transactionHash', hash => { this.lastStatus = ClientStatus.Ok; this.requests.success++; }) return result; } callSubscription<TResult extends PromiseEvent<any>>(fn: (web3: WClient) => TResult): TResult { let result = fn(this); result.on('error', error => { if (ClientErrorUtil.isConnectionFailed(error)) { this.lastStatus = ClientStatus.NetworkError; this.requests.fail++; } }); result.on('transactionHash', hash => { this.lastStatus = ClientStatus.Ok; this.requests.success++; }) return result; } // callSync<TResult>(fn: (web3: Web3) => TResult): { status: number, result?: TResult } { // try { // let result = fn(this.web3); // return { status: ClientStatus.Ok, result }; // } catch (error) { // return { status: ClientStatus.CallError, result: error } // } // } onComplete(status: ClientStatus, timeMs: number) { let callCount = this.getRequestCount(); let ping = this.requests.ping; this.lastStatus = status; switch (status) { case ClientStatus.Ok: this.requests.success++; break; default: this.requests.fail++; break; } this.requests.ping = (ping * callCount + timeMs) / (callCount + 1); } async ensureConnected(): Promise<Error> { // if (this.config.url?.startsWith('ws')) { // if (this.websocket.code === WS_STATE.ECONNRESET) { // // recreate connection when ERRCONNRESET was thrown previously // this.createWebSocketClient(); // } // let web3 = this.web3; // let provider = web3.eth.currentProvider as WebsocketProvider & { url }; // if (provider.connected === false) { // provider.connect(); // try { // await $promise.waitForTrue(() => provider.connected, { // intervalMs: 200, // timeoutMessage: `Couldn't connect to WS ${provider.url}`, // timeoutMs: 20_000 // }); // return null; // } catch (error) { // return error; // } // } // } return null; } getRequestCount() { return this.requests.success + this.requests.fail; } /** * Checks the rate limit wait time, so that the POOL can select the wClient with shortest wait time **/ getRateLimitGuardTime() { return this.rateLimitGuard?.checkWaitTime() ?? 0; } private getSpanLimit(requestCount: number) { let a = this.rateLimitGuard?.getSpanLimit() ?? Infinity; let b = this.batchLimit ?? Infinity; let min = requestCount === 0 ? Math.min(a, b) : Math.min(a, b, requestCount); $require.gt(min, 0, `Span-limit must be > 0. ${a}/${b}/${requestCount}`); return min; } } const WS_STATE = { NOTSET: 0, CONNECTED: 1, ECONNRESET: 1006 };