Aegis: BCH payments list only the payees and show a site's OP_RETURN data

The HD wallet's plan listed the change output as a recipient with no
address, so a dapp payment overlay showed "To #2: undefined" and an
Amount and Total inflated by the change; the imported wallet's total
counted change and left out the fee. Plans now list payees only, with
total = payees + fee.

A site's memo went on-chain as OP_RETURN data without appearing on the
overlay, and could ride on a silent allowance payment. It is now a row,
and a payment carrying data always asks. A 32-byte-hash cashaddr was
forced into a 20-byte template, producing an unspendable script; it is
refused.
This commit is contained in:
Local Dev 2026-10-04 04:01:58 +02:00
parent df7f26552a
commit bd6e990598
3 changed files with 24 additions and 10 deletions

View file

@ -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,

View file

@ -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,
};

View file

@ -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,
};
}