From b74c304781a7b7a36e5f2ec92ec9542df5637ca6 Mon Sep 17 00:00:00 2001 From: Oleksii Kosynskyi Date: Tue, 10 Oct 2023 10:57:33 -0400 Subject: [PATCH 01/11] draft --- .husky/pre-push | 2 +- packages/web3-eth-accounts/src/tx/index.ts | 2 + .../src/tx/transactionFactory.ts | 38 +- .../test/unit/account.test.ts | 12 +- .../test/fixtures/tx-type-eip484/index.ts | 975 ++++++++++++++++++ .../integration/web3-plugin-eip-4844.test.ts | 77 ++ yarn.lock | 16 +- 7 files changed, 1112 insertions(+), 10 deletions(-) create mode 100644 packages/web3/test/fixtures/tx-type-eip484/index.ts create mode 100644 packages/web3/test/integration/web3-plugin-eip-4844.test.ts diff --git a/.husky/pre-push b/.husky/pre-push index 3b614330e02..771259b2097 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -yarn run lint +#yarn run lint diff --git a/packages/web3-eth-accounts/src/tx/index.ts b/packages/web3-eth-accounts/src/tx/index.ts index f02f90cfdbc..57eaa60ada5 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'; 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..6604b09055b 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'; + +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 * @@ -43,11 +59,13 @@ 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); } - 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 +84,15 @@ export class TransactionFactory { txOptions, ); } + 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); + } + throw new Error(`Tx instantiation with type ${txType} not supported`); } @@ -86,8 +113,15 @@ export class TransactionFactory { return AccessListEIP2930Transaction.fromSerializedTx(data, txOptions); case 2: return FeeMarketEIP1559Transaction.fromSerializedTx(data, txOptions); - default: + default: { + const ExtraTransaction = extraTxTypes.get(data[0]); + if (ExtraTransaction) { + // @ts-ignore + return ExtraTransaction.fromSerializedTx(data, txOptions); + } + 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/test/fixtures/tx-type-eip484/index.ts b/packages/web3/test/fixtures/tx-type-eip484/index.ts new file mode 100644 index 00000000000..9253e5d0630 --- /dev/null +++ b/packages/web3/test/fixtures/tx-type-eip484/index.ts @@ -0,0 +1,975 @@ +/* +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 new file mode 100644 index 00000000000..b81b1fd5ed3 --- /dev/null +++ b/packages/web3/test/integration/web3-plugin-eip-4844.test.ts @@ -0,0 +1,77 @@ +/* +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 { SupportedProviders, Web3, Web3PluginBase } from 'web3'; +import { TransactionFactory, Web3Account } from 'web3-eth-accounts'; +import { + createAccount, + createLocalAccount, + getSystemTestProvider, + waitForOpenConnection, +} from "web3.js/scripts/system_tests_utils"; +import { BlobEIP4844Transaction } from '../fixtures/tx-type-eip484'; + +export class Eip4844Plugin extends Web3PluginBase { + public pluginNamespace = 'tx'; + + constructor() { + super(); + // @ts-expect-error + TransactionFactory.registerTransactionType( + 3, + BlobEIP4844Transaction, + ); + } +} + +describe('Plugin 4844', () => { + 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 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); + }); +}); diff --git a/yarn.lock b/yarn.lock index f937f1f8418..e3012ba1959 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4021,13 +4021,20 @@ cross-fetch@^2.1.0: node-fetch "^2.6.7" whatwg-fetch "^2.0.4" -cross-fetch@^3.0.4, cross-fetch@^3.1.5: +cross-fetch@^3.0.4: version "3.1.5" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== dependencies: node-fetch "2.6.7" +cross-fetch@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" + integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== + dependencies: + node-fetch "^2.6.12" + cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -8593,6 +8600,13 @@ node-fetch@2.6.7, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.12: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-gyp-build@4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.4.0.tgz#42e99687ce87ddeaf3a10b99dc06abc11021f3f4" From 12d325a3f7ef8ae07fb3b77ecfb94228d491c38e Mon Sep 17 00:00:00 2001 From: Oleksii Kosynskyi Date: Thu, 12 Oct 2023 10:57:03 -0400 Subject: [PATCH 02/11] 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" From 5a63e089b4a9113522e80967916347c6992987b9 Mon Sep 17 00:00:00 2001 From: Oleksii Kosynskyi Date: Thu, 12 Oct 2023 11:00:03 -0400 Subject: [PATCH 03/11] lint --- .husky/pre-push | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.husky/pre-push b/.husky/pre-push index 771259b2097..3b614330e02 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -#yarn run lint +yarn run lint From 969b55fc72cb2cfdd0fa431d8cf6d6348f0904e1 Mon Sep 17 00:00:00 2001 From: Oleksii Kosynskyi Date: Thu, 12 Oct 2023 11:22:55 -0400 Subject: [PATCH 04/11] add documentation --- .../web3_plugin_guide/plugin_authors.md | 25 +++++++++++++++++++ .../src/tx/transactionFactory.ts | 4 +-- .../integration/web3-plugin-eip-4844.test.ts | 6 +---- 3 files changed, 28 insertions(+), 7 deletions(-) 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 `@ = new Map(); +const extraTxTypes: Map> = new Map(); // eslint-disable-next-line @typescript-eslint/no-extraneous-class export class TransactionFactory { @@ -41,7 +41,7 @@ export class TransactionFactory { return Number(uint8ArrayToBigInt(toUint8Array(txType))); } - public static registerTransactionType( + public static registerTransactionType>( type: Numbers, txClass: NewTxTypeClass, ) { 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 af55577f8c7..02df781436c 100644 --- a/packages/web3/test/integration/web3-plugin-eip-4844.test.ts +++ b/packages/web3/test/integration/web3-plugin-eip-4844.test.ts @@ -31,11 +31,7 @@ class Eip4844Plugin extends Web3PluginBase { public pluginNamespace = 'txType3'; public constructor() { super(); - TransactionFactory.registerTransactionType( - TRANSACTION_TYPE, - // @ts-expect-error fix type - SomeNewTxTypeTransaction, - ); + TransactionFactory.registerTransactionType(TRANSACTION_TYPE, SomeNewTxTypeTransaction); } } From 0fd2602a486b1bb0e744f801adb6a96952b2818a Mon Sep 17 00:00:00 2001 From: Oleksii Kosynskyi Date: Thu, 12 Oct 2023 11:35:47 -0400 Subject: [PATCH 05/11] fix unit tests --- packages/web3-eth/test/unit/default_transaction_builder.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 }), From 6c16beafa2760b866e30ee3bf0a54e33d25eff9d Mon Sep 17 00:00:00 2001 From: Oleksii Kosynskyi Date: Thu, 12 Oct 2023 12:08:40 -0400 Subject: [PATCH 06/11] add unit tests --- .../src/tx/transactionFactory.ts | 2 +- .../test/unit/tx/registerNewTx.test.ts | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 packages/web3-eth-accounts/test/unit/tx/registerNewTx.test.ts diff --git a/packages/web3-eth-accounts/src/tx/transactionFactory.ts b/packages/web3-eth-accounts/src/tx/transactionFactory.ts index 718d7152850..e4da9fcfe6e 100644 --- a/packages/web3-eth-accounts/src/tx/transactionFactory.ts +++ b/packages/web3-eth-accounts/src/tx/transactionFactory.ts @@ -108,7 +108,7 @@ export class TransactionFactory { case 2: return FeeMarketEIP1559Transaction.fromSerializedTx(data, txOptions); default: { - const ExtraTransaction = extraTxTypes.get(data[0]); + const ExtraTransaction = extraTxTypes.get(Number(data[0])); if (ExtraTransaction?.fromSerializedTx) { return ExtraTransaction.fromSerializedTx( data, 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..f694729af00 --- /dev/null +++ b/packages/web3-eth-accounts/test/unit/tx/registerNewTx.test.ts @@ -0,0 +1,51 @@ +/* +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-ignore + class SomeNewTxType extends BaseTransaction { + constructor(txData: TxData, opts: TxOptions = {}) { + super(txData, opts); + // @ts-ignore + this._type = 20; + } + 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', + ); + }); +}); From 6c172319024f4dde53edd25718953045c982fbff Mon Sep 17 00:00:00 2001 From: Oleksii Kosynskyi Date: Thu, 12 Oct 2023 12:11:24 -0400 Subject: [PATCH 07/11] add unit tests --- .../web3-eth-accounts/test/unit/tx/registerNewTx.test.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/web3-eth-accounts/test/unit/tx/registerNewTx.test.ts b/packages/web3-eth-accounts/test/unit/tx/registerNewTx.test.ts index f694729af00..8b4da78a527 100644 --- a/packages/web3-eth-accounts/test/unit/tx/registerNewTx.test.ts +++ b/packages/web3-eth-accounts/test/unit/tx/registerNewTx.test.ts @@ -22,12 +22,10 @@ import { TxData, TxOptions } from '../../../src/tx'; describe('Register new TX', () => { it('validateCannotExceedMaxInteger()', () => { const TYPE = 20; - // @ts-ignore + // @ts-expect-error not implement all methods class SomeNewTxType extends BaseTransaction { - constructor(txData: TxData, opts: TxOptions = {}) { - super(txData, opts); - // @ts-ignore - this._type = 20; + public constructor(txData: TxData, opts: TxOptions = {}) { + super({ ...txData, type: TYPE }, opts); } public static fromTxData() { return 'new fromTxData'; From c6682c17608e584afb3d6c47a7738d7f24e97fc7 Mon Sep 17 00:00:00 2001 From: Oleksii Kosynskyi Date: Thu, 12 Oct 2023 17:26:30 -0400 Subject: [PATCH 08/11] increase coverage --- .../test/unit/tx/staticMethods.test.ts | 24 +++++++++++++++++++ .../test/unit/tx/transactionFactory.test.ts | 7 ++++++ ...844.test.ts => web3-plugin-add-tx.test.ts} | 4 ++-- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 packages/web3-eth-accounts/test/unit/tx/staticMethods.test.ts rename packages/web3/test/integration/{web3-plugin-eip-4844.test.ts => web3-plugin-add-tx.test.ts} (96%) 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..00cd27aa76b 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-ignore + 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/test/integration/web3-plugin-eip-4844.test.ts b/packages/web3/test/integration/web3-plugin-add-tx.test.ts similarity index 96% rename from packages/web3/test/integration/web3-plugin-eip-4844.test.ts rename to packages/web3/test/integration/web3-plugin-add-tx.test.ts index 02df781436c..38708537e72 100644 --- a/packages/web3/test/integration/web3-plugin-eip-4844.test.ts +++ b/packages/web3/test/integration/web3-plugin-add-tx.test.ts @@ -35,7 +35,7 @@ class Eip4844Plugin extends Web3PluginBase { } } -describe('Plugin 4844', () => { +describe('Add New Tx as a Plugin', () => { let web3: Web3; let clientUrl: string | SupportedProviders; let account1: Web3Account; @@ -48,7 +48,7 @@ describe('Plugin 4844', () => { web3.eth.accounts.wallet.add(account1); await waitForOpenConnection(web3.eth); }); - it('should create instance of the plugin', async () => { + it('should receive correct type of tx', async () => { web3.registerPlugin(new Eip4844Plugin()); const tx = { from: account1.address, From d96ceaabfbbefe75e4f9edd599a3235acb4ff2e5 Mon Sep 17 00:00:00 2001 From: Oleksii Kosynskyi Date: Thu, 12 Oct 2023 17:29:21 -0400 Subject: [PATCH 09/11] increase coverage --- .../web3-eth-accounts/test/unit/tx/transactionFactory.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 00cd27aa76b..72aa0efd955 100644 --- a/packages/web3-eth-accounts/test/unit/tx/transactionFactory.test.ts +++ b/packages/web3-eth-accounts/test/unit/tx/transactionFactory.test.ts @@ -121,7 +121,7 @@ describe('[TransactionFactory]: Basic functions', () => { it('fromBlockBodyData() -> error case', () => { expect(() => { - // @ts-ignore + // @ts-expect-error incorrect param type TransactionFactory.fromBlockBodyData(''); }).toThrow(); }); From 9f6f2c78e9347d26b6bf67a8766b1e3d23395eb5 Mon Sep 17 00:00:00 2001 From: Oleksii Kosynskyi Date: Thu, 12 Oct 2023 17:38:25 -0400 Subject: [PATCH 10/11] fix changelog syncing --- CHANGELOG.md | 42 ++++++++++++++++++- .../test/unit/common/utils.test.ts | 12 +++--- packages/web3-validator/CHANGELOG.md | 2 +- 3 files changed, 48 insertions(+), 8 deletions(-) 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/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-validator/CHANGELOG.md b/packages/web3-validator/CHANGELOG.md index fea00e98075..e63b8a603f3 100644 --- a/packages/web3-validator/CHANGELOG.md +++ b/packages/web3-validator/CHANGELOG.md @@ -149,6 +149,6 @@ Documentation: ## [Unreleased] -## Fixed +### Fixed - Multi-dimensional arrays are now handled properly when parsing ABIs From 6eb3cd9b89a7064682cfb2584941bbb05e52555f Mon Sep 17 00:00:00 2001 From: Oleksii Kosynskyi Date: Mon, 16 Oct 2023 15:44:30 -0400 Subject: [PATCH 11/11] fix test --- packages/web3/test/integration/web3-plugin-add-tx.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/web3/test/integration/web3-plugin-add-tx.test.ts b/packages/web3/test/integration/web3-plugin-add-tx.test.ts index 38708537e72..d1971b7f4ef 100644 --- a/packages/web3/test/integration/web3-plugin-add-tx.test.ts +++ b/packages/web3/test/integration/web3-plugin-add-tx.test.ts @@ -58,9 +58,7 @@ describe('Add New Tx as a Plugin', () => { maxPriorityFeePerGas: BigInt(5000000), maxFeePerGas: BigInt(5000000), }; - const sub = web3.eth.sendTransaction({ ...tx }, undefined, { - checkRevertBeforeSending: false, - }); + const sub = web3.eth.sendTransaction(tx); const waitForEvent: Promise = new Promise(resolve => { // eslint-disable-next-line @typescript-eslint/no-floating-promises