aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/ark/manager.js18
-rw-r--r--src/features/ark.js33
2 files changed, 42 insertions, 9 deletions
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;
}
diff --git a/src/features/ark.js b/src/features/ark.js
index 15b7687..90249a9 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -362,7 +362,7 @@ export function arkFeature(ctx) {
const tx = (wallet.txs || []).find((t2) => t2.txid === txid2);
return tx && tx.confirmed ? { confirmed: true, block_height: tx.blockHeight || NaN } : null;
},
- onUpdate: () => render(),
+ onUpdate: () => { maybeAutoSelectSpending(); render(); },
});
ui.arkError = '';
arkConnectPromise = mgr.init().then(() => {
@@ -419,6 +419,23 @@ export function arkFeature(ctx) {
return wallet.loadArkState(cfg.ark);
}
+ // A wallet that opens onto "Savings: 0" while its money sits in Spending
+ // is answering the wrong question — a stale device-global selection (or
+ // the default) shouldn't hide the balance. One shot per wallet open; any
+ // manual switch afterwards is respected.
+ let _autoSelected = false;
+ function maybeAutoSelectSpending() {
+ if (_autoSelected || wallet.watchOnly) return;
+ const b = arkBalance();
+ if (!b || b.spendableSat + b.pendingSat <= 0) return;
+ _autoSelected = true;
+ if ((wallet.spendable || 0) === 0 && ctx.getAccount() === 'savings') {
+ ui.navAnimSkip = true; // a correction at open paints, never slides
+ ctx.setAccount('spending'); // renders synchronously; the flip is decided here
+ ui.navAnimSkip = false; // don't let the one-shot eat the next real navigation
+ }
+ }
+
function arkBalance() {
const s = arkStateNow();
if (!s) return null;
@@ -2338,7 +2355,7 @@ export function arkFeature(ctx) {
return {
id: 'ark',
- init() { initArk(); },
+ init() { _autoSelected = false; initArk(); },
stop() { stopArk(); },
screenView() { return ui.arkExitPage ? arkExitPage() : null; },
receiveTakeover() {
@@ -2515,8 +2532,20 @@ export function arkFeature(ctx) {
const exits = (s.actions || [])
.filter((a) => a.type === 'exit' && !['done', 'failed'].includes(a.step))
.map((a) => ({ time: exitStartedTs(a), render: () => arkExitActionItem(a) }));
+ // Same-payment duplicates (the old settle race, or a snapshot adopted
+ // whole before the merge learned to dedupe) fold at display time too —
+ // the earliest telling wins, everywhere, however the state got here.
+ const seenPay = new Set();
return exits.concat((s.movements || [])
.filter((m) => ['receive', 'send', 'board', 'offboard', 'exit', 'ln-send', 'ln-receive', 'refresh'].includes(m.type) && m.status === 'complete')
+ .sort((m, n) => (m.ts || 0) - (n.ts || 0))
+ .filter((m) => {
+ if (!m.type.startsWith('ln-')) return true;
+ const key = m.type + ':' + (m.paymentHash || m.preimage || m.invoice || m.id);
+ if (seenPay.has(key)) return false;
+ seenPay.add(key);
+ return true;
+ })
.map((m) => ({ time: m.ts, render: () => arkHistoryItem(m) })));
},
historyDetail() {