Fix autowithdrawal logs (#1073)
* Also log autowithdrawal routing errors * Only log autowithdrawal success in worker * Use WalletType for WalletLog.wallet * Fix autowithdrawal success message * Infer walletName from walletType in upsertWallet
This commit is contained in:
parent
c19c9124ec
commit
e30dfbae57
|
@ -437,12 +437,11 @@ export default {
|
|||
data.macaroon = ensureB64(data.macaroon)
|
||||
data.cert = ensureB64(data.cert)
|
||||
|
||||
const wallet = 'walletLND'
|
||||
const walletType = 'LND'
|
||||
return await upsertWallet(
|
||||
{
|
||||
schema: LNDAutowithdrawSchema,
|
||||
walletName: wallet,
|
||||
walletType: 'LND',
|
||||
walletType,
|
||||
testConnect: async ({ cert, macaroon, socket }) => {
|
||||
try {
|
||||
const { lnd } = await authenticatedLndGrpc({
|
||||
|
@ -457,12 +456,12 @@ export default {
|
|||
expires_at: new Date()
|
||||
})
|
||||
// we wrap both calls in one try/catch since connection attempts happen on RPC calls
|
||||
await addWalletLog({ wallet, level: 'SUCCESS', message: 'connected to LND' }, { me, models })
|
||||
await addWalletLog({ wallet: walletType, level: 'SUCCESS', message: 'connected to LND' }, { me, models })
|
||||
return inv
|
||||
} catch (err) {
|
||||
// LND errors are in this shape: [code, type, { err: { code, details, metadata } }]
|
||||
const details = err[2]?.err?.details || err.message || err.toString?.()
|
||||
await addWalletLog({ wallet, level: 'ERROR', message: `could not connect to LND: ${details}` }, { me, models })
|
||||
await addWalletLog({ wallet: walletType, level: 'ERROR', message: `could not connect to LND: ${details}` }, { me, models })
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
@ -472,12 +471,11 @@ export default {
|
|||
upsertWalletCLN: async (parent, { settings, ...data }, { me, models }) => {
|
||||
data.cert = ensureB64(data.cert)
|
||||
|
||||
const wallet = 'walletCLN'
|
||||
const walletType = 'CLN'
|
||||
return await upsertWallet(
|
||||
{
|
||||
schema: CLNAutowithdrawSchema,
|
||||
walletName: wallet,
|
||||
walletType: 'CLN',
|
||||
walletType,
|
||||
testConnect: async ({ socket, rune, cert }) => {
|
||||
try {
|
||||
const inv = await createInvoiceCLN({
|
||||
|
@ -488,11 +486,11 @@ export default {
|
|||
msats: 'any',
|
||||
expiry: 0
|
||||
})
|
||||
await addWalletLog({ wallet, level: 'SUCCESS', message: 'connected to CLN' }, { me, models })
|
||||
await addWalletLog({ wallet: walletType, level: 'SUCCESS', message: 'connected to CLN' }, { me, models })
|
||||
return inv
|
||||
} catch (err) {
|
||||
const details = err.details || err.message || err.toString?.()
|
||||
await addWalletLog({ wallet, level: 'ERROR', message: `could not connect to CLN: ${details}` }, { me, models })
|
||||
await addWalletLog({ wallet: walletType, level: 'ERROR', message: `could not connect to CLN: ${details}` }, { me, models })
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
@ -500,15 +498,14 @@ export default {
|
|||
{ settings, data }, { me, models })
|
||||
},
|
||||
upsertWalletLNAddr: async (parent, { settings, ...data }, { me, models }) => {
|
||||
const wallet = 'walletLightningAddress'
|
||||
const walletType = 'LIGHTNING_ADDRESS'
|
||||
return await upsertWallet(
|
||||
{
|
||||
schema: lnAddrAutowithdrawSchema,
|
||||
walletName: wallet,
|
||||
walletType: 'LIGHTNING_ADDRESS',
|
||||
walletType,
|
||||
testConnect: async ({ address }) => {
|
||||
const options = await lnAddrOptions(address)
|
||||
await addWalletLog({ wallet, level: 'SUCCESS', message: 'fetched payment details' }, { me, models })
|
||||
await addWalletLog({ wallet: walletType, level: 'SUCCESS', message: 'fetched payment details' }, { me, models })
|
||||
return options
|
||||
}
|
||||
},
|
||||
|
@ -524,19 +521,9 @@ export default {
|
|||
throw new GraphQLError('wallet not found', { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
|
||||
// determine wallet name for logging
|
||||
let walletName = ''
|
||||
if (wallet.type === 'LND') {
|
||||
walletName = 'walletLND'
|
||||
} else if (wallet.type === 'CLN') {
|
||||
walletName = 'walletCLN'
|
||||
} else if (wallet.type === 'LIGHTNING_ADDRESS') {
|
||||
walletName = 'walletLightningAddress'
|
||||
}
|
||||
|
||||
await models.$transaction([
|
||||
models.wallet.delete({ where: { userId: me.id, id: Number(id) } }),
|
||||
models.walletLog.create({ data: { userId: me.id, wallet: walletName, level: 'SUCCESS', message: 'wallet deleted' } })
|
||||
models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'SUCCESS', message: 'wallet deleted' } })
|
||||
])
|
||||
|
||||
return true
|
||||
|
@ -580,7 +567,7 @@ export const addWalletLog = async ({ wallet, level, message }, { me, models }) =
|
|||
}
|
||||
|
||||
async function upsertWallet (
|
||||
{ schema, walletName, walletType, testConnect }, { settings, data }, { me, models }) {
|
||||
{ schema, walletType, testConnect }, { settings, data }, { me, models }) {
|
||||
if (!me) {
|
||||
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
|
||||
}
|
||||
|
@ -593,7 +580,7 @@ async function upsertWallet (
|
|||
await testConnect(data)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
await addWalletLog({ wallet: walletName, level: 'ERROR', message: 'failed to attach wallet' }, { me, models })
|
||||
await addWalletLog({ wallet: walletType, level: 'ERROR', message: 'failed to attach wallet' }, { me, models })
|
||||
throw new GraphQLError('failed to connect to wallet', { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
}
|
||||
|
@ -623,6 +610,9 @@ async function upsertWallet (
|
|||
}))
|
||||
}
|
||||
|
||||
const walletName = walletType === 'LND'
|
||||
? 'walletLND'
|
||||
: walletType === 'CLN' ? 'walletCLN' : 'walletLightningAddress'
|
||||
if (id) {
|
||||
txs.push(
|
||||
models.wallet.update({
|
||||
|
@ -637,7 +627,7 @@ async function upsertWallet (
|
|||
}
|
||||
}
|
||||
}),
|
||||
models.walletLog.create({ data: { userId: me.id, wallet: walletName, level: 'SUCCESS', message: 'wallet updated' } })
|
||||
models.walletLog.create({ data: { userId: me.id, wallet: walletType, level: 'SUCCESS', message: 'wallet updated' } })
|
||||
)
|
||||
} else {
|
||||
txs.push(
|
||||
|
@ -651,7 +641,7 @@ async function upsertWallet (
|
|||
}
|
||||
}
|
||||
}),
|
||||
models.walletLog.create({ data: { userId: me.id, wallet: walletName, level: 'SUCCESS', message: 'wallet created' } })
|
||||
models.walletLog.create({ data: { userId: me.id, wallet: walletType, level: 'SUCCESS', message: 'wallet created' } })
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -659,7 +649,7 @@ async function upsertWallet (
|
|||
return true
|
||||
}
|
||||
|
||||
export async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd, headers, autoWithdraw = false }) {
|
||||
export async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd, headers, walletId = null }) {
|
||||
assertApiKeyNotPermitted({ me })
|
||||
await ssValidate(withdrawlSchema, { invoice, maxFee })
|
||||
await assertGofacYourself({ models, headers })
|
||||
|
@ -698,10 +688,11 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model
|
|||
|
||||
const user = await models.user.findUnique({ where: { id: me.id } })
|
||||
|
||||
const autoWithdraw = !!walletId
|
||||
// create withdrawl transactionally (id, bolt11, amount, fee)
|
||||
const [withdrawl] = await serialize(
|
||||
models.$queryRaw`SELECT * FROM create_withdrawl(${decoded.id}, ${invoice},
|
||||
${Number(decoded.mtokens)}, ${msatsFee}, ${user.name}, ${autoWithdraw})`,
|
||||
${Number(decoded.mtokens)}, ${msatsFee}, ${user.name}, ${autoWithdraw}, ${walletId}::INTEGER)`,
|
||||
{ models }
|
||||
)
|
||||
|
||||
|
|
|
@ -158,9 +158,17 @@ const initIndexedDB = async (storeName) => {
|
|||
}
|
||||
|
||||
const renameWallet = (wallet) => {
|
||||
if (wallet === 'walletLightningAddress') return 'lnAddr'
|
||||
if (wallet === 'walletLND') return 'lnd'
|
||||
if (wallet === 'walletCLN') return 'cln'
|
||||
switch (wallet) {
|
||||
case 'walletLightningAddress':
|
||||
case 'LIGHTNING_ADDRESS':
|
||||
return 'lnAddr'
|
||||
case 'walletLND':
|
||||
case 'LND':
|
||||
return 'lnd'
|
||||
case 'walletCLN':
|
||||
case 'CLN':
|
||||
return 'cln'
|
||||
}
|
||||
return wallet
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Withdrawl" ADD COLUMN "walletId" INTEGER;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Withdrawl" ADD CONSTRAINT "Withdrawl_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
CREATE OR REPLACE FUNCTION create_withdrawl(lnd_id TEXT, invoice TEXT, msats_amount BIGINT, msats_max_fee BIGINT, username TEXT, auto_withdraw BOOLEAN, wallet_id INTEGER)
|
||||
RETURNS "Withdrawl"
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
user_id INTEGER;
|
||||
user_msats BIGINT;
|
||||
withdrawl "Withdrawl";
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
|
||||
SELECT msats, id INTO user_msats, user_id FROM users WHERE name = username;
|
||||
IF (msats_amount + msats_max_fee) > user_msats THEN
|
||||
RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM "Withdrawl" WHERE hash = lnd_id AND status IS NULL) THEN
|
||||
RAISE EXCEPTION 'SN_PENDING_WITHDRAWL_EXISTS';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM "Withdrawl" WHERE hash = lnd_id AND status = 'CONFIRMED') THEN
|
||||
RAISE EXCEPTION 'SN_CONFIRMED_WITHDRAWL_EXISTS';
|
||||
END IF;
|
||||
|
||||
INSERT INTO "Withdrawl" (hash, bolt11, "msatsPaying", "msatsFeePaying", "userId", "autoWithdraw", "walletId", created_at, updated_at)
|
||||
VALUES (lnd_id, invoice, msats_amount, msats_max_fee, user_id, auto_withdraw, wallet_id, now_utc(), now_utc()) RETURNING * INTO withdrawl;
|
||||
|
||||
UPDATE users SET msats = msats - msats_amount - msats_max_fee WHERE id = user_id;
|
||||
|
||||
RETURN withdrawl;
|
||||
END;
|
||||
$$;
|
|
@ -156,6 +156,7 @@ model Wallet {
|
|||
walletLightningAddress WalletLightningAddress?
|
||||
walletLND WalletLND?
|
||||
walletCLN WalletCLN?
|
||||
withdrawals Withdrawl[]
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
@ -694,7 +695,9 @@ model Withdrawl {
|
|||
msatsFeePaid BigInt?
|
||||
status WithdrawlStatus?
|
||||
autoWithdraw Boolean @default(false)
|
||||
walletId Int?
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
wallet Wallet? @relation(fields: [walletId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([createdAt], map: "Withdrawl.created_at_index")
|
||||
@@index([userId], map: "Withdrawl.userId_index")
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { authenticatedLndGrpc, createInvoice } from 'ln-service'
|
||||
import { msatsToSats, numWithUnits, satsToMsats } from '@/lib/format'
|
||||
import { msatsToSats, satsToMsats } from '@/lib/format'
|
||||
import { datePivot } from '@/lib/time'
|
||||
import { createWithdrawal, sendToLnAddr, addWalletLog } from '@/api/resolvers/wallet'
|
||||
import { createInvoice as createInvoiceCLN } from '@/lib/cln'
|
||||
|
@ -46,34 +46,18 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) {
|
|||
|
||||
for (const wallet of wallets) {
|
||||
try {
|
||||
const message = `autowithdrawal of ${numWithUnits(amount, { abbreviate: false, unitSingular: 'sat', unitPlural: 'sats' })}`
|
||||
if (wallet.type === 'LND') {
|
||||
await autowithdrawLND(
|
||||
{ amount, maxFee },
|
||||
{ models, me: user, lnd })
|
||||
await addWalletLog({
|
||||
wallet: 'walletLND',
|
||||
level: 'SUCCESS',
|
||||
message
|
||||
}, { me: user, models })
|
||||
} else if (wallet.type === 'CLN') {
|
||||
await autowithdrawCLN(
|
||||
{ amount, maxFee },
|
||||
{ models, me: user, lnd })
|
||||
await addWalletLog({
|
||||
wallet: 'walletCLN',
|
||||
level: 'SUCCESS',
|
||||
message
|
||||
}, { me: user, models })
|
||||
} else if (wallet.type === 'LIGHTNING_ADDRESS') {
|
||||
await autowithdrawLNAddr(
|
||||
{ amount, maxFee },
|
||||
{ models, me: user, lnd })
|
||||
await addWalletLog({
|
||||
wallet: 'walletLightningAddress',
|
||||
level: 'SUCCESS',
|
||||
message
|
||||
}, { me: user, models })
|
||||
}
|
||||
|
||||
return
|
||||
|
@ -82,9 +66,7 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) {
|
|||
// LND errors are in this shape: [code, type, { err: { code, details, metadata } }]
|
||||
const details = error[2]?.err?.details || error.message || error.toString?.()
|
||||
await addWalletLog({
|
||||
wallet: wallet.type === 'LND'
|
||||
? 'walletLND'
|
||||
: wallet.type === 'CLN' ? 'walletCLN' : 'walletLightningAddress',
|
||||
wallet: wallet.type,
|
||||
level: 'ERROR',
|
||||
message: 'autowithdrawal failed: ' + details
|
||||
}, { me: user, models })
|
||||
|
@ -116,7 +98,7 @@ async function autowithdrawLNAddr (
|
|||
}
|
||||
|
||||
const { walletLightningAddress: { address } } = wallet
|
||||
return await sendToLnAddr(null, { addr: address, amount, maxFee }, { me, models, lnd, autoWithdraw: true })
|
||||
return await sendToLnAddr(null, { addr: address, amount, maxFee }, { me, models, lnd, walletId: wallet.id })
|
||||
}
|
||||
|
||||
async function autowithdrawLND ({ amount, maxFee }, { me, models, lnd }) {
|
||||
|
@ -152,7 +134,7 @@ async function autowithdrawLND ({ amount, maxFee }, { me, models, lnd }) {
|
|||
expires_at: datePivot(new Date(), { seconds: 360 })
|
||||
})
|
||||
|
||||
return await createWithdrawal(null, { invoice: invoice.request, maxFee }, { me, models, lnd, autoWithdraw: true })
|
||||
return await createWithdrawal(null, { invoice: invoice.request, maxFee }, { me, models, lnd, walletId: wallet.id })
|
||||
}
|
||||
|
||||
async function autowithdrawCLN ({ amount, maxFee }, { me, models, lnd }) {
|
||||
|
@ -185,5 +167,5 @@ async function autowithdrawCLN ({ amount, maxFee }, { me, models, lnd }) {
|
|||
expiry: 360
|
||||
})
|
||||
|
||||
return await createWithdrawal(null, { invoice: inv.bolt11, maxFee }, { me, models, lnd, autoWithdraw: true })
|
||||
return await createWithdrawal(null, { invoice: inv.bolt11, maxFee }, { me, models, lnd, walletId: wallet.id })
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ import { notifyDeposit, notifyWithdrawal } from '@/lib/webPush'
|
|||
import { INVOICE_RETENTION_DAYS } from '@/lib/constants'
|
||||
import { datePivot, sleep } from '@/lib/time.js'
|
||||
import retry from 'async-retry'
|
||||
import { addWalletLog } from '@/api/resolvers/wallet'
|
||||
import { msatsToSats, numWithUnits } from '@/lib/format'
|
||||
|
||||
export async function subscribeToWallet (args) {
|
||||
await subscribeToDeposits(args)
|
||||
|
@ -205,7 +207,7 @@ async function subscribeToWithdrawals (args) {
|
|||
}
|
||||
|
||||
async function checkWithdrawal ({ data: { hash }, boss, models, lnd }) {
|
||||
const dbWdrwl = await models.withdrawl.findFirst({ where: { hash, status: null } })
|
||||
const dbWdrwl = await models.withdrawl.findFirst({ where: { hash, status: null }, include: { wallet: true } })
|
||||
if (!dbWdrwl) {
|
||||
// [WARNING] LND paid an invoice that wasn't created via the SN GraphQL API.
|
||||
// >>> an adversary might be draining our funds right now <<<
|
||||
|
@ -237,16 +239,25 @@ async function checkWithdrawal ({ data: { hash }, boss, models, lnd }) {
|
|||
if (code === 0) {
|
||||
notifyWithdrawal(dbWdrwl.userId, wdrwl)
|
||||
}
|
||||
if (dbWdrwl.wallet) {
|
||||
// this was an autowithdrawal
|
||||
const message = `autowithdrawal of ${numWithUnits(msatsToSats(paid), { abbreviate: false })} with ${numWithUnits(msatsToSats(fee), { abbreviate: false })} as fee`
|
||||
await addWalletLog({ wallet: dbWdrwl.wallet.type, level: 'SUCCESS', message }, { models, me: { id: dbWdrwl.userId } })
|
||||
}
|
||||
} else if (wdrwl?.is_failed || notFound) {
|
||||
let status = 'UNKNOWN_FAILURE'
|
||||
let status = 'UNKNOWN_FAILURE'; let message = 'unknown failure'
|
||||
if (wdrwl?.failed.is_insufficient_balance) {
|
||||
status = 'INSUFFICIENT_BALANCE'
|
||||
message = "you didn't have enough sats"
|
||||
} else if (wdrwl?.failed.is_invalid_payment) {
|
||||
status = 'INVALID_PAYMENT'
|
||||
message = 'invalid payment'
|
||||
} else if (wdrwl?.failed.is_pathfinding_timeout) {
|
||||
status = 'PATHFINDING_TIMEOUT'
|
||||
message = 'no route found'
|
||||
} else if (wdrwl?.failed.is_route_not_found) {
|
||||
status = 'ROUTE_NOT_FOUND'
|
||||
message = 'no route found'
|
||||
}
|
||||
|
||||
await serialize(
|
||||
|
@ -254,6 +265,15 @@ async function checkWithdrawal ({ data: { hash }, boss, models, lnd }) {
|
|||
SELECT reverse_withdrawl(${dbWdrwl.id}::INTEGER, ${status}::"WithdrawlStatus")`,
|
||||
{ models }
|
||||
)
|
||||
|
||||
if (dbWdrwl.wallet) {
|
||||
// add error into log for autowithdrawal
|
||||
addWalletLog({
|
||||
wallet: dbWdrwl.wallet.type,
|
||||
level: 'ERROR',
|
||||
message: 'autowithdrawal failed: ' + message
|
||||
}, { models, me: { id: dbWdrwl.userId } })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue