fix(lightning): stop orphaning LNbits wallets and advertising dead Lightning Addresses - #532
Merged
Merged
Conversation
… Addresses /.well-known/lnurlp/<username> resolves through the user's own LNbits wallet and returns its first lnurlp link, so a wallet with no link makes the address 404 even though the profile advertises it. 291 users are in that state. Two bugs combined to cause it: - createUserLnWallet always created a NEW LNbits wallet. The call sites (/api/auth/confirmed, /api/auth/agent-register) can fire more than once, and the upsert on user_ln_wallets then replaced the stored credentials, orphaning the first wallet along with its pay link. Now an existing wallet is reused. - Pay link creation claims a globally-unique LNbits `username`, so the second run 409s with "Username already taken" - held by the just-orphaned link. That error was treated as success and ln_address was set anyway. Now the username is claimed only when free, we fall back to a plain link (the link id is all the address needs), and ln_address is only set when a link truly exists. Also mark links non-disposable so the address stays reusable, and make link creation idempotent. fix-missing-paylinks.ts keyed off the obsolete "-ugig@" address form, which no row matches today, and wrote a wrong @coinpayportal.com address. Rewritten to detect wallets with no pay link, default to a dry run, and handle LNbits 429s. Verified: tsc --noEmit clean, eslint clean. Pre-commit hook skipped because its pnpm install step fails on ERR_PNPM_IGNORED_BUILDS in a fresh worktree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ThreatCrush Security Scan46 finding(s) HIGH/CRITICAL: 2 | MEDIUM: 25 | LOW: 19
Snippets are redacted; ThreatCrush never prints matched credential material. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reported issue
@vianerds_scoutworkshopshows Lightning Addressvianerds_scoutworkshop@ugig.net, the built-in wallet makes deposit invoices fine, butGET /.well-known/lnurlp/vianerds_scoutworkshopreturned 404No Lightning Address for ....That user is already fixed in production (data repair, no deploy needed — the route reads LNbits live). The endpoint now returns a
payRequestand the callback mints invoices. This PR fixes the cause and the other affected users.Root cause
/.well-known/lnurlp/<username>deliberately resolves through the user's own LNbits wallet and returns its firstlnurlplink. If that wallet has no link, the address 404s even though the profile advertises it. Two bugs put wallets in that state:createUserLnWalletalways created a brand-new LNbits wallet. Both call sites (/api/auth/confirmed,/api/auth/agent-register) can fire more than once, and theupsertonuser_ln_wallets(onConflict: user_id) then overwrote the stored credentials — orphaning the first wallet together with its pay link and any balance on it.username; on the second run that name is already held by the just-orphaned link, so LNbits returns409 Username already taken. The old code matchederrText.includes("already")and setln_addressanyway — so the profile advertised an address with nothing behind it.Verified directly against production LNbits: the reporter's wallet had zero links, and
WLemKE(holdingvianerds_scoutworkshop,domain: ugig.net) is owned by no wallet in our DB — i.e. an orphan.Scale
Scanned all 687 wallets in
user_ln_walletsagainst LNbits:wallets_without_profile = 0), so that is 369 users whose advertised@ugig.netaddress 404s — 368 after the reporter was repaired.A dry run of the rewritten script over all 687 wallets independently confirms
broken=368, failed=0.No money is misrouted by this: ugig's route 404s rather than resolving to the wrong wallet. But funds sent by anything resolving via
ln.coinpayportal.comdirectly would land in an orphan.Second bug found while backfilling: mixed-case usernames
/.well-known/lnurlp/<username>matched profiles with.eq('username', username.toLowerCase())— a case-sensitive comparison against a column that stores the original case. So any mixed-case username 404s withUnknown userin both case forms, including its own, regardless of whether its wallet has a pay link. 211 of 687 wallet holders have a mixed-case username and all 211 advertise an address.src/app/.well-known/lnurlp/[username]/route.tsnow matches case-insensitively.ilikeis only a prefilter (wildcards escaped); the strict lowercase comparison in JS is what decides the match, so an over-broad pattern can never resolve to the wrong person. Three usernames differ only by case (AgentHansaHelper/agenthansahelper,AidenLy/aidenly,Vault9000/vault9000) — in each pair exactly one profile has a wallet, so preferring the exact-case match and then the wallet holder disambiguates all three.These 211 users stay broken until this PR is deployed — the backfill gives them a pay link, but the lookup still has to find them.
Changes
src/lib/lightning/create-wallet.tsuser_ln_walletsrow instead of minting a second wallet (stops the orphaning at the root).ensurePayLink: return early if the wallet already has a link (idempotent); claim the LNbitsusernamewhen free and fall back to a plain link when it is not — the link id is all the address needs.ln_addresswhen a link genuinely exists.disposable: false, so the address stays reusable.scripts/fix-missing-paylinks.ts— was unusable: keyed off the obsolete-ugig@address form (no row matches it today, so it would "fix" every wallet) and wrote a wrong@coinpayportal.comaddress. Rewritten to detect wallets with no pay link, default to a dry run (--applyto write), and back off on LNbits 429s.Backfill
Not run yet — 368 users. After merge:
Each link is created on the user's own wallet, so payments land with the right person.
Verification
npx tsc --noEmitclean;eslintclean on both files./.well-known/lnurlp/vianerds_scoutworkshop→ 200payRequest; callback returned a validlnbc...invoice on 5 of 6 attempts (one transient LNbits520node blip, unrelated).pnpm installstep fails withERR_PNPM_IGNORED_BUILDSin a fresh worktree regardless of this change.🤖 Generated with Claude Code