From 6c075dba3fefa27c833e5c71bf91319ac2466b0b Mon Sep 17 00:00:00 2001 From: Muhammad Altabba <24407834+Muhammad-Altabba@users.noreply.github.com> Date: Wed, 3 Jan 2024 12:49:34 +0100 Subject: [PATCH] Increase Unit test coverage for web3-eth above 90% (#6663) * fix issues in Common config and add a test to accounts * add test cases * update CHAGELOG.md --- packages/web3-eth-accounts/CHANGELOG.md | 6 +- .../src/tx/baseTransaction.ts | 32 ++- .../utils/prepare_transaction_for_signing.ts | 2 +- .../prepare_transaction_for_signing.test.ts | 151 ++++++++++++- .../wait_for_transaction_receipt.test.ts | 198 +++++++++++------- .../watch_transaction_by_subscription.test.ts | 130 ++++++++++++ 6 files changed, 440 insertions(+), 79 deletions(-) create mode 100644 packages/web3-eth/test/unit/utils/watch_transaction_by_subscription.test.ts diff --git a/packages/web3-eth-accounts/CHANGELOG.md b/packages/web3-eth-accounts/CHANGELOG.md index 5e1e9bee8c1..337dd2292b0 100644 --- a/packages/web3-eth-accounts/CHANGELOG.md +++ b/packages/web3-eth-accounts/CHANGELOG.md @@ -149,4 +149,8 @@ Documentation: - Fixed `recover` function, `v` will be normalized to value 0,1 (#6344) -## [Unreleased] \ No newline at end of file +## [Unreleased] + +### Fixed + +- Send Transaction config used to be ignored if the passed `common` did not have a `copy()` and the `chainId` was not provided (#6663) diff --git a/packages/web3-eth-accounts/src/tx/baseTransaction.ts b/packages/web3-eth-accounts/src/tx/baseTransaction.ts index cb47d2bad22..3f8da6d4e41 100644 --- a/packages/web3-eth-accounts/src/tx/baseTransaction.ts +++ b/packages/web3-eth-accounts/src/tx/baseTransaction.ts @@ -15,7 +15,7 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ -import { Numbers } from 'web3-types'; +import { Common as CommonType, Numbers } from 'web3-types'; import { bytesToHex } from 'web3-utils'; import { MAX_INTEGER, MAX_UINT64, SECP256K1_ORDER_DIV_2, secp256k1 } from './constants.js'; import { toUint8Array, uint8ArrayToBigInt, unpadUint8Array } from '../common/utils.js'; @@ -389,6 +389,8 @@ export abstract class BaseTransaction { * @param chainId - Chain ID from tx options (typed txs) or signature (legacy tx) */ protected _getCommon(common?: Common, chainId?: Numbers) { + // TODO: this function needs to be reviewed and the code to be more clean + // check issue https://github.com/web3/web3.js/issues/6666 // Chain ID provided if (chainId !== undefined) { const chainIdBigInt = uint8ArrayToBigInt(toUint8Array(chainId)); @@ -425,6 +427,34 @@ export abstract class BaseTransaction { if (common?.copy && typeof common?.copy === 'function') { return common.copy(); } + // TODO: Recheck this next block when working on https://github.com/web3/web3.js/issues/6666 + // This block is to handle when `chainId` was not passed and the `common` object does not have `copy()` + // If it was meant to be unsupported to process `common` in this case, an exception should be thrown instead of the following block + if (common) { + const hardfork = + typeof common.hardfork === 'function' + ? common.hardfork() + : // eslint-disable-next-line @typescript-eslint/unbound-method + (common.hardfork as unknown as string); + + return Common.custom( + { + name: 'custom-chain', + networkId: common.networkId + ? common.networkId() + : BigInt((common as unknown as CommonType).customChain?.networkId) ?? + undefined, + chainId: common.chainId + ? common.chainId() + : BigInt((common as unknown as CommonType).customChain?.chainId) ?? + undefined, + }, + { + baseChain: this.DEFAULT_CHAIN, + hardfork: hardfork || this.DEFAULT_HARDFORK, + }, + ); + } return new Common({ chain: this.DEFAULT_CHAIN, hardfork: this.DEFAULT_HARDFORK }); } diff --git a/packages/web3-eth/src/utils/prepare_transaction_for_signing.ts b/packages/web3-eth/src/utils/prepare_transaction_for_signing.ts index 607994d05a4..fe761ada1f6 100644 --- a/packages/web3-eth/src/utils/prepare_transaction_for_signing.ts +++ b/packages/web3-eth/src/utils/prepare_transaction_for_signing.ts @@ -68,7 +68,7 @@ const getEthereumjsTransactionOptions = ( if (!hasTransactionSigningOptions) { // if defaultcommon is specified, use that. if (web3Context.defaultCommon) { - common = web3Context.defaultCommon; + common = { ...web3Context.defaultCommon }; if (isNullish(common.hardfork)) common.hardfork = transaction.hardfork ?? web3Context.defaultHardfork; diff --git a/packages/web3-eth/test/unit/prepare_transaction_for_signing.test.ts b/packages/web3-eth/test/unit/prepare_transaction_for_signing.test.ts index d8d30ee0f65..d53785c2e36 100644 --- a/packages/web3-eth/test/unit/prepare_transaction_for_signing.test.ts +++ b/packages/web3-eth/test/unit/prepare_transaction_for_signing.test.ts @@ -16,7 +16,6 @@ along with web3.js. If not, see . */ import { - Common, EthExecutionAPI, HexString, Web3NetAPI, @@ -33,6 +32,7 @@ import { AccessListEIP2930Transaction, FeeMarketEIP1559Transaction, Transaction, + Hardfork, } from 'web3-eth-accounts'; import { prepareTransactionForSigning } from '../../src/utils/prepare_transaction_for_signing'; import { validTransactions } from '../fixtures/prepare_transaction_for_signing'; @@ -70,7 +70,7 @@ describe('prepareTransactionForSigning', () => { if (isNullish(tx.common)) { if (options.web3Context.defaultCommon) { - const common = options.web3Context.defaultCommon as unknown as Common; + const common = options.web3Context.defaultCommon; const chainId = common.customChain.chainId as string; const networkId = common.customChain.networkId as string; const name = common.customChain.name as string; @@ -102,6 +102,153 @@ describe('prepareTransactionForSigning', () => { expect(ethereumjsTx.common.chainName()).toBe('test'); }); }); + + it('should be able to read Hardfork from context.defaultHardfork', async () => { + const context = new Web3Context({ + provider: new HttpProvider('http://127.0.0.1'), + config: { defaultNetworkId: '0x9' }, + }); + context.defaultChain = 'mainnet'; + context.defaultHardfork = Hardfork.Istanbul; + + async function transactionBuilder(options: { + transaction: TransactionType; + web3Context: Web3Context; + privateKey?: HexString | Uint8Array; + fillGasPrice?: boolean; + fillGasLimit?: boolean; + }): Promise { + const tx = { ...options.transaction }; + return tx as unknown as ReturnType; + } + + context.transactionBuilder = transactionBuilder; + + const ethereumjsTx = await prepareTransactionForSigning( + { + chainId: 1458, + nonce: 1, + gasPrice: BigInt(20000000000), + gas: BigInt(21000), + to: '0xF0109fC8DF283027b6285cc889F5aA624EaC1F55', + from: '0x2c7536E3605D9C16a7a3D7b1898e529396a65c23', + value: '1000000000', + input: '', + networkId: 999, + }, + context, + ); + expect(ethereumjsTx.common.hardfork()).toBe(Hardfork.Istanbul); + expect(ethereumjsTx.common.networkId().toString()).toBe('999'); + }); + + it('should be able to read Hardfork from context.config.defaultHardfork and context.defaultCommon.hardfork', async () => { + const context = new Web3Context({ + provider: new HttpProvider('http://127.0.0.1'), + config: { defaultNetworkId: '0x9' }, + }); + context.defaultChain = 'mainnet'; + + // if the value here is different from the one in context.defaultCommon.hardfork + // Then an error will be thrown: + // "ConfigHardforkMismatchError: Web3Config hardfork doesnt match in defaultHardfork london and common.hardfork istanbul" + context.config.defaultHardfork = Hardfork.Istanbul; + context.defaultCommon = { + customChain: { + name: 'test', + networkId: 111, + chainId: 1458, + }, + hardfork: Hardfork.Istanbul, + baseChain: 'mainnet', + } as any; + + async function transactionBuilder(options: { + transaction: TransactionType; + web3Context: Web3Context; + privateKey?: HexString | Uint8Array; + fillGasPrice?: boolean; + fillGasLimit?: boolean; + }): Promise { + const tx = { ...options.transaction }; + return tx as unknown as ReturnType; + } + + context.transactionBuilder = transactionBuilder; + + const ethereumjsTx = await prepareTransactionForSigning( + { + chainId: 1458, + nonce: 1, + gasPrice: BigInt(20000000000), + gas: BigInt(21000), + to: '0xF0109fC8DF283027b6285cc889F5aA624EaC1F55', + from: '0x2c7536E3605D9C16a7a3D7b1898e529396a65c23', + value: '1000000000', + input: '', + }, + context, + ); + expect(ethereumjsTx.common.hardfork()).toBe(Hardfork.Istanbul); + expect(ethereumjsTx.common.networkId().toString()).toBe('111'); + }); + + it('should give priorities to tx.hardfork and tx.networkId over values from context', async () => { + const context = new Web3Context({ + provider: new HttpProvider('http://127.0.0.1'), + config: { defaultNetworkId: '0x9' }, + }); + context.defaultChain = 'mainnet'; + + // if the value here is different from the one in context.defaultCommon.hardfork + // Then an error will be thrown: + // "ConfigHardforkMismatchError: Web3Config hardfork doesnt match in defaultHardfork london and common.hardfork istanbul" + context.config.defaultHardfork = Hardfork.Istanbul; + context.defaultCommon = { + customChain: { + name: 'test', + networkId: 111, + chainId: 1458, + }, + hardfork: Hardfork.Istanbul, + baseChain: 'mainnet', + } as any; + + async function transactionBuilder(options: { + transaction: TransactionType; + web3Context: Web3Context; + privateKey?: HexString | Uint8Array; + fillGasPrice?: boolean; + fillGasLimit?: boolean; + }): Promise { + const tx = { ...options.transaction }; + return tx as unknown as ReturnType; + } + + context.transactionBuilder = transactionBuilder; + + // context.transactionBuilder = defaultTransactionBuilder; + + const ethereumjsTx = await prepareTransactionForSigning( + { + chainId: 1458, + nonce: 1, + gasPrice: BigInt(20000000000), + gas: BigInt(21000), + to: '0xF0109fC8DF283027b6285cc889F5aA624EaC1F55', + from: '0x2c7536E3605D9C16a7a3D7b1898e529396a65c23', + value: '1000000000', + input: '', + networkId: 999, + hardfork: Hardfork.Chainstart, + chain: 'mainnet', + }, + context, + ); + expect(ethereumjsTx.common.hardfork()).toBe(Hardfork.Chainstart); + expect(ethereumjsTx.common.networkId().toString()).toBe('999'); + }); + describe('should return an web3-utils/tx instance with expected properties', () => { it.each(validTransactions)( 'mockBlock: %s\nexpectedTransaction: %s\nexpectedPrivateKey: %s\nexpectedAddress: %s\nexpectedRlpEncodedTransaction: %s\nexpectedTransactionHash: %s\nexpectedMessageToSign: %s\nexpectedV: %s\nexpectedR: %s\nexpectedS: %s', diff --git a/packages/web3-eth/test/unit/utils/wait_for_transaction_receipt.test.ts b/packages/web3-eth/test/unit/utils/wait_for_transaction_receipt.test.ts index 8b70dcdd3b2..4a598a00fe7 100644 --- a/packages/web3-eth/test/unit/utils/wait_for_transaction_receipt.test.ts +++ b/packages/web3-eth/test/unit/utils/wait_for_transaction_receipt.test.ts @@ -25,102 +25,152 @@ describe('waitForTransactionReceipt unit test', () => { it(`waitForTransactionReceipt should throw error after block timeout`, async () => { let blockNum = 1; - web3Context = new Web3Context( - { - request: async (payload: any) => { - let response: { jsonrpc: string; id: any; result: string } | undefined; - - switch (payload.method) { - case 'eth_blockNumber': - blockNum += 50; - response = { - jsonrpc: '2.0', - id: payload.id, - result: `0x${blockNum.toString(16)}`, - }; - break; - - case 'eth_getTransactionReceipt': - response = undefined; - break; - - default: - throw new Error(`Unknown payload ${payload}`); - } - - return new Promise(resolve => { - resolve(response as any); - }); - }, - supportsSubscriptions: () => false, + web3Context = new Web3Context({ + request: async (payload: any) => { + let response: { jsonrpc: string; id: any; result: string } | undefined; + + switch (payload.method) { + case 'eth_blockNumber': + blockNum += 50; + response = { + jsonrpc: '2.0', + id: payload.id, + result: `0x${blockNum.toString(16)}`, + }; + break; + + case 'eth_getTransactionReceipt': + response = undefined; + break; + + default: + throw new Error(`Unknown payload ${payload}`); + } + + return new Promise(resolve => { + resolve(response as any); + }); }, - ); + supportsSubscriptions: () => false, + }); await expect(async () => waitForTransactionReceipt( web3Context, '0x0430b701e657e634a9d5480eae0387a473913ef29af8e60c38a3cee24494ed54', - DEFAULT_RETURN_FORMAT - ) + DEFAULT_RETURN_FORMAT, + ), ).rejects.toThrow(TransactionBlockTimeoutError); - }); - it(`waitForTransactionReceipt should resolve immediatly if receipt is avalible`, async () => { + it(`waitForTransactionReceipt should resolve immediately if receipt is available`, async () => { let blockNum = 1; const txHash = '0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c5'; const blockHash = '0xa957d47df264a31badc3ae823e10ac1d444b098d9b73d204c40426e57f47e8c3'; - web3Context = new Web3Context( - { - request: async (payload: any) => { - const response = { - jsonrpc: '2.0', - id: payload.id, - result: {}, - }; - - switch (payload.method) { - case 'eth_blockNumber': - blockNum += 10; - response.result = `0x${blockNum.toString(16)}`; - break; - - case 'eth_getTransactionReceipt': - response.result = { - blockHash, - blockNumber: `0x1`, - cumulativeGasUsed: '0xa12515', - from: payload.from, - gasUsed: payload.gasLimit, - status: '0x1', - to: payload.to, - transactionHash: txHash, - transactionIndex: '0x66', - - }; - break; - - default: - throw new Error(`Unknown payload ${payload}`); - } - - return new Promise(resolve => { - resolve(response as any); - }); - }, - supportsSubscriptions: () => false, + web3Context = new Web3Context({ + request: async (payload: any) => { + const response = { + jsonrpc: '2.0', + id: payload.id, + result: {}, + }; + + switch (payload.method) { + case 'eth_blockNumber': + blockNum += 10; + response.result = `0x${blockNum.toString(16)}`; + break; + + case 'eth_getTransactionReceipt': + response.result = { + blockHash, + blockNumber: `0x1`, + cumulativeGasUsed: '0xa12515', + from: payload.from, + gasUsed: payload.gasLimit, + status: '0x1', + to: payload.to, + transactionHash: txHash, + transactionIndex: '0x66', + }; + break; + + default: + throw new Error(`Unknown payload ${payload}`); + } + + return new Promise(resolve => { + resolve(response as any); + }); }, + supportsSubscriptions: () => false, + }); + + const res = await waitForTransactionReceipt( + web3Context, + '0x0430b701e657e634a9d5480eae0387a473913ef29af8e60c38a3cee24494ed54', + DEFAULT_RETURN_FORMAT, ); + expect(res).toBeDefined(); + expect(res.transactionHash).toStrictEqual(txHash); + expect(res.blockHash).toStrictEqual(blockHash); + }); + + it(`waitForTransactionReceipt should resolve immediately if receipt is available - when subscription is enabled`, async () => { + let blockNum = 1; + const txHash = '0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c5'; + const blockHash = '0xa957d47df264a31badc3ae823e10ac1d444b098d9b73d204c40426e57f47e8c3'; + + web3Context = new Web3Context({ + request: async (payload: any) => { + const response = { + jsonrpc: '2.0', + id: payload.id, + result: {}, + }; + + switch (payload.method) { + case 'eth_blockNumber': + blockNum += 10; + response.result = `0x${blockNum.toString(16)}`; + break; + + case 'eth_getTransactionReceipt': + response.result = { + blockHash, + blockNumber: `0x1`, + cumulativeGasUsed: '0xa12515', + from: payload.from, + gasUsed: payload.gasLimit, + status: '0x1', + to: payload.to, + transactionHash: txHash, + transactionIndex: '0x66', + }; + break; + + default: + throw new Error(`Unknown payload ${payload}`); + } + + return new Promise(resolve => { + resolve(response as any); + }); + }, + supportsSubscriptions: () => true, + }); + web3Context.enableExperimentalFeatures.useSubscriptionWhenCheckingBlockTimeout = true; + const res = await waitForTransactionReceipt( web3Context, '0x0430b701e657e634a9d5480eae0387a473913ef29af8e60c38a3cee24494ed54', - DEFAULT_RETURN_FORMAT + DEFAULT_RETURN_FORMAT, ); expect(res).toBeDefined(); expect(res.transactionHash).toStrictEqual(txHash); expect(res.blockHash).toStrictEqual(blockHash); }); -}) \ No newline at end of file +}); \ No newline at end of file diff --git a/packages/web3-eth/test/unit/utils/watch_transaction_by_subscription.test.ts b/packages/web3-eth/test/unit/utils/watch_transaction_by_subscription.test.ts new file mode 100644 index 00000000000..d7f4169aeaf --- /dev/null +++ b/packages/web3-eth/test/unit/utils/watch_transaction_by_subscription.test.ts @@ -0,0 +1,130 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ +import { Web3Context, Web3RequestManager } from 'web3-core'; +import { format } from 'web3-utils'; +import { DEFAULT_RETURN_FORMAT, JsonRpcResponseWithResult, Web3EthExecutionAPI } from 'web3-types'; +import { ethRpcMethods } from 'web3-rpc-methods'; +import { WebSocketProvider } from 'web3-providers-ws'; +import * as rpcMethodWrappers from '../../../src/rpc_method_wrappers'; +import * as WatchTransactionBySubscription from '../../../src/utils/watch_transaction_by_subscription'; +import { + expectedTransactionReceipt, + expectedTransactionHash, + testData, +} from '../rpc_method_wrappers/fixtures/send_signed_transaction'; +import { transactionReceiptSchema } from '../../../src/schemas'; +import { registeredSubscriptions } from '../../../src'; + +jest.mock('web3-rpc-methods'); +jest.mock('web3-providers-ws'); +jest.mock('../../../src/utils/watch_transaction_by_polling'); + +const testMessage = + 'Title: %s\ninputSignedTransaction: %s\nexpectedTransactionHash: %s\nexpectedTransactionReceipt: %s\n'; + +async function waitUntilCalled(mock: jest.Mock, timeout = 1000): Promise { + return new Promise((resolve, reject) => { + let timeoutId: NodeJS.Timeout | undefined; + const intervalId = setInterval(() => { + if (mock.mock.calls.length > 0) { + clearInterval(intervalId); + if (timeoutId) { + clearTimeout(timeoutId); + } + resolve(mock); + } + }, 100); + timeoutId = setTimeout(() => { + clearInterval(intervalId); + if (timeoutId) { + clearTimeout(timeoutId); + } + reject(new Error('timeout')); + }, timeout); + }); +} + +describe('watchTransactionBySubscription', () => { + describe('should revert to polling in cases where getting by subscription did not workout', () => { + let web3Context: Web3Context; + + beforeEach(() => { + jest.spyOn(Web3RequestManager.prototype, 'send').mockImplementation(async () => { + return {} as Promise; + }); + jest.spyOn(WebSocketProvider.prototype, 'request').mockImplementation(async () => { + return {} as Promise>; + }); + + (ethRpcMethods.sendRawTransaction as jest.Mock).mockResolvedValue( + expectedTransactionHash, + ); + (ethRpcMethods.getTransactionReceipt as jest.Mock).mockResolvedValue( + expectedTransactionHash, + ); + web3Context = new Web3Context({ + // dummy provider that does supports subscription + provider: new WebSocketProvider('ws://localhost:8546'), + registeredSubscriptions, + }); + (web3Context.provider as any).supportsSubscriptions = () => true; + }); + afterEach(() => { + // to clear the interval inside the subscription function: + web3Context.transactionConfirmationBlocks = 0; + }); + let counter = 0; + it.each(testData)( + `should call getBlockNumber if blockHeaderTimeout reached\n ${testMessage}`, + async (_, inputTransaction) => { + if (counter > 0) { + return; + } + counter += 1; + const formattedTransactionReceipt = format( + transactionReceiptSchema, + expectedTransactionReceipt, + DEFAULT_RETURN_FORMAT, + ); + + web3Context.enableExperimentalFeatures.useSubscriptionWhenCheckingBlockTimeout = + true; + // this will case the function to revert to polling: + web3Context.blockHeaderTimeout = 0; + + web3Context.transactionSendTimeout = 2; + + const promiEvent = rpcMethodWrappers.sendSignedTransaction( + web3Context, + inputTransaction, + DEFAULT_RETURN_FORMAT, + ); + // await promiEvent; + WatchTransactionBySubscription.watchTransactionBySubscription({ + web3Context, + transactionReceipt: formattedTransactionReceipt, + transactionPromiEvent: promiEvent, + returnFormat: DEFAULT_RETURN_FORMAT, + }); + await waitUntilCalled(ethRpcMethods.getBlockNumber as jest.Mock, 5000); + + await promiEvent; + }, + 60000, + ); + }); +});