diff options
| author | Adam Soltys <[email protected]> | 2026-06-28 23:14:22 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-28 23:14:22 -0700 |
| commit | 2bc2c984c3e5b7e31d6452b9ec9df5bc5fd6e469 (patch) | |
| tree | 8c410314e4f00ba571fbdfded1a78e789a1a5c52 /src | |
| parent | 677eea94b433685c4aa5f18c2ad9f18218cab30d (diff) | |
| download | coinosv3-2bc2c984c3e5b7e31d6452b9ec9df5bc5fd6e469.tar.gz coinosv3-2bc2c984c3e5b7e31d6452b9ec9df5bc5fd6e469.zip | |
Receive: add a Lightning option (reverse swap) next to Address + Silent payment
Receive now has a segmented [Address | Lightning | Silent payment] toggle.
'Lightning' takes an amount and creates a Boltz reverse-swap bolt11 to share;
when it's paid, Boltz locks up on-chain and the SwapManager claims it into this
wallet automatically (shown live, then 'Received!'). Gated to non-watch-only
wallets (a reverse swap needs our keys). Removed the duplicate 'Receive over
Lightning' card from the Swap tab. Verified in-browser on regtest: real limits
(Min 50,000) load via the proxy and an invoice is created.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 104 | ||||
| -rw-r--r-- | src/i18n.js | 8 |
2 files changed, 77 insertions, 35 deletions
@@ -2540,7 +2540,7 @@ async function doReverse() { const lim = ui.swapLimits || (ui.swapLimits = await swaps.reverseLimits()); if (!amt || amt < lim.min || amt > lim.max) { ui.swapError = `Enter ${lim.min.toLocaleString()}–${lim.max.toLocaleString()} sats`; return render(); } ui.swapError = ''; ui.swapBusy = true; render(); - try { await swaps.startReverse(amt); ui.swapReverseAmt = ''; } + try { const rec = await swaps.startReverse(amt); ui.lnReceiveId = rec.id; ui.swapReverseAmt = ''; } catch (e) { ui.swapError = e.message; } ui.swapBusy = false; render(); } @@ -2595,33 +2595,18 @@ function swapListView() { } function swapTab() { - // Pull the selected provider's real reverse-swap limits (min/max) once, to bound - // the amount instead of guessing — providers + networks differ. - if (!ui.swapLimits && !ui._swapLimitsLoading && !wallet.watchOnly) { - ui._swapLimitsLoading = true; - swaps.reverseLimits().then((l) => { ui.swapLimits = l; ui._swapLimitsLoading = false; render(); }).catch(() => { ui._swapLimitsLoading = false; }); - } - const lim = ui.swapLimits; + // Receiving over Lightning now lives under Receive → Lightning; this tab keeps + // paying an invoice (a submarine swap) + the in-flight swap list for now. return h('div', { class: 'col', style: 'gap:12px' }, - // Swaps need the private key (to derive the preimage and claim on-chain), so a - // watch-only wallet can't do them — prompt to re-enter the seed instead. wallet.watchOnly ? h('div', { class: 'card col' }, - h('h3', {}, '⚡ Lightning swaps'), - h('p', { class: 'small muted', style: 'margin:0' }, 'Re-enter your recovery phrase to send or receive over Lightning — swaps need your keys to claim on-chain.')) + h('h3', {}, '⚡ Pay over Lightning'), + h('p', { class: 'small muted', style: 'margin:0' }, 'Re-enter your recovery phrase to pay over Lightning — swaps need your keys to claim on-chain.')) : h('div', { class: 'card col' }, - h('h3', {}, '⚡ Receive over Lightning'), - h('p', { class: 'small muted', style: 'margin:0' }, 'Get a Lightning invoice; when paid, the sats are swapped on-chain into this wallet — non-custodial (you hold the preimage and claim on-chain).'), - h('label', { class: 'field' }, - h('span', { class: 'lab' }, 'Amount (sats)'), - h('input', { type: 'number', inputmode: 'numeric', placeholder: lim ? String(lim.min) : '100000', value: ui.swapReverseAmt || '', onInput: (e) => { ui.swapReverseAmt = e.target.value; } }), - lim ? h('span', { class: 'small muted' }, `Min ${lim.min.toLocaleString()} · max ${lim.max.toLocaleString()} sats`) : null), - h('button', { disabled: !!ui.swapBusy, onClick: doReverse }, ui.swapBusy ? '…' : 'Create invoice')), - wallet.watchOnly ? null : h('div', { class: 'card col' }, - h('h3', {}, '⚡ Pay a Lightning invoice'), - h('p', { class: 'small muted', style: 'margin:0' }, 'Pay a bolt11 from your on-chain balance: funds a swap that Boltz pays over Lightning.'), - h('textarea', { rows: 3, class: 'mono-input', placeholder: 'lnbcrt1…', value: ui.swapInvoice || '', onInput: (e) => { ui.swapInvoice = e.target.value; } }), - h('button', { disabled: !!ui.swapBusy, onClick: doSubmarine }, ui.swapBusy ? '…' : 'Pay invoice')), + h('h3', {}, '⚡ Pay a Lightning invoice'), + h('p', { class: 'small muted', style: 'margin:0' }, 'Pay a bolt11 from your on-chain balance: funds a swap that Boltz pays over Lightning.'), + h('textarea', { rows: 3, class: 'mono-input', placeholder: 'lnbcrt1…', value: ui.swapInvoice || '', onInput: (e) => { ui.swapInvoice = e.target.value; } }), + h('button', { disabled: !!ui.swapBusy, onClick: doSubmarine }, ui.swapBusy ? '…' : 'Pay invoice')), ui.swapError ? h('div', { class: 'notice err' }, ui.swapError) : null, swapListView() ); @@ -2703,27 +2688,76 @@ function receiveTab() { ); } - // One card with a toggle between the standard address and the silent-payment - // address (shown only when SP is available). + // One card with a toggle: on-chain address, Lightning (a Boltz reverse swap), + // and the silent-payment address — each shown only when available. const fresh = wallet.freshReceive(); const spAddr = wallet.silentPaymentsAvailable() ? wallet.silentPaymentAddress() : null; - const sp = spAddr && ui.receiveType === 'sp'; - const addr = sp ? spAddr : fresh.address; + const canLn = !wallet.watchOnly; // a reverse swap needs our keys to claim on-chain + let mode = ui.receiveType || 'address'; + if (mode === 'sp' && !spAddr) mode = 'address'; + if (mode === 'ln' && !canLn) mode = 'address'; + const seg = (spAddr || canLn) + ? h('div', { class: 'seg', style: 'display:flex;width:100%' }, + h('button', { type: 'button', class: (mode === 'address' ? 'active ' : '') + 'grow', onClick: () => { ui.receiveType = 'address'; render(); } }, t('receiveAddressTab')), + canLn ? h('button', { type: 'button', class: (mode === 'ln' ? 'active ' : '') + 'grow', onClick: () => { ui.receiveType = 'ln'; render(); } }, t('receiveLnTab')) : null, + spAddr ? h('button', { type: 'button', class: (mode === 'sp' ? 'active ' : '') + 'grow', onClick: () => { ui.receiveType = 'sp'; render(); } }, t('receiveSpTab')) : null + ) + : null; + if (mode === 'ln') { + return h('div', { class: 'card col', style: 'align-items:center;gap:14px' }, seg, ...lnReceiveContent()); + } + const addr = mode === 'sp' ? spAddr : fresh.address; return h( 'div', { class: 'card col', style: 'align-items:center;gap:14px' }, - spAddr - ? h('div', { class: 'seg', style: 'display:flex;width:100%' }, - h('button', { type: 'button', class: (!sp ? 'active ' : '') + 'grow', onClick: () => { ui.receiveType = 'address'; render(); } }, t('receiveAddressTab')), - h('button', { type: 'button', class: (sp ? 'active ' : '') + 'grow', onClick: () => { ui.receiveType = 'sp'; render(); } }, t('receiveSpTab')) - ) - : null, + seg, h('div', { html: qrSvg(addr) }), - h('div', { class: 'addr-box' + (sp ? ' break' : ''), style: 'width:100%' + (sp ? ';font-size:12px' : '') }, addr), + h('div', { class: 'addr-box' + (mode === 'sp' ? ' break' : ''), style: 'width:100%' + (mode === 'sp' ? ';font-size:12px' : '') }, addr), copyBtn(addr, t('copyAddress')) ); } +// Lightning receive = a Boltz reverse swap: show a bolt11 the user shares; when +// it's paid, Boltz locks up on-chain and the SwapManager claims it into this +// wallet automatically. Returns the card's inner children. +function lnReceiveContent() { + if (!ui.swapLimits && !ui._swapLimitsLoading) { + ui._swapLimitsLoading = true; + 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; + 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 = ''; render(); } }, t('done')), + ]; + } + if (rec) { + let svg = null; + try { svg = qrSvg(rec.invoice.toUpperCase(), { ec: 'L', mode: 'Alphanumeric' }); } catch {} + return [ + h('p', { class: 'small muted', style: 'margin:0;text-align:center' }, t('lnReceiveAwaiting')), + svg ? h('div', { html: svg }) : null, + h('div', { class: 'addr-box break', style: 'width:100%;font-size:11px' }, rec.invoice), + copyBtn(rec.invoice, t('copyInvoice')), + h('div', { class: 'row gap6', style: 'align-items:center' }, h('span', { class: 'spinner sm' }), h('span', { class: 'small muted' }, t('lnReceiveWatching'))), + h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.lnReceiveId = null; ui.swapReverseAmt = ''; ui.swapError = ''; render(); } }, t('back')), + ]; + } + return [ + h('p', { class: 'small muted', style: 'margin:0;text-align:center' }, t('lnReceiveIntro')), + h('label', { class: 'field', style: 'width:100%' }, + h('span', { class: 'lab' }, t('amountSatsLabel')), + h('input', { type: 'number', inputmode: 'numeric', placeholder: lim ? String(lim.min) : '100000', value: ui.swapReverseAmt || '', onInput: (e) => { ui.swapReverseAmt = e.target.value; } }), + lim ? h('span', { class: 'small muted' }, `Min ${lim.min.toLocaleString()} · max ${lim.max.toLocaleString()} sats`) : null), + ui.swapError ? h('div', { class: 'notice err' }, ui.swapError) : null, + h('button', { class: 'btn-primary btn-block', disabled: !!ui.swapBusy, onClick: doReverse }, ui.swapBusy ? h('span', { class: 'spinner' }) : t('lnCreateInvoice')), + ]; +} + // Silent-payment tweak indexer picker (per network) — the endpoint used to scan // for received silent payments. Only shown for wallets that can derive SP keys. function spIndexerCard() { diff --git a/src/i18n.js b/src/i18n.js index babe3e0..041b0be 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -347,7 +347,15 @@ const STR = { spReceiveTitle: 'Silent payment address', spReceiveDesc: "A reusable address that can't be tracked.", receiveAddressTab: 'Address', + receiveLnTab: 'Lightning', receiveSpTab: 'Silent payment', + lnReceiveIntro: 'Receive over Lightning — paid into this wallet on-chain, non-custodial.', + lnReceiveAwaiting: 'Share this invoice. When it’s paid, the sats land here automatically.', + lnReceiveWatching: 'Waiting for payment…', + lnReceived: 'Received!', + lnCreateInvoice: 'Create invoice', + copyInvoice: 'Copy invoice', + amountSatsLabel: 'Amount (sats)', spScan: 'Scan for silent payments', spScanning: 'Scanning…', spWatching: 'Watching for payments', |
