diff --git a/src/parsing/index.spec.ts b/src/parsing/index.spec.ts index 8ed5fb0..05072cf 100644 --- a/src/parsing/index.spec.ts +++ b/src/parsing/index.spec.ts @@ -598,7 +598,64 @@ describe("parsePaymentDestination IntraLedger handles", () => { ) }) + it("validates a handle with flag", () => { + const paymentDestination = parsePaymentDestination({ + destination: "Nakamoto+usd", + network: "mainnet", + lnAddressDomains: [], + }) + expect(paymentDestination).toEqual( + expect.objectContaining({ + paymentType: PaymentType.IntraledgerWithFlag, + handle: "Nakamoto", + flag: "usd", + }), + ) + }) + + it("validates a handle with invalid flag", () => { + const paymentDestination = parsePaymentDestination({ + destination: "Nakamoto+btc", + network: "mainnet", + lnAddressDomains: [], + }) + expect(paymentDestination).toEqual( + expect.objectContaining({ + paymentType: PaymentType.Unknown, + valid: false, + }), + ) + }) + + it("validates a handle with flag and bad username", () => { + const paymentDestination = parsePaymentDestination({ + destination: "me+usd", + network: "mainnet", + lnAddressDomains: [], + }) + expect(paymentDestination).toEqual( + expect.objectContaining({ + paymentType: PaymentType.Unknown, + valid: false, + }), + ) + }) + it("validates an http handle", () => { + const paymentDestination = parsePaymentDestination({ + destination: "https://some.where/userName", + network: "mainnet", + lnAddressDomains: ["some.where"], + }) + expect(paymentDestination).toEqual( + expect.objectContaining({ + handle: "userName", + paymentType: PaymentType.Intraledger, + }), + ) + }) + + it("validates an http handle with invalid domain", () => { const paymentDestination = parsePaymentDestination({ destination: "https://some.where/userName", network: "mainnet", @@ -612,4 +669,19 @@ describe("parsePaymentDestination IntraLedger handles", () => { }), ) }) + + it("validates an http handle with flag", () => { + const paymentDestination = parsePaymentDestination({ + destination: "https://some.where/userName+usd", + network: "mainnet", + lnAddressDomains: ["some.where"], + }) + expect(paymentDestination).toEqual( + expect.objectContaining({ + paymentType: PaymentType.IntraledgerWithFlag, + handle: "userName", + flag: "usd", + }), + ) + }) }) diff --git a/src/parsing/index.ts b/src/parsing/index.ts index dd0df35..57fca61 100644 --- a/src/parsing/index.ts +++ b/src/parsing/index.ts @@ -75,6 +75,7 @@ export const getHashFromInvoice = ( export const PaymentType = { Lightning: "lightning", Intraledger: "intraledger", + IntraledgerWithFlag: "intraledgerWithFlag", Onchain: "onchain", Lnurl: "lnurl", NullInput: "nullInput", @@ -158,10 +159,16 @@ export type OnchainPaymentDestination = invalidReason: InvalidOnchainDestinationReason } +export const IntraledgerFlag = { + Usd: "usd", +} as const + export const InvalidIntraledgerReason = { WrongDomain: "WrongDomain", } as const +export type IntraledgerFlag = (typeof IntraledgerFlag)[keyof typeof IntraledgerFlag] + export type InvalidIntraledgerReason = (typeof InvalidIntraledgerReason)[keyof typeof InvalidIntraledgerReason] @@ -171,6 +178,12 @@ export type IntraledgerPaymentDestination = paymentType: typeof PaymentType.Intraledger handle: string } + | { + valid: true + paymentType: typeof PaymentType.IntraledgerWithFlag + handle: string + flag: IntraledgerFlag + } | { valid: false paymentType: typeof PaymentType.Intraledger @@ -205,6 +218,8 @@ export const decodeInvoiceString = ( return bolt11.decode(invoice, parseBolt11Network(network)) } +const reUsername = /(?!^(1|3|bc1|lnbc1))^[0-9a-z_]{3,50}$/iu + // from https://github.com/bitcoin/bips/blob/master/bip-0020.mediawiki#Transfer%20amount/size const reAmount = /^(([\d.]+)(X(\d+))?|x([\da-f]*)(\.([\da-f]*))?(X([\da-f]+))?)$/iu const parseAmount = (txt: string): number => { @@ -305,10 +320,21 @@ const getPaymentType = ({ ] : destinationWithoutProtocol - if (handle?.match(/(?!^(1|3|bc1|lnbc1))^[0-9a-z_]{3,50}$/iu)) { + if (handle?.match(reUsername)) { return PaymentType.Intraledger } + const handleAndFlag = handle?.split("+") + if ( + handleAndFlag?.length === 2 && + handleAndFlag[0].match(reUsername) && + Object.values(IntraledgerFlag).includes( + handleAndFlag[1].toLowerCase() as IntraledgerFlag, + ) + ) { + return PaymentType.IntraledgerWithFlag + } + return PaymentType.Unknown } @@ -321,8 +347,6 @@ const getIntraLedgerPayResponse = ({ destination: string lnAddressDomains: string[] }): IntraledgerPaymentDestination | UnknownPaymentDestination => { - const paymentType = PaymentType.Intraledger - const handle = destinationWithoutProtocol.match(/^(http|\/\/)/iu) ? destinationWithoutProtocol.split("/")[ destinationWithoutProtocol.split("/").length - 1 @@ -334,21 +358,36 @@ const getIntraLedgerPayResponse = ({ if (!lnAddressDomains.find((lnAddressDomain) => lnAddressDomain === domain)) { return { valid: false, - paymentType, + paymentType: PaymentType.Intraledger, handle, invalidReason: InvalidIntraledgerReason.WrongDomain, } } } - if (handle?.match(/(?!^(1|3|bc1|lnbc1))^[0-9a-z_]{3,50}$/iu)) { + if (handle?.match(reUsername)) { return { valid: true, - paymentType, + paymentType: PaymentType.Intraledger, handle, } } + const handleAndFlag = handle?.split("+") + const flag = handleAndFlag[1]?.toLowerCase() + if ( + handleAndFlag?.length === 2 && + handleAndFlag[0].match(reUsername) && + flag === IntraledgerFlag.Usd + ) { + return { + valid: true, + paymentType: PaymentType.IntraledgerWithFlag, + handle: handleAndFlag[0], + flag, + } + } + return { valid: false, paymentType: PaymentType.Unknown, @@ -564,7 +603,6 @@ export const parsePaymentDestination = ({ destinationWithoutProtocol, rawDestination: destination, }) - switch (paymentType) { case PaymentType.Lnurl: return getLNURLPayResponse({ @@ -576,6 +614,7 @@ export const parsePaymentDestination = ({ case PaymentType.Onchain: return getOnChainPayResponse({ destinationWithoutProtocol, network }) case PaymentType.Intraledger: + case PaymentType.IntraledgerWithFlag: return getIntraLedgerPayResponse({ destinationWithoutProtocol, destination,