/* 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 { Common as CommonType, Numbers } from 'web3-types'; import { bytesToHex } from 'web3-utils'; import { MAX_INTEGER, MAX_UINT64, SECP256K1_ORDER_DIV_2, secp256k1 } from './constants.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, FeeMarketEIP1559TxData, FeeMarketEIP1559ValuesArray, JsonTx, TxData, TxOptions, TxValuesArray, } from './types.js'; import { Capability, ECDSASignature } from './types.js'; import { Address } from './address.js'; import { checkMaxInitCodeSize } from './utils.js'; interface TransactionCache { hash: Uint8Array | undefined; dataFee?: { value: bigint; hardfork: string | Hardfork; }; } /** * This base class will likely be subject to further * refactoring along the introduction of additional tx types * on the Ethereum network. * * It is therefore not recommended to use directly. */ export abstract class BaseTransaction { private readonly _type: number; public readonly nonce: bigint; public readonly gasLimit: bigint; public readonly to?: Address; public readonly value: bigint; public readonly data: Uint8Array; public readonly v?: bigint; public readonly r?: bigint; public readonly s?: bigint; public readonly common!: Common; protected cache: TransactionCache = { hash: undefined, dataFee: undefined, }; protected readonly txOptions: TxOptions; /** * List of tx type defining EIPs, * e.g. 1559 (fee market) and 2930 (access lists) * for FeeMarketEIP1559Transaction objects */ protected activeCapabilities: number[] = []; /** * The default chain the tx falls back to if no Common * is provided and if the chain can't be derived from * a passed in chainId (only EIP-2718 typed txs) or * EIP-155 signature (legacy txs). * * @hidden */ protected DEFAULT_CHAIN = Chain.Mainnet; /** * 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: string | Hardfork = Hardfork.Merge; public constructor( txData: TxData | AccessListEIP2930TxData | FeeMarketEIP1559TxData, opts: TxOptions, ) { const { nonce, gasLimit, to, value, data, v, r, s, type } = txData; this._type = Number(uint8ArrayToBigInt(toUint8Array(type))); this.txOptions = opts; const toB = toUint8Array(to === '' ? '0x' : to); const vB = toUint8Array(v === '' ? '0x' : v); const rB = toUint8Array(r === '' ? '0x' : r); const sB = toUint8Array(s === '' ? '0x' : s); this.nonce = uint8ArrayToBigInt(toUint8Array(nonce === '' ? '0x' : nonce)); this.gasLimit = uint8ArrayToBigInt(toUint8Array(gasLimit === '' ? '0x' : gasLimit)); this.to = toB.length > 0 ? new Address(toB) : undefined; this.value = uint8ArrayToBigInt(toUint8Array(value === '' ? '0x' : value)); this.data = toUint8Array(data === '' ? '0x' : data); this.v = vB.length > 0 ? uint8ArrayToBigInt(vB) : undefined; this.r = rB.length > 0 ? uint8ArrayToBigInt(rB) : undefined; this.s = sB.length > 0 ? uint8ArrayToBigInt(sB) : undefined; this._validateCannotExceedMaxInteger({ value: this.value, r: this.r, s: this.s }); // geth limits gasLimit to 2^64-1 this._validateCannotExceedMaxInteger({ gasLimit: this.gasLimit }, 64); // EIP-2681 limits nonce to 2^64-1 (cannot equal 2^64-1) this._validateCannotExceedMaxInteger({ nonce: this.nonce }, 64, true); // eslint-disable-next-line no-null/no-null const createContract = this.to === undefined || this.to === null; const allowUnlimitedInitCodeSize = opts.allowUnlimitedInitCodeSize ?? false; const common = opts.common ?? this._getCommon(); if (createContract && common.isActivatedEIP(3860) && !allowUnlimitedInitCodeSize) { checkMaxInitCodeSize(common, this.data.length); } } /** * Returns the transaction type. * * Note: legacy txs will return tx type `0`. */ public get type() { return this._type; } /** * Checks if a tx type defining capability is active * on a tx, for example the EIP-1559 fee market mechanism * or the EIP-2930 access list feature. * * Note that this is different from the tx type itself, * so EIP-2930 access lists can very well be active * on an EIP-1559 tx for example. * * This method can be useful for feature checks if the * tx type is unknown (e.g. when instantiated with * the tx factory). * * See `Capabilites` in the `types` module for a reference * on all supported capabilities. */ public supports(capability: Capability) { return this.activeCapabilities.includes(capability); } /** * Checks if the transaction has the minimum amount of gas required * (DataFee + TxFee + Creation Fee). */ public validate(): boolean; public validate(stringError: false): boolean; public validate(stringError: true): string[]; public validate(stringError = false): boolean | string[] { const errors = []; if (this.getBaseFee() > this.gasLimit) { errors.push( `gasLimit is too low. given ${this.gasLimit}, need at least ${this.getBaseFee()}`, ); } if (this.isSigned() && !this.verifySignature()) { errors.push('Invalid Signature'); } return stringError ? errors : errors.length === 0; } protected _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); } } /** * EIP-2: All transaction signatures whose s-value is greater than secp256k1n/2are considered invalid. * Reasoning: https://ethereum.stackexchange.com/a/55728 */ protected _validateHighS() { 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); } } /** * The minimum amount of gas the tx must have (DataFee + TxFee + Creation Fee) */ public getBaseFee(): bigint { const txFee = this.common.param('gasPrices', 'tx'); let fee = this.getDataFee(); if (txFee) fee += txFee; if (this.common.gteHardfork('homestead') && this.toCreationAddress()) { const txCreationFee = this.common.param('gasPrices', 'txCreation'); if (txCreationFee) fee += txCreationFee; } return fee; } /** * The amount of gas paid for the data in this tx */ public getDataFee(): bigint { const txDataZero = this.common.param('gasPrices', 'txDataZero'); const txDataNonZero = this.common.param('gasPrices', 'txDataNonZero'); let cost = BigInt(0); // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < this.data.length; i += 1) { // eslint-disable-next-line @typescript-eslint/no-unused-expressions, no-unused-expressions this.data[i] === 0 ? (cost += txDataZero) : (cost += txDataNonZero); } // eslint-disable-next-line no-null/no-null if ((this.to === undefined || this.to === null) && this.common.isActivatedEIP(3860)) { const dataLength = BigInt(Math.ceil(this.data.length / 32)); const initCodeCost = this.common.param('gasPrices', 'initCodeWordCost') * dataLength; cost += initCodeCost; } return cost; } /** * The up front amount that an account must have for this transaction to be valid */ public abstract getUpfrontCost(): bigint; /** * If the tx's `to` is to the creation address */ public toCreationAddress(): boolean { return this.to === undefined || this.to.buf.length === 0; } /** * Returns a Uint8Array Array of the raw Uint8Arrays of this transaction, in order. * * Use {@link BaseTransaction.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 BaseTransaction.getMessageToSign}. */ public abstract raw(): | TxValuesArray | AccessListEIP2930ValuesArray | FeeMarketEIP1559ValuesArray; /** * Returns the encoding of the transaction. */ public abstract serialize(): Uint8Array; // Returns the unsigned tx (hashed or raw), which is used to sign the transaction. // // Note: do not use code docs here since VS Studio is then not able to detect the // comments from the inherited methods public abstract getMessageToSign(hashMessage: false): Uint8Array | Uint8Array[]; public abstract getMessageToSign(hashMessage?: true): Uint8Array; public abstract hash(): Uint8Array; public abstract getMessageToVerifySignature(): Uint8Array; public isSigned(): boolean { const { v, r, s } = this; if (v === undefined || r === undefined || s === undefined) { return false; } return true; } /** * Determines if the signature is valid */ public verifySignature(): boolean { try { // Main signature verification is done in `getSenderPublicKey()` const publicKey = this.getSenderPublicKey(); return unpadUint8Array(publicKey).length !== 0; } catch (e: any) { return false; } } /** * Returns the sender's address */ public getSenderAddress(): Address { return new Address(Address.publicToAddress(this.getSenderPublicKey())); } /** * Returns the public key of the sender */ public abstract getSenderPublicKey(): Uint8Array; /** * Signs a transaction. * * Note that the signed tx is returned as a new object, * use as follows: * ```javascript * const signedTx = tx.sign(privateKey) * ``` */ public sign(privateKey: Uint8Array): TransactionObject { if (privateKey.length !== 32) { const msg = this._errorMsg('Private key must be 32 bytes in length.'); throw new Error(msg); } // Hack for the constellation that we have got a legacy tx after spuriousDragon with a non-EIP155 conforming signature // and want to recreate a signature (where EIP155 should be applied) // Leaving this hack lets the legacy.spec.ts -> sign(), verifySignature() test fail // 2021-06-23 let hackApplied = false; if ( this.type === 0 && this.common.gteHardfork('spuriousDragon') && !this.supports(Capability.EIP155ReplayProtection) ) { this.activeCapabilities.push(Capability.EIP155ReplayProtection); hackApplied = true; } const msgHash = this.getMessageToSign(true); const { v, r, s } = this._ecsign(msgHash, privateKey); const tx = this._processSignature(v, r, s); // Hack part 2 if (hackApplied) { const index = this.activeCapabilities.indexOf(Capability.EIP155ReplayProtection); if (index > -1) { this.activeCapabilities.splice(index, 1); } } return tx; } /** * Returns an object with the JSON representation of the transaction */ public abstract toJSON(): JsonTx; // Accept the v,r,s values from the `sign` method, and convert this into a TransactionObject protected abstract _processSignature( v: bigint, r: Uint8Array, s: Uint8Array, ): TransactionObject; /** * Does chain ID checks on common and returns a common * to be used on instantiation * @hidden * * @param common - {@link Common} instance from tx options * @param chainId - Chain ID from tx options (typed txs) or signature (legacy tx) */ protected _getCommon(common?: Common, chainId?: Numbers) { // TODO: this function needs to be reviewed and the code to be more clean // check issue https://github.com/web3/web3.js/issues/6666 // Chain ID provided if (chainId !== undefined) { const chainIdBigInt = uint8ArrayToBigInt(toUint8Array(chainId)); if (common) { if (common.chainId() !== chainIdBigInt) { const msg = this._errorMsg( 'The chain ID does not match the chain ID of Common', ); throw new Error(msg); } // Common provided, chain ID does match // -> Return provided Common return common.copy(); } if (Common.isSupportedChainId(chainIdBigInt)) { // No Common, chain ID supported by Common // -> Instantiate Common with chain ID return new Common({ chain: chainIdBigInt, hardfork: this.DEFAULT_HARDFORK }); } // No Common, chain ID not supported by Common // -> Instantiate custom Common derived from DEFAULT_CHAIN return Common.custom( { name: 'custom-chain', networkId: chainIdBigInt, chainId: chainIdBigInt, }, { baseChain: this.DEFAULT_CHAIN, hardfork: this.DEFAULT_HARDFORK }, ); } // No chain ID provided // -> return Common provided or create new default Common if (common?.copy && typeof common?.copy === 'function') { return common.copy(); } // TODO: Recheck this next block when working on https://github.com/web3/web3.js/issues/6666 // This block is to handle when `chainId` was not passed and the `common` object does not have `copy()` // If it was meant to be unsupported to process `common` in this case, an exception should be thrown instead of the following block if (common) { const hardfork = typeof common.hardfork === 'function' ? common.hardfork() : // eslint-disable-next-line @typescript-eslint/unbound-method (common.hardfork as unknown as string); return Common.custom( { name: 'custom-chain', networkId: common.networkId ? common.networkId() : BigInt((common as unknown as CommonType).customChain?.networkId) ?? undefined, chainId: common.chainId ? common.chainId() : BigInt((common as unknown as CommonType).customChain?.chainId) ?? undefined, }, { baseChain: this.DEFAULT_CHAIN, hardfork: hardfork || this.DEFAULT_HARDFORK, }, ); } return new Common({ chain: this.DEFAULT_CHAIN, hardfork: this.DEFAULT_HARDFORK }); } /** * Validates that an object with BigInt values cannot exceed the specified bit limit. * @param values Object containing string keys and BigInt values * @param bits Number of bits to check (64 or 256) * @param cannotEqual Pass true if the number also cannot equal one less the maximum value */ protected _validateCannotExceedMaxInteger( values: { [key: string]: bigint | undefined }, bits = 256, cannotEqual = false, ) { for (const [key, value] of Object.entries(values)) { switch (bits) { case 64: if (cannotEqual) { if (value !== undefined && value >= MAX_UINT64) { const msg = this._errorMsg( `${key} cannot equal or exceed MAX_UINT64 (2^64-1), given ${value}`, ); throw new Error(msg); } } else if (value !== undefined && value > MAX_UINT64) { const msg = this._errorMsg( `${key} cannot exceed MAX_UINT64 (2^64-1), given ${value}`, ); throw new Error(msg); } break; case 256: if (cannotEqual) { if (value !== undefined && value >= MAX_INTEGER) { const msg = this._errorMsg( `${key} cannot equal or exceed MAX_INTEGER (2^256-1), given ${value}`, ); throw new Error(msg); } } else if (value !== undefined && value > MAX_INTEGER) { const msg = this._errorMsg( `${key} cannot exceed MAX_INTEGER (2^256-1), given ${value}`, ); throw new Error(msg); } break; default: { const msg = this._errorMsg('unimplemented bits value'); throw new Error(msg); } } } } protected static _validateNotArray(values: { [key: string]: any }) { const txDataKeys = [ 'nonce', 'gasPrice', 'gasLimit', 'to', 'value', 'data', 'v', 'r', 's', 'type', 'baseFee', 'maxFeePerGas', 'chainId', ]; for (const [key, value] of Object.entries(values)) { if (txDataKeys.includes(key)) { if (Array.isArray(value)) { throw new Error(`${key} cannot be an array`); } } } } /** * Return a compact error string representation of the object */ public abstract errorStr(): string; /** * Internal helper function to create an annotated error message * * @param msg Base error message * @hidden */ protected abstract _errorMsg(msg: string): string; /** * Returns the shared error postfix part for _error() method * tx type implementations. */ protected _getSharedErrorPostfix() { let hash = ''; try { hash = this.isSigned() ? bytesToHex(this.hash()) : 'not available (unsigned)'; } catch (e: any) { hash = 'error'; } let isSigned = ''; try { isSigned = this.isSigned().toString(); } catch (e: any) { hash = 'error'; } let hf = ''; try { hf = this.common.hardfork(); } catch (e: any) { hf = 'error'; } let postfix = `tx type=${this.type} hash=${hash} nonce=${this.nonce} value=${this.value} `; postfix += `signed=${isSigned} hf=${hf}`; return postfix; } // eslint-disable-next-line class-methods-use-this private _ecsign(msgHash: Uint8Array, privateKey: Uint8Array, chainId?: bigint): ECDSASignature { const signature = secp256k1.sign(msgHash, privateKey); const signatureBytes = signature.toCompactRawBytes(); const r = signatureBytes.subarray(0, 32); const s = signatureBytes.subarray(32, 64); const v = chainId === undefined ? BigInt(signature.recovery! + 27) : BigInt(signature.recovery! + 35) + BigInt(chainId) * BigInt(2); 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 {} }