From 12d325a3f7ef8ae07fb3b77ecfb94228d491c38e Mon Sep 17 00:00:00 2001 From: Oleksii Kosynskyi Date: Thu, 12 Oct 2023 10:57:03 -0400 Subject: [PATCH] finish --- 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 | 20 +- .../src/utils/get_transaction_gas_pricing.ts | 2 +- packages/web3/package.json | 1 + .../web3/test/fixtures/tx-type-15/index.ts | 446 ++++++++ .../test/fixtures/tx-type-eip484/index.ts | 975 ------------------ .../integration/web3-plugin-eip-4844.test.ts | 61 +- yarn.lock | 44 + 10 files changed, 555 insertions(+), 1027 deletions(-) create mode 100644 packages/web3/test/fixtures/tx-type-15/index.ts delete mode 100644 packages/web3/test/fixtures/tx-type-eip484/index.ts diff --git a/packages/web3-eth-accounts/CHANGELOG.md b/packages/web3-eth-accounts/CHANGELOG.md index 719333003b0..58034dc7a48 100644 --- a/packages/web3-eth-accounts/CHANGELOG.md +++ b/packages/web3-eth-accounts/CHANGELOG.md @@ -142,6 +142,8 @@ Documentation: ### Added - Added public function `privateKeyToPublicKey` +- Added exporting `BaseTransaction` from the package (#6493) +- Added exporting `txUtils` from the package (#6493) ### Fixed diff --git a/packages/web3-eth-accounts/src/tx/baseTransaction.ts b/packages/web3-eth-accounts/src/tx/baseTransaction.ts index c212b2b8113..f0cace3af4c 100644 --- a/packages/web3-eth-accounts/src/tx/baseTransaction.ts +++ b/packages/web3-eth-accounts/src/tx/baseTransaction.ts @@ -18,14 +18,9 @@ along with web3.js. If not, see . 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 57eaa60ada5..26ead20a897 100644 --- a/packages/web3-eth-accounts/src/tx/index.ts +++ b/packages/web3-eth-accounts/src/tx/index.ts @@ -21,5 +21,5 @@ 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'; +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 6604b09055b..aacdbdd1757 100644 --- a/packages/web3-eth-accounts/src/tx/transactionFactory.ts +++ b/packages/web3-eth-accounts/src/tx/transactionFactory.ts @@ -27,7 +27,7 @@ import type { TxData, TxOptions, } from './types.js'; -import { BaseTransaction } from './baseTransaction'; +import { BaseTransaction } from './baseTransaction.js'; const extraTxTypes: Map = new Map(); @@ -59,8 +59,6 @@ export class TransactionFactory { txData: TxData | TypedTransaction, txOptions: TxOptions = {}, ): TypedTransaction { - console.log('txData', txData); - console.log('txOptions', txOptions); if (!('type' in txData) || txData.type === undefined) { // Assume legacy transaction return Transaction.fromTxData(txData as TxData, txOptions); @@ -85,12 +83,8 @@ export class TransactionFactory { ); } const ExtraTransaction = extraTxTypes.get(txType); - if (ExtraTransaction) { - console.log('extra'); - // @ts-ignore - console.log('res', ExtraTransaction.fromTxData(txData, txOptions)); - // @ts-ignore - return ExtraTransaction.fromTxData(txData, txOptions); + if (ExtraTransaction?.fromTxData) { + return ExtraTransaction.fromTxData(txData, txOptions) as TypedTransaction; } throw new Error(`Tx instantiation with type ${txType} not supported`); @@ -115,9 +109,11 @@ export class TransactionFactory { return FeeMarketEIP1559Transaction.fromSerializedTx(data, txOptions); default: { const ExtraTransaction = extraTxTypes.get(data[0]); - if (ExtraTransaction) { - // @ts-ignore - return ExtraTransaction.fromSerializedTx(data, txOptions); + if (ExtraTransaction?.fromSerializedTx) { + return ExtraTransaction.fromSerializedTx( + data, + txOptions, + ) as TypedTransaction; } throw new Error(`TypedTransaction with ID ${data[0]} unknown`); 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/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/fixtures/tx-type-eip484/index.ts b/packages/web3/test/fixtures/tx-type-eip484/index.ts deleted file mode 100644 index 9253e5d0630..00000000000 --- a/packages/web3/test/fixtures/tx-type-eip484/index.ts +++ /dev/null @@ -1,975 +0,0 @@ -/* -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 { Input, RLP } from '@ethereumjs/rlp'; - -import { FeeMarketEIP1559TxData, ecrecover, padToEven, stripHexPrefix , txUtils } from 'web3-eth-accounts'; -import type { - AccessList, - TxValuesArray as AllTypesTxValuesArray, - JsonTx, - TxOptions, - Common } from 'web3-eth-accounts'; -import { keccak256 } from 'ethereum-cryptography/keccak.js'; -import { hexToBytes, numberToHex, toBigInt, toHex, utf8ToBytes } from 'web3-utils'; -import { sha256 } from 'ethereum-cryptography/sha256.js'; -import { isHexPrefixed } from 'web3-validator'; -import { secp256k1 } from "web3-eth-accounts/src/tx/constants"; -import { BaseTransaction } from "web3-eth-accounts/src/tx/baseTransaction"; - -const { getDataFeeEIP2930, verifyAccessList, getAccessListData, getAccessListJSON } = txUtils; -const MAX_INTEGER = BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'); -const SECP256K1_ORDER = secp256k1.CURVE.n; -const SECP256K1_ORDER_DIV_2 = SECP256K1_ORDER / BigInt(2); - -const BIGINT_27 = BigInt(27); -const BIGINT_0 = BigInt(0); -const BIGINT_1 = BigInt(1); -const LIMIT_BLOBS_PER_TX = 16777216; // 2 ** 24 -const FIELD_ELEMENTS_PER_BLOB = 4096; -const BYTES_PER_FIELD_ELEMENT = 32; -const USEFUL_BYTES_PER_BLOB = 32 * FIELD_ELEMENTS_PER_BLOB; -const MAX_BLOBS_PER_TX = 2; -const MAX_USEFUL_BYTES_PER_TX = USEFUL_BYTES_PER_BLOB * MAX_BLOBS_PER_TX - 1; -const BLOB_SIZE = BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB; - -const validateNoLeadingZeroes = (values: { [key: string]: Uint8Array | undefined }) => { - for (const [k, v] of Object.entries(values)) { - if (v !== undefined && v.length > 0 && v[0] === 0) { - throw new Error(`${k} cannot have leading zeroes, received: ${toHex(v)}`); - } - } -}; - -function get_padded(data: Uint8Array, blobs_len: number): Uint8Array { - const pdata = new Uint8Array(blobs_len * USEFUL_BYTES_PER_BLOB).fill(0); - pdata.set(data); - pdata[data.byteLength] = 0x80; - return pdata; -} - -function get_blob(data: Uint8Array): Uint8Array { - const blob = new Uint8Array(BLOB_SIZE); - for (let i = 0; i < FIELD_ELEMENTS_PER_BLOB; i++) { - const chunk = new Uint8Array(32); - chunk.set(data.subarray(i * 31, (i + 1) * 31), 0); - blob.set(chunk, i * 32); - } - - return blob; -} - -const getBlobs = (input: string) => { - const data = utf8ToBytes(input); - const len = data.byteLength; - if (len === 0) { - throw Error('invalid blob data'); - } - if (len > MAX_USEFUL_BYTES_PER_TX) { - throw Error('blob data is too large'); - } - - const blobs_len = Math.ceil(len / USEFUL_BYTES_PER_BLOB); - - const pdata = get_padded(data, blobs_len); - - const blobs: Uint8Array[] = []; - for (let i = 0; i < blobs_len; i++) { - const chunk = pdata.subarray(i * USEFUL_BYTES_PER_BLOB, (i + 1) * USEFUL_BYTES_PER_BLOB); - const blob = get_blob(chunk); - blobs.push(blob); - } - - return blobs; -}; - -interface Kzg { - loadTrustedSetup(filePath: string): void; - - blobToKzgCommitment(blob: Uint8Array): Uint8Array; - - computeBlobKzgProof(blob: Uint8Array, commitment: Uint8Array): Uint8Array; - - verifyKzgProof( - polynomialKzg: Uint8Array, - z: Uint8Array, - y: Uint8Array, - kzgProof: Uint8Array, - ): boolean; - - verifyBlobKzgProofBatch( - blobs: Uint8Array[], - expectedKzgCommitments: Uint8Array[], - kzgProofs: Uint8Array[], - ): boolean; -} - -function kzgNotLoaded(): never { - throw Error('kzg library not loaded'); -} - -const assertIsBytes = function (input: Uint8Array): void { - if (!(input instanceof Uint8Array)) { - const msg = `This method only supports Uint8Array but input was: ${input}`; - throw new Error(msg); - } -}; -type PrefixedHexString = string; -const stripZeros = < - T extends Uint8Array | number[] | PrefixedHexString = Uint8Array | number[] | PrefixedHexString, ->( - a: T, -): T => { - let first = a[0]; - while (a.length > 0 && first.toString() === '0') { - a = a.slice(1) as T; - first = a[0]; - } - return a; -}; -const unpadBytes = (a: Uint8Array): Uint8Array => { - assertIsBytes(a); - return stripZeros(a); -}; -const bigIntToBytes = (num: bigint): Uint8Array => { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - return toBytes(`0x${ padToEven(num.toString(16))}`); -}; -const bigIntToUnpaddedBytes = (value: bigint): Uint8Array => { - return unpadBytes(bigIntToBytes(value)); -}; - -// eslint-disable-next-line import/no-mutable-exports -const kzg: Kzg = { - loadTrustedSetup: kzgNotLoaded, - blobToKzgCommitment: kzgNotLoaded, - computeBlobKzgProof: kzgNotLoaded, - verifyKzgProof: kzgNotLoaded, - verifyBlobKzgProofBatch: kzgNotLoaded, -}; -/** - * Bytes values array for a {@link BlobEIP4844Transaction} - */ -type BlobEIP4844TxValuesArray = [ - Uint8Array, - Uint8Array, - Uint8Array, - Uint8Array, - Uint8Array, - Uint8Array, - Uint8Array, - Uint8Array, - AccessListBytes, - Uint8Array, - Uint8Array[], - Uint8Array?, - Uint8Array?, - Uint8Array?, -]; -type BlobEIP4844NetworkValuesArray = [ - BlobEIP4844TxValuesArray, - Uint8Array[], - Uint8Array[], - Uint8Array[], -]; -/** - * @param kzgLib a KZG implementation (defaults to c-kzg) - * @param trustedSetupPath the full path (e.g. "/home/linux/devnet4.txt") to a kzg trusted setup text file - */ -// function initKZG(kzgLib: Kzg, trustedSetupPath: string) { -// kzg = kzgLib; -// kzg.loadTrustedSetup(trustedSetupPath); -// } -type TxValuesArray = AllTypesTxValuesArray[TransactionType.BlobEIP4844]; - -export function equalsBytes(a: Uint8Array, b: Uint8Array): boolean { - if (a.length !== b.length) { - return false; - } - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) { - return false; - } - } - return true; -} - -function toBytes(v?: BytesLike | BigIntLike): Uint8Array { - if (v instanceof Uint8Array) { - return v; - } - if (typeof v === 'string') { - if (isHexPrefixed(v)) { - return hexToBytes(padToEven(stripHexPrefix(v))); - } - return utf8ToBytes(v); - } - if (typeof v === 'number' || typeof v === 'bigint') { - if (!v) { - return Uint8Array.from([]); - } - return hexToBytes(numberToHex(v)); - } - if (v === null || v === undefined) { - return Uint8Array.from([]); - } - throw new Error(`toBytes: received unsupported type ${ typeof v}`); -} - -const concatBytes = (...arrays: Uint8Array[]): Uint8Array => { - if (arrays.length === 1) return arrays[0]; - const length = arrays.reduce((a, arr) => a + arr.length, 0); - const result = new Uint8Array(length); - for (let i = 0, pad = 0; i < arrays.length; i++) { - const arr = arrays[i]; - result.set(arr, pad); - pad += arr.length; - } - return result; -}; - -function txTypeBytes(txType: TransactionType): Uint8Array { - return hexToBytes(`0x${ txType.toString(16).padStart(2, '0')}`); -} - -const computeVersionedHash = (commitment: Uint8Array, blobCommitmentVersion: number) => { - const computedVersionedHash = new Uint8Array(32); - computedVersionedHash.set([blobCommitmentVersion], 0); - computedVersionedHash.set(sha256(Buffer.from(commitment)).subarray(1), 1); - return computedVersionedHash; -}; -const blobsToCommitments = (blobs: Uint8Array[]) => { - const commitments: Uint8Array[] = []; - for (const blob of blobs) { - commitments.push(kzg.blobToKzgCommitment(blob)); - } - return commitments; -}; -const commitmentsToVersionedHashes = (commitments: Uint8Array[]) => { - const hashes: Uint8Array[] = []; - for (const commitment of commitments) { - hashes.push(computeVersionedHash(commitment, 0x01)); - } - return hashes; -}; -const validateBlobTransactionNetworkWrapper = ( - blobVersionedHashes: Uint8Array[], - blobs: Uint8Array[], - commitments: Uint8Array[], - kzgProofs: Uint8Array[], - version: number, -) => { - if (!(blobVersionedHashes.length === blobs.length && blobs.length === commitments.length)) { - throw new Error('Number of blobVersionedHashes, blobs, and commitments not all equal'); - } - if (blobVersionedHashes.length === 0) { - throw new Error('Invalid transaction with empty blobs'); - } - - let isValid; - try { - isValid = kzg.verifyBlobKzgProofBatch(blobs, commitments, kzgProofs); - } catch (error) { - throw new Error(`KZG verification of blobs fail with error=${error}`); - } - if (!isValid) { - throw new Error('KZG proof cannot be verified from blobs/commitments'); - } - - for (let x = 0; x < blobVersionedHashes.length; x++) { - const computedVersionedHash = computeVersionedHash(commitments[x], version); - if (!equalsBytes(computedVersionedHash, blobVersionedHashes[x])) { - throw new Error(`commitment for blob at index ${x} does not match versionedHash`); - } - } -}; -type AccessListBytesItem = [Uint8Array, Uint8Array[]]; -type AccessListBytes = AccessListBytesItem[]; -const blobsToProofs = (blobs: Uint8Array[], commitments: Uint8Array[]) => - blobs.map((blob, ctx) => kzg.computeBlobKzgProof(blob, commitments[ctx])); - -export enum TransactionType { - Legacy = 0, - AccessListEIP2930 = 1, - FeeMarketEIP1559 = 2, - BlobEIP4844 = 3, -} - -interface TransformabletoBytes { - toBytes?(): Uint8Array; -} - -type BigIntLike = bigint | PrefixedHexString | number | Uint8Array; -type BytesLike = Uint8Array | number[] | number | bigint | TransformabletoBytes | PrefixedHexString; - -interface BlobEIP4844TxData extends FeeMarketEIP1559TxData { - /** - * The versioned hashes used to validate the blobs attached to a transaction - */ - blobVersionedHashes?: BytesLike[]; - /** - * The maximum fee per blob gas paid for the transaction - */ - maxFeePerBlobGas?: BigIntLike; - /** - * The blobs associated with a transaction - */ - blobs?: BytesLike[]; - /** - * The KZG commitments corresponding to the versioned hashes for each blob - */ - kzgCommitments?: BytesLike[]; - /** - * The KZG proofs associated with the transaction - */ - kzgProofs?: BytesLike[]; - /** - * An array of arbitrary strings that blobs are to be constructed from - */ - blobsData?: string[]; -} - -/** - * Typed transaction with a new gas fee market mechanism for transactions that include "blobs" of data - * - * - TransactionType: 3 - * - EIP: [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) - */ -export class BlobEIP4844Transaction extends BaseTransaction { - public readonly chainId: bigint; - public readonly accessList: AccessListBytes; - public readonly AccessListJSON: AccessList; - public readonly maxPriorityFeePerGas: bigint; - public readonly maxFeePerGas: bigint; - public readonly maxFeePerBlobGas: bigint; - - // @ts-expect-error - public readonly common: Common; - public blobVersionedHashes: Uint8Array[]; - blobs?: Uint8Array[]; // This property should only be populated when the transaction is in the "Network Wrapper" format - kzgCommitments?: Uint8Array[]; // This property should only be populated when the transaction is in the "Network Wrapper" format - kzgProofs?: Uint8Array[]; // This property should only be populated when the transaction is in the "Network Wrapper" format - - /** - * 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 constructors or factory methods to assist in creating a Transaction object from - * varying data types. - */ - constructor(txData: BlobEIP4844TxData, opts: TxOptions = {}) { - // @ts-expect-error - super({ ...txData, type: TransactionType.BlobEIP4844 }, opts); - const { chainId, accessList, maxFeePerGas, maxPriorityFeePerGas, maxFeePerBlobGas } = - txData; - - // @ts-expect-error - 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'); - } - - if (!this.common.isActivatedEIP(4844)) { - throw new Error('EIP-4844 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 = toBigInt(toBytes(maxFeePerGas === '' ? '0x' : maxFeePerGas)); - this.maxPriorityFeePerGas = toBigInt( - toBytes(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.maxFeePerBlobGas = toBigInt( - toBytes((maxFeePerBlobGas ?? '') === '' ? '0x' : maxFeePerBlobGas), - ); - - this.blobVersionedHashes = (txData.blobVersionedHashes ?? []).map(vh => toBytes(vh)); - this.validateYParity(); - this.validateHighS(); - - for (const hash of this.blobVersionedHashes) { - if (hash.length !== 32) { - const msg = this._errorMsg('versioned hash is invalid length'); - throw new Error(msg); - } - if (BigInt(hash[0]) !== this.common.param('sharding', 'blobCommitmentVersionKzg')) { - const msg = this._errorMsg( - 'versioned hash does not start with KZG commitment version', - ); - throw new Error(msg); - } - } - if (this.blobVersionedHashes.length > LIMIT_BLOBS_PER_TX) { - const msg = this._errorMsg(`tx can contain at most ${LIMIT_BLOBS_PER_TX} blobs`); - throw new Error(msg); - } - - this.blobs = txData.blobs?.map(blob => toBytes(blob)); - this.kzgCommitments = txData.kzgCommitments?.map(commitment => toBytes(commitment)); - this.kzgProofs = txData.kzgProofs?.map(proof => toBytes(proof)); - const freeze = opts?.freeze ?? true; - if (freeze) { - Object.freeze(this); - } - } - - validateHighS(): void { - const { s } = this; - if (this.common.gteHardfork('homestead') && s !== undefined && s > SECP256K1_ORDER_DIV_2) { - const msg = this._errorMsg( - 'Invalid Signature: s-values greater than secp256k1n/2 are considered invalid', - ); - throw new Error(msg); - } - } - - validateYParity() { - const { v } = this; - if (v !== undefined && v !== BIGINT_0 && v !== BIGINT_1) { - const msg = this._errorMsg('The y-parity of the transaction should either be 0 or 1'); - throw new Error(msg); - } - } - - public static fromTxData(txData: BlobEIP4844TxData, opts?: TxOptions) { - if (txData.blobsData !== undefined) { - if (txData.blobs !== undefined) { - throw new Error('cannot have both raw blobs data and encoded blobs in constructor'); - } - if (txData.kzgCommitments !== undefined) { - throw new Error( - 'cannot have both raw blobs data and KZG commitments in constructor', - ); - } - if (txData.blobVersionedHashes !== undefined) { - throw new Error( - 'cannot have both raw blobs data and versioned hashes in constructor', - ); - } - if (txData.kzgProofs !== undefined) { - throw new Error('cannot have both raw blobs data and KZG proofs in constructor'); - } - txData.blobs = getBlobs(txData.blobsData.reduce((acc, cur) => acc + cur)); - txData.kzgCommitments = blobsToCommitments(txData.blobs as Uint8Array[]); - txData.blobVersionedHashes = commitmentsToVersionedHashes( - txData.kzgCommitments as Uint8Array[], - ); - txData.kzgProofs = blobsToProofs( - txData.blobs as Uint8Array[], - txData.kzgCommitments as Uint8Array[], - ); - } - - return new BlobEIP4844Transaction(txData, opts); - } - - /** - * Creates the minimal representation of a blob transaction from the network wrapper version. - * The minimal representation is used when adding transactions to an execution payload/block - * @param txData a {@link BlobEIP4844Transaction} containing optional blobs/kzg commitments - * @param opts - dictionary of {@link TxOptions} - * @returns the "minimal" representation of a BlobEIP4844Transaction (i.e. transaction object minus blobs and kzg commitments) - */ - public static minimalFromNetworkWrapper( - txData: BlobEIP4844Transaction, - opts?: TxOptions, - ): BlobEIP4844Transaction { - const tx = BlobEIP4844Transaction.fromTxData( - { - ...txData, - ...{ blobs: undefined, kzgCommitments: undefined, kzgProofs: undefined }, - }, - opts, - ); - return tx; - } - - /** - * Instantiate a transaction from the serialized tx. - * - * Format: `0x03 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, value, data, - * access_list, max_fee_per_data_gas, blob_versioned_hashes, y_parity, r, s])` - */ - public static fromSerializedTx(serialized: Uint8Array, opts: TxOptions = {}) { - if ( - !equalsBytes(serialized.subarray(0, 1), txTypeBytes(TransactionType.BlobEIP4844)) - ) { - throw new Error( - `Invalid serialized tx input: not an EIP-4844 transaction (wrong tx type, expected: ${ - TransactionType.BlobEIP4844 - }, received: ${toHex(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'); - } - - return BlobEIP4844Transaction.fromValuesArray(values as unknown as TxValuesArray, 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: TxValuesArray, opts: TxOptions = {}) { - if (values.length !== 11 && values.length !== 14) { - throw new Error( - 'Invalid EIP-4844 transaction. Only expecting 11 values (for unsigned tx) or 14 values (for signed tx).', - ); - } - - const [ - chainId, - nonce, - maxPriorityFeePerGas, - maxFeePerGas, - gasLimit, - to, - value, - data, - accessList, - maxFeePerBlobGas, - blobVersionedHashes, - v, - r, - s, - ] = values; - - this._validateNotArray({ chainId, v }); - validateNoLeadingZeroes({ - // @ts-expect-error - nonce, - // @ts-expect-error - maxPriorityFeePerGas, - // @ts-expect-error - maxFeePerGas, - // @ts-expect-error - gasLimit, - // @ts-expect-error - value, - // @ts-expect-error - maxFeePerBlobGas, - // @ts-expect-error - v, - // @ts-expect-error - r, - // @ts-expect-error - s, - }); - - return new BlobEIP4844Transaction( - { - chainId: toBigInt(chainId), - nonce, - maxPriorityFeePerGas, - maxFeePerGas, - gasLimit, - // @ts-expect-error - to, - value, - data, - // @ts-expect-error - accessList: accessList ?? [], - maxFeePerBlobGas, - // @ts-expect-error - blobVersionedHashes, - v: v !== undefined ? toBigInt(v) : undefined, // EIP2930 supports v's with value 0 (empty Uint8Array) - r, - s, - }, - opts, - ); - } - - /** - * Creates a transaction from the network encoding of a blob transaction (with blobs/commitments/proof) - * @param serialized a buffer representing a serialized BlobTransactionNetworkWrapper - * @param opts any TxOptions defined - * @returns a BlobEIP4844Transaction - */ - - public static fromSerializedBlobTxNetworkWrapper( - serialized: Uint8Array, - opts?: TxOptions, - ): BlobEIP4844Transaction { - if (!opts || !opts.common) { - throw new Error('common instance required to validate versioned hashes'); - } - - if ( - !equalsBytes(serialized.subarray(0, 1), txTypeBytes(TransactionType.BlobEIP4844)) - ) { - throw new Error( - `Invalid serialized tx input: not an EIP-4844 transaction (wrong tx type, expected: ${ - TransactionType.BlobEIP4844 - }, received: ${toHex(serialized.subarray(0, 1))}`, - ); - } - - // Validate network wrapper - const networkTxValues = RLP.decode(serialized.subarray(1)); - if (networkTxValues.length !== 4) { - throw Error(`Expected 4 values in the deserialized network transaction`); - } - const [txValues, blobs, kzgCommitments, kzgProofs] = - networkTxValues as BlobEIP4844NetworkValuesArray; - - // Construct the tx but don't freeze yet, we will assign blobs etc once validated - const decodedTx = BlobEIP4844Transaction.fromValuesArray( - txValues as unknown as Uint8Array, - { - ...opts, - freeze: false, - }, - ); - if (decodedTx.to === undefined) { - throw Error('BlobEIP4844Transaction can not be send without a valid `to`'); - } - - const version = Number(opts.common.param('sharding', 'blobCommitmentVersionKzg')); - validateBlobTransactionNetworkWrapper( - decodedTx.blobVersionedHashes, - blobs, - kzgCommitments, - kzgProofs, - version, - ); - - // set the network blob data on the tx - decodedTx.blobs = blobs; - decodedTx.kzgCommitments = kzgCommitments; - decodedTx.kzgProofs = kzgProofs; - - // freeze the tx - const freeze = opts?.freeze ?? true; - if (freeze) { - Object.freeze(decodedTx); - } - - return decodedTx; - } - - /** - * The amount of gas paid for the data in this tx - */ - getDataFee(): bigint { - const extraCost = BigInt(getDataFeeEIP2930(this.accessList, this.common)); - if (this.cache.dataFee && this.cache.dataFee.hardfork === this.common.hardfork()) { - return this.cache.dataFee.value; - } - - const cost = BaseTransaction.prototype.getDataFee.bind(this)() + (extraCost ?? BIGINT_0); - - 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) - */ - getUpfrontCost(baseFee: bigint = 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 Bytes of the EIP-4844 transaction, in order. - * - * Format: [chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, value, data, - * access_list, max_fee_per_data_gas, blob_versioned_hashes, y_parity, r, s]`. - * - * Use {@link BlobEIP4844Transaction.serialize} to add a transaction to a block - * with {@link Block.fromValuesArray}. - * - * For an unsigned tx this method uses the empty Bytes values for the - * signature parameters `v`, `r` and `s` for encoding. For an EIP-155 compliant - * representation for external signing use {@link BlobEIP4844Transaction.getMessageToSign}. - */ - // @ts-expect-error - raw(): TxValuesArray { - return [ - // @ts-expect-error - bigIntToUnpaddedBytes(this.chainId), - // @ts-expect-error - bigIntToUnpaddedBytes(this.nonce), - // @ts-expect-error - bigIntToUnpaddedBytes(this.maxPriorityFeePerGas), - // @ts-expect-error - bigIntToUnpaddedBytes(this.maxFeePerGas), - // @ts-expect-error - bigIntToUnpaddedBytes(this.gasLimit), - // @ts-expect-error - this.to !== undefined ? this.to.bytes : new Uint8Array(0), - // @ts-expect-error - bigIntToUnpaddedBytes(this.value), - // @ts-expect-error - this.data, - // @ts-expect-error - this.accessList, - // @ts-expect-error - bigIntToUnpaddedBytes(this.maxFeePerBlobGas), - // @ts-expect-error - this.blobVersionedHashes, - // @ts-expect-error - this.v !== undefined ? bigIntToUnpaddedBytes(this.v) : new Uint8Array(0), - // @ts-expect-error - this.r !== undefined ? bigIntToUnpaddedBytes(this.r) : new Uint8Array(0), - // @ts-expect-error - this.s !== undefined ? bigIntToUnpaddedBytes(this.s) : new Uint8Array(0), - ]; - } - - /** - * Returns the serialized encoding of the EIP-4844 transaction. - * - * Format: `0x03 || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, - * access_list, max_fee_per_data_gas, blob_versioned_hashes, y_parity, r, s])`. - * - * 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. - */ - serialize(): Uint8Array { - return this._serialize(); - } - - private _serialize(base?: Input): Uint8Array { - return concatBytes(txTypeBytes(this.type), RLP.encode(base ?? this.raw())); - } - - /** - * @returns the serialized form of a blob transaction in the network wrapper format (used for gossipping mempool transactions over devp2p) - */ - serializeNetworkWrapper(): Uint8Array { - if ( - this.blobs === undefined || - this.kzgCommitments === undefined || - this.kzgProofs === undefined - ) { - throw new Error( - 'cannot serialize network wrapper without blobs, KZG commitments and KZG proofs provided', - ); - } - return this._serialize([this.raw(), this.blobs, this.kzgCommitments, this.kzgProofs]); - } - - /** - * Returns the raw serialized unsigned tx, 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() // use this for the HW wallet input - * ``` - */ - getMessageToSign(): Uint8Array { - return this._serialize(this.raw().slice(0, 11)); - } - - /** - * Returns the hashed serialized unsigned tx, 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. - */ - getHashedMessageToSign(): Uint8Array { - return keccak256(Buffer.from(this.getMessageToSign())); - } - - /** - * Computes a sha3-256 hash of the serialized tx. - * - * This method can only be used for signed txs (it throws otherwise). - * Use {@link BlobEIP4844Transaction.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(Buffer.from(this.serialize())); - } - return this.cache.hash; - } - - return keccak256(Buffer.from(this.serialize())); - } - - getMessageToVerifySignature(): Uint8Array { - return this.getHashedMessageToSign(); - } - - /** - * Returns the public key of the sender - */ - public getSenderPublicKey(): Uint8Array { - // @ts-expect-error - if (this.cache.senderPubKey !== undefined) { - // @ts-expect-error - return this.cache.senderPubKey; - } - - const msgHash = this.getMessageToVerifySignature(); - - const { v, r, s } = this; - - this.validateHighS(); - - try { - const sender = ecrecover( - msgHash, - v!, - bigIntToUnpaddedBytes(r!), - bigIntToUnpaddedBytes(s!), - this.supports(1559) ? this.common.chainId() : undefined, - ); - if (Object.isFrozen(this)) { - // @ts-expect-error - this.cache.senderPubKey = sender; - } - return sender; - } catch (e: any) { - const msg = this._errorMsg('Invalid Signature'); - throw new Error(msg); - } - } - - toJSON(): JsonTx { - const accessListJSON = getAccessListJSON(this.accessList); - return { - type: toHex(BigInt(this.type)), - nonce: toHex(this.nonce), - gasLimit: toHex(this.gasLimit), - to: this.to !== undefined ? this.to.toString() : undefined, - value: toHex(this.value), - data: toHex(this.data), - v: this.v !== undefined ? toHex(this.v) : undefined, - r: this.r !== undefined ? toHex(this.r) : undefined, - s: this.s !== undefined ? toHex(this.s) : undefined, - chainId: toHex(this.chainId), - maxPriorityFeePerGas: toHex(this.maxPriorityFeePerGas), - maxFeePerGas: toHex(this.maxFeePerGas), - accessList: accessListJSON, - maxFeePerDataGas: toHex(this.maxFeePerBlobGas), - versionedHashes: this.blobVersionedHashes.map(hash => toHex(hash)), - }; - } - - // @ts-expect-error - protected _processSignature(v: bigint, r: Uint8Array, s: Uint8Array): BlobEIP4844Transaction { - const opts = { ...this.txOptions, common: this.common }; - - return BlobEIP4844Transaction.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: @ethereumjs/util actually adds 27 to the value, the recovery bit is either 0 or 1. - r: toBigInt(r), - s: toBigInt(s), - maxFeePerBlobGas: this.maxFeePerBlobGas, - blobVersionedHashes: this.blobVersionedHashes, - blobs: this.blobs, - kzgCommitments: this.kzgCommitments, - kzgProofs: this.kzgProofs, - }, - opts, - ); - } - - /** - * 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()})`; - } - - /** - * @returns the number of blobs included with this transaction - */ - public numBlobs(): number { - return this.blobVersionedHashes.length; - } -} diff --git a/packages/web3/test/integration/web3-plugin-eip-4844.test.ts b/packages/web3/test/integration/web3-plugin-eip-4844.test.ts index b81b1fd5ed3..af55577f8c7 100644 --- a/packages/web3/test/integration/web3-plugin-eip-4844.test.ts +++ b/packages/web3/test/integration/web3-plugin-eip-4844.test.ts @@ -17,25 +17,24 @@ along with web3.js. If not, see . /* eslint-disable @typescript-eslint/no-magic-numbers */ -import { SupportedProviders, Web3, Web3PluginBase } from 'web3'; -import { TransactionFactory, Web3Account } from 'web3-eth-accounts'; +import { Transaction, TransactionFactory, Web3Account } from 'web3-eth-accounts'; +import { SupportedProviders, Web3, Web3PluginBase } from '../../src'; import { createAccount, createLocalAccount, getSystemTestProvider, waitForOpenConnection, -} from "web3.js/scripts/system_tests_utils"; -import { BlobEIP4844Transaction } from '../fixtures/tx-type-eip484'; +} from '../shared_fixtures/system_tests_utils'; +import { SomeNewTxTypeTransaction, TRANSACTION_TYPE } from '../fixtures/tx-type-15'; -export class Eip4844Plugin extends Web3PluginBase { - public pluginNamespace = 'tx'; - - constructor() { +class Eip4844Plugin extends Web3PluginBase { + public pluginNamespace = 'txType3'; + public constructor() { super(); - // @ts-expect-error - TransactionFactory.registerTransactionType( - 3, - BlobEIP4844Transaction, + TransactionFactory.registerTransactionType( + TRANSACTION_TYPE, + // @ts-expect-error fix type + SomeNewTxTypeTransaction, ); } } @@ -55,23 +54,25 @@ describe('Plugin 4844', () => { }); it('should create instance of the plugin', async () => { web3.registerPlugin(new Eip4844Plugin()); - const gasPrice = await web3.eth.getGasPrice(); - const sentTx = web3.eth.sendTransaction( - { - from: account1.address, - to: account2.address, - gas: BigInt(500000), - gasPrice, - maxFeePerGas: BigInt(500000), - value: '0x1', - type: 3, - }, - undefined, - { - checkRevertBeforeSending: false, - }, - ); - console.log('sentTx', sentTx); - console.log('sentTx res ', await sentTx); + 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 }, undefined, { + checkRevertBeforeSending: false, + }); + + 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"