UNPKG

@ecash/lib

Version:

Library for eCash transaction building

173 lines 7.63 kB
"use strict"; // Copyright (c) 2024 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.value); } return inputSum; } prepareOutputs() { let fixedOutputSum = 0n; let leftoverIdx = undefined; let 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] = { value: 0, // placeholder script: builderOutput.copy(), }; } else { fixedOutputSum += BigInt(builderOutput.value); outputs[idx] = (0, tx_js_1.copyTxOutput)(builderOutput); } } return { fixedOutputSum, leftoverIdx, outputs }; } /** Sign the tx built by this builder and return a Tx */ sign(ecc, feePerKb, dustLimit) { 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.value for all inputs'); } if (feePerKb === undefined) { throw new Error('Using a leftover output requires setting feePerKb'); } if (!Number.isInteger(feePerKb)) { throw new Error('feePerKb must be an integer'); } if (dustLimit === undefined) { throw new Error('Using a leftover output requires setting dustLimit'); } 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, feePerKb); const leftoverValue = inputSum - (fixedOutputSum + txFee); if (leftoverValue < dustLimit) { // 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, feePerKb); } else { outputs[leftoverIdx].value = leftoverValue; } if (inputSum < fixedOutputSum + txFee) { throw new Error(`Insufficient input value (${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