diff --git a/docker/eth-providers-test.Dockerfile b/docker/eth-providers-test.Dockerfile index 3eae0344d..76033a7e5 100644 --- a/docker/eth-providers-test.Dockerfile +++ b/docker/eth-providers-test.Dockerfile @@ -6,4 +6,4 @@ WORKDIR /app ENV ENDPOINT_URL=ws://mandala-node:9944 ENV ETH_RPC=http://eth-rpc-adapter-server:8545 -CMD yarn e2e:feed-tx; yarn workspace @acala-network/eth-providers run test:CI +CMD yarn e2e:feed-tx; yarn workspace @acala-network/eth-providers run test:e2e diff --git a/package.json b/package.json index 7836a598d..dc2a6cf29 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "start:eth-rpc-adapter": "docker compose up --build -- eth-rpc-adapter-ready", "e2e:feed-tx": "yarn workspace evm-waffle-example-dex run test", "e2e:feed-tx-2": "yarn workspace evm-waffle-example-e2e run test", - "e2e:eth-providers": "yarn start:chain; yarn e2e:feed-tx; yarn start:eth-rpc-adapter; yarn workspace @acala-network/eth-providers run test:CI", + "e2e:eth-providers": "yarn start:chain; yarn e2e:feed-tx; yarn start:eth-rpc-adapter; yarn workspace @acala-network/eth-providers run test:e2e", "e2e:eth-rpc-adapter": "docker compose up --build -- eth-rpc-adapter-with-subql-ready; yarn e2e:feed-tx; yarn e2e:feed-tx-2; yarn workspace @acala-network/eth-rpc-adapter run test:CI", "e2e:waffle": "yarn start:chain; yarn run test:waffle", "e2e:truffle": "yarn start:eth-rpc-adapter; cd examples/truffle-tutorials; yarn install --immutable; yarn test:mandala", diff --git a/packages/bodhi/src/BodhiSigner.ts b/packages/bodhi/src/BodhiSigner.ts index 2220eb3cb..f668d965e 100644 --- a/packages/bodhi/src/BodhiSigner.ts +++ b/packages/bodhi/src/BodhiSigner.ts @@ -61,7 +61,7 @@ export class BodhiSigner extends AbstractSigner implements TypedDataSigner { return new BodhiSigner(provider, pair.address, new SubstrateSigner(provider.api.registry, pair)); } - connect(provider: BodhiProvider): BodhiSigner { + connect(_provider: BodhiProvider): BodhiSigner { return logger.throwError('cannot alter JSON-RPC Signer connection', Logger.errors.UNSUPPORTED_OPERATION, { operation: 'connect', }); @@ -185,7 +185,7 @@ export class BodhiSigner extends AbstractSigner implements TypedDataSigner { }); } - signTransaction(transaction: Deferrable): Promise { + signTransaction(_transaction: Deferrable): Promise { return logger.throwError('signing transactions is unsupported', Logger.errors.UNSUPPORTED_OPERATION, { operation: 'signTransaction', }); @@ -278,7 +278,7 @@ export class BodhiSigner extends AbstractSigner implements TypedDataSigner { data: dataToString(data), value: BigNumber.from(tx.value || '0'), chainId: +this.provider.api.consts.evmAccounts.chainId.toString(), - wait: (confirmations?: number): Promise => { + wait: (_confirmations?: number): Promise => { const hex = result.status.isInBlock ? result.status.asInBlock.toHex() : result.status.asFinalized.toHex(); @@ -332,10 +332,9 @@ export class BodhiSigner extends AbstractSigner implements TypedDataSigner { } async _signTypedData( - domain: TypedDataDomain, - types: Record>, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value: Record + _domain: TypedDataDomain, + _types: Record>, + _value: Record ): Promise { return logger.throwError('_signTypedData is unsupported', Logger.errors.UNSUPPORTED_OPERATION, { operation: '_signTypedData', diff --git a/packages/eth-providers/package.json b/packages/eth-providers/package.json index 86b51701e..3df49b7ca 100644 --- a/packages/eth-providers/package.json +++ b/packages/eth-providers/package.json @@ -7,7 +7,7 @@ "clean": "rm -rf lib tsconfig.tsbuildinfo", "build": "tsc --build ./tsconfig.json", "gql:typegen": "graphql-codegen --config codegen.yml", - "test:CI": "vitest --run --config vitest.config.e2e.ts" + "test:e2e": "vitest --run --config vitest.config.e2e.ts" }, "peerDependencies": { "@acala-network/api": "~5.0.3-0", diff --git a/packages/eth-providers/src/__tests__/e2e/evm-rpc-provider.test.ts b/packages/eth-providers/src/__tests__/e2e/evm-rpc-provider.test.ts index 110589c6f..974a8fe99 100644 --- a/packages/eth-providers/src/__tests__/e2e/evm-rpc-provider.test.ts +++ b/packages/eth-providers/src/__tests__/e2e/evm-rpc-provider.test.ts @@ -1,4 +1,3 @@ -import { DUMMY_BLOCK_HASH } from '../../consts'; import { EvmRpcProvider } from '../../rpc-provider'; import { afterAll, describe, expect, it } from 'vitest'; import { runWithTiming, sleep } from '../../utils'; @@ -6,36 +5,13 @@ import dotenv from 'dotenv'; dotenv.config(); -const endpoint = process.env.ENDPOINT_URL || 'ws://127.0.0.1:9944'; const ACALA_NODE_URL = 'wss://acala-rpc-0.aca-api.network'; const ACALA_SUBQL = 'https://subql-query-acala.aca-api.network'; describe('connect random', () => { it('should throw error', async () => { - try { - const provider = EvmRpcProvider.from('ws://192.-'); - await provider.isReady(); - } catch (e) { - expect((e as any).type).to.equal('error'); - } - }); -}); - -describe('initilization', async () => { - const provider = EvmRpcProvider.from(ACALA_NODE_URL); - await provider.isReady(); - - afterAll(async () => { - await sleep(5000); - await provider.disconnect(); - }); - - it('should already has initial block number and hash', async () => { - expect(provider.latestBlockNumber).to.gt(-1); - expect(provider.latestFinalizedBlockNumber).to.gt(-1); - expect(await provider.getBlockNumber()).to.gt(-1); - expect(provider.latestBlockHash).not.to.equal(DUMMY_BLOCK_HASH); - expect(provider.latestFinalizedBlockHash).not.to.equal(DUMMY_BLOCK_HASH); + const provider = EvmRpcProvider.from('ws://192.-'); + await expect(provider.isReady()).rejects.toThrowError(); }); }); diff --git a/packages/eth-providers/src/__tests__/e2e/getTransactionReceipt.test.ts b/packages/eth-providers/src/__tests__/e2e/getTransactionReceipt.test.ts index 4bbcb6400..4418eb8c7 100644 --- a/packages/eth-providers/src/__tests__/e2e/getTransactionReceipt.test.ts +++ b/packages/eth-providers/src/__tests__/e2e/getTransactionReceipt.test.ts @@ -17,14 +17,13 @@ describe('TransactionReceipt', async () => { afterAll(async () => { await sleep(5000); await provider.disconnect(); - await sleep(1000); }); it('getTransactionReceipt', async () => { const account1 = evmAccounts[0]; const account2 = evmAccounts[1]; - const account1Wallet = new Wallet(account1.privateKey).connect(provider as any); + const account1Wallet = new Wallet(account1.privateKey).connect(provider); const acaContract = new Contract(ADDRESS.ACA, ACAABI.abi, account1Wallet); const pairs = createTestPairs(); diff --git a/packages/eth-providers/src/__tests__/e2e/json-rpc-provider.test.ts b/packages/eth-providers/src/__tests__/e2e/json-rpc-provider.test.ts index 703fe87a3..3f8fa511a 100644 --- a/packages/eth-providers/src/__tests__/e2e/json-rpc-provider.test.ts +++ b/packages/eth-providers/src/__tests__/e2e/json-rpc-provider.test.ts @@ -1,10 +1,9 @@ import { Contract, ContractFactory } from 'ethers'; import { Wallet } from '@ethersproject/wallet'; import { afterAll, describe, expect, it } from 'vitest'; -import { formatEther, hexZeroPad, parseEther } from 'ethers/lib/utils'; +import { hexZeroPad, parseEther } from 'ethers/lib/utils'; import { AcalaJsonRpcProvider } from '../../json-rpc-provider'; -import { JsonRpcProvider } from '@ethersproject/providers'; import { sleep } from '../../utils'; import echoJson from '../abis/Echo.json'; import erc20Json from '../abis/IERC20.json'; @@ -127,7 +126,7 @@ describe('JsonRpcProvider', async () => { expect(name).to.eq('USD Coin'); expect(symbol).to.eq('USDC'); expect(decimals).to.eq(6); - expect(totalSupply.gt(0)).to.be.true; + expect(totalSupply.gt(0)).toBeTruthy(); }); }); @@ -175,7 +174,7 @@ describe('JsonRpcProvider', async () => { provider.on('block', onBlock); }); - expect(blockNumber).to.be.greaterThan(0); + expect(blockNumber).to.be.eq(curBlockNumber + 1); }); // TODO: need to setup whole stack diff --git a/packages/eth-providers/src/__tests__/e2e/parseBlock.test.ts b/packages/eth-providers/src/__tests__/e2e/parseBlock.test.ts index f9c90082f..1e7086bac 100644 --- a/packages/eth-providers/src/__tests__/e2e/parseBlock.test.ts +++ b/packages/eth-providers/src/__tests__/e2e/parseBlock.test.ts @@ -100,11 +100,10 @@ describe.concurrent('getAllReceiptsAtBlock', () => { }); afterAll(async () => { - await sleep(5000); + await sleep(10_000); await apiK.disconnect(); await apiA.disconnect(); await apiM.disconnect(); - await sleep(5000); }); describe.concurrent('transfer kar', async () => { diff --git a/packages/eth-providers/src/__tests__/e2e/safemode.test.ts b/packages/eth-providers/src/__tests__/e2e/safemode.test.ts index 6f810409e..77a434cf5 100644 --- a/packages/eth-providers/src/__tests__/e2e/safemode.test.ts +++ b/packages/eth-providers/src/__tests__/e2e/safemode.test.ts @@ -47,7 +47,7 @@ describe('safe mode', () => { safeProvider.getBlockData('latest'), ]); expect(curBlock.hash).to.equal(curFinalizedBlock.hash); - expect(curBlock.hash).to.equal(safeProvider.latestFinalizedBlockHash); + expect(curBlock.hash).to.equal(await safeProvider.finalizedBlockHash); // real test await newBlock(false); @@ -62,7 +62,7 @@ describe('safe mode', () => { safeProvider.getBlockData('latest'), ]); expect(curBlock.hash).to.equal(curFinalizedBlock.hash); - expect(curBlock.hash).to.equal(safeProvider.latestFinalizedBlockHash); + expect(curBlock.hash).to.equal(await safeProvider.finalizedBlockHash); // make sure next block is not finalized await newBlock(false); @@ -89,7 +89,7 @@ describe('safe mode', () => { -------------------------- */ // ① expect(await safeProvider._ensureSafeModeBlockTagFinalization('latest')).to.equal( - safeProvider.latestFinalizedBlockHash + await safeProvider.finalizedBlockHash ); expect(await safeProvider._ensureSafeModeBlockTagFinalization('latest')).to.equal(curFinalizedBlock.hash); diff --git a/packages/eth-providers/src/__tests__/e2e/tx.test.ts b/packages/eth-providers/src/__tests__/e2e/tx.test.ts index c1c3261e2..0ff0049a8 100644 --- a/packages/eth-providers/src/__tests__/e2e/tx.test.ts +++ b/packages/eth-providers/src/__tests__/e2e/tx.test.ts @@ -23,11 +23,9 @@ describe('transaction tests', () => { const account1 = evmAccounts[0]; const account2 = evmAccounts[1]; const account3 = evmAccounts[2]; - const account4 = evmAccounts[3]; const wallet1 = new Wallet(account1.privateKey).connect(provider as any); const wallet2 = new Wallet(account2.privateKey).connect(provider as any); const wallet3 = new Wallet(account3.privateKey).connect(provider as any); - const wallet4 = new Wallet(account4.privateKey).connect(provider as any); let chainId: number; let storageByteDeposit: bigint; @@ -100,7 +98,7 @@ describe('transaction tests', () => { value: BigNumber.from(amount), }); - const data = provider.validSubstrateResources({ + const _data = provider.validSubstrateResources({ gasLimit: params.gasLimit, gasPrice: params.gasPrice, }); @@ -363,7 +361,7 @@ describe('transaction tests', () => { describe('with legacy EIP-155 signature', () => { it('has correct balance after transfer', async () => { - const balance1 = await queryBalance(account1.evmAddress); + const _balance1 = await queryBalance(account1.evmAddress); const balance2 = await queryBalance(account2.evmAddress); const transferTX: AcalaEvmTX = { @@ -372,12 +370,12 @@ describe('transaction tests', () => { }; const rawTx = await wallet1.signTransaction(transferTX); - const parsedTx = parseTransaction(rawTx); + const _parsedTx = parseTransaction(rawTx); const response = await provider.sendTransaction(rawTx); - const receipt = await response.wait(0); + const _receipt = await response.wait(0); - const _balance1 = await queryBalance(account1.evmAddress); + const __balance1 = await queryBalance(account1.evmAddress); const _balance2 = await queryBalance(account2.evmAddress); // TODO: check sender's balance is correct @@ -388,7 +386,7 @@ describe('transaction tests', () => { describe('with EIP-1559 signature', () => { it('has correct balance after transfer', async () => { - const balance1 = await queryBalance(account1.evmAddress); + const _balance1 = await queryBalance(account1.evmAddress); const balance2 = await queryBalance(account2.evmAddress); const priorityFee = BigNumber.from(2); @@ -402,12 +400,12 @@ describe('transaction tests', () => { }; const rawTx = await wallet1.signTransaction(transferTX); - const parsedTx = parseTransaction(rawTx); + const _parsedTx = parseTransaction(rawTx); const response = await provider.sendTransaction(rawTx); - const receipt = await response.wait(0); + const _receipt = await response.wait(0); - const _balance1 = await queryBalance(account1.evmAddress); + const __balance1 = await queryBalance(account1.evmAddress); const _balance2 = await queryBalance(account2.evmAddress); // TODO: check sender's balance is correct @@ -418,7 +416,7 @@ describe('transaction tests', () => { describe('with EIP-712 signature', () => { it('has correct balance after transfer', async () => { - const balance1 = await queryBalance(account1.evmAddress); + const _balance1 = await queryBalance(account1.evmAddress); const balance2 = await queryBalance(account2.evmAddress); const gasLimit = BigNumber.from('210000'); @@ -438,11 +436,11 @@ describe('transaction tests', () => { const sig = signTransaction(account1.privateKey, transferTX); const rawTx = serializeTransaction(transferTX, sig); - const parsedTx = parseTransaction(rawTx); + const _parsedTx = parseTransaction(rawTx); await provider.sendTransaction(rawTx); - const _balance1 = await queryBalance(account1.evmAddress); + const __balance1 = await queryBalance(account1.evmAddress); const _balance2 = await queryBalance(account2.evmAddress); // TODO: check sender's balance is correct diff --git a/packages/eth-providers/src/__tests__/testUtils.ts b/packages/eth-providers/src/__tests__/testUtils.ts index bf043d2ca..d57cbb5e0 100644 --- a/packages/eth-providers/src/__tests__/testUtils.ts +++ b/packages/eth-providers/src/__tests__/testUtils.ts @@ -7,7 +7,10 @@ type MochBlock = { }; type MochChain = MochBlock[]; -export const randHash = (): string => Math.floor(Math.random() * 66666666).toString(16); +export const randHash = (): string => { + const hash = Math.floor(Math.random() * 66666666).toString(16); + return '0x' + hash.padStart(64, '0'); +}; export const randReceipt = (blockNumber: number, blockHash: string): FullReceipt => ({ diff --git a/packages/eth-providers/src/__tests__/utils.test.ts b/packages/eth-providers/src/__tests__/utils.test.ts index 2339d36e7..62b558266 100644 --- a/packages/eth-providers/src/__tests__/utils.test.ts +++ b/packages/eth-providers/src/__tests__/utils.test.ts @@ -107,8 +107,6 @@ describe('runwithTiming', () => { }); it('returns correct error for running errors', async () => { - const runningTime = 1000; - const funcRes = 'vegeta'; const ERR_MSG = 'goku'; const f = async () => { throw new Error(ERR_MSG); @@ -198,7 +196,7 @@ describe('getHealthResult', () => { cacheInfo, curFinalizedHeight, ethCallTiming, - listenersCount: { newHead: 0, logs: 0 }, + listenersCount: { newHead: 0, newFinalizedHead: 0, logs: 0 }, }); expect(res).toEqual(expect.objectContaining({ @@ -221,7 +219,7 @@ describe('getHealthResult', () => { cacheInfo, curFinalizedHeight, ethCallTiming, - listenersCount: { newHead: 0, logs: 0 }, + listenersCount: { newHead: 0, newFinalizedHead: 0, logs: 0 }, }); expect(res).toEqual(expect.objectContaining({ @@ -251,7 +249,7 @@ describe('getHealthResult', () => { cacheInfo, curFinalizedHeight: curFinalizedHeightBad, ethCallTiming, - listenersCount: { newHead: 0, logs: 0 }, + listenersCount: { newHead: 0, newFinalizedHead: 0, logs: 0 }, }); expect(res).toEqual(expect.objectContaining({ @@ -279,7 +277,7 @@ describe('getHealthResult', () => { }, curFinalizedHeight, ethCallTiming, - listenersCount: { newHead: 0, logs: 0 }, + listenersCount: { newHead: 0, newFinalizedHead: 0, logs: 0 }, }); expect(res).toEqual(expect.objectContaining({ @@ -306,7 +304,7 @@ describe('getHealthResult', () => { cacheInfo, curFinalizedHeight, ethCallTiming: ethCallTimingBad, - listenersCount: { newHead: 0, logs: 0 }, + listenersCount: { newHead: 0, newFinalizedHead: 0, logs: 0 }, }); expect(res).toEqual(expect.objectContaining({ @@ -335,7 +333,7 @@ describe('getHealthResult', () => { cacheInfo, curFinalizedHeight, ethCallTiming: ethCallTimingBad, - listenersCount: { newHead: 0, logs: 0 }, + listenersCount: { newHead: 0, newFinalizedHead: 0, logs: 0 }, }); expect(res).toEqual(expect.objectContaining({ @@ -360,7 +358,7 @@ describe('getHealthResult', () => { cacheInfo, curFinalizedHeight, ethCallTiming: ethCallTimingBad, - listenersCount: { newHead: 0, logs: 0 }, + listenersCount: { newHead: 0, newFinalizedHead: 0, logs: 0 }, }); expect(res).toEqual(expect.objectContaining({ diff --git a/packages/eth-providers/src/base-provider.ts b/packages/eth-providers/src/base-provider.ts index 6084ff3e8..7db95cf03 100644 --- a/packages/eth-providers/src/base-provider.ts +++ b/packages/eth-providers/src/base-provider.ts @@ -14,8 +14,10 @@ import { } from '@ethersproject/abstract-provider'; import { AcalaEvmTX, checkSignatureType, parseTransaction } from '@acala-network/eth-transactions'; import { AccessListish } from 'ethers/lib/utils'; -import { AccountId, H160, Header, RuntimeVersion } from '@polkadot/types/interfaces'; +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'; @@ -23,11 +25,13 @@ import { Formatter } from '@ethersproject/providers'; import { FrameSystemAccountInfo } from '@polkadot/types/lookup'; import { Logger } from '@ethersproject/logger'; import { Network } from '@ethersproject/networks'; +import { Observable, ReplaySubject, Subscription, firstValueFrom, throwError } from 'rxjs'; import { Option, UInt, 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 { hexToU8a, isNull, u8aToHex, u8aToU8a } from '@polkadot/util'; @@ -40,7 +44,6 @@ import { BLOCK_STORAGE_LIMIT, CACHE_SIZE_WARNING, DUMMY_ADDRESS, - DUMMY_BLOCK_HASH, DUMMY_BLOCK_NONCE, DUMMY_LOGS_BLOOM, EMPTY_HEX_STRING, @@ -289,12 +292,30 @@ export abstract class BaseProvider extends AbstractProvider { readonly finalizedBlockHashes: MaxSizeSet; network?: Network | Promise; - latestBlockHash: string; - latestFinalizedBlockHash: string; - latestBlockNumber: number; - latestFinalizedBlockNumber: number; - runtimeVersion: number | undefined; - subscriptionStarted: boolean; + + #subscription: Promise<() => void> | undefined; + head$: Observable
; + finalizedHead$: Observable
; + best$ = new ReplaySubject<{ hash: string, number: number }>(1); + finalized$ = new ReplaySubject<{ hash: string, number: number }>(1); + + readonly #async = new AsyncScheduler(AsyncAction); + + readonly #headTasks: Map = new Map(); + readonly #finalizedHeadTasks: Map = new Map(); + + get bestBlockHash() { + return firstValueFrom(this.best$).then(({ hash }) => hash); + } + get bestBlockNumber() { + return firstValueFrom(this.best$).then(({ number }) => number); + } + get finalizedBlockHash() { + return firstValueFrom(this.finalized$).then(({ hash }) => hash); + } + get finalizedBlockNumber() { + return firstValueFrom(this.finalized$).then(({ number }) => number); + } constructor({ safeMode = false, @@ -317,15 +338,11 @@ export abstract class BaseProvider extends AbstractProvider { this.localMode = localMode; this.richMode = richMode; this.verbose = verbose; - this.latestBlockHash = DUMMY_BLOCK_HASH; - this.latestFinalizedBlockHash = DUMMY_BLOCK_HASH; - this.latestBlockNumber = -1; - this.latestFinalizedBlockNumber = -1; this.maxBlockCacheSize = maxBlockCacheSize; this.storageCache = new LRUCache({ max: storageCacheSize }); this.blockCache = new BlockCache(this.maxBlockCacheSize); this.finalizedBlockHashes = new MaxSizeSet(this.maxBlockCacheSize); - this.subscriptionStarted = false; + this.subql = subqlUrl ? new SubqlProvider(subqlUrl) : undefined; /* ---------- messages ---------- */ @@ -347,52 +364,101 @@ export abstract class BaseProvider extends AbstractProvider { return !!(value && value._isProvider); } - startSubscriptions = async (): Promise => { - await Promise.all( - [this._subscribeEventListeners(), this._subscribeNewBlocks(), this._subscribeRuntimeVersion()].flat() - ); - }; + startSubscriptions = async () => { + this.head$ = this.api.rx.rpc.chain.subscribeNewHeads(); + this.finalizedHead$ = this.api.rx.rpc.chain.subscribeFinalizedHeads(); + + const headSub = this.head$.subscribe(header => { + this.best$.next({ hash: header.hash.toHex(), number: header.number.toNumber() }); + }); - _subscribeEventListeners = async () => { - const subscriptionMethod = this.safeMode - ? this.api.rpc.chain.subscribeFinalizedHeads.bind(this) - : this.api.rpc.chain.subscribeNewHeads.bind(this); + const finalizedSub = this.finalizedHead$.subscribe(header => { + this.finalizedBlockHashes.add(header.hash.toHex()); + this.finalized$.next({ hash: header.hash.toHex(), number: header.number.toNumber() }); + }); - const headsUnsub = await subscriptionMethod(async (header: Header) => { - // update block cache - const blockNumber = header.number.toNumber(); - const blockHash = header.hash.toHex(); + await firstValueFrom(this.head$); + await firstValueFrom(this.finalizedHead$); - if (blockNumber === 0) return; // getAllReceiptsAtBlock doesn't work for block 0 - const receipts = await getAllReceiptsAtBlock(this.api, blockHash); - this.blockCache.addReceipts(blockHash, receipts); + const safeHead$ = this.safeMode + ? this.finalizedHead$ + : this.head$; - // eth_subscribe - await this._notifySubscribers(blockHash, receipts); + const headTasksSub = safeHead$.pipe( + // no reciepts for genesis block + filter(header => header.number.toNumber() > 0) + ).subscribe(header => { + const task = this.#async.schedule(this._onNewHead, 0, [header, 5]); + this.#headTasks.set(header.hash.toHex(), task); }); - const finalizedHeads = this.api.rpc.chain.subscribeFinalizedHeads.bind(this); - const finalizedUnsub = await finalizedHeads(async (header: Header) => { - if (header.number.toNumber() === 0) return; - + const finalizedTasksSub = this.finalizedHead$.pipe( + filter(header => header.number.toNumber() > 0) + ).subscribe(header => { // notify subscribers - const block = await this.getBlockData(header.hash.toHex(), false); - const response = hexlifyRpcResult(block); - this.eventListeners[SubscriptionType.NewFinalizedHeads].forEach((l) => l.cb(response)); + const task = this.#async.schedule(this._onNewFinalizedHead, 0, [header, 5]); + this.#finalizedHeadTasks.set(header.hash.toHex(), task); }); return () => { - headsUnsub(); - finalizedUnsub(); + headSub.unsubscribe(); + finalizedSub.unsubscribe(); + headTasksSub.unsubscribe(); + finalizedTasksSub.unsubscribe(); }; }; - _notifySubscribers = async (blockHash: string, receipts: FullReceipt[]) => { + _onNewHead = async ([header, attempts]: [Header, number]) => { + attempts--; + const blockHash = header.hash.toHex(); + try { + const receipts = await getAllReceiptsAtBlock(this.api, blockHash); + // update block cache + this.blockCache.addReceipts(blockHash, receipts); + + // eth_subscribe + await this._notifySubscribers(header, receipts); + this.#headTasks.get(blockHash)?.unsubscribe(); + this.#headTasks.delete(blockHash); + } catch (e) { + if (attempts) { + // reschedule after 1s + const task = this.#async.schedule(this._onNewHead, 1_000, [header, attempts]); + this.#headTasks.get(blockHash)?.unsubscribe(); + this.#headTasks.set(blockHash, task); + } else { + console.log('_onNewHead task failed, give up', blockHash, e.toString()); + } + } + }; + + _onNewFinalizedHead = async ([header, attempts]: [Header, number]) => { + attempts--; + const blockHash = header.hash.toHex(); + try { + const block = await this.getBlockDataForHeader(header, false); + const response = hexlifyRpcResult(block); + this.eventListeners[SubscriptionType.NewFinalizedHeads].forEach((l) => l.cb(response)); + this.#finalizedHeadTasks.get(blockHash)?.unsubscribe(); + this.#finalizedHeadTasks.delete(blockHash); + } catch (e) { + if (attempts) { + // reschedule after 1s + const task = this.#async.schedule(this._onNewFinalizedHead, 1_000, [header, attempts]); + this.#finalizedHeadTasks.get(blockHash)?.unsubscribe(); + this.#finalizedHeadTasks.set(blockHash, task); + } else { + console.log('_onNewFinalizedHead task failed, give up', blockHash, e.toString()); + } + } + }; + + _notifySubscribers = async (header: Header, receipts: FullReceipt[]) => { const headSubscribers = this.eventListeners[SubscriptionType.NewHeads]; const logSubscribers = this.eventListeners[SubscriptionType.Logs]; if (headSubscribers.length > 0 || logSubscribers.length > 0) { - const block = await this.getBlockData(blockHash, false); + const block = await this.getBlockDataForHeader(header, false); const response = hexlifyRpcResult(block); headSubscribers.forEach((l) => l.cb(response)); @@ -408,62 +474,6 @@ export abstract class BaseProvider extends AbstractProvider { } }; - // this will ensure after isReady, latest block info is ready - _subscribeNewBlocks = () => [this._subscribeFinalizedHeadsEnsureOnce(), this._subscribeHeadsEnsureOnce()]; - - _subscribeFinalizedHeadsEnsureOnce = () => - new Promise((resolve) => { - let resolved = false; - this.api.rpc.chain - .subscribeFinalizedHeads((header: Header) => { - this.latestFinalizedBlockNumber = header.number.toNumber(); - this.latestFinalizedBlockHash = header.hash.toHex(); - - this.finalizedBlockHashes.add(this.latestFinalizedBlockHash); - - if (!resolved) { - resolved = true; - resolve(); - } - }) - .catch((e) => { - throw e; - }); - }); - - _subscribeHeadsEnsureOnce = () => - new Promise((resolve) => { - let resolved = false; - this.api.rpc.chain - .subscribeNewHeads((header: Header) => { - this.latestBlockNumber = header.number.toNumber(); - this.latestBlockHash = header.hash.toHex(); - - if (!resolved) { - resolved = true; - resolve(); - } - }) - .catch((e) => { - throw e; - }); - }); - - _subscribeRuntimeVersion = () => - this.api.rpc.state.subscribeRuntimeVersion((runtime: RuntimeVersion) => { - const version = runtime.specVersion.toNumber(); - this.verbose && logger.info(`runtime version: ${version}`); - - if (!this.runtimeVersion || this.runtimeVersion === version) { - this.runtimeVersion = version; - } else { - logger.warn( - `runtime version changed: ${this.runtimeVersion} => ${version}, shutting down myself... good bye 👋` - ); - process.exit(1); - } - }); - setApi = (api: ApiPromise): void => { defineReadOnly(this, '_api', api); }; @@ -546,10 +556,11 @@ export abstract class BaseProvider extends AbstractProvider { await this.api.isReadyOrError; await this.getNetwork(); - if (!this.subscriptionStarted) { - this.subscriptionStarted = true; - await this.startSubscriptions(); + if (!this.#subscription) { + this.#subscription = this.startSubscriptions(); } + // wait for subscription to happen + await this.#subscription; } catch (e) { await this.api.disconnect(); throw e; @@ -557,6 +568,23 @@ export abstract class BaseProvider extends AbstractProvider { }; disconnect = async (): Promise => { + this.eventListeners[SubscriptionType.NewHeads] = []; + this.eventListeners[SubscriptionType.NewFinalizedHeads] = []; + this.eventListeners[SubscriptionType.Logs] = []; + this.#subscription && (await this.#subscription)(); + + let attempts = 5; + while(attempts) { + attempts--; + + const pendingTasks = this.#headTasks.size + this.#finalizedHeadTasks.size; + if (pendingTasks === 0) break; + + // wait 1 second for all tasks to complete, then try again + await new Promise(r => setTimeout(r, 1000)); + console.log(`disconnecting, waiting for ${pendingTasks} tasks to complete`); + } + await this.api.disconnect(); }; @@ -580,12 +608,16 @@ export abstract class BaseProvider extends AbstractProvider { }; getBlockNumber = async (): Promise => { - return this.safeMode ? this.latestFinalizedBlockNumber : this.latestBlockNumber; + return this.safeMode ? this.finalizedBlockNumber : this.bestBlockNumber; }; getBlockData = async (_blockTag: BlockTag | Promise, full?: boolean): Promise => { const blockTag = await this._ensureSafeModeBlockTagFinalization(_blockTag); const header = await this._getBlockHeader(blockTag); + return this.getBlockDataForHeader(header, full); + }; + + getBlockDataForHeader = async (header: Header, full?: boolean): Promise => { const blockHash = header.hash.toHex(); const blockNumber = header.number.toNumber(); @@ -646,11 +678,11 @@ export abstract class BaseProvider extends AbstractProvider { }; }; - getBlock = async (blockHashOrBlockTag: BlockTag | string | Promise): Promise => + getBlock = async (_blockHashOrBlockTag: BlockTag | string | Promise): Promise => throwNotImplemented('getBlock (please use `getBlockData` instead)'); getBlockWithTransactions = async ( - blockHashOrBlockTag: BlockTag | string | Promise + _blockHashOrBlockTag: BlockTag | string | Promise ): Promise => throwNotImplemented('getBlockWithTransactions (please use `getBlockData` instead)'); @@ -660,10 +692,10 @@ export abstract class BaseProvider extends AbstractProvider { ): Promise => { const blockTag = await this._ensureSafeModeBlockTagFinalization(await parseBlockTag(_blockTag)); - const { address, blockHash } = await resolveProperties({ - address: this._getAddress(addressOrName), - blockHash: this._getBlockHash(blockTag), - }); + const [address, blockHash] = await Promise.all([ + this._getAddress(addressOrName), + this._getBlockHash(blockTag), + ]); const substrateAddress = await this.getSubstrateAddress(address, blockHash); @@ -736,10 +768,10 @@ export abstract class BaseProvider extends AbstractProvider { if (blockTag === 'pending') return '0x'; - const { address, blockHash } = await resolveProperties({ - address: this._getAddress(addressOrName), - blockHash: this._getBlockHash(blockTag), - }); + const [address, blockHash] = await Promise.all([ + this._getAddress(addressOrName), + this._getBlockHash(blockTag), + ]); const contractInfo = await this.queryContractInfo(address, blockHash); @@ -749,7 +781,7 @@ export abstract class BaseProvider extends AbstractProvider { const codeHash = contractInfo.unwrap().codeHash; - const api = await (blockHash ? this.api.at(blockHash) : this.api); + const api = blockHash ? await this.api.at(blockHash) : this.api; const code = await api.query.evm.codes(codeHash); @@ -763,10 +795,10 @@ export abstract class BaseProvider extends AbstractProvider { ): Promise => { const blockTag = await this._ensureSafeModeBlockTagFinalization(await parseBlockTag(_blockTag)); - const { txRequest, blockHash } = await resolveProperties({ - txRequest: getTransactionRequest(_transaction), - blockHash: this._getBlockHash(blockTag), - }); + const [txRequest, blockHash] = await Promise.all([ + getTransactionRequest(_transaction), + this._getBlockHash(blockTag), + ]); const transaction = txRequest.gasLimit && txRequest.gasPrice ? txRequest : { ...txRequest, ...(await this._getEthGas()) }; @@ -832,11 +864,11 @@ export abstract class BaseProvider extends AbstractProvider { ): Promise => { const blockTag = await this._ensureSafeModeBlockTagFinalization(await parseBlockTag(_blockTag)); - const { address, blockHash, resolvedPosition } = await resolveProperties({ - address: this._getAddress(addressOrName), - blockHash: this._getBlockHash(blockTag), - resolvedPosition: Promise.resolve(position).then((p) => hexValue(p)), - }); + const [address, blockHash, resolvedPosition] = await Promise.all([ + this._getAddress(addressOrName), + this._getBlockHash(blockTag), + Promise.resolve(position).then(hexValue), + ]); const code = await this.queryStorage('evm.accountStorages', [address, hexZeroPad(resolvedPosition, 32)], blockHash); @@ -878,13 +910,13 @@ export abstract class BaseProvider extends AbstractProvider { // tx_fee_per_gas + (current_block / 30 + 5) << 16 + 10 const txFeePerGas = BigNumber.from((this.api.consts.evm.txFeePerGas as UInt).toBigInt()); - return txFeePerGas.add(BigNumber.from(this.latestBlockNumber).div(30).add(5).shl(16)).add(10); + return txFeePerGas.add(BigNumber.from(await this.bestBlockNumber).div(30).add(5).shl(16)).add(10); }; getGasPrice = async (validBlocks = 200): Promise => { if (!process.env.V2) return this.getGasPriceV1(); - return BigNumber.from(ONE_HUNDRED_GWEI).add(this.latestBlockNumber + validBlocks); + return BigNumber.from(ONE_HUNDRED_GWEI).add(await this.bestBlockNumber + validBlocks); }; getFeeData = async (): Promise => { @@ -953,7 +985,7 @@ export abstract class BaseProvider extends AbstractProvider { _estimateGasCost = async (extrinsic: SubmittableExtrinsic<'promise', ISubmittableResult>) => { const u8a = extrinsic.toU8a(); - const apiAt = await this.api.at(this.latestBlockHash); + const apiAt = await this.api.at(await this.bestBlockHash); const feeDetails = await apiAt.call.transactionPaymentApi.queryFeeDetails(u8a, u8a.length); const { baseFee, lenFee, adjustedWeightFee } = feeDetails.inclusionFee.unwrap(); const nativeTxFee = BigNumber.from(baseFee.toBigInt() + lenFee.toBigInt() + adjustedWeightFee.toBigInt()); @@ -1129,10 +1161,10 @@ export abstract class BaseProvider extends AbstractProvider { }; getSubstrateAddress = async (addressOrName: string, blockTag?: BlockTag | Promise): Promise => { - const { address, blockHash } = await resolveProperties({ - address: this._getAddress(addressOrName), - blockHash: this._getBlockHash(blockTag), - }); + const [address, blockHash] = await Promise.all([ + this._getAddress(addressOrName), + this._getBlockHash(blockTag), + ]); const substrateAccount = await this.queryStorage>('evmAccounts.accounts', [address], blockHash); @@ -1155,10 +1187,10 @@ export abstract class BaseProvider extends AbstractProvider { blockTag = 'latest'; } - const { address, blockHash } = await resolveProperties({ - address: this._getAddress(addressOrName), - blockHash: this._getBlockHash(blockTag), - }); + const [address, blockHash] = await Promise.all([ + this._getAddress(addressOrName), + this._getBlockHash(blockTag), + ]); const accountInfo = await this.queryStorage>('evm.accounts', [address], blockHash); @@ -1421,70 +1453,32 @@ export abstract class BaseProvider extends AbstractProvider { result.timestamp = Math.floor((await this.queryStorage('timestamp.now', [], result.blockHash)).toNumber() / 1000); - result.wait = async (confirms?: number, timeout?: number) => { + result.wait = async (confirms?: number, timeoutMs?: number) => { if (confirms === null || confirms === undefined) { confirms = 1; + } else if (confirms < 0) { + throw new Error('invalid confirms value'); } - if (timeout === null || timeout === undefined) { - timeout = 0; + if (timeoutMs === null || timeoutMs === undefined) { + timeoutMs = 0; + } else if (timeoutMs < 0) { + throw new Error('invalid timeout value'); } - return new Promise((resolve, reject) => { - const cancelFuncs: Array<() => void> = []; + let wait$ = timeoutMs ? this.head$.pipe( + timeout({ + first: timeoutMs, + with: () => throwError(() => logger.makeError('timeout exceeded', Logger.errors.TIMEOUT, { timeout: timeoutMs })), + })) : this.head$; - let done = false; + wait$ = wait$.pipe(first((head) => head.number.toNumber() - startBlock + 1 >= confirms)); - const alreadyDone = function (): boolean { - if (done) { - return true; - } - done = true; - cancelFuncs.forEach((func) => { - func(); - }); - return false; - }; - - this.api.rpc.chain - .subscribeNewHeads((head) => { - const blockNumber = head.number.toNumber(); - - if ((confirms as number) <= blockNumber - startBlock + 1) { - const receipt = this.getReceiptAtBlockFromChain(hash, startBlockHash); - if (alreadyDone()) { - return; - } - - // tx was just mined so won't be null - resolve(receipt as Promise); - } - }) - .then((unsubscribe) => { - cancelFuncs.push(() => { - unsubscribe(); - }); - }) - .catch((error) => { - reject(error); - }); - - if (typeof timeout === 'number' && timeout > 0) { - const timer = setTimeout(() => { - if (alreadyDone()) { - return; - } - reject(logger.makeError('timeout exceeded', Logger.errors.TIMEOUT, { timeout: timeout })); - }, timeout); + await firstValueFrom(wait$); - if (timer.unref) { - timer.unref(); - } + const receipt = await this.getReceiptAtBlockFromChain(hash, startBlockHash); - cancelFuncs.push(() => { - clearTimeout(timer); - }); - } - }); + // tx was just mined so won't be null + return receipt; }; return result; @@ -1503,7 +1497,7 @@ export abstract class BaseProvider extends AbstractProvider { } case 'finalized': case 'safe': { - return this.latestFinalizedBlockNumber; + return this.finalizedBlockNumber; } default: { if (isHexString(blockTag, 32)) { @@ -1529,7 +1523,7 @@ export abstract class BaseProvider extends AbstractProvider { return logger.throwError('pending tag not implemented', Logger.errors.UNSUPPORTED_OPERATION); } case 'latest': { - return this.safeMode ? this.latestFinalizedBlockHash : this.latestBlockHash; + return this.safeMode ? this.finalizedBlockHash : this.bestBlockHash; } case 'earliest': { const hash = this.api.genesisHash; @@ -1537,7 +1531,7 @@ export abstract class BaseProvider extends AbstractProvider { } case 'finalized': case 'safe': { - return this.latestFinalizedBlockHash; + return this.finalizedBlockHash; } default: { if (isHexString(blockTag, 32)) { @@ -1550,7 +1544,7 @@ export abstract class BaseProvider extends AbstractProvider { return logger.throwArgumentError('block number should be less than u32', 'blockNumber', blockNumber); } - const isFinalized = blockNumber.lte(this.latestFinalizedBlockNumber); + const isFinalized = blockNumber.lte(await this.finalizedBlockNumber); const cacheKey = `blockHash-${blockNumber.toString()}`; if (isFinalized) { @@ -1597,7 +1591,7 @@ export abstract class BaseProvider extends AbstractProvider { _isBlockFinalized = async (blockTag: BlockTag): Promise => { const [blockHash, blockNumber] = await Promise.all([this._getBlockHash(blockTag), this._getBlockNumber(blockTag)]); - return this.latestFinalizedBlockNumber >= blockNumber && this._isBlockCanonical(blockHash, blockNumber); + return await this.finalizedBlockNumber >= blockNumber && this._isBlockCanonical(blockHash, blockNumber); }; _isTransactionFinalized = async (txHash: string): Promise => { @@ -1611,7 +1605,7 @@ export abstract class BaseProvider extends AbstractProvider { if (!this.safeMode || !_blockTag) return _blockTag; const blockTag = await _blockTag; - if (blockTag === 'latest') return this.latestFinalizedBlockHash; + if (blockTag === 'latest') return this.finalizedBlockHash; const isBlockFinalized = await this._isBlockFinalized(blockTag); @@ -1721,7 +1715,7 @@ export abstract class BaseProvider extends AbstractProvider { }; // Queries - getTransaction = (txHash: string): Promise => + getTransaction = (_txHash: string): Promise => throwNotImplemented('getTransaction (deprecated: please use getTransactionByHash)'); getTransactionByHash = async (txHash: string): Promise => { @@ -1742,7 +1736,7 @@ export abstract class BaseProvider extends AbstractProvider { return receiptToTransaction(receipt, block); }; - getTransactionReceipt = async (txHash: string): Promise => + getTransactionReceipt = async (_txHash: string): Promise => throwNotImplemented('getTransactionReceipt (please use `getReceiptByHash` instead)'); getReceiptByHash = async (txHash: string): Promise => @@ -1827,10 +1821,11 @@ export abstract class BaseProvider extends AbstractProvider { ); // ideally pastNblock should have EVM TX + const finalizedBlockNumber = await this.finalizedBlockNumber; const pastNblock = - this.latestFinalizedBlockNumber > HEALTH_CHECK_BLOCK_DISTANCE - ? this.latestFinalizedBlockNumber - HEALTH_CHECK_BLOCK_DISTANCE - : this.latestFinalizedBlockNumber; + finalizedBlockNumber > HEALTH_CHECK_BLOCK_DISTANCE + ? finalizedBlockNumber - HEALTH_CHECK_BLOCK_DISTANCE + : finalizedBlockNumber; const getBlockPromise = runWithTiming(async () => this.getBlockData(pastNblock, false)); const getFullBlockPromise = runWithTiming(async () => this.getBlockData(pastNblock, true)); @@ -1850,7 +1845,7 @@ export abstract class BaseProvider extends AbstractProvider { const [indexerMeta, ethCallTiming] = await Promise.all([this.getIndexerMetadata(), this._timeEthCalls()]); const cacheInfo = this.getCachInfo(); - const curFinalizedHeight = this.latestFinalizedBlockNumber; + const curFinalizedHeight = await this.finalizedBlockNumber; const listenersCount = { newHead: this.eventListeners[SubscriptionType.NewHeads]?.length || 0, newFinalizedHead: this.eventListeners[SubscriptionType.NewFinalizedHeads]?.length || 0, @@ -1867,12 +1862,12 @@ export abstract class BaseProvider extends AbstractProvider { }; // ENS - lookupAddress = (address: string | Promise): Promise => throwNotImplemented('lookupAddress'); + lookupAddress = (_address: string | Promise): Promise => throwNotImplemented('lookupAddress'); waitForTransaction = ( - transactionHash: string, - confirmations?: number, - timeout?: number + _transactionHash: string, + _confirmations?: number, + _timeout?: number ): Promise => throwNotImplemented('waitForTransaction'); // Event Emitter (ish) @@ -2032,11 +2027,11 @@ export abstract class BaseProvider extends AbstractProvider { return found; }; - on = (eventName: EventType, listener: Listener): Provider => throwNotImplemented('on'); - once = (eventName: EventType, listener: Listener): Provider => throwNotImplemented('once'); - emit = (eventName: EventType, ...args: Array): boolean => throwNotImplemented('emit'); - listenerCount = (eventName?: EventType): number => throwNotImplemented('listenerCount'); - listeners = (eventName?: EventType): Array => throwNotImplemented('listeners'); - off = (eventName: EventType, listener?: Listener): Provider => throwNotImplemented('off'); - removeAllListeners = (eventName?: EventType): Provider => throwNotImplemented('removeAllListeners'); + on = (_eventName: EventType, _listener: Listener): Provider => throwNotImplemented('on'); + once = (_eventName: EventType, _listener: Listener): Provider => throwNotImplemented('once'); + emit = (_eventName: EventType, ..._args: Array): boolean => throwNotImplemented('emit'); + listenerCount = (_eventName?: EventType): number => throwNotImplemented('listenerCount'); + listeners = (_eventName?: EventType): Array => throwNotImplemented('listeners'); + off = (_eventName: EventType, _listener?: Listener): Provider => throwNotImplemented('off'); + removeAllListeners = (_eventName?: EventType): Provider => throwNotImplemented('removeAllListeners'); } diff --git a/packages/eth-providers/src/json-rpc-provider.ts b/packages/eth-providers/src/json-rpc-provider.ts index 89972ebf6..5f638db8e 100644 --- a/packages/eth-providers/src/json-rpc-provider.ts +++ b/packages/eth-providers/src/json-rpc-provider.ts @@ -1,7 +1,6 @@ import { BigNumber, Transaction, logger } from 'ethers'; import { ConnectionInfo, Logger, hexDataLength, hexlify } from 'ethers/lib/utils'; import { JsonRpcProvider, Networkish, TransactionReceipt, TransactionResponse } from '@ethersproject/providers'; -import { sleep } from './utils'; export class AcalaJsonRpcProvider extends JsonRpcProvider { constructor(url: ConnectionInfo | string, network?: Networkish) { diff --git a/packages/eth-providers/src/rpc-provider.ts b/packages/eth-providers/src/rpc-provider.ts index 4e1a64e91..5496b478c 100644 --- a/packages/eth-providers/src/rpc-provider.ts +++ b/packages/eth-providers/src/rpc-provider.ts @@ -4,6 +4,7 @@ import { options } from '@acala-network/api'; import { runtimePatch } from './utils/temp-runtime-patch'; export class EvmRpcProvider extends BaseProvider { + constructor(endpoint: string | string[], opts?: BaseProviderOptions) { super(opts); diff --git a/packages/eth-providers/src/utils/utils.ts b/packages/eth-providers/src/utils/utils.ts index 034356d32..86205d527 100644 --- a/packages/eth-providers/src/utils/utils.ts +++ b/packages/eth-providers/src/utils/utils.ts @@ -39,6 +39,7 @@ export interface HealthResult { // listeners listenersCount: { newHead: number; + newFinalizedHead: number, logs: number; }; }; @@ -51,6 +52,7 @@ export interface HealthData { ethCallTiming: EthCallTimingResult; listenersCount: { newHead: number; + newFinalizedHead: number; logs: number; }; } diff --git a/packages/eth-providers/tsconfig.json b/packages/eth-providers/tsconfig.json index bd68f4abe..2a2442e80 100644 --- a/packages/eth-providers/tsconfig.json +++ b/packages/eth-providers/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "lib": ["es2020", "dom"], "outDir": "lib", "rootDir": "src" }, diff --git a/packages/eth-rpc-adapter/src/__tests__/e2e/bridge.test.ts b/packages/eth-rpc-adapter/src/__tests__/e2e/bridge.test.ts index a272926d6..0a02d9bab 100644 --- a/packages/eth-rpc-adapter/src/__tests__/e2e/bridge.test.ts +++ b/packages/eth-rpc-adapter/src/__tests__/e2e/bridge.test.ts @@ -1,25 +1,23 @@ import { Eip1193Bridge } from '../../eip1193-bridge'; import { EvmRpcProvider } from '@acala-network/eth-providers'; import { Wallet } from '@ethersproject/wallet'; -import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { afterAll, describe, expect, it } from 'vitest'; import dotenv from 'dotenv'; dotenv.config(); const endpoint = process.env.ENDPOINT_URL || 'ws://127.0.0.1:9944'; -describe('e2e test', () => { +describe('e2e test', async () => { const signer = new Wallet('0x5a214c9bcb10dfe58af9b349cad6f4564cd6f10d880bdfcf780e5812c3cbc855'); const provider = EvmRpcProvider.from(endpoint); + await provider.isReady(); - beforeAll(async () => { - await provider.isReady(); - }); afterAll(async () => { await provider.disconnect(); }); - const bridge = new Eip1193Bridge(provider, signer as any); + const bridge = new Eip1193Bridge(provider, signer); it('eth_getBlockByNumber latest', async () => { const result = await bridge.send('eth_getBlockByNumber', ['latest', false]); @@ -50,8 +48,4 @@ describe('e2e test', () => { bridge.send('eth_getBalance', ['0xb00cB924ae22b2BBb15E10c17258D6a2af980421', '0xffffffff']) ).rejects.toThrowError('header not found'); }); - - afterAll(async () => { - await provider.disconnect(); - }); }); diff --git a/packages/eth-rpc-adapter/src/__tests__/e2e/endpoint.test.ts b/packages/eth-rpc-adapter/src/__tests__/e2e/endpoint.test.ts index 4aa8015e9..75a19dd23 100644 --- a/packages/eth-rpc-adapter/src/__tests__/e2e/endpoint.test.ts +++ b/packages/eth-rpc-adapter/src/__tests__/e2e/endpoint.test.ts @@ -25,7 +25,7 @@ import { ApiPromise, WsProvider } from '@polkadot/api'; import { BigNumber } from '@ethersproject/bignumber'; import { Contract } from '@ethersproject/contracts'; import { DUMMY_LOGS_BLOOM, EvmRpcProvider, sleep } from '@acala-network/eth-providers'; -import { Interface, hexValue, hexlify, parseUnits } from 'ethers/lib/utils'; +import { Interface, hexValue, parseUnits } from 'ethers/lib/utils'; import { JsonRpcError } from '@acala-network/eth-rpc-adapter/server'; import { KARURA_ETH_RPC_URL, NODE_RPC_URL, SUBQL_URL, WS_URL, bigIntDiff, rpcGet } from './utils'; import { Log } from '@ethersproject/abstract-provider'; diff --git a/packages/eth-rpc-adapter/src/__tests__/e2e/errors.test.ts b/packages/eth-rpc-adapter/src/__tests__/e2e/errors.test.ts index 7e889b297..564eafd0a 100644 --- a/packages/eth-rpc-adapter/src/__tests__/e2e/errors.test.ts +++ b/packages/eth-rpc-adapter/src/__tests__/e2e/errors.test.ts @@ -1,13 +1,13 @@ -import { BigNumber, Wallet } from 'ethers'; import { JsonRpcProvider } from '@ethersproject/providers'; import { RPC_URL, rpcGet } from './utils'; +import { Wallet } from 'ethers'; import { describe, expect, it } from 'vitest'; import axios from 'axios'; const eth_getEthGas = rpcGet('eth_getEthGas', RPC_URL); -const eth_blockNumber = rpcGet('eth_blockNumber', RPC_URL); +const _eth_blockNumber = rpcGet('eth_blockNumber', RPC_URL); const eth_sendRawTransaction = rpcGet('eth_sendRawTransaction', RPC_URL); -const eth_getTransactionReceipt = rpcGet('eth_getTransactionReceipt', RPC_URL); +const _eth_getTransactionReceipt = rpcGet('eth_getTransactionReceipt', RPC_URL); const eth_chainId = rpcGet('eth_chainId', RPC_URL); describe('errors', () => { diff --git a/packages/eth-rpc-adapter/src/__tests__/e2e/signer.test.ts b/packages/eth-rpc-adapter/src/__tests__/e2e/signer.test.ts index 06b836b67..396deb182 100644 --- a/packages/eth-rpc-adapter/src/__tests__/e2e/signer.test.ts +++ b/packages/eth-rpc-adapter/src/__tests__/e2e/signer.test.ts @@ -36,7 +36,6 @@ describe('eth_accounts', async () => { }); afterAll(async () => { - await new Promise(r => setTimeout(r, 5_000)); await provider.disconnect(); }); }); diff --git a/packages/eth-rpc-adapter/src/eip1193-bridge.ts b/packages/eth-rpc-adapter/src/eip1193-bridge.ts index 381b4ec6f..e9bd11b7e 100644 --- a/packages/eth-rpc-adapter/src/eip1193-bridge.ts +++ b/packages/eth-rpc-adapter/src/eip1193-bridge.ts @@ -77,9 +77,9 @@ class Eip1193BridgeImpl { } // TODO: maybe can encapsulate all provider info into one call `net_Info` or something - async net_runtimeVersion(params: any[]): Promise { + async net_runtimeVersion(params: any[]) { validate([], params); - return this.#provider.runtimeVersion; + return this.#provider.api.runtimeVersion.specVersion.toNumber(); } /** diff --git a/packages/eth-rpc-adapter/src/index.ts b/packages/eth-rpc-adapter/src/index.ts index 7b7c23448..f9ce6be76 100644 --- a/packages/eth-rpc-adapter/src/index.ts +++ b/packages/eth-rpc-adapter/src/index.ts @@ -2,6 +2,7 @@ import 'dd-trace/init'; import { Eip1193Bridge } from './eip1193-bridge'; import { EvmRpcProvider } from '@acala-network/eth-providers'; import { Router } from './router'; +import { monitorRuntime } from './monitor-runtime'; import { yargsOptions as opts } from './utils'; import { version } from './_version'; import EthRpcServer from './server'; @@ -33,6 +34,8 @@ export async function start(): Promise { server.start(); await provider.isReady(); + await monitorRuntime(provider); + if (provider.subql) { const genesisHash = await provider.subql.checkGraphql(); if (genesisHash !== provider.genesisHash) { diff --git a/packages/eth-rpc-adapter/src/middlewares/errorHandler.ts b/packages/eth-rpc-adapter/src/middlewares/errorHandler.ts index 298250d59..6275fdbbb 100644 --- a/packages/eth-rpc-adapter/src/middlewares/errorHandler.ts +++ b/packages/eth-rpc-adapter/src/middlewares/errorHandler.ts @@ -1,7 +1,7 @@ import { ErrorHandleFunction } from 'connect'; import { InternalError, InvalidRequest, JSONRPCError } from '../errors'; -export const errorHandler: ErrorHandleFunction = (err, req, res, next) => { +export const errorHandler: ErrorHandleFunction = (err, _req, res, _next) => { if (err) { let error: JSONRPCError; diff --git a/packages/eth-rpc-adapter/src/monitor-runtime.ts b/packages/eth-rpc-adapter/src/monitor-runtime.ts new file mode 100644 index 000000000..e9b845b49 --- /dev/null +++ b/packages/eth-rpc-adapter/src/monitor-runtime.ts @@ -0,0 +1,26 @@ +import { EvmRpcProvider } from '@acala-network/eth-providers'; +import { first, map } from 'rxjs/operators'; +import { firstValueFrom } from 'rxjs'; + +export const monitorRuntime = async (provider: EvmRpcProvider) => { + await provider.isReady(); + + const runtimeVersion$ = provider.api.rx.rpc.state.subscribeRuntimeVersion(); + + const initialRuntimeVersion = await firstValueFrom(runtimeVersion$.pipe(map(runtime => runtime.specVersion.toNumber()))); + + runtimeVersion$.subscribe(runtime => { + const version = runtime.specVersion.toNumber(); + provider.verbose && console.log(`runtime version: ${version}`); + }); + + runtimeVersion$.pipe( + // runtime changed + first(runtime => runtime.specVersion.toNumber() !== initialRuntimeVersion) + ).subscribe(runtime => { + console.warn( + `runtime version changed: ${initialRuntimeVersion} => ${runtime.specVersion.toNumber()}, shutting down myself... good bye 👋` + ); + process.exit(1); + }); +}; diff --git a/packages/eth-rpc-adapter/src/validate.ts b/packages/eth-rpc-adapter/src/validate.ts index c15dcf8d3..a90b1d0d6 100644 --- a/packages/eth-rpc-adapter/src/validate.ts +++ b/packages/eth-rpc-adapter/src/validate.ts @@ -70,7 +70,7 @@ export const validateBlock = (data: any) => { } }; -export const validateTransaction = (data: any) => { +export const validateTransaction = (_data: any) => { // @TODO };