aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/ark/manager.js4
-rw-r--r--src/features/ark.js42
2 files changed, 35 insertions, 11 deletions
diff --git a/src/ark/manager.js b/src/ark/manager.js
index e60d2f3..492906d 100644
--- a/src/ark/manager.js
+++ b/src/ark/manager.js
@@ -755,6 +755,9 @@ export class ArkManager {
this._movement({
type: 'send', amountSat: action.amountSat, status: 'complete',
to: action.destAddress, vtxoId: decodeVtxo(hex.decode(destList()[0])).id,
+ // which coins this spend consumed — synced, so other devices can
+ // skip the "spent on another device" row these inputs would earn
+ inputIds: (action.parts || []).map((p2) => p2.inputId).filter(Boolean),
});
this._save();
}
@@ -1055,6 +1058,7 @@ export class ArkManager {
type: 'ln-send', amountSat: action.amountSat, status: 'complete',
detail: action.feeSat ? `fee ${action.feeSat} sat` : undefined,
invoice: action.invoice, preimage: preimageHex, paymentHash: action.paymentHash,
+ inputIds: (action.parts || []).map((p2) => p2.inputId).concat(action.inputId ? [action.inputId] : []).filter(Boolean),
});
this._save();
}
diff --git a/src/features/ark.js b/src/features/ark.js
index ace0ea7..98db559 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -137,10 +137,24 @@ export function slimArkForSync(s) {
.map((v) => ({ id: v.id, amountSat: v.amountSat, state: 'spent', keyIndex: v.keyIndex, expiryHeight: v.expiryHeight }));
return [...live, ...stubs];
})(),
- actions: (s.actions || [])
- .filter((a) => a.step === 'done' && ['board', 'offboard', 'exit'].includes(a.type))
- .slice(-50)
- .map((a) => { const c = { ...a }; for (const k of HEAVY) delete c[k]; return c; }),
+ actions: (() => {
+ const done = (s.actions || []).filter((a) => a.step === 'done');
+ const core = done.filter((a) => ['board', 'offboard', 'exit'].includes(a.type))
+ .slice(-50)
+ .map((a) => { const c = { ...a }; for (const k of HEAVY) delete c[k]; return c; });
+ // Spends travel as SLIVERS — id, net amount, and which coins they
+ // consumed. That last field is what lets ANOTHER device recognize a
+ // vanished coin as this send rather than painting a red "spent on
+ // another device" row beside the synced Sent movement (one spend was
+ // showing twice, at input magnitude the second time).
+ const spends = done.filter((a) => ['send', 'ln-pay'].includes(a.type))
+ .slice(-100)
+ .map((a) => ({
+ id: a.id, type: a.type, step: 'done', amountSat: a.amountSat,
+ inputIds: (a.parts || []).map((p) => p.inputId).concat(a.inputId ? [a.inputId] : []).filter(Boolean),
+ }));
+ return [...core, ...spends];
+ })(),
movements: (s.movements || []).slice(-200).map((m, i, arr) =>
// Older rows only need to render a history line; the bolt11 (~400 chars
// each) and preimage are what pushed snapshots past relay size limits.
@@ -2841,17 +2855,23 @@ export function arkFeature(ctx) {
// history stops summing to the balance (cooney's looked 29k sats rich).
// One reconcile pass marks many coins in the same instant — fold each
// pass (and re-imported duplicates, deduped by coin) into one row.
- // Coins a renewal is known to have consumed are excluded outright: the
- // reconcile that noticed them gone was pre-empting the claim, and the
- // renewed coin already balances the books invisibly.
- const renewed = new Set();
- for (const a of s.actions || []) if (a.type === 'refresh') for (const id of a.inputIds || []) renewed.add(id);
- for (const m of s.movements || []) if (m.type === 'refresh') for (const id of m.inputIds || []) renewed.add(id);
+ // A coin whose spend is ACCOUNTED FOR is excluded outright — a
+ // renewal's inputs (the renewed coin balances the books invisibly),
+ // and equally a send's, an offboard's, an exit's: those movements and
+ // actions sync, so painting the vanished input beside the real Sent
+ // row showed one spend twice, at input magnitude the second time.
+ // The red row survives only for spends nothing anywhere explains.
+ const accounted = new Set();
+ for (const a of s.actions || []) {
+ for (const id of a.inputIds || []) accounted.add(id);
+ for (const part of a.parts || []) if (part.inputId) accounted.add(part.inputId);
+ }
+ for (const m of s.movements || []) for (const id of m.inputIds || []) accounted.add(id);
const out = [];
const seenCoin = new Set();
for (const m of moves) {
if (m.type !== 'reconcile') { out.push(m); continue; }
- if (m.vtxoId && (seenCoin.has(m.vtxoId) || renewed.has(m.vtxoId))) continue;
+ if (m.vtxoId && (seenCoin.has(m.vtxoId) || accounted.has(m.vtxoId))) continue;
if (m.vtxoId) seenCoin.add(m.vtxoId);
const last = out[out.length - 1];
if (last && last.type === 'reconcile' && last.ts === m.ts) {