-
-
Notifications
You must be signed in to change notification settings - Fork 130
Bolt12 support #1727
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
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
e93dc91
a1b534e
702f24e
b1b37d7
7e94360
c943284
0418486
d0e9ad8
6471ad6
9c40ddb
7ab3099
7eb5234
143d7bc
edea0e5
287e114
5cd447b
bb10b6e
5a41b01
b3365bd
10a1041
c59621d
2454a1a
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 | ||
riccardobl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
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) { | ||
riccardobl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
} | ||
riccardobl marked this conversation as resolved.
Show resolved
Hide resolved
|
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 | ||
}) | ||
} |
Uh oh!
There was an error while loading. Please reload this page.