diff options
| author | Adam Soltys <[email protected]> | 2026-06-30 09:29:46 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-30 09:29:46 -0700 |
| commit | 6d649f9bdd0582520e23dff4b5a967f0a125857e (patch) | |
| tree | e8312ef64fc95a30144b50bbb563829eba6bc26b | |
| parent | c74a3181a415024eea016727a11a1fc59335697d (diff) | |
| download | coinosv3-6d649f9bdd0582520e23dff4b5a967f0a125857e.tar.gz coinosv3-6d649f9bdd0582520e23dff4b5a967f0a125857e.zip | |
Swaps: reuse the generic 'Payment received' screen for LN receives
A reverse-swap claim is an on-chain receive, so instead of a separate LN
'Received!' card (which stacked with the generic celebration and needed
suppression), credit the claimed coin to the wallet the instant we broadcast the
claim — new wallet.creditReceive() mirrors the instant-payment watcher path
(adds the utxo + pending row, advances the receive index, emits). The generic
'Payment received!' then fires immediately with the right amount, one screen, one
tap. Dropped the LN success card + the suppression logic.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
| -rw-r--r-- | src/app.js | 19 | ||||
| -rw-r--r-- | src/swap.js | 6 | ||||
| -rw-r--r-- | src/wallet.js | 30 |
3 files changed, 42 insertions, 13 deletions
@@ -2602,11 +2602,7 @@ function receiveTab() { // Until receiveSeenIndex has been baselined (post-scan, in enterWallet) it // stays null and we never celebrate. The recency guard additionally ensures an // already-old payment never celebrates when a wallet is opened. - // While the Lightning "Received!" card is up (a reverse-swap claim is also an - // on-chain receive that advances the index), suppress this generic celebration - // so the two don't stack — its Done dismisses both. - const lnClaimShown = !!(ui.lnReceiveId && (() => { const r = swaps.list().find((s) => s.id === ui.lnReceiveId); return r && ['claimed', 'success'].includes(r.status); })()); - if (!lnClaimShown && ui.receiveSeenIndex != null && wallet.nextReceiveIndex > ui.receiveSeenIndex && hasRecentIncoming()) { + if (ui.receiveSeenIndex != null && wallet.nextReceiveIndex > ui.receiveSeenIndex && hasRecentIncoming()) { // Mark it acknowledged as soon as it's shown — so a refresh or navigating // away (without tapping) won't bring the celebration back. It stays visible // this session (ui.receiveSeenIndex is unchanged) until the tap below. @@ -2670,14 +2666,13 @@ function lnReceiveContent() { swaps.reverseLimits().then((l) => { ui.swapLimits = l; ui._swapLimitsLoading = false; render(); }).catch(() => { ui._swapLimitsLoading = false; }); } const lim = ui.swapLimits; - const rec = ui.lnReceiveId ? swaps.list().find((s) => s.id === ui.lnReceiveId) : null; + let rec = ui.lnReceiveId ? swaps.list().find((s) => s.id === ui.lnReceiveId) : null; if (rec && ['claimed', 'success'].includes(rec.status)) { - return [ - h('div', { class: 'check-badge' }, '✓'), - h('h3', { style: 'margin:0' }, t('lnReceived')), - rec.received ? h('div', { class: 'amount-pos', style: 'font-size:18px' }, '+' + fmtAmount(rec.received) + ' ' + unitLabel()) : null, - h('button', { class: 'btn-block', onClick: () => { ui.lnReceiveId = null; ui.swapReverseAmt = ''; ui.receiveSeenIndex = wallet.nextReceiveIndex; wallet.setReceiveAck(wallet.nextReceiveIndex); render(); } }, t('done')), - ]; + // Claimed — the coin is credited on-chain (swap.js → creditReceive), so the + // generic "Payment received!" celebration shows it like any receive. Just + // reset the Lightning flow back to its form. + ui.lnReceiveId = null; + rec = null; } if (rec) { let svg = null; diff --git a/src/swap.js b/src/swap.js index 129712d..9afe7e7 100644 --- a/src/swap.js +++ b/src/swap.js @@ -318,10 +318,14 @@ export class SwapManager { const preimage = sha256(node.privateKey); const d = swapP2TR({ kind: 'reverse', preimageHash160: hash160(preimage), boltzPub33: hexToBytes(rec.refundPublicKey), halPub33: node.publicKey, lockTime: rec.timeoutBlockHeight, network: net }); const fee = this._spendFee(); - const hex = buildSwapSpend({ pay: d.pay, leaf: d.claimLeaf, halPriv: node.privateKey, preimage, lockupTxid: u.txid, lockupVout: u.vout, lockupValue: u.value, destAddress: w.freshReceive().address, fee, network: net }); + const destAddress = w.freshReceive().address; + const hex = buildSwapSpend({ pay: d.pay, leaf: d.claimLeaf, halPriv: node.privateKey, preimage, lockupTxid: u.txid, lockupVout: u.vout, lockupValue: u.value, destAddress, fee, network: net }); const txid = await w.api.broadcast(hex); rec.status = 'claimed'; rec.claimTxid = txid; rec.received = u.value - fee; this._save(rec); this._stop(id); + // Credit the claimed coin (single output → destAddress) now, so the balance + // + generic "payment received" fire instantly instead of waiting for a scan. + if (w.creditReceive) w.creditReceive({ txid, vout: 0, value: rec.received, address: destAddress }); } catch (e) { rec.lastError = String(e?.message || e); this.onUpdate(rec); } }; this.timers.set(id, setInterval(tick, 3000)); tick(); diff --git a/src/wallet.js b/src/wallet.js index 596ca1b..1972f22 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -1922,6 +1922,36 @@ export class Wallet { return true; } + // Credit an on-chain receive we produced ourselves (a reverse-swap claim) the + // instant we broadcast it — so the balance + generic "payment received" fire + // without waiting for the next scan, exactly like the watcher does for inbound + // payments. `address` must be one of our receive (chain-0) addresses; returns + // false (caller relies on the scan) if it isn't known or the coin already exists. + creditReceive({ txid, vout, value, address }) { + let entry = this.receive.find((e) => e.address === address); + if (!entry) { + const info = this.addrMap.get(address); + if (info && info.chain === 0) entry = this.receive.find((e) => e.index === info.index); + } + if (!entry) return false; + if (this.utxos.find((u) => utxoId(u) === `${txid}:${vout}`)) return false; + this.utxos.push({ txid, vout, value, address, chain: 0, index: entry.index, confirmed: false }); + entry.used = true; + entry.pending = (entry.pending || 0) + value; + if (!this.txs.find((t) => t.txid === txid)) { + this.txs.push({ txid, net: value, fee: 0, vsize: 0, confirmed: false, blockTime: 0, blockHeight: 0, firstSeen: Date.now() }); + } + this.nextReceiveIndex = firstUnused(this.receive); + this.utxos.sort((a, b) => b.value - a.value); + this._sortTxs(); + this._recomputeBalanceFromUtxos(); + this.loaded = true; + this.saveCache(); + this.retrack(); + this.emit(); + return true; + } + // Optimistically reflect a just-broadcast outgoing tx so the balance and // history update instantly instead of waiting for the post-send rescan: drop // the spent coins, credit our change as pending, and add a pending history |
