diff options
| author | Adam Soltys <[email protected]> | 2026-09-27 15:07:31 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-27 15:07:31 -0700 |
| commit | 31fea2d1e463424387828163bda9facc2fee8c0d (patch) | |
| tree | 9af8a89cbc44519d5a37989f402381381a056748 /src/ark/manager.js | |
| parent | 9fdbc82fecf17cefed08e8fe1cfee03546d41c5e (diff) | |
| download | coinosv3-31fea2d1e463424387828163bda9facc2fee8c0d.tar.gz coinosv3-31fea2d1e463424387828163bda9facc2fee8c0d.zip | |
Three fixes: orphaned pending coins settle with the server; a saved start screen no longer shadows a restored wallet; stop() no longer throws.
Reconcile now asks the server about a 'pending' coin no live action or
standing renewal order holds: spendable again, or gone. Thirty zap-sized
coins another device had already spent counted in a balance for weeks.
A nav saved by a page with no wallet (the start screen) was restored over a
wallet that had since been saved to the device, painting the start page.
messages.stop() referenced feedAt, which no longer exists, so the rest of the
teardown never ran on an account switch.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VhUKyXKtsJ67QAFfb7QR9D
Diffstat (limited to 'src/ark/manager.js')
| -rw-r--r-- | src/ark/manager.js | 24 |
1 files changed, 22 insertions, 2 deletions
diff --git a/src/ark/manager.js b/src/ark/manager.js index d097a44..bfb4a4e 100644 --- a/src/ark/manager.js +++ b/src/ark/manager.js @@ -21,7 +21,7 @@ import { getArkInfo, handshake, encodeAddress, decodeAddress, blindMailboxId, readMailbox, decodeVtxo, arkIdFromServerPubkey, GrpcError, grpcStream, decodeMailboxMessage, mailboxRequestBytes, - getVtxoStatus, VTXO_STATE_SPENT, vtxoBytesToStr, vtxoBytesFromStr, vtxoBytesNormalize } from './proto.js'; + getVtxoStatus, VTXO_STATE_SPENT, VTXO_STATE_SPENDABLE, vtxoBytesToStr, vtxoBytesFromStr, vtxoBytesNormalize } from './proto.js'; import { buildArkoorSend, cosignWithServer, cosignPackageWithServer, buildAllSignedVtxos, registerVtxoTransactions, postArkoorMessage, txid, @@ -597,7 +597,19 @@ export class ArkManager { // way — spendable -> spent — so a lying server can hide balance from the UI // but never mint it, and the signed bytes stay held for unilateral exit. async reconcile() { - const candidates = this.state.vtxos.filter((v) => v.state === 'spendable'); + // 'pending' means an action of this device holds the coin. One that no + // live action (or standing renewal order) names is an orphan: the pay + // that took it ran on another device and merged in mid-flight, or its + // action was pruned. Left alone it counted in the balance forever (thirty + // zap-sized coins, 645 sats, on adam's laptop); the server settles it. + const held = new Set(); + for (const a of this.state.actions || []) { + if (a.step === 'done' || a.step === 'failed') continue; + for (const id of a.inputIds || []) held.add(id); + for (const part of a.parts || []) if (part && part.inputId) held.add(part.inputId); + } + for (const e of this.state.scheduled || []) for (const id of e.inputIds || []) held.add(id); + const candidates = this.state.vtxos.filter((v) => v.state === 'spendable' || (v.state === 'pending' && !held.has(v.id) && v.bytes)); // Each status query signs a challenge (schnorr) and may decode the vtxo — // real secp256k1 work. Building them all in one synchronous map was a // ~100ms+ EC burst on boot (dozens of coins), the stall behind a carousel @@ -612,6 +624,14 @@ export class ArkManager { const states = await Promise.all(pending); // unreachable/erroring server changes nothing let changed = false; candidates.forEach((v, i) => { + if (v.state === 'pending') { + // an orphan the server still counts is simply spendable again; one it + // holds as spent is gone, and the pay that took it was recorded (or + // will merge in) elsewhere — no row of its own + if (states[i] === VTXO_STATE_SPENDABLE) { v.state = 'spendable'; changed = true; } + else if (states[i] === VTXO_STATE_SPENT) { v.state = 'spent'; changed = true; } + return; + } if (states[i] !== VTXO_STATE_SPENT) return; v.state = 'spent'; this._movement({ type: 'reconcile', amountSat: v.amountSat, status: 'complete', vtxoId: v.id, detail: 'spent elsewhere (server vtxo status)' }); |
