UNPKG

ecash-lib

Version:

Library for eCash transaction building

186 lines 8.17 kB
"use strict"; // Copyright (c) 2024-2025 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. Object.defineProperty(exports, "__esModule", { value: true }); exports.P2PKSignatory = exports.P2PKHSignatory = exports.signWithSigHash = exports.flagSignature = exports.calcTxFee = exports.TxBuilder = void 0; const ecc_js_1 = require("./ecc.js"); const hash_js_1 = require("./hash.js"); const writerbytes_js_1 = require("./io/writerbytes.js"); const op_js_1 = require("./op.js"); const script_js_1 = require("./script.js"); const sigHashType_js_1 = require("./sigHashType.js"); const tx_js_1 = require("./tx.js"); const unsignedTx_js_1 = require("./unsignedTx.js"); /** Class that can be used to build and sign txs. */ class TxBuilder { constructor(params) { this.version = params?.version ?? tx_js_1.DEFAULT_TX_VERSION; this.inputs = params?.inputs ?? []; this.outputs = params?.outputs ?? []; this.locktime = params?.locktime ?? 0; } /** Calculte sum of all sats coming in, or `undefined` if some unknown. */ inputSum() { let inputSum = 0n; for (const input of this.inputs) { if (input.input.signData === undefined) { return undefined; } inputSum += BigInt(input.input.signData.sats); } return inputSum; } prepareOutputs() { let fixedOutputSum = 0n; let leftoverIdx = undefined; const outputs = new Array(this.outputs.length); for (let idx = 0; idx < this.outputs.length; ++idx) { const builderOutput = this.outputs[idx]; if ('bytecode' in builderOutput) { // If builderOutput instanceof Script // Note that the "builderOutput instanceof Script" check may fail due // to discrepancies between nodejs and browser environments if (leftoverIdx !== undefined) { throw 'Multiple leftover outputs, can at most use one'; } leftoverIdx = idx; outputs[idx] = { sats: 0n, // placeholder script: builderOutput.copy(), }; } else { fixedOutputSum += BigInt(builderOutput.sats); outputs[idx] = (0, tx_js_1.copyTxOutput)(builderOutput); } } return { fixedOutputSum, leftoverIdx, outputs }; } /** * Create a TxBuilder from the given tx. * This is useful if tx is unsigned/partially signed and needs to be completed. **/ static fromTx(tx) { return new TxBuilder({ version: tx.version, inputs: tx.inputs.map(input => ({ input: (0, tx_js_1.copyTxInput)(input) })), outputs: tx.outputs.map(output => (0, tx_js_1.copyTxOutput)(output)), locktime: tx.locktime, }); } /** Sign the tx built by this builder and return a Tx */ sign(params) { const ecc = params?.ecc ?? new ecc_js_1.Ecc(); const { fixedOutputSum, leftoverIdx, outputs } = this.prepareOutputs(); const inputs = this.inputs.map(input => (0, tx_js_1.copyTxInput)(input.input)); const updateSignatories = (ecc, unsignedTx) => { for (let idx = 0; idx < this.inputs.length; ++idx) { const signatory = this.inputs[idx].signatory; const input = inputs[idx]; if (signatory !== undefined) { input.script = signatory(ecc, new unsignedTx_js_1.UnsignedTxInput({ inputIdx: idx, unsignedTx, })); } } }; if (leftoverIdx !== undefined) { const inputSum = this.inputSum(); if (inputSum === undefined) { throw new Error('Using a leftover output requires setting SignData.sats for all inputs'); } if (params?.feePerKb === undefined) { throw new Error('Using a leftover output requires setting feePerKb'); } if (typeof params.feePerKb !== 'bigint') { throw new Error('feePerKb must be a bigint'); } if (params?.dustSats === undefined) { throw new Error('Using a leftover output requires setting dustSats'); } const dummyUnsignedTx = unsignedTx_js_1.UnsignedTx.dummyFromTx(new tx_js_1.Tx({ version: this.version, inputs, outputs, locktime: this.locktime, })); // Must use dummy here because ECDSA sigs could be too small for fee calc updateSignatories(new ecc_js_1.EccDummy(), dummyUnsignedTx); let txSize = dummyUnsignedTx.tx.serSize(); let txFee = calcTxFee(txSize, params.feePerKb); const leftoverSats = inputSum - (fixedOutputSum + txFee); if (leftoverSats < params.dustSats) { // inputs cannot pay for a dust leftover -> remove & recalc outputs.splice(leftoverIdx, 1); dummyUnsignedTx.tx.outputs = outputs; // Must update signatories again as they might depend on outputs updateSignatories(new ecc_js_1.EccDummy(), dummyUnsignedTx); txSize = dummyUnsignedTx.tx.serSize(); txFee = calcTxFee(txSize, params.feePerKb); } else { outputs[leftoverIdx].sats = leftoverSats; } if (inputSum < fixedOutputSum + txFee) { throw new Error(`Insufficient input sats (${inputSum}): Can only pay for ${inputSum - fixedOutputSum} fees, but ${txFee} required`); } } const unsignedTx = unsignedTx_js_1.UnsignedTx.fromTx(new tx_js_1.Tx({ version: this.version, inputs, outputs, locktime: this.locktime, })); updateSignatories(ecc, unsignedTx); return unsignedTx.tx; } } exports.TxBuilder = TxBuilder; /** Calculate the required tx fee for the given txSize and feePerKb, * rounding up */ function calcTxFee(txSize, feePerKb) { return (BigInt(txSize) * BigInt(feePerKb) + 999n) / 1000n; } exports.calcTxFee = calcTxFee; /** Append the sighash flags to the signature */ function flagSignature(sig, sigHashFlags) { const writer = new writerbytes_js_1.WriterBytes(sig.length + 1); writer.putBytes(sig); writer.putU8(sigHashFlags.toInt() & 0xff); return writer.data; } exports.flagSignature = flagSignature; /** * Sign the sighash using Schnorr for BIP143 signatures and ECDSA for Legacy * signatures, and then flags the signature correctly **/ function signWithSigHash(ecc, sk, sigHash, sigHashType) { const sig = sigHashType.variant == sigHashType_js_1.SigHashTypeVariant.LEGACY ? ecc.ecdsaSign(sk, sigHash) : ecc.schnorrSign(sk, sigHash); return flagSignature(sig, sigHashType); } exports.signWithSigHash = signWithSigHash; /** Signatory for a P2PKH input. Always uses Schnorr signatures */ const P2PKHSignatory = (sk, pk, sigHashType) => { return (ecc, input) => { const preimage = input.sigHashPreimage(sigHashType); const sighash = (0, hash_js_1.sha256d)(preimage.bytes); const sigFlagged = signWithSigHash(ecc, sk, sighash, sigHashType); return script_js_1.Script.p2pkhSpend(pk, sigFlagged); }; }; exports.P2PKHSignatory = P2PKHSignatory; /** Signatory for a P2PK input. Always uses Schnorr signatures */ const P2PKSignatory = (sk, sigHashType) => { return (ecc, input) => { const preimage = input.sigHashPreimage(sigHashType); const sighash = (0, hash_js_1.sha256d)(preimage.bytes); const sigFlagged = signWithSigHash(ecc, sk, sighash, sigHashType); return script_js_1.Script.fromOps([(0, op_js_1.pushBytesOp)(sigFlagged)]); }; }; exports.P2PKSignatory = P2PKSignatory; //# sourceMappingURL=txBuilder.js.map