aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/audit-wiped-history.ts
blob: d4e94645ec003c7f0811a53f71ea488ca0af9914 (plain) (blame)
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);