aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-02-18 07:49:25 -0800
committerAdam Soltys <[email protected]>2026-02-18 07:49:25 -0800
commitb1e3435613ef092d43353d511b632a8de64efc5d (patch)
treea300812eec33177505dbba5882d25669fea4243e /lib
parent6e5c851fe1c4e7402014c5a42c8a5f919365a2c7 (diff)
downloadcoinos-server-b1e3435613ef092d43353d511b632a8de64efc5d.tar.gz
coinos-server-b1e3435613ef092d43353d511b632a8de64efc5d.zip
migrate funds to tb
Diffstat (limited to 'lib')
-rw-r--r--lib/migrate.ts33
-rw-r--r--lib/tb.ts96
2 files changed, 122 insertions, 7 deletions
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<any> {
- 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<number | null> {
+ 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