aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-28 19:19:17 -0700
committerAdam Soltys <[email protected]>2026-06-28 19:19:17 -0700
commit656726e17684071e929a2f0453cdd306c4d8dc12 (patch)
tree3530fc737a77d43974beec807aac60a6e8303c9f
parent93631253030839509551d7e1e6a995ed60e31b96 (diff)
downloadcoinosv3-656726e17684071e929a2f0453cdd306c4d8dc12.tar.gz
coinosv3-656726e17684071e929a2f0453cdd306c4d8dc12.zip
Swaps: gate 'Receive over Lightning' on watch-only (was throwing 'uint8array expected')
A watch-only wallet has no private key, so swapNode().privateKey is null and startReverse's sha256(node.privateKey) threw 'uint8array expected'. The receive card wasn't gated by watchOnly (the pay card already was). Hide both for watch-only and show a prompt to re-enter the recovery phrase, since swaps need the key to derive the preimage and claim on-chain. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
-rw-r--r--src/app.js24
1 files changed, 15 insertions, 9 deletions
diff --git a/src/app.js b/src/app.js
index 11fd88b..a0c6b8c 100644
--- a/src/app.js
+++ b/src/app.js
@@ -2597,20 +2597,26 @@ 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) {
+ 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;
return h('div', { class: 'col', style: 'gap:12px' },
- 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')),
+ // 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('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.'),