From df60a2dfdd50eaed259d2fd0dc63f2eb3e9f7cf3 Mon Sep 17 00:00:00 2001 From: Wyatt Barnes Date: Thu, 31 Mar 2022 00:43:32 -1000 Subject: [PATCH] `web3-eth` `sendTransaction` and `sendSignedTransaction` `PromiEvent` tests #4810 (#4875) * WIP Web3Eth.sendTransaction tests * Update use of ReceiptInfo and TransactionReceipt to ReceiptInfoFormatted * Add optional data field to BaseTransaction * Init sendTransaction test and fixture * Remove old sendTransactionValidData * Remove no longer needed imports * Remove errornous sendTransaction test * Init getGasPricing tests for sendTransaction * Init wait_for_transaction_receipt.ts * Init watch_transaction_for_confirmations.ts * Import waitForTransactionReceipt and watchTransactionForConfirmations from utils/ * Init waitForTransactionReceipt and watchTransactionForConfirmations tests * Fix promiEvent bug with sendSignedTransaction * Use HexString for expectedTransactionHash instead of string * Remove jest.useRealTimers * Init sendSignedTransaction tests * Update use of ReceiptInfo to ReceiptInfoFormatted for sendSignedTransaction * Remove unused import ReceiptInfo * Update SendSignedTransactionEvents to include only unique properties * Add transactionHash to TransactionMissingReceiptOrBlockHashError * Update packages/web3-eth/src/errors.ts * Add effectiveGasPrice to sendSignedTransaction and sendTransaction fixtures * Add expectedTransactionHash to watchTransactionForConfirmationsSpy tests * Update names of files to match existing casings * Merge with #4859 * Update import for ReceiptInfo * Add effectiveGasPrice to transactionSchema * Update SendTransactionEvents and SendSignedTransactionEvents types * Add transaction receipt formatting Co-authored-by: Alex --- packages/web3-common/src/eth_execution_api.ts | 3 +- packages/web3-eth-contract/src/contract.ts | 2 +- packages/web3-eth-ens/src/ens.ts | 24 +- packages/web3-eth/src/errors.ts | 15 +- packages/web3-eth/src/rpc_method_wrappers.ts | 264 +++++++--------- packages/web3-eth/src/schemas.ts | 3 + packages/web3-eth/src/types.ts | 37 +-- .../src/utils/wait_for_transaction_receipt.ts | 44 +++ .../watch_transaction_for_confirmations.ts | 65 ++++ packages/web3-eth/src/web3_eth.ts | 11 +- .../fixtures/send_signed_transaction.ts | 37 +++ .../fixtures/send_transaction.ts | 119 ++++++++ .../send_signed_transaction.test.ts | 226 ++++++++++++++ .../send_transaction.test.ts | 289 ++++++++++++++++++ 14 files changed, 945 insertions(+), 194 deletions(-) create mode 100644 packages/web3-eth/src/utils/wait_for_transaction_receipt.ts create mode 100644 packages/web3-eth/src/utils/watch_transaction_for_confirmations.ts create mode 100644 packages/web3-eth/test/unit/rpc_method_wrappers/fixtures/send_signed_transaction.ts create mode 100644 packages/web3-eth/test/unit/rpc_method_wrappers/fixtures/send_transaction.ts create mode 100644 packages/web3-eth/test/unit/rpc_method_wrappers/send_signed_transaction.test.ts create mode 100644 packages/web3-eth/test/unit/rpc_method_wrappers/send_transaction.test.ts diff --git a/packages/web3-common/src/eth_execution_api.ts b/packages/web3-common/src/eth_execution_api.ts index eb695957b8c..bf779ac9f2c 100644 --- a/packages/web3-common/src/eth_execution_api.ts +++ b/packages/web3-common/src/eth_execution_api.ts @@ -43,8 +43,9 @@ export interface BaseTransaction { readonly nonce: Uint; readonly gas: Uint; readonly value: Uint; - // TODO - Investigate if this should actually be data instead of input + // TODO - https://github.com/ethereum/execution-apis/pull/201 readonly input: HexStringBytes; + readonly data?: HexStringBytes; readonly chainId?: Uint; } diff --git a/packages/web3-eth-contract/src/contract.ts b/packages/web3-eth-contract/src/contract.ts index 3065067ac70..27df294b92d 100644 --- a/packages/web3-eth-contract/src/contract.ts +++ b/packages/web3-eth-contract/src/contract.ts @@ -396,7 +396,7 @@ export class Contract contractOptions: contractOptions ?? this.options, }); - return sendTransaction(this, tx); + return sendTransaction(this, tx, DEFAULT_RETURN_FORMAT); } private async _contractMethodEstimateGas< diff --git a/packages/web3-eth-ens/src/ens.ts b/packages/web3-eth-ens/src/ens.ts index b017201e692..8112ece3ab0 100644 --- a/packages/web3-eth-ens/src/ens.ts +++ b/packages/web3-eth-ens/src/ens.ts @@ -1,4 +1,4 @@ -import { getBlock } from 'web3-eth'; +import { getBlock, ReceiptInfo } from 'web3-eth'; import { Web3Context, SupportedProviders, Web3ContextObject } from 'web3-core'; import { getId, Web3NetAPI } from 'web3-net'; import { Address } from 'web3-utils'; @@ -9,7 +9,7 @@ import { ENSNetworkNotSyncedError, DEFAULT_RETURN_FORMAT, } from 'web3-common'; -import { NonPayableCallOptions, TransactionReceipt, Contract } from 'web3-eth-contract'; +import { NonPayableCallOptions, Contract } from 'web3-eth-contract'; import { RESOLVER } from './abi/resolver'; import { Registry } from './registry'; import { registryAddresses } from './config'; @@ -50,7 +50,7 @@ export class ENS extends Web3Context { name: string, address: Address, txConfig: NonPayableCallOptions, - ): Promise { + ): Promise { return this._registry.setResolver(name, address, txConfig); } @@ -64,7 +64,7 @@ export class ENS extends Web3Context { resolver: Address, ttl: number, txConfig: NonPayableCallOptions, - ): Promise { + ): Promise { return this._registry.setSubnodeRecord(name, label, owner, resolver, ttl, txConfig); } @@ -75,7 +75,7 @@ export class ENS extends Web3Context { operator: Address, approved: boolean, txConfig: NonPayableCallOptions, - ): Promise { + ): Promise { return this._registry.setApprovalForAll(operator, approved, txConfig); } @@ -101,7 +101,7 @@ export class ENS extends Web3Context { label: string, address: Address, txConfig: NonPayableCallOptions, - ): Promise { + ): Promise { return this._registry.setSubnodeOwner(name, label, address, txConfig); } @@ -119,7 +119,7 @@ export class ENS extends Web3Context { name: string, ttl: number, txConfig: NonPayableCallOptions, - ): Promise { + ): Promise { return this._registry.setTTL(name, ttl, txConfig); } @@ -137,7 +137,7 @@ export class ENS extends Web3Context { name: string, address: Address, txConfig: NonPayableCallOptions, - ): Promise { + ): Promise { return this._registry.setOwner(name, address, txConfig); } @@ -150,7 +150,7 @@ export class ENS extends Web3Context { resolver: Address, ttl: number, txConfig: NonPayableCallOptions, - ): Promise { + ): Promise { return this._registry.setRecord(name, owner, resolver, ttl, txConfig); } @@ -161,7 +161,7 @@ export class ENS extends Web3Context { name: string, address: Address, txConfig: NonPayableCallOptions, - ): Promise { + ): Promise { return this._resolver.setAddress(name, address, txConfig); } @@ -173,7 +173,7 @@ export class ENS extends Web3Context { x: string, y: string, txConfig: NonPayableCallOptions, - ): Promise { + ): Promise { return this._resolver.setPubkey(name, x, y, txConfig); } @@ -184,7 +184,7 @@ export class ENS extends Web3Context { name: string, hash: string, txConfig: NonPayableCallOptions, - ): Promise { + ): Promise { return this._resolver.setContenthash(name, hash, txConfig); } diff --git a/packages/web3-eth/src/errors.ts b/packages/web3-eth/src/errors.ts index f3ccc103c61..7abd5fa3f9b 100644 --- a/packages/web3-eth/src/errors.ts +++ b/packages/web3-eth/src/errors.ts @@ -21,9 +21,10 @@ import { ERR_TX_POLLING_TIMEOUT, ERR_TX_RECEIPT_MISSING_OR_BLOCKHASH_NULL, ERR_TX_RECEIPT_MISSING_BLOCK_NUMBER, - ReceiptInfo, } from 'web3-common'; -import { HexString, HexString32Bytes, Numbers, Web3Error } from 'web3-utils'; +import { Bytes, HexString, Numbers, Web3Error } from 'web3-utils'; + +import { ReceiptInfo } from './types'; export class InvalidTransactionWithSender extends Web3Error { public code = ERR_TX_INVALID_SENDER; @@ -249,9 +250,9 @@ export class TransactionDataAndInputError extends Web3Error { export class TransactionPollingTimeoutError extends Web3Error { public code = ERR_TX_POLLING_TIMEOUT; - public constructor(value: { numberOfSeconds: number; transactionHash: HexString32Bytes }) { + public constructor(value: { numberOfSeconds: number; transactionHash: Bytes }) { super( - `transactionHash: ${value.transactionHash}`, + `transactionHash: ${value.transactionHash.toString()}`, `Transaction was not mined within ${value.numberOfSeconds} seconds, please make sure your transaction was properly sent. Be aware that it might still be mined!`, ); } @@ -260,9 +261,11 @@ export class TransactionPollingTimeoutError extends Web3Error { export class TransactionMissingReceiptOrBlockHashError extends Web3Error { public code = ERR_TX_RECEIPT_MISSING_OR_BLOCKHASH_NULL; - public constructor(value: { receipt: ReceiptInfo; blockHash: HexString32Bytes }) { + public constructor(value: { receipt: ReceiptInfo; blockHash: Bytes; transactionHash: Bytes }) { super( - `receipt: ${JSON.stringify(value.receipt)}, blockHash: ${value.blockHash}`, + `receipt: ${JSON.stringify( + value.receipt, + )}, blockHash: ${value.blockHash.toString()}, transactionHash: ${value.transactionHash.toString()}`, `Receipt missing or blockHash null`, ); } diff --git a/packages/web3-eth/src/rpc_method_wrappers.ts b/packages/web3-eth/src/rpc_method_wrappers.ts index b3442a1d6f4..76fce427e38 100644 --- a/packages/web3-eth/src/rpc_method_wrappers.ts +++ b/packages/web3-eth/src/rpc_method_wrappers.ts @@ -8,28 +8,21 @@ import { TransactionWithSender, FMT_BYTES, FMT_NUMBER, - ReceiptInfo, DEFAULT_RETURN_FORMAT, } from 'web3-common'; import { Web3Context } from 'web3-core'; import { Address, BlockNumberOrTag, + Bytes, Filter, HexString32Bytes, HexStringBytes, - hexToNumber, Numbers, - numberToHex, Uint, Uint256, } from 'web3-utils'; import { isBlockTag, isHexString32Bytes, validator } from 'web3-validator'; -import { - TransactionMissingReceiptOrBlockHashError, - TransactionPollingTimeoutError, - TransactionReceiptMissingBlockNumberError, -} from './errors'; import * as rpcMethods from './rpc_methods'; import { accountSchema, @@ -43,14 +36,19 @@ import { Block, FeeHistory, Log, + ReceiptInfo, SendSignedTransactionEvents, SendTransactionEvents, + SendTransactionOptions, Transaction, TransactionCall, } from './types'; import { formatTransaction } from './utils/format_transaction'; // eslint-disable-next-line import/no-cycle import { getTransactionGasPricing } from './utils/get_transaction_gas_pricing'; +// eslint-disable-next-line import/no-cycle +import { waitForTransactionReceipt } from './utils/wait_for_transaction_receipt'; +import { watchTransactionForConfirmations } from './utils/watch_transaction_for_confirmations'; import { Web3EthExecutionAPI } from './web3_eth_execution_api'; export const getProtocolVersion = async (web3Context: Web3Context) => @@ -215,17 +213,24 @@ export async function getTransactionFromBlock( export async function getTransactionReceipt( web3Context: Web3Context, - transactionHash: HexString32Bytes, + transactionHash: Bytes, returnFormat: ReturnFormat, ) { const response = await rpcMethods.getTransactionReceipt( web3Context.requestManager, - transactionHash, + format({ eth: 'bytes32' }, transactionHash, { + number: FMT_NUMBER.HEX, + bytes: FMT_BYTES.HEX, + }), ); return response === null ? response - : format(receiptInfoSchema, response as unknown as ReceiptInfo, returnFormat); + : (format( + receiptInfoSchema, + response as unknown as ReceiptInfo, + returnFormat, + ) as ReceiptInfo); } export async function getTransactionCount( @@ -245,7 +250,7 @@ export async function getTransactionCount( export async function getPendingTransactions( web3Context: Web3Context, - returnFormat?: ReturnFormat, + returnFormat: ReturnFormat, ) { const response = await rpcMethods.getPendingTransactions(web3Context.requestManager); @@ -254,100 +259,13 @@ export async function getPendingTransactions( ); } -const waitForTransactionReceipt = async ( - web3Context: Web3Context, - transactionHash: HexString32Bytes, -): Promise => - new Promise(resolve => { - let transactionPollingDuration = 0; - // TODO - Promise returned in function argument where a void return was expected - // eslint-disable-next-line @typescript-eslint/no-misused-promises - const intervalId = setInterval(async () => { - transactionPollingDuration += - web3Context.transactionReceiptPollingInterval ?? - web3Context.transactionPollingInterval; - - if (transactionPollingDuration >= web3Context.transactionPollingTimeout) { - clearInterval(intervalId); - throw new TransactionPollingTimeoutError({ - numberOfSeconds: web3Context.transactionPollingTimeout / 1000, - transactionHash, - }); - } - - const response = await rpcMethods.getTransactionReceipt( - web3Context.requestManager, - transactionHash, - ); - - if (response !== null) { - clearInterval(intervalId); - resolve(response); - } - }, web3Context.transactionReceiptPollingInterval ?? web3Context.transactionPollingInterval); - }); - -function watchTransactionForConfirmations< - PromiEventEventType extends SendTransactionEvents | SendSignedTransactionEvents, ->( - web3Context: Web3Context, - transactionPromiEvent: PromiEvent, - transactionReceipt: ReceiptInfo, -) { - if ( - transactionReceipt === undefined || - transactionReceipt === null || - transactionReceipt.blockHash === undefined || - transactionReceipt.blockHash === null - ) - throw new TransactionMissingReceiptOrBlockHashError({ - receipt: transactionReceipt, - blockHash: transactionReceipt.blockHash, - }); - - if (transactionReceipt.blockNumber === undefined || transactionReceipt.blockNumber === null) - throw new TransactionReceiptMissingBlockNumberError({ receipt: transactionReceipt }); - - // TODO - Should check: (web3Context.requestManager.provider as Web3BaseProvider).supportsSubscriptions - // so a subscription for newBlockHeaders can be made instead of polling - - // Having a transactionReceipt means that the transaction has already been included - // in at least one block, so we start with 1 - let confirmationNumber = 1; - // TODO - Promise returned in function argument where a void return was expected - // eslint-disable-next-line @typescript-eslint/no-misused-promises - const intervalId = setInterval(async () => { - if (confirmationNumber >= web3Context.transactionConfirmationBlocks) - clearInterval(intervalId); - - const nextBlock = await getBlock( - web3Context, - numberToHex( - BigInt(hexToNumber(transactionReceipt.blockNumber)) + BigInt(confirmationNumber), - ), - false, - DEFAULT_RETURN_FORMAT, - ); - - if (nextBlock?.hash !== null) { - confirmationNumber += 1; - transactionPromiEvent.emit('confirmation', { - confirmationNumber, - receipt: transactionReceipt, - latestBlockHash: nextBlock.hash, - }); - } - }, web3Context.transactionReceiptPollingInterval ?? web3Context.transactionPollingInterval); -} - -export function sendTransaction( +export function sendTransaction( web3Context: Web3Context, transaction: Transaction, - options?: { - ignoreGasPricing: boolean; - }, + returnFormat: ReturnFormat, + options?: SendTransactionOptions, ): PromiEvent { - let _transaction = formatTransaction(transaction, { + let transactionFormatted = formatTransaction(transaction, { number: FMT_NUMBER.HEX, bytes: FMT_BYTES.HEX, }); @@ -360,9 +278,9 @@ export function sendTransaction( (transaction.maxPriorityFeePerGas === undefined || transaction.maxFeePerGas === undefined) ) { - _transaction = { - ..._transaction, - ...(await getTransactionGasPricing(_transaction, web3Context, { + transactionFormatted = { + ...transactionFormatted, + ...(await getTransactionGasPricing(transactionFormatted, web3Context, { number: FMT_NUMBER.HEX, bytes: FMT_BYTES.HEX, })), @@ -370,7 +288,7 @@ export function sendTransaction( } if (promiEvent.listenerCount('sending') > 0) { - promiEvent.emit('sending', _transaction); + promiEvent.emit('sending', transactionFormatted); } // TODO - If an account is available in wallet, sign transaction and call sendRawTransaction @@ -378,35 +296,52 @@ export function sendTransaction( const transactionHash = await rpcMethods.sendTransaction( web3Context.requestManager, - _transaction, + transactionFormatted, + ); + const transactionHashFormatted = format( + { eth: 'bytes32' }, + transactionHash, + returnFormat, ); if (promiEvent.listenerCount('sent') > 0) { - promiEvent.emit('sent', _transaction); + promiEvent.emit('sent', transactionFormatted); } if (promiEvent.listenerCount('transactionHash') > 0) { - promiEvent.emit('transactionHash', transactionHash); + promiEvent.emit('transactionHash', transactionHashFormatted); } - let transactionReceipt = await rpcMethods.getTransactionReceipt( - web3Context.requestManager, + let transactionReceipt = await getTransactionReceipt( + web3Context, transactionHash, + returnFormat, ); // Transaction hasn't been included in a block yet if (transactionReceipt === null) - transactionReceipt = await waitForTransactionReceipt(web3Context, transactionHash); + transactionReceipt = await waitForTransactionReceipt( + web3Context, + transactionHash, + returnFormat, + ); + + const transactionReceiptFormatted = format( + receiptInfoSchema, + transactionReceipt, + returnFormat, + ); - promiEvent.emit('receipt', transactionReceipt); - // TODO - Format receipt - resolve(transactionReceipt); + promiEvent.emit('receipt', transactionReceiptFormatted as ReceiptInfo); + resolve(transactionReceiptFormatted as ReceiptInfo); if (promiEvent.listenerCount('confirmation') > 0) { - watchTransactionForConfirmations( + watchTransactionForConfirmations( web3Context, promiEvent, - transactionReceipt, + transactionReceiptFormatted as ReceiptInfo, + transactionHash, + returnFormat, ); } }); @@ -415,45 +350,72 @@ export function sendTransaction( return promiEvent; } -export const sendSignedTransaction = async ( +export function sendSignedTransaction( web3Context: Web3Context, - transaction: HexStringBytes, -) => { + signedTransaction: HexStringBytes, + returnFormat: ReturnFormat, +): PromiEvent { // TODO - Promise returned in function argument where a void return was expected // eslint-disable-next-line @typescript-eslint/no-misused-promises - const promiEvent = new PromiEvent(async resolve => { - promiEvent.emit('sending', transaction); - - const transactionHash = await rpcMethods.sendRawTransaction( - web3Context.requestManager, - transaction, - ); - - promiEvent.emit('sent', transaction); - promiEvent.emit('transactionHash', transactionHash); - - let transactionReceipt = await rpcMethods.getTransactionReceipt( - web3Context.requestManager, - transactionHash, - ); - - // Transaction hasn't been included in a block yet - if (transactionReceipt === null) - transactionReceipt = await waitForTransactionReceipt(web3Context, transactionHash); - - promiEvent.emit('receipt', transactionReceipt); - // TODO - Format receipt - resolve(transactionReceipt); - - watchTransactionForConfirmations( - web3Context, - promiEvent, - transactionReceipt, - ); + const promiEvent = new PromiEvent(resolve => { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + setImmediate(async () => { + const signedTransactionFormatted = format( + { eth: 'bytes' }, + signedTransaction, + returnFormat, + ); + + promiEvent.emit('sending', signedTransactionFormatted); + + const transactionHash = await rpcMethods.sendRawTransaction( + web3Context.requestManager, + signedTransaction, + ); + const transactionHashFormatted = format( + { eth: 'bytes32' }, + transactionHash, + returnFormat, + ); + + promiEvent.emit('sent', signedTransactionFormatted); + promiEvent.emit('transactionHash', transactionHashFormatted); + + let transactionReceipt = await getTransactionReceipt( + web3Context, + transactionHash, + returnFormat, + ); + + // Transaction hasn't been included in a block yet + if (transactionReceipt === null) + transactionReceipt = await waitForTransactionReceipt( + web3Context, + transactionHash, + returnFormat, + ); + + const transactionReceiptFormatted = format( + receiptInfoSchema, + transactionReceipt, + returnFormat, + ); + + promiEvent.emit('receipt', transactionReceiptFormatted); + resolve(transactionReceiptFormatted); + + watchTransactionForConfirmations( + web3Context, + promiEvent, + transactionReceiptFormatted, + transactionHash, + returnFormat, + ); + }); }); return promiEvent; -}; +} // TODO address can be an address or the index of a local wallet in web3.eth.accounts.wallet // https://web3js.readthedocs.io/en/v1.5.2/web3-eth.html?highlight=sendTransaction#sign diff --git a/packages/web3-eth/src/schemas.ts b/packages/web3-eth/src/schemas.ts index 51623afbd73..7d8ae0d8cf5 100644 --- a/packages/web3-eth/src/schemas.ts +++ b/packages/web3-eth/src/schemas.ts @@ -79,6 +79,9 @@ export const transactionSchema = { gasPrice: { eth: 'uint', }, + effectiveGasPrice: { + eth: 'uint', + }, type: { eth: 'uint', }, diff --git a/packages/web3-eth/src/types.ts b/packages/web3-eth/src/types.ts index 253e8653e8e..42ff7c2377c 100644 --- a/packages/web3-eth/src/types.ts +++ b/packages/web3-eth/src/types.ts @@ -5,9 +5,8 @@ import { FMT_BYTES, FMT_NUMBER, FormatType, - ReceiptInfo as ReceiptInfoEthType, } from 'web3-common'; -import { Address, Bytes, HexString32Bytes, HexStringBytes, Numbers } from 'web3-utils'; +import { Address, Bytes, Numbers } from 'web3-utils'; export type ValidChains = 'goerli' | 'kovan' | 'mainnet' | 'rinkeby' | 'ropsten' | 'sepolia'; export type Hardfork = @@ -165,26 +164,30 @@ export interface Block { } export type SendTransactionEvents = { - sending: InternalTransaction; - sent: InternalTransaction; - transactionHash: HexString32Bytes; - receipt: ReceiptInfoEthType; + sending: Transaction; + sent: Transaction; + transactionHash: Bytes; + receipt: ReceiptInfo; confirmation: { - confirmationNumber: number; - receipt: ReceiptInfoEthType; - latestBlockHash: HexString32Bytes; + confirmationNumber: Numbers; + receipt: ReceiptInfo; + latestBlockHash: Bytes; }; }; -export type SendSignedTransactionEvents = { - sending: HexStringBytes; - sent: HexStringBytes; - transactionHash: HexString32Bytes; - receipt: ReceiptInfoEthType; +export interface SendTransactionOptions { + ignoreGasPricing?: boolean; +} + +export type SendSignedTransactionEvents = SendTransactionEvents & { + sending: Bytes; + sent: Bytes; + transactionHash: Bytes; + receipt: ReceiptInfo; confirmation: { - confirmationNumber: number; - receipt: ReceiptInfoEthType; - latestBlockHash: HexString32Bytes; + confirmationNumber: Numbers; + receipt: ReceiptInfo; + latestBlockHash: Bytes; }; }; diff --git a/packages/web3-eth/src/utils/wait_for_transaction_receipt.ts b/packages/web3-eth/src/utils/wait_for_transaction_receipt.ts new file mode 100644 index 00000000000..9baacb458f1 --- /dev/null +++ b/packages/web3-eth/src/utils/wait_for_transaction_receipt.ts @@ -0,0 +1,44 @@ +import { DataFormat, EthExecutionAPI } from 'web3-common'; +import { Web3Context } from 'web3-core'; +import { Bytes } from 'web3-utils'; + +import { ReceiptInfo } from '../types'; +// eslint-disable-next-line import/no-cycle +import { getTransactionReceipt } from '../rpc_method_wrappers'; +import { TransactionPollingTimeoutError } from '../errors'; + +export async function waitForTransactionReceipt( + web3Context: Web3Context, + transactionHash: Bytes, + returnFormat: ReturnFormat, +): Promise { + return new Promise(resolve => { + let transactionPollingDuration = 0; + // TODO - Promise returned in function argument where a void return was expected + // eslint-disable-next-line @typescript-eslint/no-misused-promises + const intervalId = setInterval(async () => { + transactionPollingDuration += + web3Context.transactionReceiptPollingInterval ?? + web3Context.transactionPollingInterval; + + if (transactionPollingDuration >= web3Context.transactionPollingTimeout) { + clearInterval(intervalId); + throw new TransactionPollingTimeoutError({ + numberOfSeconds: web3Context.transactionPollingTimeout / 1000, + transactionHash, + }); + } + + const response = await getTransactionReceipt( + web3Context, + transactionHash, + returnFormat, + ); + + if (response !== null) { + clearInterval(intervalId); + resolve(response); + } + }, web3Context.transactionReceiptPollingInterval ?? web3Context.transactionPollingInterval); + }); +} diff --git a/packages/web3-eth/src/utils/watch_transaction_for_confirmations.ts b/packages/web3-eth/src/utils/watch_transaction_for_confirmations.ts new file mode 100644 index 00000000000..3e11ae2b216 --- /dev/null +++ b/packages/web3-eth/src/utils/watch_transaction_for_confirmations.ts @@ -0,0 +1,65 @@ +import { DataFormat, EthExecutionAPI, format, PromiEvent } from 'web3-common'; +import { Web3Context } from 'web3-core'; +import { Bytes, numberToHex } from 'web3-utils'; + +import { + TransactionMissingReceiptOrBlockHashError, + TransactionReceiptMissingBlockNumberError, +} from '../errors'; +import { ReceiptInfo, SendSignedTransactionEvents, SendTransactionEvents } from '../types'; +import { getBlockByNumber } from '../rpc_methods'; + +export function watchTransactionForConfirmations< + PromiEventEventType extends SendTransactionEvents | SendSignedTransactionEvents, + ReturnFormat extends DataFormat, +>( + web3Context: Web3Context, + transactionPromiEvent: PromiEvent, + transactionReceipt: ReceiptInfo, + transactionHash: Bytes, + returnFormat: ReturnFormat, +) { + if ( + transactionReceipt === undefined || + transactionReceipt === null || + transactionReceipt.blockHash === undefined || + transactionReceipt.blockHash === null + ) + throw new TransactionMissingReceiptOrBlockHashError({ + receipt: transactionReceipt, + blockHash: format({ eth: 'bytes32' }, transactionReceipt.blockHash, returnFormat), + transactionHash: format({ eth: 'bytes32' }, transactionHash, returnFormat), + }); + + if (transactionReceipt.blockNumber === undefined || transactionReceipt.blockNumber === null) + throw new TransactionReceiptMissingBlockNumberError({ receipt: transactionReceipt }); + + // TODO - Should check: (web3Context.requestManager.provider as Web3BaseProvider).supportsSubscriptions + // so a subscription for newBlockHeaders can be made instead of polling + + // Having a transactionReceipt means that the transaction has already been included + // in at least one block, so we start with 1 + let confirmationNumber = 1; + // TODO - Promise returned in function argument where a void return was expected + // eslint-disable-next-line @typescript-eslint/no-misused-promises + const intervalId = setInterval(async () => { + if (confirmationNumber >= web3Context.transactionConfirmationBlocks) { + clearInterval(intervalId); + } + + const nextBlock = await getBlockByNumber( + web3Context.requestManager, + numberToHex(BigInt(transactionReceipt.blockNumber) + BigInt(confirmationNumber)), + false, + ); + + if (nextBlock?.hash !== null) { + confirmationNumber += 1; + transactionPromiEvent.emit('confirmation', { + confirmationNumber: format({ eth: 'uint' }, confirmationNumber, returnFormat), + receipt: transactionReceipt, + latestBlockHash: format({ eth: 'bytes32' }, nextBlock.hash, returnFormat), + }); + } + }, web3Context.transactionReceiptPollingInterval ?? web3Context.transactionPollingInterval); +} diff --git a/packages/web3-eth/src/web3_eth.ts b/packages/web3-eth/src/web3_eth.ts index 173a48d696f..e688568728b 100644 --- a/packages/web3-eth/src/web3_eth.ts +++ b/packages/web3-eth/src/web3_eth.ts @@ -14,7 +14,7 @@ import { } from 'web3-utils'; import * as rpcMethods from './rpc_methods'; import * as rpcMethodsWrappers from './rpc_method_wrappers'; -import { Transaction, TransactionCall } from './types'; +import { SendTransactionOptions, Transaction, TransactionCall } from './types'; import { Web3EthExecutionAPI } from './web3_eth_execution_api'; export class Web3Eth extends Web3Context { @@ -155,13 +155,12 @@ export class Web3Eth extends Web3Context { return rpcMethodsWrappers.getTransactionCount(this, address, blockNumber, returnFormat); } - public async sendTransaction( + public async sendTransaction( transaction: Transaction, - options?: { - ignoreGasPricing: boolean; - }, + returnFormat: ReturnFormat = DEFAULT_RETURN_FORMAT as ReturnFormat, + options?: SendTransactionOptions, ) { - return rpcMethodsWrappers.sendTransaction(this, transaction, options); + return rpcMethodsWrappers.sendTransaction(this, transaction, returnFormat, options); } public async sendSignedTransaction(transaction: HexStringBytes) { diff --git a/packages/web3-eth/test/unit/rpc_method_wrappers/fixtures/send_signed_transaction.ts b/packages/web3-eth/test/unit/rpc_method_wrappers/fixtures/send_signed_transaction.ts new file mode 100644 index 00000000000..00ee5f5a65a --- /dev/null +++ b/packages/web3-eth/test/unit/rpc_method_wrappers/fixtures/send_signed_transaction.ts @@ -0,0 +1,37 @@ +import { ReceiptInfo } from 'web3-common'; +import { HexString } from 'web3-utils'; + +const expectedTransactionHash = + '0xe21194c9509beb01be7e90c2bcefff2804cd85836ae12134f22ad4acda0fc547'; +const expectedReceiptInfo: ReceiptInfo = { + transactionHash: '0xe21194c9509beb01be7e90c2bcefff2804cd85836ae12134f22ad4acda0fc547', + transactionIndex: '0x41', + blockHash: '0x1d59ff54b1eb26b013ce3cb5fc9dab3705b415a67127a003c3e61eb445bb8df2', + blockNumber: '0x5daf3b', + from: '0xa7d9ddbe1f17865597fbd27ec712455208b6b76d', + to: '0xf02c1c8e6114b1dbe8937a39260b5b0a374432bb', + cumulativeGasUsed: '0x33bc', // 13244 + effectiveGasPrice: '0x13a21bc946', // 84324108614 + gasUsed: '0x4dc', // 1244 + contractAddress: '0xb60e8dd61c5d32be8058bb8eb970870f07233155', + logs: [], + logsBloom: '0xe21194c9509beb01be7e90c2bcefff2804cd85836ae12134f22ad4acda0fc547', + root: '0xe21194c9509beb01be7e90c2bcefff2804cd85836ae12134f22ad4acda0fc547', + status: '0x1', +}; + +/** + * Array consists of: + * - Test title + * - Input signed transaction + * - Expected transaction hash + * - Expected receipt info + */ +export const testData: [string, HexString, HexString, ReceiptInfo][] = [ + [ + 'Signed transaction', + '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675', + expectedTransactionHash, + expectedReceiptInfo, + ], +]; diff --git a/packages/web3-eth/test/unit/rpc_method_wrappers/fixtures/send_transaction.ts b/packages/web3-eth/test/unit/rpc_method_wrappers/fixtures/send_transaction.ts new file mode 100644 index 00000000000..2acfdd7ee00 --- /dev/null +++ b/packages/web3-eth/test/unit/rpc_method_wrappers/fixtures/send_transaction.ts @@ -0,0 +1,119 @@ +import { ReceiptInfo } from 'web3-common'; +import { HexString } from 'web3-utils'; +import { SendTransactionOptions, Transaction } from '../../../../src/types'; + +const inputTransaction = { + from: '0xa7d9ddbe1f17865597fbd27ec712455208b6b76d', + gas: '0xc350', + gasPrice: '0x4a817c800', + input: '0x68656c6c6f21', + nonce: '0x15', + to: '0xf02c1c8e6114b1dbe8937a39260b5b0a374432bb', + value: '0xf3dbb76162000', + type: '0x0', + maxFeePerGas: '0x1475505aab', + maxPriorityFeePerGas: '0x7f324180', + chainId: '0x1', +}; +const expectedTransactionHash = + '0xe21194c9509beb01be7e90c2bcefff2804cd85836ae12134f22ad4acda0fc547'; +const expectedReceiptInfo: ReceiptInfo = { + transactionHash: '0xe21194c9509beb01be7e90c2bcefff2804cd85836ae12134f22ad4acda0fc547', + transactionIndex: '0x41', + blockHash: '0x1d59ff54b1eb26b013ce3cb5fc9dab3705b415a67127a003c3e61eb445bb8df2', + blockNumber: '0x5daf3b', + from: '0xa7d9ddbe1f17865597fbd27ec712455208b6b76d', + to: '0xf02c1c8e6114b1dbe8937a39260b5b0a374432bb', + cumulativeGasUsed: '0x33bc', // 13244 + effectiveGasPrice: '0x13a21bc946', // 84324108614 + gasUsed: '0x4dc', // 1244 + contractAddress: '0xb60e8dd61c5d32be8058bb8eb970870f07233155', + logs: [], + logsBloom: '0xe21194c9509beb01be7e90c2bcefff2804cd85836ae12134f22ad4acda0fc547', + root: '0xe21194c9509beb01be7e90c2bcefff2804cd85836ae12134f22ad4acda0fc547', + status: '0x1', +}; + +/** + * Array consists of: + * - Test title + * - Input transaction + * - SendTransactionOptions + * - Expected transaction hash + * - Expected receipt info + */ +export const testData: [ + string, + Transaction, + SendTransactionOptions | undefined, + HexString, + ReceiptInfo, +][] = [ + [ + 'Transaction with all hex string values', + inputTransaction, + undefined, + expectedTransactionHash, + expectedReceiptInfo, + ], + [ + 'Transaction with all hex string values and SendTransactionOptions.ignoreGasPricing = true', + inputTransaction, + { ignoreGasPricing: true }, + expectedTransactionHash, + expectedReceiptInfo, + ], + [ + 'Transaction with all hex string values, inputTransaction.gasPrice !== undefined; inputTransaction.maxPriorityFeePerGas === undefined; inputTransaction.maxFeePerGas === undefined', + { + ...inputTransaction, + maxPriorityFeePerGas: undefined, + maxFeePerGas: undefined, + }, + { ignoreGasPricing: true }, + expectedTransactionHash, + expectedReceiptInfo, + ], + [ + 'Transaction with all hex string values, inputTransaction.gasPrice === undefined; inputTransaction.maxPriorityFeePerGas !== undefined; inputTransaction.maxFeePerGas !== undefined', + { + ...inputTransaction, + gasPrice: undefined, + }, + { ignoreGasPricing: true }, + expectedTransactionHash, + expectedReceiptInfo, + ], + [ + 'Transaction with all hex string values, inputTransaction.gasPrice === undefined; inputTransaction.maxPriorityFeePerGas === undefined; inputTransaction.maxFeePerGas !== undefined', + { + ...inputTransaction, + maxPriorityFeePerGas: undefined, + }, + { ignoreGasPricing: true }, + expectedTransactionHash, + expectedReceiptInfo, + ], + [ + 'Transaction with all hex string values, inputTransaction.gasPrice === undefined; inputTransaction.maxPriorityFeePerGas !== undefined; inputTransaction.maxFeePerGas === undefined', + { + ...inputTransaction, + maxFeePerGas: undefined, + }, + { ignoreGasPricing: true }, + expectedTransactionHash, + expectedReceiptInfo, + ], + [ + 'Transaction with all hex string values, inputTransaction.gasPrice === undefined; inputTransaction.maxPriorityFeePerGas === undefined; inputTransaction.maxFeePerGas === undefined', + { + ...inputTransaction, + gasPrice: undefined, + maxPriorityFeePerGas: undefined, + maxFeePerGas: undefined, + }, + { ignoreGasPricing: true }, + expectedTransactionHash, + expectedReceiptInfo, + ], +]; diff --git a/packages/web3-eth/test/unit/rpc_method_wrappers/send_signed_transaction.test.ts b/packages/web3-eth/test/unit/rpc_method_wrappers/send_signed_transaction.test.ts new file mode 100644 index 00000000000..582148ff11e --- /dev/null +++ b/packages/web3-eth/test/unit/rpc_method_wrappers/send_signed_transaction.test.ts @@ -0,0 +1,226 @@ +import { Web3Context } from 'web3-core'; + +import { DEFAULT_RETURN_FORMAT, format } from 'web3-common'; +import * as rpcMethods from '../../../src/rpc_methods'; +import { Web3EthExecutionAPI } from '../../../src/web3_eth_execution_api'; +import { sendSignedTransaction } from '../../../src/rpc_method_wrappers'; +import * as WaitForTransactionReceipt from '../../../src/utils/wait_for_transaction_receipt'; +import * as WatchTransactionForConfirmations from '../../../src/utils/watch_transaction_for_confirmations'; +import { testData } from './fixtures/send_signed_transaction'; +import { receiptInfoSchema } from '../../../src/schemas'; + +jest.mock('../../../src/rpc_methods'); +jest.mock('../../../src/utils/wait_for_transaction_receipt'); +jest.mock('../../../src/utils/watch_transaction_for_confirmations'); + +describe('sendTransaction', () => { + const testMessage = + 'Title: %s\ninputSignedTransaction: %s\nexpectedTransactionHash: %s\nexpectedReceiptInfo: %s\n'; + + let web3Context: Web3Context; + + beforeAll(() => { + web3Context = new Web3Context('http://127.0.0.1:8545'); + }); + + afterEach(() => jest.resetAllMocks()); + + it.each(testData)( + `sending event should emit with inputSignedTransaction\n ${testMessage}`, + async (_, inputSignedTransaction, __, ___) => { + return new Promise(done => { + const promiEvent = sendSignedTransaction( + web3Context, + inputSignedTransaction, + DEFAULT_RETURN_FORMAT, + ); + promiEvent.on('sending', signedTransaction => { + expect(signedTransaction).toStrictEqual(inputSignedTransaction); + done(null); + }); + }); + }, + ); + + it.each(testData)( + `should call rpcMethods.sendRawTransaction with expected parameters\n ${testMessage}`, + async (_, inputSignedTransaction, __, ___) => { + await sendSignedTransaction(web3Context, inputSignedTransaction, DEFAULT_RETURN_FORMAT); + expect(rpcMethods.sendRawTransaction).toHaveBeenCalledWith( + web3Context.requestManager, + inputSignedTransaction, + ); + }, + ); + + it.each(testData)( + `sent event should emit with inputSignedTransaction\n ${testMessage}`, + async (_, inputSignedTransaction, __, ___) => { + return new Promise(done => { + const promiEvent = sendSignedTransaction( + web3Context, + inputSignedTransaction, + DEFAULT_RETURN_FORMAT, + ); + promiEvent.on('sent', signedTransaction => { + expect(signedTransaction).toStrictEqual(inputSignedTransaction); + done(null); + }); + }); + }, + ); + + it.each(testData)( + `transactionHash event should emit with inputSignedTransaction\n ${testMessage}`, + async (_, inputSignedTransaction, expectedTransactionHash, ___) => { + return new Promise(done => { + (rpcMethods.sendRawTransaction as jest.Mock).mockResolvedValueOnce( + expectedTransactionHash, + ); + + const promiEvent = sendSignedTransaction( + web3Context, + inputSignedTransaction, + DEFAULT_RETURN_FORMAT, + ); + promiEvent.on('transactionHash', transactionHash => { + expect(transactionHash).toStrictEqual(expectedTransactionHash); + done(null); + }); + }); + }, + ); + + it.each(testData)( + `should call rpcMethods.getTransactionReceipt with expected parameters\n ${testMessage}`, + async (_, inputSignedTransaction, expectedTransactionHash, ___) => { + (rpcMethods.sendRawTransaction as jest.Mock).mockResolvedValueOnce( + expectedTransactionHash, + ); + + await sendSignedTransaction(web3Context, inputSignedTransaction, DEFAULT_RETURN_FORMAT); + expect(rpcMethods.getTransactionReceipt).toHaveBeenCalledWith( + web3Context.requestManager, + expectedTransactionHash, + ); + }, + ); + + it.each(testData)( + `waitForTransactionReceipt is called when expected\n ${testMessage}`, + async (_, inputSignedTransaction, expectedTransactionHash, ___) => { + const waitForTransactionReceiptSpy = jest.spyOn( + WaitForTransactionReceipt, + 'waitForTransactionReceipt', + ); + + (rpcMethods.sendRawTransaction as jest.Mock).mockResolvedValueOnce( + expectedTransactionHash, + ); + (rpcMethods.getTransactionReceipt as jest.Mock).mockResolvedValueOnce(null); + + await sendSignedTransaction(web3Context, inputSignedTransaction, DEFAULT_RETURN_FORMAT); + + expect(rpcMethods.getTransactionReceipt).toHaveBeenCalledWith( + web3Context.requestManager, + expectedTransactionHash, + ); + expect(waitForTransactionReceiptSpy).toHaveBeenCalledWith( + web3Context, + expectedTransactionHash, + DEFAULT_RETURN_FORMAT, + ); + }, + ); + + it.each(testData)( + `receipt event should emit with inputSignedTransaction\n ${testMessage}`, + async (_, inputSignedTransaction, __, expectedReceiptInfo) => { + return new Promise(done => { + const formattedReceiptInfo = format( + receiptInfoSchema, + expectedReceiptInfo, + DEFAULT_RETURN_FORMAT, + ); + + (rpcMethods.getTransactionReceipt as jest.Mock).mockResolvedValueOnce( + expectedReceiptInfo, + ); + + const promiEvent = sendSignedTransaction( + web3Context, + inputSignedTransaction, + DEFAULT_RETURN_FORMAT, + ); + promiEvent.on('receipt', receiptInfo => { + expect(receiptInfo).toStrictEqual(formattedReceiptInfo); + done(null); + }); + }); + }, + ); + + it.each(testData)( + `should resolve promiEvent with expectedReceiptInfo\n ${testMessage}`, + async (_, inputSignedTransaction, __, expectedReceiptInfo) => { + const formattedReceiptInfo = format( + receiptInfoSchema, + expectedReceiptInfo, + DEFAULT_RETURN_FORMAT, + ); + + (rpcMethods.getTransactionReceipt as jest.Mock).mockResolvedValueOnce( + expectedReceiptInfo, + ); + expect( + await sendSignedTransaction( + web3Context, + inputSignedTransaction, + DEFAULT_RETURN_FORMAT, + ), + ).toStrictEqual(formattedReceiptInfo); + }, + ); + + it.each(testData)( + `watchTransactionForConfirmations is called when expected\n ${testMessage}`, + async (_, inputTransaction, expectedTransactionHash, expectedReceiptInfo) => { + const watchTransactionForConfirmationsSpy = jest.spyOn( + WatchTransactionForConfirmations, + 'watchTransactionForConfirmations', + ); + const formattedReceiptInfo = format( + receiptInfoSchema, + expectedReceiptInfo, + DEFAULT_RETURN_FORMAT, + ); + + (rpcMethods.sendRawTransaction as jest.Mock).mockResolvedValueOnce( + expectedTransactionHash, + ); + (rpcMethods.getTransactionReceipt as jest.Mock).mockResolvedValueOnce( + expectedReceiptInfo, + ); + + const promiEvent = sendSignedTransaction( + web3Context, + inputTransaction, + DEFAULT_RETURN_FORMAT, + ); + promiEvent.on('confirmation', () => undefined); + await promiEvent; + + expect(rpcMethods.getTransactionReceipt).toHaveBeenCalledWith( + web3Context.requestManager, + expectedTransactionHash, + ); + expect(watchTransactionForConfirmationsSpy).toHaveBeenCalledWith( + web3Context, + promiEvent, + formattedReceiptInfo, + expectedTransactionHash, + DEFAULT_RETURN_FORMAT, + ); + }, + ); +}); diff --git a/packages/web3-eth/test/unit/rpc_method_wrappers/send_transaction.test.ts b/packages/web3-eth/test/unit/rpc_method_wrappers/send_transaction.test.ts new file mode 100644 index 00000000000..b0987db5fa8 --- /dev/null +++ b/packages/web3-eth/test/unit/rpc_method_wrappers/send_transaction.test.ts @@ -0,0 +1,289 @@ +import { Web3Context } from 'web3-core'; + +import { DEFAULT_RETURN_FORMAT, format } from 'web3-common'; +import * as rpcMethods from '../../../src/rpc_methods'; +import { Web3EthExecutionAPI } from '../../../src/web3_eth_execution_api'; +import { sendTransaction } from '../../../src/rpc_method_wrappers'; +import { formatTransaction } from '../../../src'; +import * as GetTransactionGasPricing from '../../../src/utils/get_transaction_gas_pricing'; +import * as WaitForTransactionReceipt from '../../../src/utils/wait_for_transaction_receipt'; +import * as WatchTransactionForConfirmations from '../../../src/utils/watch_transaction_for_confirmations'; +import { testData } from './fixtures/send_transaction'; +import { receiptInfoSchema } from '../../../src/schemas'; + +jest.mock('../../../src/rpc_methods'); +jest.mock('../../../src/utils/wait_for_transaction_receipt'); +jest.mock('../../../src/utils/watch_transaction_for_confirmations'); + +describe('sendTransaction', () => { + const testMessage = + 'Title: %s\ninputTransaction: %s\nsendTransactionOptions: %s\nexpectedTransactionHash: %s\nexpectedReceiptInfo: %s\n'; + + let web3Context: Web3Context; + + beforeAll(() => { + web3Context = new Web3Context('http://127.0.0.1:8545'); + }); + + afterEach(() => jest.resetAllMocks()); + + it.each(testData)( + `getTransactionGasPricing is called only when expected\n ${testMessage}`, + async (_, inputTransaction, sendTransactionOptions, __, ___) => { + const getTransactionGasPricingSpy = jest.spyOn( + GetTransactionGasPricing, + 'getTransactionGasPricing', + ); + + await sendTransaction( + web3Context, + inputTransaction, + DEFAULT_RETURN_FORMAT, + sendTransactionOptions, + ); + + if ( + sendTransactionOptions?.ignoreGasPricing || + inputTransaction.gasPrice !== undefined || + (inputTransaction.maxPriorityFeePerGas !== undefined && + inputTransaction.maxFeePerGas !== undefined) + ) + // eslint-disable-next-line jest/no-conditional-expect + expect(getTransactionGasPricingSpy).not.toHaveBeenCalled(); + // eslint-disable-next-line jest/no-conditional-expect + else expect(getTransactionGasPricingSpy).toHaveBeenCalled(); + }, + ); + + it.each(testData)( + `sending event should emit with formattedTransaction\n ${testMessage}`, + async (_, inputTransaction, sendTransactionOptions, __, ___) => { + return new Promise(done => { + const formattedTransaction = formatTransaction( + inputTransaction, + DEFAULT_RETURN_FORMAT, + ); + const promiEvent = sendTransaction( + web3Context, + inputTransaction, + DEFAULT_RETURN_FORMAT, + sendTransactionOptions, + ); + promiEvent.on('sending', transaction => { + expect(transaction).toStrictEqual(formattedTransaction); + done(null); + }); + }); + }, + ); + + it.each(testData)( + `should call rpcMethods.sendTransaction with expected parameters\n ${testMessage}`, + async (_, inputTransaction, sendTransactionOptions, __, ___) => { + const formattedTransaction = formatTransaction(inputTransaction, DEFAULT_RETURN_FORMAT); + await sendTransaction( + web3Context, + inputTransaction, + DEFAULT_RETURN_FORMAT, + sendTransactionOptions, + ); + expect(rpcMethods.sendTransaction).toHaveBeenCalledWith( + web3Context.requestManager, + formattedTransaction, + ); + }, + ); + + it.each(testData)( + `sent event should emit with formattedTransaction\n ${testMessage}`, + async (_, inputTransaction, sendTransactionOptions, __, ___) => { + return new Promise(done => { + const formattedTransaction = formatTransaction( + inputTransaction, + DEFAULT_RETURN_FORMAT, + ); + const promiEvent = sendTransaction( + web3Context, + inputTransaction, + DEFAULT_RETURN_FORMAT, + sendTransactionOptions, + ); + promiEvent.on('sent', transaction => { + expect(transaction).toStrictEqual(formattedTransaction); + done(null); + }); + }); + }, + ); + + it.each(testData)( + `transactionHash event should emit with expectedTransactionHash\n ${testMessage}`, + async (_, inputTransaction, sendTransactionOptions, expectedTransactionHash, __) => { + return new Promise(done => { + (rpcMethods.sendTransaction as jest.Mock).mockResolvedValueOnce( + expectedTransactionHash, + ); + + const promiEvent = sendTransaction( + web3Context, + inputTransaction, + DEFAULT_RETURN_FORMAT, + sendTransactionOptions, + ); + promiEvent.on('transactionHash', transactionHash => { + expect(transactionHash).toStrictEqual(expectedTransactionHash); + done(null); + }); + }); + }, + ); + + it.each(testData)( + `should call rpcMethods.getTransactionReceipt with expected parameters\n ${testMessage}`, + async (_, inputTransaction, sendTransactionOptions, expectedTransactionHash, __) => { + (rpcMethods.sendTransaction as jest.Mock).mockResolvedValueOnce( + expectedTransactionHash, + ); + + await sendTransaction( + web3Context, + inputTransaction, + DEFAULT_RETURN_FORMAT, + sendTransactionOptions, + ); + expect(rpcMethods.getTransactionReceipt).toHaveBeenCalledWith( + web3Context.requestManager, + expectedTransactionHash, + ); + }, + ); + + it.each(testData)( + `waitForTransactionReceipt is called when expected\n ${testMessage}`, + async (_, inputTransaction, sendTransactionOptions, expectedTransactionHash, __) => { + const waitForTransactionReceiptSpy = jest.spyOn( + WaitForTransactionReceipt, + 'waitForTransactionReceipt', + ); + + (rpcMethods.sendTransaction as jest.Mock).mockResolvedValueOnce( + expectedTransactionHash, + ); + (rpcMethods.getTransactionReceipt as jest.Mock).mockResolvedValueOnce(null); + + await sendTransaction( + web3Context, + inputTransaction, + DEFAULT_RETURN_FORMAT, + sendTransactionOptions, + ); + + expect(rpcMethods.getTransactionReceipt).toHaveBeenCalledWith( + web3Context.requestManager, + expectedTransactionHash, + ); + expect(waitForTransactionReceiptSpy).toHaveBeenCalledWith( + web3Context, + expectedTransactionHash, + DEFAULT_RETURN_FORMAT, + ); + }, + ); + + it.each(testData)( + `receipt event should emit with expectedReceiptInfo\n ${testMessage}`, + async (_, inputTransaction, sendTransactionOptions, __, expectedReceiptInfo) => { + return new Promise(done => { + const formattedReceiptInfo = format( + receiptInfoSchema, + expectedReceiptInfo, + DEFAULT_RETURN_FORMAT, + ); + (rpcMethods.getTransactionReceipt as jest.Mock).mockResolvedValueOnce( + formattedReceiptInfo, + ); + const promiEvent = sendTransaction( + web3Context, + inputTransaction, + DEFAULT_RETURN_FORMAT, + sendTransactionOptions, + ); + promiEvent.on('receipt', receiptInfo => { + expect(receiptInfo).toStrictEqual(formattedReceiptInfo); + done(null); + }); + }); + }, + ); + + it.each(testData)( + `should resolve promiEvent with expectedReceiptInfo\n ${testMessage}`, + async (_, inputTransaction, sendTransactionOptions, __, expectedReceiptInfo) => { + const formattedReceiptInfo = format( + receiptInfoSchema, + expectedReceiptInfo, + DEFAULT_RETURN_FORMAT, + ); + (rpcMethods.getTransactionReceipt as jest.Mock).mockResolvedValueOnce( + formattedReceiptInfo, + ); + expect( + await sendTransaction( + web3Context, + inputTransaction, + DEFAULT_RETURN_FORMAT, + sendTransactionOptions, + ), + ).toStrictEqual(formattedReceiptInfo); + }, + ); + + it.each(testData)( + `watchTransactionForConfirmations is called when expected\n ${testMessage}`, + async ( + _, + inputTransaction, + sendTransactionOptions, + expectedTransactionHash, + expectedReceiptInfo, + ) => { + const watchTransactionForConfirmationsSpy = jest.spyOn( + WatchTransactionForConfirmations, + 'watchTransactionForConfirmations', + ); + const formattedReceiptInfo = format( + receiptInfoSchema, + expectedReceiptInfo, + DEFAULT_RETURN_FORMAT, + ); + + (rpcMethods.sendTransaction as jest.Mock).mockResolvedValueOnce( + expectedTransactionHash, + ); + (rpcMethods.getTransactionReceipt as jest.Mock).mockResolvedValueOnce( + expectedReceiptInfo, + ); + + const promiEvent = sendTransaction( + web3Context, + inputTransaction, + DEFAULT_RETURN_FORMAT, + sendTransactionOptions, + ); + promiEvent.on('confirmation', () => undefined); + await promiEvent; + + expect(rpcMethods.getTransactionReceipt).toHaveBeenCalledWith( + web3Context.requestManager, + expectedTransactionHash, + ); + expect(watchTransactionForConfirmationsSpy).toHaveBeenCalledWith( + web3Context, + promiEvent, + formattedReceiptInfo, + expectedTransactionHash, + DEFAULT_RETURN_FORMAT, + ); + }, + ); +});