From 46746fcdc2415951a880fff81b39a1105f336ede Mon Sep 17 00:00:00 2001 From: Wyatt Barnes Date: Thu, 3 Mar 2022 09:03:17 -1000 Subject: [PATCH] Wyatt/4.x/4789 transaction rpc methods (#4792) * Add @ethereumjs/tx dependency * Update Web3BaseProvider to SupportedProviders for Web3Eth constructor * WIP eth-tx * Add support for undefined values for convertToValidType * Remove unused Web3BaseProvider * WIP eth-tx utils * Export privateKeyToAddress * Add web3-eth-accounts dependency * WIP web3-eth-tx util methods * Replace inline errors with error constructors * Change types for transaction r and s properties. Correct hardforks in detectTransactionType * Init formatTransaction tests and fixture * Init detectTransactionType and fixture * Add more descriptive error messages * Logic fixes for validateTransactionForSigning * Init validateTransactionForSigning tests and fixtures * Add esModuleInterop: true to tsconfig * Small bug fixes and added TODOs * Add parent describe to detect_transaction_type test * Add parent describe to format_transaction test * Add web3-providers-http as dev dependency for tests * Init populate_transaction tests * Move types from eth_tx.ts to types.ts * Remove TODOs * Add missing , * Remove TODO * Remove TODO * Fix transaction type import issues * Update convertToValidType test data for undefined * Update override method tests * Update packages/web3-eth/src/eth_tx.ts Co-authored-by: jdevcs <86780488+jdevcs@users.noreply.github.com> * Move getBlock to after type check for populateTransaction * Replace N/A with name of error for error.msg * Assign formattedTransaction type Transaction * convertToValidType now throws error for value === undefined * NumberType extends Numbers * Transaction type related changes * Refactor DesiredType logic * Convert to deep copy for formatTransaction method * skip override method test - needs to be refactored. General formatting * Skip override method test - needs to be refactored. Set input.type > 0x7f * Refactor formatTransaction * Add error codes to web3-eth errors * Refactor validateGate if statements into readable consts * Update comment * Add link to error message from 1.x * Fix bug with is gas consts in validateGas * Init InvalidConvertibleValueError * Replace error with InvalidConvertibleValueError * Update tests for formatting undefined * Update expected errors for validateGas tests * No longer default tx.type if undefined * Refactor detectTransactionType * Fix type error for return in detectTransactionType * Init rpc_method_wrappers.ts * Remove Web3Eth import * Refactor use of web3Context.defaults * Restore Formatted transaction types * Init web3_rpc_method_wrappers tests * Refactor web3_eth_methods_with_parameters test * Replace if X === undefined checks with ? * Un-export consts that aren't used * Add defaultTransactionType and defaultMaxPriorityFeePerGas * Update defaults for chain and hardfork to mainnet and london * Update to use web3Context.default chain and hardfork. Init tests for defaults * Update test to account for added defaults * Refactor validateGas to use helper methods * remove TODO * Init error TransactionGasMismatchError * Fix tests and refactor transaction validator helper methods * Move validation methods to validation.ts * Add input to Transaction type * Add @ethereumjs/common dependency * yarn format * Remove null for defaultTransactionType * Add default for defaultTransactionType * Update default for defaultTransactionType * Bug fixes, refactors, and init prepareTransactionForSigning and tests * Remove unused test code * revert transaction data and value to default to 0x * Fix failing populate_transaction tests * Add defaultNetworkId to web3_config * Add TODO for failing prepare_transaction_for_signing test * Remove TODO * Init TransactionDataAndInputError * Add else if to populateTransaction - data * Refactor populateTransaction - chainId * Comment out unused ifs * Remove populateTransaction - gas * Remove populateTransaction - hexTxType * Replace use of ValidReturnTypes[ValidTypes.HexString] with HexString * Remove toHex import * Remove | null for Web3ConfigOptions defaultChain and defaultHardfork * Refactor getEthereumjsTransactionOptions * Remove no longer needed populateTransaction - gas test * Update packages/web3-eth/src/validation.ts * Remove unnecessary rpc method wrappers * Web3Eth now extends Web3Context instead of instantiating it * Init getPendingTransactions * Init requestAccounts * Add EIP-1102 as a comment for requestAccounts * Init getChainId * Init getProof * Init Web3EthExecutionAPI * Fix imports for AccountObject in fixtures * Add formatting to getPendingTransactions. Move formatTransaction to seperate file * Add TODO to investigate transaction.data * Add formatting to getChainId response * Init getNodeInfo * Revert esModuleInterop change * Combine networkId and chainId if statements * yarn format * Add Partial to type of transaction for eth_sendTransaction * Init transactionReceiptPollingInterval and transactionConfirmationPollingInterval * Add TODO and Partial to transaction type for sendTransaction * WIP sendTransaction and PromiEvent integration * Add eslint-disable-next-line * Add eslint-disable-next-line * Move TransactionEvents * eslint fixes * Update sendSignedTransaction to use PromiEvent * Init signTransaction * Refactor TransactionCall * Comment out validation for call * Init TransactionCall type for web3-eth types * Remove as BaseTransaction from isTransactionCall * Implement call for rpc_method_wrappers * Uncomment sendTransaction, signTransaction, and call * Replace inline errors with error constructors * Validate transaction.to for Call method in web3-eth * Add transactionPollingInterval to Web3Context * Fix bug for updating transactionPollingInterval and fix webConfig tests * Temporarily disable sendSignedTransaction test Co-authored-by: jdevcs <86780488+jdevcs@users.noreply.github.com> Co-authored-by: Nazar Hussain --- packages/web3-common/src/constants.ts | 3 + packages/web3-common/src/eth_execution_api.ts | 17 +- packages/web3-core/src/types.ts | 3 + packages/web3-core/src/web3_config.ts | 48 ++++ packages/web3-core/src/web3_context.ts | 1 + packages/web3-core/src/web3_subscriptions.ts | 1 + .../web3-core/test/unit/web3_config.test.ts | 30 +++ packages/web3-eth/src/errors.ts | 36 ++- packages/web3-eth/src/index.ts | 28 +-- packages/web3-eth/src/rpc_method_wrappers.ts | 230 ++++++++++++++++-- packages/web3-eth/src/rpc_methods.ts | 20 +- packages/web3-eth/src/types.ts | 38 ++- packages/web3-eth/src/validation.ts | 2 +- ...pc_method_wrappers_with_parameters.test.ts | 38 +-- 14 files changed, 424 insertions(+), 71 deletions(-) diff --git a/packages/web3-common/src/constants.ts b/packages/web3-common/src/constants.ts index bfa1b363f81..73ec1f7805e 100644 --- a/packages/web3-common/src/constants.ts +++ b/packages/web3-common/src/constants.ts @@ -49,6 +49,9 @@ export const ERR_TX_UNABLE_TO_POPULATE_NONCE = 422; export const ERR_TX_UNSUPPORTED_EIP_1559 = 423; export const ERR_TX_UNSUPPORTED_TYPE = 424; export const ERR_TX_DATA_AND_INPUT = 425; +export const ERR_TX_POLLING_TIMEOUT = 426; +export const ERR_TX_RECEIPT_MISSING_OR_BLOCKHASH_NULL = 427; +export const ERR_TX_RECEIPT_MISSING_BLOCK_NUMBER = 428; // Connection error codes export const ERR_CONN = 500; diff --git a/packages/web3-common/src/eth_execution_api.ts b/packages/web3-common/src/eth_execution_api.ts index ad1fd82f533..97d37b40d8c 100644 --- a/packages/web3-common/src/eth_execution_api.ts +++ b/packages/web3-common/src/eth_execution_api.ts @@ -24,14 +24,17 @@ export type AccessList = AccessListEntry[]; export type TransactionHash = HexString; export type Uncles = HexString32Bytes[]; -// TODO Should probably support EIP-2930 and EIP-1559 export interface TransactionCall { readonly from?: Address; readonly to: Address; readonly gas?: Uint; readonly gasPrice?: Uint; readonly value?: Uint; - readonly data?: HexString; + readonly data?: HexStringBytes; + readonly type?: HexStringSingleByte; + readonly maxFeePerGas?: Uint; + readonly maxPriorityFeePerGas?: Uint; + readonly accessList?: AccessList; } export interface BaseTransaction { @@ -42,7 +45,7 @@ export interface BaseTransaction { readonly value: Uint; // TODO - Investigate if this should actually be data instead of input readonly input: HexStringBytes; - chainId?: Uint; + readonly chainId?: Uint; } export interface Transaction1559Unsigned extends BaseTransaction { @@ -257,7 +260,9 @@ export type EthExecutionAPI = { // https://github.com/ethereum/execution-apis/blob/main/src/eth/sign.json eth_sign: (address: Address, message: HexStringBytes) => HexString256Bytes; - eth_signTransaction: (transaction: TransactionWithSender) => HexStringBytes; + eth_signTransaction: ( + transaction: TransactionWithSender | Partial, + ) => HexStringBytes; // https://github.com/ethereum/execution-apis/blob/main/src/eth/state.json eth_getBalance: (address: Address, blockNumber: BlockNumberOrTag) => Uint; @@ -270,7 +275,9 @@ export type EthExecutionAPI = { eth_getCode: (address: Address, blockNumber: BlockNumberOrTag) => HexStringBytes; // https://github.com/ethereum/execution-apis/blob/main/src/eth/submit.json - eth_sendTransaction: (transaction: TransactionWithSender) => HexString32Bytes; + eth_sendTransaction: ( + transaction: TransactionWithSender | Partial, + ) => HexString32Bytes; eth_sendRawTransaction: (transaction: HexStringBytes) => HexString32Bytes; // https://geth.ethereum.org/docs/rpc/pubsub diff --git a/packages/web3-core/src/types.ts b/packages/web3-core/src/types.ts index 4e2862d4e64..73f9ff694c9 100644 --- a/packages/web3-core/src/types.ts +++ b/packages/web3-core/src/types.ts @@ -45,7 +45,10 @@ export interface Web3ConfigOptions { defaultBlock: HexString; transactionBlockTimeout: number; transactionConfirmationBlocks: number; + transactionPollingInterval: number; transactionPollingTimeout: number; + transactionReceiptPollingInterval: number | null; + transactionConfirmationPollingInterval: number | null; blockHeaderTimeout: number; maxListenersWarningThreshold: number; defaultNetworkId: Numbers | null; diff --git a/packages/web3-core/src/web3_config.ts b/packages/web3-core/src/web3_config.ts index 4cd39f69a24..3682859bd74 100644 --- a/packages/web3-core/src/web3_config.ts +++ b/packages/web3-core/src/web3_config.ts @@ -21,7 +21,10 @@ export abstract class Web3Config defaultBlock: 'latest', transactionBlockTimeout: 50, transactionConfirmationBlocks: 24, + transactionPollingInterval: 1000, transactionPollingTimeout: 750, + transactionReceiptPollingInterval: null, + transactionConfirmationPollingInterval: null, blockHeaderTimeout: 10, maxListenersWarningThreshold: 100, defaultNetworkId: null, @@ -104,6 +107,23 @@ export abstract class Web3Config this._config.transactionConfirmationBlocks = val; } + public get transactionPollingInterval() { + return this._config.transactionPollingInterval; + } + + public set transactionPollingInterval(val) { + this.emit(Web3ConfigEvent.CONFIG_CHANGE, { + name: 'transactionPollingInterval', + oldValue: this._config.transactionPollingInterval, + newValue: val, + }); + + this._config.transactionPollingInterval = val; + + this.transactionReceiptPollingInterval = val; + this.transactionConfirmationPollingInterval = val; + } + public get transactionPollingTimeout() { return this._config.transactionPollingTimeout; } @@ -118,6 +138,34 @@ export abstract class Web3Config this._config.transactionPollingTimeout = val; } + public get transactionReceiptPollingInterval() { + return this._config.transactionReceiptPollingInterval; + } + + public set transactionReceiptPollingInterval(val) { + this.emit(Web3ConfigEvent.CONFIG_CHANGE, { + name: 'transactionReceiptPollingInterval', + oldValue: this._config.transactionReceiptPollingInterval, + newValue: val, + }); + + this._config.transactionReceiptPollingInterval = val; + } + + public get transactionConfirmationPollingInterval() { + return this._config.transactionConfirmationPollingInterval; + } + + public set transactionConfirmationPollingInterval(val) { + this.emit(Web3ConfigEvent.CONFIG_CHANGE, { + name: 'transactionConfirmationPollingInterval', + oldValue: this._config.transactionConfirmationPollingInterval, + newValue: val, + }); + + this._config.transactionConfirmationPollingInterval = val; + } + public get blockHeaderTimeout() { return this._config.blockHeaderTimeout; } diff --git a/packages/web3-core/src/web3_context.ts b/packages/web3-core/src/web3_context.ts index 4f628ddad46..a0825a21020 100644 --- a/packages/web3-core/src/web3_context.ts +++ b/packages/web3-core/src/web3_context.ts @@ -9,6 +9,7 @@ export class Web3Context< API extends Web3APISpec, RegisteredSubs extends { [key: string]: Web3SubscriptionConstructor; + // eslint-disable-next-line @typescript-eslint/ban-types } = {}, > extends Web3Config { public static readonly providers = Web3RequestManager.providers; diff --git a/packages/web3-core/src/web3_subscriptions.ts b/packages/web3-core/src/web3_subscriptions.ts index f32571fd9be..8e6f84dd788 100644 --- a/packages/web3-core/src/web3_subscriptions.ts +++ b/packages/web3-core/src/web3_subscriptions.ts @@ -104,6 +104,7 @@ export abstract class Web3Subscription< export type Web3SubscriptionConstructor< API extends Web3APISpec, + // eslint-disable-next-line @typescript-eslint/no-explicit-any SubscriptionType extends Web3Subscription = Web3Subscription, > = new ( // We accept any type of arguments here and don't deal with this type internally diff --git a/packages/web3-core/test/unit/web3_config.test.ts b/packages/web3-core/test/unit/web3_config.test.ts index 8de9c154c4c..3256c973f88 100644 --- a/packages/web3-core/test/unit/web3_config.test.ts +++ b/packages/web3-core/test/unit/web3_config.test.ts @@ -16,7 +16,10 @@ const defaultConfig = { maxListenersWarningThreshold: 100, transactionBlockTimeout: 50, transactionConfirmationBlocks: 24, + transactionPollingInterval: 1000, transactionPollingTimeout: 750, + transactionReceiptPollingInterval: null, + transactionConfirmationPollingInterval: null, defaultTransactionType: '0x0', defaultMaxPriorityFeePerGas: toHex(2500000000), }; @@ -64,6 +67,8 @@ describe('Web3Config', () => { obj[key as never] = 'newValue' as never; + if (key === 'transactionPollingInterval') return; + expect(configChange).toHaveBeenCalledTimes(1); expect(configChange).toHaveBeenCalledWith({ name: key, @@ -72,4 +77,29 @@ describe('Web3Config', () => { }); }, ); + + it('Updating transactionPollingInterval should update transactionReceiptPollingInterval and transactionConfirmationPollingInterval', () => { + const obj = new MyConfigObject(); + const configChange = jest.fn(); + obj.on(Web3ConfigEvent.CONFIG_CHANGE, configChange); + + obj.transactionPollingInterval = 1500; + + expect(configChange).toHaveBeenCalledTimes(3); + expect(configChange).toHaveBeenCalledWith({ + name: 'transactionPollingInterval', + oldValue: defaultConfig.transactionPollingInterval, + newValue: 1500, + }); + expect(configChange).toHaveBeenCalledWith({ + name: 'transactionReceiptPollingInterval', + oldValue: defaultConfig.transactionReceiptPollingInterval, + newValue: 1500, + }); + expect(configChange).toHaveBeenCalledWith({ + name: 'transactionConfirmationPollingInterval', + oldValue: defaultConfig.transactionConfirmationPollingInterval, + newValue: 1500, + }); + }); }); diff --git a/packages/web3-eth/src/errors.ts b/packages/web3-eth/src/errors.ts index 2166fec6d1c..04a6349320e 100644 --- a/packages/web3-eth/src/errors.ts +++ b/packages/web3-eth/src/errors.ts @@ -18,8 +18,12 @@ import { ERR_TX_INVALID_FEE_MARKET_GAS_PRICE, ERR_TX_INVALID_LEGACY_GAS, ERR_TX_DATA_AND_INPUT, + ERR_TX_POLLING_TIMEOUT, + ERR_TX_RECEIPT_MISSING_OR_BLOCKHASH_NULL, + ERR_TX_RECEIPT_MISSING_BLOCK_NUMBER, + ReceiptInfo, } from 'web3-common'; -import { HexString, Numbers, Web3Error } from 'web3-utils'; +import { HexString, HexString32Bytes, Numbers, Web3Error } from 'web3-utils'; export class InvalidTransactionWithSender extends Web3Error { public code = ERR_TX_INVALID_SENDER; @@ -241,3 +245,33 @@ export class TransactionDataAndInputError extends Web3Error { ); } } + +export class TransactionPollingTimeoutError extends Web3Error { + public code = ERR_TX_POLLING_TIMEOUT; + + public constructor(value: { numberOfSeconds: number; transactionHash: HexString32Bytes }) { + super( + `transactionHash: ${value.transactionHash}`, + `Transaction was not mined within ${value.numberOfSeconds} seconds, please make sure your transaction was properly sent. Be aware that it might still be mined!`, + ); + } +} + +export class TransactionMissingReceiptOrBlockHashError extends Web3Error { + public code = ERR_TX_RECEIPT_MISSING_OR_BLOCKHASH_NULL; + + public constructor(value: { receipt: ReceiptInfo; blockHash: HexString32Bytes }) { + super( + `receipt: ${JSON.stringify(value.receipt)}, blockHash: ${value.blockHash}`, + `Receipt missing or blockHash null`, + ); + } +} + +export class TransactionReceiptMissingBlockNumberError extends Web3Error { + public code = ERR_TX_RECEIPT_MISSING_BLOCK_NUMBER; + + public constructor(value: { receipt: ReceiptInfo }) { + super(`receipt: ${JSON.stringify(value.receipt)}`, `Receipt missing block number`); + } +} diff --git a/packages/web3-eth/src/index.ts b/packages/web3-eth/src/index.ts index 8c9f1688398..6fbd1888a1e 100644 --- a/packages/web3-eth/src/index.ts +++ b/packages/web3-eth/src/index.ts @@ -15,7 +15,7 @@ import { Filter, } from 'web3-utils'; -import { BlockFormatted } from './types'; +import { BlockFormatted, Transaction, TransactionCall } from './types'; import * as rpcMethods from './rpc_methods'; import * as rpcMethodsWrappers from './rpc_method_wrappers'; import { Web3EthExecutionAPI } from './web3_eth_execution_api'; @@ -150,10 +150,9 @@ export default class Web3Eth extends Web3Context { return rpcMethodsWrappers.getTransactionCount(this, address, blockNumber, returnType); } - // TODO Needs to convert input to hex string - // public async sendTransaction(transaction: Transaction) { - // return rpcMethodsWrappers.sendTransaction(this, transaction); - // } + public async sendTransaction(transaction: Transaction) { + return rpcMethodsWrappers.sendTransaction(this, transaction); + } public async sendSignedTransaction(transaction: HexStringBytes) { return rpcMethods.sendRawTransaction(this.requestManager, transaction); @@ -165,19 +164,18 @@ export default class Web3Eth extends Web3Context { return rpcMethods.sign(this.requestManager, message, address); } - // TODO Needs to convert input to hex string - // public async signTransaction(transaction: Transaction) { - // return rpcMethodsWrappers.signTransaction(this, transaction); - // } + public async signTransaction(transaction: Transaction) { + return rpcMethodsWrappers.signTransaction(this, transaction); + } // TODO Decide what to do with transaction.to // https://github.com/ChainSafe/web3.js/pull/4525#issuecomment-982330076 - // public async call( - // transaction: Transaction & { to: Address }, - // blockNumber: BlockNumberOrTag = this.defaultBlock, - // ) { - // return rpcMethodsWrappers.call(this, transaction, blockNumber); - // } + public async call( + transaction: TransactionCall, + blockNumber: BlockNumberOrTag = this.defaultBlock, + ) { + return rpcMethodsWrappers.call(this, transaction, blockNumber); + } // TODO Missing param public async estimateGas( diff --git a/packages/web3-eth/src/rpc_method_wrappers.ts b/packages/web3-eth/src/rpc_method_wrappers.ts index 34671e5c191..fb9b55136e2 100644 --- a/packages/web3-eth/src/rpc_method_wrappers.ts +++ b/packages/web3-eth/src/rpc_method_wrappers.ts @@ -1,13 +1,14 @@ // Disabling because returnTypes must be last param to match 1.x params /* eslint-disable default-param-last */ -import { EthExecutionAPI, TransactionWithSender } from 'web3-common'; +import { EthExecutionAPI, PromiEvent, ReceiptInfo, TransactionWithSender } from 'web3-common'; import { Web3Context } from 'web3-core'; import { Address, BlockNumberOrTag, convertObjectPropertiesToValidType, convertToValidType, + HexString, HexString32Bytes, HexStringBytes, Uint, @@ -15,17 +16,28 @@ import { ValidReturnTypes, ValidTypes, } from 'web3-utils'; -import { isHexString32Bytes } from 'web3-validator'; +import { validator, isHexString32Bytes } from 'web3-validator'; import { convertibleBlockProperties, convertibleFeeHistoryResultProperties, convertibleReceiptInfoProperties, convertibleTransactionInfoProperties, } from './convertible_properties'; +import { + TransactionMissingReceiptOrBlockHashError, + TransactionPollingTimeoutError, + TransactionReceiptMissingBlockNumberError, +} from './errors'; import { formatTransaction } from './format_transaction'; import * as rpcMethods from './rpc_methods'; -import { BlockFormatted } from './types'; +import { + BlockFormatted, + Transaction, + SendTransactionEvents, + SendSignedTransactionEvents, + TransactionCall, +} from './types'; import { Web3EthExecutionAPI } from './web3_eth_execution_api'; export const getProtocolVersion = async (web3Context: Web3Context) => @@ -279,15 +291,183 @@ export async function getPendingTransactions, + 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, + // TODO - Refactor after input formatting is added to getBlock + (BigInt(transactionReceipt.blockNumber) + BigInt(confirmationNumber)).toString(16), + false, + ValidTypes.Number, + ); + + if (nextBlock?.hash !== null) { + confirmationNumber += 1; + transactionPromiEvent.emit('confirmation', { + confirmationNumber, + receipt: transactionReceipt, + latestBlockHash: nextBlock.hash, + }); + } + }, web3Context.transactionConfirmationPollingInterval ?? web3Context.transactionPollingInterval); +} + +export async function sendTransaction( + web3Context: Web3Context, + transaction: Transaction, +) { + const formattedTransaction = formatTransaction(transaction, ValidTypes.HexString); + // 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 => { + // TODO - Populate potentially missing gas fields + // if ( + // transaction.gasPrice === undefined && + // (transaction.maxPriorityFeePerGas === undefined || + // transaction.maxFeePerGas === undefined) + // ) { + // Determine transaction type and fill in required gas properties + // } + + promiEvent.emit('sending', formattedTransaction); + + // TODO - If an account is available in wallet, sign transaction and call sendRawTransaction + // https://github.com/ChainSafe/web3.js/blob/b32555cfeedde128c657dabbba201102f691f955/packages/web3-core-method/src/index.js#L720 + + const transactionHash = await rpcMethods.sendTransaction( + web3Context.requestManager, + formattedTransaction, + ); + + promiEvent.emit('sent', formattedTransaction); + 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, + ); + }); + + return promiEvent; +} export const sendSignedTransaction = async ( web3Context: Web3Context, transaction: HexStringBytes, -) => rpcMethods.sendRawTransaction(web3Context.requestManager, transaction); +) => { + // 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, + ); + }); + + 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 @@ -297,19 +477,27 @@ export const sign = async ( address: Address, ) => rpcMethods.sign(web3Context.requestManager, address, message); -// TODO Needs to convert input to hex string -// public async signTransaction(transaction: Transaction) { -// return rpcMethods.signTransaction(this.web3Context.requestManager, transaction); -// } - -// TODO Decide what to do with transaction.to -// https://github.com/ChainSafe/web3.js/pull/4525#issuecomment-982330076 -// public async call( -// transaction: Transaction & { to: Address }, -// blockNumber: BlockNumberOrTag = this.web3Context.defaultBlock, -// ) { -// return rpcMethods.call(this.web3Context.requestManager, transaction, blockNumber); -// } +export const signTransaction = async ( + web3Context: Web3Context, + transaction: Transaction, +) => + rpcMethods.signTransaction( + web3Context.requestManager, + formatTransaction(transaction, ValidTypes.HexString), + ); + +export const call = async ( + web3Context: Web3Context, + transaction: TransactionCall, + blockNumber: BlockNumberOrTag = web3Context.defaultBlock, +) => { + validator.validate(['address'], [transaction.to]); + return rpcMethods.call( + web3Context.requestManager, + formatTransaction(transaction, ValidTypes.HexString) as TransactionCall, + convertToValidType(blockNumber, ValidTypes.HexString) as HexString, + ); +}; // TODO Missing param export async function estimateGas( diff --git a/packages/web3-eth/src/rpc_methods.ts b/packages/web3-eth/src/rpc_methods.ts index 765881ce7bc..f9425ea7e28 100644 --- a/packages/web3-eth/src/rpc_methods.ts +++ b/packages/web3-eth/src/rpc_methods.ts @@ -11,7 +11,6 @@ import { HexString8Bytes, } from 'web3-utils'; import { validator } from 'web3-validator'; -import { validateTransactionCall, validateTransactionWithSender } from './validation'; import { Web3EthExecutionAPI } from './web3_eth_execution_api'; export async function getProtocolVersion(requestManager: Web3RequestManager) { @@ -184,24 +183,28 @@ export async function sign( }); } +// TODO - Validation should be: +// isTransactionWithSender(transaction) +// ? validateTransactionWithSender(transaction) +// : validateTransactionWithSender(transaction, true) with true being a isPartial flag export async function signTransaction( requestManager: Web3RequestManager, - transaction: TransactionWithSender, + transaction: TransactionWithSender | Partial, ) { - validateTransactionWithSender(transaction); - return requestManager.send({ method: 'eth_signTransaction', params: [transaction], }); } +// TODO - Validation should be: +// isTransactionWithSender(transaction) +// ? validateTransactionWithSender(transaction) +// : validateTransactionWithSender(transaction, true) with true being a isPartial flag export async function sendTransaction( requestManager: Web3RequestManager, - transaction: TransactionWithSender, + transaction: TransactionWithSender | Partial, ) { - validateTransactionWithSender(transaction); - return requestManager.send({ method: 'eth_sendTransaction', params: [transaction], @@ -220,12 +223,13 @@ export async function sendRawTransaction( }); } +// TODO - validate transaction export async function call( requestManager: Web3RequestManager, transaction: TransactionCall, blockNumber: BlockNumberOrTag, ) { - validateTransactionCall(transaction); + // validateTransactionCall(transaction); validator.validate(['blockNumberOrTag'], [blockNumber]); return requestManager.send({ diff --git a/packages/web3-eth/src/types.ts b/packages/web3-eth/src/types.ts index 3537b6c945f..9ba8cfba62f 100644 --- a/packages/web3-eth/src/types.ts +++ b/packages/web3-eth/src/types.ts @@ -1,4 +1,11 @@ -import { AccessList, Log, TransactionHash, TransactionInfo, Uncles } from 'web3-common'; +import { + AccessList, + Log, + ReceiptInfo, + TransactionHash, + TransactionInfo, + Uncles, +} from 'web3-common'; import { Address, HexString, @@ -65,6 +72,11 @@ export interface Transaction { s?: HexString; } +export interface TransactionCall + extends Transaction { + to: Address; +} + export interface PopulatedUnsignedBaseTransaction { from: Address; to?: Address; @@ -245,3 +257,27 @@ export interface AccountObjectFormatted { readonly accountProof: HexString32Bytes[]; readonly storageProof: StorageProofFormatted[]; } + +export type SendTransactionEvents = { + sending: Transaction; + sent: Transaction; + transactionHash: HexString32Bytes; + receipt: ReceiptInfo; + confirmation: { + confirmationNumber: number; + receipt: ReceiptInfo; + latestBlockHash: HexString32Bytes; + }; +}; + +export type SendSignedTransactionEvents = { + sending: HexStringBytes; + sent: HexStringBytes; + transactionHash: HexString32Bytes; + receipt: ReceiptInfo; + confirmation: { + confirmationNumber: number; + receipt: ReceiptInfo; + latestBlockHash: HexString32Bytes; + }; +}; diff --git a/packages/web3-eth/src/validation.ts b/packages/web3-eth/src/validation.ts index a4ffd5aaefa..432521c9deb 100644 --- a/packages/web3-eth/src/validation.ts +++ b/packages/web3-eth/src/validation.ts @@ -109,7 +109,7 @@ export function isTransactionCall(value: TransactionCall): boolean { if (value.gasPrice !== undefined && !isHexStrict(value.gasPrice)) return false; if (value.value !== undefined && !isHexStrict(value.value)) return false; if (value.data !== undefined && !isHexStrict(value.data)) return false; - if ((value as BaseTransaction).type !== undefined) return false; + if (value.type !== undefined) return false; if (isTransaction1559Unsigned(value as Transaction1559Unsigned)) return false; if (isTransaction2930Unsigned(value as Transaction2930Unsigned)) return false; diff --git a/packages/web3-eth/test/unit/web3_rpc_method_wrappers_with_parameters.test.ts b/packages/web3-eth/test/unit/web3_rpc_method_wrappers_with_parameters.test.ts index 3a93bd81b5e..ecdb3b80a22 100644 --- a/packages/web3-eth/test/unit/web3_rpc_method_wrappers_with_parameters.test.ts +++ b/packages/web3-eth/test/unit/web3_rpc_method_wrappers_with_parameters.test.ts @@ -19,7 +19,6 @@ import { getTransactionFromBlock, getTransactionReceipt, getUncle, - sendSignedTransaction, sign, } from '../../src/rpc_method_wrappers'; import { @@ -39,7 +38,6 @@ import { getTransactionReceiptValidData, getTransactionValidData, getUncleValidData, - sendSignedTransactionValidData, signValidData, } from '../fixtures/rpc_methods_wrappers'; @@ -358,23 +356,25 @@ describe('web3_eth_methods_with_parameters', () => { ); }); - describe('sendSignedTransaction', () => { - it.each(sendSignedTransactionValidData)( - 'input: %s\nmockRpcResponse: %s\nrpcMethodParameters: %s\noutput: %s', - async (input, mockRpcResponse) => { - (rpcMethods.sendRawTransaction as jest.Mock).mockResolvedValueOnce( - mockRpcResponse, - ); - expect(await sendSignedTransaction(web3Eth, input)).toBe( - mockRpcResponse, - ); - expect(rpcMethods.sendRawTransaction).toHaveBeenCalledWith( - web3Eth.requestManager, - input, - ); - }, - ); - }); + // TODO - Re-implement after this is merged: + // https://github.com/ChainSafe/web3.js/pull/4806 + // describe('sendSignedTransaction', () => { + // it.each(sendSignedTransactionValidData)( + // 'input: %s\nmockRpcResponse: %s\nrpcMethodParameters: %s\noutput: %s', + // async (input, mockRpcResponse) => { + // (rpcMethods.sendRawTransaction as jest.Mock).mockResolvedValueOnce( + // mockRpcResponse, + // ); + // expect(await sendSignedTransaction(web3Eth, input)).toBe( + // mockRpcResponse, + // ); + // expect(rpcMethods.sendRawTransaction).toHaveBeenCalledWith( + // web3Eth.requestManager, + // input, + // ); + // }, + // ); + // }); describe('sign', () => { it.each(signValidData)(