From b1e3435613ef092d43353d511b632a8de64efc5d Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 18 Feb 2026 07:49:25 -0800 Subject: migrate funds to tb --- index.ts | 3 +- lib/migrate.ts | 33 +++++++++++++++++++ lib/tb.ts | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++---- routes/payments.ts | 16 ++++----- 4 files changed, 132 insertions(+), 16 deletions(-) diff --git a/index.ts b/index.ts index c5c6b398..391dfaef 100644 --- a/index.ts +++ b/index.ts @@ -4,7 +4,7 @@ import { admin, auth, optional } from "$lib/auth"; import { fixBolt12, listenForLightning, replay } from "$lib/lightning"; import { startHealthCheck } from "$lib/health"; import { getLocations } from "$lib/locations"; -import { migrateAccounts, migrateAutowithdraw, migrateBalancesToTB, migrateToMicrosats } from "$lib/migrate"; +import { migrateAccounts, migrateAutowithdraw, migrateBalancesToTB, migrateFundsToTB, migrateToMicrosats } from "$lib/migrate"; import nwc from "$lib/nwc"; import { catchUp, check } from "$lib/payments"; import { getFx } from "$lib/rates"; @@ -39,6 +39,7 @@ try { migrateBalancesToTB().then((n) => n && console.log(`Migrated ${n} balances to TB`)); migrateToMicrosats().then((n) => n && console.log(`Migrated ${n} users to microsats`)); migrateAutowithdraw().then((n) => n && console.log(`Migrated autowithdraw to ${n} accounts`)); + migrateFundsToTB().then((n) => n && console.log(`Migrated ${n} funds to TB`)); } catch (e) { console.log(e); } diff --git a/lib/migrate.ts b/lib/migrate.ts index 79f9546c..d4bfd61a 100644 --- a/lib/migrate.ts +++ b/lib/migrate.ts @@ -4,6 +4,8 @@ import { l, warn } from "$lib/logging"; import { createBalanceAccount, createCreditAccounts, + createFundAccount, + tbFundCredit, tbSetBalance, tbSetPending, tbSetCredit, @@ -176,4 +178,35 @@ export const migrateAutowithdraw = async () => { return count; }; +export const migrateFundsToTB = async () => { + const migrated = await db.get("tb:funds-migrated"); + if (migrated) return 0; + + let count = 0; + + for await (const k of scan("fund:*")) { + try { + // Skip sub-keys like fund:name:payments, fund:name:managers, fund:limit + const parts = k.split(":"); + if (parts.length !== 2) continue; + const name = parts[1]; + if (name === "limit") continue; + + const keyType = await db.type(k); + if (keyType !== "string") continue; + + const balance = Number.parseInt((await db.get(k)) || "0"); + await createFundAccount(name); + if (balance > 0) await tbFundCredit(name, balance); + count++; + } catch (e) { + warn("failed to migrate fund", k, e.message); + } + } + + await db.set("tb:funds-migrated", Date.now().toString()); + l(`Migrated ${count} funds to TigerBeetle`); + return count; +}; + export default migrate; diff --git a/lib/tb.ts b/lib/tb.ts index 9a4dc7c8..ae1ac576 100644 --- a/lib/tb.ts +++ b/lib/tb.ts @@ -6,6 +6,7 @@ import { AccountFlags, TransferFlags, } from "tigerbeetle-node"; +import { createHash } from "crypto"; let client: any; @@ -57,6 +58,11 @@ function creditId(uid: string, type: string): bigint { 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); } @@ -591,21 +597,97 @@ export async function tbSetCredit(uid: string, type: string, target: number) { } } -// Debit from a fund:* key (Redis only, no credits) -export async function fundDebit( - fundKey: string, +export async function createFundAccount(name: string) { + const accounts = [ + { + id: u128(fundAccountId(name)), + ledger: LEDGER_SATS, + code: 1, + flags: AccountFlags.debits_must_not_exceed_credits, + 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, + timestamp: 0n, + }, + ]; + + const results = await client.createAccounts(accounts); + for (const r of results) { + if (r.result !== CreateAccountError.exists) { + warn("TB fund account creation:", r.index, r.result); + } + } +} + +export async function tbFundCredit(name: string, amount: number) { + await createFundAccount(name); + const transfers = [ + { + id: nextTransferId(), + debit_account_id: u128(HOUSE_SATS), + credit_account_id: u128(fundAccountId(name)), + amount: BigInt(amount) * MSATS, + pending_id: 0n, + user_data_128: 0n, + user_data_64: 0n, + user_data_32: 0, + timeout: 0, + ledger: LEDGER_SATS, + code: 1, + flags: 0, + timestamp: 0n, + }, + ]; + + const results = await client.createTransfers(transfers); + for (const r of results) { + warn("TB fund credit transfer error:", r.index, r.result); + } +} + +export async function tbFundDebit( + name: string, amount: number, errMsg: string, ): Promise { - const { db } = await import("$lib/db"); - const bal = Number.parseInt((await db.get(fundKey)) || "0"); - if (bal < amount) { + const transfers = [ + { + id: nextTransferId(), + debit_account_id: u128(fundAccountId(name)), + credit_account_id: u128(HOUSE_SATS), + amount: BigInt(amount) * MSATS, + pending_id: 0n, + user_data_128: 0n, + user_data_64: 0n, + user_data_32: 0, + timeout: 0, + ledger: LEDGER_SATS, + code: 1, + flags: 0, + timestamp: 0n, + }, + ]; + + const results = await client.createTransfers(transfers); + if (results.length > 0) { + const account = await getAccount(fundAccountId(name)); + const bal = accountBalance(account); return { err: `${errMsg} ⚡️${bal} / ${amount}` }; } - await db.decrBy(fundKey, amount); return 0; } +export async function getFundBalance(name: string): Promise { + const account = await getAccount(fundAccountId(name)); + if (!account) return null; + return accountBalance(account); +} + // Migration helper: multiply existing balance by MULTIPLIER to convert sats→microsats // Called by migrateToMicrosats() - adds (balance * 999999) to turn X into X*1000000 // NOTE: This migration should only run ONCE - guarded by tb:microsats flag in migrate.ts diff --git a/routes/payments.ts b/routes/payments.ts index c77da697..6bc13b1c 100644 --- a/routes/payments.ts +++ b/routes/payments.ts @@ -24,7 +24,7 @@ import { sendOnchain, } from "$lib/payments"; import { emit } from "$lib/sockets"; -import { fundDebit, getBalance, getCredit, tbConfirm, tbCredit } from "$lib/tb"; +import { getBalance, getCredit, getFundBalance, tbConfirm, tbCredit, tbFundCredit, tbFundDebit } from "$lib/tb"; import { PaymentType } from "$lib/types"; import { SATS, @@ -109,13 +109,13 @@ export default { } } else if (fund) { p = await debit({ - hash, + hash: v4(), amount, memo: fund, user, type: PaymentType.fund, }); - await db.incrBy(`fund:${fund}`, amount); + await tbFundCredit(fund, amount); await db.lPush(`fund:${fund}:payments`, p.id); l("funded fund", fund); } @@ -277,8 +277,8 @@ export default { const { params: { id }, } = req; - const amount = await g(`fund:${id}`); - if (typeof amount === "undefined" || amount === null) + const amount = await getFundBalance(id); + if (amount === null) return bail(res, "fund not found"); let payments = (await db.lRange(`fund:${id}:payments`, 0, -1)) || []; payments = await Promise.all(payments.map((hash) => gf(`payment:${hash}`))); @@ -336,7 +336,7 @@ export default { } = req; try { amount = Number.parseInt(amount); - if (amount < 0) fail("Invalid amount"); + if (amount <= 0) fail("Invalid amount"); const rates = await g("rates"); @@ -370,7 +370,7 @@ export default { type: PaymentType.fund, }); - await db.incrBy(`fund:${id}`, amount); + await tbFundCredit(id, amount); await db.lPush(`fund:${id}:payments`, pid); l("funded fund", id); } @@ -378,7 +378,7 @@ export default { const managers = await db.sMembers(`fund:${id}:managers`); if (managers.length && !managers.includes(user.id)) fail("Unauthorized"); - const result: any = await fundDebit(`fund:${id}`, amount, "Insufficient funds"); + const result: any = await tbFundDebit(id, amount, "Insufficient funds"); if (result.err) fail(result.err); const payment = await credit({ -- cgit v1.2.3