aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.js4
-rw-r--r--src/ark/manager.js24
-rw-r--r--src/features/messages.js2
3 files changed, 27 insertions, 3 deletions
diff --git a/src/app.js b/src/app.js
index 1bbbe01..8b93766 100644
--- a/src/app.js
+++ b/src/app.js
@@ -826,6 +826,10 @@ function restoreNavFromHistory(renderNow = true) {
const nav = BOOT_NAV;
if (!nav || ui.screen !== 'wallet') return;
bootPending = false;
+ // The page that saved this nav may have had no wallet (the start screen,
+ // the vault prompt); with an account open now, those screens are stale.
+ // Restoring one painted the start page over a perfectly good wallet.
+ if (['unlock', 'vault'].includes(nav.screen) && activeAccount()) nav.screen = 'wallet';
for (const f of NAV_FIELDS) if (f in nav) ui[f] = f === 'profEdit' && nav[f] ? structuredClone(nav[f]) : nav[f];
ui.lightbox = null; nav.lightbox = null; // an object URL doesn't survive a reload
navStack = [nav];
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)' });
diff --git a/src/features/messages.js b/src/features/messages.js
index 59fb0a5..a55e735 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -8581,7 +8581,7 @@ export function messagesFeature(ctx) {
for (const c of feedStates.values()) c.stopped = true;
feedStates.clear();
threadCache.clear();
- follows = null; followsAt = 0; feed = null; feedAt = 0; relayLists = null;
+ follows = null; followsAt = 0; feed = null; relayLists = null;
mutes = null; mutesAt = 0;
stCache = null; stAt = 0; // the next account's state, not this one's
if (profFlush) { clearTimeout(profFlush); flushProfiles(); } // this account's faces, written before the blob is dropped