Setup 5d15508bba929f1f074c052ac933863eadf6eb8e56984ebd5a1af75e80626643 Portable a5d346b97f5a13d85fa3bd301a72075ddb82fe636d7b1a51840ffd5a16d879f4 Bundled since 0.3.27: 32d4b75 - Aegis (bchwallet) gains its own update card in Settings > General beside Ariadne. Check for updates hits the same signed OTA endpoint the boot timer uses; Restart to apply appears when a signed newer version is staged. Uses the existing addons-check-updates + a new app-restart IPC. New Aegis versions ship without a Theseus release. 32d4b75 (same commit) - DevTools (F12 / Ctrl+Shift+I) opens docked to the right of the tab (mode: 'right') instead of a detached window. Matches stock Chrome. Users who prefer detached can drag out via the DevTools own toolbar. b71c925 - Search-engine favicons in Settings > Search now use Google's /s2/favicons service — DuckDuckGo's ip3 source returned 404 for enough hosts (Brave, Bing, Yandex, etc.) that half the list was falling through to the emoji placeholder. Deployed. Verified LIVE 0.3.28.
122 lines
5.6 KiB
JavaScript
122 lines
5.6 KiB
JavaScript
// 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 };
|
|
};
|