theseus/bundled-addons/bchwallet/lib/tx.js

123 lines
5.6 KiB
JavaScript
Raw Normal View History

// Transaction building for P2PKH spends on Bitcoin Cash: serialization, the
// BIP143-style replay-protected sighash (SIGHASH_ALL | FORKID), coin
// selection and fee estimation. Signing itself is delegated to WalletKeys so
// private keys stay in one module.
module.exports = function makeTx({ sha256 }) {
const SIGHASH_ALL_FORKID = 0x41;
const DUST = 546;
const P2PKH_INPUT_SIZE = 149; // outpoint 36 + len 1 + sig push 74 + pubkey push 34 + sequence 4
const P2PKH_OUTPUT_SIZE = 34;
const OVERHEAD = 10; // version 4 + in/out counts 2 + locktime 4
const dsha = (b) => sha256(sha256(b));
const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
const fromHex = (h) => Uint8Array.from(h.match(/../g) || [], (x) => parseInt(x, 16));
const concat = (...parts) => {
const n = parts.reduce((a, p) => a + p.length, 0);
const out = new Uint8Array(n); let o = 0;
for (const p of parts) { out.set(p, o); o += p.length; }
return out;
};
const u32le = (n) => Uint8Array.from([n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, (n >>> 24) & 0xff]);
const u64le = (n) => {
let v = BigInt(n); const out = new Uint8Array(8);
for (let i = 0; i < 8; i++) { out[i] = Number(v & 0xffn); v >>= 8n; }
return out;
};
const varint = (n) => {
if (n < 0xfd) return Uint8Array.from([n]);
if (n <= 0xffff) return Uint8Array.from([0xfd, n & 0xff, n >> 8]);
return concat(Uint8Array.from([0xfe]), u32le(n));
};
const varbytes = (b) => concat(varint(b.length), b);
const pushdata = (b) => {
if (b.length < 0x4c) return concat(Uint8Array.from([b.length]), b);
return concat(Uint8Array.from([0x4c, b.length]), b);
};
const outpoint = (inp) => concat(fromHex(inp.txid).reverse(), u32le(inp.vout));
const estimateSize = (nIn, nOut) => OVERHEAD + nIn * P2PKH_INPUT_SIZE + nOut * P2PKH_OUTPUT_SIZE;
const feeFor = (nIn, nOut, satPerByte) => Math.ceil(estimateSize(nIn, nOut) * satPerByte);
// inputs: [{ txid, vout, value, script(Uint8Array), sig?(Uint8Array) }]
// outputs: [{ value, script(Uint8Array) }]
function serialize(tx) {
return concat(
u32le(tx.version ?? 2),
varint(tx.inputs.length),
...tx.inputs.map((i) => concat(outpoint(i), varbytes(i.unlocking || new Uint8Array(0)), u32le(i.sequence ?? 0xffffffff))),
varint(tx.outputs.length),
...tx.outputs.map((o) => concat(u64le(o.value), varbytes(o.script))),
u32le(tx.locktime ?? 0),
);
}
function sighash(tx, index) {
const inp = tx.inputs[index];
const hashPrevouts = dsha(concat(...tx.inputs.map(outpoint)));
const hashSequence = dsha(concat(...tx.inputs.map((i) => u32le(i.sequence ?? 0xffffffff))));
const hashOutputs = dsha(concat(...tx.outputs.map((o) => concat(u64le(o.value), varbytes(o.script)))));
const preimage = concat(
u32le(tx.version ?? 2),
hashPrevouts, hashSequence,
outpoint(inp),
varbytes(inp.script),
u64le(inp.value),
u32le(inp.sequence ?? 0xffffffff),
hashOutputs,
u32le(tx.locktime ?? 0),
u32le(SIGHASH_ALL_FORKID),
);
return dsha(preimage);
}
// signer(input, index, digest) -> { sig: DER bytes, publicKey }
function sign(tx, signer) {
tx.inputs.forEach((inp, i) => {
const { sig, publicKey } = signer(inp, i, sighash(tx, i));
inp.unlocking = concat(pushdata(concat(sig, Uint8Array.from([SIGHASH_ALL_FORKID]))), pushdata(publicKey));
});
const raw = serialize(tx);
return { raw, hex: toHex(raw), txid: toHex(dsha(raw).reverse()) };
}
// Largest-first accumulation. `targets` = [{ value, script }]; returns
// { inputs, outputs, fee, change } or throws when funds don't cover it.
// sendMax: spend every UTXO into targets[0] and no change.
function select(utxos, targets, satPerByte, changeScript, { sendMax = false } = {}) {
const sorted = utxos.slice().sort((a, b) => b.value - a.value);
const total = sorted.reduce((a, u) => a + u.value, 0);
if (sendMax) {
if (targets.length !== 1) throw new Error("send max needs exactly one recipient");
const fee = feeFor(sorted.length, 1, satPerByte);
const value = total - fee;
if (!sorted.length || value < DUST) throw new Error("balance too small to send");
return { inputs: sorted, outputs: [{ value, script: targets[0].script }], fee, change: 0 };
}
const want = targets.reduce((a, t) => a + t.value, 0);
for (const t of targets) if (t.value < DUST) throw new Error(`amount below dust limit (${DUST} sat)`);
const chosen = []; let sum = 0;
for (const u of sorted) {
chosen.push(u); sum += u.value;
const feeWithChange = feeFor(chosen.length, targets.length + 1, satPerByte);
if (sum >= want + feeWithChange) {
const change = sum - want - feeWithChange;
if (change >= DUST) {
return { inputs: chosen, outputs: [...targets, { value: change, script: changeScript }], fee: feeWithChange, change };
}
// Change would be dust: fold it into the fee, one output fewer.
const fee = sum - want;
return { inputs: chosen, outputs: targets.slice(), fee, change: 0 };
}
const feeNoChange = feeFor(chosen.length, targets.length, satPerByte);
if (sum >= want + feeNoChange && sum - want - feeNoChange < DUST) {
return { inputs: chosen, outputs: targets.slice(), fee: sum - want, change: 0 };
}
}
const short = want + feeFor(Math.max(1, sorted.length), targets.length + 1, satPerByte) - total;
throw new Error(`insufficient funds: need about ${short} more sat`);
}
return { SIGHASH_ALL_FORKID, DUST, serialize, sighash, sign, select, feeFor, estimateSize, toHex, fromHex, dsha };
};