Compare commits

..
Author SHA1 Message Date
Vlad Stan 18ce66e507 chore: make bundle 2026-02-05 12:21:08 +02:00
Vlad Stan acce8d726b feat: configure activation codes 2026-02-05 12:21:08 +02:00
Vlad Stan 164e498476 [feat] Allow no exchange providers (#3763) 2026-02-05 12:21:08 +02:00
Vlad Stan 48e0ca80bd fix: handle validation errors when the response is binary (#3765) 2026-02-05 12:21:08 +02:00
Vlad Stan 0f4930f6c8 fix: settings reset 2026-02-05 12:21:08 +02:00
Vlad Stan e0cade01c9 test: one time code 2026-02-05 12:21:08 +02:00
Vlad Stan dd79dbc90b test: invalid code, reusable code 2026-02-05 12:21:08 +02:00
Vlad Stan 9e1ac86761 test: no code provided 2026-02-05 12:21:08 +02:00
Vlad Stan abfc8ebfb4 fix: handle empty strings better 2026-02-05 12:21:08 +02:00
Vlad Stan 872c24e6ea chore: code clean-up 2026-02-05 12:21:08 +02:00
Vlad Stan 6dcff91bf7 fix: remove nostr message 2026-02-05 12:21:08 +02:00
Vlad Stan ef253de426 feat: better wording 2026-02-05 12:21:08 +02:00
Vlad Stan 0819f51c3a feat: check invitation code 2026-02-05 12:21:08 +02:00
Vlad Stan d1cae5341f feat: update settings 2026-02-05 12:21:08 +02:00
Vlad Stan 3b249fbad2 feat: send invitation code to backend 2026-02-05 12:21:08 +02:00
Vlad Stan 9648587850 feat: add confirmation options UI 2026-02-05 12:21:08 +02:00
Vlad Stan a5523c1bf6 feat: configure ui 2026-02-05 12:21:08 +02:00
Vlad Stan eb84621f23 feat: configure activation codes 2026-02-05 12:21:08 +02:00
Vlad Stan b702229cb1 feat: add some info 2026-02-05 12:21:02 +02:00
Vlad Stan c722776bc0 refactor: reorder fields 2026-02-05 12:21:01 +02:00
Vlad Stan bd7917ab20 feat: add ui config 2026-02-05 12:21:01 +02:00
Vlad Stan db9006ea8b refactor: better query 2026-02-05 12:21:01 +02:00
Vlad Stan 15f2048bcb refactor: code cleanup 2026-02-05 12:21:01 +02:00
Vlad Stan b0079871b0 chore: clean-up 2026-02-05 12:21:01 +02:00
Vlad Stan 2bedcae9f1 refactor: simplify queries 2026-02-05 12:21:01 +02:00
Vlad Stan 341974fb35 refactor: use normal update 2026-02-05 12:21:01 +02:00
Vlad Stan d6bfbb0045 fix: better message 2026-02-05 12:21:01 +02:00
Vlad Stan dc9338b991 feat: add back toggle admin 2026-02-05 12:21:01 +02:00
Vlad Stan 6f6f7d4542 fix: clear cache 2026-02-05 12:21:01 +02:00
Vlad Stan 617f9c14ed feat: clear cache on user deactivation 2026-02-05 12:21:01 +02:00
Vlad Stan 54fdb0948c feat: basic account activate/deactivate 2026-02-05 12:21:01 +02:00
Vlad StanandGitHub 2fe7ab4f83 [feat] Allow no exchange providers (#3763) 2026-02-05 11:11:48 +01:00
dni ⚡andGitHub 10c8ca4ca1 fix: datetime in db.py added utc offset (#3762) 2026-02-05 11:05:58 +01:00
Vlad StanandGitHub f079762b71 fix: handle validation errors when the response is binary (#3765) 2026-02-05 11:55:40 +02:00
Vlad StanandGitHub 2e042d2597 fix: extension paid/free label (#3764) 2026-02-05 11:32:46 +02:00
Vlad StanandGitHub 49b57c9f0b [feat] User activation (#3749) 2026-02-05 11:31:47 +02:00
dni ⚡andGitHub 656c6cac5b hotfix: websocket with old balance was sent. (#3759) 2026-02-03 12:23:49 +01:00
45e773b66f feat: FIRST_INSTALL_TOKEN to help services like Umbrel secure the first_install endpoint (#3751)
Co-authored-by: dni  <office@dnilabs.com>
2026-01-30 11:21:52 +01:00
f692ac6f12 refactor: payment.check_status() into check_payment_status(payment) (#3747)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-01-30 11:18:17 +01:00
dni ⚡andGitHub 78202a027b fix: uiCustomisation JS Error (#3752) 2026-01-30 09:27:54 +01:00
dni ⚡andGitHub 28fa0906a6 refactor: g.settings with a PublicSettings class for mapping, for reactive WINDOW_SETTINGS (#3644) 2026-01-30 09:19:01 +01: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
50 changed files with 1049 additions and 3118 deletions
+1 -3
View File
@@ -59,7 +59,7 @@ LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos"
# LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx # LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx
# which fundingsources are allowed in the admin ui # 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 # uvicorn variable, allow https behind a proxy
# IMPORTANT: this also needs the webserver to be configured to forward the headers # IMPORTANT: this also needs the webserver to be configured to forward the headers
@@ -191,8 +191,6 @@ BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon"
# HEXSTRING instead of path also possible # HEXSTRING instead of path also possible
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert" BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
# TODO: add Spark
# StrikeWallet # StrikeWallet
STRIKE_API_ENDPOINT=https://api.strike.me/v1 STRIKE_API_ENDPOINT=https://api.strike.me/v1
STRIKE_API_KEY=YOUR_STRIKE_API_KEY STRIKE_API_KEY=YOUR_STRIKE_API_KEY
+2 -12
View File
@@ -55,21 +55,11 @@ jobs:
- name: Build and push boltz - name: Build and push boltz
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: docker/boltzclient context: .
file: Dockerfile.boltz
push: true push: true
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits-boltz:${{ inputs.tag }} tags: ${{ secrets.DOCKER_USERNAME }}/lnbits-boltz:${{ inputs.tag }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache
build-args: LNBITS_TAG=${{ inputs.tag }} 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 - name: Setup Regtest
run: | run: |
cd docker/regtest git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
cd docker
chmod +x ./start-regtest chmod +x ./start-regtest
./start-regtest ./start-regtest
sudo chmod -R a+rwx . sudo chmod -R a+rwx .
@@ -49,16 +50,16 @@ jobs:
LNBITS_DATABASE_URL: ${{ inputs.db-url }} LNBITS_DATABASE_URL: ${{ inputs.db-url }}
LNBITS_BACKEND_WALLET_CLASS: ${{ inputs.backend-wallet-class }} LNBITS_BACKEND_WALLET_CLASS: ${{ inputs.backend-wallet-class }}
LND_REST_ENDPOINT: https://localhost:8081/ LND_REST_ENDPOINT: https://localhost:8081/
LND_REST_CERT: ./docker/regtest/data/lnd-3/tls.cert LND_REST_CERT: ./docker/data/lnd-3/tls.cert
LND_REST_MACAROON: ./docker/regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon LND_REST_MACAROON: ./docker/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
LND_GRPC_ENDPOINT: localhost LND_GRPC_ENDPOINT: localhost
LND_GRPC_PORT: 10009 LND_GRPC_PORT: 10009
LND_GRPC_CERT: ./docker/regtest/data/lnd-3/tls.cert LND_GRPC_CERT: docker/data/lnd-3/tls.cert
LND_GRPC_MACAROON: ./docker/regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon LND_GRPC_MACAROON: docker/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
CORELIGHTNING_RPC: ./docker/regtest/data/clightning-1/regtest/lightning-rpc CORELIGHTNING_RPC: ./docker/data/clightning-1/regtest/lightning-rpc
CORELIGHTNING_REST_URL: https://localhost:3001 CORELIGHTNING_REST_URL: https://localhost:3001
CORELIGHTNING_REST_MACAROON: ./docker/regtest/data/clightning-2-rest/access.macaroon CORELIGHTNING_REST_MACAROON: ./docker/data/clightning-2-rest/access.macaroon
CORELIGHTNING_REST_CERT: ./docker/regtest/data/clightning-2-rest/certificate.pem CORELIGHTNING_REST_CERT: ./docker/data/clightning-2-rest/certificate.pem
LNBITS_ENDPOINT: http://localhost:5001 LNBITS_ENDPOINT: http://localhost:5001
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee" LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
ECLAIR_URL: http://127.0.0.1:8082 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.min.css.old
lnbits/static/bundle-components.min.js.old lnbits/static/bundle-components.min.js.old
lnbits/upgrades lnbits/upgrades
docker
# Nix # Nix
*result* *result*
@@ -14,13 +14,11 @@ ENV PATH="/root/.local/bin:$PATH"
# Reinstall dependencies just in case (needed for CMD usage) # Reinstall dependencies just in case (needed for CMD usage)
RUN uv sync --all-extras RUN uv sync --all-extras
# LNbits # LNbits + boltzd configuration
ENV LNBITS_PORT="5000" ENV LNBITS_PORT="5000"
ENV LNBITS_HOST="0.0.0.0" ENV LNBITS_HOST="0.0.0.0"
ENV LNBITS_BACKEND_WALLET_CLASS="BoltzWallet" ENV LNBITS_BACKEND_WALLET_CLASS="BoltzWallet"
ENV FUNDING_SOURCE_MAX_RETRIES=10 ENV FUNDING_SOURCE_MAX_RETRIES=10
# Boltzd
ENV BOLTZ_CLIENT_ENDPOINT="127.0.0.1:9002" ENV BOLTZ_CLIENT_ENDPOINT="127.0.0.1:9002"
ENV BOLTZ_CLIENT_MACAROON="/root/.boltz/macaroons/admin.macaroon" ENV BOLTZ_CLIENT_MACAROON="/root/.boltz/macaroons/admin.macaroon"
ENV BOLTZ_CLIENT_CERT="/root/.boltz/tls.cert" ENV BOLTZ_CLIENT_CERT="/root/.boltz/tls.cert"
@@ -28,7 +26,8 @@ ENV BOLTZ_CLIENT_WALLET="lnbits"
EXPOSE 5000 EXPOSE 5000
COPY entrypoint.sh /entrypoint.sh # Entrypoint to start boltzd and LNbits
RUN chmod +x /entrypoint.sh 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;
}
}
-111
View File
@@ -1,111 +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
[[pairs]]
isLegacy = true
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]]
isLegacy = true
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"
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:latest
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. | | **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. | | **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 (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 | ❌ | Medium | 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. | | **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. | | **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. | | **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. | | **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). | | **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. You can let LNbits run the sidecar internally or run your own instance (see `https://github.com/lnbits/spark_sidecar`).
### Notes for readers ### Notes for readers
+1 -43
View File
@@ -41,7 +41,6 @@ A backend wallet is selected and configured entirely through LNbits environment
| [CoreLightning](#corelightning) | [LND (gRPC)](#lnd-grpc) | [Blink](#blink) | | [CoreLightning](#corelightning) | [LND (gRPC)](#lnd-grpc) | [Blink](#blink) |
| [CoreLightning REST](#corelightning-rest) | [LNbits](#lnbits) | [Alby](#alby) | | [CoreLightning REST](#corelightning-rest) | [LNbits](#lnbits) | [Alby](#alby) |
| [Spark (Core Lightning)](#spark-core-lightning) | [LNPay](#lnpay) | [Boltz](#boltz) | | [Spark (Core Lightning)](#spark-core-lightning) | [LNPay](#lnpay) | [Boltz](#boltz) |
| [Spark (L2)](#spark-l2) | | |
| [Cliche Wallet](#cliche-wallet) | [ZBD](#zbd) | [Phoenixd](#phoenixd) | | [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) | | [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) | | [Strike](#strike) | [Eclair (ACINQ)](#eclair-acinq) | [LN.tips](#lntips) |
@@ -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)) ### 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) Should also work with the [Rust version of CLNRest](https://github.com/daywalker90/clnrest-rs)
**Environment variables** **Environment variables**
@@ -128,47 +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_URL`: `http://10.147.17.230:9737/rpc`
- `SPARK_TOKEN`: `secret_access_key` - `SPARK_TOKEN`: `secret_access_key`
## Spark (L2)
Spark L2 is a self-custodial funding source that uses a small Node.js sidecar to talk to the Spark network and expose an HTTP API. You can let LNbits run the sidecar internally, or run it yourself and point LNbits at it.
**Required env vars**
- `LNBITS_BACKEND_WALLET_CLASS`: `SparkL2Wallet`
**Internal sidecar (LNbits-managed)**
- `SPARK_L2_INTERNAL_SIDECAR_VERSION`: `0.1.1`
- `SPARK_L2_MNEMONIC`: `...` (optional; auto-generated if blank)
- `SPARK_L2_NETWORK`: `MAINNET`
Optional tuning:
- `SPARK_L2_PAY_WAIT_MS`: `4000`
- `SPARK_L2_PAY_POLL_MS`: `500`
- `SPARK_L2_STREAM_KEEPALIVE_MS`: `15000`
**External sidecar**
- `SPARK_L2_EXTERNAL_ENDPOINT`: `http://127.0.0.1:8765`
- `SPARK_L2_EXTERNAL_API_KEY`: `...` (if your sidecar requires auth)
Example: run the sidecar manually (external mode)
```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) ## LND (REST)
**Required env vars** **Required env vars**
+1 -1
View File
@@ -318,7 +318,7 @@ class RegisterUser(BaseModel):
username: str = Query(default=..., min_length=2, max_length=20) username: str = Query(default=..., min_length=2, max_length=20)
password: str = Query(default=..., min_length=8, max_length=50) password: str = Query(default=..., min_length=8, max_length=50)
password_repeat: 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) invitation_code: str | None = Query(default=None, min_length=1, max_length=256)
class CreateUser(BaseModel): class CreateUser(BaseModel):
+4 -4
View File
@@ -118,8 +118,8 @@ async def verify_paypal_webhook(headers, payload: bytes):
""" """
webhook_id = settings.paypal_webhook_id webhook_id = settings.paypal_webhook_id
if not webhook_id: if not webhook_id:
logger.warning("PayPal webhook ID not set.") logger.warning("PayPal webhook ID not set; skipping verification.")
raise ValueError("PayPal webhook cannot be verified. Missing webhook ID.") return
required_headers = { required_headers = {
"PAYPAL-TRANSMISSION-ID": headers.get("PAYPAL-TRANSMISSION-ID"), "PAYPAL-TRANSMISSION-ID": headers.get("PAYPAL-TRANSMISSION-ID"),
@@ -129,8 +129,8 @@ async def verify_paypal_webhook(headers, payload: bytes):
"PAYPAL-AUTH-ALGO": headers.get("PAYPAL-AUTH-ALGO"), "PAYPAL-AUTH-ALGO": headers.get("PAYPAL-AUTH-ALGO"),
} }
if not all(required_headers.values()): if not all(required_headers.values()):
logger.warning("Missing PayPal webhook headers.") logger.warning("Missing PayPal webhook headers; skipping verification.")
raise ValueError("PayPal webhook cannot be verified. Missing headers.") return
try: try:
async with httpx.AsyncClient(base_url=settings.paypal_api_endpoint) as client: async with httpx.AsyncClient(base_url=settings.paypal_api_endpoint) as client:
-6
View File
@@ -511,21 +511,15 @@ async def update_ui_customization(
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse: async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
if not settings.first_install: if not settings.first_install:
raise HTTPException(HTTPStatus.FORBIDDEN, "This is not your first install") raise HTTPException(HTTPStatus.FORBIDDEN, "This is not your first install")
if settings.first_install_token: if settings.first_install_token:
if not data.first_install_token: if not data.first_install_token:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing first_install_token.") raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing first_install_token.")
if settings.first_install_token != data.first_install_token: if settings.first_install_token != data.first_install_token:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid 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) account = await get_account(settings.super_user)
if not account: if not account:
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.") raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
account.username = data.username account.username = data.username
account.extra = account.extra or UserExtra() account.extra = account.extra or UserExtra()
account.extra.provider = "lnbits" account.extra.provider = "lnbits"
+14 -35
View File
@@ -24,7 +24,7 @@ callback_router = APIRouter(prefix="/api/v1/callback", tags=["callback"])
async def api_generic_webhook_handler( async def api_generic_webhook_handler(
provider_name: str, request: Request provider_name: str, request: Request
) -> SimpleStatus: ) -> SimpleStatus:
logger.info(f"Received callback from provider: '{provider_name}'.")
if provider_name.lower() == "stripe": if provider_name.lower() == "stripe":
payload = await request.body() payload = await request.body()
sig_header = request.headers.get("Stripe-Signature") sig_header = request.headers.get("Stripe-Signature")
@@ -181,10 +181,8 @@ async def _get_stripe_subscription_payment_options(
async def handle_paypal_event(event: dict): async def handle_paypal_event(event: dict):
event_id = event.get("id", "")
event_type = event.get("event_type", "") event_type = event.get("event_type", "")
resource = event.get("resource", {}) 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"): if event_type in ("CHECKOUT.ORDER.APPROVED", "PAYMENT.CAPTURE.COMPLETED"):
payment_hash = _paypal_extract_payment_hash(resource) payment_hash = _paypal_extract_payment_hash(resource)
@@ -198,17 +196,20 @@ async def handle_paypal_event(event: dict):
await check_fiat_status(payment) await check_fiat_status(payment)
return 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) await _handle_paypal_subscription_payment(resource)
return 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): async def _handle_paypal_subscription_payment(resource: dict):
amount_info = resource.get("amount") or {} amount_info = resource.get("amount") or {}
currency = (amount_info.get("currency") or "").upper() currency = (amount_info.get("currency_code") or "").upper()
total = amount_info.get("total") total = amount_info.get("value")
if not currency or total is None: if not currency or total is None:
raise ValueError("PayPal subscription event missing amount.") raise ValueError("PayPal subscription event missing amount.")
@@ -216,14 +217,18 @@ async def _handle_paypal_subscription_payment(resource: dict):
if not custom_id: if not custom_id:
raise ValueError("PayPal subscription event missing custom metadata.") 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: if not payment_options.wallet_id:
raise ValueError("PayPal subscription event missing wallet_id.") raise ValueError("PayPal subscription event missing wallet_id.")
memo = payment_options.memo or "" memo = payment_options.memo or ""
extra = { extra = {
**(payment_options.extra or {}), **(payment_options.extra or {}),
"subscription_request_id": resource.get("billing_agreement_id"),
"fiat_method": "subscription", "fiat_method": "subscription",
"tag": payment_options.tag, "tag": payment_options.tag,
"subscription": { "subscription": {
@@ -254,29 +259,3 @@ def _paypal_extract_payment_hash(resource: dict) -> str | None:
if pu.get("custom_id"): if pu.get("custom_id"):
return pu.get("custom_id") return pu.get("custom_id")
return None 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 http import HTTPStatus
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from loguru import logger
from lnbits.core.models.misc import SimpleStatus from lnbits.core.models.misc import SimpleStatus
from lnbits.core.models.wallets import WalletTypeInfo from lnbits.core.models.wallets import WalletTypeInfo
@@ -31,11 +30,6 @@ async def create_subscription(
data: CreateFiatSubscription, data: CreateFiatSubscription,
key_type: WalletTypeInfo = Depends(require_admin_key), key_type: WalletTypeInfo = Depends(require_admin_key),
) -> FiatSubscriptionResponse: ) -> FiatSubscriptionResponse:
logger.debug(
f"Creating subscription with provider '{provider}. '"
f"Subscription ID '{data.subscription_id}'."
)
fiat_provider = await get_fiat_provider(provider) fiat_provider = await get_fiat_provider(provider)
if not fiat_provider: if not fiat_provider:
raise HTTPException(404, "Fiat provider not found") raise HTTPException(404, "Fiat provider not found")
@@ -53,12 +47,6 @@ async def create_subscription(
subscription_response = await fiat_provider.create_subscription( subscription_response = await fiat_provider.create_subscription(
data.subscription_id, data.quantity, data.payment_options 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 return subscription_response
+18 -63
View File
@@ -2,7 +2,7 @@ import asyncio
import json import json
import time import time
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from typing import Any, Literal from typing import Any
import httpx import httpx
from loguru import logger from loguru import logger
@@ -24,8 +24,6 @@ from .base import (
FiatSubscriptionResponse, FiatSubscriptionResponse,
) )
FiatMethod = Literal["checkout", "subscription"]
class PayPalCheckoutOptions(BaseModel): class PayPalCheckoutOptions(BaseModel):
class Config: class Config:
@@ -36,21 +34,11 @@ class PayPalCheckoutOptions(BaseModel):
metadata: dict[str, Any] = Field(default_factory=dict) 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 PayPalCreateInvoiceOptions(BaseModel):
class Config: class Config:
extra = "ignore" extra = "ignore"
fiat_method: FiatMethod = "checkout"
checkout: PayPalCheckoutOptions | None = None checkout: PayPalCheckoutOptions | None = None
subscription: PayPalSubscriptionOptions | None = None
class PayPalWallet(FiatProvider): class PayPalWallet(FiatProvider):
@@ -108,14 +96,6 @@ class PayPalWallet(FiatProvider):
opts = self._parse_create_opts(extra or {}) opts = self._parse_create_opts(extra or {})
if opts is None: if opts is None:
return FiatInvoiceResponse(ok=False, error_message="Invalid PayPal options") 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: try:
await self._ensure_access_token() await self._ensure_access_token()
@@ -191,7 +171,6 @@ class PayPalWallet(FiatProvider):
or "https://lnbits.com" or "https://lnbits.com"
) )
logger.debug(f"Creating PayPal subscription with ID '{subscription_id}'")
if not payment_options.subscription_request_id: if not payment_options.subscription_request_id:
payment_options.subscription_request_id = urlsafe_short_hash() payment_options.subscription_request_id = urlsafe_short_hash()
payment_options.extra = payment_options.extra or {} payment_options.extra = payment_options.extra or {}
@@ -203,6 +182,7 @@ class PayPalWallet(FiatProvider):
await self._ensure_access_token() await self._ensure_access_token()
payload = { payload = {
"plan_id": subscription_id, "plan_id": subscription_id,
"quantity": str(quantity),
"custom_id": self._serialize_metadata(payment_options), "custom_id": self._serialize_metadata(payment_options),
"application_context": { "application_context": {
"return_url": success_url, "return_url": success_url,
@@ -225,7 +205,7 @@ class PayPalWallet(FiatProvider):
return FiatSubscriptionResponse( return FiatSubscriptionResponse(
ok=True, ok=True,
checkout_session_url=approval_url, checkout_session_url=approval_url,
subscription_request_id=data.get("id"), subscription_request_id=payment_options.subscription_request_id,
) )
except Exception as exc: except Exception as exc:
logger.warning(exc) logger.warning(exc)
@@ -239,10 +219,6 @@ class PayPalWallet(FiatProvider):
correlation_id: str, correlation_id: str,
**kwargs, **kwargs,
) -> FiatSubscriptionResponse: ) -> FiatSubscriptionResponse:
logger.debug(
f"Cancelling PayPal subscription '{subscription_id}'. "
f"Correlation ID '{correlation_id}'."
)
try: try:
await self._ensure_access_token() await self._ensure_access_token()
r = await self.client.post( r = await self.client.post(
@@ -264,20 +240,12 @@ class PayPalWallet(FiatProvider):
async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus: async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
try: try:
await self._ensure_access_token() await self._ensure_access_token()
paypal_id = self._normalize_paypal_id(checking_id) order_id = self._normalize_paypal_id(checking_id)
if paypal_id.startswith("subscription_"): r = await self.client.get(
capture_id = paypal_id.replace("subscription_", "", 1) f"/v2/checkout/orders/{order_id}", headers=self._auth_headers()
r = await self.client.get( )
f"v2/payments/captures/{capture_id}", headers=self._auth_headers() r.raise_for_status()
) return self._status_from_order(r.json())
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())
except Exception as exc: except Exception as exc:
logger.debug(f"Error getting PayPal order status: {exc}") logger.debug(f"Error getting PayPal order status: {exc}")
return FiatPaymentPendingStatus() return FiatPaymentPendingStatus()
@@ -302,14 +270,6 @@ class PayPalWallet(FiatProvider):
return FiatPaymentFailedStatus() return FiatPaymentFailedStatus()
return FiatPaymentPendingStatus() 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: def _normalize_paypal_id(self, checking_id: str) -> str:
return ( return (
checking_id.replace("fiat_paypal_", "", 1) checking_id.replace("fiat_paypal_", "", 1)
@@ -320,20 +280,15 @@ class PayPalWallet(FiatProvider):
def _serialize_metadata( def _serialize_metadata(
self, payment_options: FiatSubscriptionPaymentOptions self, payment_options: FiatSubscriptionPaymentOptions
) -> str: ) -> str:
meta = [ md = {
payment_options.wallet_id, "wallet_id": payment_options.wallet_id,
payment_options.tag, "memo": payment_options.memo,
payment_options.subscription_request_id, "extra": payment_options.extra,
] "tag": payment_options.tag,
if payment_options.extra: "subscription_request_id": payment_options.subscription_request_id,
meta.append(payment_options.extra.get("link", None)) }
raw = json.dumps(md)
# Keep custom_id within PayPal's 127-char limit return raw[:127] # PayPal custom_id 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)
def _parse_create_opts( def _parse_create_opts(
self, raw_opts: dict[str, Any] self, raw_opts: dict[str, Any]
-25
View File
@@ -296,10 +296,6 @@ class ThemesSettings(LNbitsSettings):
lnbits_default_border: str = Field(default="hard-border") lnbits_default_border: str = Field(default="hard-border")
lnbits_default_gradient: bool = Field(default=True) lnbits_default_gradient: bool = Field(default=True)
lnbits_default_bgimage: str | None = Field(default=None) lnbits_default_bgimage: str | None = Field(default=None)
lnbits_default_dark: bool = Field(default=True)
lnbits_default_card_rounded: bool = Field(default=False)
lnbits_default_card_gradient: bool = Field(default=False)
lnbits_default_card_shadow: bool = Field(default=False)
class OpsSettings(LNbitsSettings): class OpsSettings(LNbitsSettings):
@@ -606,15 +602,6 @@ class SparkFundingSource(LNbitsSettings):
spark_token: str | None = Field(default=None) 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_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): class LnTipsFundingSource(LNbitsSettings):
lntips_api_endpoint: str | None = Field(default=None) lntips_api_endpoint: str | None = Field(default=None)
lntips_api_key: str | None = Field(default=None) lntips_api_key: str | None = Field(default=None)
@@ -720,7 +707,6 @@ class FundingSourcesSettings(
PhoenixdFundingSource, PhoenixdFundingSource,
OpenNodeFundingSource, OpenNodeFundingSource,
SparkFundingSource, SparkFundingSource,
SparkL2FundingSource,
LnTipsFundingSource, LnTipsFundingSource,
NWCFundingSource, NWCFundingSource,
BreezSdkFundingSource, BreezSdkFundingSource,
@@ -1049,7 +1035,6 @@ class SuperUserSettings(LNbitsSettings):
"FakeWallet", "FakeWallet",
"LNPayWallet", "LNPayWallet",
"LNbitsWallet", "LNbitsWallet",
"SparkL2Wallet",
"LnTipsWallet", "LnTipsWallet",
"LndRestWallet", "LndRestWallet",
"LndWallet", "LndWallet",
@@ -1201,10 +1186,6 @@ class PublicSettings(BaseModel):
default_border: str = Field(alias="defaultBorder") default_border: str = Field(alias="defaultBorder")
default_bgimage: str | None = Field(alias="defaultBgimage") default_bgimage: str | None = Field(alias="defaultBgimage")
default_gradient: bool = Field(alias="defaultGradient") 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() denomination: str | None = Field()
extensions: list[str] = Field() extensions: list[str] = Field()
allowed_currencies: list[str] = Field(alias="allowedCurrencies") allowed_currencies: list[str] = Field(alias="allowedCurrencies")
@@ -1220,7 +1201,6 @@ class PublicSettings(BaseModel):
lnbits_user_activation_by_invitation_code: bool = Field( lnbits_user_activation_by_invitation_code: bool = Field(
alias="userActivationByInvitationCode" alias="userActivationByInvitationCode"
) )
has_first_install_token: bool = Field(alias="hasFirstInstallToken")
@classmethod @classmethod
def from_settings(cls, settings: Settings): def from_settings(cls, settings: Settings):
@@ -1262,10 +1242,6 @@ class PublicSettings(BaseModel):
defaultBorder=settings.lnbits_default_border, defaultBorder=settings.lnbits_default_border,
defaultGradient=settings.lnbits_default_gradient, defaultGradient=settings.lnbits_default_gradient,
defaultBgimage=settings.lnbits_default_bgimage, 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, denomination=settings.lnbits_denomination,
extensions=list(settings.lnbits_installed_extensions_ids), extensions=list(settings.lnbits_installed_extensions_ids),
allowedCurrencies=settings.lnbits_allowed_currencies, allowedCurrencies=settings.lnbits_allowed_currencies,
@@ -1279,7 +1255,6 @@ class PublicSettings(BaseModel):
userActivationByEmail=settings.lnbits_user_activation_by_email, userActivationByEmail=settings.lnbits_user_activation_by_email,
userActivationByPayment=settings.lnbits_user_activation_by_payment, userActivationByPayment=settings.lnbits_user_activation_by_payment,
userActivationByInvitationCode=settings.lnbits_user_activation_by_invitation_code, userActivationByInvitationCode=settings.lnbits_user_activation_by_invitation_code,
hasFirstInstallToken=settings.first_install_token is not None,
) )
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); 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 { :root {
--size: 100px; --size: 100px;
--gap: 25px; --gap: 25px;
+1 -7
View File
@@ -465,12 +465,6 @@ window.localisation.en = {
api_token_id: 'Token Id', api_token_id: 'Token Id',
toggle_gradient: 'Toggle Gradient', toggle_gradient: 'Toggle Gradient',
gradient_background: 'Gradient Background', 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', language: 'Language',
assets: 'Assets', assets: 'Assets',
max_asset_size_mb: 'Max Asset Size (MB)', max_asset_size_mb: 'Max Asset Size (MB)',
@@ -529,7 +523,7 @@ window.localisation.en = {
hash: 'Hash: ', hash: 'Hash: ',
welcome_lnbits: 'Welcome to LNbits', welcome_lnbits: 'Welcome to LNbits',
setup_su_account: 'Set up the Superuser account below.', setup_su_account: 'Set up the Superuser account below.',
first_install_token: 'First Install Token', first_install_token: 'First Install Token (optional)',
create_ticker_converter: 'Create Currency Ticker Converter', create_ticker_converter: 'Create Currency Ticker Converter',
enable_audit: 'Enable Audit', enable_audit: 'Enable Audit',
recommended: 'Recommended', recommended: 'Recommended',
@@ -228,43 +228,6 @@ window.app.component('lnbits-admin-funding-sources', {
spark_token: 'Token' spark_token: 'Token'
} }
], ],
[
'SparkL2Wallet',
'Spark (L2)',
{
spark_l2_external_endpoint: {
label: 'External Sidecar Endpoint',
hint: 'If specified then this endpoint will be used instead of the internal sidecar. Make sure to also specify the API key if your sidecar requires authentication.',
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', 'NWCWallet',
'Nostr Wallet Connect', 'Nostr Wallet Connect',
@@ -45,30 +45,6 @@ window.app.component('lnbits-theme', {
document.body.classList.remove('gradient-bg') 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) { 'g.mobileSimple'(val) {
this.$q.localStorage.set('lnbits.mobileSimple', val) this.$q.localStorage.set('lnbits.mobileSimple', val)
if (val === true) { if (val === true) {
@@ -141,15 +117,6 @@ window.app.component('lnbits-theme', {
if (this.g.gradientChoice === true) { if (this.g.gradientChoice === true) {
document.body.classList.add('gradient-bg') 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 !== '') { if (this.g.bgimageChoice !== '') {
document.body.classList.add('bg-image') document.body.classList.add('bg-image')
document.body.style.setProperty( document.body.style.setProperty(
+1 -10
View File
@@ -20,15 +20,6 @@ window.g = Vue.reactive({
themeChoice: localStore('lnbits.theme', SETTINGS.defaultTheme), themeChoice: localStore('lnbits.theme', SETTINGS.defaultTheme),
borderChoice: localStore('lnbits.border', SETTINGS.defaultBorder), borderChoice: localStore('lnbits.border', SETTINGS.defaultBorder),
gradientChoice: localStore('lnbits.gradientBg', SETTINGS.defaultGradient), 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), reactionChoice: localStore('lnbits.reactions', SETTINGS.defaultReaction),
bgimageChoice: localStore( bgimageChoice: localStore(
'lnbits.backgroundImage', 'lnbits.backgroundImage',
@@ -40,7 +31,7 @@ window.g = Vue.reactive({
mobileSimple: localStore('lnbits.mobileSimple', true), mobileSimple: localStore('lnbits.mobileSimple', true),
walletFlip: localStore('lnbits.walletFlip', false), walletFlip: localStore('lnbits.walletFlip', false),
lastActiveWallet: localStore('lnbits.lastActiveWallet', null), lastActiveWallet: localStore('lnbits.lastActiveWallet', null),
darkChoice: localStore('lnbits.darkMode', SETTINGS.defaultDark), darkChoice: localStore('lnbits.darkMode', true),
// cookie vars // cookie vars
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'), isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
isUserImpersonated: !!Quasar.Cookies.get('is_lnbits_user_impersonated'), isUserImpersonated: !!Quasar.Cookies.get('is_lnbits_user_impersonated'),
+7 -15
View File
@@ -40,7 +40,13 @@ window.PageAccount = {
} }
], ],
defaultSiteCustomisation: { defaultSiteCustomisation: {
locale: 'en' locale: 'en',
themeChoice: 'salvador',
bgimageChoice: '',
gradientChoice: true,
darkChoice: true,
borderChoice: 'hard-border',
reactionChoice: 'confettiBothSides'
}, },
reactionOptions: [ reactionOptions: [
'None', 'None',
@@ -692,20 +698,6 @@ window.PageAccount = {
} catch (e) { } catch (e) {
LNbits.utils.notifyApiError(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)
} }
}, },
-1
View File
@@ -1,7 +1,6 @@
@use 'themes'; @use 'themes';
@use 'borders'; @use 'borders';
@use 'background'; @use 'background';
@use 'cards';
@use 'home'; @use 'home';
[v-cloak] { [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
@@ -408,6 +408,7 @@
<li><code>CHECKOUT.ORDER.APPROVED</code></li> <li><code>CHECKOUT.ORDER.APPROVED</code></li>
<li><code>PAYMENT.CAPTURE.COMPLETED</code></li> <li><code>PAYMENT.CAPTURE.COMPLETED</code></li>
<li><code>PAYMENT.SALE.COMPLETED</code></li> <li><code>PAYMENT.SALE.COMPLETED</code></li>
<li><code>BILLING.SUBSCRIPTION.PAYMENT.SUCCEEDED</code></li>
</ul> </ul>
</q-card-section> </q-card-section>
</q-expansion-item> </q-expansion-item>
@@ -310,13 +310,7 @@
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<q-select <q-select
filled filled
:options="[ :options="[$t('second'), $t('minute'), $t('hour')]"
{label: $t('second'), value: 'second'},
{label: $t('minute'), value: 'minute'},
{label: $t('hour'), value: 'hour'}
]"
emit-value
map-options
v-model="formData.lnbits_rate_limit_unit" v-model="formData.lnbits_rate_limit_unit"
:label="$t('time_unit')" :label="$t('time_unit')"
></q-select> ></q-select>
@@ -257,50 +257,12 @@
> >
</q-input> </q-input>
</div> </div>
</div> <div class="col-12 col-sm-6 col-lg-3">
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12 col-sm-6 col-lg-2">
<q-toggle <q-toggle
type="bool" type="bool"
v-model="formData.lnbits_default_gradient" v-model="formData.lnbits_default_gradient"
color="primary" color="primary"
:label="$t('gradient_background')" label="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')"
> >
</q-toggle> </q-toggle>
</div> </div>
+3 -64
View File
@@ -465,69 +465,6 @@
</q-toggle> </q-toggle>
</div> </div>
</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="row q-mb-md">
<div class="col-4"> <div class="col-4">
@@ -602,7 +539,9 @@
</div> </div>
<q-card-section> <q-card-section>
<q-btn <q-btn
@click="resetThemeDefaults" @click="
siteCustomisationChanged(defaultSiteCustomisation)
"
:label="$t('reset_defaults')" :label="$t('reset_defaults')"
filled filled
color="primary" color="primary"
-1
View File
@@ -55,7 +55,6 @@
></q-input> ></q-input>
<q-input <q-input
filled filled
v-if="g.settings.hasFirstInstallToken"
v-model.trim="loginData.firstInstallToken" v-model.trim="loginData.firstInstallToken"
:type="loginData.isPwd ? 'password' : 'text'" :type="loginData.isPwd ? 'password' : 'text'"
:label="$t('first_install_token')" :label="$t('first_install_token')"
-2
View File
@@ -29,7 +29,6 @@ from .nwc import NWCWallet
from .opennode import OpenNodeWallet from .opennode import OpenNodeWallet
from .phoenixd import PhoenixdWallet from .phoenixd import PhoenixdWallet
from .spark import SparkWallet from .spark import SparkWallet
from .sparkl2 import SparkL2Wallet
from .strike import StrikeWallet from .strike import StrikeWallet
from .void import VoidWallet from .void import VoidWallet
from .zbd import ZBDWallet from .zbd import ZBDWallet
@@ -76,7 +75,6 @@ __all__ = [
"NWCWallet", "NWCWallet",
"OpenNodeWallet", "OpenNodeWallet",
"PhoenixdWallet", "PhoenixdWallet",
"SparkL2Wallet",
"SparkWallet", "SparkWallet",
"StrikeWallet", "StrikeWallet",
"VoidWallet", "VoidWallet",
-321
View File
@@ -1,321 +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 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")
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)
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
+13 -19
View File
@@ -6,7 +6,7 @@
"": { "": {
"name": "lnbits", "name": "lnbits",
"dependencies": { "dependencies": {
"axios": "^1.13.5", "axios": "^1.13.2",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"moment": "^2.30.1", "moment": "^2.30.1",
"nostr-tools": "^2.18.2", "nostr-tools": "^2.18.2",
@@ -730,17 +730,16 @@
"node_modules/asynckit": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
"license": "MIT"
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.13.5", "version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.11", "follow-redirects": "^1.15.6",
"form-data": "^4.0.5", "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
@@ -929,7 +928,6 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": { "dependencies": {
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
}, },
@@ -975,7 +973,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
} }
@@ -1071,16 +1068,15 @@
} }
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.11", "version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh" "url": "https://github.com/sponsors/RubenVerborgh"
} }
], ],
"license": "MIT",
"engines": { "engines": {
"node": ">=4.0" "node": ">=4.0"
}, },
@@ -1091,9 +1087,9 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.5", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
@@ -1368,7 +1364,6 @@
"version": "1.52.0", "version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@@ -1377,7 +1372,6 @@
"version": "2.1.35", "version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": { "dependencies": {
"mime-db": "1.52.0" "mime-db": "1.52.0"
}, },
+1 -1
View File
@@ -21,7 +21,7 @@
"terser": "^5.44.1" "terser": "^5.44.1"
}, },
"dependencies": { "dependencies": {
"axios": "^1.13.5", "axios": "^1.13.2",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"moment": "^2.30.1", "moment": "^2.30.1",
"nostr-tools": "^2.18.2", "nostr-tools": "^2.18.2",
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "lnbits" name = "lnbits"
version = "1.5.0" version = "1.4.1"
requires-python = ">=3.10,<3.13" requires-python = ">=3.10,<3.13"
description = "LNbits, free and open-source Lightning wallet and accounts system." description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }] authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
+1 -1
View File
@@ -708,7 +708,7 @@ async def test_user_activation(
f"/users/api/v1/user/{user_id}/activate", f"/users/api/v1/user/{user_id}/activate",
headers={"Authorization": f"Bearer {superuser_token}"}, headers={"Authorization": f"Bearer {superuser_token}"},
) )
print("### response", response.text)
assert response.status_code == 200 assert response.status_code == 200
assert response.json().get("message") == "User activated." assert response.json().get("message") == "User activated."
Generated
+43 -43
View File
@@ -1247,7 +1247,7 @@ wheels = [
[[package]] [[package]]
name = "lnbits" name = "lnbits"
version = "1.5.0" version = "1.4.1"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiosqlite" }, { name = "aiosqlite" },
@@ -1725,50 +1725,50 @@ wheels = [
[[package]] [[package]]
name = "pillow" name = "pillow"
version = "12.1.1" version = "12.1.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/30/5bd3d794762481f8c8ae9c80e7b76ecea73b916959eb587521358ef0b2f9/pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0", size = 5304099, upload-time = "2026-02-11T04:20:06.13Z" }, { url = "https://files.pythonhosted.org/packages/fe/41/f73d92b6b883a579e79600d391f2e21cb0df767b2714ecbd2952315dfeef/pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd", size = 5304089, upload-time = "2026-01-02T09:10:24.953Z" },
{ url = "https://files.pythonhosted.org/packages/bd/c1/aab9e8f3eeb4490180e357955e15c2ef74b31f64790ff356c06fb6cf6d84/pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713", size = 4657880, upload-time = "2026-02-11T04:20:09.291Z" }, { url = "https://files.pythonhosted.org/packages/94/55/7aca2891560188656e4a91ed9adba305e914a4496800da6b5c0a15f09edf/pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0", size = 4657815, upload-time = "2026-01-02T09:10:27.063Z" },
{ url = "https://files.pythonhosted.org/packages/f1/0a/9879e30d56815ad529d3985aeff5af4964202425c27261a6ada10f7cbf53/pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b", size = 6222587, upload-time = "2026-02-11T04:20:10.82Z" }, { url = "https://files.pythonhosted.org/packages/e9/d2/b28221abaa7b4c40b7dba948f0f6a708bd7342c4d47ce342f0ea39643974/pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8", size = 6222593, upload-time = "2026-01-02T09:10:29.115Z" },
{ url = "https://files.pythonhosted.org/packages/5a/5f/a1b72ff7139e4f89014e8d451442c74a774d5c43cd938fb0a9f878576b37/pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b", size = 8027678, upload-time = "2026-02-11T04:20:12.455Z" }, { url = "https://files.pythonhosted.org/packages/71/b8/7a61fb234df6a9b0b479f69e66901209d89ff72a435b49933f9122f94cac/pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1", size = 8027579, upload-time = "2026-01-02T09:10:31.182Z" },
{ url = "https://files.pythonhosted.org/packages/e2/c2/c7cb187dac79a3d22c3ebeae727abee01e077c8c7d930791dc592f335153/pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4", size = 6335777, upload-time = "2026-02-11T04:20:14.441Z" }, { url = "https://files.pythonhosted.org/packages/ea/51/55c751a57cc524a15a0e3db20e5cde517582359508d62305a627e77fd295/pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda", size = 6335760, upload-time = "2026-01-02T09:10:33.02Z" },
{ url = "https://files.pythonhosted.org/packages/0c/7b/f9b09a7804ec7336effb96c26d37c29d27225783dc1501b7d62dcef6ae25/pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4", size = 7027140, upload-time = "2026-02-11T04:20:16.387Z" }, { url = "https://files.pythonhosted.org/packages/dc/7c/60e3e6f5e5891a1a06b4c910f742ac862377a6fe842f7184df4a274ce7bf/pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7", size = 7027127, upload-time = "2026-01-02T09:10:35.009Z" },
{ url = "https://files.pythonhosted.org/packages/98/b2/2fa3c391550bd421b10849d1a2144c44abcd966daadd2f7c12e19ea988c4/pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e", size = 6449855, upload-time = "2026-02-11T04:20:18.554Z" }, { url = "https://files.pythonhosted.org/packages/06/37/49d47266ba50b00c27ba63a7c898f1bb41a29627ced8c09e25f19ebec0ff/pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a", size = 6449896, upload-time = "2026-01-02T09:10:36.793Z" },
{ url = "https://files.pythonhosted.org/packages/96/ff/9caf4b5b950c669263c39e96c78c0d74a342c71c4f43fd031bb5cb7ceac9/pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff", size = 7151329, upload-time = "2026-02-11T04:20:20.646Z" }, { url = "https://files.pythonhosted.org/packages/f9/e5/67fd87d2913902462cd9b79c6211c25bfe95fcf5783d06e1367d6d9a741f/pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef", size = 7151345, upload-time = "2026-01-02T09:10:39.064Z" },
{ url = "https://files.pythonhosted.org/packages/7b/f8/4b24841f582704da675ca535935bccb32b00a6da1226820845fac4a71136/pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40", size = 6325574, upload-time = "2026-02-11T04:20:22.43Z" }, { url = "https://files.pythonhosted.org/packages/bd/15/f8c7abf82af68b29f50d77c227e7a1f87ce02fdc66ded9bf603bc3b41180/pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09", size = 6325568, upload-time = "2026-01-02T09:10:41.035Z" },
{ url = "https://files.pythonhosted.org/packages/f8/f9/9f6b01c0881d7036063aa6612ef04c0e2cad96be21325a1e92d0203f8e91/pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23", size = 7032347, upload-time = "2026-02-11T04:20:23.932Z" }, { url = "https://files.pythonhosted.org/packages/d4/24/7d1c0e160b6b5ac2605ef7d8be537e28753c0db5363d035948073f5513d7/pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91", size = 7032367, upload-time = "2026-01-02T09:10:43.09Z" },
{ url = "https://files.pythonhosted.org/packages/79/13/c7922edded3dcdaf10c59297540b72785620abc0538872c819915746757d/pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9", size = 2453457, upload-time = "2026-02-11T04:20:25.392Z" }, { url = "https://files.pythonhosted.org/packages/f4/03/41c038f0d7a06099254c60f618d0ec7be11e79620fc23b8e85e5b31d9a44/pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea", size = 2452345, upload-time = "2026-01-02T09:10:44.795Z" },
{ url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, { url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" },
{ url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, { url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" },
{ url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, { url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" },
{ url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, { url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" },
{ url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, { url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" },
{ url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, { url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" },
{ url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, { url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" },
{ url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, { url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" },
{ url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, { url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655, upload-time = "2026-01-02T09:11:04.556Z" },
{ url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, { url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469, upload-time = "2026-01-02T09:11:06.538Z" },
{ url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, { url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515, upload-time = "2026-01-02T09:11:08.226Z" },
{ url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" },
{ url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" },
{ url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" },
{ url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" },
{ url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" },
{ url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" },
{ url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" },
{ url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" },
{ url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" },
{ url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" },
{ url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" },
{ url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, { url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" },
{ url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, { url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" },
{ url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, { url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" },
{ url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, { url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" },
{ url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, { url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" },
{ url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, { url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" },
{ url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" },
] ]
[[package]] [[package]]