From 6394de43d4e596aaeddd2b262bb7a595679d8fbc Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Mon, 14 Sep 2026 08:53:35 -0700 Subject: A zap answered by two tabs no longer reports as failed. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every open tab of the same wallet sees every wallet-connect request. Two hidden tabs on one laptop both tried to pay the same zap: the ASP refused the second with "payment already in progress", and that error reached Amethyst half a second before the first tab's preimage. A Web Lock per request now lets one tab (or the worker) answer while the rest stay quiet, and a device that does meet "already in progress" waits for the sibling payment's outcome — relaying its preimage only if the payer never speaks — instead of shouting a failure over sats that went through. The losing side also stops writing a failed row into history for it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QYJfZ4A1TDCEY4Foq3CApR --- src/nwc-lock.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/nwc-lock.js (limited to 'src/nwc-lock.js') diff --git a/src/nwc-lock.js b/src/nwc-lock.js new file mode 100644 index 0000000..c4fec08 --- /dev/null +++ b/src/nwc-lock.js @@ -0,0 +1,14 @@ +// One request, one answerer per origin. +// +// Every open tab of the same wallet (and its service worker) sees every NWC +// request. Without exclusion two hidden tabs on one laptop both paid a zap: +// the loser's "payment already in progress" error reached the client half a +// second before the winner's preimage, and the zap showed as failed while +// the sats had gone. Web Locks are shared by every same-origin context, so +// the first to claim a request's lock answers it and the rest stay quiet. +// Where locks are unavailable (tests, ancient engines) the caller just runs. +export async function withRequestLock(id, fn, held = () => undefined) { + const locks = typeof navigator !== 'undefined' && navigator.locks; + if (!locks || !id) return fn(); + return locks.request(`nwc-req:${id}`, { ifAvailable: true }, (lock) => (lock ? fn() : held())); +} -- cgit v1.2.3