Skip to content

Commit

Permalink
fix: detect lightning param in bip 21 QR codes (#42)
Browse files Browse the repository at this point in the history
* refactor: split `parsePaymentDestination` into multiple methods

* feat: process lightning param in BIP 21 QR strings

* test: add test cases for invalid network

* refactor: add additional test cases and refactor submethods

* test: add missing test cases

* refactor: clean up sub functions for

* refactor: remove duplicate function

* refactor: pass in destination

* chore: fix code:check errors
  • Loading branch information
allenwhite authored May 31, 2022
1 parent 6cbc2e3 commit e51b198
Show file tree
Hide file tree
Showing 2 changed files with 357 additions and 205 deletions.
301 changes: 186 additions & 115 deletions src/parsing/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ const lnUrlInvoice =
const lnInvoice =
"LNBC6864270N1P05ZVJJPP5FPEHVLV3DD2R76065R9V0L3N8QV9MFWU9RYHVPJ5XSZ3P4HY734QDZHXYSV89EQYVMZQSNFW3PXCMMRDDPX7MMDYPP8YATWVD5ZQMMWYPQH2EM4WD6ZQVESYQ5YYUN4DE3KSGZ0DEK8J2GCQZPGXQRRSS6LQA5JLLVUGLW5TPSUG4S2TMT5C8FNERR95FUH8HTCSYX52CP3WZSWJ32XJ5GEWYFN7MG293V6JLA9CZ8ZNDHWDHCNNKUL2QKF6PJLSPJ2NL3J"

const lntbInvoice =
"lntb1m1pd2awsppp54q20f42rpuzapqpxl4l5a2vhrm89pth7rj0nv3fyqvkl89hc8myqdqqcqzysms67f23xktazlazsjdwvqv7j59c34q5vqp4gnmddpkmlwqpufecxf9ledyq0ma495wrak26nvq5qcg6lgw7zwfy5yq4w54ux7qay3tsqrg02mh"

const lnbcrtInvoice =
"lnbcrt1u1psjhly3pp5mxvvnc6aw00vtvx004xrt05vfmy3nthxdd5cmyfxv45y66mfpxxqdqqcqzpgxqyz5vqsp5v399u058lal7u3dzswtntktg93wzdtggr2sqqkzy5t6ffd0n4pgq9qyyssqk204pw6as2599mdrefqx5lrycjax5559xnv2lrp9m4wpqavk6enkmtme09yxdt56552mx6v8eg9gpwxvl9mn0t5dea2gtmajzmukffsph8074z"

const expiredLNInvoice =
"LNBC11245410N1P05Z2LTPP52W2GX57TZVLM09SWZ8M0CAWGQPVTL3KUWZA836H5LG6HK2N2PRYQDPHXYSV89EQYVMJQSNFW3PXCMMRDDZXJMNWV4EZQST4VA6HXAPQXGU8G6QCQZPGXQRRSSVS7S2WT4GX90MQC9CVMA8UYDSTX5P0FA68V03U96HQDPFCT9DGDQQSENNAAGAXND6664CTKV88GMQ689LS0J7FFAD4DRN6SPLXAXZ0CQYZAU9Q"

const checkOnChain = (address: string, network: Network) => {
const { valid, paymentType } = parsePaymentDestination({
destination: address,
Expand All @@ -41,7 +50,7 @@ const checkOnChainFail = (address: string, network: Network) => {
expect(valid).toBeFalsy()
}

describe("parsePaymentDestination", () => {
describe("parsePaymentDestination validations", () => {
it("invalidates empty input", () => {
const result = parsePaymentDestination({
destination: "",
Expand All @@ -61,8 +70,80 @@ describe("parsePaymentDestination", () => {
expect(result.paymentType).toBe("lnurl")
expect(result.lnurl).toBe(lnUrlInvoice)
})
})

describe("parsePaymentDestination OnChain", () => {
it("validates bitcoin address mainnet", () => {
checkOnChain(p2pkh, "mainnet")
checkOnChain(p2sh, "mainnet")
checkOnChain(bech32, "mainnet")
checkOnChain(bech32Caps, "mainnet")
checkOnChain(p2pkhPrefix, "mainnet")
checkOnChain(p2shPrefix, "mainnet")
checkOnChain(bech32Prefix, "mainnet")
checkOnChain(bech32CapsPrefix, "mainnet")

checkOnChainFail(bech32Regtest, "mainnet")
checkOnChainFail(bech32Testnet, "mainnet")
})

it("validates bitcoin address testnet", () => {
checkOnChain(bech32Testnet, "testnet")

checkOnChainFail(bech32Regtest, "testnet")
checkOnChainFail(bech32, "testnet")
})

it("validates bitcoin address regtest", () => {
checkOnChain(bech32Regtest, "regtest")

checkOnChainFail(p2pkh, "regtest")
checkOnChainFail(bech32Testnet, "regtest")
})

it("validates an onchain destination with amount ", () => {
const addressAmount = "bc1qdx09anw82zhujxzzsn56mruv8qvd33czzy9apt?amount=0.00122"

const { valid, paymentType, amount } = parsePaymentDestination({
destination: addressAmount,
network: "mainnet",
pubKey: "",
})
expect(valid).toBeTruthy()
expect(paymentType).toBe("onchain")
expect(amount).toBe(122000)
})

it("validates an onchain destination without amount", () => {
const addressNoAmount = "bc1qdx09anw82zhujxzzsn56mruv8qvd33czzy9apt"

const { valid, paymentType, amount } = parsePaymentDestination({
destination: addressNoAmount,
network: "mainnet",
pubKey: "",
})
expect(valid).toBeTruthy()
expect(paymentType).toBe("onchain")
expect(amount).toBeUndefined()
})

it("invalidates a network mismatch", () => {
it("validates an onchain destination with a prefix", () => {
const prefixAddress =
"bitcoin:bc1qdx09anw82zhujxzzsn56mruv8qvd33czzy9apt?amount=0.00122"

const { valid, paymentType, amount } = parsePaymentDestination({
destination: prefixAddress,
network: "mainnet",
pubKey: "",
})
expect(valid).toBeTruthy()
expect(paymentType).toBe("onchain")
expect(amount).toBe(122000)
})
})

describe("parsePaymentDestination Lightning", () => {
it("invalidates a mainnet invoice on testnet", () => {
const result = parsePaymentDestination({
// lnInovice is a mainnet invoice
destination: lnInvoice,
Expand All @@ -74,142 +155,132 @@ describe("parsePaymentDestination", () => {
expect(result.errorMessage).toBe("Invalid lightning invoice for testnet network")
})

describe("OnChain", () => {
it("validates bitcoin address mainnet", () => {
checkOnChain(p2pkh, "mainnet")
checkOnChain(p2sh, "mainnet")
checkOnChain(bech32, "mainnet")
checkOnChain(bech32Caps, "mainnet")
checkOnChain(p2pkhPrefix, "mainnet")
checkOnChain(p2shPrefix, "mainnet")
checkOnChain(bech32Prefix, "mainnet")
checkOnChain(bech32CapsPrefix, "mainnet")

checkOnChainFail(bech32Regtest, "mainnet")
checkOnChainFail(bech32Testnet, "mainnet")
it("invalidates a regtest invoice on testnet", () => {
const result = parsePaymentDestination({
// lnInovice is a regtest invoice
destination: lnbcrtInvoice,
network: "testnet",
pubKey: "",
})
expect(result.valid).toBeFalsy()
expect(result.paymentType).toBe("lightning")
expect(result.errorMessage).toBe("Invalid lightning invoice for testnet network")
})

it("validates bitcoin address testnet", () => {
checkOnChain(bech32Testnet, "testnet")

checkOnChainFail(bech32Regtest, "testnet")
checkOnChainFail(bech32, "testnet")
it("invalidates a testnet invoice on mainnet", () => {
// lntbInovice is a testnet invoice
const result = parsePaymentDestination({
destination: lntbInvoice,
network: "mainnet",
pubKey: "",
})
expect(result.valid).toBeFalsy()
expect(result.paymentType).toBe("lightning")
})

it("validates bitcoin address regtest", () => {
checkOnChain(bech32Regtest, "regtest")

checkOnChainFail(p2pkh, "regtest")
checkOnChainFail(bech32Testnet, "regtest")
it("invalidates a regtest invoice on mainnet", () => {
// lntbInovice is a regtest invoice
const result = parsePaymentDestination({
destination: lnbcrtInvoice,
network: "mainnet",
pubKey: "",
})
expect(result.valid).toBeFalsy()
expect(result.paymentType).toBe("lightning")
})

it("validates an onchain destination with amount ", () => {
const addressAmount = "bc1qdx09anw82zhujxzzsn56mruv8qvd33czzy9apt?amount=0.00122"

const { valid, paymentType, amount } = parsePaymentDestination({
destination: addressAmount,
network: "mainnet",
pubKey: "",
})
expect(valid).toBeTruthy()
expect(paymentType).toBe("onchain")
expect(amount).toBe(122000)
it("invalidates a testnet invoice on regtest", () => {
// lntbInovice is a testnet invoice
const result = parsePaymentDestination({
destination: lntbInvoice,
network: "regtest",
pubKey: "",
})
expect(result.valid).toBeFalsy()
expect(result.paymentType).toBe("lightning")
})

it("validates an onchain destination without amount", () => {
const addressNoAmount = "bc1qdx09anw82zhujxzzsn56mruv8qvd33czzy9apt"

const { valid, paymentType, amount } = parsePaymentDestination({
destination: addressNoAmount,
network: "mainnet",
pubKey: "",
})
expect(valid).toBeTruthy()
expect(paymentType).toBe("onchain")
expect(amount).toBeUndefined()
it("invalidates a mainnet invoice on regtest", () => {
// lntbInovice is a mainnet invoice
const result = parsePaymentDestination({
destination: lnInvoice,
network: "regtest",
pubKey: "",
})
expect(result.valid).toBeFalsy()
expect(result.paymentType).toBe("lightning")
})

it("validates an onchain destination with a prefix", () => {
const prefixAddress =
"bitcoin:bc1qdx09anw82zhujxzzsn56mruv8qvd33czzy9apt?amount=0.00122"

const { valid, paymentType, amount } = parsePaymentDestination({
destination: prefixAddress,
network: "mainnet",
pubKey: "",
})
expect(valid).toBeTruthy()
expect(paymentType).toBe("onchain")
expect(amount).toBe(122000)
it("detects a lightning param in an onchain address", () => {
const address =
"bitcoin:bc1qylh3u67j673h6y6alv70m0pl2yz53tzhvxgg7u?amount=0.00001&label=sbddesign%3A%20For%20lunch%20Tuesday&message=For%20lunch%20Tuesday&lightning=lnbc10u1p3pj257pp5yztkwjcz5ftl5laxkav23zmzekaw37zk6kmv80pk4xaev5qhtz7qdpdwd3xger9wd5kwm36yprx7u3qd36kucmgyp282etnv3shjcqzpgxqyz5vqsp5usyc4lk9chsfp53kvcnvq456ganh60d89reykdngsmtj6yw3nhvq9qyyssqjcewm5cjwz4a6rfjx77c490yced6pemk0upkxhy89cmm7sct66k8gneanwykzgdrwrfje69h9u5u0w57rrcsysas7gadwmzxc8c6t0spjazup6"
const { valid, paymentType, errorMessage } = parsePaymentDestination({
destination: address,
network: "mainnet",
pubKey: "",
})
expect(valid).toBeTruthy()
expect(paymentType).toBe("lightning")
expect(errorMessage).not.toBe("invoice has expired")
})

describe("Lightning", () => {
it("validates an opennode invoice", () => {
const address =
"LNBC6864270N1P05ZVJJPP5FPEHVLV3DD2R76065R9V0L3N8QV9MFWU9RYHVPJ5XSZ3P4HY734QDZHXYSV89EQYVMZQSNFW3PXCMMRDDPX7MMDYPP8YATWVD5ZQMMWYPQH2EM4WD6ZQVESYQ5YYUN4DE3KSGZ0DEK8J2GCQZPGXQRRSS6LQA5JLLVUGLW5TPSUG4S2TMT5C8FNERR95FUH8HTCSYX52CP3WZSWJ32XJ5GEWYFN7MG293V6JLA9CZ8ZNDHWDHCNNKUL2QKF6PJLSPJ2NL3J"
it("validates an opennode invoice", () => {
const { valid, paymentType, errorMessage } = parsePaymentDestination({
destination: lnInvoice,
network: "mainnet",
pubKey: "",
})
expect(valid).toBeTruthy()
expect(paymentType).toBe("lightning")
expect(errorMessage).not.toBe("invoice has expired")
})

const { valid, paymentType, errorMessage } = parsePaymentDestination({
destination: address,
network: "mainnet",
pubKey: "",
})
expect(valid).toBeTruthy()
expect(paymentType).toBe("lightning")
expect(errorMessage).not.toBe("invoice has expired")
it("invalidates an expired opennode invoice", () => {
const { valid, paymentType, errorMessage } = parsePaymentDestination({
destination: expiredLNInvoice,
network: "mainnet",
pubKey: "",
})
expect(valid).toBeFalsy()
expect(paymentType).toBe("lightning")
expect(errorMessage).toBe("invoice has expired")
})

it("invalidates an expired opennode invoice", () => {
const address =
"LNBC11245410N1P05Z2LTPP52W2GX57TZVLM09SWZ8M0CAWGQPVTL3KUWZA836H5LG6HK2N2PRYQDPHXYSV89EQYVMJQSNFW3PXCMMRDDZXJMNWV4EZQST4VA6HXAPQXGU8G6QCQZPGXQRRSSVS7S2WT4GX90MQC9CVMA8UYDSTX5P0FA68V03U96HQDPFCT9DGDQQSENNAAGAXND6664CTKV88GMQ689LS0J7FFAD4DRN6SPLXAXZ0CQYZAU9Q"
it("validates a lightning invoice with prefix", () => {
const address = `LIGHTNING:${lnInvoice}`

const { valid, paymentType, errorMessage } = parsePaymentDestination({
destination: address,
network: "mainnet",
pubKey: "",
})
expect(valid).toBeFalsy()
expect(paymentType).toBe("lightning")
expect(errorMessage).toBe("invoice has expired")
const { valid, paymentType, errorMessage } = parsePaymentDestination({
destination: address,
network: "mainnet",
pubKey: "",
})

it("validates a lightning invoice with prefix", () => {
const address =
"LIGHTNING:LNBC6864270N1P05ZVJJPP5FPEHVLV3DD2R76065R9V0L3N8QV9MFWU9RYHVPJ5XSZ3P4HY734QDZHXYSV89EQYVMZQSNFW3PXCMMRDDPX7MMDYPP8YATWVD5ZQMMWYPQH2EM4WD6ZQVESYQ5YYUN4DE3KSGZ0DEK8J2GCQZPGXQRRSS6LQA5JLLVUGLW5TPSUG4S2TMT5C8FNERR95FUH8HTCSYX52CP3WZSWJ32XJ5GEWYFN7MG293V6JLA9CZ8ZNDHWDHCNNKUL2QKF6PJLSPJ2NL3J"

const { valid, paymentType, errorMessage } = parsePaymentDestination({
destination: address,
network: "mainnet",
pubKey: "",
})
expect(valid).toBeTruthy()
expect(paymentType).toBe("lightning")
expect(errorMessage).not.toBe("invoice has expired")
})
})

expect(valid).toBeTruthy()
expect(paymentType).toBe("lightning")
expect(errorMessage).not.toBe("invoice has expired")
describe("parsePaymentDestination IntraLedger handles", () => {
it("validates a regular handle", () => {
const { valid, paymentType, handle } = parsePaymentDestination({
destination: "Nakamoto",
network: "mainnet",
pubKey: "",
})
expect(valid).toBeTruthy()
expect(paymentType).toBe("intraledger")
expect(handle).toBe("Nakamoto")
})

describe("IntraLedger handles", () => {
it("validates a regular handle", () => {
const { valid, paymentType, handle } = parsePaymentDestination({
destination: "Nakamoto",
network: "mainnet",
pubKey: "",
})
expect(valid).toBeTruthy()
expect(paymentType).toBe("intraledger")
expect(handle).toBe("Nakamoto")
})

it("validates an http handle", () => {
const { valid, paymentType, handle } = parsePaymentDestination({
destination: "https://some.where/userName",
network: "mainnet",
pubKey: "",
})
expect(valid).toBeTruthy()
expect(paymentType).toBe("intraledger")
expect(handle).toBe("userName")
it("validates an http handle", () => {
const { valid, paymentType, handle } = parsePaymentDestination({
destination: "https://some.where/userName",
network: "mainnet",
pubKey: "",
})
expect(valid).toBeTruthy()
expect(paymentType).toBe("intraledger")
expect(handle).toBe("userName")
})
})
Loading

0 comments on commit e51b198

Please sign in to comment.