From 124fee3c15556d62d6efd8714fd4501e95273ebd Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 25 Jun 2026 16:01:39 -0700 Subject: Gift claim: detect already-claimed (even unconfirmed) and block the race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opening a gift link now checks whether its funding coin is already spent — via the Esplora outspend endpoint, which reports a spend even by an unconfirmed claim. The Claim button is gated until that check passes; if the coin is spent, show an 'Already claimed' screen with a link to the claim tx instead of a claimable amount. doClaim also re-checks on broadcast failure (the race window), and refuses to run while unchecked/taken — so a second opener can't attempt a doomed double-spend. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/wallet.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'src/wallet.js') diff --git a/src/wallet.js b/src/wallet.js index ccd043a..e21710c 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -1748,6 +1748,22 @@ function _sumInputs(tx) { function _giftChange(tx) { return tx.outputsLength > 0 ? tx.getOutput(0).amount : 0n; } +// The gift's funding outpoints (the coins its presigned inputs spend). If any is +// already spent — even by an unconfirmed claim — the gift has been taken and +// re-claiming would be a doomed double-spend. Used to gate the claim screen. +export function giftOutpoints(code) { + try { + const tx = btc.Transaction.fromPSBT(base64urlnopad.decode(code)); + const out = []; + for (let i = 0; i < tx.inputsLength; i++) { + const inp = tx.getInput(i); + out.push({ txid: hex.encode(inp.txid), vout: inp.index }); + } + return out; + } catch { + return []; + } +} export function previewGift(code) { try { const tx = btc.Transaction.fromPSBT(base64urlnopad.decode(code)); -- cgit v1.2.3