diff options
| author | Adam Soltys <[email protected]> | 2026-09-06 18:09:16 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-06 18:09:16 -0700 |
| commit | 95cf88f1246118eb6a672a5b4b6545331c9ddef4 (patch) | |
| tree | e704b2f49fad1fb57111db64a3500bfdc08f5a39 /src/features | |
| parent | dcfabe9165e7b0f48d9d3f645f762b84e6bc48b8 (diff) | |
| download | coinosv3-95cf88f1246118eb6a672a5b4b6545331c9ddef4.tar.gz coinosv3-95cf88f1246118eb6a672a5b4b6545331c9ddef4.zip | |
Interim safety for on-chain gifts ahead of the crypto redesign. (1) On-chain (Savings) gift creation now shows a hard bearer-risk warning: the presigned input is redirectable, a sweeper bot is actively front-running shared links, so share privately and claim immediately (Spending/ark gifts are unaffected). (2) The false "success" is fixed: a broadcast returning OK was never proof WE won — a higher-fee sweep can evict our claim (the "claimed! → empty wallet" the bot produced). verifyClaimWon now polls the funding coin after broadcast and, if it settles spent by a txid that isn't ours, retracts the celebration and shows the already-taken screen. Full architectural fix (ephemeral-key gifts, non-malleable SIGHASH_ALL claim) is next
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/gifts.js | 36 |
1 files changed, 36 insertions, 0 deletions
diff --git a/src/features/gifts.js b/src/features/gifts.js index 2b8c6c7..8aec560 100644 --- a/src/features/gifts.js +++ b/src/features/gifts.js @@ -459,6 +459,12 @@ export function giftsFeature(ctx) { render(); } }).catch(() => {}); + // A broadcast that returns OK is not proof WE won: the on-chain gift's + // presigned input is redirectable, so a higher-fee sweep can evict our + // claim and the coin lands elsewhere — the "success then empty wallet" + // the sweeper bot produced. Watch the funding coin: if it gets spent by + // a txid that ISN'T ours, retract the celebration and say it was taken. + verifyClaimWon(giftOutpoints(ui.claimCode)[0], claim.txid).catch(() => {}); } catch (e) { // Broadcast failed — most likely someone claimed it in the race window. // Re-check the funding coin and, if spent, show the "already claimed" screen. @@ -479,6 +485,29 @@ export function giftsFeature(ctx) { render(); } + // Poll the funding coin after an on-chain claim: if it settles spent by a + // DIFFERENT txid than ours, a competing sweep won the race — retract the + // false "claimed!" celebration and show the taken screen. Best-effort; + // stops as soon as it sees a confirmed spender. + async function verifyClaimWon(op, ourTxid) { + if (!op || !ourTxid) return; + for (let i = 0; i < 6; i++) { + await new Promise((r) => setTimeout(r, i < 2 ? 4000 : 12000)); + let res; + try { res = await wallet.api.outspend(op.txid, op.vout); } catch { continue; } + if (!res || !res.spent) continue; + if (res.txid && res.txid !== ourTxid) { + // someone else's tx spent the gift coin — our claim lost + ui.giftJustClaimed = null; ui.arkLnPaid = null; + ui.claimStep = 'welcome'; ui.claimedAmount = 0; + ui.claimTaken = { txid: res.txid }; + render(); + return; + } + if (res.status && res.status.confirmed) return; // ours confirmed — done + } + } + // Re-view a previously created gift's link + QR (from its stored record), so // the sender can re-share an unclaimed gift. function viewGiftView() { @@ -642,6 +671,13 @@ export function giftsFeature(ctx) { ui.giftSource === 'ark' ? t('giftArkNote') : ui.giftMax ? t('giftAllNote') : t('giftMinNote', { n: fmtAmount(giftMinimum(giftRate())) + ' ' + unitLabel() })), + // Interim safety: on-chain gift links are bearer instruments whose + // presigned input is currently redirectable (a sweeper bot is + // actively front-running shared links). Warn hard until the + // ephemeral-key redesign lands; Spending (ark) gifts are unaffected. + ui.giftSource === 'chain' + ? h('div', { class: 'notice warn small' }, t('giftChainWarn')) + : null, ui.giftError && h('div', { class: 'notice err' }, ui.giftError), ui.busy ? h('button', { class: 'btn-block', disabled: true }, h('span', { class: 'spinner sm' })) |
