aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-30 19:20:41 -0700
committerAdam Soltys <[email protected]>2026-08-30 19:20:41 -0700
commit698b6b5431d31a54ddf30c234f95a92bf5c8bf0d (patch)
tree43fd7b8515e80d25ae2876a4c49d326e85b9b3e7 /src/features
parentbd4adcb87d1fe05ce9631b51ee529016eb71a154 (diff)
downloadcoinosv3-698b6b5431d31a54ddf30c234f95a92bf5c8bf0d.tar.gz
coinosv3-698b6b5431d31a54ddf30c234f95a92bf5c8bf0d.zip
The cooney ghost spend gets laid to rest on every device: the ark-state merge now lets a movement's late-learned fields through — inputIds, unlockHash, and a fee that upgrades a recorded zero — where the by-id union used to keep each device's first copy forever, so the rescue's post-hoc enrichment never propagated and other devices painted the renewal's 16 consumed inputs as a fresh red 'Spent on another device −29,014'
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.js20
1 files changed, 19 insertions, 1 deletions
diff --git a/src/features/ark.js b/src/features/ark.js
index 1fd49ae..5a97ae1 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -175,11 +175,29 @@ export function mergeArkStates(a, b) {
return [...x, ...y.filter((i) => !ids.has(i.id))];
};
out.actions = unionById(a.actions, b.actions);
+ // Union keeps the local copy of a movement forever — but a movement can be
+ // ENRICHED after other devices synced it (the cooney rescue stamped
+ // inputIds/unlockHash/fee onto its renewal movement post-hoc, and those
+ // fields are what lets history exclude the matching "spent on another
+ // device" rows). Adopt fields the local copy lacks; never overwrite ones
+ // it has.
+ const bM = new Map((b.movements || []).map((m) => [m.id, m]));
+ const enriched = (a.movements || []).map((lm) => {
+ const rm = bM.get(lm.id);
+ if (!rm) return lm;
+ const take = {};
+ if (rm.inputIds != null && lm.inputIds == null) take.inputIds = rm.inputIds;
+ if (rm.unlockHash != null && lm.unlockHash == null) take.unlockHash = rm.unlockHash;
+ // a fee learned late (the rescue computed it from inputs − outputs)
+ // upgrades a recorded zero — the row only renders when the fee is real
+ if ((rm.feeSat || 0) > 0 && !(lm.feeSat > 0)) take.feeSat = rm.feeSat;
+ return Object.keys(take).length ? { ...lm, ...take } : lm;
+ });
// Two devices (or the push and the poll, pre-guard) can each record the
// SAME lightning payment under different movement ids — same preimage is
// the same payment, keep the earliest telling of it.
const seenPay = new Set();
- out.movements = unionById(a.movements, b.movements)
+ out.movements = unionById(enriched, b.movements)
.sort((m, n) => (m.ts || 0) - (n.ts || 0))
.filter((m) => {
if (!m.type || !m.type.startsWith('ln-') || m.status !== 'complete') return true;