UNPKG

bitcoin-tx-lib

Version:

A Typescript library for building and signing Bitcoin transactions

164 lines (138 loc) 5.29 kB
import { BaseTransaction } from "./base/txbase"; import { ECPairKey } from "./ecpairkey"; import { bytesToHex, hash256 } from "./utils"; import { TXOptions } from "./types"; export class Transaction extends BaseTransaction { /** * Creates a new Transaction instance. * @param pairkey The key pair used to sign the transaction inputs. * @param options Optional transaction parameters (version, locktime, fee, etc.). */ constructor(pairkey: ECPairKey, options?: TXOptions) { super(pairkey, options) } /** * Signs the transaction. * Caches the raw transaction data and the version used for txid calculation. */ public sign() : void { this.cachedata.set("txraw", this.build()) this.cachedata.set("txidraw", this.build("txid")) } /** * Returns the transaction ID (txid) as a hex string. * The txid is the double SHA-256 hash of the stripped raw transaction (no witness data), reversed in byte order. * * @throws Error if the transaction is not signed. * @returns The txid as a hex string. */ public getTxid(): string { if(!this.cachedata.get("txidraw")) this.sign() let hexTransaction = this.cachedata.get("txidraw") if(!hexTransaction) throw new Error("Transaction not signed, please sign the transaction") let txid = hash256(hexTransaction).reverse() return bytesToHex(txid) } /** * Calculates the total weight of the transaction according to BIP 141. * Weight = (non-witness bytes * 4) + witness bytes. * * @throws Error if the transaction is not signed. * @returns The transaction weight. */ public weight() : number { // docs https://learnmeabitcoin.com/technical/transaction/size/ if(!this.cachedata.get("txraw")) this.sign() // witness marker and flag * 1 let witnessMK = 0 // 2 bytes of marker and flag 0x00+0x01 = 2 bytes * 1 if(this.isSegwit()) witnessMK = 2 let hexTransaction = this.cachedata.get("txraw") as Uint8Array let witnessInputs = this.inputs.filter(this.isSegwitInput) // witness size * 1 let witnessSize = witnessInputs.reduce((sum, input) => { let witness = this.buildWitness(input) return sum + witness.length }, 0) // discount the size of the witness fields and multiply by 4 let transactionSize = hexTransaction.length transactionSize = (transactionSize - (witnessSize + witnessMK)) * 4 transactionSize += (witnessSize + witnessMK) // * 1 return Math.ceil(transactionSize) } /** * Calculates the virtual size (vBytes) of the transaction. * Defined as weight divided by 4. * * @throws Error if the transaction is not signed. * @returns The transaction virtual size in bytes. */ public vBytes() { // docs https://learnmeabitcoin.com/technical/transaction/size/ return Math.ceil(this.weight() / 4) } /** * Deducts the transaction fee from the output(s) according to the fee-paying strategy. * If only one output exists, deduct the entire fee from it. * If `whoPayTheFee` is "everyone", split the fee evenly among all outputs. * If `whoPayTheFee` is an address, deduct the fee from the output matching that address. * * @throws Error if the transaction is not signed. */ public resolveFee() : void { if(!this.cachedata.get("txraw")) this.sign() let satoshis = Math.ceil(this.vBytes() * (this.fee??1)) if(this.outputs.length == 1) { this.outputs[0].amount -= satoshis return } if(this.whoPayTheFee === "everyone") { satoshis = Math.ceil(this.vBytes() * (this.fee??1) / this.outputs.length) this.outputs.forEach(out => out.amount -= satoshis) } for(let i = 0; i < this.outputs.length; i++) { if(this.outputs[i].address == this.whoPayTheFee) { this.outputs[i].amount -= satoshis break } } } /** * Calculates the total fee in satoshis based on the virtual size and fee rate. * * @returns The transaction fee in satoshis. */ public getFeeSats() { if(this.whoPayTheFee && this.fee) this.resolveFee() const feeSats = Math.ceil(this.vBytes() * (this.fee??1)) return feeSats } /** * Returns the raw transaction as a hex string. * * @throws Error if the transaction is not signed. * @returns The raw transaction hex string. */ public getRawHex() : string { if(!this.cachedata.get("txraw")) this.sign() return bytesToHex(this.cachedata.get("txraw") as Uint8Array) } /** * Returns the raw transaction bytes as a Uint8Array. * * @throws Error if the transaction is not signed. * @returns The raw transaction bytes. */ public getRawBytes() : Uint8Array { if(!this.cachedata.get("txraw")) this.sign() return this.cachedata.get("txraw") as Uint8Array } }