import { hexValue } from '@ethersproject/bytes'
import { numbers } from '@/constants'
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 = 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 account
} 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 = 100
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 = 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 '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
}
}