@ecash/lib
Version:
Library for eCash transaction building
173 lines • 7.63 kB
JavaScript
;
// 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