Skip to content

Commit

Permalink
add withdraw to bolt12, improve checks and naming
Browse files Browse the repository at this point in the history
  • Loading branch information
riccardobl committed Dec 17, 2024
1 parent f927fc5 commit 494061c
Show file tree
Hide file tree
Showing 18 changed files with 248 additions and 109 deletions.
2 changes: 1 addition & 1 deletion api/payingAction/index.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
23 changes: 19 additions & 4 deletions api/resolvers/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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:')
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 })
Expand Down Expand Up @@ -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 },
Expand All @@ -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 },
{
Expand Down
1 change: 1 addition & 0 deletions api/typeDefs/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions components/bolt11-info.js
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down
7 changes: 7 additions & 0 deletions fragments/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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!) {
Expand Down
11 changes: 7 additions & 4 deletions lib/bech32b12.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
// 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))
return b8s
}

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) {
Expand Down
2 changes: 1 addition & 1 deletion lib/bolt11-info.js → lib/bolt11-tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
5 changes: 4 additions & 1 deletion lib/bolt11.js
Original file line number Diff line number Diff line change
@@ -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 }) {
Expand Down
19 changes: 12 additions & 7 deletions lib/bolt12-info.js
Original file line number Diff line number Diff line change
@@ -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')
}
Expand All @@ -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
}
13 changes: 9 additions & 4 deletions lib/bolt12.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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 })
}
22 changes: 17 additions & 5 deletions lib/invoices.js → lib/boltInvoices.js
Original file line number Diff line number Diff line change
@@ -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')
}
}
Loading

0 comments on commit 494061c

Please sign in to comment.