diff --git a/packages/eth-providers/src/__tests__/evm-rpc-provider.test.ts b/packages/eth-providers/src/__tests__/evm-rpc-provider.test.ts index d63f7203d..abc655fec 100644 --- a/packages/eth-providers/src/__tests__/evm-rpc-provider.test.ts +++ b/packages/eth-providers/src/__tests__/evm-rpc-provider.test.ts @@ -55,47 +55,6 @@ describe('getReceiptAtBlock', async () => { }); }); -// TODO: maybe setup a subway to test -describe.skip('all cache', async () => { - const provider = EvmRpcProvider.from(ACALA_NODE_URL); - await provider.isReady(); - - afterAll(async () => await provider.disconnect()); - - it('getBlockHeader at latest block => header cache', async () => { - const { time: time1, res: header1 } = await runWithTiming(() => provider._getBlockHeader('latest'), 1); - const { time: time2, res: header2 } = await runWithTiming(() => provider._getBlockHeader('latest'), 1); - - // latest header should already be cached at the start - console.log('latest header:', { time1, time2 }); - expect(time1).to.be.lt(10); - expect(time2).to.be.lt(10); - expect(header1.toJSON()).to.deep.equal(header2.toJSON()); - }); - - it('getBlockHeader at random block => header cache', async () => { - const { time: time1, res: header1 } = await runWithTiming(() => provider._getBlockHeader(1234567), 1); - const { time: time2, res: header2 } = await runWithTiming(() => provider._getBlockHeader(1234567), 1); - - // second time should be 100x faster with cache, in poor network 800ms => 0.5ms - console.log('getBlockHeader:', { time1, time2 }); - expect(time2).to.be.lt(time1 / 20); // conservative multiplier - expect(time2).to.be.lt(10); // no async call - expect(header1.toJSON()).to.deep.equal(header2.toJSON()); - }); - - it('getBlockData at random block => header cache + storage cache + receipt cache', async () => { - const { time: time1, res: blockData1 } = await runWithTiming(() => provider.getBlockData(1234321), 1); - const { time: time2, res: blockData2 } = await runWithTiming(() => provider.getBlockData(1234321), 1); - - // second time should be 100x faster with cache, usually 1500ms => 3ms - console.log('getBlockData: ', { time1, time2 }); - expect(time2).to.be.lt(time1 / 20); // conservative multiplier - expect(time2).to.be.lt(30); // no async call - expect(blockData1).to.deep.equal(blockData2); - }); -}); - describe.concurrent('rpc test', async () => { const provider = EvmRpcProvider.from(endpoint); diff --git a/packages/eth-providers/src/__tests__/provider-cache.test.ts b/packages/eth-providers/src/__tests__/provider-cache.test.ts new file mode 100644 index 000000000..3f0852f1c --- /dev/null +++ b/packages/eth-providers/src/__tests__/provider-cache.test.ts @@ -0,0 +1,115 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import dotenv from 'dotenv'; + +import { EvmRpcProvider } from '../rpc-provider'; +import { apiCache } from '../utils/ApiAtCache'; +import { runWithTiming } from '../utils'; + +dotenv.config(); + +const ACALA_NODE_URL = 'wss://acala-rpc.dwellir.com'; + +describe.concurrent('provider cache', async () => { + let provider: EvmRpcProvider; + let provider2: EvmRpcProvider; + + beforeAll(async () => { + provider = EvmRpcProvider.from(ACALA_NODE_URL); + provider2 = EvmRpcProvider.from(ACALA_NODE_URL); // provider 2 to query some info without affecting cache + await provider.isReady(); + await provider2.isReady(); + }); + + afterAll(async () => await Promise.all([ + provider.disconnect(), + provider2.disconnect(), + ])); + + it('get apiAt', async() => { + const curBlock = await provider.getBlockNumber(); + const randomBlock = curBlock - Math.floor(Math.random() * 100000); + const blockHash = await provider2._getBlockHash(randomBlock); + + const { time: time1, res: apiAt1 } = await runWithTiming(() => apiCache.getApiAt(provider.api, blockHash), 1); + const { time: time2, res: apiAt2 } = await runWithTiming(() => apiCache.getApiAt(provider.api, blockHash), 1); + + expect(time1).to.be.gt(0, 'first get apiAt failed!'); + expect(time2).to.be.gt(0, 'second get apiAt failed!'); + console.log('get random apiAt:', { time1, time2 }); + + expect(time2).to.be.lt(time1 / 20); // conservative multiplier + expect(time2).to.be.lt(50); // no async call so should be almost instant + expect(apiAt1).to.equal(apiAt2); // should be the same instance + }); + + it('get block hash', async() => { + const curBlock = await provider.getBlockNumber(); + const randomBlock = curBlock - Math.floor(Math.random() * 100000); + + const { time: time1, res: hash1 } = await runWithTiming(() => provider._getBlockHash(randomBlock), 1); + const { time: time2, res: hash2 } = await runWithTiming(() => provider._getBlockHash(randomBlock), 1); + + expect(time1).to.be.gt(0, 'first get block hash failed!'); + expect(time2).to.be.gt(0, 'second get block hash failed!'); + console.log('get random block hash:', { time1, time2 }); + + expect(time2).to.be.lt(time1 / 20); // conservative multiplier + expect(time2).to.be.lt(50); // no async call so should be almost instant + expect(hash1).to.deep.equal(hash2); + }); + + it('get block', async() => { + const curBlock = await provider.getBlockNumber(); + const randomBlock = curBlock - Math.floor(Math.random() * 100000); + const blockHash = await provider2._getBlockHash(randomBlock); + + const { time: time1, res: blockNumber1 } = await runWithTiming(() => provider._getBlockNumber(blockHash), 1); + const { time: time2, res: blockNumber2 } = await runWithTiming(() => provider._getBlockNumber(blockHash), 1); + + expect(time1).to.be.gt(0, 'first get block number failed!'); + expect(time2).to.be.gt(0, 'second get block number failed!'); + console.log('get random block number:', { time1, time2 }); + + expect(time2).to.be.lt(time1 / 20); // conservative multiplier + expect(time2).to.be.lt(50); // no async call so should be almost instant + expect(blockNumber1).to.deep.equal(blockNumber2); + }); + + it('get block header', async() => { + const curBlock = await provider.getBlockNumber(); + const randomBlock = curBlock - Math.floor(Math.random() * 100000); + + const { time: time1, res: header1 } = await runWithTiming(() => provider._getBlockHeader(randomBlock), 1); + const { time: time2, res: header2 } = await runWithTiming(() => provider._getBlockHeader(randomBlock), 1); + + expect(time1).to.be.gt(0, 'first get header failed!'); + expect(time2).to.be.gt(0, 'second get header failed!'); + console.log('get random header:', { time1, time2 }); + + expect(time2).to.be.lt(time1 / 20); // conservative multiplier + expect(time2).to.be.lt(50); // no async call so should be almost instant + expect(header1.toJSON()).to.deep.equal(header2.toJSON()); + }); + + it('get block data', async () => { + const curBlock = await provider.getBlockNumber(); + const randomBlock = curBlock - Math.floor(Math.random() * 100000); + + const { time: time1, res: blockData1 } = await runWithTiming(() => provider.getBlockData(randomBlock), 1); + const { time: time2, res: blockData2 } = await runWithTiming(() => provider.getBlockData(randomBlock), 1); + const { time: time3, res: blockData3 } = await runWithTiming(() => provider.getBlockData(randomBlock, true), 1); + + expect(time1).to.be.gt(0, 'first get blockData failed!'); + expect(time2).to.be.gt(0, 'second get blockData failed!'); + expect(time3).to.be.gt(0, 'third get blockData failed!'); + console.log('get random blockData:', { time1, time2, time3 }); + + expect(time2).to.be.lt(time1 / 20); // conservative multiplier + expect(time2).to.be.lt(50); // no async call so should be almost instant + expect(time3).to.be.lt(time1 / 20); // conservative multiplier + expect(time3).to.be.lt(50); // no async call so should be almost instant + expect(blockData1).to.deep.equal(blockData2); + expect(blockData3.hash).to.deep.equal(blockData2.hash); + }); +}); + diff --git a/packages/eth-providers/src/base-provider.ts b/packages/eth-providers/src/base-provider.ts index 9f4096137..9d7f63ff9 100644 --- a/packages/eth-providers/src/base-provider.ts +++ b/packages/eth-providers/src/base-provider.ts @@ -14,28 +14,22 @@ import { } from '@ethersproject/abstract-provider'; import { AcalaEvmTX, checkSignatureType, parseTransaction } from '@acala-network/eth-transactions'; import { AccessList, accessListify } from 'ethers/lib/utils'; -import { AccountId, H160, Header } from '@polkadot/types/interfaces'; import { ApiPromise } from '@polkadot/api'; import { AsyncAction } from 'rxjs/internal/scheduler/AsyncAction'; import { AsyncScheduler } from 'rxjs/internal/scheduler/AsyncScheduler'; import { BigNumber, BigNumberish, Wallet } from 'ethers'; import { Deferrable, defineReadOnly, resolveProperties } from '@ethersproject/properties'; -import { EvmAccountInfo, EvmContractInfo } from '@acala-network/types/interfaces'; import { Formatter } from '@ethersproject/providers'; -import { FrameSystemAccountInfo } from '@polkadot/types/lookup'; +import { Header } from '@polkadot/types/interfaces'; import { ISubmittableResult } from '@polkadot/types/types'; import { Logger } from '@ethersproject/logger'; +import { ModuleEvmModuleAccountInfo } from '@polkadot/types/lookup'; import { Network } from '@ethersproject/networks'; import { Observable, ReplaySubject, Subscription, firstValueFrom, throwError } from 'rxjs'; -import { Option, decorateStorage, unwrapStorageType } from '@polkadot/types'; -import { Storage } from '@polkadot/types/metadata/decorate/types'; import { SubmittableExtrinsic } from '@polkadot/api/types'; -import { VersionedRegistry } from '@polkadot/api/base/types'; -import { createHeaderExtended } from '@polkadot/api-derive'; import { filter, first, timeout } from 'rxjs/operators'; import { getAddress } from '@ethersproject/address'; import { hexDataLength, hexValue, hexZeroPad, hexlify, isHexString, joinSignature } from '@ethersproject/bytes'; -import { isNull, u8aToHex, u8aToU8a } from '@polkadot/util'; import BN from 'bn.js'; import LRUCache from 'lru-cache'; @@ -44,7 +38,6 @@ import { BLOCK_GAS_LIMIT, BLOCK_STORAGE_LIMIT, CACHE_SIZE_WARNING, - DUMMY_ADDRESS, DUMMY_BLOCK_NONCE, DUMMY_LOGS_BLOOM, EMPTY_HEX_STRING, @@ -98,6 +91,7 @@ import { BlockCache, CacheInspect } from './utils/BlockCache'; import { MaxSizeSet } from './utils/MaxSizeSet'; import { SubqlProvider } from './utils/subqlProvider'; import { _Metadata } from './utils/gqlTypes'; +import { apiCache } from './utils/ApiAtCache'; export interface HeadsInfo { internalState: { @@ -309,8 +303,7 @@ export abstract class BaseProvider extends AbstractProvider { readonly localMode: boolean; readonly verbose: boolean; readonly maxBlockCacheSize: number; - readonly storages: WeakMap, Storage> = new WeakMap(); - readonly storageCache: LRUCache; + readonly queryCache: LRUCache; readonly blockCache: BlockCache; readonly finalizedBlockHashes: MaxSizeSet; @@ -347,7 +340,7 @@ export abstract class BaseProvider extends AbstractProvider { this.localMode = localMode; this.verbose = verbose; this.maxBlockCacheSize = maxBlockCacheSize; - this.storageCache = new LRUCache({ max: storageCacheSize }); + this.queryCache = new LRUCache({ max: storageCacheSize }); this.blockCache = new BlockCache(this.maxBlockCacheSize); this.finalizedBlockHashes = new MaxSizeSet(this.maxBlockCacheSize); @@ -523,67 +516,6 @@ export abstract class BaseProvider extends AbstractProvider { defineReadOnly(this, '_api', api); }; - queryStorage = async ( - module: `${string}.${string}`, - args: any[], - _blockTag?: BlockTag | Promise | Eip1898BlockTag - ): Promise => { - const blockTag = await this._ensureSafeModeBlockTagFinalization(await parseBlockTag(_blockTag)); - const blockHash = await this._getBlockHash(blockTag); - - const registry = await this.api.getBlockRegistry(u8aToU8a(blockHash)); - - if (!this.storages.get(registry)) { - const storage = decorateStorage( - registry.registry, - registry.metadata.asLatest, - registry.metadata.version, - ); - this.storages.set(registry, storage); - } - - const storage = this.storages.get(registry)!; - - const [section, method] = module.split('.'); - - const entry = storage[section][method]; - const key = entry(...args); - - const outputType = unwrapStorageType( - registry.registry, - entry.meta.type, - entry.meta.modifier.isOptional, - ); - - const cacheKey = `${module}-${blockHash}-${args.join(',')}`; - const cached = this.storageCache.get(cacheKey); - - let input: Uint8Array | null = null; - - if (cached) { - input = cached; - } else { - const value: any = await this.api.rpc.state.getStorage(key, blockHash); - - const isEmpty = isNull(value); - - // we convert to Uint8Array since it maps to the raw encoding, all - // data will be correctly encoded (incl. numbers, excl. :code) - input = isEmpty - ? null - : u8aToU8a(entry.meta.modifier.isOptional ? value.toU8a() : value.isSome ? value.unwrap().toU8a() : null); - - this.storageCache.set(cacheKey, input); - } - - const result = registry.registry.createTypeUnsafe(outputType, [input], { - blockHash, - isPedantic: !entry.meta.modifier.isOptional, - }); - - return result as any as T; - }; - get api(): ApiPromise { return this._api ?? logger.throwError('the api needs to be set', Logger.errors.UNKNOWN_ERROR); } @@ -662,6 +594,12 @@ export abstract class BaseProvider extends AbstractProvider { : this.bestBlockNumber ); + getTimestamp = async (blockHash: string): Promise => { + const apiAt = await apiCache.getApiAt(this.api, blockHash); + const timestamp = await apiAt.query.timestamp.now(); + return timestamp.toNumber(); + }; + getBlockData = async (_blockTag: BlockTag | Promise, full?: boolean): Promise => { const blockTag = await this._ensureSafeModeBlockTagFinalization(_blockTag); const header = await this._getBlockHeader(blockTag); @@ -672,61 +610,76 @@ export abstract class BaseProvider extends AbstractProvider { const blockHash = header.hash.toHex(); const blockNumber = header.number.toNumber(); - const [block, validators, now, receiptsFromSubql] = await Promise.all([ - this.api.rpc.chain.getBlock(blockHash), - this.api.query.session ? this.queryStorage('session.validators', [], blockHash) : ([] as any), - this.queryStorage('timestamp.now', [], blockHash), - this.subql?.getAllReceiptsAtBlock(blockHash), - ]); + let blockDataFull: BlockData; - const headerExtended = createHeaderExtended(header.registry, header, validators); + const cacheKey = `block-${blockHash}`; + const cached = this.queryCache.get(cacheKey); + if (cached) { + blockDataFull = cached; + } else { + const [block, headerExtended, timestamp, receiptsFromSubql] = await Promise.all([ + this.api.rpc.chain.getBlock(blockHash), + this.api.derive.chain.getHeader(blockHash), + this.getTimestamp(blockHash), + this.subql?.getAllReceiptsAtBlock(blockHash), + ]); - // blockscout need `toLowerCase` - const author = headerExtended.author - ? (await this.getEvmAddress(headerExtended.author.toString())).toLowerCase() - : DUMMY_ADDRESS; + // blockscout need `toLowerCase` + const author = (await this.getEvmAddress(headerExtended.author.toString(), blockHash)).toLowerCase(); - let receipts: TransactionReceipt[]; - if (receiptsFromSubql?.length) { - receipts = receiptsFromSubql.map(subqlReceiptAdapter); - } else { - /* ---------- - if nothing is returned from subql, either no tx exists in this block, - or the block not finalized. So we still need to ask block cache. - ---------- */ - receipts = this.blockCache.getAllReceiptsAtBlock(blockHash); - } + let receipts: TransactionReceipt[]; + if (receiptsFromSubql?.length) { + receipts = receiptsFromSubql.map(subqlReceiptAdapter); + } else { + /* ---------- + if nothing is returned from subql, either no tx exists in this block, + or the block not finalized. So we still need to ask block cache. + ---------- */ + receipts = this.blockCache.getAllReceiptsAtBlock(blockHash); + } - const transactions = full - ? receipts.map(tx => receiptToTransaction(tx, block)) - : receipts.map(tx => tx.transactionHash as `0x${string}`); + const transactions = receipts.map(tx => receiptToTransaction(tx, block)); + const gasUsed = receipts.reduce((totalGas, tx) => totalGas.add(tx.gasUsed), BIGNUMBER_ZERO); + + blockDataFull = { + hash: blockHash, + parentHash: headerExtended.parentHash.toHex(), + number: blockNumber, + stateRoot: headerExtended.stateRoot.toHex(), + transactionsRoot: headerExtended.extrinsicsRoot.toHex(), + timestamp: Math.floor(timestamp / 1000), + nonce: DUMMY_BLOCK_NONCE, + mixHash: ZERO_BLOCK_HASH, + difficulty: ZERO, + totalDifficulty: ZERO, + gasLimit: BigNumber.from(BLOCK_GAS_LIMIT), + gasUsed, + + miner: author, + extraData: EMPTY_HEX_STRING, + sha3Uncles: EMTPY_UNCLE_HASH, + receiptsRoot: headerExtended.extrinsicsRoot.toHex(), + logsBloom: DUMMY_LOGS_BLOOM, // TODO: ??? + size: block.encodedLength, + uncles: EMTPY_UNCLES, + + transactions, + }; - const gasUsed = receipts.reduce((totalGas, tx) => totalGas.add(tx.gasUsed), BIGNUMBER_ZERO); + const isFinalized = blockNumber <= await this.finalizedBlockNumber; + if (isFinalized) { + this.queryCache.set(cacheKey, blockDataFull); + } + } - return { - hash: blockHash, - parentHash: headerExtended.parentHash.toHex(), - number: blockNumber, - stateRoot: headerExtended.stateRoot.toHex(), - transactionsRoot: headerExtended.extrinsicsRoot.toHex(), - timestamp: Math.floor(now.toNumber() / 1000), - nonce: DUMMY_BLOCK_NONCE, - mixHash: ZERO_BLOCK_HASH, - difficulty: ZERO, - totalDifficulty: ZERO, - gasLimit: BigNumber.from(BLOCK_GAS_LIMIT), - gasUsed, - - miner: author, - extraData: EMPTY_HEX_STRING, - sha3Uncles: EMTPY_UNCLE_HASH, - receiptsRoot: headerExtended.extrinsicsRoot.toHex(), - logsBloom: DUMMY_LOGS_BLOOM, // TODO: ??? - size: block.encodedLength, - uncles: EMTPY_UNCLES, - - transactions, - }; + const blockData = full + ? blockDataFull + : { + ...blockDataFull, + transactions: blockDataFull.transactions.map(tx => (tx as TX).hash as `0x${string}`), + }; + + return blockData; }; getBlock = async (_blockHashOrBlockTag: BlockTag | string | Promise): Promise => @@ -750,11 +703,8 @@ export abstract class BaseProvider extends AbstractProvider { const substrateAddress = await this.getSubstrateAddress(address, blockHash); - const accountInfo = await this.queryStorage( - 'system.account', - [substrateAddress], - blockHash - ); + const apiAt = await apiCache.getApiAt(this.api, blockHash); + const accountInfo = await apiAt.query.system.account(substrateAddress); return nativeToEthDecimal(accountInfo.data.free.toBigInt()); }; @@ -778,10 +728,8 @@ export abstract class BaseProvider extends AbstractProvider { ).length; } - const accountInfo = await this.queryAccountInfo(addressOrName, blockTag); - const minedNonce = accountInfo.isNone - ? 0 - : accountInfo.unwrap().nonce.toNumber(); + const evmAccountInfo = await this.queryEvmAccountInfo(addressOrName, blockTag); + const minedNonce = evmAccountInfo?.nonce?.toNumber?.() ?? 0; return minedNonce + pendingNonce; }; @@ -791,23 +739,14 @@ export abstract class BaseProvider extends AbstractProvider { _blockTag?: BlockTag | Promise | Eip1898BlockTag ): Promise => { const blockTag = await this._ensureSafeModeBlockTagFinalization(await parseBlockTag(_blockTag)); + const blockHash = await this._getBlockHash(blockTag); - const [address, blockHash] = await Promise.all([ - addressOrName, - this._getBlockHash(blockTag), - ]); - - const contractInfo = await this.queryContractInfo(address, blockHash); - - if (contractInfo.isNone) { - return '0x'; - } - - const codeHash = contractInfo.unwrap().codeHash; - - const api = blockHash ? await this.api.at(blockHash) : this.api; + const evmAccountInfo = await this.queryEvmAccountInfo(addressOrName, blockHash); + const contractInfo = evmAccountInfo?.contractInfo.unwrapOr(null); + if (!contractInfo) { return '0x'; } - const code = await api.query.evm.codes(codeHash); + const apiAt = await apiCache.getApiAt(this.api, blockHash); + const code = await apiAt.query.evm.codes(contractInfo.codeHash); return code.toHex(); }; @@ -843,7 +782,7 @@ export abstract class BaseProvider extends AbstractProvider { callRequest: SubstrateEvmCallRequest, at?: string, ): Promise => { - const api = at ? await this.api.at(at) : this.api; + const api = at ? await apiCache.getApiAt(this.api, at) : this.api; // call evm rpc when `state_call` is not supported yet if (!api.call.evmRuntimeRPCApi) { @@ -904,7 +843,8 @@ export abstract class BaseProvider extends AbstractProvider { Promise.resolve(position).then(hexValue), ]); - const code = await this.queryStorage('evm.accountStorages', [address, hexZeroPad(resolvedPosition, 32)], blockHash); + const apiAt = await apiCache.getApiAt(this.api, blockHash); + const code = await apiAt.query.evm.accountStorages(address, hexZeroPad(resolvedPosition, 32)); return code.toHex(); }; @@ -1007,7 +947,7 @@ export abstract class BaseProvider extends AbstractProvider { extrinsic: SubmittableExtrinsic<'promise', ISubmittableResult>, at?: string, ) => { - const apiAt = await this.api.at(at ?? await this.bestBlockHash); + const apiAt = await apiCache.getApiAt(this.api, at ?? await this.bestBlockHash); const u8a = extrinsic.toU8a(); const lenIncreaseAfterSignature = 100; // approximate length increase after signature @@ -1185,28 +1125,28 @@ export abstract class BaseProvider extends AbstractProvider { }; }; - getSubstrateAddress = async (addressOrName: string, blockTag?: BlockTag): Promise => { - const [address, blockHash] = await Promise.all([ - addressOrName, - this._getBlockHash(blockTag), - ]); - - const substrateAccount = await this.queryStorage>('evmAccounts.accounts', [address], blockHash); + getSubstrateAddress = async (address: string, blockTag?: BlockTag): Promise => { + const blockHash = await this._getBlockHash(blockTag); + const apiAt = await apiCache.getApiAt(this.api, blockHash); + const substrateAccount = await apiAt.query.evmAccounts.accounts(address); - return substrateAccount.isEmpty ? computeDefaultSubstrateAddress(address) : substrateAccount.toString(); + return substrateAccount.isEmpty + ? computeDefaultSubstrateAddress(address) + : substrateAccount.toString(); }; getEvmAddress = async (substrateAddress: string, blockTag?: BlockTag): Promise => { const blockHash = await this._getBlockHash(blockTag); - const evmAddress = await this.queryStorage>('evmAccounts.evmAddresses', [substrateAddress], blockHash); + const apiAt = await apiCache.getApiAt(this.api, blockHash); + const evmAddress = await apiAt.query.evmAccounts.evmAddresses(substrateAddress); return getAddress(evmAddress.isEmpty ? computeDefaultEvmAddress(substrateAddress) : evmAddress.toString()); }; - queryAccountInfo = async ( + queryEvmAccountInfo = async ( addressOrName: string | Promise, _blockTag?: BlockTag | Promise | Eip1898BlockTag - ): Promise> => { + ): Promise => { const blockTag = await this._ensureSafeModeBlockTagFinalization(await parseBlockTag(_blockTag)); const [address, blockHash] = await Promise.all([ @@ -1214,22 +1154,10 @@ export abstract class BaseProvider extends AbstractProvider { this._getBlockHash(blockTag), ]); - const accountInfo = await this.queryStorage>('evm.accounts', [address], blockHash); - - return accountInfo; - }; - - queryContractInfo = async ( - addressOrName: string | Promise, - blockTag?: BlockTag | Promise - ): Promise> => { - const accountInfo = await this.queryAccountInfo(addressOrName, blockTag); - - if (accountInfo.isNone) { - return this.api.createType>('Option', null); - } + const apiAt = await apiCache.getApiAt(this.api, blockHash); + const accountInfo = await apiAt.query.evm.accounts(address); - return accountInfo.unwrap().contractInfo; + return accountInfo.unwrapOr(null); }; _getSubstrateGasParams = (ethTx: Partial): { @@ -1287,7 +1215,7 @@ export abstract class BaseProvider extends AbstractProvider { storageLimit: storageLimit.toBigInt(), tip: 0n, }; - } catch (error) { + } catch { // v2 v2 = true; @@ -1471,7 +1399,8 @@ export abstract class BaseProvider extends AbstractProvider { result.blockNumber = startBlock; result.blockHash = startBlockHash; - result.timestamp = Math.floor((await this.queryStorage('timestamp.now', [], result.blockHash)).toNumber() / 1000); + const timestamp = await this.getTimestamp(result.blockHash); + result.timestamp = Math.floor(timestamp / 1000); result.wait = async (confirms?: number, timeoutMs?: number) => { if (confirms === null || confirms === undefined) { @@ -1560,27 +1489,24 @@ export abstract class BaseProvider extends AbstractProvider { return logger.throwArgumentError('block number should be less than u32', 'blockNumber', blockNumber); } - const isFinalized = blockNumber.lte(await this.finalizedBlockNumber); - const cacheKey = `blockHash-${blockNumber.toString()}`; + const cacheKey = `blockHash-${blockNumber.toHexString()}`; - if (isFinalized) { - const cached = this.storageCache.get(cacheKey); - if (cached) { - return u8aToHex(cached); - } + const cached = this.queryCache.get(cacheKey); + if (cached) { + return cached; } const _blockHash = await this.api.rpc.chain.getBlockHash(blockNumber.toBigInt()); - if (_blockHash.isEmpty) { //@ts-ignore return logger.throwError('header not found', PROVIDER_ERRORS.HEADER_NOT_FOUND, { blockNumber }); } - const blockHash = _blockHash.toHex(); + // no need to check for canonicality here since this blockHash is just queried from rpc + const isFinalized = blockNumber.lte(await this.finalizedBlockNumber); if (isFinalized) { - this.storageCache.set(cacheKey, _blockHash.toU8a()); + this.queryCache.set(cacheKey, blockHash); } return blockHash; @@ -1600,6 +1526,9 @@ export abstract class BaseProvider extends AbstractProvider { const blockNumber = _blockNumber ?? (await this._getBlockNumber(blockHash)); const canonicalHash = await this.api.rpc.chain.getBlockHash(blockNumber); + if (canonicalHash.isEmpty) { + return logger.throwError('header not found', Logger.errors.CALL_EXCEPTION, { blockNumber }); + } return canonicalHash.toString() === blockHash; }; @@ -1640,16 +1569,24 @@ export abstract class BaseProvider extends AbstractProvider { _getBlockHeader = async (blockTag?: BlockTag | Promise): Promise
=> { const blockHash = await this._getBlockHash(await blockTag); + const cacheKey = `header-${blockHash}`; + const cached = this.queryCache.get
(cacheKey); + if (cached) { + return cached; + } + try { const header = await this.api.rpc.chain.getHeader(blockHash); + // no need to check for canonicality here since this header is just queried from rpc + const isFinalized = header.number.toNumber() <= await this.finalizedBlockNumber; + if (isFinalized) { + this.queryCache.set(cacheKey, header); + } + return header; } catch (error) { - if ( - typeof error === 'object' && - typeof (error as any).message === 'string' && - (error as any).message.match(/Unable to retrieve header and parent from supplied hash/gi) - ) { + if ((error as any)?.message?.match?.(/Unable to retrieve header and parent from supplied hash/gi)) { //@ts-ignore return logger.throwError('header not found', PROVIDER_ERRORS.HEADER_NOT_FOUND, { blockHash }); } diff --git a/packages/eth-providers/src/utils/ApiAtCache.ts b/packages/eth-providers/src/utils/ApiAtCache.ts new file mode 100644 index 000000000..a9cc24382 --- /dev/null +++ b/packages/eth-providers/src/utils/ApiAtCache.ts @@ -0,0 +1,31 @@ +import { ApiDecoration } from '@polkadot/api/types'; +import { ApiPromise } from '@polkadot/api'; +import LRUCache from 'lru-cache'; + +class ApiAtCache { + #cache: LRUCache>; + + constructor(maxCacheSize: number = 100) { + this.#cache = new LRUCache>({ + max: maxCacheSize, + }); + } + + getApiAt = async ( + api: ApiPromise, + blockHash: string + ): Promise> => { + const cached = this.#cache.get(blockHash); + if (cached) return cached; + + const apiAt = await api.at(blockHash); + + // do we need to check for finalization here? + // ApiAt is only a decoration and the actuall result is from rpc call, so should be fine? + this.#cache.set(blockHash, apiAt); + + return apiAt; + }; +} + +export const apiCache = new ApiAtCache(100); diff --git a/packages/eth-providers/src/utils/parseBlock.ts b/packages/eth-providers/src/utils/parseBlock.ts index d46ef9bb6..2973a336e 100644 --- a/packages/eth-providers/src/utils/parseBlock.ts +++ b/packages/eth-providers/src/utils/parseBlock.ts @@ -15,6 +15,7 @@ import { GenericExtrinsic } from '@polkadot/types'; import { TransactionReceipt } from '@ethersproject/abstract-provider'; import { BIGNUMBER_ZERO, ONE_HUNDRED_GWEI } from '../consts'; +import { apiCache } from './ApiAtCache'; import { findEvmEvent, formatter, @@ -37,26 +38,32 @@ export const getAllReceiptsAtBlock = async ( blockHash: string, targetTxHash?: string ): Promise => { - const apiAtTargetBlock = await api.at(blockHash); + const apiAt = await apiCache.getApiAt(api, blockHash); const [block, blockEvents] = await Promise.all([ api.rpc.chain.getBlock(blockHash), - apiAtTargetBlock.query.system.events(), + apiAt.query.system.events(), ]); - return parseReceiptsFromBlockData(api, block, blockEvents, targetTxHash); + return await parseReceiptsFromBlockData(api, block, blockEvents, targetTxHash, true); }; export const parseReceiptsFromBlockData = async ( api: ApiPromise, block: SignedBlock, blockEvents: FrameSystemEventRecord[], - targetTxHash?: string + targetTxHash?: string, + // this method is also used by subql, so disable cacheing by default to avoid potential compatibilty issues + useCache: boolean = false, ): Promise => { const { header } = block.block; const blockNumber = header.number.toNumber(); const blockHash = header.hash.toHex(); - const _apiAtParentBlock = api.at(header.parentHash); // don't wait here in case not being used + + // don't wait here in case not being used + const _apiAtParentBlock = useCache + ? apiCache.getApiAt(api, header.parentHash.toHex()) + : api.at(header.parentHash); const succeededEvmExtrinsics = block.block.extrinsics .map((extrinsic, idx) => { diff --git a/packages/eth-providers/src/utils/utils.ts b/packages/eth-providers/src/utils/utils.ts index a26d43ff8..51d2d5eea 100644 --- a/packages/eth-providers/src/utils/utils.ts +++ b/packages/eth-providers/src/utils/utils.ts @@ -1,4 +1,5 @@ import { AnyFunction } from '@polkadot/types/types'; +import { ApiPromise } from '@polkadot/api'; import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; import { Extrinsic } from '@polkadot/types/interfaces'; import { FrameSystemEventRecord } from '@polkadot/types/lookup'; diff --git a/packages/eth-rpc-adapter/src/__tests__/endpoint-tests/eth_getBlockByNumber.test.ts b/packages/eth-rpc-adapter/src/__tests__/endpoint-tests/eth_getBlockByNumber.test.ts index daabb701b..87268db32 100644 --- a/packages/eth-rpc-adapter/src/__tests__/endpoint-tests/eth_getBlockByNumber.test.ts +++ b/packages/eth-rpc-adapter/src/__tests__/endpoint-tests/eth_getBlockByNumber.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from 'vitest'; -import { deployErc20, eth_getBlockByNumber, testSetup } from '../utils'; +import { deployErc20, eth_getBlockByHash, eth_getBlockByNumber, testSetup } from '../utils'; const { wallet, provider } = testSetup; -describe('eth_getBlockByNumber', () => { +describe('eth_getBlockByNumber and eth_getBlockByHash', () => { it('get correct block info', async () => { const token = await deployErc20(wallet); const txHash = token.deployTransaction.hash; @@ -24,5 +24,17 @@ describe('eth_getBlockByNumber', () => { to: null, value: '0x0', }); + + // getblockbyhash should return the same result + const resHash = (await eth_getBlockByHash([resFull.hash, false])).data.result; + const resHashFull = (await eth_getBlockByHash([resFull.hash, true])).data.result; + + expect(resHash).to.deep.equal(res); + expect(resHashFull).to.deep.equal(resFull); + }); + + it('returns null when block not found', async () => { + const res = (await eth_getBlockByNumber([0x12345678, false])).data.result; + expect(res).toBeNull(); }); }); diff --git a/packages/eth-rpc-adapter/src/__tests__/utils/eth-rpc-apis.ts b/packages/eth-rpc-adapter/src/__tests__/utils/eth-rpc-apis.ts index dd89153b2..0bb8f1e7f 100644 --- a/packages/eth-rpc-adapter/src/__tests__/utils/eth-rpc-apis.ts +++ b/packages/eth-rpc-adapter/src/__tests__/utils/eth-rpc-apis.ts @@ -21,6 +21,7 @@ export const rpcGet = export const eth_call = rpcGet('eth_call'); export const eth_blockNumber = rpcGet('eth_blockNumber'); export const eth_getBlockByNumber = rpcGet('eth_getBlockByNumber'); +export const eth_getBlockByHash = rpcGet('eth_getBlockByHash'); export const eth_getTransactionReceipt = rpcGet('eth_getTransactionReceipt'); export const eth_getLogs = rpcGet<{ data: { result: LogHexified[]; error?: JsonRpcError } }>('eth_getLogs'); export const eth_getTransactionByHash = rpcGet('eth_getTransactionByHash'); diff --git a/packages/eth-rpc-adapter/src/wrapped-provider.ts b/packages/eth-rpc-adapter/src/wrapped-provider.ts index 7c1793b00..a128f73e4 100644 --- a/packages/eth-rpc-adapter/src/wrapped-provider.ts +++ b/packages/eth-rpc-adapter/src/wrapped-provider.ts @@ -7,7 +7,6 @@ const TRACE_METHODS = [ '_onNewHead', '_onNewFinalizedHead', '_notifySubscribers', - 'queryStorage', 'getNetwork', 'getBlockNumber', 'getBlockData', @@ -26,8 +25,7 @@ const TRACE_METHODS = [ 'estimateResources', 'getSubstrateAddress', 'getEvmAddress', - 'queryAccountInfo', - 'queryContractInfo', + 'queryEvmAccountInfo', 'prepareTransaction', 'sendRawTransaction', 'sendTransaction',