aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-31 13:10:07 -0700
committerAdam Soltys <[email protected]>2026-08-31 13:10:07 -0700
commit21418741adfa8d16c53a20a5cc0d2d78a758ed89 (patch)
treebe989ab21ad3e41c90d0f43d8250bbd548378d0e /src/features
parent193f49a7a4fb73ef7bfa405c17e8da3ab94f4205 (diff)
downloadcoinosv3-21418741adfa8d16c53a20a5cc0d2d78a758ed89.tar.gz
coinosv3-21418741adfa8d16c53a20a5cc0d2d78a758ed89.zip
Cold boots stop lying for a few seconds: an empty profile answer (relays still dialing) no longer clobbers a remembered avatar with a punk — stale fields hold while the refresh does its rounds — and the ark merge marks coins spent the moment a synced movement or done action names them as consumed inputs, instead of double-counting a sleeping device's view (output merged in, inputs still 'spendable') until the reconcile caught up and the balance visibly ticked
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
Diffstat (limited to 'src/features')
-rw-r--r--src/features/ark.js18
-rw-r--r--src/features/messages.js12
2 files changed, 28 insertions, 2 deletions
diff --git a/src/features/ark.js b/src/features/ark.js
index b123458..ba566ee 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -227,6 +227,24 @@ export function mergeArkStates(a, b) {
else { lg.claimed = lg.claimed || rg.claimed; lg.revoked = lg.revoked || rg.revoked; }
}
out.gifts = [...gifts.values()];
+ // Targeted spend knowledge beats waiting for a reconcile: a completed
+ // movement or done action that names its consumed inputs is direct
+ // evidence those coins are gone. A device waking cold used to merge in
+ // another device's renewal/send OUTPUT while still counting the consumed
+ // inputs as spendable — the balance visibly ticked high, then dropped
+ // when the reconcile caught up. (This is narrow, evidence-based state —
+ // not the blanket remote-state adoption the rule above forbids.)
+ const consumed = new Set();
+ for (const x of out.actions || []) {
+ if (x.step !== 'done') continue;
+ for (const id of x.inputIds || []) consumed.add(id);
+ for (const part of x.parts || []) if (part.inputId) consumed.add(part.inputId);
+ }
+ for (const m of out.movements || []) {
+ if (m.status !== 'complete') continue;
+ for (const id of m.inputIds || []) consumed.add(id);
+ }
+ for (const v of out.vtxos) if (v.state === 'spendable' && consumed.has(v.id)) v.state = 'spent';
// The checkpoint is READING PROGRESS, not state: adopting a remote one
// skips every message this device never processed — balance appears (the
// vtxo unions in) but its receive movement and celebration never happen.
diff --git a/src/features/messages.js b/src/features/messages.js
index ad83389..c1f6d19 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -263,12 +263,20 @@ export function messagesFeature(ctx) {
if (cur !== undefined && (cur === null || Date.now() - (cur.t || 0) < PROFILE_TTL)) return cur;
profiles.set(pk, cur || null); // null = loading, no fallback art yet
fetchNostrProfile(pk).then((p) => {
- const entry = { ...(p || {}), t: Date.now() };
+ // An empty answer on a cold boot (relays still dialing, the 5s query
+ // came back with nothing) must never clobber a remembered face with a
+ // punk — keep the stale fields and just refresh the clock, so the
+ // known avatar holds while the full-profile fetch does its rounds.
+ const prev = profiles.get(pk);
+ const entry = p ? { ...p, t: Date.now() } : { ...(prev || {}), t: Date.now() };
profiles.set(pk, entry);
persistProfile(pk, entry);
preloadPicture(entry);
scheduleRepaint();
- }).catch(() => profiles.set(pk, { t: Date.now() }));
+ }).catch(() => {
+ const prev = profiles.get(pk);
+ profiles.set(pk, { ...(prev || {}), t: Date.now() });
+ });
return cur || null;
}
const displayName = (pk) => {