199 lines
5.2 KiB
JavaScript
199 lines
5.2 KiB
JavaScript
// import server side wallets
|
|
import * as lnd from 'wallets/lnd/server'
|
|
import * as cln from 'wallets/cln/server'
|
|
import * as lnAddr from 'wallets/lightning-address/server'
|
|
import * as lnbits from 'wallets/lnbits/server'
|
|
import * as nwc from 'wallets/nwc/server'
|
|
import * as phoenixd from 'wallets/phoenixd/server'
|
|
import * as blink from 'wallets/blink/server'
|
|
|
|
// we import only the metadata of client side wallets
|
|
import * as lnc from 'wallets/lnc'
|
|
import * as webln from 'wallets/webln'
|
|
|
|
import { walletLogger } from '@/api/resolvers/wallet'
|
|
import walletDefs from 'wallets/server'
|
|
import { parsePaymentRequest } from 'ln-service'
|
|
import { toPositiveBigInt, toPositiveNumber } from '@/lib/validate'
|
|
import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
|
import { withTimeout } from '@/lib/time'
|
|
import { canReceive } from './common'
|
|
import { formatMsats, formatSats, msatsToSats } from '@/lib/format'
|
|
import wrapInvoice from './wrap'
|
|
|
|
export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln]
|
|
|
|
const MAX_PENDING_INVOICES_PER_WALLET = 25
|
|
|
|
export async function createInvoice (userId, { msats, description, descriptionHash, expiry = 360 }, { models }) {
|
|
// get the wallets in order of priority
|
|
const wallets = await models.wallet.findMany({
|
|
where: { userId, enabled: true },
|
|
include: {
|
|
user: true
|
|
},
|
|
orderBy: [
|
|
{ priority: 'asc' },
|
|
// use id as tie breaker (older wallet first)
|
|
{ id: 'asc' }
|
|
]
|
|
})
|
|
|
|
msats = toPositiveNumber(msats)
|
|
|
|
for (const wallet of wallets) {
|
|
const w = walletDefs.find(w => w.walletType === wallet.type)
|
|
|
|
const config = wallet.wallet
|
|
if (!canReceive({ def: w, config })) {
|
|
continue
|
|
}
|
|
|
|
const logger = walletLogger({ wallet, models })
|
|
|
|
try {
|
|
logger.info(
|
|
`↙ incoming payment: ${formatSats(msatsToSats(msats))}`,
|
|
{
|
|
amount: formatMsats(msats)
|
|
})
|
|
|
|
let invoice
|
|
try {
|
|
invoice = await walletCreateInvoice(
|
|
{ msats, description, descriptionHash, expiry },
|
|
{ ...w, userId, createInvoice: w.createInvoice },
|
|
{ logger, models })
|
|
} catch (err) {
|
|
throw new Error('failed to create invoice: ' + err.message)
|
|
}
|
|
|
|
const bolt11 = await parsePaymentRequest({ request: invoice })
|
|
|
|
logger.info(`created invoice for ${formatSats(msatsToSats(bolt11.mtokens))}`, {
|
|
bolt11: invoice
|
|
})
|
|
|
|
if (BigInt(bolt11.mtokens) !== BigInt(msats)) {
|
|
if (BigInt(bolt11.mtokens) > BigInt(msats)) {
|
|
throw new Error('invoice invalid: amount too big')
|
|
}
|
|
if (BigInt(bolt11.mtokens) === 0n) {
|
|
throw new Error('invoice invalid: amount is 0 msats')
|
|
}
|
|
if (BigInt(msats) - BigInt(bolt11.mtokens) >= 1000n) {
|
|
throw new Error('invoice invalid: amount too small')
|
|
}
|
|
|
|
logger.warn('wallet does not support msats')
|
|
}
|
|
|
|
return { invoice, wallet, logger }
|
|
} catch (err) {
|
|
logger.error(err.message)
|
|
}
|
|
}
|
|
|
|
throw new Error('no wallet to receive available')
|
|
}
|
|
|
|
export async function createWrappedInvoice (userId,
|
|
{ msats, feePercent, description, descriptionHash, expiry = 360 },
|
|
{ models, me, lnd }) {
|
|
let logger, bolt11
|
|
try {
|
|
const { invoice, wallet } = await createInvoice(userId, {
|
|
// this is the amount the stacker will receive, the other (feePercent)% is our fee
|
|
msats: toPositiveBigInt(msats) * (100n - feePercent) / 100n,
|
|
description,
|
|
descriptionHash,
|
|
expiry
|
|
}, { models })
|
|
|
|
logger = walletLogger({ wallet, models })
|
|
bolt11 = invoice
|
|
|
|
const { invoice: wrappedInvoice, maxFee } =
|
|
await wrapInvoice({ bolt11, feePercent }, { msats, description, descriptionHash }, { me, lnd })
|
|
|
|
return {
|
|
invoice,
|
|
wrappedInvoice: wrappedInvoice.request,
|
|
wallet,
|
|
maxFee
|
|
}
|
|
} catch (e) {
|
|
logger?.error('invalid invoice: ' + e.message, { bolt11 })
|
|
throw e
|
|
}
|
|
}
|
|
|
|
async function walletCreateInvoice (
|
|
{
|
|
msats,
|
|
description,
|
|
descriptionHash,
|
|
expiry = 360
|
|
},
|
|
{
|
|
userId,
|
|
walletType,
|
|
walletField,
|
|
createInvoice
|
|
},
|
|
{ logger, models }) {
|
|
const wallet = await models.wallet.findFirst({
|
|
where: {
|
|
userId,
|
|
type: walletType
|
|
},
|
|
include: {
|
|
[walletField]: true,
|
|
user: true
|
|
}
|
|
})
|
|
|
|
const config = wallet[walletField]
|
|
|
|
if (!wallet || !config) {
|
|
throw new Error('wallet not found')
|
|
}
|
|
|
|
// check for pending withdrawals
|
|
const pendingWithdrawals = await models.withdrawl.count({
|
|
where: {
|
|
walletId: wallet.id,
|
|
status: null
|
|
}
|
|
})
|
|
|
|
// and pending forwards
|
|
const pendingForwards = await models.invoiceForward.count({
|
|
where: {
|
|
walletId: wallet.id,
|
|
invoice: {
|
|
actionState: {
|
|
notIn: PAID_ACTION_TERMINAL_STATES
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
const pending = pendingWithdrawals + pendingForwards
|
|
if (pendingWithdrawals + pendingForwards >= MAX_PENDING_INVOICES_PER_WALLET) {
|
|
throw new Error(`too many pending invoices: has ${pending}, max ${MAX_PENDING_INVOICES_PER_WALLET}`)
|
|
}
|
|
|
|
return await withTimeout(
|
|
createInvoice(
|
|
{
|
|
msats,
|
|
description: wallet.user.hideInvoiceDesc ? undefined : description,
|
|
descriptionHash,
|
|
expiry
|
|
},
|
|
config,
|
|
{ logger }
|
|
), 10_000)
|
|
}
|