Compare commits

...

9 Commits

Author SHA1 Message Date
ekzyis 454ad26bd7
Add migration to fix missing item bio marker (#1295) 2024-08-12 19:19:18 -05:00
k00b ce7d2b888d add back greeterMode for backwards compat 2024-08-12 17:49:01 -05:00
ekzyis ae73b0c19f
Support receiving via LNbits (#1278)
* Support receiving with LNbits

* Remove hardcoded LNbits url on server

* Fix saveConfig ignoring save errors

* saveConfig was meant to only ignore validation errors, not save errors
* on server save errors, we redirected as if save was successful
* this is now fixed with a promise chain
* logging payments vs receivals was also moved to correct place

* Fix enabled falsely disabled on SSR

If a wallet was configured for payments but not for receivals and you refreshed the configuration form, enabled was disabled even though payments were enabled.

This was the case since we don't know during SSR if it's enabled since this information is stored on the client.

* Fix missing 'receivals disabled' log message

* Move 'wallet detached for payments' log message

* Fix stale walletId during detach

If page was reloaded, walletId in clearConfig was stale since callback dependency was missing.

* Add missing callback dependencies for saveConfig

* Verify that invoiceKey != adminKey

* Verify LNbits keys are hex-encoded

* Fix local config polluted with server data

* Fix creation of duplicate wallets

* Remove unused dependency

* Fix missing error message in logs

* Fix setPriority

* Rename: localConfig -> clientConfig

* Add description to LNbits autowithdrawals

* Rename: receivals -> receives

* Use try/catch instead of promise chain in saveConfig

* add connect label to lnbits for no url found for lnbits

* Fix adminKey not saved

* Remove hardcoded LNbits url on server again

* Add LNbits ATTACH.md

* Delete old docs to attach LNbits with polar

* Add missing callback dependencies

* Set editable: false

* Only set readOnly if field is configured

---------

Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-08-12 17:23:39 -05:00
Keyan 68758b3443
Update awards.csv 2024-08-12 16:58:49 -05:00
Keyan c5f043c625
replace greeter mode with investment filter (#1291)
* replace greeter mode with investment filter

* change name to satsFilter

* drop freebie column

---------

Co-authored-by: k00b <k00b@stacker.news>
2024-08-11 18:47:03 -05:00
Keyan e897a2d1dc
Update awards.csv 2024-08-11 16:31:44 -05:00
Anis Khalfallah ed6ef2f82f
fix: constrain less important services in docker compose (#1289)
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2024-08-11 16:29:46 -05:00
ekzyis bcae5e6d2e
Fix callback set to NextJS data URL (#1292) 2024-08-10 14:38:35 -05:00
Keyan ef229b378e
Update awards.csv 2024-08-10 11:17:22 -05:00
28 changed files with 448 additions and 238 deletions

View File

@ -160,3 +160,8 @@ TOR_PROXY=http://127.0.0.1:7050/
# lnbits
LNBITS_WEB_PORT=5001
# CPU shares for each category
CPU_SHARES_IMPORTANT=1024
CPU_SHARES_MODERATE=512
CPU_SHARES_LOW=256

View File

@ -1,7 +1,7 @@
import { ANON_ITEM_SPAM_INTERVAL, ITEM_SPAM_INTERVAL, USER_ID } from '@/lib/constants'
import { notifyItemMention, notifyItemParents, notifyMention, notifyTerritorySubscribers, notifyUserSubscribers } from '@/lib/webPush'
import { getItemMentions, getMentions, performBotBehavior } from './lib/item'
import { satsToMsats } from '@/lib/format'
import { msatsToSats, satsToMsats } from '@/lib/format'
export const anonable = true
export const supportsPessimism = true
@ -51,8 +51,7 @@ export async function perform (args, context) {
itemActs.push({
msats: cost - boostMsats, act: 'FEE', userId: data.userId, ...invoiceData
})
} else {
data.freebie = true
data.cost = msatsToSats(cost - boostMsats)
}
const mentions = await getMentions(args, context)

View File

@ -30,12 +30,12 @@ function commentsOrderByClause (me, models, sort) {
return `ORDER BY COALESCE(
personal_hot_score,
${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3)) DESC NULLS LAST,
"Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC`
"Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
} else {
if (sort === 'top') {
return `ORDER BY ${orderByNumerator(models, 0)} DESC NULLS LAST, "Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC`
return `ORDER BY ${orderByNumerator(models, 0)} DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
} else {
return `ORDER BY ${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC`
return `ORDER BY ${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
}
}
}
@ -225,22 +225,16 @@ export async function filterClause (me, models, type) {
// handle freebies
// by default don't include freebies unless they have upvotes
let freebieClauses = ['NOT "Item".freebie', '"Item"."weightedVotes" - "Item"."weightedDownVotes" > 0']
let investmentClause = '("Item".cost + "Item".boost + ("Item".msats / 1000)) >= 10'
if (me) {
const user = await models.user.findUnique({ where: { id: me.id } })
// wild west mode has everything
if (user.wildWestMode) {
return ''
}
// greeter mode includes freebies if feebies haven't been flagged
if (user.greeterMode) {
freebieClauses = ['NOT "Item".freebie', '"Item"."weightedVotes" - "Item"."weightedDownVotes" >= 0']
}
// always include if it's mine
freebieClauses.push(`"Item"."userId" = ${me.id}`)
investmentClause = `(("Item".cost + "Item".boost + ("Item".msats / 1000)) >= ${user.satsFilter} OR "Item"."userId" = ${me.id})`
if (user.wildWestMode) {
return investmentClause
}
}
const freebieClause = '(' + freebieClauses.join(' OR ') + ')'
// handle outlawed
// if the item is above the threshold or is mine
@ -250,7 +244,7 @@ export async function filterClause (me, models, type) {
}
const outlawClause = '(' + outlawClauses.join(' OR ') + ')'
return [freebieClause, outlawClause]
return [investmentClause, outlawClause]
}
function typeClause (type) {
@ -268,7 +262,7 @@ function typeClause (type) {
case 'comments':
return '"Item"."parentId" IS NOT NULL'
case 'freebies':
return '"Item".freebie'
return '"Item".cost = 0'
case 'outlawed':
return `"Item"."weightedVotes" - "Item"."weightedDownVotes" <= -${ITEM_FILTER_THRESHOLD} OR "Item".outlawed`
case 'borderland':
@ -470,10 +464,10 @@ export default {
'"Item".bio = false',
activeOrMine(me),
await filterClause(me, models, type))}
ORDER BY ${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC
ORDER BY ${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC
OFFSET $1
LIMIT $2`,
orderBy: `ORDER BY ${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC`
orderBy: `ORDER BY ${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
}, decodedCursor.offset, limit, ...subArr)
}
@ -1054,6 +1048,9 @@ export default {
freedFreebie: async (item) => {
return item.weightedVotes - item.weightedDownVotes > 0
},
freebie: async (item) => {
return item.cost === 0
},
meSats: async (item, args, { me, models }) => {
if (!me) return 0
if (typeof item.meMsats !== 'undefined') {
@ -1255,7 +1252,7 @@ export const updateItem = async (parent, { sub: subName, forward, ...item }, { m
const differentSub = subName && old.subName !== subName
if (differentSub) {
const sub = await models.sub.findUnique({ where: { name: subName } })
if (old.freebie) {
if (old.cost === 0) {
if (!sub.allowFreebies) {
throw new GraphQLError(`~${subName} does not allow freebies`, { extensions: { code: 'BAD_INPUT' } })
}

View File

@ -5,7 +5,7 @@ import serialize from './serial'
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
import { SELECT, itemQueryWithMeta } from './item'
import { msatsToSats, msatsToSatsDecimal } from '@/lib/format'
import { amountSchema, ssValidate, withdrawlSchema, lnAddrSchema, formikValidate } from '@/lib/validate'
import { amountSchema, ssValidate, withdrawlSchema, lnAddrSchema, walletValidate } from '@/lib/validate'
import { ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, USER_ID, BALANCE_LIMIT_MSATS, INVOICE_RETENTION_DAYS, INV_PENDING_LIMIT, USER_IDS_BALANCE_NO_LIMIT } from '@/lib/constants'
import { datePivot } from '@/lib/time'
import assertGofacYourself from './ofac'
@ -19,20 +19,14 @@ import { lnAddrOptions } from '@/lib/lnurl'
function injectResolvers (resolvers) {
console.group('injected GraphQL resolvers:')
for (const w of walletDefs) {
const { fieldValidation, walletType, walletField, testConnectServer } = w
const resolverName = generateResolverName(walletField)
const resolverName = generateResolverName(w.walletField)
console.log(resolverName)
// check if wallet uses the form-level validation built into Formik or a Yup schema
const validateArgs = typeof fieldValidation === 'function'
? { formikValidate: fieldValidation }
: { schema: fieldValidation }
resolvers.Mutation[resolverName] = async (parent, { settings, ...data }, { me, models }) => {
await walletValidate(w, { ...data, ...settings })
return await upsertWallet({
...validateArgs,
wallet: { field: walletField, type: walletType },
testConnectServer: (data) => testConnectServer(data, { me, models })
wallet: { field: w.walletField, type: w.walletType },
testConnectServer: (data) => w.testConnectServer(data, { me, models })
}, { settings, data }, { me, models })
}
}
@ -353,7 +347,7 @@ const resolvers = {
},
WalletDetails: {
__resolveType (wallet) {
return wallet.address ? 'WalletLNAddr' : wallet.macaroon ? 'WalletLND' : 'WalletCLN'
return wallet.address ? 'WalletLNAddr' : wallet.macaroon ? 'WalletLND' : wallet.rune ? 'WalletCLN' : 'WalletLNbits'
}
},
Mutation: {
@ -462,7 +456,8 @@ const resolvers = {
await models.$transaction([
models.wallet.delete({ where: { userId: me.id, id: Number(id) } }),
models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'SUCCESS', message: 'wallet detached' } })
models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'INFO', message: 'receives disabled' } }),
models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'SUCCESS', message: 'wallet detached for receives' } })
])
return true
@ -557,26 +552,20 @@ export const addWalletLog = async ({ wallet, level, message }, { me, models }) =
}
async function upsertWallet (
{ schema, formikValidate: validate, wallet, testConnectServer }, { settings, data }, { me, models }) {
{ wallet, testConnectServer }, { settings, data }, { me, models }) {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
}
assertApiKeyNotPermitted({ me })
if (schema) {
await ssValidate(schema, { ...data, ...settings }, { me, models })
}
if (validate) {
await formikValidate(validate, { ...data, ...settings })
}
if (testConnectServer) {
try {
await testConnectServer(data)
} catch (err) {
console.error(err)
const message = err.message || err.toString?.()
await addWalletLog({ wallet, level: 'ERROR', message: 'failed to attach: ' + message }, { me, models })
const message = 'failed to create test invoice: ' + (err.message || err.toString?.())
await addWalletLog({ wallet, level: 'ERROR', message }, { me, models })
await addWalletLog({ wallet, level: 'INFO', message: 'receives disabled' }, { me, models })
throw new GraphQLError(message, { extensions: { code: 'BAD_INPUT' } })
}
}
@ -632,7 +621,7 @@ async function upsertWallet (
userId: me.id,
wallet: wallet.type,
level: 'SUCCESS',
message: id ? 'wallet updated' : 'wallet attached'
message: id ? 'receive details updated' : 'wallet attached for receives'
}
}),
models.walletLog.create({
@ -640,7 +629,7 @@ async function upsertWallet (
userId: me.id,
wallet: wallet.type,
level: enabled ? 'SUCCESS' : 'INFO',
message: enabled ? 'wallet enabled' : 'wallet disabled'
message: enabled ? 'receives enabled' : 'receives disabled'
}
})
)

View File

@ -142,7 +142,11 @@ export function getGetServerSideProps (
const { data: { me } } = await client.query({ query: ME })
if (authRequired && !me) {
const callback = process.env.NEXT_PUBLIC_URL + req.url
let callback = process.env.NEXT_PUBLIC_URL + req.url
// On client-side routing, the callback is a NextJS URL
// so we need to remove the NextJS stuff.
// Example: /_next/data/development/territory.json
callback = callback.replace(/\/_next\/data\/\w+\//, '/').replace(/\.json$/, '')
return {
redirect: {
destination: `/signup?callbackUrl=${encodeURIComponent(callback)}`

View File

@ -71,6 +71,7 @@ export default gql`
diagnostics: Boolean!
noReferralLinks: Boolean!
fiatCurrency: String!
satsFilter: Int!
greeterMode: Boolean!
hideBookmarks: Boolean!
hideCowboyHat: Boolean!
@ -140,6 +141,7 @@ export default gql`
diagnostics: Boolean!
noReferralLinks: Boolean!
fiatCurrency: String!
satsFilter: Int!
greeterMode: Boolean!
hideBookmarks: Boolean!
hideCowboyHat: Boolean!

View File

@ -2,19 +2,22 @@ import { gql } from 'graphql-tag'
import { generateResolverName } from '@/lib/wallet'
import walletDefs from 'wallets/server'
import { isServerField } from 'wallets'
function injectTypeDefs (typeDefs) {
console.group('injected GraphQL type defs:')
const injected = walletDefs.map(
(w) => {
let args = 'id: ID, '
args += w.fields.map(f => {
let arg = `${f.name}: String`
if (!f.optional) {
arg += '!'
}
return arg
}).join(', ')
args += w.fields
.filter(isServerField)
.map(f => {
let arg = `${f.name}: String`
if (!f.optional) {
arg += '!'
}
return arg
}).join(', ')
args += ', settings: AutowithdrawSettings!'
const resolverName = generateResolverName(w.walletField)
const typeDef = `${resolverName}(${args}): Boolean`
@ -74,7 +77,12 @@ const typeDefs = `
cert: String
}
union WalletDetails = WalletLNAddr | WalletLND | WalletCLN
type WalletLNbits {
url: String!
invoiceKey: String!
}
union WalletDetails = WalletLNAddr | WalletLND | WalletCLN | WalletLNbits
input AutowithdrawSettings {
autoWithdrawThreshold: Int!

View File

@ -118,4 +118,5 @@ takitakitanana,issue,,#1257,good-first-issue,,,,2k,takitakitanana@stacker.news,2
SatsAllDay,pr,#1263,#1112,medium,,,1,225k,weareallsatoshi@getalby.com,2024-07-31
OneOneSeven117,issue,#1272,#1268,easy,,,,10k,OneOneSeven@stacker.news,2024-07-31
aniskhalfallah,pr,#1264,#1226,good-first-issue,,,,20k,aniskhalfallah@stacker.news,2024-07-31
Gudnessuche,issue,#1264,#1226,good-first-issue,,,,2k,???,???
Gudnessuche,issue,#1264,#1226,good-first-issue,,,,2k,everythingsatoshi@getalby.com,2024-08-10
aniskhalfallah,pr,#1289,,easy,,,,100k,aniskhalfallah@blink.sv,2024-08-12

1 name type pr id issue ids difficulty priority changes requested notes amount receive method date paid
118 SatsAllDay pr #1263 #1112 medium 1 225k weareallsatoshi@getalby.com 2024-07-31
119 OneOneSeven117 issue #1272 #1268 easy 10k OneOneSeven@stacker.news 2024-07-31
120 aniskhalfallah pr #1264 #1226 good-first-issue 20k aniskhalfallah@stacker.news 2024-07-31
121 Gudnessuche issue #1264 #1226 good-first-issue 2k ??? everythingsatoshi@getalby.com ??? 2024-08-10
122 aniskhalfallah pr #1289 easy 100k aniskhalfallah@blink.sv 2024-08-12

View File

@ -25,10 +25,15 @@ export function AutowithdrawSettings ({ wallet }) {
setSendThreshold(Math.max(Math.floor(threshold / 10), 1))
}, [autoWithdrawThreshold])
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
return (
<>
<Checkbox
disabled={!wallet.isConfigured}
disabled={mounted && !wallet.isConfigured}
label='enabled'
id='enabled'
name='enabled'

View File

@ -97,7 +97,7 @@ export default function Comment ({
}) {
const [edit, setEdit] = useState()
const me = useMe()
const isHiddenFreebie = !me?.privates?.wildWestMode && !me?.privates?.greeterMode && !item.mine && item.freebie && !item.freedFreebie
const isHiddenFreebie = me?.privates?.satsFilter !== 0 && !item.mine && item.freebie && !item.freedFreebie
const [collapse, setCollapse] = useState(
(isHiddenFreebie || item?.user?.meMute || (item?.outlawed && !me?.privates?.wildWestMode)) && !includeParent
? 'yep'

View File

@ -243,9 +243,6 @@ export function useWalletLogger (wallet) {
return
}
// don't store logs for receiving wallets on client since logs are stored on server
if (wallet.walletType) return
// TODO:
// also send this to us if diagnostics was enabled,
// very similar to how the service worker logger works.

View File

@ -35,6 +35,7 @@ services:
- db:/var/lib/postgresql/data
labels:
CONNECT: "localhost:5431"
cpu_shares: "${CPU_SHARES_IMPORTANT}"
app:
container_name: app
stdin_open: true
@ -58,6 +59,7 @@ services:
- ./:/app
labels:
CONNECT: "localhost:3000"
cpu_shares: "${CPU_SHARES_IMPORTANT}"
capture:
container_name: capture
build:
@ -79,6 +81,7 @@ services:
- "5678:5678"
labels:
CONNECT: "localhost:5678"
cpu_shares: "${CPU_SHARES_LOW}"
worker:
container_name: worker
build:
@ -97,6 +100,7 @@ services:
entrypoint: ["/bin/sh", "-c"]
command:
- npm run worker:dev
cpu_shares: "${CPU_SHARES_IMPORTANT}"
imgproxy:
container_name: imgproxy
image: darthsim/imgproxy:v3.23.0
@ -113,6 +117,7 @@ services:
- "8080"
labels:
- "CONNECT=localhost:3001"
cpu_shares: "${CPU_SHARES_LOW}"
s3:
container_name: s3
image: localstack/localstack:s3-latest
@ -138,6 +143,7 @@ services:
- './docker/s3/cors.json:/etc/localstack/init/ready.d/cors.json'
labels:
- "CONNECT=localhost:4566"
cpu_shares: "${CPU_SHARES_LOW}"
opensearch:
image: opensearchproject/opensearch:2.12.0
container_name: opensearch
@ -177,6 +183,7 @@ services:
echo "OpenSearch index created."
fg
'
cpu_shares: "${CPU_SHARES_LOW}"
os-dashboard:
image: opensearchproject/opensearch-dashboards:2.12.0
container_name: os-dashboard
@ -198,6 +205,7 @@ services:
- opensearch
labels:
CONNECT: "localhost:5601"
cpu_shares: "${CPU_SHARES_LOW}"
bitcoin:
image: polarlightning/bitcoind:26.0
container_name: bitcoin
@ -254,6 +262,7 @@ services:
bitcoin-cli -chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS} generatetoaddress 1 ${STACKER_CLN_ADDR}
fi
'
cpu_shares: "${CPU_SHARES_MODERATE}"
sn_lnd:
build:
context: ./docker/lnd
@ -311,6 +320,7 @@ services:
--min_confs 0 --local_amt=1000000000 --push_amt=500000000
fi
"
cpu_shares: "${CPU_SHARES_MODERATE}"
stacker_lnd:
build:
context: ./docker/lnd
@ -370,6 +380,7 @@ services:
--min_confs 0 --local_amt=1000000000 --push_amt=500000000
fi
"
cpu_shares: "${CPU_SHARES_MODERATE}"
litd:
container_name: litd
build:
@ -404,6 +415,7 @@ services:
- '--loop.server.host=test.swap.lightning.today:11010'
labels:
CONNECT: "localhost:8443"
cpu_shares: "${CPU_SHARES_MODERATE}"
stacker_cln:
build:
context: ./docker/cln
@ -446,6 +458,7 @@ services:
amount=1000000000 push_msat=500000000000 minconf=0
fi
"
cpu_shares: "${CPU_SHARES_MODERATE}"
channdler:
image: mcuadros/ofelia:latest
container_name: channdler
@ -460,6 +473,7 @@ services:
command: daemon --docker -f label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
cpu_shares: "${CPU_SHARES_LOW}"
mailhog:
image: mailhog/mailhog:latest
container_name: mailhog
@ -476,6 +490,7 @@ services:
- app
labels:
CONNECT: "localhost:8025"
cpu_shares: "${CPU_SHARES_LOW}"
nwc:
build:
context: ./docker/nwc
@ -507,6 +522,7 @@ services:
- '0'
- '--daily-limit'
- '0'
cpu_shares: "${CPU_SHARES_LOW}"
lnbits:
image: lnbits/lnbits:0.12.5
container_name: lnbits
@ -525,6 +541,9 @@ services:
- LND_GRPC_MACAROON=/app/.lnd/regtest/admin.macaroon
volumes:
- ./docker/lnd/stacker:/app/.lnd
labels:
CONNECT: "localhost:${LNBITS_WEB_PORT}"
cpu_shares: "${CPU_SHARES_LOW}"
volumes:
db:
os:

View File

@ -1,81 +0,0 @@
# attach lnbits
To test sending from an attached wallet, it's easiest to use [lnbits](https://lnbits.com/) hooked up to a [local lnd node](./local-lnd.md) in your regtest network.
This will attempt to walk you through setting up lnbits with docker and connecting it to your local lnd node.
🚨 this a dev guide. do not use this guide for real funds 🚨
From [this guide](https://docs.lnbits.org/guide/installation.html#option-3-docker):
## 1. pre-configuration
Create a directory for lnbits, get the sample environment file, and create a shared data directory for lnbits to use:
```bash
mkdir lnbits
cd lnbits
wget https://raw.githubusercontent.com/lnbits/lnbits/main/.env.example -O .env
mkdir data
```
## 2. configure
To configure lnbits to use a [local lnd node](./local-lnd.md) in your regtest network, go to [polar](https://lightningpolar.com/) and click on the LND node you want to use as a funding source. Then click on `Connect`.
In the `Connect` tab, click the `File paths` tab and copy the `TLS cert` and `Admin macaroon` files to the `data` directory you created earlier.
```bash
cp /path/to/tls.cert /path/to/admin.macaroon data/
```
Then, open the `.env` file you created and override the following values:
```bash
LNBITS_ADMIN_UI=true
LNBITS_BACKEND_WALLET_CLASS=LndWallet
LND_GRPC_ENDPOINT=host.docker.internal
LND_GRPC_PORT=${Port from the polar connect page}
LND_GRPC_CERT=data/tls.cert
LND_GRPC_MACAROON=data/admin.macaroon
```
## 2. Install and run lnbits
Pull the latest image:
```bash
docker pull lnbitsdocker/lnbits-legend
docker run --detach --publish 5001:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbitsdocker/lnbits-legend
```
Note: we make lnbits available on the host's port 5001 here (on Mac, 5000 is used by AirPlay), but you can change that to whatever you want.
## 3. Accessing the admin wallet
By enabling the [Admin UI](https://docs.lnbits.org/guide/admin_ui.html), lnbits creates a so called super_user. Get this super_user id by running:
```bash
cat data/.super_user
```
Open your browser and go to `http://localhost:5001/wallet?usr=${super_user id from above}`. LNBits will redirect you to a default wallet we will use called `LNBits wallet`.
## 4. Fund the wallet
To fund `LNBits wallet`, click the `+` next the wallet balance. Enter the number of sats you want to credit the wallet and hit enter.
## 5. Attach the wallet to stackernews
Open up your local stackernews, go to `http://localhost:3000/settings/wallets` and click on `attach` in the `lnbits` card.
In the form, fill in `lnbits url` with `http://localhost:5001`.
Back in lnbits click on `API Docs` in the right pane. Copy the Admin key and paste it into the `admin key` field in the form.
Click `attach` and you should be good to go.
## Debugging
- you can view lnbits logs with `docker logs lnbits` or in `data/logs/` in the `data` directory you created earlier
- with the [Admin UI](https://docs.lnbits.org/guide/admin_ui.html), you can modify LNBits in the GUI by clicking `Server` in left pane

View File

@ -15,7 +15,7 @@ export const ME = gql`
diagnostics
noReferralLinks
fiatCurrency
greeterMode
satsFilter
hideCowboyHat
hideFromTopUsers
hideGithub
@ -104,7 +104,7 @@ export const SETTINGS_FIELDS = gql`
nostrCrossposting
nostrRelays
wildWestMode
greeterMode
satsFilter
nsfwMode
authMethods {
lightning

View File

@ -129,6 +129,10 @@ export const WALLET = gql`
rune
cert
}
... on WalletLNbits {
url
invoiceKey
}
}
}
}
@ -157,6 +161,10 @@ export const WALLET_BY_TYPE = gql`
rune
cert
}
... on WalletLNbits {
url
invoiceKey
}
}
}
}

View File

@ -6,7 +6,7 @@ import {
} from './constants'
import { SUPPORTED_CURRENCIES } from './currency'
import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32, NOSTR_PUBKEY_HEX } from './nostr'
import { msatsToSats, numWithUnits, abbrNum, ensureB64, B64_URL_REGEX } from './format'
import { msatsToSats, numWithUnits, abbrNum, ensureB64, B64_URL_REGEX, HEX_REGEX } from './format'
import * as usersFragments from '@/fragments/users'
import * as subsFragments from '@/fragments/subs'
import { isInvoicableMacaroon, isInvoiceMacaroon } from './macaroon'
@ -41,6 +41,14 @@ export async function formikValidate (validate, data) {
}
}
export async function walletValidate (wallet, data) {
if (typeof wallet.fieldValidation === 'function') {
return await formikValidate(wallet.fieldValidation, data)
} else {
await ssValidate(wallet.fieldValidation, data)
}
}
addMethod(string, 'or', function (schemas, msg) {
return this.test({
name: 'or',
@ -142,6 +150,14 @@ addMethod(string, 'wss', function (msg) {
})
})
addMethod(string, 'hex', function (msg) {
return this.test({
name: 'hex',
message: msg || 'invalid hex encoding',
test: (value) => !value || HEX_REGEX.test(value)
})
})
const titleValidator = string().required('required').trim().max(
MAX_TITLE_LENGTH,
({ max, value }) => `-${Math.abs(max - value.length)} characters remaining`
@ -579,6 +595,7 @@ export const settingsSchema = object().shape({
diagnostics: boolean(),
noReferralLinks: boolean(),
hideIsContributor: boolean(),
satsFilter: intValidator.required('required').min(0, 'must be at least 0').max(1000, 'must be at most 1000'),
zapUndos: intValidator.nullable().min(0, 'must be greater or equal to 0')
// exclude from cyclic analysis. see https://github.com/jquense/yup/issues/720
}, [['tipRandomMax', 'tipRandomMin']])
@ -620,7 +637,7 @@ export const lnAddrSchema = ({ payerData, min, max, commentAllowed } = {}) =>
return accum
}, {})))
export const lnbitsSchema = object({
export const lnbitsSchema = object().shape({
url: process.env.NODE_ENV === 'development'
? string()
.or([string().matches(/^(http:\/\/)?localhost:\d+$/), string().url()], 'invalid url')
@ -642,8 +659,25 @@ export const lnbitsSchema = object({
}
return true
}),
adminKey: string().length(32).required('required')
})
adminKey: string().length(32).hex()
.when(['invoiceKey'], ([invoiceKey], schema) => {
if (!invoiceKey) return schema.required('required if invoice key not set')
return schema.test({
test: adminKey => adminKey !== invoiceKey,
message: 'admin key cannot be the same as invoice key'
})
}),
invoiceKey: string().length(32).hex()
.when(['adminKey'], ([adminKey], schema) => {
if (!adminKey) return schema.required('required if admin key not set')
return schema.test({
test: invoiceKey => adminKey !== invoiceKey,
message: 'invoice key cannot be the same as admin key'
})
})
// need to set order to avoid cyclic dependencies in Yup schema
// see https://github.com/jquense/yup/issues/176#issuecomment-367352042
}, ['adminKey', 'invoiceKey'])
export const nwcSchema = object({
nwcUrl: string()

View File

@ -140,7 +140,7 @@ export default function Settings ({ ssrData }) {
hideTwitter: settings?.hideTwitter,
imgproxyOnly: settings?.imgproxyOnly,
wildWestMode: settings?.wildWestMode,
greeterMode: settings?.greeterMode,
satsFilter: settings?.satsFilter,
nsfwMode: settings?.nsfwMode,
nostrPubkey: settings?.nostrPubkey ? bech32encode(settings.nostrPubkey) : '',
nostrCrossposting: settings?.nostrCrossposting,
@ -152,7 +152,11 @@ export default function Settings ({ ssrData }) {
noReferralLinks: settings?.noReferralLinks
}}
schema={settingsSchema}
onSubmit={async ({ tipDefault, tipRandom, tipRandomMin, tipRandomMax, withdrawMaxFeeDefault, zapUndos, zapUndosEnabled, nostrPubkey, nostrRelays, ...values }) => {
onSubmit={async ({
tipDefault, tipRandom, tipRandomMin, tipRandomMax, withdrawMaxFeeDefault,
zapUndos, zapUndosEnabled, nostrPubkey, nostrRelays, satsFilter,
...values
}) => {
if (nostrPubkey.length === 0) {
nostrPubkey = null
} else {
@ -172,6 +176,7 @@ export default function Settings ({ ssrData }) {
tipRandomMin: tipRandom ? Number(tipRandomMin) : null,
tipRandomMax: tipRandom ? Number(tipRandomMax) : null,
withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault),
satsFilter: Number(satsFilter),
zapUndos: zapUndosEnabled ? Number(zapUndos) : null,
nostrPubkey,
nostrRelays: nostrRelaysFiltered,
@ -467,7 +472,27 @@ export default function Settings ({ ssrData }) {
label={<>don't create referral links on copy</>}
name='noReferralLinks'
/>
<div className='form-label'>content</div>
<h4>content</h4>
<Input
label={
<div className='d-flex align-items-center'>filter by sats
<Info>
<ul className='fw-bold'>
<li>hide the post if the sum of these is less than your setting:</li>
<ul>
<li>posting cost</li>
<li>total sats from zaps</li>
<li>boost</li>
</ul>
<li>set to zero to be a greeter, with the tradeoff of seeing more spam</li>
</ul>
</Info>
</div>
}
name='satsFilter'
required
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/>
<Checkbox
label={
<div className='d-flex align-items-center'>wild west mode
@ -482,21 +507,6 @@ export default function Settings ({ ssrData }) {
name='wildWestMode'
groupClassName='mb-0'
/>
<Checkbox
label={
<div className='d-flex align-items-center'>greeter mode
<Info>
<ul className='fw-bold'>
<li>see and screen free posts and comments</li>
<li>help onboard new stackers to SN and Lightning</li>
<li>you might be subject to more spam</li>
</ul>
</Info>
</div>
}
name='greeterMode'
groupClassName='mb-0'
/>
<Checkbox
label={
<div className='d-flex align-items-center'>nsfw mode

View File

@ -57,15 +57,11 @@ export default function WalletSettings () {
await wallet.save(values)
if (values.enabled) wallet.enable()
else wallet.disable()
toaster.success('saved settings')
router.push('/settings/wallets')
} catch (err) {
console.error(err)
const message = 'failed to attach: ' + err.message || err.toString?.()
toaster.danger(message)
toaster.danger(err.message || err.toString?.())
}
}}
>
@ -106,12 +102,12 @@ export default function WalletSettings () {
function WalletFields ({ wallet: { config, fields, isConfigured } }) {
return fields
.map(({ name, label, type, help, optional, editable, ...props }, i) => {
.map(({ name, label, type, help, optional, editable, clientOnly, serverOnly, ...props }, i) => {
const rawProps = {
...props,
name,
initialValue: config?.[name],
readOnly: isConfigured && editable === false,
readOnly: isConfigured && editable === false && !!config?.[name],
groupClassName: props.hidden ? 'd-none' : undefined,
label: label
? (

View File

@ -0,0 +1,24 @@
-- AlterEnum
ALTER TYPE "WalletType" ADD VALUE 'LNBITS';
-- CreateTable
CREATE TABLE "WalletLNbits" (
"int" SERIAL NOT NULL,
"walletId" INTEGER NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"url" TEXT NOT NULL,
"invoiceKey" TEXT NOT NULL,
CONSTRAINT "WalletLNbits_pkey" PRIMARY KEY ("int")
);
-- CreateIndex
CREATE UNIQUE INDEX "WalletLNbits_walletId_key" ON "WalletLNbits"("walletId");
-- AddForeignKey
ALTER TABLE "WalletLNbits" ADD CONSTRAINT "WalletLNbits_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TRIGGER wallet_lnbits_as_jsonb
AFTER INSERT OR UPDATE ON "WalletLNbits"
FOR EACH ROW EXECUTE PROCEDURE wallet_wallet_type_as_jsonb();

View File

@ -0,0 +1,14 @@
-- AlterTable
ALTER TABLE "Item" ADD COLUMN "cost" INTEGER NOT NULL DEFAULT 0;
-- use existing "ItemAct".act = FEE AND "Item"."userId" = "ItemAct"."userId" to calculate the cost for existing "Item"s
UPDATE "Item" SET "cost" = "ItemAct"."msats" / 1000
FROM "ItemAct"
WHERE "Item"."id" = "ItemAct"."itemId" AND "ItemAct"."act" = 'FEE' AND "Item"."userId" = "ItemAct"."userId";
ALTER TABLE "users" ADD COLUMN "satsFilter" INTEGER NOT NULL DEFAULT 10;
UPDATE "users" SET "satsFilter" = 0 WHERE "greeterMode";
-- CreateIndex
CREATE INDEX "Item_cost_idx" ON "Item"("cost");

View File

@ -0,0 +1,4 @@
-- fix missing 'bio' marker for bios
UPDATE "Item" SET bio = 't' WHERE id IN (
SELECT "bioId" FROM users WHERE "bioId" IS NOT NULL
);

View File

@ -54,7 +54,7 @@ model User {
upvoteTrust Float @default(0)
hideInvoiceDesc Boolean @default(false)
wildWestMode Boolean @default(false)
greeterMode Boolean @default(false)
satsFilter Int @default(10)
nsfwMode Boolean @default(false)
fiatCurrency String @default("USD")
withdrawMaxFeeDefault Int @default(10)
@ -66,6 +66,7 @@ model User {
hideWalletBalance Boolean @default(false)
referrerId Int?
nostrPubkey String?
greeterMode Boolean @default(false)
nostrAuthPubkey String? @unique(map: "users.nostrAuthPubkey_unique")
nostrCrossposting Boolean @default(false)
slashtagId String? @unique(map: "users.slashtagId_unique")
@ -167,6 +168,7 @@ enum WalletType {
LIGHTNING_ADDRESS
LND
CLN
LNBITS
}
model Wallet {
@ -190,6 +192,7 @@ model Wallet {
walletLightningAddress WalletLightningAddress?
walletLND WalletLND?
walletCLN WalletCLN?
walletLNbits WalletLNbits?
withdrawals Withdrawl[]
@@index([userId])
@ -238,6 +241,16 @@ model WalletCLN {
cert String?
}
model WalletLNbits {
int Int @id @default(autoincrement())
walletId Int @unique
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
url String
invoiceKey String
}
model Mute {
muterId Int
mutedId Int
@ -425,6 +438,7 @@ model Item {
lastZapAt DateTime?
ncomments Int @default(0)
msats BigInt @default(0)
cost Int @default(0)
weightedDownVotes Float @default(0)
bio Boolean @default(false)
freebie Boolean @default(false)
@ -489,6 +503,7 @@ model Item {
@@index([weightedVotes], map: "Item.weightedVotes_index")
@@index([invoiceId])
@@index([invoiceActionState])
@@index([cost])
}
// we use this to denormalize a user's aggregated interactions (zaps) with an item

View File

@ -1,6 +1,6 @@
import { useCallback } from 'react'
import { useMe } from '@/components/me'
import useLocalConfig from '@/components/use-local-state'
import useClientConfig from '@/components/use-local-state'
import { useWalletLogger } from '@/components/wallet-logger'
import { SSR } from '@/lib/constants'
import { bolt11Tags } from '@/lib/bolt11'
@ -12,6 +12,7 @@ import { autowithdrawInitial } from '@/components/autowithdraw-shared'
import { useShowModal } from '@/components/modal'
import { useToast } from '../components/toast'
import { generateResolverName } from '@/lib/wallet'
import { walletValidate } from '@/lib/validate'
export const Status = {
Initialized: 'Initialized',
@ -32,6 +33,22 @@ export function useWallet (name) {
const hasConfig = wallet?.fields.length > 0
const _isConfigured = isConfigured({ ...wallet, config })
const enablePayments = useCallback(() => {
enableWallet(name, me)
logger.ok('payments enabled')
}, [name, me, logger])
const disablePayments = useCallback(() => {
disableWallet(name, me)
logger.info('payments disabled')
}, [name, me, logger])
if (wallet) {
wallet.isConfigured = _isConfigured
wallet.enablePayments = enablePayments
wallet.disablePayments = disablePayments
}
const status = config?.enabled ? Status.Enabled : Status.Initialized
const enabled = status === Status.Enabled
const priority = config?.priority
@ -49,53 +66,40 @@ export function useWallet (name) {
}
}, [me, wallet, config, logger, status])
const enable = useCallback(() => {
enableWallet(name, me)
logger.ok('wallet enabled')
}, [name, me, logger])
const disable = useCallback(() => {
disableWallet(name, me)
logger.info('wallet disabled')
}, [name, me, logger])
const setPriority = useCallback(async (priority) => {
if (_isConfigured && priority !== config.priority) {
try {
await saveConfig({ ...config, priority })
await saveConfig({ ...config, priority }, { logger })
} catch (err) {
toaster.danger(`failed to change priority of ${wallet.name} wallet: ${err.message}`)
}
}
}, [wallet, config, logger, toaster])
}, [wallet, config, toaster])
const save = useCallback(async (newConfig) => {
// testConnectClient should log custom INFO and OK message
// testConnectClient is optional since validation might happen during save on server
// TODO: add timeout
let validConfig
try {
// testConnectClient should log custom INFO and OK message
// testConnectClient is optional since validation might happen during save on server
// TODO: add timeout
const validConfig = await wallet.testConnectClient?.(newConfig, { me, logger })
await saveConfig(validConfig ?? newConfig)
logger.ok(_isConfigured ? 'wallet updated' : 'wallet attached')
validConfig = await wallet.testConnectClient?.(newConfig, { me, logger })
} catch (err) {
const message = err.message || err.toString?.()
logger.error('failed to attach: ' + message)
logger.error(err.message)
throw err
}
}, [_isConfigured, saveConfig, me, logger])
await saveConfig(validConfig ?? newConfig, { logger })
}, [saveConfig, me, logger])
// delete is a reserved keyword
const delete_ = useCallback(async () => {
try {
await clearConfig()
logger.ok('wallet detached')
disable()
await clearConfig({ logger })
} catch (err) {
const message = err.message || err.toString?.()
logger.error(message)
throw err
}
}, [clearConfig, logger, disable])
}, [clearConfig, logger, disablePayments])
if (!wallet) return null
@ -106,11 +110,8 @@ export function useWallet (name) {
save,
delete: delete_,
deleteLogs,
enable,
disable,
setPriority,
hasConfig,
isConfigured: _isConfigured,
status,
enabled,
priority,
@ -118,34 +119,111 @@ export function useWallet (name) {
}
}
function extractConfig (fields, config, client) {
return Object.entries(config).reduce((acc, [key, value]) => {
const field = fields.find(({ name }) => name === key)
// filter server config which isn't specified as wallet fields
if (client && (key.startsWith('autoWithdraw') || key === 'id')) return acc
// field might not exist because config.enabled doesn't map to a wallet field
if (!field || (client ? isClientField(field) : isServerField(field))) {
return {
...acc,
[key]: value
}
} else {
return acc
}
}, {})
}
export function isServerField (f) {
return f.serverOnly || !f.clientOnly
}
export function isClientField (f) {
return f.clientOnly || !f.serverOnly
}
function extractClientConfig (fields, config) {
return extractConfig(fields, config, true)
}
function extractServerConfig (fields, config) {
return extractConfig(fields, config, false)
}
function useConfig (wallet) {
const me = useMe()
const storageKey = getStorageKey(wallet?.name, me)
const [localConfig, setLocalConfig, clearLocalConfig] = useLocalConfig(storageKey)
const [clientConfig, setClientConfig, clearClientConfig] = useClientConfig(storageKey)
const [serverConfig, setServerConfig, clearServerConfig] = useServerConfig(wallet)
const hasLocalConfig = !!wallet?.sendPayment
const hasClientConfig = !!wallet?.sendPayment
const hasServerConfig = !!wallet?.walletType
const config = {
// only include config if it makes sense for this wallet
// since server config always returns default values for autowithdraw settings
// which might be confusing to have for wallets that don't support autowithdraw
...(hasLocalConfig ? localConfig : {}),
...(hasServerConfig ? serverConfig : {})
let config = {}
if (hasClientConfig) config = clientConfig
if (hasServerConfig) {
const { enabled } = config || {}
config = {
...config,
...serverConfig
}
// wallet is enabled if enabled is set in client or server config
config.enabled ||= enabled
}
const saveConfig = useCallback(async (config) => {
if (hasLocalConfig) setLocalConfig(config)
if (hasServerConfig) await setServerConfig(config)
}, [wallet])
const saveConfig = useCallback(async (newConfig, { logger }) => {
// NOTE:
// verifying the client/server configuration before saving it
// prevents unsetting just one configuration if both are set.
// This means there is no way of unsetting just one configuration
// since 'detach' detaches both.
// Not optimal UX but the trade-off is saving invalid configurations
// and maybe it's not that big of an issue.
if (hasClientConfig) {
const newClientConfig = extractClientConfig(wallet.fields, newConfig)
const clearConfig = useCallback(async () => {
if (hasLocalConfig) clearLocalConfig()
let valid = true
try {
await walletValidate(wallet, newClientConfig)
} catch {
valid = false
}
if (valid) {
setClientConfig(newClientConfig)
logger.ok(wallet.isConfigured ? 'payment details updated' : 'wallet attached for payments')
if (newConfig.enabled) wallet.enablePayments()
else wallet.disablePayments()
}
}
if (hasServerConfig) {
const newServerConfig = extractServerConfig(wallet.fields, newConfig)
let valid = true
try {
await walletValidate(wallet, newServerConfig)
} catch {
valid = false
}
if (valid) await setServerConfig(newServerConfig)
}
}, [hasClientConfig, hasServerConfig, setClientConfig, setServerConfig, wallet])
const clearConfig = useCallback(async ({ logger }) => {
if (hasClientConfig) {
clearClientConfig()
wallet.disablePayments()
logger.ok('wallet detached for payments')
}
if (hasServerConfig) await clearServerConfig()
}, [wallet])
}, [hasClientConfig, hasServerConfig, clearClientConfig, clearServerConfig, wallet])
return [config, saveConfig, clearConfig]
}
@ -174,6 +252,8 @@ function useServerConfig (wallet) {
enabled: data?.walletByType?.enabled,
...data?.walletByType?.wallet
}
delete serverConfig.__typename
const autowithdrawSettings = autowithdrawInitial({ me })
const config = { ...serverConfig, ...autowithdrawSettings }
@ -189,8 +269,8 @@ function useServerConfig (wallet) {
return await client.mutate({
mutation,
variables: {
id: walletId,
...config,
id: walletId,
settings: {
autoWithdrawThreshold: Number(autoWithdrawThreshold),
autoWithdrawMaxFeePercent: Number(autoWithdrawMaxFeePercent),
@ -206,6 +286,9 @@ function useServerConfig (wallet) {
}, [client, walletId])
const clearConfig = useCallback(async () => {
// only remove wallet if there is a wallet to remove
if (!walletId) return
try {
await client.mutate({
mutation: REMOVE_WALLET,
@ -224,17 +307,21 @@ function generateMutation (wallet) {
const resolverName = generateResolverName(wallet.walletField)
let headerArgs = '$id: ID, '
headerArgs += wallet.fields.map(f => {
let arg = `$${f.name}: String`
if (!f.optional) {
arg += '!'
}
return arg
}).join(', ')
headerArgs += wallet.fields
.filter(isServerField)
.map(f => {
let arg = `$${f.name}: String`
if (!f.optional) {
arg += '!'
}
return arg
}).join(', ')
headerArgs += ', $settings: AutowithdrawSettings!'
let inputArgs = 'id: $id, '
inputArgs += wallet.fields.map(f => `${f.name}: $${f.name}`).join(', ')
inputArgs += wallet.fields
.filter(isServerField)
.map(f => `${f.name}: $${f.name}`).join(', ')
inputArgs += ', settings: $settings'
return gql`mutation ${resolverName}(${headerArgs}) {

27
wallets/lnbits/ATTACH.md Normal file
View File

@ -0,0 +1,27 @@
For testing LNbits, you need to create a LNbits account first via the web interface.
By default, you can access it at `localhost:5001` (see `LNBITS_WEB_PORT` in .env.development).
After you created a wallet, you should find the invoice and admin key under `Node URL, API keys and API docs`.
> [!IMPORTANT]
>
> 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.
>
> For now, you need to patch the `_createInvoice` function in wallets/lnbits/server.js to always use `lnbits:5000` as the URL:
>
> ```diff
> diff --git a/wallets/lnbits/server.js b/wallets/lnbits/server.js
> index 39949775..e3605c45 100644
> --- a/wallets/lnbits/server.js
> +++ b/wallets/lnbits/server.js
> @@ -11,6 +11,7 @@ async function _createInvoice ({ url, invoiceKey, amount, expiry }, { me }) {
> const memo = me.hideInvoiceDesc ? undefined : 'autowithdraw to LNbits from SN'
> const body = JSON.stringify({ amount, unit: 'sat', expiry, memo, out: false })
>
> + url = 'http://lnbits:5000'
> const res = await fetch(url + path, { method: 'POST', headers, body })
> if (!res.ok) {
> const errBody = await res.json()
> ```
>

View File

@ -1,10 +1,10 @@
export * from 'wallets/lnbits'
export async function testConnectClient ({ url, adminKey }, { logger }) {
export async function testConnectClient ({ url, adminKey, invoiceKey }, { logger }) {
logger.info('trying to fetch wallet')
url = url.replace(/\/+$/, '')
await getWallet({ url, adminKey })
await getWallet({ url, adminKey, invoiceKey })
logger.ok('wallet found')
}
@ -23,13 +23,13 @@ export async function sendPayment (bolt11, { url, adminKey }) {
return { preimage }
}
async function getWallet ({ url, adminKey }) {
async function getWallet ({ url, adminKey, invoiceKey }) {
const path = '/api/v1/wallet'
const headers = new Headers()
headers.append('Accept', 'application/json')
headers.append('Content-Type', 'application/json')
headers.append('X-Api-Key', adminKey)
headers.append('X-Api-Key', adminKey || invoiceKey)
const res = await fetch(url + path, { method: 'GET', headers })
if (!res.ok) {

View File

@ -8,17 +8,32 @@ export const fields = [
label: 'lnbits url',
type: 'text'
},
{
name: 'invoiceKey',
label: 'invoice key',
type: 'password',
optional: 'for receiving',
serverOnly: true,
editable: false
},
{
name: 'adminKey',
label: 'admin key',
type: 'password'
type: 'password',
optional: 'for sending',
clientOnly: true,
editable: false
}
]
export const card = {
title: 'LNbits',
subtitle: 'use [LNbits](https://lnbits.com/) for payments',
badges: ['send only']
badges: ['send & receive']
}
export const fieldValidation = lnbitsSchema
export const walletType = 'LNBITS'
export const walletField = 'walletLNbits'

30
wallets/lnbits/server.js Normal file
View File

@ -0,0 +1,30 @@
export * from 'wallets/lnbits'
async function _createInvoice ({ url, invoiceKey, amount, expiry }, { me }) {
const path = '/api/v1/payments'
const headers = new Headers()
headers.append('Accept', 'application/json')
headers.append('Content-Type', 'application/json')
headers.append('X-Api-Key', invoiceKey)
const memo = me.hideInvoiceDesc ? undefined : 'autowithdraw to LNbits from SN'
const body = JSON.stringify({ amount, unit: 'sat', expiry, memo, out: false })
const res = await fetch(url + path, { method: 'POST', headers, body })
if (!res.ok) {
const errBody = await res.json()
throw new Error(errBody.detail)
}
const payment = await res.json()
return payment.payment_request
}
export async function testConnectServer ({ url, invoiceKey }, { me }) {
return await _createInvoice({ url, invoiceKey, amount: 1, expiry: 1 }, { me })
}
export async function createInvoice ({ amount, maxFee }, { url, invoiceKey }, { me }) {
return await _createInvoice({ url, invoiceKey, amount, expiry: 360 }, { me })
}

View File

@ -1,5 +1,6 @@
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'
export default [lnd, cln, lnAddr]
export default [lnd, cln, lnAddr, lnbits]