Skip to content

Commit

Permalink
feat: add preimage validation + lud-10 support (#2)
Browse files Browse the repository at this point in the history
* v0.1.1

* feat: add lud-10 support

* feat: add preimage validation method

* docs: update readme with new helper methods

* refactor: update validate preimage method
  • Loading branch information
dolcalmi authored Feb 4, 2022
1 parent 6812a19 commit 2d205e1
Show file tree
Hide file tree
Showing 12 changed files with 432 additions and 27 deletions.
25 changes: 16 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,24 @@ yarn add lnurl-pay
```js
import { requestInvoice } from 'lnurl-pay'

const { invoice, params, successAction } = await requestInvoice({
lnUrlOrAddress:
'lnurl1dp68gurn8ghj7urp0yh8xarpva5kueewvaskcmme9e5k7tewwajkcmpdddhx7amw9akxuatjd3cz7atnv4erqgfuvv5',
tokens: 333, // satoshis
})
const { invoice, params, successAction, validatePreimage } =
await requestInvoice({
lnUrlOrAddress:
'lnurl1dp68gurn8ghj7urp0yh8xarpva5kueewvaskcmme9e5k7tewwajkcmpdddhx7amw9akxuatjd3cz7atnv4erqgfuvv5',
tokens: 333, // satoshis
})
```

### Lightning Address

```js
import { requestInvoice } from 'lnurl-pay'

const { invoice, params, successAction } = await requestInvoice({
lnUrlOrAddress: '[email protected]',
tokens: 333, // satoshis
})
const { invoice, params, successAction, validatePreimage } =
await requestInvoice({
lnUrlOrAddress: '[email protected]',
tokens: 333, // satoshis
})
```

## Methods
Expand Down Expand Up @@ -74,6 +76,7 @@ Request an invoice for lnurl o lightning address
image: <Metadata base64 image String>
commentAllowed: <Number of characters accepted for the comment query parameter Number> // Default to 0 - not allowed
}
validatePreimage: <validates if preimage param is valid for invoice Function> // (preimage: string) => boolean
}
```

Expand Down Expand Up @@ -166,6 +169,7 @@ Request an invoice for lnurl o lightning address with the given service params (
image: <Metadata base64 image String>
commentAllowed: <Number of characters accepted for the comment query parameter Number> // Default to 0 - not allowed
}
validatePreimage: <validates if preimage param is valid for invoice Function> // (preimage: string) => boolean
}
```

