diff options
Diffstat (limited to 'src/ark')
| -rw-r--r-- | src/ark/manager.js | 41 |
1 files changed, 38 insertions, 3 deletions
diff --git a/src/ark/manager.js b/src/ark/manager.js index 6741237..a2907c5 100644 --- a/src/ark/manager.js +++ b/src/ark/manager.js @@ -79,6 +79,38 @@ const actionTs = (a) => Number(String(a.id || '').split('-').pop()) || 0; // Renewals another device has in flight, as this device should show them: // not finished there, and not already finished as far as this device's // history knows (the "Renewed" row synced ahead of the final telling). +// A Lightning receive the ASP settles is claimed as an arkoor — and that same +// coin can reach the mailbox too, where it used to be recorded again as a +// plain receive: one payment, two "Received" rows, and a celebration that +// added them up (The Daniel saw 2,010 for 1,005). The claim now stamps its +// coins' ids on its ln-receive movement; a receive movement for one of those +// coins is the same money and is left out. An ln-receive recorded before the +// ids were stamped is matched by amount within a few minutes. Derived, never +// deleted: a synced copy from another device can't bring the twin back. +export function lnFoldedIds(movements) { + const out = new Set(); + const lns = (movements || []).filter((m) => m.type === 'ln-receive' && m.status === 'complete'); + if (!lns.length) return out; + const recs = (movements || []).filter((m) => m.type === 'receive' && m.status === 'complete' && m.vtxoId); + for (const ln of lns) { + if (ln.vtxoIds && ln.vtxoIds.length) { + for (const r of recs) if (ln.vtxoIds.includes(r.vtxoId)) out.add(r.id); + continue; + } + let best = null; + for (const r of recs) { + // (a receive the registrar's receipts tagged viaLn came over the + // name@ path — its own payment, not this one's twin) + if (out.has(r.id) || r.viaLn || r.amountSat !== ln.amountSat) continue; + const dt = Math.abs((r.ts || 0) - (ln.ts || 0)); + if (dt > 10 * 60_000) continue; + if (!best || dt < Math.abs((best.ts || 0) - (ln.ts || 0))) best = r; + } + if (best) out.add(best.id); + } + return out; +} + export function remoteRenewalsInFlight(state) { const settled = new Set((state.movements || []) .filter((m) => m.type === 'refresh' && m.status === 'complete' && m.unlockHash).map((m) => m.unlockHash)); @@ -438,8 +470,9 @@ export class ArkManager { unseenReceives() { const ack = this.state.receiveAckTs || 0; const cutoff = Date.now() - 2 * 3600 * 1000; + const folded = lnFoldedIds(this.state.movements); return this.state.movements.filter((m) => - ['receive', 'ln-receive'].includes(m.type) && m.status === 'complete' && m.ts > ack && m.ts > cutoff); + ['receive', 'ln-receive'].includes(m.type) && m.status === 'complete' && m.ts > ack && m.ts > cutoff && !folded.has(m.id)); } ackReceives() { this.state.receiveAckTs = Date.now(); @@ -583,7 +616,7 @@ export class ArkManager { // The vtxo beat the message here (a mirror or snapshot merge carried // it) — the money is present but the story is missing. Backfill the // receive movement so history matches the balance. - if (!this.state.movements.some((x) => x.vtxoId === v.id)) { + if (!this.state.movements.some((x) => x.vtxoId === v.id || (x.vtxoIds && x.vtxoIds.includes(v.id)))) { this._movement({ type: 'receive', amountSat: v.amountSat, status: 'complete', vtxoId: v.id, ts: ckTs ?? (live ? Date.now() : this._estimateReceiveTs(this._decoded(this._vtxo(v.id)), tip)) }); changed = true; } @@ -1797,13 +1830,15 @@ export class ArkManager { }); await registerVtxoTransactions(this.arkUrl, outBytes).catch(() => {}); let total = 0; + const vtxoIds = []; for (const bts of outBytes) { const v = decodeVtxo(bts); this._addVtxo(v, bts, action.keyIndex); total += v.amountSat; + vtxoIds.push(v.id); } action.step = 'done'; - this._movement({ type: 'ln-receive', amountSat: total, status: 'complete', invoice: action.invoice }); + this._movement({ type: 'ln-receive', amountSat: total, status: 'complete', invoice: action.invoice, vtxoIds }); this._save(); } } |
