diff options
| author | Adam Soltys <[email protected]> | 2026-07-31 08:04:33 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-07-31 08:04:33 -0700 |
| commit | ad281e832ed9e36b777f03ed54e799df7d70ffc9 (patch) | |
| tree | 2e7ba6df785e6b8f74266c56a7ca2006cbb9579b /src/app.js | |
| parent | 72e407bf6d7d09923d15461be82685017a0887be (diff) | |
| download | coinosv3-ad281e832ed9e36b777f03ed54e799df7d70ffc9.tar.gz coinosv3-ad281e832ed9e36b777f03ed54e799df7d70ffc9.zip | |
Fix send-form UX around lightning pays; harden ln state machines
- a rejected cosign (gRPC INVALID_ARGUMENT) returns the input vtxo to
spendable and fails the action instead of leaving it pending forever —
the stuck-pending state that pushed small zaps onto Boltz's 25k minimum
- ark ln-pay now takes over when funds are settling (retrying quote) rather
than falling through to Boltz
- typing a destination clears stale send errors (also cleared on tab
switch); paste path likewise
- typed lightning addresses no longer auto-advance mid-keystroke (the
matcher fired at `[email protected]` before `.com`, yanking the form away): the send
form offers an explicit "Zap <addr>" button; pasted/scanned addresses
still advance immediately
- ln-recv: HTLC vtxos anchored to a not-yet-confirmed pool round park and
retry instead of throwing; the requested HTLC expiry is anchored on first
prepare so retries can't inflate it past the server's grant
Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat (limited to 'src/app.js')
| -rw-r--r-- | src/app.js | 20 |
1 files changed, 14 insertions, 6 deletions
@@ -38,7 +38,9 @@ const featureHook = (name, ...args) => { return null; }; const featureAll = (name, ...args) => FEATURES.flatMap((f) => (f[name] ? f[name](...args) : [])); -const featureMatchSend = (text) => FEATURES.some((f) => f.matchSendText && f.matchSendText(text)); +// `typed` distinguishes keystroke input from paste/scan: some matchers (a +// lightning address) match mid-typing and must not yank the form away. +const featureMatchSend = (text, typed) => FEATURES.some((f) => f.matchSendText && f.matchSendText(text, typed)); @@ -1973,6 +1975,7 @@ function tabsBar() { tabs.map(([id, label]) => tabBtn(label, ui.tab === id, () => { ui.tab = id; + ui.sendError = ''; // a stale send error never survives a tab change ui.revealShown = false; // re-mask the recovery phrase whenever tabs change ui.txDetail = null; // back to the history list when leaving/returning ui.arkMoveDetail = null; @@ -2418,15 +2421,18 @@ function recipientRow(s, r, i) { // A pasted/typed/scanned payload a feature recognizes (e.g. a bolt11) jumps // straight to that feature's own confirmation flow. - const tryFeature = (v) => featureMatchSend(v); + const tryFeature = (v, typed) => featureMatchSend(v, typed); const addrInput = h('input', { type: 'text', class: 'mono-input grow', placeholder: i === 0 ? t('destPlaceholder') : 'bc1q…', autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: r.address, onInput: (e) => { const v = e.target.value; r.address = v; syncCheck(); - if (tryFeature(v)) return; // a bolt11 advances to its own confirmation - if (destReady(v) !== r._ready) render(); // reveal/hide amount + controls as validity flips + const hadError = !!ui.sendError; + ui.sendError = ''; // editing the destination starts over — drop stale errors + if (tryFeature(v, true)) return; // a bolt11 advances to its own confirmation + // reveal/hide amount + controls as validity flips (or an error clears) + if (hadError || destReady(v) !== r._ready) render(); }, }); const row = h( @@ -2434,7 +2440,7 @@ function recipientRow(s, r, i) { { class: 'col gap6' }, h('div', { class: 'input-group' }, addrInput, - pasteBtn((text) => { addrInput.value = text; r.address = text; syncCheck(); if (!tryFeature(text) && destReady(text) !== r._ready) render(); }), + pasteBtn((text) => { addrInput.value = text; r.address = text; ui.sendError = ''; syncCheck(); if (!tryFeature(text)) render(); }), i === 0 && canScan() && h('button', { type: 'button', class: 'btn-sm', title: t('scanQr'), onClick: scanIntoSend, html: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8V5a1 1 0 0 1 1-1h3M16 4h3a1 1 0 0 1 1 1v3M20 16v3a1 1 0 0 1-1 1h-3M8 20H5a1 1 0 0 1-1-1v-3"/></svg>', @@ -2500,7 +2506,9 @@ function sendForm() { onClick: () => { s.recipients.push({ address: '', amount: '' }); s.max = false; render(); }, }, t('addRecipient')) ), - ready && arkDest && (featureHook('sendFormNote', plainAddr) || null), + // any feature may annotate the destination (ark hint, lightning-address + // zap offer, …) — each hook decides for itself whether the text is its + s.recipients.length === 1 ? (featureHook('sendFormNote', plainAddr) || null) : null, ready && !arkDest && h( 'div', { class: 'field' }, |
