aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-30 20:44:09 -0700
committerAdam Soltys <[email protected]>2026-08-30 20:44:09 -0700
commitd8b0e275ad76d2950e3d3ffcafd3c6c3d0ea0e7a (patch)
tree4c9ccec107ad76e27714a8af0a3327b210f590b3
parent735ee60d386e9fcf5e8e7c62ab53ba0534e43850 (diff)
downloadcoinosv3-d8b0e275ad76d2950e3d3ffcafd3c6c3d0ea0e7a.tar.gz
coinosv3-d8b0e275ad76d2950e3d3ffcafd3c6c3d0ea0e7a.zip
A board that never funded stops masquerading as work in progress: the tx build failing now marks the action failed on the spot, and connect sweeps any unfunded board stub older than an hour — a lingering 'created' stub silently blocked auto-renewal on that wallet for good (cooney had one from the Max shortfall, another wallet a 50k one)
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
-rw-r--r--src/features/ark.js23
1 files changed, 23 insertions, 0 deletions
diff --git a/src/features/ark.js b/src/features/ark.js
index dc4c0f1..bacf49e 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -386,6 +386,19 @@ export function arkFeature(ctx) {
arkConnectPromise = mgr.init().then(() => {
if (gen !== arkInitGen) throw new Error('superseded'); // wallet switched mid-connect
ark = mgr;
+ // Boards that died before funding (a failed tx build, a closed tab)
+ // used to linger at 'created' forever — and an in-flight-looking
+ // action blocks auto-renewal. Anything unfunded after an hour is a
+ // stub, not a plan; the id carries its birth time.
+ {
+ let swept = false;
+ for (const a of mgr.state.actions || []) {
+ if (a.type !== 'board' || a.fundingTxid || ['done', 'failed'].includes(a.step)) continue;
+ const ts = Number((a.id || '').split('-')[1]) || 0;
+ if (ts && Date.now() - ts > 3600_000) { a.step = 'failed'; a.error = a.error || 'never funded'; swept = true; }
+ }
+ if (swept) mgr._save();
+ }
announceArkAddress(mgr); // ark zaps: tell nostr where our mailbox lives
startNwcFunding(mgr); // NWC bridge: honor funding requests within the allowance
// Publish this device's ark state to its per-device sync slot on connect.
@@ -1076,12 +1089,15 @@ export function arkFeature(ctx) {
const sats = parseInt((ui.arkBoardAmt || '').trim(), 10);
if (!sats) return;
ui.arkBusy = 'board'; ui.arkError = ''; render();
+ let unfunded = null; // the action id, until the funding tx is on the wire
try {
const { actionId, fundingAddress, feeSat } = await ark.startBoard(sats);
+ unfunded = actionId;
const feeRate = (wallet.feeRates && wallet.feeRates.halfHourFee) || 5;
const draft = wallet.buildTx({ recipients: [{ address: fundingAddress, amount: sats }], feeRate, noSort: true });
const hexTx = wallet.sign(draft.tx);
const txid = await wallet.broadcast(hexTx);
+ unfunded = null; // money moved — this action must live on whatever happens next
// Like the main send flow: reflect the spend locally and let the
// poll/watcher reconcile. A scan here would race the explorer's indexing
// and could resurrect the just-spent coin.
@@ -1090,6 +1106,13 @@ export function arkFeature(ctx) {
ui.arkBoardAmt = '';
ui.arkBoarded = { txid, netSat: sats - feeSat };
} catch (e) {
+ // A board that never funded is not in flight. Leaving the stub at
+ // 'created' looked like an in-flight action forever — which silently
+ // blocked auto-renewal (it refuses to start a round beside one).
+ if (unfunded && ark && ark.state) {
+ const a = (ark.state.actions || []).find((x) => x.id === unfunded);
+ if (a && !a.fundingTxid) { a.step = 'failed'; a.error = e.message; try { ark._save(); } catch {} }
+ }
ui.arkError = e.message;
}
ui.arkBusy = null; render();