-
-
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
Conversation
@@ -10,6 +11,11 @@ const lnd = global.lnd || authenticatedLndGrpc({ | |||
macaroon: process.env.LND_MACAROON, | |||
socket: process.env.LND_SOCKET | |||
}).lnd | |||
installLNDK(lnd, { |
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 pass lnd
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:
const lndk = lnd?.lndk
if (!lndk) throw new Error('lndk not installed, please use installLNDK')
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 comment
The reason will be displayed to describe this comment to others. Learn more.
i've renamed @/lib/bolt11
to @/lib/bolt11-tags
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.
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 comment
The 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 comment
The 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 comment
The 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.
Are they supposed to work reliably after your patch @huumn ?
return invoice.startsWith('lni1') || invoice.startsWith('lno1') | ||
} | ||
|
||
export function bolt12Info (bolt12) { |
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.
This is the bolt12 equivalent of the bolt11Tags helper.
Similar to the bolt11 counterpart, i've used a light parser just for the few tags we need in the ui, since the lndk parser needs access to the grpc endpoint.
I couldn't find an established pure js bolt12 parser, so i wrote a small tlv parser (@/lib/tlv
) following the specs.
} | ||
} | ||
|
||
const out = { |
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.
This function converts the output from lndk to an object with a layout compatible with the output from ln-service.
I've followed the doc so it should match the output very closely, but honestly i don't like the fact that ln-service returns so many values that mean the same thing with different types (eg, tokens, mtokens, safe_tokens...), i've implemented this conversion because it makes our code simpler with fewer changes, since we can handle both bolt11 and bolt12 invoices in the same way, but imo we should provide our own abstraction at some point, using only bigint msats and things that make more sense for our codebase.
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.
I agree, ln-service tries to make the grpc interface nicer, but it isn't as well documented as the grpc interface so it ends up just being confusing most of the time.
lib/lndk.js
Outdated
response_invoice_timeout: timeout | ||
}, async (error, response) => { | ||
if (error) return reject(error) | ||
const bech32invoice = 'lni1' + bech32b12.encode(Buffer.from(response.invoice_hex_str, 'hex')) |
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.
Bolt12 invoices are not supposed to be presented to the user, so the spec doesn't define an human readable representation, instead it is left to apps and nodes to handle the representation how they see more fit.
LNDK uses an hex string, while CLN use bech32.
For consistency with bolt11, i've decided to convert them to bech32 like in CLN.
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.
Tested normal and automated withdrawals and they work well!
But I noticed the followings things I mentioned in comments that I'd like to see discussed before approval:
-
lib/ was meant to be files that the server and client can import. Can we use dynamic imports so we don't need to split bolt files by server and client and can have just one file per bolt?
-
how did you test
max_fee
? -
duplicate checks via
if
and schema -
function signature of
parseBolt12Request
-
error handling via async callback
-
add similar function for bolt tags like for
payInvoice
,parseInvoice
,estimateFees
? -
Showing BOLT12 invoices to the user: I think ideally they would always only see the BOLT12 offer. That's also what other wallets do. Is that possible in this PR or should we keep it for another PR?
-
mutating the
lnd
object instead of passinglndk
separately in context -
LND custom options
I read in the LNDK docs that we need to run LND with custom options:
[protocol]
protocol.custom-message=513
protocol.custom-nodeann=39
protocol.custom-init=39
You've added these options in #1702.
Afaict, this means @huumn needs to configure LND differently in prod. You should have mentioned this since I think this wouldn't have worked as-is in prod. 👀
- other small stuff (see comments)
placeholder: 'lno....', | ||
clear: true, | ||
serverOnly: true, | ||
validate: string() |
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.
If I enter asdf
, I get this error as a toast:
failed to create test invoice: 3 INVALID_ARGUMENT: The provided offer was invalid. Please provide a valid offer in bech32 format, i.e. starting with 'lno'. Error: Bech32(MissingSeparator)
Validation should make sure the input actually makes sense
export const card = { | ||
title: 'Bolt12', | ||
subtitle: 'receive payments to a bolt12 offer', | ||
image: { src: '/wallets/bolt12.svg' } |
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.
nitpick: I am not a fan of this (unofficial?) logo. I would prefer to just use text for the protocols but no strong opinion. I let you or @huumn decide.
|
||
|
||
-- Update wallet json | ||
CREATE TRIGGER wallet_blink_as_jsonb |
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.
I didn't know that triggers are scoped to the tables, so this worked because the table WalletBolt12
does not have a trigger with this name yet.
So maybe we should rename all triggers to use a name like wallet_jsonb_trigger
so there are less unnecessary changes between wallet tables? 🤔
But I think for now, in this PR, this should use the consistent name wallet_bolt12_as_jsonb
.
|
||
export async function testCreateInvoice ({ offer }, { lnd }) { | ||
const invoice = await fetchBolt12InvoiceFromOffer({ lnd, offer, msats: 1000, description: 'test' }) | ||
if (!isBolt12Invoice(invoice)) throw new Error('not a bolt12 invoice') |
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.
Can fetchBolt12InvoiceFromOffer
ever return someting that is not a bolt12 invoice?
wallets/server.js
Outdated
} | ||
|
||
checkInvoice(invoice, { msats }, { lnd, logger }) |
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.
await
missing 👀
logger.info(`created invoice for ${formatSats(msatsToSats(parsedInvoice.mtokens))}`, { | ||
bolt11: invoice | ||
}) |
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.
I think this logger call should be in createInvoice
immediately after walletCreateInvoice
like it was before
// get the wallets in order of priority | ||
const wallets = await getInvoiceableWallets(userId, { predecessorId, models }) | ||
|
||
msats = toPositiveNumber(msats) | ||
|
||
for (const { def, wallet } of wallets) { | ||
const logger = walletLogger({ wallet, models }) | ||
if (def.isBolt12OnlyWallet && !supportBolt12) continue |
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.
Do you still need this? You also check below for supportBolt12
after the invoice was fetched
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.
yes, this early check is to exclude bolt12-only wallets when WE cannot use them, logging an error for a perfectly fine wallet just because someone is asking an invoice via lnurlp, would be confusing for the user
export async function estimateBolt12RouteFee ({ lnd, destination, tokens, mtokens, request, timeout }) { | ||
const lndk = lnd?.lndk | ||
if (!lndk) throw new Error('lndk not installed, please use installLNDK') | ||
const parsedInvoice = request ? await parseBolt12Request({ lnd, request }) : {} | ||
|
||
if (!tokens && mtokens) tokens = toPositiveNumber(msatsToSats(mtokens)) | ||
tokens ??= toPositiveNumber(parsedInvoice.tokens) | ||
destination ??= parsedInvoice.destination | ||
|
||
if (!destination) throw new Error('no destination provided') | ||
if (!tokens) throw new Error('no tokens provided') | ||
|
||
return await new Promise((resolve, reject) => { | ||
lnd.router.estimateRouteFee({ | ||
dest: Buffer.from(destination, 'hex'), | ||
amt_sat: tokens, | ||
timeout | ||
}, (err, res) => { | ||
if (err) { | ||
if (res?.failure_reason) { | ||
reject(new Error(`unable to estimate route: ${res.failure_reason}`)) | ||
} else { | ||
reject(err) | ||
} | ||
return | ||
} | ||
|
||
if (res.routing_fee_msat < 0 || res.time_lock_delay <= 0) { | ||
reject(new Error('unable to estimate route, excessive values: ' + JSON.stringify(res))) | ||
return | ||
} | ||
|
||
resolve({ | ||
routingFeeMsat: toPositiveNumber(res.routing_fee_msat), | ||
timeLockDelay: toPositiveNumber(res.time_lock_delay) | ||
}) | ||
}) | ||
}) | ||
} |
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.
I think this is the same as estimateRouteFee
for bolt11 except it needs to parse the bolt12 request before. This should afterwards call estimateRouteFee
imo.
installLNDK(lnd, { | ||
cert: process.env.LNDK_CERT, | ||
macaroon: process.env.LNDK_MACAROON, | ||
socket: process.env.LNDK_SOCKET | ||
}) |
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.
see other comment
Co-authored-by: ekzyis <[email protected]>
Co-authored-by: ekzyis <[email protected]>
Description
This PR adds support to pay, estimate fees and decode bolt12 invoices and a bolt12 wallet attachment.
The codebase uses the bolt11 nomenclature pretty much everywhere. After this PR, some areas will be generalized to also account for the possibility of a bolt12 invoice. However, to avoid weighing this PR down with unrelated, mostly graphical or naming changes, I’ve decided to leave those changes for a follow-up PR.
Closes #1317
Screenshots
Checklist
Are your changes backwards compatible? Please answer below:
yes
On a scale of 1-10 how well and how have you QA'd this change and any features it might affect? Please answer below:
For frontend changes: Tested on mobile, light and dark mode? Please answer below:
Did you introduce any new environment variables? If so, call them out explicitly here:
yes
LNDK_SOCKET
LNDK_CERT
andLNDK_MACAROON
: can be the same values used for LND (see https://github.com/lndk-org/lndk/blob/master/docs/cli_commands.md )Testing
How to get a bolt12 offer string:
bash sndev cli eclair channels
should return at least 1 channel marked as NORMALbash sndev cli eclair tipjarshowoffer
bash sndev cli eclair usablebalances
Tests
Regression tests