From f06644e729ab5be2fcfa45e32f8b1de52ea36eb3 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 29 Aug 2026 00:08:41 -0700 Subject: The mailbox checkpoint was a timestamp all along: receives are dated by when the server actually delivered them (verified to the millisecond), so an import shows hours-old payments as hours old — no celebration for news that isn't; a wallet whose Savings holds nothing opens onto the Spending that holds everything; and same-payment duplicates fold at display time on every device, however the state arrived MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019eA8JRq9Wr7z2fRQk99HLi --- src/ark/manager.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) (limited to 'src/ark') diff --git a/src/ark/manager.js b/src/ark/manager.js index 508cd85..50214fc 100644 --- a/src/ark/manager.js +++ b/src/ark/manager.js @@ -321,19 +321,23 @@ export class ArkManager { if (m.kind !== 'arkoor') return changed; const ourKeys = [hex.encode(this._key(0).pubkey)]; const tip = await this._tipMemo(); - // Expiry-based dating is for the FIRST catch-up only, where old receipts - // deserve old dates. A delivery landing on a live wallet happened NOW — - // the estimate mis-dated a fresh receive by a month when the server's - // vtxo lifetime changed under it (the math assumes every coin began with - // the full current lifetime). + // The mailbox checkpoint IS the delivery timestamp (ms << 20, verified + // to the millisecond against the server's rows) — the one honest date + // for a receive, whether it lands live or surfaces in an import's + // catch-up. Expiry-based estimation stays only as the fallback for + // messages without a usable checkpoint. const live = !this.state.baselinePending; + const ckTs = (() => { + const t = Math.floor(Number(m.checkpoint || 0) / 1048576); + return t > 1.6e12 && t < Date.now() + 60_000 ? t : null; + })(); for (const v of m.vtxos) { if (this._vtxo(v.id)) { // 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)) { - this._movement({ type: 'receive', amountSat: v.amountSat, status: 'complete', vtxoId: v.id, ts: live ? Date.now() : this._estimateReceiveTs(this._decoded(this._vtxo(v.id)), tip) }); + 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; } continue; @@ -354,7 +358,7 @@ export class ArkManager { if (!this._addVtxo(v, v._raw.bytes, 0)) continue; this._movement({ type: 'receive', amountSat: v.amountSat, status: 'complete', vtxoId: v.id, - ts: live ? Date.now() : this._estimateReceiveTs(v, tip), + ts: ckTs ?? (live ? Date.now() : this._estimateReceiveTs(v, tip)), }); changed = true; } -- cgit v1.2.3