1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
|
#!/usr/bin/env bun
// Find accounts whose payment history index was wiped — the changeid bug
// signature, distinct from the harmless arc-only archived state.
//
// Two buckets:
// wiped-with-balance: balance > 0 but :payments list is empty in BOTH db and
// arc. Real funds, no visible history. These are the changeid-rekeyed
// accounts (password == "reset") whose list entries got dropped. Highest
// priority — user sees a balance with no transactions.
// wiped-no-balance: :payments empty in both, balance 0, but has :invoices
// (so they did transact once). History gone but nothing owed; lower priority.
//
// Read-only. Usage: bun scripts/audit-wiped-history.ts [--full] [--examples N]
import { createClient } from "redis";
const full = process.argv.includes("--full");
const exN = Number(process.argv[process.argv.indexOf("--examples") + 1]) || 30;
const db = createClient({ url: "redis://127.0.0.1:6379", socket: { reconnectStrategy: () => false } });
const arc = createClient({ url: "redis://127.0.0.1:6380", socket: { reconnectStrategy: () => false } });
await db.connect();
await arc.connect();
let scanned = 0;
const wipedWithBal: { name: string; uid: string; bal: number; reset: boolean }[] = [];
const wipedNoBal: { name: string; uid: string; invoices: number }[] = [];
let cleanCount = 0;
const t0 = Date.now();
for await (const batch of db.scanIterator({ MATCH: "user:*", COUNT: 2000 })) {
// This redis client version yields ARRAYS of keys per iteration, not single keys.
const keys = Array.isArray(batch) ? batch : [batch];
for (const key of keys) {
const uid = (key as string).slice("user:".length);
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-/.test(uid)) continue; // skip username pointers
scanned++;
const dbLen = await db.lLen(`${uid}:payments`);
if (dbLen > 0) { cleanCount++; continue; } // has visible history
// Empty in db — is it also empty in arc, or just archived?
const arcLen = await arc.lLen(`${uid}:payments`);
if (arcLen > 0) { cleanCount++; continue; } // renders via gf(), fine
// :payments empty in both. Does it matter? Check balance + whether they ever transacted.
const bal = parseInt((await db.get(`balance:${uid}`)) || "0", 10);
const invLen = await db.lLen(`${uid}:invoices`);
if (bal > 0) {
const u = await db.get(`user:${uid}`);
let name = uid.slice(0, 8), reset = false;
if (u && u[0] === "{") { try { const j = JSON.parse(u); name = j.username || name; reset = j.password === "reset"; } catch {} }
wipedWithBal.push({ name, uid, bal, reset });
} else if (invLen > 0) {
const u = await db.get(`user:${uid}`);
let name = uid.slice(0, 8);
if (u && u[0] === "{") { try { name = JSON.parse(u).username || name; } catch {} }
wipedNoBal.push({ name, uid, invoices: invLen });
}
}
}
const secs = ((Date.now() - t0) / 1000).toFixed(1);
wipedWithBal.sort((a, b) => b.bal - a.bal);
console.log("═".repeat(70));
console.log(` WIPED-HISTORY AUDIT — scanned ${scanned} users in ${secs}s`);
console.log("═".repeat(70));
console.log(`\n Accounts with BALANCE but no history (priority — funds visible, no txns):`);
console.log(` count: ${wipedWithBal.length}`);
const totalBal = wipedWithBal.reduce((s, x) => s + x.bal, 0);
console.log(` total balance affected: ${totalBal.toLocaleString()} sats`);
console.log(` (of which password=="reset" / changeid'd: ${wipedWithBal.filter((x) => x.reset).length})`);
console.log("");
for (const x of wipedWithBal.slice(0, exN)) {
console.log(` ${x.name.padEnd(24)} ${(x.bal + "").padStart(12)} sats ${x.reset ? "[changeid]" : ""} ${x.uid}`);
}
console.log(`\n Accounts wiped, no balance (had invoices, lower priority): ${wipedNoBal.length}`);
for (const x of wipedNoBal.slice(0, 10)) {
console.log(` ${x.name.padEnd(24)} ${x.invoices} invoices ${x.uid}`);
}
console.log("═".repeat(70));
process.exit(0);
|