Skip to content

Commit

Permalink
fix: add amount and metadata validations (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
dolcalmi authored Dec 28, 2022
1 parent a45ce3c commit 9d565fb
Show file tree
Hide file tree
Showing 11 changed files with 878 additions and 937 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Request an invoice for lnurl o lightning address
tokens: <Amount in satoshis Number>
[comment]: <Comment String>
[onionAllowed]: <Onion url allowed Bool> // Default to false
[validateInvoice]: <If true validates the invoice amount and description hash Bool> // Default to false
[fetchGet]: <Function to make a GET request Function> // Default to axios get
}
Expand All @@ -71,11 +72,14 @@ Request an invoice for lnurl o lightning address
max: <Max amount in satoshis Number>
domain: <Callback domain String>
metadata: <Decoded metadata Array>
metadataHash: <Metadata hash String>
identifier: <Metadata identifier String>
description: <Metadata description String>
image: <Metadata base64 image String>
commentAllowed: <Number of characters accepted for the comment query parameter Number> // Default to 0 - not allowed
}
hasValidAmount: <True if the returned invoice amount is equal to tokens param Bool>
hasValidDescriptionHash: <True if the returned invoice description hash is equal to metadata hash Bool>
validatePreimage: <validates if preimage param is valid for invoice Function> // (preimage: string) => boolean
}
```
Expand Down Expand Up @@ -111,6 +115,7 @@ Request pay service params for lnurl o lightning address (1st step)
max: <Max amount in satoshis Number>
domain: <Callback domain String>
metadata: <Decoded metadata Array>
metadataHash: <Metadata hash String>
identifier: <Metadata identifier String>
description: <Metadata description String>
image: <Metadata base64 image String>
Expand Down Expand Up @@ -140,6 +145,7 @@ Request an invoice for lnurl o lightning address with the given service params (
max: <Max amount in satoshis Number>
domain: <Callback domain String>
metadata: <Decoded metadata Array>
metadataHash: <Metadata hash String>
identifier: <Metadata identifier String>
description: <Metadata description String>
image: <Metadata base64 image String>
Expand All @@ -148,6 +154,7 @@ Request an invoice for lnurl o lightning address with the given service params (
tokens: <Amount in satoshis Number>
[comment]: <Comment String>
[onionAllowed]: <Onion url allowed Bool> // Default to false
[validateInvoice]: <If true validates the invoice amount and description hash Bool> // Default to false
[fetchGet]: <Function to make a GET request Function> // Default to axios get
}
Expand All @@ -164,11 +171,14 @@ Request an invoice for lnurl o lightning address with the given service params (
max: <Max amount in satoshis Number>
domain: <Callback domain String>
metadata: <Decoded metadata Array>
metadataHash: <Metadata hash String>
identifier: <Metadata identifier String>
description: <Metadata description String>
image: <Metadata base64 image String>
commentAllowed: <Number of characters accepted for the comment query parameter Number> // Default to 0 - not allowed
}
hasValidAmount: <True if the returned invoice amount is equal to tokens param>
hasValidDescriptionHash: <True if the returned invoice description hash is equal to metadata hash>
validatePreimage: <validates if preimage param is valid for invoice Function> // (preimage: string) => boolean
}
```
Expand Down
14 changes: 7 additions & 7 deletions config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,19 @@ module.exports = {
fallback: {
buffer: require.resolve('buffer/'),
stream: require.resolve('stream-browserify'),
zlib: require.resolve("browserify-zlib"),
https: require.resolve("https-browserify"),
http: require.resolve("stream-http"),
url: require.resolve("url/"),
assert: require.resolve("assert/"),
zlib: require.resolve('browserify-zlib'),
https: require.resolve('https-browserify'),
http: require.resolve('stream-http'),
url: require.resolve('url/'),
assert: require.resolve('assert/'),
},
},
plugins: [
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
new webpack.ProvidePlugin({
process: 'process/browser'
})
process: 'process/browser',
}),
],
}
28 changes: 14 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"@aws-crypto/sha256-js": "^2.0.2",
"aes-js": "^3.1.2",
"assert": "^2.0.0",
"axios": "^1.1.3",
"axios": "^1.2.1",
"base64-js": "^1.5.1",
"bech32": "^2.0.0",
"bolt11": "^1.4.0",
Expand All @@ -60,29 +60,29 @@
"is-url": "^1.2.4",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"tslib": "^2.4.0",
"tslib": "^2.4.1",
"url": "^0.11.0"
},
"devDependencies": {
"@commitlint/cli": "^17.1.2",
"@commitlint/config-conventional": "^17.1.0",
"@commitlint/cli": "^17.3.0",
"@commitlint/config-conventional": "^17.3.0",
"@types/aes-js": "^3.1.1",
"@types/is-url": "^1.2.30",
"@types/jest": "^29.1.2",
"@typescript-eslint/eslint-plugin": "^5.40.0",
"@typescript-eslint/parser": "^5.40.0",
"eslint": "^8.25.0",
"@types/jest": "^29.2.4",
"@typescript-eslint/eslint-plugin": "^5.47.0",
"@typescript-eslint/parser": "^5.47.0",
"eslint": "^8.30.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.1",
"jest": "^29.2.0",
"husky": "^8.0.2",
"jest": "^29.3.1",
"pinst": "^3.0.0",
"prettier": "^2.7.1",
"prettier": "^2.8.1",
"process": "^0.11.10",
"ts-jest": "^29.0.3",
"ts-loader": "^9.4.1",
"typescript": "^4.8.4",
"webpack": "^5.74.0",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-cli": "^4.9.2"
}
}
24 changes: 24 additions & 0 deletions src/request-invoice.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
decipherAES,
decodeInvoice,
getJson,
isOnionUrl,
isUrl,
Expand All @@ -19,6 +20,7 @@ export const requestInvoiceWithServiceParams = async ({
tokens,
comment,
onionAllowed = false,
validateInvoice = false,
fetchGet = getJson,
}: LnUrlrequestInvoiceWithServiceParamsArgs): Promise<LnUrlRequestInvoiceResponse> => {
const { callback, commentAllowed, min, max } = params
Expand All @@ -43,6 +45,24 @@ export const requestInvoiceWithServiceParams = async ({
const invoice = data && data.pr && data.pr.toString()
if (!invoice) throw new Error('Invalid pay service invoice')

const decodedInvoice = decodeInvoice(invoice)
const descriptionHash = decodedInvoice?.tags.find(
(t) => t.tagName === 'purpose_commit_hash'
)
const hasValidDescriptionHash = descriptionHash
? params.metadataHash === descriptionHash.data
: false
if (validateInvoice && !hasValidDescriptionHash)
throw new Error(`Invoice description hash doesn't match metadata hash.`)

const hasValidAmount = decodedInvoice
? decodedInvoice.satoshis === tokens
: false
if (validateInvoice && !hasValidAmount)
throw new Error(
`Invalid invoice amount ${decodedInvoice?.satoshis}. Expected ${tokens}`
)

let successAction: LNURLPaySuccessAction | undefined = undefined
if (data.successAction) {
const decipher = (preimage: string): string | null =>
Expand All @@ -54,6 +74,8 @@ export const requestInvoiceWithServiceParams = async ({
params,
invoice,
successAction,
hasValidAmount,
hasValidDescriptionHash,
validatePreimage: (preimage: string): boolean =>
isValidPreimage({ invoice, preimage }),
}
Expand All @@ -64,6 +86,7 @@ export const requestInvoice = async ({
tokens,
comment,
onionAllowed = false,
validateInvoice = false,
fetchGet = getJson,
}: LnUrlRequestInvoiceArgs): Promise<LnUrlRequestInvoiceResponse> => {
const params = await requestPayServiceParams({
Expand All @@ -76,6 +99,7 @@ export const requestInvoice = async ({
tokens,
comment,
onionAllowed,
validateInvoice,
fetchGet,
})
}
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type LnUrlRequestInvoiceBaseArgs = {
comment?: string
onionAllowed?: boolean
fetchGet?: (args: FetcGetArgs) => Promise<Json>
validateInvoice?: boolean
}

export type LnUrlrequestInvoiceWithServiceParamsArgs =
Expand All @@ -64,6 +65,8 @@ export type LnUrlRequestInvoiceArgs = LnUrlRequestInvoiceBaseArgs & {
export type LnUrlRequestInvoiceResponse = {
params: LnUrlPayServiceResponse
invoice: string
hasValidAmount: boolean
hasValidDescriptionHash: boolean
successAction?: LNURLPaySuccessAction
validatePreimage: (preimage: string) => boolean
}
27 changes: 25 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,35 @@ export const sha256 = (data: string, encoding: BufferEncoding = 'hex') => {
return Buffer.from(sha256.digestSync()).toString('hex')
}

export const decodeInvoice = (
invoice: string
): (bolt11.PaymentRequestObject & { tagsObject: bolt11.TagsObject }) | null => {
if (!invoice) return null

try {
let network: bolt11.Network | undefined = undefined
// hack to support signet invoices, remove when it is supported in bolt11
if (invoice.startsWith('lntbs')) {
network = {
bech32: 'tbs',
pubKeyHash: 0x6f,
scriptHash: 0xc4,
validWitnessVersions: [0, 1],
}
}

return bolt11.decode(invoice, network)
} catch {
return null
}
}

export const getHashFromInvoice = (invoice: string): string | null => {
if (!invoice) return null

try {
const decoded = bolt11.decode(invoice)
if (!decoded.tags) return null
const decoded = decodeInvoice(invoice)
if (!decoded || !decoded.tags) return null

const hashTag = decoded.tags.find(
(value) => value.tagName === 'payment_hash'
Expand Down
44 changes: 22 additions & 22 deletions test/helper.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,75 @@
export const validPayServiceParams = [
{
lnUrlOrAddress:
'lnurl1dp68gurn8ghj7urp0yh8xarpva5kueewvaskcmme9e5k7tewwajkcmpdddhx7amw9akxuatjd3cz7atnv4erqgfuvv5',
'lnurl1dp68gurn8ghj7urp0yh8xarpva5kueewvaskcmme9e5k7tewwajkcmpdddhx7amw9akxuatjd3cz7ar9wd6xjmn8h9qlv7',
tokens: 5000,
serviceParams: {
callback: 'https://pay.staging.galoy.io:443/.well-known/lnurlp/user0',
callback: 'https://pay.staging.galoy.io:443/.well-known/lnurlp/testing',
minSendable: 1000,
maxSendable: 500000000,
metadata:
'[["text/plain","Payment to user0"],["text/identifier","user0@pay.staging.galoy.io"]]',
'[["text/plain","Payment to testing"],["text/identifier","testing@pay.staging.galoy.io"]]',
tag: 'payRequest',
},
serviceInvoice: {
pr: 'lntb13130n1psl9la5pp57pd5fud26tpk5a893yp4snl90mda5sns6uj8589422rw0fndz03qsp5umjxesrm440437ykfjykt2rst8wfdlegg32f0xq5k4val9zvz38sdqqcqzynxqyz5vq9qxpq9qsqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cnggqd7qqqq3qqqgqqqqlgqqqqqeqqjqtz9qwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cngqqqq05qqqqqvsqfqvu7u2ke04028p35n2xr9k23m53ggg6rjx045k2kpvysj0q6zhq74cxnh27xa3vdkqq0dypsfeyqys5mtcn0hz3hrdqvyl3n8tndhlpgpzchyh7',
pr: 'lntbs50u1p363g52pp5axkm79jg9llq9suwf8w2gygvaa6z0jtdhl89kjl8vtphnpejk9xqhp5s02v4jhp0nxhkx9qqqraj24ga4cjnfpf3tgr9f2ymq2kx4ap8umscqzpuxqyz5vqsp5vasapyafj0w3lvethpyvqj6224clulvp8dwusxkw79hmdxtnn0dq9qyyssqgz0eqsag9jh7mcy4tvcz9p77ngh946a0a5kakxfjyagdhxv26d8qrum2wtk7709r2n6qlcqnu409a9wjt2tyqdnf2aqx7rhv905lksqptz820d',
routes: [],
successAction: undefined,
},
preimage:
'4cad8d163234461cb4de6c5ad35e5938f190df261065ef51b9708433466d0c3b',
'507498a304d040752566f69583a2d6a28c6d8fd3469c13786f2e44b18532d7da',
serviceParamsExpected: {
callback: 'https://pay.staging.galoy.io:443/.well-known/lnurlp/user0',
callback: 'https://pay.staging.galoy.io:443/.well-known/lnurlp/testing',
commentAllowed: 0,
description: 'Payment to user0',
description: 'Payment to testing',
domain: 'pay.staging.galoy.io',
fixed: false,
identifier: 'user0@pay.staging.galoy.io',
identifier: 'testing@pay.staging.galoy.io',
image: '',
max: 500000,
metadata: [
['text/plain', 'Payment to user0'],
['text/identifier', 'user0@pay.staging.galoy.io'],
['text/plain', 'Payment to testing'],
['text/identifier', 'testing@pay.staging.galoy.io'],
],
metadataHash:
'ad2c140d48fb958bc57d6d92536eedf2b21f6e3128c858cc27117b48a6d33abe',
'83d4cacae17ccd7b18a00007d92aa8ed7129a4298ad032a544d8156357a13f37',
min: 1,
},
},
{
lnUrlOrAddress: 'user11@pay.staging.galoy.io',
lnUrlOrAddress: 'testing@pay.staging.galoy.io',
tokens: 10,
serviceParams: {
callback: 'https://pay.staging.galoy.io:443/.well-known/lnurlp/user11',
callback: 'https://pay.staging.galoy.io:443/.well-known/lnurlp/testing',
minSendable: 10000,
maxSendable: 10000,
metadata:
'[["text/plain","Payment to user11"],["text/identifier","user11@pay.staging.galoy.io"]]',
'[["text/plain","Payment to testing"],["text/identifier","testing@pay.staging.galoy.io"]]',
tag: 'payRequest',
},
serviceInvoice: {
pr: 'lnbc10m1ps79mp5pp5eat0y56jcfym7eruu049280n9yssw5flmzvnp99up995zq4p2r4qdqqcqzpuxqyz5vqsp5r88jwj2er6nlj40748slw3zy3rwaydtu2x4s84yqmgq0w63fxjdq9qyyssqyc58k4lmayagad3upndzny07w777axtcfv4w9dj4q3tjvj877y99t2r7vgtxc3pw0xtk9j7mftvx72fg5m8rjnphcrkf3mnzzg8l6lcprw8apg',
pr: 'lntbs100n1p36njvkpp53dl62nzwkp3d7sxj5pgf5dppgkddgkprdfk823s3st4cy7vsujsshp5s02v4jhp0nxhkx9qqqraj24ga4cjnfpf3tgr9f2ymq2kx4ap8umscqzpuxqyz5vqsp5edcvjjav3k94ndjdgfrs5cr5ygne09dqryuw483xlpd7smxzp5wq9qyyssqky8c279vrlpy87wrj7vt698yltjq7jmcj0hulp09c3engvplkkykr0qm5jg83vqjf7pxw7nq0lce895eq2jyjdr7j2tlap2sh97vznsp5qydfk',
routes: [],
successAction: undefined,
},
preimage:
'b956cb683997640065bad032aa177e7609c6a6c6289b73c2bf6237cea32fa6c6',
'b7a2a9d3e54566d3b8ef2ac14d828fc336bef5bf3ca60d3e8c2b207283fcfb36',
serviceParamsExpected: {
callback: 'https://pay.staging.galoy.io:443/.well-known/lnurlp/user11',
callback: 'https://pay.staging.galoy.io:443/.well-known/lnurlp/testing',
commentAllowed: 0,
description: 'Payment to user11',
description: 'Payment to testing',
domain: 'pay.staging.galoy.io',
fixed: true,
identifier: 'user11@pay.staging.galoy.io',
identifier: 'testing@pay.staging.galoy.io',
image: '',
min: 10,
max: 10,
metadata: [
['text/plain', 'Payment to user11'],
['text/identifier', 'user11@pay.staging.galoy.io'],
['text/plain', 'Payment to testing'],
['text/identifier', 'testing@pay.staging.galoy.io'],
],
metadataHash:
'382190a9edf28041fa43931f9f047c04252be80d6ae6483407fa3711df6d4152',
'83d4cacae17ccd7b18a00007d92aa8ed7129a4298ad032a544d8156357a13f37',
},
},
]
7 changes: 6 additions & 1 deletion test/request-invoice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('requestInvoice', () => {
const result = await requestInvoice({
lnUrlOrAddress,
tokens: toSats(tokens),
validateInvoice: true,
})
expect(result).toMatchObject({
invoice: serviceInvoice.pr,
Expand All @@ -42,7 +43,11 @@ describe('requestInvoice', () => {
.mockResolvedValueOnce({ data: serviceInvoice })

await expect(async () => {
await requestInvoice({ lnUrlOrAddress, tokens: toSats(5000000) })
await requestInvoice({
lnUrlOrAddress,
tokens: toSats(5000000),
validateInvoice: true,
})
}).rejects.toThrowError('Invalid amount')
}
)
Expand Down
8 changes: 8 additions & 0 deletions test/utils/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,19 @@ export const validInvoices = [
preimage:
'b956cb683997640065bad032aa177e7609c6a6c6289b73c2bf6237cea32fa6c6',
},
{
invoice:
'lntbs50u1p36n3tupp5x4dptrj0yum32ezd5nsgngteg553mlt5wgmrvr7p4knenr823ahqhp5s02v4jhp0nxhkx9qqqraj24ga4cjnfpf3tgr9f2ymq2kx4ap8umscqzpuxqyz5vqsp5ghfamw9l53ud04hp8dn5ftrx87ntmlsum9re5c9nmryhmjk4vyxs9qyyssqalen93zdlm0s4psh3gn9yshc7dzsvpey4dv4g7n5th8au4ht0dnhhztqhne9hj03pehxlmna4xxnj6ngzx2p8kzs6fhej5tv9x9e65gql568f4',
hash: '355a158e4f273715644da4e089a17945291dfd747236360fc1ada7998cea8f6e',
preimage:
'255c7b1f94982adc00129f8b77590b2abe4c5cfabc5dfdab9aa2082525b6a8d7',
},
]

export const invalidInvoices = [
'lnbcrt1pslcav8pp5y8lae09uqd69fv6mccgp7lp42j6nefmzlgfpxzm7289ddvvj43jsdqqcqzpuxqr23ssp50jylhv3wzfnfk2lsgrga0v2px742pnml9tcajfwdulqppeyg5hdq9qyyssq7hmj9wkzen6nnpv9040wxgpcx6dy0zdmkj7k539ll32ecjc6s9r4cj08cj05nszcmc3zkhnu52yhdsmajzmg2ueww7m7z5790vy3cugqn2zsd',
'lntb349820n1pslcagupp53qzw32u7s70s2uaejhtmpq76fr0lv80c28f7gtv3tw2vgv59n8wsdqqcqzpuxqyz5vqsp560xz2teaz74cltfmvpw6wmcqkrc5a6a0njy8n90w7ptfe42q2h4s9qyyssqdzu9da88zs6qdre3pqxdppls4xj2mnghv3facn2vztjvc2x6p3h9lann5hwx7a972ffna6zhzqrke7mz4cq7xt2d487g2x48pyjvlhsp9cuhy',
'lnbc1pslca2wpp5zrwrnh2unz02e2qve8ue26dd4l862vumxx0ry0j64tjljucw9jhqdqqcqzpuxqyz5vqsp58mdlrld5wnwhg06skyk4tcwdfyx6t9qk4apc4qka5ah0usduptrq9qyyssqj0rwcgeuz46l3q0jtyazjr9j9lhgq7mgf2xydpdf6cqjenpgaxur43f35c6g9nz634q7r2ze7s5ujqxlvq84wfvxesc78y7khhdy6wspkuesj',
'lntbs50u1p36n3tupp5x4dptrj0yum32ezd5nsgngteg553mlt5wgmrvr7p4knenr823ahqhp5s02v4jhp0nxhkx9qqqraj24ga4cjnfpf3tgr9f2ymq2kx4ap8umscqzpuxqyz5vqsp5ghfamw9l53ud04hp8dn5ftrx87ntmlsum9re5c9nmryhmjk4vyxs9qyyssqalen93zdlm0s4psh3gn9yshc7dzsvpey4dv4g7n5th8au4ht0dnhhztqhne9hj03pehxlmna4xxnj6ngzx2p8kzs6fhej5tv9x9e65gql56',
'abc',
]
Loading

0 comments on commit 9d565fb

Please sign in to comment.