aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-22 23:55:09 -0700
committerAdam Soltys <[email protected]>2026-10-22 23:55:09 -0700
commitc143b7dbe900df9a644bb67b87a92fd208e1ee74 (patch)
treeac759084137091e795f788fa7c35a71e2d6e098c /src/features
parenta89bce97319d5860da8d581ff41cdace9acbb077 (diff)
downloadcoinosv3-c143b7dbe900df9a644bb67b87a92fd208e1ee74.tar.gz
coinosv3-c143b7dbe900df9a644bb67b87a92fd208e1ee74.zip
The wallet opens on the Ark server's last published info when the server is gone, and a unilateral exit needs only thatHEADmaster
Adam, 2026-10-23: the app must stay navigable and able to exit if Coinos disappears. The shell already was (the worker precaches every chunk; verified with every coinos host unresolvable: boot, wallet creation, Savings address, Send all work, and Electrum fails over to public servers). The Ark side was not: connectArk's init asked the server for its info and threw, so the Manage page's Exit stayed disabled and doArkExit waited on a connect that could never finish. ArkManager takes infoHint (the btc-wallet-arkinfo cache the app keeps per server): when GetArkInfo fails and a hint exists, init proceeds on it with `offline` set; the server key, fee tables and lifetimes are in it, and an exit needs the chain (esplora, not Coinos) rather than the server. doArkExit uses the initialized manager and waits on init alone, not on the Nostr snapshot gate the full connect also waits for (its relays may be gone too). tools/ark-offline-init.test.js. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'src/features')
-rw-r--r--src/features/ark.js20
1 files changed, 18 insertions, 2 deletions
diff --git a/src/features/ark.js b/src/features/ark.js
index 6952fc0..94dfc09 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -494,6 +494,7 @@ export function arkFeature(ctx) {
}
let arkTimer = null;
let arkConnectPromise = null;
+ let arkInitPromise = null; // the manager's own init, apart from the snapshot gate
let arkInitGen = 0; // guards against a stale init() resolving after a wallet switch
function stopArk() {
@@ -514,6 +515,7 @@ export function arkFeature(ctx) {
ark = null;
arkInited = null;
arkConnectPromise = null;
+ arkInitPromise = null;
// The renewal warning describes the coins of the wallet we're leaving.
// Switching accounts used to carry it onto the next one — with the old
// wallet's amount and deadline, over the new wallet's balance — and the
@@ -663,6 +665,9 @@ export function arkFeature(ctx) {
esploraUrl: cfg.esplora,
network: getNetwork(),
honorsExpired: !!cfg.honorsExpired,
+ // the server's last published info: the wallet opens on it when the
+ // server is gone, so coins can still be read and exited
+ infoHint: (cachedArkInfo(cfg.ark) || {}).info || null,
// the wallet's own tx table as a second witness for confirmations —
// its data source is user-configurable and often healthier than the
// ark preset's esplora from this device
@@ -679,9 +684,10 @@ export function arkFeature(ctx) {
const inited = mgr.init().then(() => {
if (gen !== arkInitGen) return;
arkInited = mgr; // readable now; `ark` (and the mailbox) wait for the snapshot
- rememberArkInfo(cfg.ark, mgr.info, mgr._tipH);
+ if (!mgr.offline) rememberArkInfo(cfg.ark, mgr.info, mgr._tipH);
render();
});
+ arkInitPromise = inited;
// The tip: expiries and exit costs are judged against it, and init alone
// never asks for it (the first sync does, after the snapshot gate). Ask
// now, off the connect path, and remember it for the next refresh.
@@ -2938,7 +2944,17 @@ export function arkFeature(ctx) {
async function doArkExit(ids = null) {
ui.arkBusy = 'exit'; ui.arkError = ''; render();
try {
- const mgr = await connectArk();
+ // The manager's own init is enough for an exit — the server may be
+ // gone (that is what a unilateral exit is for), and the snapshot gate
+ // the full connect also waits on may never clear without the relays.
+ let mgr = arkView();
+ if (!mgr) {
+ connectArk().catch(() => {});
+ for (let i = 0; i < 50 && !arkInitPromise; i++) await new Promise((r) => setTimeout(r, 100));
+ try { await arkInitPromise; } catch {}
+ mgr = arkView();
+ }
+ if (!mgr) throw new Error(t('arkNotConnected'));
const spendables = exitableCoins(mgr.state, mgr._tipH || 0).filter((v) => !ids || ids.includes(v.id));
if (!spendables.length) throw new Error(freeCoins(mgr.state).length ? t('arkExitNoneExitable') : t('arkNotConnected'));
// Fees are paid hop by hop from Savings as the exit unrolls — an exit