From 7841b2d61830f13260e46d24ce69f24b2dbcea29 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 31 May 2026 23:27:59 -0700 Subject: debit: exempt internal sends from server liquidity limit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit freezeCheck only populates ${type}:limit for external asset types (lightning/fund/ecash/bitcoin/liquid/bolt12), so internal:limit stays 0 forever. The atomic limit check added in 609fcebe applied to all types, so amount > 0 blocked every internal coinos-to-coinos payment with "serverLimit 0". Internal sends are pure ledger moves that never touch an external wallet — there's no hot-wallet liquidity to reserve — so exempt them, matching the existing soft-freeze and withdraw-lock gates. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/payments.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) (limited to 'lib') diff --git a/lib/payments.ts b/lib/payments.ts index 52434739..acc9a462 100644 --- a/lib/payments.ts +++ b/lib/payments.ts @@ -135,14 +135,22 @@ export const debit = async ({ // Atomic check + reserve against the per-asset-type server limit. Decrement // immediately so concurrent calls can't reuse the same budget; freezeCheck // reconciles to actual on-chain balance every 10s. - await withLimitLock(type, async () => { - const serverLimit = Number.parseInt((await g(`${type}:limit`)) ?? "0", 10) || 0; - if (amount > serverLimit) { - warn("Blocking", user.username, amount, hash, user.id, type, "serverLimit", serverLimit); - fail("Problem sending payment"); - } - await db.decrBy(`${type}:limit`, amount); - }); + // + // Internal sends are exempt: they're pure ledger moves between coinos users + // and never touch an external wallet, so there's no hot-wallet liquidity to + // reserve against. freezeCheck only ever populates limits for external types + // (lightning/fund/ecash/bolt12/bitcoin/liquid), so `internal:limit` stays 0 + // forever — applying the check here blocks every internal payment. + if (type !== PaymentType.internal) { + await withLimitLock(type, async () => { + const serverLimit = Number.parseInt((await g(`${type}:limit`)) ?? "0", 10) || 0; + if (amount > serverLimit) { + warn("Blocking", user.username, amount, hash, user.id, type, "serverLimit", serverLimit); + fail("Problem sending payment"); + } + await db.decrBy(`${type}:limit`, amount); + }); + } let ref; const { id: uid, currency } = user; -- cgit v1.2.3