From e803efe17585c8278145fc940f394c04f5a0087d Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 17 Dec 2024 01:06:05 +0100 Subject: [PATCH 01/42] bolt12 attachment --- .env.development | 7 + api/lnd/index.js | 6 + api/paidAction/index.js | 8 +- api/payingAction/index.js | 6 +- api/resolvers/wallet.js | 15 +- docker/lndk/Dockerfile | 6 +- fragments/wallet.js | 3 + lib/bech32b12.js | 46 +++ lib/invoices.js | 72 +++++ lib/lndk.js | 268 ++++++++++++++++++ lib/lndkrpc-proto.js | 142 ++++++++++ .../migration.sql | 25 ++ prisma/schema.prisma | 11 + wallets/bolt12/client.js | 1 + wallets/bolt12/index.js | 23 ++ wallets/bolt12/server.js | 11 + wallets/client.js | 3 +- wallets/server.js | 79 ++++-- wallets/wrap.js | 11 +- worker/autowithdraw.js | 2 +- worker/index.js | 6 + worker/paidAction.js | 12 +- 22 files changed, 704 insertions(+), 59 deletions(-) create mode 100644 lib/bech32b12.js create mode 100644 lib/invoices.js create mode 100644 lib/lndk.js create mode 100644 lib/lndkrpc-proto.js create mode 100644 prisma/migrations/20241212160430_bolt12_attachment/migration.sql create mode 100644 wallets/bolt12/client.js create mode 100644 wallets/bolt12/index.js create mode 100644 wallets/bolt12/server.js diff --git a/.env.development b/.env.development index 419252140..0b80c9760 100644 --- a/.env.development +++ b/.env.development @@ -61,6 +61,13 @@ LND_CERT=2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494943516a434 LND_MACAROON=0201036c6e6402f801030a106cf4e146abffa5d766befbbf4c73b5a31201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e6572617465120472656164000006202c3bfd55c191e925cbffd73712c9d4b9b4a8440410bde5f8a0a6e33af8b3d876 LND_SOCKET=sn_lnd:10009 +# xxd -p -c0 docker/lndk/tls-cert.pem +LNDK_CERT=2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494942614443434151326741774942416749554f6d7333785a2b704256556e746e4644374a306d374c6c314d5a5977436759494b6f5a497a6a3045417749770a495445664d4230474131554541777757636d4e6e5a573467633256735a69427a615764755a5751675932567964444167467730334e5441784d4445774d4441770a4d444261474138304d446b324d4445774d5441774d4441774d466f77495445664d4230474131554541777757636d4e6e5a573467633256735a69427a615764750a5a575167593256796444425a4d424d4742797147534d34394167454743437147534d3439417745484130494142476475396358554753504979635343626d47620a362f34552b74787645306153767a734d632b704b4669586c422b502f33782f5778594d786c4842306c68396654515538746456694a3241592f516e485677556b0a4f34436a495441664d42304741315564455151574d42534343577876593246736147397a64494948633235666247356b617a414b42676771686b6a4f505151440a41674e4a41444247416945413738556450486764615856797474717432312b7557546c466e344236717565474c2f636d5970516269497343495143777859306e0a783276357a58457750552f624f6e61514e657139463841542b2f346c4b656c48664f4e2f47773d3d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a +LNDK_MACAROON=0201036c6e6402f801030a106cf4e146abffa5d766befbbf4c73b5a31201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e6572617465120472656164000006202c3bfd55c191e925cbffd73712c9d4b9b4a8440410bde5f8a0a6e33af8b3d876 +LNDK_SOCKET=sn_lndk:7000 + + + # nostr (NIP-57 zap receipts) # openssl rand -hex 32 NOSTR_PRIVATE_KEY=5f30b7e7714360f51f2be2e30c1d93b7fdf67366e730658e85777dfcc4e4245f diff --git a/api/lnd/index.js b/api/lnd/index.js index f09996288..ae347714c 100644 --- a/api/lnd/index.js +++ b/api/lnd/index.js @@ -1,6 +1,7 @@ import { cachedFetcher } from '@/lib/fetch' import { toPositiveNumber } from '@/lib/format' import { authenticatedLndGrpc } from '@/lib/lnd' +import { installLNDK } from '@/lib/lndk' import { getIdentity, getHeight, getWalletInfo, getNode, getPayment } from 'ln-service' import { datePivot } from '@/lib/time' import { LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants' @@ -10,6 +11,11 @@ const lnd = global.lnd || authenticatedLndGrpc({ macaroon: process.env.LND_MACAROON, socket: process.env.LND_SOCKET }).lnd +installLNDK(lnd, { + cert: process.env.LNDK_CERT, + macaroon: process.env.LNDK_MACAROON, + socket: process.env.LNDK_SOCKET +}) if (process.env.NODE_ENV === 'development') global.lnd = lnd diff --git a/api/paidAction/index.js b/api/paidAction/index.js index 7e65c4eb0..cb97fbf5c 100644 --- a/api/paidAction/index.js +++ b/api/paidAction/index.js @@ -1,10 +1,11 @@ -import { createHodlInvoice, createInvoice, parsePaymentRequest } from 'ln-service' +import { createHodlInvoice, createInvoice } from 'ln-service' import { datePivot } from '@/lib/time' import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants' 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 * as ITEM_CREATE from './itemCreate' import * as ITEM_UPDATE from './itemUpdate' @@ -271,7 +272,7 @@ async function performDirectAction (actionType, args, incomingContext) { } const { invoice, wallet } = invoiceObject - const hash = parsePaymentRequest({ request: invoice }).id + const hash = await parseBolt11({ request: invoice }).id // direct payments are always to bolt11 invoices const payment = await models.directPayment.create({ data: { @@ -419,8 +420,9 @@ async function createDbInvoice (actionType, args, context) { throw new Error('The cost of the action must be at least 1 sat') } + // note: served invoice is always bolt11 const servedBolt11 = wrappedBolt11 ?? bolt11 - const servedInvoice = parsePaymentRequest({ request: servedBolt11 }) + const servedInvoice = await parseBolt11({ request: servedBolt11 }) const expiresAt = new Date(servedInvoice.expires_at) const invoiceData = { diff --git a/api/payingAction/index.js b/api/payingAction/index.js index 2ff7117a7..731733a15 100644 --- a/api/payingAction/index.js +++ b/api/payingAction/index.js @@ -1,7 +1,7 @@ import { LND_PATHFINDING_TIME_PREF_PPM, LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants' import { msatsToSats, satsToMsats, toPositiveBigInt } from '@/lib/format' import { Prisma } from '@prisma/client' -import { parsePaymentRequest, payViaPaymentRequest } from 'ln-service' +import { payInvoice, parseInvoice } from '@/lib/invoices' // paying actions are completely distinct from paid actions // and there's only one paying action: send @@ -14,7 +14,7 @@ export default async function performPayingAction ({ bolt11, maxFee, walletId }, throw new Error('You must be logged in to perform this action') } - const decoded = await parsePaymentRequest({ request: bolt11 }) + const decoded = await parseInvoice({ request: bolt11, lnd }) const cost = toPositiveBigInt(toPositiveBigInt(decoded.mtokens) + satsToMsats(maxFee)) console.log('cost', cost) @@ -40,7 +40,7 @@ export default async function performPayingAction ({ bolt11, maxFee, walletId }, }) }, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted }) - payViaPaymentRequest({ + payInvoice({ lnd, request: withdrawal.bolt11, max_fee: msatsToSats(withdrawal.msatsFeePaying), diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index a794eb4be..f5db3d3b3 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -1,6 +1,5 @@ import { - getInvoice as getInvoiceFromLnd, deletePayment, getPayment, - parsePaymentRequest + getInvoice as getInvoiceFromLnd, deletePayment, getPayment } from 'ln-service' import crypto, { timingSafeEqual } from 'crypto' import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor' @@ -24,6 +23,8 @@ import validateWallet from '@/wallets/validate' import { canReceive } from '@/wallets/common' import performPaidAction from '../paidAction' import performPayingAction from '../payingAction' +import { parseInvoice } from '@/lib/invoices' +import lnd from '@/api/lnd' function injectResolvers (resolvers) { console.group('injected GraphQL resolvers:') @@ -721,8 +722,8 @@ export const walletLogger = ({ wallet, models }) => { const log = (level) => async (message, context = {}) => { try { if (context?.bolt11) { - // automatically populate context from bolt11 to avoid duplicating this code - const decoded = await parsePaymentRequest({ request: context.bolt11 }) + // automatically populate context from invoice to avoid duplicating this code + const decoded = await parseInvoice({ request: context.bolt11, lnd }) context = { ...context, amount: formatMsats(decoded.mtokens), @@ -899,7 +900,7 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model // decode invoice to get amount let decoded, sockets try { - decoded = await parsePaymentRequest({ request: invoice }) + decoded = await parseInvoice({ request: invoice, lnd }) } catch (error) { console.log(error) throw new GqlInputError('could not decode invoice') @@ -938,7 +939,7 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model throw new GqlInputError('SN cannot pay an invoice that SN is proxying') } - return await performPayingAction({ bolt11: invoice, maxFee, walletId: wallet?.id }, { me, models, lnd }) + return await performPayingAction({ invoice, maxFee, walletId: wallet?.id }, { me, models, lnd }) } export async function sendToLnAddr (parent, { addr, amount, maxFee, comment, ...payer }, @@ -999,7 +1000,7 @@ export async function fetchLnAddrInvoice ( // decode invoice try { - const decoded = await parsePaymentRequest({ request: res.pr }) + const decoded = await parseInvoice({ request: res.pr, lnd }) const ourPubkey = await getOurPubkey({ lnd }) if (autoWithdraw && decoded.destination === ourPubkey && process.env.NODE_ENV === 'production') { // unset lnaddr so we don't trigger another withdrawal with same destination diff --git a/docker/lndk/Dockerfile b/docker/lndk/Dockerfile index a421053a6..75dc3f443 100644 --- a/docker/lndk/Dockerfile +++ b/docker/lndk/Dockerfile @@ -1,8 +1,10 @@ # This image uses fedora 40 because the official pre-built lndk binaries require # glibc 2.39 which is not available on debian or ubuntu images. FROM fedora:40 -RUN useradd -u 1000 -m lndk +ENV INSTALLER_DOWNLOAD_URL="https://github.com/riccardobl/lndk/releases/download/v0.2.0-maxfee" + +RUN useradd -u 1000 -m lndk RUN mkdir -p /home/lndk/.lndk COPY ["./tls-*", "/home/lndk/.lndk"] RUN chown 1000:1000 -Rvf /home/lndk/.lndk && \ @@ -10,7 +12,7 @@ RUN chown 1000:1000 -Rvf /home/lndk/.lndk && \ chmod 600 /home/lndk/.lndk/tls-key.pem USER lndk -RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/lndk-org/lndk/releases/download/v0.2.0/lndk-installer.sh | sh +RUN curl --proto '=https' --tlsv1.2 -LsSf $INSTALLER_DOWNLOAD_URL/lndk-installer.sh | sh RUN echo 'source /home/lndk/.cargo/env' >> $HOME/.bashrc WORKDIR /home/lndk EXPOSE 7000 diff --git a/fragments/wallet.js b/fragments/wallet.js index 6f84f4afd..e1b7a288a 100644 --- a/fragments/wallet.js +++ b/fragments/wallet.js @@ -169,6 +169,9 @@ export const WALLET_FIELDS = gql` apiKeyRecv currencyRecv } + ... on WalletBolt12 { + offer + } } } ` diff --git a/lib/bech32b12.js b/lib/bech32b12.js new file mode 100644 index 000000000..611515e06 --- /dev/null +++ b/lib/bech32b12.js @@ -0,0 +1,46 @@ +const ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' + +export function decode (str) { + const b5s = [] + for (const char of str) { + const i = ALPHABET.indexOf(char) + if (i === -1) throw new Error('Invalid bech32 character') + b5s.push(i) + } + const b8s = Buffer.from(converBits(b5s, 5, 8, false)) + return b8s +} + +export function encode (b8s) { + const b5s = converBits(b8s, 8, 5, true) + const str = [] + for (const b5 of b5s) str.push(ALPHABET[b5]) + return str.join('') +} + +function converBits (data, frombits, tobits, pad) { + let acc = 0 + let bits = 0 + const ret = [] + const maxv = (1 << tobits) - 1 + for (let p = 0; p < data.length; ++p) { + const value = data[p] + if (value < 0 || (value >> frombits) !== 0) { + throw new RangeError('input value is outside of range') + } + acc = (acc << frombits) | value + bits += frombits + while (bits >= tobits) { + bits -= tobits + ret.push((acc >> bits) & maxv) + } + } + if (pad) { + if (bits > 0) { + ret.push((acc << (tobits - bits)) & maxv) + } + } else if (bits >= frombits || ((acc << (tobits - bits)) & maxv)) { + throw new RangeError('could not convert bits') + } + return ret +} diff --git a/lib/invoices.js b/lib/invoices.js new file mode 100644 index 000000000..64d21d6d8 --- /dev/null +++ b/lib/invoices.js @@ -0,0 +1,72 @@ +/* eslint-disable camelcase */ + +import { payViaPaymentRequest, parsePaymentRequest } from 'ln-service' +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 }) + } else { + return await payBolt11({ lnd, request: invoice, max_fee, ...args }) + } +} + +export async function parseInvoice ({ lnd, request }) { + if (isBolt12Invoice(request)) { + return await parseBolt12({ lnd, request }) + } else { + return await parseBolt11({ request }) + } +} + +export async function estimateFees ({ lnd, destination, tokens, mtokens, request, timeout }) { + if (isBolt12Invoice(request)) { + return await estimateBolt12RouteFee({ lnd, destination, tokens, mtokens, request, timeout }) + } else { + return await estimateRouteFee({ lnd, destination, tokens, request, mtokens, timeout }) + } +} diff --git a/lib/lndk.js b/lib/lndk.js new file mode 100644 index 000000000..fb3502470 --- /dev/null +++ b/lib/lndk.js @@ -0,0 +1,268 @@ +import { msatsToSats, toPositiveNumber } from '@/lib/format' +import { loadPackageDefinition } from '@grpc/grpc-js' +import LNDK_RPC_PROTO from '@/lib/lndkrpc-proto' +import protobuf from 'protobufjs' +import grpcCredentials from 'lightning/lnd_grpc/grpc_credentials' +import { defaultSocket, grpcSslCipherSuites } from 'lightning/grpc/index' +import { fromJSON } from '@grpc/proto-loader' +import * as bech32b12 from '@/lib/bech32b12' + +/* eslint-disable camelcase */ +const { GRPC_SSL_CIPHER_SUITES } = process.env + +export function installLNDK (lnd, { cert, macaroon, socket }, withProxy) { + if (lnd.lndk) return // already installed + + // workaround to load from string + const protoArgs = { + keepCase: true, + longs: Number, + defaults: true, + oneofs: true + } + const proto = protobuf.parse(LNDK_RPC_PROTO, protoArgs).root + const packageDefinition = fromJSON(proto.toJSON(), protoArgs) + + const protoDescriptor = loadPackageDefinition(packageDefinition) + const OffersService = protoDescriptor.lndkrpc.Offers + const { credentials } = grpcCredentials({ cert, macaroon }) + const params = { + 'grpc.max_receive_message_length': -1, + 'grpc.max_send_message_length': -1, + 'grpc.enable_http_proxy': withProxy ? 1 : 0 + } + const lndSocket = socket || defaultSocket + + if (!!cert && GRPC_SSL_CIPHER_SUITES !== grpcSslCipherSuites) { + process.env.GRPC_SSL_CIPHER_SUITES = grpcSslCipherSuites + } + + const client = new OffersService(lndSocket, credentials, params) + lnd.lndk = client +} + +export async function fetchBolt12InvoiceFromOffer ({ lnd, offer, amount, description, timeout = 10_000 }) { + const lndk = lnd.lndk + if (!lndk) throw new Error('lndk not installed, please use installLNDK') + return new Promise((resolve, reject) => { + lndk.GetInvoice({ + offer, + amount: toPositiveNumber(amount), + payer_note: description, + response_invoice_timeout: timeout + }, (error, response) => { + if (error) return reject(error) + const bech32invoice = 'lni1' + bech32b12.encode(Buffer.from(response.invoice_hex_str, 'hex')) + resolve(bech32invoice) + }) + }) +} + +export async function payViaBolt12PaymentRequest ({ + lnd, + request: invoice_hex_str, + max_fee +}) { + const lndk = lnd.lndk + if (!lndk) throw new Error('lndk not installed, please use installLNDK') + + const bolt12 = await parseBolt12Request({ lnd, request: invoice_hex_str }) + + const req = { + invoice: bolt12.payment, + amount: toPositiveNumber(bolt12.mtokens), + max_fee + } + + return new Promise((resolve, reject) => { + lndk.PayInvoice(req, (error, response) => { + if (error) { + return reject(error) + } + resolve({ + secret: response.payment_preimage + }) + }) + }) +} + +const featureBitMap = { + 0: { bit: 0, type: 'DATALOSS_PROTECT_REQ', is_required: true }, + 1: { bit: 1, type: 'DATALOSS_PROTECT_OPT', is_required: false }, + 3: { bit: 3, type: 'INITIAL_ROUTING_SYNC', is_required: true }, + 4: { bit: 4, type: 'UPFRONT_SHUTDOWN_SCRIPT_REQ', is_required: true }, + 5: { bit: 5, type: 'UPFRONT_SHUTDOWN_SCRIPT_OPT', is_required: false }, + 6: { bit: 6, type: 'GOSSIP_QUERIES_REQ', is_required: true }, + 7: { bit: 7, type: 'GOSSIP_QUERIES_OPT', is_required: false }, + 8: { bit: 8, type: 'TLV_ONION_REQ', is_required: true }, + 9: { bit: 9, type: 'TLV_ONION_OPT', is_required: false }, + 10: { bit: 10, type: 'EXT_GOSSIP_QUERIES_REQ', is_required: true }, + 11: { bit: 11, type: 'EXT_GOSSIP_QUERIES_OPT', is_required: false }, + 12: { bit: 12, type: 'STATIC_REMOTE_KEY_REQ', is_required: true }, + 13: { bit: 13, type: 'STATIC_REMOTE_KEY_OPT', is_required: false }, + 14: { bit: 14, type: 'PAYMENT_ADDR_REQ', is_required: true }, + 15: { bit: 15, type: 'PAYMENT_ADDR_OPT', is_required: false }, + 16: { bit: 16, type: 'MPP_REQ', is_required: true }, + 17: { bit: 17, type: 'MPP_OPT', is_required: false }, + 18: { bit: 18, type: 'WUMBO_CHANNELS_REQ', is_required: true }, + 19: { bit: 19, type: 'WUMBO_CHANNELS_OPT', is_required: false }, + 20: { bit: 20, type: 'ANCHORS_REQ', is_required: true }, + 21: { bit: 21, type: 'ANCHORS_OPT', is_required: false }, + 22: { bit: 22, type: 'ANCHORS_ZERO_FEE_HTLC_REQ', is_required: true }, + 23: { bit: 23, type: 'ANCHORS_ZERO_FEE_HTLC_OPT', is_required: false }, + 24: { bit: 24, type: 'ROUTE_BLINDING_REQUIRED', is_required: true }, + 25: { bit: 25, type: 'ROUTE_BLINDING_OPTIONAL', is_required: false }, + 30: { bit: 30, type: 'AMP_REQ', is_required: true }, + 31: { bit: 31, type: 'AMP_OPT', is_required: false } +} + +const chainsMap = { + '06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f': 'regtest', + '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 +}) { + const lndk = lnd?.lndk + if (!lndk) throw new Error('lndk not installed, please use installLNDK') + + const invoice_hex_str = request.startsWith('lni1') ? bech32b12.decode(request.slice(4)).toString('hex') : request + + const invoice_contents = await new Promise((resolve, reject) => { + lndk.DecodeInvoice({ + invoice: invoice_hex_str + }, (error, response) => { + if (error) return reject(error) + resolve(response) + }) + }) + + const { + amount_msats, + description, + node_id, + chain, + payment_hash, + created_at, + relative_expiry, + features + } = invoice_contents + + // convert from lndk response to ln-service parsePaymentRequest output layout + let minCltvDelta + for (const path of invoice_contents.payment_paths) { + const info = path.blinded_pay_info + if (minCltvDelta === undefined || info.cltv_expiry_delta < minCltvDelta) { + minCltvDelta = info.cltv_expiry_delta + } + } + + const out = { + created_at: new Date(created_at * 1000).toISOString(), + // [chain_addresses] + cltv_delta: minCltvDelta, + description, + // [description_hash] + destination: Buffer.from(node_id.key).toString('hex'), + expires_at: new Date((created_at + relative_expiry) * 1000).toISOString(), + features: features.map(bit => featureBitMap[bit]), + id: Buffer.from(payment_hash.hash).toString('hex'), + is_expired: new Date().getTime() / 1000 > created_at + relative_expiry, + // [metadata] + mtokens: '' + amount_msats, + network: chainsMap[chain], + payment: invoice_hex_str, + routes: invoice_contents.payment_paths.map((path) => { + const info = path.blinded_pay_info + const { introduction_node } = path.blinded_path + return { + base_fee_mtokens: '' + info.fee_base_msat, + cltv_delta: info.cltv_expiry_delta, + public_key: Buffer.from(introduction_node.node_id.key).toString('hex') + } + }), + safe_tokens: Math.round(toPositiveNumber(BigInt(amount_msats)) / 1000), + tokens: Math.floor(toPositiveNumber(BigInt(amount_msats)) / 1000) + } + + // mark as bolt12 invoice so we can differentiate it later (this will be used also to pass bolt12 only data) + out.bolt12 = invoice_contents + + return out +} + +export async function estimateBolt12RouteFee ({ lnd, destination, tokens, mtokens, request, timeout }) { + const lndk = lnd?.lndk + if (!lndk) throw new Error('lndk not installed, please use installLNDK') + const parsedInvoice = request ? await parseBolt12Request({ lnd, request }) : {} + mtokens ??= parsedInvoice.mtokens + destination ??= parsedInvoice.destination + + return await new Promise((resolve, reject) => { + const params = {} + params.dest = Buffer.from(destination, 'hex') + params.amt_sat = null + if (tokens) params.amt_sat = toPositiveNumber(tokens) + else if (mtokens) params.amt_sat = msatsToSats(mtokens) + + if (params.amt_sat === null) { + throw new Error('No tokens or mtokens provided') + } + + lnd.router.estimateRouteFee({ + ...params, + timeout + }, (err, res) => { + if (err) { + if (res?.failure_reason) { + reject(new Error(`Unable to estimate route: ${res.failure_reason}`)) + } else { + reject(err) + } + return + } + + if (res.routing_fee_msat < 0 || res.time_lock_delay <= 0) { + reject(new Error('Unable to estimate route, excessive values: ' + JSON.stringify(res))) + return + } + + resolve({ + routingFeeMsat: toPositiveNumber(res.routing_fee_msat), + timeLockDelay: toPositiveNumber(res.time_lock_delay) + }) + }) + }) +} diff --git a/lib/lndkrpc-proto.js b/lib/lndkrpc-proto.js new file mode 100644 index 000000000..c4d42db73 --- /dev/null +++ b/lib/lndkrpc-proto.js @@ -0,0 +1,142 @@ +export default ` +syntax = "proto3"; +package lndkrpc; + +service Offers { + rpc PayOffer (PayOfferRequest) returns (PayOfferResponse); + rpc GetInvoice (GetInvoiceRequest) returns (GetInvoiceResponse); + rpc DecodeInvoice (DecodeInvoiceRequest) returns (Bolt12InvoiceContents); + rpc PayInvoice (PayInvoiceRequest) returns (PayInvoiceResponse); +} + +message PayOfferRequest { + string offer = 1; + optional uint64 amount = 2; + optional string payer_note = 3; + optional uint32 response_invoice_timeout = 4; + optional uint64 max_fee = 5; +} + +message PayOfferResponse { + string payment_preimage = 2; +} + +message GetInvoiceRequest { + string offer = 1; + optional uint64 amount = 2; + optional string payer_note = 3; + optional uint32 response_invoice_timeout = 4; +} + +message DecodeInvoiceRequest { + string invoice = 1; +} + +message GetInvoiceResponse { + string invoice_hex_str = 1; + Bolt12InvoiceContents invoice_contents = 2; +} + +message PayInvoiceRequest { + string invoice = 1; + optional uint64 amount = 2; + optional uint64 max_fee = 3; +} + +message PayInvoiceResponse { + string payment_preimage = 1; +} + +message Bolt12InvoiceContents { + string chain = 1; + optional uint64 quantity = 2; + uint64 amount_msats = 3; + optional string description = 4; + PaymentHash payment_hash = 5; + repeated PaymentPaths payment_paths = 6; + int64 created_at = 7; + uint64 relative_expiry = 8; + PublicKey node_id = 9; + string signature = 10; + repeated FeatureBit features = 11; + optional string payer_note = 12; +} + +message PaymentHash { + bytes hash = 1; +} + +message PublicKey { + bytes key = 1; +} + +message BlindedPayInfo { + uint32 fee_base_msat = 1; + uint32 fee_proportional_millionths = 2; + uint32 cltv_expiry_delta = 3; + uint64 htlc_minimum_msat = 4; + uint64 htlc_maximum_msat = 5; + repeated FeatureBit features = 6; +} + +message BlindedHop { + PublicKey blinded_node_id = 1; + bytes encrypted_payload = 2; +} + +message BlindedPath { + IntroductionNode introduction_node = 1; + PublicKey blinding_point = 2; + repeated BlindedHop blinded_hops = 3; +} + +message PaymentPaths { + BlindedPayInfo blinded_pay_info = 1; + BlindedPath blinded_path = 2; +} + +message IntroductionNode { + optional PublicKey node_id = 1; + optional DirectedShortChannelId directed_short_channel_id = 2; +} + +message DirectedShortChannelId { + Direction direction = 1; + uint64 scid = 2; +} + +enum Direction { + NODE_ONE = 0; + NODE_TWO = 1; +} + +enum FeatureBit { + DATALOSS_PROTECT_REQ = 0; + DATALOSS_PROTECT_OPT = 1; + INITIAL_ROUING_SYNC = 3; + UPFRONT_SHUTDOWN_SCRIPT_REQ = 4; + UPFRONT_SHUTDOWN_SCRIPT_OPT = 5; + GOSSIP_QUERIES_REQ = 6; + GOSSIP_QUERIES_OPT = 7; + TLV_ONION_REQ = 8; + TLV_ONION_OPT = 9; + EXT_GOSSIP_QUERIES_REQ = 10; + EXT_GOSSIP_QUERIES_OPT = 11; + STATIC_REMOTE_KEY_REQ = 12; + STATIC_REMOTE_KEY_OPT = 13; + PAYMENT_ADDR_REQ = 14; + PAYMENT_ADDR_OPT = 15; + MPP_REQ = 16; + MPP_OPT = 17; + WUMBO_CHANNELS_REQ = 18; + WUMBO_CHANNELS_OPT = 19; + ANCHORS_REQ = 20; + ANCHORS_OPT = 21; + ANCHORS_ZERO_FEE_HTLC_REQ = 22; + ANCHORS_ZERO_FEE_HTLC_OPT = 23; + ROUTE_BLINDING_REQUIRED = 24; + ROUTE_BLINDING_OPTIONAL = 25; + AMP_REQ = 30; + AMP_OPT = 31; +} +` diff --git a/prisma/migrations/20241212160430_bolt12_attachment/migration.sql b/prisma/migrations/20241212160430_bolt12_attachment/migration.sql new file mode 100644 index 000000000..cb718f171 --- /dev/null +++ b/prisma/migrations/20241212160430_bolt12_attachment/migration.sql @@ -0,0 +1,25 @@ +-- AlterEnum +ALTER TYPE "WalletType" ADD VALUE 'BOLT12'; + +-- CreateTable +CREATE TABLE "WalletBolt12" ( + "id" SERIAL NOT NULL, + "walletId" INTEGER NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "offer" TEXT, + + CONSTRAINT "WalletBolt12_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "WalletBolt12_walletId_key" ON "WalletBolt12"("walletId"); + +-- AddForeignKey +ALTER TABLE "WalletBolt12" ADD CONSTRAINT "WalletBolt12_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE; + + +-- Update wallet json +CREATE TRIGGER wallet_blink_as_jsonb +AFTER INSERT OR UPDATE ON "WalletBolt12" +FOR EACH ROW EXECUTE PROCEDURE wallet_wallet_type_as_jsonb(); \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 59685b931..0edfd5b85 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -189,6 +189,7 @@ enum WalletType { BLINK LNC WEBLN + BOLT12 } model Wallet { @@ -216,6 +217,7 @@ model Wallet { walletNWC WalletNWC? walletPhoenixd WalletPhoenixd? walletBlink WalletBlink? + walletBolt12 WalletBolt12? vaultEntries VaultEntry[] @relation("VaultEntries") withdrawals Withdrawl[] @@ -325,6 +327,15 @@ model WalletPhoenixd { secondaryPassword String? } +model WalletBolt12 { + id Int @id @default(autoincrement()) + walletId Int @unique + wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @default(now()) @updatedAt @map("updated_at") + offer String? +} + model Mute { muterId Int mutedId Int diff --git a/wallets/bolt12/client.js b/wallets/bolt12/client.js new file mode 100644 index 000000000..5d7374099 --- /dev/null +++ b/wallets/bolt12/client.js @@ -0,0 +1 @@ +export * from '@/wallets/bolt12' diff --git a/wallets/bolt12/index.js b/wallets/bolt12/index.js new file mode 100644 index 000000000..8cee7f8ad --- /dev/null +++ b/wallets/bolt12/index.js @@ -0,0 +1,23 @@ +import { string } from '@/lib/yup' + +export const name = 'bolt12' +export const walletType = 'BOLT12' +export const walletField = 'walletBolt12' + +export const fields = [ + { + name: 'offer', + label: 'bolt12 offer', + type: 'text', + placeholder: 'lno....', + hint: 'bolt 12 offer', + clear: true, + serverOnly: true, + validate: string() + } +] + +export const card = { + title: 'Bolt12', + subtitle: 'bolt12' +} diff --git a/wallets/bolt12/server.js b/wallets/bolt12/server.js new file mode 100644 index 000000000..469cf0128 --- /dev/null +++ b/wallets/bolt12/server.js @@ -0,0 +1,11 @@ +import { withTimeout } from '@/lib/time' +export * from '@/wallets/bolt12' + +export async function testCreateInvoice ({ offer }) { + const timeout = 15_000 + return await withTimeout(createInvoice({ msats: 1000, expiry: 1 }, { offer }), timeout) +} + +export async function createInvoice ({ msats, description, expiry }, { offer }) { + return offer +} diff --git a/wallets/client.js b/wallets/client.js index 8bd44698f..c6b67d5fc 100644 --- a/wallets/client.js +++ b/wallets/client.js @@ -7,5 +7,6 @@ import * as lnd from '@/wallets/lnd/client' import * as webln from '@/wallets/webln/client' import * as blink from '@/wallets/blink/client' import * as phoenixd from '@/wallets/phoenixd/client' +import * as bolt12 from '@/wallets/bolt12/client' -export default [nwc, lnbits, lnc, lnAddr, cln, lnd, webln, blink, phoenixd] +export default [nwc, lnbits, lnc, lnAddr, cln, lnd, webln, blink, phoenixd, bolt12] diff --git a/wallets/server.js b/wallets/server.js index 9edd3bd45..5384e73c8 100644 --- a/wallets/server.js +++ b/wallets/server.js @@ -6,6 +6,7 @@ import * as lnbits from '@/wallets/lnbits/server' import * as nwc from '@/wallets/nwc/server' import * as phoenixd from '@/wallets/phoenixd/server' import * as blink from '@/wallets/blink/server' +import * as bolt12 from '@/wallets/bolt12/server' // we import only the metadata of client side wallets import * as lnc from '@/wallets/lnc' @@ -13,18 +14,40 @@ import * as webln from '@/wallets/webln' import { walletLogger } from '@/api/resolvers/wallet' import walletDefs from '@/wallets/server' -import { parsePaymentRequest } from 'ln-service' +import { isBolt12Offer, parseInvoice } from '@/lib/invoices' import { toPositiveBigInt, toPositiveNumber, formatMsats, formatSats, msatsToSats } from '@/lib/format' import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants' import { withTimeout } from '@/lib/time' import { canReceive } from './common' import wrapInvoice from './wrap' +import { fetchBolt12InvoiceFromOffer } from '@/lib/lndk' -export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln] +export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln, bolt12] const MAX_PENDING_INVOICES_PER_WALLET = 25 -export async function createInvoice (userId, { msats, description, descriptionHash, expiry = 360 }, { predecessorId, models }) { +async function checkInvoice (invoice, { msats }, { lnd, logger }) { + const parsedInvoice = await parseInvoice({ lnd, request: invoice }) + console.log('parsedInvoice', parsedInvoice) + logger.info(`created invoice for ${formatSats(msatsToSats(parsedInvoice.mtokens))}`, { + bolt11: invoice + }) + if (BigInt(parsedInvoice.mtokens) !== BigInt(msats)) { + if (BigInt(parsedInvoice.mtokens) > BigInt(msats)) { + throw new Error('invoice invalid: amount too big') + } + if (BigInt(parsedInvoice.mtokens) === 0n) { + throw new Error('invoice invalid: amount is 0 msats') + } + if (BigInt(msats) - BigInt(parsedInvoice.mtokens) >= 1000n) { + throw new Error('invoice invalid: amount too small') + } + + logger.warn('wallet does not support msats') + } +} + +export async function createInvoice (userId, { msats, description, descriptionHash, expiry = 360 }, { predecessorId, models, lnd }) { // get the wallets in order of priority const wallets = await getInvoiceableWallets(userId, { predecessorId, models }) @@ -45,29 +68,13 @@ export async function createInvoice (userId, { msats, description, descriptionHa invoice = await walletCreateInvoice( { wallet, def }, { msats, description, descriptionHash, expiry }, - { logger, models }) + { logger, models, lnd }) } catch (err) { throw new Error('failed to create invoice: ' + err.message) } - const bolt11 = await parsePaymentRequest({ request: invoice }) - - logger.info(`created invoice for ${formatSats(msatsToSats(bolt11.mtokens))}`, { - bolt11: invoice - }) - - if (BigInt(bolt11.mtokens) !== BigInt(msats)) { - if (BigInt(bolt11.mtokens) > BigInt(msats)) { - throw new Error('invoice invalid: amount too big') - } - if (BigInt(bolt11.mtokens) === 0n) { - throw new Error('invoice invalid: amount is 0 msats') - } - if (BigInt(msats) - BigInt(bolt11.mtokens) >= 1000n) { - throw new Error('invoice invalid: amount too small') - } - - logger.warn('wallet does not support msats') + if (!isBolt12Offer(invoice)) { + checkInvoice(invoice, { msats }, { lnd, logger }) } return { invoice, wallet, logger } @@ -82,21 +89,31 @@ export async function createInvoice (userId, { msats, description, descriptionHa export async function createWrappedInvoice (userId, { msats, feePercent, description, descriptionHash, expiry = 360 }, { predecessorId, models, me, lnd }) { - let logger, bolt11 + let logger, invoice, wallet try { - const { invoice, wallet } = await createInvoice(userId, { + const innerAmount = toPositiveBigInt(msats) * (100n - feePercent) / 100n + ;({ invoice, wallet } = await createInvoice(userId, { // this is the amount the stacker will receive, the other (feePercent)% is our fee - msats: toPositiveBigInt(msats) * (100n - feePercent) / 100n, + msats: innerAmount, description, descriptionHash, expiry - }, { predecessorId, models }) + }, { predecessorId, models, lnd })) logger = walletLogger({ wallet, models }) - bolt11 = invoice + + // We need a bolt12 invoice to wrap, so we fetch one + if (isBolt12Offer(invoice)) { + invoice = await fetchBolt12InvoiceFromOffer({ lnd, offer: invoice, amount: innerAmount, description }) + checkInvoice(invoice, { msats: innerAmount }, { lnd, logger }) + } const { invoice: wrappedInvoice, maxFee } = - await wrapInvoice({ bolt11, feePercent }, { msats, description, descriptionHash }, { me, lnd }) + await wrapInvoice( + { bolt11: invoice, feePercent }, + { msats, description, descriptionHash }, + { me, lnd } + ) return { invoice, @@ -105,7 +122,7 @@ export async function createWrappedInvoice (userId, maxFee } } catch (e) { - logger?.error('invalid invoice: ' + e.message, { bolt11 }) + logger?.error('invalid invoice: ' + e.message, { bolt11: invoice }) throw e } } @@ -166,7 +183,7 @@ async function walletCreateInvoice ({ wallet, def }, { description, descriptionHash, expiry = 360 -}, { logger, models }) { +}, { logger, models, lnd }) { // check for pending withdrawals const pendingWithdrawals = await models.withdrawl.count({ where: { @@ -201,6 +218,6 @@ async function walletCreateInvoice ({ wallet, def }, { expiry }, wallet.wallet, - { logger } + { logger, lnd } ), 10_000) } diff --git a/wallets/wrap.js b/wallets/wrap.js index 26f83ca53..f3246035b 100644 --- a/wallets/wrap.js +++ b/wallets/wrap.js @@ -1,6 +1,7 @@ -import { createHodlInvoice, parsePaymentRequest } from 'ln-service' -import { estimateRouteFee, getBlockHeight } from '../api/lnd' +import { createHodlInvoice } from 'ln-service' +import { getBlockHeight } from '../api/lnd' import { toBigInt, toPositiveBigInt, toPositiveNumber } from '@/lib/format' +import { parseInvoice, estimateFees } from '@/lib/invoices' const MIN_OUTGOING_MSATS = BigInt(900) // the minimum msats we'll allow for the outgoing invoice const MAX_OUTGOING_MSATS = BigInt(900_000_000) // the maximum msats we'll allow for the outgoing invoice @@ -15,7 +16,7 @@ const MAX_FEE_ESTIMATE_PERCENT = 3n // the maximum fee relative to outgoing we'l The wrapInvoice function is used to wrap an outgoing invoice with the necessary parameters for an incoming hold invoice. @param args {object} { - bolt11: {string} the bolt11 invoice to wrap + bolt11: {string} the bolt11 or bolt12 invoice to wrap feePercent: {bigint} the fee percent to use for the incoming invoice } @param options {object} { @@ -37,7 +38,7 @@ export default async function wrapInvoice ({ bolt11, feePercent }, { msats, desc let outgoingMsat // decode the invoice - const inv = await parsePaymentRequest({ request: bolt11 }) + const inv = await parseInvoice({ request: bolt11, lnd }) if (!inv) { throw new Error('Unable to decode invoice') } @@ -147,7 +148,7 @@ export default async function wrapInvoice ({ bolt11, feePercent }, { msats, desc // get routing estimates const { routingFeeMsat, timeLockDelay } = - await estimateRouteFee({ + await estimateFees({ lnd, destination: inv.destination, mtokens: inv.mtokens, diff --git a/worker/autowithdraw.js b/worker/autowithdraw.js index c2105d021..deb0954d7 100644 --- a/worker/autowithdraw.js +++ b/worker/autowithdraw.js @@ -42,7 +42,7 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) { if (pendingOrFailed.exists) return - const { invoice, wallet, logger } = await createInvoice(id, { msats, description: 'SN: autowithdrawal', expiry: 360 }, { models }) + const { invoice, wallet, logger } = await createInvoice(id, { msats, description: 'SN: autowithdrawal', expiry: 360 }, { models, lnd }) try { return await createWithdrawal(null, diff --git a/worker/index.js b/worker/index.js index 16d48c59c..16946e9e3 100644 --- a/worker/index.js +++ b/worker/index.js @@ -17,6 +17,7 @@ import { computeStreaks, checkStreak } from './streak' import { nip57 } from './nostr' import fetch from 'cross-fetch' import { authenticatedLndGrpc } from '@/lib/lnd' +import { installLNDK } from '@/lib/lndk' import { views, rankViews } from './views' import { imgproxy } from './imgproxy' import { deleteItem } from './ephemeralItems' @@ -73,6 +74,11 @@ async function work () { macaroon: process.env.LND_MACAROON, socket: process.env.LND_SOCKET }) + installLNDK(lnd, { + cert: process.env.LNDK_CERT, + macaroon: process.env.LNDK_MACAROON, + socket: process.env.LNDK_SOCKET + }) const args = { boss, models, apollo, lnd } diff --git a/worker/paidAction.js b/worker/paidAction.js index 9b3ecb5a6..686b22d66 100644 --- a/worker/paidAction.js +++ b/worker/paidAction.js @@ -7,11 +7,11 @@ import { datePivot } from '@/lib/time' import { Prisma } from '@prisma/client' import { cancelHodlInvoice, - getInvoice, parsePaymentRequest, - payViaPaymentRequest, settleHodlInvoice + getInvoice, + settleHodlInvoice } from 'ln-service' +import { payInvoice, parseInvoice } from '@/lib/invoices' import { MIN_SETTLEMENT_CLTV_DELTA } from '@/wallets/wrap' - // aggressive finalization retry options const FINALIZE_OPTIONS = { retryLimit: 2 ** 31 - 1, retryBackoff: false, retryDelay: 5, priority: 1000 } @@ -211,7 +211,7 @@ export async function paidActionForwarding ({ data: { invoiceId, ...args }, mode const { expiryHeight, acceptHeight } = hodlInvoiceCltvDetails(lndInvoice) const { bolt11, maxFeeMsats } = invoiceForward - const invoice = await parsePaymentRequest({ request: bolt11 }) + const invoice = await parseInvoice({ request: bolt11, lnd }) // maxTimeoutDelta is the number of blocks left for the outgoing payment to settle const maxTimeoutDelta = toPositiveNumber(expiryHeight) - toPositiveNumber(acceptHeight) - MIN_SETTLEMENT_CLTV_DELTA if (maxTimeoutDelta - toPositiveNumber(invoice.cltv_delta) < 0) { @@ -265,7 +265,7 @@ export async function paidActionForwarding ({ data: { invoiceId, ...args }, mode console.log('forwarding with max fee', maxFeeMsats, 'max_timeout_height', maxTimeoutHeight, 'accept_height', acceptHeight, 'expiry_height', expiryHeight) - payViaPaymentRequest({ + payInvoice({ lnd, request: bolt11, max_fee_mtokens: String(maxFeeMsats), @@ -445,7 +445,7 @@ export async function paidActionCanceling ({ data: { invoiceId, ...args }, model if (transitionedInvoice.invoiceForward) { const { wallet, bolt11 } = transitionedInvoice.invoiceForward const logger = walletLogger({ wallet, models }) - const decoded = await parsePaymentRequest({ request: bolt11 }) + const decoded = await parseInvoice({ request: bolt11, lnd }) logger.info(`invoice for ${formatSats(msatsToSats(decoded.mtokens))} canceled by payer`, { bolt11 }) } } From 332d1e170d2c3314e5d13832a0122eac22809723 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 17 Dec 2024 01:12:39 +0100 Subject: [PATCH 02/42] don't support direct payments to bolt12 --- api/paidAction/index.js | 5 +++-- wallets/server.js | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/api/paidAction/index.js b/api/paidAction/index.js index cb97fbf5c..a5c5b0d74 100644 --- a/api/paidAction/index.js +++ b/api/paidAction/index.js @@ -264,7 +264,8 @@ async function performDirectAction (actionType, args, incomingContext) { invoiceObject = await createUserInvoice(userId, { msats: cost, description, - expiry: INVOICE_EXPIRE_SECS + expiry: INVOICE_EXPIRE_SECS, + supportBolt12: false // direct payment is not supported to bolt12 for compatibility reasons }, { models, lnd }) } catch (e) { console.error('failed to create outside invoice', e) @@ -272,7 +273,7 @@ async function performDirectAction (actionType, args, incomingContext) { } const { invoice, wallet } = invoiceObject - const hash = await parseBolt11({ request: invoice }).id // direct payments are always to bolt11 invoices + const hash = await parseBolt11({ request: invoice }).id const payment = await models.directPayment.create({ data: { diff --git a/wallets/server.js b/wallets/server.js index 5384e73c8..172e6cf52 100644 --- a/wallets/server.js +++ b/wallets/server.js @@ -47,7 +47,7 @@ async function checkInvoice (invoice, { msats }, { lnd, logger }) { } } -export async function createInvoice (userId, { msats, description, descriptionHash, expiry = 360 }, { predecessorId, models, lnd }) { +export async function createInvoice (userId, { msats, description, descriptionHash, expiry = 360, supportBolt12 = true }, { predecessorId, models, lnd }) { // get the wallets in order of priority const wallets = await getInvoiceableWallets(userId, { predecessorId, models }) @@ -74,6 +74,7 @@ export async function createInvoice (userId, { msats, description, descriptionHa } if (!isBolt12Offer(invoice)) { + if (!supportBolt12) continue checkInvoice(invoice, { msats }, { lnd, logger }) } From f927fc53fa78270b48192f8f9b8d9a7eaa54ebe9 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 17 Dec 2024 01:55:13 +0100 Subject: [PATCH 03/42] code cleanup, add bolt12info (bolt11 tags equivalent) --- api/paidAction/index.js | 2 +- api/resolvers/wallet.js | 5 ++-- components/bolt11-info.js | 5 ++-- lib/bolt11-info.js | 10 ++++++++ lib/bolt11.js | 22 +++++++++++++++--- lib/bolt12-info.js | 28 ++++++++++++++++++++++ lib/bolt12.js | 25 ++++++++++++++++++++ lib/invoices.js | 49 +++------------------------------------ lib/lndk.js | 31 +------------------------ lib/tlv.js | 36 ++++++++++++++++++++++++++++ wallets/server.js | 3 ++- 11 files changed, 131 insertions(+), 85 deletions(-) create mode 100644 lib/bolt11-info.js create mode 100644 lib/bolt12-info.js create mode 100644 lib/bolt12.js create mode 100644 lib/tlv.js 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' From 494061c5e9e76a88691f65ca3b3aa4b2c7251677 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 17 Dec 2024 20:17:07 +0100 Subject: [PATCH 04/42] add withdraw to bolt12, improve checks and naming --- api/payingAction/index.js | 2 +- api/resolvers/wallet.js | 23 ++++- api/typeDefs/wallet.js | 1 + components/bolt11-info.js | 4 +- fragments/wallet.js | 7 ++ lib/bech32b12.js | 11 +- lib/{bolt11-info.js => bolt11-tags.js} | 2 +- lib/bolt11.js | 5 +- lib/bolt12-info.js | 19 ++-- lib/bolt12.js | 13 ++- lib/{invoices.js => boltInvoices.js} | 22 +++- lib/lndk.js | 137 +++++++++++++------------ lib/tlv.js | 11 +- lib/validate.js | 12 ++- pages/wallet/index.js | 79 +++++++++++++- wallets/server.js | 5 +- wallets/wrap.js | 2 +- worker/paidAction.js | 2 +- 18 files changed, 248 insertions(+), 109 deletions(-) rename lib/{bolt11-info.js => bolt11-tags.js} (88%) rename lib/{invoices.js => boltInvoices.js} (53%) diff --git a/api/payingAction/index.js b/api/payingAction/index.js index 731733a15..745357b04 100644 --- a/api/payingAction/index.js +++ b/api/payingAction/index.js @@ -1,7 +1,7 @@ import { LND_PATHFINDING_TIME_PREF_PPM, LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants' import { msatsToSats, satsToMsats, toPositiveBigInt } from '@/lib/format' import { Prisma } from '@prisma/client' -import { payInvoice, parseInvoice } from '@/lib/invoices' +import { payInvoice, parseInvoice } from '@/lib/boltInvoices' // paying actions are completely distinct from paid actions // and there's only one paying action: send diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index 618fa85f3..8ae997608 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -12,7 +12,7 @@ import { import { amountSchema, validateSchema, withdrawlSchema, lnAddrSchema } from '@/lib/validate' import assertGofacYourself from './ofac' import assertApiKeyNotPermitted from './apiKey' -import { bolt11Info, isBolt11 } from '@/lib/bolt11-info' +import { bolt11Tags, isBolt11 } from '@/lib/bolt11-tags' import { bolt12Info } from '@/lib/bolt12-info' import { finalizeHodlInvoice } from '@/worker/wallet' import walletDefs from '@/wallets/server' @@ -24,8 +24,10 @@ import validateWallet from '@/wallets/validate' import { canReceive } from '@/wallets/common' import performPaidAction from '../paidAction' import performPayingAction from '../payingAction' -import { parseInvoice } from '@/lib/invoices' +import { parseInvoice } from '@/lib/boltInvoices' import lnd from '@/api/lnd' +import { isBolt12Offer } from '@/lib/bolt12' +import { fetchBolt12InvoiceFromOffer } from '@/lib/lndk' function injectResolvers (resolvers) { console.group('injected GraphQL resolvers:') @@ -369,7 +371,7 @@ const resolvers = { f = { ...f, ...f.other } if (f.bolt11) { - f.description = isBolt11(f.bolt11) ? bolt11Info(f.bolt11).description : bolt12Info(f.bolt11).description + f.description = isBolt11(f.bolt11) ? bolt11Tags(f.bolt11).description : bolt12Info(f.bolt11).description } switch (f.type) { @@ -481,6 +483,7 @@ const resolvers = { }, createWithdrawl: createWithdrawal, sendToLnAddr, + sendToBolt12Offer, cancelInvoice: async (parent, { hash, hmac }, { models, lnd, boss }) => { verifyHmac(hash, hmac) await finalizeHodlInvoice({ data: { hash }, lnd, models, boss }) @@ -940,7 +943,7 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model throw new GqlInputError('SN cannot pay an invoice that SN is proxying') } - return await performPayingAction({ invoice, maxFee, walletId: wallet?.id }, { me, models, lnd }) + return await performPayingAction({ bolt11: invoice, maxFee, walletId: wallet?.id }, { me, models, lnd }) } export async function sendToLnAddr (parent, { addr, amount, maxFee, comment, ...payer }, @@ -961,6 +964,18 @@ export async function sendToLnAddr (parent, { addr, amount, maxFee, comment, ... return await createWithdrawal(parent, { invoice: res.pr, maxFee }, { me, models, lnd, headers }) } +export async function sendToBolt12Offer (parent, { offer, amountSats, maxFee, comment }, { me, models, lnd, headers }) { + if (!me) { + throw new GqlAuthenticationError() + } + assertApiKeyNotPermitted({ me }) + if (!isBolt12Offer(offer)) { + throw new GqlInputError('not a bolt12 offer') + } + const invoice = await fetchBolt12InvoiceFromOffer({ lnd, offer, msats: satsToMsats(amountSats), description: comment }) + return await createWithdrawal(parent, { invoice, maxFee }, { me, models, lnd, headers }) +} + export async function fetchLnAddrInvoice ( { addr, amount, maxFee, comment, ...payer }, { diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js index 932b67bcd..b1746c061 100644 --- a/api/typeDefs/wallet.js +++ b/api/typeDefs/wallet.js @@ -78,6 +78,7 @@ const typeDefs = ` createInvoice(amount: Int!): InvoiceOrDirect! createWithdrawl(invoice: String!, maxFee: Int!): Withdrawl! sendToLnAddr(addr: String!, amount: Int!, maxFee: Int!, comment: String, identifier: Boolean, name: String, email: String): Withdrawl! + sendToBolt12Offer(offer: String!, amountSats: Int!, maxFee: Int!, comment: String): Withdrawl! cancelInvoice(hash: String!, hmac: String!): Invoice! dropBolt11(hash: String!): Boolean removeWallet(id: ID!): Boolean diff --git a/components/bolt11-info.js b/components/bolt11-info.js index 81ff8fe44..f6b3bb761 100644 --- a/components/bolt11-info.js +++ b/components/bolt11-info.js @@ -1,12 +1,12 @@ import AccordianItem from './accordian-item' import { CopyInput } from './form' -import { bolt11Info, isBolt11 } from '@/lib/bolt11-info' +import { bolt11Tags, isBolt11 } from '@/lib/bolt11-tags' import { bolt12Info } from '@/lib/bolt12-info' export default ({ bolt11, preimage, children }) => { let description, paymentHash if (bolt11) { - ({ description, payment_hash: paymentHash } = isBolt11(bolt11) ? bolt11Info(bolt11) : bolt12Info(bolt11)) + ({ description, payment_hash: paymentHash } = isBolt11(bolt11) ? bolt11Tags(bolt11) : bolt12Info(bolt11)) } return ( diff --git a/fragments/wallet.js b/fragments/wallet.js index e1b7a288a..96b6a775d 100644 --- a/fragments/wallet.js +++ b/fragments/wallet.js @@ -121,6 +121,13 @@ export const SEND_TO_LNADDR = gql` } }` +export const SEND_TO_BOLT12_OFFER = gql` + mutation sendToBolt12Offer($offer: String!, $amountSats: Int!, $maxFee: Int!, $comment: String) { + sendToBolt12Offer(offer: $offer, amountSats: $amountSats, maxFee: $maxFee, comment: $comment) { + id + } +}` + export const REMOVE_WALLET = gql` mutation removeWallet($id: ID!) { diff --git a/lib/bech32b12.js b/lib/bech32b12.js index 611515e06..be5427d21 100644 --- a/lib/bech32b12.js +++ b/lib/bech32b12.js @@ -1,10 +1,14 @@ +// bech32 without the checksum +// used for bolt12 + const ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' export function decode (str) { + if (str.length > 2048) throw new Error('input is too long') const b5s = [] for (const char of str) { const i = ALPHABET.indexOf(char) - if (i === -1) throw new Error('Invalid bech32 character') + if (i === -1) throw new Error('invalid bech32 character') b5s.push(i) } const b8s = Buffer.from(converBits(b5s, 5, 8, false)) @@ -12,10 +16,9 @@ export function decode (str) { } export function encode (b8s) { + if (b8s.length > 2048) throw new Error('input is too long') const b5s = converBits(b8s, 8, 5, true) - const str = [] - for (const b5 of b5s) str.push(ALPHABET[b5]) - return str.join('') + return b5s.map(b5 => ALPHABET[b5]).join('') } function converBits (data, frombits, tobits, pad) { diff --git a/lib/bolt11-info.js b/lib/bolt11-tags.js similarity index 88% rename from lib/bolt11-info.js rename to lib/bolt11-tags.js index 498aa139f..04248a1a4 100644 --- a/lib/bolt11-info.js +++ b/lib/bolt11-tags.js @@ -4,7 +4,7 @@ export function isBolt11 (request) { return request.startsWith('lnbc') || request.startsWith('lntb') || request.startsWith('lntbs') || request.startsWith('lnbcrt') } -export function bolt11Info (bolt11) { +export function bolt11Tags (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 c616e7f9b..42ee584e6 100644 --- a/lib/bolt11.js +++ b/lib/bolt11.js @@ -1,8 +1,11 @@ /* eslint-disable camelcase */ import { payViaPaymentRequest, parsePaymentRequest } from 'ln-service' +import { bolt11InvoiceSchema } from './validate' export function isBolt11 (request) { - return request.startsWith('lnbc') || request.startsWith('lntb') || request.startsWith('lntbs') || request.startsWith('lnbcrt') + if (!request.startsWith('lnbc') && !request.startsWith('lntb') && !request.startsWith('lntbs') && !request.startsWith('lnbcrt')) return false + bolt11InvoiceSchema.validateSync(request) + return true } export async function parseBolt11 ({ request }) { diff --git a/lib/bolt12-info.js b/lib/bolt12-info.js index 7b98bab23..99a8ef7ce 100644 --- a/lib/bolt12-info.js +++ b/lib/bolt12-info.js @@ -1,6 +1,10 @@ import { deserializeTLVStream } from './tlv' import * as bech32b12 from '@/lib/bech32b12' +const TYPE_DESCRIPTION = 10n +const TYPE_PAYER_NOTE = 89n +const TYPE_PAYMENT_HASH = 168n + export function isBolt12 (invoice) { return invoice.startsWith('lni1') || invoice.startsWith('lno1') } @@ -9,20 +13,21 @@ 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) { + if (type === TYPE_DESCRIPTION) { + info.description = value.toString() || info.description + } else if (type === TYPE_PAYER_NOTE) { + info.description = value.toString() || info.description + } else if (type === TYPE_PAYMENT_HASH) { info.payment_hash = value.toString('hex') } } + return info } diff --git a/lib/bolt12.js b/lib/bolt12.js index f6af6a164..cb6d671a9 100644 --- a/lib/bolt12.js +++ b/lib/bolt12.js @@ -1,13 +1,18 @@ /* eslint-disable camelcase */ import { payViaBolt12PaymentRequest, parseBolt12Request } from '@/lib/lndk' +import { bolt12OfferSchema, bolt12InvoiceSchema } from './validate' export function isBolt12Offer (invoice) { - return invoice.startsWith('lno1') + if (!invoice.startsWith('lno1')) return false + bolt12OfferSchema.validateSync(invoice) + return true } export function isBolt12Invoice (invoice) { - return invoice.startsWith('lni1') + if (!invoice.startsWith('lni1')) return false + bolt12InvoiceSchema.validateSync(invoice) + return true } export function isBolt12 (invoice) { @@ -19,7 +24,7 @@ export async function payBolt12 ({ lnd, request: invoice, max_fee }) { return await payViaBolt12PaymentRequest({ lnd, request: invoice, max_fee }) } -export function parseBolt12 ({ lnd, request: invoice }) { +export async function parseBolt12 ({ lnd, request: invoice }) { if (!isBolt12Invoice(invoice)) throw new Error('not a bolt12 request') - return parseBolt12Request({ lnd, request: invoice }) + return await parseBolt12Request({ lnd, request: invoice }) } diff --git a/lib/invoices.js b/lib/boltInvoices.js similarity index 53% rename from lib/invoices.js rename to lib/boltInvoices.js index bf7a3697d..57e7092c1 100644 --- a/lib/invoices.js +++ b/lib/boltInvoices.js @@ -1,29 +1,41 @@ /* eslint-disable camelcase */ -import { payBolt12, parseBolt12, isBolt12Invoice } from './bolt12' -import { payBolt11, parseBolt11 } from './bolt11' +import { payBolt12, parseBolt12, isBolt12Invoice, isBolt12Offer } from './bolt12' +import { payBolt11, parseBolt11, isBolt11 } from './bolt11' import { estimateBolt12RouteFee } from '@/lib/lndk' import { estimateRouteFee } from '@/api/lnd' export async function payInvoice ({ lnd, request: invoice, max_fee, ...args }) { if (isBolt12Invoice(invoice)) { return await payBolt12({ lnd, request: invoice, max_fee, ...args }) - } else { + } else if (isBolt11(invoice)) { return await payBolt11({ lnd, request: invoice, max_fee, ...args }) + } else if (isBolt12Offer(invoice)) { + throw new Error('cannot pay bolt12 offer directly, please fetch a bolt12 invoice from the offer first') + } else { + throw new Error('unknown invoice type') } } export async function parseInvoice ({ lnd, request }) { if (isBolt12Invoice(request)) { return await parseBolt12({ lnd, request }) - } else { + } else if (isBolt11(request)) { return await parseBolt11({ request }) + } else if (isBolt12Offer(request)) { + throw new Error('bolt12 offer instead of invoice, please fetch a bolt12 invoice from the offer first') + } else { + throw new Error('unknown invoice type') } } export async function estimateFees ({ lnd, destination, tokens, mtokens, request, timeout }) { if (isBolt12Invoice(request)) { return await estimateBolt12RouteFee({ lnd, destination, tokens, mtokens, request, timeout }) - } else { + } else if (isBolt11(request)) { return await estimateRouteFee({ lnd, destination, tokens, request, mtokens, timeout }) + } else if (isBolt12Offer(request)) { + throw new Error('bolt12 offer instead of invoice, please fetch a bolt12 invoice from the offer first') + } else { + throw new Error('unknown invoice type') } } diff --git a/lib/lndk.js b/lib/lndk.js index f46ddaa64..564eb1d43 100644 --- a/lib/lndk.js +++ b/lib/lndk.js @@ -6,7 +6,6 @@ import grpcCredentials from 'lightning/lnd_grpc/grpc_credentials' import { defaultSocket, grpcSslCipherSuites } from 'lightning/grpc/index' import { fromJSON } from '@grpc/proto-loader' import * as bech32b12 from '@/lib/bech32b12' - /* eslint-disable camelcase */ const { GRPC_SSL_CIPHER_SUITES } = process.env @@ -41,51 +40,6 @@ export function installLNDK (lnd, { cert, macaroon, socket }, withProxy) { lnd.lndk = client } -export async function fetchBolt12InvoiceFromOffer ({ lnd, offer, amount, description, timeout = 10_000 }) { - const lndk = lnd.lndk - if (!lndk) throw new Error('lndk not installed, please use installLNDK') - return new Promise((resolve, reject) => { - lndk.GetInvoice({ - offer, - amount: toPositiveNumber(amount), - payer_note: description, - response_invoice_timeout: timeout - }, (error, response) => { - if (error) return reject(error) - const bech32invoice = 'lni1' + bech32b12.encode(Buffer.from(response.invoice_hex_str, 'hex')) - resolve(bech32invoice) - }) - }) -} - -export async function payViaBolt12PaymentRequest ({ - lnd, - request: invoice_hex_str, - max_fee -}) { - const lndk = lnd.lndk - if (!lndk) throw new Error('lndk not installed, please use installLNDK') - - const bolt12 = await parseBolt12Request({ lnd, request: invoice_hex_str }) - - const req = { - invoice: bolt12.payment, - amount: toPositiveNumber(bolt12.mtokens), - max_fee - } - - return new Promise((resolve, reject) => { - lndk.PayInvoice(req, (error, response) => { - if (error) { - return reject(error) - } - resolve({ - secret: response.payment_preimage - }) - }) - }) -} - const featureBitMap = { 0: { bit: 0, type: 'DATALOSS_PROTECT_REQ', is_required: true }, 1: { bit: 1, type: 'DATALOSS_PROTECT_OPT', is_required: false }, @@ -148,7 +102,8 @@ export async function parseBolt12Request ({ payment_hash, created_at, relative_expiry, - features + features, + payer_note } = invoice_contents // convert from lndk response to ln-service parsePaymentRequest output layout @@ -164,7 +119,7 @@ export async function parseBolt12Request ({ created_at: new Date(created_at * 1000).toISOString(), // [chain_addresses] cltv_delta: minCltvDelta, - description, + description: payer_note || description, // [description_hash] destination: Buffer.from(node_id.key).toString('hex'), expires_at: new Date((created_at + relative_expiry) * 1000).toISOString(), @@ -185,40 +140,90 @@ export async function parseBolt12Request ({ } }), safe_tokens: Math.round(toPositiveNumber(BigInt(amount_msats)) / 1000), - tokens: Math.floor(toPositiveNumber(BigInt(amount_msats)) / 1000) + tokens: Math.floor(toPositiveNumber(BigInt(amount_msats)) / 1000), + bolt12: invoice_contents } - // mark as bolt12 invoice so we can differentiate it later (this will be used also to pass bolt12 only data) - out.bolt12 = invoice_contents - return out } +export async function fetchBolt12InvoiceFromOffer ({ lnd, offer, msats, description, timeout = 10_000 }) { + const lndk = lnd?.lndk + if (!lndk) throw new Error('lndk not installed, please use installLNDK') + + return new Promise((resolve, reject) => { + lndk.GetInvoice({ + offer, + // expects msats https://github.com/lndk-org/lndk/blob/bce93885f5fc97f3ceb15dc470117e10061de018/src/lndk_offers.rs#L182 + amount: toPositiveNumber(msats), + payer_note: description, + response_invoice_timeout: timeout + }, async (error, response) => { + if (error) return reject(error) + const bech32invoice = 'lni1' + bech32b12.encode(Buffer.from(response.invoice_hex_str, 'hex')) + + // sanity check + const parsedInvoice = await parseBolt12Request({ lnd, request: bech32invoice }) + if ( + !parsedInvoice || + toPositiveNumber(parsedInvoice.mtokens) !== toPositiveNumber(msats) || + toPositiveNumber(parsedInvoice.tokens) !== toPositiveNumber(msatsToSats(msats)) + ) { + return reject(new Error('invalid invoice response')) + } + resolve(bech32invoice) + }) + }) +} + +export async function payViaBolt12PaymentRequest ({ + lnd, + request: invoice_hex_str, + max_fee +}) { + const lndk = lnd?.lndk + if (!lndk) throw new Error('lndk not installed, please use installLNDK') + + const parsedInvoice = await parseBolt12Request({ lnd, request: invoice_hex_str }) + + return new Promise((resolve, reject) => { + lndk.PayInvoice({ + invoice: parsedInvoice.payment, + // expects msats amount: https://github.com/lndk-org/lndk/blob/bce93885f5fc97f3ceb15dc470117e10061de018/src/lib.rs#L403 + amount: toPositiveNumber(parsedInvoice.mtokens), + max_fee + }, (error, response) => { + if (error) { + return reject(error) + } + resolve({ + secret: response.payment_preimage + }) + }) + }) +} + export async function estimateBolt12RouteFee ({ lnd, destination, tokens, mtokens, request, timeout }) { const lndk = lnd?.lndk if (!lndk) throw new Error('lndk not installed, please use installLNDK') const parsedInvoice = request ? await parseBolt12Request({ lnd, request }) : {} - mtokens ??= parsedInvoice.mtokens + + if (!tokens && mtokens) tokens = toPositiveNumber(msatsToSats(mtokens)) + tokens ??= toPositiveNumber(parsedInvoice.tokens) destination ??= parsedInvoice.destination - return await new Promise((resolve, reject) => { - const params = {} - params.dest = Buffer.from(destination, 'hex') - params.amt_sat = null - if (tokens) params.amt_sat = toPositiveNumber(tokens) - else if (mtokens) params.amt_sat = msatsToSats(mtokens) - - if (params.amt_sat === null) { - throw new Error('No tokens or mtokens provided') - } + if (!destination) throw new Error('no destination provided') + if (!tokens) throw new Error('no tokens provided') + return await new Promise((resolve, reject) => { lnd.router.estimateRouteFee({ - ...params, + dest: Buffer.from(destination, 'hex'), + amt_sat: tokens, timeout }, (err, res) => { if (err) { if (res?.failure_reason) { - reject(new Error(`Unable to estimate route: ${res.failure_reason}`)) + reject(new Error(`unable to estimate route: ${res.failure_reason}`)) } else { reject(err) } @@ -226,7 +231,7 @@ export async function estimateBolt12RouteFee ({ lnd, destination, tokens, mtoken } if (res.routing_fee_msat < 0 || res.time_lock_delay <= 0) { - reject(new Error('Unable to estimate route, excessive values: ' + JSON.stringify(res))) + reject(new Error('unable to estimate route, excessive values: ' + JSON.stringify(res))) return } diff --git a/lib/tlv.js b/lib/tlv.js index a62ced692..62df3478c 100644 --- a/lib/tlv.js +++ b/lib/tlv.js @@ -5,18 +5,17 @@ export function deserializeTLVStream (buff) { const [type, typeLength] = readBigSize(buff, bytePos) bytePos += typeLength - let [length, lengthLength] = readBigSize(buff, bytePos) - length = Number(length) + const [length, lengthLength] = readBigSize(buff, bytePos) bytePos += lengthLength - if (bytePos + length > buff.length) { + if (bytePos + Number(length) > buff.length) { throw new Error('invalid tlv stream') } - const value = buff.subarray(bytePos, bytePos + length) - bytePos += length + const value = buff.subarray(bytePos, bytePos + Number(length)) + bytePos += Number(length) - tlvs.push({ type, length, value }) + tlvs.push({ type, length: Number(length), value }) } return tlvs } diff --git a/lib/validate.js b/lib/validate.js index 86b03bded..6e9d48d5f 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -223,6 +223,16 @@ export const lnAddrSchema = ({ payerData, min, max, commentAllowed } = {}) => return accum }, {}))) +export const bolt11InvoiceSchema = string().trim().matches(process.env.NODE_ENV === 'development' ? /^lnbcrt/ : /^lnbc/, 'invalid bolt11 invoice') +export const bolt12OfferSchema = string().trim().matches(/^lno1/, 'invalid bolt12 offer') +export const bolt12InvoiceSchema = string().trim().matches(/^lni1/, 'invalid bolt12 invoice') +export const bolt12WithdrawSchema = object({ + offer: bolt12OfferSchema.required('required'), + amount: intValidator.required('required').positive('must be positive').min(1, 'must be at least 1'), + maxFee: intValidator.required('required').min(0, 'must be at least 0'), + comment: string().max(128, 'must be less than 128') +}) + export function bountySchema (args) { return object({ title: titleValidator, @@ -468,7 +478,7 @@ export const lastAuthRemovalSchema = object({ }) export const withdrawlSchema = object({ - invoice: string().required('required').trim(), + invoice: string().required('required'), maxFee: intValidator.required('required').min(0, 'must be at least 0') }) diff --git a/pages/wallet/index.js b/pages/wallet/index.js index 759e6e52c..8a1ca563c 100644 --- a/pages/wallet/index.js +++ b/pages/wallet/index.js @@ -11,9 +11,9 @@ import { useMe } from '@/components/me' import { useEffect, useState } from 'react' import { requestProvider } from 'webln' import Alert from 'react-bootstrap/Alert' -import { CREATE_WITHDRAWL, SEND_TO_LNADDR } from '@/fragments/wallet' +import { CREATE_WITHDRAWL, SEND_TO_BOLT12_OFFER, SEND_TO_LNADDR } from '@/fragments/wallet' import { getGetServerSideProps } from '@/api/ssrApollo' -import { amountSchema, lnAddrSchema, withdrawlSchema } from '@/lib/validate' +import { amountSchema, lnAddrSchema, withdrawlSchema, bolt12WithdrawSchema } from '@/lib/validate' import Nav from 'react-bootstrap/Nav' import { BALANCE_LIMIT_MSATS, FAST_POLL_INTERVAL, SSR } from '@/lib/constants' import { msatsToSats, numWithUnits } from '@/lib/format' @@ -195,6 +195,11 @@ export function WithdrawalForm () { lightning address + + + bolt12 offer + + @@ -211,6 +216,8 @@ export function SelectedWithdrawalForm () { return case 'lnaddr-withdraw': return + case 'bolt12-withdraw': + return } } @@ -512,3 +519,71 @@ export function LnAddrWithdrawal () { ) } + +export function Bolt12Withdrawal () { + const { me } = useMe() + const router = useRouter() + const [sendToBolt12Offer, { called, error }] = useMutation(SEND_TO_BOLT12_OFFER) + + const maxFeeDefault = me?.privates?.withdrawMaxFeeDefault + + return ( + <> + {called && !error && } +
{ + const { data } = await sendToBolt12Offer({ + variables: { + offer, + amountSats: Number(amount), + maxFee: Number(maxFee), + comment + } + }) + router.push(`/withdrawals/${data.sendToBolt12Offer.id}`) + }} + > + + sats} + /> + sats} + /> + comment optional} + name='comment' + maxLength={128} + /> + send +
+ + ) +} diff --git a/wallets/server.js b/wallets/server.js index b6eb0584b..81309efb8 100644 --- a/wallets/server.js +++ b/wallets/server.js @@ -14,7 +14,7 @@ import * as webln from '@/wallets/webln' import { walletLogger } from '@/api/resolvers/wallet' import walletDefs from '@/wallets/server' -import { parseInvoice } from '@/lib/invoices' +import { parseInvoice } from '@/lib/boltInvoices' import { isBolt12Offer } from '@/lib/bolt12' import { toPositiveBigInt, toPositiveNumber, formatMsats, formatSats, msatsToSats } from '@/lib/format' import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants' @@ -29,7 +29,6 @@ const MAX_PENDING_INVOICES_PER_WALLET = 25 async function checkInvoice (invoice, { msats }, { lnd, logger }) { const parsedInvoice = await parseInvoice({ lnd, request: invoice }) - console.log('parsedInvoice', parsedInvoice) logger.info(`created invoice for ${formatSats(msatsToSats(parsedInvoice.mtokens))}`, { bolt11: invoice }) @@ -106,7 +105,7 @@ export async function createWrappedInvoice (userId, // We need a bolt12 invoice to wrap, so we fetch one if (isBolt12Offer(invoice)) { - invoice = await fetchBolt12InvoiceFromOffer({ lnd, offer: invoice, amount: innerAmount, description }) + invoice = await fetchBolt12InvoiceFromOffer({ lnd, offer: invoice, msats: innerAmount, description }) checkInvoice(invoice, { msats: innerAmount }, { lnd, logger }) } diff --git a/wallets/wrap.js b/wallets/wrap.js index f3246035b..7f345de3a 100644 --- a/wallets/wrap.js +++ b/wallets/wrap.js @@ -1,7 +1,7 @@ import { createHodlInvoice } from 'ln-service' import { getBlockHeight } from '../api/lnd' import { toBigInt, toPositiveBigInt, toPositiveNumber } from '@/lib/format' -import { parseInvoice, estimateFees } from '@/lib/invoices' +import { parseInvoice, estimateFees } from '@/lib/boltInvoices' const MIN_OUTGOING_MSATS = BigInt(900) // the minimum msats we'll allow for the outgoing invoice const MAX_OUTGOING_MSATS = BigInt(900_000_000) // the maximum msats we'll allow for the outgoing invoice diff --git a/worker/paidAction.js b/worker/paidAction.js index 686b22d66..0b52fcaf1 100644 --- a/worker/paidAction.js +++ b/worker/paidAction.js @@ -10,7 +10,7 @@ import { getInvoice, settleHodlInvoice } from 'ln-service' -import { payInvoice, parseInvoice } from '@/lib/invoices' +import { payInvoice, parseInvoice } from '@/lib/boltInvoices' import { MIN_SETTLEMENT_CLTV_DELTA } from '@/wallets/wrap' // aggressive finalization retry options const FINALIZE_OPTIONS = { retryLimit: 2 ** 31 - 1, retryBackoff: false, retryDelay: 5, priority: 1000 } From 8fbf5c25ec25c11ccd413c941666b011621691e6 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 17 Dec 2024 20:43:06 +0100 Subject: [PATCH 05/42] Add create invoice test --- wallets/bolt12/server.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/wallets/bolt12/server.js b/wallets/bolt12/server.js index 469cf0128..e3db9faad 100644 --- a/wallets/bolt12/server.js +++ b/wallets/bolt12/server.js @@ -1,9 +1,20 @@ import { withTimeout } from '@/lib/time' +import lnd from '@/api/lnd' +import { fetchBolt12InvoiceFromOffer } from '@/lib/lndk' +import { isBolt12Invoice } from '@/lib/bolt12' +import { parseInvoice } from '@/lib/boltInvoices' +import { toPositiveNumber } from '@/lib/format' export * from '@/wallets/bolt12' export async function testCreateInvoice ({ offer }) { const timeout = 15_000 - return await withTimeout(createInvoice({ msats: 1000, expiry: 1 }, { offer }), timeout) + return await withTimeout((async () => { + const invoice = await fetchBolt12InvoiceFromOffer({ lnd, offer, msats: 1000, description: 'test' }) + if (!isBolt12Invoice(invoice)) throw new Error('not a bolt12 invoice') + const parsedInvoice = await parseInvoice({ lnd, request: invoice }) + if (toPositiveNumber(parsedInvoice.mtokens) !== 1000) throw new Error('invalid invoice response') + return offer + })(), timeout) } export async function createInvoice ({ msats, description, expiry }, { offer }) { From f68b69debc5cffadb2e378031462fae33a602c09 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 17 Dec 2024 20:48:00 +0100 Subject: [PATCH 06/42] Add bolt12 logo --- public/wallets/bolt12-dark.svg | 23 +++++++++++++++++++++++ public/wallets/bolt12.svg | 23 +++++++++++++++++++++++ wallets/bolt12/index.js | 4 +++- 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 public/wallets/bolt12-dark.svg create mode 100644 public/wallets/bolt12.svg diff --git a/public/wallets/bolt12-dark.svg b/public/wallets/bolt12-dark.svg new file mode 100644 index 000000000..5f50d1ef4 --- /dev/null +++ b/public/wallets/bolt12-dark.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/wallets/bolt12.svg b/public/wallets/bolt12.svg new file mode 100644 index 000000000..5f50d1ef4 --- /dev/null +++ b/public/wallets/bolt12.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wallets/bolt12/index.js b/wallets/bolt12/index.js index 8cee7f8ad..db4ead0e6 100644 --- a/wallets/bolt12/index.js +++ b/wallets/bolt12/index.js @@ -19,5 +19,7 @@ export const fields = [ export const card = { title: 'Bolt12', - subtitle: 'bolt12' + subtitle: 'bolt12', + image: { src: '/wallets/bolt12.svg' } + } From 7ff3e1bb4e3da0a5dbd27240d37e9beb0aac18fe Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 17 Dec 2024 20:49:30 +0100 Subject: [PATCH 07/42] improve labels --- wallets/bolt12/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/wallets/bolt12/index.js b/wallets/bolt12/index.js index db4ead0e6..af31f5eb7 100644 --- a/wallets/bolt12/index.js +++ b/wallets/bolt12/index.js @@ -10,7 +10,6 @@ export const fields = [ label: 'bolt12 offer', type: 'text', placeholder: 'lno....', - hint: 'bolt 12 offer', clear: true, serverOnly: true, validate: string() @@ -19,7 +18,7 @@ export const fields = [ export const card = { title: 'Bolt12', - subtitle: 'bolt12', + subtitle: 'receive payments to a bolt12 offer', image: { src: '/wallets/bolt12.svg' } } From 95246bd59c1ef90367d62f6d5db70d399e764591 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 17 Dec 2024 20:52:35 +0100 Subject: [PATCH 08/42] download from sn fork --- docker/lndk/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/lndk/Dockerfile b/docker/lndk/Dockerfile index 75dc3f443..b19f6ef3f 100644 --- a/docker/lndk/Dockerfile +++ b/docker/lndk/Dockerfile @@ -2,7 +2,7 @@ # glibc 2.39 which is not available on debian or ubuntu images. FROM fedora:40 -ENV INSTALLER_DOWNLOAD_URL="https://github.com/riccardobl/lndk/releases/download/v0.2.0-maxfee" +ENV INSTALLER_DOWNLOAD_URL="https://github.com/stackernews/lndk/releases/download/v0.2.0-maxfee" RUN useradd -u 1000 -m lndk RUN mkdir -p /home/lndk/.lndk From d3263225be8d3639ccc69be68a48d17a9f84c642 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 18 Dec 2024 13:54:01 +0100 Subject: [PATCH 09/42] resolve bolt12 invoice inside attachment --- api/resolvers/wallet.js | 4 ++-- wallets/bolt12/index.js | 1 + wallets/bolt12/server.js | 8 ++++---- wallets/server.js | 24 ++++++++++++------------ 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index 8ae997608..a9b0cc106 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -34,7 +34,7 @@ function injectResolvers (resolvers) { for (const walletDef of walletDefs) { const resolverName = generateResolverName(walletDef.walletField) console.log(resolverName) - resolvers.Mutation[resolverName] = async (parent, { settings, validateLightning, vaultEntries, ...data }, { me, models }) => { + resolvers.Mutation[resolverName] = async (parent, { settings, validateLightning, vaultEntries, ...data }, { me, models, lnd }) => { console.log('resolving', resolverName, { settings, validateLightning, vaultEntries, ...data }) let existingVaultEntries @@ -69,7 +69,7 @@ function injectResolvers (resolvers) { wallet, testCreateInvoice: walletDef.testCreateInvoice && validateLightning && canReceive({ def: walletDef, config: data }) - ? (data) => walletDef.testCreateInvoice(data, { logger }) + ? (data) => walletDef.testCreateInvoice(data, { logger, lnd }) : null }, { settings, diff --git a/wallets/bolt12/index.js b/wallets/bolt12/index.js index af31f5eb7..930093edb 100644 --- a/wallets/bolt12/index.js +++ b/wallets/bolt12/index.js @@ -3,6 +3,7 @@ import { string } from '@/lib/yup' export const name = 'bolt12' export const walletType = 'BOLT12' export const walletField = 'walletBolt12' +export const isBolt12OnlyWallet = true export const fields = [ { diff --git a/wallets/bolt12/server.js b/wallets/bolt12/server.js index e3db9faad..bf945d303 100644 --- a/wallets/bolt12/server.js +++ b/wallets/bolt12/server.js @@ -1,12 +1,11 @@ import { withTimeout } from '@/lib/time' -import lnd from '@/api/lnd' import { fetchBolt12InvoiceFromOffer } from '@/lib/lndk' import { isBolt12Invoice } from '@/lib/bolt12' import { parseInvoice } from '@/lib/boltInvoices' import { toPositiveNumber } from '@/lib/format' export * from '@/wallets/bolt12' -export async function testCreateInvoice ({ offer }) { +export async function testCreateInvoice ({ offer }, { lnd }) { const timeout = 15_000 return await withTimeout((async () => { const invoice = await fetchBolt12InvoiceFromOffer({ lnd, offer, msats: 1000, description: 'test' }) @@ -17,6 +16,7 @@ export async function testCreateInvoice ({ offer }) { })(), timeout) } -export async function createInvoice ({ msats, description, expiry }, { offer }) { - return offer +export async function createInvoice ({ msats, description, expiry }, { offer }, { lnd }) { + const invoice = await fetchBolt12InvoiceFromOffer({ lnd, offer, msats, description }) + return invoice } diff --git a/wallets/server.js b/wallets/server.js index 81309efb8..905cf64d2 100644 --- a/wallets/server.js +++ b/wallets/server.js @@ -15,13 +15,12 @@ import * as webln from '@/wallets/webln' import { walletLogger } from '@/api/resolvers/wallet' import walletDefs from '@/wallets/server' import { parseInvoice } from '@/lib/boltInvoices' -import { isBolt12Offer } from '@/lib/bolt12' +import { isBolt12Offer, isBolt12Invoice } 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' import { canReceive } from './common' import wrapInvoice from './wrap' -import { fetchBolt12InvoiceFromOffer } from '@/lib/lndk' export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln, bolt12] @@ -55,6 +54,9 @@ export async function createInvoice (userId, { msats, description, descriptionHa for (const { def, wallet } of wallets) { const logger = walletLogger({ wallet, models }) + if (def.isBolt12OnlyWallet) { + if (!supportBolt12) continue + } try { logger.info( @@ -68,16 +70,20 @@ export async function createInvoice (userId, { msats, description, descriptionHa invoice = await walletCreateInvoice( { wallet, def }, { msats, description, descriptionHash, expiry }, - { logger, models, lnd }) + { logger, models, lnd, supportBolt12 }) } catch (err) { throw new Error('failed to create invoice: ' + err.message) } - if (!isBolt12Offer(invoice)) { - if (!supportBolt12) continue - checkInvoice(invoice, { msats }, { lnd, logger }) + if (isBolt12Invoice(invoice)) { + if (!supportBolt12) { + throw new Error('the wallet returned a bolt12 invoice, but a bolt11 invoice was expected') + } + } else if (isBolt12Offer(invoice)) { + throw new Error('the wallet returned a bolt12 offer, but an invoice was expected') } + checkInvoice(invoice, { msats }, { lnd, logger }) return { invoice, wallet, logger } } catch (err) { logger.error(err.message, { status: true }) @@ -103,12 +109,6 @@ export async function createWrappedInvoice (userId, logger = walletLogger({ wallet, models }) - // We need a bolt12 invoice to wrap, so we fetch one - if (isBolt12Offer(invoice)) { - invoice = await fetchBolt12InvoiceFromOffer({ lnd, offer: invoice, msats: innerAmount, description }) - checkInvoice(invoice, { msats: innerAmount }, { lnd, logger }) - } - const { invoice: wrappedInvoice, maxFee } = await wrapInvoice( { bolt11: invoice, feePercent }, From 20c3e58f926fd85010764c37931cbaa7367289c2 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 18 Dec 2024 14:10:31 +0100 Subject: [PATCH 10/42] rebase --- wallets/bolt12/server.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/wallets/bolt12/server.js b/wallets/bolt12/server.js index bf945d303..1ce9c0d2e 100644 --- a/wallets/bolt12/server.js +++ b/wallets/bolt12/server.js @@ -1,4 +1,3 @@ -import { withTimeout } from '@/lib/time' import { fetchBolt12InvoiceFromOffer } from '@/lib/lndk' import { isBolt12Invoice } from '@/lib/bolt12' import { parseInvoice } from '@/lib/boltInvoices' @@ -6,14 +5,11 @@ import { toPositiveNumber } from '@/lib/format' export * from '@/wallets/bolt12' export async function testCreateInvoice ({ offer }, { lnd }) { - const timeout = 15_000 - return await withTimeout((async () => { - const invoice = await fetchBolt12InvoiceFromOffer({ lnd, offer, msats: 1000, description: 'test' }) - if (!isBolt12Invoice(invoice)) throw new Error('not a bolt12 invoice') - const parsedInvoice = await parseInvoice({ lnd, request: invoice }) - if (toPositiveNumber(parsedInvoice.mtokens) !== 1000) throw new Error('invalid invoice response') - return offer - })(), timeout) + const invoice = await fetchBolt12InvoiceFromOffer({ lnd, offer, msats: 1000, description: 'test' }) + if (!isBolt12Invoice(invoice)) throw new Error('not a bolt12 invoice') + const parsedInvoice = await parseInvoice({ lnd, request: invoice }) + if (toPositiveNumber(parsedInvoice.mtokens) !== 1000) throw new Error('invalid invoice response') + return offer } export async function createInvoice ({ msats, description, expiry }, { offer }, { lnd }) { From 90f2c9ca5ca7e34ea6f9ac390ea218afb29d377b Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 18 Dec 2024 15:30:44 +0100 Subject: [PATCH 11/42] add support for max_fee_mtokens in bolt12 interface --- lib/bolt11.js | 3 ++- lib/bolt12.js | 4 ++-- lib/boltInvoices.js | 6 +++--- lib/lndk.js | 11 ++++++++--- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/bolt11.js b/lib/bolt11.js index 42ee584e6..c6a0fc9f3 100644 --- a/lib/bolt11.js +++ b/lib/bolt11.js @@ -13,12 +13,13 @@ export async function parseBolt11 ({ request }) { return parsePaymentRequest({ request }) } -export async function payBolt11 ({ lnd, request, max_fee, ...args }) { +export async function payBolt11 ({ lnd, request, max_fee, max_fee_mtokens, ...args }) { if (!isBolt11(request)) throw new Error('not a bolt11 invoice') return payViaPaymentRequest({ lnd, request, max_fee, + max_fee_mtokens, ...args }) } diff --git a/lib/bolt12.js b/lib/bolt12.js index cb6d671a9..71a2c09f4 100644 --- a/lib/bolt12.js +++ b/lib/bolt12.js @@ -19,9 +19,9 @@ export function isBolt12 (invoice) { return isBolt12Offer(invoice) || isBolt12Invoice(invoice) } -export async function payBolt12 ({ lnd, request: invoice, max_fee }) { +export async function payBolt12 ({ lnd, request: invoice, max_fee, max_fee_mtokens }) { if (!isBolt12Invoice(invoice)) throw new Error('not a bolt12 invoice') - return await payViaBolt12PaymentRequest({ lnd, request: invoice, max_fee }) + return await payViaBolt12PaymentRequest({ lnd, request: invoice, max_fee, max_fee_mtokens }) } export async function parseBolt12 ({ lnd, request: invoice }) { diff --git a/lib/boltInvoices.js b/lib/boltInvoices.js index 57e7092c1..32937c125 100644 --- a/lib/boltInvoices.js +++ b/lib/boltInvoices.js @@ -4,11 +4,11 @@ import { payBolt11, parseBolt11, isBolt11 } from './bolt11' import { estimateBolt12RouteFee } from '@/lib/lndk' import { estimateRouteFee } from '@/api/lnd' -export async function payInvoice ({ lnd, request: invoice, max_fee, ...args }) { +export async function payInvoice ({ lnd, request: invoice, max_fee, max_fee_mtokens, ...args }) { if (isBolt12Invoice(invoice)) { - return await payBolt12({ lnd, request: invoice, max_fee, ...args }) + return await payBolt12({ lnd, request: invoice, max_fee, max_fee_mtokens, ...args }) } else if (isBolt11(invoice)) { - return await payBolt11({ lnd, request: invoice, max_fee, ...args }) + return await payBolt11({ lnd, request: invoice, max_fee, max_fee_mtokens, ...args }) } else if (isBolt12Offer(invoice)) { throw new Error('cannot pay bolt12 offer directly, please fetch a bolt12 invoice from the offer first') } else { diff --git a/lib/lndk.js b/lib/lndk.js index 564eb1d43..880deba82 100644 --- a/lib/lndk.js +++ b/lib/lndk.js @@ -1,4 +1,4 @@ -import { msatsToSats, toPositiveNumber } from '@/lib/format' +import { msatsToSats, satsToMsats, toPositiveNumber } from '@/lib/format' import { loadPackageDefinition } from '@grpc/grpc-js' import LNDK_RPC_PROTO from '@/lib/lndkrpc-proto' import protobuf from 'protobufjs' @@ -179,19 +179,24 @@ export async function fetchBolt12InvoiceFromOffer ({ lnd, offer, msats, descript export async function payViaBolt12PaymentRequest ({ lnd, request: invoice_hex_str, - max_fee + max_fee, + max_fee_mtokens }) { const lndk = lnd?.lndk if (!lndk) throw new Error('lndk not installed, please use installLNDK') const parsedInvoice = await parseBolt12Request({ lnd, request: invoice_hex_str }) + if (!max_fee_mtokens && max_fee) { + max_fee_mtokens = toPositiveNumber(satsToMsats(max_fee)) + } + return new Promise((resolve, reject) => { lndk.PayInvoice({ invoice: parsedInvoice.payment, // expects msats amount: https://github.com/lndk-org/lndk/blob/bce93885f5fc97f3ceb15dc470117e10061de018/src/lib.rs#L403 amount: toPositiveNumber(parsedInvoice.mtokens), - max_fee + max_fee: toPositiveNumber(max_fee_mtokens) }, (error, response) => { if (error) { return reject(error) From b6cc65f0402fa16c8ff104e4962917c7661856bc Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 18 Dec 2024 17:22:00 +0100 Subject: [PATCH 12/42] revert some unrelated changes --- wallets/server.js | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/wallets/server.js b/wallets/server.js index 8db3dff7b..1e984ef0f 100644 --- a/wallets/server.js +++ b/wallets/server.js @@ -54,9 +54,7 @@ export async function createInvoice (userId, { msats, description, descriptionHa for (const { def, wallet } of wallets) { const logger = walletLogger({ wallet, models }) - if (def.isBolt12OnlyWallet) { - if (!supportBolt12) continue - } + if (def.isBolt12OnlyWallet && !supportBolt12) continue try { logger.info( @@ -96,25 +94,20 @@ export async function createInvoice (userId, { msats, description, descriptionHa export async function createWrappedInvoice (userId, { msats, feePercent, description, descriptionHash, expiry = 360 }, { predecessorId, models, me, lnd }) { - let logger, invoice, wallet + let logger, bolt11 try { - const innerAmount = toPositiveBigInt(msats) * (100n - feePercent) / 100n - ;({ invoice, wallet } = await createInvoice(userId, { + const { invoice, wallet } = await createInvoice(userId, { // this is the amount the stacker will receive, the other (feePercent)% is our fee - msats: innerAmount, + msats: toPositiveBigInt(msats) * (100n - feePercent) / 100n, description, descriptionHash, expiry - }, { predecessorId, models, lnd })) - + }, { predecessorId, models, lnd }) logger = walletLogger({ wallet, models }) + bolt11 = invoice const { invoice: wrappedInvoice, maxFee } = - await wrapInvoice( - { bolt11: invoice, feePercent }, - { msats, description, descriptionHash }, - { me, lnd } - ) + await wrapInvoice({ bolt11, feePercent }, { msats, description, descriptionHash }, { me, lnd }) return { invoice, @@ -123,7 +116,7 @@ export async function createWrappedInvoice (userId, maxFee } } catch (e) { - logger?.error('invalid invoice: ' + e.message, { bolt11: invoice }) + logger?.error('invalid invoice: ' + e.message, { bolt11 }) throw e } } From 0e56bc87d3e8b0c415feb6751fefe39eac91048c Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Fri, 20 Dec 2024 13:11:11 +0100 Subject: [PATCH 13/42] deduplicate code --- lib/bolt11-tags.js | 5 ++++- lib/bolt11.js | 9 ++------- lib/bolt12-info.js | 16 +++++++++++++++- lib/bolt12.js | 19 ++----------------- 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/lib/bolt11-tags.js b/lib/bolt11-tags.js index 04248a1a4..514085b8e 100644 --- a/lib/bolt11-tags.js +++ b/lib/bolt11-tags.js @@ -1,7 +1,10 @@ import { decode } from 'bolt11' +import { bolt11InvoiceSchema } from '@/lib/validate' export function isBolt11 (request) { - return request.startsWith('lnbc') || request.startsWith('lntb') || request.startsWith('lntbs') || request.startsWith('lnbcrt') + if (!request.startsWith('lnbc') && !request.startsWith('lntb') && !request.startsWith('lntbs') && !request.startsWith('lnbcrt')) return false + bolt11InvoiceSchema.validateSync(request) + return true } export function bolt11Tags (bolt11) { diff --git a/lib/bolt11.js b/lib/bolt11.js index c6a0fc9f3..f068f416d 100644 --- a/lib/bolt11.js +++ b/lib/bolt11.js @@ -1,12 +1,7 @@ /* eslint-disable camelcase */ import { payViaPaymentRequest, parsePaymentRequest } from 'ln-service' -import { bolt11InvoiceSchema } from './validate' - -export function isBolt11 (request) { - if (!request.startsWith('lnbc') && !request.startsWith('lntb') && !request.startsWith('lntbs') && !request.startsWith('lnbcrt')) return false - bolt11InvoiceSchema.validateSync(request) - return true -} +import { isBolt11 } from '@/lib/bolt11-tags' +export { isBolt11 } export async function parseBolt11 ({ request }) { if (!isBolt11(request)) throw new Error('not a bolt11 invoice') diff --git a/lib/bolt12-info.js b/lib/bolt12-info.js index 99a8ef7ce..130657b1a 100644 --- a/lib/bolt12-info.js +++ b/lib/bolt12-info.js @@ -1,12 +1,26 @@ import { deserializeTLVStream } from './tlv' import * as bech32b12 from '@/lib/bech32b12' +import { bolt12OfferSchema, bolt12InvoiceSchema } from './validate' + const TYPE_DESCRIPTION = 10n const TYPE_PAYER_NOTE = 89n const TYPE_PAYMENT_HASH = 168n +export function isBolt12Offer (invoice) { + if (!invoice.startsWith('lno1')) return false + bolt12OfferSchema.validateSync(invoice) + return true +} + +export function isBolt12Invoice (invoice) { + if (!invoice.startsWith('lni1')) return false + bolt12InvoiceSchema.validateSync(invoice) + return true +} + export function isBolt12 (invoice) { - return invoice.startsWith('lni1') || invoice.startsWith('lno1') + return isBolt12Offer(invoice) || isBolt12Invoice(invoice) } export function bolt12Info (bolt12) { diff --git a/lib/bolt12.js b/lib/bolt12.js index 71a2c09f4..25d3462aa 100644 --- a/lib/bolt12.js +++ b/lib/bolt12.js @@ -1,23 +1,8 @@ /* eslint-disable camelcase */ import { payViaBolt12PaymentRequest, parseBolt12Request } from '@/lib/lndk' -import { bolt12OfferSchema, bolt12InvoiceSchema } from './validate' - -export function isBolt12Offer (invoice) { - if (!invoice.startsWith('lno1')) return false - bolt12OfferSchema.validateSync(invoice) - return true -} - -export function isBolt12Invoice (invoice) { - if (!invoice.startsWith('lni1')) return false - bolt12InvoiceSchema.validateSync(invoice) - return true -} - -export function isBolt12 (invoice) { - return isBolt12Offer(invoice) || isBolt12Invoice(invoice) -} +import { isBolt12Invoice, isBolt12Offer, isBolt12 } from '@/lib/bolt12-info' +export { isBolt12Invoice, isBolt12Offer, isBolt12 } export async function payBolt12 ({ lnd, request: invoice, max_fee, max_fee_mtokens }) { if (!isBolt12Invoice(invoice)) throw new Error('not a bolt12 invoice') From efcd9a2d43a34be6ce236273e6731efaa913c563 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 25 Dec 2024 01:56:22 +0100 Subject: [PATCH 14/42] Update lib/validate.js Co-authored-by: ekzyis --- lib/validate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/validate.js b/lib/validate.js index 6e9d48d5f..f57ba571b 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -228,7 +228,7 @@ export const bolt12OfferSchema = string().trim().matches(/^lno1/, 'invalid bolt1 export const bolt12InvoiceSchema = string().trim().matches(/^lni1/, 'invalid bolt12 invoice') export const bolt12WithdrawSchema = object({ offer: bolt12OfferSchema.required('required'), - amount: intValidator.required('required').positive('must be positive').min(1, 'must be at least 1'), + amount: intValidator.required('required').min(1, 'must be at least 1'), maxFee: intValidator.required('required').min(0, 'must be at least 0'), comment: string().max(128, 'must be less than 128') }) From 2411e999d7e7bfc27385636f5076f64cab21d342 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 25 Dec 2024 02:03:03 +0100 Subject: [PATCH 15/42] Update wallets/bolt12/index.js Co-authored-by: ekzyis --- wallets/bolt12/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/wallets/bolt12/index.js b/wallets/bolt12/index.js index 930093edb..43bc32eb0 100644 --- a/wallets/bolt12/index.js +++ b/wallets/bolt12/index.js @@ -21,5 +21,4 @@ export const card = { title: 'Bolt12', subtitle: 'receive payments to a bolt12 offer', image: { src: '/wallets/bolt12.svg' } - } From 28c24d537982662774c723011c98fca6ae517b18 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 25 Dec 2024 02:12:23 +0100 Subject: [PATCH 16/42] fix trigger name --- .../migrations/20241212160430_bolt12_attachment/migration.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma/migrations/20241212160430_bolt12_attachment/migration.sql b/prisma/migrations/20241212160430_bolt12_attachment/migration.sql index cb718f171..d700a37ee 100644 --- a/prisma/migrations/20241212160430_bolt12_attachment/migration.sql +++ b/prisma/migrations/20241212160430_bolt12_attachment/migration.sql @@ -20,6 +20,6 @@ ALTER TABLE "WalletBolt12" ADD CONSTRAINT "WalletBolt12_walletId_fkey" FOREIGN K -- Update wallet json -CREATE TRIGGER wallet_blink_as_jsonb +CREATE TRIGGER wallet_bolt12_as_jsonb AFTER INSERT OR UPDATE ON "WalletBolt12" FOR EACH ROW EXECUTE PROCEDURE wallet_wallet_type_as_jsonb(); \ No newline at end of file From f735d6866040eaa72de967b8c238d2b8f71b48ab Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 25 Dec 2024 02:13:16 +0100 Subject: [PATCH 17/42] fix typo --- lib/bech32b12.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/bech32b12.js b/lib/bech32b12.js index be5427d21..dbe501b48 100644 --- a/lib/bech32b12.js +++ b/lib/bech32b12.js @@ -11,17 +11,17 @@ export function decode (str) { if (i === -1) throw new Error('invalid bech32 character') b5s.push(i) } - const b8s = Buffer.from(converBits(b5s, 5, 8, false)) + const b8s = Buffer.from(convertBits(b5s, 5, 8, false)) return b8s } export function encode (b8s) { if (b8s.length > 2048) throw new Error('input is too long') - const b5s = converBits(b8s, 8, 5, true) + const b5s = convertBits(b8s, 8, 5, true) return b5s.map(b5 => ALPHABET[b5]).join('') } -function converBits (data, frombits, tobits, pad) { +function convertBits (data, frombits, tobits, pad) { let acc = 0 let bits = 0 const ret = [] From 86a36aecd7e829cc71db51266b2ed7f3b84ac6c5 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 25 Dec 2024 02:27:15 +0100 Subject: [PATCH 18/42] use String() to cast strings --- lib/lndk.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/lndk.js b/lib/lndk.js index 880deba82..cde908fc3 100644 --- a/lib/lndk.js +++ b/lib/lndk.js @@ -127,14 +127,14 @@ export async function parseBolt12Request ({ id: Buffer.from(payment_hash.hash).toString('hex'), is_expired: new Date().getTime() / 1000 > created_at + relative_expiry, // [metadata] - mtokens: '' + amount_msats, + mtokens: String(amount_msats), network: chainsMap[chain], payment: invoice_hex_str, routes: invoice_contents.payment_paths.map((path) => { const info = path.blinded_pay_info const { introduction_node } = path.blinded_path return { - base_fee_mtokens: '' + info.fee_base_msat, + base_fee_mtokens: String(info.fee_base_msat), cltv_delta: info.cltv_expiry_delta, public_key: Buffer.from(introduction_node.node_id.key).toString('hex') } From aae6de91c8c43d337344aca6b2912c75bdb6de82 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 25 Dec 2024 02:27:29 +0100 Subject: [PATCH 19/42] catch errors in async callback --- lib/lndk.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/lndk.js b/lib/lndk.js index cde908fc3..d4557286a 100644 --- a/lib/lndk.js +++ b/lib/lndk.js @@ -159,19 +159,23 @@ export async function fetchBolt12InvoiceFromOffer ({ lnd, offer, msats, descript payer_note: description, response_invoice_timeout: timeout }, async (error, response) => { - if (error) return reject(error) - const bech32invoice = 'lni1' + bech32b12.encode(Buffer.from(response.invoice_hex_str, 'hex')) - - // sanity check - const parsedInvoice = await parseBolt12Request({ lnd, request: bech32invoice }) - if ( - !parsedInvoice || - toPositiveNumber(parsedInvoice.mtokens) !== toPositiveNumber(msats) || - toPositiveNumber(parsedInvoice.tokens) !== toPositiveNumber(msatsToSats(msats)) - ) { - return reject(new Error('invalid invoice response')) + try { + if (error) return reject(error) + const bech32invoice = 'lni1' + bech32b12.encode(Buffer.from(response.invoice_hex_str, 'hex')) + + // sanity check + const parsedInvoice = await parseBolt12Request({ lnd, request: bech32invoice }) + if ( + !parsedInvoice || + toPositiveNumber(parsedInvoice.mtokens) !== toPositiveNumber(msats) || + toPositiveNumber(parsedInvoice.tokens) !== toPositiveNumber(msatsToSats(msats)) + ) { + return reject(new Error('invalid invoice response')) + } + resolve(bech32invoice) + } catch (e) { + reject(e) } - resolve(bech32invoice) }) }) } From 41919199f01d772075e977882698b055794a1579 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 25 Dec 2024 02:27:49 +0100 Subject: [PATCH 20/42] readd trim removed by mistake --- lib/validate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/validate.js b/lib/validate.js index f57ba571b..2e99588e4 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -478,7 +478,7 @@ export const lastAuthRemovalSchema = object({ }) export const withdrawlSchema = object({ - invoice: string().required('required'), + invoice: string().required('required').trim(), maxFee: intValidator.required('required').min(0, 'must be at least 0') }) From fa9ede49f26ab816056242a6d7c77e5352b0d2c1 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 25 Dec 2024 02:31:26 +0100 Subject: [PATCH 21/42] removed unused default, rename lndSocket to lndkSocket --- lib/lndk.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/lndk.js b/lib/lndk.js index d4557286a..87e332ab1 100644 --- a/lib/lndk.js +++ b/lib/lndk.js @@ -3,13 +3,13 @@ import { loadPackageDefinition } from '@grpc/grpc-js' import LNDK_RPC_PROTO from '@/lib/lndkrpc-proto' import protobuf from 'protobufjs' import grpcCredentials from 'lightning/lnd_grpc/grpc_credentials' -import { defaultSocket, grpcSslCipherSuites } from 'lightning/grpc/index' +import { grpcSslCipherSuites } from 'lightning/grpc/index' import { fromJSON } from '@grpc/proto-loader' import * as bech32b12 from '@/lib/bech32b12' /* eslint-disable camelcase */ const { GRPC_SSL_CIPHER_SUITES } = process.env -export function installLNDK (lnd, { cert, macaroon, socket }, withProxy) { +export function installLNDK (lnd, { cert, macaroon, socket: lndkSocket }, withProxy) { if (lnd.lndk) return // already installed // workaround to load from string @@ -30,13 +30,12 @@ export function installLNDK (lnd, { cert, macaroon, socket }, withProxy) { 'grpc.max_send_message_length': -1, 'grpc.enable_http_proxy': withProxy ? 1 : 0 } - const lndSocket = socket || defaultSocket if (!!cert && GRPC_SSL_CIPHER_SUITES !== grpcSslCipherSuites) { process.env.GRPC_SSL_CIPHER_SUITES = grpcSslCipherSuites } - const client = new OffersService(lndSocket, credentials, params) + const client = new OffersService(lndkSocket, credentials, params) lnd.lndk = client } From 63013c07a7ccbae2b2ef158a7176fb3ae6f41dbb Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 25 Dec 2024 02:47:34 +0100 Subject: [PATCH 22/42] improve feature bit mapping --- lib/lndk.js | 62 ++++++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/lib/lndk.js b/lib/lndk.js index 87e332ab1..a5e8968d3 100644 --- a/lib/lndk.js +++ b/lib/lndk.js @@ -39,34 +39,34 @@ export function installLNDK (lnd, { cert, macaroon, socket: lndkSocket }, withPr lnd.lndk = client } -const featureBitMap = { - 0: { bit: 0, type: 'DATALOSS_PROTECT_REQ', is_required: true }, - 1: { bit: 1, type: 'DATALOSS_PROTECT_OPT', is_required: false }, - 3: { bit: 3, type: 'INITIAL_ROUTING_SYNC', is_required: true }, - 4: { bit: 4, type: 'UPFRONT_SHUTDOWN_SCRIPT_REQ', is_required: true }, - 5: { bit: 5, type: 'UPFRONT_SHUTDOWN_SCRIPT_OPT', is_required: false }, - 6: { bit: 6, type: 'GOSSIP_QUERIES_REQ', is_required: true }, - 7: { bit: 7, type: 'GOSSIP_QUERIES_OPT', is_required: false }, - 8: { bit: 8, type: 'TLV_ONION_REQ', is_required: true }, - 9: { bit: 9, type: 'TLV_ONION_OPT', is_required: false }, - 10: { bit: 10, type: 'EXT_GOSSIP_QUERIES_REQ', is_required: true }, - 11: { bit: 11, type: 'EXT_GOSSIP_QUERIES_OPT', is_required: false }, - 12: { bit: 12, type: 'STATIC_REMOTE_KEY_REQ', is_required: true }, - 13: { bit: 13, type: 'STATIC_REMOTE_KEY_OPT', is_required: false }, - 14: { bit: 14, type: 'PAYMENT_ADDR_REQ', is_required: true }, - 15: { bit: 15, type: 'PAYMENT_ADDR_OPT', is_required: false }, - 16: { bit: 16, type: 'MPP_REQ', is_required: true }, - 17: { bit: 17, type: 'MPP_OPT', is_required: false }, - 18: { bit: 18, type: 'WUMBO_CHANNELS_REQ', is_required: true }, - 19: { bit: 19, type: 'WUMBO_CHANNELS_OPT', is_required: false }, - 20: { bit: 20, type: 'ANCHORS_REQ', is_required: true }, - 21: { bit: 21, type: 'ANCHORS_OPT', is_required: false }, - 22: { bit: 22, type: 'ANCHORS_ZERO_FEE_HTLC_REQ', is_required: true }, - 23: { bit: 23, type: 'ANCHORS_ZERO_FEE_HTLC_OPT', is_required: false }, - 24: { bit: 24, type: 'ROUTE_BLINDING_REQUIRED', is_required: true }, - 25: { bit: 25, type: 'ROUTE_BLINDING_OPTIONAL', is_required: false }, - 30: { bit: 30, type: 'AMP_REQ', is_required: true }, - 31: { bit: 31, type: 'AMP_OPT', is_required: false } +const featureBitTypes = { + 0: 'DATALOSS_PROTECT_REQ', + 1: 'DATALOSS_PROTECT_OPT', + 3: 'INITIAL_ROUTING_SYNC', + 4: 'UPFRONT_SHUTDOWN_SCRIPT_REQ', + 5: 'UPFRONT_SHUTDOWN_SCRIPT_OPT', + 6: 'GOSSIP_QUERIES_REQ', + 7: 'GOSSIP_QUERIES_OPT', + 8: 'TLV_ONION_REQ', + 9: 'TLV_ONION_OPT', + 10: 'EXT_GOSSIP_QUERIES_REQ', + 11: 'EXT_GOSSIP_QUERIES_OPT', + 12: 'STATIC_REMOTE_KEY_REQ', + 13: 'STATIC_REMOTE_KEY_OPT', + 14: 'PAYMENT_ADDR_REQ', + 15: 'PAYMENT_ADDR_OPT', + 16: 'MPP_REQ', + 17: 'MPP_OPT', + 18: 'WUMBO_CHANNELS_REQ', + 19: 'WUMBO_CHANNELS_OPT', + 20: 'ANCHORS_REQ', + 21: 'ANCHORS_OPT', + 22: 'ANCHORS_ZERO_FEE_HTLC_REQ', + 23: 'ANCHORS_ZERO_FEE_HTLC_OPT', + 24: 'ROUTE_BLINDING_REQUIRED', + 25: 'ROUTE_BLINDING_OPTIONAL', + 30: 'AMP_REQ', + 31: 'AMP_OPT' } const chainsMap = { @@ -122,7 +122,11 @@ export async function parseBolt12Request ({ // [description_hash] destination: Buffer.from(node_id.key).toString('hex'), expires_at: new Date((created_at + relative_expiry) * 1000).toISOString(), - features: features.map(bit => featureBitMap[bit]), + features: features.map(bit => ({ + bit, + is_required: (bit % 2) === 0, + type: featureBitTypes[bit] + })), id: Buffer.from(payment_hash.hash).toString('hex'), is_expired: new Date().getTime() / 1000 > created_at + relative_expiry, // [metadata] From 406d3aa1828f0b5e60b6689a660667693b5a8e37 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 25 Dec 2024 02:49:50 +0100 Subject: [PATCH 23/42] add missing await --- wallets/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wallets/server.js b/wallets/server.js index 1e984ef0f..5afa453a0 100644 --- a/wallets/server.js +++ b/wallets/server.js @@ -81,7 +81,7 @@ export async function createInvoice (userId, { msats, description, descriptionHa throw new Error('the wallet returned a bolt12 offer, but an invoice was expected') } - checkInvoice(invoice, { msats }, { lnd, logger }) + await checkInvoice(invoice, { msats }, { lnd, logger }) return { invoice, wallet, logger } } catch (err) { logger.error(err.message, { status: true }) From 501d272413a5497bd608d25292cdff14b2ec3521 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 25 Dec 2024 02:57:17 +0100 Subject: [PATCH 24/42] permalink to repo --- lib/lndkrpc-proto.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/lndkrpc-proto.js b/lib/lndkrpc-proto.js index c4d42db73..54bff47bd 100644 --- a/lib/lndkrpc-proto.js +++ b/lib/lndkrpc-proto.js @@ -1,3 +1,5 @@ +// https://github.com/stackernews/lndk/blob/561aab3f038f937970d91f02a5b49e2ef1188e8f/proto/lndkrpc.proto +// diff https://github.com/stackernews/lndk/commit/e36a2c0c8812185e11a51e38ce9d8fcb513e7446 export default ` syntax = "proto3"; package lndkrpc; From e93dc91a38efcb2fe179c233ba77c8b3ab87324b Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Fri, 27 Dec 2024 11:12:44 +0100 Subject: [PATCH 25/42] remove duplicate checks --- lib/bolt11-tags.js | 4 +--- lib/bolt12-info.js | 8 ++------ wallets/bolt12/server.js | 2 -- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/lib/bolt11-tags.js b/lib/bolt11-tags.js index 514085b8e..8a2a47028 100644 --- a/lib/bolt11-tags.js +++ b/lib/bolt11-tags.js @@ -2,9 +2,7 @@ import { decode } from 'bolt11' import { bolt11InvoiceSchema } from '@/lib/validate' export function isBolt11 (request) { - if (!request.startsWith('lnbc') && !request.startsWith('lntb') && !request.startsWith('lntbs') && !request.startsWith('lnbcrt')) return false - bolt11InvoiceSchema.validateSync(request) - return true + return bolt11InvoiceSchema.isValidSync(request) } export function bolt11Tags (bolt11) { diff --git a/lib/bolt12-info.js b/lib/bolt12-info.js index 130657b1a..cdd5cc5ec 100644 --- a/lib/bolt12-info.js +++ b/lib/bolt12-info.js @@ -8,15 +8,11 @@ const TYPE_PAYER_NOTE = 89n const TYPE_PAYMENT_HASH = 168n export function isBolt12Offer (invoice) { - if (!invoice.startsWith('lno1')) return false - bolt12OfferSchema.validateSync(invoice) - return true + return bolt12OfferSchema.isValidSync(invoice) } export function isBolt12Invoice (invoice) { - if (!invoice.startsWith('lni1')) return false - bolt12InvoiceSchema.validateSync(invoice) - return true + return bolt12InvoiceSchema.isValidSync(invoice) } export function isBolt12 (invoice) { diff --git a/wallets/bolt12/server.js b/wallets/bolt12/server.js index 1ce9c0d2e..4d1b74f1d 100644 --- a/wallets/bolt12/server.js +++ b/wallets/bolt12/server.js @@ -1,12 +1,10 @@ import { fetchBolt12InvoiceFromOffer } from '@/lib/lndk' -import { isBolt12Invoice } from '@/lib/bolt12' import { parseInvoice } from '@/lib/boltInvoices' import { toPositiveNumber } from '@/lib/format' export * from '@/wallets/bolt12' export async function testCreateInvoice ({ offer }, { lnd }) { const invoice = await fetchBolt12InvoiceFromOffer({ lnd, offer, msats: 1000, description: 'test' }) - if (!isBolt12Invoice(invoice)) throw new Error('not a bolt12 invoice') const parsedInvoice = await parseInvoice({ lnd, request: invoice }) if (toPositiveNumber(parsedInvoice.mtokens) !== 1000) throw new Error('invalid invoice response') return offer From a1b534e829464c4773c0dc11d1b812b64ef76473 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Fri, 27 Dec 2024 11:12:58 +0100 Subject: [PATCH 26/42] validate offer string --- wallets/bolt12/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wallets/bolt12/index.js b/wallets/bolt12/index.js index 43bc32eb0..8b90b9103 100644 --- a/wallets/bolt12/index.js +++ b/wallets/bolt12/index.js @@ -13,7 +13,7 @@ export const fields = [ placeholder: 'lno....', clear: true, serverOnly: true, - validate: string() + validate: string().matches(/^lno[a-z0-9]+$/, 'invalid offer string') } ] From 702f24e73c93d77ea898143a698ed103b871dfcc Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Fri, 27 Dec 2024 11:13:16 +0100 Subject: [PATCH 27/42] revert change from older iteration --- wallets/server.js | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/wallets/server.js b/wallets/server.js index 5afa453a0..d7ec2dced 100644 --- a/wallets/server.js +++ b/wallets/server.js @@ -26,26 +26,6 @@ export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln, bolt const MAX_PENDING_INVOICES_PER_WALLET = 25 -async function checkInvoice (invoice, { msats }, { lnd, logger }) { - const parsedInvoice = await parseInvoice({ lnd, request: invoice }) - logger.info(`created invoice for ${formatSats(msatsToSats(parsedInvoice.mtokens))}`, { - bolt11: invoice - }) - if (BigInt(parsedInvoice.mtokens) !== BigInt(msats)) { - if (BigInt(parsedInvoice.mtokens) > BigInt(msats)) { - throw new Error('invoice invalid: amount too big') - } - if (BigInt(parsedInvoice.mtokens) === 0n) { - throw new Error('invoice invalid: amount is 0 msats') - } - if (BigInt(msats) - BigInt(parsedInvoice.mtokens) >= 1000n) { - throw new Error('invoice invalid: amount too small') - } - - logger.warn('wallet does not support msats') - } -} - export async function createInvoice (userId, { msats, description, descriptionHash, expiry = 360, supportBolt12 = true }, { predecessorId, models, lnd }) { // get the wallets in order of priority const wallets = await getInvoiceableWallets(userId, { predecessorId, models }) @@ -81,7 +61,25 @@ export async function createInvoice (userId, { msats, description, descriptionHa throw new Error('the wallet returned a bolt12 offer, but an invoice was expected') } - await checkInvoice(invoice, { msats }, { lnd, logger }) + const parsedInvoice = await parseInvoice({ lnd, request: invoice }) + logger.info(`created invoice for ${formatSats(msatsToSats(parsedInvoice.mtokens))}`, { + bolt11: invoice + }) + + if (BigInt(parsedInvoice.mtokens) !== BigInt(msats)) { + if (BigInt(parsedInvoice.mtokens) > BigInt(msats)) { + throw new Error('invoice invalid: amount too big') + } + if (BigInt(parsedInvoice.mtokens) === 0n) { + throw new Error('invoice invalid: amount is 0 msats') + } + if (BigInt(msats) - BigInt(parsedInvoice.mtokens) >= 1000n) { + throw new Error('invoice invalid: amount too small') + } + + logger.warn('wallet does not support msats') + } + return { invoice, wallet, logger } } catch (err) { logger.error(err.message, { status: true }) From b1b37d7a5d707d796e63e4b274d96801645108ba Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 29 Dec 2024 11:19:37 +0100 Subject: [PATCH 28/42] refactor lndk client --- lib/bolt12.js | 101 +++++++++++++++++++++++++++++++- lib/lndk.js | 156 +++++++++++--------------------------------------- 2 files changed, 134 insertions(+), 123 deletions(-) diff --git a/lib/bolt12.js b/lib/bolt12.js index 25d3462aa..3039ccaca 100644 --- a/lib/bolt12.js +++ b/lib/bolt12.js @@ -1,7 +1,8 @@ /* eslint-disable camelcase */ -import { payViaBolt12PaymentRequest, parseBolt12Request } from '@/lib/lndk' +import { payViaBolt12PaymentRequest, decodeBolt12Invoice } from '@/lib/lndk' import { isBolt12Invoice, isBolt12Offer, isBolt12 } from '@/lib/bolt12-info' +import { toPositiveNumber } from '@/lib/format' export { isBolt12Invoice, isBolt12Offer, isBolt12 } export async function payBolt12 ({ lnd, request: invoice, max_fee, max_fee_mtokens }) { @@ -11,5 +12,101 @@ export async function payBolt12 ({ lnd, request: invoice, max_fee, max_fee_mtoke export async function parseBolt12 ({ lnd, request: invoice }) { if (!isBolt12Invoice(invoice)) throw new Error('not a bolt12 request') - return await parseBolt12Request({ lnd, request: invoice }) + const decodedInvoice = await decodeBolt12Invoice({ lnd, request: invoice }) + return convertBolt12RequestToLNRequest(decodedInvoice) +} + +const featureBitTypes = { + 0: 'DATALOSS_PROTECT_REQ', + 1: 'DATALOSS_PROTECT_OPT', + 3: 'INITIAL_ROUTING_SYNC', + 4: 'UPFRONT_SHUTDOWN_SCRIPT_REQ', + 5: 'UPFRONT_SHUTDOWN_SCRIPT_OPT', + 6: 'GOSSIP_QUERIES_REQ', + 7: 'GOSSIP_QUERIES_OPT', + 8: 'TLV_ONION_REQ', + 9: 'TLV_ONION_OPT', + 10: 'EXT_GOSSIP_QUERIES_REQ', + 11: 'EXT_GOSSIP_QUERIES_OPT', + 12: 'STATIC_REMOTE_KEY_REQ', + 13: 'STATIC_REMOTE_KEY_OPT', + 14: 'PAYMENT_ADDR_REQ', + 15: 'PAYMENT_ADDR_OPT', + 16: 'MPP_REQ', + 17: 'MPP_OPT', + 18: 'WUMBO_CHANNELS_REQ', + 19: 'WUMBO_CHANNELS_OPT', + 20: 'ANCHORS_REQ', + 21: 'ANCHORS_OPT', + 22: 'ANCHORS_ZERO_FEE_HTLC_REQ', + 23: 'ANCHORS_ZERO_FEE_HTLC_OPT', + 24: 'ROUTE_BLINDING_REQUIRED', + 25: 'ROUTE_BLINDING_OPTIONAL', + 30: 'AMP_REQ', + 31: 'AMP_OPT' +} + +const chainsMap = { + '06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f': 'regtest', + '43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000': 'testnet', + '6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000': 'mainnet' +} + +async function convertBolt12RequestToLNRequest (decodedInvoice) { + const { + amount_msats, + description, + node_id, + chain, + payment_hash, + created_at, + relative_expiry, + features, + payer_note, + payment_paths, + invoice_hex_str + } = decodedInvoice + + // convert from lndk response to ln-service parsePaymentRequest output layout + let minCltvDelta + for (const path of payment_paths) { + const info = path.blinded_pay_info + if (minCltvDelta === undefined || info.cltv_expiry_delta < minCltvDelta) { + minCltvDelta = info.cltv_expiry_delta + } + } + + const out = { + created_at: new Date(created_at * 1000).toISOString(), + // [chain_addresses] + cltv_delta: minCltvDelta, + description: payer_note || description, + // [description_hash] + destination: Buffer.from(node_id.key).toString('hex'), + expires_at: new Date((created_at + relative_expiry) * 1000).toISOString(), + features: features.map(bit => ({ + bit, + is_required: (bit % 2) === 0, + type: featureBitTypes[bit] + })), + id: Buffer.from(payment_hash.hash).toString('hex'), + is_expired: new Date().getTime() / 1000 > created_at + relative_expiry, + // [metadata] + mtokens: String(amount_msats), + network: chainsMap[chain], + payment: invoice_hex_str, + routes: payment_paths.map((path) => { + const info = path.blinded_pay_info + const { introduction_node } = path.blinded_path + return { + base_fee_mtokens: String(info.fee_base_msat), + cltv_delta: info.cltv_expiry_delta, + public_key: Buffer.from(introduction_node.node_id.key).toString('hex') + } + }), + safe_tokens: Math.round(toPositiveNumber(BigInt(amount_msats)) / 1000), + tokens: Math.floor(toPositiveNumber(BigInt(amount_msats)) / 1000), + bolt12: decodedInvoice + } + return out } diff --git a/lib/lndk.js b/lib/lndk.js index a5e8968d3..f8f8cb1fc 100644 --- a/lib/lndk.js +++ b/lib/lndk.js @@ -9,22 +9,21 @@ import * as bech32b12 from '@/lib/bech32b12' /* eslint-disable camelcase */ const { GRPC_SSL_CIPHER_SUITES } = process.env +const lndkInstances = new WeakMap() + export function installLNDK (lnd, { cert, macaroon, socket: lndkSocket }, withProxy) { - if (lnd.lndk) return // already installed + // already installed + if (lndkInstances.has(lnd)) return // workaround to load from string - const protoArgs = { - keepCase: true, - longs: Number, - defaults: true, - oneofs: true - } + const protoArgs = { keepCase: true, longs: Number, defaults: true, oneofs: true } const proto = protobuf.parse(LNDK_RPC_PROTO, protoArgs).root const packageDefinition = fromJSON(proto.toJSON(), protoArgs) const protoDescriptor = loadPackageDefinition(packageDefinition) const OffersService = protoDescriptor.lndkrpc.Offers const { credentials } = grpcCredentials({ cert, macaroon }) + const params = { 'grpc.max_receive_message_length': -1, 'grpc.max_send_message_length': -1, @@ -36,55 +35,27 @@ export function installLNDK (lnd, { cert, macaroon, socket: lndkSocket }, withPr } const client = new OffersService(lndkSocket, credentials, params) - lnd.lndk = client -} - -const featureBitTypes = { - 0: 'DATALOSS_PROTECT_REQ', - 1: 'DATALOSS_PROTECT_OPT', - 3: 'INITIAL_ROUTING_SYNC', - 4: 'UPFRONT_SHUTDOWN_SCRIPT_REQ', - 5: 'UPFRONT_SHUTDOWN_SCRIPT_OPT', - 6: 'GOSSIP_QUERIES_REQ', - 7: 'GOSSIP_QUERIES_OPT', - 8: 'TLV_ONION_REQ', - 9: 'TLV_ONION_OPT', - 10: 'EXT_GOSSIP_QUERIES_REQ', - 11: 'EXT_GOSSIP_QUERIES_OPT', - 12: 'STATIC_REMOTE_KEY_REQ', - 13: 'STATIC_REMOTE_KEY_OPT', - 14: 'PAYMENT_ADDR_REQ', - 15: 'PAYMENT_ADDR_OPT', - 16: 'MPP_REQ', - 17: 'MPP_OPT', - 18: 'WUMBO_CHANNELS_REQ', - 19: 'WUMBO_CHANNELS_OPT', - 20: 'ANCHORS_REQ', - 21: 'ANCHORS_OPT', - 22: 'ANCHORS_ZERO_FEE_HTLC_REQ', - 23: 'ANCHORS_ZERO_FEE_HTLC_OPT', - 24: 'ROUTE_BLINDING_REQUIRED', - 25: 'ROUTE_BLINDING_OPTIONAL', - 30: 'AMP_REQ', - 31: 'AMP_OPT' + lndkInstances.set(lnd, client) } -const chainsMap = { - '06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f': 'regtest', - '43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000': 'testnet', - '6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000': 'mainnet' +export function getLNDK (lnd) { + if (!lndkInstances.has(lnd)) { + throw new Error('lndk not available, please use installLNDK first') + } + return lndkInstances.get(lnd) } -export async function parseBolt12Request ({ +export async function decodeBolt12Invoice ({ lnd, request }) { - const lndk = lnd?.lndk - if (!lndk) throw new Error('lndk not installed, please use installLNDK') + const lndk = getLNDK(lnd) - const invoice_hex_str = request.startsWith('lni1') ? bech32b12.decode(request.slice(4)).toString('hex') : request + // decode bech32 bolt12 invoice to hex string + if (!request.startsWith('lni1')) throw new Error('not a valid bech32 encoded bolt12 invoice') + const invoice_hex_str = bech32b12.decode(request.slice(4)).toString('hex') - const invoice_contents = await new Promise((resolve, reject) => { + const decodedRequest = await new Promise((resolve, reject) => { lndk.DecodeInvoice({ invoice: invoice_hex_str }, (error, response) => { @@ -93,66 +64,11 @@ export async function parseBolt12Request ({ }) }) - const { - amount_msats, - description, - node_id, - chain, - payment_hash, - created_at, - relative_expiry, - features, - payer_note - } = invoice_contents - - // convert from lndk response to ln-service parsePaymentRequest output layout - let minCltvDelta - for (const path of invoice_contents.payment_paths) { - const info = path.blinded_pay_info - if (minCltvDelta === undefined || info.cltv_expiry_delta < minCltvDelta) { - minCltvDelta = info.cltv_expiry_delta - } - } - - const out = { - created_at: new Date(created_at * 1000).toISOString(), - // [chain_addresses] - cltv_delta: minCltvDelta, - description: payer_note || description, - // [description_hash] - destination: Buffer.from(node_id.key).toString('hex'), - expires_at: new Date((created_at + relative_expiry) * 1000).toISOString(), - features: features.map(bit => ({ - bit, - is_required: (bit % 2) === 0, - type: featureBitTypes[bit] - })), - id: Buffer.from(payment_hash.hash).toString('hex'), - is_expired: new Date().getTime() / 1000 > created_at + relative_expiry, - // [metadata] - mtokens: String(amount_msats), - network: chainsMap[chain], - payment: invoice_hex_str, - routes: invoice_contents.payment_paths.map((path) => { - const info = path.blinded_pay_info - const { introduction_node } = path.blinded_path - return { - base_fee_mtokens: String(info.fee_base_msat), - cltv_delta: info.cltv_expiry_delta, - public_key: Buffer.from(introduction_node.node_id.key).toString('hex') - } - }), - safe_tokens: Math.round(toPositiveNumber(BigInt(amount_msats)) / 1000), - tokens: Math.floor(toPositiveNumber(BigInt(amount_msats)) / 1000), - bolt12: invoice_contents - } - - return out + return { ...decodedRequest, invoice_hex_str } } export async function fetchBolt12InvoiceFromOffer ({ lnd, offer, msats, description, timeout = 10_000 }) { - const lndk = lnd?.lndk - if (!lndk) throw new Error('lndk not installed, please use installLNDK') + const lndk = getLNDK(lnd) return new Promise((resolve, reject) => { lndk.GetInvoice({ @@ -164,17 +80,15 @@ export async function fetchBolt12InvoiceFromOffer ({ lnd, offer, msats, descript }, async (error, response) => { try { if (error) return reject(error) + // encode hex string invoice to bech32 const bech32invoice = 'lni1' + bech32b12.encode(Buffer.from(response.invoice_hex_str, 'hex')) // sanity check - const parsedInvoice = await parseBolt12Request({ lnd, request: bech32invoice }) - if ( - !parsedInvoice || - toPositiveNumber(parsedInvoice.mtokens) !== toPositiveNumber(msats) || - toPositiveNumber(parsedInvoice.tokens) !== toPositiveNumber(msatsToSats(msats)) - ) { + const { amount_msats } = await decodeBolt12Invoice({ lnd, request: bech32invoice }) + if (toPositiveNumber(amount_msats) !== toPositiveNumber(msats)) { return reject(new Error('invalid invoice response')) } + resolve(bech32invoice) } catch (e) { reject(e) @@ -189,10 +103,9 @@ export async function payViaBolt12PaymentRequest ({ max_fee, max_fee_mtokens }) { - const lndk = lnd?.lndk - if (!lndk) throw new Error('lndk not installed, please use installLNDK') + const lndk = getLNDK(lnd) - const parsedInvoice = await parseBolt12Request({ lnd, request: invoice_hex_str }) + const { amount_msats } = await decodeBolt12Invoice({ lnd, request: invoice_hex_str }) if (!max_fee_mtokens && max_fee) { max_fee_mtokens = toPositiveNumber(satsToMsats(max_fee)) @@ -200,9 +113,9 @@ export async function payViaBolt12PaymentRequest ({ return new Promise((resolve, reject) => { lndk.PayInvoice({ - invoice: parsedInvoice.payment, + invoice: invoice_hex_str, // expects msats amount: https://github.com/lndk-org/lndk/blob/bce93885f5fc97f3ceb15dc470117e10061de018/src/lib.rs#L403 - amount: toPositiveNumber(parsedInvoice.mtokens), + amount: toPositiveNumber(amount_msats), max_fee: toPositiveNumber(max_fee_mtokens) }, (error, response) => { if (error) { @@ -216,13 +129,14 @@ export async function payViaBolt12PaymentRequest ({ } export async function estimateBolt12RouteFee ({ lnd, destination, tokens, mtokens, request, timeout }) { - const lndk = lnd?.lndk - if (!lndk) throw new Error('lndk not installed, please use installLNDK') - const parsedInvoice = request ? await parseBolt12Request({ lnd, request }) : {} - if (!tokens && mtokens) tokens = toPositiveNumber(msatsToSats(mtokens)) - tokens ??= toPositiveNumber(parsedInvoice.tokens) - destination ??= parsedInvoice.destination + + if ((!tokens || !destination) && request) { + // read tokens and destination from the invoice if they are not provided + const { amount_msats, node_id } = request ? await decodeBolt12Invoice({ lnd, request }) : {} + tokens ||= msatsToSats(toPositiveNumber(amount_msats)) + destination ??= Buffer.from(node_id.key).toString('hex') + } if (!destination) throw new Error('no destination provided') if (!tokens) throw new Error('no tokens provided') From 7e94360cad03f1cc40c9c7a8c817d18f5f925197 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 29 Dec 2024 11:25:09 +0100 Subject: [PATCH 29/42] move bolt libs into lib/bolt --- api/paidAction/index.js | 2 +- api/payingAction/index.js | 2 +- api/resolvers/wallet.js | 8 ++++---- components/bolt11-info.js | 4 ++-- lib/{ => bolt}/bolt11-tags.js | 0 lib/{ => bolt}/bolt11.js | 2 +- lib/{ => bolt}/bolt12-info.js | 4 ++-- lib/{ => bolt}/bolt12.js | 2 +- lib/{boltInvoices.js => bolt/index.js} | 4 ++-- wallets/bolt12/server.js | 2 +- wallets/server.js | 4 ++-- wallets/wrap.js | 2 +- worker/paidAction.js | 2 +- 13 files changed, 19 insertions(+), 19 deletions(-) rename lib/{ => bolt}/bolt11-tags.js (100%) rename lib/{ => bolt}/bolt11.js (91%) rename lib/{ => bolt}/bolt12-info.js (90%) rename lib/{ => bolt}/bolt12.js (99%) rename lib/{boltInvoices.js => bolt/index.js} (94%) diff --git a/api/paidAction/index.js b/api/paidAction/index.js index 714a96e3d..d1dff39e6 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/bolt11' +import { parseBolt11 } from '@/lib/bolt/bolt11' import * as ITEM_CREATE from './itemCreate' import * as ITEM_UPDATE from './itemUpdate' diff --git a/api/payingAction/index.js b/api/payingAction/index.js index 745357b04..2d5a6bb8e 100644 --- a/api/payingAction/index.js +++ b/api/payingAction/index.js @@ -1,7 +1,7 @@ import { LND_PATHFINDING_TIME_PREF_PPM, LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants' import { msatsToSats, satsToMsats, toPositiveBigInt } from '@/lib/format' import { Prisma } from '@prisma/client' -import { payInvoice, parseInvoice } from '@/lib/boltInvoices' +import { payInvoice, parseInvoice } from '@/lib/bolt' // paying actions are completely distinct from paid actions // and there's only one paying action: send diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index 580f673ec..9da72629a 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -13,8 +13,8 @@ import { import { amountSchema, validateSchema, withdrawlSchema, lnAddrSchema } from '@/lib/validate' import assertGofacYourself from './ofac' import assertApiKeyNotPermitted from './apiKey' -import { bolt11Tags, isBolt11 } from '@/lib/bolt11-tags' -import { bolt12Info } from '@/lib/bolt12-info' +import { bolt11Tags, isBolt11 } from '@/lib/bolt/bolt11-tags' +import { bolt12Info } from '@/lib/bolt/bolt12-info' import { finalizeHodlInvoice } from '@/worker/wallet' import walletDefs from '@/wallets/server' import { generateResolverName, generateTypeDefName } from '@/wallets/graphql' @@ -25,9 +25,9 @@ import validateWallet from '@/wallets/validate' import { canReceive, getWalletByType } from '@/wallets/common' import performPaidAction from '../paidAction' import performPayingAction from '../payingAction' -import { parseInvoice } from '@/lib/boltInvoices' +import { parseInvoice } from '@/lib/bolt' import lnd from '@/api/lnd' -import { isBolt12Offer } from '@/lib/bolt12' +import { isBolt12Offer } from '@/lib/bolt/bolt12' import { fetchBolt12InvoiceFromOffer } from '@/lib/lndk' import { timeoutSignal, withTimeout } from '@/lib/time' diff --git a/components/bolt11-info.js b/components/bolt11-info.js index f6b3bb761..feac4a4ce 100644 --- a/components/bolt11-info.js +++ b/components/bolt11-info.js @@ -1,7 +1,7 @@ import AccordianItem from './accordian-item' import { CopyInput } from './form' -import { bolt11Tags, isBolt11 } from '@/lib/bolt11-tags' -import { bolt12Info } from '@/lib/bolt12-info' +import { bolt11Tags, isBolt11 } from '@/lib/bolt/bolt11-tags' +import { bolt12Info } from '@/lib/bolt/bolt12-info' export default ({ bolt11, preimage, children }) => { let description, paymentHash diff --git a/lib/bolt11-tags.js b/lib/bolt/bolt11-tags.js similarity index 100% rename from lib/bolt11-tags.js rename to lib/bolt/bolt11-tags.js diff --git a/lib/bolt11.js b/lib/bolt/bolt11.js similarity index 91% rename from lib/bolt11.js rename to lib/bolt/bolt11.js index f068f416d..bfb4ccb4a 100644 --- a/lib/bolt11.js +++ b/lib/bolt/bolt11.js @@ -1,6 +1,6 @@ /* eslint-disable camelcase */ import { payViaPaymentRequest, parsePaymentRequest } from 'ln-service' -import { isBolt11 } from '@/lib/bolt11-tags' +import { isBolt11 } from '@/lib/bolt/bolt11-tags' export { isBolt11 } export async function parseBolt11 ({ request }) { diff --git a/lib/bolt12-info.js b/lib/bolt/bolt12-info.js similarity index 90% rename from lib/bolt12-info.js rename to lib/bolt/bolt12-info.js index cdd5cc5ec..75be93fdc 100644 --- a/lib/bolt12-info.js +++ b/lib/bolt/bolt12-info.js @@ -1,7 +1,7 @@ -import { deserializeTLVStream } from './tlv' +import { deserializeTLVStream } from '../tlv' import * as bech32b12 from '@/lib/bech32b12' -import { bolt12OfferSchema, bolt12InvoiceSchema } from './validate' +import { bolt12OfferSchema, bolt12InvoiceSchema } from '../validate' const TYPE_DESCRIPTION = 10n const TYPE_PAYER_NOTE = 89n diff --git a/lib/bolt12.js b/lib/bolt/bolt12.js similarity index 99% rename from lib/bolt12.js rename to lib/bolt/bolt12.js index 3039ccaca..2ff0fac1e 100644 --- a/lib/bolt12.js +++ b/lib/bolt/bolt12.js @@ -1,7 +1,7 @@ /* eslint-disable camelcase */ import { payViaBolt12PaymentRequest, decodeBolt12Invoice } from '@/lib/lndk' -import { isBolt12Invoice, isBolt12Offer, isBolt12 } from '@/lib/bolt12-info' +import { isBolt12Invoice, isBolt12Offer, isBolt12 } from '@/lib/bolt/bolt12-info' import { toPositiveNumber } from '@/lib/format' export { isBolt12Invoice, isBolt12Offer, isBolt12 } diff --git a/lib/boltInvoices.js b/lib/bolt/index.js similarity index 94% rename from lib/boltInvoices.js rename to lib/bolt/index.js index 32937c125..89ebc0150 100644 --- a/lib/boltInvoices.js +++ b/lib/bolt/index.js @@ -1,6 +1,6 @@ /* eslint-disable camelcase */ -import { payBolt12, parseBolt12, isBolt12Invoice, isBolt12Offer } from './bolt12' -import { payBolt11, parseBolt11, isBolt11 } from './bolt11' +import { payBolt12, parseBolt12, isBolt12Invoice, isBolt12Offer } from '@/lib/bolt/bolt12' +import { payBolt11, parseBolt11, isBolt11 } from '@/lib/bolt/bolt11' import { estimateBolt12RouteFee } from '@/lib/lndk' import { estimateRouteFee } from '@/api/lnd' diff --git a/wallets/bolt12/server.js b/wallets/bolt12/server.js index 4d1b74f1d..23e1e6827 100644 --- a/wallets/bolt12/server.js +++ b/wallets/bolt12/server.js @@ -1,5 +1,5 @@ import { fetchBolt12InvoiceFromOffer } from '@/lib/lndk' -import { parseInvoice } from '@/lib/boltInvoices' +import { parseInvoice } from '@/lib/bolt' import { toPositiveNumber } from '@/lib/format' export * from '@/wallets/bolt12' diff --git a/wallets/server.js b/wallets/server.js index d7ec2dced..820b7009c 100644 --- a/wallets/server.js +++ b/wallets/server.js @@ -14,8 +14,8 @@ import * as webln from '@/wallets/webln' import { walletLogger } from '@/api/resolvers/wallet' import walletDefs from '@/wallets/server' -import { parseInvoice } from '@/lib/boltInvoices' -import { isBolt12Offer, isBolt12Invoice } from '@/lib/bolt12' +import { parseInvoice } from '@/lib/bolt' +import { isBolt12Offer, isBolt12Invoice } from '@/lib/bolt/bolt12' import { toPositiveBigInt, toPositiveNumber, formatMsats, formatSats, msatsToSats } from '@/lib/format' import { PAID_ACTION_TERMINAL_STATES, WALLET_CREATE_INVOICE_TIMEOUT_MS } from '@/lib/constants' import { timeoutSignal, withTimeout } from '@/lib/time' diff --git a/wallets/wrap.js b/wallets/wrap.js index 7f345de3a..37abb760f 100644 --- a/wallets/wrap.js +++ b/wallets/wrap.js @@ -1,7 +1,7 @@ import { createHodlInvoice } from 'ln-service' import { getBlockHeight } from '../api/lnd' import { toBigInt, toPositiveBigInt, toPositiveNumber } from '@/lib/format' -import { parseInvoice, estimateFees } from '@/lib/boltInvoices' +import { parseInvoice, estimateFees } from '@/lib/bolt' const MIN_OUTGOING_MSATS = BigInt(900) // the minimum msats we'll allow for the outgoing invoice const MAX_OUTGOING_MSATS = BigInt(900_000_000) // the maximum msats we'll allow for the outgoing invoice diff --git a/worker/paidAction.js b/worker/paidAction.js index 0b52fcaf1..6aeed53c2 100644 --- a/worker/paidAction.js +++ b/worker/paidAction.js @@ -10,7 +10,7 @@ import { getInvoice, settleHodlInvoice } from 'ln-service' -import { payInvoice, parseInvoice } from '@/lib/boltInvoices' +import { payInvoice, parseInvoice } from '@/lib/bolt' import { MIN_SETTLEMENT_CLTV_DELTA } from '@/wallets/wrap' // aggressive finalization retry options const FINALIZE_OPTIONS = { retryLimit: 2 ** 31 - 1, retryBackoff: false, retryDelay: 5, priority: 1000 } From c9432843c51ca98e339e5b0be97bea067f28fc1c Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 29 Dec 2024 11:26:52 +0100 Subject: [PATCH 30/42] use schema from lib/validate --- wallets/bolt12/index.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/wallets/bolt12/index.js b/wallets/bolt12/index.js index 8b90b9103..32e3517b8 100644 --- a/wallets/bolt12/index.js +++ b/wallets/bolt12/index.js @@ -1,5 +1,4 @@ -import { string } from '@/lib/yup' - +import { bolt12OfferSchema } from '@/lib/validate' export const name = 'bolt12' export const walletType = 'BOLT12' export const walletField = 'walletBolt12' @@ -13,7 +12,7 @@ export const fields = [ placeholder: 'lno....', clear: true, serverOnly: true, - validate: string().matches(/^lno[a-z0-9]+$/, 'invalid offer string') + validate: bolt12OfferSchema } ] From 0418486440867117fda82276491a219b855fa872 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 29 Dec 2024 11:41:09 +0100 Subject: [PATCH 31/42] use /api/lnd/estimateRouteFee in estimateBolt12RouteFee --- lib/lndk.js | 44 +++++++++----------------------------------- 1 file changed, 9 insertions(+), 35 deletions(-) diff --git a/lib/lndk.js b/lib/lndk.js index f8f8cb1fc..0dd7fc50d 100644 --- a/lib/lndk.js +++ b/lib/lndk.js @@ -1,11 +1,13 @@ -import { msatsToSats, satsToMsats, toPositiveNumber } from '@/lib/format' +import { satsToMsats, toPositiveNumber } from '@/lib/format' import { loadPackageDefinition } from '@grpc/grpc-js' import LNDK_RPC_PROTO from '@/lib/lndkrpc-proto' import protobuf from 'protobufjs' import grpcCredentials from 'lightning/lnd_grpc/grpc_credentials' import { grpcSslCipherSuites } from 'lightning/grpc/index' import { fromJSON } from '@grpc/proto-loader' +import { estimateRouteFee } from '@/api/lnd' import * as bech32b12 from '@/lib/bech32b12' + /* eslint-disable camelcase */ const { GRPC_SSL_CIPHER_SUITES } = process.env @@ -129,42 +131,14 @@ export async function payViaBolt12PaymentRequest ({ } export async function estimateBolt12RouteFee ({ lnd, destination, tokens, mtokens, request, timeout }) { - if (!tokens && mtokens) tokens = toPositiveNumber(msatsToSats(mtokens)) + const { amount_msats, node_id } = request ? await decodeBolt12Invoice({ lnd, request }) : {} - if ((!tokens || !destination) && request) { - // read tokens and destination from the invoice if they are not provided - const { amount_msats, node_id } = request ? await decodeBolt12Invoice({ lnd, request }) : {} - tokens ||= msatsToSats(toPositiveNumber(amount_msats)) - destination ??= Buffer.from(node_id.key).toString('hex') - } + // extract mtokens and destination from invoice if they are not provided + if (!tokens && !mtokens) mtokens = toPositiveNumber(amount_msats) + destination ??= Buffer.from(node_id.key).toString('hex') if (!destination) throw new Error('no destination provided') - if (!tokens) throw new Error('no tokens provided') - - return await new Promise((resolve, reject) => { - lnd.router.estimateRouteFee({ - dest: Buffer.from(destination, 'hex'), - amt_sat: tokens, - timeout - }, (err, res) => { - if (err) { - if (res?.failure_reason) { - reject(new Error(`unable to estimate route: ${res.failure_reason}`)) - } else { - reject(err) - } - return - } - - if (res.routing_fee_msat < 0 || res.time_lock_delay <= 0) { - reject(new Error('unable to estimate route, excessive values: ' + JSON.stringify(res))) - return - } + if (!tokens && !mtokens) throw new Error('no tokens amount provided') - resolve({ - routingFeeMsat: toPositiveNumber(res.routing_fee_msat), - timeLockDelay: toPositiveNumber(res.time_lock_delay) - }) - }) - }) + return await estimateRouteFee({ lnd, destination, tokens, mtokens, timeout }) } From d0e9ad8460cd007f966f5bbeac7fd59c563e8479 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 29 Dec 2024 11:43:15 +0100 Subject: [PATCH 32/42] rename installLNDK to enableLNDK --- api/lnd/index.js | 4 ++-- lib/lndk.js | 3 ++- worker/index.js | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/api/lnd/index.js b/api/lnd/index.js index ae347714c..32eaf1fc3 100644 --- a/api/lnd/index.js +++ b/api/lnd/index.js @@ -1,7 +1,7 @@ import { cachedFetcher } from '@/lib/fetch' import { toPositiveNumber } from '@/lib/format' import { authenticatedLndGrpc } from '@/lib/lnd' -import { installLNDK } from '@/lib/lndk' +import { enableLNDK } from '@/lib/lndk' import { getIdentity, getHeight, getWalletInfo, getNode, getPayment } from 'ln-service' import { datePivot } from '@/lib/time' import { LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants' @@ -11,7 +11,7 @@ const lnd = global.lnd || authenticatedLndGrpc({ macaroon: process.env.LND_MACAROON, socket: process.env.LND_SOCKET }).lnd -installLNDK(lnd, { +enableLNDK(lnd, { cert: process.env.LNDK_CERT, macaroon: process.env.LNDK_MACAROON, socket: process.env.LNDK_SOCKET diff --git a/lib/lndk.js b/lib/lndk.js index 0dd7fc50d..aef235fc3 100644 --- a/lib/lndk.js +++ b/lib/lndk.js @@ -13,9 +13,10 @@ const { GRPC_SSL_CIPHER_SUITES } = process.env const lndkInstances = new WeakMap() -export function installLNDK (lnd, { cert, macaroon, socket: lndkSocket }, withProxy) { +export function enableLNDK (lnd, { cert, macaroon, socket: lndkSocket }, withProxy) { // already installed if (lndkInstances.has(lnd)) return + console.log('enabling lndk', lndkSocket, 'withProxy', withProxy) // workaround to load from string const protoArgs = { keepCase: true, longs: Number, defaults: true, oneofs: true } diff --git a/worker/index.js b/worker/index.js index 16946e9e3..4841ad921 100644 --- a/worker/index.js +++ b/worker/index.js @@ -17,7 +17,7 @@ import { computeStreaks, checkStreak } from './streak' import { nip57 } from './nostr' import fetch from 'cross-fetch' import { authenticatedLndGrpc } from '@/lib/lnd' -import { installLNDK } from '@/lib/lndk' +import { enableLNDK } from '@/lib/lndk' import { views, rankViews } from './views' import { imgproxy } from './imgproxy' import { deleteItem } from './ephemeralItems' @@ -74,7 +74,7 @@ async function work () { macaroon: process.env.LND_MACAROON, socket: process.env.LND_SOCKET }) - installLNDK(lnd, { + enableLNDK(lnd, { cert: process.env.LNDK_CERT, macaroon: process.env.LNDK_MACAROON, socket: process.env.LNDK_SOCKET From 6471ad6aaf9cc6721246e44537f196152111050a Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 29 Dec 2024 11:57:40 +0100 Subject: [PATCH 33/42] fix error --- lib/lndk.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/lndk.js b/lib/lndk.js index aef235fc3..81454b128 100644 --- a/lib/lndk.js +++ b/lib/lndk.js @@ -43,7 +43,7 @@ export function enableLNDK (lnd, { cert, macaroon, socket: lndkSocket }, withPro export function getLNDK (lnd) { if (!lndkInstances.has(lnd)) { - throw new Error('lndk not available, please use installLNDK first') + throw new Error('lndk not available, please use enableLNDK first') } return lndkInstances.get(lnd) } From 9c40ddbfb6971a0ac2f7009ba53f9fda1936527f Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 29 Dec 2024 12:30:11 +0100 Subject: [PATCH 34/42] fix regression: pass hex string not bech32 invoice to PayInvoice --- lib/lndk.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/lndk.js b/lib/lndk.js index 81454b128..6db997d96 100644 --- a/lib/lndk.js +++ b/lib/lndk.js @@ -102,13 +102,13 @@ export async function fetchBolt12InvoiceFromOffer ({ lnd, offer, msats, descript export async function payViaBolt12PaymentRequest ({ lnd, - request: invoice_hex_str, + request: invoiceBech32, max_fee, max_fee_mtokens }) { const lndk = getLNDK(lnd) - const { amount_msats } = await decodeBolt12Invoice({ lnd, request: invoice_hex_str }) + const { amount_msats, invoice_hex_str } = await decodeBolt12Invoice({ lnd, request: invoiceBech32 }) if (!max_fee_mtokens && max_fee) { max_fee_mtokens = toPositiveNumber(satsToMsats(max_fee)) From 7ab309930589d4051a0cfda99ca9f80d3f9709d9 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 29 Dec 2024 12:32:07 +0100 Subject: [PATCH 35/42] add attach.md --- wallets/bolt12/ATTACH.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 wallets/bolt12/ATTACH.md diff --git a/wallets/bolt12/ATTACH.md b/wallets/bolt12/ATTACH.md new file mode 100644 index 000000000..466ac634d --- /dev/null +++ b/wallets/bolt12/ATTACH.md @@ -0,0 +1,15 @@ + +# wait for channels to be ready + +You'll need to wait for eclair to have at least one channel in NORMAL state + +`sndev cli eclair channels` + + +# get bolt12 offer + +`sndev cli eclair tipjarshowoffer` + +# check channels balance + +`sndev cli eclair usablebalances` From 7eb5234bb3abd1935e32d0737377a2afda2339f9 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 29 Dec 2024 12:59:51 +0100 Subject: [PATCH 36/42] refactor bolt12/bolt11 client parser lib --- api/resolvers/wallet.js | 5 ++--- components/bolt11-info.js | 6 +++--- lib/bolt/bolt-info.js | 12 ++++++++++++ lib/bolt/bolt11-tags.js | 13 ++++++++++++- lib/bolt/bolt12-info.js | 25 +++++++++++++------------ 5 files changed, 42 insertions(+), 19 deletions(-) create mode 100644 lib/bolt/bolt-info.js diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index 9da72629a..5acf39e1f 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -13,8 +13,7 @@ import { import { amountSchema, validateSchema, withdrawlSchema, lnAddrSchema } from '@/lib/validate' import assertGofacYourself from './ofac' import assertApiKeyNotPermitted from './apiKey' -import { bolt11Tags, isBolt11 } from '@/lib/bolt/bolt11-tags' -import { bolt12Info } from '@/lib/bolt/bolt12-info' +import { getInvoiceDescription } from '@/lib/bolt/bolt-info' import { finalizeHodlInvoice } from '@/worker/wallet' import walletDefs from '@/wallets/server' import { generateResolverName, generateTypeDefName } from '@/wallets/graphql' @@ -380,7 +379,7 @@ const resolvers = { f = { ...f, ...f.other } if (f.bolt11) { - f.description = isBolt11(f.bolt11) ? bolt11Tags(f.bolt11).description : bolt12Info(f.bolt11).description + f.description = getInvoiceDescription(f.bolt11) } switch (f.type) { diff --git a/components/bolt11-info.js b/components/bolt11-info.js index feac4a4ce..7c9681dc8 100644 --- a/components/bolt11-info.js +++ b/components/bolt11-info.js @@ -1,12 +1,12 @@ import AccordianItem from './accordian-item' import { CopyInput } from './form' -import { bolt11Tags, isBolt11 } from '@/lib/bolt/bolt11-tags' -import { bolt12Info } from '@/lib/bolt/bolt12-info' +import { getInvoiceDescription, getInvoicePaymentHash } from '@/lib/bolt/bolt-info' export default ({ bolt11, preimage, children }) => { let description, paymentHash if (bolt11) { - ({ description, payment_hash: paymentHash } = isBolt11(bolt11) ? bolt11Tags(bolt11) : bolt12Info(bolt11)) + description = getInvoiceDescription(bolt11) + paymentHash = getInvoicePaymentHash(bolt11) } return ( diff --git a/lib/bolt/bolt-info.js b/lib/bolt/bolt-info.js new file mode 100644 index 000000000..743b8cab3 --- /dev/null +++ b/lib/bolt/bolt-info.js @@ -0,0 +1,12 @@ +import { getBolt11Description, getBolt11PaymentHash } from '@/lib/bolt/bolt11-tags' +import { getBolt12Description, getBolt12PaymentHash, isBolt12 } from '@/lib/bolt/bolt12-info' + +export function getInvoiceDescription (bolt) { + if (isBolt12(bolt)) return getBolt12Description(bolt) + return getBolt11Description(bolt) +} + +export function getInvoicePaymentHash (bolt) { + if (isBolt12(bolt)) return getBolt12PaymentHash(bolt) + return getBolt11PaymentHash(bolt) +} diff --git a/lib/bolt/bolt11-tags.js b/lib/bolt/bolt11-tags.js index 8a2a47028..fc4b56c98 100644 --- a/lib/bolt/bolt11-tags.js +++ b/lib/bolt/bolt11-tags.js @@ -1,3 +1,4 @@ +/* eslint-disable camelcase */ import { decode } from 'bolt11' import { bolt11InvoiceSchema } from '@/lib/validate' @@ -5,7 +6,17 @@ export function isBolt11 (request) { return bolt11InvoiceSchema.isValidSync(request) } -export function bolt11Tags (bolt11) { +function bolt11Tags (bolt11) { if (!isBolt11(bolt11)) throw new Error('not a bolt11 invoice') return decode(bolt11).tagsObject } + +export function getBolt11Description (bolt11) { + const { description } = bolt11Tags(bolt11) + return description +} + +export function getBolt11PaymentHash (bolt11) { + const { payment_hash } = bolt11Tags(bolt11) + return payment_hash +} diff --git a/lib/bolt/bolt12-info.js b/lib/bolt/bolt12-info.js index 75be93fdc..6d7e88cb0 100644 --- a/lib/bolt/bolt12-info.js +++ b/lib/bolt/bolt12-info.js @@ -19,25 +19,26 @@ export function isBolt12 (invoice) { return isBolt12Offer(invoice) || isBolt12Invoice(invoice) } -export function bolt12Info (bolt12) { +export function getBolt12Description (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 = { - description: '', - payment_hash: '' - } - + let description = '' for (const { type, value } of tlv) { if (type === TYPE_DESCRIPTION) { - info.description = value.toString() || info.description + description = value.toString() || description } else if (type === TYPE_PAYER_NOTE) { - info.description = value.toString() || info.description - } else if (type === TYPE_PAYMENT_HASH) { - info.payment_hash = value.toString('hex') + description = value.toString() || description + break } } + return description +} - return info +export function getBolt12PaymentHash (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 paymentHash = tlv.find(({ type }) => type === TYPE_PAYMENT_HASH) + return paymentHash?.value?.toString('hex') } From 143d7bc5d8552184fdb1701c589244a078171fdc Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 29 Dec 2024 16:51:56 +0100 Subject: [PATCH 37/42] move server-only libs to api/lib --- {lib => api/lib}/bolt/bolt11.js | 0 {lib => api/lib}/bolt/bolt12.js | 2 +- api/lib/bolt/index.js | 41 ++++++++++++++++++++++++++++ {lib => api/lib}/lndk.js | 2 +- {lib => api/lib}/lndkrpc-proto.js | 0 api/lnd/index.js | 2 +- api/paidAction/index.js | 2 +- api/payingAction/index.js | 2 +- api/resolvers/wallet.js | 8 +++--- components/bolt11-info.js | 2 +- lib/bolt/bolt-info.js | 12 --------- lib/bolt/index.js | 45 ++++++------------------------- wallets/bolt12/server.js | 4 +-- wallets/server.js | 4 +-- wallets/wrap.js | 2 +- worker/index.js | 2 +- worker/paidAction.js | 2 +- 17 files changed, 66 insertions(+), 66 deletions(-) rename {lib => api/lib}/bolt/bolt11.js (100%) rename {lib => api/lib}/bolt/bolt12.js (99%) create mode 100644 api/lib/bolt/index.js rename {lib => api/lib}/lndk.js (98%) rename {lib => api/lib}/lndkrpc-proto.js (100%) delete mode 100644 lib/bolt/bolt-info.js diff --git a/lib/bolt/bolt11.js b/api/lib/bolt/bolt11.js similarity index 100% rename from lib/bolt/bolt11.js rename to api/lib/bolt/bolt11.js diff --git a/lib/bolt/bolt12.js b/api/lib/bolt/bolt12.js similarity index 99% rename from lib/bolt/bolt12.js rename to api/lib/bolt/bolt12.js index 2ff0fac1e..cca860456 100644 --- a/lib/bolt/bolt12.js +++ b/api/lib/bolt/bolt12.js @@ -1,6 +1,6 @@ /* eslint-disable camelcase */ -import { payViaBolt12PaymentRequest, decodeBolt12Invoice } from '@/lib/lndk' +import { payViaBolt12PaymentRequest, decodeBolt12Invoice } from '@/api/lib/lndk' import { isBolt12Invoice, isBolt12Offer, isBolt12 } from '@/lib/bolt/bolt12-info' import { toPositiveNumber } from '@/lib/format' export { isBolt12Invoice, isBolt12Offer, isBolt12 } diff --git a/api/lib/bolt/index.js b/api/lib/bolt/index.js new file mode 100644 index 000000000..70d69618e --- /dev/null +++ b/api/lib/bolt/index.js @@ -0,0 +1,41 @@ +/* eslint-disable camelcase */ +import { payBolt12, parseBolt12, isBolt12Invoice, isBolt12Offer } from '@/api/lib/bolt/bolt12' +import { payBolt11, parseBolt11, isBolt11 } from '@/api/lib/bolt/bolt11' +import { estimateBolt12RouteFee } from '@/api/lib/lndk' +import { estimateRouteFee } from '@/api/lnd' + +export async function payInvoice ({ lnd, request: invoice, max_fee, max_fee_mtokens, ...args }) { + if (isBolt12Invoice(invoice)) { + return await payBolt12({ lnd, request: invoice, max_fee, max_fee_mtokens, ...args }) + } else if (isBolt11(invoice)) { + return await payBolt11({ lnd, request: invoice, max_fee, max_fee_mtokens, ...args }) + } else if (isBolt12Offer(invoice)) { + throw new Error('cannot pay bolt12 offer directly, please fetch a bolt12 invoice from the offer first') + } else { + throw new Error('unknown invoice type') + } +} + +export async function parseInvoice ({ lnd, request }) { + if (isBolt12Invoice(request)) { + return await parseBolt12({ lnd, request }) + } else if (isBolt11(request)) { + return await parseBolt11({ request }) + } else if (isBolt12Offer(request)) { + throw new Error('bolt12 offer instead of invoice, please fetch a bolt12 invoice from the offer first') + } else { + throw new Error('unknown invoice type') + } +} + +export async function estimateFees ({ lnd, destination, tokens, mtokens, request, timeout }) { + if (isBolt12Invoice(request)) { + return await estimateBolt12RouteFee({ lnd, destination, tokens, mtokens, request, timeout }) + } else if (isBolt11(request)) { + return await estimateRouteFee({ lnd, destination, tokens, request, mtokens, timeout }) + } else if (isBolt12Offer(request)) { + throw new Error('bolt12 offer instead of invoice, please fetch a bolt12 invoice from the offer first') + } else { + throw new Error('unknown invoice type') + } +} diff --git a/lib/lndk.js b/api/lib/lndk.js similarity index 98% rename from lib/lndk.js rename to api/lib/lndk.js index 6db997d96..f589d4348 100644 --- a/lib/lndk.js +++ b/api/lib/lndk.js @@ -1,6 +1,6 @@ import { satsToMsats, toPositiveNumber } from '@/lib/format' import { loadPackageDefinition } from '@grpc/grpc-js' -import LNDK_RPC_PROTO from '@/lib/lndkrpc-proto' +import LNDK_RPC_PROTO from '@/api/lib/lndkrpc-proto' import protobuf from 'protobufjs' import grpcCredentials from 'lightning/lnd_grpc/grpc_credentials' import { grpcSslCipherSuites } from 'lightning/grpc/index' diff --git a/lib/lndkrpc-proto.js b/api/lib/lndkrpc-proto.js similarity index 100% rename from lib/lndkrpc-proto.js rename to api/lib/lndkrpc-proto.js diff --git a/api/lnd/index.js b/api/lnd/index.js index 32eaf1fc3..03af5eaa6 100644 --- a/api/lnd/index.js +++ b/api/lnd/index.js @@ -1,7 +1,7 @@ import { cachedFetcher } from '@/lib/fetch' import { toPositiveNumber } from '@/lib/format' import { authenticatedLndGrpc } from '@/lib/lnd' -import { enableLNDK } from '@/lib/lndk' +import { enableLNDK } from '@/api/lib/lndk' import { getIdentity, getHeight, getWalletInfo, getNode, getPayment } from 'ln-service' import { datePivot } from '@/lib/time' import { LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants' diff --git a/api/paidAction/index.js b/api/paidAction/index.js index d1dff39e6..6976a82a1 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/bolt/bolt11' +import { parseBolt11 } from '@/api/lib/bolt/bolt11' import * as ITEM_CREATE from './itemCreate' import * as ITEM_UPDATE from './itemUpdate' diff --git a/api/payingAction/index.js b/api/payingAction/index.js index 2d5a6bb8e..ebe6a79d1 100644 --- a/api/payingAction/index.js +++ b/api/payingAction/index.js @@ -1,7 +1,7 @@ import { LND_PATHFINDING_TIME_PREF_PPM, LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants' import { msatsToSats, satsToMsats, toPositiveBigInt } from '@/lib/format' import { Prisma } from '@prisma/client' -import { payInvoice, parseInvoice } from '@/lib/bolt' +import { payInvoice, parseInvoice } from '@/api/lib/bolt' // paying actions are completely distinct from paid actions // and there's only one paying action: send diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index 5acf39e1f..fb5024133 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -13,7 +13,7 @@ import { import { amountSchema, validateSchema, withdrawlSchema, lnAddrSchema } from '@/lib/validate' import assertGofacYourself from './ofac' import assertApiKeyNotPermitted from './apiKey' -import { getInvoiceDescription } from '@/lib/bolt/bolt-info' +import { getInvoiceDescription } from '@/lib/bolt' import { finalizeHodlInvoice } from '@/worker/wallet' import walletDefs from '@/wallets/server' import { generateResolverName, generateTypeDefName } from '@/wallets/graphql' @@ -24,10 +24,10 @@ import validateWallet from '@/wallets/validate' import { canReceive, getWalletByType } from '@/wallets/common' import performPaidAction from '../paidAction' import performPayingAction from '../payingAction' -import { parseInvoice } from '@/lib/bolt' +import { parseInvoice } from '@/api/lib/bolt' import lnd from '@/api/lnd' -import { isBolt12Offer } from '@/lib/bolt/bolt12' -import { fetchBolt12InvoiceFromOffer } from '@/lib/lndk' +import { isBolt12Offer } from '@/api/lib/bolt/bolt12' +import { fetchBolt12InvoiceFromOffer } from '@/api/lib/lndk' import { timeoutSignal, withTimeout } from '@/lib/time' function injectResolvers (resolvers) { diff --git a/components/bolt11-info.js b/components/bolt11-info.js index 7c9681dc8..0ce208110 100644 --- a/components/bolt11-info.js +++ b/components/bolt11-info.js @@ -1,6 +1,6 @@ import AccordianItem from './accordian-item' import { CopyInput } from './form' -import { getInvoiceDescription, getInvoicePaymentHash } from '@/lib/bolt/bolt-info' +import { getInvoiceDescription, getInvoicePaymentHash } from '@/lib/bolt' export default ({ bolt11, preimage, children }) => { let description, paymentHash diff --git a/lib/bolt/bolt-info.js b/lib/bolt/bolt-info.js deleted file mode 100644 index 743b8cab3..000000000 --- a/lib/bolt/bolt-info.js +++ /dev/null @@ -1,12 +0,0 @@ -import { getBolt11Description, getBolt11PaymentHash } from '@/lib/bolt/bolt11-tags' -import { getBolt12Description, getBolt12PaymentHash, isBolt12 } from '@/lib/bolt/bolt12-info' - -export function getInvoiceDescription (bolt) { - if (isBolt12(bolt)) return getBolt12Description(bolt) - return getBolt11Description(bolt) -} - -export function getInvoicePaymentHash (bolt) { - if (isBolt12(bolt)) return getBolt12PaymentHash(bolt) - return getBolt11PaymentHash(bolt) -} diff --git a/lib/bolt/index.js b/lib/bolt/index.js index 89ebc0150..743b8cab3 100644 --- a/lib/bolt/index.js +++ b/lib/bolt/index.js @@ -1,41 +1,12 @@ -/* eslint-disable camelcase */ -import { payBolt12, parseBolt12, isBolt12Invoice, isBolt12Offer } from '@/lib/bolt/bolt12' -import { payBolt11, parseBolt11, isBolt11 } from '@/lib/bolt/bolt11' -import { estimateBolt12RouteFee } from '@/lib/lndk' -import { estimateRouteFee } from '@/api/lnd' +import { getBolt11Description, getBolt11PaymentHash } from '@/lib/bolt/bolt11-tags' +import { getBolt12Description, getBolt12PaymentHash, isBolt12 } from '@/lib/bolt/bolt12-info' -export async function payInvoice ({ lnd, request: invoice, max_fee, max_fee_mtokens, ...args }) { - if (isBolt12Invoice(invoice)) { - return await payBolt12({ lnd, request: invoice, max_fee, max_fee_mtokens, ...args }) - } else if (isBolt11(invoice)) { - return await payBolt11({ lnd, request: invoice, max_fee, max_fee_mtokens, ...args }) - } else if (isBolt12Offer(invoice)) { - throw new Error('cannot pay bolt12 offer directly, please fetch a bolt12 invoice from the offer first') - } else { - throw new Error('unknown invoice type') - } +export function getInvoiceDescription (bolt) { + if (isBolt12(bolt)) return getBolt12Description(bolt) + return getBolt11Description(bolt) } -export async function parseInvoice ({ lnd, request }) { - if (isBolt12Invoice(request)) { - return await parseBolt12({ lnd, request }) - } else if (isBolt11(request)) { - return await parseBolt11({ request }) - } else if (isBolt12Offer(request)) { - throw new Error('bolt12 offer instead of invoice, please fetch a bolt12 invoice from the offer first') - } else { - throw new Error('unknown invoice type') - } -} - -export async function estimateFees ({ lnd, destination, tokens, mtokens, request, timeout }) { - if (isBolt12Invoice(request)) { - return await estimateBolt12RouteFee({ lnd, destination, tokens, mtokens, request, timeout }) - } else if (isBolt11(request)) { - return await estimateRouteFee({ lnd, destination, tokens, request, mtokens, timeout }) - } else if (isBolt12Offer(request)) { - throw new Error('bolt12 offer instead of invoice, please fetch a bolt12 invoice from the offer first') - } else { - throw new Error('unknown invoice type') - } +export function getInvoicePaymentHash (bolt) { + if (isBolt12(bolt)) return getBolt12PaymentHash(bolt) + return getBolt11PaymentHash(bolt) } diff --git a/wallets/bolt12/server.js b/wallets/bolt12/server.js index 23e1e6827..a39fdc484 100644 --- a/wallets/bolt12/server.js +++ b/wallets/bolt12/server.js @@ -1,5 +1,5 @@ -import { fetchBolt12InvoiceFromOffer } from '@/lib/lndk' -import { parseInvoice } from '@/lib/bolt' +import { fetchBolt12InvoiceFromOffer } from '@/api/lib/lndk' +import { parseInvoice } from '@/api/lib/bolt' import { toPositiveNumber } from '@/lib/format' export * from '@/wallets/bolt12' diff --git a/wallets/server.js b/wallets/server.js index 820b7009c..d8459b34d 100644 --- a/wallets/server.js +++ b/wallets/server.js @@ -14,8 +14,8 @@ import * as webln from '@/wallets/webln' import { walletLogger } from '@/api/resolvers/wallet' import walletDefs from '@/wallets/server' -import { parseInvoice } from '@/lib/bolt' -import { isBolt12Offer, isBolt12Invoice } from '@/lib/bolt/bolt12' +import { parseInvoice } from '@/api/lib/bolt' +import { isBolt12Offer, isBolt12Invoice } from '@/api/lib/bolt/bolt12' import { toPositiveBigInt, toPositiveNumber, formatMsats, formatSats, msatsToSats } from '@/lib/format' import { PAID_ACTION_TERMINAL_STATES, WALLET_CREATE_INVOICE_TIMEOUT_MS } from '@/lib/constants' import { timeoutSignal, withTimeout } from '@/lib/time' diff --git a/wallets/wrap.js b/wallets/wrap.js index 37abb760f..e343798f9 100644 --- a/wallets/wrap.js +++ b/wallets/wrap.js @@ -1,7 +1,7 @@ import { createHodlInvoice } from 'ln-service' import { getBlockHeight } from '../api/lnd' import { toBigInt, toPositiveBigInt, toPositiveNumber } from '@/lib/format' -import { parseInvoice, estimateFees } from '@/lib/bolt' +import { parseInvoice, estimateFees } from '@/api/lib/bolt' const MIN_OUTGOING_MSATS = BigInt(900) // the minimum msats we'll allow for the outgoing invoice const MAX_OUTGOING_MSATS = BigInt(900_000_000) // the maximum msats we'll allow for the outgoing invoice diff --git a/worker/index.js b/worker/index.js index 4841ad921..f80643613 100644 --- a/worker/index.js +++ b/worker/index.js @@ -17,7 +17,7 @@ import { computeStreaks, checkStreak } from './streak' import { nip57 } from './nostr' import fetch from 'cross-fetch' import { authenticatedLndGrpc } from '@/lib/lnd' -import { enableLNDK } from '@/lib/lndk' +import { enableLNDK } from '@/api/lib/lndk' import { views, rankViews } from './views' import { imgproxy } from './imgproxy' import { deleteItem } from './ephemeralItems' diff --git a/worker/paidAction.js b/worker/paidAction.js index 6aeed53c2..c26bf2f8f 100644 --- a/worker/paidAction.js +++ b/worker/paidAction.js @@ -10,7 +10,7 @@ import { getInvoice, settleHodlInvoice } from 'ln-service' -import { payInvoice, parseInvoice } from '@/lib/bolt' +import { payInvoice, parseInvoice } from '@/api/lib/bolt' import { MIN_SETTLEMENT_CLTV_DELTA } from '@/wallets/wrap' // aggressive finalization retry options const FINALIZE_OPTIONS = { retryLimit: 2 ** 31 - 1, retryBackoff: false, retryDelay: 5, priority: 1000 } From 287e114151e562ea604a36097dbe7487dd4700c8 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Mon, 30 Dec 2024 19:33:11 +0100 Subject: [PATCH 38/42] filter out POST /api/graphql spam from sndev logs --- sndev | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sndev b/sndev index 5d3ad8004..eb9c0e3f1 100755 --- a/sndev +++ b/sndev @@ -7,6 +7,10 @@ if [ -f .env.local ]; then . ./.env.local fi +logFilter() { + grep -v --line-buffered --color=never -P 'POST /api/graphql \x1b\[32m200\x1b\[39m in [0-9]+ms' +} + docker__compose() { if [ ! -x "$(command -v docker)" ]; then echo "docker compose is not installed" @@ -128,7 +132,7 @@ OPTIONS" sndev__logs() { shift if [ $# -eq 1 ]; then - docker__compose logs -t --tail=1000 -f "$@" + docker__compose logs -t --tail=1000 -f "$@" | logFilter exit 0 fi From bb10b6e8093bbbf6f0ad4de5bb83b299ff06b50c Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 7 Jan 2025 13:03:15 +0100 Subject: [PATCH 39/42] implement context maze --- api/lib/bolt/bolt11.js | 8 +++++ api/lib/bolt/bolt12.js | 28 +++++++++++++++--- api/lib/bolt/index.js | 17 +++++------ api/lib/lndk.js | 47 +++++------------------------- api/lnd/index.js | 4 +-- api/paidAction/README.md | 1 + api/paidAction/index.js | 8 ++--- api/payingAction/index.js | 5 ++-- api/resolvers/item.js | 8 ++--- api/resolvers/paidAction.js | 4 +-- api/resolvers/wallet.js | 38 ++++++++++++------------ api/ssrApollo.js | 3 +- pages/api/graphql.js | 3 +- pages/api/lnurlp/[username]/pay.js | 4 +-- pages/api/lnwith.js | 4 +-- wallets/bolt12/server.js | 10 +++---- wallets/server.js | 15 +++++----- wallets/wrap.js | 5 ++-- worker/autowithdraw.js | 6 ++-- worker/index.js | 7 +++-- worker/paidAction.js | 9 +++--- worker/wallet.js | 8 ++--- worker/weeklyPosts.js | 3 +- 23 files changed, 125 insertions(+), 120 deletions(-) diff --git a/api/lib/bolt/bolt11.js b/api/lib/bolt/bolt11.js index bfb4ccb4a..a8a7cc0da 100644 --- a/api/lib/bolt/bolt11.js +++ b/api/lib/bolt/bolt11.js @@ -1,6 +1,7 @@ /* eslint-disable camelcase */ import { payViaPaymentRequest, parsePaymentRequest } from 'ln-service' import { isBolt11 } from '@/lib/bolt/bolt11-tags' +import { estimateRouteFee } from '@/api/lnd' export { isBolt11 } export async function parseBolt11 ({ request }) { @@ -9,6 +10,7 @@ export async function parseBolt11 ({ request }) { } export async function payBolt11 ({ lnd, request, max_fee, max_fee_mtokens, ...args }) { + if (!lnd) throw new Error('lnd required') // check if forgot to pass lnd if (!isBolt11(request)) throw new Error('not a bolt11 invoice') return payViaPaymentRequest({ lnd, @@ -18,3 +20,9 @@ export async function payBolt11 ({ lnd, request, max_fee, max_fee_mtokens, ...ar ...args }) } + +export async function estimateBolt11RouteFee ({ lnd, destination, tokens, mtokens, request, timeout }) { + if (!lnd) throw new Error('lnd required') // check if forgot to pass lnd + if (request && !isBolt11(request)) throw new Error('not a bolt11 request') + return await estimateRouteFee({ lnd, destination, tokens, mtokens, request, timeout }) +} diff --git a/api/lib/bolt/bolt12.js b/api/lib/bolt/bolt12.js index cca860456..a8771f2bd 100644 --- a/api/lib/bolt/bolt12.js +++ b/api/lib/bolt/bolt12.js @@ -3,19 +3,39 @@ import { payViaBolt12PaymentRequest, decodeBolt12Invoice } from '@/api/lib/lndk' import { isBolt12Invoice, isBolt12Offer, isBolt12 } from '@/lib/bolt/bolt12-info' import { toPositiveNumber } from '@/lib/format' +import { estimateRouteFee } from '@/api/lnd' export { isBolt12Invoice, isBolt12Offer, isBolt12 } -export async function payBolt12 ({ lnd, request: invoice, max_fee, max_fee_mtokens }) { +export async function payBolt12 ({ lndk, request: invoice, max_fee, max_fee_mtokens }) { + if (!lndk) throw new Error('lndk required') // check if forgot to pass lndk if (!isBolt12Invoice(invoice)) throw new Error('not a bolt12 invoice') - return await payViaBolt12PaymentRequest({ lnd, request: invoice, max_fee, max_fee_mtokens }) + return await payViaBolt12PaymentRequest({ lndk, request: invoice, max_fee, max_fee_mtokens }) } -export async function parseBolt12 ({ lnd, request: invoice }) { +export async function parseBolt12 ({ lndk, request: invoice }) { + if (!lndk) throw new Error('lndk required') // check if forgot to pass lndk if (!isBolt12Invoice(invoice)) throw new Error('not a bolt12 request') - const decodedInvoice = await decodeBolt12Invoice({ lnd, request: invoice }) + const decodedInvoice = await decodeBolt12Invoice({ lndk, request: invoice }) return convertBolt12RequestToLNRequest(decodedInvoice) } +export async function estimateBolt12RouteFee ({ lnd, lndk, destination, tokens, mtokens, request, timeout }) { + if (!lndk) throw new Error('lndk required') // check if forgot to pass lndk + if (!lnd) throw new Error('lnd required') // check if forgot to pass lnd + if (request && !isBolt12Invoice(request)) throw new Error('not a bolt12 request') + + const { amount_msats, node_id } = request ? await decodeBolt12Invoice({ lndk, request }) : {} + + // extract mtokens and destination from invoice if they are not provided + if (!tokens && !mtokens) mtokens = toPositiveNumber(amount_msats) + destination ??= Buffer.from(node_id.key).toString('hex') + + if (!destination) throw new Error('no destination provided') + if (!tokens && !mtokens) throw new Error('no tokens amount provided') + + return await estimateRouteFee({ lnd, destination, tokens, mtokens, timeout }) +} + const featureBitTypes = { 0: 'DATALOSS_PROTECT_REQ', 1: 'DATALOSS_PROTECT_OPT', diff --git a/api/lib/bolt/index.js b/api/lib/bolt/index.js index 70d69618e..d0777273a 100644 --- a/api/lib/bolt/index.js +++ b/api/lib/bolt/index.js @@ -1,12 +1,11 @@ /* eslint-disable camelcase */ import { payBolt12, parseBolt12, isBolt12Invoice, isBolt12Offer } from '@/api/lib/bolt/bolt12' -import { payBolt11, parseBolt11, isBolt11 } from '@/api/lib/bolt/bolt11' +import { payBolt11, parseBolt11, isBolt11, estimateBolt11RouteFee } from '@/api/lib/bolt/bolt11' import { estimateBolt12RouteFee } from '@/api/lib/lndk' -import { estimateRouteFee } from '@/api/lnd' -export async function payInvoice ({ lnd, request: invoice, max_fee, max_fee_mtokens, ...args }) { +export async function payInvoice ({ lnd, lndk, request: invoice, max_fee, max_fee_mtokens, ...args }) { if (isBolt12Invoice(invoice)) { - return await payBolt12({ lnd, request: invoice, max_fee, max_fee_mtokens, ...args }) + return await payBolt12({ lndk, request: invoice, max_fee, max_fee_mtokens, ...args }) } else if (isBolt11(invoice)) { return await payBolt11({ lnd, request: invoice, max_fee, max_fee_mtokens, ...args }) } else if (isBolt12Offer(invoice)) { @@ -16,9 +15,9 @@ export async function payInvoice ({ lnd, request: invoice, max_fee, max_fee_mtok } } -export async function parseInvoice ({ lnd, request }) { +export async function parseInvoice ({ lndk, request }) { if (isBolt12Invoice(request)) { - return await parseBolt12({ lnd, request }) + return await parseBolt12({ lndk, request }) } else if (isBolt11(request)) { return await parseBolt11({ request }) } else if (isBolt12Offer(request)) { @@ -28,11 +27,11 @@ export async function parseInvoice ({ lnd, request }) { } } -export async function estimateFees ({ lnd, destination, tokens, mtokens, request, timeout }) { +export async function estimateFees ({ lnd, lndk, destination, tokens, mtokens, request, timeout }) { if (isBolt12Invoice(request)) { - return await estimateBolt12RouteFee({ lnd, destination, tokens, mtokens, request, timeout }) + return await estimateBolt12RouteFee({ lnd, lndk, destination, tokens, mtokens, request, timeout }) } else if (isBolt11(request)) { - return await estimateRouteFee({ lnd, destination, tokens, request, mtokens, timeout }) + return await estimateBolt11RouteFee({ lnd, destination, tokens, request, mtokens, timeout }) } else if (isBolt12Offer(request)) { throw new Error('bolt12 offer instead of invoice, please fetch a bolt12 invoice from the offer first') } else { diff --git a/api/lib/lndk.js b/api/lib/lndk.js index f589d4348..527e88403 100644 --- a/api/lib/lndk.js +++ b/api/lib/lndk.js @@ -5,19 +5,12 @@ import protobuf from 'protobufjs' import grpcCredentials from 'lightning/lnd_grpc/grpc_credentials' import { grpcSslCipherSuites } from 'lightning/grpc/index' import { fromJSON } from '@grpc/proto-loader' -import { estimateRouteFee } from '@/api/lnd' import * as bech32b12 from '@/lib/bech32b12' /* eslint-disable camelcase */ const { GRPC_SSL_CIPHER_SUITES } = process.env -const lndkInstances = new WeakMap() - -export function enableLNDK (lnd, { cert, macaroon, socket: lndkSocket }, withProxy) { - // already installed - if (lndkInstances.has(lnd)) return - console.log('enabling lndk', lndkSocket, 'withProxy', withProxy) - +export function authenticatedLndkGrpc ({ cert, macaroon, socket: lndkSocket }, withProxy) { // workaround to load from string const protoArgs = { keepCase: true, longs: Number, defaults: true, oneofs: true } const proto = protobuf.parse(LNDK_RPC_PROTO, protoArgs).root @@ -38,22 +31,13 @@ export function enableLNDK (lnd, { cert, macaroon, socket: lndkSocket }, withPro } const client = new OffersService(lndkSocket, credentials, params) - lndkInstances.set(lnd, client) -} - -export function getLNDK (lnd) { - if (!lndkInstances.has(lnd)) { - throw new Error('lndk not available, please use enableLNDK first') - } - return lndkInstances.get(lnd) + return client } export async function decodeBolt12Invoice ({ - lnd, + lndk, request }) { - const lndk = getLNDK(lnd) - // decode bech32 bolt12 invoice to hex string if (!request.startsWith('lni1')) throw new Error('not a valid bech32 encoded bolt12 invoice') const invoice_hex_str = bech32b12.decode(request.slice(4)).toString('hex') @@ -70,9 +54,7 @@ export async function decodeBolt12Invoice ({ return { ...decodedRequest, invoice_hex_str } } -export async function fetchBolt12InvoiceFromOffer ({ lnd, offer, msats, description, timeout = 10_000 }) { - const lndk = getLNDK(lnd) - +export async function fetchBolt12InvoiceFromOffer ({ lndk, offer, msats, description, timeout = 10_000 }) { return new Promise((resolve, reject) => { lndk.GetInvoice({ offer, @@ -87,7 +69,7 @@ export async function fetchBolt12InvoiceFromOffer ({ lnd, offer, msats, descript const bech32invoice = 'lni1' + bech32b12.encode(Buffer.from(response.invoice_hex_str, 'hex')) // sanity check - const { amount_msats } = await decodeBolt12Invoice({ lnd, request: bech32invoice }) + const { amount_msats } = await decodeBolt12Invoice({ lndk, request: bech32invoice }) if (toPositiveNumber(amount_msats) !== toPositiveNumber(msats)) { return reject(new Error('invalid invoice response')) } @@ -101,14 +83,12 @@ export async function fetchBolt12InvoiceFromOffer ({ lnd, offer, msats, descript } export async function payViaBolt12PaymentRequest ({ - lnd, + lndk, request: invoiceBech32, max_fee, max_fee_mtokens }) { - const lndk = getLNDK(lnd) - - const { amount_msats, invoice_hex_str } = await decodeBolt12Invoice({ lnd, request: invoiceBech32 }) + const { amount_msats, invoice_hex_str } = await decodeBolt12Invoice({ lndk, request: invoiceBech32 }) if (!max_fee_mtokens && max_fee) { max_fee_mtokens = toPositiveNumber(satsToMsats(max_fee)) @@ -130,16 +110,3 @@ export async function payViaBolt12PaymentRequest ({ }) }) } - -export async function estimateBolt12RouteFee ({ lnd, destination, tokens, mtokens, request, timeout }) { - const { amount_msats, node_id } = request ? await decodeBolt12Invoice({ lnd, request }) : {} - - // extract mtokens and destination from invoice if they are not provided - if (!tokens && !mtokens) mtokens = toPositiveNumber(amount_msats) - destination ??= Buffer.from(node_id.key).toString('hex') - - if (!destination) throw new Error('no destination provided') - if (!tokens && !mtokens) throw new Error('no tokens amount provided') - - return await estimateRouteFee({ lnd, destination, tokens, mtokens, timeout }) -} diff --git a/api/lnd/index.js b/api/lnd/index.js index 03af5eaa6..8ec7439c4 100644 --- a/api/lnd/index.js +++ b/api/lnd/index.js @@ -1,7 +1,7 @@ import { cachedFetcher } from '@/lib/fetch' import { toPositiveNumber } from '@/lib/format' import { authenticatedLndGrpc } from '@/lib/lnd' -import { enableLNDK } from '@/api/lib/lndk' +import { authenticatedLndkGrpc } from '@/api/lib/lndk' import { getIdentity, getHeight, getWalletInfo, getNode, getPayment } from 'ln-service' import { datePivot } from '@/lib/time' import { LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants' @@ -11,7 +11,7 @@ const lnd = global.lnd || authenticatedLndGrpc({ macaroon: process.env.LND_MACAROON, socket: process.env.LND_SOCKET }).lnd -enableLNDK(lnd, { +export const lndk = authenticatedLndkGrpc({ cert: process.env.LNDK_CERT, macaroon: process.env.LNDK_MACAROON, socket: process.env.LNDK_SOCKET diff --git a/api/paidAction/README.md b/api/paidAction/README.md index a32588076..d478b20bf 100644 --- a/api/paidAction/README.md +++ b/api/paidAction/README.md @@ -193,6 +193,7 @@ All functions have the following signature: `function(args: Object, context: Obj - `tx`: the current transaction (for anything that needs to be done atomically with the payment) - `models`: the current prisma client (for anything that doesn't need to be done atomically with the payment) - `lnd`: the current lnd client +- `lndk`: the current lndk client ## Recording Cowboy Credits diff --git a/api/paidAction/index.js b/api/paidAction/index.js index c1e7d1ac4..c1246cd74 100644 --- a/api/paidAction/index.js +++ b/api/paidAction/index.js @@ -213,7 +213,7 @@ async function beginPessimisticAction (actionType, args, context) { async function performP2PAction (actionType, args, incomingContext) { // if the action has an invoiceable peer, we'll create a peer invoice // wrap it, and return the wrapped invoice - const { cost, sybilFeePercent, models, lnd, me } = incomingContext + const { cost, sybilFeePercent, models, lnd, lndk, me } = incomingContext if (!sybilFeePercent) { throw new Error('sybil fee percent is not set for an invoiceable peer action') } @@ -233,7 +233,7 @@ async function performP2PAction (actionType, args, incomingContext) { feePercent: sybilFeePercent, description, expiry: INVOICE_EXPIRE_SECS - }, { models, me, lnd }) + }, { models, me, lnd, lndk }) context = { ...incomingContext, @@ -257,7 +257,7 @@ async function performP2PAction (actionType, args, incomingContext) { // we don't need to use the module for perform-ing outside actions // because we can't track the state of outside invoices we aren't paid/paying async function performDirectAction (actionType, args, incomingContext) { - const { models, lnd, cost } = incomingContext + const { models, lnd, cost, lndk } = incomingContext const { comment, lud18Data, noteStr, description: actionDescription } = args const userId = await paidActions[actionType]?.getInvoiceablePeer?.(args, incomingContext) @@ -276,7 +276,7 @@ async function performDirectAction (actionType, args, incomingContext) { description, expiry: INVOICE_EXPIRE_SECS, supportBolt12: false // direct payment is not supported to bolt12 for compatibility reasons - }, { models, lnd }) + }, { models, lnd, lndk }) } catch (e) { console.error('failed to create outside invoice', e) throw new NonInvoiceablePeerError() diff --git a/api/payingAction/index.js b/api/payingAction/index.js index ebe6a79d1..c75e5cd13 100644 --- a/api/payingAction/index.js +++ b/api/payingAction/index.js @@ -6,7 +6,7 @@ import { payInvoice, parseInvoice } from '@/api/lib/bolt' // paying actions are completely distinct from paid actions // and there's only one paying action: send // ... still we want the api to at least be similar -export default async function performPayingAction ({ bolt11, maxFee, walletId }, { me, models, lnd }) { +export default async function performPayingAction ({ bolt11, maxFee, walletId }, { me, models, lnd, lndk }) { try { console.group('performPayingAction', `${bolt11.slice(0, 10)}...`, maxFee, walletId) @@ -14,7 +14,7 @@ export default async function performPayingAction ({ bolt11, maxFee, walletId }, throw new Error('You must be logged in to perform this action') } - const decoded = await parseInvoice({ request: bolt11, lnd }) + const decoded = await parseInvoice({ request: bolt11, lnd, lndk }) const cost = toPositiveBigInt(toPositiveBigInt(decoded.mtokens) + satsToMsats(maxFee)) console.log('cost', cost) @@ -42,6 +42,7 @@ export default async function performPayingAction ({ bolt11, maxFee, walletId }, payInvoice({ lnd, + lndk, request: withdrawal.bolt11, max_fee: msatsToSats(withdrawal.msatsFeePaying), pathfinding_timeout: LND_PATHFINDING_TIMEOUT_MS, diff --git a/api/resolvers/item.js b/api/resolvers/item.js index cac80f53a..702fff028 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -945,7 +945,7 @@ export default { return await performPaidAction('POLL_VOTE', { id }, { me, models, lnd }) }, - act: async (parent, { id, sats, act = 'TIP', hasSendWallet }, { me, models, lnd, headers }) => { + act: async (parent, { id, sats, act = 'TIP', hasSendWallet }, { me, models, lnd, lndk, headers }) => { assertApiKeyNotPermitted({ me }) await validateSchema(actSchema, { sats, act }) await assertGofacYourself({ models, headers }) @@ -979,11 +979,11 @@ export default { } if (act === 'TIP') { - return await performPaidAction('ZAP', { id, sats, hasSendWallet }, { me, models, lnd }) + return await performPaidAction('ZAP', { id, sats, hasSendWallet }, { me, models, lnd, lndk }) } else if (act === 'DONT_LIKE_THIS') { - return await performPaidAction('DOWN_ZAP', { id, sats }, { me, models, lnd }) + return await performPaidAction('DOWN_ZAP', { id, sats }, { me, models, lnd, lndk }) } else if (act === 'BOOST') { - return await performPaidAction('BOOST', { id, sats }, { me, models, lnd }) + return await performPaidAction('BOOST', { id, sats }, { me, models, lnd, lndk }) } else { throw new GqlInputError('unknown act') } diff --git a/api/resolvers/paidAction.js b/api/resolvers/paidAction.js index 2b993c1f0..f6785bb15 100644 --- a/api/resolvers/paidAction.js +++ b/api/resolvers/paidAction.js @@ -50,7 +50,7 @@ export default { } }, Mutation: { - retryPaidAction: async (parent, { invoiceId }, { models, me, lnd }) => { + retryPaidAction: async (parent, { invoiceId }, { models, me, lnd, lndk }) => { if (!me) { throw new Error('You must be logged in') } @@ -67,7 +67,7 @@ export default { throw new Error(`Invoice is not in failed state: ${invoice.actionState}`) } - const result = await retryPaidAction(invoice.actionType, { invoice }, { models, me, lnd }) + const result = await retryPaidAction(invoice.actionType, { invoice }, { models, me, lnd, lndk }) return { ...result, diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index 3df417a14..b937a8d3e 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -25,7 +25,7 @@ import { canReceive, getWalletByType } from '@/wallets/common' import performPaidAction from '../paidAction' import performPayingAction from '../payingAction' import { parseInvoice } from '@/api/lib/bolt' -import lnd from '@/api/lnd' +import lnd, { lndk } from '@/api/lnd' import { isBolt12Offer } from '@/api/lib/bolt/bolt12' import { fetchBolt12InvoiceFromOffer } from '@/api/lib/lndk' import { timeoutSignal, withTimeout } from '@/lib/time' @@ -35,7 +35,7 @@ function injectResolvers (resolvers) { for (const walletDef of walletDefs) { const resolverName = generateResolverName(walletDef.walletField) console.log(resolverName) - resolvers.Mutation[resolverName] = async (parent, { settings, validateLightning, vaultEntries, ...data }, { me, models, lnd }) => { + resolvers.Mutation[resolverName] = async (parent, { settings, validateLightning, vaultEntries, ...data }, { me, models, lnd, lndk }) => { console.log('resolving', resolverName, { settings, validateLightning, vaultEntries, ...data }) let existingVaultEntries @@ -75,6 +75,7 @@ function injectResolvers (resolvers) { walletDef.testCreateInvoice(data, { logger, lnd, + lndk, signal: timeoutSignal(WALLET_CREATE_INVOICE_TIMEOUT_MS) }), WALLET_CREATE_INVOICE_TIMEOUT_MS) @@ -477,13 +478,13 @@ const resolvers = { __resolveType: invoiceOrDirect => invoiceOrDirect.__resolveType }, Mutation: { - createInvoice: async (parent, { amount }, { me, models, lnd, headers }) => { + createInvoice: async (parent, { amount }, { me, models, lnd, lndk, headers }) => { await validateSchema(amountSchema, { amount }) await assertGofacYourself({ models, headers }) const { invoice, paymentMethod } = await performPaidAction('RECEIVE', { msats: satsToMsats(amount) - }, { models, lnd, me }) + }, { models, lnd, lndk, me }) return { ...invoice, @@ -494,7 +495,7 @@ const resolvers = { createWithdrawl: createWithdrawal, sendToLnAddr, sendToBolt12Offer, - cancelInvoice: async (parent, { hash, hmac, userCancel }, { me, models, lnd, boss }) => { + cancelInvoice: async (parent, { hash, hmac, userCancel }, { me, models, lnd, lndk, boss }) => { // stackers can cancel their own invoices without hmac if (me && !hmac) { const inv = await models.invoice.findUnique({ where: { hash } }) @@ -503,7 +504,7 @@ const resolvers = { } else { verifyHmac(hash, hmac) } - await finalizeHodlInvoice({ data: { hash }, lnd, models, boss }) + await finalizeHodlInvoice({ data: { hash }, lnd, lndk, models, boss }) return await models.invoice.update({ where: { hash }, data: { userCancel: !!userCancel } }) }, dropBolt11: async (parent, { hash }, { me, models, lnd }) => { @@ -753,7 +754,7 @@ export const walletLogger = ({ wallet, models }) => { try { if (context?.bolt11) { // automatically populate context from invoice to avoid duplicating this code - const decoded = await parseInvoice({ request: context.bolt11, lnd }) + const decoded = await parseInvoice({ request: context.bolt11, lnd, lndk }) context = { ...context, amount: formatMsats(decoded.mtokens), @@ -921,7 +922,7 @@ async function upsertWallet ( return upsertedWallet } -export async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd, headers, wallet, logger }) { +export async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd, lndk, headers, wallet, logger }) { assertApiKeyNotPermitted({ me }) await validateSchema(withdrawlSchema, { invoice, maxFee }) await assertGofacYourself({ models, headers }) @@ -932,7 +933,7 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model // decode invoice to get amount let decoded, sockets try { - decoded = await parseInvoice({ request: invoice, lnd }) + decoded = await parseInvoice({ request: invoice, lnd, lndk }) } catch (error) { console.log(error) throw new GqlInputError('could not decode invoice') @@ -971,11 +972,11 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model throw new GqlInputError('SN cannot pay an invoice that SN is proxying') } - return await performPayingAction({ bolt11: invoice, maxFee, walletId: wallet?.id }, { me, models, lnd }) + return await performPayingAction({ bolt11: invoice, maxFee, walletId: wallet?.id }, { me, models, lnd, lndk }) } export async function sendToLnAddr (parent, { addr, amount, maxFee, comment, ...payer }, - { me, models, lnd, headers }) { + { me, models, lnd, lndk, headers }) { if (!me) { throw new GqlAuthenticationError() } @@ -985,14 +986,15 @@ export async function sendToLnAddr (parent, { addr, amount, maxFee, comment, ... { me, models, - lnd + lnd, + lndk }) // take pr and createWithdrawl - return await createWithdrawal(parent, { invoice: res.pr, maxFee }, { me, models, lnd, headers }) + return await createWithdrawal(parent, { invoice: res.pr, maxFee }, { me, models, lnd, lndk, headers }) } -export async function sendToBolt12Offer (parent, { offer, amountSats, maxFee, comment }, { me, models, lnd, headers }) { +export async function sendToBolt12Offer (parent, { offer, amountSats, maxFee, comment }, { me, models, lnd, lndk, headers }) { if (!me) { throw new GqlAuthenticationError() } @@ -1000,14 +1002,14 @@ export async function sendToBolt12Offer (parent, { offer, amountSats, maxFee, co if (!isBolt12Offer(offer)) { throw new GqlInputError('not a bolt12 offer') } - const invoice = await fetchBolt12InvoiceFromOffer({ lnd, offer, msats: satsToMsats(amountSats), description: comment }) - return await createWithdrawal(parent, { invoice, maxFee }, { me, models, lnd, headers }) + const invoice = await fetchBolt12InvoiceFromOffer({ lnd, lndk, offer, msats: satsToMsats(amountSats), description: comment }) + return await createWithdrawal(parent, { invoice, maxFee }, { me, models, lnd, lndk, headers }) } export async function fetchLnAddrInvoice ( { addr, amount, maxFee, comment, ...payer }, { - me, models, lnd, autoWithdraw = false + me, models, lnd, lndk, autoWithdraw = false }) { const options = await lnAddrOptions(addr) await validateSchema(lnAddrSchema, { addr, amount, maxFee, comment, ...payer }, options) @@ -1044,7 +1046,7 @@ export async function fetchLnAddrInvoice ( // decode invoice try { - const decoded = await parseInvoice({ request: res.pr, lnd }) + const decoded = await parseInvoice({ request: res.pr, lnd, lndk }) const ourPubkey = await getOurPubkey({ lnd }) if (autoWithdraw && decoded.destination === ourPubkey && process.env.NODE_ENV === 'production') { // unset lnaddr so we don't trigger another withdrawal with same destination diff --git a/api/ssrApollo.js b/api/ssrApollo.js index 7af73317e..e4cf21f9b 100644 --- a/api/ssrApollo.js +++ b/api/ssrApollo.js @@ -5,7 +5,7 @@ import resolvers from './resolvers' import typeDefs from './typeDefs' import models from './models' import { print } from 'graphql' -import lnd from './lnd' +import lnd, { lndk } from './lnd' import search from './search' import { ME } from '@/fragments/users' import { PRICE } from '@/fragments/price' @@ -31,6 +31,7 @@ export default async function getSSRApolloClient ({ req, res, me = null }) { ? session.user : me, lnd, + lndk, search } }), diff --git a/pages/api/graphql.js b/pages/api/graphql.js index 9d6626e93..9e8b4375a 100644 --- a/pages/api/graphql.js +++ b/pages/api/graphql.js @@ -2,7 +2,7 @@ import { ApolloServer } from '@apollo/server' import { startServerAndCreateNextHandler } from '@as-integrations/next' import resolvers from '@/api/resolvers' import models from '@/api/models' -import lnd from '@/api/lnd' +import lnd, { lndk } from '@/api/lnd' import typeDefs from '@/api/typeDefs' import { getServerSession } from 'next-auth/next' import { getAuthOptions } from './auth/[...nextauth]' @@ -74,6 +74,7 @@ export default startServerAndCreateNextHandler(apolloServer, { models, headers: req.headers, lnd, + lndk, me: session ? session.user : null, diff --git a/pages/api/lnurlp/[username]/pay.js b/pages/api/lnurlp/[username]/pay.js index fc7e21220..37e0cee3a 100644 --- a/pages/api/lnurlp/[username]/pay.js +++ b/pages/api/lnurlp/[username]/pay.js @@ -1,5 +1,5 @@ import models from '@/api/models' -import lnd from '@/api/lnd' +import lnd, { lndk } from '@/api/lnd' import { lnurlPayDescriptionHashForUser, lnurlPayMetadataString, lnurlPayDescriptionHash } from '@/lib/lnurl' import { schnorr } from '@noble/curves/secp256k1' import { createHash } from 'crypto' @@ -85,7 +85,7 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa comment: comment || '', lud18Data: parsedPayerData, noteStr - }, { models, lnd, me: user }) + }, { models, lnd, lndk, me: user }) if (!invoice?.bolt11) throw new Error('could not generate invoice') diff --git a/pages/api/lnwith.js b/pages/api/lnwith.js index ed22d499f..57ed88582 100644 --- a/pages/api/lnwith.js +++ b/pages/api/lnwith.js @@ -2,7 +2,7 @@ // send back import models from '@/api/models' import { datePivot } from '@/lib/time' -import lnd from '@/api/lnd' +import lnd, { lndk } from '@/api/lnd' import { createWithdrawal } from '@/api/resolvers/wallet' export default async ({ query, headers }, res) => { @@ -68,7 +68,7 @@ async function doWithdrawal (query, res, headers) { try { const withdrawal = await createWithdrawal(null, { invoice: query.pr, maxFee: me.withdrawMaxFeeDefault }, - { me, models, lnd, headers }) + { me, models, lnd, lndk, headers }) // store withdrawal id lnWith so client can show it await models.lnWith.update({ where: { k1: query.k1 }, data: { withdrawalId: Number(withdrawal.id) } }) diff --git a/wallets/bolt12/server.js b/wallets/bolt12/server.js index a39fdc484..de012023a 100644 --- a/wallets/bolt12/server.js +++ b/wallets/bolt12/server.js @@ -3,14 +3,14 @@ import { parseInvoice } from '@/api/lib/bolt' import { toPositiveNumber } from '@/lib/format' export * from '@/wallets/bolt12' -export async function testCreateInvoice ({ offer }, { lnd }) { - const invoice = await fetchBolt12InvoiceFromOffer({ lnd, offer, msats: 1000, description: 'test' }) - const parsedInvoice = await parseInvoice({ lnd, request: invoice }) +export async function testCreateInvoice ({ offer }, { lnd, lndk }) { + const invoice = await fetchBolt12InvoiceFromOffer({ lnd, lndk, offer, msats: 1000, description: 'test' }) + const parsedInvoice = await parseInvoice({ lnd, lndk, request: invoice }) if (toPositiveNumber(parsedInvoice.mtokens) !== 1000) throw new Error('invalid invoice response') return offer } -export async function createInvoice ({ msats, description, expiry }, { offer }, { lnd }) { - const invoice = await fetchBolt12InvoiceFromOffer({ lnd, offer, msats, description }) +export async function createInvoice ({ msats, description, expiry }, { offer }, { lnd, lndk }) { + const invoice = await fetchBolt12InvoiceFromOffer({ lnd, lndk, offer, msats, description }) return invoice } diff --git a/wallets/server.js b/wallets/server.js index d8459b34d..b3c166cee 100644 --- a/wallets/server.js +++ b/wallets/server.js @@ -26,7 +26,7 @@ export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln, bolt const MAX_PENDING_INVOICES_PER_WALLET = 25 -export async function createInvoice (userId, { msats, description, descriptionHash, expiry = 360, supportBolt12 = true }, { predecessorId, models, lnd }) { +export async function createInvoice (userId, { msats, description, descriptionHash, expiry = 360, supportBolt12 = true }, { predecessorId, models, lnd, lndk }) { // get the wallets in order of priority const wallets = await getInvoiceableWallets(userId, { predecessorId, models }) @@ -48,7 +48,7 @@ export async function createInvoice (userId, { msats, description, descriptionHa invoice = await walletCreateInvoice( { wallet, def }, { msats, description, descriptionHash, expiry }, - { logger, models, lnd, supportBolt12 }) + { logger, models, lnd, lndk, supportBolt12 }) } catch (err) { throw new Error('failed to create invoice: ' + err.message) } @@ -61,7 +61,7 @@ export async function createInvoice (userId, { msats, description, descriptionHa throw new Error('the wallet returned a bolt12 offer, but an invoice was expected') } - const parsedInvoice = await parseInvoice({ lnd, request: invoice }) + const parsedInvoice = await parseInvoice({ lnd, lndk, request: invoice }) logger.info(`created invoice for ${formatSats(msatsToSats(parsedInvoice.mtokens))}`, { bolt11: invoice }) @@ -91,7 +91,7 @@ export async function createInvoice (userId, { msats, description, descriptionHa export async function createWrappedInvoice (userId, { msats, feePercent, description, descriptionHash, expiry = 360 }, - { predecessorId, models, me, lnd }) { + { predecessorId, models, me, lnd, lndk }) { let logger, bolt11 try { const { invoice, wallet } = await createInvoice(userId, { @@ -100,12 +100,12 @@ export async function createWrappedInvoice (userId, description, descriptionHash, expiry - }, { predecessorId, models, lnd }) + }, { predecessorId, models, lnd, lndk }) logger = walletLogger({ wallet, models }) bolt11 = invoice const { invoice: wrappedInvoice, maxFee } = - await wrapInvoice({ bolt11, feePercent }, { msats, description, descriptionHash }, { me, lnd }) + await wrapInvoice({ bolt11, feePercent }, { msats, description, descriptionHash }, { me, lnd, lndk }) return { invoice, @@ -175,7 +175,7 @@ async function walletCreateInvoice ({ wallet, def }, { description, descriptionHash, expiry = 360 -}, { logger, models, lnd }) { +}, { logger, models, lnd, lndk }) { // check for pending withdrawals const pendingWithdrawals = await models.withdrawl.count({ where: { @@ -213,6 +213,7 @@ async function walletCreateInvoice ({ wallet, def }, { { logger, lnd, + lndk, signal: timeoutSignal(WALLET_CREATE_INVOICE_TIMEOUT_MS) } ), WALLET_CREATE_INVOICE_TIMEOUT_MS) diff --git a/wallets/wrap.js b/wallets/wrap.js index b7d230938..eb9d0dc91 100644 --- a/wallets/wrap.js +++ b/wallets/wrap.js @@ -29,7 +29,7 @@ const MAX_FEE_ESTIMATE_PERCENT = 3n // the maximum fee relative to outgoing we'l maxFee: number } */ -export default async function wrapInvoice ({ bolt11, feePercent }, { msats, description, descriptionHash }, { me, lnd }) { +export default async function wrapInvoice ({ bolt11, feePercent }, { msats, description, descriptionHash }, { me, lnd, lndk }) { try { console.group('wrapInvoice', description) @@ -38,7 +38,7 @@ export default async function wrapInvoice ({ bolt11, feePercent }, { msats, desc let outgoingMsat // decode the invoice - const inv = await parseInvoice({ request: bolt11, lnd }) + const inv = await parseInvoice({ request: bolt11, lnd, lndk }) if (!inv) { throw new Error('Unable to decode invoice') } @@ -150,6 +150,7 @@ export default async function wrapInvoice ({ bolt11, feePercent }, { msats, desc const { routingFeeMsat, timeLockDelay } = await estimateFees({ lnd, + lndk, destination: inv.destination, mtokens: inv.mtokens, request: bolt11, diff --git a/worker/autowithdraw.js b/worker/autowithdraw.js index deb0954d7..29bd246e9 100644 --- a/worker/autowithdraw.js +++ b/worker/autowithdraw.js @@ -2,7 +2,7 @@ import { msatsSatsFloor, msatsToSats, satsToMsats } from '@/lib/format' import { createWithdrawal } from '@/api/resolvers/wallet' import { createInvoice } from '@/wallets/server' -export async function autoWithdraw ({ data: { id }, models, lnd }) { +export async function autoWithdraw ({ data: { id }, models, lnd, lndk }) { const user = await models.user.findUnique({ where: { id } }) if ( user.autoWithdrawThreshold === null || @@ -42,12 +42,12 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) { if (pendingOrFailed.exists) return - const { invoice, wallet, logger } = await createInvoice(id, { msats, description: 'SN: autowithdrawal', expiry: 360 }, { models, lnd }) + const { invoice, wallet, logger } = await createInvoice(id, { msats, description: 'SN: autowithdrawal', expiry: 360 }, { models, lnd, lndk }) try { return await createWithdrawal(null, { invoice, maxFee: msatsToSats(maxFeeMsats) }, - { me: { id }, models, lnd, wallet, logger }) + { me: { id }, models, lnd, lndk, wallet, logger }) } catch (err) { logger.error(`incoming payment failed: ${err}`, { bolt11: invoice }) throw err diff --git a/worker/index.js b/worker/index.js index f80643613..0704e01cc 100644 --- a/worker/index.js +++ b/worker/index.js @@ -17,7 +17,7 @@ import { computeStreaks, checkStreak } from './streak' import { nip57 } from './nostr' import fetch from 'cross-fetch' import { authenticatedLndGrpc } from '@/lib/lnd' -import { enableLNDK } from '@/api/lib/lndk' +import { authenticatedLndkGrpc } from '@/api/lib/lndk' import { views, rankViews } from './views' import { imgproxy } from './imgproxy' import { deleteItem } from './ephemeralItems' @@ -74,13 +74,14 @@ async function work () { macaroon: process.env.LND_MACAROON, socket: process.env.LND_SOCKET }) - enableLNDK(lnd, { + + const lndk = authenticatedLndkGrpc({ cert: process.env.LNDK_CERT, macaroon: process.env.LNDK_MACAROON, socket: process.env.LNDK_SOCKET }) - const args = { boss, models, apollo, lnd } + const args = { boss, models, apollo, lnd, lndk } boss.on('error', error => console.error(error)) diff --git a/worker/paidAction.js b/worker/paidAction.js index c26bf2f8f..47de8921c 100644 --- a/worker/paidAction.js +++ b/worker/paidAction.js @@ -194,7 +194,7 @@ export async function paidActionPaid ({ data: { invoiceId, ...args }, models, ln } // this performs forward creating the outgoing payment -export async function paidActionForwarding ({ data: { invoiceId, ...args }, models, lnd, boss }) { +export async function paidActionForwarding ({ data: { invoiceId, ...args }, models, lnd, lndk, boss }) { const transitionedInvoice = await transitionInvoice('paidActionForwarding', { invoiceId, fromState: 'PENDING_HELD', @@ -211,7 +211,7 @@ export async function paidActionForwarding ({ data: { invoiceId, ...args }, mode const { expiryHeight, acceptHeight } = hodlInvoiceCltvDetails(lndInvoice) const { bolt11, maxFeeMsats } = invoiceForward - const invoice = await parseInvoice({ request: bolt11, lnd }) + const invoice = await parseInvoice({ request: bolt11, lnd, lndk }) // maxTimeoutDelta is the number of blocks left for the outgoing payment to settle const maxTimeoutDelta = toPositiveNumber(expiryHeight) - toPositiveNumber(acceptHeight) - MIN_SETTLEMENT_CLTV_DELTA if (maxTimeoutDelta - toPositiveNumber(invoice.cltv_delta) < 0) { @@ -267,6 +267,7 @@ export async function paidActionForwarding ({ data: { invoiceId, ...args }, mode payInvoice({ lnd, + lndk, request: bolt11, max_fee_mtokens: String(maxFeeMsats), pathfinding_timeout: LND_PATHFINDING_TIMEOUT_MS, @@ -426,7 +427,7 @@ export async function paidActionHeld ({ data: { invoiceId, ...args }, models, ln }, { models, lnd, boss }) } -export async function paidActionCanceling ({ data: { invoiceId, ...args }, models, lnd, boss }) { +export async function paidActionCanceling ({ data: { invoiceId, ...args }, models, lnd, lndk, boss }) { const transitionedInvoice = await transitionInvoice('paidActionCanceling', { invoiceId, fromState: ['HELD', 'PENDING', 'PENDING_HELD', 'FAILED_FORWARD'], @@ -445,7 +446,7 @@ export async function paidActionCanceling ({ data: { invoiceId, ...args }, model if (transitionedInvoice.invoiceForward) { const { wallet, bolt11 } = transitionedInvoice.invoiceForward const logger = walletLogger({ wallet, models }) - const decoded = await parseInvoice({ request: bolt11, lnd }) + const decoded = await parseInvoice({ request: bolt11, lnd, lndk }) logger.info(`invoice for ${formatSats(msatsToSats(decoded.mtokens))} canceled by payer`, { bolt11 }) } } diff --git a/worker/wallet.js b/worker/wallet.js index ac09c7ac4..035bc54ef 100644 --- a/worker/wallet.js +++ b/worker/wallet.js @@ -115,7 +115,7 @@ function subscribeToHodlInvoice (args) { // if we already have the invoice from a subscription event or previous call, // we can skip a getInvoice call -export async function checkInvoice ({ data: { hash, invoice }, boss, models, lnd }) { +export async function checkInvoice ({ data: { hash, invoice }, boss, models, lnd, lndk }) { const inv = invoice ?? await getInvoice({ id: hash, lnd }) // invoice could be created by LND but wasn't inserted into the database yet @@ -148,7 +148,7 @@ export async function checkInvoice ({ data: { hash, invoice }, boss, models, lnd // transitions when held are dependent on the withdrawl status return await checkWithdrawal({ data: { hash: dbInv.invoiceForward.withdrawl.hash, invoice: inv }, models, lnd, boss }) } - return await paidActionForwarding({ data: { invoiceId: dbInv.id, invoice: inv }, models, lnd, boss }) + return await paidActionForwarding({ data: { invoiceId: dbInv.id, invoice: inv }, models, lnd, lndk, boss }) } return await paidActionHeld({ data: { invoiceId: dbInv.id, invoice: inv }, models, lnd, boss }) } @@ -241,7 +241,7 @@ export async function checkWithdrawal ({ data: { hash, withdrawal, invoice }, bo // The callback subscriptions above will NOT get called for JIT invoices that are already paid. // So we manually cancel the HODL invoice here if it wasn't settled by user action -export async function finalizeHodlInvoice ({ data: { hash }, models, lnd, boss, ...args }) { +export async function finalizeHodlInvoice ({ data: { hash }, models, lnd, lndk, boss, ...args }) { const inv = await getInvoice({ id: hash, lnd }) if (inv.is_confirmed) { return @@ -256,7 +256,7 @@ export async function finalizeHodlInvoice ({ data: { hash }, models, lnd, boss, await paidActionCanceling({ data: { invoiceId: dbInv.id, invoice: inv }, models, lnd, boss }) // sync LND invoice status with invoice status in database - await checkInvoice({ data: { hash }, models, lnd, boss }) + await checkInvoice({ data: { hash }, models, lnd, lndk, boss }) } export async function checkPendingDeposits (args) { diff --git a/worker/weeklyPosts.js b/worker/weeklyPosts.js index 275b23bab..02c55f677 100644 --- a/worker/weeklyPosts.js +++ b/worker/weeklyPosts.js @@ -22,7 +22,7 @@ export async function weeklyPost (args) { } } -export async function payWeeklyPostBounty ({ data: { id }, models, apollo, lnd }) { +export async function payWeeklyPostBounty ({ data: { id }, models, apollo, lnd, lndk }) { const itemQ = await apollo.query({ query: gql` query item($id: ID!) { @@ -56,6 +56,7 @@ export async function payWeeklyPostBounty ({ data: { id }, models, apollo, lnd } models, me: { id: USER_ID.sn }, lnd, + lndk, forcePaymentMethod: PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT }) } From 5a41b01a23714a24f6d3ace8689dc93e9463cd71 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 7 Jan 2025 15:01:42 +0100 Subject: [PATCH 40/42] fix merge issue --- pages/withdraw.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/withdraw.js b/pages/withdraw.js index 4c555900e..dbb7e241a 100644 --- a/pages/withdraw.js +++ b/pages/withdraw.js @@ -66,7 +66,7 @@ function WithdrawForm () { - + bolt12 offer @@ -84,7 +84,7 @@ export function SelectedWithdrawalForm () { return case 'lnaddr': return - case 'bolt12-withdraw': + case 'bolt12': return default: return From b3365bd9d4e804c31a7561c042d8076ca46b208f Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 7 Jan 2025 15:02:00 +0100 Subject: [PATCH 41/42] fix refactoring issue --- api/lib/bolt/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/lib/bolt/index.js b/api/lib/bolt/index.js index d0777273a..425f9a238 100644 --- a/api/lib/bolt/index.js +++ b/api/lib/bolt/index.js @@ -1,7 +1,6 @@ /* eslint-disable camelcase */ -import { payBolt12, parseBolt12, isBolt12Invoice, isBolt12Offer } from '@/api/lib/bolt/bolt12' +import { payBolt12, parseBolt12, isBolt12Invoice, isBolt12Offer, estimateBolt12RouteFee } from '@/api/lib/bolt/bolt12' import { payBolt11, parseBolt11, isBolt11, estimateBolt11RouteFee } from '@/api/lib/bolt/bolt11' -import { estimateBolt12RouteFee } from '@/api/lib/lndk' export async function payInvoice ({ lnd, lndk, request: invoice, max_fee, max_fee_mtokens, ...args }) { if (isBolt12Invoice(invoice)) { From 10a10415394a1bb976d6e7b959f25588625333c0 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 7 Jan 2025 15:02:07 +0100 Subject: [PATCH 42/42] add safety check --- api/lib/lndk.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/lib/lndk.js b/api/lib/lndk.js index 527e88403..f769d2223 100644 --- a/api/lib/lndk.js +++ b/api/lib/lndk.js @@ -94,6 +94,9 @@ export async function payViaBolt12PaymentRequest ({ max_fee_mtokens = toPositiveNumber(satsToMsats(max_fee)) } + // safety check + if (!max_fee_mtokens || isNaN(max_fee_mtokens)) throw new Error('invalid max_fee') + return new Promise((resolve, reject) => { lndk.PayInvoice({ invoice: invoice_hex_str,