aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-05-19 08:34:54 -0700
committerAdam Soltys <[email protected]>2026-05-19 08:34:54 -0700
commit9142b4dbcce407e331a2c1c9b56bb1ead3d3df76 (patch)
treea9bdb40f6cbcb07f4865f9bc8fc124290eee50bc /lib
parentcddafe5ef8fc90559d5163d4ecaa0da37c7947f7 (diff)
downloadcoinos-server-9142b4dbcce407e331a2c1c9b56bb1ead3d3df76.tar.gz
coinos-server-9142b4dbcce407e331a2c1c9b56bb1ead3d3df76.zip
withdraw-lock: refuse external sends when /locks/<type>.locked is present
Out-of-band emergency stop. The companion nobal.sh creates a sentinel file on the host (bind-mounted at /locks in the container) when a wallet balance drops below its failsafe threshold; debit() checks for the file on every external send and refuses with "External withdrawals temporarily disabled". /locks/ALL.locked is a global override. Lives outside the db so a db-write vector (the May 18 /gateway pattern) can't disable it. Internal sends are exempt — receives and user-to-user transfers keep working with the site fully online during a lock. For bitcoin and liquid this is a secondary defense on top of daemon- level walletpassphrase rotation. For lightning, it's the only available mechanism since CLN has no walletlock equivalent. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Diffstat (limited to 'lib')
-rw-r--r--lib/payments.ts25
1 files changed, 25 insertions, 0 deletions
diff --git a/lib/payments.ts b/lib/payments.ts
index 380b5e7b..01492975 100644
--- a/lib/payments.ts
+++ b/lib/payments.ts
@@ -1,6 +1,21 @@
import config from "$config";
+import { existsSync } from "fs";
import api from "$lib/api";
import { archive, db, g, ga, gf, s, sa } from "$lib/db";
+
+// External-withdrawal lockfiles. Out-of-band emergency stop: nobal.sh (or a
+// human) can `touch /home/adam/locks/<type>.locked` on the host, which is
+// bind-mounted into this container at /locks. Existence of /locks/<type>.locked
+// (or /locks/ALL.locked) blocks all external sends of that type. Lives outside
+// db so it can't be disabled by a db-write vector (May 18 /gateway lesson).
+//
+// Internal payments are never blocked by these — receives and internal
+// transfers between coinos users keep working with the site fully online.
+const isWithdrawLocked = (type: string): string | null => {
+ if (existsSync("/locks/ALL.locked")) return "ALL";
+ if (existsSync(`/locks/${type}.locked`)) return type;
+ return null;
+};
import { generate } from "$lib/invoices";
import ln from "$lib/ln";
import { err, l, warn } from "$lib/logging";
@@ -102,6 +117,16 @@ export const debit = async ({
(await g("hardfreeze")) ||
((await g("freeze")) && type !== PaymentType.internal);
+ // Out-of-band withdrawal lock (file-based, can't be defeated by db writes).
+ // Internal sends are exempt — they don't touch external wallets.
+ if (type !== PaymentType.internal) {
+ const lockedKind = isWithdrawLocked(type);
+ if (lockedKind) {
+ warn("Blocking", user.username, amount, hash, user.id, type, "withdraw-lock", lockedKind);
+ fail("External withdrawals temporarily disabled");
+ }
+ }
+
if (frozen || (amount > userLimit && !whitelisted)) {
warn("Blocking", user.username, amount, hash, user.id, type, frozen, userLimit);
fail("Problem sending payment");