From 70d1957e0a8642d1e5dba47c39ea65504689b6ec Mon Sep 17 00:00:00 2001 From: Oleksii Kosynskyi Date: Mon, 16 Oct 2023 21:06:11 -0400 Subject: [PATCH] Add functionality to extend tx types (#6493) * draft * finish * lint * add documentation * fix unit tests * add unit tests * add unit tests * increase coverage * increase coverage * fix changelog syncing * fix test --- CHANGELOG.md | 42 +- .../web3_plugin_guide/plugin_authors.md | 25 + packages/web3-eth-accounts/CHANGELOG.md | 2 + .../src/tx/baseTransaction.ts | 29 +- packages/web3-eth-accounts/src/tx/index.ts | 2 + .../src/tx/transactionFactory.ts | 34 +- .../test/unit/account.test.ts | 12 +- .../test/unit/common/utils.test.ts | 12 +- .../test/unit/tx/registerNewTx.test.ts | 49 ++ .../test/unit/tx/staticMethods.test.ts | 24 + .../test/unit/tx/transactionFactory.test.ts | 7 + .../src/utils/get_transaction_gas_pricing.ts | 2 +- .../unit/default_transaction_builder.test.ts | 2 +- packages/web3-validator/CHANGELOG.md | 2 +- packages/web3/package.json | 1 + .../web3/test/fixtures/tx-type-15/index.ts | 446 ++++++++++++++++++ .../integration/web3-plugin-add-tx.test.ts | 72 +++ yarn.lock | 44 ++ 18 files changed, 781 insertions(+), 26 deletions(-) create mode 100644 packages/web3-eth-accounts/test/unit/tx/registerNewTx.test.ts create mode 100644 packages/web3-eth-accounts/test/unit/tx/staticMethods.test.ts create mode 100644 packages/web3/test/fixtures/tx-type-15/index.ts create mode 100644 packages/web3/test/integration/web3-plugin-add-tx.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bd3230f5c6..a58e999f9a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2089,19 +2089,59 @@ If there are any bugs, improvements, optimizations or any new feature proposal f - Added `ALL_EVENTS` and `ALL_EVENTS_ABI` constants, `SendTransactionEventsBase` type, `decodeEventABI` method (#6410) +#### web3-eth-accounts + +- Added public function `privateKeyToPublicKey` +- Added exporting `BaseTransaction` from the package (#6493) +- Added exporting `txUtils` from the package (#6493) + #### web3-types - Interface `EventLog` was added. (#6410) +#### web3-utils + +- As a replacment of the node EventEmitter, a custom `EventEmitter` has been implemented and exported. (#6398) + ### Fixed +#### web3-core + +- Fix the issue: "Uncaught TypeError: Class extends value undefined is not a constructor or null #6371". (#6398) + #### web3-eth - Ensure provider.supportsSubscriptions exists before watching by subscription (#6440) -- Fixed `withdrawalsSchema.address` property type `bytes32` to `address` (#6470) +- Fixed param sent to `checkRevertBeforeSending` in `sendSignedTransaction` + +#### web3-eth-accounts + +- Fixed `recover` function, `v` will be normalized to value 0,1 (#6344) + +#### web3-providers-http + +- Fix issue lquixada/cross-fetch#78, enabling to run web3.js in service worker (#6463) + +#### web3-validator + +- Multi-dimensional arrays are now handled properly when parsing ABIs ### Changed +#### web3-core + +- defaultTransactionType is now type 0x2 instead of 0x0 (#6282) +- Allows formatter to parse large base fee (#6456) +- The package now uses `EventEmitter` from `web3-utils` that works in node envrioment as well as in the browser. (#6398) + +#### web3-eth + +- Transactions will now default to type 2 transactions instead of type 0, similar to 1.x version. (#6282) + #### web3-eth-contract - The `events` property was added to the `receipt` object (#6410) + +#### web3-providers-http + +- Bump cross-fetch to version 4 (#6463). diff --git a/docs/docs/guides/web3_plugin_guide/plugin_authors.md b/docs/docs/guides/web3_plugin_guide/plugin_authors.md index f7f057edd07..31f26d6ce3f 100644 --- a/docs/docs/guides/web3_plugin_guide/plugin_authors.md +++ b/docs/docs/guides/web3_plugin_guide/plugin_authors.md @@ -34,6 +34,31 @@ It is important to note that the plugin name should be structured as `@. import { Numbers } from 'web3-types'; import { bytesToHex } from 'web3-utils'; import { MAX_INTEGER, MAX_UINT64, SECP256K1_ORDER_DIV_2, secp256k1 } from './constants.js'; -import { - Chain, - Common, - Hardfork, - toUint8Array, - uint8ArrayToBigInt, - unpadUint8Array, -} from '../common/index.js'; +import { toUint8Array, uint8ArrayToBigInt, unpadUint8Array } from '../common/utils.js'; +import { Common } from '../common/common.js'; +import { Hardfork, Chain } from '../common/enums.js'; import type { AccessListEIP2930TxData, AccessListEIP2930ValuesArray, @@ -565,4 +560,22 @@ export abstract class BaseTransaction { return { r, s, v }; } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public static fromSerializedTx( + // @ts-expect-error unused variable + serialized: Uint8Array, + // @ts-expect-error unused variable + opts: TxOptions = {}, + // eslint-disable-next-line @typescript-eslint/no-empty-function + ): any {} + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public static fromTxData( + // @ts-expect-error unused variable + txData: any, + // @ts-expect-error unused variable + opts: TxOptions = {}, + // eslint-disable-next-line @typescript-eslint/no-empty-function + ): any {} } diff --git a/packages/web3-eth-accounts/src/tx/index.ts b/packages/web3-eth-accounts/src/tx/index.ts index f02f90cfdbc..26ead20a897 100644 --- a/packages/web3-eth-accounts/src/tx/index.ts +++ b/packages/web3-eth-accounts/src/tx/index.ts @@ -20,4 +20,6 @@ export { FeeMarketEIP1559Transaction } from './eip1559Transaction.js'; export { AccessListEIP2930Transaction } from './eip2930Transaction.js'; export { Transaction } from './legacyTransaction.js'; export { TransactionFactory } from './transactionFactory.js'; +export { BaseTransaction } from './baseTransaction.js'; +export * as txUtils from './utils.js'; export * from './types.js'; diff --git a/packages/web3-eth-accounts/src/tx/transactionFactory.ts b/packages/web3-eth-accounts/src/tx/transactionFactory.ts index dd74bee4eee..e4da9fcfe6e 100644 --- a/packages/web3-eth-accounts/src/tx/transactionFactory.ts +++ b/packages/web3-eth-accounts/src/tx/transactionFactory.ts @@ -14,6 +14,7 @@ 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 { Numbers } from 'web3-types'; import { toUint8Array, uint8ArrayToBigInt } from '../common/utils.js'; import { FeeMarketEIP1559Transaction } from './eip1559Transaction.js'; import { AccessListEIP2930Transaction } from './eip2930Transaction.js'; @@ -26,6 +27,9 @@ import type { TxData, TxOptions, } from './types.js'; +import { BaseTransaction } from './baseTransaction.js'; + +const extraTxTypes: Map> = new Map(); // eslint-disable-next-line @typescript-eslint/no-extraneous-class export class TransactionFactory { @@ -33,6 +37,18 @@ export class TransactionFactory { // eslint-disable-next-line @typescript-eslint/no-empty-function, no-useless-constructor private constructor() {} + public static typeToInt(txType: Numbers) { + return Number(uint8ArrayToBigInt(toUint8Array(txType))); + } + + public static registerTransactionType>( + type: Numbers, + txClass: NewTxTypeClass, + ) { + const txType = TransactionFactory.typeToInt(type); + extraTxTypes.set(txType, txClass); + } + /** * Create a transaction from a `txData` object * @@ -47,7 +63,7 @@ export class TransactionFactory { // Assume legacy transaction return Transaction.fromTxData(txData as TxData, txOptions); } - const txType = Number(uint8ArrayToBigInt(toUint8Array(txData.type))); + const txType = TransactionFactory.typeToInt(txData.type); if (txType === 0) { return Transaction.fromTxData(txData as TxData, txOptions); } @@ -66,6 +82,11 @@ export class TransactionFactory { txOptions, ); } + const ExtraTransaction = extraTxTypes.get(txType); + if (ExtraTransaction?.fromTxData) { + return ExtraTransaction.fromTxData(txData, txOptions) as TypedTransaction; + } + throw new Error(`Tx instantiation with type ${txType} not supported`); } @@ -86,8 +107,17 @@ export class TransactionFactory { return AccessListEIP2930Transaction.fromSerializedTx(data, txOptions); case 2: return FeeMarketEIP1559Transaction.fromSerializedTx(data, txOptions); - default: + default: { + const ExtraTransaction = extraTxTypes.get(Number(data[0])); + if (ExtraTransaction?.fromSerializedTx) { + return ExtraTransaction.fromSerializedTx( + data, + txOptions, + ) as TypedTransaction; + } + throw new Error(`TypedTransaction with ID ${data[0]} unknown`); + } } } else { return Transaction.fromSerializedTx(data, txOptions); diff --git a/packages/web3-eth-accounts/test/unit/account.test.ts b/packages/web3-eth-accounts/test/unit/account.test.ts index d49689a38c5..713ac9e4829 100644 --- a/packages/web3-eth-accounts/test/unit/account.test.ts +++ b/packages/web3-eth-accounts/test/unit/account.test.ts @@ -28,7 +28,7 @@ import { recoverTransaction, sign, signTransaction, - privateKeyToPublicKey + privateKeyToPublicKey, } from '../../src/account'; import { invalidDecryptData, @@ -98,8 +98,8 @@ describe('accounts', () => { it.each(validPrivateKeyToPublicKeyData)('%s', (privateKey, isCompressed, output) => { expect(privateKeyToPublicKey(privateKey, isCompressed)).toEqual(output); }); - }) - }) + }); + }); describe('Signing and Recovery of Transaction', () => { it.each(transactionsTestData)('sign transaction', async txData => { @@ -228,8 +228,8 @@ describe('accounts', () => { describe('valid signatures for recover', () => { it.each(validRecover)('&s', (data, signature) => { - recover(data, signature) - }) - }) + recover(data, signature); + }); + }); }); }); diff --git a/packages/web3-eth-accounts/test/unit/common/utils.test.ts b/packages/web3-eth-accounts/test/unit/common/utils.test.ts index b3ce631bb3b..ae1660fc591 100644 --- a/packages/web3-eth-accounts/test/unit/common/utils.test.ts +++ b/packages/web3-eth-accounts/test/unit/common/utils.test.ts @@ -42,18 +42,18 @@ describe('[Utils/Parse]', () => { merge: '0x013fd1b5', }; - it('should parse geth params file', async () => { + it('should parse geth params file', () => { const params = parseGethGenesis(testnet, 'rinkeby'); expect(params.genesis.nonce).toBe('0x0000000000000042'); }); - it('should throw with invalid Spurious Dragon blocks', async () => { + it('should throw with invalid Spurious Dragon blocks', () => { expect(() => { parseGethGenesis(invalidSpuriousDragon, 'bad_params'); }).toThrow(); }); - it('should import poa network params correctly', async () => { + it('should import poa network params correctly', () => { let params = parseGethGenesis(poa, 'poa'); expect(params.genesis.nonce).toBe('0x0000000000000000'); expect(params.consensus).toEqual({ @@ -67,18 +67,18 @@ describe('[Utils/Parse]', () => { expect(params.hardfork).toEqual(Hardfork.London); }); - it('should generate expected hash with london block zero and base fee per gas defined', async () => { + it('should generate expected hash with london block zero and base fee per gas defined', () => { const params = parseGethGenesis(postMerge, 'post-merge'); expect(params.genesis.baseFeePerGas).toEqual(postMerge.baseFeePerGas); }); - it('should successfully parse genesis file with no extraData', async () => { + it('should successfully parse genesis file with no extraData', () => { const params = parseGethGenesis(noExtraData, 'noExtraData'); expect(params.genesis.extraData).toBe('0x'); expect(params.genesis.timestamp).toBe('0x10'); }); - it('should successfully parse kiln genesis and set forkhash', async () => { + it('should successfully parse kiln genesis and set forkhash', () => { const common = Common.fromGethGenesis(gethGenesisKiln, { chain: 'customChain', genesisHash: hexToBytes( diff --git a/packages/web3-eth-accounts/test/unit/tx/registerNewTx.test.ts b/packages/web3-eth-accounts/test/unit/tx/registerNewTx.test.ts new file mode 100644 index 00000000000..8b4da78a527 --- /dev/null +++ b/packages/web3-eth-accounts/test/unit/tx/registerNewTx.test.ts @@ -0,0 +1,49 @@ +/* +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 { TransactionFactory } from '../../../src/tx/transactionFactory'; +import { BaseTransaction } from '../../../src/tx/baseTransaction'; +import { TxData, TxOptions } from '../../../src/tx'; + +describe('Register new TX', () => { + it('validateCannotExceedMaxInteger()', () => { + const TYPE = 20; + // @ts-expect-error not implement all methods + class SomeNewTxType extends BaseTransaction { + public constructor(txData: TxData, opts: TxOptions = {}) { + super({ ...txData, type: TYPE }, opts); + } + public static fromTxData() { + return 'new fromTxData'; + } + public static fromSerializedTx() { + return 'new fromSerializedData'; + } + } + TransactionFactory.registerTransactionType(TYPE, SomeNewTxType); + const txData = { + from: '0x', + to: '0x', + value: '0x1', + type: TYPE, + }; + expect(TransactionFactory.fromTxData(txData)).toBe('new fromTxData'); + expect(TransactionFactory.fromSerializedData(new Uint8Array([TYPE, 10]))).toBe( + 'new fromSerializedData', + ); + }); +}); diff --git a/packages/web3-eth-accounts/test/unit/tx/staticMethods.test.ts b/packages/web3-eth-accounts/test/unit/tx/staticMethods.test.ts new file mode 100644 index 00000000000..e68893c70aa --- /dev/null +++ b/packages/web3-eth-accounts/test/unit/tx/staticMethods.test.ts @@ -0,0 +1,24 @@ +/* +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 { BaseTransaction } from '../../../src/tx/baseTransaction'; + +describe('[BaseTransaction]', () => { + it('Initialization', () => { + expect(typeof BaseTransaction.fromTxData).toBe('function'); + expect(typeof BaseTransaction.fromSerializedTx).toBe('function'); + }); +}); diff --git a/packages/web3-eth-accounts/test/unit/tx/transactionFactory.test.ts b/packages/web3-eth-accounts/test/unit/tx/transactionFactory.test.ts index 9d07d8be7a6..72aa0efd955 100644 --- a/packages/web3-eth-accounts/test/unit/tx/transactionFactory.test.ts +++ b/packages/web3-eth-accounts/test/unit/tx/transactionFactory.test.ts @@ -119,6 +119,13 @@ describe('[TransactionFactory]: Basic functions', () => { } }); + it('fromBlockBodyData() -> error case', () => { + expect(() => { + // @ts-expect-error incorrect param type + TransactionFactory.fromBlockBodyData(''); + }).toThrow(); + }); + it('fromTxData() -> success cases', () => { for (const txType of txTypes) { const tx = TransactionFactory.fromTxData({ type: txType.type }, { common }); diff --git a/packages/web3-eth/src/utils/get_transaction_gas_pricing.ts b/packages/web3-eth/src/utils/get_transaction_gas_pricing.ts index 035d217065d..e5bbcdedfd4 100644 --- a/packages/web3-eth/src/utils/get_transaction_gas_pricing.ts +++ b/packages/web3-eth/src/utils/get_transaction_gas_pricing.ts @@ -89,7 +89,7 @@ export async function getTransactionGasPricing( throw new UnsupportedTransactionTypeError(transactionType); // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2718.md#transactions - if (transactionType < '0x0' || transactionType > '0x7f') + if (Number(transactionType) < 0 || Number(transactionType) > 127) throw new UnsupportedTransactionTypeError(transactionType); if ( diff --git a/packages/web3-eth/test/unit/default_transaction_builder.test.ts b/packages/web3-eth/test/unit/default_transaction_builder.test.ts index 2aecbd64098..6199f7e1a3f 100644 --- a/packages/web3-eth/test/unit/default_transaction_builder.test.ts +++ b/packages/web3-eth/test/unit/default_transaction_builder.test.ts @@ -457,7 +457,7 @@ describe('defaultTransactionBuilder', () => { describe('should populate type', () => { it('should throw UnsupportedTransactionTypeError', async () => { const input = { ...transaction }; - input.type = '0x8'; // // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2718.md#transactions + input.type = '0x80'; // // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2718.md#transactions await expect( defaultTransactionBuilder({ transaction: input, web3Context, fillGasPrice: true }), diff --git a/packages/web3-validator/CHANGELOG.md b/packages/web3-validator/CHANGELOG.md index 54cfe6080fd..44852713656 100644 --- a/packages/web3-validator/CHANGELOG.md +++ b/packages/web3-validator/CHANGELOG.md @@ -149,7 +149,7 @@ Documentation: ## [Unreleased] -## Fixed +### Fixed - Multi-dimensional arrays are now handled properly when parsing ABIs (#6435) - Fix issue with default config with babel (and React): "TypeError: Cannot convert a BigInt value to a number #6187" (#6506) diff --git a/packages/web3/package.json b/packages/web3/package.json index b3f25773569..b60812065df 100644 --- a/packages/web3/package.json +++ b/packages/web3/package.json @@ -69,6 +69,7 @@ "eslint-config-base-web3": "0.1.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", + "ethereum-cryptography": "^2.1.2", "ganache": "^7.5.0", "hardhat": "^2.12.2", "in3": "^3.3.3", diff --git a/packages/web3/test/fixtures/tx-type-15/index.ts b/packages/web3/test/fixtures/tx-type-15/index.ts new file mode 100644 index 00000000000..8dd34b35e25 --- /dev/null +++ b/packages/web3/test/fixtures/tx-type-15/index.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 { keccak256 } from 'ethereum-cryptography/keccak'; +import { validateNoLeadingZeroes } from 'web3-validator'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { RLP } from '@ethereumjs/rlp'; +import { bytesToHex, hexToBytes, uint8ArrayConcat, uint8ArrayEquals } from 'web3-utils'; +import { + BaseTransaction, + FeeMarketEIP1559Transaction, + txUtils, + Common, + bigIntToHex, + toUint8Array, + ecrecover, + uint8ArrayToBigInt, + bigIntToUnpaddedUint8Array, + AccessList, + AccessListUint8Array, + FeeMarketEIP1559TxData, + FeeMarketEIP1559ValuesArray, + JsonTx, + TxOptions, +} from 'web3-eth-accounts'; + +const { getAccessListData, getAccessListJSON, getDataFeeEIP2930, verifyAccessList } = txUtils; + +const MAX_INTEGER = BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'); +export const TRANSACTION_TYPE = 15; +const TRANSACTION_TYPE_UINT8ARRAY = hexToBytes(TRANSACTION_TYPE.toString(16).padStart(2, '0')); + +/** + * Typed transaction with a new gas fee market mechanism + * + * - TransactionType: 2 + * - EIP: [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) + */ +// eslint-disable-next-line no-use-before-define +export class SomeNewTxTypeTransaction extends BaseTransaction { + public readonly chainId: bigint; + public readonly accessList: AccessListUint8Array; + public readonly AccessListJSON: AccessList; + public readonly maxPriorityFeePerGas: bigint; + public readonly maxFeePerGas: bigint; + + public readonly common: Common; + + /** + * The default HF if the tx type is active on that HF + * or the first greater HF where the tx is active. + * + * @hidden + */ + protected DEFAULT_HARDFORK = 'london'; + + /** + * Instantiate a transaction from a data dictionary. + * + * Format: { chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, + * accessList, v, r, s } + * + * Notes: + * - `chainId` will be set automatically if not provided + * - All parameters are optional and have some basic default values + */ + public static fromTxData(txData: FeeMarketEIP1559TxData, opts: TxOptions = {}) { + return new SomeNewTxTypeTransaction(txData, opts); + } + + /** + * Instantiate a transaction from the serialized tx. + * + * Format: `0x02 || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, + * accessList, signatureYParity, signatureR, signatureS])` + */ + public static fromSerializedTx(serialized: Uint8Array, opts: TxOptions = {}) { + if (!uint8ArrayEquals(serialized.subarray(0, 1), TRANSACTION_TYPE_UINT8ARRAY)) { + throw new Error( + `Invalid serialized tx input: not an EIP-1559 transaction (wrong tx type, expected: ${TRANSACTION_TYPE}, received: ${bytesToHex( + serialized.subarray(0, 1), + )}`, + ); + } + const values = RLP.decode(serialized.subarray(1)); + + if (!Array.isArray(values)) { + throw new Error('Invalid serialized tx input: must be array'); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return FeeMarketEIP1559Transaction.fromValuesArray(values as any, opts); + } + + /** + * Create a transaction from a values array. + * + * Format: `[chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, + * accessList, signatureYParity, signatureR, signatureS]` + */ + public static fromValuesArray(values: FeeMarketEIP1559ValuesArray, opts: TxOptions = {}) { + if (values.length !== 9 && values.length !== 12) { + throw new Error( + 'Invalid EIP-1559 transaction. Only expecting 9 values (for unsigned tx) or 12 values (for signed tx).', + ); + } + + const [ + chainId, + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + gasLimit, + to, + value, + data, + accessList, + v, + r, + s, + ] = values; + + this._validateNotArray({ chainId, v }); + validateNoLeadingZeroes({ + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + gasLimit, + value, + v, + r, + s, + }); + + return new FeeMarketEIP1559Transaction( + { + chainId: uint8ArrayToBigInt(chainId), + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + gasLimit, + to, + value, + data, + accessList: accessList ?? [], + v: v !== undefined ? uint8ArrayToBigInt(v) : undefined, // EIP2930 supports v's with value 0 (empty Uint8Array) + r, + s, + }, + opts, + ); + } + + /** + * This constructor takes the values, validates them, assigns them and freezes the object. + * + * It is not recommended to use this constructor directly. Instead use + * the static factory methods to assist in creating a Transaction object from + * varying data types. + */ + public constructor(txData: FeeMarketEIP1559TxData, opts: TxOptions = {}) { + super({ ...txData, type: TRANSACTION_TYPE }, opts); + const { chainId, accessList, maxFeePerGas, maxPriorityFeePerGas } = txData; + + this.common = this._getCommon(opts.common, chainId); + this.chainId = this.common.chainId(); + + if (!this.common.isActivatedEIP(1559)) { + throw new Error('EIP-1559 not enabled on Common'); + } + this.activeCapabilities = this.activeCapabilities.concat([1559, 2718, 2930]); + + // Populate the access list fields + const accessListData = getAccessListData(accessList ?? []); + this.accessList = accessListData.accessList; + this.AccessListJSON = accessListData.AccessListJSON; + // Verify the access list format. + verifyAccessList(this.accessList); + + this.maxFeePerGas = uint8ArrayToBigInt( + toUint8Array(maxFeePerGas === '' ? '0x' : maxFeePerGas), + ); + this.maxPriorityFeePerGas = uint8ArrayToBigInt( + toUint8Array(maxPriorityFeePerGas === '' ? '0x' : maxPriorityFeePerGas), + ); + + this._validateCannotExceedMaxInteger({ + maxFeePerGas: this.maxFeePerGas, + maxPriorityFeePerGas: this.maxPriorityFeePerGas, + }); + + BaseTransaction._validateNotArray(txData); + + if (this.gasLimit * this.maxFeePerGas > MAX_INTEGER) { + const msg = this._errorMsg( + 'gasLimit * maxFeePerGas cannot exceed MAX_INTEGER (2^256-1)', + ); + throw new Error(msg); + } + + if (this.maxFeePerGas < this.maxPriorityFeePerGas) { + const msg = this._errorMsg( + 'maxFeePerGas cannot be less than maxPriorityFeePerGas (The total must be the larger of the two)', + ); + throw new Error(msg); + } + + this._validateYParity(); + this._validateHighS(); + + const freeze = opts?.freeze ?? true; + if (freeze) { + Object.freeze(this); + } + } + + /** + * The amount of gas paid for the data in this tx + */ + public getDataFee(): bigint { + if (this.cache.dataFee && this.cache.dataFee.hardfork === this.common.hardfork()) { + return this.cache.dataFee.value; + } + + let cost = super.getDataFee(); + cost += BigInt(getDataFeeEIP2930(this.accessList, this.common)); + + if (Object.isFrozen(this)) { + this.cache.dataFee = { + value: cost, + hardfork: this.common.hardfork(), + }; + } + + return cost; + } + + /** + * The up front amount that an account must have for this transaction to be valid + * @param baseFee The base fee of the block (will be set to 0 if not provided) + */ + public getUpfrontCost(baseFee = BigInt(0)): bigint { + const prio = this.maxPriorityFeePerGas; + const maxBase = this.maxFeePerGas - baseFee; + const inclusionFeePerGas = prio < maxBase ? prio : maxBase; + const gasPrice = inclusionFeePerGas + baseFee; + return this.gasLimit * gasPrice + this.value; + } + + /** + * Returns a Uint8Array Array of the raw Uint8Arrays of the EIP-1559 transaction, in order. + * + * Format: `[chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, + * accessList, signatureYParity, signatureR, signatureS]` + * + * Use {@link FeeMarketEIP1559Transaction.serialize} to add a transaction to a block + * with {@link Block.fromValuesArray}. + * + * For an unsigned tx this method uses the empty Uint8Array values for the + * signature parameters `v`, `r` and `s` for encoding. For an EIP-155 compliant + * representation for external signing use {@link FeeMarketEIP1559Transaction.getMessageToSign}. + */ + public raw(): FeeMarketEIP1559ValuesArray { + return [ + bigIntToUnpaddedUint8Array(this.chainId), + bigIntToUnpaddedUint8Array(this.nonce), + bigIntToUnpaddedUint8Array(this.maxPriorityFeePerGas), + bigIntToUnpaddedUint8Array(this.maxFeePerGas), + bigIntToUnpaddedUint8Array(this.gasLimit), + this.to !== undefined ? this.to.buf : Uint8Array.from([]), + bigIntToUnpaddedUint8Array(this.value), + this.data, + this.accessList, + this.v !== undefined ? bigIntToUnpaddedUint8Array(this.v) : Uint8Array.from([]), + this.r !== undefined ? bigIntToUnpaddedUint8Array(this.r) : Uint8Array.from([]), + this.s !== undefined ? bigIntToUnpaddedUint8Array(this.s) : Uint8Array.from([]), + ]; + } + + /** + * Returns the serialized encoding of the EIP-1559 transaction. + * + * Format: `0x02 || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, + * accessList, signatureYParity, signatureR, signatureS])` + * + * Note that in contrast to the legacy tx serialization format this is not + * valid RLP any more due to the raw tx type preceding and concatenated to + * the RLP encoding of the values. + */ + public serialize(): Uint8Array { + const base = this.raw(); + return uint8ArrayConcat(TRANSACTION_TYPE_UINT8ARRAY, RLP.encode(base)); + } + + /** + * Returns the serialized unsigned tx (hashed or raw), which can be used + * to sign the transaction (e.g. for sending to a hardware wallet). + * + * Note: in contrast to the legacy tx the raw message format is already + * serialized and doesn't need to be RLP encoded any more. + * + * ```javascript + * const serializedMessage = tx.getMessageToSign(false) // use this for the HW wallet input + * ``` + * + * @param hashMessage - Return hashed message if set to true (default: true) + */ + public getMessageToSign(hashMessage = true): Uint8Array { + const base = this.raw().slice(0, 9); + const message = uint8ArrayConcat(TRANSACTION_TYPE_UINT8ARRAY, RLP.encode(base)); + if (hashMessage) { + return keccak256(message); + } + return message; + } + + /** + * Computes a sha3-256 hash of the serialized tx. + * + * This method can only be used for signed txs (it throws otherwise). + * Use {@link FeeMarketEIP1559Transaction.getMessageToSign} to get a tx hash for the purpose of signing. + */ + public hash(): Uint8Array { + if (!this.isSigned()) { + const msg = this._errorMsg('Cannot call hash method if transaction is not signed'); + throw new Error(msg); + } + + if (Object.isFrozen(this)) { + if (!this.cache.hash) { + this.cache.hash = keccak256(this.serialize()); + } + return this.cache.hash; + } + return keccak256(this.serialize()); + } + + /** + * Computes a sha3-256 hash which can be used to verify the signature + */ + public getMessageToVerifySignature(): Uint8Array { + return this.getMessageToSign(); + } + + /** + * Returns the public key of the sender + */ + public getSenderPublicKey(): Uint8Array { + if (!this.isSigned()) { + const msg = this._errorMsg('Cannot call this method if transaction is not signed'); + throw new Error(msg); + } + + const msgHash = this.getMessageToVerifySignature(); + const { v, r, s } = this; + + this._validateHighS(); + + try { + return ecrecover( + msgHash, + v! + BigInt(27), // Recover the 27 which was stripped from ecsign + bigIntToUnpaddedUint8Array(r!), + bigIntToUnpaddedUint8Array(s!), + ); + } catch (e: any) { + const msg = this._errorMsg('Invalid Signature'); + throw new Error(msg); + } + } + + public _processSignature(v: bigint, r: Uint8Array, s: Uint8Array) { + const opts = { ...this.txOptions, common: this.common }; + + return FeeMarketEIP1559Transaction.fromTxData( + { + chainId: this.chainId, + nonce: this.nonce, + maxPriorityFeePerGas: this.maxPriorityFeePerGas, + maxFeePerGas: this.maxFeePerGas, + gasLimit: this.gasLimit, + to: this.to, + value: this.value, + data: this.data, + accessList: this.accessList, + v: v - BigInt(27), // This looks extremely hacky: /util actually adds 27 to the value, the recovery bit is either 0 or 1. + r: uint8ArrayToBigInt(r), + s: uint8ArrayToBigInt(s), + }, + opts, + ); + } + + /** + * Returns an object with the JSON representation of the transaction + */ + public toJSON(): JsonTx { + const accessListJSON = getAccessListJSON(this.accessList); + + return { + chainId: bigIntToHex(this.chainId), + nonce: bigIntToHex(this.nonce), + maxPriorityFeePerGas: bigIntToHex(this.maxPriorityFeePerGas), + maxFeePerGas: bigIntToHex(this.maxFeePerGas), + gasLimit: bigIntToHex(this.gasLimit), + to: this.to !== undefined ? this.to.toString() : undefined, + value: bigIntToHex(this.value), + data: bytesToHex(this.data), + accessList: accessListJSON, + v: this.v !== undefined ? bigIntToHex(this.v) : undefined, + r: this.r !== undefined ? bigIntToHex(this.r) : undefined, + s: this.s !== undefined ? bigIntToHex(this.s) : undefined, + }; + } + + /** + * Return a compact error string representation of the object + */ + public errorStr() { + let errorStr = this._getSharedErrorPostfix(); + errorStr += ` maxFeePerGas=${this.maxFeePerGas} maxPriorityFeePerGas=${this.maxPriorityFeePerGas}`; + return errorStr; + } + + /** + * Internal helper function to create an annotated error message + * + * @param msg Base error message + * @hidden + */ + protected _errorMsg(msg: string) { + return `${msg} (${this.errorStr()})`; + } +} diff --git a/packages/web3/test/integration/web3-plugin-add-tx.test.ts b/packages/web3/test/integration/web3-plugin-add-tx.test.ts new file mode 100644 index 00000000000..d1971b7f4ef --- /dev/null +++ b/packages/web3/test/integration/web3-plugin-add-tx.test.ts @@ -0,0 +1,72 @@ +/* +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 . +*/ + +/* eslint-disable @typescript-eslint/no-magic-numbers */ + +import { Transaction, TransactionFactory, Web3Account } from 'web3-eth-accounts'; +import { SupportedProviders, Web3, Web3PluginBase } from '../../src'; +import { + createAccount, + createLocalAccount, + getSystemTestProvider, + waitForOpenConnection, +} from '../shared_fixtures/system_tests_utils'; +import { SomeNewTxTypeTransaction, TRANSACTION_TYPE } from '../fixtures/tx-type-15'; + +class Eip4844Plugin extends Web3PluginBase { + public pluginNamespace = 'txType3'; + public constructor() { + super(); + TransactionFactory.registerTransactionType(TRANSACTION_TYPE, SomeNewTxTypeTransaction); + } +} + +describe('Add New Tx as a Plugin', () => { + let web3: Web3; + let clientUrl: string | SupportedProviders; + let account1: Web3Account; + let account2: Web3Account; + beforeEach(async () => { + clientUrl = getSystemTestProvider(); + web3 = new Web3(clientUrl); + account1 = await createLocalAccount(web3); + account2 = createAccount(); + web3.eth.accounts.wallet.add(account1); + await waitForOpenConnection(web3.eth); + }); + it('should receive correct type of tx', async () => { + web3.registerPlugin(new Eip4844Plugin()); + const tx = { + from: account1.address, + to: account2.address, + value: '0x1', + type: TRANSACTION_TYPE, + maxPriorityFeePerGas: BigInt(5000000), + maxFeePerGas: BigInt(5000000), + }; + const sub = web3.eth.sendTransaction(tx); + + const waitForEvent: Promise = new Promise(resolve => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + sub.on('sending', txData => { + resolve(txData as unknown as Transaction); + }); + }); + expect(Number((await waitForEvent).type)).toBe(TRANSACTION_TYPE); + await expect(sub).rejects.toThrow(); + }); +}); diff --git a/yarn.lock b/yarn.lock index d06c544ec86..2ef8320c531 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1113,6 +1113,13 @@ dependencies: "@noble/hashes" "1.3.0" +"@noble/curves@1.1.0", "@noble/curves@~1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d" + integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA== + dependencies: + "@noble/hashes" "1.3.1" + "@noble/hashes@1.1.2", "@noble/hashes@~1.1.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.2.tgz#e9e035b9b166ca0af657a7848eb2718f0f22f183" @@ -1123,6 +1130,16 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1" integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg== +"@noble/hashes@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" + integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== + +"@noble/hashes@~1.3.1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + "@noble/secp256k1@1.6.3", "@noble/secp256k1@~1.6.0": version "1.6.3" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.6.3.tgz#7eed12d9f4404b416999d0c87686836c4c5c9b94" @@ -1765,6 +1782,15 @@ "@noble/hashes" "~1.3.0" "@scure/base" "~1.1.0" +"@scure/bip32@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.1.tgz#7248aea723667f98160f593d621c47e208ccbb10" + integrity sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A== + dependencies: + "@noble/curves" "~1.1.0" + "@noble/hashes" "~1.3.1" + "@scure/base" "~1.1.0" + "@scure/bip39@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.0.tgz#92f11d095bae025f166bef3defcc5bf4945d419a" @@ -1781,6 +1807,14 @@ "@noble/hashes" "~1.3.0" "@scure/base" "~1.1.0" +"@scure/bip39@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" + integrity sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg== + dependencies: + "@noble/hashes" "~1.3.0" + "@scure/base" "~1.1.0" + "@sentry/core@5.30.0": version "5.30.0" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.30.0.tgz#6b203664f69e75106ee8b5a2fe1d717379b331f3" @@ -5093,6 +5127,16 @@ ethereum-cryptography@^2.0.0: "@scure/bip32" "1.3.0" "@scure/bip39" "1.2.0" +ethereum-cryptography@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.1.2.tgz#18fa7108622e56481157a5cb7c01c0c6a672eb67" + integrity sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug== + dependencies: + "@noble/curves" "1.1.0" + "@noble/hashes" "1.3.1" + "@scure/bip32" "1.3.1" + "@scure/bip39" "1.2.1" + ethereum-protocol@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/ethereum-protocol/-/ethereum-protocol-1.0.1.tgz#b7d68142f4105e0ae7b5e178cf42f8d4dc4b93cf"