From e803efe17585c8278145fc940f394c04f5a0087d Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 17 Dec 2024 01:06:05 +0100 Subject: [PATCH 01/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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;