aboutsummaryrefslogtreecommitdiffstats
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
parent6e5c851fe1c4e7402014c5a42c8a5f919365a2c7 (diff)
downloadcoinos-server-b1e3435613ef092d43353d511b632a8de64efc5d.tar.gz
coinos-server-b1e3435613ef092d43353d511b632a8de64efc5d.zip
migrate funds to tb
-rw-r--r--index.ts3
-rw-r--r--lib/migrate.ts33
-rw-r--r--lib/tb.ts96
-rw-r--r--routes/payments.ts16
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<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
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({