Skip to content

Commit 15d17e0

Browse files
committed
bolt12 attachment
1 parent e6e82b0 commit 15d17e0

File tree

23 files changed

+727
-54
lines changed

23 files changed

+727
-54
lines changed

.env.development

+7
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ LND_CERT=2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494943516a434
6161
LND_MACAROON=0201036c6e6402f801030a106cf4e146abffa5d766befbbf4c73b5a31201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e6572617465120472656164000006202c3bfd55c191e925cbffd73712c9d4b9b4a8440410bde5f8a0a6e33af8b3d876
6262
LND_SOCKET=sn_lnd:10009
6363

64+
# xxd -p -c0 docker/lndk/tls-cert.pem
65+
LNDK_CERT=2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494942614443434151326741774942416749554f6d7333785a2b704256556e746e4644374a306d374c6c314d5a5977436759494b6f5a497a6a3045417749770a495445664d4230474131554541777757636d4e6e5a573467633256735a69427a615764755a5751675932567964444167467730334e5441784d4445774d4441770a4d444261474138304d446b324d4445774d5441774d4441774d466f77495445664d4230474131554541777757636d4e6e5a573467633256735a69427a615764750a5a575167593256796444425a4d424d4742797147534d34394167454743437147534d3439417745484130494142476475396358554753504979635343626d47620a362f34552b74787645306153767a734d632b704b4669586c422b502f33782f5778594d786c4842306c68396654515538746456694a3241592f516e485677556b0a4f34436a495441664d42304741315564455151574d42534343577876593246736147397a64494948633235666247356b617a414b42676771686b6a4f505151440a41674e4a41444247416945413738556450486764615856797474717432312b7557546c466e344236717565474c2f636d5970516269497343495143777859306e0a783276357a58457750552f624f6e61514e657139463841542b2f346c4b656c48664f4e2f47773d3d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a
66+
LNDK_MACAROON=0201036c6e6402f801030a106cf4e146abffa5d766befbbf4c73b5a31201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e6572617465120472656164000006202c3bfd55c191e925cbffd73712c9d4b9b4a8440410bde5f8a0a6e33af8b3d876
67+
LNDK_SOCKET=sn_lndk:7000
68+
69+
70+
6471
# nostr (NIP-57 zap receipts)
6572
# openssl rand -hex 32
6673
NOSTR_PRIVATE_KEY=5f30b7e7714360f51f2be2e30c1d93b7fdf67366e730658e85777dfcc4e4245f

