-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add merchant qr content support
- Loading branch information
Showing
7 changed files
with
269 additions
and
5 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from "./parsing/index" | ||
export * from "./parsing/merchants" |
This file contains 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
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import type { Network } from "./types" | ||
import { convertMerchantQRToLightningAddress } from "./merchants" | ||
|
||
describe("convertMerchantQRToLightningAddress", () => { | ||
// Test cases for valid QR contents and networks | ||
test.each([ | ||
{ | ||
description: "PicknPay EMV QR code on mainnet", | ||
qrContent: | ||
"00020129530023za.co.electrum.picknpay0122RD2HAK3KTI53EC/confirm520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2", | ||
network: "mainnet" as Network, | ||
expected: | ||
"00020129530023za.co.electrum.picknpay0122RD2HAK3KTI53EC%2Fconfirm520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2@cryptoqr.net", | ||
}, | ||
{ | ||
description: "PicknPay EMV QR code on signet", | ||
qrContent: | ||
"00020129530023za.co.electrum.picknpay0122RD2HAK3KTI53EC/confirm520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2", | ||
network: "signet" as Network, | ||
expected: | ||
"00020129530023za.co.electrum.picknpay0122RD2HAK3KTI53EC%2Fconfirm520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2@staging.cryptoqr.net", | ||
}, | ||
{ | ||
description: "Ecentric EMV QR code on mainnet", | ||
qrContent: | ||
"00020129530019za.co.ecentric.payment0122RD2HAK3KTI53EC/confirm520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2", | ||
network: "mainnet" as Network, | ||
expected: | ||
"00020129530019za.co.ecentric.payment0122RD2HAK3KTI53EC%2Fconfirm520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2@cryptoqr.net", | ||
}, | ||
{ | ||
description: "PicknPay QR code with uppercase content", | ||
qrContent: | ||
"00020129530023ZA.CO.ELECTRUM.PICKNPAY0122RD2HAK3KTI53EC/CONFIRM520458125303710540115802ZA5916CRYPTOQRTESTSCAN6002CT63049BE2", | ||
network: "mainnet" as Network, | ||
expected: | ||
"00020129530023ZA.CO.ELECTRUM.PICKNPAY0122RD2HAK3KTI53EC%2FCONFIRM520458125303710540115802ZA5916CRYPTOQRTESTSCAN6002CT63049BE2@cryptoqr.net", | ||
}, | ||
{ | ||
description: "Ecentric QR code with mixed case", | ||
qrContent: | ||
"00020129530019Za.Co.EcEnTrIc.payment0122RD2HAK3KTI53EC/confirm520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2", | ||
network: "mainnet" as Network, | ||
expected: | ||
"00020129530019Za.Co.EcEnTrIc.payment0122RD2HAK3KTI53EC%2Fconfirm520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2@cryptoqr.net", | ||
}, | ||
{ | ||
description: "PicknPay QR code with Unicode characters", | ||
qrContent: | ||
"00020129530023za.co.electrum.picknpay0122RD2HAK3KTI53EC/confirm★測試520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2", | ||
network: "mainnet" as Network, | ||
expected: | ||
"00020129530023za.co.electrum.picknpay0122RD2HAK3KTI53EC%2Fconfirm%E2%98%85%E6%B8%AC%E8%A9%A6520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2@cryptoqr.net", | ||
}, | ||
{ | ||
description: "Ecentric QR code with emoji", | ||
qrContent: | ||
"00020129530019za.co.ecentric.payment0122RD2HAK3KTI53EC/confirm🎉test520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2", | ||
network: "mainnet" as Network, | ||
expected: | ||
"00020129530019za.co.ecentric.payment0122RD2HAK3KTI53EC%2Fconfirm%F0%9F%8E%89test520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2@cryptoqr.net", | ||
}, | ||
])("$description", ({ qrContent, network, expected }) => { | ||
const result = convertMerchantQRToLightningAddress({ qrContent, network }) | ||
expect(result).toBe(expected) | ||
}) | ||
|
||
// Test cases for invalid QR contents | ||
test.each([ | ||
{ | ||
description: "non-matching merchant in EMV format", | ||
qrContent: | ||
"00020129530023other.merchant.code0122RD2HAK3KTI53EC/confirm520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2", | ||
network: "mainnet" as Network, | ||
}, | ||
{ | ||
description: "empty QR content", | ||
qrContent: "", | ||
network: "mainnet" as Network, | ||
}, | ||
{ | ||
description: "malformed EMV QR format", | ||
qrContent: "000201za.co.picknpay", | ||
network: "mainnet" as Network, | ||
}, | ||
{ | ||
description: "invalid merchant identifier", | ||
qrContent: "Nakamoto+btc", | ||
network: "mainnet" as Network, | ||
}, | ||
{ | ||
description: "invalid merchant identifier in EMV format", | ||
qrContent: | ||
"00020129530023za.co.unknown.merchant0122RD2HAK3KTI53EC/confirm520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2", | ||
network: "mainnet" as Network, | ||
}, | ||
])("returns null for $description", ({ qrContent, network }) => { | ||
const result = convertMerchantQRToLightningAddress({ qrContent, network }) | ||
expect(result).toBeNull() | ||
}) | ||
|
||
// Edge cases and special scenarios | ||
test("handles multiple merchant identifiers in the same QR content", () => { | ||
const qrContent = | ||
"00020129530023za.co.electrum.picknpay.za.co.ecentric0122RD2HAK3KTI53EC/confirm520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2" | ||
const result = convertMerchantQRToLightningAddress({ | ||
qrContent, | ||
network: "mainnet", | ||
}) | ||
expect(result).toBe( | ||
"00020129530023za.co.electrum.picknpay.za.co.ecentric0122RD2HAK3KTI53EC%2Fconfirm520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2@cryptoqr.net", | ||
) | ||
}) | ||
|
||
test("handles URL-unsafe characters in EMV format", () => { | ||
const qrContent = | ||
"00020129530023za.co.electrum.picknpay0122RD2HAK3KTI53EC?param=value&other=123520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2" | ||
const result = convertMerchantQRToLightningAddress({ | ||
qrContent, | ||
network: "mainnet", | ||
}) | ||
expect(result).toBe( | ||
"00020129530023za.co.electrum.picknpay0122RD2HAK3KTI53EC%3Fparam%3Dvalue%26other%3D123520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2@cryptoqr.net", | ||
) | ||
}) | ||
|
||
test("preserves original case in EMV format", () => { | ||
const qrContent = | ||
"00020129530023ZA.co.ELECTRUM.picknpay0122RD2HAK3KTI53EC/confirm520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2" | ||
const result = convertMerchantQRToLightningAddress({ | ||
qrContent, | ||
network: "mainnet", | ||
}) | ||
expect(result).toBe( | ||
"00020129530023ZA.co.ELECTRUM.picknpay0122RD2HAK3KTI53EC%2Fconfirm520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2@cryptoqr.net", | ||
) | ||
}) | ||
}) |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import type { Network } from "./types.ts" | ||
|
||
type MerchantConfig = { | ||
id: string | ||
identifierRegex: RegExp | ||
defaultDomain: string | ||
domains: { [K in Network]: string } | ||
} | ||
|
||
export const merchants: MerchantConfig[] = [ | ||
{ | ||
id: "picknpay", | ||
identifierRegex: /(?<identifier>.*za\.co\.electrum\.picknpay.*)/iu, | ||
defaultDomain: "cryptoqr.net", | ||
domains: { | ||
mainnet: "cryptoqr.net", | ||
signet: "staging.cryptoqr.net", | ||
regtest: "staging.cryptoqr.net", | ||
}, | ||
}, | ||
{ | ||
id: "ecentric", | ||
identifierRegex: /(?<identifier>.*za\.co\.ecentric.*)/iu, | ||
defaultDomain: "cryptoqr.net", | ||
domains: { | ||
mainnet: "cryptoqr.net", | ||
signet: "staging.cryptoqr.net", | ||
regtest: "staging.cryptoqr.net", | ||
}, | ||
}, | ||
] | ||
|
||
export const convertMerchantQRToLightningAddress = ({ | ||
qrContent, | ||
network, | ||
}: { | ||
qrContent: string | ||
network: Network | ||
}): string | null => { | ||
if (!qrContent) { | ||
return null | ||
} | ||
|
||
for (const merchant of merchants) { | ||
const match = qrContent.match(merchant.identifierRegex) | ||
if (match?.groups?.identifier) { | ||
const domain = merchant.domains[network] || merchant.defaultDomain | ||
return `${encodeURIComponent(match.groups.identifier)}@${domain}` | ||
} | ||
} | ||
|
||
return null | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export type Network = "mainnet" | "signet" | "regtest" |
This file contains 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