UNPKG

@vechain/sdk-core

Version:

This module is crafted for dApp development and various blockchain operations that seamlessly unfold offline

1,064 lines (1,009 loc) 40.6 kB
import * as nc_utils from '@noble/curves/abstract/utils'; import { InvalidDataType, InvalidSecp256k1PrivateKey, InvalidTransactionField, NotDelegatedTransaction, UnavailableTransactionField } from '@vechain/sdk-errors'; import { Secp256k1 } from '../secp256k1'; import { Address, BufferKind, CompactFixedHexBlobKind, Hex, HexBlobKind, HexUInt, NumericKind, OptionalFixedHexBlobKind, type RLPProfile, RLPProfiler, type RLPValidObject, Units, VTHO } from '../vcdm'; import { Blake2b256 } from '../vcdm/hash/Blake2b256'; import type { TransactionClause } from './TransactionClause'; import { TransactionType } from './TransactionType'; import { type TransactionBody } from './TransactionBody'; /** * Represents an immutable transaction entity. */ class Transaction { /** * Represent the block reference length in bytes. */ private static readonly BLOCK_REF_LENGTH = 8; /** * A collection of constants used for gas calculations in transactions. * * Properties * - `TX_GAS` - The base gas cost for a transaction. * - `CLAUSE_GAS` - The gas cost for executing a clause in a transaction. * - `CLAUSE_GAS_CONTRACT_CREATION` - The gas cost for creating a contract via a clause. * - `ZERO_GAS_DATA` - The gas cost for transmitting zero bytes of data. * - `NON_ZERO_GAS_DATA` - The gas cost for transmitting non-zero bytes of data. */ public static readonly GAS_CONSTANTS = { TX_GAS: 5000n, CLAUSE_GAS: 16000n, CLAUSE_GAS_CONTRACT_CREATION: 48000n, ZERO_GAS_DATA: 4n, NON_ZERO_GAS_DATA: 68n }; /** * Represents the prefix for raw EIP-1559 transaction type. */ private static readonly EIP1559_TX_TYPE_PREFIX = 0x51; /** * RLP_FIELDS is an array of objects that defines the structure and encoding scheme * for various components in a transaction using Recursive Length Prefix (RLP) encoding. * Each object in the array represents a field in the transaction, specifying its name and kind. * The `kind` attribute is an instance of an RLP coder that determines how the field is encoded. * * Properties * - `chainTag` - Represent the id of the chain the transaction is sent to. * - `blockRef` - Represent the last block of the chain the transaction is sent to. * - `expiration` - Represent the expiration date of the transaction. * - `clauses` - List of clause objects, each containing: * - `to` - Represent the destination of the transaction. * - `value` - Represent the 'wei' quantity (VET or VTHO) value the transaction is worth. * - `data` - Represent the content of the transaction. * - `gasPriceCoef` - Represent the gas price coefficient of the transaction. * - `gas` - Represent the gas limit of the transaction. * - `dependsOn` - Represent the hash of the transaction the current transaction depends on. * - `nonce` - Represent the nonce of the transaction. * - `reserved` - Reserved field. */ private static readonly LEGACY_RLP_FIELDS = [ { name: 'chainTag', kind: new NumericKind(1) }, { name: 'blockRef', kind: new CompactFixedHexBlobKind(8) }, { name: 'expiration', kind: new NumericKind(4) }, { name: 'clauses', kind: { item: [ { name: 'to', kind: new OptionalFixedHexBlobKind(20) }, { name: 'value', kind: new NumericKind(32) }, { name: 'data', kind: new HexBlobKind() } ] } }, { name: 'gasPriceCoef', kind: new NumericKind(1) }, { name: 'gas', kind: new NumericKind(8) }, { name: 'dependsOn', kind: new OptionalFixedHexBlobKind(32) }, { name: 'nonce', kind: new NumericKind(8) }, { name: 'reserved', kind: { item: new BufferKind() } } ]; /** * Represents the RLP fields for EIP-1559 transactions. */ private static readonly EIP1559_RLP_FIELDS = [ { name: 'chainTag', kind: new NumericKind(1) }, { name: 'blockRef', kind: new CompactFixedHexBlobKind(8) }, { name: 'expiration', kind: new NumericKind(4) }, { name: 'clauses', kind: { item: [ { name: 'to', kind: new OptionalFixedHexBlobKind(20) }, { name: 'value', kind: new NumericKind(32) }, { name: 'data', kind: new HexBlobKind() } ] } }, { name: 'maxPriorityFeePerGas', kind: new NumericKind(32) }, { name: 'maxFeePerGas', kind: new NumericKind(32) }, { name: 'gas', kind: new NumericKind(8) }, { name: 'dependsOn', kind: new OptionalFixedHexBlobKind(32) }, { name: 'nonce', kind: new NumericKind(8) }, { name: 'reserved', kind: { item: new BufferKind() } } ]; /** * Represent the Recursive Length Prefix (RLP) of the transaction features. * * Properties * - `name` - A string indicating the name of the field in the RLP structure. * - `kind` - RLP profile type. */ private static readonly RLP_FEATURES = { name: 'reserved.features', kind: new NumericKind(4) }; /** * Represents a Recursive Length Prefix (RLP) of the transaction signature. * * Properties * - `name` - A string indicating the name of the field in the RLP structure. * - `kind` - RLP profile type. */ private static readonly RLP_SIGNATURE = { name: 'signature', kind: new BufferKind() }; /** * Represents a Recursive Length Prefix (RLP) of the signed transaction. * * Properties * - `name` - A string indicating the name of the field in the RLP structure. * - `kind` - RLP profile type. */ private static readonly RLP_SIGNED_LEGACY_TRANSACTION_PROFILE: RLPProfile = { name: 'tx', kind: Transaction.LEGACY_RLP_FIELDS.concat([ Transaction.RLP_SIGNATURE ]) }; /** * Represents a Recursive Length Prefix (RLP) of the unsigned transaction. * * Properties * - `name` - A string indicating the name of the field in the RLP structure. * - `kind` - RLP profile type. */ private static readonly RLP_UNSIGNED_LEGACY_TRANSACTION_PROFILE: RLPProfile = { name: 'tx', kind: Transaction.LEGACY_RLP_FIELDS }; /** * Represents a Recursive Length Prefix (RLP) of the signed EIP-1559 transaction. * * Properties * - `name` - A string indicating the name of the field in the RLP structure. * - `kind` - RLP profile type. */ private static readonly RLP_SIGNED_EIP1559_TRANSACTION_PROFILE: RLPProfile = { name: 'tx', kind: Transaction.EIP1559_RLP_FIELDS.concat([ Transaction.RLP_SIGNATURE ]) }; /** * Represents a Recursive Length Prefix (RLP) of the unsigned EIP-1559 transaction. * * Properties * - `name` - A string indicating the name of the field in the RLP structure. * - `kind` - RLP profile type. */ private static readonly RLP_UNSIGNED_EIP1559_TRANSACTION_PROFILE: RLPProfile = { name: 'tx', kind: Transaction.EIP1559_RLP_FIELDS }; /** * It represents the content of the transaction. */ public readonly body: TransactionBody; /** * It represents the type of the transaction. */ public readonly transactionType: TransactionType; /** * It represents the signature of the transaction content. */ public readonly signature?: Uint8Array; /** * Creates a new instance of the class with the specified transaction body and optional signature. * * @param {TransactionBody} body The transaction body to be used. * @param {Uint8Array} [signature] The optional signature for the transaction. */ protected constructor( body: TransactionBody, type: TransactionType, signature?: Uint8Array ) { this.body = body; this.transactionType = type; this.signature = signature; } // ********** GET COMPUTED PROPERTIES ********** /** * Get the gas payer's address if the transaction is delegated. * * If the transaction is delegated and a signature is available, this method recovers * the gas payer parameter from the signature and subsequently recovers the gas payer's public key * to derive the gas payer's address. * * @return {Address} The address of the gas payer. * @throws {UnavailableTransactionField} If the transaction is delegated but the signature is missing. * @throws {NotDelegatedTransaction} If the transaction is not delegated. * * @remarks Security auditable method, depends on * - {@link Address.ofPublicKey}; * - {@link Secp256k1.recover}; * - {@link Transaction.getTransactionHash}. */ public get gasPayer(): Address { if (this.isDelegated) { if (this.signature !== undefined) { // Recover the gas payer param from the signature const gasPayer = this.signature.slice( Secp256k1.SIGNATURE_LENGTH, this.signature.length ); // Recover the gas payer's public key. const gasPayerPublicKey = Secp256k1.recover( this.getTransactionHash(this.origin).bytes, gasPayer ); return Address.ofPublicKey(gasPayerPublicKey); } throw new UnavailableTransactionField( 'Transaction.gasPayer()', 'missing gas payer signature', { fieldName: 'gasPayer' } ); } throw new NotDelegatedTransaction( 'Transaction.gasPayer()', 'not delegated transaction', undefined ); } /** * Get the encoded bytes as a Uint8Array. * The encoding is determined by whether the data is signed. * * @return {Uint8Array} The encoded byte array. * * @see decode */ public get encoded(): Uint8Array { return this.encode(this.isSigned); } /** * Get transaction ID. * * The ID is the Blake2b256 hash of the transaction's signature * concatenated with the origin's address. * If the transaction is not signed, * it throws an UnavailableTransactionField error. * * @return {Blake2b256} The concatenated hash of the signature * and origin if the transaction is signed. * @throws {UnavailableTransactionField} If the transaction is not signed. * * @remarks Security auditable method, depends on * - {@link Blake2b256.of} */ public get id(): Blake2b256 { if (this.isSigned) { return Blake2b256.of( nc_utils.concatBytes( this.getTransactionHash().bytes, this.origin.bytes ) ); } throw new UnavailableTransactionField( 'Transaction.id()', 'not signed transaction: id unavailable', { fieldName: 'id' } ); } /** * Return the intrinsic gas required for this transaction. * * @return {VTHO} The computed intrinsic gas for the transaction. */ public get intrinsicGas(): VTHO { return Transaction.intrinsicGas(this.body.clauses); } /** * Returns `true` if the transaction is delegated, otherwise `false`. * * @return {boolean} `true` if the transaction is delegated, * otherwise `false`. */ public get isDelegated(): boolean { return Transaction.isDelegated(this.body); } /** * Return `true` if the signature is defined and complete, otherwise `false`. * * @return {boolean} return `true` if the signature is defined and complete, otherwise `false`. * * @remarks Any delegated transaction signed with {@link signAsSender} * but not yet signed with {@link signAsGasPayer} is not signed. */ public get isSigned(): boolean { if (this.signature !== undefined) { return Transaction.isSignatureLengthValid( this.body, this.signature ); } return false; } /** * Return the origin (also known as sender) address of the transaction. * * The origin is determined by recovering the public key from the transaction's sender. * * @return {Address} The address derived from the public key of the transaction's sender. * @throws {UnavailableTransactionField} If the transaction is not signed, an exception is thrown indicating the absence of the origin field. * * @remarks Security auditable method, depends on * - {@link Address.ofPublicKey}; * - {@link Secp256k1.recover}. */ public get origin(): Address { if (this.signature !== undefined) { return Address.ofPublicKey( // Get the origin public key. Secp256k1.recover( this.getTransactionHash().bytes, // Get the (r, s) of ECDSA digital signature without gas payer params. this.signature.slice(0, Secp256k1.SIGNATURE_LENGTH) ) ); } throw new UnavailableTransactionField( 'Transaction.origin()', 'not signed transaction, no origin', { fieldName: 'origin' } ); } // ********** PUBLIC METHODS ********** /** * Decodes a raw transaction byte array into a new Transaction object. * * @param {Uint8Array} rawTransaction - The raw transaction bytes to decode. * @param {boolean} isSigned - Flag indicating if the transaction is signed. * @return {Transaction} The decoded transaction object. * * @see encoded */ public static decode( rawTransaction: Uint8Array, isSigned: boolean ): Transaction { // check prefix to get tx type const rawPrefix = rawTransaction[0]; let txType: TransactionType = TransactionType.Legacy; if (Number(rawPrefix) === Transaction.EIP1559_TX_TYPE_PREFIX) { txType = TransactionType.EIP1559; } // Get correct decoder profiler const profile = isSigned ? txType === TransactionType.Legacy ? Transaction.RLP_SIGNED_LEGACY_TRANSACTION_PROFILE : Transaction.RLP_SIGNED_EIP1559_TRANSACTION_PROFILE : txType === TransactionType.Legacy ? Transaction.RLP_UNSIGNED_LEGACY_TRANSACTION_PROFILE : Transaction.RLP_UNSIGNED_EIP1559_TRANSACTION_PROFILE; // if eip1559, remove prefix if (txType === TransactionType.EIP1559) { rawTransaction = rawTransaction.slice(1); } // Get decoded body const decodedRLPBody = RLPProfiler.ofObjectEncoded( rawTransaction, profile ).object as RLPValidObject; // Create correct transaction body without reserved field const bodyWithoutReservedField: TransactionBody = { blockRef: decodedRLPBody.blockRef as string, chainTag: decodedRLPBody.chainTag as number, clauses: decodedRLPBody.clauses as [], dependsOn: decodedRLPBody.dependsOn as string | null, expiration: decodedRLPBody.expiration as number, gas: decodedRLPBody.gas as number, nonce: decodedRLPBody.nonce as number, // Handle both legacy and EIP-1559 gas pricing ...(decodedRLPBody.gasPriceCoef !== undefined ? { gasPriceCoef: decodedRLPBody.gasPriceCoef as number } : { maxFeePerGas: decodedRLPBody.maxFeePerGas as string, maxPriorityFeePerGas: decodedRLPBody.maxPriorityFeePerGas as string }) }; // Create correct transaction body (with correct reserved field) const correctTransactionBody: TransactionBody = (decodedRLPBody.reserved as Uint8Array[]).length > 0 ? { ...bodyWithoutReservedField, reserved: Transaction.decodeReservedField( decodedRLPBody.reserved as Uint8Array[] ) } : bodyWithoutReservedField; // Return decoded transaction (with signature or not) return decodedRLPBody.signature !== undefined ? Transaction.of( correctTransactionBody, decodedRLPBody.signature as Uint8Array ) : Transaction.of(correctTransactionBody); } /** * Computes the transaction hash, optionally incorporating a gas payer's address. * * @param {Address} [sender] - Optional transaction origin's address to include in the hash computation. * @return {Blake2b256} - The computed transaction hash. * * @remarks * `sender` is used to sign a transaction on behalf of another account. * * @remarks Security auditable method, depends on * - {@link Blake2b256.of}. */ public getTransactionHash(sender?: Address): Blake2b256 { const txHash = Blake2b256.of(this.encode(false)); if (sender !== undefined) { return Blake2b256.of( nc_utils.concatBytes(txHash.bytes, sender.bytes) ); } return txHash; } /** * Calculates the intrinsic gas required for the given transaction clauses. * * @param {TransactionClause[]} clauses - An array of transaction clauses to calculate the intrinsic gas for. * @return {VTHO} The total intrinsic gas required for the provided clauses. * @throws {InvalidDataType} If clauses have invalid data as invalid addresses. */ public static intrinsicGas(clauses: TransactionClause[]): VTHO { if (clauses.length > 0) { // Some clauses. return VTHO.of( clauses.reduce((sum: bigint, clause: TransactionClause) => { if (clause.to !== null) { // Invalid address or no vet.domains name if ( !Address.isValid(clause.to) && !clause.to.includes('.') ) throw new InvalidDataType( 'Transaction.intrinsicGas', 'invalid data type in clause: each `to` field must be a valid address.', { clause } ); sum += Transaction.GAS_CONSTANTS.CLAUSE_GAS; } else { sum += Transaction.GAS_CONSTANTS .CLAUSE_GAS_CONTRACT_CREATION; } sum += Transaction.computeUsedGasFor(clause.data); return sum; }, Transaction.GAS_CONSTANTS.TX_GAS), Units.wei ); } // No clauses. return VTHO.of( Transaction.GAS_CONSTANTS.TX_GAS + Transaction.GAS_CONSTANTS.CLAUSE_GAS, Units.wei ); } /** * Validates the transaction body's fields according to the transaction type. * * @param {TransactionBody} body - The transaction body to validate. * @param {TransactionType} type - The transaction type to validate the body against. * @return {boolean} True if the transaction body is valid for the given type. */ public static isValidBody( body: TransactionBody, type: TransactionType ): boolean { // Legacy transactions shouldn't have any EIP-1559 parameters if ( type === TransactionType.Legacy && (body.maxFeePerGas !== undefined || body.maxPriorityFeePerGas !== undefined) ) { return false; } // EIP-1559 transactions shouldn't have legacy parameters if ( type === TransactionType.EIP1559 && body.gasPriceCoef !== undefined ) { return false; } // validate common fields const isValidCommonFields = // Chain tag body.chainTag !== undefined && body.chainTag >= 0 && body.chainTag <= 255 && // Block reference body.blockRef !== undefined && Hex.isValid0x(body.blockRef) && HexUInt.of(body.blockRef).bytes.length === Transaction.BLOCK_REF_LENGTH && // Expiration body.expiration !== undefined && // Clauses body.clauses !== undefined && // Gas body.gas !== undefined && // Depends on body.dependsOn !== undefined && // Nonce body.nonce !== undefined; // validate eip1559 fields const isValidEip1559Fields = type === TransactionType.EIP1559 && body.maxFeePerGas !== undefined && body.maxPriorityFeePerGas !== undefined && ((typeof body.maxFeePerGas === 'string' && Hex.isValid0x(body.maxFeePerGas)) || typeof body.maxFeePerGas === 'number') && ((typeof body.maxPriorityFeePerGas === 'string' && Hex.isValid0x(body.maxPriorityFeePerGas)) || typeof body.maxPriorityFeePerGas === 'number'); // validate legacy fields const isValidLegacyFields = type === TransactionType.Legacy && body.gasPriceCoef !== undefined; // return true if the transaction body is valid if (type === TransactionType.EIP1559) { return isValidCommonFields && isValidEip1559Fields; } return isValidCommonFields && isValidLegacyFields; } /** * Returns the type of the transaction. * * @param {TransactionBody} body - The transaction body to get the type of. * @return {TransactionType} The type of the transaction. */ private static getTransactionType(body: TransactionBody): TransactionType { if (body.gasPriceCoef !== undefined) { return TransactionType.Legacy; } if ( body.maxFeePerGas !== undefined && body.maxPriorityFeePerGas !== undefined ) { return TransactionType.EIP1559; } throw new InvalidTransactionField( 'Transaction.getTransactionType', 'invalid transaction body', { fieldName: 'body', body } ); } /** * Creates a new Transaction instance if the provided body is valid. * * @param {TransactionBody} body - The transaction body to be validated. * @param {Uint8Array} [signature] - Optional signature. * @return {Transaction} A new Transaction instance if validation is successful. * @throws {InvalidTransactionField} If the provided body is invalid. */ public static of( body: TransactionBody, signature?: Uint8Array ): Transaction { const txType = Transaction.getTransactionType(body); if (Transaction.isValidBody(body, txType)) { return new Transaction(body, txType, signature); } throw new InvalidTransactionField('Transaction.of', 'invalid body', { fieldName: 'body', body }); } /** * Signs the transaction using the provided private key of the transaction sender. * * @param {Uint8Array} senderPrivateKey - The private key used to sign the transaction. * @return {Transaction} The signed transaction. * @throws {InvalidTransactionField} If attempting to sign a delegated transaction. * @throws {InvalidSecp256k1PrivateKey} If the provided private key is not valid. * * @remarks Security auditable method, depends on * - {@link Secp256k1.isValidPrivateKey}; * - {@link Secp256k1.sign}. */ public sign(senderPrivateKey: Uint8Array): Transaction { // Check if the private key is valid. if (Secp256k1.isValidPrivateKey(senderPrivateKey)) { if (!this.isDelegated) { // Sign transaction const signature = Secp256k1.sign( this.getTransactionHash().bytes, senderPrivateKey ); // Return new signed transaction. return Transaction.of(this.body, signature); } throw new InvalidTransactionField( `Transaction.sign`, 'delegated transaction: use signAsSenderAndGasPayer method', { fieldName: 'gasPayer', body: this.body } ); } throw new InvalidSecp256k1PrivateKey( `Transaction.sign`, 'invalid private key: ensure it is a secp256k1 key', undefined ); } /** * Signs a transaction as a gas payer using the provided private key. This is applicable only if the transaction * has been marked as delegated and already contains the signature of the transaction sender * that needs to be extended with the gas payer's signature. * * @param {Address} sender - The address of the sender for whom the transaction hash is generated. * @param {Uint8Array} gasPayerPrivateKey - The private key of the gas payer. Must be a valid secp256k1 key. * * @return {Transaction} - A new transaction object with the gas payer's signature appended. * * @throws {InvalidSecp256k1PrivateKey} If the provided gas payer private key is not valid. * @throws {InvalidTransactionField} If the transaction is unsigned or lacks a valid signature. * @throws {NotDelegatedTransaction} If the transaction is not set as delegated. * * @remarks Security auditable method, depends on * - {@link Secp256k1.isValidPrivateKey}; * - {@link Secp256k1.sign}. */ public signAsGasPayer( sender: Address, gasPayerPrivateKey: Uint8Array ): Transaction { if (Secp256k1.isValidPrivateKey(gasPayerPrivateKey)) { if (this.isDelegated) { const senderHash = this.getTransactionHash(sender).bytes; if (this.signature !== undefined) { return new Transaction( this.body, this.transactionType, nc_utils.concatBytes( // Drop any previous gas payer signature. this.signature.slice(0, Secp256k1.SIGNATURE_LENGTH), Secp256k1.sign(senderHash, gasPayerPrivateKey) ) ); } else { return new Transaction( this.body, this.transactionType, Secp256k1.sign(senderHash, gasPayerPrivateKey) ); } } throw new NotDelegatedTransaction( 'Transaction.signAsGasPayer', 'not delegated transaction: use sign method', undefined ); } throw new InvalidSecp256k1PrivateKey( `Transaction.signAsGasPayer`, 'invalid gas payer private key: ensure it is a secp256k1 key', undefined ); } /** * Signs a delegated transaction using the provided transaction sender's private key, * call the {@link signAsGasPayer} to complete the signature, * before such call {@link isDelegated} returns `true` but * {@link isSigned} returns `false`. * * @param senderPrivateKey The private key of the transaction sender, represented as a Uint8Array. It must be a valid secp256k1 private key. * @return A new Transaction object with the signature applied, if the transaction is delegated and the private key is valid. * @throws NotDelegatedTransaction if the current transaction is not marked as delegated, instructing to use the regular sign method instead. * @throws InvalidSecp256k1PrivateKey if the provided senderPrivateKey is not a valid secp256k1 private key. * * @remarks Security auditable method, depends on * - {@link Secp256k1.isValidPrivateKey}; * - {@link Secp256k1.sign}. */ public signAsSender(senderPrivateKey: Uint8Array): Transaction { if (Secp256k1.isValidPrivateKey(senderPrivateKey)) { if (this.isDelegated) { const transactionHash = this.getTransactionHash().bytes; return new Transaction( this.body, this.transactionType, Secp256k1.sign(transactionHash, senderPrivateKey) ); } throw new NotDelegatedTransaction( 'Transaction.signAsSender', 'not delegated transaction: use sign method', undefined ); } throw new InvalidSecp256k1PrivateKey( `Transaction.signAsSender`, 'invalid sender private key: ensure it is a secp256k1 key', undefined ); } /** * Signs the transaction using both the transaction sender and the gas payer private keys. * * @param {Uint8Array} senderPrivateKey - The private key of the transaction sender. * @param {Uint8Array} gasPayerPrivateKey - The private key of the gas payer. * @return {Transaction} A new transaction with the concatenated signatures * of the transaction sender and the gas payer. * @throws {InvalidSecp256k1PrivateKey} - If either the private key of the transaction sender or gas payer is invalid. * @throws {NotDelegatedTransaction} - If the transaction is not delegated. * * @remarks Security auditable method, depends on * - {@link Address.ofPublicKey} * - {@link Secp256k1.isValidPrivateKey}; * - {@link Secp256k1.sign}. */ public signAsSenderAndGasPayer( senderPrivateKey: Uint8Array, gasPayerPrivateKey: Uint8Array ): Transaction { // Check if the private key of the sender is valid. if (Secp256k1.isValidPrivateKey(senderPrivateKey)) { // Check if the private key of the gas payer is valid. if (Secp256k1.isValidPrivateKey(gasPayerPrivateKey)) { if (this.isDelegated) { const senderHash = this.getTransactionHash().bytes; const gasPayerHash = this.getTransactionHash( Address.ofPublicKey( Secp256k1.derivePublicKey(senderPrivateKey) ) ).bytes; // Return new signed transaction return Transaction.of( this.body, nc_utils.concatBytes( Secp256k1.sign(senderHash, senderPrivateKey), Secp256k1.sign(gasPayerHash, gasPayerPrivateKey) ) ); } throw new NotDelegatedTransaction( 'Transaction.signAsSenderAndGasPayer', 'not delegated transaction: use sign method', undefined ); } throw new InvalidSecp256k1PrivateKey( `Transaction.signAsSenderAndGasPayer`, 'invalid gas payer private key: ensure it is a secp256k1 key', undefined ); } throw new InvalidSecp256k1PrivateKey( `Transaction.signAsSenderAndGasPayer`, 'invalid sender private key: ensure it is a secp256k1 key', undefined ); } // ********** PRIVATE FUNCTIONS ********** /** * Computes the amount of gas used for the given data. * * @param {string} data - The hexadecimal string data for which the gas usage is computed. * @return {bigint} The total gas used for the provided data. * @throws {InvalidDataType} If the data is not a valid hexadecimal string. * * @remarks gas value is expressed in {@link Units.wei} unit. */ private static computeUsedGasFor(data: string): bigint { // Invalid data if (data !== '' && !Hex.isValid(data)) throw new InvalidDataType( 'calculateDataUsedGas()', `Invalid data type for gas calculation. Data should be a hexadecimal string.`, { data } ); let sum = 0n; for (let i = 2; i < data.length; i += 2) { if (data.substring(i, i + 2) === '00') { sum += Transaction.GAS_CONSTANTS.ZERO_GAS_DATA; } else { sum += Transaction.GAS_CONSTANTS.NON_ZERO_GAS_DATA; } } return sum; } /** * Decodes the {@link TransactionBody.reserved} field from the given buffer array. * * @param {Buffer[]} reserved - An array of Uint8Array objects representing the reserved field data. * @return {Object} An object containing the decoded features and any unused buffer data. * @return {number} [return.features] The decoded features from the reserved field. * @return {Buffer[]} [return.unused] An array of Buffer objects representing unused data, if any. * @throws {InvalidTransactionField} Thrown if the reserved field is not properly trimmed. */ private static decodeReservedField(reserved: Uint8Array[]): { features?: number; unused?: Uint8Array[]; } { // Not trimmed reserved field if (reserved[reserved.length - 1].length > 0) { // Get features field. const featuresField = Transaction.RLP_FEATURES.kind .buffer(reserved[0], Transaction.RLP_FEATURES.name) .decode() as number; // Return encoded reserved field return reserved.length > 1 ? { features: featuresField, unused: reserved.slice(1) } : { features: featuresField }; } throw new InvalidTransactionField( 'Transaction.decodeReservedField', 'invalid reserved field: fields in the `reserved` property must be properly trimmed', { fieldName: 'reserved', reserved } ); } /** * Encodes the transaction body using RLP encoding. * * @param {boolean} isSigned - Indicates whether the transaction is signed. * @return {Uint8Array} The RLP encoded transaction body. * * @see encoded */ private encode(isSigned: boolean): Uint8Array { // Encode transaction body with RLP const encodedBody = this.encodeBodyField( { // Existing body and the optional `reserved` field if present. ...this.body, /* * The `body.clauses` property is already an array, * albeit TypeScript realize, hence cast is needed * otherwise encodeObject will throw an error. */ clauses: this.body.clauses as Array<{ to: string | null; value: string | number; data: string; }>, // New reserved field. reserved: this.encodeReservedField() }, isSigned ); // add prefix if eip1559 if (this.transactionType === TransactionType.EIP1559) { return nc_utils.concatBytes( Uint8Array.from([Transaction.EIP1559_TX_TYPE_PREFIX]), encodedBody ); } return encodedBody; } /** * Encodes the given transaction body into a Uint8Array, depending on whether * the transaction is signed or not. * * @param body - The transaction object adhering to the RLPValidObject structure. * @param isSigned - A boolean indicating if the transaction is signed. * @return A Uint8Array representing the encoded transaction. * * @see encoded */ private encodeBodyField( body: RLPValidObject, isSigned: boolean ): Uint8Array { // Encode transaction object - SIGNED if (isSigned) { return RLPProfiler.ofObject( { ...body, signature: Uint8Array.from(this.signature as Uint8Array) }, this.transactionType === TransactionType.EIP1559 ? Transaction.RLP_SIGNED_EIP1559_TRANSACTION_PROFILE : Transaction.RLP_SIGNED_LEGACY_TRANSACTION_PROFILE ).encoded; } // Encode transaction object - UNSIGNED return RLPProfiler.ofObject( body, this.transactionType === TransactionType.EIP1559 ? Transaction.RLP_UNSIGNED_EIP1559_TRANSACTION_PROFILE : Transaction.RLP_UNSIGNED_LEGACY_TRANSACTION_PROFILE ).encoded; } /** * Encodes the {@link TransactionBody.reserved} field data for a transaction. * * @return {Uint8Array[]} The encoded list of reserved features. * It removes any trailing unused features that have zero length from the list. * * @remarks The {@link TransactionBody.reserved} is optional, albeit * is required to perform RLP encoding. * * @see encode */ private encodeReservedField(): Uint8Array[] { // Check if is reserved or not const reserved = this.body.reserved ?? {}; // Init kind for features const featuresKind = Transaction.RLP_FEATURES.kind; // Features list const featuresList = [ featuresKind .data(reserved.features ?? 0, Transaction.RLP_FEATURES.name) .encode(), ...(reserved.unused ?? []) ]; // Trim features list while (featuresList.length > 0) { if (featuresList[featuresList.length - 1].length === 0) { featuresList.pop(); } else { break; } } return featuresList; } /** * Return `true` if the transaction is delegated, else `false`. * * @param {TransactionBody} body - The transaction body. * @return {boolean} `true` if the transaction is delegated, else `false`. */ private static isDelegated(body: TransactionBody): boolean { // Check if is reserved or not const reserved = body.reserved ?? {}; // Features const features = reserved.features ?? 0; // Fashion bitwise way to check if a number is even or not return (features & 1) === 1; } /** * Validates the length of a given signature against the expected length. * * @param {TransactionBody} body - The body of the transaction being validated. * @param {Uint8Array} signature - The signature to verify the length of. * @return {boolean} Returns true if the signature length matches the expected length, otherwise false. */ private static isSignatureLengthValid( body: TransactionBody, signature: Uint8Array ): boolean { // Verify signature length const expectedSignatureLength = this.isDelegated(body) ? Secp256k1.SIGNATURE_LENGTH * 2 : Secp256k1.SIGNATURE_LENGTH; return signature.length === expectedSignatureLength; } } export { Transaction };