Merge branch 'master' into blinkreceiver

This commit is contained in:
ekzyis 2024-10-22 21:21:56 +02:00
commit 455f6ab665
26 changed files with 160 additions and 76 deletions

3
.gitignore vendored
View File

@ -56,3 +56,6 @@ docker-compose.*.yml
# nostr wallet connect # nostr wallet connect
scripts/nwc-keys.json scripts/nwc-keys.json
# lnbits
docker/lnbits/data

View File

@ -2,11 +2,13 @@ import { cachedFetcher } from '@/lib/fetch'
import { toPositiveNumber } from '@/lib/validate' import { toPositiveNumber } from '@/lib/validate'
import { authenticatedLndGrpc, getIdentity, getHeight, getWalletInfo, getNode } from 'ln-service' import { authenticatedLndGrpc, getIdentity, getHeight, getWalletInfo, getNode } from 'ln-service'
const { lnd } = authenticatedLndGrpc({ const lnd = global.lnd || authenticatedLndGrpc({
cert: process.env.LND_CERT, cert: process.env.LND_CERT,
macaroon: process.env.LND_MACAROON, macaroon: process.env.LND_MACAROON,
socket: process.env.LND_SOCKET socket: process.env.LND_SOCKET
}) }).lnd
if (process.env.NODE_ENV === 'development') global.lnd = lnd
// Check LND GRPC connection // Check LND GRPC connection
getWalletInfo({ lnd }, (err, result) => { getWalletInfo({ lnd }, (err, result) => {
@ -19,19 +21,24 @@ getWalletInfo({ lnd }, (err, result) => {
export async function estimateRouteFee ({ lnd, destination, tokens, mtokens, request, timeout }) { export async function estimateRouteFee ({ lnd, destination, tokens, mtokens, request, timeout }) {
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
const params = {}
if (request) {
params.payment_request = request
} else {
params.dest = Buffer.from(destination, 'hex')
params.amt_sat = tokens ? toPositiveNumber(tokens) : toPositiveNumber(BigInt(mtokens) / BigInt(1e3))
}
lnd.router.estimateRouteFee({ lnd.router.estimateRouteFee({
dest: Buffer.from(destination, 'hex'), ...params,
amt_sat: tokens ? toPositiveNumber(tokens) : toPositiveNumber(BigInt(mtokens) / BigInt(1e3)),
payment_request: request,
timeout timeout
}, (err, res) => { }, (err, res) => {
if (err) { if (err) {
reject(err)
return
}
if (res?.failure_reason) { if (res?.failure_reason) {
reject(new Error(`Unable to estimate route: ${res.failure_reason}`)) reject(new Error(`Unable to estimate route: ${res.failure_reason}`))
} else {
reject(err)
}
return return
} }
@ -124,7 +131,7 @@ export const getBlockHeight = cachedFetcher(async function fetchBlockHeight ({ l
export const getOurPubkey = cachedFetcher(async function fetchOurPubkey ({ lnd, ...args }) { export const getOurPubkey = cachedFetcher(async function fetchOurPubkey ({ lnd, ...args }) {
try { try {
const { identity } = await getIdentity({ lnd, ...args }) const identity = await getIdentity({ lnd, ...args })
return identity.public_key return identity.public_key
} catch (err) { } catch (err) {
throw new Error(`Unable to fetch identity: ${err.message}`) throw new Error(`Unable to fetch identity: ${err.message}`)

View File

@ -260,7 +260,7 @@ export default {
const { name } = data const { name } = data
await ssValidate(territorySchema, data, { models, me, sub: { name } }) await ssValidate(territorySchema, data, { models, me })
const oldSub = await models.sub.findUnique({ where: { name } }) const oldSub = await models.sub.findUnique({ where: { name } })
if (!oldSub) { if (!oldSub) {

View File

@ -482,12 +482,12 @@ const resolvers = {
FROM "Withdrawl" FROM "Withdrawl"
WHERE "userId" = ${me.id} WHERE "userId" = ${me.id}
AND id = ${Number(id)} AND id = ${Number(id)}
AND now() > created_at + interval '${retention}' AND now() > created_at + ${retention}::INTERVAL
AND hash IS NOT NULL AND hash IS NOT NULL
AND status IS NOT NULL AND status IS NOT NULL
), updated_rows AS ( ), updated_rows AS (
UPDATE "Withdrawl" UPDATE "Withdrawl"
SET hash = NULL, bolt11 = NULL SET hash = NULL, bolt11 = NULL, preimage = NULL
FROM to_be_updated FROM to_be_updated
WHERE "Withdrawl".id = to_be_updated.id) WHERE "Withdrawl".id = to_be_updated.id)
SELECT * FROM to_be_updated;` SELECT * FROM to_be_updated;`
@ -499,7 +499,7 @@ const resolvers = {
console.error(error) console.error(error)
await models.withdrawl.update({ await models.withdrawl.update({
where: { id: invoice.id }, where: { id: invoice.id },
data: { hash: invoice.hash, bolt11: invoice.bolt11 } data: { hash: invoice.hash, bolt11: invoice.bolt11, preimage: invoice.preimage }
}) })
throw new GqlInputError('failed to drop bolt11 from lnd') throw new GqlInputError('failed to drop bolt11 from lnd')
} }
@ -647,14 +647,21 @@ async function upsertWallet (
} }
const { id, ...walletData } = data const { id, ...walletData } = data
const { autoWithdrawThreshold, autoWithdrawMaxFeePercent, enabled, priority } = settings const {
autoWithdrawThreshold,
autoWithdrawMaxFeePercent,
autoWithdrawMaxFeeTotal,
enabled,
priority
} = settings
const txs = [ const txs = [
models.user.update({ models.user.update({
where: { id: me.id }, where: { id: me.id },
data: { data: {
autoWithdrawMaxFeePercent, autoWithdrawMaxFeePercent,
autoWithdrawThreshold autoWithdrawThreshold,
autoWithdrawMaxFeeTotal
} }
}) })
] ]

View File

@ -182,6 +182,7 @@ export default gql`
withdrawMaxFeeDefault: Int! withdrawMaxFeeDefault: Int!
autoWithdrawThreshold: Int autoWithdrawThreshold: Int
autoWithdrawMaxFeePercent: Float autoWithdrawMaxFeePercent: Float
autoWithdrawMaxFeeTotal: Int
} }
type UserOptional { type UserOptional {

View File

@ -91,6 +91,7 @@ const typeDefs = `
input AutowithdrawSettings { input AutowithdrawSettings {
autoWithdrawThreshold: Int! autoWithdrawThreshold: Int!
autoWithdrawMaxFeePercent: Float! autoWithdrawMaxFeePercent: Float!
autoWithdrawMaxFeeTotal: Int!
priority: Int priority: Int
enabled: Boolean enabled: Boolean
} }

View File

@ -135,3 +135,5 @@ toyota-corolla0,pr,#1449,,good-first-issue,,,,20k,toyota_corolla0@stacker.news,2
toyota-corolla0,pr,#1455,#1437,good-first-issue,,,,20k,toyota_corolla0@stacker.news,2024-10-02 toyota-corolla0,pr,#1455,#1437,good-first-issue,,,,20k,toyota_corolla0@stacker.news,2024-10-02
SouthKoreaLN,issue,#1436,,easy,,,,10k,south_korea_ln@stacker.news,2024-10-02 SouthKoreaLN,issue,#1436,,easy,,,,10k,south_korea_ln@stacker.news,2024-10-02
TonyGiorgio,issue,#1462,,easy,urgent,,,30k,TonyGiorgio@stacker.news,2024-10-07 TonyGiorgio,issue,#1462,,easy,urgent,,,30k,TonyGiorgio@stacker.news,2024-10-07
hkarani,issue,#1369,#1458,good-first-issue,,,,2k,asterisk32@stacker.news,2024-10-21
toyota-corolla0,pr,#1369,#1458,good-first-issue,,,,20k,toyota_corolla0@stacker.news,2024-10-20

1 name type pr id issue ids difficulty priority changes requested notes amount receive method date paid
135 toyota-corolla0 pr #1455 #1437 good-first-issue 20k toyota_corolla0@stacker.news 2024-10-02
136 SouthKoreaLN issue #1436 easy 10k south_korea_ln@stacker.news 2024-10-02
137 TonyGiorgio issue #1462 easy urgent 30k TonyGiorgio@stacker.news 2024-10-07
138 hkarani issue #1369 #1458 good-first-issue 2k asterisk32@stacker.news 2024-10-21
139 toyota-corolla0 pr #1369 #1458 good-first-issue 20k toyota_corolla0@stacker.news 2024-10-20

View File

@ -4,6 +4,7 @@ import { useMe } from './me'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { isNumber } from '@/lib/validate' import { isNumber } from '@/lib/validate'
import { useIsClient } from './use-client' import { useIsClient } from './use-client'
import Link from 'next/link'
function autoWithdrawThreshold ({ me }) { function autoWithdrawThreshold ({ me }) {
return isNumber(me?.privates?.autoWithdrawThreshold) ? me?.privates?.autoWithdrawThreshold : 10000 return isNumber(me?.privates?.autoWithdrawThreshold) ? me?.privates?.autoWithdrawThreshold : 10000
@ -12,7 +13,8 @@ function autoWithdrawThreshold ({ me }) {
export function autowithdrawInitial ({ me }) { export function autowithdrawInitial ({ me }) {
return { return {
autoWithdrawThreshold: autoWithdrawThreshold({ me }), autoWithdrawThreshold: autoWithdrawThreshold({ me }),
autoWithdrawMaxFeePercent: isNumber(me?.privates?.autoWithdrawMaxFeePercent) ? me?.privates?.autoWithdrawMaxFeePercent : 1 autoWithdrawMaxFeePercent: isNumber(me?.privates?.autoWithdrawMaxFeePercent) ? me?.privates?.autoWithdrawMaxFeePercent : 1,
autoWithdrawMaxFeeTotal: isNumber(me?.privates?.autoWithdrawMaxFeeTotal) ? me?.privates?.autoWithdrawMaxFeeTotal : 1
} }
} }
@ -51,13 +53,30 @@ export function AutowithdrawSettings ({ wallet }) {
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>} append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
required required
/> />
<h3 className='text-center text-muted pt-3'>network fees</h3>
<h6 className='text-center pb-3'>
we'll use whichever setting is higher during{' '}
<Link
target='_blank'
href='https://docs.lightning.engineering/the-lightning-network/pathfinding'
rel='noreferrer'
>pathfinding
</Link>
</h6>
<Input <Input
label='max fee' label='max fee rate'
name='autoWithdrawMaxFeePercent' name='autoWithdrawMaxFeePercent'
hint='max fee as percent of withdrawal amount' hint='max fee as percent of withdrawal amount'
append={<InputGroup.Text>%</InputGroup.Text>} append={<InputGroup.Text>%</InputGroup.Text>}
required required
/> />
<Input
label='max fee total'
name='autoWithdrawMaxFeeTotal'
hint='max fee for any withdrawal amount'
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
required
/>
</div> </div>
</div> </div>
</> </>

View File

@ -12,10 +12,7 @@ import useIndexedDB from './use-indexeddb'
import { SSR } from '@/lib/constants' import { SSR } from '@/lib/constants'
export function WalletLogs ({ wallet, embedded }) { export function WalletLogs ({ wallet, embedded }) {
const { logs, setLogs, hasMore, loadMore, loadLogs, loading } = useWalletLogs(wallet) const { logs, setLogs, hasMore, loadMore, loading } = useWalletLogs(wallet)
useEffect(() => {
loadLogs()
}, [loadLogs])
const showModal = useShowModal() const showModal = useShowModal()
@ -46,7 +43,7 @@ export function WalletLogs ({ wallet, embedded }) {
? <div className='w-100 text-center'>loading...</div> ? <div className='w-100 text-center'>loading...</div>
: logs.length === 0 && <div className='w-100 text-center'>empty</div>} : logs.length === 0 && <div className='w-100 text-center'>empty</div>}
{hasMore {hasMore
? <Button onClick={loadMore} size='sm' className='mt-3'>Load More</Button> ? <div className='w-100 text-center'><Button onClick={loadMore} size='sm' className='mt-3'>more</Button></div>
: <div className='w-100 text-center'>------ start of logs ------</div>} : <div className='w-100 text-center'>------ start of logs ------</div>}
</div> </div>
</> </>
@ -228,7 +225,7 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
const loadMore = useCallback(async () => { const loadMore = useCallback(async () => {
if (hasMore) { if (hasMore) {
setLoading(true) setLoading(true)
const result = await loadLogsPage(page, logsPerPage, wallet) const result = await loadLogsPage(page + 1, logsPerPage, wallet)
setLogs(prevLogs => [...prevLogs, ...result.data]) setLogs(prevLogs => [...prevLogs, ...result.data])
setHasMore(result.hasMore) setHasMore(result.hasMore)
setTotal(result.total) setTotal(result.total)
@ -247,5 +244,9 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
setLoading(false) setLoading(false)
}, [wallet, loadLogsPage]) }, [wallet, loadLogsPage])
useEffect(() => {
loadLogs()
}, [wallet])
return { logs, hasMore, total, loadMore, loadLogs, setLogs, loading } return { logs, hasMore, total, loadMore, loadLogs, setLogs, loading }
} }

View File

@ -586,7 +586,8 @@ services:
- 'keys-file.json' - 'keys-file.json'
cpu_shares: "${CPU_SHARES_LOW}" cpu_shares: "${CPU_SHARES_LOW}"
lnbits: lnbits:
image: lnbits/lnbits:0.12.5 build:
context: ./docker/lnbits
container_name: lnbits container_name: lnbits
profiles: profiles:
- wallets - wallets
@ -596,6 +597,7 @@ services:
depends_on: depends_on:
- stacker_lnd - stacker_lnd
environment: environment:
- LNBITS_ADMIN_UI=true
- LNBITS_BACKEND_WALLET_CLASS=LndWallet - LNBITS_BACKEND_WALLET_CLASS=LndWallet
- LND_GRPC_ENDPOINT=stacker_lnd - LND_GRPC_ENDPOINT=stacker_lnd
- LND_GRPC_PORT=10009 - LND_GRPC_PORT=10009

5
docker/lnbits/Dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM lnbits/lnbits:0.12.5
COPY ["./data/database.sqlite3", "/app/data/database.sqlite3"]
COPY ["./data/.super_user", "/app/data/.super_user"]

View File

@ -0,0 +1 @@
e46288268b67457399a5fca81809573e

Binary file not shown.

View File

@ -1,4 +1,4 @@
FROM polarlightning/lnd:0.17.5-beta FROM polarlightning/lnd:0.18.0-beta
ARG LN_NODE_FOR ARG LN_NODE_FOR
ENV LN_NODE_FOR=$LN_NODE_FOR ENV LN_NODE_FOR=$LN_NODE_FOR

View File

@ -27,6 +27,7 @@ ${STREAK_FIELDS}
noReferralLinks noReferralLinks
fiatCurrency fiatCurrency
autoWithdrawMaxFeePercent autoWithdrawMaxFeePercent
autoWithdrawMaxFeeTotal
autoWithdrawThreshold autoWithdrawThreshold
withdrawMaxFeeDefault withdrawMaxFeeDefault
satsFilter satsFilter

View File

@ -26,6 +26,10 @@ class LRUCache {
return value return value
} }
delete (key) {
this.cache.delete(key)
}
set (key, value) { set (key, value) {
if (this.cache.has(key)) this.cache.delete(key) if (this.cache.has(key)) this.cache.delete(key)
else if (this.cache.size >= this.maxSize) { else if (this.cache.size >= this.maxSize) {

View File

@ -366,7 +366,8 @@ export function advSchema (args) {
export const autowithdrawSchemaMembers = { export const autowithdrawSchemaMembers = {
enabled: boolean(), enabled: boolean(),
autoWithdrawThreshold: intValidator.required('required').min(0, 'must be at least 0').max(msatsToSats(BALANCE_LIMIT_MSATS), `must be at most ${abbrNum(msatsToSats(BALANCE_LIMIT_MSATS))}`), autoWithdrawThreshold: intValidator.required('required').min(0, 'must be at least 0').max(msatsToSats(BALANCE_LIMIT_MSATS), `must be at most ${abbrNum(msatsToSats(BALANCE_LIMIT_MSATS))}`),
autoWithdrawMaxFeePercent: floatValidator.required('required').min(0, 'must be at least 0').max(50, 'must not exceed 50') autoWithdrawMaxFeePercent: floatValidator.required('required').min(0, 'must be at least 0').max(50, 'must not exceed 50'),
autoWithdrawMaxFeeTotal: intValidator.required('required').min(0, 'must be at least 0').max(1_000, 'must not exceed 1000')
} }
export const lnAddrAutowithdrawSchema = object({ export const lnAddrAutowithdrawSchema = object({
@ -726,7 +727,8 @@ export const lnbitsSchema = object().shape({
test: invoiceKey => adminKey !== invoiceKey, test: invoiceKey => adminKey !== invoiceKey,
message: 'invoice key cannot be the same as admin key' message: 'invoice key cannot be the same as admin key'
}) })
}) }),
...autowithdrawSchemaMembers
// need to set order to avoid cyclic dependencies in Yup schema // need to set order to avoid cyclic dependencies in Yup schema
// see https://github.com/jquense/yup/issues/176#issuecomment-367352042 // see https://github.com/jquense/yup/issues/176#issuecomment-367352042
}, ['adminKey', 'invoiceKey']) }, ['adminKey', 'invoiceKey'])
@ -745,7 +747,8 @@ export const nwcSchema = object().shape({
test: nwcUrlRecv => nwcUrlRecv !== nwcUrl, test: nwcUrlRecv => nwcUrlRecv !== nwcUrl,
message: 'connection for receiving cannot be the same as for sending' message: 'connection for receiving cannot be the same as for sending'
}) })
}) }),
...autowithdrawSchemaMembers
}, ['nwcUrl', 'nwcUrlRecv']) }, ['nwcUrl', 'nwcUrlRecv'])
export const blinkSchema = object().shape({ export const blinkSchema = object().shape({
@ -815,7 +818,8 @@ export const phoenixdSchema = object().shape({
test: secondary => primary !== secondary, test: secondary => primary !== secondary,
message: 'secondary password cannot be the same as primary password' message: 'secondary password cannot be the same as primary password'
}) })
}) }),
...autowithdrawSchemaMembers
}, ['primaryPassword', 'secondaryPassword']) }, ['primaryPassword', 'secondaryPassword'])
export const bioSchema = object({ export const bioSchema = object({

24
package-lock.json generated
View File

@ -7057,9 +7057,9 @@
} }
}, },
"node_modules/bitcore-lib": { "node_modules/bitcore-lib": {
"version": "8.25.40", "version": "8.25.47",
"resolved": "https://registry.npmjs.org/bitcore-lib/-/bitcore-lib-8.25.40.tgz", "resolved": "https://registry.npmjs.org/bitcore-lib/-/bitcore-lib-8.25.47.tgz",
"integrity": "sha512-mb6kvfhoiIdoyFsDlhIFVst3HpeGjGYBf0XDxTdZ+H07EC4JuiViA3bnQ5uZbZjHFngEl0GTPaoK1Zaolutw4A==", "integrity": "sha512-qDZr42HuP4P02I8kMGZUx/vvwuDsz8X3rQxXLfM0BtKzlQBcbSM7ycDkDN99Xc5jzpd4fxNQyyFXOmc6owUsrQ==",
"dependencies": { "dependencies": {
"bech32": "=2.0.0", "bech32": "=2.0.0",
"bip-schnorr": "=0.6.4", "bip-schnorr": "=0.6.4",
@ -15626,9 +15626,9 @@
"integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="
}, },
"node_modules/node-addon-api": { "node_modules/node-addon-api": {
"version": "2.0.2", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
}, },
"node_modules/node-fetch": { "node_modules/node-fetch": {
"version": "2.7.0", "version": "2.7.0",
@ -18038,17 +18038,17 @@
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==" "integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw=="
}, },
"node_modules/secp256k1": { "node_modules/secp256k1": {
"version": "4.0.3", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz", "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.4.tgz",
"integrity": "sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==", "integrity": "sha512-6JfvwvjUOn8F/jUoBY2Q1v5WY5XS+rj8qSe0v8Y4ezH4InLgTEeOOPQsRll9OV429Pvo6BCHGavIyJfr3TAhsw==",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"elliptic": "^6.5.4", "elliptic": "^6.5.7",
"node-addon-api": "^2.0.0", "node-addon-api": "^5.0.0",
"node-gyp-build": "^4.2.0" "node-gyp-build": "^4.2.0"
}, },
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=18.0.0"
} }
}, },
"node_modules/secure-json-parse": { "node_modules/secure-json-parse": {

View File

@ -0,0 +1,8 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "autoWithdrawMaxFeeTotal" INTEGER;
-- set max total fee for users with autowithdrawals enabled to not interfere with them.
-- we set it to 0 instead of 1 because that preserves old behavior.
UPDATE "users"
SET "autoWithdrawMaxFeeTotal" = 0
WHERE "autoWithdrawMaxFeePercent" IS NOT NULL;

View File

@ -118,6 +118,7 @@ model User {
lnAddr String? lnAddr String?
autoWithdrawMaxFeePercent Float? autoWithdrawMaxFeePercent Float?
autoWithdrawThreshold Int? autoWithdrawThreshold Int?
autoWithdrawMaxFeeTotal Int?
muters Mute[] @relation("muter") muters Mute[] @relation("muter")
muteds Mute[] @relation("muted") muteds Mute[] @relation("muted")
ArcOut Arc[] @relation("fromUser") ArcOut Arc[] @relation("fromUser")

View File

@ -125,7 +125,8 @@ function extractConfig (fields, config, client) {
const field = fields.find(({ name }) => name === key) const field = fields.find(({ name }) => name === key)
// filter server config which isn't specified as wallet fields // filter server config which isn't specified as wallet fields
if (client && (key.startsWith('autoWithdraw') || key === 'id')) return acc // (we allow autowithdraw members to pass validation)
if (client && key === 'id') return acc
// field might not exist because config.enabled doesn't map to a wallet field // field might not exist because config.enabled doesn't map to a wallet field
if (!field || (client ? isClientField(field) : isServerField(field))) { if (!field || (client ? isClientField(field) : isServerField(field))) {
@ -198,6 +199,10 @@ function useConfig (wallet) {
if (transformedConfig) { if (transformedConfig) {
newClientConfig = Object.assign(newClientConfig, transformedConfig) newClientConfig = Object.assign(newClientConfig, transformedConfig)
} }
// these are stored on the server
delete newClientConfig.autoWithdrawMaxFeePercent
delete newClientConfig.autoWithdrawThreshold
delete newClientConfig.autoWithdrawMaxFeeTotal
} catch { } catch {
valid = false valid = false
} }
@ -292,6 +297,7 @@ function useServerConfig (wallet) {
const saveConfig = useCallback(async ({ const saveConfig = useCallback(async ({
autoWithdrawThreshold, autoWithdrawThreshold,
autoWithdrawMaxFeePercent, autoWithdrawMaxFeePercent,
autoWithdrawMaxFeeTotal,
priority, priority,
enabled, enabled,
...config ...config
@ -306,6 +312,7 @@ function useServerConfig (wallet) {
settings: { settings: {
autoWithdrawThreshold: Number(autoWithdrawThreshold), autoWithdrawThreshold: Number(autoWithdrawThreshold),
autoWithdrawMaxFeePercent: Number(autoWithdrawMaxFeePercent), autoWithdrawMaxFeePercent: Number(autoWithdrawMaxFeePercent),
autoWithdrawMaxFeeTotal: Number(autoWithdrawMaxFeeTotal),
priority, priority,
enabled enabled
}, },

View File

@ -1,27 +1,24 @@
For testing LNbits, you need to create a LNbits account first via the web interface. LNbits' database is seeded with a superuser (see https://docs.lnbits.org/guide/admin_ui.html).
By default, you can access it at `localhost:5001` (see `LNBITS_WEB_PORT` in .env.development). The following credentials were used:
After you created a wallet, you should find the invoice and admin key under `Node URL, API keys and API docs`. - username: `stackernews`
- password: `stackernews`
> [!IMPORTANT] To get access to the superuser, you need to visit the admin UI:
>
> Since your browser is running on your host machine but the server is running inside a docker container, the server will not be able to reach LNbits with `localhost:5001` to create invoices. This makes it hard to test send+receive at the same time. http://localhost:5001/wallet?usr=e46288268b67457399a5fca81809573e
>
> For now, you need to patch the `_createInvoice` function in wallets/lnbits/server.js to always use `lnbits:5000` as the URL: After that, the cookies will be set to access this wallet:
>
> ```diff http://localhost:5001/wallet?&wal=15ffe06c74cc4082a91f528d016d9028
> diff --git a/wallets/lnbits/server.js b/wallets/lnbits/server.js
> index 39949775..e3605c45 100644 Or simply copy the keys from here:
> --- a/wallets/lnbits/server.js
> +++ b/wallets/lnbits/server.js * admin key: `640cc7b031eb427c891eeaa4d9c34180`
> @@ -11,6 +11,7 @@ async function _createInvoice ({ url, invoiceKey, amount, expiry }, { me }) {
> const memo = me.hideInvoiceDesc ? undefined : 'autowithdraw to LNbits from SN' * invoice key: `5deed7cd634e4306bb5e696f4a03cdac`
> const body = JSON.stringify({ amount, unit: 'sat', expiry, memo, out: false })
> ( These keys can be found under `Node URL, API keys and API docs`. )
> + url = 'http://lnbits:5000'
> const res = await fetch(url + path, { method: 'POST', headers, body }) To use the same URL to connect to LNbits in the browser and server during local development, `localhost:<port>` is mapped to `lnbits:5000` on the server.
> if (!res.ok) {
> const errBody = await res.json()
> ```
>

View File

@ -28,9 +28,14 @@ export async function createInvoice (
out: false out: false
}) })
const hostname = url.replace(/^https?:\/\//, '') let hostname = url.replace(/^https?:\/\//, '')
const agent = getAgent({ hostname }) const agent = getAgent({ hostname })
if (process.env.NODE_ENV !== 'production' && hostname.startsWith('localhost:')) {
// to make it possible to attach LNbits for receives during local dev
hostname = 'lnbits:5000'
}
const res = await fetch(`${agent.protocol}//${hostname}${path}`, { const res = await fetch(`${agent.protocol}//${hostname}${path}`, {
method: 'POST', method: 'POST',
headers, headers,

View File

@ -36,7 +36,7 @@ export default async function wrapInvoice (bolt11, { msats, description, descrip
throw new Error('Unable to decode invoice') throw new Error('Unable to decode invoice')
} }
console.log('invoice', inv.mtokens, inv.expires_at, inv.cltv_delta) console.log('invoice', inv.id, inv.mtokens, inv.expires_at, inv.cltv_delta, inv.destination)
// validate outgoing amount // validate outgoing amount
if (inv.mtokens) { if (inv.mtokens) {
@ -77,6 +77,8 @@ export default async function wrapInvoice (bolt11, { msats, description, descrip
case 49: case 49:
case 149: // trampoline routing case 149: // trampoline routing
case 151: // electrum trampoline routing case 151: // electrum trampoline routing
case 262:
case 263: // blinded paths
break break
default: default:
throw new Error(`Unsupported feature bit: ${f.bit}`) throw new Error(`Unsupported feature bit: ${f.bit}`)

View File

@ -4,7 +4,10 @@ import { createInvoice } from 'wallets/server'
export async function autoWithdraw ({ data: { id }, models, lnd }) { export async function autoWithdraw ({ data: { id }, models, lnd }) {
const user = await models.user.findUnique({ where: { id } }) const user = await models.user.findUnique({ where: { id } })
if (user.autoWithdrawThreshold === null || user.autoWithdrawMaxFeePercent === null) return if (
user.autoWithdrawThreshold === null ||
user.autoWithdrawMaxFeePercent === null ||
user.autoWithdrawMaxFeeTotal === null) return
const threshold = satsToMsats(user.autoWithdrawThreshold) const threshold = satsToMsats(user.autoWithdrawThreshold)
const excess = Number(user.msats - threshold) const excess = Number(user.msats - threshold)
@ -13,7 +16,10 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) {
if (excess < Number(threshold) * 0.1) return if (excess < Number(threshold) * 0.1) return
// floor fee to nearest sat but still denominated in msats // floor fee to nearest sat but still denominated in msats
const maxFeeMsats = msatsSatsFloor(Math.ceil(excess * (user.autoWithdrawMaxFeePercent / 100.0))) const maxFeeMsats = msatsSatsFloor(Math.max(
Math.ceil(excess * (user.autoWithdrawMaxFeePercent / 100.0)),
Number(satsToMsats(user.autoWithdrawMaxFeeTotal))
))
// msats will be floored by createInvoice if it needs to be // msats will be floored by createInvoice if it needs to be
const msats = BigInt(excess) - maxFeeMsats const msats = BigInt(excess) - maxFeeMsats

View File

@ -346,12 +346,12 @@ export async function autoDropBolt11s ({ models, lnd }) {
SELECT id, hash, bolt11 SELECT id, hash, bolt11
FROM "Withdrawl" FROM "Withdrawl"
WHERE "userId" IN (SELECT id FROM users WHERE "autoDropBolt11s") WHERE "userId" IN (SELECT id FROM users WHERE "autoDropBolt11s")
AND now() > created_at + interval '${retention}' AND now() > created_at + ${retention}::INTERVAL
AND hash IS NOT NULL AND hash IS NOT NULL
AND status IS NOT NULL AND status IS NOT NULL
), updated_rows AS ( ), updated_rows AS (
UPDATE "Withdrawl" UPDATE "Withdrawl"
SET hash = NULL, bolt11 = NULL SET hash = NULL, bolt11 = NULL, preimage = NULL
FROM to_be_updated FROM to_be_updated
WHERE "Withdrawl".id = to_be_updated.id) WHERE "Withdrawl".id = to_be_updated.id)
SELECT * FROM to_be_updated;` SELECT * FROM to_be_updated;`
@ -364,7 +364,7 @@ export async function autoDropBolt11s ({ models, lnd }) {
console.error(`Error removing invoice with hash ${invoice.hash}:`, error) console.error(`Error removing invoice with hash ${invoice.hash}:`, error)
await models.withdrawl.update({ await models.withdrawl.update({
where: { id: invoice.id }, where: { id: invoice.id },
data: { hash: invoice.hash, bolt11: invoice.bolt11 } data: { hash: invoice.hash, bolt11: invoice.bolt11, preimage: invoice.preimage }
}) })
} }
} }