diff options
| author | Adam Soltys <[email protected]> | 2026-02-05 09:34:17 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-02-06 09:00:09 -0800 |
| commit | 7c40346355346fb59a86d11cdbef1a7da00e8da8 (patch) | |
| tree | 4112444b006f5f9cfcd52a3783c305305fc21a55 | |
| parent | 1ebc40afc14a75d7fe157b2c6c20ffea9dedb5eb (diff) | |
| download | coinos-server-7c40346355346fb59a86d11cdbef1a7da00e8da8.tar.gz coinos-server-7c40346355346fb59a86d11cdbef1a7da00e8da8.zip | |
microsats
| -rw-r--r-- | index.ts | 3 | ||||
| -rw-r--r-- | lib/migrate.ts | 31 | ||||
| -rw-r--r-- | lib/tb.ts | 197 | ||||
| -rw-r--r-- | scripts/ark-send.ts | 21 | ||||
| -rwxr-xr-x | scripts/tb.sh | 82 |
5 files changed, 248 insertions, 86 deletions
@@ -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, migrateBalancesToTB } from "$lib/migrate"; +import { migrateAccounts, migrateBalancesToTB, migrateToMicrosats } from "$lib/migrate"; import nwc from "$lib/nwc"; import { check } from "$lib/payments"; import { getFx } from "$lib/rates"; @@ -36,6 +36,7 @@ try { startZmq(); migrateAccounts().then((n) => n && console.log(`Migrated ${n} accounts`)); migrateBalancesToTB().then((n) => n && console.log(`Migrated ${n} balances to TB`)); + migrateToMicrosats().then((n) => n && console.log(`Migrated ${n} users to microsats`)); } catch (e) { console.log(e); } diff --git a/lib/migrate.ts b/lib/migrate.ts index fe804107..4b64bee5 100644 --- a/lib/migrate.ts +++ b/lib/migrate.ts @@ -7,6 +7,7 @@ import { tbSetBalance, tbSetPending, tbSetCredit, + tbMultiplyForMicrosats, } from "$lib/tb"; async function migrate(id) { @@ -105,4 +106,34 @@ export const migrateBalancesToTB = async () => { return count; }; +export const migrateToMicrosats = async () => { + const migrated = await db.get("tb:microsats"); + if (migrated) return 0; + + let count = 0; + + // Iterate through all users and multiply their TB balances + for await (const k of db.scanIterator({ MATCH: "user:*" })) { + try { + const raw = await db.get(k); + if (!raw) continue; + + // Skip user alias keys (they just contain another key reference) + if (!raw.startsWith("{")) continue; + + const user = JSON.parse(raw); + if (!user.id) continue; + + const multiplied = await tbMultiplyForMicrosats(user.id); + if (multiplied > 0) count++; + } catch (e) { + warn("microsats migration error", k, e.message); + } + } + + await db.set("tb:microsats", Date.now().toString()); + l(`Migrated ${count} users to microsats`); + return count; +}; + export default migrate; @@ -15,6 +15,10 @@ const LEDGER_CREDIT_BTC = 2; const LEDGER_CREDIT_LN = 3; const LEDGER_CREDIT_LQ = 4; +// Microsatoshi precision: 1 sat = 1,000,000 microsats +const MSATS = 1_000_000n; // BigInt for TB operations +const MSATS_NUM = 1_000_000; // Number for JS math + const creditLedger: Record<string, number> = { bitcoin: LEDGER_CREDIT_BTC, lightning: LEDGER_CREDIT_LN, @@ -173,9 +177,16 @@ async function getAccount(id: bigint) { return accounts[0]; } +// Raw microsats balance for internal use +function accountBalanceMicro(account: any): bigint { + if (!account) return 0n; + return account.credits_posted - account.debits_posted; +} + function accountBalance(account: any): number { if (!account) return 0; - return Number(account.credits_posted - account.debits_posted); + const micro = account.credits_posted - account.debits_posted; + return Number(micro / MSATS); // Floor division to sats } export async function getBalance(aid: string): Promise<number> { @@ -210,32 +221,41 @@ export async function tbDebit( frozen: number, errMsg: string, ): Promise<number> { - // Look up credit balance - let covered = 0; + // Look up credit balance in microsats for precision + let coveredMicro = 0n; if (ourfee > 0 && creditType && creditLedger[creditType]) { - const creditBal = await getCredit( - uid === aid ? uid : "00000000-0000-0000-0000-000000000000", - creditType, + const creditAcct = await getAccount( + creditId( + uid === aid ? uid : "00000000-0000-0000-0000-000000000000", + creditType, + ), ); - covered = Math.min(creditBal, ourfee); - ourfee -= covered; + const creditBalMicro = accountBalanceMicro(creditAcct); + const ourfeeMicro = BigInt(ourfee) * MSATS; + coveredMicro = creditBalMicro < ourfeeMicro ? creditBalMicro : ourfeeMicro; + ourfee = Number((ourfeeMicro - coveredMicro) / MSATS); } - const totalDebit = amount + tip + fee + ourfee; + // Total debit in microsats + const totalDebitMicro = BigInt(amount + tip + fee + ourfee) * MSATS; - // Check balance is sufficient (accounting for frozen funds) - const currentBalance = await getBalance(aid); - if (currentBalance - frozen < totalDebit) { + // Check balance is sufficient (accounting for frozen funds) in microsats + const balAcct = await getAccount(balanceId(aid)); + const currentBalMicro = accountBalanceMicro(balAcct); + const frozenMicro = BigInt(frozen) * MSATS; + if (currentBalMicro - frozenMicro < totalDebitMicro) { + const availSats = Number((currentBalMicro - frozenMicro) / MSATS); + const needSats = Number(totalDebitMicro / MSATS); return { - err: `${errMsg} ⚡️${currentBalance - frozen} / ${totalDebit}`, + err: `${errMsg} ⚡️${availSats} / ${needSats}`, } as any; } // Build linked transfers const transfers: any[] = []; - // 1. Consume credits (user credit → house credit) if any - if (covered > 0) { + // 1. Consume credits (user credit → house credit) if any - in microsats + if (coveredMicro > 0n) { const creditUserAcctId = creditId( uid === aid ? uid : "00000000-0000-0000-0000-000000000000", creditType, @@ -244,7 +264,7 @@ export async function tbDebit( id: nextTransferId(), debit_account_id: u128(creditUserAcctId), credit_account_id: u128(houseCreditAccount[creditType]), - amount: BigInt(covered), + amount: coveredMicro, pending_id: 0n, user_data_128: 0n, user_data_64: 0n, @@ -257,12 +277,12 @@ export async function tbDebit( }); } - // 2. Debit balance (user → house) + // 2. Debit balance (user → house) in microsats transfers.push({ id: nextTransferId(), debit_account_id: u128(balanceId(aid)), credit_account_id: u128(HOUSE_SATS), - amount: BigInt(totalDebit), + amount: totalDebitMicro, pending_id: 0n, user_data_128: 0n, user_data_64: 0n, @@ -283,8 +303,10 @@ export async function tbDebit( const results = await client.createTransfers(transfers); if (results.length > 0) { + const availSats = Number((currentBalMicro - frozenMicro) / MSATS); + const needSats = Number(totalDebitMicro / MSATS); return { - err: `${errMsg} ⚡️${currentBalance - frozen} / ${totalDebit}`, + err: `${errMsg} ⚡️${availSats} / ${needSats}`, } as any; } @@ -300,13 +322,13 @@ export async function tbCredit( ) { const transfers: any[] = []; - // Transfer house → user balance (or house → pending) + // Transfer house → user balance (or house → pending) in microsats 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), + amount: BigInt(amount) * MSATS, pending_id: 0n, user_data_128: 0n, user_data_64: 0n, @@ -318,15 +340,18 @@ export async function tbCredit( timestamp: 0n, }); - // Add fee credit if applicable + // Add fee credit if applicable - MICROSATS for sub-satoshi precision! + // e.g., 2% of 10 sats = 0.2 sats = 200,000 microsats (accumulates over time) if (creditType && creditLedger[creditType] && config.fee[creditType]) { - const creditAmount = Math.round(amount * config.fee[creditType]); - if (creditAmount > 0) { + const creditAmountMicro = BigInt( + Math.round(amount * MSATS_NUM * config.fee[creditType]), + ); + if (creditAmountMicro > 0n) { transfers.push({ id: nextTransferId(), debit_account_id: u128(houseCreditAccount[creditType]), credit_account_id: u128(creditId(uid, creditType)), - amount: BigInt(creditAmount), + amount: creditAmountMicro, pending_id: 0n, user_data_128: 0n, user_data_64: 0n, @@ -347,15 +372,16 @@ export async function tbCredit( } export async function tbConfirm(aid: string, amount: number) { - // Transfer pending → balance (via house as intermediary) + // Transfer pending → balance (via house as intermediary) in microsats const linked = TransferFlags.linked; + const amountMicro = BigInt(amount) * MSATS; const transfers = [ { id: nextTransferId(), debit_account_id: u128(pendingId(aid)), credit_account_id: u128(HOUSE_SATS), - amount: BigInt(amount), + amount: amountMicro, pending_id: 0n, user_data_128: 0n, user_data_64: 0n, @@ -370,7 +396,7 @@ export async function tbConfirm(aid: string, amount: number) { id: nextTransferId(), debit_account_id: u128(HOUSE_SATS), credit_account_id: u128(balanceId(aid)), - amount: BigInt(amount), + amount: amountMicro, pending_id: 0n, user_data_128: 0n, user_data_64: 0n, @@ -397,12 +423,12 @@ export async function tbReverse( const transfers: any[] = []; const linked = TransferFlags.linked; - // House → user balance + // House → user balance in microsats transfers.push({ id: nextTransferId(), debit_account_id: u128(HOUSE_SATS), credit_account_id: u128(balanceId(uid)), - amount: BigInt(total), + amount: BigInt(total) * MSATS, pending_id: 0n, user_data_128: 0n, user_data_64: 0n, @@ -414,13 +440,13 @@ export async function tbReverse( timestamp: 0n, }); - // House credit → user credit (lightning) + // House credit → user credit (lightning) in microsats if (creditAmount > 0) { transfers.push({ id: nextTransferId(), debit_account_id: u128(HOUSE_LN), credit_account_id: u128(creditId(uid, "lightning")), - amount: BigInt(creditAmount), + amount: BigInt(creditAmount) * MSATS, pending_id: 0n, user_data_128: 0n, user_data_64: 0n, @@ -447,7 +473,7 @@ export async function tbRefund(uid: string, amount: number) { id: nextTransferId(), debit_account_id: u128(HOUSE_SATS), credit_account_id: u128(balanceId(uid)), - amount: BigInt(amount), + amount: BigInt(amount) * MSATS, pending_id: 0n, user_data_128: 0n, user_data_64: 0n, @@ -467,16 +493,19 @@ export async function tbRefund(uid: string, amount: number) { } export async function tbSetBalance(aid: string, target: number) { - const current = await getBalance(aid); - const diff = target - current; - if (diff === 0) return; + // Work entirely in microsats + const balAcct = await getAccount(balanceId(aid)); + const currentMicro = accountBalanceMicro(balAcct); + const targetMicro = BigInt(target) * MSATS; + const diff = targetMicro - currentMicro; + if (diff === 0n) 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)), + debit_account_id: diff > 0n ? u128(HOUSE_SATS) : u128(balanceId(aid)), + credit_account_id: diff > 0n ? u128(balanceId(aid)) : u128(HOUSE_SATS), + amount: diff > 0n ? diff : -diff, pending_id: 0n, user_data_128: 0n, user_data_64: 0n, @@ -496,16 +525,19 @@ export async function tbSetBalance(aid: string, target: number) { } export async function tbSetPending(aid: string, target: number) { - const current = await getPending(aid); - const diff = target - current; - if (diff === 0) return; + // Work entirely in microsats + const pendAcct = await getAccount(pendingId(aid)); + const currentMicro = accountBalanceMicro(pendAcct); + const targetMicro = BigInt(target) * MSATS; + const diff = targetMicro - currentMicro; + if (diff === 0n) 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)), + debit_account_id: diff > 0n ? u128(HOUSE_SATS) : u128(pendingId(aid)), + credit_account_id: diff > 0n ? u128(pendingId(aid)) : u128(HOUSE_SATS), + amount: diff > 0n ? diff : -diff, pending_id: 0n, user_data_128: 0n, user_data_64: 0n, @@ -525,19 +557,22 @@ export async function tbSetPending(aid: string, target: number) { } export async function tbSetCredit(uid: string, type: string, target: number) { - const current = await getCredit(uid, type); - const diff = target - current; - if (diff === 0) return; - + // Work entirely in microsats const ledger = creditLedger[type]; if (!ledger) return; + const creditAcct = await getAccount(creditId(uid, type)); + const currentMicro = accountBalanceMicro(creditAcct); + const targetMicro = BigInt(target) * MSATS; + const diff = targetMicro - currentMicro; + if (diff === 0n) 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)), + debit_account_id: diff > 0n ? u128(houseCreditAccount[type]) : u128(creditId(uid, type)), + credit_account_id: diff > 0n ? u128(creditId(uid, type)) : u128(houseCreditAccount[type]), + amount: diff > 0n ? diff : -diff, pending_id: 0n, user_data_128: 0n, user_data_64: 0n, @@ -570,3 +605,61 @@ export async function fundDebit( await db.decrBy(fundKey, amount); return 0; } + +// 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 +export async function tbMultiplyForMicrosats(uid: string): Promise<number> { + // Ensure client is initialized (needed when called during startup) + if (!client) { + await initTigerBeetle(); + } + + const MULTIPLIER = 999999n; // Adding this turns X into X*1000000 + let count = 0; + + const multiplyAccount = async ( + accountId: bigint, + ledger: number, + houseId: bigint, + ) => { + const acct = await client.lookupAccounts([u128(accountId)]); + if (!acct.length) return; + const current = acct[0].credits_posted - acct[0].debits_posted; + if (current <= 0n) return; + + const results = await client.createTransfers([ + { + id: nextTransferId(), + debit_account_id: u128(houseId), + credit_account_id: u128(accountId), + amount: current * MULTIPLIER, + pending_id: 0n, + user_data_128: 0n, + user_data_64: 0n, + user_data_32: 0, + timeout: 0, + ledger, + code: 1, + flags: 0, + timestamp: 0n, + }, + ]); + if (results.length === 0) count++; + }; + + const balId = balanceId(uid); + const pendId = pendingId(uid); + + await multiplyAccount(balId, LEDGER_SATS, HOUSE_SATS); + await multiplyAccount(pendId, LEDGER_SATS, HOUSE_SATS); + await multiplyAccount(creditId(uid, "bitcoin"), LEDGER_CREDIT_BTC, HOUSE_BTC); + await multiplyAccount( + creditId(uid, "lightning"), + LEDGER_CREDIT_LN, + HOUSE_LN, + ); + await multiplyAccount(creditId(uid, "liquid"), LEDGER_CREDIT_LQ, HOUSE_LQ); + + return count; +} diff --git a/scripts/ark-send.ts b/scripts/ark-send.ts new file mode 100644 index 00000000..c6b3140d --- /dev/null +++ b/scripts/ark-send.ts @@ -0,0 +1,21 @@ +import { sendArk } from "../lib/ark"; + +const address = process.argv[2]; +const amount = parseInt(process.argv[3]); + +if (!address || !amount) { + console.log("Usage: bun scripts/ark-send.ts <ark_address> <amount_sats>"); + process.exit(1); +} + +console.log(`Sending ${amount} sats to ${address}...`); + +sendArk(address, amount) + .then((result) => { + console.log("Success!"); + console.log("Result:", JSON.stringify(result, null, 2)); + }) + .catch((e) => { + console.error("Error:", e.message); + process.exit(1); + }); diff --git a/scripts/tb.sh b/scripts/tb.sh index 03daead8..c9450b54 100755 --- a/scripts/tb.sh +++ b/scripts/tb.sh @@ -16,7 +16,8 @@ _user_id() { # Query TB accounts via bun in app container # Args: space-separated list of bigint IDs (as decimal strings) -# Returns: id credits_posted debits_posted balance (one line per account) +# Returns: id credits_posted debits_posted balance_micro balance_sats (one line per account) +# Note: balances stored in microsats (1 sat = 1,000,000 microsats) _tb_lookup() { local ids="$1" docker exec app bun -e " @@ -31,10 +32,11 @@ _tb_lookup() { for (const id of ids) { const a = found.get(id.toString()); if (a) { - const bal = a.credits_posted - a.debits_posted; - console.log(id.toString() + ' ' + a.credits_posted.toString() + ' ' + a.debits_posted.toString() + ' ' + bal.toString()); + const micro = a.credits_posted - a.debits_posted; + const sats = Number(micro / 1000000n); + console.log(id.toString() + ' ' + a.credits_posted.toString() + ' ' + a.debits_posted.toString() + ' ' + micro.toString() + ' ' + sats.toString()); } else { - console.log(id.toString() + ' 0 0 0'); + console.log(id.toString() + ' 0 0 0 0'); } } c.destroy(); @@ -67,8 +69,9 @@ tb-balance() { if [ -z "$uid" ]; then echo "User not found: $1"; return 1; fi local account_id=$(_uuid2int "$uid") local result=$(_tb_lookup "$account_id") - local bal=$(echo "$result" | awk '{print $4}') - echo "$1: $bal sats" + local micro=$(echo "$result" | awk '{print $4}') + local sats=$(echo "$result" | awk '{print $5}') + echo "$1: $sats sats ($micro μsats)" } # Get pending balance for a username @@ -78,8 +81,9 @@ tb-pending() { if [ -z "$uid" ]; then echo "User not found: $1"; return 1; fi local pending_id=$(python3 -c "print(int('${uid//-/}', 16) ^ (5 << 64))") local result=$(_tb_lookup "$pending_id") - local bal=$(echo "$result" | awk '{print $4}') - echo "$1: $bal sats pending" + local micro=$(echo "$result" | awk '{print $4}') + local sats=$(echo "$result" | awk '{print $5}') + echo "$1: $sats sats pending ($micro μsats)" } # Get fee credits for a username @@ -90,13 +94,16 @@ tb-credits() { local ids IFS=$'\n' read -d '' -ra ids < <(_derive_ids "$uid") local result=$(_tb_lookup "${ids[2]},${ids[3]},${ids[4]}") - local btc=$(echo "$result" | sed -n '1p' | awk '{print $4}') - local ln=$(echo "$result" | sed -n '2p' | awk '{print $4}') - local lq=$(echo "$result" | sed -n '3p' | awk '{print $4}') + local btc_micro=$(echo "$result" | sed -n '1p' | awk '{print $4}') + local btc_sats=$(echo "$result" | sed -n '1p' | awk '{print $5}') + local ln_micro=$(echo "$result" | sed -n '2p' | awk '{print $4}') + local ln_sats=$(echo "$result" | sed -n '2p' | awk '{print $5}') + local lq_micro=$(echo "$result" | sed -n '3p' | awk '{print $4}') + local lq_sats=$(echo "$result" | sed -n '3p' | awk '{print $5}') echo "$1 credits:" - echo " bitcoin: $btc" - echo " lightning: $ln" - echo " liquid: $lq" + echo " bitcoin: $btc_sats sats ($btc_micro μsats)" + echo " lightning: $ln_sats sats ($ln_micro μsats)" + echo " liquid: $lq_sats sats ($lq_micro μsats)" } # Full summary for a username @@ -107,32 +114,41 @@ tb-user() { local ids IFS=$'\n' read -d '' -ra ids < <(_derive_ids "$uid") local result=$(_tb_lookup "${ids[0]},${ids[1]},${ids[2]},${ids[3]},${ids[4]}") - local bal=$(echo "$result" | sed -n '1p' | awk '{print $4}') - local pending=$(echo "$result" | sed -n '2p' | awk '{print $4}') - local btc=$(echo "$result" | sed -n '3p' | awk '{print $4}') - local ln=$(echo "$result" | sed -n '4p' | awk '{print $4}') - local lq=$(echo "$result" | sed -n '5p' | awk '{print $4}') + local bal_micro=$(echo "$result" | sed -n '1p' | awk '{print $4}') + local bal_sats=$(echo "$result" | sed -n '1p' | awk '{print $5}') + local pend_micro=$(echo "$result" | sed -n '2p' | awk '{print $4}') + local pend_sats=$(echo "$result" | sed -n '2p' | awk '{print $5}') + local btc_micro=$(echo "$result" | sed -n '3p' | awk '{print $4}') + local btc_sats=$(echo "$result" | sed -n '3p' | awk '{print $5}') + local ln_micro=$(echo "$result" | sed -n '4p' | awk '{print $4}') + local ln_sats=$(echo "$result" | sed -n '4p' | awk '{print $5}') + local lq_micro=$(echo "$result" | sed -n '5p' | awk '{print $4}') + local lq_sats=$(echo "$result" | sed -n '5p' | awk '{print $5}') echo "$1 ($uid)" - echo " balance: $bal sats" - echo " pending: $pending sats" + echo " balance: $bal_sats sats ($bal_micro μsats)" + echo " pending: $pend_sats sats ($pend_micro μsats)" echo " credits:" - echo " bitcoin: $btc" - echo " lightning: $ln" - echo " liquid: $lq" + echo " bitcoin: $btc_sats sats ($btc_micro μsats)" + echo " lightning: $ln_sats sats ($ln_micro μsats)" + echo " liquid: $lq_sats sats ($lq_micro μsats)" } # Show house account balances tb-house() { local result=$(_tb_lookup "1,2,3,4") - local sats=$(echo "$result" | sed -n '1p' | awk '{print $4}') - local btc=$(echo "$result" | sed -n '2p' | awk '{print $4}') - local ln=$(echo "$result" | sed -n '3p' | awk '{print $4}') - local lq=$(echo "$result" | sed -n '4p' | awk '{print $4}') - echo "House accounts:" - echo " sats (1): $sats" - echo " btc credit (2): $btc" - echo " ln credit (3): $ln" - echo " lq credit (4): $lq" + local sats_micro=$(echo "$result" | sed -n '1p' | awk '{print $4}') + local sats=$(echo "$result" | sed -n '1p' | awk '{print $5}') + local btc_micro=$(echo "$result" | sed -n '2p' | awk '{print $4}') + local btc=$(echo "$result" | sed -n '2p' | awk '{print $5}') + local ln_micro=$(echo "$result" | sed -n '3p' | awk '{print $4}') + local ln=$(echo "$result" | sed -n '3p' | awk '{print $5}') + local lq_micro=$(echo "$result" | sed -n '4p' | awk '{print $4}') + local lq=$(echo "$result" | sed -n '4p' | awk '{print $5}') + echo "House accounts (in microsats, 1 sat = 1,000,000 μsats):" + echo " sats (1): $sats sats ($sats_micro μsats)" + echo " btc credit (2): $btc sats ($btc_micro μsats)" + echo " ln credit (3): $ln sats ($ln_micro μsats)" + echo " lq credit (4): $lq sats ($lq_micro μsats)" } # Raw TB account lookup by numeric ID(s), comma-separated |
