diff --git a/CHANGELOG.md b/CHANGELOG.md index dc15600d92f..a4c648f425e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1155,11 +1155,15 @@ should use 4.0.1-alpha.0 for testing. #### web3-errors - Added error class `InvalidMethodParamsError` and error code `ERR_INVALID_METHOD_PARAMS = 207` (#5824) +- `request` property to `ResponseError` (#5854) +- `data` property to `TransactionRevertInstructionError` (#5854) +- `TransactionRevertWithCustomError` was added to handle custom solidity errors (#5854) #### web3-eth - Added `createAccessList` functionality ( #5780 ) - Added support of `safe` and `finalized` block tags (#5823) +- `contractAbi` option to `SendTransactionOptions` and `SendSignedTransactionOptions` to added the ability to parse custom solidity errors (#5854) #### web3-eth-abi @@ -1211,10 +1215,12 @@ should use 4.0.1-alpha.0 for testing. #### web3-errors - The abstract class `Web3Error` is renamed to `BaseWeb3Error` (#5771) +- Renamed `TransactionRevertError` to `TransactionRevertInstructionError` to remain consistent with `1.x` (#5854) #### web3-eth - Update imports statements for objects that was moved between web3 packages (#5771) +- `sendTransaction` and `sendSignedTransaction` now errors with (and `error` event emits) the following possible errors: `TransactionRevertedWithoutReasonError`, `TransactionRevertInstructionError`, `TransactionRevertWithCustomError`, `InvalidResponseError`, or `ContractExecutionError` (#5854) #### web3-eth-contract @@ -1257,3 +1263,4 @@ should use 4.0.1-alpha.0 for testing. #### web3-eth-contract - Fix contract defaults (#5756) +- Fixed getPastEventsError (#5819) diff --git a/docs/docs/guides/web3_migration_guide/providers_migration_guide.md b/docs/docs/guides/web3_migration_guide/providers_migration_guide.md index dbedccdc94c..23615b29889 100644 --- a/docs/docs/guides/web3_migration_guide/providers_migration_guide.md +++ b/docs/docs/guides/web3_migration_guide/providers_migration_guide.md @@ -187,4 +187,4 @@ provider.on('error', error => { // the `maxAttempts` is equal to the provided value by the user, or the default value `5`. } }); -``` +``` \ No newline at end of file diff --git a/docs/docs/guides/web3_providers_guide/index.md b/docs/docs/guides/web3_providers_guide/index.md index 97a803bab4c..651d3b9f723 100644 --- a/docs/docs/guides/web3_providers_guide/index.md +++ b/docs/docs/guides/web3_providers_guide/index.md @@ -207,4 +207,4 @@ provider.on('error', error => { // the `maxAttempts` is equal to the provided value by the user, or the default value `5`. } }); -``` +``` \ No newline at end of file diff --git a/packages/web3-core/src/web3_request_manager.ts b/packages/web3-core/src/web3_request_manager.ts index 327d9f24edf..15975825e78 100644 --- a/packages/web3-core/src/web3_request_manager.ts +++ b/packages/web3-core/src/web3_request_manager.ts @@ -369,7 +369,7 @@ export class Web3RequestManager< throw new RpcError(rpcErrorResponse); } } else if (!Web3RequestManager._isReverted(response)) { - throw new InvalidResponseError(response); + throw new InvalidResponseError(response, payload); } } diff --git a/packages/web3-errors/CHANGELOG.md b/packages/web3-errors/CHANGELOG.md index 06068537093..c6ba69b3aab 100644 --- a/packages/web3-errors/CHANGELOG.md +++ b/packages/web3-errors/CHANGELOG.md @@ -76,8 +76,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - The abstract class `Web3Error` is renamed to `BaseWeb3Error` (#5771) +- Renamed TransactionRevertError to TransactionRevertInstructionError to remain consistent with 1.x - Using `MaxAttemptsReachedOnReconnectingError` with the same message for 1.x but also adding the `maxAttempts` (#5894) ### Added - Added error class `InvalidMethodParamsError` and error code `ERR_INVALID_METHOD_PARAMS = 207` (#5824) +- `request` property to `ResponseError` (#5854) +- `data` property to `TransactionRevertInstructionError` (#5854) +- `TransactionRevertWithCustomError` was added to handle custom solidity errors (#5854) diff --git a/packages/web3-errors/src/error_codes.ts b/packages/web3-errors/src/error_codes.ts index 81c50434454..5efc073bbfe 100644 --- a/packages/web3-errors/src/error_codes.ts +++ b/packages/web3-errors/src/error_codes.ts @@ -86,6 +86,7 @@ export const ERR_TX_GAS_MISMATCH = 434; export const ERR_TX_CHAIN_MISMATCH = 435; export const ERR_TX_HARDFORK_MISMATCH = 436; export const ERR_TX_INVALID_RECEIVER = 437; +export const ERR_TX_REVERT_TRANSACTION_CUSTOM_ERROR = 438; // Connection error codes export const ERR_CONN = 500; diff --git a/packages/web3-errors/src/errors/response_errors.ts b/packages/web3-errors/src/errors/response_errors.ts index fd0afb45128..fdd6ad5fe49 100644 --- a/packages/web3-errors/src/errors/response_errors.ts +++ b/packages/web3-errors/src/errors/response_errors.ts @@ -16,7 +16,12 @@ along with web3.js. If not, see . */ // eslint-disable-next-line max-classes-per-file -import { JsonRpcError, JsonRpcResponse, JsonRpcResponseWithError } from 'web3-types'; +import { + JsonRpcError, + JsonRpcPayload, + JsonRpcResponse, + JsonRpcResponseWithError, +} from 'web3-types'; import { BaseWeb3Error } from '../web3_error_base'; import { ERR_INVALID_RESPONSE, ERR_RESPONSE } from '../error_codes'; @@ -36,11 +41,16 @@ const isResponseWithError = ( const buildErrorMessage = (response: JsonRpcResponse): string => isResponseWithError(response) ? response.error.message : ''; -export class ResponseError extends BaseWeb3Error { +export class ResponseError extends BaseWeb3Error { public code = ERR_RESPONSE; public data?: ErrorType | ErrorType[]; + public request?: JsonRpcPayload; - public constructor(response: JsonRpcResponse, message?: string) { + public constructor( + response: JsonRpcResponse, + message?: string, + request?: JsonRpcPayload, + ) { super( message ?? `Returned error: ${ @@ -55,16 +65,24 @@ export class ResponseError extends BaseWeb3Error { ? response.map(r => r.error?.data as ErrorType) : response?.error?.data; } + + this.request = request; } public toJSON() { - return { ...super.toJSON(), data: this.data }; + return { ...super.toJSON(), data: this.data, request: this.request }; } } -export class InvalidResponseError extends ResponseError { - public constructor(result: JsonRpcResponse) { - super(result); +export class InvalidResponseError extends ResponseError< + ErrorType, + RequestType +> { + public constructor( + result: JsonRpcResponse, + request?: JsonRpcPayload, + ) { + super(result, undefined, request); this.code = ERR_INVALID_RESPONSE; let errorOrErrors: JsonRpcError | JsonRpcError[] | undefined; diff --git a/packages/web3-errors/src/errors/transaction_errors.ts b/packages/web3-errors/src/errors/transaction_errors.ts index 74d1318feda..807578a4097 100644 --- a/packages/web3-errors/src/errors/transaction_errors.ts +++ b/packages/web3-errors/src/errors/transaction_errors.ts @@ -57,6 +57,7 @@ import { ERR_TX_UNABLE_TO_POPULATE_NONCE, ERR_TX_UNSUPPORTED_EIP_1559, ERR_TX_UNSUPPORTED_TYPE, + ERR_TX_REVERT_TRANSACTION_CUSTOM_ERROR, } from '../error_codes'; import { InvalidValueError, BaseWeb3Error } from '../web3_error_base'; @@ -84,16 +85,21 @@ export class RevertInstructionError extends BaseWeb3Error { } } -export class TransactionRevertError extends BaseWeb3Error { +export class TransactionRevertInstructionError< + ReceiptType = TransactionReceipt, +> extends BaseWeb3Error { public code = ERR_TX_REVERT_TRANSACTION; public constructor( public reason: string, public signature?: string, - public receipt?: TransactionReceipt, + public receipt?: ReceiptType, + public data?: string, ) { super( - `Transaction has been reverted by the EVM:\n ${JSON.stringify(receipt, undefined, 2)}`, + `Transaction has been reverted by the EVM${ + receipt === undefined ? '' : `:\n ${BaseWeb3Error.convertToString(receipt)}` + }`, ); } @@ -103,6 +109,43 @@ export class TransactionRevertError extends BaseWeb3Error { reason: this.reason, signature: this.signature, receipt: this.receipt, + data: this.data, + }; + } +} + +/** + * This error is used when a transaction to a smart contract fails and + * a custom user error (https://blog.soliditylang.org/2021/04/21/custom-errors/) + * is able to be parsed from the revert reason + */ +export class TransactionRevertWithCustomError< + ReceiptType = TransactionReceipt, +> extends TransactionRevertInstructionError { + public code = ERR_TX_REVERT_TRANSACTION_CUSTOM_ERROR; + + public constructor( + public reason: string, + public customErrorName: string, + public customErrorDecodedSignature: string, + public customErrorArguments: Record, + public signature?: string, + public receipt?: ReceiptType, + public data?: string, + ) { + super(reason); + } + + public toJSON() { + return { + ...super.toJSON(), + reason: this.reason, + customErrorName: this.customErrorName, + customErrorDecodedSignature: this.customErrorDecodedSignature, + customErrorArguments: this.customErrorArguments, + signature: this.signature, + receipt: this.receipt, + data: this.data, }; } } @@ -125,10 +168,14 @@ export class ContractCodeNotStoredError extends TransactionError { } } -export class TransactionRevertedWithoutReasonError extends TransactionError { - public constructor(receipt: TransactionReceipt) { +export class TransactionRevertedWithoutReasonError< + ReceiptType = TransactionReceipt, +> extends TransactionError { + public constructor(receipt?: ReceiptType) { super( - `Transaction has been reverted by the EVM:\n ${JSON.stringify(receipt, undefined, 2)}`, + `Transaction has been reverted by the EVM${ + receipt === undefined ? '' : `:\n ${BaseWeb3Error.convertToString(receipt)}` + }`, receipt, ); this.code = ERR_TX_REVERT_WITHOUT_REASON; diff --git a/packages/web3-errors/test/unit/__snapshots__/errors.test.ts.snap b/packages/web3-errors/test/unit/__snapshots__/errors.test.ts.snap index e53b1600818..e25b90874dd 100644 --- a/packages/web3-errors/test/unit/__snapshots__/errors.test.ts.snap +++ b/packages/web3-errors/test/unit/__snapshots__/errors.test.ts.snap @@ -172,6 +172,7 @@ Object { }, "message": "Returned error: error message", "name": "InvalidResponseError", + "request": undefined, } `; @@ -229,6 +230,7 @@ Object { "innerError": undefined, "message": "Returned error: error message", "name": "ResponseError", + "request": undefined, } `; @@ -239,6 +241,7 @@ Object { "innerError": undefined, "message": "Returned error: error message", "name": "ResponseError", + "request": undefined, } `; @@ -280,15 +283,14 @@ Object { } `; -exports[`errors TransactionRevertError should have valid json structure 1`] = ` +exports[`errors TransactionRevertInstructionError should have valid json structure 1`] = ` Object { "code": 402, + "data": undefined, "innerError": undefined, "message": "Transaction has been reverted by the EVM: - { - \\"attr1\\": \\"attr1\\" -}", - "name": "TransactionRevertError", + {\\"attr1\\":\\"attr1\\"}", + "name": "TransactionRevertInstructionError", "reason": "message", "receipt": Object { "attr1": "attr1", @@ -297,14 +299,32 @@ Object { } `; +exports[`errors TransactionRevertWithCustomError should have valid json structure 1`] = ` +Object { + "code": 438, + "customErrorArguments": Object { + "customErrorArgument": "customErrorArgument", + }, + "customErrorDecodedSignature": "customErrorDecodedSignature", + "customErrorName": "customErrorName", + "data": "data", + "innerError": undefined, + "message": "Transaction has been reverted by the EVM", + "name": "TransactionRevertWithCustomError", + "reason": "reason", + "receipt": Object { + "attr1": "attr1", + }, + "signature": "signature", +} +`; + exports[`errors TransactionRevertedWithoutReasonError should have valid json structure 1`] = ` Object { "code": 405, "innerError": undefined, "message": "Transaction has been reverted by the EVM: - { - \\"attr1\\": \\"attr1\\" -}", + {\\"attr1\\":\\"attr1\\"}", "name": "TransactionRevertedWithoutReasonError", "receipt": Object { "attr1": "attr1", diff --git a/packages/web3-errors/test/unit/errors.test.ts b/packages/web3-errors/test/unit/errors.test.ts index 8492aaa3e00..dc36a7d979d 100644 --- a/packages/web3-errors/test/unit/errors.test.ts +++ b/packages/web3-errors/test/unit/errors.test.ts @@ -53,7 +53,7 @@ describe('errors', () => { // To disable error for the abstract class // eslint-disable-next-line @typescript-eslint/ban-ts-comment - const err = new ErrorClass({} as never, {} as never, {} as never); + const err = new ErrorClass({} as never, {} as never, {} as never, {} as never); errorCodes.push(err.code); } @@ -162,16 +162,34 @@ describe('errors', () => { }); }); - describe('TransactionRevertError', () => { + describe('TransactionRevertInstructionError', () => { it('should have valid json structure', () => { expect( - new transactionErrors.TransactionRevertError('message', 'signature', { + new transactionErrors.TransactionRevertInstructionError('message', 'signature', { attr1: 'attr1', } as any).toJSON(), ).toMatchSnapshot(); }); }); + describe('TransactionRevertWithCustomError', () => { + it('should have valid json structure', () => { + expect( + new transactionErrors.TransactionRevertWithCustomError( + 'reason', + 'customErrorName', + 'customErrorDecodedSignature', + { customErrorArgument: 'customErrorArgument' }, + 'signature', + { + attr1: 'attr1', + } as any, + 'data', + ).toJSON(), + ).toMatchSnapshot(); + }); + }); + describe('NoContractAddressFoundError', () => { it('should have valid json structure', () => { expect( diff --git a/packages/web3-eth-contract/src/contract.ts b/packages/web3-eth-contract/src/contract.ts index 666df4ecb8d..bca93dd7c2e 100644 --- a/packages/web3-eth-contract/src/contract.ts +++ b/packages/web3-eth-contract/src/contract.ts @@ -1050,10 +1050,13 @@ export class Contract options, contractOptions: modifiedContractOptions, }); - const transactionToSend = sendTransaction(this, tx, DEFAULT_RETURN_FORMAT); + const transactionToSend = sendTransaction(this, tx, DEFAULT_RETURN_FORMAT, { + // TODO Should make this configurable by the user + checkRevertBeforeSending: false, + }); // eslint-disable-next-line no-void - void transactionToSend.on('contractExecutionError', (error: unknown) => { + void transactionToSend.on('error', (error: unknown) => { if (error instanceof ContractExecutionError) { // this will parse the error data by trying to decode the ABI error inputs according to EIP-838 decodeContractErrorData(errorsAbi, error.innerError); @@ -1094,6 +1097,8 @@ export class Contract newContract.options.address = receipt.contractAddress; return newContract; }, + // TODO Should make this configurable by the user + checkRevertBeforeSending: false, }); } diff --git a/packages/web3-eth-contract/test/integration/contract_defaults_extra.test.ts b/packages/web3-eth-contract/test/integration/contract_defaults_extra.test.ts index da5bbf143e8..676d86b0de1 100644 --- a/packages/web3-eth-contract/test/integration/contract_defaults_extra.test.ts +++ b/packages/web3-eth-contract/test/integration/contract_defaults_extra.test.ts @@ -186,6 +186,7 @@ describe('contract defaults (extra)', () => { }), expect.any(Object), expect.any(Object), + expect.any(Object), ); }); diff --git a/packages/web3-eth-contract/test/integration/contract_methods.test.ts b/packages/web3-eth-contract/test/integration/contract_methods.test.ts index 7cfed117bdc..1fa72966f00 100644 --- a/packages/web3-eth-contract/test/integration/contract_methods.test.ts +++ b/packages/web3-eth-contract/test/integration/contract_methods.test.ts @@ -146,27 +146,24 @@ describe('contract', () => { expect(await deployedTempContract.methods.getStringValue().call()).toBe('TEST'); }); - // TODO: Get and match the revert error message it('should returns errors on reverts', async () => { - try { - await contractDeployed.methods.reverts().send(sendOptions); - } catch (receipt: any) { - // eslint-disable-next-line jest/no-conditional-expect - expect(receipt).toEqual( - // eslint-disable-next-line jest/no-conditional-expect - expect.objectContaining({ - // eslint-disable-next-line jest/no-conditional-expect - transactionHash: expect.any(String), - }), - ); - - // To avoid issue with the `objectContaining` and `cypress` had to add - // these expectations explicitly on each attribute - // eslint-disable-next-line jest/no-conditional-expect - expect(receipt.status).toEqual(BigInt(0)); - } - - expect.assertions(2); + await expect( + contractDeployed.methods.reverts().send(sendOptions), + ).rejects.toMatchObject({ + name: 'TransactionRevertedWithoutReasonError', + receipt: { + cumulativeGasUsed: BigInt(21543), + from: acc.address, + gasUsed: BigInt(21543), + logs: [], + logsBloom: + '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + status: BigInt(0), + to: contractDeployed.options.address?.toLowerCase(), + transactionIndex: BigInt(0), + type: BigInt(0), + }, + }); }); }); }); diff --git a/packages/web3-eth-ens/src/registry.ts b/packages/web3-eth-ens/src/registry.ts index 88b91378862..c4989ceafe7 100644 --- a/packages/web3-eth-ens/src/registry.ts +++ b/packages/web3-eth-ens/src/registry.ts @@ -43,7 +43,7 @@ export class Registry { return result; } catch (error) { - throw new Error(); // TODO: TransactionRevertError Needs to be added after web3-eth call method is implemented + throw new Error(); // TODO: TransactionRevertInstructionError Needs to be added after web3-eth call method is implemented } } @@ -60,7 +60,7 @@ export class Registry { return receipt; } catch (error) { - throw new Error(); // TODO: TransactionRevertError Needs to be added after web3-eth call method is implemented + throw new Error(); // TODO: TransactionRevertInstructionError Needs to be added after web3-eth call method is implemented } } @@ -68,7 +68,7 @@ export class Registry { try { return this.contract.methods.ttl(namehash(name)).call(); } catch (error) { - throw new Error(); // TODO: TransactionRevertError Needs to be added after web3-eth call method is implemented + throw new Error(); // TODO: TransactionRevertInstructionError Needs to be added after web3-eth call method is implemented } } @@ -82,7 +82,7 @@ export class Registry { return promiEvent; } catch (error) { - throw new Error(); // TODO: TransactionRevertError Needs to be added after web3-eth call method is implemented + throw new Error(); // TODO: TransactionRevertInstructionError Needs to be added after web3-eth call method is implemented } } @@ -104,7 +104,7 @@ export class Registry { .send(txConfig); return receipt; } catch (error) { - throw new Error(); // TODO: TransactionRevertError Needs to be added after web3-eth call method is implemented + throw new Error(); // TODO: TransactionRevertInstructionError Needs to be added after web3-eth call method is implemented } } @@ -165,7 +165,7 @@ export class Registry { return result; } catch (error) { - throw new Error(); // TODO: TransactionRevertError Needs to be added after web3-eth call method is implemented + throw new Error(); // TODO: TransactionRevertInstructionError Needs to be added after web3-eth call method is implemented } } @@ -175,7 +175,7 @@ export class Registry { return promise; } catch (error) { - throw new Error(); // TODO: TransactionRevertError Needs to be added after web3-eth call method is implemented + throw new Error(); // TODO: TransactionRevertInstructionError Needs to be added after web3-eth call method is implemented } } @@ -194,7 +194,7 @@ export class Registry { throw new Error(); }); } catch (error) { - throw new Error(); // TODO: TransactionRevertError Needs to be added after web3-eth call method is implemented + throw new Error(); // TODO: TransactionRevertInstructionError Needs to be added after web3-eth call method is implemented } } @@ -209,7 +209,7 @@ export class Registry { .setResolver(namehash(name), format({ eth: 'address' }, address, returnFormat)) .send(txConfig); } catch (error) { - throw new Error(); // TODO: TransactionRevertError Needs to be added after web3-eth call method is implemented + throw new Error(); // TODO: TransactionRevertInstructionError Needs to be added after web3-eth call method is implemented } } @@ -231,7 +231,7 @@ export class Registry { ) .send(txConfig); } catch (error) { - throw new Error(); // TODO: TransactionRevertError Needs to be added after web3-eth call method is implemented + throw new Error(); // TODO: TransactionRevertInstructionError Needs to be added after web3-eth call method is implemented } } public get events() { diff --git a/packages/web3-eth/CHANGELOG.md b/packages/web3-eth/CHANGELOG.md index 00162a400a2..203b5637db3 100644 --- a/packages/web3-eth/CHANGELOG.md +++ b/packages/web3-eth/CHANGELOG.md @@ -94,11 +94,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Update imports statements for objects that was moved between web3 packages (#5771) +- `sendTransaction` and `sendSignedTransaction` now errors with (and `error` event emits) the following possible errors: `TransactionRevertedWithoutReasonError`, `TransactionRevertInstructionError`, `TransactionRevertWithCustomError`, `InvalidResponseError`, or `ContractExecutionError` (#5854) ### Added - Added `createAccessList` functionality ( #5780 ) - Added support of `safe` and `finalized` block tags (#5823) +- `contractAbi` option to `SendTransactionOptions` and `SendSignedTransactionOptions` to added the ability to parse custom solidity errors (#5854) ### Removed diff --git a/packages/web3-eth/src/rpc_method_wrappers.ts b/packages/web3-eth/src/rpc_method_wrappers.ts index 410eaf10471..5317e66e603 100644 --- a/packages/web3-eth/src/rpc_method_wrappers.ts +++ b/packages/web3-eth/src/rpc_method_wrappers.ts @@ -46,10 +46,27 @@ import { AccessListResult, } from 'web3-types'; import { Web3Context, Web3PromiEvent } from 'web3-core'; -import { ETH_DATA_FORMAT, FormatType, DataFormat, DEFAULT_RETURN_FORMAT, format } from 'web3-utils'; +import { + ETH_DATA_FORMAT, + FormatType, + DataFormat, + DEFAULT_RETURN_FORMAT, + format, + hexToBytes, + bytesToBuffer, +} from 'web3-utils'; import { isBlockTag, isBytes, isNullish, isString } from 'web3-validator'; -import { SignatureError, TransactionError, ContractExecutionError } from 'web3-errors'; +import { + ContractExecutionError, + InvalidResponseError, + SignatureError, + TransactionRevertedWithoutReasonError, + TransactionRevertInstructionError, + TransactionRevertWithCustomError, +} from 'web3-errors'; import { ethRpcMethods } from 'web3-rpc-methods'; +import { TransactionFactory } from '@ethereumjs/tx'; + import { decodeSignedTransaction } from './utils/decode_signed_transaction'; import { accountSchema, @@ -78,6 +95,8 @@ import { waitForTransactionReceipt } from './utils/wait_for_transaction_receipt' import { watchTransactionForConfirmations } from './utils/watch_transaction_for_confirmations'; import { NUMBER_DATA_FORMAT } from './constants'; // eslint-disable-next-line import/no-cycle +import { getTransactionError } from './utils/get_transaction_error'; +// eslint-disable-next-line import/no-cycle import { getRevertReason } from './utils/get_revert_reason'; /** @@ -1062,46 +1081,63 @@ export function sendTransaction< | TransactionWithToLocalWalletIndex | TransactionWithFromAndToLocalWalletIndex, returnFormat: ReturnFormat, - options?: SendTransactionOptions, + options: SendTransactionOptions = { checkRevertBeforeSending: true }, ): Web3PromiEvent> { const promiEvent = new Web3PromiEvent>( (resolve, reject) => { setImmediate(() => { (async () => { - try { - let transactionFormatted = formatTransaction( - { - ...transaction, - from: getTransactionFromOrToAttr('from', web3Context, transaction), - to: getTransactionFromOrToAttr('to', web3Context, transaction), - }, - ETH_DATA_FORMAT, - ); + let transactionFormatted = formatTransaction( + { + ...transaction, + from: getTransactionFromOrToAttr('from', web3Context, transaction), + to: getTransactionFromOrToAttr('to', web3Context, transaction), + }, + ETH_DATA_FORMAT, + ); + + if ( + !options?.ignoreGasPricing && + isNullish(transactionFormatted.gasPrice) && + (isNullish(transaction.maxPriorityFeePerGas) || + isNullish(transaction.maxFeePerGas)) + ) { + transactionFormatted = { + ...transactionFormatted, + // TODO gasPrice, maxPriorityFeePerGas, maxFeePerGas + // should not be included if undefined, but currently are + ...(await getTransactionGasPricing( + transactionFormatted, + web3Context, + ETH_DATA_FORMAT, + )), + }; + } - if (web3Context.handleRevert) { - // eslint-disable-next-line no-use-before-define - await getRevertReason( + try { + if (options.checkRevertBeforeSending !== false) { + const reason = await getRevertReason( web3Context, transactionFormatted as TransactionCall, + options.contractAbi, ); - } - - if ( - !options?.ignoreGasPricing && - isNullish(transactionFormatted.gasPrice) && - (isNullish(transaction.maxPriorityFeePerGas) || - isNullish(transaction.maxFeePerGas)) - ) { - transactionFormatted = { - ...transactionFormatted, - // TODO gasPrice, maxPriorityFeePerGas, maxFeePerGas - // should not be included if undefined, but currently are - ...(await getTransactionGasPricing( - transactionFormatted, + if (reason !== undefined) { + const error = await getTransactionError( web3Context, - ETH_DATA_FORMAT, - )), - }; + transactionFormatted as TransactionCall, + undefined, + undefined, + options.contractAbi, + reason, + ); + + if (promiEvent.listenerCount('error') > 0) { + promiEvent.emit('error', error); + } + + reject(error); + return; + } } if (promiEvent.listenerCount('sending') > 0) { @@ -1177,17 +1213,19 @@ export function sendTransaction< ) as unknown as ResolveType, ); } else if (transactionReceipt.status === BigInt(0)) { + const error = await getTransactionError( + web3Context, + transactionFormatted as TransactionCall, + transactionReceiptFormatted, + undefined, + options?.contractAbi, + ); + if (promiEvent.listenerCount('error') > 0) { - promiEvent.emit( - 'error', - new TransactionError( - 'Transaction failed', - transactionReceiptFormatted, - ), - ); + promiEvent.emit('error', error); } - reject(transactionReceiptFormatted as unknown as ResolveType); - return; + + reject(error); } else { resolve(transactionReceiptFormatted as unknown as ResolveType); } @@ -1206,17 +1244,30 @@ export function sendTransaction< ); } } catch (error) { - if (error instanceof ContractExecutionError) { - promiEvent.emit('contractExecutionError', error); - } - if (promiEvent.listenerCount('error') > 0) { - promiEvent.emit( - 'error', - new TransactionError((error as Error).message), + let _error = error; + + if (_error instanceof ContractExecutionError && web3Context.handleRevert) { + _error = await getTransactionError( + web3Context, + transactionFormatted as TransactionCall, + undefined, + undefined, + options?.contractAbi, ); } - reject(error); + if ( + (_error instanceof InvalidResponseError || + _error instanceof ContractExecutionError || + _error instanceof TransactionRevertWithCustomError || + _error instanceof TransactionRevertedWithoutReasonError || + _error instanceof TransactionRevertInstructionError) && + promiEvent.listenerCount('error') > 0 + ) { + promiEvent.emit('error', _error); + } + + reject(_error); } })() as unknown; }); @@ -1317,7 +1368,7 @@ export function sendSignedTransaction< web3Context: Web3Context, signedTransaction: Bytes, returnFormat: ReturnFormat, - options?: SendSignedTransactionOptions, + options: SendSignedTransactionOptions = { checkRevertBeforeSending: true }, ): Web3PromiEvent> { // TODO - Promise returned in function argument where a void return was expected // eslint-disable-next-line @typescript-eslint/no-misused-promises @@ -1325,22 +1376,54 @@ export function sendSignedTransaction< (resolve, reject) => { setImmediate(() => { (async () => { + // Formatting signedTransaction to be send to RPC endpoint + const signedTransactionFormattedHex = format( + { eth: 'bytes' }, + signedTransaction, + ETH_DATA_FORMAT, + ); + const unSerializedTransaction = TransactionFactory.fromSerializedData( + bytesToBuffer(hexToBytes(signedTransactionFormattedHex)), + ); + const unSerializedTransactionWithFrom = { + ...unSerializedTransaction.toJSON(), + // Some providers will default `from` to address(0) causing the error + // reported from `eth_call` to not be the reason the user's tx failed + // e.g. `eth_call` will return an Out of Gas error for a failed + // smart contract execution contract, because the sender, address(0), + // has no balance to pay for the gas of the transaction execution + from: unSerializedTransaction.getSenderAddress().toString(), + }; + try { - // Formatting signedTransaction to be send to RPC endpoint - const signedTransactionFormattedHex = format( - { eth: 'bytes' }, - signedTransaction, - ETH_DATA_FORMAT, - ); + if (options.checkRevertBeforeSending !== false) { + const reason = await getRevertReason( + web3Context, + unSerializedTransactionWithFrom as TransactionCall, + options.contractAbi, + ); + if (reason !== undefined) { + const error = await getTransactionError( + web3Context, + unSerializedTransactionWithFrom as TransactionCall, + undefined, + undefined, + options.contractAbi, + reason, + ); + + if (promiEvent.listenerCount('error') > 0) { + promiEvent.emit('error', error); + } + + reject(error); + return; + } + } if (promiEvent.listenerCount('sending') > 0) { promiEvent.emit('sending', signedTransactionFormattedHex); } - // todo enable handleRevert for sendSignedTransaction when we have a function to decode transactions - // importing a package for this would increase the size of the library - // if (web3Context.handleRevert) { - // await getRevertReason(web3Context, transaction, returnFormat); - // } const transactionHash = await trySendTransaction( web3Context, @@ -1388,17 +1471,19 @@ export function sendSignedTransaction< ) as unknown as ResolveType, ); } else if (transactionReceipt.status === BigInt(0)) { + const error = await getTransactionError( + web3Context, + unSerializedTransactionWithFrom as TransactionCall, + transactionReceiptFormatted, + undefined, + options?.contractAbi, + ); + if (promiEvent.listenerCount('error') > 0) { - promiEvent.emit( - 'error', - new TransactionError( - 'Transaction failed', - transactionReceiptFormatted, - ), - ); + promiEvent.emit('error', error); } - reject(transactionReceiptFormatted as unknown as ResolveType); - return; + + reject(error); } else { resolve(transactionReceiptFormatted as unknown as ResolveType); } @@ -1417,13 +1502,30 @@ export function sendSignedTransaction< ); } } catch (error) { - if (promiEvent.listenerCount('error') > 0) { - promiEvent.emit( - 'error', - new TransactionError((error as Error).message), + let _error = error; + + if (_error instanceof ContractExecutionError && web3Context.handleRevert) { + _error = await getTransactionError( + web3Context, + unSerializedTransactionWithFrom as TransactionCall, + undefined, + undefined, + options?.contractAbi, ); } - reject(error); + + if ( + (_error instanceof InvalidResponseError || + _error instanceof ContractExecutionError || + _error instanceof TransactionRevertWithCustomError || + _error instanceof TransactionRevertedWithoutReasonError || + _error instanceof TransactionRevertInstructionError) && + promiEvent.listenerCount('error') > 0 + ) { + promiEvent.emit('error', _error); + } + + reject(_error); } })() as unknown; }); diff --git a/packages/web3-eth/src/types.ts b/packages/web3-eth/src/types.ts index bae3f0a9e2a..fc9b5121543 100644 --- a/packages/web3-eth/src/types.ts +++ b/packages/web3-eth/src/types.ts @@ -15,8 +15,21 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ -import { TransactionError, ContractExecutionError, ResponseError } from 'web3-errors'; -import { Bytes, HexString, Numbers, Transaction, TransactionReceipt } from 'web3-types'; +import { + ContractExecutionError, + TransactionRevertedWithoutReasonError, + TransactionRevertInstructionError, + TransactionRevertWithCustomError, + InvalidResponseError, +} from 'web3-errors'; +import { + Bytes, + ContractAbi, + HexString, + Numbers, + Transaction, + TransactionReceipt, +} from 'web3-types'; import { DataFormat, ETH_DATA_FORMAT, FormatType } from 'web3-utils'; export type InternalTransaction = FormatType; @@ -31,8 +44,12 @@ export type SendTransactionEvents = { receipt: FormatType; latestBlockHash: FormatType; }; - error: TransactionError>; - contractExecutionError: ContractExecutionError | ResponseError; + error: + | TransactionRevertedWithoutReasonError> + | TransactionRevertInstructionError> + | TransactionRevertWithCustomError> + | InvalidResponseError + | ContractExecutionError; }; export type SendSignedTransactionEvents = { @@ -45,17 +62,25 @@ export type SendSignedTransactionEvents = { receipt: FormatType; latestBlockHash: FormatType; }; - error: TransactionError>; - contractExecutionError: ContractExecutionError | ResponseError; + error: + | TransactionRevertedWithoutReasonError> + | TransactionRevertInstructionError> + | TransactionRevertWithCustomError> + | InvalidResponseError + | ContractExecutionError; }; export interface SendTransactionOptions { ignoreGasPricing?: boolean; transactionResolver?: (receipt: TransactionReceipt) => ResolveType; + contractAbi?: ContractAbi; + checkRevertBeforeSending?: boolean; } export interface SendSignedTransactionOptions { transactionResolver?: (receipt: TransactionReceipt) => ResolveType; + contractAbi?: ContractAbi; + checkRevertBeforeSending?: boolean; } export interface RevertReason { diff --git a/packages/web3-eth/src/utils/get_revert_reason.ts b/packages/web3-eth/src/utils/get_revert_reason.ts index 40ca1d71617..095a7e0fc31 100644 --- a/packages/web3-eth/src/utils/get_revert_reason.ts +++ b/packages/web3-eth/src/utils/get_revert_reason.ts @@ -25,6 +25,45 @@ import { DataFormat, DEFAULT_RETURN_FORMAT } from 'web3-utils'; import { call } from '../rpc_method_wrappers'; import { RevertReason, RevertReasonWithCustomError } from '../types'; +export const parseTransactionError = (error: unknown, contractAbi?: ContractAbi) => { + if ( + error instanceof ContractExecutionError && + error.innerError instanceof Eip838ExecutionError + ) { + if (contractAbi !== undefined) { + const errorsAbi = contractAbi.filter(abi => + isAbiErrorFragment(abi), + ) as unknown as AbiErrorFragment[]; + decodeContractErrorData(errorsAbi, error.innerError); + + return { + reason: error.innerError.message, + signature: error.innerError.data?.slice(0, 10), + data: error.innerError.data?.substring(10), + customErrorName: error.innerError.errorName, + customErrorDecodedSignature: error.innerError.errorSignature, + customErrorArguments: error.innerError.errorArgs, + } as RevertReasonWithCustomError; + } + + return { + reason: error.innerError.message, + signature: error.innerError.data?.slice(0, 10), + data: error.innerError.data?.substring(10), + } as RevertReason; + } + + if ( + error instanceof InvalidResponseError && + !Array.isArray(error.innerError) && + error.innerError !== undefined + ) { + return error.innerError.message; + } + + throw error; +}; + /** * Returns the revert reason generated by the EVM if the transaction were to be executed. * @@ -44,41 +83,6 @@ export async function getRevertReason< await call(web3Context, transaction, web3Context.defaultBlock, returnFormat); return undefined; } catch (error) { - if ( - error instanceof ContractExecutionError && - error.innerError instanceof Eip838ExecutionError - ) { - if (contractAbi !== undefined) { - const errorsAbi = contractAbi.filter(abi => - isAbiErrorFragment(abi), - ) as unknown as AbiErrorFragment[]; - decodeContractErrorData(errorsAbi, error.innerError); - - return { - reason: error.innerError.message, - signature: error.innerError.data?.slice(0, 10), - data: error.innerError.data?.substring(10), - customErrorName: error.innerError.errorName, - customErrorDecodedSignature: error.innerError.errorSignature, - customErrorArguments: error.innerError.errorArgs, - } as RevertReasonWithCustomError; - } - - return { - reason: error.innerError.message, - signature: error.innerError.data?.slice(0, 10), - data: error.innerError.data?.substring(10), - } as RevertReason; - } - - if ( - error instanceof InvalidResponseError && - !Array.isArray(error.innerError) && - error.innerError !== undefined - ) { - return error.innerError.message; - } - - throw error; + return parseTransactionError(error, contractAbi); } } diff --git a/packages/web3-eth/src/utils/get_transaction_error.ts b/packages/web3-eth/src/utils/get_transaction_error.ts new file mode 100644 index 00000000000..2623ec53845 --- /dev/null +++ b/packages/web3-eth/src/utils/get_transaction_error.ts @@ -0,0 +1,88 @@ +/* +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 } from 'web3-core'; +import { + TransactionRevertedWithoutReasonError, + TransactionRevertInstructionError, + TransactionRevertWithCustomError, +} from 'web3-errors'; +import { ContractAbi, TransactionCall, TransactionReceipt } from 'web3-types'; +import { DataFormat, FormatType } from 'web3-utils'; +import { RevertReason, RevertReasonWithCustomError } from '../types'; +// eslint-disable-next-line import/no-cycle +import { getRevertReason, parseTransactionError } from './get_revert_reason'; + +export async function getTransactionError( + web3Context: Web3Context, + transactionFormatted?: TransactionCall, + transactionReceiptFormatted?: FormatType, + receivedError?: unknown, + contractAbi?: ContractAbi, + knownReason?: string | RevertReason | RevertReasonWithCustomError, +) { + let _reason: string | RevertReason | RevertReasonWithCustomError | undefined = knownReason; + + if (_reason === undefined) { + if (receivedError !== undefined) { + _reason = parseTransactionError(receivedError); + } else if (web3Context.handleRevert && transactionFormatted !== undefined) { + _reason = await getRevertReason(web3Context, transactionFormatted, contractAbi); + } + } + + let error: + | TransactionRevertedWithoutReasonError> + | TransactionRevertInstructionError> + | TransactionRevertWithCustomError>; + if (_reason === undefined) { + error = new TransactionRevertedWithoutReasonError< + FormatType + >(transactionReceiptFormatted); + } else if (typeof _reason === 'string') { + error = new TransactionRevertInstructionError>( + _reason, + undefined, + transactionReceiptFormatted, + ); + } else if ( + (_reason as RevertReasonWithCustomError).customErrorName !== undefined && + (_reason as RevertReasonWithCustomError).customErrorDecodedSignature !== undefined && + (_reason as RevertReasonWithCustomError).customErrorArguments !== undefined + ) { + const reasonWithCustomError: RevertReasonWithCustomError = + _reason as RevertReasonWithCustomError; + error = new TransactionRevertWithCustomError>( + reasonWithCustomError.reason, + reasonWithCustomError.customErrorName, + reasonWithCustomError.customErrorDecodedSignature, + reasonWithCustomError.customErrorArguments, + reasonWithCustomError.signature, + transactionReceiptFormatted, + reasonWithCustomError.data, + ); + } else { + error = new TransactionRevertInstructionError>( + _reason.reason, + _reason.signature, + transactionReceiptFormatted, + _reason.data, + ); + } + + return error; +} diff --git a/packages/web3-eth/src/web3_eth.ts b/packages/web3-eth/src/web3_eth.ts index 6c0ecb7794f..8308ca60b52 100644 --- a/packages/web3-eth/src/web3_eth.ts +++ b/packages/web3-eth/src/web3_eth.ts @@ -1028,8 +1028,9 @@ export class Web3Eth extends Web3Context( transaction: Bytes, returnFormat: ReturnFormat = DEFAULT_RETURN_FORMAT as ReturnFormat, + options?: SendTransactionOptions, ) { - return rpcMethodsWrappers.sendSignedTransaction(this, transaction, returnFormat); + return rpcMethodsWrappers.sendSignedTransaction(this, transaction, returnFormat, options); } /** diff --git a/packages/web3-eth/test/integration/helper.ts b/packages/web3-eth/test/integration/helper.ts index c4ca69f1585..77243cc4826 100644 --- a/packages/web3-eth/test/integration/helper.ts +++ b/packages/web3-eth/test/integration/helper.ts @@ -47,11 +47,15 @@ export const sendFewTxes = async ({ const tx: Web3PromiEvent< TransactionReceipt, SendTransactionEvents - > = web3Eth.sendTransaction({ - to, - value, - from, - }); + > = web3Eth.sendTransaction( + { + to, + value, + from, + }, + DEFAULT_RETURN_FORMAT, + { checkRevertBeforeSending: false }, + ); res.push( // eslint-disable-next-line no-await-in-loop (await new Promise((resolve: Resolve, reject) => { diff --git a/packages/web3-eth/test/integration/web3_eth/send_signed_transaction.test.ts b/packages/web3-eth/test/integration/web3_eth/send_signed_transaction.test.ts index 894966391b9..685ec8842bf 100644 --- a/packages/web3-eth/test/integration/web3_eth/send_signed_transaction.test.ts +++ b/packages/web3-eth/test/integration/web3_eth/send_signed_transaction.test.ts @@ -15,16 +15,22 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ -import { Bytes, SignedTransactionInfoAPI, Transaction } from 'web3-types'; +// TODO Seems to be an issue with linter falsely reporting this +// error for Transaction Error Scenarios tests +/* eslint-disable jest/no-conditional-expect */ + +import { Address, Bytes, SignedTransactionInfoAPI, Transaction } from 'web3-types'; import { DEFAULT_RETURN_FORMAT, FMT_BYTES, FMT_NUMBER, format, hexToNumber } from 'web3-utils'; import { isHexStrict } from 'web3-validator'; import { Web3Eth, InternalTransaction, transactionSchema } from '../../../src'; import { closeOpenConnection, createTempAccount, + getSystemTestBackend, getSystemTestProvider, } from '../../fixtures/system_test_utils'; import { getTransactionGasPricing } from '../../../src/utils/get_transaction_gas_pricing'; +import { SimpleRevertAbi, SimpleRevertDeploymentData } from '../../fixtures/simple_revert'; const HEX_NUMBER_DATA_FORMAT = { bytes: FMT_BYTES.HEX, number: FMT_NUMBER.HEX } as const; @@ -293,4 +299,247 @@ describe('Web3Eth.sendSignedTransaction', () => { expect.assertions(1); }); }); + + describe('Transaction Error Scenarios', () => { + let simpleRevertContractAddress: Address; + + beforeAll(async () => { + const simpleRevertDeployTransaction: Transaction = { + from: tempAcc.address, + data: SimpleRevertDeploymentData, + }; + simpleRevertDeployTransaction.gas = await web3Eth.estimateGas( + simpleRevertDeployTransaction, + ); + simpleRevertContractAddress = ( + await web3Eth.sendTransaction(simpleRevertDeployTransaction) + ).contractAddress as Address; + }); + + it('Should throw TransactionRevertInstructionError because gas too low', async () => { + const transaction: Transaction = { + from: tempAcc.address, + to: '0x0000000000000000000000000000000000000000', + value: BigInt(1), + gas: 1, + gasPrice: 1, + nonce: await web3Eth.getTransactionCount(tempAcc.address), + }; + const signedTransaction = await web3Eth.signTransaction(transaction, { + number: FMT_NUMBER.BIGINT, + bytes: FMT_BYTES.BUFFER, + }); + + const expectedThrownError = { + name: 'TransactionRevertInstructionError', + innerError: undefined, + reason: + getSystemTestBackend() === 'geth' + ? expect.stringContaining( + 'err: max fee per gas less than block base fee: address 0x', + ) + : 'VM Exception while processing transaction: out of gas', + signature: undefined, + receipt: undefined, + data: undefined, + code: 402, + }; + + await expect( + web3Eth + .sendSignedTransaction(signedTransaction.raw) + .on('error', error => expect(error).toMatchObject(expectedThrownError)), + ).rejects.toMatchObject(expectedThrownError); + }); + + it('Should throw InvalidResponseError because insufficient funds', async () => { + const transaction: Transaction = { + from: tempAcc.address, + to: '0x0000000000000000000000000000000000000000', + value: BigInt('999999999999999999999999999999999999999999999999999999999'), + gas: 21000, + nonce: await web3Eth.getTransactionCount(tempAcc.address), + }; + transaction.gasPrice = await web3Eth.getGasPrice(); + const signedTransaction = await web3Eth.signTransaction(transaction, { + number: FMT_NUMBER.BIGINT, + bytes: FMT_BYTES.BUFFER, + }); + + const expectedThrownError = { + name: 'TransactionRevertInstructionError', + message: 'Transaction has been reverted by the EVM', + innerError: undefined, + reason: + getSystemTestBackend() === 'geth' + ? expect.stringContaining( + 'err: insufficient funds for gas * price + value: address 0x', + ) + : 'VM Exception while processing transaction: insufficient balance', + signature: undefined, + receipt: undefined, + data: undefined, + code: 402, + }; + + await expect( + web3Eth + .sendSignedTransaction(signedTransaction.raw) + .on('error', error => expect(error).toMatchObject(expectedThrownError)), + ).rejects.toMatchObject(expectedThrownError); + }); + + it('Should throw TransactionRevertInstructionError because of contract revert and return revert reason', async () => { + const transaction: Transaction = { + from: tempAcc.address, + to: simpleRevertContractAddress, + data: '0xba57a511000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000067265766572740000000000000000000000000000000000000000000000000000', + gasPrice: 2000000000, + gas: 23605, + nonce: await web3Eth.getTransactionCount(tempAcc.address), + }; + const signedTransaction = await web3Eth.signTransaction(transaction, { + number: FMT_NUMBER.BIGINT, + bytes: FMT_BYTES.BUFFER, + }); + + web3Eth.handleRevert = true; + + const expectedThrownError = { + name: 'TransactionRevertInstructionError', + code: 402, + reason: + getSystemTestBackend() === 'geth' + ? 'execution reverted: This is a send revert' + : 'VM Exception while processing transaction: revert This is a send revert', + signature: '0x08c379a0', + data: '000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000155468697320697320612073656e64207265766572740000000000000000000000', + receipt: undefined, + }; + + await expect( + web3Eth + .sendSignedTransaction(signedTransaction.raw) + .on('error', error => expect(error).toMatchObject(expectedThrownError)), + ).rejects.toMatchObject(expectedThrownError); + }); + + it('Should throw TransactionRevertWithCustomError because of contract revert and return custom error ErrorWithNoParams', async () => { + const transaction: Transaction = { + from: tempAcc.address, + to: simpleRevertContractAddress, + data: '0x3ebf4d9c', + gasPrice: 2000000000, + gas: 21222, + nonce: await web3Eth.getTransactionCount(tempAcc.address), + }; + const signedTransaction = await web3Eth.signTransaction(transaction, { + number: FMT_NUMBER.BIGINT, + bytes: FMT_BYTES.BUFFER, + }); + + web3Eth.handleRevert = true; + + const expectedThrownError = { + name: 'TransactionRevertWithCustomError', + code: 438, + reason: + getSystemTestBackend() === 'geth' + ? 'execution reverted' + : 'VM Exception while processing transaction: revert', + signature: '0x72090e4d', + customErrorName: 'ErrorWithNoParams', + customErrorDecodedSignature: 'ErrorWithNoParams()', + customErrorArguments: {}, + receipt: undefined, + }; + + await expect( + web3Eth + .sendSignedTransaction(signedTransaction.raw, undefined, { + contractAbi: SimpleRevertAbi, + }) + .on('error', error => expect(error).toMatchObject(expectedThrownError)), + ).rejects.toMatchObject(expectedThrownError); + }); + + it('Should throw TransactionRevertWithCustomError because of contract revert and return custom error ErrorWithParams', async () => { + const transaction: Transaction = { + from: tempAcc.address, + to: simpleRevertContractAddress, + data: '0x819f48fe', + gasPrice: 2000000000, + gas: 21730, + nonce: await web3Eth.getTransactionCount(tempAcc.address), + }; + const signedTransaction = await web3Eth.signTransaction(transaction, { + number: FMT_NUMBER.BIGINT, + bytes: FMT_BYTES.BUFFER, + }); + + web3Eth.handleRevert = true; + + const expectedThrownError = { + name: 'TransactionRevertWithCustomError', + code: 438, + reason: + getSystemTestBackend() === 'geth' + ? 'execution reverted' + : 'VM Exception while processing transaction: revert', + signature: '0xc85bda60', + data: '000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001c5468697320697320616e206572726f72207769746820706172616d7300000000', + customErrorName: 'ErrorWithParams', + customErrorDecodedSignature: 'ErrorWithParams(uint256,string)', + customErrorArguments: { + code: BigInt(42), + message: 'This is an error with params', + }, + receipt: undefined, + }; + + await expect( + web3Eth + .sendSignedTransaction(signedTransaction.raw, undefined, { + contractAbi: SimpleRevertAbi, + }) + .on('error', error => expect(error).toMatchObject(expectedThrownError)), + ).rejects.toMatchObject(expectedThrownError); + }); + + it('Should throw TransactionRevertInstructionError because of contract revert', async () => { + const transaction: Transaction = { + from: tempAcc.address, + to: simpleRevertContractAddress, + data: '0xba57a511000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000067265766572740000000000000000000000000000000000000000000000000000', + gasPrice: 2000000000, + gas: 23605, + nonce: await web3Eth.getTransactionCount(tempAcc.address), + }; + const signedTransaction = await web3Eth.signTransaction(transaction, { + number: FMT_NUMBER.BIGINT, + bytes: FMT_BYTES.BUFFER, + }); + + web3Eth.handleRevert = false; + + const expectedThrownError = { + name: 'TransactionRevertInstructionError', + innerError: undefined, + reason: + getSystemTestBackend() === 'geth' + ? 'execution reverted: This is a send revert' + : 'VM Exception while processing transaction: revert This is a send revert', + signature: '0x08c379a0', + receipt: undefined, + data: '000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000155468697320697320612073656e64207265766572740000000000000000000000', + code: 402, + }; + + await expect( + web3Eth + .sendSignedTransaction(signedTransaction.raw) + .on('error', error => expect(error).toMatchObject(expectedThrownError)), + ).rejects.toMatchObject(expectedThrownError); + }); + }); }); diff --git a/packages/web3-eth/test/integration/web3_eth/send_transaction.test.ts b/packages/web3-eth/test/integration/web3_eth/send_transaction.test.ts index c5d8d339ddd..896010aa2e5 100644 --- a/packages/web3-eth/test/integration/web3_eth/send_transaction.test.ts +++ b/packages/web3-eth/test/integration/web3_eth/send_transaction.test.ts @@ -15,11 +15,16 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ +// TODO Seems to be an issue with linter falsely reporting this +// error for Transaction Error Scenarios tests +/* eslint-disable jest/no-conditional-expect */ + import { Transaction, TransactionWithFromLocalWalletIndex, TransactionWithToLocalWalletIndex, TransactionWithFromAndToLocalWalletIndex, + Address, } from 'web3-types'; import { Wallet } from 'web3-eth-accounts'; import { isHexStrict } from 'web3-validator'; @@ -29,8 +34,10 @@ import { closeOpenConnection, createAccountProvider, createTempAccount, + getSystemTestBackend, getSystemTestProvider, } from '../../fixtures/system_test_utils'; +import { SimpleRevertAbi, SimpleRevertDeploymentData } from '../../fixtures/simple_revert'; describe('Web3Eth.sendTransaction', () => { let web3Eth: Web3Eth; @@ -355,4 +362,190 @@ describe('Web3Eth.sendTransaction', () => { // expect.assertions(1); }); }); + + describe('Transaction Error Scenarios', () => { + let simpleRevertContractAddress: Address; + + beforeAll(async () => { + const simpleRevertDeployTransaction: Transaction = { + from: tempAcc.address, + data: SimpleRevertDeploymentData, + }; + simpleRevertDeployTransaction.gas = await web3Eth.estimateGas( + simpleRevertDeployTransaction, + ); + simpleRevertContractAddress = ( + await web3Eth.sendTransaction(simpleRevertDeployTransaction) + ).contractAddress as Address; + }); + + it('Should throw TransactionRevertInstructionError because gas too low', async () => { + const transaction: Transaction = { + from: tempAcc.address, + to: '0x0000000000000000000000000000000000000000', + value: BigInt(1), + gas: 1, + }; + + const expectedThrownError = { + name: 'TransactionRevertInstructionError', + code: 402, + reason: + getSystemTestBackend() === 'geth' + ? 'err: intrinsic gas too low: have 1, want 21000 (supplied gas 1)' + : 'VM Exception while processing transaction: out of gas', + }; + + await expect( + web3Eth + .sendTransaction(transaction) + .on('error', error => expect(error).toMatchObject(expectedThrownError)), + ).rejects.toMatchObject(expectedThrownError); + }); + + it('Should throw TransactionRevertInstructionError because insufficient funds', async () => { + const transaction: Transaction = { + from: tempAcc.address, + to: '0x0000000000000000000000000000000000000000', + value: BigInt('999999999999999999999999999999999999999999999999999999999'), + }; + + const expectedThrownError = { + name: 'TransactionRevertInstructionError', + message: 'Transaction has been reverted by the EVM', + code: 402, + reason: + getSystemTestBackend() === 'geth' + ? expect.stringContaining( + 'err: insufficient funds for gas * price + value: address', + ) + : 'VM Exception while processing transaction: insufficient balance', + }; + + await expect( + web3Eth + .sendTransaction(transaction) + .on('error', error => expect(error).toMatchObject(expectedThrownError)), + ).rejects.toMatchObject(expectedThrownError); + }); + + it('Should throw TransactionRevertInstructionError because of contract revert and return revert reason', async () => { + const transaction: Transaction = { + from: tempAcc.address, + to: simpleRevertContractAddress, + data: '0xba57a511000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000067265766572740000000000000000000000000000000000000000000000000000', + }; + + web3Eth.handleRevert = true; + + const expectedThrownError = { + name: 'TransactionRevertInstructionError', + code: 402, + reason: + getSystemTestBackend() === 'geth' + ? 'execution reverted: This is a send revert' + : 'VM Exception while processing transaction: revert This is a send revert', + signature: '0x08c379a0', + data: '000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000155468697320697320612073656e64207265766572740000000000000000000000', + receipt: undefined, + }; + + await expect( + web3Eth + .sendTransaction(transaction) + .on('error', error => expect(error).toMatchObject(expectedThrownError)), + ).rejects.toMatchObject(expectedThrownError); + }); + + it('Should throw TransactionRevertWithCustomError because of contract revert and return custom error ErrorWithNoParams', async () => { + const transaction: Transaction = { + from: tempAcc.address, + to: simpleRevertContractAddress, + data: '0x3ebf4d9c', + }; + + web3Eth.handleRevert = true; + + const expectedThrownError = { + name: 'TransactionRevertWithCustomError', + code: 438, + reason: + getSystemTestBackend() === 'geth' + ? 'execution reverted' + : 'VM Exception while processing transaction: revert', + signature: '0x72090e4d', + customErrorName: 'ErrorWithNoParams', + customErrorDecodedSignature: 'ErrorWithNoParams()', + customErrorArguments: {}, + receipt: undefined, + }; + + await expect( + web3Eth + .sendTransaction(transaction, undefined, { contractAbi: SimpleRevertAbi }) + .on('error', error => expect(error).toMatchObject(expectedThrownError)), + ).rejects.toMatchObject(expectedThrownError); + }); + + it('Should throw TransactionRevertWithCustomError because of contract revert and return custom error ErrorWithParams', async () => { + const transaction: Transaction = { + from: tempAcc.address, + to: simpleRevertContractAddress, + data: '0x819f48fe', + }; + + web3Eth.handleRevert = true; + + const expectedThrownError = { + name: 'TransactionRevertWithCustomError', + code: 438, + reason: + getSystemTestBackend() === 'geth' + ? 'execution reverted' + : 'VM Exception while processing transaction: revert', + signature: '0xc85bda60', + data: '000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001c5468697320697320616e206572726f72207769746820706172616d7300000000', + customErrorName: 'ErrorWithParams', + customErrorDecodedSignature: 'ErrorWithParams(uint256,string)', + customErrorArguments: { + code: BigInt(42), + message: 'This is an error with params', + }, + receipt: undefined, + }; + + await expect( + web3Eth + .sendTransaction(transaction, undefined, { contractAbi: SimpleRevertAbi }) + .on('error', error => expect(error).toMatchObject(expectedThrownError)), + ).rejects.toMatchObject(expectedThrownError); + }); + + it('Should throw TransactionRevertInstructionError because of contract revert', async () => { + const transaction: Transaction = { + from: tempAcc.address, + to: simpleRevertContractAddress, + data: '0xba57a511000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000067265766572740000000000000000000000000000000000000000000000000000', + }; + + web3Eth.handleRevert = false; + + const expectedThrownError = { + name: 'TransactionRevertInstructionError', + code: 402, + reason: + getSystemTestBackend() === 'geth' + ? 'execution reverted: This is a send revert' + : 'VM Exception while processing transaction: revert This is a send revert', + signature: '0x08c379a0', + data: '000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000155468697320697320612073656e64207265766572740000000000000000000000', + }; + + await expect( + web3Eth + .sendTransaction(transaction) + .on('error', error => expect(error).toMatchObject(expectedThrownError)), + ).rejects.toMatchObject(expectedThrownError); + }); + }); }); 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 index 0a4656be696..c08cbea7efc 100644 --- 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 @@ -15,6 +15,7 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ import { Bytes, TransactionReceipt } from 'web3-types'; +import { hexToBytes } from 'web3-utils'; export const expectedTransactionHash = '0xe21194c9509beb01be7e90c2bcefff2804cd85836ae12134f22ad4acda0fc547'; @@ -23,8 +24,8 @@ export const expectedTransactionReceipt: TransactionReceipt = { transactionIndex: '0x41', blockHash: '0x1d59ff54b1eb26b013ce3cb5fc9dab3705b415a67127a003c3e61eb445bb8df2', blockNumber: '0x5daf3b', - from: '0xa7d9ddbe1f17865597fbd27ec712455208b6b76d', - to: '0xf02c1c8e6114b1dbe8937a39260b5b0a374432bb', + from: '0x7ed0e85b8e1e925600b4373e6d108f34ab38a401', + to: '0x0000000000000000000000000000000000000000', cumulativeGasUsed: '0x33bc', // 13244 effectiveGasPrice: '0x13a21bc946', // 84324108614 gasUsed: '0x4dc', // 1244 @@ -41,24 +42,10 @@ export const expectedTransactionReceipt: TransactionReceipt = { * - Input parameters: * - signedTransaction */ +const signedTransaction = + '0xf8650f8415aa14088252089400000000000000000000000000000000000000000180820a95a0e6d6bc9c7af306733eb44b2a8a4a4efed5db2fbff947e21521fe81dfb144a00aa01a8a87c872f59564abbbe60e9d4e54dee5e1f1647477ab170ecd7e2704d3c94d'; export const testData: [string, Bytes][] = [ - [ - 'signedTransaction = HexString', - '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675', - ], - [ - 'signedTransaction = Buffer', - Buffer.from( - '0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675', - ), - ], - [ - 'signedTransaction = Uint8Array', - new Uint8Array([ - 30, 78, 64, 34, 36, 65, 38, 64, 64, 36, 37, 63, 35, 64, 33, 32, 62, 65, 38, 64, 34, 36, - 65, 38, 64, 64, 36, 37, 63, 35, 64, 33, 32, 62, 65, 38, 30, 35, 38, 62, 62, 38, 65, 62, - 39, 37, 30, 38, 37, 30, 66, 30, 37, 32, 34, 34, 35, 36, 37, 35, 30, 35, 38, 62, 62, 38, - 65, 62, 39, 37, 30, 38, 37, 30, 66, 30, 37, 32, 34, 34, 35, 36, 37, 35, - ]), - ], + ['signedTransaction = HexString', signedTransaction], + ['signedTransaction = Buffer', hexToBytes(signedTransaction)], + ['signedTransaction = Uint8Array', new Uint8Array(hexToBytes(signedTransaction))], ]; diff --git a/packages/web3-eth/test/unit/utils/get_revert_reason.test.ts b/packages/web3-eth/test/unit/utils/get_revert_reason.test.ts new file mode 100644 index 00000000000..7ae0b6493b0 --- /dev/null +++ b/packages/web3-eth/test/unit/utils/get_revert_reason.test.ts @@ -0,0 +1,125 @@ +/* +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 } from 'web3-core'; +import { DEFAULT_RETURN_FORMAT } from 'web3-utils'; + +import * as RpcMethodWrappers from '../../../src/rpc_method_wrappers'; +import * as GetRevertReason from '../../../src/utils/get_revert_reason'; +import { SimpleRevertAbi } from '../../fixtures/simple_revert'; + +describe('getRevertReason', () => { + const web3Context = new Web3Context(); + + it('should use the call rpc wrapper', async () => { + const callSpy = jest.spyOn(RpcMethodWrappers, 'call').mockImplementation(); + + const transaction = { + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x36361143b7e2c676f8ccd67743a89d26437f0529', + data: '0x819f48fe', + gasPrice: '0x15ab8f14', + maxPriorityFeePerGas: undefined, + maxFeePerGas: undefined, + }; + + await GetRevertReason.getRevertReason(web3Context, transaction); + + expect(callSpy).toHaveBeenCalledWith( + web3Context, + transaction, + web3Context.defaultBlock, + DEFAULT_RETURN_FORMAT, + ); + }); + + it('should return undefined', async () => { + jest.spyOn(RpcMethodWrappers, 'call').mockResolvedValueOnce( + '0x000000000000000000000000000000000000000000000000000000000000000a', + ); + + const transaction = { + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x36361143b7e2c676f8ccd67743a89d26437f0529', + data: '0x819f48fe', + gasPrice: '0x15ab8f14', + maxPriorityFeePerGas: undefined, + maxFeePerGas: undefined, + }; + + const result = await GetRevertReason.getRevertReason(web3Context, transaction); + + expect(result).toBeUndefined(); + }); + + it('should call parseTransactionError without contractAbi', async () => { + const expectedError = { + jsonrpc: '2.0', + id: 1, + error: { + code: -32000, + message: + 'err: insufficient funds for gas * price + value: address 0x0000000000000000000000000000000000000000 have 66 want 9983799287684 (supplied gas 26827)', + }, + }; + const parseTransactionErrorSpy = jest + .spyOn(GetRevertReason, 'parseTransactionError') + .mockImplementation(); + jest.spyOn(RpcMethodWrappers, 'call').mockRejectedValueOnce(expectedError); + + const transaction = { + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x36361143b7e2c676f8ccd67743a89d26437f0529', + data: '0x819f48fe', + gasPrice: '0x15ab8f14', + maxPriorityFeePerGas: undefined, + maxFeePerGas: undefined, + }; + + await GetRevertReason.getRevertReason(web3Context, transaction); + + expect(parseTransactionErrorSpy).toHaveBeenCalledWith(expectedError, undefined); + }); + + it('should call parseTransactionError with contractAbi', async () => { + const expectedError = { + jsonrpc: '2.0', + id: 1, + error: { + code: -32000, + message: + 'err: insufficient funds for gas * price + value: address 0x0000000000000000000000000000000000000000 have 66 want 9983799287684 (supplied gas 26827)', + }, + }; + const parseTransactionErrorSpy = jest + .spyOn(GetRevertReason, 'parseTransactionError') + .mockImplementation(); + jest.spyOn(RpcMethodWrappers, 'call').mockRejectedValueOnce(expectedError); + + const transaction = { + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x36361143b7e2c676f8ccd67743a89d26437f0529', + data: '0x819f48fe', + gasPrice: '0x15ab8f14', + maxPriorityFeePerGas: undefined, + maxFeePerGas: undefined, + }; + + await GetRevertReason.getRevertReason(web3Context, transaction, SimpleRevertAbi); + + expect(parseTransactionErrorSpy).toHaveBeenCalledWith(expectedError, SimpleRevertAbi); + }); +}); diff --git a/packages/web3-eth/test/unit/utils/get_transaction_error.test.ts b/packages/web3-eth/test/unit/utils/get_transaction_error.test.ts new file mode 100644 index 00000000000..898947f4889 --- /dev/null +++ b/packages/web3-eth/test/unit/utils/get_transaction_error.test.ts @@ -0,0 +1,446 @@ +/* +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 } from 'web3-core'; +import { + InvalidResponseError, + TransactionRevertedWithoutReasonError, + TransactionRevertInstructionError, + TransactionRevertWithCustomError, +} from 'web3-errors'; + +import * as GetRevertReasonUtils from '../../../src/utils/get_revert_reason'; +import { getTransactionError } from '../../../src/utils/get_transaction_error'; +import { SimpleRevertAbi } from '../../fixtures/simple_revert'; + +describe('getTransactionError', () => { + let web3Context: Web3Context; + + beforeEach(() => { + web3Context = new Web3Context('http://127.0.0.1:8545'); + }); + + it('should call parseTransactionError to get error from receivedError', async () => { + const parseTransactionErrorSpy = jest.spyOn(GetRevertReasonUtils, 'parseTransactionError'); + + const receivedError = new InvalidResponseError( + { + jsonrpc: '2.0', + id: '3f839900-afdd-4553-bca7-b4e2b835c687', + error: { code: -32000, message: 'intrinsic gas too low' }, + }, + { + jsonrpc: '2.0', + id: '2568856d-8ee5-43f4-a8db-dbd22cf97a53', + method: 'eth_sendTransaction', + params: [ + { + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x0000000000000000000000000000000000000000', + value: '0x1', + gas: '0x1', + gasPrice: '0x15b61074', + maxPriorityFeePerGas: undefined, + maxFeePerGas: undefined, + }, + ], + }, + ); + await getTransactionError(web3Context, undefined, undefined, receivedError); + expect(parseTransactionErrorSpy).toHaveBeenCalledWith(receivedError); + }); + + it('should call getRevertReason to get error from transactionFormatted without contractAbi', async () => { + const getRevertReasonSpy = jest + .spyOn(GetRevertReasonUtils, 'getRevertReason') + .mockImplementation(); + + const transaction = { + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x36361143b7e2c676f8ccd67743a89d26437f0529', + data: '0x819f48fe', + gasPrice: '0x15ab8f14', + maxPriorityFeePerGas: undefined, + maxFeePerGas: undefined, + }; + + web3Context.handleRevert = true; + await getTransactionError(web3Context, transaction); + expect(getRevertReasonSpy).toHaveBeenCalledWith(web3Context, transaction, undefined); + }); + + it('should call getRevertReason to get error from transactionFormatted with contractAbi', async () => { + const getRevertReasonSpy = jest + .spyOn(GetRevertReasonUtils, 'getRevertReason') + .mockImplementation(); + + const transaction = { + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x36361143b7e2c676f8ccd67743a89d26437f0529', + data: '0x819f48fe', + gasPrice: '0x15ab8f14', + maxPriorityFeePerGas: undefined, + maxFeePerGas: undefined, + }; + + web3Context.handleRevert = true; + await getTransactionError(web3Context, transaction, undefined, undefined, SimpleRevertAbi); + expect(getRevertReasonSpy).toHaveBeenCalledWith(web3Context, transaction, SimpleRevertAbi); + }); + + describe('TransactionRevertedWithoutReasonError', () => { + it('should throw TransactionRevertedWithoutReasonError without receipt', async () => { + const transaction = { + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x36361143b7e2c676f8ccd67743a89d26437f0529', + data: '0x819f48fe', + gasPrice: '0x15ab8f14', + maxPriorityFeePerGas: undefined, + maxFeePerGas: undefined, + }; + + expect(await getTransactionError(web3Context, transaction)).toMatchObject( + new TransactionRevertedWithoutReasonError(), + ); + }); + + it('should throw TransactionRevertedWithoutReasonError with receipt', async () => { + const transaction = { + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x36361143b7e2c676f8ccd67743a89d26437f0529', + data: '0x819f48fe', + gasPrice: '0x15ab8f14', + maxPriorityFeePerGas: undefined, + maxFeePerGas: undefined, + }; + const receipt = { + transactionHash: + '0x55de60905fb9efdaa5dc5ac6a2e05736e92067d44b8a3077c80ec849545cbcf0', + transactionIndex: BigInt(0), + blockHash: '0xc150c0a7f7f5c9014ea965d19b1be5f5ced07a6b17ea3b1126769d745dde9b2d', + blockNumber: BigInt(16738176), + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x36361143b7e2c676f8ccd67743a89d26437f0529', + cumulativeGasUsed: BigInt(23605), + gasUsed: BigInt(23605), + effectiveGasPrice: BigInt(2000000000), + logs: [], + logsBloom: + '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + root: '', + status: BigInt(0), + type: BigInt(0), + }; + + expect(await getTransactionError(web3Context, transaction, receipt)).toMatchObject( + new TransactionRevertedWithoutReasonError(receipt), + ); + }); + }); + + describe('TransactionRevertInstructionError', () => { + it('should throw TransactionRevertInstructionError without transaction and receipt', async () => { + const receivedError = new InvalidResponseError( + { + jsonrpc: '2.0', + id: '3f839900-afdd-4553-bca7-b4e2b835c687', + error: { code: -32000, message: 'intrinsic gas too low' }, + }, + { + jsonrpc: '2.0', + id: '2568856d-8ee5-43f4-a8db-dbd22cf97a53', + method: 'eth_sendTransaction', + params: [ + { + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x0000000000000000000000000000000000000000', + value: '0x1', + gas: '0x1', + gasPrice: '0x15b61074', + maxPriorityFeePerGas: undefined, + maxFeePerGas: undefined, + }, + ], + }, + ); + + expect( + await getTransactionError(web3Context, undefined, undefined, receivedError), + ).toMatchObject(new TransactionRevertInstructionError('intrinsic gas too low')); + }); + + it('should throw TransactionRevertInstructionError without transaction and with receipt', async () => { + const transaction = { + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x36361143b7e2c676f8ccd67743a89d26437f0529', + data: '0x819f48fe', + gasPrice: '0x15ab8f14', + maxPriorityFeePerGas: undefined, + maxFeePerGas: undefined, + }; + const receipt = { + transactionHash: + '0x55de60905fb9efdaa5dc5ac6a2e05736e92067d44b8a3077c80ec849545cbcf0', + transactionIndex: BigInt(0), + blockHash: '0xc150c0a7f7f5c9014ea965d19b1be5f5ced07a6b17ea3b1126769d745dde9b2d', + blockNumber: BigInt(16738176), + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x36361143b7e2c676f8ccd67743a89d26437f0529', + cumulativeGasUsed: BigInt(23605), + gasUsed: BigInt(23605), + effectiveGasPrice: BigInt(2000000000), + logs: [], + logsBloom: + '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + root: '', + status: BigInt(0), + type: BigInt(0), + }; + const receivedError = new InvalidResponseError( + { + jsonrpc: '2.0', + id: '3f839900-afdd-4553-bca7-b4e2b835c687', + error: { code: -32000, message: 'intrinsic gas too low' }, + }, + { + jsonrpc: '2.0', + id: '2568856d-8ee5-43f4-a8db-dbd22cf97a53', + method: 'eth_sendTransaction', + params: [ + { + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x0000000000000000000000000000000000000000', + value: '0x1', + gas: '0x1', + gasPrice: '0x15b61074', + maxPriorityFeePerGas: undefined, + maxFeePerGas: undefined, + }, + ], + }, + ); + + expect( + await getTransactionError(web3Context, transaction, receipt, receivedError), + ).toMatchObject( + new TransactionRevertInstructionError('intrinsic gas too low', undefined, receipt), + ); + }); + + it('should throw TransactionRevertInstructionError without receipt', async () => { + jest.spyOn(GetRevertReasonUtils, 'getRevertReason').mockResolvedValueOnce({ + reason: 'execution reverted', + signature: '0xc85bda60', + data: '000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001c5468697320697320616e206572726f72207769746820706172616d7300000000', + }); + + const transaction = { + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x36361143b7e2c676f8ccd67743a89d26437f0529', + data: '0x819f48fe', + gasPrice: '0x15ab8f14', + maxPriorityFeePerGas: undefined, + maxFeePerGas: undefined, + }; + + web3Context.handleRevert = true; + expect( + await getTransactionError( + web3Context, + transaction, + undefined, + undefined, + SimpleRevertAbi, + ), + ).toMatchObject( + new TransactionRevertInstructionError( + 'execution reverted', + '0xc85bda60', + undefined, + '000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001c5468697320697320616e206572726f72207769746820706172616d7300000000', + ), + ); + }); + + it('should throw TransactionRevertInstructionError with receipt', async () => { + jest.spyOn(GetRevertReasonUtils, 'getRevertReason').mockResolvedValueOnce({ + reason: 'execution reverted', + signature: '0xc85bda60', + data: '000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001c5468697320697320616e206572726f72207769746820706172616d7300000000', + }); + + const transaction = { + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x36361143b7e2c676f8ccd67743a89d26437f0529', + data: '0x819f48fe', + gasPrice: '0x15ab8f14', + maxPriorityFeePerGas: undefined, + maxFeePerGas: undefined, + }; + const receipt = { + transactionHash: + '0x55de60905fb9efdaa5dc5ac6a2e05736e92067d44b8a3077c80ec849545cbcf0', + transactionIndex: BigInt(0), + blockHash: '0xc150c0a7f7f5c9014ea965d19b1be5f5ced07a6b17ea3b1126769d745dde9b2d', + blockNumber: BigInt(16738176), + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x36361143b7e2c676f8ccd67743a89d26437f0529', + cumulativeGasUsed: BigInt(23605), + gasUsed: BigInt(23605), + effectiveGasPrice: BigInt(2000000000), + logs: [], + logsBloom: + '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + root: '', + status: BigInt(0), + type: BigInt(0), + }; + + web3Context.handleRevert = true; + expect( + await getTransactionError( + web3Context, + transaction, + receipt, + undefined, + SimpleRevertAbi, + ), + ).toMatchObject( + new TransactionRevertInstructionError( + 'execution reverted', + '0xc85bda60', + receipt, + '000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001c5468697320697320616e206572726f72207769746820706172616d7300000000', + ), + ); + }); + }); + + describe('TransactionRevertWithCustomError', () => { + it('should throw TransactionRevertWithCustomError without receipt', async () => { + jest.spyOn(GetRevertReasonUtils, 'getRevertReason').mockResolvedValueOnce({ + reason: 'execution reverted', + signature: '0xc85bda60', + data: '000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001c5468697320697320616e206572726f72207769746820706172616d7300000000', + customErrorName: 'ErrorWithParams', + customErrorDecodedSignature: 'ErrorWithParams(uint256,string)', + customErrorArguments: { + code: BigInt(42), + message: 'This is an error with params', + }, + }); + + const transaction = { + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x36361143b7e2c676f8ccd67743a89d26437f0529', + data: '0x819f48fe', + gasPrice: '0x15ab8f14', + maxPriorityFeePerGas: undefined, + maxFeePerGas: undefined, + }; + + web3Context.handleRevert = true; + expect( + await getTransactionError( + web3Context, + transaction, + undefined, + undefined, + SimpleRevertAbi, + ), + ).toMatchObject( + new TransactionRevertWithCustomError( + 'execution reverted', + 'ErrorWithParams', + 'ErrorWithParams(uint256,string)', + { + code: BigInt(42), + message: 'This is an error with params', + }, + '0xc85bda60', + undefined, + '000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001c5468697320697320616e206572726f72207769746820706172616d7300000000', + ), + ); + }); + + it('should throw TransactionRevertWithCustomError with receipt', async () => { + jest.spyOn(GetRevertReasonUtils, 'getRevertReason').mockResolvedValueOnce({ + reason: 'execution reverted', + signature: '0xc85bda60', + data: '000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001c5468697320697320616e206572726f72207769746820706172616d7300000000', + customErrorName: 'ErrorWithParams', + customErrorDecodedSignature: 'ErrorWithParams(uint256,string)', + customErrorArguments: { + code: BigInt(42), + message: 'This is an error with params', + }, + }); + + const transaction = { + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x36361143b7e2c676f8ccd67743a89d26437f0529', + data: '0x819f48fe', + gasPrice: '0x15ab8f14', + maxPriorityFeePerGas: undefined, + maxFeePerGas: undefined, + }; + const receipt = { + transactionHash: + '0x55de60905fb9efdaa5dc5ac6a2e05736e92067d44b8a3077c80ec849545cbcf0', + transactionIndex: BigInt(0), + blockHash: '0xc150c0a7f7f5c9014ea965d19b1be5f5ced07a6b17ea3b1126769d745dde9b2d', + blockNumber: BigInt(16738176), + from: '0x4fec0a51024b13030d26e70904b066c6d41157a5', + to: '0x36361143b7e2c676f8ccd67743a89d26437f0529', + cumulativeGasUsed: BigInt(23605), + gasUsed: BigInt(23605), + effectiveGasPrice: BigInt(2000000000), + logs: [], + logsBloom: + '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + root: '', + status: BigInt(0), + type: BigInt(0), + }; + + web3Context.handleRevert = true; + expect( + await getTransactionError( + web3Context, + transaction, + receipt, + undefined, + SimpleRevertAbi, + ), + ).toMatchObject( + new TransactionRevertWithCustomError( + 'execution reverted', + 'ErrorWithParams', + 'ErrorWithParams(uint256,string)', + { + code: BigInt(42), + message: 'This is an error with params', + }, + '0xc85bda60', + receipt, + '000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001c5468697320697320616e206572726f72207769746820706172616d7300000000', + ), + ); + }); + }); +}); diff --git a/packages/web3-eth/test/unit/utils/parse_transaction_error.test.ts b/packages/web3-eth/test/unit/utils/parse_transaction_error.test.ts new file mode 100644 index 00000000000..e76baa54cf0 --- /dev/null +++ b/packages/web3-eth/test/unit/utils/parse_transaction_error.test.ts @@ -0,0 +1,89 @@ +/* +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 { ContractExecutionError, InvalidResponseError } from 'web3-errors'; + +import { parseTransactionError } from '../../../src/utils/get_revert_reason'; +import { SimpleRevertAbi } from '../../fixtures/simple_revert'; + +describe('parseTransactionError', () => { + it('should return object of type RevertReason', () => { + const error = new ContractExecutionError({ + code: 3, + message: 'execution reverted: This is a send revert', + data: '0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000155468697320697320612073656e64207265766572740000000000000000000000', + }); + + const result = parseTransactionError(error); + expect(result).toStrictEqual({ + reason: 'execution reverted: This is a send revert', + signature: '0x08c379a0', + data: '000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000155468697320697320612073656e64207265766572740000000000000000000000', + }); + }); + + it('should return object of type RevertReasonWithCustomError', () => { + const error = new ContractExecutionError({ + code: 3, + message: 'execution reverted', + data: '0xc85bda60000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001c5468697320697320616e206572726f72207769746820706172616d7300000000', + }); + + const result = parseTransactionError(error, SimpleRevertAbi); + expect(result).toStrictEqual({ + reason: 'execution reverted', + signature: '0xc85bda60', + data: '000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001c5468697320697320616e206572726f72207769746820706172616d7300000000', + customErrorName: 'ErrorWithParams', + customErrorDecodedSignature: 'ErrorWithParams(uint256,string)', + customErrorArguments: { + '0': BigInt(42), + '1': 'This is an error with params', + __length__: 2, + code: BigInt(42), + message: 'This is an error with params', + }, + }); + }); + + it('should return object of type string', () => { + const error = new InvalidResponseError({ + jsonrpc: '2.0', + id: '3f839900-afdd-4553-bca7-b4e2b835c687', + error: { code: -32000, message: 'intrinsic gas too low' }, + }); + + const result = parseTransactionError(error); + expect(result).toBe('intrinsic gas too low'); + }); + + it('should throw an error', () => { + const error = new InvalidResponseError([ + { + jsonrpc: '2.0', + id: '3f839900-afdd-4553-bca7-b4e2b835c687', + error: { code: -32000, message: 'intrinsic gas too low' }, + }, + { + jsonrpc: '2.0', + id: '3f839900-afdd-4553-bca7-b4e2b835c687', + error: { code: -32000, message: 'intrinsic gas too low' }, + }, + ]); + + expect(() => parseTransactionError(error)).toThrowError(error); + }); +}); diff --git a/packages/web3/test/integration/handle_revert.test.ts b/packages/web3/test/integration/handle_revert.test.ts index a0f65c500cc..db5fd91afdb 100644 --- a/packages/web3/test/integration/handle_revert.test.ts +++ b/packages/web3/test/integration/handle_revert.test.ts @@ -16,7 +16,7 @@ along with web3.js. If not, see . */ import WebSocketProvider from 'web3-providers-ws'; import { Contract } from 'web3-eth-contract'; -import { TransactionRevertError } from 'web3-errors'; +import { TransactionRevertInstructionError } from 'web3-errors'; import Web3 from '../../src/index'; import { closeOpenConnection, @@ -89,7 +89,7 @@ describe.skip('eth', () => { it('should get revert reason', async () => { contract.handleRevert = true; await expect(contract.methods.reverts().send({ from: accounts[0] })).rejects.toThrow( - new TransactionRevertError( + new TransactionRevertInstructionError( 'Returned error: execution reverted: REVERTED WITH REVERT', ), ); @@ -112,7 +112,7 @@ describe.skip('eth', () => { s: '0x39f77e0b68d5524826e4385ad4e1f01e748f32c177840184ae65d9592fdfe5c', }), ).rejects.toThrow( - new TransactionRevertError( + new TransactionRevertInstructionError( 'Returned error: invalid argument 0: json: cannot unmarshal invalid hex string into Go struct field TransactionArgs.data of type hexutil.Bytes', ), ); diff --git a/scripts/system_tests_utils.ts b/scripts/system_tests_utils.ts index 3debb639814..a1da5816e67 100644 --- a/scripts/system_tests_utils.ts +++ b/scripts/system_tests_utils.ts @@ -37,7 +37,6 @@ import { Bytes, Web3BaseProvider, Transaction, - Receipt, KeyStore, ProviderConnectInfo, Web3ProviderEventCallback, @@ -335,7 +334,7 @@ export const signTxAndSendEIP1559 = async ( provider: unknown, tx: Record, privateKey: string, -): Promise => { +) => { const web3 = new Web3(provider as Web3BaseProvider); const acc = web3.eth.accounts.privateKeyToAccount(privateKey); const signedTx = await acc.signTransaction({ @@ -344,14 +343,16 @@ export const signTxAndSendEIP1559 = async ( gas: tx.gas ?? '1000000', from: acc.address, }); - return web3.eth.sendSignedTransaction(signedTx.rawTransaction); + return web3.eth.sendSignedTransaction(signedTx.rawTransaction, undefined, { + checkRevertBeforeSending: false, + }); }; export const signTxAndSendEIP2930 = async ( provider: unknown, tx: Record, privateKey: string, -): Promise => { +) => { const web3 = new Web3(provider as Web3BaseProvider); const acc = web3.eth.accounts.privateKeyToAccount(privateKey); const signedTx = await acc.signTransaction({ @@ -360,7 +361,9 @@ export const signTxAndSendEIP2930 = async ( gas: tx.gas ?? '1000000', from: acc.address, }); - return web3.eth.sendSignedTransaction(signedTx.rawTransaction); + return web3.eth.sendSignedTransaction(signedTx.rawTransaction, undefined, { + checkRevertBeforeSending: false, + }); }; export const signAndSendContractMethodEIP1559 = async (