diff --git a/bundled-addons/aegis/index.js b/bundled-addons/aegis/index.js index 8c858cc9..cd38ef95 100644 --- a/bundled-addons/aegis/index.js +++ b/bundled-addons/aegis/index.js @@ -3656,7 +3656,8 @@ function registerPageMessages(api) { now[origin] = cur; api.storage.set("permissions", now); }; - if (budget && d.total <= remaining) { + // A payment carrying data for the chain is never silent. + if (budget && d.total <= remaining && !plan.memo) { // An allowance skips the overlay, not the PIN the user asked for on // every transaction. await requireDappTxPin(origin, "Bitcoin Cash payment"); @@ -3673,6 +3674,8 @@ function registerPageMessages(api) { rows.push({ label: "Amount", value: fmtBch(d.recipients.reduce((a, r) => a + r.value, 0)) + " BCH", strong: true }); rows.push({ label: "Fee", value: `${plan.fee} sat (${plan.feeRate} sat/B)` }); rows.push({ label: "Total", value: fmtBch(d.total) + " BCH" }); + // The site's OP_RETURN data is written on-chain under this payment. + if (plan.memo) rows.push({ label: "Data (OP_RETURN)", value: previewText(plan.memo, 220), mono: true }); const pick = await api.approvalModal({ title: "Send Bitcoin Cash?", origin, diff --git a/bundled-addons/aegis/lib/chain-bch-imported.js b/bundled-addons/aegis/lib/chain-bch-imported.js index 70865566..dd499dee 100644 --- a/bundled-addons/aegis/lib/chain-bch-imported.js +++ b/bundled-addons/aegis/lib/chain-bch-imported.js @@ -266,6 +266,7 @@ module.exports = function makeImportedBchAdapter({ : [{ to: spec?.to, value: spec?.amount ?? spec?.value }]; const outs = targets.map((t) => { const a = cashaddr.parseAny(t.to, sha256, this._net.prefix); + if (a.hash.length !== 20) throw new Error("this address type (32-byte hash) is not supported for sending yet"); const script = a.type === 0 ? Uint8Array.from([0x76, 0xa9, 0x14, ...a.hash, 0x88, 0xac]) : Uint8Array.from([0xa9, 0x14, ...a.hash, 0x87]); @@ -283,14 +284,16 @@ module.exports = function makeImportedBchAdapter({ const spendable = this._state.utxos.filter((u) => !u.token).sort((a, b) => (b.height > 0) - (a.height > 0)); if (!spendable.length) throw new Error("every unspent output in this wallet carries a token; there is no plain BCH to spend"); const sel = tx.select(spendable, outs, rate, changeScript, { sendMax: !!spec?.sendMax }); - const nonData = sel.outputs.filter((o) => !o.data); - const total = sel.outputs.reduce((a, o) => a + o.value, 0); + // Payees only (change is not a recipient); total = what leaves the + // wallet, i.e. payees + fee. It used to be every output including + // change, without the fee. + const payees = outs.filter((o) => !o.data); + const recipients = payees.map((o, i) => ({ to: o.to, value: sel.outputs[i].value })); + const total = recipients.reduce((a, r) => a + r.value, 0) + sel.fee; return { ...sel, feeRate: rate, - recipients: nonData - .filter((_, i) => outs[i] && !outs[i].data) - .map((o, i) => ({ to: outs[i].to, value: o.value })), + recipients, memo: spec?.memo || null, total, }; diff --git a/bundled-addons/aegis/lib/wallet.js b/bundled-addons/aegis/lib/wallet.js index 14b2e421..ef830f79 100644 --- a/bundled-addons/aegis/lib/wallet.js +++ b/bundled-addons/aegis/lib/wallet.js @@ -431,6 +431,9 @@ module.exports = function makeWallet({ client, keys, tx, cashaddr, sha256, stora const rate = Math.min(10, Math.max(1, Number(feeRate) || 1)); const outs = targets.map((t) => { const a = cashaddr.parseAny(t.to, sha256, keys.prefix); + // A 32-byte hash (P2SH32) does not fit the 20-byte templates below; + // it used to be forced into one, producing a script nobody can spend. + if (a.hash.length !== 20) throw new Error("this address type (32-byte hash) is not supported for sending yet"); const script = a.type === 0 ? Uint8Array.from([0x76, 0xa9, 0x14, ...a.hash, 0x88, 0xac]) : Uint8Array.from([0xa9, 0x14, ...a.hash, 0x87]); @@ -447,12 +450,17 @@ module.exports = function makeWallet({ client, keys, tx, cashaddr, sha256, stora .slice() .sort((a, b) => (b.height > 0) - (a.height > 0)); const sel = tx.select(spendable, outs, rate, changeEntry().script, { sendMax }); - // recipients only lists spendable (non-data) outputs, keeping the - // panel's summary honest — the memo is surfaced separately as .memo. - const spendable_outs = sel.outputs.filter((o) => !o.data); + // recipients lists the payees only — the memo is surfaced separately + // as .memo, and the change output (which select() appends after the + // payees) is not a recipient. It used to be listed as one with no + // address, and summed into Amount and Total. + const payees = outs.filter((o) => !o.data); + const recipients = payees.map((o, i) => ({ to: o.to, value: sel.outputs[i].value })); + const sent = recipients.reduce((a, r) => a + r.value, 0); return { ...sel, feeRate: rate, - recipients: spendable_outs.map((o, i) => ({ to: outs[i]?.to, value: o.value })), + recipients, + total: sent + sel.fee, memo: memo || null, }; }