api/lnd/index.js

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { cachedFetcher } from '@/lib/fetch'
22
import { toPositiveNumber } from '@/lib/format'
33
import { authenticatedLndGrpc } from '@/lib/lnd'
4+
import { installLNDK } from '@/lib/lndk'
45
import { getIdentity, getHeight, getWalletInfo, getNode, getPayment } from 'ln-service'
56
import { datePivot } from '@/lib/time'
67
import { LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants'
@@ -10,6 +11,11 @@ const lnd = global.lnd || authenticatedLndGrpc({
1011
macaroon: process.env.LND_MACAROON,
1112
socket: process.env.LND_SOCKET
1213
}).lnd
14+
installLNDK(lnd, {
15+
cert: process.env.LNDK_CERT,
16+
macaroon: process.env.LNDK_MACAROON,
17+
socket: process.env.LNDK_SOCKET
18+
})
1319

1420
if (process.env.NODE_ENV === 'development') global.lnd = lnd
1521

api/paidAction/index.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { createHodlInvoice, createInvoice, parsePaymentRequest } from 'ln-service'
1+
import { createHodlInvoice, createInvoice } from 'ln-service'
22
import { datePivot } from '@/lib/time'
33
import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
44
import { createHmac } from '@/api/resolvers/wallet'
55
import { Prisma } from '@prisma/client'
66
import { createWrappedInvoice, createInvoice as createUserInvoice } from '@/wallets/server'
77
import { assertBelowMaxPendingInvoices, assertBelowMaxPendingDirectPayments } from './lib/assert'
8+
import { parseInvoice } from '@/lib/invoices'
9+
import lnd from '@/api/lnd'
810

911
import * as ITEM_CREATE from './itemCreate'
1012
import * as ITEM_UPDATE from './itemUpdate'
@@ -271,7 +273,7 @@ async function performDirectAction (actionType, args, incomingContext) {
271273
}
272274

273275
const { invoice, wallet } = invoiceObject
274-
const hash = parsePaymentRequest({ request: invoice }).id
276+
const hash = await parseInvoice({ request: invoice, lnd }).id
275277

276278
const payment = await models.directPayment.create({
277279
data: {
@@ -415,7 +417,7 @@ async function createDbInvoice (actionType, args, context) {
415417
}
416418

417419
const servedBolt11 = wrappedBolt11 ?? bolt11
418-
const servedInvoice = parsePaymentRequest({ request: servedBolt11 })
420+
const servedInvoice = await parseInvoice({ request: servedBolt11, lnd })
419421
const expiresAt = new Date(servedInvoice.expires_at)
420422

421423
const invoiceData = {

api/payingAction/index.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants'
22
import { msatsToSats, satsToMsats, toPositiveBigInt } from '@/lib/format'
33
import { Prisma } from '@prisma/client'
4-
import { parsePaymentRequest, payViaPaymentRequest } from 'ln-service'
4+
import { payInvoice, parseInvoice } from '@/lib/invoices'
55

66
// paying actions are completely distinct from paid actions
77
// and there's only one paying action: send
@@ -14,7 +14,7 @@ export default async function performPayingAction ({ bolt11, maxFee, walletId },
1414
throw new Error('You must be logged in to perform this action')
1515
}
1616

17-
const decoded = await parsePaymentRequest({ request: bolt11 })
17+
const decoded = await parseInvoice({ request: bolt11, lnd })
1818
const cost = toPositiveBigInt(toPositiveBigInt(decoded.mtokens) + satsToMsats(maxFee))
1919

2020
console.log('cost', cost)
@@ -40,7 +40,7 @@ export default async function performPayingAction ({ bolt11, maxFee, walletId },
4040
})
4141
}, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted })
4242

43-
payViaPaymentRequest({
43+
payInvoice({
4444
lnd,
4545
request: withdrawal.bolt11,
4646
max_fee: msatsToSats(withdrawal.msatsFeePaying),

api/resolvers/wallet.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {
2-
getInvoice as getInvoiceFromLnd, deletePayment, getPayment,
3-
parsePaymentRequest
2+
getInvoice as getInvoiceFromLnd, deletePayment, getPayment
43
} from 'ln-service'
54
import crypto, { timingSafeEqual } from 'crypto'
65
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
@@ -24,6 +23,8 @@ import validateWallet from '@/wallets/validate'
2423
import { canReceive } from '@/wallets/common'
2524
import performPaidAction from '../paidAction'
2625
import performPayingAction from '../payingAction'
26+
import { parseInvoice } from '@/lib/invoices'
27+
import lnd from '@/api/lnd'
2728

2829
function injectResolvers (resolvers) {
2930
console.group('injected GraphQL resolvers:')
@@ -713,7 +714,7 @@ export const walletLogger = ({ wallet, models }) => {
713714
try {
714715
if (context?.bolt11) {
715716
// automatically populate context from bolt11 to avoid duplicating this code
716-
const decoded = await parsePaymentRequest({ request: context.bolt11 })
717+
const decoded = await parseInvoice({ request: context.bolt11, lnd })
717718
context = {
718719
...context,
719720
amount: formatMsats(decoded.mtokens),
@@ -890,7 +891,7 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model
890891
// decode invoice to get amount
891892
let decoded, sockets
892893
try {
893-
decoded = await parsePaymentRequest({ request: invoice })
894+
decoded = await parseInvoice({ request: invoice, lnd })
894895
} catch (error) {
895896
console.log(error)
896897
throw new GqlInputError('could not decode invoice')
@@ -990,7 +991,7 @@ export async function fetchLnAddrInvoice (
990991

991992
// decode invoice
992993
try {
993-
const decoded = await parsePaymentRequest({ request: res.pr })
994+
const decoded = await parseInvoice({ request: res.pr, lnd })
994995
const ourPubkey = await getOurPubkey({ lnd })
995996
if (autoWithdraw && decoded.destination === ourPubkey && process.env.NODE_ENV === 'production') {
996997
// unset lnaddr so we don't trigger another withdrawal with same destination

docker/lndk/Dockerfile

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
# This image uses fedora 40 because the official pre-built lndk binaries require
22
# glibc 2.39 which is not available on debian or ubuntu images.
33
FROM fedora:40
4-
RUN useradd -u 1000 -m lndk
54

5+
ENV INSTALLER_DOWNLOAD_URL="https://github.com/riccardobl/lndk/releases/download/v0.2.0-maxfee"
6+
7+
RUN useradd -u 1000 -m lndk
68
RUN mkdir -p /home/lndk/.lndk
79
COPY ["./tls-*", "/home/lndk/.lndk"]
810
RUN chown 1000:1000 -Rvf /home/lndk/.lndk && \
911
chmod 644 /home/lndk/.lndk/tls-cert.pem && \
1012
chmod 600 /home/lndk/.lndk/tls-key.pem
1113

1214
USER lndk
13-
RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/lndk-org/lndk/releases/download/v0.2.0/lndk-installer.sh | sh
15+
RUN curl --proto '=https' --tlsv1.2 -LsSf $INSTALLER_DOWNLOAD_URL/lndk-installer.sh | sh
1416
RUN echo 'source /home/lndk/.cargo/env' >> $HOME/.bashrc
1517
WORKDIR /home/lndk
1618
EXPOSE 7000

fragments/wallet.js

+3
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ export const WALLET_FIELDS = gql`
168168
apiKeyRecv
169169
currencyRecv
170170
}
171+
... on WalletBolt12 {
172+
offer
173+
}
171174
}
172175
}
173176
`

lib/bech32b12.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
2+
3+
export function decode (str) {
4+
const b5s = []
5+
for (const char of str) {
6+
const i = ALPHABET.indexOf(char)
7+
if (i === -1) throw new Error('Invalid bech32 character')
8+
b5s.push(i)
9+
}
10+
const b8s = Buffer.from(converBits(b5s, 5, 8, false))
11+
return b8s
12+
}
13+
14+
export function encode (b8s) {
15+
const b5s = converBits(b8s, 8, 5, true)
16+
const str = []
17+
for (const b5 of b5s) str.push(ALPHABET[b5])
18+
return str.join('')
19+
}
20+
21+
function converBits (data, frombits, tobits, pad) {
22+
let acc = 0
23+
let bits = 0
24+
const ret = []
25+
const maxv = (1 << tobits) - 1
26+
for (let p = 0; p < data.length; ++p) {
27+
const value = data[p]
28+
if (value < 0 || (value >> frombits) !== 0) {
29+
throw new RangeError('input value is outside of range')
30+
}
31+
acc = (acc << frombits) | value
32+
bits += frombits
33+
while (bits >= tobits) {
34+
bits -= tobits
35+
ret.push((acc >> bits) & maxv)
36+
}
37+
}
38+
if (pad) {
39+
if (bits > 0) {
40+
ret.push((acc << (tobits - bits)) & maxv)
41+
}
42+
} else if (bits >= frombits || ((acc << (tobits - bits)) & maxv)) {
43+
throw new RangeError('could not convert bits')
44+
}
45+
return ret
46+
}

lib/invoices.js

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/* eslint-disable camelcase */
2+
3+
import { payViaPaymentRequest, parsePaymentRequest } from 'ln-service'
4+
import { estimateRouteFee } from '@/api/lnd'
5+
6+
import { payViaBolt12PaymentRequest, parseBolt12Request, estimateBolt12RouteFee } from '@/lib/lndk'
7+
8+
export function isBolt11 (request) {
9+
return request.startsWith('lnbc') || request.startsWith('lntb') || request.startsWith('lntbs') || request.startsWith('lnbcrt')
10+
}
11+
12+
export function parseBolt11 ({ request }) {
13+
if (!isBolt11(request)) {
14+
throw new Error('Not a bolt11 invoice')
15+
}
16+
return parsePaymentRequest({ request })
17+
}
18+
19+
export function payBolt11 ({ lnd, request, max_fee, ...args }) {
20+
if (!isBolt11(request)) {
21+
throw new Error('Not a bolt11 invoice')
22+
}
23+
return payViaPaymentRequest({
24+
lnd,
25+
request,
26+
max_fee,
27+
...args
28+
})
29+
}
30+
31+
export function isBolt12Offer (invoice) {
32+
return invoice.startsWith('lno1')
33+
}
34+
35+
export function isBolt12Invoice (invoice) {
36+
console.log('isBolt12Invoice', invoice)
37+
console.trace()
38+
return invoice.startsWith('lni1')
39+
}
40+
41+
export async function payBolt12 ({ lnd, request: invoice, max_fee }) {
42+
if (!isBolt12Invoice(invoice)) {
43+
throw new Error('Not a bolt12 invoice')
44+
}
45+
if (!invoice) throw new Error('No invoice in bolt12, please use prefetchBolt12Invoice')
46+
return await payViaBolt12PaymentRequest({ lnd, request: invoice, max_fee })
47+
}
48+
49+
export function parseBolt12 ({ lnd, request: invoice }) {
50+
if (!isBolt12Invoice(invoice)) {
51+
throw new Error('Not a bolt12 request')
52+
}
53+
return parseBolt12Request({ lnd, request: invoice })
54+
}
55+
56+
export async function payInvoice ({ lnd, request: invoice, max_fee, ...args }) {
57+
if (isBolt12Invoice(invoice)) {
58+
return await payBolt12({ lnd, request: invoice, max_fee, ...args })
59+
} else {
60+
return await payBolt11({ lnd, request: invoice, max_fee, ...args })
61+
}
62+
}
63+
64+
export async function parseInvoice ({ lnd, request }) {
65+
if (isBolt12Invoice(request)) {
66+
return await parseBolt12({ lnd, request })
67+
} else {
68+
return await parseBolt11({ request })
69+
}
70+
}
71+
72+
export async function estimateFees ({ lnd, destination, tokens, mtokens, request, timeout }) {
73+
if (isBolt12Invoice(request)) {
74+
return await estimateBolt12RouteFee({ lnd, destination, tokens, mtokens, request, timeout })
75+
} else {
76+
return await estimateRouteFee({ lnd, destination, tokens, request, mtokens, timeout })
77+
}
78+
}

0 commit comments

Comments
 (0)