// 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 }; };