Compare commits

..
20 Commits
Author SHA1 Message Date
Arc 962a09bb30 fundle 2026-01-29 18:21:05 +00:00
Arc 92c86eb837 make 2026-01-29 18:17:07 +00:00
Arc dadff18a9c feat: route whitelist/blacklist
security feature middleware to block/allow routes
2026-01-29 18:11:28 +00:00
Arc 87c1684a63 init 2026-01-29 17:45:44 +00:00
Vlad StanandGitHub 91354f8be4 feat: wallet featured button (#3740) 2026-01-29 10:49:19 +02:00
Vlad StanandGitHub 5f5d45e89f [feat] persist user ui customization (#3743) 2026-01-29 10:35:05 +02:00
dni ⚡andGitHub 52bb125a25 chore: remove ecdsa in favor of coincurve (#3746) 2026-01-29 10:02:16 +02:00
dni ⚡andGitHub 2ca1ed897c CI: use uv for publishing on pypi (#3745) 2026-01-29 08:54:42 +01:00
Vlad StanandGitHub 631f4e8455 [feat] admin impersonate user (#3741) 2026-01-28 16:59:23 +01:00
Vlad StanandGitHub b22f88b264 fix: DB migration conflict (#3739) 2026-01-27 08:31:38 +02:00
dni ⚡andGitHub c08bc02930 chore: update python packages (#3707) 2026-01-26 08:54:37 +01:00
Vlad StanandGitHub e020a7c5a0 feat: allow text upload (#3738) 2026-01-26 09:43:52 +02:00
Vlad StanandGitHub d2e8914835 fix: better validation error messages (#3736) 2026-01-21 14:28:57 +02:00
dni ⚡andGitHub 5e79ca35ab chore: remove windowMixin from components and pages (#3632) 2026-01-21 11:26:45 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4690d178a2 chore(deps): bump cryptography from 42.0.8 to 44.0.1 (#3735)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-21 11:02:28 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c5825aa4c3 chore(deps): bump aiohttp from 3.12.15 to 3.13.3 (#3734)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-20 17:27:12 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ebe0c4e3c3 chore(deps): bump urllib3 from 2.5.0 to 2.6.3 (#3733)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-20 17:26:45 +02:00
Djuri BaarsandGitHub 383bdb6312 [fix] Fix missing timezone information 2026-01-20 17:26:02 +02:00
a947686d79 [feat] Update extension builder for 1.4.1 (#3725)
Co-authored-by: dni  <office@dnilabs.com>
2026-01-20 14:47:54 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
28e9d32b44 chore(deps): bump werkzeug from 3.1.3 to 3.1.5 (#3732)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-20 14:46:06 +02:00
108 changed files with 2480 additions and 5277 deletions
+1 -14
View File
@@ -6,10 +6,6 @@
# The following settings are ONLY set in your .env file.
# They are NOT managed by the Admin UI and are not stored in the database.
# === First Install Token ===
# if set the user is required to enter this token on the /first_install page
# FIRST_INSTALL_TOKEN="myaccesstoken"
# === Logging and Development ===
DEBUG=False
@@ -59,7 +55,7 @@ LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos"
# LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx
# which fundingsources are allowed in the admin ui
# LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet, NWCWallet, BreezSdkWallet, BoltzWallet, StrikeWallet, CLNRestWallet, SparkWallet, SparkL2Wallet"
# LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet, NWCWallet, BreezSdkWallet, BoltzWallet, StrikeWallet, CLNRestWallet"
# uvicorn variable, allow https behind a proxy
# IMPORTANT: this also needs the webserver to be configured to forward the headers
@@ -191,15 +187,6 @@ BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon"
# HEXSTRING instead of path also possible
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
# SparkL2Wallet (external sidecar: https://github.com/lnbits/spark_sidecar)
SPARK_L2_NETWORK=MAINNET
SPARK_L2_EXTERNAL_ENDPOINT=http://127.0.0.1:8765
SPARK_L2_EXTERNAL_API_KEY=
# optional tuning
# SPARK_L2_PAY_WAIT_MS=4000
# SPARK_L2_PAY_POLL_MS=500
# SPARK_L2_STREAM_KEEPALIVE_MS=15000
# StrikeWallet
STRIKE_API_ENDPOINT=https://api.strike.me/v1
STRIKE_API_KEY=YOUR_STRIKE_API_KEY
+2 -12
View File
@@ -55,21 +55,11 @@ jobs:
- name: Build and push boltz
uses: docker/build-push-action@v5
with:
context: docker/boltzclient
context: .
file: Dockerfile.boltz
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits-boltz:${{ inputs.tag }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
build-args: LNBITS_TAG=${{ inputs.tag }}
- name: Build and push sparkl2
uses: docker/build-push-action@v5
with:
context: docker/sparkl2
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits-sparkl2:${{ inputs.tag }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
build-args: LNBITS_TAG=${{ inputs.tag }}
+9 -8
View File
@@ -38,7 +38,8 @@ jobs:
- name: Setup Regtest
run: |
cd docker/regtest
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
cd docker
chmod +x ./start-regtest
./start-regtest
sudo chmod -R a+rwx .
@@ -49,16 +50,16 @@ jobs:
LNBITS_DATABASE_URL: ${{ inputs.db-url }}
LNBITS_BACKEND_WALLET_CLASS: ${{ inputs.backend-wallet-class }}
LND_REST_ENDPOINT: https://localhost:8081/
LND_REST_CERT: ./docker/regtest/data/lnd-3/tls.cert
LND_REST_MACAROON: ./docker/regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
LND_REST_CERT: ./docker/data/lnd-3/tls.cert
LND_REST_MACAROON: ./docker/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
LND_GRPC_ENDPOINT: localhost
LND_GRPC_PORT: 10009
LND_GRPC_CERT: ./docker/regtest/data/lnd-3/tls.cert
LND_GRPC_MACAROON: ./docker/regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
CORELIGHTNING_RPC: ./docker/regtest/data/clightning-1/regtest/lightning-rpc
LND_GRPC_CERT: docker/data/lnd-3/tls.cert
LND_GRPC_MACAROON: docker/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
CORELIGHTNING_RPC: ./docker/data/clightning-1/regtest/lightning-rpc
CORELIGHTNING_REST_URL: https://localhost:3001
CORELIGHTNING_REST_MACAROON: ./docker/regtest/data/clightning-2-rest/access.macaroon
CORELIGHTNING_REST_CERT: ./docker/regtest/data/clightning-2-rest/certificate.pem
CORELIGHTNING_REST_MACAROON: ./docker/data/clightning-2-rest/access.macaroon
CORELIGHTNING_REST_CERT: ./docker/data/clightning-2-rest/certificate.pem
LNBITS_ENDPOINT: http://localhost:5001
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
ECLAIR_URL: http://127.0.0.1:8082
+1
View File
@@ -42,6 +42,7 @@ lnbits/static/bundle.min.js.old
lnbits/static/bundle.min.css.old
lnbits/static/bundle-components.min.js.old
lnbits/upgrades
docker
# Nix
*result*
@@ -14,13 +14,11 @@ ENV PATH="/root/.local/bin:$PATH"
# Reinstall dependencies just in case (needed for CMD usage)
RUN uv sync --all-extras
# LNbits
# LNbits + boltzd configuration
ENV LNBITS_PORT="5000"
ENV LNBITS_HOST="0.0.0.0"
ENV LNBITS_BACKEND_WALLET_CLASS="BoltzWallet"
ENV FUNDING_SOURCE_MAX_RETRIES=10
# Boltzd
ENV BOLTZ_CLIENT_ENDPOINT="127.0.0.1:9002"
ENV BOLTZ_CLIENT_MACAROON="/root/.boltz/macaroons/admin.macaroon"
ENV BOLTZ_CLIENT_CERT="/root/.boltz/tls.cert"
@@ -28,7 +26,8 @@ ENV BOLTZ_CLIENT_WALLET="lnbits"
EXPOSE 5000
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Entrypoint to start boltzd and LNbits
COPY dockerboltz.sh /dockerboltz.sh
RUN chmod +x /dockerboltz.sh
CMD ["/entrypoint.sh"]
CMD ["/dockerboltz.sh"]
-18
View File
@@ -1,18 +0,0 @@
!data
data/*
!data/boltz
data/boltz/*
!data/boltz/boltz.conf
!data/boltz-client
data/boltz-client/*
!data/boltz-client/boltz.toml
!data/boltz-nginx
data/boltz-nginx/*
!data/boltz-nginx/default.conf
!data/eclair
data/eclair/*
!data/eclair/eclair.conf
-99
View File
@@ -1,99 +0,0 @@
![TESTS](https://github.com/lnbits/legend-regtest-enviroment/actions/workflows/ci.yml/badge.svg)
# nodes
- lnd-1: for locally testing your current lnbits
- lnd-2: used for boltz backend
- lnd-3: used for lnbits inside docker
- cln-1: for locally testing your current lnbits
- cln-2: used for clightning-REST
- eclair-1: for locally testing your current lnbits
# Installing regtest
get the regtest enviroment ready
```sh
# Install docker https://docs.docker.com/engine/install/
# Make sure your user has permission to use docker 'sudo usermod -aG docker ${USER}' then reboot
# Stop/start docker 'sudo systemctl stop docker' 'sudo systemctl start docker'
sudo apt install jq
git clone https://github.com/lnbits/lnbits.git
cd lnbits
docker build -t lnbits/lnbits .
mkdir docker
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
cd docker
chmod +x ./start-regtest
./start-regtest # start the regtest and also run tests
sudo chown -R $USER ./data # Give the data file permissions for user
```
# Running LNbits on regtest
add this ENV variables to your `.env` file
```sh
DEBUG=true
# LND
LNBITS_BACKEND_WALLET_CLASS="LndRestWallet"
LND_REST_ENDPOINT=https://127.0.0.1:8081/
LND_REST_CERT=/home/user/repos/lnbits/docker/data/lnd-1/tls.cert
LND_REST_MACAROON=/home/user/repos/lnbits/docker/data/lnd-1/data/chain/bitcoin/regtest/admin.macaroon
# CLN
LNBITS_BACKEND_WALLET_CLASS="CoreLightningWallet"
CORELIGHTNING_RPC=./docker/data/clightning-1/regtest/lightning-rpc
# Run LNbits
uv run lnbits
# Run LNbits with hot reload
make dev
```
# testing
```sh
chmod +x ./start-regtest
./start-regtest
# short answer :)
./start-regtest && echo "PASSED" || echo "FAILED" > /dev/null
```
usage of the `bitcoin-cli-sim`, `lightning-cli-sim` and `lncli-sim` aliases
```sh
cd ~/lnbits/docker
source docker-scripts.sh
# use bitcoin core, mine a block
bitcoin-cli-sim -generate 1
# use c-lightning nodes
lightning-cli-sim 1 newaddr | jq -r '.bech32' # use node 1
lightning-cli-sim 2 getinfo # use node 2
lightning-cli-sim 3 getinfo # use node 3
# use lnd nodes
lncli-sim 1 newaddr p2wsh
lncli-sim 2 listpeers
```
# urls
- mempool: http://localhost:8080/
- boltz api: http://localhost:9001/
- lnd-1 rest: http://localhost:8081/
- lnbits: http://localhost:5001/
# debugging docker logs
```sh
docker logs lnbits-lnbits-1 -f
docker logs lnbits-boltz-1 -f
docker logs lnbits-clightning-1-1 -f
docker logs lnbits-lnd-2-1 -f
```
@@ -1,49 +0,0 @@
standalone = true
network = "regtest"
# Path the the log file
logfile = ""
electrumUrl = "electrs:19001"
electrumLiquidUrl = "electrs-liquid:19002"
[BOLTZ]
# By default the daemon automatically connects to the official Boltz instance for the network LND is on
# This value is used to override that
url = "http://boltz-nginx:9001"
[DATABASE]
# Path to the SQLite database file
# path = "/home/michael/test.db"
[RPC]
# Host of
host = "0.0.0.0"
# Port of the gRPC interface
port = 9002
# Whether the REST proxy for the gRPC interface should be disabled
restDisabled = false
# Host of the REST proxy
restHost = "0.0.0.0"
# Port of the REST proxy
restPort = 9003
# Path to the TLS cert for the gRPC and REST interface
tlsCert = ""
# Path to the TLS private key for the gRPC and REST interface
tlsKey = ""
noTls = true
# Whether the macaroon authentication for the gRPC and REST interface should be disabled
noMacaroons = true
# Path to the admin macaroon for the gRPC and REST interface
adminMacaroonPath = ""
# Path to the read only macaroon for the gRPC and REST interface
readOnlyMacaroonPath = ""
@@ -1,49 +0,0 @@
upstream boltz {
server boltz:9001;
}
upstream boltzr {
server boltz:9005;
}
upstream ws {
server boltz:9004;
}
server {
listen 9001;
listen [::]:9001;
server_name localhost;
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods 'GET, PATCH, DELETE, POST, OPTIONS' always;
add_header Access-Control-Allow-Headers "*" always;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
if ($request_method = OPTIONS) {
return 204;
}
location /v2/ws {
proxy_pass http://ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location ~ ^/v2/(lightning|swap/rescue|swap/restore) {
proxy_pass http://boltzr;
}
location /streamswapstatus {
proxy_pass http://boltzr;
}
location / {
proxy_pass http://boltz;
}
}
-114
View File
@@ -1,114 +0,0 @@
[api]
host = "0.0.0.0"
port = 9_001
[grpc]
host = "0.0.0.0"
port = 9_000
[postgres]
host = "boltz-postgres"
port = 5432
database = "boltz"
username = "boltz"
password = "boltz"
[sidecar]
[sidecar.grpc]
host = "127.0.0.1"
port = 9003
[sidecar.ws]
host = "0.0.0.0"
port = 9004
[sidecar.api]
host = "0.0.0.0"
port = 9005
[swap]
deferredClaimSymbols = ["BTC", "L-BTC"]
[[pairs]]
base = "BTC"
quote = "BTC"
rate = 1
fee = 0.5
swapInFee = 0.1
maxSwapAmount = 40_294_967
minSwapAmount = 50_000
[pairs.timeoutDelta]
chain = 1440
reverse = 1440
swapMinimal = 1440
swapMaximal = 2880
swapTaproot = 10080
[[pairs]]
base = "L-BTC"
quote = "BTC"
fee = 0.25
swapInFee = 0.1
rate = 1
maxSwapAmount = 40_294_967
minSwapAmount = 100
[pairs.submarineSwap]
minSwapAmount = 1_000
minBatchedAmount = 21
[pairs.chainSwap]
minSwapAmount = 25_000
[pairs.timeoutDelta]
chain = 1440
reverse = 1440
swapMinimal = 1440
swapMaximal = 2880
swapTaproot = 10080
[[currencies]]
symbol = "BTC"
network = "bitcoinRegtest"
minWalletBalance = 10_000_000
minChannelBalance = 10_000_000
maxSwapAmount = 40_294_967
minSwapAmount = 10_000
maxZeroConfAmount = 0
[currencies.chain]
# mempoolSpace = "http://mempool-web:8090/api"
host = "bitcoind"
zmqpubrawtx = "tcp://bitcoind:29000"
zmqpubrawblock = "tcp://bitcoind:29001"
port = 18_443
cookie = "/root/.bitcoin/regtest/.cookie"
feeFloor = 0.2
wallet = "lnbits"
[currencies.lnd]
host = "lnd-2"
port = 10_009
certpath = "/data/lnd/tls.cert"
macaroonpath = "/data/lnd/data/chain/bitcoin/regtest/admin.macaroon"
[liquid]
symbol = "L-BTC"
network = "liquidRegtest"
maxSwapAmount = 40_294_967
minSwapAmount = 10_000
maxZeroConfAmount = 40_294_967
[liquid.chain]
host = "elementsd"
port = 18884
cookie = "/root/.elements/liquidregtest/.cookie"
zmqpubrawtx = "tcp://elementsd:31000"
zmqpubhashblock = "tcp://elementsd:31002"
wallet = "lnbits"
-28
View File
@@ -1,28 +0,0 @@
eclair {
chain = "regtest"
api {
binding-ip = "0.0.0.0"
enabled = true
port = 8080
password = "lnbits"
}
bitcoind {
host = "bitcoind"
rpcport = 18443
auth = "safecookie"
cookie = "/root/.bitcoin/regtest/.cookie"
zmqblock = "tcp://bitcoind:29002"
zmqtx = "tcp://bitcoind:29000"
}
channel {
max-funding-satoshis = 10000000000
}
features {
option_support_large_channel = mandatory
}
}
-434
View File
@@ -1,434 +0,0 @@
services:
lnbits:
hostname: lnbits
depends_on:
- lnd-3
image: lnbits/lnbits
restart: on-failure
user: "0:0"
environment:
LNBITS_PORT: 5001
DEBUG: true
LNBITS_ADMIN_UI: false
LNBITS_BACKEND_WALLET_CLASS: "LndRestWallet"
LNBITS_DATA_FOLDER: "./data"
LND_REST_ENDPOINT: "https://lnd-3:8081/"
LND_REST_CERT: "./lnd/tls.cert"
LND_REST_MACAROON: "./lnd/data/chain/bitcoin/regtest/admin.macaroon"
ports:
- 5001:5001
volumes:
- lnbits-data:/app/data
- ./data/lnd-3:/app/lnd:uid=1000,gid=1000
boltz:
hostname: boltz
depends_on:
- lnd-2
- boltz-postgres
restart: always
image: boltz/boltz:v3.12.1
ports:
- 9000:9000
entrypoint: "sh -c 'sleep 30; /boltz-backend/bin/boltzd'"
volumes:
- ./data/lnd-2:/data/lnd/
- ./data/boltz/:/root/.boltz/
- elements-data:/root/.elements
- bitcoin-data:/root/.bitcoin
boltz-client:
hostname: boltz-client
depends_on:
- boltz
restart: always
image: boltz/boltz-client:latest
ports:
- 9002:9002
- 9003:9003
expose:
- 9002
healthcheck:
test: ['CMD', 'boltzcli', '--host', 'boltz-client', 'getinfo']
interval: 5s
timeout: 3s
retries: 10
start_period: 0s
volumes:
- elements-data:/root/.elements
- ./data/boltz-client:/root/.boltz
boltz-backend-nginx:
hostname: boltz-nginx
restart: always
image: nginx:latest
ports:
- 9001:9001
volumes:
- nginx-data:/etc/nginx/conf.d
healthcheck:
test: ['CMD-SHELL', 'curl http://localhost:9001/version']
timeout: 1s
retries: 10
interval: 1s
start_period: 0s
boltz-postgres:
hostname: boltz-postgres
restart: always
image: postgres:14-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready --dbname boltz --username boltz"]
interval: 5s
timeout: 30s
retries: 10
start_period: 5s
environment:
- POSTGRES_DB=boltz
- POSTGRES_USER=boltz
- POSTGRES_PASSWORD=boltz
expose:
- 5432
bitcoind:
hostname: bitcoind
image: boltz/bitcoin-core:25.0
command:
- -regtest
- -fallbackfee=0.00000253
- -zmqpubrawtx=tcp://0.0.0.0:29000
- -zmqpubrawblock=tcp://0.0.0.0:29001
- -zmqpubhashblock=tcp://0.0.0.0:29002
- -txindex
- -rpcallowip=0.0.0.0/0
- -rpcbind=0.0.0.0
- -addresstype=bech32
- -changetype=bech32
- -dbcache=2048
- -rpcworkqueue=256
volumes:
- bitcoin-data:/root/.bitcoin
expose:
- 29000
- 29001
- 29002
- 18443
- 18444
healthcheck:
test:
[
"CMD",
"bitcoin-cli",
"--rpccookiefile=/root/.bitcoin/regtest/.cookie",
"-regtest",
"getblockchaininfo",
]
timeout: 1s
retries: 1
interval: 1s
start_period: 0s
clightning-1:
hostname: clightning-1
depends_on:
- bitcoind
image: boltz/c-lightning:24.11
command:
- --large-channels
- --network=regtest
- --grpc-port=9736
- --bind-addr=0.0.0.0:9735
- --bitcoin-rpcconnect=bitcoind
- --bitcoin-rpcport=18443
- --clnrest-host=0.0.0.0
- --clnrest-port=3010
expose:
- 9735
ports:
- 9736:9736
- 3010:3010
volumes:
- ./data/clightning-1:/root/.lightning/
- bitcoin-data:/root/.bitcoin
clightning-2:
hostname: clightning-2
depends_on:
- bitcoind
image: boltz/c-lightning:22.11.1
command:
- --large-channels
- --network=regtest
- --grpc-port=9737
- --bind-addr=0.0.0.0:9735
- --bitcoin-rpcconnect=bitcoind
- --bitcoin-rpcport=18443
expose:
- 9735
ports:
- 9737:9737
volumes:
- ./data/clightning-2:/root/.lightning/
- bitcoin-data:/root/.bitcoin
clightning-2-rest:
hostname: clightning-2-rest
depends_on:
- clightning-2
image: saubyk/c-lightning-rest:0.10.7
entrypoint: "sh -c 'sleep 35 && /sbin/tini -g -- ./docker-entrypoint.sh'"
ports:
- 3001:3001
expose:
- 3001
volumes:
- ./data/clightning-2:/root/.lightning/:uid=1000,gid=1000
- ./data/clightning-2-rest:/usr/src/app/certs/
- bitcoin-data:/root/.bitcoin
clightning-3:
hostname: clightning-3
depends_on:
- bitcoind
image: boltz/c-lightning:24.11
command:
- --large-channels
- --network=regtest
- --grpc-port=9738
- --bind-addr=0.0.0.0:9735
- --bitcoin-rpcconnect=bitcoind
- --bitcoin-rpcport=18443
expose:
- 9735
ports:
- 9738:9738
volumes:
- ./data/clightning-3:/root/.lightning/
- bitcoin-data:/root/.bitcoin
lnd-1:
hostname: lnd-1
depends_on:
- bitcoind
image: boltz/lnd:0.18.4-beta
restart: on-failure
command:
- --listen=lnd-1:9735
- --rpclisten=lnd-1:10009
- --restlisten=lnd-1:8081
- --bitcoin.active
- --bitcoin.regtest
- --bitcoin.node=bitcoind
- --bitcoind.rpchost=bitcoind
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
- --bitcoind.zmqpubrawtx=bitcoind:29000
- --bitcoind.zmqpubrawblock=bitcoind:29001
- --noseedbackup
- --protocol.wumbo-channels
expose:
- 8081
- 9735
- 10009
volumes:
- ./data/lnd-1:/root/.lnd/
- bitcoin-data:/root/.bitcoin
lnd-2:
hostname: lnd-2
depends_on:
- bitcoind
image: boltz/lnd:0.19.3-beta
restart: on-failure
command:
- --listen=lnd-2:9735
- --rpclisten=lnd-2:10009
- --restlisten=lnd-2:8081
- --bitcoin.active
- --bitcoin.regtest
- --bitcoin.node=bitcoind
- --bitcoind.rpchost=bitcoind
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
- --bitcoind.zmqpubrawtx=bitcoind:29000
- --bitcoind.zmqpubrawblock=bitcoind:29001
- --noseedbackup
- --protocol.wumbo-channels
expose:
- 8081
- 9735
- 10009
volumes:
- ./data/lnd-2:/root/.lnd/
- bitcoin-data:/root/.bitcoin
lnd-3:
hostname: lnd-3
depends_on:
- bitcoind
image: boltz/lnd:0.18.4-beta
restart: on-failure
command:
- --listen=lnd-3:9735
- --rpclisten=lnd-3:10009
- --restlisten=lnd-3:8081
- --bitcoin.active
- --bitcoin.regtest
- --bitcoin.node=bitcoind
- --bitcoind.rpchost=bitcoind
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
- --bitcoind.zmqpubrawtx=bitcoind:29000
- --bitcoind.zmqpubrawblock=bitcoind:29001
- --noseedbackup
- --protocol.wumbo-channels
ports:
- 8081:8081
- 10009:10009
expose:
- 8081
- 9735
- 10009
volumes:
- ./data/lnd-3:/root/.lnd/
- bitcoin-data:/root/.bitcoin
lnd-4:
hostname: lnd-4
depends_on:
- bitcoind
image: boltz/lnd:0.18.4-beta
restart: on-failure
command:
- --listen=lnd-4:9735
- --rpclisten=lnd-4:10009
- --restlisten=lnd-4:8081
- --bitcoin.active
- --bitcoin.regtest
- --bitcoin.node=bitcoind
- --bitcoind.rpchost=bitcoind
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
- --bitcoind.zmqpubrawtx=bitcoind:29000
- --bitcoind.zmqpubrawblock=bitcoind:29001
- --noseedbackup
- --protocol.wumbo-channels
expose:
- 8081
- 9735
- 10009
volumes:
- ./data/lnd-4:/root/.lnd/
- bitcoin-data:/root/.bitcoin
eclair:
hostname: eclair
depends_on:
- bitcoind
image: boltz/eclair:0.8.0
restart: on-failure
entrypoint: "sh -c 'JAVA_OPTS=-Xmx512m /eclair-node/bin/eclair-node.sh -Declair.datadir=/root/eclair -Declair.printToConsole'"
ports:
- 8082:8080
expose:
- 9735
- 8080
volumes:
- ./data/eclair:/root/eclair
- bitcoin-data:/root/.bitcoin
electrs:
hostname: electrs
restart: always
image: boltz/electrs:latest
entrypoint: ["electrs-bitcoin"]
command:
- --electrum-rpc-addr
- electrs:19001
- --http-addr
- electrs:3002
- --daemon-rpc-addr
- bitcoind:18443
- --network
- regtest
- --jsonrpc-import
healthcheck:
test: ["CMD-SHELL", "curl -s $(hostname):3002/blocks/tip/height"]
timeout: 1s
retries: 20
interval: 2s
start_period: 5s
ports:
- 19001:19001
- 3002:3002
volumes:
- bitcoin-data:/root/.bitcoin
elementsd:
hostname: elementsd
restart: always
image: boltz/elements:latest
expose:
- 31001
ports:
- 31000:31000
- 31002:31002
- 18884:18884
command:
- -chain=liquidregtest
- -txindex=1
- -rest=1
- -server=1
- -rpcallowip=0.0.0.0/0
- -validatepegin=0
- -initialfreecoins=2100000000000000
- -fallbackfee=0.000001
- -rpcbind=0.0.0.0
- -rpcport=18884
- -zmqpubrawtx=tcp://0.0.0.0:31000
- -zmqpubrawblock=tcp://0.0.0.0:31001
- -zmqpubhashblock=tcp://0.0.0.0:31002
- -acceptdiscountct=1
- -creatediscountct=1
volumes:
- elements-data:/root/.elements
electrs-liquid:
hostname: electrs-liquid
restart: always
image: boltz/electrs:latest
entrypoint: ["electrs-liquid"]
command:
- --electrum-rpc-addr
- electrs-liquid:19002
- --http-addr
- electrs-liquid:3003
- --daemon-rpc-addr
- elementsd:18884
- --daemon-dir
- /root/.elements
- --network
- liquidregtest
- --parent-network
- regtest
- --jsonrpc-import
healthcheck:
test: ["CMD-SHELL", "curl -s $(hostname):3003/blocks/tip/height"]
timeout: 15s
retries: 20
interval: 2s
start_period: 5s
ports:
- 19002:19002
- 3003:3003
volumes:
- elements-data:/root/.elements
volumes:
lnbits-data:
bitcoin-data:
elements-data:
nginx-data:
name: nginx-data
driver: local
driver_opts:
type: none
o: bind
device: ./data/boltz-nginx/
-322
View File
@@ -1,322 +0,0 @@
#!/bin/sh
export COMPOSE_PROJECT_NAME=lnbits
boltzcli-sim() {
docker exec -it lnbits-boltz-client-1 boltzcli "$@"
}
bitcoin-cli-sim() {
docker exec lnbits-bitcoind-1 bitcoin-cli -regtest "$@"
}
elements-cli-sim() {
docker exec lnbits-elementsd-1 elements-cli -rpcport=18884 -chain=liquidregtest "$@"
}
# args(i, cmd)
lightning-cli-sim() {
i=$1
shift # shift first argument so we can use $@
docker exec lnbits-clightning-$i-1 lightning-cli --network regtest "$@"
}
# args(i, cmd)
lncli-sim() {
i=$1
shift # shift first argument so we can use $@
docker exec lnbits-lnd-$i-1 lncli --network regtest --rpcserver=lnd-$i:10009 "$@"
}
get-eclair-pubkey() {
while true; do
pubkey=$(docker exec lnbits-eclair-1 curl http://localhost:8080/getinfo -X POST -s -u :lnbits | jq -r .nodeId 2> /dev/null)
pubkeyPrefix=$(echo $pubkey | cut -c1,2)
if [[ "$pubkeyPrefix" == "02" || "$pubkeyPrefix" == "03" ]]; then
echo $pubkey
break
fi
sleep 1
done
}
wait-for-eclair-channel() {
while true; do
state=$(docker exec lnbits-eclair-1 curl http://localhost:8080/channels -X POST -s -u :lnbits | jq -r ".[0].state")
pending=$(docker exec lnbits-eclair-1 curl -s http://localhost:8080/channels -X POST -u :lnbits| jq '. | length')
echo "eclair-1 pendingchannels: $pending, current state: $state"
if [[ "$state" == "NORMAL" ]]; then
break
fi
sleep 1
done
}
# args(i)
fund_boltz_client() {
# first address of seed: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
address="el1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f3mmz5l7uw5pqmx6xf5xy50hsn6vhkm5euwt72x878eq6zxx2z0z676mna6kdq"
echo "funding: $address on boltz-client"
elements-cli-sim -named sendtoaddress address=$address amount=30 fee_rate=100 > /dev/null
}
# args(i)
fund_clightning_node() {
address=$(lightning-cli-sim $1 newaddr | jq -r .bech32)
echo "funding: $address on clightning-node: $1"
bitcoin-cli-sim -named sendtoaddress address=$address amount=30 fee_rate=100 > /dev/null
}
# args(i)
fund_lnd_node() {
address=$(lncli-sim $1 newaddress p2wkh | jq -r .address)
echo "funding: $address on lnd-node: $1"
bitcoin-cli-sim -named sendtoaddress address=$address amount=30 fee_rate=100 > /dev/null
}
# args(i, j)
connect_clightning_node() {
pubkey=$(lightning-cli-sim $2 getinfo | jq -r '.id')
lightning-cli-sim $1 connect $pubkey@lnbits-clightning-$2-1:9735 | jq -r '.id'
}
lnbits-regtest-start(){
if ! command -v jq &> /dev/null
then
echo "jq is not installed"
exit
fi
if ! command -v docker &> /dev/null
then
echo "docker is not installed"
exit
fi
if ! command -v docker version &> /dev/null
then
echo "dockerd is not running"
exit
fi
lnbits-regtest-stop
docker compose up -d --remove-orphans
lnbits-regtest-init
}
lnbits-regtest-start-log(){
lnbits-regtest-stop
docker compose up --remove-orphans
lnbits-regtest-init
}
lnbits-regtest-stop(){
docker compose down --volumes
# clean up lightning node data
sudo rm -rf ./data/clightning-1 ./data/clightning-2 ./data/clightning-3 ./data/lnd-1 ./data/lnd-2 ./data/lnd-3 ./data/lnd-4 ./data/boltz/boltz.db ./data/eclair/regtest ./data/boltz-client/liquid-wallet ./data/boltz-client/bitcoin-wallet ./data/boltz-client/wallet ./data/boltz-client/boltz.db
# recreate lightning node data folders preventing permission errors
mkdir ./data/clightning-1 ./data/clightning-2 ./data/clightning-3 ./data/lnd-1 ./data/lnd-2 ./data/lnd-3 ./data/lnd-4
}
lnbits-regtest-restart(){
lnbits-regtest-stop
lnbits-regtest-start
}
boltz-client-init(){
for i in 0 1 2; do
fund_boltz_client
done
elements-cli-sim -generate 3 > /dev/null
}
lnbits-bitcoin-init(){
echo "init_bitcoin_wallet..."
bitcoin-cli-sim createwallet lnbits || bitcoin-cli-sim loadwallet lnbits
echo "mining 150 blocks..."
bitcoin-cli-sim -generate 150 > /dev/null
}
lnbits-elements-init(){
echo "init_elements_wallet..."
elements-cli-sim createwallet lnbits || elements-cli-sim loadwallet lnbits
echo "mining 150 blocks..."
elements-cli-sim -generate 150 > /dev/null
elements-cli-sim rescanblockchain
}
lnbits-init(){
echo "init_lnbits..."
docker exec lnbits-lnbits-1 uv run python tools/create_fake_admin.py
}
lnbits-regtest-init(){
lnbits-bitcoin-init
lnbits-elements-init
lnbits-lightning-sync
lnbits-lightning-init
boltz-client-init
lnbits-init
}
lnbits-lightning-sync(){
wait-for-clightning-sync 1
wait-for-clightning-sync 2
wait-for-clightning-sync 3
wait-for-lnd-sync 1
wait-for-lnd-sync 2
wait-for-lnd-sync 3
wait-for-lnd-sync 4
}
lnbits-lightning-init(){
# create 10 UTXOs for each node
for i in 0 1 2; do
fund_clightning_node 1
fund_clightning_node 2
fund_clightning_node 3
fund_lnd_node 1
fund_lnd_node 2
fund_lnd_node 3
fund_lnd_node 4
done
echo "mining 3 blocks..."
bitcoin-cli-sim -generate 3 > /dev/null
lnbits-lightning-sync
channel_confirms=6
channel_size=24000000 # 0.024 btc
balance_size=12000000 # 0.12 btc
balance_size_msat=12000000000 # 0.12 btc
# lnd-1 -> lnd-2
lncli-sim 1 connect $(lncli-sim 2 getinfo | jq -r '.identity_pubkey')@lnbits-lnd-2-1 > /dev/null
echo "open channel from lnd-1 to lnd-2"
lncli-sim 1 openchannel $(lncli-sim 2 getinfo | jq -r '.identity_pubkey') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 1
# lnd-1 -> lnd-3
lncli-sim 1 connect $(lncli-sim 3 getinfo | jq -r '.identity_pubkey')@lnbits-lnd-3-1 > /dev/null
echo "open channel from lnd-1 to lnd-3"
lncli-sim 1 openchannel $(lncli-sim 3 getinfo | jq -r '.identity_pubkey') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 1
# lnd-1 -> cln-1
lncli-sim 1 connect $(lightning-cli-sim 1 getinfo | jq -r '.id')@lnbits-clightning-1-1 > /dev/null
echo "open channel from lnd-1 to cln-1"
lncli-sim 1 openchannel $(lightning-cli-sim 1 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 1
# lnd-1 -> cln-2
lncli-sim 1 connect $(lightning-cli-sim 2 getinfo | jq -r '.id')@lnbits-clightning-2-1 > /dev/null
echo "open channel from lnd-1 to cln-2"
lncli-sim 1 openchannel $(lightning-cli-sim 2 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 1
# lnd-1 -> cln-3
lncli-sim 1 connect $(lightning-cli-sim 3 getinfo | jq -r '.id')@lnbits-clightning-3-1 > /dev/null
echo "open channel from lnd-1 to cln-3"
lncli-sim 1 openchannel $(lightning-cli-sim 3 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 1
# lnd-2 -> cln-2
lncli-sim 2 connect $(lightning-cli-sim 2 getinfo | jq -r '.id')@lnbits-clightning-2-1 > /dev/null
echo "open channel from lnd-2 to cln-2"
lncli-sim 2 openchannel $(lightning-cli-sim 2 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 2
# lnd-3 -> cln-3
lncli-sim 3 connect $(lightning-cli-sim 3 getinfo | jq -r '.id')@lnbits-clightning-3-1 > /dev/null
echo "open channel from lnd-3 to cln-1"
lncli-sim 3 openchannel $(lightning-cli-sim 3 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 3
# lnd-3 -> cln-1
lncli-sim 3 connect $(lightning-cli-sim 1 getinfo | jq -r '.id')@lnbits-clightning-1-1 > /dev/null
echo "open channel from lnd-3 to cln-1"
lncli-sim 3 openchannel $(lightning-cli-sim 1 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 3
# lnd-1 -> eclair-1
lncli-sim 1 connect $(get-eclair-pubkey)@lnbits-eclair-1 > /dev/null
echo "open channel from lnd-2 to eclair-1"
lncli-sim 1 openchannel $(get-eclair-pubkey) $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 1
# lnd-2 -> eclair-1
lncli-sim 2 connect $(get-eclair-pubkey)@lnbits-eclair-1 > /dev/null
echo "open channel from lnd-2 to eclair-1"
lncli-sim 2 openchannel $(get-eclair-pubkey) $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 2
wait-for-clightning-channel 1
wait-for-clightning-channel 2
wait-for-clightning-channel 3
wait-for-eclair-channel
lnbits-lightning-sync
}
wait-for-lnd-channel(){
while true; do
pending=$(lncli-sim $1 pendingchannels | jq -r '.pending_open_channels | length')
echo "lnd-$1 pendingchannels: $pending"
if [[ "$pending" == "0" ]]; then
break
fi
sleep 1
done
}
wait-for-lnd-sync(){
while true; do
if [[ "$(lncli-sim $1 getinfo 2>&1 | jq -r '.synced_to_chain' 2> /dev/null)" == "true" ]]; then
echo "lnd-$1 is synced!"
break
fi
echo "waiting for lnd-$1 to sync..."
sleep 1
done
}
wait-for-clightning-channel(){
while true; do
pending=$(lightning-cli-sim $1 getinfo | jq -r '.num_pending_channels | length')
echo "cln-$1 pendingchannels: $pending"
if [[ "$pending" == "0" ]]; then
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_bitcoind_sync' 2> /dev/null)" == "null" ]]; then
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_lightningd_sync' 2> /dev/null)" == "null" ]]; then
break
fi
fi
fi
sleep 1
done
}
wait-for-clightning-sync(){
while true; do
if [[ ! "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.id' 2> /dev/null)" == "null" ]]; then
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_bitcoind_sync' 2> /dev/null)" == "null" ]]; then
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_lightningd_sync' 2> /dev/null)" == "null" ]]; then
echo "cln-$1 is synced!"
break
fi
fi
fi
echo "waiting for cln-$1 to sync..."
sleep 1
done
}
-74
View File
@@ -1,74 +0,0 @@
#!/bin/bash
print_success() {
printf "\033[;1;32mPASSED\033[;0m $1\n"
}
print_error() {
printf "\033[;1;31mFAILED\033[;0m $1\n"
}
run(){
label=$1
value=$2
cmd=$3
if [[ "$cmd" == "$value" ]]; then
print_success "$label is $cmd"
else
print_error "$label is $cmd, should be $value"
failed="true"
fi
}
failed="false"
blockheight=213
utxos=3
channel_size=24000000 # 0.024 btc
balance_size=12000000 # 0.012 btc
source $(pwd)/docker-scripts.sh
lnbits-regtest-start
echo "=================================="
printf "\033[;1;36mregtest started! starting tests...\033[;0m\n"
echo "=================================="
echo ""
for i in 1 2 3; do
run "lnd-$i .synced_to_chain" "true" $(lncli-sim $i getinfo | jq -r ".synced_to_chain")
run "lnd-$i utxo count" $utxos $(lncli-sim $i listunspent | jq -r ".utxos | length")
run "lnd-$i .block_height" $blockheight $(lncli-sim $i getinfo | jq -r ".block_height")
if [[ "$i" == "1" ]]; then
channel_count=6
else
channel_count=3
fi
run "lnd-$i openchannels" $channel_count $(lncli-sim $i listchannels | jq -r ".channels | length")
run "lnd-$i .channels[0].capacity" $channel_size $(lncli-sim $i listchannels | jq -r ".channels[0].capacity")
run "lnd-$i .channels[0].push_amount_sat" $balance_size $(lncli-sim $i listchannels | jq -r ".channels[0].push_amount_sat")
done
for i in 1 2 3; do
# run "cln-$i blockheight" $blockheight $(lightning-cli-sim $i getinfo | jq -r ".blockheight")
run "cln-$i utxo count" $utxos $(lightning-cli-sim $i listfunds | jq -r ".outputs | length")
run "cln-$i openchannels" 2 $(lightning-cli-sim $i getinfo | jq -r ".num_active_channels")
run "cln-$i channel[0].state" "CHANNELD_NORMAL" $(lightning-cli-sim $i listfunds | jq -r ".channels[0].state")
run "cln-$i channel[0].amount_msat" $(($channel_size * 1000)) $(lightning-cli-sim $i listfunds | jq -r ".channels[0].amount_msat" | sed 's/msat//g')
run "cln-$i channel[0].our_amount_msat" $(($balance_size * 1000)) $(lightning-cli-sim $i listfunds | jq -r ".channels[0].our_amount_msat" | sed 's/msat//g')
done
run "eclair-1 openchannels" 2 $(docker exec lnbits-eclair-1 curl -s http://localhost:8080/channels -X POST -u :lnbits| jq '. | length')
run "eclair-1 blockHeight" $blockheight $(docker exec lnbits-eclair-1 curl -s http://localhost:8080/getinfo -X POST -u :lnbits| jq '.blockHeight')
run "lnbits service status" "200" $(curl -s -o /dev/null -w "%{http_code}" "http://localhost:5001/")
run "boltz service status" "200" $(curl -s -o /dev/null --head -w "%{http_code}" "http://localhost:9001/version")
# return non-zero exit code if a test fails
if [[ "$failed" == "true" ]]; then
echo ""
echo "=================================="
print_error "one more more tests failed"
echo "=================================="
exit 1
else
echo ""
echo "=================================="
print_success "all tests passed! yay!"
echo "=================================="
fi
-36
View File
@@ -1,36 +0,0 @@
ARG LNBITS_TAG=latest
FROM lnbits/lnbits:${LNBITS_TAG}
RUN apt-get update && apt-get -y upgrade
RUN apt-get install -y netcat-openbsd npm nodejs git
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:$PATH"
# install sparksidecar
RUN git clone https://github.com/lnbits/spark_sidecar
RUN cd spark_sidecar && npm install
# Reinstall dependencies for lnbits just in case (needed for CMD usage)
RUN uv sync --all-extras
# LNBITS
ENV LNBITS_PORT="5000"
ENV LNBITS_HOST="0.0.0.0"
ENV LNBITS_BACKEND_WALLET_CLASS="SparkL2Wallet"
ENV FUNDING_SOURCE_MAX_RETRIES=10
# spark sidecar
ENV SPARK_MNEMONIC="bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom"
ENV SPARK_NETWORK="MAINNET"
ENV SPARK_SIDECAR_PORT="8765"
ENV SPARK_PAY_WAIT_MS="20000"
EXPOSE 5000
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Entrypoint to start sparksidebar and LNbits
CMD ["/entrypoint.sh"]
-25
View File
@@ -1,25 +0,0 @@
#!/bin/bash
set -e
node spark_sidecar/server.mjs &
SIDECAR_PID=$!
# Wait for startup
for i in {1..10}; do
if nc -z localhost $SPARK_SIDECAR_PORT; then
echo "sparksidebar is up!"
break
fi
echo "Waiting for sparksidebar to start..."
sleep 1
done
# Optional: check if still not up
if ! nc -z localhost $SPARK_SIDECAR_PORT; then
echo "sparksidebar did not start successfully."
exit 1
fi
echo "Starting LNbits on $LNBITS_HOST:$LNBITS_PORT..."
exec uv run lnbits --port "$LNBITS_PORT" --host "$LNBITS_HOST" --forwarded-allow-ips='*'
+1 -4
View File
@@ -50,7 +50,6 @@ Below is a side-by-side comparison of Lightning funding sources you can use with
| **Blink** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; focuses on mobile integrations. |
| **ZBD** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Gaming-focused payment platform. |
| **Spark (CLN)** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | Web interface for CLN; requires Spark server setup. |
| **Spark (L2)** | Self-custodial | ❌ | Medium | ❌ | High | Automatic | Easy | Low | Minimal fees | Medium | Runs via Spark sidecar; seed-based self-custody. |
| **Cliche Wallet** | Self-custodial | ❌ | Medium | ❌ | Medium | Manual | Moderate | Moderate | Minimal fees | Medium | Lightweight wallet; suitable for embedded systems. |
| **Strike** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
| **LNPay** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
@@ -58,9 +57,7 @@ Below is a side-by-side comparison of Lightning funding sources you can use with
| **LN.tips** | Custodial/Self-Custodial | Depends on provider | Medium | ❌ | Low | Provider-managed | Moderate | Low | Transaction fees may apply | Medium | Simple hosted service; use LN.tips API as your backend. |
| **Fake Wallet** | Testing (simulated) | ❌ | Low | ❌ | N/A | N/A | Easy | Low | None (test only) | N/A | For testing only; mints accounting units in LNbits (no real sats, unit name configurable). |
## Spark (L2)
Spark L2 uses a local Node.js sidecar to expose an HTTP API that LNbits can use as a funding source. It is self-custodial and secured by a standard mnemonic seed. Sidecar repo `https://github.com/lnbits/spark_sidecar`.
---
### Notes for readers
+2 -30
View File
@@ -41,8 +41,7 @@ A backend wallet is selected and configured entirely through LNbits environment
| [CoreLightning](#corelightning) | [LND (gRPC)](#lnd-grpc) | [Blink](#blink) |
| [CoreLightning REST](#corelightning-rest) | [LNbits](#lnbits) | [Alby](#alby) |
| [Spark (Core Lightning)](#spark-core-lightning) | [LNPay](#lnpay) | [Boltz](#boltz) |
| [Spark L2](#spark-l2) | [ZBD](#zbd) | [Phoenixd](#phoenixd) |
| [Cliche Wallet](#cliche-wallet) | | |
| [Cliche Wallet](#cliche-wallet) | [ZBD](#zbd) | [Phoenixd](#phoenixd) |
| [Breez SDK](#breez-sdk) | [Breez Liquid SDK](#breez-liquid-sdk) | [Nostr Wallet Connect](#nostr-wallet-connect-nwc) |
| [Strike](#strike) | [Eclair (ACINQ)](#eclair-acinq) | [LN.tips](#lntips) |
| [Fake Wallet](#fake-wallet) | | |
@@ -53,7 +52,7 @@ A backend wallet is selected and configured entirely through LNbits environment
### CLNRest (using [runes](https://docs.corelightning.org/reference/lightning-createrune))
[Core Lightning REST API docs](https://docs.corelightning.org/docs/rest)
[Core Lightning REST API docs](https://docs.corelightning.org/docs/rest)
Should also work with the [Rust version of CLNRest](https://github.com/daywalker90/clnrest-rs)
**Environment variables**
@@ -128,33 +127,6 @@ Old REST interface using [RTL c-lightning-REST](https://github.com/Ride-The-Ligh
- `SPARK_URL`: `http://10.147.17.230:9737/rpc`
- `SPARK_TOKEN`: `secret_access_key`
## Spark L2
Self-custodial funding source using the [Spark L2](https://docs.spark.money/start/overview) network. Requires a Node.js [sidecar](https://github.com/lnbits/spark_sidecar) that bridges lnbits talking to Spark. Works in addition with any Spark-compatible seed (Wallet of Satoshi, BuhoGO, BlitzWallet).
If the sidecar is started with a `mnemonic` then that mnemonic will be used. Otherwhise if a mnemonic is set for the `Spark L2` LNbits funding source then that mnemonic will be used.
### Optional tuning
- `SPARK_L2_PAY_WAIT_MS`: `4000` _(payment timeout in ms)_
- `SPARK_L2_PAY_POLL_MS`: `500` _(polling interval in ms)_
- `SPARK_L2_STREAM_KEEPALIVE_MS`: `15000` _(SSE keepalive in ms)_
### Example: run the sidecar
```bash
git clone https://github.com/lnbits/spark_sidecar.git
cd spark_sidecar
npm install
SPARK_MNEMONIC="bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom" \
SPARK_NETWORK=MAINNET \
SPARK_SIDECAR_PORT=8765 \
SPARK_PAY_WAIT_MS=20000 \
node server.mjs
```
For testing, you can generate a 12-word mnemonic at https://iancoleman.io/bip39/. Store it securely — it controls your funds. Then select Spark (L2) as the funding source in LNbits.
## LND (REST)
**Required env vars**
+2
View File
@@ -66,6 +66,7 @@ from .middleware import (
add_first_install_middleware,
add_ip_block_middleware,
add_ratelimit_middleware,
add_route_access_middleware,
)
from .tasks import internal_invoice_listener, invoice_listener, run_interval
@@ -192,6 +193,7 @@ def create_app() -> FastAPI:
# adds security middleware
add_ip_block_middleware(app)
add_route_access_middleware(app)
add_ratelimit_middleware(app)
register_exception_handlers(app)
+21 -75
View File
@@ -4,12 +4,10 @@ from typing import Any
from uuid import uuid4
from lnbits.core.crud.extensions import get_user_active_extensions_ids
from lnbits.core.crud.wallets import clear_wallet_cache, create_wallet, get_wallets
from lnbits.core.crud.wallets import create_wallet, get_wallets
from lnbits.core.db import db
from lnbits.core.models import UserAcls
from lnbits.db import Connection, Filters, Page
from lnbits.helpers import sha256s
from lnbits.utils.cache import cache
from ..models import (
Account,
@@ -43,7 +41,6 @@ async def delete_account(user_id: str, conn: Connection | None = None) -> None:
"DELETE from accounts WHERE id = :user",
{"user": user_id},
)
await clear_user_id_cache(user_id)
async def get_accounts(
@@ -72,7 +69,6 @@ async def get_accounts(
accounts.email,
accounts.pubkey,
accounts.external_id,
accounts.activated,
SUM(COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0)) as balance_msat,
@@ -97,18 +93,12 @@ async def get_accounts(
)
async def get_account(
user_id: str, active_only: bool = True, conn: Connection | None = None
) -> Account | None:
async def get_account(user_id: str, conn: Connection | None = None) -> Account | None:
if len(user_id) == 0:
return None
return await (conn or db).fetchone(
"""
SELECT * FROM accounts
WHERE id = :id AND (activated = true OR activated = :activated)
""",
{"id": user_id, "activated": active_only},
"SELECT * FROM accounts WHERE id = :id",
{"id": user_id},
Account,
)
@@ -134,79 +124,55 @@ async def delete_accounts_no_wallets(
async def get_account_by_username(
username: str, active_only: bool = True, conn: Connection | None = None
username: str, conn: Connection | None = None
) -> Account | None:
if len(username) == 0:
return None
return await (conn or db).fetchone(
"""
SELECT * FROM accounts
WHERE
LOWER(username) = :username
AND (activated = true OR activated = :activated)
""",
{"username": username.lower(), "activated": active_only},
"SELECT * FROM accounts WHERE LOWER(username) = :username",
{"username": username.lower()},
Account,
)
async def get_account_by_pubkey(
pubkey: str, active_only: bool = True, conn: Connection | None = None
pubkey: str, conn: Connection | None = None
) -> Account | None:
return await (conn or db).fetchone(
"""
SELECT * FROM accounts
WHERE
LOWER(pubkey) = :pubkey
AND (activated = true OR activated = :activated)
""",
{"pubkey": pubkey.lower(), "activated": active_only},
"SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
{"pubkey": pubkey.lower()},
Account,
)
async def get_account_by_email(
email: str, active_only: bool = True, conn: Connection | None = None
email: str, conn: Connection | None = None
) -> Account | None:
if len(email) == 0:
return None
return await (conn or db).fetchone(
"""
SELECT * FROM accounts
WHERE
LOWER(email) = :email
AND (activated = true OR activated = :activated)
""",
{"email": email.lower(), "activated": active_only},
"SELECT * FROM accounts WHERE LOWER(email) = :email",
{"email": email.lower()},
Account,
)
async def get_account_by_username_or_email(
username_or_email: str,
active_only: bool = True,
conn: Connection | None = None,
username_or_email: str, conn: Connection | None = None
) -> Account | None:
return await (conn or db).fetchone(
"""
SELECT * FROM accounts
WHERE
(LOWER(email) = :value or LOWER(username) = :value)
AND (activated = true OR activated = :activated)
WHERE LOWER(email) = :value or LOWER(username) = :value
""",
{"value": username_or_email.lower(), "activated": active_only},
{"value": username_or_email.lower()},
Account,
)
async def get_user(
user_id: str, active_only: bool = True, conn: Connection | None = None
) -> User | None:
async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
async with db.reuse_conn(conn) if conn else db.connect() as conn:
account = await get_account(user_id, active_only, conn=conn)
account = await get_account(user_id, conn=conn)
if not account:
return None
return await get_user_from_account(account, conn=conn)
@@ -225,7 +191,6 @@ async def get_user_from_account(
return User(
id=account.id,
activated=account.activated,
email=account.email,
username=account.username,
pubkey=account.pubkey,
@@ -251,31 +216,12 @@ async def update_user_access_control_list(
async def get_user_access_control_lists(
user_id: str, active_only: bool = True, conn: Connection | None = None
user_id: str, conn: Connection | None = None
) -> UserAcls:
user_acls = await (conn or db).fetchone(
"""
SELECT id, access_control_list FROM accounts
WHERE id = :user_id AND (activated = true OR activated = :activated)
""",
{"user_id": user_id, "activated": active_only},
"SELECT id, access_control_list FROM accounts WHERE id = :id",
{"id": user_id},
UserAcls,
)
return user_acls or UserAcls(id=user_id)
async def clear_user_id_cache(user_id: str):
user = await get_user(user_id, active_only=True)
if user:
clear_user_cache(user)
def clear_user_cache(user: User):
user_cache_key: str | None = cache.pop(
f"auth:user:cache_key:{sha256s(user.id)}", None
)
if user_cache_key:
cache.pop(user_cache_key)
for wallet in user.wallets:
clear_wallet_cache(wallet)
+8 -20
View File
@@ -50,7 +50,7 @@ async def delete_wallet(
deleted: bool = True,
conn: Connection | None = None,
) -> None:
clear_wallet_id_cache(wallet_id)
_clear_wallet_cache(wallet_id)
now = int(time())
await (conn or db).execute(
@@ -65,7 +65,7 @@ async def delete_wallet(
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
clear_wallet_id_cache(wallet_id)
_clear_wallet_cache(wallet_id)
await (conn or db).execute(
"DELETE FROM wallets WHERE id = :wallet",
{"wallet": wallet_id},
@@ -75,7 +75,7 @@ async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) ->
async def delete_wallet_by_id(
wallet_id: str, conn: Connection | None = None
) -> int | None:
clear_wallet_id_cache(wallet_id)
_clear_wallet_cache(wallet_id)
now = int(time())
result = await (conn or db).execute(
# Timestamp placeholder is safe from SQL injection (not user input)
@@ -226,14 +226,11 @@ async def get_wallet_for_key(
) -> Wallet | None:
wallet = await (conn or db).fetchone(
"""
SELECT wallets.*, COALESCE((
SELECT *, COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0)
AS balance_msat FROM wallets
INNER JOIN accounts ON wallets.user = accounts.id
WHERE (adminkey = :key OR inkey = :key)
AND deleted = false
AND accounts.activated = true
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
""",
{"key": key},
Wallet,
@@ -253,11 +250,8 @@ async def get_base_wallet_for_key(
) -> BaseWallet | None:
wallet = await (conn or db).fetchone(
"""
SELECT wallets.id, "user", wallet_type, adminkey, inkey FROM wallets
INNER JOIN accounts ON wallets.user = accounts.id
WHERE (adminkey = :key OR inkey = :key)
AND deleted = false
AND accounts.activated = true
SELECT id, "user", wallet_type, adminkey, inkey FROM wallets
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
""",
{"key": key},
BaseWallet,
@@ -300,14 +294,8 @@ async def get_total_balance(conn: Connection | None = None):
return row.get("balance", 0) or 0
def clear_wallet_id_cache(wallet_id: str):
def _clear_wallet_cache(wallet_id):
cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}")
if cached_wallet:
cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}")
cache.pop(f"auth:x-api-key:{cached_wallet.inkey}")
def clear_wallet_cache(wallet: Wallet):
cache.pop(f"auth:wallet:{wallet.id}")
cache.pop(f"auth:x-api-key:{wallet.adminkey}")
cache.pop(f"auth:x-api-key:{wallet.inkey}")
-8
View File
@@ -856,11 +856,3 @@ async def m043_add_ui_customization_to_accounts(db: Connection):
Used for server side persistence of UI customization settings.
"""
await db.execute("ALTER TABLE accounts ADD COLUMN ui_customization TEXT")
async def m044_add_activated_to_accounts(db: Connection):
"""
Adds activated column to accounts.
Used for account activation status.
"""
await db.execute("ALTER TABLE accounts ADD COLUMN activated BOOLEAN DEFAULT true")
+53 -10
View File
@@ -6,16 +6,23 @@ from typing import Literal
from fastapi import Query
from lnurl import LnurlWithdrawResponse
from loguru import logger
from pydantic import BaseModel, Field, validator
from lnbits.db import FilterModel
from lnbits.fiat import get_fiat_provider
from lnbits.fiat.base import (
FiatPaymentFailedStatus,
FiatPaymentPendingStatus,
FiatPaymentStatus,
FiatPaymentSuccessStatus,
)
from lnbits.utils.exchange_rates import allowed_currencies
from lnbits.wallets import get_funding_source
from lnbits.wallets.base import (
PaymentFailedStatus,
PaymentPendingStatus,
PaymentStatus,
PaymentSuccessStatus,
)
@@ -123,23 +130,59 @@ class Payment(BaseModel):
"fiat_"
)
# DEPRECATED: in v1.5.0, use service check_payment_status instead
async def check_status(
self, skip_internal_payment_notifications: bool | None = False
) -> PaymentStatus:
logger.warning("payment.check_status() is deprecated.")
from lnbits.core.services.payments import check_payment_status
if self.is_internal:
if self.success:
return PaymentSuccessStatus()
if self.failed:
return PaymentFailedStatus()
if self.is_in and self.fiat_provider:
fiat_status = await self.check_fiat_status(
skip_internal_payment_notifications
)
return PaymentStatus(paid=fiat_status.paid)
return PaymentPendingStatus()
funding_source = get_funding_source()
if self.is_out:
status = await funding_source.get_payment_status(self.checking_id)
else:
status = await funding_source.get_invoice_status(self.checking_id)
return status
return await check_payment_status(self, skip_internal_payment_notifications)
# DEPRECATED: in v1.5.0, use service check_payment_status instead
async def check_fiat_status(
self, skip_internal_payment_notifications: bool | None = False
) -> FiatPaymentStatus:
logger.warning("payment.check_fiat_status() is deprecated.")
from lnbits.core.services.fiat_providers import check_fiat_status
if not self.is_internal:
return FiatPaymentPendingStatus()
if self.success:
return FiatPaymentSuccessStatus()
if self.failed:
return FiatPaymentFailedStatus()
return await check_fiat_status(self, skip_internal_payment_notifications)
if not self.fiat_provider:
return FiatPaymentPendingStatus()
checking_id = self.extra.get("fiat_checking_id")
if not checking_id:
return FiatPaymentPendingStatus()
fiat_provider = await get_fiat_provider(self.fiat_provider)
if not fiat_provider:
return FiatPaymentPendingStatus()
fiat_status = await fiat_provider.get_invoice_status(checking_id)
if skip_internal_payment_notifications:
return fiat_status
if fiat_status.success:
# notify receivers asynchronously
from lnbits.tasks import internal_invoice_queue
await internal_invoice_queue.put(self.checking_id)
return fiat_status
class PaymentFilters(FilterModel):
-5
View File
@@ -181,7 +181,6 @@ class AccountId(BaseModel):
class Account(AccountId):
activated: bool = True
external_id: str | None = None # for external account linking
username: str | None = None
password_hash: str | None = None
@@ -242,7 +241,6 @@ class Account(AccountId):
class AccountOverview(Account):
activated: bool = True
transaction_count: int | None = 0
wallet_count: int | None = 0
balance_msat: int | None = 0
@@ -278,7 +276,6 @@ class AccountFilters(FilterModel):
class User(BaseModel):
id: str
activated: bool = True
created_at: datetime
updated_at: datetime
email: str | None = None
@@ -318,7 +315,6 @@ class RegisterUser(BaseModel):
username: str = Query(default=..., min_length=2, max_length=20)
password: str = Query(default=..., min_length=8, max_length=50)
password_repeat: str = Query(default=..., min_length=8, max_length=50)
invitation_code: str | None = Query(default=None, max_length=256)
class CreateUser(BaseModel):
@@ -362,7 +358,6 @@ class UpdateSuperuserPassword(BaseModel):
username: str = Query(default=..., min_length=2, max_length=20)
password: str = Query(default=..., min_length=8, max_length=50)
password_repeat: str = Query(default=..., min_length=8, max_length=50)
first_install_token: str | None = Query(None)
class LoginUsr(BaseModel):
-4
View File
@@ -1,4 +1,3 @@
from .fiat_providers import check_fiat_status
from .funding_source import (
get_balance_delta,
switch_to_voidwallet,
@@ -8,7 +7,6 @@ from .notifications import enqueue_admin_notification, send_payment_notification
from .payments import (
calculate_fiat_amounts,
cancel_hold_invoice,
check_payment_status,
check_transaction_status,
check_wallet_limits,
create_fiat_invoice,
@@ -42,8 +40,6 @@ __all__ = [
"calculate_fiat_amounts",
"cancel_hold_invoice",
"check_admin_settings",
"check_fiat_status",
"check_payment_status",
"check_transaction_status",
"check_wallet_limits",
"check_webpush_settings",
+4 -44
View File
@@ -12,12 +12,6 @@ from lnbits.core.models import CreatePayment, Payment, PaymentState
from lnbits.core.models.misc import SimpleStatus
from lnbits.db import Connection
from lnbits.fiat import get_fiat_provider
from lnbits.fiat.base import (
FiatPaymentFailedStatus,
FiatPaymentPendingStatus,
FiatPaymentStatus,
FiatPaymentSuccessStatus,
)
from lnbits.settings import settings
@@ -35,40 +29,6 @@ async def handle_fiat_payment_confirmation(
logger.warning(e)
async def check_fiat_status(
payment: Payment, skip_internal_payment_notifications: bool | None = False
) -> FiatPaymentStatus:
if not payment.is_internal:
return FiatPaymentPendingStatus()
if payment.success:
return FiatPaymentSuccessStatus()
if payment.failed:
return FiatPaymentFailedStatus()
if not payment.fiat_provider:
return FiatPaymentPendingStatus()
checking_id = payment.extra.get("fiat_checking_id")
if not checking_id:
return FiatPaymentPendingStatus()
fiat_provider = await get_fiat_provider(payment.fiat_provider)
if not fiat_provider:
return FiatPaymentPendingStatus()
fiat_status = await fiat_provider.get_invoice_status(checking_id)
if skip_internal_payment_notifications:
return fiat_status
if fiat_status.success:
# notify receivers asynchronously
from lnbits.tasks import internal_invoice_queue
await internal_invoice_queue.put(payment.checking_id)
return fiat_status
def check_stripe_signature(
payload: bytes,
sig_header: str | None,
@@ -118,8 +78,8 @@ async def verify_paypal_webhook(headers, payload: bytes):
"""
webhook_id = settings.paypal_webhook_id
if not webhook_id:
logger.warning("PayPal webhook ID not set.")
raise ValueError("PayPal webhook cannot be verified. Missing webhook ID.")
logger.warning("PayPal webhook ID not set; skipping verification.")
return
required_headers = {
"PAYPAL-TRANSMISSION-ID": headers.get("PAYPAL-TRANSMISSION-ID"),
@@ -129,8 +89,8 @@ async def verify_paypal_webhook(headers, payload: bytes):
"PAYPAL-AUTH-ALGO": headers.get("PAYPAL-AUTH-ALGO"),
}
if not all(required_headers.values()):
logger.warning("Missing PayPal webhook headers.")
raise ValueError("PayPal webhook cannot be verified. Missing headers.")
logger.warning("Missing PayPal webhook headers; skipping verification.")
return
try:
async with httpx.AsyncClient(base_url=settings.paypal_api_endpoint) as client:
+5 -40
View File
@@ -25,7 +25,6 @@ from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount
from lnbits.wallets import fake_wallet, get_funding_source
from lnbits.wallets.base import (
InvoiceResponse,
PaymentFailedStatus,
PaymentPendingStatus,
PaymentResponse,
PaymentStatus,
@@ -48,7 +47,6 @@ from ..models import (
PaymentState,
Wallet,
)
from .fiat_providers import check_fiat_status
from .notifications import send_payment_notification_in_background
payment_lock = asyncio.Lock()
@@ -366,7 +364,7 @@ async def update_pending_payments(wallet_id: str):
async def update_pending_payment(
payment: Payment, conn: Connection | None = None
) -> Payment:
status = await check_payment_status(payment)
status = await payment.check_status()
if status.failed:
payment.status = PaymentState.FAILED
await update_payment(payment, conn=conn)
@@ -620,29 +618,7 @@ async def check_transaction_status(
if payment.status == PaymentState.SUCCESS.value:
return PaymentSuccessStatus(fee_msat=payment.fee)
return await check_payment_status(payment)
async def check_payment_status(
payment: Payment, skip_internal_payment_notifications: bool | None = False
) -> PaymentStatus:
if payment.is_internal:
if payment.success:
return PaymentSuccessStatus()
if payment.failed:
return PaymentFailedStatus()
if payment.is_in and payment.fiat_provider:
fiat_status = await check_fiat_status(
payment, skip_internal_payment_notifications
)
return PaymentStatus(paid=fiat_status.paid)
return PaymentPendingStatus()
funding_source = get_funding_source()
if payment.is_out:
status = await funding_source.get_payment_status(payment.checking_id)
else:
status = await funding_source.get_invoice_status(payment.checking_id)
return status
return await payment.check_status()
async def get_payments_daily_stats(
@@ -776,7 +752,7 @@ async def _pay_internal_invoice(
await update_payment(internal_payment, conn=conn)
logger.success(f"internal payment successful {internal_payment.checking_id}")
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
send_payment_notification_in_background(wallet, payment)
# notify receiver asynchronously
from lnbits.tasks import internal_invoice_queue
@@ -849,8 +825,7 @@ async def _pay_external_invoice(
payment = await update_payment_success_status(
payment, payment_response, conn=conn
)
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
send_payment_notification_in_background(wallet, payment)
logger.success(f"payment successful {payment_response.checking_id}")
payment.checking_id = payment_response.checking_id
@@ -893,7 +868,7 @@ async def _verify_external_payment(
raise PaymentError("Payment already paid.", status="success")
# payment failed
status = await check_payment_status(payment)
status = await payment.check_status()
if status.failed:
raise PaymentError(
"Payment is failed node, retrying is not possible.", status="failed"
@@ -1058,13 +1033,3 @@ async def cancel_hold_invoice(payment: Payment) -> InvoiceResponse:
await update_payment(payment)
return response
async def _send_payment_notification_in_background(
wallet_id: str, payment: Payment, conn: Connection | None = None
):
# fetch balance again
wallet = await get_wallet(wallet_id, conn=conn)
if not wallet:
raise PaymentError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
send_payment_notification_in_background(wallet, payment)
-24
View File
@@ -3,10 +3,8 @@ from uuid import uuid4
from loguru import logger
from lnbits.core.crud.settings import set_settings_field
from lnbits.core.db import db
from lnbits.core.models.extensions import UserExtension
from lnbits.core.models.users import RegisterUser
from lnbits.db import Connection
from lnbits.settings import (
EditableSettings,
@@ -194,25 +192,3 @@ async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
editable_settings = EditableSettings.from_dict(settings.dict())
return await create_admin_settings(account.id, editable_settings.dict())
async def check_register_activation_settings(data: RegisterUser):
if not settings.lnbits_require_user_activation:
return None
if settings.lnbits_user_activation_by_invitation_code:
code = data.invitation_code.strip() if data.invitation_code else ""
if len(code) == 0:
raise ValueError("Invitation code cannot be empty.")
if code == settings.lnbits_register_reusable_activation_code:
return None
if code in settings.lnbits_register_one_time_activation_codes:
settings.lnbits_register_one_time_activation_codes.remove(code)
await set_settings_field(
"lnbits_register_one_time_activation_codes",
settings.lnbits_register_one_time_activation_codes,
)
return None
raise ValueError("Invalid invitation code.")
raise ValueError("No activation method provided.")
+4 -24
View File
@@ -26,10 +26,7 @@ from lnbits.core.models.users import (
UpdateAccessControlList,
)
from lnbits.core.services import create_user_account
from lnbits.core.services.users import (
check_register_activation_settings,
update_user_account,
)
from lnbits.core.services.users import update_user_account
from lnbits.decorators import (
access_token_payload,
check_account_exists,
@@ -89,7 +86,6 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
account = await get_account_by_username_or_email(data.username)
if not account or not account.verify_password(data.password):
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
return _auth_success_response(account.username, account.id, account.email)
@@ -98,7 +94,7 @@ async def nostr_login(request: Request) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
event = _nostr_nip98_event(request)
account = await get_account_by_pubkey(event["pubkey"], active_only=False)
account = await get_account_by_pubkey(event["pubkey"])
if not account:
account = Account(
id=uuid4().hex,
@@ -106,8 +102,6 @@ async def nostr_login(request: Request) -> JSONResponse:
extra=UserExtra(provider="nostr"),
)
await create_user_account(account)
if not account.activated:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User is not activated.")
return _auth_success_response(account.username or "", account.id, account.email)
@@ -363,14 +357,12 @@ async def register(data: RegisterUser) -> JSONResponse:
if not is_valid_username(data.username):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
if await get_account_by_username(data.username, active_only=False):
if await get_account_by_username(data.username):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
if data.email and not is_valid_email_address(data.email):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
await check_register_activation_settings(data)
account = Account(
id=uuid4().hex,
email=data.email,
@@ -511,21 +503,9 @@ async def update_ui_customization(
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
if not settings.first_install:
raise HTTPException(HTTPStatus.FORBIDDEN, "This is not your first install")
if settings.first_install_token:
if not data.first_install_token:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing first_install_token.")
if settings.first_install_token != data.first_install_token:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid first_install_token.")
account = await get_account_by_username(data.username, False)
if account:
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
account = await get_account(settings.super_user)
if not account:
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
account.username = data.username
account.extra = account.extra or UserExtra()
account.extra.provider = "lnbits"
@@ -541,7 +521,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: str | None = Non
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
redirect_path = "/wallet"
account = await get_account_by_email(email, active_only=False)
account = await get_account_by_email(email)
if verified_user_id:
if account:
+19 -41
View File
@@ -9,7 +9,6 @@ from lnbits.core.crud.payments import (
from lnbits.core.models.misc import SimpleStatus
from lnbits.core.models.payments import CreateInvoice
from lnbits.core.services.fiat_providers import (
check_fiat_status,
check_stripe_signature,
verify_paypal_webhook,
)
@@ -24,7 +23,7 @@ callback_router = APIRouter(prefix="/api/v1/callback", tags=["callback"])
async def api_generic_webhook_handler(
provider_name: str, request: Request
) -> SimpleStatus:
logger.info(f"Received callback from provider: '{provider_name}'.")
if provider_name.lower() == "stripe":
payload = await request.body()
sig_header = request.headers.get("Stripe-Signature")
@@ -88,7 +87,7 @@ async def _handle_stripe_intent_session_completed(event: dict):
if not payment:
logger.warning(f"No payment found for hash: '{payment_hash}'.")
return
await check_fiat_status(payment)
await payment.check_fiat_status()
async def _handle_stripe_checkout_session_completed(event: dict):
@@ -111,7 +110,7 @@ async def _handle_stripe_checkout_session_completed(event: dict):
payment = await get_standalone_payment(payment_hash)
if not payment:
raise ValueError(f"No payment found for hash: '{payment_hash}'.")
await check_fiat_status(payment)
await payment.check_fiat_status()
async def _handle_stripe_subscription_invoice_paid(event: dict):
@@ -156,7 +155,7 @@ async def _handle_stripe_subscription_invoice_paid(event: dict):
),
)
await check_fiat_status(payment)
await payment.check_fiat_status()
async def _get_stripe_subscription_payment_options(
@@ -181,10 +180,8 @@ async def _get_stripe_subscription_payment_options(
async def handle_paypal_event(event: dict):
event_id = event.get("id", "")
event_type = event.get("event_type", "")
resource = event.get("resource", {})
logger.info(f"Handling PayPal event: '{event_id}'. Type: '{event_type}'.")
if event_type in ("CHECKOUT.ORDER.APPROVED", "PAYMENT.CAPTURE.COMPLETED"):
payment_hash = _paypal_extract_payment_hash(resource)
@@ -195,20 +192,23 @@ async def handle_paypal_event(event: dict):
if not payment:
logger.warning(f"No payment found for hash: '{payment_hash}'.")
return
await check_fiat_status(payment)
await payment.check_fiat_status()
return
if event_type in ("PAYMENT.SALE.COMPLETED"):
if event_type in (
"PAYMENT.SALE.COMPLETED",
"BILLING.SUBSCRIPTION.PAYMENT.SUCCEEDED",
):
await _handle_paypal_subscription_payment(resource)
return
logger.warning(f"Unhandled PayPal event type: '{event_type}'.")
logger.info(f"Unhandled PayPal event type: '{event_type}'.")
async def _handle_paypal_subscription_payment(resource: dict):
amount_info = resource.get("amount") or {}
currency = (amount_info.get("currency") or "").upper()
total = amount_info.get("total")
currency = (amount_info.get("currency_code") or "").upper()
total = amount_info.get("value")
if not currency or total is None:
raise ValueError("PayPal subscription event missing amount.")
@@ -216,14 +216,18 @@ async def _handle_paypal_subscription_payment(resource: dict):
if not custom_id:
raise ValueError("PayPal subscription event missing custom metadata.")
payment_options = _deserialize_paypal_metadata(custom_id)
try:
metadata = json.loads(custom_id)
except json.JSONDecodeError:
metadata = {}
payment_options = FiatSubscriptionPaymentOptions(**metadata)
if not payment_options.wallet_id:
raise ValueError("PayPal subscription event missing wallet_id.")
memo = payment_options.memo or ""
extra = {
**(payment_options.extra or {}),
"subscription_request_id": resource.get("billing_agreement_id"),
"fiat_method": "subscription",
"tag": payment_options.tag,
"subscription": {
@@ -243,7 +247,7 @@ async def _handle_paypal_subscription_payment(resource: dict):
),
)
await check_fiat_status(payment)
await payment.check_fiat_status()
def _paypal_extract_payment_hash(resource: dict) -> str | None:
@@ -254,29 +258,3 @@ def _paypal_extract_payment_hash(resource: dict) -> str | None:
if pu.get("custom_id"):
return pu.get("custom_id")
return None
def _deserialize_paypal_metadata(custom_id: str) -> FiatSubscriptionPaymentOptions:
try:
meta = json.loads(custom_id)
wallet_id = meta[0] if len(meta) > 0 else None
tag = meta[1] if len(meta) > 1 else None
subscription_request_id = meta[2] if len(meta) > 2 else None
extra_link = meta[3] if len(meta) > 3 else None
memo = meta[4] if len(meta) > 4 else None
extra = {
"link": extra_link,
"subscription_request_id": subscription_request_id,
}
return FiatSubscriptionPaymentOptions(
wallet_id=wallet_id,
tag=tag,
subscription_request_id=subscription_request_id,
extra=extra,
memo=memo,
)
except (json.JSONDecodeError, IndexError) as e:
logger.warning(f"Failed to deserialize PayPal metadata: {e}")
return FiatSubscriptionPaymentOptions()
-12
View File
@@ -1,7 +1,6 @@
from http import HTTPStatus
from fastapi import APIRouter, Depends, HTTPException
from loguru import logger
from lnbits.core.models.misc import SimpleStatus
from lnbits.core.models.wallets import WalletTypeInfo
@@ -31,11 +30,6 @@ async def create_subscription(
data: CreateFiatSubscription,
key_type: WalletTypeInfo = Depends(require_admin_key),
) -> FiatSubscriptionResponse:
logger.debug(
f"Creating subscription with provider '{provider}. '"
f"Subscription ID '{data.subscription_id}'."
)
fiat_provider = await get_fiat_provider(provider)
if not fiat_provider:
raise HTTPException(404, "Fiat provider not found")
@@ -53,12 +47,6 @@ async def create_subscription(
subscription_response = await fiat_provider.create_subscription(
data.subscription_id, data.quantity, data.payment_options
)
if subscription_response.error_message:
logger.warning(
f"Failed to create subscription with provider '{provider}': "
f"{subscription_response.error_message}"
)
return subscription_response
+2 -40
View File
@@ -20,7 +20,6 @@ from lnbits.core.crud import (
update_admin_settings,
update_wallet,
)
from lnbits.core.crud.users import clear_user_id_cache, get_account, update_account
from lnbits.core.crud.wallets import delete_wallet_by_id
from lnbits.core.models import (
AccountFilters,
@@ -74,7 +73,7 @@ async def api_get_users(
summary="Get user by Id",
)
async def api_get_user(user_id: str) -> User:
user = await get_user(user_id, active_only=False)
user = await get_user(user_id)
if not user:
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
return user
@@ -198,7 +197,7 @@ async def api_users_reset_password(user_id: str) -> str:
return f"reset_key_{reset_key_b64}"
@users_router.put(
@users_router.get(
"/user/{user_id}/admin",
dependencies=[Depends(check_super_user)],
name="Give or revoke admin permsisions to a user",
@@ -221,43 +220,6 @@ async def api_users_toggle_admin(user_id: str) -> SimpleStatus:
)
@users_router.put(
"/user/{user_id}/activate",
name="Activate or deactivate a user",
)
async def api_users_toggle_activated(
user_id: str, admin_account: Account = Depends(check_admin)
) -> SimpleStatus:
if user_id == settings.super_user:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Cannot deactivate super user.",
)
if user_id == admin_account.id:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="You cannot deactivate yourself.",
)
if settings.is_admin_user(user_id):
settings.lnbits_admin_users.remove(user_id)
user_account = await get_account(user_id, active_only=False)
if not user_account:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="User not found.",
)
user_account.activated = not user_account.activated
await update_account(user_account)
await clear_user_id_cache(user_id)
return SimpleStatus(
success=True,
message=f"User {'activated' if user_account.activated else 'deactivated'}.",
)
@users_router.get("/user/{user_id}/wallet", name="Get wallets for user")
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
return await get_wallets(user_id, deleted=None)
+4 -2
View File
@@ -9,7 +9,6 @@ from fastapi import (
)
from lnbits.core.crud.wallets import (
clear_wallet_cache,
create_wallet,
get_wallets_paginated,
)
@@ -38,6 +37,7 @@ from lnbits.decorators import (
require_invoice_key,
)
from lnbits.helpers import generate_filter_params_openapi
from lnbits.utils.cache import cache
from ..crud import (
delete_wallet,
@@ -134,7 +134,9 @@ async def api_reset_wallet_keys(
if not wallet or wallet.user != account_id.id:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
clear_wallet_cache(wallet)
cache.pop(f"auth:wallet:{wallet.id}")
cache.pop(f"auth:x-api-key:{wallet.adminkey}")
cache.pop(f"auth:x-api-key:{wallet.inkey}")
wallet.adminkey = uuid4().hex
wallet.inkey = uuid4().hex
+42 -9
View File
@@ -12,6 +12,7 @@ from typing import Any, Generic, Literal, TypeVar, get_origin
from loguru import logger
from pydantic import BaseModel, ValidationError, root_validator
from sqlalchemy import event
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
from sqlalchemy.sql import text
@@ -55,6 +56,14 @@ def compat_timestamp_placeholder(key: str):
return f":{key}"
def get_placeholder(model: Any, field: str) -> str:
type_ = model.__fields__[field].type_
if type_ == datetime:
return compat_timestamp_placeholder(field)
else:
return f":{field}"
class Compat:
type: str | None = "<inherited>"
schema: str | None = "<inherited>"
@@ -317,7 +326,31 @@ class Database(Compat):
self.engine: AsyncEngine = create_async_engine(
database_uri, echo=settings.debug_database
)
if self.type in {POSTGRES, COCKROACH}:
@event.listens_for(self.engine.sync_engine, "connect")
def register_custom_types(dbapi_connection, *_):
def _parse_date(value) -> datetime:
if value is None:
value = "1970-01-01 00:00:00"
f = "%Y-%m-%d %H:%M:%S.%f"
if "." not in value:
f = "%Y-%m-%d %H:%M:%S"
# Parse and add UTC timezone info
return datetime.strptime(value, f).replace(tzinfo=timezone.utc)
dbapi_connection.run_async(
lambda connection: connection.set_type_codec(
"TIMESTAMP",
encoder=datetime,
decoder=_parse_date,
schema="pg_catalog",
)
)
self.lock = asyncio.Lock()
logger.trace(f"database {self.type} added for {self.name}")
@asynccontextmanager
@@ -630,7 +663,7 @@ def insert_query(table_name: str, model: BaseModel) -> str:
placeholders = []
keys = model_to_dict(model).keys()
for field in keys:
placeholders.append(f":{field}")
placeholders.append(get_placeholder(model, field))
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
fields = ", ".join([f'"{key}"' for key in keys])
values = ", ".join(placeholders)
@@ -648,8 +681,9 @@ def update_query(
"""
fields = []
for field in model_to_dict(model).keys():
placeholder = get_placeholder(model, field)
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
fields.append(f'"{field}" = :{field}')
fields.append(f'"{field}" = {placeholder}')
query = ", ".join(fields)
return f"UPDATE {table_name} SET {query} {where}" # noqa: S608
@@ -667,12 +701,7 @@ def model_to_dict(model: BaseModel) -> dict:
if model.__fields__[key].field_info.extra.get("no_database", False):
continue
if isinstance(value, datetime):
if DB_TYPE == SQLITE:
_dict[key] = value.timestamp()
else:
# remove tz. postgres and cockroach TIMESTAMP is not tz aware
# so it will throw if we dont remove the UTC.
_dict[key] = value.replace(tzinfo=None)
_dict[key] = value.timestamp()
continue
if (
type(type_) is type(BaseModel)
@@ -728,7 +757,11 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel: # noqa: C901
if DB_TYPE == SQLITE:
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
else:
_dict[key] = value.replace(tzinfo=timezone.utc)
# Ensure PostgreSQL datetime values have timezone info
if isinstance(value, datetime) and value.tzinfo is None:
_dict[key] = value.replace(tzinfo=timezone.utc)
else:
_dict[key] = value
continue
if issubclass(type_, BaseModel):
_dict[key] = dict_to_submodel(type_, value)
-1
View File
@@ -261,7 +261,6 @@ async def check_account_id_exists(
account_id,
expiry=settings.auth_authentication_cache_minutes * 60,
)
cache.set(f"auth:user:cache_key:{sha256s(account.id)}", cache_key)
return account_id
+18 -63
View File
@@ -2,7 +2,7 @@ import asyncio
import json
import time
from collections.abc import AsyncGenerator
from typing import Any, Literal
from typing import Any
import httpx
from loguru import logger
@@ -24,8 +24,6 @@ from .base import (
FiatSubscriptionResponse,
)
FiatMethod = Literal["checkout", "subscription"]
class PayPalCheckoutOptions(BaseModel):
class Config:
@@ -36,21 +34,11 @@ class PayPalCheckoutOptions(BaseModel):
metadata: dict[str, Any] = Field(default_factory=dict)
class PayPalSubscriptionOptions(BaseModel):
class Config:
extra = "ignore"
checking_id: str | None = None
payment_request: str | None = None
class PayPalCreateInvoiceOptions(BaseModel):
class Config:
extra = "ignore"
fiat_method: FiatMethod = "checkout"
checkout: PayPalCheckoutOptions | None = None
subscription: PayPalSubscriptionOptions | None = None
class PayPalWallet(FiatProvider):
@@ -108,14 +96,6 @@ class PayPalWallet(FiatProvider):
opts = self._parse_create_opts(extra or {})
if opts is None:
return FiatInvoiceResponse(ok=False, error_message="Invalid PayPal options")
if opts.fiat_method == "subscription":
term = opts.subscription or PayPalSubscriptionOptions()
checking_id = term.checking_id or urlsafe_short_hash()
return FiatInvoiceResponse(
ok=True,
checking_id=f"subscription_{checking_id}",
payment_request=term.payment_request or "",
)
try:
await self._ensure_access_token()
@@ -191,7 +171,6 @@ class PayPalWallet(FiatProvider):
or "https://lnbits.com"
)
logger.debug(f"Creating PayPal subscription with ID '{subscription_id}'")
if not payment_options.subscription_request_id:
payment_options.subscription_request_id = urlsafe_short_hash()
payment_options.extra = payment_options.extra or {}
@@ -203,6 +182,7 @@ class PayPalWallet(FiatProvider):
await self._ensure_access_token()
payload = {
"plan_id": subscription_id,
"quantity": str(quantity),
"custom_id": self._serialize_metadata(payment_options),
"application_context": {
"return_url": success_url,
@@ -225,7 +205,7 @@ class PayPalWallet(FiatProvider):
return FiatSubscriptionResponse(
ok=True,
checkout_session_url=approval_url,
subscription_request_id=data.get("id"),
subscription_request_id=payment_options.subscription_request_id,
)
except Exception as exc:
logger.warning(exc)
@@ -239,10 +219,6 @@ class PayPalWallet(FiatProvider):
correlation_id: str,
**kwargs,
) -> FiatSubscriptionResponse:
logger.debug(
f"Cancelling PayPal subscription '{subscription_id}'. "
f"Correlation ID '{correlation_id}'."
)
try:
await self._ensure_access_token()
r = await self.client.post(
@@ -264,20 +240,12 @@ class PayPalWallet(FiatProvider):
async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
try:
await self._ensure_access_token()
paypal_id = self._normalize_paypal_id(checking_id)
if paypal_id.startswith("subscription_"):
capture_id = paypal_id.replace("subscription_", "", 1)
r = await self.client.get(
f"v2/payments/captures/{capture_id}", headers=self._auth_headers()
)
r.raise_for_status()
return self._status_from_capture(r.json())
else:
r = await self.client.get(
f"/v2/checkout/orders/{paypal_id}", headers=self._auth_headers()
)
r.raise_for_status()
return self._status_from_order(r.json())
order_id = self._normalize_paypal_id(checking_id)
r = await self.client.get(
f"/v2/checkout/orders/{order_id}", headers=self._auth_headers()
)
r.raise_for_status()
return self._status_from_order(r.json())
except Exception as exc:
logger.debug(f"Error getting PayPal order status: {exc}")
return FiatPaymentPendingStatus()
@@ -302,14 +270,6 @@ class PayPalWallet(FiatProvider):
return FiatPaymentFailedStatus()
return FiatPaymentPendingStatus()
def _status_from_capture(self, order: dict[str, Any]) -> FiatPaymentStatus:
status = (order.get("status") or "").upper()
if status in ["COMPLETED"]:
return FiatPaymentSuccessStatus()
if status in ["DECLINED", "FAILED", "CANCELLED", "CANCELED"]:
return FiatPaymentFailedStatus()
return FiatPaymentPendingStatus()
def _normalize_paypal_id(self, checking_id: str) -> str:
return (
checking_id.replace("fiat_paypal_", "", 1)
@@ -320,20 +280,15 @@ class PayPalWallet(FiatProvider):
def _serialize_metadata(
self, payment_options: FiatSubscriptionPaymentOptions
) -> str:
meta = [
payment_options.wallet_id,
payment_options.tag,
payment_options.subscription_request_id,
]
if payment_options.extra:
meta.append(payment_options.extra.get("link", None))
# Keep custom_id within PayPal's 127-char limit
memo_limit = 120 - len(json.dumps(meta))
if memo_limit > 0 and payment_options.memo:
meta.append(payment_options.memo[:memo_limit])
return json.dumps(meta)
md = {
"wallet_id": payment_options.wallet_id,
"memo": payment_options.memo,
"extra": payment_options.extra,
"tag": payment_options.tag,
"subscription_request_id": payment_options.subscription_request_id,
}
raw = json.dumps(md)
return raw[:127] # PayPal custom_id limit
def _parse_create_opts(
self, raw_opts: dict[str, Any]
+54 -8
View File
@@ -70,11 +70,60 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
t.env.globals["static_url_for"] = static_url_for
t.env.globals["normalize_path"] = normalize_path
# used in base.html
t.env.globals["SITE_TITLE"] = settings.lnbits_site_title
t.env.globals["LNBITS_APPLE_TOUCH_ICON"] = settings.lnbits_apple_touch_icon
t.env.globals["SETTINGS"] = settings.to_public().dict(by_alias=True)
t.env.globals["CURRENCIES"] = list(currencies.keys())
window_settings = {
"AD_SPACE": settings.lnbits_ad_space,
"AD_SPACE_ENABLED": settings.lnbits_ad_space_enabled,
"AD_SPACE_TITLE": settings.lnbits_ad_space_title,
"EXTENSIONS": list(settings.lnbits_installed_extensions_ids),
"HIDE_API": settings.lnbits_hide_api,
"SITE_TITLE": settings.lnbits_site_title,
"SITE_TAGLINE": settings.lnbits_site_tagline,
"SITE_DESCRIPTION": settings.lnbits_site_description,
"LNBITS_ADMIN_UI": settings.lnbits_admin_ui,
"LNBITS_AUDIT_ENABLED": settings.lnbits_audit_enabled,
"LNBITS_AUTH_METHODS": settings.auth_allowed_methods,
"LNBITS_AUTH_KEYCLOAK_ORG": settings.keycloak_client_custom_org,
"LNBITS_AUTH_KEYCLOAK_ICON": settings.keycloak_client_custom_icon,
"LNBITS_CUSTOM_IMAGE": settings.lnbits_custom_image,
"LNBITS_CUSTOM_BADGE": settings.lnbits_custom_badge,
"LNBITS_CUSTOM_BADGE_COLOR": settings.lnbits_custom_badge_color,
"LNBITS_EXTENSIONS_DEACTIVATE_ALL": settings.lnbits_extensions_deactivate_all,
"LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed,
"LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager,
"LNBITS_NODE_UI_AVAILABLE": settings.has_nodemanager,
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
"LNBITS_APPLE_TOUCH_ICON": settings.lnbits_apple_touch_icon,
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
"LNBITS_SERVICE_FEE_WALLET": settings.lnbits_service_fee_wallet,
"LNBITS_SHOW_HOME_PAGE_ELEMENTS": settings.lnbits_show_home_page_elements,
"LNBITS_THEME_OPTIONS": settings.lnbits_theme_options,
"WALLET_FEATURED_BUTTON_LABEL": settings.lnbits_wallet_featured_button_label,
"WALLET_FEATURED_BUTTON_URL": settings.lnbits_wallet_featured_button_url,
"WALLET_FEATURED_BUTTON_ICON": settings.lnbits_wallet_featured_button_icon,
"LNBITS_VERSION": settings.version,
"USE_CUSTOM_LOGO": settings.lnbits_custom_logo,
"LNBITS_DEFAULT_REACTION": settings.lnbits_default_reaction,
"LNBITS_DEFAULT_THEME": settings.lnbits_default_theme,
"LNBITS_DEFAULT_BORDER": settings.lnbits_default_border,
"LNBITS_DEFAULT_GRADIENT": settings.lnbits_default_gradient,
"LNBITS_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
"LNBITS_EXTENSIONS_REVIEWS_URL": settings.lnbits_extensions_reviews_url,
"LNBITS_DENOMINATION": settings.lnbits_denomination,
"has_holdinvoice": settings.has_holdinvoice,
"LNBITS_NOSTR_CONFIGURED": settings.is_nostr_notifications_configured(),
"LNBITS_TELEGRAM_CONFIGURED": settings.is_telegram_notifications_configured(),
"LNBITS_EXT_BUILDER": settings.lnbits_extensions_builder_activate_non_admins,
"LNBITS_CURRENCIES": list(currencies.keys()),
"LNBITS_ALLOWED_CURRENCIES": settings.lnbits_allowed_currencies,
"CACHE_KEY": settings.server_startup_time,
}
t.env.globals["WINDOW_SETTINGS"] = window_settings
for key, value in window_settings.items():
t.env.globals[key] = value
if settings.bundle_assets:
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
@@ -88,9 +137,6 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
# backwards compatibility for extensions (tpos)
t.env.globals["LNBITS_DENOMINATION"] = settings.lnbits_denomination
return t
+84
View File
@@ -1,5 +1,6 @@
import asyncio
import json
import re
from datetime import datetime, timezone
from http import HTTPStatus
from typing import Any
@@ -18,6 +19,53 @@ from lnbits.core.models import AuditEntry
from lnbits.helpers import normalize_path, template_renderer
from lnbits.settings import settings
_LOCALHOST_IPS = {"127.0.0.1", "::1"}
_PUBLIC_ASSET_PATHS = {
"/favicon.ico",
"/service-worker.js",
}
def _normalize_match_path(path: str) -> str:
if not path:
return "/"
if path != "/" and path.endswith("/"):
return path.rstrip("/")
return path
def _route_pattern_matches(pattern: str, path: str) -> bool:
if not pattern:
return False
if not pattern.startswith("/"):
pattern = f"/{pattern}"
pattern = _normalize_match_path(pattern)
path = _normalize_match_path(path)
if pattern.endswith("*"):
prefix = pattern.rstrip("*")
return path.startswith(prefix)
if pattern == path:
return True
escaped = re.escape(pattern)
escaped = re.sub(r"\\\{[^/]+\\\}", r"[^/]+", escaped)
return re.fullmatch(escaped, path) is not None
def _response_by_accepted_type(request: Request, msg: str, status_code: HTTPStatus):
accept_header = request.headers.get("accept", "")
if "text/html" in accept_header.split(","):
return HTMLResponse(
status_code=status_code,
content=template_renderer()
.TemplateResponse(
request,
"error.html",
{"err": msg, "status_code": status_code, "message": msg},
)
.body,
)
return JSONResponse(status_code=status_code, content={"detail": msg})
class InstalledExtensionMiddleware:
# This middleware class intercepts calls made to the extensions API and:
@@ -235,6 +283,42 @@ def add_ip_block_middleware(app: FastAPI):
return await call_next(request)
def add_route_access_middleware(app: FastAPI):
@app.middleware("http")
async def route_access_middleware(request: Request, call_next):
if not settings.lnbits_route_access_control_enabled:
return await call_next(request)
if not request.client:
return JSONResponse(
status_code=HTTPStatus.FORBIDDEN,
content={"detail": "No request client"},
)
if request.client.host in _LOCALHOST_IPS:
return await call_next(request)
path = request.url.path or "/"
if "/static/" in path or path in _PUBLIC_ASSET_PATHS:
return await call_next(request)
whitelist = settings.lnbits_route_access_whitelist
blacklist = settings.lnbits_route_access_blacklist
if whitelist:
if any(_route_pattern_matches(route, path) for route in whitelist):
return await call_next(request)
return _response_by_accepted_type(
request, f"Route not whitelisted: {path}", HTTPStatus.FORBIDDEN
)
if blacklist and any(
_route_pattern_matches(route, path) for route in blacklist
):
return _response_by_accepted_type(
request, f"Route is blacklisted: {path}", HTTPStatus.FORBIDDEN
)
return await call_next(request)
def add_first_install_middleware(app: FastAPI):
@app.middleware("http")
async def first_install_middleware(request: Request, call_next):
+4 -153
View File
@@ -41,14 +41,6 @@ class UsersSettings(LNbitsSettings):
lnbits_admin_users: list[str] = Field(default=[])
lnbits_allowed_users: list[str] = Field(default=[])
lnbits_allow_new_accounts: bool = Field(default=True)
lnbits_require_user_activation: bool = Field(default=False)
lnbits_user_activation_by_email: bool = Field(default=False)
lnbits_user_activation_by_payment: bool = Field(default=False)
lnbits_user_activation_by_invitation_code: bool = Field(default=False)
lnbits_register_reusable_activation_code: str = Field(default="")
lnbits_register_one_time_activation_codes: list[str] = Field(default=[])
@property
def new_accounts_allowed(self) -> bool:
@@ -292,14 +284,10 @@ class ThemesSettings(LNbitsSettings):
lnbits_qr_logo: str = Field(default="/static/images/favicon_qr_logo.png")
lnbits_apple_touch_icon: str | None = Field(default=None)
lnbits_default_reaction: str = Field(default="confettiBothSides")
lnbits_default_theme: str = Field(default="bitcoin")
lnbits_default_theme: str = Field(default="salvador")
lnbits_default_border: str = Field(default="hard-border")
lnbits_default_gradient: bool = Field(default=True)
lnbits_default_bgimage: str | None = Field(default=None)
lnbits_default_dark: bool = Field(default=True)
lnbits_default_card_rounded: bool = Field(default=True)
lnbits_default_card_gradient: bool = Field(default=True)
lnbits_default_card_shadow: bool = Field(default=False)
class OpsSettings(LNbitsSettings):
@@ -433,6 +421,9 @@ class SecuritySettings(LNbitsSettings):
lnbits_rate_limit_unit: str = Field(default="minute")
lnbits_allowed_ips: list[str] = Field(default=[])
lnbits_blocked_ips: list[str] = Field(default=[])
lnbits_route_access_control_enabled: bool = Field(default=False)
lnbits_route_access_whitelist: list[str] = Field(default=[])
lnbits_route_access_blacklist: list[str] = Field(default=[])
lnbits_callback_url_rules: list[str] = Field(
default=["https?://([a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})(:\\d+)?"]
)
@@ -606,16 +597,6 @@ class SparkFundingSource(LNbitsSettings):
spark_token: str | None = Field(default=None)
class SparkL2FundingSource(LNbitsSettings):
spark_l2_network: str = Field(default="MAINNET")
spark_l2_external_endpoint: str | None = Field(default="http://localhost:8765")
spark_l2_external_api_key: str | None = Field(default=None)
spark_l2_mnemonic: str | None = Field(default=None)
spark_l2_pay_wait_ms: int = Field(default=4000, ge=0)
spark_l2_pay_poll_ms: int = Field(default=500, ge=0)
spark_l2_stream_keepalive_ms: int = Field(default=15000, ge=0)
class LnTipsFundingSource(LNbitsSettings):
lntips_api_endpoint: str | None = Field(default=None)
lntips_api_key: str | None = Field(default=None)
@@ -721,7 +702,6 @@ class FundingSourcesSettings(
PhoenixdFundingSource,
OpenNodeFundingSource,
SparkFundingSource,
SparkL2FundingSource,
LnTipsFundingSource,
NWCFundingSource,
BreezSdkFundingSource,
@@ -1006,7 +986,6 @@ class EnvSettings(LNbitsSettings):
enable_log_to_file: bool = Field(default=True)
log_rotation: str = Field(default="100 MB")
log_retention: str = Field(default="3 months")
first_install_token: str | None = Field(default=None)
cleanup_wallets_days: int = Field(default=90, ge=0)
funding_source_max_retries: int = Field(default=4, ge=0)
@@ -1050,7 +1029,6 @@ class SuperUserSettings(LNbitsSettings):
"FakeWallet",
"LNPayWallet",
"LNbitsWallet",
"SparkL2Wallet",
"LnTipsWallet",
"LndRestWallet",
"LndWallet",
@@ -1156,133 +1134,6 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
and ext_id in self.lnbits_all_extensions_ids
)
def to_public(self) -> PublicSettings:
return PublicSettings.from_settings(self)
class PublicSettings(BaseModel):
"""
PublicSettings is used for templating and public information.
WARNING: do not expose private information, PublicSettings is exposed publicly.
"""
allow_register: bool = Field(alias="allowRegister")
qr_logo: str = Field(alias="qrLogo")
site_title: str = Field(alias="siteTitle")
site_tagline: str = Field(alias="siteTagline")
site_description: str | None = Field(alias="siteDescription")
auth_methods: list[str] = Field(alias="authMethods")
keycloak_org: str | None = Field(alias="keycloakOrg")
keycloak_icon: str | None = Field(alias="keycloakIcon")
has_holdinvoice: bool = Field(alias="hasHoldinvoice")
has_nodemanager: bool = Field(alias="hasNodemanager")
show_nodemanager: bool = Field(alias="showNodemanager")
custom_logo: str | None = Field(alias="customLogo")
show_voidwallet: bool = Field(alias="showVoidwallet")
version: str = Field(alias="version")
show_home_page_elements: bool = Field(alias="showHomePageElements")
hide_api: bool = Field(alias="hideApi")
cache_key: str = Field(alias="cacheKey")
webpush_pubkey: str | None = Field(alias="webpushPubkey")
show_extensions: bool = Field(alias="showExtensions")
show_audit: bool = Field(alias="showAudit")
show_admin: bool = Field(alias="showAdmin")
ad_space: list[list[str]] = Field(alias="adSpace")
ad_space_title: str = Field(alias="adSpaceTitle")
show_ad_space: bool = Field(alias="showAdSpace")
custom_image: str | None = Field(alias="customImage")
custom_badge: str | None = Field(alias="customBadge")
custom_badge_color: str | None = Field(alias="customBadgeColor")
service_fee: float = Field(alias="serviceFee")
service_fee_max: int = Field(alias="serviceFeeMax")
service_fee_wallet: str | None = Field(alias="serviceFeeWallet")
theme_options: list[str] = Field(alias="themeOptions")
default_reaction: str = Field(alias="defaultReaction")
default_theme: str = Field(alias="defaultTheme")
default_border: str = Field(alias="defaultBorder")
default_bgimage: str | None = Field(alias="defaultBgimage")
default_gradient: bool = Field(alias="defaultGradient")
default_dark: bool = Field(alias="defaultDark")
default_card_rounded: bool = Field(alias="defaultCardRounded")
default_card_gradient: bool = Field(alias="defaultCardGradient")
default_card_shadow: bool = Field(alias="defaultCardShadow")
denomination: str | None = Field()
extensions: list[str] = Field()
allowed_currencies: list[str] = Field(alias="allowedCurrencies")
extensions_reviews_url: str = Field(alias="extensionsReviewsUrl")
ext_builder: bool = Field(alias="extBuilder")
nostr_configured: bool = Field(alias="nostrConfigured")
telegram_configured: bool = Field(alias="telegramConfigured")
wallet_featured_button_label: str | None = Field(alias="walletFeaturedButtonLabel")
wallet_featured_button_url: str | None = Field(alias="walletFeaturedButtonUrl")
wallet_featured_button_icon: str | None = Field(alias="walletFeaturedButtonIcon")
lnbits_user_activation_by_email: bool = Field(alias="userActivationByEmail")
lnbits_user_activation_by_payment: bool = Field(alias="userActivationByPayment")
lnbits_user_activation_by_invitation_code: bool = Field(
alias="userActivationByInvitationCode"
)
has_first_install_token: bool = Field(alias="hasFirstInstallToken")
@classmethod
def from_settings(cls, settings: Settings):
ads = [x.split(";") for x in settings.lnbits_ad_space.split(",")]
return cls(
adSpace=ads,
adSpaceTitle=settings.lnbits_ad_space_title,
showAdSpace=settings.lnbits_ad_space_enabled and len(ads) > 0,
allowRegister=settings.new_accounts_allowed,
qrLogo=settings.lnbits_qr_logo,
siteDescription=settings.lnbits_site_description,
siteTitle=settings.lnbits_site_title,
siteTagline=settings.lnbits_site_tagline,
authMethods=settings.auth_allowed_methods,
keycloakOrg=settings.keycloak_client_custom_org,
keycloakIcon=settings.keycloak_client_custom_icon,
hasHoldinvoice=settings.has_holdinvoice,
hasNodemanager=settings.has_nodemanager,
showNodemanager=settings.lnbits_node_ui and settings.has_nodemanager,
customLogo=settings.lnbits_custom_logo,
showVoidwallet=settings.lnbits_backend_wallet_class == "VoidWallet",
version=settings.version,
showHomePageElements=settings.lnbits_show_home_page_elements,
hideApi=settings.lnbits_hide_api,
cacheKey=str(settings.server_startup_time),
webpushPubkey=settings.lnbits_webpush_pubkey,
showExtensions=not settings.lnbits_extensions_deactivate_all,
showAudit=settings.lnbits_audit_enabled,
showAdmin=settings.lnbits_admin_ui,
customImage=settings.lnbits_custom_image,
customBadge=settings.lnbits_custom_badge,
customBadgeColor=settings.lnbits_custom_badge_color,
serviceFee=settings.lnbits_service_fee,
serviceFeeMax=settings.lnbits_service_fee_max,
serviceFeeWallet=settings.lnbits_service_fee_wallet,
themeOptions=settings.lnbits_theme_options,
defaultReaction=settings.lnbits_default_reaction,
defaultTheme=settings.lnbits_default_theme,
defaultBorder=settings.lnbits_default_border,
defaultGradient=settings.lnbits_default_gradient,
defaultBgimage=settings.lnbits_default_bgimage,
defaultDark=settings.lnbits_default_dark,
defaultCardRounded=settings.lnbits_default_card_rounded,
defaultCardGradient=settings.lnbits_default_card_gradient,
defaultCardShadow=settings.lnbits_default_card_shadow,
denomination=settings.lnbits_denomination,
extensions=list(settings.lnbits_installed_extensions_ids),
allowedCurrencies=settings.lnbits_allowed_currencies,
extensionsReviewsUrl=settings.lnbits_extensions_reviews_url,
extBuilder=settings.lnbits_extensions_builder_activate_non_admins,
nostrConfigured=settings.is_nostr_notifications_configured(),
telegramConfigured=settings.is_telegram_notifications_configured(),
walletFeaturedButtonLabel=settings.lnbits_wallet_featured_button_label,
walletFeaturedButtonUrl=settings.lnbits_wallet_featured_button_url,
walletFeaturedButtonIcon=settings.lnbits_wallet_featured_button_icon,
userActivationByEmail=settings.lnbits_user_activation_by_email,
userActivationByPayment=settings.lnbits_user_activation_by_payment,
userActivationByInvitationCode=settings.lnbits_user_activation_by_invitation_code,
hasFirstInstallToken=settings.first_install_token is not None,
)
class SuperSettings(EditableSettings):
super_user: str
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+11 -13
View File
File diff suppressed because one or more lines are too long
-175
View File
@@ -220,181 +220,6 @@ body.body--dark .q-drawer {
backdrop-filter: blur(6px) brightness(0.8);
}
body.rounded-ui .q-card,
body.rounded-ui .q-btn {
border-radius: 10px;
}
body[data-theme=classic].card-gradient .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgba(103, 58, 183, 0.08), rgba(156, 39, 176, 0.06));
}
body[data-theme=classic].card-gradient .q-drawer {
background-image: linear-gradient(165deg, rgba(103, 58, 183, 0.06), rgba(156, 39, 176, 0.04));
}
body[data-theme=classic].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgb(38.6192771084, 42.356626506, 64.7807228916), rgba(103, 58, 183, 0.07));
}
body[data-theme=classic].card-gradient.body--dark .q-drawer {
background-image: linear-gradient(165deg, rgb(36.7144578313, 40.2674698795, 61.5855421687), rgba(103, 58, 183, 0.05));
}
body[data-theme=bitcoin].card-gradient .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgba(234, 97, 29, 0.08), rgba(229, 111, 53, 0.06));
}
body[data-theme=bitcoin].card-gradient .q-drawer {
background-image: linear-gradient(165deg, rgba(234, 97, 29, 0.06), rgba(229, 111, 53, 0.04));
}
body[data-theme=bitcoin].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgb(54.18, 49.364, 71.036), rgba(234, 97, 29, 0.07));
}
body[data-theme=bitcoin].card-gradient.body--dark .q-drawer {
background-image: linear-gradient(165deg, rgb(51.885, 47.273, 68.027), rgba(234, 97, 29, 0.05));
}
body[data-theme=freedom].card-gradient .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgba(226, 33, 86, 0.08), rgba(185, 26, 69, 0.06));
}
body[data-theme=freedom].card-gradient .q-drawer {
background-image: linear-gradient(165deg, rgba(226, 33, 86, 0.06), rgba(185, 26, 69, 0.04));
}
body[data-theme=freedom].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgb(82.2051282051, 55.1948717949, 63.4153846154), rgba(226, 33, 86, 0.07));
}
body[data-theme=freedom].card-gradient.body--dark .q-drawer {
background-image: linear-gradient(165deg, rgb(79.1538461538, 53.1461538462, 61.0615384615), rgba(226, 33, 86, 0.05));
}
body[data-theme=cyber].card-gradient .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgba(124, 179, 66, 0.08), rgba(85, 139, 47, 0.06));
}
body[data-theme=cyber].card-gradient .q-drawer {
background-image: linear-gradient(165deg, rgba(124, 179, 66, 0.06), rgba(85, 139, 47, 0.04));
}
body[data-theme=cyber].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgb(10.2, 10.2, 10.2), rgba(124, 179, 66, 0.07));
}
body[data-theme=cyber].card-gradient.body--dark .q-drawer {
background-image: linear-gradient(165deg, rgb(7.65, 7.65, 7.65), rgba(124, 179, 66, 0.05));
}
body[data-theme=mint].card-gradient .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgba(58, 183, 125, 0.08), rgba(39, 176, 101, 0.06));
}
body[data-theme=mint].card-gradient .q-drawer {
background-image: linear-gradient(165deg, rgba(58, 183, 125, 0.06), rgba(39, 176, 101, 0.04));
}
body[data-theme=mint].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgb(38.6192771084, 64.7807228916, 53.5686746988), rgba(58, 183, 125, 0.07));
}
body[data-theme=mint].card-gradient.body--dark .q-drawer {
background-image: linear-gradient(165deg, rgb(36.7144578313, 61.5855421687, 50.9265060241), rgba(58, 183, 125, 0.05));
}
body[data-theme=autumn].card-gradient .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgba(183, 118, 58, 0.08), rgba(176, 121, 39, 0.06));
}
body[data-theme=autumn].card-gradient .q-drawer {
background-image: linear-gradient(165deg, rgba(183, 118, 58, 0.06), rgba(176, 121, 39, 0.04));
}
body[data-theme=autumn].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgb(64.7807228916, 51.0771084337, 38.6192771084), rgba(183, 118, 58, 0.07));
}
body[data-theme=autumn].card-gradient.body--dark .q-drawer {
background-image: linear-gradient(165deg, rgb(61.5855421687, 48.5578313253, 36.7144578313), rgba(183, 118, 58, 0.05));
}
body[data-theme=flamingo].card-gradient .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgba(255, 0, 255, 0.08), rgba(253, 163, 253, 0.06));
}
body[data-theme=flamingo].card-gradient .q-drawer {
background-image: linear-gradient(165deg, rgba(255, 0, 255, 0.06), rgba(253, 163, 253, 0.04));
}
body[data-theme=flamingo].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgb(66.176, 4.224, 66.176), rgba(255, 0, 255, 0.07));
}
body[data-theme=flamingo].card-gradient.body--dark .q-drawer {
background-image: linear-gradient(165deg, rgb(61.382, 3.918, 61.382), rgba(255, 0, 255, 0.05));
}
body[data-theme=monochrome].card-gradient .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgba(73, 73, 73, 0.08), rgba(107, 107, 107, 0.06));
}
body[data-theme=monochrome].card-gradient .q-drawer {
background-image: linear-gradient(165deg, rgba(73, 73, 73, 0.06), rgba(107, 107, 107, 0.04));
}
body[data-theme=monochrome].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgb(10.2, 10.2, 10.2), rgba(73, 73, 73, 0.07));
}
body[data-theme=monochrome].card-gradient.body--dark .q-drawer {
background-image: linear-gradient(165deg, rgb(7.65, 7.65, 7.65), rgba(73, 73, 73, 0.05));
}
body[data-theme=salvador].card-gradient .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgba(25, 118, 210, 0.08), rgba(38, 166, 154, 0.06));
}
body[data-theme=salvador].card-gradient .q-drawer {
background-image: linear-gradient(165deg, rgba(25, 118, 210, 0.06), rgba(38, 166, 154, 0.04));
}
body[data-theme=salvador].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgb(43.9888888889, 64.2, 84.4111111111), rgba(25, 118, 210, 0.07));
}
body[data-theme=salvador].card-gradient.body--dark .q-drawer {
background-image: linear-gradient(165deg, rgb(42.2416666667, 61.65, 81.0583333333), rgba(25, 118, 210, 0.05));
}
body.card-shadow .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.18));
}
body.card-shadow.body--dark .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.45));
}
:root {
--size: 100px;
--gap: 25px;
+97 -122
View File
@@ -6,7 +6,7 @@ window.localisation.en = {
funding: 'Funding',
users: 'Users',
audit: 'Audit',
api_watch: 'API Watch',
api_watch: 'Api Watch',
apps: 'Apps',
channels: 'Channels',
transactions: 'Transactions',
@@ -171,7 +171,6 @@ window.localisation.en = {
drop_db: 'Remove Data',
enable: 'Enable',
enabled: 'Enabled',
disabled: 'Disabled',
pay_to_enable: 'Pay To Enable',
enable_extension_details: 'Enable extension for current user',
disable: 'Disable',
@@ -179,8 +178,6 @@ window.localisation.en = {
installed: 'Installed',
activated: 'Activated',
deactivated: 'Deactivated',
activate: 'Activate',
deactivate: 'Deactivate',
release_notes: 'Release Notes',
activate_extension_details: 'Make extension available/unavailable for users',
featured: 'Featured',
@@ -189,8 +186,6 @@ window.localisation.en = {
only_admins_can_create_extensions:
'Only admin accounts can create extensions',
admin_only: 'Admin Only',
make_user_admin: 'Make User Admin',
revoke_admin: 'Revoke Admin',
new_version: 'New Version',
reviews_url: 'Reviews URL',
reviews_url_label: 'Reviews server URL',
@@ -271,16 +266,12 @@ window.localisation.en = {
update: 'Update',
update_available: 'Update {version} available!',
funding_sources: 'Funding Sources',
funding_source: 'Funding Source',
requires_server_restart:
'Changing these settings requires a server restart to take effect.',
funding_source_info: 'Select the active funding wallet',
latest_update: 'You are on the latest version {version}.',
notifications: 'Notifications',
notifications_configure: 'Configure Notifications',
notifications_nostr_config: 'Nostr Configuration',
notifications_enable_nostr: 'Enable Nostr',
notifications_enable_nostr_desc: 'Send notifications over Nostr',
notifications_enable_nostr_desc: 'Send notfications over Nostr',
notifications_nostr_private_key: 'Nostr Private Key',
notifications_nostr_private_key_desc:
'Private key (hex or nsec) to sign the messages sent to Nostr',
@@ -289,11 +280,11 @@ window.localisation.en = {
'Nip5 identifier to send notifications to',
notifications_nostr_identifiers: 'Nostr Identifiers',
notifications_nostr_identifiers_desc:
'List of identifiers to send notifications to.',
'List of identifiers to send notifications to',
notifications_telegram_config: 'Telegram Configuration',
notifications_enable_telegram: 'Enable Telegram',
notifications_enable_telegram_desc: 'Send notifications over Telegram',
notifications_enable_telegram_desc: 'Send notfications over Telegram',
notifications_telegram_access_token: 'Access Token',
notifications_telegram_access_token_desc: 'Access token for the bot',
notifications_chat_id: 'Telegram Chat ID',
@@ -322,15 +313,15 @@ window.localisation.en = {
notification_settings_update: 'Settings updated',
notification_settings_update_desc:
'Send a notification when server settings have been updated',
'Notify when server settings have been updated',
notification_server_start_stop: 'Server Start/Stop',
notification_server_start_stop_desc:
'Send a notification when the server has been started/stopped',
'Notify when the server has been started/stopped',
notification_watchdog_limit: 'Watchdog Limit Notification',
notification_watchdog_limit_desc:
'Send a notification when the watchdog limit has been reached (does not affect the funding source)',
'Notify when the watchdog limit has been reached (does not affect the funding source)',
notification_server_status: 'Server Status',
notification_server_status_desc:
@@ -338,31 +329,29 @@ window.localisation.en = {
notification_incoming_payment: 'Incoming Payments',
notification_incoming_payment_desc:
'Send a notification when a wallet has received a payment above the specified amount (sats)',
'Notify when a wallet has received a payment above the specified amount (sats)',
notification_outgoing_payment: 'Outgoing Payments',
notification_outgoing_payment_desc:
'Send a notification when a wallet has sent a payment above the specified amount (sats)',
'Notify when a wallet has sent a payment above the specified amount (sats)',
notification_credit_debit: 'Credit / Debit',
notification_credit_debit_desc:
'Send a notification when a wallet has been credited/debited by the superuser',
'Notify when a wallet has been credited/debited by the superuser',
notification_balance_delta_changed: 'Balance Delta Changed',
notification_balance_delta_changed_desc:
'Send a notification when the difference between the node balance and the LNbits balance has changed by more than the specified amount (in sats). Set to 0 to disable. This runs every minute.',
'Notify when the difference between the node balance and the LNbits balance has changed by more than the specified amount (in sats). Set to 0 to disable. This runs every minute.',
watchdog_introduction:
"Watchdog is a feature that allows you to automatically switch the LNbits funding source to VoidWallet if your node balance is lower than the LNbits balance by a certain threshold. This can help prevent overspending and keep your node's funds safe.",
enable_watchdog: 'Enable Watchdog',
enable_watchdog: 'Enable Watchdog Switch',
enable_watchdog_desc:
'You will need to re-enable this manually after an update.',
watchdog_interval: 'Watchdog Check Interval',
'If enabled it will change your funding source to VoidWallet automatically if your balance is lower than the LNbits balance. You will need to enable manually after an update.',
watchdog_interval: 'Watchdog Interval',
watchdog_interval_desc:
'How often LNbits should check for a killswitch signal in the watchdog threshold delta value [node_balance - lnbits_balance] (in minutes).',
watchdog_delta: 'Watchdog Threshold Delta',
'How often the background task should check for a killswitch signal in the watchdog delta [node_balance - lnbits_balance] (in minutes).',
watchdog_delta: 'Watchdog Delta',
watchdog_delta_desc:
"The LNbit's > Node balance delta threshold. If this threshold is exceeded, the funding source is changed to VoidWallet.",
'Limit before killswitch changes funding source to VoidWallet [lnbits_balance - node_balance > delta]',
status: 'Status',
notification_source: 'Notification Source',
notification_source_label:
@@ -373,12 +362,24 @@ window.localisation.en = {
releases: 'Releases',
watchdog: 'Watchdog',
server_logs: 'Server Logs',
ip_blocker: 'IP Blacklist/Whitelist',
ip_blocker: 'IP Blocker',
route_access_control: 'Route Access Control',
route_access_control_enable: 'Enable route access control',
route_access_control_hint:
'When enabled, non-local requests are restricted. Requests from 127.0.0.1 (and ::1) are always allowed.',
route_access_whitelist_label: 'Route Whitelist',
route_access_whitelist_hint:
'Only allow non-local access to these routes (leave empty to disable whitelist mode).',
route_access_blacklist_label: 'Route Blacklist',
route_access_blacklist_hint:
'Block non-local access to these routes (ignored when a whitelist is set).',
route_access_save_confirm:
'Are you sure you want to save? This can impact access to LNbits if you are not accessing locally.',
security: 'Security',
security_tools: 'Security Tools',
security_tools: 'Security tools',
block_access_hint: 'Block access by IP',
allow_access_hint: 'Allow access by IP (will override blocked IPs)',
enter_ip: 'Enter an IP address and press enter',
enter_ip: 'Enter IP and hit enter',
rate_limiter: 'Rate Limiter',
callback_url_rules: 'Callback URL Rules',
enter_callback_url_rule: 'Enter URL rule as regex and hit enter',
@@ -392,16 +393,14 @@ window.localisation.en = {
wallet_max_ballance: 'Wallet max balance in sats (0 to disable)',
wallet_limit_secs_between_trans:
'Min secs between transactions per wallet (0 to disable)',
only_incoming_payments_allowed: 'Allow incoming payments only',
only_incoming_payments_allowed: 'Only incoming payments allowed',
disable_outgoing_payments: 'Disable outgoing payments',
number_of_requests: 'Number of requests to allow',
number_of_requests_hint:
'Number of requests to allow per "time unit" for the rate limiter. Set to 0 to disable.',
number_of_requests: 'Number of requests',
time_unit: 'Time unit',
minute: 'Minute',
minute: 'minute',
settings: 'Settings',
second: 'Second',
hour: 'Hour',
second: 'second',
hour: 'hour',
disable_server_log: 'Disable Server Log',
enable_server_log: 'Enable Server Log',
coming_soon: 'Feature coming soon',
@@ -473,12 +472,6 @@ window.localisation.en = {
api_token_id: 'Token Id',
toggle_gradient: 'Toggle Gradient',
gradient_background: 'Gradient Background',
rounded_ui: 'Rounded Cards & Buttons',
toggle_rounded_ui: 'Toggle rounded corners for cards and buttons',
card_gradient: 'Card Gradient',
toggle_card_gradient: 'Toggle gradient on cards',
card_shadow: 'Card Shadow',
toggle_card_shadow: 'Toggle shadow on cards',
language: 'Language',
assets: 'Assets',
max_asset_size_mb: 'Max Asset Size (MB)',
@@ -537,34 +530,36 @@ window.localisation.en = {
hash: 'Hash: ',
welcome_lnbits: 'Welcome to LNbits',
setup_su_account: 'Set up the Superuser account below.',
first_install_token: 'First Install Token',
create_ticker_converter: 'Create Currency Ticker Converter',
enable_audit: 'Enable Audit',
recommended: 'Recommended',
audit_desc: 'Log HTTP requests according to the filters specified below.',
audit_record_req: 'Log Request Body',
audit_record_warning: 'Warning!',
audit_record_req_warning_1: 'Sensitive data (like passwords) will be logged.',
audit_record_req_warning_2:
'The request body can be large. This can fill up your logs quickly.',
audit_record_use: 'Use this with caution!',
audit_ip: 'Log IP Address',
audit_ip_desc: 'Log the IP address of users making requests to LNbits.',
audit_path_params: 'Log Path Parameters',
audit_query_params: 'Log Query Parameters',
audit_desc: 'Record HTTP requests according with the specified filters',
audit_record_req: 'Record Request Body',
audit_record_warning: 'Warning: ',
audit_record_req_warning_1:
'confidential data (like passwords) will be logged.',
audit_record_req_warning_2: 'the request body can have large size.',
audit_record_use: 'Use it with caution.',
audit_ip: 'Record IP Address',
audit_ip_desc: 'Record the IP address of the client',
audit_path_params: 'Record Path Parameters',
audit_query_params: 'Record Query Parameters',
audit_http_methods: 'Include HTTP Methods',
audit_http_methods_hint:
'List of HTTP methods to be logged. No value means all methods will be logged.',
audit_http_methods_label: 'HTTP Methods to Log',
'List of HTTP methods to be included. Empty lists means all.',
audit_http_methods_label: 'HTTP Methods',
audit_resp_codes: 'Include HTTP Response Codes',
audit_resp_codes_hint:
'List of HTTP codes to be included (regex match). Empty lists means all. Eg: 4.*, 5.*',
audit_resp_codes_label: 'HTTP Response Codes to Log (regex)',
audit_resp_codes_label: 'HTTP Response code (regex)',
audit_paths: 'Include Paths',
audit_paths_hint:
'List of paths to be included (regex match). Empty list means all.',
audit_paths_label: 'HTTP Paths to Log (regex)',
audit_paths_label: 'HTTP Path (regex)',
audit_paths_exclude: 'Exclude Paths',
audit_paths_exclude_hint:
'List of paths to be excluded (regex match). Empty list means none.',
audit_paths_exclude_label: 'HTTP Paths to Exclude from Logging (regex)',
audit_paths_exclude_label: 'HTTP Path (regex)',
exchange_providers: 'Exchange Providers',
admin_extensions: 'Admin Extensions',
admin_extensions_label: 'Admin extensions',
@@ -584,16 +579,16 @@ window.localisation.en = {
misc_disable_extensions_builder_label:
'Enable Extensions Builder for non admin users.',
misc_hide_api: 'Hide API',
misc_hide_api_label: 'Hides wallet API, extensions can choose to honor',
misc_hide_api_label: 'Hides wallet api, extensions can choose to honor',
wallets_management: 'Wallets Management',
funding_source_info: 'Funding Source Information',
funding_source: 'Funding source: {wallet_class}',
node_balance: 'Node balance: {balance} sats',
lnbits_balance: 'LNbits balance: {balance} sats',
funding_reserve_percent: 'Funding reserve percentage: {percent} %',
funding_source_info: 'Funding Source Info',
funding_source: 'Funding Source: {wallet_class}',
node_balance: 'Node Balance: {balance} sats',
lnbits_balance: 'LNbits Balance: {balance} sats',
funding_reserve_percent: 'Reserve Percent: {percent} %',
node_management: 'Node Management',
node_management_not_supported:
'Node management is not supported by the active funding source',
'Node Management not supported by active funding source',
toggle_node_ui: 'Node UI',
toggle_public_node_ui: 'Public Node UI',
toggle_transactions_node_ui: 'Transactions Tab (Disable on large CLN nodes)',
@@ -609,15 +604,16 @@ window.localisation.en = {
payment_wait_time_tooltip:
'Controls how long LNbits waits for an outgoing payment attempt to confirm before marking it as pending. Higher values help when paying slow-settling invoices (e.g., HODL invoices, Boltz). The payment will be rechecked later and updated automatically or manually.',
server_management: 'Server Management',
base_url_label: 'Base URL of the server',
base_url: 'Base URL',
base_url_label: 'Static/Base url for the server',
authentication: 'Authentication',
auth_token_expiry_label: 'Token expiry (minutes)',
auth_token_expiry_label: 'Token expire minutes',
auth_token_expiry_hint: 'Time in minutes until the token expires',
auth_authentication_cache_label: 'Cache time (minutes)',
auth_authentication_cache_hint:
'Time in minutes to cache successful authentication (0 to disable)',
auth_allowed_methods_label: 'Allowed authorization methods',
auth_allowed_methods_hint: 'Select allowed authorization methods',
auth_allowed_methods_hint: 'Select authorization methods',
auth_nostr_label: 'Nostr Request URL',
auth_nostr_hint: 'Absolute URL that the clients will use to login.',
auth_google_ci_label: 'Google Client ID',
@@ -637,27 +633,31 @@ window.localisation.en = {
auth_keycloak_custom_icon_label: 'Keycloak Custom Icon (URL)',
currency_settings: 'Currency Settings',
allowed_currencies: 'Allowed Currencies',
allowed_currencies_hint:
'Set the allowed fiat currencies for the exchange features',
default_account_currency: 'Default Accounting Currency',
default_account_currency_hint:
'The default currency to use for accounting features.',
allowed_currencies_hint: 'Limit the number of available fiat currencies',
default_account_currency: 'Default Account Currency',
default_account_currency_hint: 'Default currency for accounting',
min_incoming_payment_amount: 'Min Incoming Payment Amount',
min_incoming_payment_amount_desc:
'Minimum amount allowed for generating an invoice',
max_incoming_payment_amount: 'Maximum Incoming Payment Amount',
max_incoming_payment_amount: 'Max Incoming Payment Amount',
max_incoming_payment_amount_desc:
'Maximum amount allowed when generating an invoice',
max_outgoing_payment_amount: 'Maximum Outgoing Payment Amount',
'Maximum amount allowed for generating an invoice',
max_outgoing_payment_amount: 'Max Outgoing Payment Amount',
max_outgoing_payment_amount_desc:
'Maximum amount allowed when making a payment',
service_fees: 'Service Fees',
'Maximum amount allowed for making a payment',
service_fee: 'Service Fee',
service_fee_label: 'Service Fee Charged Per Transaction',
service_fee_max_label: 'Maximum Service Fee Limit',
fee_wallet_label: 'Service Fee Wallet ID',
fee_wallet_hint: 'The ID of the wallet to which to send service funds',
disable_fee: 'Disable Service Fees for Internal Payments',
service_fee_label: 'Service fee (%)',
service_fee_hint: 'Fee charged per tx (%)',
service_fee_max: 'Service Fee Max',
service_fee_max_label: 'Service fee max (sats)',
service_fee_max_hint: 'Max service fee to charge in (sats)',
fee_wallet: 'Fee Wallet',
fee_wallet_label: 'Fee wallet (wallet ID)',
fee_wallet_hint: 'Wallet ID to send funds to',
disable_fee: 'Disable Fee',
disable_fee_internal: 'Disable Service Fee for Internal Payments',
disable_fee_internal_desc:
'Disable Service Fee for Internal Lightning Payments',
ui_management: 'UI Management',
ui_site_title: 'Site Title',
ui_changing_remove_lnbits_elements:
@@ -670,7 +670,6 @@ window.localisation.en = {
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
ui_default_wallet_name: 'Default Wallet Name',
ui_default_theme: 'Default Theme',
wallet_featured_button_title: 'Wallet - Featured Button',
wallet_featured_button_label: 'Wallet Featured Button Label',
wallet_featured_button_label_hint:
'Show featured button on the wallet homepage',
@@ -679,38 +678,33 @@ window.localisation.en = {
'On click the button will open this URL. Leave empty to hide the button.',
wallet_featured_button_icon: 'Featured Button Icon',
wallet_featured_button_icon_hint:
'Icon shown on the featured button (Quasar icon name e.g. "bolt")',
'Icon shown on the featured button (check quasar icons)',
lnbits_wallet: 'LNbits wallet',
denomination: 'Denomination',
denomination_hint: 'The name for the FakeWallet token',
denomination_error: 'Denomination must be 3 characters, or `sats`',
ui_qr_code_logo: 'QR Code/Favicon Logo',
ui_qr_code_logo_hint: 'QR code icon and favicon logo URL',
ui_qr_code_logo_hint: 'QR code and favicon logo url',
ui_apple_touch_icon: 'Apple Touch Icon',
ui_apple_touch_icon_hint: 'Apple touch icon URL',
ui_apple_touch_icon_hint: 'Apple touch icon url',
ui_custom_image: 'Custom Image',
ui_custom_image_label: 'URL to custom image',
ui_custom_image_hint:
'This image is shown on the LNbits homepage and login screen.',
ui_custom_badge_title: 'Custom Badge Settings',
ui_custom_badge_desc: 'Show a custom badge in the header of LNbits',
ui_custom_badge: 'Custom Badge Text',
ui_custom_image_hint: 'Image showed at homepage/login',
ui_custom_badge: 'Custom Badge',
ui_custom_badge_label: "Custom Badge 'USE WITH CAUTION'",
ui_custom_badge_color_label: 'Custom Badge Color',
themes: 'Themes',
themes_hint: 'Choose themes available for users',
custom_logo: 'Custom Logo',
custom_logo_hint: 'URL to logo image',
ad_space_section_title: 'Advertisement Space',
ad_space_section_desc:
'Configure the advertisement space on the wallet sidebar.',
ad_space_title: 'Advertisement Space Title',
ad_space_title_hint: 'Title shown above the advertisement space',
ad_space_title: 'Ad Space Title',
ad_space_title_label: 'Supported by',
ad_slots: 'Advertisement Slots',
ad_slots_hint:
'Advertisement image filepaths in CSV format, extensions can choose to honor. Format: url;img_light_url;img_dark_url, url..',
ads_enabled: 'Enable Advertisement',
ads_disabled: 'Disabled Advertisement',
'Ad url and image filepaths in CSV format, extensions can choose to honor',
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Ads Enabled',
ads_disabled: 'Ads Disabled',
user_management: 'User Management',
admin_users: 'Admin Users',
admin_users_hint: 'Users with admin privileges',
@@ -721,25 +715,6 @@ window.localisation.en = {
allowed_users_label: 'User ID',
allow_creation_user: 'Allow creation of new users',
allow_creation_user_desc: 'Allow creation of new users on the index page',
require_user_activation: 'Require user activation',
require_user_activation_desc:
'New users will be activated only after they pass one of the confirmation methods. Admins can activate users manually from the admin panel.',
reusable_activation_code: 'Reusable activation code',
reusable_activation_code_label: 'Reusable activation code',
reusable_activation_code_hint:
'This activation code can be used multiple times by different users.',
one_time_activation_code: 'One-time activation codes',
one_time_activation_code_label: 'Add activation code',
one_time_activation_code_hint:
'List of one-time activation codes. Each code can be used only once, then will be reomved from the list.',
invitation_code: 'Invitation Code',
invitation_code_hint: 'The invitation code that you have received.',
email: 'Email',
email_confirmation_hint: 'Email address to send the confirmation code to.',
nostr_identifier: 'Nostr Identifier',
nostr_identifier_hint:
'Nostr nip5 identifier or <npub> to send the confirmation code to.',
new_user_not_allowed: 'Registration is disabled.',
start_user_impersonation: 'Impersonate this user',
stop_user_impersonation: 'Stop User Impersonation',
+3 -4
View File
@@ -66,7 +66,7 @@ window._lnbitsApi = {
name: name
})
},
register(username, email, password, password_repeat, invitation_code) {
register(username, email, password, password_repeat) {
return axios({
method: 'POST',
url: '/api/v1/auth/register',
@@ -74,8 +74,7 @@ window._lnbitsApi = {
username,
email,
password,
password_repeat,
invitation_code
password_repeat
}
})
},
@@ -148,7 +147,7 @@ window._lnbitsApi = {
name: name,
wallet_type: walletType,
...opts
})
}).catch(LNbits.utils.notifyApiError)
},
updateWallet(name, wallet) {
return this.request('patch', '/api/v1/wallet', wallet.adminkey, {
+19 -26
View File
@@ -35,6 +35,23 @@ window.app.component('lnbits-extension-rating', {
window.app.component('lnbits-manage', {
template: '#lnbits-manage',
computed: {
showAdmin() {
return this.LNBITS_ADMIN_UI
},
showUsers() {
return this.LNBITS_ADMIN_UI
},
showNode() {
return this.LNBITS_NODE_UI
},
showAudit() {
return this.LNBITS_AUDIT_ENABLED
},
showExtensions() {
return this.LNBITS_EXTENSIONS_DEACTIVATE_ALL === false
}
},
methods: {
isActive(path) {
return window.location.pathname === path
@@ -432,10 +449,8 @@ window.app.component('username-password', {
username: String,
password_1: String,
password_2: String,
invitationCode: String,
resetKey: String
},
data() {
return {
oauth: [
@@ -448,10 +463,8 @@ window.app.component('username-password', {
password: this.password_1,
passwordRepeat: this.password_2,
reset_key: this.resetKey,
confirmationMethod: 'code',
confirmationEmail: '',
confirmationCode: this.invitationCode || '',
showConfirmationCode: false
keycloakOrg: LNBITS_AUTH_KEYCLOAK_ORG || 'Keycloak',
keycloakIcon: LNBITS_AUTH_KEYCLOAK_ICON
}
},
methods: {
@@ -464,7 +477,6 @@ window.app.component('username-password', {
this.$emit('update:userName', this.username)
this.$emit('update:password_1', this.password)
this.$emit('update:password_2', this.passwordRepeat)
this.$emit('update:invitationCode', this.confirmationCode)
this.$emit('register')
},
reset() {
@@ -556,25 +568,6 @@ window.app.component('username-password', {
computed: {
showOauth() {
return this.oauth.some(m => this.authMethods.includes(m))
},
disableRegister() {
const usernameOK = !!this.username
const passwordOK = !!this.password && this.password.length >= 8
const passwordsMatch = this.password === this.passwordRepeat
const codeOk =
this.confirmationMethodsCount === 0 ||
this.confirmationMethod !== 'code' ||
this.confirmationCode.length > 0
return !usernameOK || !passwordOK || !passwordsMatch || !codeOk
},
confirmationMethodsCount() {
const methods = [
this.g.settings.userActivationByEmail,
this.g.settings.userActivationByPayment,
this.g.settings.userActivationByInvitationCode
]
return methods.filter(Boolean).length
}
},
created() {}
@@ -228,48 +228,6 @@ window.app.component('lnbits-admin-funding-sources', {
spark_token: 'Token'
}
],
[
'SparkL2Wallet',
'Spark (L2)',
{
spark_l2_external_endpoint: {
label: 'External Sidecar Endpoint',
hint: 'Make sure to also specify the API key if your sidecar requires authentication.',
value: ''
},
spark_l2_mnemonic: {
label: 'External Sidecar Mnemonic',
hint: 'Mnemonic for the Spark wallet on the external sidecar. Required if the side car does not have its own mnemonic.',
value: ''
},
spark_l2_external_api_key: {
label: 'External Sidecar API Key',
hint: 'API key for authenticating with the external sidecar if it requires authentication.',
value: ''
},
spark_l2_network: {
label: 'Network',
value: 'MAINNET',
hint: 'The network to use for the Spark wallet.',
advanced: true
},
spark_l2_pay_wait_ms: {
label: 'Payment Wait Time (ms)',
hint: 'The time to wait for a payment to be processed before considering it failed.',
advanced: true
},
spark_l2_pay_poll_ms: {
label: 'Payment Poll Time (ms)',
hint: 'The time to wait between polling for payment status updates.',
advanced: true
},
spark_l2_stream_keepalive_ms: {
label: 'Stream Keepalive Time (ms)',
hint: 'The time to wait between sending keepalive messages to the Spark sidecar to keep the connection open.',
advanced: true
}
}
],
[
'NWCWallet',
'Nostr Wallet Connect',
@@ -8,11 +8,58 @@ window.app.component('lnbits-admin-security', {
serverlogEnabled: false,
nostrAcceptedUrl: '',
formAllowedIPs: '',
formCallbackUrlRule: ''
formCallbackUrlRule: '',
routeOptions: [],
routeOptionsFiltered: [],
routeOptionsLoading: false
}
},
created() {},
created() {
this.loadOpenApiRoutes()
},
methods: {
async loadOpenApiRoutes() {
this.routeOptionsLoading = true
try {
const response = await fetch('/openapi.json')
if (!response.ok) {
throw new Error('Failed to load OpenAPI spec')
}
const data = await response.json()
const paths = Object.keys(data.paths || {}).sort()
this.routeOptions = paths
this.routeOptionsFiltered = paths
} catch (error) {
console.warn(error)
} finally {
this.routeOptionsLoading = false
}
},
addRouteOption(value, done) {
const route = value.trim()
if (!route) {
done()
return
}
if (!this.routeOptions.includes(route)) {
this.routeOptions.push(route)
this.routeOptions.sort()
}
this.routeOptionsFiltered = this.routeOptions
done(route)
},
filterRouteOptions(val, update) {
update(() => {
if (!val) {
this.routeOptionsFiltered = this.routeOptions
return
}
const needle = val.toLowerCase()
this.routeOptionsFiltered = this.routeOptions.filter(route =>
route.toLowerCase().includes(needle)
)
})
},
addAllowedIPs() {
const allowedIPs = this.formAllowedIPs.trim()
const allowed_ips = this.formData.lnbits_allowed_ips
@@ -4,9 +4,7 @@ window.app.component('lnbits-admin-users', {
data() {
return {
formAddUser: '',
formAddAdmin: '',
formAddActivationCode: '',
showReusableActivationCode: false
formAddAdmin: ''
}
},
methods: {
@@ -33,24 +31,6 @@ window.app.component('lnbits-admin-users', {
removeAdminUser(user) {
let admin_users = this.formData.lnbits_admin_users
this.formData.lnbits_admin_users = admin_users.filter(u => u !== user)
},
addOneTimeActivationCode() {
const code = this.formAddActivationCode
const activationCodes =
this.formData.lnbits_register_one_time_activation_codes
if (code?.length && !activationCodes.includes(code)) {
this.formData.lnbits_register_one_time_activation_codes = [
...activationCodes,
code
]
this.formAddActivationCode = ''
}
},
removeOneTimeActivationCode(code) {
const codes = this.formData.lnbits_register_one_time_activation_codes
this.formData.lnbits_register_one_time_activation_codes = codes.filter(
u => u !== code
)
}
}
})
+7 -4
View File
@@ -1,11 +1,14 @@
window.app.component('lnbits-footer', {
template: '#lnbits-footer',
computed: {
title() {
return `${this.g.settings.siteTitle}, ${this.g.settings.siteTagline}`
},
version() {
return this.$t('lnbits_version') + ': ' + this.g.settings.version
return this.LNBITS_VERSION || 'unknown version'
},
title() {
return `${this.SITE_TITLE}, ${this.SITE_TAGLINE}`
},
showFooter() {
return this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
}
}
})
@@ -1,9 +1,48 @@
window.app.component('lnbits-header', {
template: '#lnbits-header',
computed: {
hasServiceFeeMax() {
return (
this.g.user &&
this.LNBITS_SERVICE_FEE_MAX &&
this.LNBITS_SERVICE_FEE_MAX > 0
)
},
serviceFeeMax() {
return this.LNBITS_SERVICE_FEE_MAX || 0
},
hasServiceFee() {
return (
this.g.user && this.LNBITS_SERVICE_FEE && this.LNBITS_SERVICE_FEE > 0
)
},
serviceFee() {
return this.LNBITS_SERVICE_FEE || 0
},
hasCustomBadge() {
return this.LNBITS_CUSTOM_BADGE && this.LNBITS_CUSTOM_BADGE != ''
},
customBadge() {
return this.LNBITS_CUSTOM_BADGE || ''
},
customBadgeColor() {
return this.LNBITS_CUSTOM_BADGE_COLOR || ''
},
title() {
return this.SITE_TITLE
},
titleIsLnbits() {
return this.SITE_TITLE == 'LNbits'
},
customLogoUrl() {
return this.USE_CUSTOM_LOGO || null
},
showAdmin() {
return this.g.user && (this.g.user.super_user || this.g.user.admin)
},
showVoidwallet() {
return this.g.user && this.VOIDWALLET == true
},
displayName() {
return (
this.g.user?.extra?.display_name ||
@@ -95,8 +95,8 @@ window.app.component('lnbits-home-logos', {
showLogos() {
return (
this.g.isSatsDenomination &&
this.g.settings.siteTitle == 'LNbits' &&
this.g.settings.showHomePageElements == true
this.SITE_TITLE == 'LNbits' &&
this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
)
}
}
@@ -17,13 +17,6 @@ window.app.component('lnbits-manage-wallet-list', {
} else {
this.activeWalletId = null
}
},
'g.user.wallets': {
handler() {
this.paymentEvents()
},
deep: true,
immediate: true
}
},
created() {
+1 -1
View File
@@ -34,7 +34,7 @@ window.app.component('lnbits-qrcode', {
},
logo: {
type: String,
default: window.g.settings.qrLogo || null
default: LNBITS_QR_LOGO
}
},
data() {
+1 -34
View File
@@ -45,30 +45,6 @@ window.app.component('lnbits-theme', {
document.body.classList.remove('gradient-bg')
}
},
'g.cardRoundedChoice'(val) {
this.$q.localStorage.set('lnbits.cardRounded', val)
if (val === true) {
document.body.classList.add('rounded-ui')
} else {
document.body.classList.remove('rounded-ui')
}
},
'g.cardGradientChoice'(val) {
this.$q.localStorage.set('lnbits.cardGradient', val)
if (val === true) {
document.body.classList.add('card-gradient')
} else {
document.body.classList.remove('card-gradient')
}
},
'g.cardShadowChoice'(val) {
this.$q.localStorage.set('lnbits.cardShadow', val)
if (val === true) {
document.body.classList.add('card-shadow')
} else {
document.body.classList.remove('card-shadow')
}
},
'g.mobileSimple'(val) {
this.$q.localStorage.set('lnbits.mobileSimple', val)
if (val === true) {
@@ -141,15 +117,6 @@ window.app.component('lnbits-theme', {
if (this.g.gradientChoice === true) {
document.body.classList.add('gradient-bg')
}
if (this.g.cardRoundedChoice === true) {
document.body.classList.add('rounded-ui')
}
if (this.g.cardGradientChoice === true) {
document.body.classList.add('card-gradient')
}
if (this.g.cardShadowChoice === true) {
document.body.classList.add('card-shadow')
}
if (this.g.bgimageChoice !== '') {
document.body.classList.add('bg-image')
document.body.style.setProperty(
@@ -160,7 +127,7 @@ window.app.component('lnbits-theme', {
if (this.g.mobileSimple === true) {
document.body.classList.add('mobile-simple')
}
Object.entries(this.g.user?.uiCustomization || {}).forEach(
Object.entries(this.g.user.uiCustomization || {}).forEach(
([key, value]) => {
if (key in this.g) {
this.g[key] = value
@@ -71,10 +71,6 @@ window.app.component('lnbits-wallet-extra', {
'lnbits.exchangeRate.' + this.g.wallet.currency,
this.g.exchangeRate
)
if (this.g.exchangeRate <= 0) {
this.g.fiatTracking = false
this.g.isFiatPriority = false
}
})
.catch(e => console.error(e))
}
@@ -97,7 +97,6 @@ window.app.component('lnbits-wallet-new', {
this.g.lastWalletId = res.data.id
this.$router.push(`/wallet/${res.data.id}`)
})
.catch(LNbits.utils.notifyApiError)
}
},
created() {
+32 -36
View File
@@ -9,50 +9,17 @@ const localStore = (key, defaultValue) => {
}
window.g = Vue.reactive({
// vars from server
settings: SETTINGS,
currencies: CURRENCIES,
extensions: SETTINGS.extensions,
allowedCurrencies: SETTINGS.allowedCurrencies,
denomination: SETTINGS.denomination,
isSatsDenomination: SETTINGS.denomination == 'sats',
// local storage vars
themeChoice: localStore('lnbits.theme', SETTINGS.defaultTheme),
borderChoice: localStore('lnbits.border', SETTINGS.defaultBorder),
gradientChoice: localStore('lnbits.gradientBg', SETTINGS.defaultGradient),
cardRoundedChoice: localStore(
'lnbits.cardRounded',
SETTINGS.defaultCardRounded
),
cardGradientChoice: localStore(
'lnbits.cardGradient',
SETTINGS.defaultCardGradient
),
cardShadowChoice: localStore('lnbits.cardShadow', SETTINGS.defaultCardShadow),
reactionChoice: localStore('lnbits.reactions', SETTINGS.defaultReaction),
bgimageChoice: localStore(
'lnbits.backgroundImage',
SETTINGS.defaultBgimage || ''
),
locale: localStore('lnbits.lang', navigator.languages[1] ?? 'en'),
disclaimerShown: localStore('lnbits.disclaimerShown', false),
isFiatPriority: localStore('lnbits.isFiatPriority', false),
mobileSimple: localStore('lnbits.mobileSimple', true),
walletFlip: localStore('lnbits.walletFlip', false),
lastActiveWallet: localStore('lnbits.lastActiveWallet', null),
darkChoice: localStore('lnbits.darkMode', SETTINGS.defaultDark),
// cookie vars
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
isUserImpersonated: !!Quasar.Cookies.get('is_lnbits_user_impersonated'),
// frontend vars
errorCode: null,
errorMessage: null,
user: null,
wallet: null,
isPublicPage: true,
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
isUserImpersonated: !!Quasar.Cookies.get('is_lnbits_user_impersonated'),
offline: !navigator.onLine,
hasCamera: false,
visibleDrawer: false,
extensions: WINDOW_SETTINGS.EXTENSIONS,
fiatBalance: 0,
exchangeRate: 0,
fiatTracking: false,
@@ -60,6 +27,35 @@ window.g = Vue.reactive({
walletEventListeners: [],
updatePayments: false, // used for updating the lnbits-payment-list
updatePaymentsHash: false, // used for closing the receive dialog
currencies: WINDOW_SETTINGS.LNBITS_CURRENCIES ?? [],
allowedCurrencies: WINDOW_SETTINGS.LNBITS_ALLOWED_CURRENCIES ?? [],
locale: localStore('lnbits.lang', navigator.languages[1] ?? 'en'),
disclaimerShown: localStore('lnbits.disclaimerShown', false),
isFiatPriority: localStore('lnbits.isFiatPriority', false),
mobileSimple: localStore('lnbits.mobileSimple', true),
walletFlip: localStore('lnbits.walletFlip', false),
lastActiveWallet: localStore('lnbits.lastActiveWallet', null),
darkChoice: localStore('lnbits.darkMode', true),
themeChoice: localStore('lnbits.theme', WINDOW_SETTINGS.LNBITS_DEFAULT_THEME),
borderChoice: localStore(
'lnbits.border',
WINDOW_SETTINGS.LNBITS_DEFAULT_BORDER || 'hard-border'
),
gradientChoice: localStore(
'lnbits.gradientBg',
WINDOW_SETTINGS.LNBITS_DEFAULT_GRADIENT || false
),
reactionChoice: localStore(
'lnbits.reactions',
WINDOW_SETTINGS.LNBITS_DEFAULT_REACTION || 'confettiBothSides'
),
bgimageChoice: localStore(
'lnbits.backgroundImage',
WINDOW_SETTINGS.LNBITS_DEFAULT_BGIMAGE || ''
),
ads: WINDOW_SETTINGS.AD_SPACE.split(',').map(ad => ad.split(';')),
denomination: WINDOW_SETTINGS.LNBITS_DENOMINATION,
isSatsDenomination: WINDOW_SETTINGS.LNBITS_DENOMINATION == 'sats',
scanner: null,
newWalletType: null
})
+2 -1
View File
@@ -157,7 +157,8 @@ window.app.mixin({
return {
api: window._lnbitsApi,
utils: window._lnbitsUtils,
g: window.g
g: window.g,
...WINDOW_SETTINGS
}
},
// backwards compatibility for extensions, should not be used in the future
+8 -16
View File
@@ -40,7 +40,13 @@ window.PageAccount = {
}
],
defaultSiteCustomisation: {
locale: 'en'
locale: 'en',
themeChoice: 'salvador',
bgimageChoice: '',
gradientChoice: true,
darkChoice: true,
borderChoice: 'hard-border',
reactionChoice: 'confettiBothSides'
},
reactionOptions: [
'None',
@@ -692,20 +698,6 @@ window.PageAccount = {
} catch (e) {
LNbits.utils.notifyApiError(e)
}
},
resetThemeDefaults() {
const defaults = {
themeChoice: this.g.settings.defaultTheme,
borderChoice: this.g.settings.defaultBorder,
gradientChoice: this.g.settings.defaultGradient,
bgimageChoice: this.g.settings.defaultBgimage || '',
reactionChoice: this.g.settings.defaultReaction,
darkChoice: this.g.settings.defaultDark,
cardRoundedChoice: this.g.settings.defaultCardRounded,
cardGradientChoice: this.g.settings.defaultCardGradient,
cardShadowChoice: this.g.settings.defaultCardShadow
}
this.siteCustomisationChanged(defaults)
}
},
@@ -719,7 +711,7 @@ window.PageAccount = {
await this.getUserAssets()
// filter out themes that are not allowed
this.themeOptions = this.themeOptions.filter(theme =>
this.g.settings.themeOptions.includes(theme.name)
this.LNBITS_THEME_OPTIONS.includes(theme.name)
)
}
}
+22 -1
View File
@@ -8,7 +8,9 @@ window.PageAdmin = {
lnbits_exchange_rate_providers: [],
lnbits_audit_exclude_paths: [],
lnbits_audit_include_paths: [],
lnbits_audit_http_response_codes: []
lnbits_audit_http_response_codes: [],
lnbits_route_access_whitelist: [],
lnbits_route_access_blacklist: []
},
isSuperUser: false,
needsRestart: false
@@ -69,6 +71,25 @@ window.PageAdmin = {
.catch(LNbits.utils.notifyApiError)
},
updateSettings() {
if (this.shouldConfirmRouteAccess()) {
LNbits.utils
.confirmDialog(this.$t('route_access_save_confirm'))
.onOk(() => this.persistSettings())
return
}
this.persistSettings()
},
shouldConfirmRouteAccess() {
const fields = [
'lnbits_route_access_control_enabled',
'lnbits_route_access_whitelist',
'lnbits_route_access_blacklist'
]
return fields.some(
field => !_.isEqual(this.settings[field], this.formData[field])
)
},
persistSettings() {
const data = _.omit(this.formData, [
'is_super_user',
'lnbits_allowed_funding_sources',
+2 -2
View File
@@ -842,8 +842,8 @@ window.PageExtensions = {
},
async created() {
this.extensions = await this.fetchAllExtensions()
this.extbuilderEnabled = this.g.user.admin || this.g.settings.extBuilder
this.reviewsUrl = this.g.settings.extensionsReviewsUrl
this.extbuilderEnabled = this.g.user.admin || this.LNBITS_EXT_BUILDER
this.reviewsUrl = this.LNBITS_EXTENSIONS_REVIEWS_URL
if (this.g.user.extensions.length === 0) {
this.tab = 'all'
+10 -16
View File
@@ -7,8 +7,7 @@ window.PageFirstInstall = {
isPwdRepeat: true,
username: '',
password: '',
passwordRepeat: '',
firstInstallToken: ''
passwordRepeat: ''
}
}
},
@@ -18,25 +17,20 @@ window.PageFirstInstall = {
}
},
methods: {
setPassword() {
LNbits.api
.request('PUT', `/api/v1/auth/first_install`, null, {
async setPassword() {
try {
await LNbits.api.request('PUT', '/api/v1/auth/first_install', null, {
username: this.loginData.username,
password: this.loginData.password,
password_repeat: this.loginData.passwordRepeat,
first_install_token: this.loginData.firstInstallToken
password_repeat: this.loginData.passwordRepeat
})
.then(async () => {
const res = await LNbits.api.getAuthUser()
this.g.user = LNbits.map.user(res.data)
this.g.isPublicPage = false
this.$router.push('/admin')
})
.catch(this.utils.notifyApiError)
window.location.href = '/admin'
} catch (e) {
LNbits.utils.notifyApiError(e)
}
}
},
created() {
const params = new URLSearchParams(window.location.search)
this.loginData.firstInstallToken = params.get('token') || ''
document.title = 'First Install - LNbits'
}
}
+29 -7
View File
@@ -11,7 +11,6 @@ window.PageHome = {
email: '',
password: '',
passwordRepeat: '',
invitationCode: '',
walletName: '',
signup: false
}
@@ -20,15 +19,40 @@ window.PageHome = {
showClaimLnurl() {
return (
this.lnurl !== '' &&
this.g.settings.allowRegister &&
'user-id-only' in this.g.settings.authMethods
this.allowRegister &&
'user-id-only' in this.LNBITS_AUTH_METHODS
)
},
formatDescription() {
return LNbits.utils.convertMarkdown(this.g.settings.siteDescription)
return LNbits.utils.convertMarkdown(this.SITE_DESCRIPTION)
},
isAccessTokenExpired() {
return this.$q.cookies.get('is_access_token_expired')
},
allowRegister() {
return this.LNBITS_NEW_ACCOUNTS_ALLOWED
},
// TODO: this makes no sense
hasCustomImage() {
return this.LNBITS_CUSTOM_IMAGE
},
showHomepageElements() {
return this.LNBITS_SHOW_HOME_PAGE_ELEMENTS === true
},
siteTitle() {
return this.SITE_TITLE || ''
},
siteTagline() {
return this.SITE_TAGLINE || ''
},
adsEnabled() {
return this.AD_SPACE_ENABLED && this.AD_SPACE && this.AD_SPACE.length > 0
},
adsTitle() {
return this.AD_SPACE_TITLE || ''
},
ads() {
return this.AD_SPACE.map(ad => ad.split(';'))
}
},
methods: {
@@ -41,7 +65,6 @@ window.PageHome = {
this.username = null
this.password = null
this.passwordRepeat = null
this.invitationCode = null
this.authAction = 'register'
this.authMethod = authMethod
@@ -53,8 +76,7 @@ window.PageHome = {
this.username,
this.email,
this.password,
this.passwordRepeat,
this.invitationCode
this.passwordRepeat
)
this.refreshAuthUser()
} catch (e) {
+4 -17
View File
@@ -70,10 +70,10 @@ window.PageUsers = {
usersTable: {
columns: [
{
name: 'activated',
name: 'admin',
align: 'left',
label: this.$t('activated'),
field: 'activated',
label: 'Admin',
field: 'admin',
sortable: false
},
{
@@ -401,7 +401,7 @@ window.PageUsers = {
toggleAdmin(userId) {
LNbits.api
.request('PUT', `/users/api/v1/user/${userId}/admin`)
.request('GET', `/users/api/v1/user/${userId}/admin`)
.then(() => {
this.fetchUsers()
Quasar.Notify.create({
@@ -412,19 +412,6 @@ window.PageUsers = {
})
.catch(LNbits.utils.notifyApiError)
},
toggleUserActivated(userId) {
LNbits.api
.request('PUT', `/users/api/v1/user/${userId}/activate`)
.then(res => {
this.fetchUsers()
Quasar.Notify.create({
type: 'positive',
message: res.data.message,
icon: null
})
})
.catch(LNbits.utils.notifyApiError)
},
async showAccountPage(user_id) {
this.activeUser.showPassword = false
this.activeUser.showUserId = false
+2 -22
View File
@@ -1,7 +1,7 @@
window._lnbitsUtils = {
url_for(url) {
const _url = new URL(url, window.location.origin)
_url.searchParams.set('v', window.g.settings.cacheKey)
_url.searchParams.set('v', WINDOW_SETTINGS.CACHE_KEY)
return _url.toString()
},
loadScript(src) {
@@ -131,22 +131,6 @@ window._lnbitsUtils = {
currency: currency || 'sat'
}).format(value)
},
getCurrencySymbol(currency) {
const code = (currency || '').toUpperCase()
if (code === 'BTC' || code === 'XBT' || code === 'SAT' || code === 'SATS') {
return '₿'
}
try {
const parts = new Intl.NumberFormat(window.i18n.global.locale, {
style: 'currency',
currency: code
}).formatToParts(0)
const symbolPart = parts.find(part => part.type === 'currency')
return symbolPart?.value || code || '¤'
} catch (e) {
return code || '¤'
}
},
formatSat(value) {
return new Intl.NumberFormat(window.i18n.global.locale).format(value)
},
@@ -160,7 +144,7 @@ window._lnbitsUtils = {
return null
}
},
async notifyApiError(error) {
notifyApiError(error) {
if (!error.response) {
return console.error(error)
}
@@ -170,10 +154,6 @@ window._lnbitsUtils = {
500: 'negative'
}
let messages = error.response.data.detail
if (!messages) {
const text = await error.response.data?.text()
messages = this.parseJSONSafe(text)?.detail
}
if (messages) {
messages = Array.isArray(messages)
? messages.map(e => e.msg + ` (${e.loc?.join('/')})`)
-1
View File
@@ -1,7 +1,6 @@
@use 'themes';
@use 'borders';
@use 'background';
@use 'cards';
@use 'home';
[v-cloak] {
-72
View File
@@ -1,72 +0,0 @@
@use 'sass:color';
@use 'sass:map';
@use 'themes';
$card-radius: 10px;
body.rounded-ui {
.q-card,
.q-btn {
border-radius: $card-radius;
}
}
@each $theme, $colors in themes.$themes {
$primary: map.get($colors, primary);
$secondary: map.get($colors, secondary);
$dark: map.get($colors, dark);
body[data-theme='#{$theme}'].card-gradient {
.q-card:not(
.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark
) {
background-image: linear-gradient(
135deg,
color.adjust($primary, $alpha: -0.92),
color.adjust($secondary, $alpha: -0.94)
);
}
.q-drawer {
background-image: linear-gradient(
165deg,
color.adjust($primary, $alpha: -0.94),
color.adjust($secondary, $alpha: -0.96)
);
}
}
body[data-theme='#{$theme}'].card-gradient.body--dark {
.q-card:not(
.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark
) {
background-image: linear-gradient(
135deg,
color.adjust($dark, $lightness: 4%),
color.adjust($primary, $alpha: -0.93)
);
}
.q-drawer {
background-image: linear-gradient(
165deg,
color.adjust($dark, $lightness: 3%),
color.adjust($primary, $alpha: -0.95)
);
}
}
}
body.card-shadow {
.q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.18));
}
}
body.card-shadow.body--dark {
.q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.45));
}
}
+904 -786
View File
File diff suppressed because it is too large Load Diff
+1 -5
View File
@@ -178,11 +178,7 @@ async def invoice_callback_dispatcher(checking_id: str, is_internal: bool = Fals
logger.warning(f"Payment '{checking_id}' is not incoming, skipping.")
return
from lnbits.core.services.payments import check_payment_status
status = await check_payment_status(
payment, skip_internal_payment_notifications=True
)
status = await payment.check_status(skip_internal_payment_notifications=True)
payment.fee = status.fee_msat or payment.fee
# only overwrite preimage if status.preimage provides it
payment.preimage = status.preimage or payment.preimage
+4 -4
View File
@@ -70,10 +70,10 @@
</div>
<script type="text/javascript">
const RENDERED_ROUTE = '{{ normalize_path(request.path) }}'
const SETTINGS = {{ SETTINGS | tojson | safe }}
const CURRENCIES = {{ CURRENCIES | tojson | safe }}
// backwards compatibility for extensions (tpos)
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson | safe }}
const WINDOW_SETTINGS = {{ WINDOW_SETTINGS | tojson }}
Object.keys(WINDOW_SETTINGS).forEach(key => {
window[key] = WINDOW_SETTINGS[key]
})
</script>
<!-- vue templates -->
{% include('components.vue') %} {% include('pages.vue') %}
+16 -90
View File
@@ -36,7 +36,7 @@ include('components/lnbits-error.vue') %}
<q-list v-if="g.user" dense class="lnbits-drawer__q-list">
<q-item-label header v-text="$t('manage')"></q-item-label>
<div v-if="g.user.admin">
<q-item v-if="g.settings.showAdmin" to="/admin">
<q-item v-if="showAdmin" to="/admin">
<q-item-section side>
<q-icon
name="settings"
@@ -51,7 +51,7 @@ include('components/lnbits-error.vue') %}
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
</q-item-section>
</q-item>
<q-item v-if="g.settings.showNodemanager" to="/node">
<q-item v-if="showNode" to="/node">
<q-item-section side>
<q-icon
name="developer_board"
@@ -66,7 +66,7 @@ include('components/lnbits-error.vue') %}
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
</q-item-section>
</q-item>
<q-item v-if="g.settings.showAdmin" to="/users">
<q-item v-if="showUsers" to="/users">
<q-item-section side>
<q-icon
name="groups"
@@ -81,7 +81,7 @@ include('components/lnbits-error.vue') %}
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
</q-item-section>
</q-item>
<q-item v-if="g.settings.showAudit" to="/audit">
<q-item v-if="showAudit" to="/audit">
<q-item-section side>
<q-icon
name="playlist_add_check_circle"
@@ -112,7 +112,7 @@ include('components/lnbits-error.vue') %}
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
</q-item-section>
</q-item>
<q-item v-if="g.settings.showExtensions" to="/extensions">
<q-item v-if="showExtensions" to="/extensions">
<q-item-section side>
<q-icon
name="extension"
@@ -686,7 +686,7 @@ include('components/lnbits-error.vue') %}
dense
filled
v-model="walletName"
:label="$t('name_your_wallet', {name: g.settings.siteTitle + ' *'})"
:label="$t('name_your_wallet', {name: SITE_TITLE + ' *'})"
></q-input>
<q-card-actions vertical align="center" class="q-pa-none">
<q-btn
@@ -814,90 +814,16 @@ include('components/lnbits-error.vue') %}
type="password"
:rules="[val => !val || val.length >= 8 || $t('invalid_password')]"
></q-input>
<div
v-if="confirmationMethodsCount > 1"
class="row justify-center q-mb-md"
>
<q-tabs
v-model="confirmationMethod"
dense
active-color="primary"
indicator-color="primary"
>
<q-tab
v-if="g.settings.userActivationByInvitationCode"
name="code"
icon="confirmation_number"
label="Code"
></q-tab>
<q-tab
v-if="g.settings.userActivationByPayment"
name="payment"
icon="bolt"
label="Payment"
></q-tab>
<q-tab
v-if="g.settings.userActivationByEmail"
name="email"
icon="email"
label="Email"
></q-tab>
</q-tabs>
</div>
<div v-if="confirmationMethodsCount > 0" class="q-mb-md">
<q-tab-panels v-model="confirmationMethod">
<q-tab-panel name="code" class="q-pa-none">
<div
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
>
<q-icon name="info" color="orange-4" class="q-mr-xs"></q-icon>
You need an invitation code to register.
</div>
<div>
<q-input
dense
filled
v-model="confirmationCode"
:label="$t('invitation_code')"
:type="showConfirmationCode ? 'text' : 'password'"
:hint="$t('invitation_code_hint')"
>
<q-btn
@click="showConfirmationCode = !showConfirmationCode"
dense
flat
:icon="
showConfirmationCode ? 'visibility_off' : 'visibility'
"
color="grey"
></q-btn>
</q-input>
</div>
</q-tab-panel>
<q-tab-panel name="payment">
<div>payment</div>
</q-tab-panel>
<q-tab-panel name="email" class="q-pa-none">
<div>
<q-input
dense
filled
v-model="confirmationEmail"
:label="$t('email')"
:hint="$t('email_confirmation_hint')"
>
</q-input>
</div>
</q-tab-panel>
</q-tab-panels>
</div>
<div class="row justify-end">
<q-btn
unelevated
color="primary"
:disable="disableRegister"
:disable="
!password ||
!passwordRepeat ||
!username ||
password !== passwordRepeat
"
type="submit"
class="full-width"
:label="$t('create_account')"
@@ -1034,9 +960,9 @@ include('components/lnbits-error.vue') %}
<q-avatar size="32px" class="q-mr-md">
<q-img
:src="
g.settings.keycloakIcon
? g.settings.keycloakIcon
: utils.url_for('lnbits/static/images/keycloak-logo.png')
keycloakIcon
? keycloakIcon
: 'lnbits/static/images/keycloak-logo.png'
"
></q-img>
</q-avatar>
@@ -1044,7 +970,7 @@ include('components/lnbits-error.vue') %}
<span
v-text="
$t('signin_with_custom_org', {
custom_org: g.settings.keycloakOrg || 'Keycloak'
custom_org: keycloakOrg
})
"
></span>
@@ -3,6 +3,9 @@
<h6 class="q-my-none q-mb-sm">Assets</h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-3">
<p>
<span v-text="$t('max_asset_size_mb')"></span>
</p>
<q-input
filled
type="number"
@@ -15,6 +18,9 @@
</div>
<div class="col-12 col-md-9">
<p>
<span v-text="$t('assets_allowed_mime_types')"></span>
</p>
<q-input
filled
v-model.number="newAllowedAssetMimeType"
@@ -50,8 +56,11 @@
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none q-mb-sm">Thumbnails</h6>
<div class="row q-col-gutter-md q-my-lg">
<div class="col-12 col-md-4">
<div class="row q-col-gutter-md">
<div class="col-12 col-md-3">
<p>
<span v-text="$t('thumbnail_width')"></span>
</p>
<q-input
filled
type="number"
@@ -62,7 +71,10 @@
:hint="$t('thumbnail_width_desc')"
></q-input>
</div>
<div class="col-12 col-md-4">
<div class="col-12 col-md-3">
<p>
<span v-text="$t('thumbnail_height')"></span>
</p>
<q-input
filled
type="number"
@@ -73,7 +85,10 @@
:hint="$t('thumbnail_height_desc')"
></q-input>
</div>
<div class="col-12 col-md-4">
<div class="col-12 col-md-3">
<p>
<span v-text="$t('thumbnail_format')"></span>
</p>
<q-input
filled
v-model.number="formData.lnbits_asset_thumbnail_format"
@@ -86,6 +101,9 @@
<h6 class="q-my-none q-mb-sm">Users</h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-3">
<p>
<span v-text="$t('max_assets_per_user')"></span>
</p>
<q-input
filled
type="number"
@@ -97,6 +115,9 @@
></q-input>
</div>
<div class="col-12 col-md-9">
<p>
<span v-text="$t('assets_no_limit_users')"></span>
</p>
<q-input
filled
v-model="newNoLimitUser"
+10 -4
View File
@@ -32,7 +32,7 @@
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<div class="row">
<div class="col-md-6 col-sm-12 q-pr-sm">
<div class="col-md-3 col-sm-12 q-pr-sm">
<q-item tag="label" v-ripple>
<q-item-section avatar>
<q-toggle
@@ -62,7 +62,7 @@
</q-item-section>
</q-item>
</div>
<div class="col-md-6 col-sm-12 q-pr-sm">
<div class="col-md-3 col-sm-12 q-pr-sm">
<q-item tag="label" v-ripple>
<q-item-section avatar>
<q-toggle
@@ -83,7 +83,7 @@
</q-item-section>
</q-item>
</div>
<div class="col-md-6 col-sm-12 q-pr-sm">
<div class="col-md-3 col-sm-12 q-pr-sm">
<q-item tag="label" v-ripple>
<q-item-section avatar>
<q-toggle
@@ -104,7 +104,7 @@
</q-item-section>
</q-item>
</div>
<div class="col-md-6 col-sm-12 q-pr-sm">
<div class="col-md-3 col-sm-12 q-pr-sm">
<q-item tag="label" v-ripple>
<q-item-section avatar>
<q-toggle
@@ -129,6 +129,7 @@
<q-separator class="q-mb-xl q-mt-sm"></q-separator>
<div class="row q-mb-lg">
<div class="col-md-6 col-sm-12 q-pr-sm">
<p><span v-text="$t('audit_http_methods')"></span></p>
<q-select
filled
v-model="formData.lnbits_audit_http_methods"
@@ -147,6 +148,7 @@
></q-select>
</div>
<div class="col-md-6 col-sm-12 q-pr-sm">
<p><span v-text="$t('audit_resp_codes')"></span></p>
<q-input
class="q-mb-md"
filled
@@ -175,6 +177,7 @@
<div class="row">
<div class="col-md-6 col-sm-12 q-pr-sm">
<p><span v-text="$t('audit_paths')"></span></p>
<q-input
class="q-mb-md"
filled
@@ -201,6 +204,9 @@
<br />
</div>
<div class="col-md-6 col-sm-12">
<p>
<span v-text="$t('audit_paths_exclude')"></span>
</p>
<q-input
class="q-mb-md"
filled
@@ -19,7 +19,6 @@
</div>
<div class="col-12 col-md-4">
<q-input
class="q-mb-md"
filled
v-model="formData.lnbits_exchange_history_refresh_interval_seconds"
type="number"
@@ -137,7 +137,7 @@
:hint="$t('extension_builder_manifest_url_hint')"
></q-input>
</div>
<div class="col-12 col-md-6 q-pb-md">
<div class="col-12 col-md-6">
<p>
<span v-text="$t('reviews_url')"></span>
</p>
@@ -128,7 +128,7 @@
</ul>
</q-card-section>
</q-expansion-item>
<q-expansion-item :label="$t('service_fees')">
<q-expansion-item :label="$t('service_fee')">
<q-card-section>
<div class="row">
<div class="col-md-4 col-sm-12">
@@ -140,6 +140,7 @@
v-model="formData.stripe_limits.service_fee_percent"
@update:model-value="formData.touch = null"
:label="$t('service_fee_label')"
:hint="$t('service_fee_hint')"
></q-input>
</div>
<div class="col-md-4 col-sm-12">
@@ -150,7 +151,8 @@
min="0"
v-model="formData.stripe_limits.service_max_fee_sats"
@update:model-value="formData.touch = null"
:label="$t('service_fee_max_label')"
:label="$t('service_fee_max')"
:hint="$t('service_fee_max_hint')"
></q-input>
</div>
<div class="col-md-4 col-sm-12">
@@ -406,6 +408,7 @@
<li><code>CHECKOUT.ORDER.APPROVED</code></li>
<li><code>PAYMENT.CAPTURE.COMPLETED</code></li>
<li><code>PAYMENT.SALE.COMPLETED</code></li>
<li><code>BILLING.SUBSCRIPTION.PAYMENT.SUCCEEDED</code></li>
</ul>
</q-card-section>
</q-expansion-item>
@@ -50,7 +50,7 @@
<br />
</div>
<div class="col">
<div v-if="g.settings.hasNodemanager">
<div v-if="LNBITS_NODE_UI">
<p>
<span v-text="$t('node_management')"></span>
</p>
@@ -70,7 +70,7 @@
v-model="formData.lnbits_node_ui_transactions"
></q-toggle>
</div>
<p v-if="!g.settings.hasNodemanager">
<p v-if="!LNBITS_NODE_UI">
<span v-text="$t('node_management_not_supported')"></span>
</p>
</div>
@@ -165,11 +165,6 @@
<h6 class="q-mt-lg q-mb-sm">
<p v-text="$t('watchdog')"></p>
</h6>
<div class="row q-col-gutter-md">
<div class="col-12">
<p v-text="$t('watchdog_introduction')"></p>
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<q-item tag="label" v-ripple>
@@ -244,7 +239,6 @@
<q-input
filled
v-model="formData.lnbits_watchdog_delta"
:suffix="$t('sats')"
type="number"
/>
</q-item-section>
@@ -12,14 +12,11 @@
<div class="row">
<div class="col-12">
<p>
<span v-text="$t('funding_source')"></span>
<small><span v-text="$t('requires_server_restart')"></span></small>
</p>
<p>Active Funding<small> (Requires server restart)</small></p>
<q-select
filled
v-model="formData.lnbits_backend_wallet_class"
:hint="$t('funding_source_info')"
hint="Select the active funding wallet"
:options="sortedAllowedFundingSources"
:option-label="item => getFundingSourceLabel(item)"
></q-select>
+54 -10
View File
@@ -1,10 +1,11 @@
<template id="lnbits-admin-security">
<q-card-section class="q-pa-none">
<h6 class="q-my-none q-mb-sm">
<h6 class="q-my-none">
<span v-text="$t('server_management')"></span>
</h6>
<div class="row">
<div class="col-12 col-md-6">
<p><span v-text="$t('base_url')"></span></p>
<q-input
filled
v-model.number="formData.lnbits_baseurl"
@@ -16,7 +17,7 @@
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('authentication')"></span>
</h6>
<div class="row q-col-gutter-sm q-mb-md">
<div class="row q-col-gutter-sm">
<div class="col-12 col-md-6">
<q-input
filled
@@ -295,6 +296,56 @@
</div>
</div>
<div class="col-12 col-md-12">
<p v-text="$t('route_access_control')"></p>
<div class="row q-col-gutter-md">
<div class="col-12">
<q-toggle
v-model="formData.lnbits_route_access_control_enabled"
:label="$t('route_access_control_enable')"
></q-toggle>
<div
class="text-caption text-grey-6 q-mt-xs"
v-text="$t('route_access_control_hint')"
></div>
</div>
<div class="col-12 col-md-6">
<q-select
filled
multiple
use-chips
use-input
input-debounce="0"
new-value-mode="add-unique"
@new-value="addRouteOption"
:options="routeOptionsFiltered"
:loading="routeOptionsLoading"
@filter="filterRouteOptions"
v-model="formData.lnbits_route_access_whitelist"
:label="$t('route_access_whitelist_label')"
:hint="$t('route_access_whitelist_hint')"
></q-select>
</div>
<div class="col-12 col-md-6">
<q-select
filled
multiple
use-chips
use-input
input-debounce="0"
new-value-mode="add-unique"
@new-value="addRouteOption"
:options="routeOptionsFiltered"
:loading="routeOptionsLoading"
@filter="filterRouteOptions"
v-model="formData.lnbits_route_access_blacklist"
:label="$t('route_access_blacklist_label')"
:hint="$t('route_access_blacklist_hint')"
></q-select>
</div>
</div>
</div>
<div class="col-12 col-md-12">
<p v-text="$t('rate_limiter')"></p>
<div class="row q-col-gutter-md">
@@ -304,19 +355,12 @@
type="number"
v-model.number="formData.lnbits_rate_limit_no"
:label="$t('number_of_requests')"
:hint="$t('number_of_requests_hint')"
></q-input>
</div>
<div class="col-12 col-md-6">
<q-select
filled
:options="[
{label: $t('second'), value: 'second'},
{label: $t('minute'), value: 'minute'},
{label: $t('hour'), value: 'hour'}
]"
emit-value
map-options
:options="[$t('second'), $t('minute'), $t('hour')]"
v-model="formData.lnbits_rate_limit_unit"
:label="$t('time_unit')"
></q-select>
+26 -10
View File
@@ -38,11 +38,14 @@
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none q-mb-sm">
<h6 class="q-my-none">
<span v-text="$t('payments')"></span>
</h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-4">
<div class="col-12 col-md-3">
<p>
<span v-text="$t('max_outgoing_payment_amount')"></span>
</p>
<q-input
filled
type="number"
@@ -54,7 +57,10 @@
></q-input>
</div>
<div class="col-12 col-md-4">
<div class="col-12 col-md-3">
<p>
<span v-text="$t('max_incoming_payment_amount')"></span>
</p>
<q-input
filled
type="number"
@@ -69,7 +75,7 @@
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none q-mb-sm">
<h6 class="q-my-none">
<span v-text="$t('wallet_limiter')"></span>
</h6>
<div class="row q-col-gutter-md">
@@ -107,32 +113,35 @@
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('service_fees')"></span>
<h6 class="q-my-none">
<span v-text="$t('service_fee')"></span>
</h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-4">
<div class="col-12 col-md-6">
<p><span v-text="$t('service_fee')"></span></p>
<q-input
filled
type="number"
v-model.number="formData.lnbits_service_fee"
:label="$t('service_fee_label')"
step="0.1"
:suffix="'%'"
:hint="$t('service_fee_hint')"
></q-input>
<br />
</div>
<div class="col-12 col-md-4">
<div class="col-12 col-md-6">
<p><span v-text="$t('service_fee_max')"></span></p>
<q-input
filled
type="number"
v-model.number="formData.lnbits_service_fee_max"
:label="$t('service_fee_max_label')"
:suffix="$t('sats')"
:hint="$t('service_fee_max_hint')"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('fee_wallet')"></span></p>
<q-input
filled
v-model="formData.lnbits_service_fee_wallet"
@@ -142,9 +151,16 @@
<br />
</div>
<div class="col-12 col-md-6">
<p>
<span v-text="$t('disable_fee_internal')"></span>
</p>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('disable_fee')"></q-item-label>
<q-item-label
caption
v-text="$t('disable_fee_internal_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
@@ -6,7 +6,8 @@
<br />
<div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<div class="col-12 col-md-5">
<p><span v-text="$t('ui_site_title')"></span></p>
<q-input
filled
type="text"
@@ -17,7 +18,8 @@
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<div class="col-12 col-md-5">
<p><span v-text="$t('ui_site_tagline')"></span></p>
<q-input
filled
type="text"
@@ -26,9 +28,7 @@
></q-input>
<br />
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-12">
<div class="col-12 col-md-2 q-mt-xl">
<q-toggle
:tip="$t('ui_toggle_elements_tip')"
v-model="formData.lnbits_show_home_page_elements"
@@ -40,66 +40,78 @@
></q-toggle>
</div>
</div>
<div>
<p>
<span v-text="$t('ui_site_description')"></span>
</p>
<q-input
v-model="formData.lnbits_site_description"
filled
type="textarea"
:hint="$t('ui_site_description_hint')"
/>
</div>
<br />
<div class="row q-col-gutter-md">
<div class="col-12">
<q-input
v-model="formData.lnbits_site_description"
filled
type="textarea"
:label="$t('ui_site_description')"
:hint="$t('ui_site_description_hint')"
/>
</div>
<div class="col-12 col-md-4">
<p>
<span v-text="$t('ui_default_wallet_name')"></span>
</p>
<q-input
filled
type="text"
v-model="formData.lnbits_default_wallet_name"
:label="$t('ui_default_wallet_name')"
:label="$t('lnbits_wallet')"
></q-input>
</div>
<div class="col-12 col-md-4">
<p><span v-text="$t('ui_qr_code_logo')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_qr_logo"
:label="$t('ui_qr_code_logo')"
label="https://example.com/image.svg"
:hint="$t('ui_qr_code_logo_hint')"
></q-input>
</div>
<div class="col-12 col-md-4">
<p><span v-text="$t('ui_apple_touch_icon')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_apple_touch_icon"
:label="$t('ui_apple_touch_icon')"
label="https://example.com/image.png"
:hint="$t('ui_apple_touch_icon_hint')"
></q-input>
</div>
</div>
<br />
<div class="row q-col-gutter-md">
<div class="col-12 col-md-12">
<p><span v-text="$t('wallet_featured_button_title')"></span></p>
</div>
<div class="col-12 col-md-4">
<p>
<span v-text="$t('wallet_featured_button_label')"></span>
</p>
<q-input
filled
type="text"
v-model="formData.lnbits_wallet_featured_button_label"
:label="$t('wallet_featured_button_label')"
label="Loop to Onchain"
:hint="$t('wallet_featured_button_label_hint')"
></q-input>
</div>
<div class="col-12 col-md-4">
<p><span v-text="$t('wallet_featured_button_url')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_wallet_featured_button_url"
:label="$t('wallet_featured_button_url')"
label="/boltz"
:hint="$t('wallet_featured_button_url_hint')"
></q-input>
</div>
<div class="col-12 col-md-4">
<p><span v-text="$t('wallet_featured_button_icon')"></span></p>
<q-input
filled
type="text"
@@ -109,27 +121,21 @@
></q-input>
</div>
</div>
<div class="row q-col-gutter-md q-mt-md">
<div class="col-12 col-md-12">
<h6 class="q-my-sm">
<span v-text="$t('ui_custom_badge_title')"></span>
</h6>
<p class="q-mb-none">
<span v-text="$t('ui_custom_badge_desc')"></span>
</p>
</div>
<div class="col-12">
<div class="col-12 col-md-6">
<p><span v-text="$t('ui_custom_badge')"></span></p>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<div class="col-12 col-md-8">
<q-input
filled
type="text"
tip="Custom Badge"
v-model.trim="formData.lnbits_custom_badge"
:label="$t('ui_custom_badge')"
:label="$t('ui_custom_badge_label')"
></q-input>
</div>
<div class="col-12 col-md-6">
<div class="col-12 col-md-4">
<q-select
filled
v-model="formData.lnbits_custom_badge_color"
@@ -139,8 +145,8 @@
</div>
</div>
</div>
<div class="col-12">
<h6 class="q-my-sm"><span v-text="$t('ui_custom_image')"></span></h6>
<div class="col-12 col-md-6">
<p><span v-text="$t('ui_custom_image')"></span></p>
<q-input
filled
type="text"
@@ -151,8 +157,10 @@
></q-input>
</div>
</div>
<br />
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p><span v-text="$t('themes')"></span></p>
<q-select
filled
v-model="formData.lnbits_theme_options"
@@ -164,26 +172,41 @@
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('custom_logo')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_custom_logo"
:label="$t('custom_logo')"
label="https://example.com/image.png"
:hint="$t('custom_logo_hint')"
></q-input>
<br />
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-12">
<h6 class="q-my-sm">
<span v-text="$t('ad_space_section_title')"></span>
</h6>
<p class="q-my-none">
<span v-text="$t('ad_space_section_desc')"></span>
</p>
<div class="col-12 col-md-6">
<p><span v-text="$t('ad_space_title')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_ad_space_title"
:label="$t('ad_space_title_label')"
></q-input>
<br />
</div>
<div class="col-12 col-md-12">
<div class="col-12 col-md-6">
<p><span v-text="$t('ad_slots')"></span></p>
<q-input
class="q-mb-md"
filled
v-model="formData.lnbits_ad_space"
type="text"
:label="$t('ad_slots_label')"
:hint="$t('ad_slots_hint')"
><q-tooltip>
format {url};{img-light};{img-dark},{url};{img-light};{img-dark}"
</q-tooltip>
</q-input>
<q-toggle
v-model="formData.lnbits_ad_space_enabled"
:label="
@@ -192,30 +215,8 @@
: $t('ads_disabled')
"
/>
</div>
<div class="col-12 col-md-6">
<q-input
filled
type="text"
v-model="formData.lnbits_ad_space_title"
:label="$t('ad_space_title')"
:hint="$t('ad_space_title_hint')"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<q-input
class="q-mb-md"
filled
v-model="formData.lnbits_ad_space"
type="text"
:label="$t('ad_slots')"
:hint="$t('ad_slots_hint')"
><q-tooltip>
format {url};{img-light};{img-dark},{url};{img-light};{img-dark}"
</q-tooltip>
</q-input>
</div>
</div>
<div class="row q-col-gutter-md q-my-md">
<div class="col-12" v-text="$t('ui_default_theme')"></div>
@@ -256,50 +257,12 @@
>
</q-input>
</div>
</div>
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12 col-sm-6 col-lg-2">
<div class="col-12 col-sm-6 col-lg-3">
<q-toggle
type="bool"
v-model="formData.lnbits_default_gradient"
color="primary"
:label="$t('gradient_background')"
>
</q-toggle>
</div>
<div class="col-12 col-sm-6 col-lg-2">
<q-toggle
type="bool"
v-model="formData.lnbits_default_dark"
color="primary"
:label="$t('toggle_darkmode')"
>
</q-toggle>
</div>
<div class="col-12 col-sm-6 col-lg-2">
<q-toggle
type="bool"
v-model="formData.lnbits_default_card_rounded"
color="primary"
:label="$t('rounded_ui')"
>
</q-toggle>
</div>
<div class="col-12 col-sm-6 col-lg-2">
<q-toggle
type="bool"
v-model="formData.lnbits_default_card_gradient"
color="primary"
:label="$t('card_gradient')"
>
</q-toggle>
</div>
<div class="col-12 col-sm-6 col-lg-2">
<q-toggle
type="bool"
v-model="formData.lnbits_default_card_shadow"
color="primary"
:label="$t('card_shadow')"
label="Gradient Background"
>
</q-toggle>
</div>
+3 -234
View File
@@ -30,6 +30,7 @@
>
</q-chip>
</div>
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('allowed_users')"></span></p>
@@ -56,10 +57,7 @@
>
</q-chip>
</div>
</div>
</div>
<div class="row">
<div class="col-12 col-md-6 q-mt-sm">
<br />
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('allow_creation_user')"></q-item-label>
@@ -78,237 +76,8 @@
/>
</q-item-section>
</q-item>
</div>
<div class="col-12 col-md-6 q-mt-sm">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('require_user_activation')"></q-item-label>
<q-item-label
caption
v-text="$t('require_user_activation_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_require_user_activation"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<br />
</div>
</div>
</q-card-section>
<div v-if="formData.lnbits_require_user_activation" class="row">
<div class="col">
<q-list bordered class="rounded-borders">
<q-expansion-item header-class="text-primary text-bold">
<template v-slot:header>
<q-item-section avatar>
<q-icon name="confirmation_number" size="md"></q-icon>
</q-item-section>
<q-item-section> Invitation Code </q-item-section>
<q-item-section side>
<div class="row items-center">
<q-toggle
size="md"
:label="
formData.lnbits_user_activation_by_invitation_code
? $t('enabled')
: $t('disabled')
"
v-model="formData.lnbits_user_activation_by_invitation_code"
color="green"
unchecked-icon="clear"
/>
</div>
</q-item-section>
</template>
<q-card class="q-pb-xl">
<q-card-section>
<div
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
>
<q-icon
name="info"
color="orange-4"
size="18px"
class="q-mr-xs"
></q-icon>
Users will need to provide a valid invitation code during
registration to activate their account.
</div>
</q-card-section>
<q-card-section>
<div class="row">
<div class="col-12 col-md-6 q-pr-sm">
<p><span v-text="$t('reusable_activation_code')"></span></p>
<q-input
filled
v-model="formData.lnbits_register_reusable_activation_code"
:type="showReusableActivationCode ? 'text' : 'password'"
:label="$t('reusable_activation_code_label')"
:hint="$t('reusable_activation_code_hint')"
>
<q-btn
@click="
showReusableActivationCode = !showReusableActivationCode
"
dense
flat
:icon="
showReusableActivationCode
? 'visibility_off'
: 'visibility'
"
color="grey"
></q-btn>
<q-btn
@click="
utils.copyText(
formData.lnbits_register_reusable_activation_code
)
"
dense
flat
icon="content_copy"
color="grey"
></q-btn>
</q-input>
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('one_time_activation_code')"></span></p>
<q-input
filled
v-model="formAddActivationCode"
@keydown.enter="addOneTimeActivationCode"
type="text"
:label="$t('one_time_activation_code_label')"
:hint="$t('one_time_activation_code_hint')"
>
<q-btn
@click="addOneTimeActivationCode"
dense
flat
icon="add"
></q-btn>
</q-input>
<div>
<q-chip
v-for="code in formData.lnbits_register_one_time_activation_codes"
:key="code"
removable
@remove="removeOneTimeActivationCode(code)"
color="primary"
text-color="white"
:label="code"
class="ellipsis"
>
</q-chip>
</div>
</div>
</div>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item header-class="text-primary text-bold">
<template v-slot:header>
<q-item-section avatar>
<q-avatar>
<q-icon name="bolt" size="md"></q-icon>
</q-avatar>
</q-item-section>
<q-item-section> Payment </q-item-section>
<q-item-section side>
<div class="row items-center">
<q-toggle
size="md"
disable
:label="
formData.lnbits_user_activation_by_payment
? $t('enabled')
: $t('disabled')
"
v-model="formData.lnbits_user_activation_by_payment"
color="green"
unchecked-icon="clear"
/>
</div>
</q-item-section>
</template>
<q-card class="q-pb-xl">
<q-card-section>
<div
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
>
<q-icon
name="info"
color="orange-4"
size="18px"
class="q-mr-xs"
></q-icon>
Users will need to make a payment during registration to
activate their account.
</div>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item header-class="text-primary text-bold">
<template v-slot:header>
<q-item-section avatar>
<q-avatar>
<q-icon name="email"></q-icon>
</q-avatar>
</q-item-section>
<q-item-section> Email </q-item-section>
<q-item-section side>
<div class="row items-center">
<q-toggle
disable
size="md"
:label="
formData.lnbits_user_activation_by_email
? $t('enabled')
: $t('disabled')
"
v-model="formData.lnbits_user_activation_by_email"
color="green"
unchecked-icon="clear"
/>
</div>
</q-item-section>
</template>
<q-card class="q-pb-xl">
<q-card-section>
<div
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
>
<q-icon
name="info"
color="orange-4"
size="18px"
class="q-mr-xs"
></q-icon>
Users will receive a confirmation email.
</div>
</q-card-section>
</q-card>
</q-expansion-item>
</q-list>
</div>
</div>
</template>
@@ -1,6 +1,6 @@
<template id="lnbits-footer">
<q-footer
v-if="g.settings.showHomePageElements"
v-if="showFooter"
class="bg-transparent q-px-lg q-py-md"
:class="{'text-dark': !$q.dark.isActive}"
>
@@ -9,7 +9,7 @@
<q-toolbar-title class="text-caption">
<span v-text="title"></span>
<br />
<small v-text="version"></small>
<small v-text="$t('lnbits_version') + ': ' + version"></small>
</q-toolbar-title>
<q-space></q-space>
<q-btn
+17 -20
View File
@@ -1,10 +1,6 @@
<template id="lnbits-header">
<q-header bordered class="bg-marginal-bg">
<q-banner
v-if="g.settings.showVoidwallet"
class="bg-warning text-white"
dense
>
<q-banner v-if="showVoidwallet" class="bg-warning text-white" dense>
<template v-slot:avatar>
<q-icon name="warning" color="white" />
</template>
@@ -27,13 +23,14 @@
></q-btn>
<q-toolbar-title>
<q-btn flat no-caps dense class="q-mr-sm" size="lg" type="a" href="/">
<q-avatar v-if="g.settings.customLogo" height="30px">
<img alt="Logo" :src="g.settings.customLogo" />
</q-avatar>
<span v-else-if="g.settings.siteTitle == 'LNbits'"
><strong>LN</strong>bits</span
>
<span v-else v-text="g.settings.siteTitle"></span>
<q-img
v-if="customLogoUrl"
height="30px"
alt="Logo"
:src="customLogoUrl"
></q-img>
<span v-else-if="!titleIsLnbits"><strong>LN</strong>bits</span>
<span v-else v-text="title"></span>
</q-btn>
<q-badge v-if="g.user && g.user.super_user">Super User</q-badge>
<q-badge v-else-if="g.user && g.user.admin">Admin User</q-badge>
@@ -41,31 +38,31 @@
<q-badge
class="q-mr-md"
v-show="$q.screen.gt.sm"
v-if="g.settings.customBadge"
:label="g.settings.customBadge"
:color="g.settings.customBadgeColor"
v-if="hasCustomBadge"
:label="customBadge"
:color="customBadgeColor"
>
</q-badge>
<q-badge
v-show="$q.screen.gt.sm"
v-if="g.user && g.settings.serviceFee > 0"
v-if="hasServiceFee"
color="green"
class="q-mr-md"
>
<span
v-if="g.user && g.settings.serviceFeeMax > 0"
v-if="hasServiceFeeMax"
v-text="
$t('service_fee_max_badge', {
amount: g.settings.serviceFee,
max: g.settings.serviceFeeMax,
amount: serviceFee,
max: serviceFeeMax,
denom: g.denomination
})
"
></span>
<span
v-else
v-text="$t('service_fee_badge', {amount: g.settings.serviceFee})"
v-text="$t('service_fee_badge', {amount: serviceFee})"
></span>
<q-tooltip><span v-text="$t('service_fee_tooltip')"></span></q-tooltip>
</q-badge>
File diff suppressed because it is too large Load Diff
@@ -118,7 +118,7 @@
</q-list>
</q-card-section>
<q-expansion-item
v-if="!g.settings.hideApi"
v-if="!HIDE_API"
group="api"
dense
expand-separator
@@ -152,7 +152,7 @@
</q-expansion-item>
<q-expansion-item
v-if="!g.settings.hideApi"
v-if="!HIDE_API"
group="api"
dense
expand-separator
@@ -197,7 +197,7 @@
</q-card>
</q-expansion-item>
<q-expansion-item
v-if="!g.settings.hideApi"
v-if="!HIDE_API"
group="api"
dense
expand-separator
@@ -244,7 +244,7 @@
</q-expansion-item>
<q-expansion-item
v-if="!g.settings.hideApi"
v-if="!HIDE_API"
group="api"
dense
expand-separator
@@ -273,7 +273,7 @@
</q-card>
</q-expansion-item>
<q-expansion-item
v-if="!g.settings.hideApi"
v-if="!HIDE_API"
group="api"
dense
expand-separator
+15 -82
View File
@@ -258,20 +258,15 @@
class="q-mb-md"
>
</q-input>
<div v-if="!g.user.email" class="row"></div>
<div v-if="!g.user.email" class="row">
<div
v-if="
'google-auth' in g.settings.authMethods ||
'github-auth' in g.settings.authMethods
"
class="col q-pa-sm text-h6"
>
{% if "google-auth" in LNBITS_AUTH_METHODS or
"github-auth" in LNBITS_AUTH_METHODS %}
<div class="col q-pa-sm text-h6">
<span v-text="$t('verify_email')"></span>:
</div>
<div
v-if="'google-auth' in g.settings.authMethods"
class="col q-pa-sm"
>
{%endif%} {% if "google-auth" in LNBITS_AUTH_METHODS %}
<div class="col q-pa-sm">
<q-btn
:href="`/api/v1/auth/google?user_id=${g.user.id}`"
type="a"
@@ -289,10 +284,8 @@
<div>Google</div>
</q-btn>
</div>
<div
v-if="'github-auth' in g.settings.authMethods"
class="col q-pa-sm"
>
{%endif%} {% if "github-auth" in LNBITS_AUTH_METHODS %}
<div class="col q-pa-sm">
<q-btn
:href="`/api/v1/auth/github?user_id=${g.user.id}`"
type="a"
@@ -310,6 +303,7 @@
<div>GitHub</div>
</q-btn>
</div>
{%endif%}
</div>
</q-card-section>
@@ -465,69 +459,6 @@
</q-toggle>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('rounded_ui')"></span>
</div>
<div class="col-8">
<q-toggle
dense
flat
round
icon="rounded_corner"
v-model="g.cardRoundedChoice"
@update:model-value="
siteCustomisationChanged({cardRoundedChoice: $event})
"
>
<q-tooltip
><span v-text="$t('toggle_rounded_ui')"></span
></q-tooltip>
</q-toggle>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('card_gradient')"></span>
</div>
<div class="col-8">
<q-toggle
dense
flat
round
icon="gradient"
v-model="g.cardGradientChoice"
@update:model-value="
siteCustomisationChanged({cardGradientChoice: $event})
"
>
<q-tooltip
><span v-text="$t('toggle_card_gradient')"></span
></q-tooltip>
</q-toggle>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('card_shadow')"></span>
</div>
<div class="col-8">
<q-toggle
dense
flat
round
icon="blur_on"
v-model="g.cardShadowChoice"
@update:model-value="
siteCustomisationChanged({cardShadowChoice: $event})
"
>
<q-tooltip
><span v-text="$t('toggle_card_shadow')"></span
></q-tooltip>
</q-toggle>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
@@ -577,7 +508,7 @@
<div class="col-8">
<lnbits-notifications-btn
v-if="g.user"
pubkey="g.settings.webpushPubkey"
pubkey="{{ WEBPUSH_PUBKEY }}"
></lnbits-notifications-btn>
</div>
</div>
@@ -602,7 +533,9 @@
</div>
<q-card-section>
<q-btn
@click="resetThemeDefaults"
@click="
siteCustomisationChanged(defaultSiteCustomisation)
"
:label="$t('reset_defaults')"
filled
color="primary"
@@ -619,7 +552,7 @@
></span>
<br />
<q-badge
v-if="!g.settings.nostrConfigured"
v-if="!LNBITS_NOSTR_CONFIGURED"
v-text="$t('not_connected')"
></q-badge>
</div>
@@ -639,7 +572,7 @@
<span v-text="$t('notifications_chat_id')"></span>
<br />
<q-badge
v-if="!g.settings.telegramConfigured"
v-if="!LNBITS_TELEGRAM_CONFIGURED"
v-text="$t('not_connected')"
></q-badge>
</div>
+1 -1
View File
@@ -178,7 +178,7 @@
"
:clickable="!!reviewsUrl"
@click="openReviews(extension)"
></lnbits-extension-rating>
/>
<q-btn-group size="xs" style="margin: 5px 0">
<q-btn
v-if="extension.hasFreeRelease"
-13
View File
@@ -53,19 +53,6 @@
@click="loginData.isPwdRepeat = !loginData.isPwdRepeat"
/> </template
></q-input>
<q-input
filled
v-if="g.settings.hasFirstInstallToken"
v-model.trim="loginData.firstInstallToken"
:type="loginData.isPwd ? 'password' : 'text'"
:label="$t('first_install_token')"
><template v-slot:append>
<q-icon
:name="loginData.isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="loginData.isPwd = !loginData.isPwd"
/> </template
></q-input>
<q-btn
@click="setPassword()"
unelevated
+15 -29
View File
@@ -2,14 +2,14 @@
<div class="home row justify-center items-center">
<div
class="full-width content-center"
:style="`max-width: ${g.settings.customImage ? '850' : '600'}px; min-height: 55vh;`"
:style="`max-width: ${hasCustomImage ? '850' : '600'}px; min-height: 55vh;`"
>
<div v-if="g.settings.showHomepageElements" class="row q-mb-md">
<div v-if="showHomepageElements" class="row q-mb-md">
<div class="col-12">
<div>
<h5 v-text="g.settings.siteTitle" class="q-my-none"></h5>
<h5 v-text="siteTitle" class="q-my-none"></h5>
<template v-if="$q.screen.gt.sm">
<h6 class="q-my-sm" v-text="g.settings.siteTagline"></h6>
<h6 class="q-my-sm" v-text="siteTagline"></h6>
<p class="q-my-sm" v-html="formatDescription"></p>
</template>
</div>
@@ -31,10 +31,7 @@
<div class="row">
<div
class="col-12"
:class="{
'col-sm-7': g.settings.customImage,
'col-lg-6': g.settings.customImage
}"
:class="{'col-sm-7': hasCustomImage, 'col-lg-6': hasCustomImage}"
>
<div v-if="showClaimLnurl">
<q-card-section>
@@ -57,13 +54,12 @@
<div v-else>
<username-password
v-if="authMethod != 'user-id-only'"
:allowed_new_users="g.settings.allowRegister"
:auth-methods="g.settings.authMethods"
:allowed_new_users="allowRegister"
:auth-methods="LNBITS_AUTH_METHODS"
:auth-action="authAction"
v-model:user-name="username"
v-model:password_1="password"
v-model:password_2="passwordRepeat"
v-model:invitation-code="invitationCode"
v-model:reset-key="reset_key"
@login="login"
@register="register"
@@ -74,7 +70,7 @@
v-if="authAction !== 'reset'"
>
<p
v-if="authAction === 'login' && g.settings.allowRegister"
v-if="authAction === 'login' && allowRegister"
class="q-mb-none"
>
Not registered?
@@ -86,9 +82,7 @@
>
</p>
<p
v-else-if="
authAction === 'login' && !g.settings.allowRegister
"
v-else-if="authAction === 'login' && !allowRegister"
class="q-mb-none"
>
<span v-text="$t('new_user_not_allowed')"></span>
@@ -107,7 +101,7 @@
</username-password>
<user-id-only
v-if="authMethod == 'user-id-only'"
:allowed_new_users="g.settings.allowRegister"
:allowed_new_users="allowRegister"
v-model:usr="usr"
v-model:wallet="walletName"
:auth-action="authAction"
@@ -120,20 +114,16 @@
</user-id-only>
</div>
</div>
<div v-if="g.settings.customImage" class="col-sm-5 col-lg-6 gt-xs">
<div v-if="hasCustomImage" class="col-sm-5 col-lg-6 gt-xs">
<div class="full-height flex flex-center q-pa-lg">
<q-img
:src="g.settings.customImage"
:ratio="1"
width="250px"
></q-img>
<q-img :src="hasCustomImage" :ratio="1" width="250px"></q-img>
</div>
</div>
</div>
</q-card>
</div>
<div v-if="g.settings.showHomepageElements">
<div v-if="showHomepageElements">
<div class="flex flex-center q-gutter-md q-py-md">
<q-btn
outline
@@ -158,14 +148,10 @@
</div>
<div
v-if="g.settings.showAdSpace"
v-if="adsEnabled"
class="justify-center col-10 q-my-lg row q-col-gutter-sm"
>
<a
:href="ad[0]"
v-for="ad in g.settings.adSpace"
class="lnbits-ad col-12 col-md-4"
>
<a :href="ad[0]" v-for="ad in g.ads" class="lnbits-ad col-12 col-md-4">
<q-img v-if="$q.dark.isActive" :src="ad[1]"></q-img>
<q-img v-else :src="ad[2]"></q-img>
</a>
+5 -33
View File
@@ -218,26 +218,6 @@
:label="$t('update_account')"
class="q-ml-md"
></q-btn>
<q-btn
v-if="activeUser.data.id"
outline
color="primary"
class="q-ml-md"
>
<q-toggle
size="xs"
color="secondary"
v-model="activeUser.data.admin"
@update:model-value="toggleAdmin(activeUser.data.id)"
:label="
activeUser.data.admin
? $t('revoke_admin')
: $t('make_user_admin')
"
>
</q-toggle>
</q-btn>
<q-btn
v-else
@click="createUser()"
@@ -610,9 +590,9 @@
<q-btn
@click="showAccountPage(props.row.id)"
round
:icon="props.row.is_admin ? 'admin_panel_settings' : 'edit'"
icon="edit"
size="sm"
:color="props.row.is_admin ? 'primary' : 'secondary'"
color="secondary"
class="q-ml-xs"
>
<q-tooltip>
@@ -625,18 +605,10 @@
size="xs"
v-if="!props.row.is_super_user"
color="secondary"
v-model="props.row.activated"
@update:model-value="toggleUserActivated(props.row.id)"
v-model="props.row.is_admin"
@update:model-value="toggleAdmin(props.row.id)"
>
<q-tooltip
><span
v-text="
props.row.activated
? $t('deactivate')
: $t('activate')
"
></span
></q-tooltip>
<q-tooltip>Toggle Admin</q-tooltip>
</q-toggle>
<q-btn
round
+9 -9
View File
@@ -172,15 +172,15 @@
></q-tooltip>
</q-btn>
<div
v-if="g.settings.walletFeaturedButtonUrl"
v-if="WALLET_FEATURED_BUTTON_URL"
class="float-right q-mt-sm q-ml-sm"
>
<q-btn
color="primary"
:label="g.settings.walletFeaturedButtonLabel"
:icon="g.settings.walletFeaturedButtonIcon || undefined"
:label="WALLET_FEATURED_BUTTON_LABEL"
:icon="WALLET_FEATURED_BUTTON_ICON || undefined"
size="sm"
:to="g.settings.walletFeaturedButtonUrl"
:to="WALLET_FEATURED_BUTTON_URL"
>
</q-btn>
</div>
@@ -204,7 +204,7 @@
</q-card>
</div>
<div
v-show="!g.mobileSimple || !$q.screen.lt.md"
v-if="!g.mobileSimple || !$q.screen.lt.md"
class="col-12 col-md-5 q-gutter-y-md"
>
<lnbits-wallet-extra
@@ -212,10 +212,10 @@
@send-lnurl="handleSendLnurl"
:chart-config="chartConfig"
></lnbits-wallet-extra>
<q-card class="lnbits-wallet-ads" v-if="g.settings.showAdSpace">
<q-card class="lnbits-wallet-ads" v-if="AD_SPACE_ENABLED">
<q-card-section class="text-subtitle1">
<span v-text="g.settings.adSpaceTitle"></span>
<a :href="ad[0]" class="lnbits-ad" v-for="ad in g.settings.adSpace">
<span v-text="AD_SPACE_TITLE"></span>
<a :href="ad[0]" class="lnbits-ad" v-for="ad in g.ads">
<q-img class="q-mb-xs" v-if="$q.dark.isActive" :src="ad[1]"></q-img>
<q-img class="q-mb-xs" v-else :src="ad[2]"></q-img>
</a>
@@ -289,7 +289,7 @@
></q-input>
</div>
<q-input
v-if="g.settings.hasHoldinvoice"
v-if="has_holdinvoice"
filled
dense
v-model="receive.data.payment_hash"
+4 -2
View File
@@ -45,8 +45,10 @@
</div>
<script type="text/javascript">
const SETTINGS = {{ SETTINGS | tojson | safe }}
const CURRENCIES = {{ CURRENCIES | tojson | safe }}
const WINDOW_SETTINGS = {{ WINDOW_SETTINGS | tojson }}
Object.keys(WINDOW_SETTINGS).forEach(key => {
window[key] = WINDOW_SETTINGS[key]
})
</script>
{% include('components.vue') %}{% block vue_templates %}{% endblock %} {%
for url in INCLUDED_JS %}
+4 -10
View File
@@ -292,8 +292,7 @@ async def btc_rates(currency: str) -> list[tuple[str, float]]:
async def btc_price(currency: str) -> float:
rates = await btc_rates(currency)
if not rates:
logger.warning("Could not fetch any Bitcoin price.")
return 0.0
raise ValueError("Could not fetch any Bitcoin price.")
elif len(rates) == 1:
logger.warning("Could only fetch one Bitcoin price.")
@@ -307,8 +306,7 @@ async def get_fiat_rate_and_price_satoshis(currency: str) -> tuple[float, float]
f"btc-price-{currency}",
settings.lnbits_exchange_rate_cache_seconds,
)
rate = float(100_000_000 / price) if price > 0 else 0.0
return rate, price
return float(100_000_000 / price), price
async def get_fiat_rate_satoshis(currency: str) -> float:
@@ -318,13 +316,9 @@ async def get_fiat_rate_satoshis(currency: str) -> float:
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
rate = await get_fiat_rate_satoshis(currency)
if rate > 0:
return int(amount * rate)
raise ValueError(f"Could not get exchange rate for {currency}.")
return int(amount * (rate))
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
rate = await get_fiat_rate_satoshis(currency)
if rate > 0:
return float(amount / rate)
raise ValueError(f"Could not get exchange rate for {currency}.")
return float(amount / rate)
-7
View File
@@ -14,13 +14,6 @@ from lnbits.settings import settings
def log_server_info():
logger.info("LNbits Info")
if settings.first_install:
logger.success("This is a fresh install of LNbits.")
if settings.first_install_token:
logger.success(
f"FIRST_INSTALL_TOKEN: `{settings.first_install_token}`. "
"Please provide this token on /first_install."
)
logger.info(f"Version: {settings.version}")
logger.info(f"Baseurl: {settings.lnbits_baseurl}")
logger.info(f"Host: {settings.host}")
-2
View File
@@ -29,7 +29,6 @@ from .nwc import NWCWallet
from .opennode import OpenNodeWallet
from .phoenixd import PhoenixdWallet
from .spark import SparkWallet
from .sparkl2 import SparkL2Wallet
from .strike import StrikeWallet
from .void import VoidWallet
from .zbd import ZBDWallet
@@ -76,7 +75,6 @@ __all__ = [
"NWCWallet",
"OpenNodeWallet",
"PhoenixdWallet",
"SparkL2Wallet",
"SparkWallet",
"StrikeWallet",
"VoidWallet",
-355
View File
@@ -1,355 +0,0 @@
import asyncio
import hashlib
import json
import uuid
from collections.abc import AsyncGenerator
from pathlib import Path
from typing import Any, cast
import httpx
from bolt11 import decode as bolt11_decode
from coincurve.keys import PrivateKey
from embit.bip39 import mnemonic_from_bytes, mnemonic_is_valid
from loguru import logger
from lnbits.helpers import normalize_endpoint
from lnbits.settings import settings
from .base import (
InvoiceResponse,
PaymentFailedStatus,
PaymentPendingStatus,
PaymentResponse,
PaymentStatus,
PaymentSuccessStatus,
StatusResponse,
Wallet,
)
class SparkSidecarError(Exception):
pass
class SparkL2Wallet(Wallet):
"""
Spark L2 funding source via a local sidecar service.
Required settings/env:
- SPARK_L2_ENDPOINT (default http://127.0.0.1:8765)
Optional:
- SPARK_L2_API_KEY
"""
def __init__(self):
self._status = "Initializing"
self._sidecar_path = Path(settings.lnbits_data_folder, "light_spark")
self.pending_invoices: list[str] = []
self.endpoint = "http://127.0.0.1:8765"
self._api_key = uuid.uuid4().hex
if settings.spark_l2_external_endpoint:
self.endpoint = normalize_endpoint(
cast(str, settings.spark_l2_external_endpoint)
)
logger.info(f"Using external Spark sidecar endpoint: {self.endpoint}")
else:
logger.error("No Spark sidecar endpoint configuration found.")
if settings.spark_l2_external_api_key:
self._api_key = cast(str, settings.spark_l2_external_api_key)
logger.info("Using external Spark sidecar API.")
else:
logger.warning("No Spark sidecar API key configured.")
headers = {"User-Agent": settings.user_agent, "X-Api-Key": self._api_key}
self.client = httpx.AsyncClient(
base_url=self.endpoint,
headers=headers,
timeout=60,
)
async def cleanup(self):
try:
await self.client.aclose()
except RuntimeError as e:
logger.warning(f"Error closing wallet connection: {e}")
async def status(self) -> StatusResponse:
try:
res = await self._request("POST", "/v1/balance")
status = res.get("status")
if status == "missing_mnemonic":
await self._check_sidecar_mnemonic()
return StatusResponse("Spark sidecar mnemonic not set", 0)
balance_msat = res.get("balance_msat")
if balance_msat is not None:
return StatusResponse(None, int(balance_msat))
balance_sats = res.get("balance_sats")
if balance_sats is None:
return StatusResponse("Spark sidecar: missing balance.", 0)
return StatusResponse(None, int(balance_sats) * 1000)
except Exception as e:
logger.warning(e)
return StatusResponse(f"Spark sidecar status error: {e}", 0)
async def create_invoice(
self,
amount: int,
memo: str | None = None,
description_hash: bytes | None = None,
unhashed_description: bytes | None = None,
**kwargs,
) -> InvoiceResponse:
expiry = kwargs.get("expiry")
expiry_secs = int(expiry) if expiry else None
description_hash_hex = None
if description_hash:
description_hash_hex = description_hash.hex()
elif unhashed_description:
description_hash_hex = hashlib.sha256(unhashed_description).hexdigest()
try:
payload = {
"amount_sats": int(amount),
"memo": (memo or "") if not description_hash_hex else None,
"description_hash": description_hash_hex,
"expiry_seconds": expiry_secs,
}
res = await self._request("POST", "/v1/invoices", payload)
bolt11 = res.get("payment_request")
checking_id = res.get("checking_id")
if not bolt11 or not checking_id:
return InvoiceResponse(
ok=False,
error_message="Spark sidecar invoice response missing fields.",
)
self.pending_invoices.append(checking_id)
return InvoiceResponse(
ok=True,
payment_request=bolt11,
checking_id=checking_id,
preimage=res.get("preimage", None),
)
except Exception as e:
return InvoiceResponse(ok=False, error_message=str(e))
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
try:
max_fee_sats = (int(fee_limit_msat) + 999) // 1000
payment_hash = None
try:
payment_hash = bolt11_decode(bolt11).payment_hash
except Exception as exc:
logger.warning(exc)
payment_hash = None
payload = {
"bolt11": bolt11,
"max_fee_sats": max_fee_sats,
"payment_hash": payment_hash,
}
res = await self._request("POST", "/v1/payments", payload)
checking_id = payment_hash or res.get("checking_id")
if not checking_id:
return PaymentResponse(
ok=False,
error_message="Spark sidecar payment response missing checking_id.",
)
status = res.get("status")
fee_msat = res.get("fee_msat")
ok = None
if status:
ok = self._map_payment_ok(status)
return PaymentResponse(
ok=ok,
checking_id=checking_id,
fee_msat=int(fee_msat) if fee_msat is not None else None,
preimage=res.get("preimage"),
)
except Exception as e:
return PaymentResponse(ok=False, error_message=str(e))
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
try:
res = await self._request("GET", f"/v1/invoices/{checking_id}")
status = res.get("status")
if not status:
return PaymentPendingStatus()
return self._map_invoice_status(status)
except Exception as exc:
logger.warning(exc)
return PaymentPendingStatus()
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
try:
res = await self._request("GET", f"/v1/payments/{checking_id}")
status = res.get("status")
fee_msat = res.get("fee_msat")
preimage = res.get("preimage")
if not status:
return PaymentPendingStatus()
mapped = self._map_payment_status(status)
if mapped.success:
return PaymentSuccessStatus(
fee_msat=int(fee_msat) if fee_msat is not None else None,
preimage=preimage,
)
if mapped.failed:
return PaymentFailedStatus()
return PaymentPendingStatus()
except Exception as exc:
logger.warning(exc)
return PaymentPendingStatus()
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
stream_path = "/v1/invoices/stream"
while settings.lnbits_running:
try:
async with self.client.stream("GET", stream_path, timeout=None) as r:
if r.status_code in {404, 405}:
logger.warning(
"Spark sidecar invoice stream not available, "
"falling back to polling."
)
async for checking_id in self._poll_pending_invoices():
yield checking_id
return
r.raise_for_status()
logger.info("connected to Spark sidecar invoice stream.")
async for line in r.aiter_lines():
if not line or not line.startswith("data:"):
continue
data = json.loads(line[5:].strip())
checking_id = data.get("checking_id")
if not checking_id:
continue
yield checking_id
except Exception as exc:
logger.error(
"lost connection to Spark sidecar invoice stream: "
f"'{exc}' retrying in 5 seconds"
)
await asyncio.sleep(5)
async def _request(
self, method: str, path: str, json_data: dict[str, Any] | None = None
) -> dict[str, Any]:
error_message = None
try:
r = await self.client.request(method, path, json=json_data, timeout=30)
r.raise_for_status()
j = r.json()
except (httpx.RequestError, httpx.HTTPStatusError, json.JSONDecodeError) as exc:
if isinstance(exc, httpx.HTTPStatusError) and exc.response is not None:
try:
error_json = exc.response.json()
if "error" in error_json:
error_message = error_json["error"]
except Exception as json_exc:
logger.error(
f"Failed to parse Spark error response as JSON: {json_exc}"
)
raise SparkSidecarError(
error_message or f"Spark sidecar request error: '{exc}'"
) from exc
if error_message or j.get("error"):
raise SparkSidecarError(
error_message or f"Spark sidecar error: {j['error']}"
)
return j
async def _poll_pending_invoices(self) -> AsyncGenerator[str, None]:
while settings.lnbits_running:
for invoice in list(self.pending_invoices):
try:
status = await self.get_invoice_status(invoice)
if status.paid:
yield invoice
self.pending_invoices.remove(invoice)
elif status.failed:
self.pending_invoices.remove(invoice)
except Exception as exc:
logger.error(f"could not get status of invoice {invoice}: '{exc}' ")
await asyncio.sleep(5)
def _map_invoice_status(self, status: str) -> PaymentStatus:
success = {
"LIGHTNING_PAYMENT_RECEIVED",
"TRANSFER_COMPLETED",
"PAYMENT_PREIMAGE_RECOVERED",
}
failed = {
"TRANSFER_FAILED",
"PAYMENT_PREIMAGE_RECOVERING_FAILED",
"REFUND_SIGNING_FAILED",
"REFUND_SIGNING_COMMITMENTS_QUERYING_FAILED",
"TRANSFER_CREATION_FAILED",
}
if status in success:
return PaymentSuccessStatus()
if status in failed:
return PaymentFailedStatus()
return PaymentPendingStatus()
def _map_payment_status(self, status: str) -> PaymentStatus:
success = {
"LIGHTNING_PAYMENT_SUCCEEDED",
"TRANSFER_COMPLETED",
"PREIMAGE_PROVIDED",
}
failed = {
"LIGHTNING_PAYMENT_FAILED",
"TRANSFER_FAILED",
"PREIMAGE_PROVIDING_FAILED",
"USER_TRANSFER_VALIDATION_FAILED",
"USER_SWAP_RETURN_FAILED",
}
if status in success:
return PaymentSuccessStatus()
if status in failed:
return PaymentFailedStatus()
return PaymentPendingStatus()
def _map_payment_ok(self, status: str) -> bool | None:
mapped = self._map_payment_status(status)
if mapped.success:
return True
if mapped.failed:
return False
return None
async def _check_sidecar_mnemonic(self):
if settings.spark_l2_mnemonic:
valid = mnemonic_is_valid(settings.spark_l2_mnemonic)
if not valid:
logger.warning("SPARK_L2_MNEMONIC is set but invalid. Please recheck!")
return
await self._set_sidecar_mnemonic(settings.spark_l2_mnemonic)
return
logger.info("SPARK_L2_MNEMONIC is not set, one will be generated for you.")
mnemonic = mnemonic_from_bytes(PrivateKey().secret)
await self._set_sidecar_mnemonic(mnemonic)
async def _set_sidecar_mnemonic(self, mnemonic: str):
logger.info("Checking 'SPARK_L2_MNEMONIC' on the Spark sidecar.")
payload = {"mnemonic": mnemonic}
resp = await self._request("POST", "/v1/mnemonic", payload)
status = resp.get("status")
logger.info(f"Spark sidecar mnemonic status: {status}")
if status == "set":
logger.info("Updating 'SPARK_L2_MNEMONIC' mnemonic settings.")
from lnbits.core.crud.settings import set_settings_field
await set_settings_field("spark_l2_mnemonic", mnemonic)
else:
logger.info("Nothing to do for 'SPARK_L2_MNEMONIC' on the Spark sidecar.")
+16 -22
View File
@@ -6,7 +6,7 @@
"": {
"name": "lnbits",
"dependencies": {
"axios": "^1.13.5",
"axios": "^1.13.2",
"chart.js": "^4.5.1",
"moment": "^2.30.1",
"nostr-tools": "^2.18.2",
@@ -730,17 +730,16 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -929,7 +928,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -975,7 +973,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
@@ -1071,16 +1068,15 @@
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
@@ -1091,9 +1087,9 @@
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@@ -1368,7 +1364,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -1377,7 +1372,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
@@ -1386,9 +1380,9 @@
}
},
"node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {

Some files were not shown because too many files have changed in this diff Show More