aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/tb-migrate-cluster.ts
blob: 4b39a777ee1f23cbc06bc9981c3b2abd9a3c54bc (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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
/**
 * Migrate TigerBeetle from cluster_id 0 to cluster_id 1
 *
 * Steps:
 * 1. Connect to existing TB, dump all account balances
 * 2. Stop TB, rename old data file, create new one with cluster_id 1
 * 3. Start TB, recreate all accounts, replay balances via house transfers
 *
 * Usage: bun scripts/tb-migrate-cluster.ts dump    # step 1: export balances
 *        bun scripts/tb-migrate-cluster.ts restore # step 3: import balances
 */

import { createClient, AccountFlags, CreateAccountError } from "tigerbeetle-node";
import { createHash } from "crypto";
import { readFileSync, writeFileSync } from "fs";

const LEDGER_SATS = 1;
const LEDGER_CREDIT_BTC = 2;
const LEDGER_CREDIT_LN = 3;
const LEDGER_CREDIT_LQ = 4;

const HOUSE_SATS = 1n;
const HOUSE_BTC = 2n;
const HOUSE_LN = 3n;
const HOUSE_LQ = 4n;

const MSATS = 1_000_000n;

const creditLedger: Record<string, number> = {
  bitcoin: LEDGER_CREDIT_BTC,
  lightning: LEDGER_CREDIT_LN,
  liquid: LEDGER_CREDIT_LQ,
};


function uuidToBigInt(uuid: string): bigint {
  return BigInt(`0x${uuid.replace(/-/g, "")}`);
}

function balanceId(aid: string): bigint {
  return uuidToBigInt(aid);
}

function pendingId(aid: string): bigint {
  return uuidToBigInt(aid) ^ (5n << 64n);
}

function creditId(uid: string, type: string): bigint {
  const ledger = BigInt(creditLedger[type] || 0);
  return uuidToBigInt(uid) ^ (ledger << 64n);
}

function fundAccountId(name: string): bigint {
  const hash = createHash("sha256").update(`fund:${name}`).digest("hex");
  return BigInt(`0x${hash.slice(0, 32)}`) ^ (6n << 64n);
}

function u128(n: bigint): bigint {
  return n & ((1n << 128n) - 1n);
}

let transferIdCounter = BigInt(Date.now()) << 64n;
function nextTransferId(): bigint {
  transferIdCounter += 1n;
  return u128(transferIdCounter);
}

// User UUIDs (the real accounts, not aliases)
const USER_UIDS = ["73e3f718-57d1-434c-87e4-e459c80db910", "15cd4878-7f55-4e98-94a5-859744d0499a"];

// Nostr pubkey users (also have accounts derived from their hex)
const NOSTR_UIDS = [
  "3b98c943b27d8c0a456e46d430b26fee7a896c1574638d9e45743418c0c00192",
  "0c3a53c8538b38c98d5bcc53352bd4a55f61eb4710ed8a2d774d5d7f7a97470b",
];

const FUND_NAMES = ["limit"];

const DUMP_FILE = "/home/adam/coinos-server/scripts/tb-dump.json";

interface AccountDump {
  id: string; // bigint as string
  ledger: number;
  code: number;
  flags: number;
  balance_micro: string; // bigint as string
}

async function dump() {
  const client = createClient({ cluster_id: 0n, replica_addresses: ["127.0.0.1:3001"] });
  const accounts: AccountDump[] = [];

  async function lookupAndRecord(id: bigint, ledger: number, flags: number) {
    const results = await client.lookupAccounts([u128(id)]);
    if (results.length > 0) {
      const acct = results[0];
      const balance = acct.credits_posted - acct.debits_posted;
      accounts.push({
        id: u128(id).toString(),
        ledger,
        code: 1,
        flags,
        balance_micro: balance.toString(),
      });
      console.log(`  account ${u128(id)} ledger=${ledger} balance_micro=${balance}`);
    }
  }

  console.log("Dumping house accounts...");
  await lookupAndRecord(HOUSE_SATS, LEDGER_SATS, 0);
  await lookupAndRecord(HOUSE_BTC, LEDGER_CREDIT_BTC, 0);
  await lookupAndRecord(HOUSE_LN, LEDGER_CREDIT_LN, 0);
  await lookupAndRecord(HOUSE_LQ, LEDGER_CREDIT_LQ, 0);

  const allUids = [...USER_UIDS, ...NOSTR_UIDS];

  for (const uid of allUids) {
    console.log(`Dumping user ${uid}...`);
    const constrained = AccountFlags.debits_must_not_exceed_credits;
    await lookupAndRecord(balanceId(uid), LEDGER_SATS, constrained);
    await lookupAndRecord(pendingId(uid), LEDGER_SATS, constrained);
    await lookupAndRecord(creditId(uid, "bitcoin"), LEDGER_CREDIT_BTC, constrained);
    await lookupAndRecord(creditId(uid, "lightning"), LEDGER_CREDIT_LN, constrained);
    await lookupAndRecord(creditId(uid, "liquid"), LEDGER_CREDIT_LQ, constrained);
  }

  for (const name of FUND_NAMES) {
    console.log(`Dumping fund ${name}...`);
    const constrained = AccountFlags.debits_must_not_exceed_credits;
    await lookupAndRecord(fundAccountId(name), LEDGER_SATS, constrained);
  }

  writeFileSync(DUMP_FILE, JSON.stringify(accounts, null, 2));
  console.log(`\nDumped ${accounts.length} accounts to ${DUMP_FILE}`);
  client.destroy();
}

async function restore() {
  const data: AccountDump[] = JSON.parse(readFileSync(DUMP_FILE, "utf-8"));
  console.log(`Restoring ${data.length} accounts to cluster_id 1...`);

  const client = createClient({ cluster_id: 1n, replica_addresses: ["127.0.0.1:3001"] });

  // Create all accounts
  const accountBatch = data.map((a) => ({
    id: BigInt(a.id),
    debits_pending: 0n,
    debits_posted: 0n,
    credits_pending: 0n,
    credits_posted: 0n,
    user_data_128: 0n,
    user_data_64: 0n,
    user_data_32: 0,
    reserved: 0,
    ledger: a.ledger,
    code: a.code,
    flags: a.flags,
    timestamp: 0n,
  }));

  console.log("Creating accounts...");
  const createResults = await client.createAccounts(accountBatch);
  for (const r of createResults) {
    if (r.result !== CreateAccountError.exists) {
      console.error(`  account creation error: index=${r.index} result=${r.result}`);
    }
  }

  // Replay balances via transfers from/to house accounts
  // House accounts have flags=0 (no constraints), so they can go negative
  console.log("Replaying balances...");
  for (const a of data) {
    const balMicro = BigInt(a.balance_micro);
    if (balMicro === 0n) continue;

    // Skip house accounts (they'll get their balances from the user transfers)
    const id = BigInt(a.id);
    if (
      id === u128(HOUSE_SATS) ||
      id === u128(HOUSE_BTC) ||
      id === u128(HOUSE_LN) ||
      id === u128(HOUSE_LQ)
    ) {
      continue;
    }

    // Determine the house account for this ledger
    const houseId =
      a.ledger === LEDGER_SATS
        ? HOUSE_SATS
        : a.ledger === LEDGER_CREDIT_BTC
          ? HOUSE_BTC
          : a.ledger === LEDGER_CREDIT_LN
            ? HOUSE_LN
            : HOUSE_LQ;

    if (balMicro > 0n) {
      // Positive balance: house → user
      const results = await client.createTransfers([
        {
          id: nextTransferId(),
          debit_account_id: u128(houseId),
          credit_account_id: id,
          amount: balMicro,
          pending_id: 0n,
          user_data_128: 0n,
          user_data_64: 0n,
          user_data_32: 0,
          timeout: 0,
          ledger: a.ledger,
          code: 1,
          flags: 0,
          timestamp: 0n,
        },
      ]);
      if (results.length > 0) {
        console.error(`  transfer error for account ${id}: ${results[0].result}`);
      } else {
        console.log(`  restored account ${id} balance=${balMicro}`);
      }
    }
  }

  // Verify house balances
  console.log("\nVerifying house account balances...");
  for (const [name, houseId] of [
    ["SATS", HOUSE_SATS],
    ["BTC", HOUSE_BTC],
    ["LN", HOUSE_LN],
    ["LQ", HOUSE_LQ],
  ] as const) {
    const results = await client.lookupAccounts([u128(houseId)]);
    if (results.length > 0) {
      const bal = results[0].credits_posted - results[0].debits_posted;
      console.log(`  HOUSE_${name}: balance_micro=${bal} (${Number(bal / MSATS)} sats)`);
    }
  }

  console.log("\nDone!");
  client.destroy();
}

const cmd = process.argv[2];
if (cmd === "dump") {
  await dump();
} else if (cmd === "restore") {
  await restore();
} else {
  console.log("Usage: bun scripts/tb-migrate-cluster.ts [dump|restore]");
}