aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-09 19:55:37 -0700
committerAdam Soltys <[email protected]>2026-08-09 19:55:37 -0700
commit03fda2bc96a9bcaf2f864e5e80ad338948689c98 (patch)
treef6abe40154e36db6fa73cb0f022ec748f664365f /src
parent75270f0329c95422d28d7c15d2b532673eb8343d (diff)
downloadcoinosv3-03fda2bc96a9bcaf2f864e5e80ad338948689c98.tar.gz
coinosv3-03fda2bc96a9bcaf2f864e5e80ad338948689c98.zip
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 <[email protected]> Claude-Session: https://claude.ai/code/session_01DbA53PJA8fgSvMcEzfuw22
Diffstat (limited to 'src')
-rw-r--r--src/ark/manager.js18
-rw-r--r--src/features/ark.js22
-rw-r--r--src/i18n.js1
3 files changed, 30 insertions, 11 deletions
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.',