-
-
Notifications
You must be signed in to change notification settings - Fork 114
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
base: master
Are you sure you want to change the base?
Bolt12 support #1727
Changes from 13 commits
e803efe
332d1e1
f927fc5
494061c
8fbf5c2
f68b69d
7ff3e1b
95246bd
d326322
bae01b3
20c3e58
90f2c9c
b6cc65f
6c863d8
0e56bc8
cc993ff
efcd9a2
2411e99
28c24d5
f735d68
86a36ae
aae6de9
4191919
fa9ede4
63013c0
406d3aa
501d272
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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,8 @@ import { | |
import { amountSchema, validateSchema, withdrawlSchema, lnAddrSchema } from '@/lib/validate' | ||
import assertGofacYourself from './ofac' | ||
import assertApiKeyNotPermitted from './apiKey' | ||
import { bolt11Tags } from '@/lib/bolt11' | ||
import { bolt11Tags, isBolt11 } from '@/lib/bolt11-tags' | ||
import { bolt12Info } from '@/lib/bolt12-info' | ||
import { finalizeHodlInvoice } from '@/worker/wallet' | ||
import walletDefs from '@/wallets/server' | ||
import { generateResolverName, generateTypeDefName } from '@/wallets/graphql' | ||
|
@@ -25,14 +25,18 @@ import validateWallet from '@/wallets/validate' | |
import { canReceive, getWalletByType } from '@/wallets/common' | ||
import performPaidAction from '../paidAction' | ||
import performPayingAction from '../payingAction' | ||
import { parseInvoice } from '@/lib/boltInvoices' | ||
import lnd from '@/api/lnd' | ||
import { isBolt12Offer } from '@/lib/bolt12' | ||
import { fetchBolt12InvoiceFromOffer } from '@/lib/lndk' | ||
import { timeoutSignal, withTimeout } from '@/lib/time' | ||
|
||
function injectResolvers (resolvers) { | ||
console.group('injected GraphQL resolvers:') | ||
for (const walletDef of walletDefs) { | ||
const resolverName = generateResolverName(walletDef.walletField) | ||
console.log(resolverName) | ||
resolvers.Mutation[resolverName] = async (parent, { settings, validateLightning, vaultEntries, ...data }, { me, models }) => { | ||
resolvers.Mutation[resolverName] = async (parent, { settings, validateLightning, vaultEntries, ...data }, { me, models, lnd }) => { | ||
console.log('resolving', resolverName, { settings, validateLightning, vaultEntries, ...data }) | ||
|
||
let existingVaultEntries | ||
|
@@ -71,6 +75,7 @@ function injectResolvers (resolvers) { | |
? (data) => withTimeout( | ||
walletDef.testCreateInvoice(data, { | ||
logger, | ||
lnd, | ||
signal: timeoutSignal(WALLET_CREATE_INVOICE_TIMEOUT_MS) | ||
}), | ||
WALLET_CREATE_INVOICE_TIMEOUT_MS) | ||
|
@@ -375,7 +380,7 @@ const resolvers = { | |
f = { ...f, ...f.other } | ||
|
||
if (f.bolt11) { | ||
f.description = bolt11Tags(f.bolt11).description | ||
f.description = isBolt11(f.bolt11) ? bolt11Tags(f.bolt11).description : bolt12Info(f.bolt11).description | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mhh, there is |
||
} | ||
|
||
switch (f.type) { | ||
|
@@ -487,6 +492,7 @@ const resolvers = { | |
}, | ||
createWithdrawl: createWithdrawal, | ||
sendToLnAddr, | ||
sendToBolt12Offer, | ||
cancelInvoice: async (parent, { hash, hmac }, { models, lnd, boss }) => { | ||
verifyHmac(hash, hmac) | ||
await finalizeHodlInvoice({ data: { hash }, lnd, models, boss }) | ||
|
@@ -732,8 +738,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 }) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mhh, I think ideally this would store the bolt12 offer as But I am not sure how easy it is to get the offer back from the invoice? But I think we can at least save it as I really have to do #1598 soon 👀 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i don't think the offer is available as part of the invoice tlv. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I just meant the user-facing stuff (so not just cosmetic imo), not renaming variables but yeah, we can do it in a separate PR. Just wanted to flag this. |
||
context = { | ||
...context, | ||
amount: formatMsats(decoded.mtokens), | ||
|
@@ -912,7 +918,7 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model | |
// decode invoice to get amount | ||
let decoded, sockets | ||
try { | ||
decoded = await parsePaymentRequest({ request: invoice }) | ||
decoded = await parseInvoice({ request: invoice, lnd }) | ||
} catch (error) { | ||
console.log(error) | ||
throw new GqlInputError('could not decode invoice') | ||
|
@@ -972,6 +978,18 @@ export async function sendToLnAddr (parent, { addr, amount, maxFee, comment, ... | |
return await createWithdrawal(parent, { invoice: res.pr, maxFee }, { me, models, lnd, headers }) | ||
} | ||
|
||
export async function sendToBolt12Offer (parent, { offer, amountSats, maxFee, comment }, { me, models, lnd, headers }) { | ||
if (!me) { | ||
throw new GqlAuthenticationError() | ||
} | ||
assertApiKeyNotPermitted({ me }) | ||
if (!isBolt12Offer(offer)) { | ||
throw new GqlInputError('not a bolt12 offer') | ||
} | ||
const invoice = await fetchBolt12InvoiceFromOffer({ lnd, offer, msats: satsToMsats(amountSats), description: comment }) | ||
return await createWithdrawal(parent, { invoice, maxFee }, { me, models, lnd, headers }) | ||
} | ||
|
||
export async function fetchLnAddrInvoice ( | ||
{ addr, amount, maxFee, comment, ...payer }, | ||
{ | ||
|
@@ -1012,7 +1030,7 @@ export async function fetchLnAddrInvoice ( | |
|
||
// decode invoice | ||
try { | ||
const decoded = await parsePaymentRequest({ request: res.pr }) | ||
const decoded = await parseInvoice({ request: res.pr, lnd }) | ||
const ourPubkey = await getOurPubkey({ lnd }) | ||
if (autoWithdraw && decoded.destination === ourPubkey && process.env.NODE_ENV === 'production') { | ||
// unset lnaddr so we don't trigger another withdrawal with same destination | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,12 @@ | ||
import AccordianItem from './accordian-item' | ||
import { CopyInput } from './form' | ||
import { bolt11Tags } from '@/lib/bolt11' | ||
import { bolt11Tags, isBolt11 } from '@/lib/bolt11-tags' | ||
import { bolt12Info } from '@/lib/bolt12-info' | ||
|
||
export default ({ bolt11, preimage, children }) => { | ||
let description, paymentHash | ||
if (bolt11) { | ||
({ description, payment_hash: paymentHash } = bolt11Tags(bolt11)) | ||
({ description, payment_hash: paymentHash } = isBolt11(bolt11) ? bolt11Tags(bolt11) : bolt12Info(bolt11)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see other comment |
||
} | ||
|
||
return ( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still need to take a closer look at this custom parser. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(converBits(b5s, 5, 8, false)) | ||
return b8s | ||
} | ||
|
||
export function encode (b8s) { | ||
if (b8s.length > 2048) throw new Error('input is too long') | ||
const b5s = converBits(b8s, 8, 5, true) | ||
return b5s.map(b5 => ALPHABET[b5]).join('') | ||
} | ||
|
||
function converBits (data, frombits, tobits, pad) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo in function name |
||
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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { decode } from 'bolt11' | ||
|
||
export function isBolt11 (request) { | ||
return request.startsWith('lnbc') || request.startsWith('lntb') || request.startsWith('lntbs') || request.startsWith('lnbcrt') | ||
} | ||
|
||
export function bolt11Tags (bolt11) { | ||
if (!isBolt11(bolt11)) throw new Error('not a bolt11 invoice') | ||
return decode(bolt11).tagsObject | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i've renamed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not put everything bolt11 related into lib/bolt11.js? Same for bolt12 stuff? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. because ln-service uses 'fs' to load protobuf files and it breaks webpack builds for the browser, iirc there are also a bunch of other node specific imports that would require a polyfill There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mhhh, the files in lib/ should be importable by the client and server afaik. Is there a way to fix this by dynamically importing server stuff if a server function is called without significant downsides? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, it is possible, but last time dynamic imports caused a lot of issue in production. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,25 @@ | ||
import { decode } from 'bolt11' | ||
/* eslint-disable camelcase */ | ||
import { payViaPaymentRequest, parsePaymentRequest } from 'ln-service' | ||
import { bolt11InvoiceSchema } from './validate' | ||
|
||
export function bolt11Tags (bolt11) { | ||
return decode(bolt11).tagsObject | ||
export function isBolt11 (request) { | ||
if (!request.startsWith('lnbc') && !request.startsWith('lntb') && !request.startsWith('lntbs') && !request.startsWith('lnbcrt')) return false | ||
bolt11InvoiceSchema.validateSync(request) | ||
return true | ||
} | ||
|
||
export async function parseBolt11 ({ request }) { | ||
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 (!isBolt11(request)) throw new Error('not a bolt11 invoice') | ||
return payViaPaymentRequest({ | ||
lnd, | ||
request, | ||
max_fee, | ||
max_fee_mtokens, | ||
...args | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since lndk is just adding few apis to lnd, we are going to need them both to do most of the api work, by appending the lndk client to the lnd object we reflect its nature as an extension and we avoid passing another field to every context
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't mutate objects from an external library in any way for any reason.
I know it's highly unlikely that this is an issue now (and probably also not in the future), but if this ever becomes an issue, it's going to be extremely hard to debug because it will break in ways we cannot anticipate.
I also don't want to even have to ask myself if this might break. I see this like the Sword of Damocles. Just don't do it, the risk/reward ratio just isn't worth it. I suggest to just pass
lndk
in the context like we passlnd
around.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can then also remove all these checks: