Compare commits

..

No commits in common. "454ad26bd792633fa686c33696b458854f6c0f69" and "3863edb8718045285f52c013d06842b870ea719d" have entirely different histories.

28 changed files with 237 additions and 447 deletions

View File

@ -160,8 +160,3 @@ 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 { msatsToSats, satsToMsats } from '@/lib/format'
import { satsToMsats } from '@/lib/format'
export const anonable = true
export const supportsPessimism = true
@ -51,7 +51,8 @@ export async function perform (args, context) {
itemActs.push({
msats: cost - boostMsats, act: 'FEE', userId: data.userId, ...invoiceData
})
data.cost = msatsToSats(cost - boostMsats)
} else {
data.freebie = true
}
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".cost > 0) DESC, "Item".id DESC`
"Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC`
} else {
if (sort === 'top') {
return `ORDER BY ${orderByNumerator(models, 0)} DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
return `ORDER BY ${orderByNumerator(models, 0)} DESC NULLS LAST, "Item".msats DESC, ("Item".freebie IS FALSE) 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".cost > 0) 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".freebie IS FALSE) DESC, "Item".id DESC`
}
}
}
@ -225,17 +225,23 @@ export async function filterClause (me, models, type) {
// handle freebies
// by default don't include freebies unless they have upvotes
let investmentClause = '("Item".cost + "Item".boost + ("Item".msats / 1000)) >= 10'
let freebieClauses = ['NOT "Item".freebie', '"Item"."weightedVotes" - "Item"."weightedDownVotes" > 0']
if (me) {
const user = await models.user.findUnique({ where: { id: me.id } })
investmentClause = `(("Item".cost + "Item".boost + ("Item".msats / 1000)) >= ${user.satsFilter} OR "Item"."userId" = ${me.id})`
// wild west mode has everything
if (user.wildWestMode) {
return investmentClause
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}`)
}
const freebieClause = '(' + freebieClauses.join(' OR ') + ')'
// handle outlawed
// if the item is above the threshold or is mine
const outlawClauses = [`"Item"."weightedVotes" - "Item"."weightedDownVotes" > -${ITEM_FILTER_THRESHOLD} AND NOT "Item".outlawed`]
@ -244,7 +250,7 @@ export async function filterClause (me, models, type) {
}
const outlawClause = '(' + outlawClauses.join(' OR ') + ')'
return [investmentClause, outlawClause]
return [freebieClause, outlawClause]
}
function typeClause (type) {
@ -262,7 +268,7 @@ function typeClause (type) {
case 'comments':
return '"Item"."parentId" IS NOT NULL'
case 'freebies':
return '"Item".cost = 0'
return '"Item".freebie'
case 'outlawed':
return `"Item"."weightedVotes" - "Item"."weightedDownVotes" <= -${ITEM_FILTER_THRESHOLD} OR "Item".outlawed`
case 'borderland':
@ -464,10 +470,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".cost > 0) 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".freebie IS FALSE) 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".cost > 0) 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".freebie IS FALSE) DESC, "Item".id DESC`
}, decodedCursor.offset, limit, ...subArr)
}
@ -1048,9 +1054,6 @@ 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') {
@ -1252,7 +1255,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.cost === 0) {
if (old.freebie) {
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, walletValidate } from '@/lib/validate'
import { amountSchema, ssValidate, withdrawlSchema, lnAddrSchema, formikValidate } 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,14 +19,20 @@ import { lnAddrOptions } from '@/lib/lnurl'
function injectResolvers (resolvers) {
console.group('injected GraphQL resolvers:')
for (const w of walletDefs) {
const resolverName = generateResolverName(w.walletField)
const { fieldValidation, walletType, walletField, testConnectServer } = w
const resolverName = generateResolverName(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({
wallet: { field: w.walletField, type: w.walletType },
testConnectServer: (data) => w.testConnectServer(data, { me, models })
...validateArgs,
wallet: { field: walletField, type: walletType },
testConnectServer: (data) => testConnectServer(data, { me, models })
}, { settings, data }, { me, models })
}
}
@ -347,7 +353,7 @@ const resolvers = {
},
WalletDetails: {
__resolveType (wallet) {
return wallet.address ? 'WalletLNAddr' : wallet.macaroon ? 'WalletLND' : wallet.rune ? 'WalletCLN' : 'WalletLNbits'
return wallet.address ? 'WalletLNAddr' : wallet.macaroon ? 'WalletLND' : 'WalletCLN'
}
},
Mutation: {
@ -456,8 +462,7 @@ 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: 'INFO', message: 'receives disabled' } }),
models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'SUCCESS', message: 'wallet detached for receives' } })
models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'SUCCESS', message: 'wallet detached' } })
])
return true
@ -552,20 +557,26 @@ export const addWalletLog = async ({ wallet, level, message }, { me, models }) =
}
async function upsertWallet (
{ wallet, testConnectServer }, { settings, data }, { me, models }) {
{ schema, formikValidate: validate, 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 = '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 })
const message = err.message || err.toString?.()
await addWalletLog({ wallet, level: 'ERROR', message: 'failed to attach: ' + message }, { me, models })
throw new GraphQLError(message, { extensions: { code: 'BAD_INPUT' } })
}
}
@ -621,7 +632,7 @@ async function upsertWallet (
userId: me.id,
wallet: wallet.type,
level: 'SUCCESS',
message: id ? 'receive details updated' : 'wallet attached for receives'
message: id ? 'wallet updated' : 'wallet attached'
}
}),
models.walletLog.create({
@ -629,7 +640,7 @@ async function upsertWallet (
userId: me.id,
wallet: wallet.type,
level: enabled ? 'SUCCESS' : 'INFO',
message: enabled ? 'receives enabled' : 'receives disabled'
message: enabled ? 'wallet enabled' : 'wallet disabled'
}
})
)

View File

@ -142,11 +142,7 @@ export function getGetServerSideProps (
const { data: { me } } = await client.query({ query: ME })
if (authRequired && !me) {
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$/, '')
const callback = process.env.NEXT_PUBLIC_URL + req.url
return {
redirect: {
destination: `/signup?callbackUrl=${encodeURIComponent(callback)}`

View File

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

View File

@ -2,16 +2,13 @@ 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
.filter(isServerField)
.map(f => {
args += w.fields.map(f => {
let arg = `${f.name}: String`
if (!f.optional) {
arg += '!'
@ -77,12 +74,7 @@ const typeDefs = `
cert: String
}
type WalletLNbits {
url: String!
invoiceKey: String!
}
union WalletDetails = WalletLNAddr | WalletLND | WalletCLN | WalletLNbits
union WalletDetails = WalletLNAddr | WalletLND | WalletCLN
input AutowithdrawSettings {
autoWithdrawThreshold: Int!

View File

@ -118,5 +118,4 @@ 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,everythingsatoshi@getalby.com,2024-08-10
aniskhalfallah,pr,#1289,,easy,,,,100k,aniskhalfallah@blink.sv,2024-08-12
Gudnessuche,issue,#1264,#1226,good-first-issue,,,,2k,???,???

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 ???
aniskhalfallah pr #1289 easy 100k aniskhalfallah@blink.sv 2024-08-12

View File

@ -25,15 +25,10 @@ export function AutowithdrawSettings ({ wallet }) {
setSendThreshold(Math.max(Math.floor(threshold / 10), 1))
}, [autoWithdrawThreshold])
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
return (
<>
<Checkbox
disabled={mounted && !wallet.isConfigured}
disabled={!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?.satsFilter !== 0 && !item.mine && item.freebie && !item.freedFreebie
const isHiddenFreebie = !me?.privates?.wildWestMode && !me?.privates?.greeterMode && !item.mine && item.freebie && !item.freedFreebie
const [collapse, setCollapse] = useState(
(isHiddenFreebie || item?.user?.meMute || (item?.outlawed && !me?.privates?.wildWestMode)) && !includeParent
? 'yep'

View File

@ -243,6 +243,9 @@ 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,7 +35,6 @@ services:
- db:/var/lib/postgresql/data
labels:
CONNECT: "localhost:5431"
cpu_shares: "${CPU_SHARES_IMPORTANT}"
app:
container_name: app
stdin_open: true
@ -59,7 +58,6 @@ services:
- ./:/app
labels:
CONNECT: "localhost:3000"
cpu_shares: "${CPU_SHARES_IMPORTANT}"
capture:
container_name: capture
build:
@ -81,7 +79,6 @@ services:
- "5678:5678"
labels:
CONNECT: "localhost:5678"
cpu_shares: "${CPU_SHARES_LOW}"
worker:
container_name: worker
build:
@ -100,7 +97,6 @@ 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
@ -117,7 +113,6 @@ services:
- "8080"
labels:
- "CONNECT=localhost:3001"
cpu_shares: "${CPU_SHARES_LOW}"
s3:
container_name: s3
image: localstack/localstack:s3-latest
@ -143,7 +138,6 @@ 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
@ -183,7 +177,6 @@ services:
echo "OpenSearch index created."
fg
'
cpu_shares: "${CPU_SHARES_LOW}"
os-dashboard:
image: opensearchproject/opensearch-dashboards:2.12.0
container_name: os-dashboard
@ -205,7 +198,6 @@ services:
- opensearch
labels:
CONNECT: "localhost:5601"
cpu_shares: "${CPU_SHARES_LOW}"
bitcoin:
image: polarlightning/bitcoind:26.0
container_name: bitcoin
@ -262,7 +254,6 @@ 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
@ -320,7 +311,6 @@ services:
--min_confs 0 --local_amt=1000000000 --push_amt=500000000
fi
"
cpu_shares: "${CPU_SHARES_MODERATE}"
stacker_lnd:
build:
context: ./docker/lnd
@ -380,7 +370,6 @@ services:
--min_confs 0 --local_amt=1000000000 --push_amt=500000000
fi
"
cpu_shares: "${CPU_SHARES_MODERATE}"
litd:
container_name: litd
build:
@ -415,7 +404,6 @@ services:
- '--loop.server.host=test.swap.lightning.today:11010'
labels:
CONNECT: "localhost:8443"
cpu_shares: "${CPU_SHARES_MODERATE}"
stacker_cln:
build:
context: ./docker/cln
@ -458,7 +446,6 @@ services:
amount=1000000000 push_msat=500000000000 minconf=0
fi
"
cpu_shares: "${CPU_SHARES_MODERATE}"
channdler:
image: mcuadros/ofelia:latest
container_name: channdler
@ -473,7 +460,6 @@ 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
@ -490,7 +476,6 @@ services:
- app
labels:
CONNECT: "localhost:8025"
cpu_shares: "${CPU_SHARES_LOW}"
nwc:
build:
context: ./docker/nwc
@ -522,7 +507,6 @@ services:
- '0'
- '--daily-limit'
- '0'
cpu_shares: "${CPU_SHARES_LOW}"
lnbits:
image: lnbits/lnbits:0.12.5
container_name: lnbits
@ -541,9 +525,6 @@ 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:

81
docs/attach-lnbits.md Normal file
View File

@ -0,0 +1,81 @@
# 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
satsFilter
greeterMode
hideCowboyHat
hideFromTopUsers
hideGithub
@ -104,7 +104,7 @@ export const SETTINGS_FIELDS = gql`
nostrCrossposting
nostrRelays
wildWestMode
satsFilter
greeterMode
nsfwMode
authMethods {
lightning

View File

@ -129,10 +129,6 @@ export const WALLET = gql`
rune
cert
}
... on WalletLNbits {
url
invoiceKey
}
}
}
}
@ -161,10 +157,6 @@ 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, HEX_REGEX } from './format'
import { msatsToSats, numWithUnits, abbrNum, ensureB64, B64_URL_REGEX } from './format'
import * as usersFragments from '@/fragments/users'
import * as subsFragments from '@/fragments/subs'
import { isInvoicableMacaroon, isInvoiceMacaroon } from './macaroon'
@ -41,14 +41,6 @@ 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',
@ -150,14 +142,6 @@ 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`
@ -595,7 +579,6 @@ 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']])
@ -637,7 +620,7 @@ export const lnAddrSchema = ({ payerData, min, max, commentAllowed } = {}) =>
return accum
}, {})))
export const lnbitsSchema = object().shape({
export const lnbitsSchema = object({
url: process.env.NODE_ENV === 'development'
? string()
.or([string().matches(/^(http:\/\/)?localhost:\d+$/), string().url()], 'invalid url')
@ -659,25 +642,8 @@ export const lnbitsSchema = object().shape({
}
return true
}),
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'
adminKey: string().length(32).required('required')
})
}),
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,
satsFilter: settings?.satsFilter,
greeterMode: settings?.greeterMode,
nsfwMode: settings?.nsfwMode,
nostrPubkey: settings?.nostrPubkey ? bech32encode(settings.nostrPubkey) : '',
nostrCrossposting: settings?.nostrCrossposting,
@ -152,11 +152,7 @@ export default function Settings ({ ssrData }) {
noReferralLinks: settings?.noReferralLinks
}}
schema={settingsSchema}
onSubmit={async ({
tipDefault, tipRandom, tipRandomMin, tipRandomMax, withdrawMaxFeeDefault,
zapUndos, zapUndosEnabled, nostrPubkey, nostrRelays, satsFilter,
...values
}) => {
onSubmit={async ({ tipDefault, tipRandom, tipRandomMin, tipRandomMax, withdrawMaxFeeDefault, zapUndos, zapUndosEnabled, nostrPubkey, nostrRelays, ...values }) => {
if (nostrPubkey.length === 0) {
nostrPubkey = null
} else {
@ -176,7 +172,6 @@ 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,
@ -472,27 +467,7 @@ export default function Settings ({ ssrData }) {
label={<>don't create referral links on copy</>}
name='noReferralLinks'
/>
<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>}
/>
<div className='form-label'>content</div>
<Checkbox
label={
<div className='d-flex align-items-center'>wild west mode
@ -507,6 +482,21 @@ 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,11 +57,15 @@ 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)
toaster.danger(err.message || err.toString?.())
const message = 'failed to attach: ' + err.message || err.toString?.()
toaster.danger(message)
}
}}
>
@ -102,12 +106,12 @@ export default function WalletSettings () {
function WalletFields ({ wallet: { config, fields, isConfigured } }) {
return fields
.map(({ name, label, type, help, optional, editable, clientOnly, serverOnly, ...props }, i) => {
.map(({ name, label, type, help, optional, editable, ...props }, i) => {
const rawProps = {
...props,
name,
initialValue: config?.[name],
readOnly: isConfigured && editable === false && !!config?.[name],
readOnly: isConfigured && editable === false,
groupClassName: props.hidden ? 'd-none' : undefined,
label: label
? (

View File

@ -1,24 +0,0 @@
-- 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

@ -1,14 +0,0 @@
-- 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

@ -1,4 +0,0 @@
-- 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)
satsFilter Int @default(10)
greeterMode Boolean @default(false)
nsfwMode Boolean @default(false)
fiatCurrency String @default("USD")
withdrawMaxFeeDefault Int @default(10)
@ -66,7 +66,6 @@ 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")
@ -168,7 +167,6 @@ enum WalletType {
LIGHTNING_ADDRESS
LND
CLN
LNBITS
}
model Wallet {
@ -192,7 +190,6 @@ model Wallet {
walletLightningAddress WalletLightningAddress?
walletLND WalletLND?
walletCLN WalletCLN?
walletLNbits WalletLNbits?
withdrawals Withdrawl[]
@@index([userId])
@ -241,16 +238,6 @@ 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
@ -438,7 +425,6 @@ 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)
@ -503,7 +489,6 @@ 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 useClientConfig from '@/components/use-local-state'
import useLocalConfig from '@/components/use-local-state'
import { useWalletLogger } from '@/components/wallet-logger'
import { SSR } from '@/lib/constants'
import { bolt11Tags } from '@/lib/bolt11'
@ -12,7 +12,6 @@ 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',
@ -33,22 +32,6 @@ 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
@ -66,40 +49,53 @@ 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 }, { logger })
await saveConfig({ ...config, priority })
} catch (err) {
toaster.danger(`failed to change priority of ${wallet.name} wallet: ${err.message}`)
}
}
}, [wallet, config, toaster])
}, [wallet, config, logger, toaster])
const save = useCallback(async (newConfig) => {
try {
// 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 {
validConfig = await wallet.testConnectClient?.(newConfig, { me, logger })
const validConfig = await wallet.testConnectClient?.(newConfig, { me, logger })
await saveConfig(validConfig ?? newConfig)
logger.ok(_isConfigured ? 'wallet updated' : 'wallet attached')
} catch (err) {
logger.error(err.message)
const message = err.message || err.toString?.()
logger.error('failed to attach: ' + message)
throw err
}
await saveConfig(validConfig ?? newConfig, { logger })
}, [saveConfig, me, logger])
}, [_isConfigured, saveConfig, me, logger])
// delete is a reserved keyword
const delete_ = useCallback(async () => {
try {
await clearConfig({ logger })
await clearConfig()
logger.ok('wallet detached')
disable()
} catch (err) {
const message = err.message || err.toString?.()
logger.error(message)
throw err
}
}, [clearConfig, logger, disablePayments])
}, [clearConfig, logger, disable])
if (!wallet) return null
@ -110,8 +106,11 @@ export function useWallet (name) {
save,
delete: delete_,
deleteLogs,
enable,
disable,
setPriority,
hasConfig,
isConfigured: _isConfigured,
status,
enabled,
priority,
@ -119,111 +118,34 @@ 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 [clientConfig, setClientConfig, clearClientConfig] = useClientConfig(storageKey)
const [localConfig, setLocalConfig, clearLocalConfig] = useLocalConfig(storageKey)
const [serverConfig, setServerConfig, clearServerConfig] = useServerConfig(wallet)
const hasClientConfig = !!wallet?.sendPayment
const hasLocalConfig = !!wallet?.sendPayment
const hasServerConfig = !!wallet?.walletType
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 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 : {})
}
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 saveConfig = useCallback(async (config) => {
if (hasLocalConfig) setLocalConfig(config)
if (hasServerConfig) await setServerConfig(config)
}, [wallet])
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')
}
const clearConfig = useCallback(async () => {
if (hasLocalConfig) clearLocalConfig()
if (hasServerConfig) await clearServerConfig()
}, [hasClientConfig, hasServerConfig, clearClientConfig, clearServerConfig, wallet])
}, [wallet])
return [config, saveConfig, clearConfig]
}
@ -252,8 +174,6 @@ function useServerConfig (wallet) {
enabled: data?.walletByType?.enabled,
...data?.walletByType?.wallet
}
delete serverConfig.__typename
const autowithdrawSettings = autowithdrawInitial({ me })
const config = { ...serverConfig, ...autowithdrawSettings }
@ -269,8 +189,8 @@ function useServerConfig (wallet) {
return await client.mutate({
mutation,
variables: {
...config,
id: walletId,
...config,
settings: {
autoWithdrawThreshold: Number(autoWithdrawThreshold),
autoWithdrawMaxFeePercent: Number(autoWithdrawMaxFeePercent),
@ -286,9 +206,6 @@ 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,
@ -307,9 +224,7 @@ function generateMutation (wallet) {
const resolverName = generateResolverName(wallet.walletField)
let headerArgs = '$id: ID, '
headerArgs += wallet.fields
.filter(isServerField)
.map(f => {
headerArgs += wallet.fields.map(f => {
let arg = `$${f.name}: String`
if (!f.optional) {
arg += '!'
@ -319,9 +234,7 @@ function generateMutation (wallet) {
headerArgs += ', $settings: AutowithdrawSettings!'
let inputArgs = 'id: $id, '
inputArgs += wallet.fields
.filter(isServerField)
.map(f => `${f.name}: $${f.name}`).join(', ')
inputArgs += wallet.fields.map(f => `${f.name}: $${f.name}`).join(', ')
inputArgs += ', settings: $settings'
return gql`mutation ${resolverName}(${headerArgs}) {

View File

@ -1,27 +0,0 @@
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, invoiceKey }, { logger }) {
export async function testConnectClient ({ url, adminKey }, { logger }) {
logger.info('trying to fetch wallet')
url = url.replace(/\/+$/, '')
await getWallet({ url, adminKey, invoiceKey })
await getWallet({ url, adminKey })
logger.ok('wallet found')
}
@ -23,13 +23,13 @@ export async function sendPayment (bolt11, { url, adminKey }) {
return { preimage }
}
async function getWallet ({ url, adminKey, invoiceKey }) {
async function getWallet ({ url, adminKey }) {
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 || invoiceKey)
headers.append('X-Api-Key', adminKey)
const res = await fetch(url + path, { method: 'GET', headers })
if (!res.ok) {

View File

@ -8,32 +8,17 @@ 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',
optional: 'for sending',
clientOnly: true,
editable: false
type: 'password'
}
]
export const card = {
title: 'LNbits',
subtitle: 'use [LNbits](https://lnbits.com/) for payments',
badges: ['send & receive']
badges: ['send only']
}
export const fieldValidation = lnbitsSchema
export const walletType = 'LNBITS'
export const walletField = 'walletLNbits'

View File

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