import { hexValue } from '@ethersproject/bytes' import { numbers } from '@/constants' import { toChecksumAddress } from '@/utilities' import { Params, Address, RpcProvider, Transaction, ProviderOptions, OldRequestParams, ProviderInstance, OnListenerParams, GetBalanceParams, SendRequestParams, TransactionResult, TransactionByHash, BatchRequestParams, WaitForTxReceiptParams, } from './@types' // TODO create type & constants for RPC methods export class AbstractProvider implements ProviderInstance { protected readonly config: { callRetryAttempt: number } public version: string public address: string public networkId: number public provider: RpcProvider public constructor(options: ProviderOptions) { this.address = '' this.version = 'new' this.networkId = 1 this.config = { callRetryAttempt: 15, } this.provider = options.provider } public async setupProvider(): Promise
{ if (!this.provider) { throw new Error('Please, connect your wallet to the browser') } try { await this.checkVersion() return await this.initProvider() } catch (err) { throw new Error(`Provider method setupProvider has error: ${err.message}`) } } public async sendRequest(params: SendRequestParams): Promise { try { const args = this.prepareRequest(params) // TODO rename gasLimit to gas if (this.version === 'old') { return await this.sendAsync({ ...args, from: this.address }) } return await this.provider.request(args) } catch (err) { throw new Error(`Provider method sendRequest has error: ${err.message}`) } } public async getBalance({ address }: GetBalanceParams): Promise { const { callRetryAttempt } = this.config try { const params = { method: 'eth_getBalance', params: [address, 'latest'], } return await this.repeatRequestUntilResult(params, callRetryAttempt) } catch (err) { throw new Error(`Provider method getBalance has error: ${err.message}`) } } public async waitForTxReceipt({ txHash }: WaitForTxReceiptParams): Promise { const { callRetryAttempt } = this.config try { const multiplier = 10 const receiptParams = { method: 'eth_getTransactionReceipt', params: [txHash], } const txParams = { method: 'eth_getTransactionByHash', params: [txHash], } const totalAttempt = callRetryAttempt * multiplier const [receipt, transaction] = await Promise.all([ this.repeatRequestUntilResult(receiptParams, totalAttempt), this.repeatRequestUntilResult(txParams, totalAttempt), ]) return Object.assign(receipt, { value: transaction.value }) } catch (err) { throw new Error(`Provider method waitForTxReceipt has error: ${err.message}`) } } public async batchRequest({ txs, callback }: BatchRequestParams): Promise { try { const txsPromisesBucket = [] const EVERY_SECOND = 2 for (const [index, params] of txs.entries()) { const txPromise = this.sendRequest({ method: 'eth_sendTransaction', params: [params], }) await this.sleep(numbers.SECOND) if (index % EVERY_SECOND === numbers.ZERO && index !== numbers.ZERO) { await txPromise } txsPromisesBucket.push(txPromise) } if (typeof callback === 'function') { callback(txsPromisesBucket) } return await Promise.all(txsPromisesBucket) } catch (err) { throw new Error(err.message) } } public async checkNetworkVersion(): Promise { try { const result = await this.sendRequest({ method: 'eth_chainId' }) return Number(result) } catch (err) { throw new Error(`Provider method checkNetworkVersion has error: ${err.message}`) } } public on({ method, callback }: OnListenerParams): void { try { if (typeof this.provider.on === 'function') { this.provider.on(method, callback) } } catch (err) { throw new Error(`Provider method subscribe has error: ${err.message}`) } } private async initProvider(): Promise
{ try { let account: string | null if (this.version === 'old') { ;[account] = await this.provider.enable() } else { ;[account] = await this.sendRequest({ method: 'eth_requestAccounts' }) } if (account == null) { throw new Error('Locked provider') } this.address = toChecksumAddress(account) if (typeof this.provider.on === 'function') { this.provider.on('accountsChanged', (accounts: string[]) => this.onAccountsChanged(accounts)) this.provider.on('chainChanged', (id: number) => this.onNetworkChanged({ id })) } this.networkId = await this.checkNetworkVersion() return this.address } catch (err) { throw new Error(`Provider method initProvider has error: ${err.message}`) } } private async sleep(time: number): Promise { return await new Promise((resolve) => { setTimeout(() => { resolve() }, time) }) } private async sendAsync({ method, params, from }: OldRequestParams): Promise { const SPOA = 77 const POA = 99 const XDAI = 99 switch (this.networkId) { case SPOA: case POA: case XDAI: from = '' break } return await new Promise((resolve, reject) => { const callback = (err: Error, response: { error: Error; result: T }): void => { if (err.message !== '' || response.error.message !== '') { reject(err) } resolve(response.result) } this.provider.sendAsync( { from, method, params, jsonrpc: '2.0', id: this.generateId(), }, callback, ) }) } private onNetworkChanged({ id }: { id: number }): void { if (!isNaN(id)) { this.networkId = id } } private onAccountsChanged(accounts: string[]): void { const [account] = accounts if (account !== '') { this.address = toChecksumAddress(account) } } private checkVersion(): void { if (typeof this.provider.request === 'function') { this.version = 'new' } else { this.version = 'old' } } private prepareRequest({ method, params }: SendRequestParams) { switch (method) { case 'eth_call': case 'eth_estimateGas': case 'eth_sendTransaction': { if (params instanceof Array) { const [args] = params return { method, params: [this.hexlifyParams(args)] } } break } } return { method, params } } private hexlifyParams(params: Params): Params { const result: Params = Object.assign({}, params) const numericParams: Array = [ 'gas', 'type', 'nonce', 'value', 'gasPrice', 'maxFeePerGas', 'maxPriorityFeePerGas', ] numericParams.forEach((key) => { const value = params[key] if (value) { result[key] = hexValue(value) } }) return result } private async repeatRequestUntilResult( params: SendRequestParams, totalAttempts: number, // eslint-disable-next-line @typescript-eslint/no-magic-numbers retryAttempt: number = 1, ): Promise { return await new Promise((resolve, reject) => { const iteration = async (): Promise => { try { const result = await this.sendRequest(params) if (!result) { if (retryAttempt <= totalAttempts) { retryAttempt++ setTimeout(() => { // eslint-disable-next-line no-void void iteration() }, numbers.SECOND * retryAttempt) } else { return reject(new Error('Tx not minted')) } } else { resolve(result) } } catch (err) { reject(err) } } // eslint-disable-next-line no-void void iteration() }) } private generateId(): number { const base = 10 const exponent = 3 const date = Date.now() * Math.pow(base, exponent) const extra = Math.floor(Math.random() * Math.pow(base, exponent)) return date + extra } }