From 112e8af4101e902993fb02a2b0ba6740f2e037b7 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 24 Feb 2026 17:07:24 -0800 Subject: tb dump scripts --- scripts/tb-dump.json | 58 ++++++++++ scripts/tb-migrate-cluster.ts | 255 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 313 insertions(+) create mode 100644 scripts/tb-dump.json create mode 100644 scripts/tb-migrate-cluster.ts (limited to 'scripts') diff --git a/scripts/tb-dump.json b/scripts/tb-dump.json new file mode 100644 index 00000000..aa347e86 --- /dev/null +++ b/scripts/tb-dump.json @@ -0,0 +1,58 @@ +[ + { + "id": "1", + "ledger": 1, + "code": 1, + "flags": 0, + "balance_micro": "-2099000000" + }, + { + "id": "2", + "ledger": 2, + "code": 1, + "flags": 0, + "balance_micro": "0" + }, + { + "id": "3", + "ledger": 3, + "code": 1, + "flags": 0, + "balance_micro": "0" + }, + { + "id": "4", + "ledger": 4, + "code": 1, + "flags": 0, + "balance_micro": "0" + }, + { + "id": "154044882585978912721608634723889690896", + "ledger": 1, + "code": 1, + "flags": 2, + "balance_micro": "2099000000" + }, + { + "id": "154044882585978912666268402502761036048", + "ledger": 1, + "code": 1, + "flags": 2, + "balance_micro": "0" + }, + { + "id": "28979678647761563727999679858331371930", + "ledger": 1, + "code": 1, + "flags": 2, + "balance_micro": "0" + }, + { + "id": "28979678647761563820233400226879130010", + "ledger": 1, + "code": 1, + "flags": 2, + "balance_micro": "0" + } +] \ No newline at end of file diff --git a/scripts/tb-migrate-cluster.ts b/scripts/tb-migrate-cluster.ts new file mode 100644 index 00000000..08724007 --- /dev/null +++ b/scripts/tb-migrate-cluster.ts @@ -0,0 +1,255 @@ +/** + * 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 = { + bitcoin: LEDGER_CREDIT_BTC, + lightning: LEDGER_CREDIT_LN, + liquid: LEDGER_CREDIT_LQ, +}; + +const houseCreditAccount: Record = { + bitcoin: HOUSE_BTC, + lightning: HOUSE_LN, + liquid: HOUSE_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]"); +} -- cgit v1.2.3