-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Summary
Lightning Addresses with purely numeric usernames (e.g., [email protected]) are incorrectly rejected by parsePaymentDestination() with an "Unknown" payment type, causing blink-mobile to display "Please enter a valid destination" error.
Steps to Reproduce
- Open Blink mobile app
- Navigate to Send Bitcoin screen
- Enter
[email protected]as the destination - Tap Next/Send
Expected: The app recognizes this as a valid external Lightning Address and proceeds to the payment flow.
Actual: The app displays "Please enter a valid destination" error.
Root Cause
The issue is in the reUsername regex in src/parsing/index.ts (line 225):
const reUsername = /^(?!\d+$)(?!^(1|3|bc1|lnbc1))[0-9a-z_]{3,50}$/iuThe (?!\d+$) negative lookahead explicitly rejects usernames that are entirely numeric. While this makes sense for Blink intraledger usernames (to avoid confusion with Bitcoin addresses), it incorrectly blocks valid external Lightning Addresses.
Code Flow Analysis
When parsing [email protected]:
getPaymentType()checksutils.parseLightningAddress()fromlnurl-paylibrary- If the domain is in
lnAddressDomains, it callsgetIntraLedgerPayResponse()which validates the username againstreUsername - The regex rejects
254793673300because of(?!\d+$) - For external domains,
getLNURLPayResponse()is called, which also may fall back togetIntraLedgerPayResponse()if the domain matches - Result:
PaymentType.Unknown→ "Please enter a valid destination"
Why This Matters
- Phone number-based usernames are common: Many Lightning Address providers (especially in emerging markets) use phone numbers as usernames
- LUD-16 doesn't prohibit numeric usernames: The Lightning Address specification doesn't restrict username format beyond email-like structure
- Real-world example:
[email protected]is a valid Kenyan phone number-based Lightning Address
Suggested Fix
Option 1: Separate validation for internal vs external Lightning Addresses
Create separate regex patterns:
// For Blink intraledger usernames (keep numeric restriction to avoid BTC address confusion)
const reIntraledgerUsername = /^(?!\d+$)(?!^(1|3|bc1|lnbc1))[0-9a-z_]{3,50}$/iu
// For external Lightning Address usernames (more permissive)
const reExternalLnAddressUsername = /^[0-9a-z_.+-]{1,64}$/iuThen update getIntraLedgerPayResponse() to use reIntraledgerUsername and ensure external Lightning Addresses bypass intraledger validation entirely.
Option 2: Trust lnurl-pay's parsing for external addresses
If utils.parseLightningAddress() successfully parses the address and the domain is NOT in lnAddressDomains, skip the reUsername validation entirely and treat it as a valid LNURL destination:
const lnAddress = utils.parseLightningAddress(destination)
if (lnAddress) {
const { username, domain } = lnAddress
// Only validate against reUsername if it's an internal domain
if (lnAddressDomains.find((lnAddressDomain) => lnAddressDomain === domain)) {
return getIntraLedgerPayResponse({
destinationWithoutProtocol: username,
lnAddressDomains,
destination,
})
}
// External domain - accept as valid LNURL without username validation
return {
valid: true,
paymentType: PaymentType.Lnurl,
lnurl: `${username}@${domain}`,
isMerchant: false,
}
}Test Cases to Add
describe("parsePaymentDestination - Numeric Lightning Addresses", () => {
it("validates an external lightning address with numeric username", () => {
const result = parsePaymentDestination({
destination: "[email protected]",
network: "mainnet",
lnAddressDomains: ["blink.sv"],
})
expect(result).toEqual(
expect.objectContaining({
paymentType: PaymentType.Lnurl,
valid: true,
lnurl: "[email protected]",
isMerchant: false,
}),
)
})
it("validates an external lightning address with phone number username", () => {
const result = parsePaymentDestination({
destination: "[email protected]",
network: "mainnet",
lnAddressDomains: ["blink.sv"],
})
expect(result).toEqual(
expect.objectContaining({
paymentType: PaymentType.Lnurl,
valid: true,
lnurl: "[email protected]",
isMerchant: false,
}),
)
})
// Internal domains should still reject purely numeric usernames
it("rejects internal lightning address with purely numeric username", () => {
const result = parsePaymentDestination({
destination: "[email protected]",
network: "mainnet",
lnAddressDomains: ["blink.sv"],
})
expect(result).toEqual(
expect.objectContaining({
paymentType: PaymentType.Unknown,
valid: false,
}),
)
})
})Environment
- Package:
@blinkbitcoin/blink-clientv0.5.2 - Affected clients: blink-mobile (and any other client using this library)
- File:
src/parsing/index.ts
Related Code
src/parsing/index.ts:225-reUsernameregex definitionsrc/parsing/index.ts:356- Usage ingetPaymentType()src/parsing/index.ts:401- Usage ingetIntraLedgerPayResponse()src/parsing/index.ts:474-491- Lightning Address handling ingetLNURLPayResponse()
cc @dolcalmi