diff options
| author | Adam Soltys <[email protected]> | 2026-07-05 22:06:51 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-07-05 22:06:51 -0700 |
| commit | ba5e28249aac75310303174ac48b4c67b67224cb (patch) | |
| tree | 8f40dc3843ba1cd00883c9234c00d5d17cd76b4f /src | |
| parent | 76e1a88b9c2eae1de0145ff077f038b57e68c3a8 (diff) | |
| download | coinosv3-ba5e28249aac75310303174ac48b4c67b67224cb.tar.gz coinosv3-ba5e28249aac75310303174ac48b4c67b67224cb.zip | |
Exit works on plain-esplora regtest; human-readable ark status lines
submitPackage falls back to sequential /tx posts when the esplora lacks
POST /txs/package (regtest electrs) — works wherever the node relays
zero-fee v3 parents. The ark card stops speaking developer: in-flight
operations summarize as '{n} deposit(s) confirming' instead of
'board:funded, exit:chain, …', exit lines read 'Exiting N sats —
publishing on-chain…', and transient errors show 'hit a snag — retrying
automatically' instead of raw gRPC/HTTP text.
Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/ark/exit.js | 17 | ||||
| -rw-r--r-- | src/features/ark.js | 14 | ||||
| -rw-r--r-- | src/i18n.js | 11 |
3 files changed, 32 insertions, 10 deletions
diff --git a/src/ark/exit.js b/src/ark/exit.js index 2770284..9310b65 100644 --- a/src/ark/exit.js +++ b/src/ark/exit.js @@ -150,11 +150,22 @@ export function buildExitClaim({ vtxo, keys, serverPub, destScript, feeRate }) { // Submit a [parent, child] (or single-tx) package via esplora -> bitcoind // submitpackage. Treats already-known txs as success so retries are safe. +// Esploras without the endpoint (regtest electrs) fall back to sequential +// /tx posts — which relays only if the node accepts zero-fee v3 parents +// (regtest with -minrelaytxfee=0); otherwise the node's error surfaces. export async function submitPackage(esploraBase, hexes) { const r = await fetch(`${esploraBase}/txs/package`, { method: 'POST', body: JSON.stringify(hexes) }); const body = await r.text(); - if (!r.ok && !/already|duplicate/i.test(body)) { - throw new Error(`package submit failed: ${body.slice(0, 160)}`); + if (r.ok || /already|duplicate/i.test(body)) return body; + if (r.status === 404 || /endpoint does not exist/i.test(body)) { + for (const hx of hexes) { + const br = await fetch(`${esploraBase}/tx`, { method: 'POST', body: hx }); + const bb = await br.text(); + if (!br.ok && !/already|duplicate/i.test(bb)) { + throw new Error(`broadcast failed: ${bb.slice(0, 160)}`); + } + } + return 'sequential'; } - return body; + throw new Error(`package submit failed: ${body.slice(0, 160)}`); } diff --git a/src/features/ark.js b/src/features/ark.js index 9a180f7..57a8f82 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -278,9 +278,15 @@ export function arkFeature(ctx) { { class: 'card col', style: 'gap:10px' }, ...head, spendables.length ? row(t('arkVtxos'), String(spendables.length)) : null, - pendingActions.length - ? h('div', { class: 'small muted' }, t('arkPendingActions', { n: pendingActions.length }) + ' — ' + pendingActions.map((a) => `${a.type}:${a.step}`).join(', ')) - : null, + // in-flight operations, summarized in plain words (exits get their own + // per-exit status lines below) + (() => { + const counts = {}; + for (const a of pendingActions) { if (a.type !== 'exit') counts[a.type] = (counts[a.type] || 0) + 1; } + const LABEL = { board: 'arkOpsBoard', send: 'arkOpsSend', refresh: 'arkOpsRefresh', offboard: 'arkOpsOffboard' }; + const parts = Object.entries(counts).map(([type, n]) => t(LABEL[type] || 'arkPendingActions', { n })); + return parts.length ? h('div', { class: 'small muted' }, parts.join(' · ')) : null; + })(), spendables.length >= 1 ? h('button', { class: 'btn-ghost btn-block', disabled: !!ui.arkBusy, onClick: doArkRefresh }, ui.arkBusy === 'refresh' ? h('span', { class: 'spinner sm' }) : t('arkRefreshBtn', { n: spendables.length })) @@ -295,7 +301,7 @@ export function arkFeature(ctx) { a.step === 'chain' ? t('arkExitChainStatus', { n: fmtAmount(a.amountSat) }) : a.step === 'timelock' ? t('arkExitTimelockStatus', { n: fmtAmount(a.amountSat), blocks: String(a.claimableAt) }) : t('arkExitClaimingStatus', { n: fmtAmount(a.amountSat) }), - a.lastError ? h('div', { class: 'small err' }, a.lastError) : null)) + a.lastError ? h('div', { class: 'small faint' }, t('arkExitRetrying')) : null)) ); } diff --git a/src/i18n.js b/src/i18n.js index 47e93f4..6c40a74 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -391,9 +391,14 @@ const STR = { arkExitBtn: 'Exit on-chain without the server ({n} coin(s))', arkExitStarted: 'Unilateral exit started for {n} coin(s) — broadcasting the exit chain.', arkExitNoFeeCoin: 'Exiting needs an on-chain coin to pay the exit-chain fees.', - arkExitChainStatus: 'Exit {n}: broadcasting the exit chain…', - arkExitTimelockStatus: 'Exit {n}: waiting for the timelock (claimable at block {blocks})', - arkExitClaimingStatus: 'Exit {n}: claim broadcast, awaiting confirmation', + arkExitChainStatus: 'Exiting {n} sats — publishing on-chain…', + arkExitTimelockStatus: 'Exiting {n} sats — waiting out the timelock (block {blocks})', + arkExitClaimingStatus: 'Exiting {n} sats — almost done, awaiting confirmation', + arkExitRetrying: 'hit a snag — retrying automatically', + arkOpsBoard: '{n} deposit(s) confirming', + arkOpsSend: '{n} send(s) finishing up', + arkOpsRefresh: '{n} refresh(es) waiting for the next round', + arkOpsOffboard: '{n} withdrawal(s) confirming', arkExited: 'Exited on-chain', arkExitHistory: 'Ark exit', arkRefreshStarted: 'Refresh submitted — completes with the next round.', |
