diff options
| author | Adam Soltys <[email protected]> | 2026-07-05 23:11:32 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-07-05 23:11:32 -0700 |
| commit | eb89b9e3681d9eea331b6471ecc576f39a0fd5ec (patch) | |
| tree | 63135d74a974502bf1d64fce9bdeb2696b43cdb1 /src/features/ark.js | |
| parent | d24ba23df05bbc8072cbd97bec84a2a198951d03 (diff) | |
| download | coinosv3-eb89b9e3681d9eea331b6471ecc576f39a0fd5ec.tar.gz coinosv3-eb89b9e3681d9eea331b6471ecc576f39a0fd5ec.zip | |
Exit status: real progress, blocks-left countdown, honest failures
submitpackage answers HTTP 200 even when it rejects the package — the
verdict hides in package_msg/tx-results — so a mainnet no-fee-coin exit
sat at 'publishing on-chain…' forever believing its bare hop had
relayed. Rejections now surface (and become the actionable needs-a-fee-
coin message on the bumpless path). The status line shows hop progress
(step 2 of 9), the timelock wait counts down in blocks with a rough
wall-clock estimate, and a stuck exit that never published anything
gets a 'Cancel this exit' link that returns the vtxo to spendable.
Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat (limited to 'src/features/ark.js')
| -rw-r--r-- | src/features/ark.js | 33 |
1 files changed, 27 insertions, 6 deletions
diff --git a/src/features/ark.js b/src/features/ark.js index 972dc53..1f87541 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -297,15 +297,34 @@ export function arkFeature(ctx) { ? h('button', { class: 'btn-ghost btn-block', disabled: !!ui.arkBusy, onClick: doArkExit }, ui.arkBusy === 'exit' ? h('span', { class: 'spinner sm' }) : t('arkExitBtn', { n: spendables.length })) : null, - ...ark.state.actions.filter((a) => a.type === 'exit' && !['done', 'failed'].includes(a.step)).map((a) => - h('div', { class: 'small muted' }, - a.step === 'chain' ? t('arkExitChainStatus', { n: fmtAmount(a.amountSat) }) - : a.step === 'timelock' ? t('arkExitTimelockStatus', { n: fmtAmount(a.amountSat), blocks: String(a.claimableAt) }) + ...ark.state.actions.filter((a) => a.type === 'exit' && !['done', 'failed'].includes(a.step)).map((a) => { + // blocks left in the timelock, with a rough wall-clock estimate + // (~10 min/block everywhere but regtest, where blocks are manual) + const left = a.step === 'timelock' ? Math.max(0, a.claimableAt - (a.tipSeen || 0)) : 0; + const mins = left * 10; + const eta = getNetwork() === 'regtest' ? '' + : mins >= 2880 ? ` (≈ ${Math.round(mins / 1440)} d)` + : mins >= 120 ? ` (≈ ${Math.round(mins / 60)} h)` + : ` (≈ ${mins} min)`; + return h('div', { class: 'small muted' }, + a.step === 'chain' ? t('arkExitChainStatus', { n: fmtAmount(a.amountSat), done: String(a.hopsDone || 0), total: String((a.txids || []).length) }) + : a.step === 'timelock' ? t('arkExitTimelockStatus', { n: fmtAmount(a.amountSat), blocks: String(left), eta }) : t('arkExitClaimingStatus', { n: fmtAmount(a.amountSat) }), a.lastError ? h('div', { class: a.actionable ? 'small err' : 'small faint' }, a.actionable ? a.lastError : t('arkExitRetrying')) - : null)) + : null, + // a stuck exit that never published anything can be safely undone + a.actionable && !(a.hopsDone > 0) + ? h('button', { class: 'linklike small', onClick: () => { + a.step = 'failed'; + const v = ark._vtxo(a.vtxoId); + if (v && v.state === 'pending') v.state = 'spendable'; + ark._save(); + render(); + } }, t('arkExitCancel')) + : null); + }) ); } @@ -379,9 +398,10 @@ export function arkFeature(ctx) { if (action.step === 'chain') { const txs = signedExitTxs(decoded, mgr.serverPub); let lastConfirmedHeight = 0; + let hopsDone = 0; for (const txi of txs) { const st = await mgr.chain.getTxStatus(txi.txid); - if (st?.confirmed) { lastConfirmedHeight = st.block_height; continue; } + if (st?.confirmed) { lastConfirmedHeight = st.block_height; hopsDone++; if (action.hopsDone !== hopsDone) { action.hopsDone = hopsDone; mgr._save(); } continue; } // /tx/:txid/status answers {confirmed:false} even for UNKNOWN txids // (electrs + mempool.space) — only /tx/:txid 404s definitively if (await mgr.chain.getTxHex(txi.txid)) return; // in mempool — wait @@ -433,6 +453,7 @@ export function arkFeature(ctx) { } if (action.step === 'timelock') { const tip = await mgr.chain.tipHeight(); + if (action.tipSeen !== tip) { action.tipSeen = tip; mgr._save(); } // for the blocks-left display if (tip < action.claimableAt) return; const keys = mgr._keyForVtxo(rec); const feeRate = Math.max(1, (wallet.feeRates && wallet.feeRates.halfHourFee) || 2); |
