From 2f0e6d052efbca7f695dda942ab7da6221da3aa9 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 19 Jun 2026 00:09:21 -0700 Subject: receive/invoice: build POST body from snapshot, don't mutate $derived The merchant receive and invoice pages mutated the $derived `invoice` proxy (setType/setAmount/setMemo) then POSTed it. Mutating a $derived and JSON.stringify-ing it serializes unreliably, so a stale type (on-chain when lightning was expected) and a stale/10x-shifted amount could leak from a prior interaction into POST /invoice (funk POS bug). Build the body as { ...$state.snapshot(invoice), ...changes } so the intended type/amount/address_type reliably win; update() takes the plain body. Front-end only. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/routes/(app)/[username]/receive/+page.svelte | 23 ++++++++++++----------- src/routes/(app)/invoice/[id]/+page.svelte | 23 ++++++++++++----------- 2 files changed, 24 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/src/routes/(app)/[username]/receive/+page.svelte b/src/routes/(app)/[username]/receive/+page.svelte index cd692abf..4bc37a7f 100644 --- a/src/routes/(app)/[username]/receive/+page.svelte +++ b/src/routes/(app)/[username]/receive/+page.svelte @@ -53,10 +53,10 @@ amountFiat, tip; - let update = async () => { + let update = async (body) => { try { ({ id } = await post(`/invoice`, { - invoice, + invoice: body, user: { username, currency }, })); @@ -77,10 +77,12 @@ if (type === types.lightning && !amount && typeof newAmount === "undefined") goto(`/${username}/receive`, { invalidateAll: true, noScroll: true }); else { - if (typeof newAmount !== "undefined") invoice.amount = newAmount; - invoice.address_type = address_type; - invoice.type = type; - await update(); + // Build the POST body from a snapshot + explicit changes. Mutating the + // $derived `invoice` proxy and POSTing it serializes unreliably, leaking + // stale type/amount from a prior interaction (see funk POS bug). + const body = { ...$state.snapshot(invoice), type, address_type }; + if (typeof newAmount !== "undefined") body.amount = newAmount; + await update(body); } settingType = false; }; @@ -93,10 +95,9 @@ if (typeof $amountPrompt === "undefined") $amountPrompt = true; amount = newAmount; - invoice.amount = newAmount; - invoice.tip = null; + const body = { ...$state.snapshot(invoice), amount: newAmount, tip: null }; - await update(); + await update(body); }; let newAmount = $state(0); @@ -109,8 +110,8 @@ e.stopPropagation(); settingMemo = false; - invoice.memo = memo; - await update(); + const body = { ...$state.snapshot(invoice), memo }; + await update(body); }; let memo = $state(); diff --git a/src/routes/(app)/invoice/[id]/+page.svelte b/src/routes/(app)/invoice/[id]/+page.svelte index 29dd815d..a2a299bd 100644 --- a/src/routes/(app)/invoice/[id]/+page.svelte +++ b/src/routes/(app)/invoice/[id]/+page.svelte @@ -70,10 +70,10 @@ if ($amountPrompt && !amount) toggleAmount(); }); - let update = async () => { + let update = async (body) => { try { ({ id } = await post(`/invoice`, { - invoice, + invoice: body, user: { username, currency }, })); @@ -94,10 +94,12 @@ if (type === types.lightning && !amount && typeof newAmount === "undefined") goto(`/${username}/receive`, { invalidateAll: true, noScroll: true }); else { - if (typeof newAmount !== "undefined") invoice.amount = newAmount; - invoice.address_type = address_type; - invoice.type = type; - await update(); + // Build the POST body from a snapshot + explicit changes. Mutating the + // $derived `invoice` proxy and POSTing it serializes unreliably, leaking + // stale type/amount/hash from a prior interaction (see funk POS bug). + const body = { ...$state.snapshot(invoice), type, address_type }; + if (typeof newAmount !== "undefined") body.amount = newAmount; + await update(body); } settingType = false; }; @@ -113,10 +115,9 @@ settingAmount = false; if (typeof $amountPrompt === "undefined") $amountPrompt = true; - invoice.amount = newAmount; - invoice.tip = 0; + const body = { ...$state.snapshot(invoice), amount: newAmount, tip: 0 }; - await update(); + await update(body); }; let setMemo = async (e) => { @@ -124,8 +125,8 @@ e.stopPropagation(); settingMemo = false; - invoice.memo = memo; - await update(); + const body = { ...$state.snapshot(invoice), memo }; + await update(body); }; let settingMemo = $state(); -- cgit v1.2.3