Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bolt12 support #1727

Open
wants to merge 49 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
e803efe
bolt12 attachment
riccardobl Dec 17, 2024
332d1e1
don't support direct payments to bolt12
riccardobl Dec 17, 2024
f927fc5
code cleanup, add bolt12info (bolt11 tags equivalent)
riccardobl Dec 17, 2024
494061c
add withdraw to bolt12, improve checks and naming
riccardobl Dec 17, 2024
8fbf5c2
Add create invoice test
riccardobl Dec 17, 2024
f68b69d
Add bolt12 logo
riccardobl Dec 17, 2024
7ff3e1b
improve labels
riccardobl Dec 17, 2024
95246bd
download from sn fork
riccardobl Dec 17, 2024
d326322
resolve bolt12 invoice inside attachment
riccardobl Dec 18, 2024
bae01b3
Merge branch 'master' into bolt12a
riccardobl Dec 18, 2024
20c3e58
rebase
riccardobl Dec 18, 2024
90f2c9c
add support for max_fee_mtokens in bolt12 interface
riccardobl Dec 18, 2024
b6cc65f
revert some unrelated changes
riccardobl Dec 18, 2024
6c863d8
Merge branch 'master' into bolt12a
riccardobl Dec 18, 2024
0e56bc8
deduplicate code
riccardobl Dec 20, 2024
cc993ff
Merge branch 'master' into bolt12a
riccardobl Dec 22, 2024
efcd9a2
Update lib/validate.js
riccardobl Dec 25, 2024
2411e99
Update wallets/bolt12/index.js
riccardobl Dec 25, 2024
28c24d5
fix trigger name
riccardobl Dec 25, 2024
f735d68
fix typo
riccardobl Dec 25, 2024
86a36ae
use String() to cast strings
riccardobl Dec 25, 2024
aae6de9
catch errors in async callback
riccardobl Dec 25, 2024
4191919
readd trim removed by mistake
riccardobl Dec 25, 2024
fa9ede4
removed unused default, rename lndSocket to lndkSocket
riccardobl Dec 25, 2024
63013c0
improve feature bit mapping
riccardobl Dec 25, 2024
406d3aa
add missing await
riccardobl Dec 25, 2024
501d272
permalink to repo
riccardobl Dec 25, 2024
e93dc91
remove duplicate checks
riccardobl Dec 27, 2024
a1b534e
validate offer string
riccardobl Dec 27, 2024
702f24e
revert change from older iteration
riccardobl Dec 27, 2024
b1b37d7
refactor lndk client
riccardobl Dec 29, 2024
7e94360
move bolt libs into lib/bolt
riccardobl Dec 29, 2024
c943284
use schema from lib/validate
riccardobl Dec 29, 2024
0418486
use /api/lnd/estimateRouteFee in estimateBolt12RouteFee
riccardobl Dec 29, 2024
d0e9ad8
rename installLNDK to enableLNDK
riccardobl Dec 29, 2024
6471ad6
fix error
riccardobl Dec 29, 2024
9c40ddb
fix regression: pass hex string not bech32 invoice to PayInvoice
riccardobl Dec 29, 2024
7ab3099
add attach.md
riccardobl Dec 29, 2024
7eb5234
refactor bolt12/bolt11 client parser lib
riccardobl Dec 29, 2024
143d7bc
move server-only libs to api/lib
riccardobl Dec 29, 2024
edea0e5
Merge branch 'master' into bolt12a
riccardobl Dec 30, 2024
287e114
filter out POST /api/graphql spam from sndev logs
riccardobl Dec 30, 2024
5cd447b
Merge remote-tracking branch 'upstream/master' into bolt12a
riccardobl Jan 6, 2025
bb10b6e
implement context maze
riccardobl Jan 7, 2025
5a41b01
fix merge issue
riccardobl Jan 7, 2025
b3365bd
fix refactoring issue
riccardobl Jan 7, 2025
10a1041
add safety check
riccardobl Jan 7, 2025
c59621d
Merge branch 'master' into bolt12a
riccardobl Jan 7, 2025
2454a1a
Merge branch 'master' into bolt12a
huumn Jan 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions api/lib/bolt/bolt11.js
Original file line number Diff line number Diff line change
@@ -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 })
}
132 changes: 132 additions & 0 deletions api/lib/bolt/bolt12.js
Original file line number Diff line number Diff line change
@@ -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
}
39 changes: 39 additions & 0 deletions api/lib/bolt/index.js
Original file line number Diff line number Diff line change
@@ -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')
}
}
115 changes: 115 additions & 0 deletions api/lib/lndk.js
Original file line number Diff line number Diff line change
@@ -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
})
})
})
}
Loading