aboutsummaryrefslogtreecommitdiffstats
path: root/src/app.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-07-31 08:04:33 -0700
committerAdam Soltys <[email protected]>2026-07-31 08:04:33 -0700
commitad281e832ed9e36b777f03ed54e799df7d70ffc9 (patch)
tree2e7ba6df785e6b8f74266c56a7ca2006cbb9579b /src/app.js
parent72e407bf6d7d09923d15461be82685017a0887be (diff)
downloadcoinosv3-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.js20
1 files changed, 14 insertions, 6 deletions
diff --git a/src/app.js b/src/app.js
index b0a5fec..5aa1316 100644
--- a/src/app.js
+++ b/src/app.js
@@ -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' },