From 79375a55955cae888e44044f0de376447927bec1 Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Fri, 9 Oct 2020 10:18:57 -0500 Subject: [PATCH] Batch JSON RPC calls using an RPC provider from Uniswap This is taken from https://github.com/Uniswap/uniswap-interface/blob/master/src/connectors/NetworkConnector.ts#L31 --- src/api/EthereumAccountManager.ts | 132 +++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 1 deletion(-) diff --git a/src/api/EthereumAccountManager.ts b/src/api/EthereumAccountManager.ts index 42840f41..b1586abf 100644 --- a/src/api/EthereumAccountManager.ts +++ b/src/api/EthereumAccountManager.ts @@ -4,6 +4,136 @@ import { providers, Contract, Wallet, utils } from 'ethers'; import { EthAddress } from '../_types/global/GlobalTypes'; import { address } from '../utils/CheckedTypeUtils'; +// taken from ethers.js, compatible interface with web3 provider +type AsyncSendable = { + isMetaMask?: boolean + host?: string + path?: string + sendAsync?: (request: any, callback: (error: any, response: any) => void) => void + send?: (request: any, callback: (error: any, response: any) => void) => void +} + +class RequestError extends Error { + constructor(message: string, public code: number, public data?: unknown) { + super(message) + } +} + +interface BatchItem { + request: { jsonrpc: '2.0'; id: number; method: string; params: unknown } + resolve: (result: any) => void + reject: (error: Error) => void +} + +class MiniRpcProvider implements AsyncSendable { + public readonly isMetaMask: false = false + public readonly chainId: number + public readonly url: string + public readonly host: string + public readonly path: string + public readonly batchWaitTimeMs: number + + private nextId = 1 + private batchTimeoutId: ReturnType | null = null + private batch: BatchItem[] = [] + + constructor(chainId: number, url: string, batchWaitTimeMs?: number) { + this.chainId = chainId + this.url = url + const parsed = new URL(url) + this.host = parsed.host + this.path = parsed.pathname + // how long to wait to batch calls + this.batchWaitTimeMs = batchWaitTimeMs ?? 50 + } + + public readonly clearBatch = async () => { + console.debug('Clearing batch', this.batch) + const batch = this.batch + this.batch = [] + this.batchTimeoutId = null + let response: Response + try { + response = await fetch(this.url, { + method: 'POST', + headers: { 'content-type': 'application/json', accept: 'application/json' }, + body: JSON.stringify(batch.map(item => item.request)) + }) + } catch (error) { + batch.forEach(({ reject }) => reject(new Error('Failed to send batch call'))) + return + } + + if (!response.ok) { + batch.forEach(({ reject }) => reject(new RequestError(`${response.status}: ${response.statusText}`, -32000))) + return + } + + let json + try { + json = await response.json() + } catch (error) { + batch.forEach(({ reject }) => reject(new Error('Failed to parse JSON response'))) + return + } + const byKey = batch.reduce<{ [id: number]: BatchItem }>((memo, current) => { + memo[current.request.id] = current + return memo + }, {}) + for (const result of json) { + const { + resolve, + reject, + request: { method } + } = byKey[result.id] + if (resolve && reject) { + if ('error' in result) { + reject(new RequestError(result?.error?.message, result?.error?.code, result?.error?.data)) + } else if ('result' in result) { + resolve(result.result) + } else { + reject(new RequestError(`Received unexpected JSON-RPC response to ${method} request.`, -32000, result)) + } + } + } + } + + public readonly sendAsync = ( + request: { jsonrpc: '2.0'; id: number | string | null; method: string; params?: unknown[] | object }, + callback: (error: any, response: any) => void + ): void => { + this.request(request.method, request.params) + .then(result => callback(null, { jsonrpc: '2.0', id: request.id, result })) + .catch(error => callback(error, null)) + } + + public readonly request = async ( + method: string | { method: string; params: unknown[] }, + params?: unknown[] | object + ): Promise => { + if (typeof method !== 'string') { + return this.request(method.method, method.params) + } + if (method === 'eth_chainId') { + return `0x${this.chainId.toString(16)}` + } + const promise = new Promise((resolve, reject) => { + this.batch.push({ + request: { + jsonrpc: '2.0', + id: this.nextId++, + method, + params + }, + resolve, + reject + }) + }) + this.batchTimeoutId = this.batchTimeoutId ?? setTimeout(this.clearBatch, this.batchWaitTimeMs) + return promise + } +} + class EthereumAccountManager { static instance: EthereumAccountManager | null = null; @@ -14,7 +144,7 @@ class EthereumAccountManager { private constructor() { const isProd = process.env.NODE_ENV === 'production'; const url = isProd ? 'https://rpc.xdaichain.com/' : 'http://localhost:8545'; - this.provider = new providers.JsonRpcProvider(url); + this.provider = new providers.Web3Provider((new MiniRpcProvider(100, url, 100)) as providers.ExternalProvider); this.signer = null; this.knownAddresses = []; const knownAddressesStr = localStorage.getItem('KNOWN_ADDRESSES');