Files
xo-stack/packages/primitives/src/transaction.ts
T

516 lines
13 KiB
TypeScript

import { Bytes } from './bytes.js';
import {
binToHex,
hexToBin,
encodeTransaction,
decodeTransaction,
decodeTransactionBCH,
hashTransaction,
} from '@bitauth/libauth';
import type { ResolvedTransactionBCH, TransactionBCH } from '@bitauth/libauth';
class BaseTransaction {
protected readonly transaction: TransactionBCH;
protected constructor(transaction: TransactionBCH) {
this.transaction = transaction;
}
/**
* Serializes the transaction to a byte array.
* @returns The transaction serialized as a Uint8Array
*/
public toBytes(): Uint8Array {
const bytes = encodeTransaction(this.transaction);
return bytes;
}
/**
* Serializes the transaction to a byte array (represented as a hex string).
* @returns The transaction serialized in hex
*/
public toHex(): string {
const bytes = this.toBytes();
return binToHex(bytes);
}
/**
* Hashes the transaction and returns a TransactionHash primitive.
* @returns The transaction hash primitive.
*/
public toHash(): TransactionHash {
return TransactionHash.fromTransactionBytes(this.toBytes());
}
/**
* Hashes the transaction and returns the hash as hexadecimal.
* @returns The transaction hash hex.
*/
public toHashHex(): string {
return this.toHash().toHex();
}
/**
* Hashes the transaction and returns the hash as a Uint8Array.
* @returns The transaction hash bytes.
*/
public toHashBytes(): Uint8Array {
return this.toHash().toBytes();
}
/**
* Gets the version for this transaction.
*
* @experimental
*
* @returns The version of this transaction
*/
public getVersion() {
return this.transaction.version;
}
/**
* Gets the inputs in this transaction.
*
* @experimental
*
* @returns The inputs in this transaction
*/
public getInputs() {
return this.transaction.inputs;
}
/**
* Gets the outputs in this transaction.
*
* @experimental
*
* @returns The outputs in this transaction
*/
public getOutputs() {
return this.transaction.outputs;
}
/**
* Gets the locktime for this transaction.
*
* @experimental
*
* @returns The locktime of this transaction
*/
public getLocktime() {
return this.transaction.locktime;
}
/**
* Checks whether this transaction is a Coinbase Transaction.
*
* @remarks UNTESTED
*
* @experimental
*
* @returns True if this is a Coinbase Transaction
*/
public isCoinbase(): boolean {
// A coinbase transaction MUST only have one input
if (this.getInputs().length !== 1) {
return false;
}
const input = this.getInputs()[0];
// Check if the outpoint transaction hash is all zeros (32 bytes of 0x00)
const isOutpointZero = input.outpointTransactionHash.every(
(byte) => byte === 0x00,
);
// Check if the outpoint index is 0xFFFFFFFF (or -1 in signed 32-bit integer representation)
const isOutpointIndexMax = input.outpointIndex === 0xffffffff;
// Both conditions must be true for a coinbase transaction
return isOutpointZero && isOutpointIndexMax;
}
}
/**
* Transaction Primitive.
*
* @example
*
* ```ts
* // Import our primitives.
* import { Transaction } from '@xocash/primitive';
*
* // Instantiate primitive from Transaction Hex.
* const transaction = Transaction.from('transactionHex');
*
* // Output Transaction Information.
* console.log(transaction.getVersion());
* console.log(transaction.getLocktime());
* console.log(transaction.getInputs());
* console.log(transaction.getOutputs());
*
* // Output the Transaction Hash.
* console.log(transaction.toHash().toHex());
* ```
*/
export class Transaction extends BaseTransaction {
protected constructor(transaction: TransactionBCH) {
super(transaction);
}
/**
* Instantiates a transaction from either:
*
* 1. Transaction bytecode
* 2. Transaction bytecode (in hexadecimal format)
*
* @param transaction The transaction in one of the above formats.
*
* @throws {Error} If transaction data cannot be decoded.
*
* @returns The created Transaction Primitive.
*/
public static from(transaction: Uint8Array | string) {
return transaction instanceof Uint8Array
? this.fromBytes(transaction)
: this.fromHex(transaction);
}
/**
* Create a Transaction Primitive from the given byte data.
*
* @param bytes The raw byte data of the transaction.
*
* @throws {Error} If transaction data cannot be decoded.
*
* @returns {Transaction} The created Transaction Primitive.
*/
public static fromBytes(bytes: Uint8Array): Transaction {
// Attempt to decode the raw transaction bytes.
const decodeResult = decodeTransaction(bytes);
// If a string is returned, this indicates an error...
if (typeof decodeResult === 'string') {
throw new Error(decodeResult);
}
// Return a new instance of the Transaction Primitive created from the decode result.
return new this(decodeResult);
}
/**
* Create a Transaction Primitive from the given byte data encoded as hexadecimal string.
*
* @param hex The raw byte data (encoded as hexadecimal string) of the transaction.
*
* @throws {Error} If transaction cannot be decoded.
*
* @returns {Transaction} The created Transaction Primitive.
*/
public static fromHex(hex: string): Transaction {
const bytes = Bytes.fromHex(hex).toUint8Array();
return this.fromBytes(bytes);
}
/**
* Create a Transaction Primitive from LibAuth's TransactionBCH type.
*
* @remarks This method is UNSAFE and MUST only be used where input is already verified or trusted.
*
* @param transaction The transaction object (as per LibAuth).
*
* @returns The created Transaction Primitive.
*/
public static fromRaw(transaction: TransactionBCH) {
return new this(transaction);
}
/**
* Validates the structure of a transaction.
*
* @param transaction The transaction (as hex or bytes).
*
* @returns boolean indicating whether the transaction structure is valid.
*/
public static isValid(transaction: Uint8Array | string): boolean {
try {
this.from(transaction);
} catch (_error) {
return false;
}
return true;
}
/**
* Converts transaction to a raw object representation.
* @returns A TransactionBCH LibAuth object.
*/
public toRaw(): TransactionBCH {
return { ...this.transaction };
}
}
export class TransactionResolved extends BaseTransaction {
protected sourceOutputs: Array<any>;
protected constructor(
transaction: TransactionBCH,
sourceOutputs: Array<any>,
) {
super(transaction);
this.sourceOutputs = sourceOutputs;
}
/**
* Instantiates a transaction from either:
*
* 1. Transaction bytecode
* 2. Transaction bytecode (in hexadecimal format)
*
* @param transaction The transaction in one of the above formats.
*
* @throws {Error} If transaction data cannot be decoded.
*
* @returns The created Transaction Primitive.
*/
public static from(
transaction: Uint8Array | string,
sourceOutputs: Array<any>,
) {
return transaction instanceof Uint8Array
? this.fromBytes(transaction, sourceOutputs)
: this.fromHex(transaction, sourceOutputs);
}
/**
* Create a Transaction Primitive from the given byte data.
*
* @param bytes The raw byte data of the transaction.
*
* @throws {Error} If transaction data cannot be decoded.
*
* @returns {Transaction} The created Transaction Primitive.
*/
public static fromBytes(bytes: Uint8Array, sourceOutputs: Array<any>) {
// Attempt to decode the raw transaction bytes.
const decodeResult = decodeTransactionBCH(bytes);
// If a string is returned, this indicates an error...
if (typeof decodeResult === 'string') {
throw new Error(decodeResult);
}
// Return a new instance of the Transaction Primitive created from the decode result.
return new this(decodeResult, sourceOutputs);
}
/**
* Create a Transaction Primitive from the given byte data encoded as hexadecimal string.
*
* @param hex The raw byte data (encoded as hexadecimal string) of the transaction.
*
* @throws {Error} If transaction cannot be decoded.
*
* @returns {Transaction} The created Transaction Primitive.
*/
public static fromHex(hex: string, sourceOutputs: Array<any>) {
const bytes = Bytes.fromHex(hex).toUint8Array();
return this.fromBytes(bytes, sourceOutputs);
}
/**
* Create a Transaction Primitive from LibAuth's TransactionBCH type.
*
* @remarks This method is UNSAFE and MUST only be used where input is already verified or trusted.
*
* @param transaction The transaction object (as per LibAuth).
*
* @returns The created Transaction Primitive.
*/
public static fromRaw(resolvedTransaction: ResolvedTransactionBCH) {
return new this(
resolvedTransaction.transaction,
resolvedTransaction.sourceOutputs,
);
}
/**
* Validates the structure of a transaction.
*
* @param transaction The transaction (as hex or bytes).
*
* @returns boolean indicating whether the transaction structure is valid.
*/
public static isValid(
transaction: Uint8Array | string,
sourceOutputs: Array<any>,
): boolean {
try {
this.from(transaction, sourceOutputs);
} catch (_error) {
return false;
}
return true;
}
public toRaw() {
return {
transaction: this.transaction,
sourceOutputs: this.sourceOutputs,
};
}
}
/**
* Transaction Hash Primitive.
*/
export class TransactionHash {
/**
* Instantiates TransactionHash from a Transaction Hash's bytes.
*
* @param bytes {Uint8Array} The Transaction Hash's bytes.
*/
protected constructor(protected readonly bytes: Uint8Array) {
this.bytes = bytes;
}
/**
* Instantiates a TransactionHash from either the Hash Hex or Hash Bytes.
*
* @param hash The Transaction Hash as either hex or bytes.
*
* @throws {Error} If the hash is invalid.
*
* @returns The created Public Key Primitive.
*/
public static from(hash: Uint8Array | string) {
return hash instanceof Uint8Array
? this.fromBytes(hash)
: this.fromHex(hash);
}
/**
* Instantiates TransactionHash from Hash Bytes.
*
* @param bytes The hash as bytes.
*
* @throws {Error} If hash is invalid.
*
* @returns {TransactionHash} Instance of TransactionHash.
*/
public static fromBytes(bytes: Uint8Array): TransactionHash {
// If bytes is not exactly 32, then this is an invalid transaction hash...
if (bytes.length !== 32) {
throw new Error(
`Invalid Transaction Hash: Expected 32 bytes but received ${bytes.length}`,
);
}
return new this(bytes);
}
/**
* Instantiates TransactionHash from a hex string.
*
* @param hex {string} The hex string.
*
* @throws {Error} If hex is invalid.
*
* @returns {TransactionHash} Instance of TransactionHash.
*/
public static fromHex(hex: string): TransactionHash {
const bytes = Bytes.fromHex(hex).toUint8Array();
return this.fromBytes(bytes);
}
/**
* Instantiates a TransactionHash from either the Transaction Hex or Transaction Hash.
*
* @param hash The Transaction as either hex or bytes.
*
* @throws {Error} If the public key cannot be decoded.
*
* @returns The created Public Key Primitive.
*/
public static fromTransaction(transaction: Uint8Array | string) {
return transaction instanceof Uint8Array
? this.fromTransactionBytes(transaction)
: this.fromTransactionHex(transaction);
}
/**
* Instantiates TransactionHash from a Transaction's bytes.
*
* @param transactionBytes {Uint8Array} The Transaction's bytes.
*
* @returns {TransactionHash} Instance of TransactionHash.
*/
public static fromTransactionBytes(
transactionBytes: Uint8Array,
): TransactionHash {
const transactionHash = hashTransaction(transactionBytes);
return new this(hexToBin(transactionHash));
}
/**
* Instantiates TransactionHash from a Transaction's bytes in hex.
*
* @param transactionHex {Uint8Array} The Transaction's bytes in hex.
*
* @throws {Error} If hex is invalid.
*
* @returns {TransactionHash} Instance of TransactionHash.
*/
public static fromTransactionHex(transactionHex: string): TransactionHash {
const bytes = Bytes.fromHex(transactionHex).toUint8Array();
return this.fromTransactionBytes(bytes);
}
/**
* Creates an instance from serialized data.
*
* @remarks This method is UNSAFE and MUST only be used where input is already verified or trusted.
*
* @param bytes - Serialized Primitive Data
*
* @returns A new instance of this primitive.
*/
public static fromRaw(bytes: Uint8Array) {
return new this(bytes);
}
/**
* Returns transaction hash as a hex string.
*
* @returns {string} Hex string of the Transaction Hash.
*/
public toHex(): string {
return binToHex(this.bytes);
}
/**
* Returns transaction hash as a Uint8Array.
*
* @returns {Uint8Array} Uint8Array of the Transaction Hash.
*/
public toBytes(): Uint8Array {
return this.bytes;
}
/**
* Converts this primitive to a serializable representation.
*
* @returns Serialzied primitive data.
*/
toRaw() {
return this.bytes;
}
}