diff --git a/api/paidAction/index.js b/api/paidAction/index.js index a5c5b0d74..714a96e3d 100644 --- a/api/paidAction/index.js +++ b/api/paidAction/index.js @@ -5,7 +5,7 @@ import { createHmac } from '@/api/resolvers/wallet' import { Prisma } from '@prisma/client' import { createWrappedInvoice, createInvoice as createUserInvoice } from '@/wallets/server' import { assertBelowMaxPendingInvoices, assertBelowMaxPendingDirectPayments } from './lib/assert' -import { parseBolt11 } from '@/lib/invoices' +import { parseBolt11 } from '@/lib/bolt11' import * as ITEM_CREATE from './itemCreate' import * as ITEM_UPDATE from './itemUpdate' diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index f5db3d3b3..618fa85f3 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -12,7 +12,8 @@ import { import { amountSchema, validateSchema, withdrawlSchema, lnAddrSchema } from '@/lib/validate' import assertGofacYourself from './ofac' import assertApiKeyNotPermitted from './apiKey' -import { bolt11Tags } from '@/lib/bolt11' +import { bolt11Info, isBolt11 } from '@/lib/bolt11-info' +import { bolt12Info } from '@/lib/bolt12-info' import { finalizeHodlInvoice } from '@/worker/wallet' import walletDefs from '@/wallets/server' import { generateResolverName, generateTypeDefName } from '@/wallets/graphql' @@ -368,7 +369,7 @@ const resolvers = { f = { ...f, ...f.other } if (f.bolt11) { - f.description = bolt11Tags(f.bolt11).description + f.description = isBolt11(f.bolt11) ? bolt11Info(f.bolt11).description : bolt12Info(f.bolt11).description } switch (f.type) { diff --git a/components/bolt11-info.js b/components/bolt11-info.js index 1dd4dff87..81ff8fe44 100644 --- a/components/bolt11-info.js +++ b/components/bolt11-info.js @@ -1,11 +1,12 @@ import AccordianItem from './accordian-item' import { CopyInput } from './form' -import { bolt11Tags } from '@/lib/bolt11' +import { bolt11Info, isBolt11 } from '@/lib/bolt11-info' +import { bolt12Info } from '@/lib/bolt12-info' export default ({ bolt11, preimage, children }) => { let description, paymentHash if (bolt11) { - ({ description, payment_hash: paymentHash } = bolt11Tags(bolt11)) + ({ description, payment_hash: paymentHash } = isBolt11(bolt11) ? bolt11Info(bolt11) : bolt12Info(bolt11)) } return ( diff --git a/lib/bolt11-info.js b/lib/bolt11-info.js new file mode 100644 index 000000000..498aa139f --- /dev/null +++ b/lib/bolt11-info.js @@ -0,0 +1,10 @@ +import { decode } from 'bolt11' + +export function isBolt11 (request) { + return request.startsWith('lnbc') || request.startsWith('lntb') || request.startsWith('lntbs') || request.startsWith('lnbcrt') +} + +export function bolt11Info (bolt11) { + if (!isBolt11(bolt11)) throw new Error('not a bolt11 invoice') + return decode(bolt11).tagsObject +} diff --git a/lib/bolt11.js b/lib/bolt11.js index f04770167..c616e7f9b 100644 --- a/lib/bolt11.js +++ b/lib/bolt11.js @@ -1,5 +1,21 @@ -import { decode } from 'bolt11' +/* eslint-disable camelcase */ +import { payViaPaymentRequest, parsePaymentRequest } from 'ln-service' -export function bolt11Tags (bolt11) { - return decode(bolt11).tagsObject +export function isBolt11 (request) { + return request.startsWith('lnbc') || request.startsWith('lntb') || request.startsWith('lntbs') || request.startsWith('lnbcrt') +} + +export async function parseBolt11 ({ request }) { + if (!isBolt11(request)) throw new Error('not a bolt11 invoice') + return parsePaymentRequest({ request }) +} + +export async function payBolt11 ({ lnd, request, max_fee, ...args }) { + if (!isBolt11(request)) throw new Error('not a bolt11 invoice') + return payViaPaymentRequest({ + lnd, + request, + max_fee, + ...args + }) } diff --git a/lib/bolt12-info.js b/lib/bolt12-info.js new file mode 100644 index 000000000..7b98bab23 --- /dev/null +++ b/lib/bolt12-info.js @@ -0,0 +1,28 @@ +import { deserializeTLVStream } from './tlv' +import * as bech32b12 from '@/lib/bech32b12' + +export function isBolt12 (invoice) { + return invoice.startsWith('lni1') || invoice.startsWith('lno1') +} + +export function bolt12Info (bolt12) { + if (!isBolt12(bolt12)) throw new Error('not a bolt12 invoice or offer') + const buf = bech32b12.decode(bolt12.substring(4)/* remove lni1 or lno1 prefix */) + const tlv = deserializeTLVStream(buf) + const INFO_TYPES = { + description: 10n, + payment_hash: 168n + } + const info = { + description: '', + payment_hash: '' + } + for (const { type, value } of tlv) { + if (type === INFO_TYPES.description) { + info.description = value.toString() + } else if (type === INFO_TYPES.payment_hash) { + info.payment_hash = value.toString('hex') + } + } + return info +} diff --git a/lib/bolt12.js b/lib/bolt12.js new file mode 100644 index 000000000..f6af6a164 --- /dev/null +++ b/lib/bolt12.js @@ -0,0 +1,25 @@ +/* eslint-disable camelcase */ + +import { payViaBolt12PaymentRequest, parseBolt12Request } from '@/lib/lndk' + +export function isBolt12Offer (invoice) { + return invoice.startsWith('lno1') +} + +export function isBolt12Invoice (invoice) { + return invoice.startsWith('lni1') +} + +export function isBolt12 (invoice) { + return isBolt12Offer(invoice) || isBolt12Invoice(invoice) +} + +export async function payBolt12 ({ lnd, request: invoice, max_fee }) { + if (!isBolt12Invoice(invoice)) throw new Error('not a bolt12 invoice') + return await payViaBolt12PaymentRequest({ lnd, request: invoice, max_fee }) +} + +export function parseBolt12 ({ lnd, request: invoice }) { + if (!isBolt12Invoice(invoice)) throw new Error('not a bolt12 request') + return parseBolt12Request({ lnd, request: invoice }) +} diff --git a/lib/invoices.js b/lib/invoices.js index 64d21d6d8..bf7a3697d 100644 --- a/lib/invoices.js +++ b/lib/invoices.js @@ -1,52 +1,9 @@ /* eslint-disable camelcase */ - -import { payViaPaymentRequest, parsePaymentRequest } from 'ln-service' +import { payBolt12, parseBolt12, isBolt12Invoice } from './bolt12' +import { payBolt11, parseBolt11 } from './bolt11' +import { estimateBolt12RouteFee } from '@/lib/lndk' import { estimateRouteFee } from '@/api/lnd' -import { payViaBolt12PaymentRequest, parseBolt12Request, estimateBolt12RouteFee } from '@/lib/lndk' - -export function isBolt11 (request) { - return request.startsWith('lnbc') || request.startsWith('lntb') || request.startsWith('lntbs') || request.startsWith('lnbcrt') -} - -export function parseBolt11 ({ request }) { - if (!isBolt11(request)) throw new Error('not a bolt11 invoice') - return parsePaymentRequest({ request }) -} - -export function payBolt11 ({ lnd, request, max_fee, ...args }) { - if (!isBolt11(request)) throw new Error('not a bolt11 invoice') - - return payViaPaymentRequest({ - lnd, - request, - max_fee, - ...args - }) -} - -export function isBolt12Offer (invoice) { - return invoice.startsWith('lno1') -} - -export function isBolt12Invoice (invoice) { - console.log('isBolt12Invoice', invoice) - console.trace() - return invoice.startsWith('lni1') -} - -export async function payBolt12 ({ lnd, request: invoice, max_fee }) { - if (!isBolt12Invoice(invoice)) throw new Error('not a bolt12 invoice') - - if (!invoice) throw new Error('No invoice in bolt12, please use prefetchBolt12Invoice') - return await payViaBolt12PaymentRequest({ lnd, request: invoice, max_fee }) -} - -export function parseBolt12 ({ lnd, request: invoice }) { - if (!isBolt12Invoice(invoice)) throw new Error('not a bolt12 request') - return parseBolt12Request({ lnd, request: invoice }) -} - export async function payInvoice ({ lnd, request: invoice, max_fee, ...args }) { if (isBolt12Invoice(invoice)) { return await payBolt12({ lnd, request: invoice, max_fee, ...args }) diff --git a/lib/lndk.js b/lib/lndk.js index fb3502470..f46ddaa64 100644 --- a/lib/lndk.js +++ b/lib/lndk.js @@ -121,36 +121,7 @@ const chainsMap = { '43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000': 'testnet', '6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000': 'mainnet' } -// @returns -// { -// [chain_addresses]: [] -// cltv_delta: -// created_at: -// [description]: -// [description_hash]: -// destination: -// expires_at: -// features: [{ -// bit: -// is_required: -// type: -// }] -// id: -// is_expired: -// [metadata]: -// [mtokens]: (can exceed Number limit) -// network: -// [payment]: -// [routes]: [[{ -// [base_fee_mtokens]: -// [channel]: -// [cltv_delta]: -// [fee_rate]: -// public_key: -// }]] -// [safe_tokens]: -// [tokens]: (note: can differ from mtokens) -// } + export async function parseBolt12Request ({ lnd, request diff --git a/lib/tlv.js b/lib/tlv.js new file mode 100644 index 000000000..a62ced692 --- /dev/null +++ b/lib/tlv.js @@ -0,0 +1,36 @@ +export function deserializeTLVStream (buff) { + const tlvs = [] + let bytePos = 0 + while (bytePos < buff.length) { + const [type, typeLength] = readBigSize(buff, bytePos) + bytePos += typeLength + + let [length, lengthLength] = readBigSize(buff, bytePos) + length = Number(length) + bytePos += lengthLength + + if (bytePos + length > buff.length) { + throw new Error('invalid tlv stream') + } + + const value = buff.subarray(bytePos, bytePos + length) + bytePos += length + + tlvs.push({ type, length, value }) + } + return tlvs +} + +function readBigSize (buf, offset) { + if (buf[offset] <= 252) { + return [BigInt(buf[offset]), 1] + } else if (buf[offset] === 253) { + return [BigInt(buf.readUInt16BE(offset + 1)), 3] + } else if (buf[offset] === 254) { + return [BigInt(buf.readUInt32BE(offset + 1)), 5] + } else if (buf[offset] === 255) { + return [buf.readBigUInt64BE(offset + 1), 9] + } else { + throw new Error('Invalid bigsize') + } +} diff --git a/wallets/server.js b/wallets/server.js index 172e6cf52..b6eb0584b 100644 --- a/wallets/server.js +++ b/wallets/server.js @@ -14,7 +14,8 @@ import * as webln from '@/wallets/webln' import { walletLogger } from '@/api/resolvers/wallet' import walletDefs from '@/wallets/server' -import { isBolt12Offer, parseInvoice } from '@/lib/invoices' +import { parseInvoice } from '@/lib/invoices' +import { isBolt12Offer } from '@/lib/bolt12' import { toPositiveBigInt, toPositiveNumber, formatMsats, formatSats, msatsToSats } from '@/lib/format' import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants' import { withTimeout } from '@/lib/time'