Skip to content

Commit

Permalink
Fix autowithdrawal logs (#1073)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ekzyis committed Apr 16, 2024
1 parent c19c912 commit e30dfba
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 59 deletions.
53 changes: 22 additions & 31 deletions api/resolvers/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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
}
}
Expand All @@ -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({
Expand All @@ -488,27 +486,26 @@ 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
}
}
},
{ 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
}
},
Expand All @@ -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
Expand Down Expand Up @@ -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' } })
}
Expand All @@ -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' } })
}
}
Expand Down Expand Up @@ -623,6 +610,9 @@ async function upsertWallet (
}))
}

const walletName = walletType === 'LND'
? 'walletLND'
: walletType === 'CLN' ? 'walletCLN' : 'walletLightningAddress'
if (id) {
txs.push(
models.wallet.update({
Expand All @@ -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(
Expand All @@ -651,15 +641,15 @@ 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' } })
)
}

await models.$transaction(txs)
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 })
Expand Down Expand Up @@ -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 }
)

Expand Down
14 changes: 11 additions & 3 deletions components/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
$$;
3 changes: 3 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ model Wallet {
walletLightningAddress WalletLightningAddress?
walletLND WalletLND?
walletCLN WalletCLN?
withdrawals Withdrawl[]
@@index([userId])
}
Expand Down Expand Up @@ -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")
Expand Down
28 changes: 5 additions & 23 deletions worker/autowithdraw.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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 })
Expand Down Expand Up @@ -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 }) {
Expand Down Expand Up @@ -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 }) {
Expand Down Expand Up @@ -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 })
}
24 changes: 22 additions & 2 deletions worker/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 <<<
Expand Down Expand Up @@ -237,23 +239,41 @@ 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(
models.$executeRaw`
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 } })
}
}
}

Expand Down

0 comments on commit e30dfba

Please sign in to comment.