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/lib/bolt/bolt11.js b/api/lib/bolt/bolt11.js new file mode 100644 index 000000000..a8a7cc0da --- /dev/null +++ b/api/lib/bolt/bolt11.js @@ -0,0 +1,28 @@ +/* eslint-disable camelcase */ +import { payViaPaymentRequest, parsePaymentRequest } from 'ln-service' +import { isBolt11 } from '@/lib/bolt/bolt11-tags' +import { estimateRouteFee } from '@/api/lnd' +export { isBolt11 } + +export async function parseBolt11 ({ request }) { + if (!isBolt11(request)) throw new Error('not a bolt11 invoice') + return parsePaymentRequest({ request }) +} + +export async function payBolt11 ({ lnd, request, max_fee, max_fee_mtokens, ...args }) { + if (!lnd) throw new Error('lnd required') // check if forgot to pass lnd + if (!isBolt11(request)) throw new Error('not a bolt11 invoice') + return payViaPaymentRequest({ + lnd, + request, + max_fee, + max_fee_mtokens, + ...args + }) +} + +export async function estimateBolt11RouteFee ({ lnd, destination, tokens, mtokens, request, timeout }) { + if (!lnd) throw new Error('lnd required') // check if forgot to pass lnd + if (request && !isBolt11(request)) throw new Error('not a bolt11 request') + return await estimateRouteFee({ lnd, destination, tokens, mtokens, request, timeout }) +} diff --git a/api/lib/bolt/bolt12.js b/api/lib/bolt/bolt12.js new file mode 100644 index 000000000..a8771f2bd --- /dev/null +++ b/api/lib/bolt/bolt12.js @@ -0,0 +1,132 @@ +/* eslint-disable camelcase */ + +import { payViaBolt12PaymentRequest, decodeBolt12Invoice } from '@/api/lib/lndk' +import { isBolt12Invoice, isBolt12Offer, isBolt12 } from '@/lib/bolt/bolt12-info' +import { toPositiveNumber } from '@/lib/format' +import { estimateRouteFee } from '@/api/lnd' +export { isBolt12Invoice, isBolt12Offer, isBolt12 } + +export async function payBolt12 ({ lndk, request: invoice, max_fee, max_fee_mtokens }) { + if (!lndk) throw new Error('lndk required') // check if forgot to pass lndk + if (!isBolt12Invoice(invoice)) throw new Error('not a bolt12 invoice') + return await payViaBolt12PaymentRequest({ lndk, request: invoice, max_fee, max_fee_mtokens }) +} + +export async function parseBolt12 ({ lndk, request: invoice }) { + if (!lndk) throw new Error('lndk required') // check if forgot to pass lndk + if (!isBolt12Invoice(invoice)) throw new Error('not a bolt12 request') + const decodedInvoice = await decodeBolt12Invoice({ lndk, request: invoice }) + return convertBolt12RequestToLNRequest(decodedInvoice) +} + +export async function estimateBolt12RouteFee ({ lnd, lndk, destination, tokens, mtokens, request, timeout }) { + if (!lndk) throw new Error('lndk required') // check if forgot to pass lndk + if (!lnd) throw new Error('lnd required') // check if forgot to pass lnd + if (request && !isBolt12Invoice(request)) throw new Error('not a bolt12 request') + + const { amount_msats, node_id } = request ? await decodeBolt12Invoice({ lndk, request }) : {} + + // extract mtokens and destination from invoice if they are not provided + if (!tokens && !mtokens) mtokens = toPositiveNumber(amount_msats) + destination ??= Buffer.from(node_id.key).toString('hex') + + if (!destination) throw new Error('no destination provided') + if (!tokens && !mtokens) throw new Error('no tokens amount provided') + + return await estimateRouteFee({ lnd, destination, tokens, mtokens, timeout }) +} + +const featureBitTypes = { + 0: 'DATALOSS_PROTECT_REQ', + 1: 'DATALOSS_PROTECT_OPT', + 3: 'INITIAL_ROUTING_SYNC', + 4: 'UPFRONT_SHUTDOWN_SCRIPT_REQ', + 5: 'UPFRONT_SHUTDOWN_SCRIPT_OPT', + 6: 'GOSSIP_QUERIES_REQ', + 7: 'GOSSIP_QUERIES_OPT', + 8: 'TLV_ONION_REQ', + 9: 'TLV_ONION_OPT', + 10: 'EXT_GOSSIP_QUERIES_REQ', + 11: 'EXT_GOSSIP_QUERIES_OPT', + 12: 'STATIC_REMOTE_KEY_REQ', + 13: 'STATIC_REMOTE_KEY_OPT', + 14: 'PAYMENT_ADDR_REQ', + 15: 'PAYMENT_ADDR_OPT', + 16: 'MPP_REQ', + 17: 'MPP_OPT', + 18: 'WUMBO_CHANNELS_REQ', + 19: 'WUMBO_CHANNELS_OPT', + 20: 'ANCHORS_REQ', + 21: 'ANCHORS_OPT', + 22: 'ANCHORS_ZERO_FEE_HTLC_REQ', + 23: 'ANCHORS_ZERO_FEE_HTLC_OPT', + 24: 'ROUTE_BLINDING_REQUIRED', + 25: 'ROUTE_BLINDING_OPTIONAL', + 30: 'AMP_REQ', + 31: 'AMP_OPT' +} + +const chainsMap = { + '06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f': 'regtest', + '43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000': 'testnet', + '6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000': 'mainnet' +} + +async function convertBolt12RequestToLNRequest (decodedInvoice) { + const { + amount_msats, + description, + node_id, + chain, + payment_hash, + created_at, + relative_expiry, + features, + payer_note, + payment_paths, + invoice_hex_str + } = decodedInvoice + + // convert from lndk response to ln-service parsePaymentRequest output layout + let minCltvDelta + for (const path of payment_paths) { + const info = path.blinded_pay_info + if (minCltvDelta === undefined || info.cltv_expiry_delta < minCltvDelta) { + minCltvDelta = info.cltv_expiry_delta + } + } + + const out = { + created_at: new Date(created_at * 1000).toISOString(), + // [chain_addresses] + cltv_delta: minCltvDelta, + description: payer_note || description, + // [description_hash] + destination: Buffer.from(node_id.key).toString('hex'), + expires_at: new Date((created_at + relative_expiry) * 1000).toISOString(), + features: features.map(bit => ({ + bit, + is_required: (bit % 2) === 0, + type: featureBitTypes[bit] + })), + id: Buffer.from(payment_hash.hash).toString('hex'), + is_expired: new Date().getTime() / 1000 > created_at + relative_expiry, + // [metadata] + mtokens: String(amount_msats), + network: chainsMap[chain], + payment: invoice_hex_str, + routes: payment_paths.map((path) => { + const info = path.blinded_pay_info + const { introduction_node } = path.blinded_path + return { + base_fee_mtokens: String(info.fee_base_msat), + cltv_delta: info.cltv_expiry_delta, + public_key: Buffer.from(introduction_node.node_id.key).toString('hex') + } + }), + safe_tokens: Math.round(toPositiveNumber(BigInt(amount_msats)) / 1000), + tokens: Math.floor(toPositiveNumber(BigInt(amount_msats)) / 1000), + bolt12: decodedInvoice + } + return out +} diff --git a/api/lib/bolt/index.js b/api/lib/bolt/index.js new file mode 100644 index 000000000..425f9a238 --- /dev/null +++ b/api/lib/bolt/index.js @@ -0,0 +1,39 @@ +/* eslint-disable camelcase */ +import { payBolt12, parseBolt12, isBolt12Invoice, isBolt12Offer, estimateBolt12RouteFee } from '@/api/lib/bolt/bolt12' +import { payBolt11, parseBolt11, isBolt11, estimateBolt11RouteFee } from '@/api/lib/bolt/bolt11' + +export async function payInvoice ({ lnd, lndk, request: invoice, max_fee, max_fee_mtokens, ...args }) { + if (isBolt12Invoice(invoice)) { + return await payBolt12({ lndk, request: invoice, max_fee, max_fee_mtokens, ...args }) + } else if (isBolt11(invoice)) { + return await payBolt11({ lnd, request: invoice, max_fee, max_fee_mtokens, ...args }) + } else if (isBolt12Offer(invoice)) { + 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 ({ lndk, request }) { + if (isBolt12Invoice(request)) { + return await parseBolt12({ lndk, request }) + } else if (isBolt11(request)) { + return await parseBolt11({ request }) + } else if (isBolt12Offer(request)) { + throw new Error('bolt12 offer instead of invoice, please fetch a bolt12 invoice from the offer first') + } else { + throw new Error('unknown invoice type') + } +} + +export async function estimateFees ({ lnd, lndk, destination, tokens, mtokens, request, timeout }) { + if (isBolt12Invoice(request)) { + return await estimateBolt12RouteFee({ lnd, lndk, destination, tokens, mtokens, request, timeout }) + } else if (isBolt11(request)) { + return await estimateBolt11RouteFee({ lnd, destination, tokens, request, mtokens, timeout }) + } else if (isBolt12Offer(request)) { + throw new Error('bolt12 offer instead of invoice, please fetch a bolt12 invoice from the offer first') + } else { + throw new Error('unknown invoice type') + } +} diff --git a/api/lib/lndk.js b/api/lib/lndk.js new file mode 100644 index 000000000..f769d2223 --- /dev/null +++ b/api/lib/lndk.js @@ -0,0 +1,115 @@ +import { satsToMsats, toPositiveNumber } from '@/lib/format' +import { loadPackageDefinition } from '@grpc/grpc-js' +import LNDK_RPC_PROTO from '@/api/lib/lndkrpc-proto' +import protobuf from 'protobufjs' +import grpcCredentials from 'lightning/lnd_grpc/grpc_credentials' +import { grpcSslCipherSuites } from 'lightning/grpc/index' +import { fromJSON } from '@grpc/proto-loader' +import * as bech32b12 from '@/lib/bech32b12' + +/* eslint-disable camelcase */ +const { GRPC_SSL_CIPHER_SUITES } = process.env + +export function authenticatedLndkGrpc ({ cert, macaroon, socket: lndkSocket }, withProxy) { + // workaround to load from string + const protoArgs = { keepCase: true, longs: Number, defaults: true, oneofs: true } + const proto = protobuf.parse(LNDK_RPC_PROTO, protoArgs).root + 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 + } + + if (!!cert && GRPC_SSL_CIPHER_SUITES !== grpcSslCipherSuites) { + process.env.GRPC_SSL_CIPHER_SUITES = grpcSslCipherSuites + } + + const client = new OffersService(lndkSocket, credentials, params) + return client +} + +export async function decodeBolt12Invoice ({ + lndk, + request +}) { + // decode bech32 bolt12 invoice to hex string + if (!request.startsWith('lni1')) throw new Error('not a valid bech32 encoded bolt12 invoice') + const invoice_hex_str = bech32b12.decode(request.slice(4)).toString('hex') + + const decodedRequest = await new Promise((resolve, reject) => { + lndk.DecodeInvoice({ + invoice: invoice_hex_str + }, (error, response) => { + if (error) return reject(error) + resolve(response) + }) + }) + + return { ...decodedRequest, invoice_hex_str } +} + +export async function fetchBolt12InvoiceFromOffer ({ lndk, offer, msats, description, timeout = 10_000 }) { + 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) => { + try { + if (error) return reject(error) + // encode hex string invoice to bech32 + const bech32invoice = 'lni1' + bech32b12.encode(Buffer.from(response.invoice_hex_str, 'hex')) + + // sanity check + const { amount_msats } = await decodeBolt12Invoice({ lndk, request: bech32invoice }) + if (toPositiveNumber(amount_msats) !== toPositiveNumber(msats)) { + return reject(new Error('invalid invoice response')) + } + + resolve(bech32invoice) + } catch (e) { + reject(e) + } + }) + }) +} + +export async function payViaBolt12PaymentRequest ({ + lndk, + request: invoiceBech32, + max_fee, + max_fee_mtokens +}) { + const { amount_msats, invoice_hex_str } = await decodeBolt12Invoice({ lndk, request: invoiceBech32 }) + + if (!max_fee_mtokens && max_fee) { + max_fee_mtokens = toPositiveNumber(satsToMsats(max_fee)) + } + + // safety check + if (!max_fee_mtokens || isNaN(max_fee_mtokens)) throw new Error('invalid max_fee') + + return new Promise((resolve, reject) => { + lndk.PayInvoice({ + invoice: invoice_hex_str, + // expects msats amount: https://github.com/lndk-org/lndk/blob/bce93885f5fc97f3ceb15dc470117e10061de018/src/lib.rs#L403 + amount: toPositiveNumber(amount_msats), + max_fee: toPositiveNumber(max_fee_mtokens) + }, (error, response) => { + if (error) { + return reject(error) + } + resolve({ + secret: response.payment_preimage + }) + }) + }) +} diff --git a/api/lib/lndkrpc-proto.js b/api/lib/lndkrpc-proto.js new file mode 100644 index 000000000..54bff47bd --- /dev/null +++ b/api/lib/lndkrpc-proto.js @@ -0,0 +1,144 @@ +// https://github.com/stackernews/lndk/blob/561aab3f038f937970d91f02a5b49e2ef1188e8f/proto/lndkrpc.proto +// diff https://github.com/stackernews/lndk/commit/e36a2c0c8812185e11a51e38ce9d8fcb513e7446 +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/api/lnd/index.js b/api/lnd/index.js index f09996288..8ec7439c4 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 { authenticatedLndkGrpc } from '@/api/lib/lndk' import { getIdentity, getHeight, getWalletInfo, getNode, getPayment } from 'ln-service' import { datePivot } from '@/lib/time' import { LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants' @@ -10,6 +11,11 @@ const lnd = global.lnd || authenticatedLndGrpc({ macaroon: process.env.LND_MACAROON, socket: process.env.LND_SOCKET }).lnd +export const lndk = authenticatedLndkGrpc({ + 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/README.md b/api/paidAction/README.md index a32588076..d478b20bf 100644 --- a/api/paidAction/README.md +++ b/api/paidAction/README.md @@ -193,6 +193,7 @@ All functions have the following signature: `function(args: Object, context: Obj - `tx`: the current transaction (for anything that needs to be done atomically with the payment) - `models`: the current prisma client (for anything that doesn't need to be done atomically with the payment) - `lnd`: the current lnd client +- `lndk`: the current lndk client ## Recording Cowboy Credits diff --git a/api/paidAction/index.js b/api/paidAction/index.js index ad067a3ca..c1246cd74 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 '@/api/lib/bolt/bolt11' import * as ITEM_CREATE from './itemCreate' import * as ITEM_UPDATE from './itemUpdate' @@ -212,7 +213,7 @@ async function beginPessimisticAction (actionType, args, context) { async function performP2PAction (actionType, args, incomingContext) { // if the action has an invoiceable peer, we'll create a peer invoice // wrap it, and return the wrapped invoice - const { cost, sybilFeePercent, models, lnd, me } = incomingContext + const { cost, sybilFeePercent, models, lnd, lndk, me } = incomingContext if (!sybilFeePercent) { throw new Error('sybil fee percent is not set for an invoiceable peer action') } @@ -232,7 +233,7 @@ async function performP2PAction (actionType, args, incomingContext) { feePercent: sybilFeePercent, description, expiry: INVOICE_EXPIRE_SECS - }, { models, me, lnd }) + }, { models, me, lnd, lndk }) context = { ...incomingContext, @@ -256,7 +257,7 @@ async function performP2PAction (actionType, args, incomingContext) { // we don't need to use the module for perform-ing outside actions // because we can't track the state of outside invoices we aren't paid/paying async function performDirectAction (actionType, args, incomingContext) { - const { models, lnd, cost } = incomingContext + const { models, lnd, cost, lndk } = incomingContext const { comment, lud18Data, noteStr, description: actionDescription } = args const userId = await paidActions[actionType]?.getInvoiceablePeer?.(args, incomingContext) @@ -273,15 +274,16 @@ async function performDirectAction (actionType, args, incomingContext) { invoiceObject = await createUserInvoice(userId, { msats: cost, description, - expiry: INVOICE_EXPIRE_SECS - }, { models, lnd }) + expiry: INVOICE_EXPIRE_SECS, + supportBolt12: false // direct payment is not supported to bolt12 for compatibility reasons + }, { models, lnd, lndk }) } catch (e) { console.error('failed to create outside invoice', e) throw new NonInvoiceablePeerError() } const { invoice, wallet } = invoiceObject - const hash = parsePaymentRequest({ request: invoice }).id + const hash = await parseBolt11({ request: invoice }).id const payment = await models.directPayment.create({ data: { @@ -429,8 +431,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..c75e5cd13 100644 --- a/api/payingAction/index.js +++ b/api/payingAction/index.js @@ -1,12 +1,12 @@ 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 '@/api/lib/bolt' // paying actions are completely distinct from paid actions // and there's only one paying action: send // ... still we want the api to at least be similar -export default async function performPayingAction ({ bolt11, maxFee, walletId }, { me, models, lnd }) { +export default async function performPayingAction ({ bolt11, maxFee, walletId }, { me, models, lnd, lndk }) { try { console.group('performPayingAction', `${bolt11.slice(0, 10)}...`, maxFee, walletId) @@ -14,7 +14,7 @@ export default async function performPayingAction ({ bolt11, maxFee, walletId }, throw new Error('You must be logged in to perform this action') } - const decoded = await parsePaymentRequest({ request: bolt11 }) + const decoded = await parseInvoice({ request: bolt11, lnd, lndk }) const cost = toPositiveBigInt(toPositiveBigInt(decoded.mtokens) + satsToMsats(maxFee)) console.log('cost', cost) @@ -40,8 +40,9 @@ export default async function performPayingAction ({ bolt11, maxFee, walletId }, }) }, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted }) - payViaPaymentRequest({ + payInvoice({ lnd, + lndk, request: withdrawal.bolt11, max_fee: msatsToSats(withdrawal.msatsFeePaying), pathfinding_timeout: LND_PATHFINDING_TIMEOUT_MS, diff --git a/api/resolvers/item.js b/api/resolvers/item.js index a2c7ef647..ba45620e4 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -945,7 +945,7 @@ export default { return await performPaidAction('POLL_VOTE', { id }, { me, models, lnd }) }, - act: async (parent, { id, sats, act = 'TIP', hasSendWallet }, { me, models, lnd, headers }) => { + act: async (parent, { id, sats, act = 'TIP', hasSendWallet }, { me, models, lnd, lndk, headers }) => { assertApiKeyNotPermitted({ me }) await validateSchema(actSchema, { sats, act }) await assertGofacYourself({ models, headers }) @@ -979,11 +979,11 @@ export default { } if (act === 'TIP') { - return await performPaidAction('ZAP', { id, sats, hasSendWallet }, { me, models, lnd }) + return await performPaidAction('ZAP', { id, sats, hasSendWallet }, { me, models, lnd, lndk }) } else if (act === 'DONT_LIKE_THIS') { - return await performPaidAction('DOWN_ZAP', { id, sats }, { me, models, lnd }) + return await performPaidAction('DOWN_ZAP', { id, sats }, { me, models, lnd, lndk }) } else if (act === 'BOOST') { - return await performPaidAction('BOOST', { id, sats }, { me, models, lnd }) + return await performPaidAction('BOOST', { id, sats }, { me, models, lnd, lndk }) } else { throw new GqlInputError('unknown act') } diff --git a/api/resolvers/paidAction.js b/api/resolvers/paidAction.js index 2b993c1f0..f6785bb15 100644 --- a/api/resolvers/paidAction.js +++ b/api/resolvers/paidAction.js @@ -50,7 +50,7 @@ export default { } }, Mutation: { - retryPaidAction: async (parent, { invoiceId }, { models, me, lnd }) => { + retryPaidAction: async (parent, { invoiceId }, { models, me, lnd, lndk }) => { if (!me) { throw new Error('You must be logged in') } @@ -67,7 +67,7 @@ export default { throw new Error(`Invoice is not in failed state: ${invoice.actionState}`) } - const result = await retryPaidAction(invoice.actionType, { invoice }, { models, me, lnd }) + const result = await retryPaidAction(invoice.actionType, { invoice }, { models, me, lnd, lndk }) return { ...result, diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index c71749ac0..b937a8d3e 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' @@ -14,7 +13,7 @@ import { import { amountSchema, validateSchema, withdrawlSchema, lnAddrSchema } from '@/lib/validate' import assertGofacYourself from './ofac' import assertApiKeyNotPermitted from './apiKey' -import { bolt11Tags } from '@/lib/bolt11' +import { getInvoiceDescription } from '@/lib/bolt' import { finalizeHodlInvoice } from '@/worker/wallet' import walletDefs from '@/wallets/server' import { generateResolverName, generateTypeDefName } from '@/wallets/graphql' @@ -25,6 +24,10 @@ import validateWallet from '@/wallets/validate' import { canReceive, getWalletByType } from '@/wallets/common' import performPaidAction from '../paidAction' import performPayingAction from '../payingAction' +import { parseInvoice } from '@/api/lib/bolt' +import lnd, { lndk } from '@/api/lnd' +import { isBolt12Offer } from '@/api/lib/bolt/bolt12' +import { fetchBolt12InvoiceFromOffer } from '@/api/lib/lndk' import { timeoutSignal, withTimeout } from '@/lib/time' function injectResolvers (resolvers) { @@ -32,7 +35,7 @@ function injectResolvers (resolvers) { for (const walletDef of walletDefs) { const resolverName = generateResolverName(walletDef.walletField) console.log(resolverName) - resolvers.Mutation[resolverName] = async (parent, { settings, validateLightning, vaultEntries, ...data }, { me, models }) => { + resolvers.Mutation[resolverName] = async (parent, { settings, validateLightning, vaultEntries, ...data }, { me, models, lnd, lndk }) => { console.log('resolving', resolverName, { settings, validateLightning, vaultEntries, ...data }) let existingVaultEntries @@ -71,6 +74,8 @@ function injectResolvers (resolvers) { ? (data) => withTimeout( walletDef.testCreateInvoice(data, { logger, + lnd, + lndk, signal: timeoutSignal(WALLET_CREATE_INVOICE_TIMEOUT_MS) }), WALLET_CREATE_INVOICE_TIMEOUT_MS) @@ -377,7 +382,7 @@ const resolvers = { f = { ...f, ...f.other } if (f.bolt11) { - f.description = bolt11Tags(f.bolt11).description + f.description = getInvoiceDescription(f.bolt11) } switch (f.type) { @@ -473,13 +478,13 @@ const resolvers = { __resolveType: invoiceOrDirect => invoiceOrDirect.__resolveType }, Mutation: { - createInvoice: async (parent, { amount }, { me, models, lnd, headers }) => { + createInvoice: async (parent, { amount }, { me, models, lnd, lndk, headers }) => { await validateSchema(amountSchema, { amount }) await assertGofacYourself({ models, headers }) const { invoice, paymentMethod } = await performPaidAction('RECEIVE', { msats: satsToMsats(amount) - }, { models, lnd, me }) + }, { models, lnd, lndk, me }) return { ...invoice, @@ -489,7 +494,8 @@ const resolvers = { }, createWithdrawl: createWithdrawal, sendToLnAddr, - cancelInvoice: async (parent, { hash, hmac, userCancel }, { me, models, lnd, boss }) => { + sendToBolt12Offer, + cancelInvoice: async (parent, { hash, hmac, userCancel }, { me, models, lnd, lndk, boss }) => { // stackers can cancel their own invoices without hmac if (me && !hmac) { const inv = await models.invoice.findUnique({ where: { hash } }) @@ -498,7 +504,7 @@ const resolvers = { } else { verifyHmac(hash, hmac) } - await finalizeHodlInvoice({ data: { hash }, lnd, models, boss }) + await finalizeHodlInvoice({ data: { hash }, lnd, lndk, models, boss }) return await models.invoice.update({ where: { hash }, data: { userCancel: !!userCancel } }) }, dropBolt11: async (parent, { hash }, { me, models, lnd }) => { @@ -747,8 +753,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, lndk }) context = { ...context, amount: formatMsats(decoded.mtokens), @@ -916,7 +922,7 @@ async function upsertWallet ( return upsertedWallet } -export async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd, headers, wallet, logger }) { +export async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd, lndk, headers, wallet, logger }) { assertApiKeyNotPermitted({ me }) await validateSchema(withdrawlSchema, { invoice, maxFee }) await assertGofacYourself({ models, headers }) @@ -927,7 +933,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, lndk }) } catch (error) { console.log(error) throw new GqlInputError('could not decode invoice') @@ -966,11 +972,11 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model throw new GqlInputError('SN cannot pay an invoice that SN is proxying') } - return await performPayingAction({ bolt11: invoice, maxFee, walletId: wallet?.id }, { me, models, lnd }) + return await performPayingAction({ bolt11: invoice, maxFee, walletId: wallet?.id }, { me, models, lnd, lndk }) } export async function sendToLnAddr (parent, { addr, amount, maxFee, comment, ...payer }, - { me, models, lnd, headers }) { + { me, models, lnd, lndk, headers }) { if (!me) { throw new GqlAuthenticationError() } @@ -980,17 +986,30 @@ export async function sendToLnAddr (parent, { addr, amount, maxFee, comment, ... { me, models, - lnd + lnd, + lndk }) // take pr and createWithdrawl - return await createWithdrawal(parent, { invoice: res.pr, maxFee }, { me, models, lnd, headers }) + return await createWithdrawal(parent, { invoice: res.pr, maxFee }, { me, models, lnd, lndk, headers }) +} + +export async function sendToBolt12Offer (parent, { offer, amountSats, maxFee, comment }, { me, models, lnd, lndk, headers }) { + if (!me) { + throw new GqlAuthenticationError() + } + assertApiKeyNotPermitted({ me }) + if (!isBolt12Offer(offer)) { + throw new GqlInputError('not a bolt12 offer') + } + const invoice = await fetchBolt12InvoiceFromOffer({ lnd, lndk, offer, msats: satsToMsats(amountSats), description: comment }) + return await createWithdrawal(parent, { invoice, maxFee }, { me, models, lnd, lndk, headers }) } export async function fetchLnAddrInvoice ( { addr, amount, maxFee, comment, ...payer }, { - me, models, lnd, autoWithdraw = false + me, models, lnd, lndk, autoWithdraw = false }) { const options = await lnAddrOptions(addr) await validateSchema(lnAddrSchema, { addr, amount, maxFee, comment, ...payer }, options) @@ -1027,7 +1046,7 @@ export async function fetchLnAddrInvoice ( // decode invoice try { - const decoded = await parsePaymentRequest({ request: res.pr }) + const decoded = await parseInvoice({ request: res.pr, lnd, lndk }) const ourPubkey = await getOurPubkey({ lnd }) if (autoWithdraw && decoded.destination === ourPubkey && process.env.NODE_ENV === 'production') { // unset lnaddr so we don't trigger another withdrawal with same destination diff --git a/api/ssrApollo.js b/api/ssrApollo.js index 7af73317e..e4cf21f9b 100644 --- a/api/ssrApollo.js +++ b/api/ssrApollo.js @@ -5,7 +5,7 @@ import resolvers from './resolvers' import typeDefs from './typeDefs' import models from './models' import { print } from 'graphql' -import lnd from './lnd' +import lnd, { lndk } from './lnd' import search from './search' import { ME } from '@/fragments/users' import { PRICE } from '@/fragments/price' @@ -31,6 +31,7 @@ export default async function getSSRApolloClient ({ req, res, me = null }) { ? session.user : me, lnd, + lndk, search } }), diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js index 3006fc7a4..2b7805b66 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, userCancel: Boolean): Invoice! dropBolt11(hash: String!): Boolean removeWallet(id: ID!): Boolean diff --git a/components/bolt11-info.js b/components/bolt11-info.js index 1dd4dff87..0ce208110 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 { getInvoiceDescription, getInvoicePaymentHash } from '@/lib/bolt' export default ({ bolt11, preimage, children }) => { let description, paymentHash if (bolt11) { - ({ description, payment_hash: paymentHash } = bolt11Tags(bolt11)) + description = getInvoiceDescription(bolt11) + paymentHash = getInvoicePaymentHash(bolt11) } return ( diff --git a/docker/lndk/Dockerfile b/docker/lndk/Dockerfile index a421053a6..b19f6ef3f 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/stackernews/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 f75d6547e..a7bfcf2fc 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!) { @@ -169,6 +176,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..dbe501b48 --- /dev/null +++ b/lib/bech32b12.js @@ -0,0 +1,49 @@ +// 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') + b5s.push(i) + } + 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 = convertBits(b8s, 8, 5, true) + return b5s.map(b5 => ALPHABET[b5]).join('') +} + +function convertBits (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/bolt/bolt11-tags.js b/lib/bolt/bolt11-tags.js new file mode 100644 index 000000000..fc4b56c98 --- /dev/null +++ b/lib/bolt/bolt11-tags.js @@ -0,0 +1,22 @@ +/* eslint-disable camelcase */ +import { decode } from 'bolt11' +import { bolt11InvoiceSchema } from '@/lib/validate' + +export function isBolt11 (request) { + return bolt11InvoiceSchema.isValidSync(request) +} + +function bolt11Tags (bolt11) { + if (!isBolt11(bolt11)) throw new Error('not a bolt11 invoice') + return decode(bolt11).tagsObject +} + +export function getBolt11Description (bolt11) { + const { description } = bolt11Tags(bolt11) + return description +} + +export function getBolt11PaymentHash (bolt11) { + const { payment_hash } = bolt11Tags(bolt11) + return payment_hash +} diff --git a/lib/bolt/bolt12-info.js b/lib/bolt/bolt12-info.js new file mode 100644 index 000000000..6d7e88cb0 --- /dev/null +++ b/lib/bolt/bolt12-info.js @@ -0,0 +1,44 @@ +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) { + return bolt12OfferSchema.isValidSync(invoice) +} + +export function isBolt12Invoice (invoice) { + return bolt12InvoiceSchema.isValidSync(invoice) +} + +export function isBolt12 (invoice) { + return isBolt12Offer(invoice) || isBolt12Invoice(invoice) +} + +export function getBolt12Description (bolt12) { + if (!isBolt12(bolt12)) throw new Error('not a bolt12 invoice or offer') + const buf = bech32b12.decode(bolt12.substring(4)/* remove lni1 or lno1 prefix */) + const tlv = deserializeTLVStream(buf) + let description = '' + for (const { type, value } of tlv) { + if (type === TYPE_DESCRIPTION) { + description = value.toString() || description + } else if (type === TYPE_PAYER_NOTE) { + description = value.toString() || description + break + } + } + return description +} + +export function getBolt12PaymentHash (bolt12) { + if (!isBolt12(bolt12)) throw new Error('not a bolt12 invoice or offer') + const buf = bech32b12.decode(bolt12.substring(4)/* remove lni1 or lno1 prefix */) + const tlv = deserializeTLVStream(buf) + const paymentHash = tlv.find(({ type }) => type === TYPE_PAYMENT_HASH) + return paymentHash?.value?.toString('hex') +} diff --git a/lib/bolt/index.js b/lib/bolt/index.js new file mode 100644 index 000000000..743b8cab3 --- /dev/null +++ b/lib/bolt/index.js @@ -0,0 +1,12 @@ +import { getBolt11Description, getBolt11PaymentHash } from '@/lib/bolt/bolt11-tags' +import { getBolt12Description, getBolt12PaymentHash, isBolt12 } from '@/lib/bolt/bolt12-info' + +export function getInvoiceDescription (bolt) { + if (isBolt12(bolt)) return getBolt12Description(bolt) + return getBolt11Description(bolt) +} + +export function getInvoicePaymentHash (bolt) { + if (isBolt12(bolt)) return getBolt12PaymentHash(bolt) + return getBolt11PaymentHash(bolt) +} diff --git a/lib/bolt11.js b/lib/bolt11.js deleted file mode 100644 index f04770167..000000000 --- a/lib/bolt11.js +++ /dev/null @@ -1,5 +0,0 @@ -import { decode } from 'bolt11' - -export function bolt11Tags (bolt11) { - return decode(bolt11).tagsObject -} diff --git a/lib/tlv.js b/lib/tlv.js new file mode 100644 index 000000000..62df3478c --- /dev/null +++ b/lib/tlv.js @@ -0,0 +1,35 @@ +export function deserializeTLVStream (buff) { + const tlvs = [] + let bytePos = 0 + while (bytePos < buff.length) { + const [type, typeLength] = readBigSize(buff, bytePos) + bytePos += typeLength + + const [length, lengthLength] = readBigSize(buff, bytePos) + bytePos += lengthLength + + if (bytePos + Number(length) > buff.length) { + throw new Error('invalid tlv stream') + } + + const value = buff.subarray(bytePos, bytePos + Number(length)) + bytePos += Number(length) + + tlvs.push({ type, length: Number(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/lib/validate.js b/lib/validate.js index 12bce77a8..f70c0f8b6 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').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, diff --git a/pages/api/graphql.js b/pages/api/graphql.js index 9d6626e93..9e8b4375a 100644 --- a/pages/api/graphql.js +++ b/pages/api/graphql.js @@ -2,7 +2,7 @@ import { ApolloServer } from '@apollo/server' import { startServerAndCreateNextHandler } from '@as-integrations/next' import resolvers from '@/api/resolvers' import models from '@/api/models' -import lnd from '@/api/lnd' +import lnd, { lndk } from '@/api/lnd' import typeDefs from '@/api/typeDefs' import { getServerSession } from 'next-auth/next' import { getAuthOptions } from './auth/[...nextauth]' @@ -74,6 +74,7 @@ export default startServerAndCreateNextHandler(apolloServer, { models, headers: req.headers, lnd, + lndk, me: session ? session.user : null, diff --git a/pages/api/lnurlp/[username]/pay.js b/pages/api/lnurlp/[username]/pay.js index fc7e21220..37e0cee3a 100644 --- a/pages/api/lnurlp/[username]/pay.js +++ b/pages/api/lnurlp/[username]/pay.js @@ -1,5 +1,5 @@ import models from '@/api/models' -import lnd from '@/api/lnd' +import lnd, { lndk } from '@/api/lnd' import { lnurlPayDescriptionHashForUser, lnurlPayMetadataString, lnurlPayDescriptionHash } from '@/lib/lnurl' import { schnorr } from '@noble/curves/secp256k1' import { createHash } from 'crypto' @@ -85,7 +85,7 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa comment: comment || '', lud18Data: parsedPayerData, noteStr - }, { models, lnd, me: user }) + }, { models, lnd, lndk, me: user }) if (!invoice?.bolt11) throw new Error('could not generate invoice') diff --git a/pages/api/lnwith.js b/pages/api/lnwith.js index ed22d499f..57ed88582 100644 --- a/pages/api/lnwith.js +++ b/pages/api/lnwith.js @@ -2,7 +2,7 @@ // send back import models from '@/api/models' import { datePivot } from '@/lib/time' -import lnd from '@/api/lnd' +import lnd, { lndk } from '@/api/lnd' import { createWithdrawal } from '@/api/resolvers/wallet' export default async ({ query, headers }, res) => { @@ -68,7 +68,7 @@ async function doWithdrawal (query, res, headers) { try { const withdrawal = await createWithdrawal(null, { invoice: query.pr, maxFee: me.withdrawMaxFeeDefault }, - { me, models, lnd, headers }) + { me, models, lnd, lndk, headers }) // store withdrawal id lnWith so client can show it await models.lnWith.update({ where: { k1: query.k1 }, data: { withdrawalId: Number(withdrawal.id) } }) diff --git a/pages/withdraw.js b/pages/withdraw.js index 1a39386e3..dbb7e241a 100644 --- a/pages/withdraw.js +++ b/pages/withdraw.js @@ -5,13 +5,13 @@ import { useRouter } from 'next/router' import { InputGroup, Nav } from 'react-bootstrap' import styles from '@/components/user-header.module.css' import { gql, useMutation, useQuery } from '@apollo/client' -import { CREATE_WITHDRAWL, SEND_TO_LNADDR } from '@/fragments/wallet' +import { CREATE_WITHDRAWL, SEND_TO_LNADDR, SEND_TO_BOLT12_OFFER } from '@/fragments/wallet' import { requestProvider } from 'webln' import { useEffect, useState } from 'react' import { useMe } from '@/components/me' import { WithdrawlSkeleton } from './withdrawals/[id]' import { Checkbox, Form, Input, InputUserSuggest, SubmitButton } from '@/components/form' -import { lnAddrSchema, withdrawlSchema } from '@/lib/validate' +import { lnAddrSchema, withdrawlSchema, bolt12WithdrawSchema } from '@/lib/validate' import { useShowModal } from '@/components/modal' import { useField } from 'formik' import { useToast } from '@/components/toast' @@ -65,6 +65,11 @@ function WithdrawForm () { lightning address + + + bolt12 offer + + @@ -79,6 +84,8 @@ export function SelectedWithdrawalForm () { return case 'lnaddr': return + case 'bolt12': + return default: return } @@ -386,3 +393,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/prisma/migrations/20241212160430_bolt12_attachment/migration.sql b/prisma/migrations/20241212160430_bolt12_attachment/migration.sql new file mode 100644 index 000000000..d700a37ee --- /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_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 diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dcdb3b938..788d4b09b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -193,6 +193,7 @@ enum WalletType { BLINK LNC WEBLN + BOLT12 } model Wallet { @@ -220,6 +221,7 @@ model Wallet { walletNWC WalletNWC? walletPhoenixd WalletPhoenixd? walletBlink WalletBlink? + walletBolt12 WalletBolt12? vaultEntries VaultEntry[] @relation("VaultEntries") withdrawals Withdrawl[] @@ -329,6 +331,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/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/ATTACH.md b/wallets/bolt12/ATTACH.md new file mode 100644 index 000000000..466ac634d --- /dev/null +++ b/wallets/bolt12/ATTACH.md @@ -0,0 +1,15 @@ + +# wait for channels to be ready + +You'll need to wait for eclair to have at least one channel in NORMAL state + +`sndev cli eclair channels` + + +# get bolt12 offer + +`sndev cli eclair tipjarshowoffer` + +# check channels balance + +`sndev cli eclair usablebalances` 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..32e3517b8 --- /dev/null +++ b/wallets/bolt12/index.js @@ -0,0 +1,23 @@ +import { bolt12OfferSchema } from '@/lib/validate' +export const name = 'bolt12' +export const walletType = 'BOLT12' +export const walletField = 'walletBolt12' +export const isBolt12OnlyWallet = true + +export const fields = [ + { + name: 'offer', + label: 'bolt12 offer', + type: 'text', + placeholder: 'lno....', + clear: true, + serverOnly: true, + validate: bolt12OfferSchema + } +] + +export const card = { + title: 'Bolt12', + subtitle: 'receive payments to a bolt12 offer', + image: { src: '/wallets/bolt12.svg' } +} diff --git a/wallets/bolt12/server.js b/wallets/bolt12/server.js new file mode 100644 index 000000000..de012023a --- /dev/null +++ b/wallets/bolt12/server.js @@ -0,0 +1,16 @@ +import { fetchBolt12InvoiceFromOffer } from '@/api/lib/lndk' +import { parseInvoice } from '@/api/lib/bolt' +import { toPositiveNumber } from '@/lib/format' +export * from '@/wallets/bolt12' + +export async function testCreateInvoice ({ offer }, { lnd, lndk }) { + const invoice = await fetchBolt12InvoiceFromOffer({ lnd, lndk, offer, msats: 1000, description: 'test' }) + const parsedInvoice = await parseInvoice({ lnd, lndk, request: invoice }) + if (toPositiveNumber(parsedInvoice.mtokens) !== 1000) throw new Error('invalid invoice response') + return offer +} + +export async function createInvoice ({ msats, description, expiry }, { offer }, { lnd, lndk }) { + const invoice = await fetchBolt12InvoiceFromOffer({ lnd, lndk, offer, msats, description }) + return invoice +} 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 f14e9fb36..b3c166cee 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,19 @@ import * as webln from '@/wallets/webln' import { walletLogger } from '@/api/resolvers/wallet' import walletDefs from '@/wallets/server' -import { parsePaymentRequest } from 'ln-service' +import { parseInvoice } from '@/api/lib/bolt' +import { isBolt12Offer, isBolt12Invoice } from '@/api/lib/bolt/bolt12' import { toPositiveBigInt, toPositiveNumber, formatMsats, formatSats, msatsToSats } from '@/lib/format' import { PAID_ACTION_TERMINAL_STATES, WALLET_CREATE_INVOICE_TIMEOUT_MS } from '@/lib/constants' import { timeoutSignal, withTimeout } from '@/lib/time' import { canReceive } from './common' import wrapInvoice from './wrap' -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 }) { +export async function createInvoice (userId, { msats, description, descriptionHash, expiry = 360, supportBolt12 = true }, { predecessorId, models, lnd, lndk }) { // get the wallets in order of priority const wallets = await getInvoiceableWallets(userId, { predecessorId, models }) @@ -32,6 +34,7 @@ export async function createInvoice (userId, { msats, description, descriptionHa for (const { def, wallet } of wallets) { const logger = walletLogger({ wallet, models }) + if (def.isBolt12OnlyWallet && !supportBolt12) continue try { logger.info( @@ -45,25 +48,32 @@ export async function createInvoice (userId, { msats, description, descriptionHa invoice = await walletCreateInvoice( { wallet, def }, { msats, description, descriptionHash, expiry }, - { logger, models }) + { logger, models, lnd, lndk, supportBolt12 }) } catch (err) { throw new Error('failed to create invoice: ' + err.message) } - const bolt11 = await parsePaymentRequest({ request: invoice }) + 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') + } - logger.info(`created invoice for ${formatSats(msatsToSats(bolt11.mtokens))}`, { + const parsedInvoice = await parseInvoice({ lnd, lndk, request: invoice }) + logger.info(`created invoice for ${formatSats(msatsToSats(parsedInvoice.mtokens))}`, { bolt11: invoice }) - if (BigInt(bolt11.mtokens) !== BigInt(msats)) { - if (BigInt(bolt11.mtokens) > BigInt(msats)) { + if (BigInt(parsedInvoice.mtokens) !== BigInt(msats)) { + if (BigInt(parsedInvoice.mtokens) > BigInt(msats)) { throw new Error('invoice invalid: amount too big') } - if (BigInt(bolt11.mtokens) === 0n) { + if (BigInt(parsedInvoice.mtokens) === 0n) { throw new Error('invoice invalid: amount is 0 msats') } - if (BigInt(msats) - BigInt(bolt11.mtokens) >= 1000n) { + if (BigInt(msats) - BigInt(parsedInvoice.mtokens) >= 1000n) { throw new Error('invoice invalid: amount too small') } @@ -81,7 +91,7 @@ export async function createInvoice (userId, { msats, description, descriptionHa export async function createWrappedInvoice (userId, { msats, feePercent, description, descriptionHash, expiry = 360 }, - { predecessorId, models, me, lnd }) { + { predecessorId, models, me, lnd, lndk }) { let logger, bolt11 try { const { invoice, wallet } = await createInvoice(userId, { @@ -90,13 +100,12 @@ export async function createWrappedInvoice (userId, description, descriptionHash, expiry - }, { predecessorId, models }) - + }, { predecessorId, models, lnd, lndk }) logger = walletLogger({ wallet, models }) bolt11 = invoice const { invoice: wrappedInvoice, maxFee } = - await wrapInvoice({ bolt11, feePercent }, { msats, description, descriptionHash }, { me, lnd }) + await wrapInvoice({ bolt11, feePercent }, { msats, description, descriptionHash }, { me, lnd, lndk }) return { invoice, @@ -166,7 +175,7 @@ async function walletCreateInvoice ({ wallet, def }, { description, descriptionHash, expiry = 360 -}, { logger, models }) { +}, { logger, models, lnd, lndk }) { // check for pending withdrawals const pendingWithdrawals = await models.withdrawl.count({ where: { @@ -203,6 +212,8 @@ async function walletCreateInvoice ({ wallet, def }, { wallet.wallet, { logger, + lnd, + lndk, signal: timeoutSignal(WALLET_CREATE_INVOICE_TIMEOUT_MS) } ), WALLET_CREATE_INVOICE_TIMEOUT_MS) diff --git a/wallets/wrap.js b/wallets/wrap.js index 038422229..eb9d0dc91 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 '@/api/lib/bolt' const MIN_OUTGOING_MSATS = BigInt(900) // the minimum msats we'll allow for the outgoing invoice const MAX_OUTGOING_MSATS = BigInt(900_000_000) // the maximum msats we'll allow for the outgoing invoice @@ -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} { @@ -28,7 +29,7 @@ const MAX_FEE_ESTIMATE_PERCENT = 3n // the maximum fee relative to outgoing we'l maxFee: number } */ -export default async function wrapInvoice ({ bolt11, feePercent }, { msats, description, descriptionHash }, { me, lnd }) { +export default async function wrapInvoice ({ bolt11, feePercent }, { msats, description, descriptionHash }, { me, lnd, lndk }) { try { console.group('wrapInvoice', description) @@ -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, lndk }) if (!inv) { throw new Error('Unable to decode invoice') } @@ -147,8 +148,9 @@ export default async function wrapInvoice ({ bolt11, feePercent }, { msats, desc // get routing estimates const { routingFeeMsat, timeLockDelay } = - await estimateRouteFee({ + await estimateFees({ lnd, + lndk, destination: inv.destination, mtokens: inv.mtokens, request: bolt11, diff --git a/worker/autowithdraw.js b/worker/autowithdraw.js index c2105d021..29bd246e9 100644 --- a/worker/autowithdraw.js +++ b/worker/autowithdraw.js @@ -2,7 +2,7 @@ import { msatsSatsFloor, msatsToSats, satsToMsats } from '@/lib/format' import { createWithdrawal } from '@/api/resolvers/wallet' import { createInvoice } from '@/wallets/server' -export async function autoWithdraw ({ data: { id }, models, lnd }) { +export async function autoWithdraw ({ data: { id }, models, lnd, lndk }) { const user = await models.user.findUnique({ where: { id } }) if ( user.autoWithdrawThreshold === null || @@ -42,12 +42,12 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) { if (pendingOrFailed.exists) return - const { invoice, wallet, logger } = await createInvoice(id, { msats, description: 'SN: autowithdrawal', expiry: 360 }, { models }) + const { invoice, wallet, logger } = await createInvoice(id, { msats, description: 'SN: autowithdrawal', expiry: 360 }, { models, lnd, lndk }) try { return await createWithdrawal(null, { invoice, maxFee: msatsToSats(maxFeeMsats) }, - { me: { id }, models, lnd, wallet, logger }) + { me: { id }, models, lnd, lndk, wallet, logger }) } catch (err) { logger.error(`incoming payment failed: ${err}`, { bolt11: invoice }) throw err diff --git a/worker/index.js b/worker/index.js index 16d48c59c..0704e01cc 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 { authenticatedLndkGrpc } from '@/api/lib/lndk' import { views, rankViews } from './views' import { imgproxy } from './imgproxy' import { deleteItem } from './ephemeralItems' @@ -74,7 +75,13 @@ async function work () { socket: process.env.LND_SOCKET }) - const args = { boss, models, apollo, lnd } + const lndk = authenticatedLndkGrpc({ + cert: process.env.LNDK_CERT, + macaroon: process.env.LNDK_MACAROON, + socket: process.env.LNDK_SOCKET + }) + + const args = { boss, models, apollo, lnd, lndk } boss.on('error', error => console.error(error)) diff --git a/worker/paidAction.js b/worker/paidAction.js index 9b3ecb5a6..47de8921c 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 '@/api/lib/bolt' import { MIN_SETTLEMENT_CLTV_DELTA } from '@/wallets/wrap' - // aggressive finalization retry options const FINALIZE_OPTIONS = { retryLimit: 2 ** 31 - 1, retryBackoff: false, retryDelay: 5, priority: 1000 } @@ -194,7 +194,7 @@ export async function paidActionPaid ({ data: { invoiceId, ...args }, models, ln } // this performs forward creating the outgoing payment -export async function paidActionForwarding ({ data: { invoiceId, ...args }, models, lnd, boss }) { +export async function paidActionForwarding ({ data: { invoiceId, ...args }, models, lnd, lndk, boss }) { const transitionedInvoice = await transitionInvoice('paidActionForwarding', { invoiceId, fromState: 'PENDING_HELD', @@ -211,7 +211,7 @@ export async function paidActionForwarding ({ data: { invoiceId, ...args }, mode const { expiryHeight, acceptHeight } = hodlInvoiceCltvDetails(lndInvoice) const { bolt11, maxFeeMsats } = invoiceForward - const invoice = await parsePaymentRequest({ request: bolt11 }) + const invoice = await parseInvoice({ request: bolt11, lnd, lndk }) // maxTimeoutDelta is the number of blocks left for the outgoing payment to settle const maxTimeoutDelta = toPositiveNumber(expiryHeight) - toPositiveNumber(acceptHeight) - MIN_SETTLEMENT_CLTV_DELTA if (maxTimeoutDelta - toPositiveNumber(invoice.cltv_delta) < 0) { @@ -265,8 +265,9 @@ 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, + lndk, request: bolt11, max_fee_mtokens: String(maxFeeMsats), pathfinding_timeout: LND_PATHFINDING_TIMEOUT_MS, @@ -426,7 +427,7 @@ export async function paidActionHeld ({ data: { invoiceId, ...args }, models, ln }, { models, lnd, boss }) } -export async function paidActionCanceling ({ data: { invoiceId, ...args }, models, lnd, boss }) { +export async function paidActionCanceling ({ data: { invoiceId, ...args }, models, lnd, lndk, boss }) { const transitionedInvoice = await transitionInvoice('paidActionCanceling', { invoiceId, fromState: ['HELD', 'PENDING', 'PENDING_HELD', 'FAILED_FORWARD'], @@ -445,7 +446,7 @@ export async function paidActionCanceling ({ data: { invoiceId, ...args }, model if (transitionedInvoice.invoiceForward) { const { wallet, bolt11 } = transitionedInvoice.invoiceForward const logger = walletLogger({ wallet, models }) - const decoded = await parsePaymentRequest({ request: bolt11 }) + const decoded = await parseInvoice({ request: bolt11, lnd, lndk }) logger.info(`invoice for ${formatSats(msatsToSats(decoded.mtokens))} canceled by payer`, { bolt11 }) } } diff --git a/worker/wallet.js b/worker/wallet.js index ac09c7ac4..035bc54ef 100644 --- a/worker/wallet.js +++ b/worker/wallet.js @@ -115,7 +115,7 @@ function subscribeToHodlInvoice (args) { // if we already have the invoice from a subscription event or previous call, // we can skip a getInvoice call -export async function checkInvoice ({ data: { hash, invoice }, boss, models, lnd }) { +export async function checkInvoice ({ data: { hash, invoice }, boss, models, lnd, lndk }) { const inv = invoice ?? await getInvoice({ id: hash, lnd }) // invoice could be created by LND but wasn't inserted into the database yet @@ -148,7 +148,7 @@ export async function checkInvoice ({ data: { hash, invoice }, boss, models, lnd // transitions when held are dependent on the withdrawl status return await checkWithdrawal({ data: { hash: dbInv.invoiceForward.withdrawl.hash, invoice: inv }, models, lnd, boss }) } - return await paidActionForwarding({ data: { invoiceId: dbInv.id, invoice: inv }, models, lnd, boss }) + return await paidActionForwarding({ data: { invoiceId: dbInv.id, invoice: inv }, models, lnd, lndk, boss }) } return await paidActionHeld({ data: { invoiceId: dbInv.id, invoice: inv }, models, lnd, boss }) } @@ -241,7 +241,7 @@ export async function checkWithdrawal ({ data: { hash, withdrawal, invoice }, bo // The callback subscriptions above will NOT get called for JIT invoices that are already paid. // So we manually cancel the HODL invoice here if it wasn't settled by user action -export async function finalizeHodlInvoice ({ data: { hash }, models, lnd, boss, ...args }) { +export async function finalizeHodlInvoice ({ data: { hash }, models, lnd, lndk, boss, ...args }) { const inv = await getInvoice({ id: hash, lnd }) if (inv.is_confirmed) { return @@ -256,7 +256,7 @@ export async function finalizeHodlInvoice ({ data: { hash }, models, lnd, boss, await paidActionCanceling({ data: { invoiceId: dbInv.id, invoice: inv }, models, lnd, boss }) // sync LND invoice status with invoice status in database - await checkInvoice({ data: { hash }, models, lnd, boss }) + await checkInvoice({ data: { hash }, models, lnd, lndk, boss }) } export async function checkPendingDeposits (args) { diff --git a/worker/weeklyPosts.js b/worker/weeklyPosts.js index 275b23bab..02c55f677 100644 --- a/worker/weeklyPosts.js +++ b/worker/weeklyPosts.js @@ -22,7 +22,7 @@ export async function weeklyPost (args) { } } -export async function payWeeklyPostBounty ({ data: { id }, models, apollo, lnd }) { +export async function payWeeklyPostBounty ({ data: { id }, models, apollo, lnd, lndk }) { const itemQ = await apollo.query({ query: gql` query item($id: ID!) { @@ -56,6 +56,7 @@ export async function payWeeklyPostBounty ({ data: { id }, models, apollo, lnd } models, me: { id: USER_ID.sn }, lnd, + lndk, forcePaymentMethod: PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT }) }