// Check the Electrum server's word on what each input is worth before // signing a BTC/DGB transaction. // // Coin selection, change and the fee on the approval overlay are computed // from listunspent's `value`. A legacy (P2PKH) signature does not commit to // the value of the coin it spends, so a server that under-reported a UTXO // made Aegis sign a transaction whose real fee was the difference — up to // bitcoinjs's 5000 sat/vB ceiling — while the overlay showed the small, // planned fee. A segwit v0 signature commits to its own input's value only, // which still leaves the two-request variant (lie about a different input // each time, combine the signatures). Taproot commits to every input's // amount and script, so a lie there just makes the signature invalid. // // For every non-taproot input the previous transaction is fetched, its txid // recomputed (so the server cannot hand over a different one), and the // output's value and script compared with what was planned. Any mismatch // refuses to sign. "use strict"; async function verifyFunding({ chosen, client, Transaction }) { const need = chosen.filter((u) => u.entry.family !== "bip86"); const uniq = [...new Set(need.map((u) => u.txid))]; const got = await Promise.all(uniq.map((txid) => client.call("blockchain.transaction.get", [txid, false]))); const prevHex = new Map(); uniq.forEach((txid, i) => prevHex.set(txid, String(got[i] || ""))); for (const u of need) { const hex = prevHex.get(u.txid); let tx; try { tx = Transaction.fromHex(hex); } catch { throw new Error(`the server sent an unreadable transaction for input ${u.txid}:${u.vout}; not signing`); } if (tx.getId() !== u.txid) throw new Error(`the server sent a different transaction for input ${u.txid}:${u.vout}; not signing`); const out = tx.outs[u.vout]; if (!out) throw new Error(`input ${u.txid}:${u.vout} does not exist; not signing`); if (BigInt(out.value) !== BigInt(u.value)) { throw new Error(`the server misreported input ${u.txid}:${u.vout} (said ${u.value}, the transaction says ${out.value}); not signing`); } const script = u.entry.script ? Buffer.from(u.entry.script) : (u.entry.scriptHex ? Buffer.from(u.entry.scriptHex, "hex") : null); if (script && !Buffer.from(out.script).equals(script)) { throw new Error(`input ${u.txid}:${u.vout} is not paid to this wallet's address; not signing`); } } return prevHex; } // The fee the signed transaction actually pays must be the one the user // approved. function assertFee(psbt, plan) { let actual; try { actual = psbt.getFee(); } catch { return; } // a taproot-only PSBT without full prevouts if (BigInt(actual) !== BigInt(plan.fee)) { throw new Error(`the transaction would pay a fee of ${actual}, not the ${plan.fee} you approved; not signing`); } } module.exports = { verifyFunding, assertFee };