diff options
| author | Adam Soltys <[email protected]> | 2026-02-04 23:13:33 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-02-06 09:00:09 -0800 |
| commit | 1ebc40afc14a75d7fe157b2c6c20ffea9dedb5eb (patch) | |
| tree | 960318969072afa63536e9429c27fa40ca2919d3 /lib | |
| parent | c0481268da25eed1f25281508b5c4777d26c2b03 (diff) | |
| download | coinos-server-1ebc40afc14a75d7fe157b2c6c20ffea9dedb5eb.tar.gz coinos-server-1ebc40afc14a75d7fe157b2c6c20ffea9dedb5eb.zip | |
tigerbeetle
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/db.ts | 67 | ||||
| -rw-r--r-- | lib/lightning.ts | 4 | ||||
| -rw-r--r-- | lib/migrate.ts | 58 | ||||
| -rw-r--r-- | lib/nwc.ts | 3 | ||||
| -rw-r--r-- | lib/payments.ts | 90 | ||||
| -rw-r--r-- | lib/register.ts | 5 | ||||
| -rw-r--r-- | lib/tb.ts | 572 |
7 files changed, 687 insertions, 112 deletions
@@ -1,75 +1,10 @@ import config from "$config"; import { err, warn } from "$lib/logging"; import { fail, sleep } from "$lib/utils"; -import { createClient, defineScript } from "redis"; - -const DEBIT = ` -local balanceKey = KEYS[1] -local creditKey = KEYS[2] -local insufficientString = KEYS[3]; -local amount = tonumber(ARGV[1]) -local tip = tonumber(ARGV[2]) -local fee = tonumber(ARGV[3]) -local ourfee = tonumber(ARGV[4]) -local frozen = tonumber(ARGV[5]) - -local balance = tonumber(redis.call('get', balanceKey) or '0') -local credit = tonumber(redis.call('get', creditKey) or '0') - -local covered = math.min(credit, ourfee) -ourfee = ourfee - covered - -if balance - frozen < amount + tip + fee + ourfee then - return {err = insufficientString .. ' ⚡️' .. balance - frozen .. ' / ' .. amount + tip + fee + ourfee} -end - -redis.call('decrby', creditKey, tostring(math.floor(covered))) -redis.call('decrby', balanceKey, tostring(math.floor(amount + tip + fee + ourfee))) - -return ourfee -`; - -const REVERSE = ` -local paymentKey = KEYS[1] -local balanceKey = KEYS[2] -local creditKey = KEYS[3] -local hashKey = KEYS[4] -local paymentsKey = KEYS[5] -local pid = KEYS[6] - -local total = tonumber(ARGV[1]) -local credit = tonumber(ARGV[2]) -local hash = ARGV[3] - -if redis.call('exists', paymentKey) == 1 then - redis.call('del', paymentKey) - redis.call('srem', 'pending', hash) - redis.call('incrby', balanceKey, total) - redis.call('incrby', creditKey, credit) - redis.call('del', hashKey) - redis.call('lrem', paymentsKey, 0, pid) - redis.call('lrem', 'payments', 0, pid) - return pid -else - error("Payment has already been reversed" .. paymentKey) -end -`; - -const debit = defineScript({ - NUMBER_OF_KEYS: 3, - SCRIPT: DEBIT, - transformArguments: (...args) => args.map((a) => a.toString()), -}); - -const reverse = defineScript({ - NUMBER_OF_KEYS: 6, - SCRIPT: REVERSE, - transformArguments: (...args) => args.map((a) => a.toString()), -}); +import { createClient } from "redis"; export const db = createClient({ url: config.db, - scripts: { debit, reverse }, socket: { reconnectStrategy: (retries) => Math.min(retries * 50, 5000), }, diff --git a/lib/lightning.ts b/lib/lightning.ts index 1f3b1665..e4c2e3cb 100644 --- a/lib/lightning.ts +++ b/lib/lightning.ts @@ -3,6 +3,7 @@ import ln, { lnListen, LightningUnavailableError } from "$lib/ln"; import { err, l, warn } from "$lib/logging"; import { handleZap } from "$lib/nostr"; import { credit } from "$lib/payments"; +import { tbSetBalance, getBalance } from "$lib/tb"; import { PaymentType } from "$lib/types"; import { getInvoice, getPayment, getUser } from "$lib/utils"; @@ -181,7 +182,8 @@ export const fixBolt12 = async (_, res) => { if (op) { db.del(`payment:${oid}`); db.del(`payment:${local_offer_id}`); - db.decrBy(`balance:${op.uid}`, op.amount); + const bal = await getBalance(op.uid); + await tbSetBalance(op.uid, bal - op.amount); } } } diff --git a/lib/migrate.ts b/lib/migrate.ts index 8fd4ec1a..fe804107 100644 --- a/lib/migrate.ts +++ b/lib/migrate.ts @@ -1,6 +1,13 @@ import { db, g, s } from "$lib/db"; import { findLastUsedIndex, parseDescriptor } from "$lib/esplora"; -import { warn } from "$lib/logging"; +import { l, warn } from "$lib/logging"; +import { + createBalanceAccount, + createCreditAccounts, + tbSetBalance, + tbSetPending, + tbSetCredit, +} from "$lib/tb"; async function migrate(id) { const k = id?.replace(/\s/g, "").toLowerCase(); @@ -49,4 +56,53 @@ export const migrateAccounts = async () => { return migrated; }; +export const migrateBalancesToTB = async () => { + const migrated = await db.get("tb:migrated"); + if (migrated) return 0; + + let count = 0; + + // Migrate balance:* keys + for await (const k of db.scanIterator({ MATCH: "balance:*" })) { + try { + const id = k.split(":")[1]; + const balance = Number.parseInt((await db.get(k)) || "0"); + await createBalanceAccount(id); + if (balance > 0) await tbSetBalance(id, balance); + count++; + } catch (e) { + warn("failed to migrate balance", k, e.message); + } + } + + // Migrate pending:* keys + for await (const k of db.scanIterator({ MATCH: "pending:*" })) { + try { + const id = k.split(":")[1]; + const pending = Number.parseInt((await db.get(k)) || "0"); + if (pending > 0) await tbSetPending(id, pending); + } catch (e) { + warn("failed to migrate pending", k, e.message); + } + } + + // Migrate credit:*:* keys + for await (const k of db.scanIterator({ MATCH: "credit:*:*" })) { + try { + const parts = k.split(":"); + const type = parts[1]; + const uid = parts[2]; + const amount = Number.parseInt((await db.get(k)) || "0"); + await createCreditAccounts(uid); + if (amount > 0) await tbSetCredit(uid, type, amount); + } catch (e) { + warn("failed to migrate credit", k, e.message); + } + } + + await db.set("tb:migrated", Date.now().toString()); + l(`Migrated ${count} balances to TigerBeetle`); + return count; +}; + export default migrate; @@ -11,6 +11,7 @@ import { serverSecret2, } from "$lib/nostr"; import { sendInternal, sendKeysend, sendLightning } from "$lib/payments"; +import { getBalance } from "$lib/tb"; import { fail, getInvoice, sleep } from "$lib/utils"; import rpc from "@coinos/rpc"; import { hexToBytes } from "@noble/hashes/utils"; @@ -327,7 +328,7 @@ const handle = (method, params, ev, app, user) => }, async get_balance() { - let balance = await g(`balance:${user.id}`); + let balance = await getBalance(user.id); balance *= 1000; return result({ balance }); }, diff --git a/lib/payments.ts b/lib/payments.ts index 4fddcba8..011807d5 100644 --- a/lib/payments.ts +++ b/lib/payments.ts @@ -22,6 +22,17 @@ import { notify, nwcNotify } from "$lib/notifications"; import { emit } from "$lib/sockets"; import { squarePayment } from "$lib/square"; import { + getBalance, + getCredit, + tbConfirm, + tbCredit, + tbDebit, + tbRefund, + tbReverse, + tbSetBalance, + tbSetPending, +} from "$lib/tb"; +import { SATS, btc, fail, @@ -137,17 +148,18 @@ export const debit = async ({ if (aid !== uid) ourfee = 0; const frozenBalance = - !blacklisted || whitelisted ? 0 : await g(`balance:${uid}`); + !blacklisted || whitelisted ? 0 : await getBalance(uid); - ourfee = await db.debit( - `balance:${aid}`, - `credit:${creditType}:${aid !== uid ? 0 : uid}`, - t(user).insufficientFunds, + ourfee = await tbDebit( + aid, + uid, + creditType, amount || 0, tip || 0, fee || 0, ourfee || 0, frozenBalance || 0, + t(user).insufficientFunds, ); if (ourfee.err) fail(ourfee.err); @@ -275,24 +287,17 @@ export const credit = async ({ await s(`payment:${hash}`, id); } - const m = await db.multi(); - let creditType = type; if (creditType === PaymentType.bolt12) creditType = PaymentType.lightning; - if ( - [PaymentType.bitcoin, PaymentType.liquid, PaymentType.lightning].includes( - creditType, - ) - ) - m.incrBy( - `credit:${creditType}:${uid}`, - Math.round(amount * config.fee[creditType]), - ); + const isPending = balanceKey === "pending"; + + await tbCredit(aid || uid, uid, creditType, amount, isPending); - m.set(`invoice:${inv.id}`, JSON.stringify(inv)) + await db + .multi() + .set(`invoice:${inv.id}`, JSON.stringify(inv)) .set(`payment:${p.id}`, JSON.stringify(p)) .lPush(`${aid || uid}:payments`, p.id) - .incrBy(`${balanceKey}:${aid || uid}`, amount) .set(`${aid || uid}:payments:last`, p.created) .exec(); @@ -313,7 +318,7 @@ export const completePayment = async (inv, p, user) => { if (autowithdraw) { try { const to = destination.trim(); - const balance = await g(`balance:${id}`); + const balance = await getBalance(id); const amount = balance - reserve; if (balance > threshold) { l("initiating autowithdrawal", amount, to, balance, threshold); @@ -349,8 +354,7 @@ const confirmWatchedIncoming = async (address, existing) => { await s(`invoice:${inv.id}`, inv); } await s(`payment:${existing.id}`, existing); - await db.decrBy(`pending:${existing.aid || existing.uid}`, existing.amount); - await db.incrBy(`balance:${existing.aid || existing.uid}`, existing.amount); + await tbConfirm(existing.aid || existing.uid, existing.amount); const user = await getUser(existing.uid); if (inv) await completePayment(inv, existing, user); await db.sRem("watching", address); @@ -872,7 +876,7 @@ const buildNonCustodial = async ({ }; }); - const balance = await g(`balance:${aid}`); + const balance = await getBalance(aid); let ourfee = 0; // Non-custodial accounts don't pay platform fee const outputs = [{ address, amount: BigInt(amount) }]; @@ -999,10 +1003,10 @@ export const build = async ({ else throw e; } - const balance = await g(`balance:${aid}`); + const balance = await getBalance(aid); let ourfee = Math.round(amount * config.fee[type]); - const credit = await g(`credit:${type}:${aid}`); - const covered = Math.min(credit, ourfee); + const creditBal = await getCredit(aid, type); + const covered = Math.min(creditBal, ourfee); ourfee -= covered; if (subtract || amount + fee + ourfee > balance) { @@ -1232,11 +1236,8 @@ export const importAccountHistory = async (account) => { .exec(); } - await db - .multi() - .set(`balance:${id}`, total) - .set(`pending:${id}`, 0) - .exec(); + await tbSetBalance(id, total); + await tbSetPending(id, 0); account.importedAt = account.importedAt || Date.now(); await s(`account:${id}`, account); @@ -1295,7 +1296,7 @@ const finalize = async (r, p) => { await s(`payment:${p.id}`, p); l("refunding fee", maxfee, p.fee, maxfee - p.fee, p.ref); - await db.incrBy(`balance:${p.uid}`, maxfee - p.fee); + await tbRefund(p.uid, maxfee - p.fee); } return p; @@ -1310,17 +1311,22 @@ const reverse = async (p) => { l("reversing", p.id, p.amount, p.fee, total, ourfee, credit); - await db.reverse( - `payment:${p.id}`, - `balance:${p.uid}`, - `credit:${PaymentType.lightning}:${p.uid}`, - `payment:${p.hash}`, - `${p.uid}:payments`, - p.id, - total, - credit, - p.hash, - ); + // Check payment still exists before reversing + const exists = await db.exists(`payment:${p.id}`); + if (!exists) throw new Error("Payment has already been reversed"); + + // TB: restore balance + credits + await tbReverse(p.uid, total, credit); + + // Redis: clean up payment records + await db + .multi() + .del(`payment:${p.id}`) + .sRem("pending", p.hash) + .del(`payment:${p.hash}`) + .lRem(`${p.uid}:payments`, 0, p.id) + .lRem("payments", 0, p.id) + .exec(); warn("reversed", p.id); }; diff --git a/lib/register.ts b/lib/register.ts index a74ea640..acffa9f2 100644 --- a/lib/register.ts +++ b/lib/register.ts @@ -2,6 +2,7 @@ import config from "$config"; import countries from "$lib/countries"; import { db, s } from "$lib/db"; import { l, warn } from "$lib/logging"; +import { createBalanceAccount, createCreditAccounts } from "$lib/tb"; import { fail } from "$lib/utils"; import { bytesToHex, randomBytes } from "@noble/hashes/utils"; import { got } from "got"; @@ -88,11 +89,13 @@ export default async (user, ip) => { await s(`app:${app.pubkey}`, app); await db.sAdd(`${id}:apps`, app.pubkey); + await createBalanceAccount(id); + await createCreditAccounts(id); + db.multi() .set(`user:${id}`, JSON.stringify(user)) .set(`user:${username}`, id) .set(`user:${pubkey}`, id) - .set(`balance:${id}`, 0) .set(`account:${id}`, account) .set(`${pubkey}:follows:n`, 0) .set(`${pubkey}:followers:n`, 0) diff --git a/lib/tb.ts b/lib/tb.ts new file mode 100644 index 00000000..9293422e --- /dev/null +++ b/lib/tb.ts @@ -0,0 +1,572 @@ +import config from "$config"; +import { warn } from "$lib/logging"; +import { + createClient, + CreateAccountError, + AccountFlags, + TransferFlags, +} from "tigerbeetle-node"; + +let client: any; + +// Ledger IDs +const LEDGER_SATS = 1; +const LEDGER_CREDIT_BTC = 2; +const LEDGER_CREDIT_LN = 3; +const LEDGER_CREDIT_LQ = 4; + +const creditLedger: Record<string, number> = { + bitcoin: LEDGER_CREDIT_BTC, + lightning: LEDGER_CREDIT_LN, + liquid: LEDGER_CREDIT_LQ, +}; + +// House account IDs match their ledger +const HOUSE_SATS = 1n; +const HOUSE_BTC = 2n; +const HOUSE_LN = 3n; +const HOUSE_LQ = 4n; + +const houseCreditAccount: Record<string, bigint> = { + bitcoin: HOUSE_BTC, + lightning: HOUSE_LN, + liquid: HOUSE_LQ, +}; + +// Convert UUID string to BigInt +function uuidToBigInt(uuid: string): bigint { + const hex = uuid.replace(/-/g, ""); + return BigInt(`0x${hex}`); +} + +// ID derivation +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 u128(n: bigint): bigint { + return n & ((1n << 128n) - 1n); +} + +async function resolveAddresses(addresses: string[]): Promise<string[]> { + const { lookup } = await import("node:dns/promises"); + const resolved: string[] = []; + for (const addr of addresses) { + const [host, port] = addr.split(":"); + try { + const { address: ip } = await lookup(host); + resolved.push(`${ip}:${port}`); + } catch { + resolved.push(addr); + } + } + return resolved; +} + +export async function initTigerBeetle() { + const { cluster_id, replica_addresses } = config.tigerbeetle; + const resolved = await resolveAddresses(replica_addresses); + client = createClient({ cluster_id, replica_addresses: resolved }); + + // Create house accounts (no flags = no constraints) + const houseAccounts = [ + { id: u128(HOUSE_SATS), ledger: LEDGER_SATS, code: 1 }, + { id: u128(HOUSE_BTC), ledger: LEDGER_CREDIT_BTC, code: 1 }, + { id: u128(HOUSE_LN), ledger: LEDGER_CREDIT_LN, code: 1 }, + { id: u128(HOUSE_LQ), ledger: LEDGER_CREDIT_LQ, code: 1 }, + ].map((a) => ({ + id: 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: 0, + timestamp: 0n, + })); + + const results = await client.createAccounts(houseAccounts); + for (const r of results) { + if (r.result !== CreateAccountError.exists) { + warn("TB house account creation:", r.index, r.result); + } + } +} + +export async function createBalanceAccount(aid: string) { + const accounts = [ + { + id: u128(balanceId(aid)), + ledger: LEDGER_SATS, + code: 1, + flags: AccountFlags.debits_must_not_exceed_credits, + }, + { + id: u128(pendingId(aid)), + ledger: LEDGER_SATS, + code: 1, + flags: AccountFlags.debits_must_not_exceed_credits, + }, + ].map((a) => ({ + ...a, + 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 balance account creation:", r.index, r.result); + } + } +} + +export async function createCreditAccounts(uid: string) { + const accounts = ["bitcoin", "lightning", "liquid"].map((type) => ({ + id: u128(creditId(uid, type)), + ledger: creditLedger[type], + 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 credit account creation:", r.index, r.result); + } + } +} + +async function getAccount(id: bigint) { + const accounts = await client.lookupAccounts([u128(id)]); + if (accounts.length === 0) return null; + return accounts[0]; +} + +function accountBalance(account: any): number { + if (!account) return 0; + return Number(account.credits_posted - account.debits_posted); +} + +export async function getBalance(aid: string): Promise<number> { + const account = await getAccount(balanceId(aid)); + return accountBalance(account); +} + +export async function getPending(aid: string): Promise<number> { + const account = await getAccount(pendingId(aid)); + return accountBalance(account); +} + +export async function getCredit(uid: string, type: string): Promise<number> { + const account = await getAccount(creditId(uid, type)); + return accountBalance(account); +} + +let transferIdCounter = BigInt(Date.now()) << 64n; +function nextTransferId(): bigint { + transferIdCounter += 1n; + return u128(transferIdCounter); +} + +export async function tbDebit( + aid: string, + uid: string, + creditType: string, + amount: number, + tip: number, + fee: number, + ourfee: number, + frozen: number, + errMsg: string, +): Promise<number> { + // Look up credit balance + let covered = 0; + if (ourfee > 0 && creditType && creditLedger[creditType]) { + const creditBal = await getCredit( + uid === aid ? uid : "00000000-0000-0000-0000-000000000000", + creditType, + ); + covered = Math.min(creditBal, ourfee); + ourfee -= covered; + } + + const totalDebit = amount + tip + fee + ourfee; + + // Check balance is sufficient (accounting for frozen funds) + const currentBalance = await getBalance(aid); + if (currentBalance - frozen < totalDebit) { + return { + err: `${errMsg} ⚡️${currentBalance - frozen} / ${totalDebit}`, + } as any; + } + + // Build linked transfers + const transfers: any[] = []; + + // 1. Consume credits (user credit → house credit) if any + if (covered > 0) { + const creditUserAcctId = creditId( + uid === aid ? uid : "00000000-0000-0000-0000-000000000000", + creditType, + ); + transfers.push({ + id: nextTransferId(), + debit_account_id: u128(creditUserAcctId), + credit_account_id: u128(houseCreditAccount[creditType]), + amount: BigInt(covered), + pending_id: 0n, + user_data_128: 0n, + user_data_64: 0n, + user_data_32: 0, + timeout: 0, + ledger: creditLedger[creditType], + code: 1, + flags: 0, // linked flag set below + timestamp: 0n, + }); + } + + // 2. Debit balance (user → house) + transfers.push({ + id: nextTransferId(), + debit_account_id: u128(balanceId(aid)), + credit_account_id: u128(HOUSE_SATS), + amount: BigInt(totalDebit), + 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, + }); + + // Link all but the last transfer + const linked = TransferFlags.linked; + + for (let i = 0; i < transfers.length - 1; i++) { + transfers[i].flags |= linked; + } + + const results = await client.createTransfers(transfers); + if (results.length > 0) { + return { + err: `${errMsg} ⚡️${currentBalance - frozen} / ${totalDebit}`, + } as any; + } + + return ourfee; +} + +export async function tbCredit( + aid: string, + uid: string, + creditType: string, + amount: number, + isPending: boolean, +) { + const transfers: any[] = []; + + // Transfer house → user balance (or house → pending) + const targetAccount = isPending ? pendingId(aid) : balanceId(aid); + transfers.push({ + id: nextTransferId(), + debit_account_id: u128(HOUSE_SATS), + credit_account_id: u128(targetAccount), + amount: BigInt(amount), + 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, + }); + + // Add fee credit if applicable + if (creditType && creditLedger[creditType] && config.fee[creditType]) { + const creditAmount = Math.round(amount * config.fee[creditType]); + if (creditAmount > 0) { + transfers.push({ + id: nextTransferId(), + debit_account_id: u128(houseCreditAccount[creditType]), + credit_account_id: u128(creditId(uid, creditType)), + amount: BigInt(creditAmount), + pending_id: 0n, + user_data_128: 0n, + user_data_64: 0n, + user_data_32: 0, + timeout: 0, + ledger: creditLedger[creditType], + code: 1, + flags: 0, + timestamp: 0n, + }); + } + } + + const results = await client.createTransfers(transfers); + for (const r of results) { + warn("TB credit transfer error:", r.index, r.result); + } +} + +export async function tbConfirm(aid: string, amount: number) { + // Transfer pending → balance (via house as intermediary) + const linked = TransferFlags.linked; + + const transfers = [ + { + id: nextTransferId(), + debit_account_id: u128(pendingId(aid)), + credit_account_id: u128(HOUSE_SATS), + amount: BigInt(amount), + pending_id: 0n, + user_data_128: 0n, + user_data_64: 0n, + user_data_32: 0, + timeout: 0, + ledger: LEDGER_SATS, + code: 1, + flags: linked, + timestamp: 0n, + }, + { + id: nextTransferId(), + debit_account_id: u128(HOUSE_SATS), + credit_account_id: u128(balanceId(aid)), + amount: BigInt(amount), + 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 confirm transfer error:", r.index, r.result); + } +} + +export async function tbReverse( + uid: string, + total: number, + creditAmount: number, +) { + const transfers: any[] = []; + const linked = TransferFlags.linked; + + // House → user balance + transfers.push({ + id: nextTransferId(), + debit_account_id: u128(HOUSE_SATS), + credit_account_id: u128(balanceId(uid)), + amount: BigInt(total), + pending_id: 0n, + user_data_128: 0n, + user_data_64: 0n, + user_data_32: 0, + timeout: 0, + ledger: LEDGER_SATS, + code: 1, + flags: creditAmount > 0 ? linked : 0, + timestamp: 0n, + }); + + // House credit → user credit (lightning) + if (creditAmount > 0) { + transfers.push({ + id: nextTransferId(), + debit_account_id: u128(HOUSE_LN), + credit_account_id: u128(creditId(uid, "lightning")), + amount: BigInt(creditAmount), + pending_id: 0n, + user_data_128: 0n, + user_data_64: 0n, + user_data_32: 0, + timeout: 0, + ledger: LEDGER_CREDIT_LN, + code: 1, + flags: 0, + timestamp: 0n, + }); + } + + const results = await client.createTransfers(transfers); + for (const r of results) { + warn("TB reverse transfer error:", r.index, r.result); + } +} + +export async function tbRefund(uid: string, amount: number) { + if (amount <= 0) return; + + const transfers = [ + { + id: nextTransferId(), + debit_account_id: u128(HOUSE_SATS), + credit_account_id: u128(balanceId(uid)), + amount: BigInt(amount), + 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 refund transfer error:", r.index, r.result); + } +} + +export async function tbSetBalance(aid: string, target: number) { + const current = await getBalance(aid); + const diff = target - current; + if (diff === 0) return; + + const transfers = [ + { + id: nextTransferId(), + debit_account_id: diff > 0 ? u128(HOUSE_SATS) : u128(balanceId(aid)), + credit_account_id: diff > 0 ? u128(balanceId(aid)) : u128(HOUSE_SATS), + amount: BigInt(Math.abs(diff)), + 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 setBalance transfer error:", r.index, r.result); + } +} + +export async function tbSetPending(aid: string, target: number) { + const current = await getPending(aid); + const diff = target - current; + if (diff === 0) return; + + const transfers = [ + { + id: nextTransferId(), + debit_account_id: diff > 0 ? u128(HOUSE_SATS) : u128(pendingId(aid)), + credit_account_id: diff > 0 ? u128(pendingId(aid)) : u128(HOUSE_SATS), + amount: BigInt(Math.abs(diff)), + 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 setPending transfer error:", r.index, r.result); + } +} + +export async function tbSetCredit(uid: string, type: string, target: number) { + const current = await getCredit(uid, type); + const diff = target - current; + if (diff === 0) return; + + const ledger = creditLedger[type]; + if (!ledger) return; + + const transfers = [ + { + id: nextTransferId(), + debit_account_id: diff > 0 ? u128(houseCreditAccount[type]) : u128(creditId(uid, type)), + credit_account_id: diff > 0 ? u128(creditId(uid, type)) : u128(houseCreditAccount[type]), + amount: BigInt(Math.abs(diff)), + pending_id: 0n, + user_data_128: 0n, + user_data_64: 0n, + user_data_32: 0, + timeout: 0, + ledger, + code: 1, + flags: 0, + timestamp: 0n, + }, + ]; + + const results = await client.createTransfers(transfers); + for (const r of results) { + warn("TB setCredit transfer error:", r.index, r.result); + } +} + +// Debit from a fund:* key (Redis only, no credits) +export async function fundDebit( + fundKey: 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) { + return { err: `${errMsg} ⚡️${bal} / ${amount}` }; + } + await db.decrBy(fundKey, amount); + return 0; +} |
