From 494061c5e9e76a88691f65ca3b3aa4b2c7251677 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 17 Dec 2024 20:17:07 +0100 Subject: [PATCH] add withdraw to bolt12, improve checks and naming --- api/payingAction/index.js | 2 +- api/resolvers/wallet.js | 23 ++++- api/typeDefs/wallet.js | 1 + components/bolt11-info.js | 4 +- fragments/wallet.js | 7 ++ lib/bech32b12.js | 11 +- lib/{bolt11-info.js => bolt11-tags.js} | 2 +- lib/bolt11.js | 5 +- lib/bolt12-info.js | 19 ++-- lib/bolt12.js | 13 ++- lib/{invoices.js => boltInvoices.js} | 22 +++- lib/lndk.js | 137 +++++++++++++------------ lib/tlv.js | 11 +- lib/validate.js | 12 ++- pages/wallet/index.js | 79 +++++++++++++- wallets/server.js | 5 +- wallets/wrap.js | 2 +- worker/paidAction.js | 2 +- 18 files changed, 248 insertions(+), 109 deletions(-) rename lib/{bolt11-info.js => bolt11-tags.js} (88%) rename lib/{invoices.js => boltInvoices.js} (53%) diff --git a/api/payingAction/index.js b/api/payingAction/index.js index 731733a15..745357b04 100644 --- a/api/payingAction/index.js +++ b/api/payingAction/index.js @@ -1,7 +1,7 @@ import { LND_PATHFINDING_TIME_PREF_PPM, LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants' import { msatsToSats, satsToMsats, toPositiveBigInt } from '@/lib/format' import { Prisma } from '@prisma/client' -import { payInvoice, parseInvoice } from '@/lib/invoices' +import { payInvoice, parseInvoice } from '@/lib/boltInvoices' // paying actions are completely distinct from paid actions // and there's only one paying action: send diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index 618fa85f3..8ae997608 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -12,7 +12,7 @@ import { import { amountSchema, validateSchema, withdrawlSchema, lnAddrSchema } from '@/lib/validate' import assertGofacYourself from './ofac' import assertApiKeyNotPermitted from './apiKey' -import { bolt11Info, isBolt11 } from '@/lib/bolt11-info' +import { bolt11Tags, isBolt11 } from '@/lib/bolt11-tags' import { bolt12Info } from '@/lib/bolt12-info' import { finalizeHodlInvoice } from '@/worker/wallet' import walletDefs from '@/wallets/server' @@ -24,8 +24,10 @@ import validateWallet from '@/wallets/validate' import { canReceive } from '@/wallets/common' import performPaidAction from '../paidAction' import performPayingAction from '../payingAction' -import { parseInvoice } from '@/lib/invoices' +import { parseInvoice } from '@/lib/boltInvoices' import lnd from '@/api/lnd' +import { isBolt12Offer } from '@/lib/bolt12' +import { fetchBolt12InvoiceFromOffer } from '@/lib/lndk' function injectResolvers (resolvers) { console.group('injected GraphQL resolvers:') @@ -369,7 +371,7 @@ const resolvers = { f = { ...f, ...f.other } if (f.bolt11) { - f.description = isBolt11(f.bolt11) ? bolt11Info(f.bolt11).description : bolt12Info(f.bolt11).description + f.description = isBolt11(f.bolt11) ? bolt11Tags(f.bolt11).description : bolt12Info(f.bolt11).description } switch (f.type) { @@ -481,6 +483,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 }) @@ -940,7 +943,7 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model throw new GqlInputError('SN cannot pay an invoice that SN is proxying') } - return await performPayingAction({ invoice, maxFee, walletId: wallet?.id }, { me, models, lnd }) + return await performPayingAction({ bolt11: invoice, maxFee, walletId: wallet?.id }, { me, models, lnd }) } export async function sendToLnAddr (parent, { addr, amount, maxFee, comment, ...payer }, @@ -961,6 +964,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 }, { diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js index 932b67bcd..b1746c061 100644 --- a/api/typeDefs/wallet.js +++ b/api/typeDefs/wallet.js @@ -78,6 +78,7 @@ const typeDefs = ` createInvoice(amount: Int!): InvoiceOrDirect! createWithdrawl(invoice: String!, maxFee: Int!): Withdrawl! sendToLnAddr(addr: String!, amount: Int!, maxFee: Int!, comment: String, identifier: Boolean, name: String, email: String): Withdrawl! + sendToBolt12Offer(offer: String!, amountSats: Int!, maxFee: Int!, comment: String): Withdrawl! cancelInvoice(hash: String!, hmac: String!): Invoice! dropBolt11(hash: String!): Boolean removeWallet(id: ID!): Boolean diff --git a/components/bolt11-info.js b/components/bolt11-info.js index 81ff8fe44..f6b3bb761 100644 --- a/components/bolt11-info.js +++ b/components/bolt11-info.js @@ -1,12 +1,12 @@ import AccordianItem from './accordian-item' import { CopyInput } from './form' -import { bolt11Info, isBolt11 } from '@/lib/bolt11-info' +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 } = isBolt11(bolt11) ? bolt11Info(bolt11) : bolt12Info(bolt11)) + ({ description, payment_hash: paymentHash } = isBolt11(bolt11) ? bolt11Tags(bolt11) : bolt12Info(bolt11)) } return ( diff --git a/fragments/wallet.js b/fragments/wallet.js index e1b7a288a..96b6a775d 100644 --- a/fragments/wallet.js +++ b/fragments/wallet.js @@ -121,6 +121,13 @@ export const SEND_TO_LNADDR = gql` } }` +export const SEND_TO_BOLT12_OFFER = gql` + mutation sendToBolt12Offer($offer: String!, $amountSats: Int!, $maxFee: Int!, $comment: String) { + sendToBolt12Offer(offer: $offer, amountSats: $amountSats, maxFee: $maxFee, comment: $comment) { + id + } +}` + export const REMOVE_WALLET = gql` mutation removeWallet($id: ID!) { diff --git a/lib/bech32b12.js b/lib/bech32b12.js index 611515e06..be5427d21 100644 --- a/lib/bech32b12.js +++ b/lib/bech32b12.js @@ -1,10 +1,14 @@ +// 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') + if (i === -1) throw new Error('invalid bech32 character') b5s.push(i) } const b8s = Buffer.from(converBits(b5s, 5, 8, false)) @@ -12,10 +16,9 @@ export function decode (str) { } export function encode (b8s) { + if (b8s.length > 2048) throw new Error('input is too long') const b5s = converBits(b8s, 8, 5, true) - const str = [] - for (const b5 of b5s) str.push(ALPHABET[b5]) - return str.join('') + return b5s.map(b5 => ALPHABET[b5]).join('') } function converBits (data, frombits, tobits, pad) { diff --git a/lib/bolt11-info.js b/lib/bolt11-tags.js similarity index 88% rename from lib/bolt11-info.js rename to lib/bolt11-tags.js index 498aa139f..04248a1a4 100644 --- a/lib/bolt11-info.js +++ b/lib/bolt11-tags.js @@ -4,7 +4,7 @@ export function isBolt11 (request) { return request.startsWith('lnbc') || request.startsWith('lntb') || request.startsWith('lntbs') || request.startsWith('lnbcrt') } -export function bolt11Info (bolt11) { +export function bolt11Tags (bolt11) { if (!isBolt11(bolt11)) throw new Error('not a bolt11 invoice') return decode(bolt11).tagsObject } diff --git a/lib/bolt11.js b/lib/bolt11.js index c616e7f9b..42ee584e6 100644 --- a/lib/bolt11.js +++ b/lib/bolt11.js @@ -1,8 +1,11 @@ /* eslint-disable camelcase */ import { payViaPaymentRequest, parsePaymentRequest } from 'ln-service' +import { bolt11InvoiceSchema } from './validate' export function isBolt11 (request) { - return request.startsWith('lnbc') || request.startsWith('lntb') || request.startsWith('lntbs') || request.startsWith('lnbcrt') + 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 }) { diff --git a/lib/bolt12-info.js b/lib/bolt12-info.js index 7b98bab23..99a8ef7ce 100644 --- a/lib/bolt12-info.js +++ b/lib/bolt12-info.js @@ -1,6 +1,10 @@ import { deserializeTLVStream } from './tlv' import * as bech32b12 from '@/lib/bech32b12' +const TYPE_DESCRIPTION = 10n +const TYPE_PAYER_NOTE = 89n +const TYPE_PAYMENT_HASH = 168n + export function isBolt12 (invoice) { return invoice.startsWith('lni1') || invoice.startsWith('lno1') } @@ -9,20 +13,21 @@ export function bolt12Info (bolt12) { if (!isBolt12(bolt12)) throw new Error('not a bolt12 invoice or offer') const buf = bech32b12.decode(bolt12.substring(4)/* remove lni1 or lno1 prefix */) const tlv = deserializeTLVStream(buf) - const INFO_TYPES = { - description: 10n, - payment_hash: 168n - } + const info = { description: '', payment_hash: '' } + for (const { type, value } of tlv) { - if (type === INFO_TYPES.description) { - info.description = value.toString() - } else if (type === INFO_TYPES.payment_hash) { + if (type === TYPE_DESCRIPTION) { + info.description = value.toString() || info.description + } else if (type === TYPE_PAYER_NOTE) { + info.description = value.toString() || info.description + } else if (type === TYPE_PAYMENT_HASH) { info.payment_hash = value.toString('hex') } } + return info } diff --git a/lib/bolt12.js b/lib/bolt12.js index f6af6a164..cb6d671a9 100644 --- a/lib/bolt12.js +++ b/lib/bolt12.js @@ -1,13 +1,18 @@ /* eslint-disable camelcase */ import { payViaBolt12PaymentRequest, parseBolt12Request } from '@/lib/lndk' +import { bolt12OfferSchema, bolt12InvoiceSchema } from './validate' export function isBolt12Offer (invoice) { - return invoice.startsWith('lno1') + if (!invoice.startsWith('lno1')) return false + bolt12OfferSchema.validateSync(invoice) + return true } export function isBolt12Invoice (invoice) { - return invoice.startsWith('lni1') + if (!invoice.startsWith('lni1')) return false + bolt12InvoiceSchema.validateSync(invoice) + return true } export function isBolt12 (invoice) { @@ -19,7 +24,7 @@ export async function payBolt12 ({ lnd, request: invoice, max_fee }) { return await payViaBolt12PaymentRequest({ lnd, request: invoice, max_fee }) } -export function parseBolt12 ({ lnd, request: invoice }) { +export async function parseBolt12 ({ lnd, request: invoice }) { if (!isBolt12Invoice(invoice)) throw new Error('not a bolt12 request') - return parseBolt12Request({ lnd, request: invoice }) + return await parseBolt12Request({ lnd, request: invoice }) } diff --git a/lib/invoices.js b/lib/boltInvoices.js similarity index 53% rename from lib/invoices.js rename to lib/boltInvoices.js index bf7a3697d..57e7092c1 100644 --- a/lib/invoices.js +++ b/lib/boltInvoices.js @@ -1,29 +1,41 @@ /* eslint-disable camelcase */ -import { payBolt12, parseBolt12, isBolt12Invoice } from './bolt12' -import { payBolt11, parseBolt11 } from './bolt11' +import { payBolt12, parseBolt12, isBolt12Invoice, isBolt12Offer } from './bolt12' +import { payBolt11, parseBolt11, isBolt11 } from './bolt11' import { estimateBolt12RouteFee } from '@/lib/lndk' import { estimateRouteFee } from '@/api/lnd' export async function payInvoice ({ lnd, request: invoice, max_fee, ...args }) { if (isBolt12Invoice(invoice)) { return await payBolt12({ lnd, request: invoice, max_fee, ...args }) - } else { + } else if (isBolt11(invoice)) { return await payBolt11({ lnd, request: invoice, max_fee, ...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 ({ lnd, request }) { if (isBolt12Invoice(request)) { return await parseBolt12({ lnd, request }) - } else { + } 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, destination, tokens, mtokens, request, timeout }) { if (isBolt12Invoice(request)) { return await estimateBolt12RouteFee({ lnd, destination, tokens, mtokens, request, timeout }) - } else { + } else if (isBolt11(request)) { return await estimateRouteFee({ 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') } } diff --git a/lib/lndk.js b/lib/lndk.js index f46ddaa64..564eb1d43 100644 --- a/lib/lndk.js +++ b/lib/lndk.js @@ -6,7 +6,6 @@ import grpcCredentials from 'lightning/lnd_grpc/grpc_credentials' import { defaultSocket, 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 @@ -41,51 +40,6 @@ export function installLNDK (lnd, { cert, macaroon, socket }, withProxy) { lnd.lndk = client } -export async function fetchBolt12InvoiceFromOffer ({ lnd, offer, amount, description, timeout = 10_000 }) { - const lndk = lnd.lndk - if (!lndk) throw new Error('lndk not installed, please use installLNDK') - return new Promise((resolve, reject) => { - lndk.GetInvoice({ - offer, - amount: toPositiveNumber(amount), - payer_note: description, - response_invoice_timeout: timeout - }, (error, response) => { - if (error) return reject(error) - const bech32invoice = 'lni1' + bech32b12.encode(Buffer.from(response.invoice_hex_str, 'hex')) - resolve(bech32invoice) - }) - }) -} - -export async function payViaBolt12PaymentRequest ({ - lnd, - request: invoice_hex_str, - max_fee -}) { - const lndk = lnd.lndk - if (!lndk) throw new Error('lndk not installed, please use installLNDK') - - const bolt12 = await parseBolt12Request({ lnd, request: invoice_hex_str }) - - const req = { - invoice: bolt12.payment, - amount: toPositiveNumber(bolt12.mtokens), - max_fee - } - - return new Promise((resolve, reject) => { - lndk.PayInvoice(req, (error, response) => { - if (error) { - return reject(error) - } - resolve({ - secret: response.payment_preimage - }) - }) - }) -} - const featureBitMap = { 0: { bit: 0, type: 'DATALOSS_PROTECT_REQ', is_required: true }, 1: { bit: 1, type: 'DATALOSS_PROTECT_OPT', is_required: false }, @@ -148,7 +102,8 @@ export async function parseBolt12Request ({ payment_hash, created_at, relative_expiry, - features + features, + payer_note } = invoice_contents // convert from lndk response to ln-service parsePaymentRequest output layout @@ -164,7 +119,7 @@ export async function parseBolt12Request ({ created_at: new Date(created_at * 1000).toISOString(), // [chain_addresses] cltv_delta: minCltvDelta, - description, + description: payer_note || description, // [description_hash] destination: Buffer.from(node_id.key).toString('hex'), expires_at: new Date((created_at + relative_expiry) * 1000).toISOString(), @@ -185,40 +140,90 @@ export async function parseBolt12Request ({ } }), safe_tokens: Math.round(toPositiveNumber(BigInt(amount_msats)) / 1000), - tokens: Math.floor(toPositiveNumber(BigInt(amount_msats)) / 1000) + tokens: Math.floor(toPositiveNumber(BigInt(amount_msats)) / 1000), + bolt12: invoice_contents } - // mark as bolt12 invoice so we can differentiate it later (this will be used also to pass bolt12 only data) - out.bolt12 = invoice_contents - return out } +export async function fetchBolt12InvoiceFromOffer ({ lnd, offer, msats, description, timeout = 10_000 }) { + const lndk = lnd?.lndk + if (!lndk) throw new Error('lndk not installed, please use installLNDK') + + 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) => { + if (error) return reject(error) + const bech32invoice = 'lni1' + bech32b12.encode(Buffer.from(response.invoice_hex_str, 'hex')) + + // sanity check + const parsedInvoice = await parseBolt12Request({ lnd, request: bech32invoice }) + if ( + !parsedInvoice || + toPositiveNumber(parsedInvoice.mtokens) !== toPositiveNumber(msats) || + toPositiveNumber(parsedInvoice.tokens) !== toPositiveNumber(msatsToSats(msats)) + ) { + return reject(new Error('invalid invoice response')) + } + resolve(bech32invoice) + }) + }) +} + +export async function payViaBolt12PaymentRequest ({ + lnd, + request: invoice_hex_str, + max_fee +}) { + const lndk = lnd?.lndk + if (!lndk) throw new Error('lndk not installed, please use installLNDK') + + const parsedInvoice = await parseBolt12Request({ lnd, request: invoice_hex_str }) + + return new Promise((resolve, reject) => { + lndk.PayInvoice({ + invoice: parsedInvoice.payment, + // expects msats amount: https://github.com/lndk-org/lndk/blob/bce93885f5fc97f3ceb15dc470117e10061de018/src/lib.rs#L403 + amount: toPositiveNumber(parsedInvoice.mtokens), + max_fee + }, (error, response) => { + if (error) { + return reject(error) + } + resolve({ + secret: response.payment_preimage + }) + }) + }) +} + 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 }) : {} - mtokens ??= parsedInvoice.mtokens + + if (!tokens && mtokens) tokens = toPositiveNumber(msatsToSats(mtokens)) + tokens ??= toPositiveNumber(parsedInvoice.tokens) destination ??= parsedInvoice.destination - return await new Promise((resolve, reject) => { - const params = {} - params.dest = Buffer.from(destination, 'hex') - params.amt_sat = null - if (tokens) params.amt_sat = toPositiveNumber(tokens) - else if (mtokens) params.amt_sat = msatsToSats(mtokens) - - if (params.amt_sat === null) { - throw new Error('No tokens or mtokens provided') - } + 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({ - ...params, + 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}`)) + reject(new Error(`unable to estimate route: ${res.failure_reason}`)) } else { reject(err) } @@ -226,7 +231,7 @@ export async function estimateBolt12RouteFee ({ lnd, destination, tokens, mtoken } if (res.routing_fee_msat < 0 || res.time_lock_delay <= 0) { - reject(new Error('Unable to estimate route, excessive values: ' + JSON.stringify(res))) + reject(new Error('unable to estimate route, excessive values: ' + JSON.stringify(res))) return } diff --git a/lib/tlv.js b/lib/tlv.js index a62ced692..62df3478c 100644 --- a/lib/tlv.js +++ b/lib/tlv.js @@ -5,18 +5,17 @@ export function deserializeTLVStream (buff) { const [type, typeLength] = readBigSize(buff, bytePos) bytePos += typeLength - let [length, lengthLength] = readBigSize(buff, bytePos) - length = Number(length) + const [length, lengthLength] = readBigSize(buff, bytePos) bytePos += lengthLength - if (bytePos + length > buff.length) { + if (bytePos + Number(length) > buff.length) { throw new Error('invalid tlv stream') } - const value = buff.subarray(bytePos, bytePos + length) - bytePos += length + const value = buff.subarray(bytePos, bytePos + Number(length)) + bytePos += Number(length) - tlvs.push({ type, length, value }) + tlvs.push({ type, length: Number(length), value }) } return tlvs } diff --git a/lib/validate.js b/lib/validate.js index 86b03bded..6e9d48d5f 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -223,6 +223,16 @@ export const lnAddrSchema = ({ payerData, min, max, commentAllowed } = {}) => return accum }, {}))) +export const bolt11InvoiceSchema = string().trim().matches(process.env.NODE_ENV === 'development' ? /^lnbcrt/ : /^lnbc/, 'invalid bolt11 invoice') +export const bolt12OfferSchema = string().trim().matches(/^lno1/, 'invalid bolt12 offer') +export const bolt12InvoiceSchema = string().trim().matches(/^lni1/, 'invalid bolt12 invoice') +export const bolt12WithdrawSchema = object({ + offer: bolt12OfferSchema.required('required'), + amount: intValidator.required('required').positive('must be positive').min(1, 'must be at least 1'), + maxFee: intValidator.required('required').min(0, 'must be at least 0'), + comment: string().max(128, 'must be less than 128') +}) + export function bountySchema (args) { return object({ title: titleValidator, @@ -468,7 +478,7 @@ export const lastAuthRemovalSchema = object({ }) export const withdrawlSchema = object({ - invoice: string().required('required').trim(), + invoice: string().required('required'), maxFee: intValidator.required('required').min(0, 'must be at least 0') }) diff --git a/pages/wallet/index.js b/pages/wallet/index.js index 759e6e52c..8a1ca563c 100644 --- a/pages/wallet/index.js +++ b/pages/wallet/index.js @@ -11,9 +11,9 @@ import { useMe } from '@/components/me' import { useEffect, useState } from 'react' import { requestProvider } from 'webln' import Alert from 'react-bootstrap/Alert' -import { CREATE_WITHDRAWL, SEND_TO_LNADDR } from '@/fragments/wallet' +import { CREATE_WITHDRAWL, SEND_TO_BOLT12_OFFER, SEND_TO_LNADDR } from '@/fragments/wallet' import { getGetServerSideProps } from '@/api/ssrApollo' -import { amountSchema, lnAddrSchema, withdrawlSchema } from '@/lib/validate' +import { amountSchema, lnAddrSchema, withdrawlSchema, bolt12WithdrawSchema } from '@/lib/validate' import Nav from 'react-bootstrap/Nav' import { BALANCE_LIMIT_MSATS, FAST_POLL_INTERVAL, SSR } from '@/lib/constants' import { msatsToSats, numWithUnits } from '@/lib/format' @@ -195,6 +195,11 @@ export function WithdrawalForm () { lightning address + + + bolt12 offer + + @@ -211,6 +216,8 @@ export function SelectedWithdrawalForm () { return case 'lnaddr-withdraw': return + case 'bolt12-withdraw': + return } } @@ -512,3 +519,71 @@ export function LnAddrWithdrawal () { ) } + +export function Bolt12Withdrawal () { + const { me } = useMe() + const router = useRouter() + const [sendToBolt12Offer, { called, error }] = useMutation(SEND_TO_BOLT12_OFFER) + + const maxFeeDefault = me?.privates?.withdrawMaxFeeDefault + + return ( + <> + {called && !error && } +
{ + const { data } = await sendToBolt12Offer({ + variables: { + offer, + amountSats: Number(amount), + maxFee: Number(maxFee), + comment + } + }) + router.push(`/withdrawals/${data.sendToBolt12Offer.id}`) + }} + > + + sats} + /> + sats} + /> + comment optional} + name='comment' + maxLength={128} + /> + send +
+ + ) +} diff --git a/wallets/server.js b/wallets/server.js index b6eb0584b..81309efb8 100644 --- a/wallets/server.js +++ b/wallets/server.js @@ -14,7 +14,7 @@ import * as webln from '@/wallets/webln' import { walletLogger } from '@/api/resolvers/wallet' import walletDefs from '@/wallets/server' -import { parseInvoice } from '@/lib/invoices' +import { parseInvoice } from '@/lib/boltInvoices' import { isBolt12Offer } from '@/lib/bolt12' import { toPositiveBigInt, toPositiveNumber, formatMsats, formatSats, msatsToSats } from '@/lib/format' import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants' @@ -29,7 +29,6 @@ const MAX_PENDING_INVOICES_PER_WALLET = 25 async function checkInvoice (invoice, { msats }, { lnd, logger }) { const parsedInvoice = await parseInvoice({ lnd, request: invoice }) - console.log('parsedInvoice', parsedInvoice) logger.info(`created invoice for ${formatSats(msatsToSats(parsedInvoice.mtokens))}`, { bolt11: invoice }) @@ -106,7 +105,7 @@ export async function createWrappedInvoice (userId, // We need a bolt12 invoice to wrap, so we fetch one if (isBolt12Offer(invoice)) { - invoice = await fetchBolt12InvoiceFromOffer({ lnd, offer: invoice, amount: innerAmount, description }) + invoice = await fetchBolt12InvoiceFromOffer({ lnd, offer: invoice, msats: innerAmount, description }) checkInvoice(invoice, { msats: innerAmount }, { lnd, logger }) } diff --git a/wallets/wrap.js b/wallets/wrap.js index f3246035b..7f345de3a 100644 --- a/wallets/wrap.js +++ b/wallets/wrap.js @@ -1,7 +1,7 @@ import { createHodlInvoice } from 'ln-service' import { getBlockHeight } from '../api/lnd' import { toBigInt, toPositiveBigInt, toPositiveNumber } from '@/lib/format' -import { parseInvoice, estimateFees } from '@/lib/invoices' +import { parseInvoice, estimateFees } from '@/lib/boltInvoices' const MIN_OUTGOING_MSATS = BigInt(900) // the minimum msats we'll allow for the outgoing invoice const MAX_OUTGOING_MSATS = BigInt(900_000_000) // the maximum msats we'll allow for the outgoing invoice diff --git a/worker/paidAction.js b/worker/paidAction.js index 686b22d66..0b52fcaf1 100644 --- a/worker/paidAction.js +++ b/worker/paidAction.js @@ -10,7 +10,7 @@ import { getInvoice, settleHodlInvoice } from 'ln-service' -import { payInvoice, parseInvoice } from '@/lib/invoices' +import { payInvoice, parseInvoice } from '@/lib/boltInvoices' import { MIN_SETTLEMENT_CLTV_DELTA } from '@/wallets/wrap' // aggressive finalization retry options const FINALIZE_OPTIONS = { retryLimit: 2 ** 31 - 1, retryBackoff: false, retryDelay: 5, priority: 1000 }