2024-02-13 19:17:56 +00:00
|
|
|
import { authenticatedLndGrpc, createInvoice } from 'ln-service'
|
2024-04-16 18:59:46 +00:00
|
|
|
import { msatsToSats, satsToMsats } from '@/lib/format'
|
2024-03-20 00:37:31 +00:00
|
|
|
import { datePivot } from '@/lib/time'
|
Wallet Logs (#994)
* nwc wallet logs
* persist logs in IndexedDB
* Potential fix for empty error message
* load logs limited to 5m ago from IDB
* load logs from past via query param
* Add 5m, 1h, 6h links for earlier logs
* Show end of log
* Clamp to logStart
* Add log.module.css
* Remove TODO about persistence
* Use table for logs
* <table> fixes bad format with fixed width and message overflow into start of next row
* also using ---start of log--- instead of ---end of log--- now
* removed time string in header nav
* Rename .header to .logNav
* Simply load all logs and remove navigation
I realized the code for navigation was most likely premature optimization which even resulted in worse UX:
Using the buttons to load logs from 5m, 1h, 6h ago sometimes meant that nothing happened at all since there were no logs from 5m, 1h, 6h ago.
That's why I added a time string as "start of logs" so it's at least visible that it changed but that looked bad so I removed it.
But all of this was not necessary: I can simply load all logs at once and then the user can scroll around however they like.
I was worried that it would be bad for performance to load all logs at once since we might store a lot of logs but as mentioned, that's probably premature optimization.
WHEN a lot of logs are stored AND this becomes a problem (What problem even? Slow page load?), THEN we can think about this.
If page load ever becomes slow because of loading logs, we could probably simply not load the logs at page load but only when /wallet/logs is visited.
But for now, this works fine.
* Add follow checkbox
* Create WalletLogs component
* Embed wallet logs
* Remove test error
* Fix level padding
* Add LNbits logs
* Add logs for attaching LND and lnAddr
* Use err.message || err.toString?.() consistently
* Autowithdrawal logs
* Use details from LND error
* Don't log test invoice individually
* Also refetch logs on error
* Remove obsolete and annoying toasts
* Replace scrollIntoView with scroll
* Use constant embedded max-height
* Fix missing width: 100% for embedded logs
* Show full payment hash and preimage in logs
* Also parse details from LND errors on autowithdrawal failures
* Remove TODO
* Fix accidental removal of wss:// check
* Fix alignment of start marker and show empty if empty
* Fix sendPayment loop
* Split context in two
2024-04-03 22:27:21 +00:00
|
|
|
import { createWithdrawal, sendToLnAddr, addWalletLog } from '@/api/resolvers/wallet'
|
2024-04-14 22:34:21 +00:00
|
|
|
import { createInvoice as createInvoiceCLN } from '@/lib/cln'
|
2024-05-03 19:14:33 +00:00
|
|
|
import { Wallet } from '@/lib/constants'
|
2024-02-13 19:17:56 +00:00
|
|
|
|
|
|
|
export async function autoWithdraw ({ data: { id }, models, lnd }) {
|
|
|
|
const user = await models.user.findUnique({ where: { id } })
|
|
|
|
if (user.autoWithdrawThreshold === null || user.autoWithdrawMaxFeePercent === null) return
|
|
|
|
|
|
|
|
const threshold = satsToMsats(user.autoWithdrawThreshold)
|
|
|
|
const excess = Number(user.msats - threshold)
|
|
|
|
|
|
|
|
// excess must be greater than 10% of threshold
|
|
|
|
if (excess < Number(threshold) * 0.1) return
|
|
|
|
|
|
|
|
const maxFee = msatsToSats(Math.ceil(excess * (user.autoWithdrawMaxFeePercent / 100.0)))
|
|
|
|
const amount = msatsToSats(excess) - maxFee
|
|
|
|
|
|
|
|
// must be >= 1 sat
|
|
|
|
if (amount < 1) return
|
|
|
|
|
|
|
|
// check that
|
|
|
|
// 1. the user doesn't have an autowithdraw pending
|
|
|
|
// 2. we have not already attempted to autowithdraw this fee recently
|
|
|
|
const [pendingOrFailed] = await models.$queryRaw`
|
|
|
|
SELECT EXISTS(
|
|
|
|
SELECT *
|
|
|
|
FROM "Withdrawl"
|
|
|
|
WHERE "userId" = ${id} AND "autoWithdraw"
|
|
|
|
AND (status IS NULL
|
|
|
|
OR (
|
|
|
|
status <> 'CONFIRMED' AND
|
|
|
|
now() < created_at + interval '1 hour' AND
|
|
|
|
"msatsFeePaying" >= ${satsToMsats(maxFee)}
|
|
|
|
))
|
|
|
|
)`
|
|
|
|
|
|
|
|
if (pendingOrFailed.exists) return
|
|
|
|
|
|
|
|
// get the wallets in order of priority
|
|
|
|
const wallets = await models.wallet.findMany({
|
|
|
|
where: { userId: user.id },
|
|
|
|
orderBy: { priority: 'desc' }
|
|
|
|
})
|
|
|
|
|
|
|
|
for (const wallet of wallets) {
|
|
|
|
try {
|
2024-05-03 19:14:33 +00:00
|
|
|
if (wallet.type === Wallet.LND.type) {
|
2024-02-13 19:17:56 +00:00
|
|
|
await autowithdrawLND(
|
|
|
|
{ amount, maxFee },
|
|
|
|
{ models, me: user, lnd })
|
2024-05-03 19:14:33 +00:00
|
|
|
} else if (wallet.type === Wallet.CLN.type) {
|
2024-04-14 22:34:21 +00:00
|
|
|
await autowithdrawCLN(
|
|
|
|
{ amount, maxFee },
|
|
|
|
{ models, me: user, lnd })
|
2024-05-03 19:14:33 +00:00
|
|
|
} else if (wallet.type === Wallet.LnAddr.type) {
|
2024-02-13 19:17:56 +00:00
|
|
|
await autowithdrawLNAddr(
|
|
|
|
{ amount, maxFee },
|
|
|
|
{ models, me: user, lnd })
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error)
|
Wallet Logs (#994)
* nwc wallet logs
* persist logs in IndexedDB
* Potential fix for empty error message
* load logs limited to 5m ago from IDB
* load logs from past via query param
* Add 5m, 1h, 6h links for earlier logs
* Show end of log
* Clamp to logStart
* Add log.module.css
* Remove TODO about persistence
* Use table for logs
* <table> fixes bad format with fixed width and message overflow into start of next row
* also using ---start of log--- instead of ---end of log--- now
* removed time string in header nav
* Rename .header to .logNav
* Simply load all logs and remove navigation
I realized the code for navigation was most likely premature optimization which even resulted in worse UX:
Using the buttons to load logs from 5m, 1h, 6h ago sometimes meant that nothing happened at all since there were no logs from 5m, 1h, 6h ago.
That's why I added a time string as "start of logs" so it's at least visible that it changed but that looked bad so I removed it.
But all of this was not necessary: I can simply load all logs at once and then the user can scroll around however they like.
I was worried that it would be bad for performance to load all logs at once since we might store a lot of logs but as mentioned, that's probably premature optimization.
WHEN a lot of logs are stored AND this becomes a problem (What problem even? Slow page load?), THEN we can think about this.
If page load ever becomes slow because of loading logs, we could probably simply not load the logs at page load but only when /wallet/logs is visited.
But for now, this works fine.
* Add follow checkbox
* Create WalletLogs component
* Embed wallet logs
* Remove test error
* Fix level padding
* Add LNbits logs
* Add logs for attaching LND and lnAddr
* Use err.message || err.toString?.() consistently
* Autowithdrawal logs
* Use details from LND error
* Don't log test invoice individually
* Also refetch logs on error
* Remove obsolete and annoying toasts
* Replace scrollIntoView with scroll
* Use constant embedded max-height
* Fix missing width: 100% for embedded logs
* Show full payment hash and preimage in logs
* Also parse details from LND errors on autowithdrawal failures
* Remove TODO
* Fix accidental removal of wss:// check
* Fix alignment of start marker and show empty if empty
* Fix sendPayment loop
* Split context in two
2024-04-03 22:27:21 +00:00
|
|
|
// LND errors are in this shape: [code, type, { err: { code, details, metadata } }]
|
|
|
|
const details = error[2]?.err?.details || error.message || error.toString?.()
|
|
|
|
await addWalletLog({
|
2024-05-03 19:14:33 +00:00
|
|
|
wallet,
|
Wallet Logs (#994)
* nwc wallet logs
* persist logs in IndexedDB
* Potential fix for empty error message
* load logs limited to 5m ago from IDB
* load logs from past via query param
* Add 5m, 1h, 6h links for earlier logs
* Show end of log
* Clamp to logStart
* Add log.module.css
* Remove TODO about persistence
* Use table for logs
* <table> fixes bad format with fixed width and message overflow into start of next row
* also using ---start of log--- instead of ---end of log--- now
* removed time string in header nav
* Rename .header to .logNav
* Simply load all logs and remove navigation
I realized the code for navigation was most likely premature optimization which even resulted in worse UX:
Using the buttons to load logs from 5m, 1h, 6h ago sometimes meant that nothing happened at all since there were no logs from 5m, 1h, 6h ago.
That's why I added a time string as "start of logs" so it's at least visible that it changed but that looked bad so I removed it.
But all of this was not necessary: I can simply load all logs at once and then the user can scroll around however they like.
I was worried that it would be bad for performance to load all logs at once since we might store a lot of logs but as mentioned, that's probably premature optimization.
WHEN a lot of logs are stored AND this becomes a problem (What problem even? Slow page load?), THEN we can think about this.
If page load ever becomes slow because of loading logs, we could probably simply not load the logs at page load but only when /wallet/logs is visited.
But for now, this works fine.
* Add follow checkbox
* Create WalletLogs component
* Embed wallet logs
* Remove test error
* Fix level padding
* Add LNbits logs
* Add logs for attaching LND and lnAddr
* Use err.message || err.toString?.() consistently
* Autowithdrawal logs
* Use details from LND error
* Don't log test invoice individually
* Also refetch logs on error
* Remove obsolete and annoying toasts
* Replace scrollIntoView with scroll
* Use constant embedded max-height
* Fix missing width: 100% for embedded logs
* Show full payment hash and preimage in logs
* Also parse details from LND errors on autowithdrawal failures
* Remove TODO
* Fix accidental removal of wss:// check
* Fix alignment of start marker and show empty if empty
* Fix sendPayment loop
* Split context in two
2024-04-03 22:27:21 +00:00
|
|
|
level: 'ERROR',
|
|
|
|
message: 'autowithdrawal failed: ' + details
|
2024-04-15 14:17:37 +00:00
|
|
|
}, { me: user, models })
|
2024-02-13 19:17:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// none of the wallets worked
|
|
|
|
}
|
|
|
|
|
|
|
|
async function autowithdrawLNAddr (
|
|
|
|
{ amount, maxFee },
|
|
|
|
{ me, models, lnd, headers, autoWithdraw = false }) {
|
|
|
|
if (!me) {
|
|
|
|
throw new Error('me not specified')
|
|
|
|
}
|
|
|
|
|
|
|
|
const wallet = await models.wallet.findFirst({
|
|
|
|
where: {
|
|
|
|
userId: me.id,
|
2024-05-03 19:14:33 +00:00
|
|
|
type: Wallet.LnAddr.type
|
2024-02-13 19:17:56 +00:00
|
|
|
},
|
|
|
|
include: {
|
|
|
|
walletLightningAddress: true
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!wallet || !wallet.walletLightningAddress) {
|
|
|
|
throw new Error('no lightning address wallet found')
|
|
|
|
}
|
|
|
|
|
2024-02-15 14:59:45 +00:00
|
|
|
const { walletLightningAddress: { address } } = wallet
|
2024-04-16 18:59:46 +00:00
|
|
|
return await sendToLnAddr(null, { addr: address, amount, maxFee }, { me, models, lnd, walletId: wallet.id })
|
2024-02-13 19:17:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function autowithdrawLND ({ amount, maxFee }, { me, models, lnd }) {
|
|
|
|
if (!me) {
|
|
|
|
throw new Error('me not specified')
|
|
|
|
}
|
|
|
|
|
|
|
|
const wallet = await models.wallet.findFirst({
|
|
|
|
where: {
|
|
|
|
userId: me.id,
|
2024-05-03 19:14:33 +00:00
|
|
|
type: Wallet.LND.type
|
2024-02-13 19:17:56 +00:00
|
|
|
},
|
|
|
|
include: {
|
|
|
|
walletLND: true
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!wallet || !wallet.walletLND) {
|
2024-04-10 13:59:42 +00:00
|
|
|
throw new Error('no lnd wallet found')
|
2024-02-13 19:17:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const { walletLND: { cert, macaroon, socket } } = wallet
|
|
|
|
const { lnd: lndOut } = await authenticatedLndGrpc({
|
|
|
|
cert,
|
|
|
|
macaroon,
|
|
|
|
socket
|
|
|
|
})
|
|
|
|
|
|
|
|
const invoice = await createInvoice({
|
|
|
|
description: me.hideInvoiceDesc ? undefined : 'autowithdraw to LND from SN',
|
|
|
|
lnd: lndOut,
|
|
|
|
tokens: amount,
|
|
|
|
expires_at: datePivot(new Date(), { seconds: 360 })
|
|
|
|
})
|
|
|
|
|
2024-04-16 18:59:46 +00:00
|
|
|
return await createWithdrawal(null, { invoice: invoice.request, maxFee }, { me, models, lnd, walletId: wallet.id })
|
2024-02-13 19:17:56 +00:00
|
|
|
}
|
2024-04-14 22:34:21 +00:00
|
|
|
|
|
|
|
async function autowithdrawCLN ({ amount, maxFee }, { me, models, lnd }) {
|
|
|
|
if (!me) {
|
|
|
|
throw new Error('me not specified')
|
|
|
|
}
|
|
|
|
|
|
|
|
const wallet = await models.wallet.findFirst({
|
|
|
|
where: {
|
|
|
|
userId: me.id,
|
2024-05-03 19:14:33 +00:00
|
|
|
type: Wallet.CLN.type
|
2024-04-14 22:34:21 +00:00
|
|
|
},
|
|
|
|
include: {
|
|
|
|
walletCLN: true
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!wallet || !wallet.walletCLN) {
|
|
|
|
throw new Error('no cln wallet found')
|
|
|
|
}
|
|
|
|
|
|
|
|
const { walletCLN: { cert, rune, socket } } = wallet
|
|
|
|
|
|
|
|
const inv = await createInvoiceCLN({
|
|
|
|
socket,
|
|
|
|
rune,
|
|
|
|
cert,
|
|
|
|
description: me.hideInvoiceDesc ? undefined : 'autowithdraw to CLN from SN',
|
|
|
|
msats: amount + 'sat',
|
|
|
|
expiry: 360
|
|
|
|
})
|
|
|
|
|
2024-04-16 18:59:46 +00:00
|
|
|
return await createWithdrawal(null, { invoice: inv.bolt11, maxFee }, { me, models, lnd, walletId: wallet.id })
|
2024-04-14 22:34:21 +00:00
|
|
|
}
|