/* eslint-disable @typescript-eslint/no-unused-vars */ import { options } from '@reef-defi/api'; import { EvmContractInfo, EvmAccountInfo } from '@reef-defi/types/interfaces'; import type { Block, BlockTag, BlockWithTransactions, EventType, FeeData, Filter, Listener, Log, Provider as AbstractProvider, TransactionReceipt, TransactionRequest, TransactionResponse } from '@ethersproject/abstract-provider'; import { isHexString } from '@ethersproject/bytes'; import { resolveProperties, Deferrable } from '@ethersproject/properties'; import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; import { Logger } from '@ethersproject/logger'; import type { Network } from '@ethersproject/networks'; import Scanner from '@open-web3/scanner'; import { ApiPromise } from '@polkadot/api'; import { ApiOptions } from '@polkadot/api/types'; import type { WsProvider } from '@polkadot/rpc-provider'; import { Option } from '@polkadot/types'; import { hexToU8a, isHex, isNumber, numberToHex, u8aConcat, u8aFixLength } from '@polkadot/util'; import { encodeAddress } from '@polkadot/util-crypto'; import type BN from 'bn.js'; import { AbstractDataProvider } from './DataProvider'; import { toBN } from './utils'; const logger = new Logger('evm-provider'); export class Provider implements AbstractProvider { readonly api: ApiPromise; readonly resolveApi: Promise<ApiPromise>; readonly _isProvider: boolean; readonly dataProvider?: AbstractDataProvider; readonly scanner: Scanner; /** * * @param _apiOptions * @param dataProvider */ constructor(_apiOptions: ApiOptions, dataProvider?: AbstractDataProvider) { const apiOptions = options(_apiOptions); this.api = new ApiPromise(apiOptions); this.resolveApi = this.api.isReady; this._isProvider = true; this.dataProvider = dataProvider; this.scanner = new Scanner({ wsProvider: apiOptions.provider as WsProvider, types: apiOptions.types, typesAlias: apiOptions.typesAlias, typesSpec: apiOptions.typesSpec, typesChain: apiOptions.typesChain, typesBundle: apiOptions.typesBundle }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types static isProvider(value: any): boolean { return !!(value && value._isProvider); } async init(): Promise<void> { await this.api.isReady; this.dataProvider && (await this.dataProvider.init()); } /** * Get the network the provider is connected to. * @returns A promise resolving to the name and chain ID of the connected chain. */ async getNetwork(): Promise<Network> { await this.resolveApi; return { name: this.api.runtimeVersion.specName.toString(), chainId: 13939 }; } /** * Get the block number of the chain's head. * @returns A promise resolving to the block number of the head block. */ async getBlockNumber(): Promise<number> { await this.resolveApi; const r = await this.api.rpc.chain.getHeader(); return r.number.toNumber(); } async getGasPrice(): Promise<BigNumber> { // return logger.throwError(`Unsupport getGasPrice`); return BigNumber.from(0); } async getFeeData(): Promise<FeeData> { return { maxFeePerGas: BigNumber.from(1), maxPriorityFeePerGas: BigNumber.from(1), gasPrice: BigNumber.from(1) }; } /** * Get an account's balance by address or name. * @param addressOrName The address or name of the account * @param blockTag The block to get the balance of, defaults to the head * @returns A promise resolving to the account's balance */ async getBalance( addressOrName: string | Promise<string>, blockTag?: BlockTag | Promise<BlockTag> ): Promise<BigNumber> { await this.resolveApi; let address = await this._resolveAddress(addressOrName); if (!address) { address = await this._toAddress(addressOrName); } const blockHash = await this._resolveBlockHash(blockTag); const accountInfo = blockHash ? await this.api.query.system.account.at(blockHash, address) : await this.api.query.system.account(address); return BigNumber.from(accountInfo.data.free.toBn().toString()); } /** * Get the transaction count of an account at a specified block. * @param addressOrName The address or name of the account * @param blockTag The block to get the transaction count of, defaults to the head block * @returns A promise resolving to the account's transaction count */ async getTransactionCount( addressOrName: string | Promise<string>, blockTag?: BlockTag | Promise<BlockTag> ): Promise<number> { await this.resolveApi; const resolvedBlockTag = await blockTag; const address = await this._resolveEvmAddress(addressOrName); let account: Option<EvmAccountInfo>; if (resolvedBlockTag === 'pending') { account = await this.api.query.evm.accounts<Option<EvmAccountInfo>>( address ); } else { const blockHash = await this._resolveBlockHash(blockTag); account = blockHash ? await this.api.query.evm.accounts.at<Option<EvmAccountInfo>>( blockHash, address ) : await this.api.query.evm.accounts<Option<EvmAccountInfo>>(address); } if (!account.isNone) { return account.unwrap().nonce.toNumber(); } else { return 0; } } /** * Get the code hash at a given address * @param addressOrName The address of the code * @param blockTag The block to look up the address, defaults to latest * @returns A promise resolving in the code hash */ async getCode( addressOrName: string | Promise<string>, blockTag?: BlockTag | Promise<BlockTag> ): Promise<string> { await this.resolveApi; const { address, blockHash } = await resolveProperties({ address: this._resolveEvmAddress(addressOrName), blockHash: this._getBlockTag(blockTag) }); const contractInfo = await this.queryContractInfo(address, blockHash); if (contractInfo.isNone) { return '0x'; } const codeHash = contractInfo.unwrap().codeHash; const api = await (blockHash ? this.api.at(blockHash) : this.api); const code = await api.query.evm.codes(codeHash); return code.toHex(); } async _getBlockTag(blockTag?: BlockTag | Promise<BlockTag>): Promise<string> { blockTag = await blockTag; if (blockTag === undefined) { blockTag = 'latest'; } switch (blockTag) { case 'pending': { return logger.throwError( 'pending tag not implemented', Logger.errors.UNSUPPORTED_OPERATION ); } case 'latest': { const hash = await this.api.rpc.chain.getBlockHash(); return hash.toHex(); } case 'earliest': { const hash = this.api.genesisHash; return hash.toHex(); } default: { if (!isHexString(blockTag)) { return logger.throwArgumentError( 'blocktag should be a hex string', 'blockTag', blockTag ); } // block hash if (typeof blockTag === 'string' && isHexString(blockTag, 32)) { return blockTag; } const blockNumber = BigNumber.from(blockTag).toNumber(); const hash = await this.api.rpc.chain.getBlockHash(blockNumber); return hash.toHex(); } } } async queryAccountInfo( addressOrName: string | Promise<string>, blockTag?: BlockTag | Promise<BlockTag> ): Promise<Option<EvmAccountInfo>> { // pending tag const resolvedBlockTag = await blockTag; if (resolvedBlockTag === 'pending') { const address = await this._resolveEvmAddress(addressOrName); return this.api.query.evm.accounts<Option<EvmAccountInfo>>(address); } const { address, blockHash } = await resolveProperties({ address: this._resolveEvmAddress(addressOrName), blockHash: this._getBlockTag(blockTag) }); const apiAt = await this.api.at(blockHash); const accountInfo = await apiAt.query.evm.accounts<Option<EvmAccountInfo>>( address ); return accountInfo; } async queryContractInfo( addressOrName: string | Promise<string>, blockTag?: BlockTag | Promise<BlockTag> ): Promise<Option<EvmContractInfo>> { const accountInfo = await this.queryAccountInfo(addressOrName, blockTag); if (accountInfo.isNone) { return this.api.createType<Option<EvmContractInfo>>( 'Option<EvmContractInfo>', null ); } return accountInfo.unwrap().contractInfo; } /** * Get the storage from a block. * @param addressOrName The address to retrieve the storage from * @param position * @param blockTag The block to retrieve the storage from, defaults to head * @returns The storage data as a hash */ async getStorageAt( addressOrName: string | Promise<string>, position: BigNumberish | Promise<BigNumberish>, blockTag?: BlockTag | Promise<BlockTag> ): Promise<string> { await this.resolveApi; const address = await this._resolveEvmAddress(addressOrName); const blockHash = await this._resolveBlockHash(blockTag); const code = blockHash ? await this.api.query.evm.accountStorages.at(blockHash, address) : await this.api.query.evm.accountStorages(address); return code.toHex(); } /** * Unimplemented */ async sendTransaction( signedTransaction: string | Promise<string> ): Promise<TransactionResponse> { return this._fail('sendTransaction'); } /** * Submit a transaction to be executed on chain. * @param transaction The transaction to call * @param blockTag * @returns The call result as a hash */ async call( transaction: Deferrable<TransactionRequest>, blockTag?: BlockTag | Promise<BlockTag> ): Promise<string> { const resolved = await this._resolveTransaction(transaction); if (blockTag) { const blockHash = await this._resolveBlockHash(blockTag); // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = await (this.api.rpc as any).evm.call(resolved, blockHash); return result.toHex(); } else { // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = await (this.api.rpc as any).evm.call({ to: resolved.to, from: resolved.from, data: resolved.data, storageLimit: '0' }); return result.toHex(); } } /** * Estimate gas for a transaction. * @param transaction The transaction to estimate the gas of * @returns The estimated gas used by this transaction */ async estimateGas( transaction: Deferrable<TransactionRequest> ): Promise<BigNumber> { const resources = await this.estimateResources(transaction); return resources.gas.add(resources.storage); } /** * Estimate resources for a transaction. * @param transaction The transaction to estimate the resources of * @returns The estimated resources used by this transaction */ async estimateResources( transaction: Deferrable<TransactionRequest> ): Promise<{ gas: BigNumber; storage: BigNumber; weightFee: BigNumber; }> { const resolved = await this._resolveTransaction(transaction); const from = await resolved.from; const value = await resolved.value; const to = await resolved.to; const data = await resolved.data; const storageLimit = await this._resolveStorageLimit(resolved); if (!from) { return logger.throwError('From cannot be undefined'); } // construct extrinsic to estimate const extrinsic = !to ? this.api.tx.evm.create( data, toBN(value), toBN(await resolved.gasLimit), toBN(storageLimit) ) : this.api.tx.evm.call( to, data, toBN(value), toBN(await resolved.gasLimit), toBN(storageLimit) ); // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = await (this.api.rpc as any).evm.estimateResources( resolved.from, extrinsic.toHex() // returns transaction bytecode? ); return { gas: BigNumber.from((result.gas as BN).toString()), storage: BigNumber.from((result.storage as BN).toString()), weightFee: BigNumber.from((result.weightFee as BN).toString()) }; } /** * Unimplemented, will always fail. */ async getBlock( blockHashOrBlockTag: BlockTag | string | Promise<BlockTag | string> ): Promise<Block> { return this._fail('getBlock'); } /** * Unimplemented, will always fail. */ async getBlockWithTransactions( blockHashOrBlockTag: BlockTag | string | Promise<BlockTag | string> ): Promise<BlockWithTransactions> { return this._fail('getBlockWithTransactions'); } /** * Unimplemented, will always fail. */ async getTransaction(transactionHash: string): Promise<TransactionResponse> { return this._fail('getTransaction'); } async getTransactionReceipt(txHash: string): Promise<TransactionReceipt> { if (!this.dataProvider) return this._fail('getTransactionReceipt'); return this.dataProvider.getTransactionReceipt( txHash, this._resolveBlockNumber ); } async resolveName(name: string | Promise<string>): Promise<string> { return name; } async lookupAddress(address: string | Promise<string>): Promise<string> { return address; } /** * Unimplemented, will always fail. */ async waitForTransaction( transactionHash: string, confirmations?: number, timeout?: number ): Promise<TransactionReceipt> { return this._fail('waitForTransaction'); } /** * Get an array of filtered logs from the chain's head. * @param filter The filter to apply to the logs * @returns A promise that resolves to an array of filtered logs */ async getLogs(filter: Filter): Promise<Array<Log>> { if (!this.dataProvider) return this._fail('getLogs'); return this.dataProvider.getLogs(filter, this._resolveBlockNumber); } // eslint-disable-next-line @typescript-eslint/no-explicit-any _fail(operation: string): Promise<any> { return Promise.resolve().then(() => { logger.throwError(`Unsupport ${operation}`); }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any emit(eventName: EventType, ...args: Array<any>): boolean { return logger.throwError('Unsupport Event'); } listenerCount(eventName?: EventType): number { return logger.throwError('Unsupport Event'); } listeners(eventName?: EventType): Array<Listener> { return logger.throwError('Unsupport Event'); } off(eventName: EventType, listener?: Listener): AbstractProvider { return logger.throwError('Unsupport Event'); } on(eventName: EventType, listener: Listener): AbstractProvider { return logger.throwError('Unsupport Event'); } once(eventName: EventType, listener: Listener): AbstractProvider { return logger.throwError('Unsupport Event'); } removeAllListeners(eventName?: EventType): AbstractProvider { return logger.throwError('Unsupport Event'); } addListener(eventName: EventType, listener: Listener): AbstractProvider { return this.on(eventName, listener); } removeListener(eventName: EventType, listener: Listener): AbstractProvider { return this.off(eventName, listener); } async _resolveTransactionReceipt( transactionHash: string, blockHash: string, from: string ): Promise<TransactionReceipt> { const detail = await this.scanner.getBlockDetail({ blockHash: blockHash }); const blockNumber = detail.number; const extrinsic = detail.extrinsics.find( ({ hash }) => hash === transactionHash ); if (!extrinsic) { return logger.throwError(`Transaction hash not found`); } const transactionIndex = extrinsic.index; const events = detail.events.filter( ({ phaseIndex }) => phaseIndex === transactionIndex ); const findCreated = events.find( (x) => x.section.toUpperCase() === 'EVM' && x.method.toUpperCase() === 'CREATED' ); const findExecuted = events.find( (x) => x.section.toUpperCase() === 'EVM' && x.method.toUpperCase() === 'EXECUTED' ); const result = events.find( (x) => x.section.toUpperCase() === 'SYSTEM' && x.method.toUpperCase() === 'EXTRINSICSUCCESS' ); if (!result) { return logger.throwError(`Can't find event`); } const status = findCreated || findExecuted ? 1 : 0; const contractAddress = findCreated ? findCreated.args[0] : null; const to = findExecuted ? findExecuted.args[0] : null; const logs = events .filter((e) => { return ( e.method.toUpperCase() === 'LOG' && e.section.toUpperCase() === 'EVM' ); }) .map((log, index) => { return { transactionHash, blockNumber, blockHash, transactionIndex, removed: false, address: log.args[0].address, data: log.args[0].data, topics: log.args[0].topics, logIndex: index }; }); const gasUsed = BigNumber.from(result.args[0].weight); return { to, from, contractAddress, transactionIndex, gasUsed, logsBloom: '0x', blockHash, transactionHash, logs, blockNumber, confirmations: 4, cumulativeGasUsed: gasUsed, byzantium: false, status, effectiveGasPrice: BigNumber.from('1'), type: 0 }; } async _resolveBlockHash( blockTag?: BlockTag | Promise<BlockTag> ): Promise<string> { await this.resolveApi; if (!blockTag) return undefined; const resolvedBlockHash = await blockTag; if (resolvedBlockHash === 'pending') { throw new Error('Unsupport Block Pending'); } if (resolvedBlockHash === 'latest') { const hash = await this.api.query.system.blockHash(); return hash.toString(); } if (resolvedBlockHash === 'earliest') { const hash = this.api.query.system.blockHash(0); return hash.toString(); } if (isHex(resolvedBlockHash)) { return resolvedBlockHash; } const hash = await this.api.query.system.blockHash(resolvedBlockHash); return hash.toString(); } async _resolveBlockNumber( blockTag?: BlockTag | Promise<BlockTag> ): Promise<number> { await this.resolveApi; if (!blockTag) { return logger.throwError(`Blocktag cannot be undefined`); } const resolvedBlockNumber = await blockTag; if (resolvedBlockNumber === 'pending') { throw new Error('Unsupport Block Pending'); } if (resolvedBlockNumber === 'latest') { const header = await this.api.rpc.chain.getHeader(); return header.number.toNumber(); } if (resolvedBlockNumber === 'earliest') { return 0; } if (isNumber(resolvedBlockNumber)) { return resolvedBlockNumber; } else { throw new Error('Expect blockHash to be a number or tag'); } } async _resolveAddress( addressOrName: string | Promise<string> ): Promise<string> { const resolved = await addressOrName; const result = await this.api.query.evmAccounts.accounts(resolved); return result.toString(); } async _toAddress(addressOrName: string | Promise<string>): Promise<string> { const resolved = await addressOrName; const address = encodeAddress( u8aFixLength(u8aConcat('evm:', hexToU8a(resolved)), 256, true) ); return address.toString(); } async _resolveEvmAddress( addressOrName: string | Promise<string> ): Promise<string> { const resolved = await addressOrName; if (resolved.length === 42) { return resolved; } const result = await this.api.query.evmAccounts.evmAddresses(resolved); return result.toString(); } async _resolveTransaction( tx: Deferrable<TransactionRequest> ): Promise<Deferrable<TransactionRequest>> { for (const key of ['gasLimit', 'value']) { const typeKey = key as 'gasLimit' | 'value'; if (tx[typeKey]) { if (BigNumber.isBigNumber(tx[typeKey])) { tx[typeKey] = (tx[typeKey] as BigNumber).toHexString(); } else if (isNumber(tx[typeKey])) { tx[typeKey] = numberToHex(tx[typeKey] as number); } } } delete tx.nonce; delete tx.gasPrice; delete tx.chainId; return tx; } async _resolveStorageLimit( tx: Deferrable<TransactionRequest> ): Promise<BigNumber> { if (tx.customData) { if ('storageLimit' in tx.customData) { const storageLimit = tx.customData.storageLimit; if (BigNumber.isBigNumber(storageLimit)) { return storageLimit; } else if (isNumber(storageLimit)) { return BigNumber.from(storageLimit); } } } // At least 60 REEF are needed to deploy return BigNumber.from(60_000); } }