From 03fda2bc96a9bcaf2f864e5e80ad338948689c98 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 9 Aug 2026 19:55:37 -0700 Subject: A waiting board says what it waits for: the Moving line counts confirmations, a snag shows itself, and one stuck action no longer starves the queue behind it Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DbA53PJA8fgSvMcEzfuw22 --- src/ark/manager.js | 18 ++++++++++++------ src/features/ark.js | 22 +++++++++++++++++----- src/i18n.js | 1 + 3 files changed, 30 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/ark/manager.js b/src/ark/manager.js index 84c0769..a57b280 100644 --- a/src/ark/manager.js +++ b/src/ark/manager.js @@ -350,9 +350,11 @@ export class ArkManager { if (action.type === 'ln-recv') await this._driveLnRecv(action); if (action.lastError) { delete action.lastError; this._save(); } } catch (e) { - // transient errors leave the action where it is; a later sync retries - if (!(e instanceof GrpcError)) throw e; - if (action.type.startsWith('ln-')) { action.lastError = e.message; this._save(); } + // transient errors leave the action where it is; a later sync retries. + // Recorded and swallowed for EVERY type: one broken action rethrowing + // here would starve everything queued behind it, forever and silently. + action.lastError = e.message; + this._save(); } } } @@ -1209,10 +1211,14 @@ export class ArkManager { if (action.step === 'created') return; // waiting for funding txid const keys = this._key(action.keyIndex); if (action.step === 'funded') { + const need = this.info.requiredBoardConfirmations; const status = await this.chain.getTxStatus(action.fundingTxid); - if (!status?.confirmed) return; // wait for confirmations; retried on sync - const tip = await this.chain.tipHeight(); - if (tip - status.block_height + 1 < this.info.requiredBoardConfirmations) return; + // record progress so the UI can say WHERE the wait is (n/need confs) + const confs = status?.confirmed ? (await this.chain.tipHeight()) - status.block_height + 1 : 0; + if (confs !== action.confs || need !== action.needConfs) { action.confs = confs; action.needConfs = need; this._save(); } + // NB not `confs < need`: a failed tip fetch makes confs NaN, which must + // wait for the next sync rather than slip past the comparison + if (!(confs >= need)) return; // retried on sync const fundingOutpointRaw = new Uint8Array(36); fundingOutpointRaw.set(hex.decode(action.fundingTxid).reverse(), 0); // vout 0 diff --git a/src/features/ark.js b/src/features/ark.js index 54bd32b..30ca90c 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -2098,16 +2098,28 @@ export function arkFeature(ctx) { const b = arkBalance(); if (!b) return []; // Money mid-move: it has left Saving but hasn't landed in Spending, so - // say where it went rather than letting it vanish for a confirmation. - return b.boardingSat > 0 ? [{ label: t('movingLabel'), sat: b.boardingSat }] : []; + // say where it went rather than letting it vanish for a confirmation — + // with the confirmation count, so the wait has a visible reason. + if (!(b.boardingSat > 0)) return []; + const a = ((arkStateNow() || {}).actions || []).find( + (x) => x.type === 'board' && x.fundingTxid && !['done', 'failed'].includes(x.step) && x.needConfs); + const prog = a ? ` · ${Math.min(a.confs || 0, a.needConfs)}/${a.needConfs}` : ''; + return [{ label: t('movingLabel') + prog, sat: b.boardingSat }]; }, walletNotices() { - if (!arkRenewWarn || wallet.watchOnly) return []; - return [h('div', { class: 'notice err', style: 'margin:12px 0 0' }, + const out = []; + // A pending move that keeps erroring would otherwise wait in silence. + for (const a of ((arkStateNow() || {}).actions || [])) { + if (a.type === 'board' && a.lastError && !['done', 'failed'].includes(a.step)) + out.push(h('div', { class: 'notice err', style: 'margin:12px 0 0' }, + t('arkBoardStuck', { error: a.lastError }))); + } + if (arkRenewWarn && !wallet.watchOnly) out.push(h('div', { class: 'notice err', style: 'margin:12px 0 0' }, t('arkRenewWarn', { amount: fmtAmount(arkRenewWarn.sat) + ' ' + unitLabel(), date: new Date(arkRenewWarn.deadlineMs).toLocaleDateString(), - }))]; + }))); + return out; }, decorateTxRow(tx) { const s = arkStateNow(); diff --git a/src/i18n.js b/src/i18n.js index 1a2652f..677f4f8 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -464,6 +464,7 @@ const STR = { giftSourceArk: 'Spending', giftArkNote: 'Sent from Spending — the link claims instantly, with no fee.', arkRenewWarn: 'Your Spending balance ({amount}) is too small to renew itself and will expire around {date}. Receiving any amount — or spending it — keeps it alive.', + arkBoardStuck: 'Moving money hit a snag and will keep retrying: {error}', arkRenewWarnPush: 'Your Spending balance is too small to renew itself and expires around {date}. Receive any amount or spend it to keep it alive.', giftArkOutstanding: '{n} unclaimed instant gift(s)', giftArkRevoked: 'Gift swept back into Spending.', -- cgit v1.2.3