aboutsummaryrefslogtreecommitdiffstats
Commit message (Collapse)AuthorAgeFilesLines
* invoices: don't re-list an invoice id on update (dedupe :invoices)stagingAdam Soltys2026-06-182-1/+93
| | | | | | | | | | | | | | | | Port of the master fix (2d2f9856) + cleanup script to staging. generate() unconditionally lPush'd the invoice id to `${aid}:invoices`, but it's also called from update() (tip/webhook changes, repeated bolt12 offer creation) re-using the existing id — so each update re-appended the same id and the user's :invoices accumulated duplicate entries, rendering the same offer/invoice multiple times. Guard the push with LPOS so it only adds the id the first time. Also add scripts/dedup-invoices.ts to scrub already-accumulated dupes (dry-run by default, --apply to write, idempotent). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* lightning: watchdog only recycles on real backlog, not normal idleAdam Soltys2026-06-171-10/+26
| | | | | | | | | | | | | Port of master 7b71c96b, refining the zombie-listener watchdog staging adopted in 7c34b431. Treating "waitanyinvoice blocked > 5min while cl is alive" as a stall false-fires during normal quiet periods (~26x in 6h on master 2026-06-07) and needlessly recycles the cl listen socket. Instead, when blocked past the threshold, probe cl on the MAIN socket with waitanyinvoice(pay_index, 2s server-side timeout): an invoice returned means a real backlog the wedged listener missed -> recycle; a timeout (CLN 904) means the listener is just idle -> leave it alone. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* ln: time out short-lived RPCs so a hung cl reconnects without restartAdam Soltys2026-06-171-2/+64
| | | | | | | | | | | | | Port of master a664af99. A unix-domain RPC socket has no TCP keepalive, so if cl goes unresponsive while the socket stays open (zombie/half-open) an awaited call hangs forever — nothing throws, the reconnect-on-error path never fires, and only an app restart recovers. Wrap short-lived calls in a 30s Promise.race timeout (matching lib/health.ts); a timeout rejects with ETIMEDOUT, now part of isSocketDied, so the client is dropped and the next call reconnects. Long-polls, in-flight payments (pay/xpay/sendpay/keysend) and bolt12 round-trips are exempt to avoid leaked debits. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* nwc: fix pay_invoice reply reliability, rate-limit drops, always replyAdam Soltys2026-06-171-42/+84
| | | | | | | | | | | | | | | | | | | | | | Port of master 8cb18821 (refs #80), adapted to staging's nwc event loop. (a) pay_invoice: sendLightning runs xpay to completion and finalize() sets p.ref = preimage, so return that directly instead of polling listpays and racing a 20s timeout that returned a misleading "Payment timed out" even when the payment settled. Keep a longer listpays loop as a fallback; on still-in-flight return an explicit "check with lookup_invoice". (b) rate limiting: raised per-pubkey cap 5 -> 60/min, and over-limit requests now get a NIP-47 RATE_LIMITED reply instead of a silent drop that looked like a lost reply. Every request path now always sends a 23195 reply (success, error, or handler-returned-nothing) so a client never hangs. Pulled the reply/encrypt logic into a reply() helper. The notify default-on part of 8cb18821 is intentionally NOT ported: master 10284aa1 reverted it to opt-in, which is already staging's behavior (notify = String(notify) === "true"). Co-Authored-By: Claude Opus 4.8 <[email protected]>
* nwc: publish 13194 info event only when missing, recheck hourlyAdam Soltys2026-06-171-26/+65
| | | | | | | | | | | | | | Port of master 6372d918, adapted to staging's nwc relay loop (no heartbeat here). NWC clients fetch the kind 13194 info event before they talk to the wallet; without it they show "no info event" and an unknown balance. 13194 is replaceable and meant to persist, so instead of blindly republishing on every connect, query the relay (ensureInfo) and publish only the server pubkeys whose event is actually missing. Re-check hourly so a relay restart that leaves our socket intact self-heals; interval is cleared on close/reconnect. Verified [email protected] Relay exposes ws/ readyState, on("eose") and unsubscribe. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* nwc: pay_invoice honors request amount for amountless bolt11Adam Soltys2026-06-171-2/+14
| | | | | | | | | | | Port of master ce9616f6. pay_invoice derived the amount solely from the decoded invoice, so an amountless bolt11 yielded amount = round(undefined/ 1000) = NaN and sendLightning rejected it with "Invalid amount". NIP-47 lets the client specify the amount (msat) in the request; fall back to it when the invoice carries none, with a clear error if still missing. Fixes #83. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* nwc: fix make_invoice timestamps (ms -> seconds) breaking NWC clientsAdam Soltys2026-06-171-1/+8
| | | | | | | | | | | Port of master 0154bb06. generate() stores `created` in milliseconds, but NWC make_invoice returned it directly as created_at and computed expires_at = created_at + expiry. NWC timestamps are Unix SECONDS (NIP-47), so clients parsed a ~1.78e12 expires_at that overflowed their date types (ptcpay's .NET DateTimeOffset, issue #84). Convert created to seconds. Fixes #84. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* analytics: missing-history audit + lightning->liquid arb finderAdam Soltys2026-06-172-0/+232
| | | | | | | | | | | Port of master be896868. scripts/audit-missing-history.ts flags users whose payment history has gaps vs their recorded balance. scripts/ ln-liquid-arb.ts surfaces lightning->liquid rebalancing opportunities. Note: written against the master stack (Redis balances); on staging balances live in TigerBeetle and may need the balance source adjusted. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* analytics: monthly insight reports + signup instrumentationAdam Soltys2026-06-173-0/+426
| | | | | | | | | | | | | | | | Port of master 183a3cb0 + 3db24424. scripts/analytics.ts streams live db (SCAN, never KEYS) and reports a trailing window: MAU/WAU, new users, custody total + top holders, network volume by type, most-active/highest- volume users, bolt12 recipients; --csv flag for month-over-month trends. scripts/analytics-geo.ts reports top countries from the request log. lib/register.ts stamps `created` and stores `country` on new users so reports get signup timing/geography from the db directly. Note: these scripts were written against the master stack; on staging balances live in TigerBeetle, so custody/holder figures may need the balance source adjusted from the Redis balance: keys. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* finalize: don't crash on non-integer fee delta (tbRefund NaN)Adam Soltys2026-06-171-3/+23
| | | | | | | | | | | | Port of master 8aba0bc2, adapted to this stack's tbRefund. When xpay's response shape is unexpected (or a zero-amount invoice's decoded amount_msat is undefined) the computed p.fee becomes NaN, and the refund delta crashes the balance write, leaving finalize partial (user loses up to maxfee). Validate the computed fee is a finite integer; if not, keep the originally-reserved maxfee (zero refund, no balance corruption) and warn. Same guard on the refund delta before tbRefund. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* sendLightning/sendKeysend: instrument exit paths, tighten LEAKED DEBIT guardAdam Soltys2026-06-171-12/+111
| | | | | | | | | | | | | | Port of master 51107ed4 + fe767002 + cb0f1962 (combined) to the staging stack. Track an outcome string through every exit path, handle the xpay failed_parts branch explicitly (verify with listpays then finalize/ reverse/leave-pending), and dump abnormal xpay/keysend response shapes. Leak guard re-reads the db record before firing: if ref is persisted the payment is settled regardless of which path threw (finalize may set ref then throw on the later tbRefund), so only a genuinely missing ref logs "LEAKED DEBIT". Uses warn (no warnThrottled on this stack) and keeps the existing retry_for: 20. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* withdraw-lock: refuse external sends when /locks/<type>.locked is presentAdam Soltys2026-06-171-0/+25
| | | | | | | | | | | Port of master 9142b4db. Out-of-band emergency stop: nobal.sh (or a human) creates a sentinel file on the host, bind-mounted at /locks in the container; 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 can't disable it. Internal sends are exempt. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* users: add authenticated self-delete endpointAdam Soltys2026-06-172-0/+105
| | | | | | | | | | | | Port of master 2548f757, adapted from Express to this stack's Hono handlers (c.get("user"), c.req.json(), setCookie, bail) and TigerBeetle balances (getBalance/getPending instead of the Redis balance:/pending: keys). POST /delete (auth) wipes every key tied to the calling user: user records, payments/invoices, sub-accounts, NWC apps, nostr follow graph + nip5. Gated by typing the username and refuses while balance+pending > 10k sats so funds can't be destroyed by accident. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* payments: freeze matches uid as well as username (rename-proof)Adam Soltys2026-06-171-1/+6
| | | | | | | | | Port of master 43427afe. The blacklist (send freeze) only matched the current username, so a compromised account could shake the freeze by renaming itself. Match on either the lowercased username OR the immutable uid; add the uid to the blacklist set to freeze durably. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* payments: guard /replay double-credit race with SET NX idempotencyAdam Soltys2026-06-172-1/+16
| | | | | | | | | | | | Port of master ca28c19f to the staging stack. The getPayment() pre-check in the lightning listener/replay isn't atomic, so concurrent re-credits of the same settlement each ran the credit path. Claim each lightning/bolt12 settlement with an atomic SET NX on credited:<ref> (ref = preimage) and bail if already claimed. Guard sits before tbCredit and the pointer writes so a duplicate bails cleanly. Also disable the /replay route that triggered it. Co-Authored-By: Claude Opus 4.8 <[email protected]>
* ark: skip refresh when ark unconfigured; document LN listener watchdogAdam Soltys2026-06-152-0/+4
| | | | | | | | Guard refreshArkWallet against running when config.ark.arkPrivateKey is unset, and add a comment explaining the ensureListenerAlive watchdog's zombie-socket recovery. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* ln/lightning: watchdog recycles zombie receive-listener socketAdam Soltys2026-06-083-1/+42
| | | | | | | | | | | | | | | | | | Port of master bd1ece3 to staging. When cl restarts (e.g. autoheal after a healthcheck timeout), the app's waitanyinvoice long-poll can hang on the now-dead listen socket: listenerActive stays true forever, nothing restarts it, and incoming lightning payments silently stop crediting until a manual app restart. The per-call RPC timeout can't catch this — waitanyinvoice is meant to block. ensureListenerAlive (run every 120s) now also detects a stalled listener — active but blocked in waitanyinvoice for >5min — probes cl via the main socket (getinfo), and only if cl is actually alive treats the listen socket as a zombie: bumps a listenerEpoch, calls lnListen.reset() (new: drops the proxy socket so the next call reconnects), and re-arms. The epoch guard makes the wedged invocation bow out when its waitanyinvoice finally rejects, so we never run two listeners or double-credit. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* bump core imageAdam Soltys2026-06-072-2/+2
|
* getAddressType: surface lq downtime as actionable errorAdam Soltys2026-05-171-1/+11
| | | | | | | | | | | When lq.getAddressInfo throws on a connection error (ECONNREFUSED / ETIMEDOUT / "Unable to connect"), the generic catch was reporting "unrecognized address" — misleading for both users (who think their address is wrong) and operators (who can't tell user error from infrastructure problems in the logs). Map connection errors to a specific "liquid temporarily unavailable" message instead. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* handle socket-level errors that bypass Bun's JS error pathAdam Soltys2026-05-171-0/+38
| | | | | | | | | | | | | | | | | Two related defenses around the CLN unix socket connection: - Global net.Socket.prototype.connect patch: when Bun's net.createConnection is called on a non-existent unix socket, the ENOENT it throws bypasses all JS error handlers and crashes the process. existsSync-check the path first; if missing, emit a normal 'error' event so the caller can handle it gracefully. - lightningProxy: existsSync gate before constructing LightningClient, plus a client.on("error") listener that clears the cached client on socket-died errors instead of letting the unhandled event take down the process. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* don't remove orphan payment ids from user payment listsAdam Soltys2026-05-171-6/+4
| | | | | | | | | | When the underlying payment record is missing, removing the id from the user's payments list was destructive — a transiently unavailable record (arc fallback miss, database hiccup) would cause us to permanently drop the pointer. Leave the orphan in place so a back-fill from an RDB backup can restore the record under the same id later. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* nwc: handle stale user record and skip reverted-duplicate recordsAdam Soltys2026-05-171-0/+2
| | | | | | | | | | - fail loudly when an NWC request references an app whose user record no longer exists (migrated/deleted account); previously crashed inside handle() on `user.id` - filter `revertedDuplicate` payments out of list_transactions so clients never see the doubled records that were later reverted Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix lnurl description_hash mismatch when sender provides a commentAdam Soltys2026-05-171-2/+6
| | | | | | | | | | | | | | | The bolt11 description_hash is computed from the metadata JSON, which was being constructed with the sender's `comment` substituted into text/plain. Wallets verify description_hash against the metadata they received in the lnurl-pay response, so injecting the comment into the hash made the verification fail — the invoice would settle but description_hash verification would reject it as inconsistent. Keep metadata canonical ("Paying user@host") so the description_hash the wallet receives matches what they verify against. The comment is still preserved as the human-readable memo on the invoice record. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* verifyRecaptcha: check x-forwarded-host before host headerAdam Soltys2026-05-171-1/+1
| | | | | | | | | Tor users access via .onion through a forwarding layer that puts the real hostname in x-forwarded-host while the literal host header is the internal target. Without checking the forwarded header first, captcha prompted for .onion visitors who should have been exempt. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* mirror payment records to arc + atomic /confirm lock to prevent double-creditsAdam Soltys2026-05-172-4/+35
| | | | | | | | | | | | | | | | | | | | | Apr 29 double-credit prevention, ported from master 0e368dab and adapted to staging's hono/tigerbeetle structure. Two parallel callers of /confirm (walletnotify, catchUp, bulk sweeps) could each read p.confirmed=false and each run tbConfirm — doubling the pending→balance transfer. tbConfirm itself isn't idempotent on its own, so the race materializes as a real double-credit even with TB. - routes/payments.ts: SET NX EX 60 lock around the confirmation block keyed by `confirmlock:${txid}:${vout}`. Concurrent callers short-circuit cleanly. Released on success and on every early-return. - lib/payments.ts credit() + routes/payments.ts /confirm: mirror payment + invoice records and the `payment:<txid>:<vout>` / `payment:<hash>` pointer keys into arc as soon as they're written. gf() will then find the prior credit via arc fallback even if the main-db copy is wiped by a future data-loss event. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* remove /gateway endpointAdam Soltys2026-05-172-9/+0
| | | | | | | | | | | Unauthenticated arbitrary db.set abuse vector. Exploited on the master deployment to inflate user balances and override safety controls in real time, leading to a hot-wallet drain. Route and handler both fully removed; if a webhook-by-short-channel-id mapping is ever needed again it must be rebuilt with auth + SCID format validation + webhook URL validation + per-user ownership check. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* sendLightning / sendKeysend: only reverse when CLN reports the payment failedAdam Soltys2026-05-171-2/+26
| | | | | | | | | | | | | | | | | | | | | The catch blocks for ln.xpay and ln.keysend reversed the user's debit whenever the RPC call threw, regardless of the actual payment state in CLN. If the RPC errored transiently while the payment was still pending, the user's balance got refunded — and then the lightning payment could still complete, leaving the user with both the refund and the outgoing sats. Both call sites now mirror the safe check() logic: const failed = !pays.length || pays.every((p) => p.status === "failed"); if (completed) finalize ... else if (failed) reverse ... // else (pending) leave the debit; check() reconciles when CLN settles. If listpays itself errors, log via warn() and skip the reverse — the debit stays in the pending set and check() will retry. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat: blocked address protection, LNURL proxy, replay timestampsAdam Soltys2026-03-275-1/+144
| | | | | | | | | | - Add lib/changeid.ts for in-process account rekeying - Block sends to addresses in redis blocked_addresses set, auto-rekey on attempt - Add created param to credit() for accurate replay timestamps - Add paid_at to replay() for correct payment dates - Add /lnurl/proxy endpoint for server-side LNURL callback fetching Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* verify ark amountsAdam Soltys2026-03-225-21/+94
|
* remove USER bun from base image, remove unused DockerfileAdam Soltys2026-03-192-10/+0
| | | | | | | App and other containers need root access for unix sockets and file permissions. All derived images already override USER as needed. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* postgres dockerfileAdam Soltys2026-03-191-0/+0
|
* dockerfilesAdam Soltys2026-03-1912-2/+154
|
* don't republishAdam Soltys2026-03-181-3/+7
|
* stop republishing external events and add NWC rate limitingAdam Soltys2026-03-183-5/+15
| | | | Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* quieter logsAdam Soltys2026-03-172-14/+13
|
* handle ark dustAdam Soltys2026-03-173-5/+6
|
* fix: add account ownership checks and harden PIN validationAdam Soltys2026-03-173-3/+22
| | | | | | | | - Validate account ownership (aid) before allowing ark send/sync/vault operations and in debit/sendOnchain as defense-in-depth - Harden PIN check to reject missing/empty pin when user has one set Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* ui envAdam Soltys2026-03-171-0/+5
|
* use -w flagAdam Soltys2026-03-171-15/+15
|
* disk space logAdam Soltys2026-03-171-1/+1
|
* staging checkoutAdam Soltys2026-03-172-4/+1
|
* setup scriptAdam Soltys2026-03-173-8/+61
|
* prompt compose overwriteAdam Soltys2026-03-172-6/+17
|
* update readmeAdam Soltys2026-03-172-48/+77
|
* streamline dev setup and docker imagesAdam Soltys2026-03-1611-282/+1026
| | | | | | | | | | | | | | | | | | | - Rewrite setup.sh: auto-installs Docker, downloads regtest snapshot, pulls images, starts all services. Idempotent, works on fresh Ubuntu/macOS. - Create coinos-base image (196MB) shared by all containers: bitcoin, elements, lightning, strfry, kvrocks, electrs, tigerbeetle, chopsticks. Total pull ~490MB, down from ~4.3GB. - Move all images to ghcr.io/coinos/* namespace. - Drop nutshell (mint/wallet), postgres, mon from default dev compose. - Comment out ark stack (optional), guard ark.ts against empty config. - Add missing sampledata: lightningc, strfry, kvrocks, valkey configs. - Strip strfry binary (145MB debug symbols removed). - Use single CLN version (v25.12.1) for all 3 lightning nodes. - Switch valkey to alpine variant (110MB → 41MB). - Update config.ts.sample with correct service URLs. - Regtest snapshot hosted on GitHub Releases (~2MB). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* quieter logsAdam Soltys2026-03-166-35/+40
|
* update migrate scriptAdam Soltys2026-03-164-300/+1783
|
* conditional lnbAdam Soltys2026-03-161-1/+1
|
* cln migrateAdam Soltys2026-03-152-0/+300
|
* skip bolt12 integration tests gracefully when Lightning unavailableAdam Soltys2026-03-151-23/+29
| | | | | | | Adds timeout to exec helper and catches setup failures so tests skip instead of hanging for 60s when Docker Lightning nodes aren't reachable. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>