* Use same naming scheme between ln containers and env vars * Add router_lnd container * Only open channels to router_lnd * Use 1sat base fee and 0ppm fee rate * Add script to test routing * Also fund router_lnd wallet * Receiver fallbacks * Rename to predecessorId * Remove useless wallet table join * Missing renaming to predecessor * Fix payment stuck on sender error We want to await the invoice poll promise so we can check for receiver errors, but in case of sender errors, the promise will never settle. * Don't log failed forwards as sender errors * fix check for receiver error --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> Co-authored-by: k00b <k00b@stacker.news>
206 lines
6.7 KiB
JavaScript
206 lines
6.7 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, formatMsats, formatSats, msatsToSats } from '@/lib/format'
|
|
import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
|
import { withTimeout } from '@/lib/time'
|
|
import { canReceive } from './common'
|
|
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 }, { predecessorId, models }) {
|
|
// 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 })
|
|
|
|
try {
|
|
logger.info(
|
|
`↙ incoming payment: ${formatSats(msatsToSats(msats))}`,
|
|
{
|
|
amount: formatMsats(msats)
|
|
})
|
|
|
|
let invoice
|
|
try {
|
|
invoice = await walletCreateInvoice(
|
|
{ wallet, def },
|
|
{ msats, description, descriptionHash, expiry },
|
|
{ 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, { status: true })
|
|
}
|
|
}
|
|
|
|
throw new Error('no wallet to receive available')
|
|
}
|
|
|
|
export async function createWrappedInvoice (userId,
|
|
{ msats, feePercent, description, descriptionHash, expiry = 360 },
|
|
{ predecessorId, 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
|
|
}, { predecessorId, 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
|
|
}
|
|
}
|
|
|
|
export async function getInvoiceableWallets (userId, { predecessorId, models }) {
|
|
// filter out all wallets that have already been tried by recursively following the retry chain of predecessor invoices.
|
|
// the current predecessor invoice is in state 'FAILED' and not in state 'RETRYING' because we are currently retrying it
|
|
// so it has not been updated yet.
|
|
// if predecessorId is not provided, the subquery will be empty and thus no wallets are filtered out.
|
|
const wallets = await models.$queryRaw`
|
|
SELECT
|
|
"Wallet".*,
|
|
jsonb_build_object(
|
|
'id', "users"."id",
|
|
'hideInvoiceDesc', "users"."hideInvoiceDesc"
|
|
) AS "user"
|
|
FROM "Wallet"
|
|
JOIN "users" ON "users"."id" = "Wallet"."userId"
|
|
WHERE
|
|
"Wallet"."userId" = ${userId}
|
|
AND "Wallet"."enabled" = true
|
|
AND "Wallet"."id" NOT IN (
|
|
WITH RECURSIVE "Retries" AS (
|
|
-- select the current failed invoice that we are currently retrying
|
|
-- this failed invoice will be used to start the recursion
|
|
SELECT "Invoice"."id", "Invoice"."predecessorId"
|
|
FROM "Invoice"
|
|
WHERE "Invoice"."id" = ${predecessorId} AND "Invoice"."actionState" = 'FAILED'
|
|
|
|
UNION ALL
|
|
|
|
-- recursive part: use predecessorId to select the previous invoice that failed in the chain
|
|
-- until there is no more previous invoice
|
|
SELECT "Invoice"."id", "Invoice"."predecessorId"
|
|
FROM "Invoice"
|
|
JOIN "Retries" ON "Invoice"."id" = "Retries"."predecessorId"
|
|
WHERE "Invoice"."actionState" = 'RETRYING'
|
|
)
|
|
SELECT
|
|
"InvoiceForward"."walletId"
|
|
FROM "Retries"
|
|
JOIN "InvoiceForward" ON "InvoiceForward"."invoiceId" = "Retries"."id"
|
|
WHERE "InvoiceForward"."withdrawlId" IS NOT NULL
|
|
)
|
|
ORDER BY "Wallet"."priority" ASC, "Wallet"."id" ASC`
|
|
|
|
const walletsWithDefs = wallets.map(wallet => {
|
|
const w = walletDefs.find(w => w.walletType === wallet.type)
|
|
return { wallet, def: w }
|
|
})
|
|
|
|
return walletsWithDefs.filter(({ def, wallet }) => canReceive({ def, config: wallet.wallet }))
|
|
}
|
|
|
|
async function walletCreateInvoice ({ wallet, def }, {
|
|
msats,
|
|
description,
|
|
descriptionHash,
|
|
expiry = 360
|
|
}, { logger, models }) {
|
|
// 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(
|
|
def.createInvoice(
|
|
{
|
|
msats,
|
|
description: wallet.user.hideInvoiceDesc ? undefined : description,
|
|
descriptionHash,
|
|
expiry
|
|
},
|
|
wallet.wallet,
|
|
{ logger }
|
|
), 10_000)
|
|
}
|