Expand All @@ -186,6 +190,9 @@ const params = await requestInvoiceWithServiceParams({
- [isLightningAddress](#isLightningAddress) - Verify if a string is a lightning adress
- [parseLightningAddress](#parseLightningAddress) - Parse an address and return username and domain
- [isOnionUrl](#isOnionUrl) - Verify if a string is an onion url
- [getHashFromInvoice](#getHashFromInvoice) - Decodes an invoice(string) and get the hash
- [isValidPreimage](#isValidPreimage) - Returns true if the given preimage is valid for invoice
- [decipherAES](#decipherAES) - Decipher ciphertext with a preimage

## Test

Expand Down
2 changes: 1 addition & 1 deletion config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ module.exports = {
},
resolve: {
extensions: ['.ts', '.js', '.tsx', '.jsx'],
fallback: { url: false },
fallback: { url: false, buffer: false, crypto: false, stream: false },
},
}
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lnurl-pay",
"version": "0.1.0",
"version": "0.1.1",
"description": "Client library for lnurl-pay and lightning address",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
Expand Down Expand Up @@ -47,12 +47,16 @@
"url": "https://github.com/dolcalmi/lnurl-pay/issues"
},
"dependencies": {
"aes-js": "^3.1.2",
"axios": "^0.24.0",
"bech32": "^2.0.0"
"base64-js": "^1.5.1",
"bech32": "^2.0.0",
"bolt11": "^1.3.4"
},
"devDependencies": {
"@commitlint/cli": "^15.0.0",
"@commitlint/config-conventional": "^15.0.0",
"@types/aes-js": "^3.1.1",
"@types/jest": "^27.0.1",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
Expand Down
21 changes: 19 additions & 2 deletions src/request-invoice.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { getJson, isOnionUrl, isUrl, isValidAmount } from './utils'
import {
decipherAES,
getJson,
isOnionUrl,
isUrl,
isValidAmount,
isValidPreimage,
} from './utils'
import type {
LNURLPaySuccessAction,
LnUrlRequestInvoiceArgs,
LnUrlRequestInvoiceResponse,
LnUrlrequestInvoiceWithServiceParamsArgs,
Expand Down Expand Up @@ -35,10 +43,19 @@ export const requestInvoiceWithServiceParams = async ({
const invoice = data && data.pr && data.pr.toString()
if (!invoice) throw new Error('Invalid pay service invoice')

let successAction: LNURLPaySuccessAction | undefined = undefined
if (data.successAction) {
const decipher = (preimage: string): string | null =>
decipherAES({ preimage, successAction: data.successAction })
successAction = Object.assign({ decipher }, data.successAction)
}

return {
params,
invoice,
successAction: data.successAction,
successAction,
validatePreimage: (preimage: string): boolean =>
isValidPreimage({ invoice, preimage }),
}
}

Expand Down
13 changes: 12 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ export type LightningAddress = {
domain: string
}

export type LNURLPaySuccessAction = {
tag: string
description: string | null
url: string | null
message: string | null
ciphertext: string | null
iv: string | null
decipher: (preimage: string) => string | null
}

export type FetcGetArgs = {
url: string
params?: Json
Expand Down Expand Up @@ -52,5 +62,6 @@ export type LnUrlRequestInvoiceArgs = LnUrlRequestInvoiceBaseArgs & {
export type LnUrlRequestInvoiceResponse = {
params: LnUrlPayServiceResponse
invoice: string
successAction?: Json
successAction?: LNURLPaySuccessAction
validatePreimage: (preimage: string) => boolean
}
78 changes: 77 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { URL } from 'url'
import { bech32 } from 'bech32'
import type { LightningAddress, Satoshis } from './types'
import axios from 'axios'
import aesjs from 'aes-js'
import Base64 from 'base64-js'
import * as bolt11 from 'bolt11'
import * as crypto from 'crypto'

import type { LightningAddress, LNURLPaySuccessAction, Satoshis } from './types'

const LNURL_REGEX =
/^(?:http.*[&?]lightning=|lightning:)?(lnurl[0-9]{1,}[02-9ac-hj-np-z]+)/
Expand Down Expand Up @@ -155,3 +160,74 @@ export const getJson = async ({
return response.data
})
}

export const sha256 = (data: string) =>
crypto.createHash('sha256').update(data, 'hex').digest('hex')

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

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

const hashTag = decoded.tags.find(
(value) => value.tagName === 'payment_hash'
)
if (!hashTag || !hashTag.data) return null

return hashTag.data.toString()
} catch {
return null
}
}

export const isValidPreimage = ({
invoice,
preimage,
}: {
invoice: string
preimage: string
}): boolean => {
if (!invoice || !preimage) return false

const invoiceHash = getHashFromInvoice(invoice)
if (!invoiceHash) return false

try {
const preimageHash = sha256(preimage)
return invoiceHash === preimageHash
} catch {
return false
}
}

export const decipherAES = ({
successAction,
preimage,
}: {
successAction: LNURLPaySuccessAction
preimage: string
}): string | null => {
if (
successAction.tag !== 'aes' ||
!successAction.iv ||
!successAction.ciphertext ||
!preimage
)
return null

const key = aesjs.utils.hex.toBytes(preimage)
const iv = Base64.toByteArray(successAction.iv)
const ciphertext = Base64.toByteArray(successAction.ciphertext)

const cbc = new aesjs.ModeOfOperation.cbc(key, iv)
let plaintext = cbc.decrypt(ciphertext)

// remove padding
const size = plaintext.length
const pad = plaintext[size - 1]
plaintext = plaintext.slice(0, size - pad)

return aesjs.utils.utf8.fromBytes(plaintext)
}
8 changes: 6 additions & 2 deletions test/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ export const validPayServiceParams = [
tag: 'payRequest',
},
serviceInvoice: {
pr: 'lntb5m1psu9df3pp59ukuqcahss6uyngxq907j0v2njy56m9qsf9s3t5dknjxt2r5nraqhp545kpgr2glw2ch3tadkf9xmhd72ep7m339ry93np8z9a53fkn82lqcqzpuxqyz5vqsp5rhn6p6cfpmeqxcn8h62lngz8ezvxgh9hqqn2ka4utst4gmg4z79s9qyyssq3f9xgq7amzl6nlh3h9ctfmj2z99zplmwkh0re9ucc0dqrm0rjz0pjz32rgkw8pcqvdjrc4s3rsa57gkhwd8jgde6nyn2ee4exy7rxqcpqt6xz4',
pr: 'lntb13130n1psl9la5pp57pd5fud26tpk5a893yp4snl90mda5sns6uj8589422rw0fndz03qsp5umjxesrm440437ykfjykt2rst8wfdlegg32f0xq5k4val9zvz38sdqqcqzynxqyz5vq9qxpq9qsqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cnggqd7qqqq3qqqgqqqqlgqqqqqeqqjqtz9qwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cngqqqq05qqqqqvsqfqvu7u2ke04028p35n2xr9k23m53ggg6rjx045k2kpvysj0q6zhq74cxnh27xa3vdkqq0dypsfeyqys5mtcn0hz3hrdqvyl3n8tndhlpgpzchyh7',
routes: [],
successAction: undefined,
},
preimage:
'4cad8d163234461cb4de6c5ad35e5938f190df261065ef51b9708433466d0c3b',
serviceParamsExpected: {
callback: 'https://pay.staging.galoy.io:443/.well-known/lnurlp/user0',
commentAllowed: 0,
Expand Down Expand Up @@ -44,10 +46,12 @@ export const validPayServiceParams = [
tag: 'payRequest',
},
serviceInvoice: {
pr: 'lntb100n1psu9dd9pp5vkrhqjq60fd49wheygkrx7rw5rafvy6xs5ask5lk6g89udmalzrqhp58qsep20d72qyr7jrjv0e7pruqsjjh6qddtnysdq8lgm3rhmdg9fqcqzpuxqyz5vqsp57f2tcn3qj73st880v2ezmz6e75nv58khttawjkkrtymk623whses9qyyssqkqqk509zydy5p2f9jgr9wgzy7ky5zvqrj8spqdxdqcz6f4p8xej9ku2mq07wvx2kyddsjsgca9vepy3nfln78hmpcaq72entrl0vwggpnuuhht',
pr: 'lnbc10m1ps79mp5pp5eat0y56jcfym7eruu049280n9yssw5flmzvnp99up995zq4p2r4qdqqcqzpuxqyz5vqsp5r88jwj2er6nlj40748slw3zy3rwaydtu2x4s84yqmgq0w63fxjdq9qyyssqyc58k4lmayagad3upndzny07w777axtcfv4w9dj4q3tjvj877y99t2r7vgtxc3pw0xtk9j7mftvx72fg5m8rjnphcrkf3mnzzg8l6lcprw8apg',
routes: [],
successAction: undefined,
},
preimage:
'b956cb683997640065bad032aa177e7609c6a6c6289b73c2bf6237cea32fa6c6',
serviceParamsExpected: {
callback: 'https://pay.staging.galoy.io:443/.well-known/lnurlp/user11',
commentAllowed: 0,
Expand Down
8 changes: 3 additions & 5 deletions test/request-invoice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe('requestInvoice', () => {
serviceParams,
serviceParamsExpected,
serviceInvoice,
preimage,
}) => {
axios.get = jest
.fn()
Expand All @@ -28,16 +29,13 @@ describe('requestInvoice', () => {
successAction: serviceInvoice.successAction,
params: { ...serviceParamsExpected },
})
expect(result.validatePreimage(preimage)).toBeTruthy()
}
)

test.each(validPayServiceParams)(
'$lnUrlOrAddress throws with invalid amount',
async ({
lnUrlOrAddress,
serviceParams,
serviceInvoice,
}) => {
async ({ lnUrlOrAddress, serviceParams, serviceInvoice }) => {
axios.get = jest
.fn()
.mockResolvedValueOnce({ data: serviceParams })
Expand Down
15 changes: 15 additions & 0 deletions test/utils/get-hash-from-invoice.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getHashFromInvoice } from '../../src/utils'
import { validInvoices, invalidInvoices } from './helper'

describe('getHashFromInvoice', () => {
test.each(validInvoices)(
'decodes $invoice and get a valid hash',
({ invoice, hash }) => {
expect(getHashFromInvoice(invoice)).toBe(hash)
}
)

test.each(invalidInvoices)('%s return null', (invoice) => {
expect(getHashFromInvoice(invoice)).toBeFalsy()
})
})
31 changes: 31 additions & 0 deletions test/utils/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,34 @@ export const invalidLnUrls = [
`http://www.domain.com/pagina.html?lightning=abcd`,
`http://www.domain.com/pagina.html?lightning=lnurl`,
]

export const validInvoices = [
{
invoice:
'lnbcrt10u1pslcly8pp55ytcf99kf6t75vkfnewzgtgxav8dv8fyd0nxw3hfkfp8xtnhckgsdqqcqzpuxqyz5vqsp5zukyzq2lcflnz8es0djh2jyza087nkn8jvx0qdcqrlt4rfrcxlcq9qyyssqmvr8937qug6au92nz97p4z8tdggkhksq6yvclsfy6ts732l9kz28ctt9qhw8gwygfq8a3t73f5689lwff6ul2xfl7m77p7ks4gmzjtgqvnvqyv',
hash: 'a1178494b64e97ea32c99e5c242d06eb0ed61d246be66746e9b242732e77c591',
preimage:
'fbc30fd92d7829ed871f29fdeef71c652e1f697e22800b88e5297bdb57bdace0',
},
{
invoice:
'lntb13130n1psl9la5pp57pd5fud26tpk5a893yp4snl90mda5sns6uj8589422rw0fndz03qsp5umjxesrm440437ykfjykt2rst8wfdlegg32f0xq5k4val9zvz38sdqqcqzynxqyz5vq9qxpq9qsqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cnggqd7qqqq3qqqgqqqqlgqqqqqeqqjqtz9qwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cngqqqq05qqqqqvsqfqvu7u2ke04028p35n2xr9k23m53ggg6rjx045k2kpvysj0q6zhq74cxnh27xa3vdkqq0dypsfeyqys5mtcn0hz3hrdqvyl3n8tndhlpgpzchyh7',
hash: 'f05b44f1aad2c36a74e58903584fe57edbda4270d7247a1cb55286e7a66d13e2',
preimage:
'4cad8d163234461cb4de6c5ad35e5938f190df261065ef51b9708433466d0c3b',
},
{
invoice:
'lnbc10m1ps79mp5pp5eat0y56jcfym7eruu049280n9yssw5flmzvnp99up995zq4p2r4qdqqcqzpuxqyz5vqsp5r88jwj2er6nlj40748slw3zy3rwaydtu2x4s84yqmgq0w63fxjdq9qyyssqyc58k4lmayagad3upndzny07w777axtcfv4w9dj4q3tjvj877y99t2r7vgtxc3pw0xtk9j7mftvx72fg5m8rjnphcrkf3mnzzg8l6lcprw8apg',
hash: 'cf56f25352c249bf647ce3ea551df3292107513fd8993094bc094b4102a150ea',
preimage:
'b956cb683997640065bad032aa177e7609c6a6c6289b73c2bf6237cea32fa6c6',
},
]

export const invalidInvoices = [
'lnbcrt1pslcav8pp5y8lae09uqd69fv6mccgp7lp42j6nefmzlgfpxzm7289ddvvj43jsdqqcqzpuxqr23ssp50jylhv3wzfnfk2lsgrga0v2px742pnml9tcajfwdulqppeyg5hdq9qyyssq7hmj9wkzen6nnpv9040wxgpcx6dy0zdmkj7k539ll32ecjc6s9r4cj08cj05nszcmc3zkhnu52yhdsmajzmg2ueww7m7z5790vy3cugqn2zsd',
'lntb349820n1pslcagupp53qzw32u7s70s2uaejhtmpq76fr0lv80c28f7gtv3tw2vgv59n8wsdqqcqzpuxqyz5vqsp560xz2teaz74cltfmvpw6wmcqkrc5a6a0njy8n90w7ptfe42q2h4s9qyyssqdzu9da88zs6qdre3pqxdppls4xj2mnghv3facn2vztjvc2x6p3h9lann5hwx7a972ffna6zhzqrke7mz4cq7xt2d487g2x48pyjvlhsp9cuhy',
'lnbc1pslca2wpp5zrwrnh2unz02e2qve8ue26dd4l862vumxx0ry0j64tjljucw9jhqdqqcqzpuxqyz5vqsp58mdlrld5wnwhg06skyk4tcwdfyx6t9qk4apc4qka5ah0usduptrq9qyyssqj0rwcgeuz46l3q0jtyazjr9j9lhgq7mgf2xydpdf6cqjenpgaxur43f35c6g9nz634q7r2ze7s5ujqxlvq84wfvxesc78y7khhdy6wspkuesj',
'abc',
]
19 changes: 19 additions & 0 deletions test/utils/is-valid-preimage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { isValidPreimage } from '../../src/utils'
import { validInvoices, invalidInvoices } from './helper'

describe('isValidPreimage', () => {
test.each(validInvoices)(
'$preimage is valid for $invoice',
({ invoice, preimage }) => {
expect(isValidPreimage({ invoice, preimage })).toBeTruthy()
}
)

test.each(validInvoices)('%s return false', ({ invoice }) => {
expect(isValidPreimage({ invoice, preimage: 'some preimage' })).toBeFalsy()
})

test.each(invalidInvoices)('%s return false', (invoice) => {
expect(isValidPreimage({ invoice, preimage: 'some preimage' })).toBeFalsy()
})
})
Loading

0 comments on commit 2d205e1

Please sign in to comment.