| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
| |
A NIP-47 request whose pubkey/user no longer exists (deleted app or migrated
user) was silently dropped, leaving the client spinning. Send an UNAUTHORIZED
error reply so the client can surface it. Still not logged (not a server fault).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Replace bare `client = null` with dropClient(): stop the clightning-client
library's internal reconnect loop (clear reconnectTimeout, neuter reconnect,
destroy the socket, drop listeners). A dead client was otherwise orphaned and
spun forever retrying `connect ENOENT`, spamming logs and leaking a socket+timer
on every cl restart. Guard the error handler on `c === client` so a stale
client's late error can't drop a newer, healthy one.
Add lnListen.onDrop so the invoice listener recycles immediately when its
long-poll socket dies (bump epoch, re-arm) instead of waiting up to 5 minutes for
the stall watchdog. Stop counting LightningUnavailableError toward the listener's
process.exit escalation, so a cl restart no longer makes the app self-exit — it
retries until cl returns.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The pending reconciliation set is keyed by bolt11, shared across attempts
at the same invoice. A retry made after a prior attempt's CLN parts
resolved (but before check() reconciled its debit) would pass the
listpays-only guard, and the retry's own reverse()/finalize() then
removed the shared entry — permanently hiding the first attempt's debit
from check(), so a failed send was never auto-refunded.
- sendLightning: refuse to pay while the invoice's bolt11 is still in
the pending set; the caller is told to retry shortly.
- check(): an entry whose payment record no longer exists in db or
archive is dropped from the set instead of skipped, so a dead entry
can't block its invoice forever under the new guard.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The house Cashu holdings live as one token under redis `cash`. claim()
and mint() decoded the current proofs, swapped at the mint, then wrote
back — with no lock. Two concurrent claims read the same proofs and the
later write clobbered the earlier, so already-swapped proofs vanished
from the token while both users stayed credited (an insolvency leak,
triggerable via the unauthenticated /ecash/:id callback). Wrap both
read-swap-write sequences in an in-process mutex.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
credit() ran fail("memo too long") on memos >5000 chars, but it runs
AFTER settlement: an incoming lightning/bolt12 deposit has already
advanced pay_index, and an internal send has already debited the sender.
The throw was only logged, replay() hit the same throw, and the sats sat
in the house wallet uncredited. The memo is attacker-controllable and was
uncapped via the lnurl comment param and the bolt12 payer_note.
Truncate the memo in credit() instead of throwing (a clipped note is
harmless), and cap the lnurl comment to the advertised 512 at ingress.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
- pay_keysend never called checkBudget and never recorded to
${pubkey}:payments, so a budget-limited connection string was a
full-drain credential. Budget-check it, honor max_fee, and record the
debit so subsequent budgeted calls count it (S4).
- Replace the non-atomic zScore-then-(unawaited)-zAdd dedup with an
awaited SET NX claim, so two concurrent deliveries of the same signed
event can't both reach handle() and pay twice; also reject
future-dated created_at beyond a small skew (S6).
- Serialize budget-affecting methods per app with an async mutex so a
concurrent request can't read a stale spent-total and overspend (S8).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
The request logger persisted full req.headers to the "req" log,
including the Authorization bearer token and the token cookie — both
non-expiring JWTs, so log-read access became account takeover. Redact
the authorization and cookie headers. Also set httpOnly/secure/sameSite
on the token cookie so it isn't readable by injected JS or sent over
plaintext HTTP. (Revocation is already handled by the evicted set.)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
| |
Registration, password change, and admin reset all hashed at bcrypt
cost 4 (16 rounds, ~1,300 guesses/s/thread) — the single factor gating
the custodial balance and the nip49 passphrase for the stored nostr key.
Raise all three sites to cost 12, and on successful login re-hash any
legacy cost-4 hash now that we hold the verified plaintext.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
The websocket auth gate used jwt.decode(), which parses the payload but
never checks the signature — anyone could forge {id: <victim uid>}
(alg:none or any key), register their socket under the victim's uid, and
receive every emit for that account. Switch both track() and the "login"
gate to jwt.verify(token, config.jwt), rejecting forged/altered tokens.
Also project the full-record socket emits through pick(user, whitelist)
so a "user" event can never carry the bcrypt hash, TOTP secret, or PIN —
matching the shape /login and /me already return.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
The stored replaceable 13194 info event was intermittently disappearing
from the relay, leaving NWC clients (Alby etc.) with "no info event"
until the next hourly existence-check happened to republish it. Drop the
check-then-publish dance and just republish both server info events every
2 minutes — a replaceable event overwrites in place, so it's cheap and
self-heals immediately. Silence the per-publish success log since it now
fires on every interval.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dpqu6wizYe1c1g4k5Gd8JK
|
| |
|
|
|
|
|
|
|
|
|
|
| |
A `migrated` flag on the user, settable through /user and visible on the
public user endpoint. It changes exactly one thing: lnurlp for that
username defers to the names registrar, so payments to [email protected]
arrive in the person's v3 wallet. The account itself is untouched — same
username, same history, still usable here — which is why this is a flag
and not a rename or a shutdown.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Clamp a non-internal receipt's tip to the settled amount so a tip
mutated larger than the amount actually received can't produce a
negative, reversible payment record on the recipient.
Also reject an autowithdraw whose destination resolves back to the
sender: that self-send re-fires completePayment and, since the balance
nets out, never drops below threshold, looping unbounded. The route's
create() already has the equivalent self-send guard; the internal
pay() helper lacked it.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dpqu6wizYe1c1g4k5Gd8JK
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
sendKeysend debited, ran ln.keysend, and on a throw decided whether to
refund by calling listpays({payment_hash: hash}) with the CALLER's id (a
Nostr ev.id on the NWC path). keysend randomizes its own payment hash —
the CLN command takes no preimage arg — so that lookup could never match:
it returned [], `failed` was always true, and reverse() ran on EVERY
throw. A keysend that threw but actually settled was re-credited while
the sats had left the node, a repeatable hot-wallet drain. Nothing
corrected it either: unlike sendLightning, keysends are never enrolled in
`pending`, so check() never revisits them.
Recovering the real hash from the error payload doesn't work — verified
on CLN v26.06.2 that keysend errors carry no payment_hash under any key
(the error is {code, message, attempts}), so that approach never reverses
and instead eats the user's money on every ordinary routing failure.
Instead tag the payment with our own unique label and record where the
sendpays index stands first (`wait sendpays created 0` returns the
current index without blocking), so a throw can list only sendpays
created since — no full scan — and match ours by label whatever the error
looks like. CLN writes the sendpay record BEFORE sending the HTLC, so no
record at all proves nothing left the node and the debit is safe to
reverse. Verified both directions on regtest: an unroutable keysend is
refunded, and a settled one is correctly detected and left alone.
Known gap: a keysend still in flight when the RPC throws stays debited,
since keysends are not enrolled in `pending` for check() to reconcile.
Reported-by: Muhammed Shekho (@SYR-ROOT) <[email protected]>
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Tightens our implementation of the BIP-321-based NWC methods proposed
in https://github.com/nostr-wallet-connect/nwc/pull/2 — the successor
to pay_invoice/make_invoice that clients are asked to transition to
for bolt12 zaps in https://github.com/nostr-protocol/nips/pull/2421:
- reject cross-network payment instructions before paying
(UNSUPPORTED_NETWORK) instead of failing after the debit
- reject unknown req- BIP-321 params, including req-pop since we
can't open proof-of-payment callbacks
- reject a request amount that conflicts with the instruction amount
- payer_note is delivered or rejected, never dropped: bolt11+note is
rejected, internal settlements hand the note over as the payment memo
- receive mints a per-request bolt12 offer carrying the requested
description/amount instead of reusing the standing amountless offer
- get_info reports the node's actual network instead of "mainnet"
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
The zap dispatch parsed the invoice description and, when it wasn't JSON,
suppressed the error only if the message contained "Unexpected" — V8's parse-
error wording. On Bun the message is "Unable to parse JSON string", so every
paid invoice with a non-JSON memo logged a warning. Parse-then-check instead:
a non-JSON description is simply not a zap and is skipped silently; warn only
when an actual kind-9734 zap request fails to process.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
| |
A pubkey can be in `${uid}:apps` before its `app:<pubkey>` record is written
(NWC-connection creation race) or can outlive its record. Destructuring `notify`
off the null lookup threw, and since the catch wraps the whole loop it aborted
notifications for ALL of the user's apps. Skip null/absent apps and continue.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
- invoices: reject caller-supplied bolt11/bolt12; the server always mints its
own. Closes the invoice-registration path behind the shared-node credit and
payment-hijack vectors.
- lightning: skip mint-owned invoices (lbl* label) in the paid-invoice listener.
- payments: lightning free-tier fee credit, withheld for flagged high-throughput
(ln:nofree) accounts which pay an elevated rate; the failed-send reversal
restores credit at the same rate. freezeCheck made crash-resilient. Lightning
and bolt12 sends stay pending until the preimage settles.
- payments (route): validate payment-request / invoice input types.
- logging: collapse repeated xpay failure lines into one.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
createproof's default proof omits invreq_payer_note, and passing include
replaces the default disclosure set — so for zap payments request both
invreq_payer_note and invoice_amount, which NIP-177 verification requires.
Verified end to end on v26.06.2: NWC pay over nip44 returns a proof that
decodes valid with note + amount disclosed and preimage matching the
payment hash.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
- sendLightning fetches an invoice from lno1 offers (payer_note passthrough
for NIP-177 zap intents) and pays it through the existing bolt12 path
- NWC: new nwc#2 pay/receive methods (BIP-321 URIs, payer proofs stubbed
until CLN 26.06 createproof), nip44_v2 alongside nip04 with per-request
scheme detection, dual 23196/23197 notifications, 13194 refreshed when
its content or encryption tag goes stale
- standing per-user bolt12 offer via GET /offer; incoming bolt12 payments
record invreq_payer_note as memo
- parse/fetchinvoice routes handle offers; zap display parses kind 9736
receipts (amount tag + embedded intent) and drops a call to a sync()
helper that never existed
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
POST /preimages now accepts fiat + optional currency (defaults to the
merchant's) as an alternative to a sat price. The fiat amount is stored
per entry and converted to sats at the moment a usePreimage invoice is
created, so the price tracks the exchange rate instead of freezing at
upload time. The invoice also takes on the entry's currency and rate so
receipts display the fiat price exactly as the merchant set it. If no
rate is available for the entry's currency, the preimage is pushed back
onto the queue and creation fails rather than issuing an any-amount
invoice.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Invoice creation is unauthenticated, so buyers could name their own
price for a queued preimage (create the usePreimage invoice with
amount: 1). POST /preimages now takes an optional price (sats) that is
stored per entry and overrides the requested amount when the preimage
is consumed; omitting it keeps the old any-amount behavior. Also gate
the stored-preimage reveal in credit() on the invoice being fully paid,
since internal sends can settle partial amounts. Bare-hex entries
queued before this change still parse (treated as priceless).
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| |\
| |
| |
| |
| | |
# Conflicts:
# lib/payments.ts
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
Merchants POST /preimages (auth) to pre-load 32-byte hex preimages into a
FIFO queue; creating an invoice with usePreimage: true consumes the next
one so the invoice's payment hash is sha256(preimage). Paying the invoice
reveals the merchant-chosen secret to the buyer — via their own wallet on
external settlements, and via GET /invoice/:id once settled — enabling
backend-less digital download stores (encrypted files on static hosting,
preimage + public string = decryption key).
- GET /preimages lists the queue with payment hashes so the merchant can
map files to future invoices; POST /preimages/delete clears it
- empty queue fails invoice creation rather than issuing a random
preimage the buyer would pay for but couldn't decrypt with
- preimages cln rejects as already used are discarded and the next tried
- consumed preimages persist at preimage:${payment_hash} (TTL = invoice
expiry) and credit() prefers them as the settlement ref, so internal
coinos-to-coinos payments reveal the real preimage instead of the
sender's uid
Co-Authored-By: Claude Fable 5 <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
apiv2.nobitex.ir became unreachable from our egress, and it was fetched
inline on every binance tick, so it failed and logged on every tick.
Move the Iranian market-rate fetch onto its own 60s timer with a request
timeout, cache the last-good value (falling back to the fixer rate after
an hour), and throttle failure logging. Switch the source to Wallex
(api.wallex.ir BTCTMN), which is reachable.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
On-chain deposits below 300 sats are dropped by the dust filter in the
confirm/credit path, so a specific-amount bitcoin/liquid invoice under
that threshold can never be credited. Reject creating one at generation
time and point the user at lightning. Any-amount (0) addresses are
unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| | |
| |
| |
| |
| |
| |
| |
| | |
The preHandler request logger wrote the full request body, which included
plaintext passwords on auth requests. Redact password/confirm fields
before logging.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
credit() now credits the tip the sender was actually debited (passed in
by the internal-transfer callers) instead of independently re-reading the
invoice. This closes a time-of-check/time-of-use gap where an invoice's
tip could be changed between the sender debit and the receiver credit,
letting a payer be credited a tip they were never charged.
External receipts (lightning/ecash) keep reading the invoice tip, since
there is no paired coinos debit to diverge from.
Also harden POST /invoice/:id: validate the tip is a sane non-negative
integer and reject tip changes on an already-settled invoice.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
| |
The nobitex endpoint host changed (old host now NXDOMAIN) and the fetch
was swallowed by an empty catch, silently blanking Iranian rates. Point
at the current host, and since the API quotes in Rial despite the IRT
pair name, set IRR from lastTradePrice and IRT = Rial/10. Log fetch
failures instead of ignoring them.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
publishInfo sent the kind-13194 info event over the long-lived NWC socket
fire-and-forget. The nostr lib Relay.send is async (await wait_connected; ws.send)
but was called without await, and success was logged immediately. A send that
rejected (e.g. ws in CLOSING state during a strfry reconnect) was silently
dropped while the log claimed success, leaving NWC clients with "no info event"
until a manual republish.
Publish over a fresh short-lived socket and wait for the relay OK, retrying up to
3x with honest logging (warn on unconfirmed, err on give-up). ensureInfos
receive-based detection is unchanged. Verified on prod: deleted both info events,
restarted, both republished within seconds.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
| |
Emit a greppable EVICTED_AUTH line (username + cf-connecting-ip) when an
evicted account's JWT is used, so the real-time auto-banner can block the
source IP at the edge immediately rather than waiting for the hourly scan.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
| |
Add an `evicted` set check in the JWT strategy: an evicted account fails
authentication on every request and every endpoint, regardless of source
IP. The existing blacklist only blocks sends, so a compromised/abusive
JWT could still authenticate and read/probe; eviction kills it outright.
Matches on uid or username so a rename can't evade it.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
BOLT11 is case-insensitive, but an uppercase-pasted invoice was stored as
the payment hash and the pending-set entry verbatim. check()'s
startsWith("ln") test is case-sensitive, so it treated such pending
entries as non-lightning and evicted them without reversing — a failed
send would never be auto-refunded.
- sendLightning: lowercase the invoice up front so the stored hash, the
pending entry, and xpay all use the same form.
- check(): lowercase before the startsWith("ln") test (defensive; also
rescues any already-stored uppercase entries).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
check() skipped pending payments younger than 10s, but xpay retries for
30s (retry_for: 30). During that window listpays can transiently show all
parts failed before the winning part lands, so check() could reverse a
payment sendLightning was still driving; the part then completed and
finalize() ran against an already-removed record.
- check(): raise the skip threshold 10s -> 60s so sendLightning finishes
finalize()/reverse() and removes the payment from `pending` before
check() ever touches it.
- finalize(): if the record is gone, raise a clear error instead of a
cryptic null-deref.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
| |
generate() unconditionally lPush'd the invoice id to `${aid}:invoices`,
but it's also called from the update() handler (tip/webhook changes, and
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. Only push when
the id isn't already in the list (LPOS guard).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The /lnurl/proxy, /decode and /encode handlers fetched an arbitrary
user-supplied URL via got() and returned the body — an unauthenticated
SSRF reaching internal services. Route those fetches through a new
safeGot() that rejects hosts resolving to loopback/private/link-local/
CGNAT/metadata ranges, re-validates on each redirect hop, restricts the
scheme to http(s), and caps timeout + response size. Also validate the
NIP-57 zap request (kind 9734 + signature) before storing it, instead of
trusting the client-supplied event.
These endpoints can't be removed — they back lightning-address/LNURL
parsing and resolution in the wallet UI.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
| |
After broadcasting an on-chain send via bitcoind, also POST the raw tx to
mempool.space and blockstream (fire-and-forget, non-blocking) so those
explorers see it immediately instead of waiting ~8s for p2p propagation.
Skipped on regtest/testnet.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
| |
getPayment() pre-check in the lightning listener/replay isn't atomic, so
concurrent re-credits of the same settlement each ran incrBy. Claim each
lightning/bolt12 settlement with an atomic SET NX on credited:<ref>
(ref = preimage) and bail if already claimed. Also disable the /replay
route that triggered it.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
| |
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 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The zombie-listener watchdog (bd1ece38) treated "waitanyinvoice blocked > 5min
while cl is alive" as a stall — but that's NORMAL during quiet periods, so it
false-fired and needlessly recycled the cl listen socket every few minutes
(~26x in 6h on 2026-06-07). Receives still worked, but it was churn + noise and
would mask a genuine zombie.
Fix: a blocked long-poll is only a zombie if cl actually has a paid invoice the
listener isn't picking up. When blocked past the threshold, probe cl on the MAIN
socket with waitanyinvoice(pay_index, 2s server-side timeout): if it returns an
invoice there's a real backlog the wedged listener missed -> recycle; if it
times out (CLN code 904 = nothing waiting) the listener is just idle and healthy
-> leave it alone. Only recycle on a confirmed backlog.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Three issues reported by an NWC client integrator (refs #80):
(a) pay_invoice sync reply: the handler polled listpays for ~20s then returned
a misleading "Payment timed out" error on slow routes even though the payment
settled — client never got the preimage. sendLightning already runs xpay to
completion and finalize() sets p.ref = preimage, so return that directly; keep
a longer listpays loop only as a fallback, and on still-in-flight return an
explicit "check with lookup_invoice" rather than implying failure.
(b) rate limiting: raised per-pubkey cap 5/min -> 60/min (normal usage — a
balance check + payment + a few lookup_invoice reconciles — trivially exceeded
5), and over-limit requests now get a NIP-47 RATE_LIMITED error reply instead
of a silent drop that looked exactly like a lost reply. Also: every request
path now always sends a 23195 reply (success, error, or handler-returned-
nothing) so a client never hangs waiting.
(c) notifications: payment_received/payment_sent are advertised in the info
event but app.notify defaulted to false unless the client sent notify:"true"
(only ~1% of apps had it on), so they never fired. Default notify ON; only
disable when explicitly "false". (Existing apps still false — backfill TBD.)
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
double-receive
completePayment logs once when an on-chain deposit is first seen (pending) and
again from /confirm when it confirms, both as bare "received" — so a single
liquid/bitcoin deposit appeared twice in the logs and looked like a double
credit. It never was (the /confirm p.confirmed guard and the seen set prevent
double-crediting); purely misleading log noise. Now logs "receiving (pending)"
on first sight and "received" on confirmation. Instant lightning receives are
confirmed=true so they still log a single "received".
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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 allows the client
to specify the amount (msat) in the request for exactly this case (AlbyHub
supports it; Alby Go and others hit the error). Fall back to the request's
`amount` field when the invoice carries none; invoice amount still takes
precedence when present, and a still-missing amount returns a clear error
instead of NaN. sendLightning/sendInternal already handle amountless
invoices given a valid amount — the bug was only the handler dropping it.
Fixes #83
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
generate() stores `created` in milliseconds (Date.now()), but the NWC
make_invoice response 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 —
e.g. ptcpay's .NET DateTimeOffset, whose max is 253402300799 (issue #84:
"creating lightning invoices via NWC fails ... Valid values are between
-62135596800 and 253402300799"). Convert created to seconds before use.
The other NWC handlers were already correct (lookup_invoice reads seconds
straight from cln; list_transactions divides p.created by 1000). Only
make_invoice leaked milliseconds.
Fixes #84
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
On 2026-06-04 cl restarted (autoheal, after its 15s healthcheck timed out),
and the app's waitanyinvoice long-poll hung on the now-dead listen socket.
listenerActive stayed true forever, so ensureListenerAlive never restarted
it, and incoming lightning payments stopped crediting for ~2h until a manual
app restart. The per-call RPC timeout in ln.ts can't catch this: waitanyinvoice
is intentionally in NO_TIMEOUT (it's meant to block).
Fix: ensureListenerAlive (runs every 120s) now also detects a stalled listener
— active but blocked in waitanyinvoice for >5min. It probes cl via the main
ln socket (getinfo); only if cl is actually alive does it treat the listen
socket as a zombie, call lnListen.reset() (new: drops the proxy's socket so the
next call reconnects), and re-arm. A listenerEpoch guard makes the wedged
invocation bow out when its waitanyinvoice finally rejects, so we never run two
listeners concurrently and never double-credit. Verified with a state-machine
test covering normal receive, quiet-but-healthy (left alone), stall+cl-alive
(recycled), stall+cl-down (left alone), and the no-double-listener race.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
scripts/analytics.ts — streams live db (SCAN, never KEYS) and reports for
a trailing window (default 30d): MAU/WAU, new users, custody total + top
holders, network volume by type (lightning/liquid/bitcoin/internal/bolt12/
fund), most-active and highest-volume users, and bolt12 recipients with a
"regular" flag for the Ocean mining-reward stream.
scripts/analytics-geo.ts — top countries from the request log. Counts only
/login + /register events tied to a username (raw cf-ipcountry volume is
dominated by datacenter/nostr traffic). Surfaces a distinct-IP-per-country
column so proxy/VPN egress is visible, and filters a known credential-test
bot IP. Honest result: US #1, then GB/CA/JP/DE/ES/BR.
lib/register.ts — stamp `created` (ms epoch) and store `country` (CF/
ipregistry code) on new users, so future reports get real signup timing and
geography from the db directly instead of log archaeology. Existing users
predate this; backfill is out of scope.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
NWC clients (Alby Go, Lightning Piggies) fetch the kind 13194 info event
(capability advertisement) before they'll talk to the wallet; without it
they report "no info event" and show an unknown balance. The events went
missing after strfry restarted on 2026-05-29 and stayed gone for days,
because the app only published them once in the relay's on-open handler
and nothing re-asserted them.
13194 is a replaceable event meant to persist, so instead of blindly
republishing, query the relay on connect and publish only the server
pubkeys whose info event is actually missing (ensureInfo). Re-check
hourly so a relay restart that leaves our socket intact also self-heals.
Interval is cleared on close/reconnect alongside the heartbeat to avoid
leaks.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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 — the "stuck spinner past /pay/bob, no error"
symptom. Nothing threw, so the existing reconnect-on-error path never
fired and only an app restart recovered it.
Wrap short-lived calls in a 30s Promise.race timeout (matching
health.ts). A timeout rejects with ETIMEDOUT, now part of isSocketDied,
so the client is dropped and the next call reconnects — no process
restart. Long-polls (waitanyinvoice et al), in-flight payments
(pay/xpay/sendpay/keysend), and bolt12 round-trips (fetchinvoice/
sendinvoice/offer) are exempt: timing those out client-side would risk
leaked debits or break intentional blocking.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
| |
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) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Each successful lightning send was emitting 5 INFO lines:
"<user> sent lightning <amount>" (debit, KEEP)
"paying lightning invoice <pr> <amt> <fee>" (drop — redundant)
"payment completed <id> <preimage>" (drop — implicit from absence of warns)
"refunding fee <maxfee> <fee> <delta> <ref>" (drop — the warn for non-integer
delta already covers anomalies)
"sendLightning outcome <id> = finalized" (drop for "finalized" only —
keep for the recovery-via-fallback
variants since those are diagnostic)
Net: only the "<user> sent lightning <amount>" line fires per healthy send,
matching the other asset types' style. Anything that needed a fallback path
or threw still surfaces via the existing warn/err lines.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Each xpay with failed_parts > 0 that ultimately settled (multi-path
retry — common, not concerning) was firing 3 WARN lines:
"xpay returned (abnormal)"
"xpay returned failed_parts without throwing"
"xpay failed_parts but listpays complete — finalizing"
These are all the SAME successful payment. The presence of failed_parts
just means CLN tried multiple paths before one succeeded. Adam was
seeing noise.
Now:
- Only warn on actually-missing preimage (xpay silently failed).
- Drop the unconditional "without throwing" warn — the failed_parts
branch handles the recovery silently in the success case.
- Drop the "listpays complete — finalizing" warn — that's the success
path, not a concern.
- KEEP warns for the genuinely interesting cases: listpays says failed
(we're about to reverse), listpays says pending (debit left for
check() to reconcile), or finalize itself throws.
Net effect: a healthy multi-path payment now emits only the standard
"sendLightning outcome <id> = finalized-via-listpays-after-failed_parts"
info line instead of 3 WARN lines.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|