PHP WebShell
Текущая директория: /usr/lib/node_modules/bitgo/node_modules/@vechain/sdk-core/src/transaction
Просмотр файла: Transaction.ts
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 };
Выполнить команду
Для локальной разработки. Не используйте в интернете!