Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
648a657922 | ||
|
|
cf251b1f33 | ||
|
|
f94c4e432e | ||
|
|
6360c49ed0 | ||
|
|
bfa35d9c69 | ||
|
|
561d0976b3 | ||
|
|
d40515e1ba | ||
|
|
766fe3b5c6 | ||
|
|
82940a91b6 | ||
|
|
5bd524660f | ||
|
|
be2bb929c0 | ||
|
|
7d34f459fa | ||
|
|
11d16620dd | ||
|
|
c99bb28963 | ||
|
|
631f4e8455 | ||
|
|
b22f88b264 | ||
|
|
c08bc02930 | ||
|
|
e020a7c5a0 | ||
|
|
2bd398b59c | ||
|
|
0e69aa536d | ||
|
|
984fc05caa | ||
|
|
d2e8914835 | ||
|
|
5e79ca35ab | ||
|
|
4690d178a2 | ||
|
|
c5825aa4c3 | ||
|
|
ebe0c4e3c3 | ||
|
|
383bdb6312 | ||
|
|
a947686d79 | ||
|
|
28e9d32b44 | ||
|
|
213cde5ec6 | ||
|
|
2cd694c36e | ||
|
|
fb9cb6f2ed | ||
|
|
2852caeb42 | ||
|
|
34fa193dc5 | ||
|
|
0f38fcfaa9 | ||
|
|
2cac4e6b4d | ||
|
|
9983af4eec | ||
|
|
a061dda5c1 | ||
|
|
6b233cc067 |
+1
-5
@@ -6,10 +6,6 @@
|
|||||||
# The following settings are ONLY set in your .env file.
|
# The following settings are ONLY set in your .env file.
|
||||||
# They are NOT managed by the Admin UI and are not stored in the database.
|
# They are NOT managed by the Admin UI and are not stored in the database.
|
||||||
|
|
||||||
# === First Install Token ===
|
|
||||||
# if set the user is required to enter this token on the /first_install page
|
|
||||||
# FIRST_INSTALL_TOKEN="myaccesstoken"
|
|
||||||
|
|
||||||
# === Logging and Development ===
|
# === Logging and Development ===
|
||||||
|
|
||||||
DEBUG=False
|
DEBUG=False
|
||||||
@@ -59,7 +55,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, SparkWallet, LightsparkSparkWallet"
|
||||||
|
|
||||||
# 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
|
||||||
|
|||||||
@@ -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 }}
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -44,19 +44,15 @@ jobs:
|
|||||||
if: github.repository == 'lnbits/lnbits'
|
if: github.repository == 'lnbits/lnbits'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install dependencies for building secp256k1
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y build-essential automake libtool libffi-dev libgmp-dev
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.10
|
- name: Build and publish to pypi
|
||||||
uses: actions/setup-python@v5
|
uses: JRubics/poetry-publish@v1.15
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v6
|
|
||||||
- name: Build the project
|
|
||||||
run: uv build
|
|
||||||
- name: Publish to pypi
|
|
||||||
env:
|
|
||||||
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_KEY }}
|
|
||||||
run: uv publish
|
|
||||||
|
|
||||||
appimage:
|
appimage:
|
||||||
needs: [ release ]
|
needs: [ release ]
|
||||||
|
|||||||
@@ -56,19 +56,15 @@ jobs:
|
|||||||
if: github.repository == 'lnbits/lnbits'
|
if: github.repository == 'lnbits/lnbits'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install dependencies for building secp256k1
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y build-essential automake libtool libffi-dev libgmp-dev
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.10
|
- name: Build and publish to pypi
|
||||||
uses: actions/setup-python@v5
|
uses: JRubics/poetry-publish@v1.15
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v6
|
|
||||||
- name: Build the project
|
|
||||||
run: uv build
|
|
||||||
- name: Publish to pypi
|
|
||||||
env:
|
|
||||||
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_KEY }}
|
|
||||||
run: uv publish
|
|
||||||
|
|
||||||
appimage:
|
appimage:
|
||||||
needs: [ release ]
|
needs: [ release ]
|
||||||
|
|||||||
@@ -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"]
|
||||||
@@ -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
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||

|
|
||||||
|
|
||||||
# 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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/
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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"]
|
|
||||||
@@ -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='*'
|
|
||||||
@@ -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)
|
## TODO: add Spark
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
+2
-42
@@ -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) |
|
||||||
@@ -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**
|
||||||
@@ -380,6 +338,8 @@ Configure in the admin UI or via env vars:
|
|||||||
|
|
||||||
<a id="strike"></a>
|
<a id="strike"></a>
|
||||||
|
|
||||||
|
## TODO: add Spark
|
||||||
|
|
||||||
## Strike (alpha)
|
## Strike (alpha)
|
||||||
|
|
||||||
Custodial provider integrated via **Strike OAuth Connect** (OAuth 2.0 / OIDC). Authenticate a Strike user in your app, then call Strike APIs on the user’s behalf once scopes are granted. Requires a Strike business account, registered OAuth client, minimal scopes, and login/logout redirect URLs.
|
Custodial provider integrated via **Strike OAuth Connect** (OAuth 2.0 / OIDC). Authenticate a Strike user in your app, then call Strike APIs on the user’s behalf once scopes are granted. Requires a Strike business account, registered OAuth client, minimal scopes, and login/logout redirect URLs.
|
||||||
|
|||||||
+21
-76
@@ -4,12 +4,10 @@ from typing import Any
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
||||||
from lnbits.core.crud.wallets import clear_wallet_cache, create_wallet, get_wallets
|
from lnbits.core.crud.wallets import create_wallet, get_wallets
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models import UserAcls
|
from lnbits.core.models import UserAcls
|
||||||
from lnbits.db import Connection, Filters, Page
|
from lnbits.db import Connection, Filters, Page
|
||||||
from lnbits.helpers import sha256s
|
|
||||||
from lnbits.utils.cache import cache
|
|
||||||
|
|
||||||
from ..models import (
|
from ..models import (
|
||||||
Account,
|
Account,
|
||||||
@@ -43,7 +41,6 @@ async def delete_account(user_id: str, conn: Connection | None = None) -> None:
|
|||||||
"DELETE from accounts WHERE id = :user",
|
"DELETE from accounts WHERE id = :user",
|
||||||
{"user": user_id},
|
{"user": user_id},
|
||||||
)
|
)
|
||||||
await clear_user_id_cache(user_id)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_accounts(
|
async def get_accounts(
|
||||||
@@ -72,7 +69,6 @@ async def get_accounts(
|
|||||||
accounts.email,
|
accounts.email,
|
||||||
accounts.pubkey,
|
accounts.pubkey,
|
||||||
accounts.external_id,
|
accounts.external_id,
|
||||||
accounts.activated,
|
|
||||||
SUM(COALESCE((
|
SUM(COALESCE((
|
||||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
), 0)) as balance_msat,
|
), 0)) as balance_msat,
|
||||||
@@ -97,18 +93,12 @@ async def get_accounts(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account(
|
async def get_account(user_id: str, conn: Connection | None = None) -> Account | None:
|
||||||
user_id: str, active_only: bool = True, conn: Connection | None = None
|
|
||||||
) -> Account | None:
|
|
||||||
if len(user_id) == 0:
|
if len(user_id) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"""
|
"SELECT * FROM accounts WHERE id = :id",
|
||||||
SELECT * FROM accounts
|
{"id": user_id},
|
||||||
WHERE id = :id AND (activated = true OR activated = :activated)
|
|
||||||
""",
|
|
||||||
{"id": user_id, "activated": active_only},
|
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -134,79 +124,55 @@ async def delete_accounts_no_wallets(
|
|||||||
|
|
||||||
|
|
||||||
async def get_account_by_username(
|
async def get_account_by_username(
|
||||||
username: str, active_only: bool = True, conn: Connection | None = None
|
username: str, conn: Connection | None = None
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
if len(username) == 0:
|
if len(username) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"""
|
"SELECT * FROM accounts WHERE LOWER(username) = :username",
|
||||||
SELECT * FROM accounts
|
{"username": username.lower()},
|
||||||
WHERE
|
|
||||||
LOWER(username) = :username
|
|
||||||
AND (activated = true OR activated = :activated)
|
|
||||||
""",
|
|
||||||
{"username": username.lower(), "activated": active_only},
|
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account_by_pubkey(
|
async def get_account_by_pubkey(
|
||||||
pubkey: str, active_only: bool = True, conn: Connection | None = None
|
pubkey: str, conn: Connection | None = None
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"""
|
"SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
|
||||||
SELECT * FROM accounts
|
{"pubkey": pubkey.lower()},
|
||||||
WHERE
|
|
||||||
LOWER(pubkey) = :pubkey
|
|
||||||
AND (activated = true OR activated = :activated)
|
|
||||||
""",
|
|
||||||
{"pubkey": pubkey.lower(), "activated": active_only},
|
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account_by_email(
|
async def get_account_by_email(
|
||||||
email: str, active_only: bool = True, conn: Connection | None = None
|
email: str, conn: Connection | None = None
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
if len(email) == 0:
|
if len(email) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"""
|
"SELECT * FROM accounts WHERE LOWER(email) = :email",
|
||||||
SELECT * FROM accounts
|
{"email": email.lower()},
|
||||||
WHERE
|
|
||||||
LOWER(email) = :email
|
|
||||||
AND (activated = true OR activated = :activated)
|
|
||||||
""",
|
|
||||||
{"email": email.lower(), "activated": active_only},
|
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account_by_username_or_email(
|
async def get_account_by_username_or_email(
|
||||||
username_or_email: str,
|
username_or_email: str, conn: Connection | None = None
|
||||||
active_only: bool = True,
|
|
||||||
conn: Connection | None = None,
|
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM accounts
|
SELECT * FROM accounts
|
||||||
WHERE
|
WHERE LOWER(email) = :value or LOWER(username) = :value
|
||||||
(LOWER(email) = :value or LOWER(username) = :value)
|
|
||||||
AND (activated = true OR activated = :activated)
|
|
||||||
""",
|
""",
|
||||||
{"value": username_or_email.lower(), "activated": active_only},
|
{"value": username_or_email.lower()},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_user(
|
async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
|
||||||
user_id: str, active_only: bool = True, conn: Connection | None = None
|
|
||||||
) -> User | None:
|
|
||||||
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||||
account = await get_account(user_id, active_only, conn=conn)
|
account = await get_account(user_id, conn=conn)
|
||||||
if not account:
|
if not account:
|
||||||
return None
|
return None
|
||||||
return await get_user_from_account(account, conn=conn)
|
return await get_user_from_account(account, conn=conn)
|
||||||
@@ -225,7 +191,6 @@ async def get_user_from_account(
|
|||||||
|
|
||||||
return User(
|
return User(
|
||||||
id=account.id,
|
id=account.id,
|
||||||
activated=account.activated,
|
|
||||||
email=account.email,
|
email=account.email,
|
||||||
username=account.username,
|
username=account.username,
|
||||||
pubkey=account.pubkey,
|
pubkey=account.pubkey,
|
||||||
@@ -239,7 +204,6 @@ async def get_user_from_account(
|
|||||||
super_user=account.is_super_user,
|
super_user=account.is_super_user,
|
||||||
fiat_providers=account.fiat_providers,
|
fiat_providers=account.fiat_providers,
|
||||||
has_password=account.password_hash is not None,
|
has_password=account.password_hash is not None,
|
||||||
ui_customization=account.ui_customization or {},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -251,31 +215,12 @@ async def update_user_access_control_list(
|
|||||||
|
|
||||||
|
|
||||||
async def get_user_access_control_lists(
|
async def get_user_access_control_lists(
|
||||||
user_id: str, active_only: bool = True, conn: Connection | None = None
|
user_id: str, conn: Connection | None = None
|
||||||
) -> UserAcls:
|
) -> UserAcls:
|
||||||
user_acls = await (conn or db).fetchone(
|
user_acls = await (conn or db).fetchone(
|
||||||
"""
|
"SELECT id, access_control_list FROM accounts WHERE id = :id",
|
||||||
SELECT id, access_control_list FROM accounts
|
{"id": user_id},
|
||||||
WHERE id = :user_id AND (activated = true OR activated = :activated)
|
|
||||||
""",
|
|
||||||
{"user_id": user_id, "activated": active_only},
|
|
||||||
UserAcls,
|
UserAcls,
|
||||||
)
|
)
|
||||||
|
|
||||||
return user_acls or UserAcls(id=user_id)
|
return user_acls or UserAcls(id=user_id)
|
||||||
|
|
||||||
|
|
||||||
async def clear_user_id_cache(user_id: str):
|
|
||||||
user = await get_user(user_id, active_only=True)
|
|
||||||
if user:
|
|
||||||
clear_user_cache(user)
|
|
||||||
|
|
||||||
|
|
||||||
def clear_user_cache(user: User):
|
|
||||||
user_cache_key: str | None = cache.pop(
|
|
||||||
f"auth:user:cache_key:{sha256s(user.id)}", None
|
|
||||||
)
|
|
||||||
if user_cache_key:
|
|
||||||
cache.pop(user_cache_key)
|
|
||||||
for wallet in user.wallets:
|
|
||||||
clear_wallet_cache(wallet)
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ async def delete_wallet(
|
|||||||
deleted: bool = True,
|
deleted: bool = True,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
clear_wallet_id_cache(wallet_id)
|
_clear_wallet_cache(wallet_id)
|
||||||
now = int(time())
|
now = int(time())
|
||||||
|
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
@@ -65,7 +65,7 @@ async def delete_wallet(
|
|||||||
|
|
||||||
|
|
||||||
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
|
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
|
||||||
clear_wallet_id_cache(wallet_id)
|
_clear_wallet_cache(wallet_id)
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"DELETE FROM wallets WHERE id = :wallet",
|
"DELETE FROM wallets WHERE id = :wallet",
|
||||||
{"wallet": wallet_id},
|
{"wallet": wallet_id},
|
||||||
@@ -75,7 +75,7 @@ async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) ->
|
|||||||
async def delete_wallet_by_id(
|
async def delete_wallet_by_id(
|
||||||
wallet_id: str, conn: Connection | None = None
|
wallet_id: str, conn: Connection | None = None
|
||||||
) -> int | None:
|
) -> int | None:
|
||||||
clear_wallet_id_cache(wallet_id)
|
_clear_wallet_cache(wallet_id)
|
||||||
now = int(time())
|
now = int(time())
|
||||||
result = await (conn or db).execute(
|
result = await (conn or db).execute(
|
||||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
@@ -226,14 +226,11 @@ async def get_wallet_for_key(
|
|||||||
) -> Wallet | None:
|
) -> Wallet | None:
|
||||||
wallet = await (conn or db).fetchone(
|
wallet = await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT wallets.*, COALESCE((
|
SELECT *, COALESCE((
|
||||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
), 0)
|
), 0)
|
||||||
AS balance_msat FROM wallets
|
AS balance_msat FROM wallets
|
||||||
INNER JOIN accounts ON wallets.user = accounts.id
|
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
|
||||||
WHERE (adminkey = :key OR inkey = :key)
|
|
||||||
AND deleted = false
|
|
||||||
AND accounts.activated = true
|
|
||||||
""",
|
""",
|
||||||
{"key": key},
|
{"key": key},
|
||||||
Wallet,
|
Wallet,
|
||||||
@@ -253,11 +250,8 @@ async def get_base_wallet_for_key(
|
|||||||
) -> BaseWallet | None:
|
) -> BaseWallet | None:
|
||||||
wallet = await (conn or db).fetchone(
|
wallet = await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT wallets.id, "user", wallet_type, adminkey, inkey FROM wallets
|
SELECT id, "user", wallet_type, adminkey, inkey FROM wallets
|
||||||
INNER JOIN accounts ON wallets.user = accounts.id
|
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
|
||||||
WHERE (adminkey = :key OR inkey = :key)
|
|
||||||
AND deleted = false
|
|
||||||
AND accounts.activated = true
|
|
||||||
""",
|
""",
|
||||||
{"key": key},
|
{"key": key},
|
||||||
BaseWallet,
|
BaseWallet,
|
||||||
@@ -300,14 +294,8 @@ async def get_total_balance(conn: Connection | None = None):
|
|||||||
return row.get("balance", 0) or 0
|
return row.get("balance", 0) or 0
|
||||||
|
|
||||||
|
|
||||||
def clear_wallet_id_cache(wallet_id: str):
|
def _clear_wallet_cache(wallet_id):
|
||||||
cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}")
|
cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}")
|
||||||
if cached_wallet:
|
if cached_wallet:
|
||||||
cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}")
|
cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}")
|
||||||
cache.pop(f"auth:x-api-key:{cached_wallet.inkey}")
|
cache.pop(f"auth:x-api-key:{cached_wallet.inkey}")
|
||||||
|
|
||||||
|
|
||||||
def clear_wallet_cache(wallet: Wallet):
|
|
||||||
cache.pop(f"auth:wallet:{wallet.id}")
|
|
||||||
cache.pop(f"auth:x-api-key:{wallet.adminkey}")
|
|
||||||
cache.pop(f"auth:x-api-key:{wallet.inkey}")
|
|
||||||
|
|||||||
@@ -848,19 +848,3 @@ async def m042_index_accounts(db: Connection):
|
|||||||
CREATE INDEX IF NOT EXISTS idx_accounts_{index} ON accounts ("{index}");
|
CREATE INDEX IF NOT EXISTS idx_accounts_{index} ON accounts ("{index}");
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m043_add_ui_customization_to_accounts(db: Connection):
|
|
||||||
"""
|
|
||||||
Adds ui_customization column to accounts.
|
|
||||||
Used for server side persistence of UI customization settings.
|
|
||||||
"""
|
|
||||||
await db.execute("ALTER TABLE accounts ADD COLUMN ui_customization TEXT")
|
|
||||||
|
|
||||||
|
|
||||||
async def m044_add_activated_to_accounts(db: Connection):
|
|
||||||
"""
|
|
||||||
Adds activated column to accounts.
|
|
||||||
Used for account activation status.
|
|
||||||
"""
|
|
||||||
await db.execute("ALTER TABLE accounts ADD COLUMN activated BOOLEAN DEFAULT true")
|
|
||||||
|
|||||||
@@ -6,16 +6,23 @@ from typing import Literal
|
|||||||
|
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
from lnurl import LnurlWithdrawResponse
|
from lnurl import LnurlWithdrawResponse
|
||||||
from loguru import logger
|
|
||||||
from pydantic import BaseModel, Field, validator
|
from pydantic import BaseModel, Field, validator
|
||||||
|
|
||||||
from lnbits.db import FilterModel
|
from lnbits.db import FilterModel
|
||||||
|
from lnbits.fiat import get_fiat_provider
|
||||||
from lnbits.fiat.base import (
|
from lnbits.fiat.base import (
|
||||||
|
FiatPaymentFailedStatus,
|
||||||
|
FiatPaymentPendingStatus,
|
||||||
FiatPaymentStatus,
|
FiatPaymentStatus,
|
||||||
|
FiatPaymentSuccessStatus,
|
||||||
)
|
)
|
||||||
from lnbits.utils.exchange_rates import allowed_currencies
|
from lnbits.utils.exchange_rates import allowed_currencies
|
||||||
|
from lnbits.wallets import get_funding_source
|
||||||
from lnbits.wallets.base import (
|
from lnbits.wallets.base import (
|
||||||
|
PaymentFailedStatus,
|
||||||
|
PaymentPendingStatus,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
|
PaymentSuccessStatus,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -123,23 +130,59 @@ class Payment(BaseModel):
|
|||||||
"fiat_"
|
"fiat_"
|
||||||
)
|
)
|
||||||
|
|
||||||
# DEPRECATED: in v1.5.0, use service check_payment_status instead
|
|
||||||
async def check_status(
|
async def check_status(
|
||||||
self, skip_internal_payment_notifications: bool | None = False
|
self, skip_internal_payment_notifications: bool | None = False
|
||||||
) -> PaymentStatus:
|
) -> PaymentStatus:
|
||||||
logger.warning("payment.check_status() is deprecated.")
|
if self.is_internal:
|
||||||
from lnbits.core.services.payments import check_payment_status
|
if self.success:
|
||||||
|
return PaymentSuccessStatus()
|
||||||
|
if self.failed:
|
||||||
|
return PaymentFailedStatus()
|
||||||
|
if self.is_in and self.fiat_provider:
|
||||||
|
fiat_status = await self.check_fiat_status(
|
||||||
|
skip_internal_payment_notifications
|
||||||
|
)
|
||||||
|
return PaymentStatus(paid=fiat_status.paid)
|
||||||
|
return PaymentPendingStatus()
|
||||||
|
funding_source = get_funding_source()
|
||||||
|
if self.is_out:
|
||||||
|
status = await funding_source.get_payment_status(self.checking_id)
|
||||||
|
else:
|
||||||
|
status = await funding_source.get_invoice_status(self.checking_id)
|
||||||
|
return status
|
||||||
|
|
||||||
return await check_payment_status(self, skip_internal_payment_notifications)
|
|
||||||
|
|
||||||
# DEPRECATED: in v1.5.0, use service check_payment_status instead
|
|
||||||
async def check_fiat_status(
|
async def check_fiat_status(
|
||||||
self, skip_internal_payment_notifications: bool | None = False
|
self, skip_internal_payment_notifications: bool | None = False
|
||||||
) -> FiatPaymentStatus:
|
) -> FiatPaymentStatus:
|
||||||
logger.warning("payment.check_fiat_status() is deprecated.")
|
if not self.is_internal:
|
||||||
from lnbits.core.services.fiat_providers import check_fiat_status
|
return FiatPaymentPendingStatus()
|
||||||
|
if self.success:
|
||||||
|
return FiatPaymentSuccessStatus()
|
||||||
|
if self.failed:
|
||||||
|
return FiatPaymentFailedStatus()
|
||||||
|
|
||||||
return await check_fiat_status(self, skip_internal_payment_notifications)
|
if not self.fiat_provider:
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
|
||||||
|
checking_id = self.extra.get("fiat_checking_id")
|
||||||
|
if not checking_id:
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
|
||||||
|
fiat_provider = await get_fiat_provider(self.fiat_provider)
|
||||||
|
if not fiat_provider:
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
fiat_status = await fiat_provider.get_invoice_status(checking_id)
|
||||||
|
|
||||||
|
if skip_internal_payment_notifications:
|
||||||
|
return fiat_status
|
||||||
|
|
||||||
|
if fiat_status.success:
|
||||||
|
# notify receivers asynchronously
|
||||||
|
from lnbits.tasks import internal_invoice_queue
|
||||||
|
|
||||||
|
await internal_invoice_queue.put(self.checking_id)
|
||||||
|
|
||||||
|
return fiat_status
|
||||||
|
|
||||||
|
|
||||||
class PaymentFilters(FilterModel):
|
class PaymentFilters(FilterModel):
|
||||||
|
|||||||
@@ -181,14 +181,12 @@ class AccountId(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class Account(AccountId):
|
class Account(AccountId):
|
||||||
activated: bool = True
|
|
||||||
external_id: str | None = None # for external account linking
|
external_id: str | None = None # for external account linking
|
||||||
username: str | None = None
|
username: str | None = None
|
||||||
password_hash: str | None = None
|
password_hash: str | None = None
|
||||||
pubkey: str | None = None
|
pubkey: str | None = None
|
||||||
email: str | None = None
|
email: str | None = None
|
||||||
extra: UserExtra = UserExtra()
|
extra: UserExtra = UserExtra()
|
||||||
ui_customization: dict = Field(default_factory=dict)
|
|
||||||
|
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
@@ -242,7 +240,6 @@ class Account(AccountId):
|
|||||||
|
|
||||||
|
|
||||||
class AccountOverview(Account):
|
class AccountOverview(Account):
|
||||||
activated: bool = True
|
|
||||||
transaction_count: int | None = 0
|
transaction_count: int | None = 0
|
||||||
wallet_count: int | None = 0
|
wallet_count: int | None = 0
|
||||||
balance_msat: int | None = 0
|
balance_msat: int | None = 0
|
||||||
@@ -278,7 +275,6 @@ class AccountFilters(FilterModel):
|
|||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
activated: bool = True
|
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
email: str | None = None
|
email: str | None = None
|
||||||
@@ -292,7 +288,6 @@ class User(BaseModel):
|
|||||||
fiat_providers: list[str] = []
|
fiat_providers: list[str] = []
|
||||||
has_password: bool = False
|
has_password: bool = False
|
||||||
extra: UserExtra = UserExtra()
|
extra: UserExtra = UserExtra()
|
||||||
ui_customization: dict = Field(default_factory=dict)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def wallet_ids(self) -> list[str]:
|
def wallet_ids(self) -> list[str]:
|
||||||
@@ -318,7 +313,6 @@ 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)
|
|
||||||
|
|
||||||
|
|
||||||
class CreateUser(BaseModel):
|
class CreateUser(BaseModel):
|
||||||
@@ -362,7 +356,6 @@ class UpdateSuperuserPassword(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)
|
||||||
first_install_token: str | None = Query(None)
|
|
||||||
|
|
||||||
|
|
||||||
class LoginUsr(BaseModel):
|
class LoginUsr(BaseModel):
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from .fiat_providers import check_fiat_status
|
|
||||||
from .funding_source import (
|
from .funding_source import (
|
||||||
get_balance_delta,
|
get_balance_delta,
|
||||||
switch_to_voidwallet,
|
switch_to_voidwallet,
|
||||||
@@ -8,7 +7,6 @@ from .notifications import enqueue_admin_notification, send_payment_notification
|
|||||||
from .payments import (
|
from .payments import (
|
||||||
calculate_fiat_amounts,
|
calculate_fiat_amounts,
|
||||||
cancel_hold_invoice,
|
cancel_hold_invoice,
|
||||||
check_payment_status,
|
|
||||||
check_transaction_status,
|
check_transaction_status,
|
||||||
check_wallet_limits,
|
check_wallet_limits,
|
||||||
create_fiat_invoice,
|
create_fiat_invoice,
|
||||||
@@ -42,8 +40,6 @@ __all__ = [
|
|||||||
"calculate_fiat_amounts",
|
"calculate_fiat_amounts",
|
||||||
"cancel_hold_invoice",
|
"cancel_hold_invoice",
|
||||||
"check_admin_settings",
|
"check_admin_settings",
|
||||||
"check_fiat_status",
|
|
||||||
"check_payment_status",
|
|
||||||
"check_transaction_status",
|
"check_transaction_status",
|
||||||
"check_wallet_limits",
|
"check_wallet_limits",
|
||||||
"check_webpush_settings",
|
"check_webpush_settings",
|
||||||
|
|||||||
@@ -12,12 +12,6 @@ from lnbits.core.models import CreatePayment, Payment, PaymentState
|
|||||||
from lnbits.core.models.misc import SimpleStatus
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
from lnbits.db import Connection
|
from lnbits.db import Connection
|
||||||
from lnbits.fiat import get_fiat_provider
|
from lnbits.fiat import get_fiat_provider
|
||||||
from lnbits.fiat.base import (
|
|
||||||
FiatPaymentFailedStatus,
|
|
||||||
FiatPaymentPendingStatus,
|
|
||||||
FiatPaymentStatus,
|
|
||||||
FiatPaymentSuccessStatus,
|
|
||||||
)
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
|
||||||
@@ -35,40 +29,6 @@ async def handle_fiat_payment_confirmation(
|
|||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
|
|
||||||
|
|
||||||
async def check_fiat_status(
|
|
||||||
payment: Payment, skip_internal_payment_notifications: bool | None = False
|
|
||||||
) -> FiatPaymentStatus:
|
|
||||||
if not payment.is_internal:
|
|
||||||
return FiatPaymentPendingStatus()
|
|
||||||
if payment.success:
|
|
||||||
return FiatPaymentSuccessStatus()
|
|
||||||
if payment.failed:
|
|
||||||
return FiatPaymentFailedStatus()
|
|
||||||
|
|
||||||
if not payment.fiat_provider:
|
|
||||||
return FiatPaymentPendingStatus()
|
|
||||||
|
|
||||||
checking_id = payment.extra.get("fiat_checking_id")
|
|
||||||
if not checking_id:
|
|
||||||
return FiatPaymentPendingStatus()
|
|
||||||
|
|
||||||
fiat_provider = await get_fiat_provider(payment.fiat_provider)
|
|
||||||
if not fiat_provider:
|
|
||||||
return FiatPaymentPendingStatus()
|
|
||||||
fiat_status = await fiat_provider.get_invoice_status(checking_id)
|
|
||||||
|
|
||||||
if skip_internal_payment_notifications:
|
|
||||||
return fiat_status
|
|
||||||
|
|
||||||
if fiat_status.success:
|
|
||||||
# notify receivers asynchronously
|
|
||||||
from lnbits.tasks import internal_invoice_queue
|
|
||||||
|
|
||||||
await internal_invoice_queue.put(payment.checking_id)
|
|
||||||
|
|
||||||
return fiat_status
|
|
||||||
|
|
||||||
|
|
||||||
def check_stripe_signature(
|
def check_stripe_signature(
|
||||||
payload: bytes,
|
payload: bytes,
|
||||||
sig_header: str | None,
|
sig_header: str | None,
|
||||||
@@ -118,8 +78,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 +89,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:
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount
|
|||||||
from lnbits.wallets import fake_wallet, get_funding_source
|
from lnbits.wallets import fake_wallet, get_funding_source
|
||||||
from lnbits.wallets.base import (
|
from lnbits.wallets.base import (
|
||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
PaymentFailedStatus,
|
|
||||||
PaymentPendingStatus,
|
PaymentPendingStatus,
|
||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
@@ -48,7 +47,6 @@ from ..models import (
|
|||||||
PaymentState,
|
PaymentState,
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
from .fiat_providers import check_fiat_status
|
|
||||||
from .notifications import send_payment_notification_in_background
|
from .notifications import send_payment_notification_in_background
|
||||||
|
|
||||||
payment_lock = asyncio.Lock()
|
payment_lock = asyncio.Lock()
|
||||||
@@ -366,7 +364,7 @@ async def update_pending_payments(wallet_id: str):
|
|||||||
async def update_pending_payment(
|
async def update_pending_payment(
|
||||||
payment: Payment, conn: Connection | None = None
|
payment: Payment, conn: Connection | None = None
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
status = await check_payment_status(payment)
|
status = await payment.check_status()
|
||||||
if status.failed:
|
if status.failed:
|
||||||
payment.status = PaymentState.FAILED
|
payment.status = PaymentState.FAILED
|
||||||
await update_payment(payment, conn=conn)
|
await update_payment(payment, conn=conn)
|
||||||
@@ -620,29 +618,7 @@ async def check_transaction_status(
|
|||||||
if payment.status == PaymentState.SUCCESS.value:
|
if payment.status == PaymentState.SUCCESS.value:
|
||||||
return PaymentSuccessStatus(fee_msat=payment.fee)
|
return PaymentSuccessStatus(fee_msat=payment.fee)
|
||||||
|
|
||||||
return await check_payment_status(payment)
|
return await payment.check_status()
|
||||||
|
|
||||||
|
|
||||||
async def check_payment_status(
|
|
||||||
payment: Payment, skip_internal_payment_notifications: bool | None = False
|
|
||||||
) -> PaymentStatus:
|
|
||||||
if payment.is_internal:
|
|
||||||
if payment.success:
|
|
||||||
return PaymentSuccessStatus()
|
|
||||||
if payment.failed:
|
|
||||||
return PaymentFailedStatus()
|
|
||||||
if payment.is_in and payment.fiat_provider:
|
|
||||||
fiat_status = await check_fiat_status(
|
|
||||||
payment, skip_internal_payment_notifications
|
|
||||||
)
|
|
||||||
return PaymentStatus(paid=fiat_status.paid)
|
|
||||||
return PaymentPendingStatus()
|
|
||||||
funding_source = get_funding_source()
|
|
||||||
if payment.is_out:
|
|
||||||
status = await funding_source.get_payment_status(payment.checking_id)
|
|
||||||
else:
|
|
||||||
status = await funding_source.get_invoice_status(payment.checking_id)
|
|
||||||
return status
|
|
||||||
|
|
||||||
|
|
||||||
async def get_payments_daily_stats(
|
async def get_payments_daily_stats(
|
||||||
@@ -776,7 +752,7 @@ async def _pay_internal_invoice(
|
|||||||
await update_payment(internal_payment, conn=conn)
|
await update_payment(internal_payment, conn=conn)
|
||||||
logger.success(f"internal payment successful {internal_payment.checking_id}")
|
logger.success(f"internal payment successful {internal_payment.checking_id}")
|
||||||
|
|
||||||
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
|
send_payment_notification_in_background(wallet, payment)
|
||||||
|
|
||||||
# notify receiver asynchronously
|
# notify receiver asynchronously
|
||||||
from lnbits.tasks import internal_invoice_queue
|
from lnbits.tasks import internal_invoice_queue
|
||||||
@@ -849,8 +825,7 @@ async def _pay_external_invoice(
|
|||||||
payment = await update_payment_success_status(
|
payment = await update_payment_success_status(
|
||||||
payment, payment_response, conn=conn
|
payment, payment_response, conn=conn
|
||||||
)
|
)
|
||||||
|
send_payment_notification_in_background(wallet, payment)
|
||||||
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
|
|
||||||
logger.success(f"payment successful {payment_response.checking_id}")
|
logger.success(f"payment successful {payment_response.checking_id}")
|
||||||
|
|
||||||
payment.checking_id = payment_response.checking_id
|
payment.checking_id = payment_response.checking_id
|
||||||
@@ -893,7 +868,7 @@ async def _verify_external_payment(
|
|||||||
raise PaymentError("Payment already paid.", status="success")
|
raise PaymentError("Payment already paid.", status="success")
|
||||||
|
|
||||||
# payment failed
|
# payment failed
|
||||||
status = await check_payment_status(payment)
|
status = await payment.check_status()
|
||||||
if status.failed:
|
if status.failed:
|
||||||
raise PaymentError(
|
raise PaymentError(
|
||||||
"Payment is failed node, retrying is not possible.", status="failed"
|
"Payment is failed node, retrying is not possible.", status="failed"
|
||||||
@@ -1058,13 +1033,3 @@ async def cancel_hold_invoice(payment: Payment) -> InvoiceResponse:
|
|||||||
await update_payment(payment)
|
await update_payment(payment)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
async def _send_payment_notification_in_background(
|
|
||||||
wallet_id: str, payment: Payment, conn: Connection | None = None
|
|
||||||
):
|
|
||||||
# fetch balance again
|
|
||||||
wallet = await get_wallet(wallet_id, conn=conn)
|
|
||||||
if not wallet:
|
|
||||||
raise PaymentError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
|
||||||
send_payment_notification_in_background(wallet, payment)
|
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ from uuid import uuid4
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.crud.settings import set_settings_field
|
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models.extensions import UserExtension
|
from lnbits.core.models.extensions import UserExtension
|
||||||
from lnbits.core.models.users import RegisterUser
|
|
||||||
from lnbits.db import Connection
|
from lnbits.db import Connection
|
||||||
from lnbits.settings import (
|
from lnbits.settings import (
|
||||||
EditableSettings,
|
EditableSettings,
|
||||||
@@ -194,25 +192,3 @@ async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
|
|||||||
|
|
||||||
editable_settings = EditableSettings.from_dict(settings.dict())
|
editable_settings = EditableSettings.from_dict(settings.dict())
|
||||||
return await create_admin_settings(account.id, editable_settings.dict())
|
return await create_admin_settings(account.id, editable_settings.dict())
|
||||||
|
|
||||||
|
|
||||||
async def check_register_activation_settings(data: RegisterUser):
|
|
||||||
if not settings.lnbits_require_user_activation:
|
|
||||||
return None
|
|
||||||
if settings.lnbits_user_activation_by_invitation_code:
|
|
||||||
code = data.invitation_code.strip() if data.invitation_code else ""
|
|
||||||
if len(code) == 0:
|
|
||||||
raise ValueError("Invitation code cannot be empty.")
|
|
||||||
|
|
||||||
if code == settings.lnbits_register_reusable_activation_code:
|
|
||||||
return None
|
|
||||||
if code in settings.lnbits_register_one_time_activation_codes:
|
|
||||||
settings.lnbits_register_one_time_activation_codes.remove(code)
|
|
||||||
await set_settings_field(
|
|
||||||
"lnbits_register_one_time_activation_codes",
|
|
||||||
settings.lnbits_register_one_time_activation_codes,
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
raise ValueError("Invalid invitation code.")
|
|
||||||
|
|
||||||
raise ValueError("No activation method provided.")
|
|
||||||
|
|||||||
@@ -26,10 +26,7 @@ from lnbits.core.models.users import (
|
|||||||
UpdateAccessControlList,
|
UpdateAccessControlList,
|
||||||
)
|
)
|
||||||
from lnbits.core.services import create_user_account
|
from lnbits.core.services import create_user_account
|
||||||
from lnbits.core.services.users import (
|
from lnbits.core.services.users import update_user_account
|
||||||
check_register_activation_settings,
|
|
||||||
update_user_account,
|
|
||||||
)
|
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
access_token_payload,
|
access_token_payload,
|
||||||
check_account_exists,
|
check_account_exists,
|
||||||
@@ -89,7 +86,6 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
|
|||||||
account = await get_account_by_username_or_email(data.username)
|
account = await get_account_by_username_or_email(data.username)
|
||||||
if not account or not account.verify_password(data.password):
|
if not account or not account.verify_password(data.password):
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||||
|
|
||||||
return _auth_success_response(account.username, account.id, account.email)
|
return _auth_success_response(account.username, account.id, account.email)
|
||||||
|
|
||||||
|
|
||||||
@@ -98,7 +94,7 @@ async def nostr_login(request: Request) -> JSONResponse:
|
|||||||
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
|
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
|
||||||
event = _nostr_nip98_event(request)
|
event = _nostr_nip98_event(request)
|
||||||
account = await get_account_by_pubkey(event["pubkey"], active_only=False)
|
account = await get_account_by_pubkey(event["pubkey"])
|
||||||
if not account:
|
if not account:
|
||||||
account = Account(
|
account = Account(
|
||||||
id=uuid4().hex,
|
id=uuid4().hex,
|
||||||
@@ -106,8 +102,6 @@ async def nostr_login(request: Request) -> JSONResponse:
|
|||||||
extra=UserExtra(provider="nostr"),
|
extra=UserExtra(provider="nostr"),
|
||||||
)
|
)
|
||||||
await create_user_account(account)
|
await create_user_account(account)
|
||||||
if not account.activated:
|
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User is not activated.")
|
|
||||||
return _auth_success_response(account.username or "", account.id, account.email)
|
return _auth_success_response(account.username or "", account.id, account.email)
|
||||||
|
|
||||||
|
|
||||||
@@ -363,14 +357,12 @@ async def register(data: RegisterUser) -> JSONResponse:
|
|||||||
if not is_valid_username(data.username):
|
if not is_valid_username(data.username):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
|
||||||
|
|
||||||
if await get_account_by_username(data.username, active_only=False):
|
if await get_account_by_username(data.username):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
||||||
|
|
||||||
if data.email and not is_valid_email_address(data.email):
|
if data.email and not is_valid_email_address(data.email):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
||||||
|
|
||||||
await check_register_activation_settings(data)
|
|
||||||
|
|
||||||
account = Account(
|
account = Account(
|
||||||
id=uuid4().hex,
|
id=uuid4().hex,
|
||||||
email=data.email,
|
email=data.email,
|
||||||
@@ -475,7 +467,7 @@ async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
|||||||
return _auth_success_response(account.username, user_id, account.email)
|
return _auth_success_response(account.username, user_id, account.email)
|
||||||
|
|
||||||
|
|
||||||
@auth_router.patch("")
|
@auth_router.put("/update")
|
||||||
async def update(
|
async def update(
|
||||||
data: UpdateUser, account: Account = Depends(check_account_exists)
|
data: UpdateUser, account: Account = Depends(check_account_exists)
|
||||||
) -> User | None:
|
) -> User | None:
|
||||||
@@ -491,41 +483,13 @@ async def update(
|
|||||||
return await get_user_from_account(account)
|
return await get_user_from_account(account)
|
||||||
|
|
||||||
|
|
||||||
@auth_router.patch("/ui")
|
|
||||||
async def update_ui_customization(
|
|
||||||
req: Request, account: Account = Depends(check_account_exists)
|
|
||||||
) -> Account:
|
|
||||||
ui_customization = await req.json()
|
|
||||||
account.ui_customization = {**(account.ui_customization or {}), **ui_customization}
|
|
||||||
|
|
||||||
if len(account.ui_customization or {}) > 1000 * 1024:
|
|
||||||
raise HTTPException(
|
|
||||||
HTTPStatus.BAD_REQUEST, "UI customization too large. Drop some fields."
|
|
||||||
)
|
|
||||||
|
|
||||||
await update_user_account(account)
|
|
||||||
return account
|
|
||||||
|
|
||||||
|
|
||||||
@auth_router.put("/first_install")
|
@auth_router.put("/first_install")
|
||||||
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 not data.first_install_token:
|
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing first_install_token.")
|
|
||||||
if settings.first_install_token != data.first_install_token:
|
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid first_install_token.")
|
|
||||||
|
|
||||||
account = await get_account_by_username(data.username, False)
|
|
||||||
if account:
|
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
|
||||||
|
|
||||||
account = await get_account(settings.super_user)
|
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"
|
||||||
@@ -541,7 +505,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: str | None = Non
|
|||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
||||||
|
|
||||||
redirect_path = "/wallet"
|
redirect_path = "/wallet"
|
||||||
account = await get_account_by_email(email, active_only=False)
|
account = await get_account_by_email(email)
|
||||||
|
|
||||||
if verified_user_id:
|
if verified_user_id:
|
||||||
if account:
|
if account:
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from lnbits.core.crud.payments import (
|
|||||||
from lnbits.core.models.misc import SimpleStatus
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
from lnbits.core.models.payments import CreateInvoice
|
from lnbits.core.models.payments import CreateInvoice
|
||||||
from lnbits.core.services.fiat_providers import (
|
from lnbits.core.services.fiat_providers import (
|
||||||
check_fiat_status,
|
|
||||||
check_stripe_signature,
|
check_stripe_signature,
|
||||||
verify_paypal_webhook,
|
verify_paypal_webhook,
|
||||||
)
|
)
|
||||||
@@ -24,7 +23,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")
|
||||||
@@ -88,7 +87,7 @@ async def _handle_stripe_intent_session_completed(event: dict):
|
|||||||
if not payment:
|
if not payment:
|
||||||
logger.warning(f"No payment found for hash: '{payment_hash}'.")
|
logger.warning(f"No payment found for hash: '{payment_hash}'.")
|
||||||
return
|
return
|
||||||
await check_fiat_status(payment)
|
await payment.check_fiat_status()
|
||||||
|
|
||||||
|
|
||||||
async def _handle_stripe_checkout_session_completed(event: dict):
|
async def _handle_stripe_checkout_session_completed(event: dict):
|
||||||
@@ -111,7 +110,7 @@ async def _handle_stripe_checkout_session_completed(event: dict):
|
|||||||
payment = await get_standalone_payment(payment_hash)
|
payment = await get_standalone_payment(payment_hash)
|
||||||
if not payment:
|
if not payment:
|
||||||
raise ValueError(f"No payment found for hash: '{payment_hash}'.")
|
raise ValueError(f"No payment found for hash: '{payment_hash}'.")
|
||||||
await check_fiat_status(payment)
|
await payment.check_fiat_status()
|
||||||
|
|
||||||
|
|
||||||
async def _handle_stripe_subscription_invoice_paid(event: dict):
|
async def _handle_stripe_subscription_invoice_paid(event: dict):
|
||||||
@@ -156,7 +155,7 @@ async def _handle_stripe_subscription_invoice_paid(event: dict):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
await check_fiat_status(payment)
|
await payment.check_fiat_status()
|
||||||
|
|
||||||
|
|
||||||
async def _get_stripe_subscription_payment_options(
|
async def _get_stripe_subscription_payment_options(
|
||||||
@@ -181,10 +180,8 @@ async def _get_stripe_subscription_payment_options(
|
|||||||
|
|
||||||
|
|
||||||
async def handle_paypal_event(event: dict):
|
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)
|
||||||
@@ -195,20 +192,23 @@ async def handle_paypal_event(event: dict):
|
|||||||
if not payment:
|
if not payment:
|
||||||
logger.warning(f"No payment found for hash: '{payment_hash}'.")
|
logger.warning(f"No payment found for hash: '{payment_hash}'.")
|
||||||
return
|
return
|
||||||
await check_fiat_status(payment)
|
await payment.check_fiat_status()
|
||||||
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 +216,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": {
|
||||||
@@ -243,7 +247,7 @@ async def _handle_paypal_subscription_payment(resource: dict):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
await check_fiat_status(payment)
|
await payment.check_fiat_status()
|
||||||
|
|
||||||
|
|
||||||
def _paypal_extract_payment_hash(resource: dict) -> str | None:
|
def _paypal_extract_payment_hash(resource: dict) -> str | None:
|
||||||
@@ -254,29 +258,3 @@ def _paypal_extract_payment_hash(resource: dict) -> str | None:
|
|||||||
if pu.get("custom_id"):
|
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()
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from lnbits.core.crud import (
|
|||||||
update_admin_settings,
|
update_admin_settings,
|
||||||
update_wallet,
|
update_wallet,
|
||||||
)
|
)
|
||||||
from lnbits.core.crud.users import clear_user_id_cache, get_account, update_account
|
|
||||||
from lnbits.core.crud.wallets import delete_wallet_by_id
|
from lnbits.core.crud.wallets import delete_wallet_by_id
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
AccountFilters,
|
AccountFilters,
|
||||||
@@ -74,7 +73,7 @@ async def api_get_users(
|
|||||||
summary="Get user by Id",
|
summary="Get user by Id",
|
||||||
)
|
)
|
||||||
async def api_get_user(user_id: str) -> User:
|
async def api_get_user(user_id: str) -> User:
|
||||||
user = await get_user(user_id, active_only=False)
|
user = await get_user(user_id)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
|
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
|
||||||
return user
|
return user
|
||||||
@@ -198,7 +197,7 @@ async def api_users_reset_password(user_id: str) -> str:
|
|||||||
return f"reset_key_{reset_key_b64}"
|
return f"reset_key_{reset_key_b64}"
|
||||||
|
|
||||||
|
|
||||||
@users_router.put(
|
@users_router.get(
|
||||||
"/user/{user_id}/admin",
|
"/user/{user_id}/admin",
|
||||||
dependencies=[Depends(check_super_user)],
|
dependencies=[Depends(check_super_user)],
|
||||||
name="Give or revoke admin permsisions to a user",
|
name="Give or revoke admin permsisions to a user",
|
||||||
@@ -221,43 +220,6 @@ async def api_users_toggle_admin(user_id: str) -> SimpleStatus:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@users_router.put(
|
|
||||||
"/user/{user_id}/activate",
|
|
||||||
name="Activate or deactivate a user",
|
|
||||||
)
|
|
||||||
async def api_users_toggle_activated(
|
|
||||||
user_id: str, admin_account: Account = Depends(check_admin)
|
|
||||||
) -> SimpleStatus:
|
|
||||||
if user_id == settings.super_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail="Cannot deactivate super user.",
|
|
||||||
)
|
|
||||||
if user_id == admin_account.id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail="You cannot deactivate yourself.",
|
|
||||||
)
|
|
||||||
|
|
||||||
if settings.is_admin_user(user_id):
|
|
||||||
settings.lnbits_admin_users.remove(user_id)
|
|
||||||
|
|
||||||
user_account = await get_account(user_id, active_only=False)
|
|
||||||
if not user_account:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
|
||||||
detail="User not found.",
|
|
||||||
)
|
|
||||||
user_account.activated = not user_account.activated
|
|
||||||
await update_account(user_account)
|
|
||||||
await clear_user_id_cache(user_id)
|
|
||||||
|
|
||||||
return SimpleStatus(
|
|
||||||
success=True,
|
|
||||||
message=f"User {'activated' if user_account.activated else 'deactivated'}.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@users_router.get("/user/{user_id}/wallet", name="Get wallets for user")
|
@users_router.get("/user/{user_id}/wallet", name="Get wallets for user")
|
||||||
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
|
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
|
||||||
return await get_wallets(user_id, deleted=None)
|
return await get_wallets(user_id, deleted=None)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from fastapi import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from lnbits.core.crud.wallets import (
|
from lnbits.core.crud.wallets import (
|
||||||
clear_wallet_cache,
|
|
||||||
create_wallet,
|
create_wallet,
|
||||||
get_wallets_paginated,
|
get_wallets_paginated,
|
||||||
)
|
)
|
||||||
@@ -38,6 +37,7 @@ from lnbits.decorators import (
|
|||||||
require_invoice_key,
|
require_invoice_key,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import generate_filter_params_openapi
|
from lnbits.helpers import generate_filter_params_openapi
|
||||||
|
from lnbits.utils.cache import cache
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
delete_wallet,
|
delete_wallet,
|
||||||
@@ -134,7 +134,9 @@ async def api_reset_wallet_keys(
|
|||||||
if not wallet or wallet.user != account_id.id:
|
if not wallet or wallet.user != account_id.id:
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
||||||
|
|
||||||
clear_wallet_cache(wallet)
|
cache.pop(f"auth:wallet:{wallet.id}")
|
||||||
|
cache.pop(f"auth:x-api-key:{wallet.adminkey}")
|
||||||
|
cache.pop(f"auth:x-api-key:{wallet.inkey}")
|
||||||
|
|
||||||
wallet.adminkey = uuid4().hex
|
wallet.adminkey = uuid4().hex
|
||||||
wallet.inkey = uuid4().hex
|
wallet.inkey = uuid4().hex
|
||||||
|
|||||||
+42
-9
@@ -12,6 +12,7 @@ from typing import Any, Generic, Literal, TypeVar, get_origin
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel, ValidationError, root_validator
|
from pydantic import BaseModel, ValidationError, root_validator
|
||||||
|
from sqlalchemy import event
|
||||||
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
|
||||||
from sqlalchemy.sql import text
|
from sqlalchemy.sql import text
|
||||||
|
|
||||||
@@ -55,6 +56,14 @@ def compat_timestamp_placeholder(key: str):
|
|||||||
return f":{key}"
|
return f":{key}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_placeholder(model: Any, field: str) -> str:
|
||||||
|
type_ = model.__fields__[field].type_
|
||||||
|
if type_ == datetime:
|
||||||
|
return compat_timestamp_placeholder(field)
|
||||||
|
else:
|
||||||
|
return f":{field}"
|
||||||
|
|
||||||
|
|
||||||
class Compat:
|
class Compat:
|
||||||
type: str | None = "<inherited>"
|
type: str | None = "<inherited>"
|
||||||
schema: str | None = "<inherited>"
|
schema: str | None = "<inherited>"
|
||||||
@@ -317,7 +326,31 @@ class Database(Compat):
|
|||||||
self.engine: AsyncEngine = create_async_engine(
|
self.engine: AsyncEngine = create_async_engine(
|
||||||
database_uri, echo=settings.debug_database
|
database_uri, echo=settings.debug_database
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.type in {POSTGRES, COCKROACH}:
|
||||||
|
|
||||||
|
@event.listens_for(self.engine.sync_engine, "connect")
|
||||||
|
def register_custom_types(dbapi_connection, *_):
|
||||||
|
def _parse_date(value) -> datetime:
|
||||||
|
if value is None:
|
||||||
|
value = "1970-01-01 00:00:00"
|
||||||
|
f = "%Y-%m-%d %H:%M:%S.%f"
|
||||||
|
if "." not in value:
|
||||||
|
f = "%Y-%m-%d %H:%M:%S"
|
||||||
|
# Parse and add UTC timezone info
|
||||||
|
return datetime.strptime(value, f).replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
dbapi_connection.run_async(
|
||||||
|
lambda connection: connection.set_type_codec(
|
||||||
|
"TIMESTAMP",
|
||||||
|
encoder=datetime,
|
||||||
|
decoder=_parse_date,
|
||||||
|
schema="pg_catalog",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self.lock = asyncio.Lock()
|
self.lock = asyncio.Lock()
|
||||||
|
|
||||||
logger.trace(f"database {self.type} added for {self.name}")
|
logger.trace(f"database {self.type} added for {self.name}")
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -630,7 +663,7 @@ def insert_query(table_name: str, model: BaseModel) -> str:
|
|||||||
placeholders = []
|
placeholders = []
|
||||||
keys = model_to_dict(model).keys()
|
keys = model_to_dict(model).keys()
|
||||||
for field in keys:
|
for field in keys:
|
||||||
placeholders.append(f":{field}")
|
placeholders.append(get_placeholder(model, field))
|
||||||
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
||||||
fields = ", ".join([f'"{key}"' for key in keys])
|
fields = ", ".join([f'"{key}"' for key in keys])
|
||||||
values = ", ".join(placeholders)
|
values = ", ".join(placeholders)
|
||||||
@@ -648,8 +681,9 @@ def update_query(
|
|||||||
"""
|
"""
|
||||||
fields = []
|
fields = []
|
||||||
for field in model_to_dict(model).keys():
|
for field in model_to_dict(model).keys():
|
||||||
|
placeholder = get_placeholder(model, field)
|
||||||
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
||||||
fields.append(f'"{field}" = :{field}')
|
fields.append(f'"{field}" = {placeholder}')
|
||||||
query = ", ".join(fields)
|
query = ", ".join(fields)
|
||||||
return f"UPDATE {table_name} SET {query} {where}" # noqa: S608
|
return f"UPDATE {table_name} SET {query} {where}" # noqa: S608
|
||||||
|
|
||||||
@@ -667,12 +701,7 @@ def model_to_dict(model: BaseModel) -> dict:
|
|||||||
if model.__fields__[key].field_info.extra.get("no_database", False):
|
if model.__fields__[key].field_info.extra.get("no_database", False):
|
||||||
continue
|
continue
|
||||||
if isinstance(value, datetime):
|
if isinstance(value, datetime):
|
||||||
if DB_TYPE == SQLITE:
|
_dict[key] = value.timestamp()
|
||||||
_dict[key] = value.timestamp()
|
|
||||||
else:
|
|
||||||
# remove tz. postgres and cockroach TIMESTAMP is not tz aware
|
|
||||||
# so it will throw if we dont remove the UTC.
|
|
||||||
_dict[key] = value.replace(tzinfo=None)
|
|
||||||
continue
|
continue
|
||||||
if (
|
if (
|
||||||
type(type_) is type(BaseModel)
|
type(type_) is type(BaseModel)
|
||||||
@@ -728,7 +757,11 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel: # noqa: C901
|
|||||||
if DB_TYPE == SQLITE:
|
if DB_TYPE == SQLITE:
|
||||||
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
|
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
|
||||||
else:
|
else:
|
||||||
_dict[key] = value.replace(tzinfo=timezone.utc)
|
# Ensure PostgreSQL datetime values have timezone info
|
||||||
|
if isinstance(value, datetime) and value.tzinfo is None:
|
||||||
|
_dict[key] = value.replace(tzinfo=timezone.utc)
|
||||||
|
else:
|
||||||
|
_dict[key] = value
|
||||||
continue
|
continue
|
||||||
if issubclass(type_, BaseModel):
|
if issubclass(type_, BaseModel):
|
||||||
_dict[key] = dict_to_submodel(type_, value)
|
_dict[key] = dict_to_submodel(type_, value)
|
||||||
|
|||||||
@@ -261,7 +261,6 @@ async def check_account_id_exists(
|
|||||||
account_id,
|
account_id,
|
||||||
expiry=settings.auth_authentication_cache_minutes * 60,
|
expiry=settings.auth_authentication_cache_minutes * 60,
|
||||||
)
|
)
|
||||||
cache.set(f"auth:user:cache_key:{sha256s(account.id)}", cache_key)
|
|
||||||
|
|
||||||
return account_id
|
return account_id
|
||||||
|
|
||||||
|
|||||||
+18
-63
@@ -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]
|
||||||
|
|||||||
+51
-8
@@ -70,11 +70,57 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||||||
t.env.globals["static_url_for"] = static_url_for
|
t.env.globals["static_url_for"] = static_url_for
|
||||||
t.env.globals["normalize_path"] = normalize_path
|
t.env.globals["normalize_path"] = normalize_path
|
||||||
|
|
||||||
# used in base.html
|
window_settings = {
|
||||||
t.env.globals["SITE_TITLE"] = settings.lnbits_site_title
|
"AD_SPACE": settings.lnbits_ad_space,
|
||||||
t.env.globals["LNBITS_APPLE_TOUCH_ICON"] = settings.lnbits_apple_touch_icon
|
"AD_SPACE_ENABLED": settings.lnbits_ad_space_enabled,
|
||||||
t.env.globals["SETTINGS"] = settings.to_public().dict(by_alias=True)
|
"AD_SPACE_TITLE": settings.lnbits_ad_space_title,
|
||||||
t.env.globals["CURRENCIES"] = list(currencies.keys())
|
"EXTENSIONS": list(settings.lnbits_installed_extensions_ids),
|
||||||
|
"HIDE_API": settings.lnbits_hide_api,
|
||||||
|
"SITE_TITLE": settings.lnbits_site_title,
|
||||||
|
"SITE_TAGLINE": settings.lnbits_site_tagline,
|
||||||
|
"SITE_DESCRIPTION": settings.lnbits_site_description,
|
||||||
|
"LNBITS_ADMIN_UI": settings.lnbits_admin_ui,
|
||||||
|
"LNBITS_AUDIT_ENABLED": settings.lnbits_audit_enabled,
|
||||||
|
"LNBITS_AUTH_METHODS": settings.auth_allowed_methods,
|
||||||
|
"LNBITS_AUTH_KEYCLOAK_ORG": settings.keycloak_client_custom_org,
|
||||||
|
"LNBITS_AUTH_KEYCLOAK_ICON": settings.keycloak_client_custom_icon,
|
||||||
|
"LNBITS_CUSTOM_IMAGE": settings.lnbits_custom_image,
|
||||||
|
"LNBITS_CUSTOM_BADGE": settings.lnbits_custom_badge,
|
||||||
|
"LNBITS_CUSTOM_BADGE_COLOR": settings.lnbits_custom_badge_color,
|
||||||
|
"LNBITS_EXTENSIONS_DEACTIVATE_ALL": settings.lnbits_extensions_deactivate_all,
|
||||||
|
"LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed,
|
||||||
|
"LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager,
|
||||||
|
"LNBITS_NODE_UI_AVAILABLE": settings.has_nodemanager,
|
||||||
|
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
|
||||||
|
"LNBITS_APPLE_TOUCH_ICON": settings.lnbits_apple_touch_icon,
|
||||||
|
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
|
||||||
|
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
|
||||||
|
"LNBITS_SERVICE_FEE_WALLET": settings.lnbits_service_fee_wallet,
|
||||||
|
"LNBITS_SHOW_HOME_PAGE_ELEMENTS": settings.lnbits_show_home_page_elements,
|
||||||
|
"LNBITS_THEME_OPTIONS": settings.lnbits_theme_options,
|
||||||
|
"LNBITS_VERSION": settings.version,
|
||||||
|
"USE_CUSTOM_LOGO": settings.lnbits_custom_logo,
|
||||||
|
"LNBITS_DEFAULT_REACTION": settings.lnbits_default_reaction,
|
||||||
|
"LNBITS_DEFAULT_THEME": settings.lnbits_default_theme,
|
||||||
|
"LNBITS_DEFAULT_BORDER": settings.lnbits_default_border,
|
||||||
|
"LNBITS_DEFAULT_GRADIENT": settings.lnbits_default_gradient,
|
||||||
|
"LNBITS_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
||||||
|
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
||||||
|
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
||||||
|
"LNBITS_EXTENSIONS_REVIEWS_URL": settings.lnbits_extensions_reviews_url,
|
||||||
|
"LNBITS_DENOMINATION": settings.lnbits_denomination,
|
||||||
|
"has_holdinvoice": settings.has_holdinvoice,
|
||||||
|
"LNBITS_NOSTR_CONFIGURED": settings.is_nostr_notifications_configured(),
|
||||||
|
"LNBITS_TELEGRAM_CONFIGURED": settings.is_telegram_notifications_configured(),
|
||||||
|
"LNBITS_EXT_BUILDER": settings.lnbits_extensions_builder_activate_non_admins,
|
||||||
|
"LNBITS_CURRENCIES": list(currencies.keys()),
|
||||||
|
"LNBITS_ALLOWED_CURRENCIES": settings.lnbits_allowed_currencies,
|
||||||
|
"CACHE_KEY": settings.server_startup_time,
|
||||||
|
}
|
||||||
|
|
||||||
|
t.env.globals["WINDOW_SETTINGS"] = window_settings
|
||||||
|
for key, value in window_settings.items():
|
||||||
|
t.env.globals[key] = value
|
||||||
|
|
||||||
if settings.bundle_assets:
|
if settings.bundle_assets:
|
||||||
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
|
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
|
||||||
@@ -88,9 +134,6 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||||||
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
|
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
|
||||||
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
|
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
|
||||||
|
|
||||||
# backwards compatibility for extensions (tpos)
|
|
||||||
t.env.globals["LNBITS_DENOMINATION"] = settings.lnbits_denomination
|
|
||||||
|
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+4
-147
@@ -41,14 +41,6 @@ class UsersSettings(LNbitsSettings):
|
|||||||
lnbits_admin_users: list[str] = Field(default=[])
|
lnbits_admin_users: list[str] = Field(default=[])
|
||||||
lnbits_allowed_users: list[str] = Field(default=[])
|
lnbits_allowed_users: list[str] = Field(default=[])
|
||||||
lnbits_allow_new_accounts: bool = Field(default=True)
|
lnbits_allow_new_accounts: bool = Field(default=True)
|
||||||
lnbits_require_user_activation: bool = Field(default=False)
|
|
||||||
|
|
||||||
lnbits_user_activation_by_email: bool = Field(default=False)
|
|
||||||
lnbits_user_activation_by_payment: bool = Field(default=False)
|
|
||||||
lnbits_user_activation_by_invitation_code: bool = Field(default=False)
|
|
||||||
|
|
||||||
lnbits_register_reusable_activation_code: str = Field(default="")
|
|
||||||
lnbits_register_one_time_activation_codes: list[str] = Field(default=[])
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def new_accounts_allowed(self) -> bool:
|
def new_accounts_allowed(self) -> bool:
|
||||||
@@ -260,11 +252,6 @@ class ThemesSettings(LNbitsSettings):
|
|||||||
)
|
)
|
||||||
lnbits_show_home_page_elements: bool = Field(default=True)
|
lnbits_show_home_page_elements: bool = Field(default=True)
|
||||||
lnbits_default_wallet_name: str = Field(default="LNbits wallet")
|
lnbits_default_wallet_name: str = Field(default="LNbits wallet")
|
||||||
|
|
||||||
lnbits_wallet_featured_button_label: str | None = Field(default=None)
|
|
||||||
lnbits_wallet_featured_button_url: str | None = Field(default=None)
|
|
||||||
lnbits_wallet_featured_button_icon: str | None = Field(default=None)
|
|
||||||
|
|
||||||
lnbits_custom_badge: str | None = Field(default=None)
|
lnbits_custom_badge: str | None = Field(default=None)
|
||||||
lnbits_custom_badge_color: str = Field(default="warning")
|
lnbits_custom_badge_color: str = Field(default="warning")
|
||||||
lnbits_theme_options: list[str] = Field(
|
lnbits_theme_options: list[str] = Field(
|
||||||
@@ -296,10 +283,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):
|
||||||
@@ -607,8 +590,10 @@ class SparkFundingSource(LNbitsSettings):
|
|||||||
|
|
||||||
|
|
||||||
class SparkL2FundingSource(LNbitsSettings):
|
class SparkL2FundingSource(LNbitsSettings):
|
||||||
|
spark_l2_mnemonic: str | None = Field(default=None)
|
||||||
spark_l2_network: str = Field(default="MAINNET")
|
spark_l2_network: str = Field(default="MAINNET")
|
||||||
spark_l2_external_endpoint: str | None = Field(default="http://localhost:8765")
|
spark_l2_internal_sidecar_version: str | None = Field(default="0.1.1")
|
||||||
|
spark_l2_external_endpoint: str | None = Field(default=None)
|
||||||
spark_l2_external_api_key: str | None = Field(default=None)
|
spark_l2_external_api_key: str | None = Field(default=None)
|
||||||
spark_l2_pay_wait_ms: int = Field(default=4000, ge=0)
|
spark_l2_pay_wait_ms: int = Field(default=4000, ge=0)
|
||||||
spark_l2_pay_poll_ms: int = Field(default=500, ge=0)
|
spark_l2_pay_poll_ms: int = Field(default=500, ge=0)
|
||||||
@@ -1005,7 +990,6 @@ class EnvSettings(LNbitsSettings):
|
|||||||
enable_log_to_file: bool = Field(default=True)
|
enable_log_to_file: bool = Field(default=True)
|
||||||
log_rotation: str = Field(default="100 MB")
|
log_rotation: str = Field(default="100 MB")
|
||||||
log_retention: str = Field(default="3 months")
|
log_retention: str = Field(default="3 months")
|
||||||
first_install_token: str | None = Field(default=None)
|
|
||||||
|
|
||||||
cleanup_wallets_days: int = Field(default=90, ge=0)
|
cleanup_wallets_days: int = Field(default=90, ge=0)
|
||||||
funding_source_max_retries: int = Field(default=4, ge=0)
|
funding_source_max_retries: int = Field(default=4, ge=0)
|
||||||
@@ -1049,7 +1033,7 @@ class SuperUserSettings(LNbitsSettings):
|
|||||||
"FakeWallet",
|
"FakeWallet",
|
||||||
"LNPayWallet",
|
"LNPayWallet",
|
||||||
"LNbitsWallet",
|
"LNbitsWallet",
|
||||||
"SparkL2Wallet",
|
"LightsparkSparkWallet",
|
||||||
"LnTipsWallet",
|
"LnTipsWallet",
|
||||||
"LndRestWallet",
|
"LndRestWallet",
|
||||||
"LndWallet",
|
"LndWallet",
|
||||||
@@ -1155,133 +1139,6 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
|
|||||||
and ext_id in self.lnbits_all_extensions_ids
|
and ext_id in self.lnbits_all_extensions_ids
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_public(self) -> PublicSettings:
|
|
||||||
return PublicSettings.from_settings(self)
|
|
||||||
|
|
||||||
|
|
||||||
class PublicSettings(BaseModel):
|
|
||||||
"""
|
|
||||||
PublicSettings is used for templating and public information.
|
|
||||||
WARNING: do not expose private information, PublicSettings is exposed publicly.
|
|
||||||
"""
|
|
||||||
|
|
||||||
allow_register: bool = Field(alias="allowRegister")
|
|
||||||
qr_logo: str = Field(alias="qrLogo")
|
|
||||||
site_title: str = Field(alias="siteTitle")
|
|
||||||
site_tagline: str = Field(alias="siteTagline")
|
|
||||||
site_description: str | None = Field(alias="siteDescription")
|
|
||||||
auth_methods: list[str] = Field(alias="authMethods")
|
|
||||||
keycloak_org: str | None = Field(alias="keycloakOrg")
|
|
||||||
keycloak_icon: str | None = Field(alias="keycloakIcon")
|
|
||||||
has_holdinvoice: bool = Field(alias="hasHoldinvoice")
|
|
||||||
has_nodemanager: bool = Field(alias="hasNodemanager")
|
|
||||||
show_nodemanager: bool = Field(alias="showNodemanager")
|
|
||||||
custom_logo: str | None = Field(alias="customLogo")
|
|
||||||
show_voidwallet: bool = Field(alias="showVoidwallet")
|
|
||||||
version: str = Field(alias="version")
|
|
||||||
show_home_page_elements: bool = Field(alias="showHomePageElements")
|
|
||||||
hide_api: bool = Field(alias="hideApi")
|
|
||||||
cache_key: str = Field(alias="cacheKey")
|
|
||||||
webpush_pubkey: str | None = Field(alias="webpushPubkey")
|
|
||||||
show_extensions: bool = Field(alias="showExtensions")
|
|
||||||
show_audit: bool = Field(alias="showAudit")
|
|
||||||
show_admin: bool = Field(alias="showAdmin")
|
|
||||||
ad_space: list[list[str]] = Field(alias="adSpace")
|
|
||||||
ad_space_title: str = Field(alias="adSpaceTitle")
|
|
||||||
show_ad_space: bool = Field(alias="showAdSpace")
|
|
||||||
custom_image: str | None = Field(alias="customImage")
|
|
||||||
custom_badge: str | None = Field(alias="customBadge")
|
|
||||||
custom_badge_color: str | None = Field(alias="customBadgeColor")
|
|
||||||
service_fee: float = Field(alias="serviceFee")
|
|
||||||
service_fee_max: int = Field(alias="serviceFeeMax")
|
|
||||||
service_fee_wallet: str | None = Field(alias="serviceFeeWallet")
|
|
||||||
theme_options: list[str] = Field(alias="themeOptions")
|
|
||||||
default_reaction: str = Field(alias="defaultReaction")
|
|
||||||
default_theme: str = Field(alias="defaultTheme")
|
|
||||||
default_border: str = Field(alias="defaultBorder")
|
|
||||||
default_bgimage: str | None = Field(alias="defaultBgimage")
|
|
||||||
default_gradient: bool = Field(alias="defaultGradient")
|
|
||||||
default_dark: bool = Field(alias="defaultDark")
|
|
||||||
default_card_rounded: bool = Field(alias="defaultCardRounded")
|
|
||||||
default_card_gradient: bool = Field(alias="defaultCardGradient")
|
|
||||||
default_card_shadow: bool = Field(alias="defaultCardShadow")
|
|
||||||
denomination: str | None = Field()
|
|
||||||
extensions: list[str] = Field()
|
|
||||||
allowed_currencies: list[str] = Field(alias="allowedCurrencies")
|
|
||||||
extensions_reviews_url: str = Field(alias="extensionsReviewsUrl")
|
|
||||||
ext_builder: bool = Field(alias="extBuilder")
|
|
||||||
nostr_configured: bool = Field(alias="nostrConfigured")
|
|
||||||
telegram_configured: bool = Field(alias="telegramConfigured")
|
|
||||||
wallet_featured_button_label: str | None = Field(alias="walletFeaturedButtonLabel")
|
|
||||||
wallet_featured_button_url: str | None = Field(alias="walletFeaturedButtonUrl")
|
|
||||||
wallet_featured_button_icon: str | None = Field(alias="walletFeaturedButtonIcon")
|
|
||||||
lnbits_user_activation_by_email: bool = Field(alias="userActivationByEmail")
|
|
||||||
lnbits_user_activation_by_payment: bool = Field(alias="userActivationByPayment")
|
|
||||||
lnbits_user_activation_by_invitation_code: bool = Field(
|
|
||||||
alias="userActivationByInvitationCode"
|
|
||||||
)
|
|
||||||
has_first_install_token: bool = Field(alias="hasFirstInstallToken")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_settings(cls, settings: Settings):
|
|
||||||
ads = [x.split(";") for x in settings.lnbits_ad_space.split(",")]
|
|
||||||
return cls(
|
|
||||||
adSpace=ads,
|
|
||||||
adSpaceTitle=settings.lnbits_ad_space_title,
|
|
||||||
showAdSpace=settings.lnbits_ad_space_enabled and len(ads) > 0,
|
|
||||||
allowRegister=settings.new_accounts_allowed,
|
|
||||||
qrLogo=settings.lnbits_qr_logo,
|
|
||||||
siteDescription=settings.lnbits_site_description,
|
|
||||||
siteTitle=settings.lnbits_site_title,
|
|
||||||
siteTagline=settings.lnbits_site_tagline,
|
|
||||||
authMethods=settings.auth_allowed_methods,
|
|
||||||
keycloakOrg=settings.keycloak_client_custom_org,
|
|
||||||
keycloakIcon=settings.keycloak_client_custom_icon,
|
|
||||||
hasHoldinvoice=settings.has_holdinvoice,
|
|
||||||
hasNodemanager=settings.has_nodemanager,
|
|
||||||
showNodemanager=settings.lnbits_node_ui and settings.has_nodemanager,
|
|
||||||
customLogo=settings.lnbits_custom_logo,
|
|
||||||
showVoidwallet=settings.lnbits_backend_wallet_class == "VoidWallet",
|
|
||||||
version=settings.version,
|
|
||||||
showHomePageElements=settings.lnbits_show_home_page_elements,
|
|
||||||
hideApi=settings.lnbits_hide_api,
|
|
||||||
cacheKey=str(settings.server_startup_time),
|
|
||||||
webpushPubkey=settings.lnbits_webpush_pubkey,
|
|
||||||
showExtensions=not settings.lnbits_extensions_deactivate_all,
|
|
||||||
showAudit=settings.lnbits_audit_enabled,
|
|
||||||
showAdmin=settings.lnbits_admin_ui,
|
|
||||||
customImage=settings.lnbits_custom_image,
|
|
||||||
customBadge=settings.lnbits_custom_badge,
|
|
||||||
customBadgeColor=settings.lnbits_custom_badge_color,
|
|
||||||
serviceFee=settings.lnbits_service_fee,
|
|
||||||
serviceFeeMax=settings.lnbits_service_fee_max,
|
|
||||||
serviceFeeWallet=settings.lnbits_service_fee_wallet,
|
|
||||||
themeOptions=settings.lnbits_theme_options,
|
|
||||||
defaultReaction=settings.lnbits_default_reaction,
|
|
||||||
defaultTheme=settings.lnbits_default_theme,
|
|
||||||
defaultBorder=settings.lnbits_default_border,
|
|
||||||
defaultGradient=settings.lnbits_default_gradient,
|
|
||||||
defaultBgimage=settings.lnbits_default_bgimage,
|
|
||||||
defaultDark=settings.lnbits_default_dark,
|
|
||||||
defaultCardRounded=settings.lnbits_default_card_rounded,
|
|
||||||
defaultCardGradient=settings.lnbits_default_card_gradient,
|
|
||||||
defaultCardShadow=settings.lnbits_default_card_shadow,
|
|
||||||
denomination=settings.lnbits_denomination,
|
|
||||||
extensions=list(settings.lnbits_installed_extensions_ids),
|
|
||||||
allowedCurrencies=settings.lnbits_allowed_currencies,
|
|
||||||
extensionsReviewsUrl=settings.lnbits_extensions_reviews_url,
|
|
||||||
extBuilder=settings.lnbits_extensions_builder_activate_non_admins,
|
|
||||||
nostrConfigured=settings.is_nostr_notifications_configured(),
|
|
||||||
telegramConfigured=settings.is_telegram_notifications_configured(),
|
|
||||||
walletFeaturedButtonLabel=settings.lnbits_wallet_featured_button_label,
|
|
||||||
walletFeaturedButtonUrl=settings.lnbits_wallet_featured_button_url,
|
|
||||||
walletFeaturedButtonIcon=settings.lnbits_wallet_featured_button_icon,
|
|
||||||
userActivationByEmail=settings.lnbits_user_activation_by_email,
|
|
||||||
userActivationByPayment=settings.lnbits_user_activation_by_payment,
|
|
||||||
userActivationByInvitationCode=settings.lnbits_user_activation_by_invitation_code,
|
|
||||||
hasFirstInstallToken=settings.first_install_token is not None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SuperSettings(EditableSettings):
|
class SuperSettings(EditableSettings):
|
||||||
super_user: str
|
super_user: str
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+11
-13
File diff suppressed because one or more lines are too long
@@ -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;
|
||||||
|
|||||||
@@ -171,7 +171,6 @@ window.localisation.en = {
|
|||||||
drop_db: 'Remove Data',
|
drop_db: 'Remove Data',
|
||||||
enable: 'Enable',
|
enable: 'Enable',
|
||||||
enabled: 'Enabled',
|
enabled: 'Enabled',
|
||||||
disabled: 'Disabled',
|
|
||||||
pay_to_enable: 'Pay To Enable',
|
pay_to_enable: 'Pay To Enable',
|
||||||
enable_extension_details: 'Enable extension for current user',
|
enable_extension_details: 'Enable extension for current user',
|
||||||
disable: 'Disable',
|
disable: 'Disable',
|
||||||
@@ -179,8 +178,6 @@ window.localisation.en = {
|
|||||||
installed: 'Installed',
|
installed: 'Installed',
|
||||||
activated: 'Activated',
|
activated: 'Activated',
|
||||||
deactivated: 'Deactivated',
|
deactivated: 'Deactivated',
|
||||||
activate: 'Activate',
|
|
||||||
deactivate: 'Deactivate',
|
|
||||||
release_notes: 'Release Notes',
|
release_notes: 'Release Notes',
|
||||||
activate_extension_details: 'Make extension available/unavailable for users',
|
activate_extension_details: 'Make extension available/unavailable for users',
|
||||||
featured: 'Featured',
|
featured: 'Featured',
|
||||||
@@ -189,8 +186,6 @@ window.localisation.en = {
|
|||||||
only_admins_can_create_extensions:
|
only_admins_can_create_extensions:
|
||||||
'Only admin accounts can create extensions',
|
'Only admin accounts can create extensions',
|
||||||
admin_only: 'Admin Only',
|
admin_only: 'Admin Only',
|
||||||
make_user_admin: 'Make User Admin',
|
|
||||||
revoke_admin: 'Revoke Admin',
|
|
||||||
new_version: 'New Version',
|
new_version: 'New Version',
|
||||||
reviews_url: 'Reviews URL',
|
reviews_url: 'Reviews URL',
|
||||||
reviews_url_label: 'Reviews server URL',
|
reviews_url_label: 'Reviews server URL',
|
||||||
@@ -285,7 +280,7 @@ window.localisation.en = {
|
|||||||
'Nip5 identifier to send notifications to',
|
'Nip5 identifier to send notifications to',
|
||||||
notifications_nostr_identifiers: 'Nostr Identifiers',
|
notifications_nostr_identifiers: 'Nostr Identifiers',
|
||||||
notifications_nostr_identifiers_desc:
|
notifications_nostr_identifiers_desc:
|
||||||
'List of identifiers to send notifications to.',
|
'List of identifiers to send notifications to',
|
||||||
|
|
||||||
notifications_telegram_config: 'Telegram Configuration',
|
notifications_telegram_config: 'Telegram Configuration',
|
||||||
notifications_enable_telegram: 'Enable Telegram',
|
notifications_enable_telegram: 'Enable Telegram',
|
||||||
@@ -465,12 +460,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 +518,6 @@ 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',
|
|
||||||
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',
|
||||||
@@ -670,15 +658,6 @@ window.localisation.en = {
|
|||||||
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
|
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
|
||||||
ui_default_wallet_name: 'Default Wallet Name',
|
ui_default_wallet_name: 'Default Wallet Name',
|
||||||
ui_default_theme: 'Default Theme',
|
ui_default_theme: 'Default Theme',
|
||||||
wallet_featured_button_label: 'Wallet Featured Button Label',
|
|
||||||
wallet_featured_button_label_hint:
|
|
||||||
'Show featured button on the wallet homepage',
|
|
||||||
wallet_featured_button_url: 'Featured Button URL',
|
|
||||||
wallet_featured_button_url_hint:
|
|
||||||
'On click the button will open this URL. Leave empty to hide the button.',
|
|
||||||
wallet_featured_button_icon: 'Featured Button Icon',
|
|
||||||
wallet_featured_button_icon_hint:
|
|
||||||
'Icon shown on the featured button (check quasar icons)',
|
|
||||||
lnbits_wallet: 'LNbits wallet',
|
lnbits_wallet: 'LNbits wallet',
|
||||||
denomination: 'Denomination',
|
denomination: 'Denomination',
|
||||||
denomination_hint: 'The name for the FakeWallet token',
|
denomination_hint: 'The name for the FakeWallet token',
|
||||||
@@ -715,25 +694,6 @@ window.localisation.en = {
|
|||||||
allowed_users_label: 'User ID',
|
allowed_users_label: 'User ID',
|
||||||
allow_creation_user: 'Allow creation of new users',
|
allow_creation_user: 'Allow creation of new users',
|
||||||
allow_creation_user_desc: 'Allow creation of new users on the index page',
|
allow_creation_user_desc: 'Allow creation of new users on the index page',
|
||||||
require_user_activation: 'Require user activation',
|
|
||||||
require_user_activation_desc:
|
|
||||||
'New users will be activated only after they pass one of the confirmation methods. Admins can activate users manually from the admin panel.',
|
|
||||||
reusable_activation_code: 'Reusable activation code',
|
|
||||||
reusable_activation_code_label: 'Reusable activation code',
|
|
||||||
reusable_activation_code_hint:
|
|
||||||
'This activation code can be used multiple times by different users.',
|
|
||||||
one_time_activation_code: 'One-time activation codes',
|
|
||||||
one_time_activation_code_label: 'Add activation code',
|
|
||||||
one_time_activation_code_hint:
|
|
||||||
'List of one-time activation codes. Each code can be used only once, then will be reomved from the list.',
|
|
||||||
invitation_code: 'Invitation Code',
|
|
||||||
invitation_code_hint: 'The invitation code that you have received.',
|
|
||||||
email: 'Email',
|
|
||||||
email_confirmation_hint: 'Email address to send the confirmation code to.',
|
|
||||||
nostr_identifier: 'Nostr Identifier',
|
|
||||||
nostr_identifier_hint:
|
|
||||||
'Nostr nip5 identifier or <npub> to send the confirmation code to.',
|
|
||||||
|
|
||||||
new_user_not_allowed: 'Registration is disabled.',
|
new_user_not_allowed: 'Registration is disabled.',
|
||||||
start_user_impersonation: 'Impersonate this user',
|
start_user_impersonation: 'Impersonate this user',
|
||||||
stop_user_impersonation: 'Stop User Impersonation',
|
stop_user_impersonation: 'Stop User Impersonation',
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ window._lnbitsApi = {
|
|||||||
name: name
|
name: name
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
register(username, email, password, password_repeat, invitation_code) {
|
register(username, email, password, password_repeat) {
|
||||||
return axios({
|
return axios({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/v1/auth/register',
|
url: '/api/v1/auth/register',
|
||||||
@@ -74,8 +74,7 @@ window._lnbitsApi = {
|
|||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
password_repeat,
|
password_repeat
|
||||||
invitation_code
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -148,16 +147,13 @@ window._lnbitsApi = {
|
|||||||
name: name,
|
name: name,
|
||||||
wallet_type: walletType,
|
wallet_type: walletType,
|
||||||
...opts
|
...opts
|
||||||
})
|
}).catch(LNbits.utils.notifyApiError)
|
||||||
},
|
},
|
||||||
updateWallet(name, wallet) {
|
updateWallet(name, wallet) {
|
||||||
return this.request('patch', '/api/v1/wallet', wallet.adminkey, {
|
return this.request('patch', '/api/v1/wallet', wallet.adminkey, {
|
||||||
name: name
|
name: name
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateUiCustomization(data = {}) {
|
|
||||||
return this.request('patch', '/api/v1/auth/ui', null, data)
|
|
||||||
},
|
|
||||||
resetWalletKeys(wallet) {
|
resetWalletKeys(wallet) {
|
||||||
return this.request('put', `/api/v1/wallet/reset/${wallet.id}`).then(
|
return this.request('put', `/api/v1/wallet/reset/${wallet.id}`).then(
|
||||||
res => {
|
res => {
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ window.LNbits = {
|
|||||||
fiat_providers: data.fiat_providers || [],
|
fiat_providers: data.fiat_providers || [],
|
||||||
super_user: data.super_user,
|
super_user: data.super_user,
|
||||||
extra: data.extra ?? {},
|
extra: data.extra ?? {},
|
||||||
hasPassword: data.has_password ?? false,
|
hasPassword: data.has_password ?? false
|
||||||
uiCustomization: data.ui_customization || {}
|
|
||||||
}
|
}
|
||||||
const mapWallet = this.wallet
|
const mapWallet = this.wallet
|
||||||
obj.wallets = obj.wallets.map(mapWallet).sort((a, b) => {
|
obj.wallets = obj.wallets.map(mapWallet).sort((a, b) => {
|
||||||
|
|||||||
@@ -35,6 +35,23 @@ window.app.component('lnbits-extension-rating', {
|
|||||||
|
|
||||||
window.app.component('lnbits-manage', {
|
window.app.component('lnbits-manage', {
|
||||||
template: '#lnbits-manage',
|
template: '#lnbits-manage',
|
||||||
|
computed: {
|
||||||
|
showAdmin() {
|
||||||
|
return this.LNBITS_ADMIN_UI
|
||||||
|
},
|
||||||
|
showUsers() {
|
||||||
|
return this.LNBITS_ADMIN_UI
|
||||||
|
},
|
||||||
|
showNode() {
|
||||||
|
return this.LNBITS_NODE_UI
|
||||||
|
},
|
||||||
|
showAudit() {
|
||||||
|
return this.LNBITS_AUDIT_ENABLED
|
||||||
|
},
|
||||||
|
showExtensions() {
|
||||||
|
return this.LNBITS_EXTENSIONS_DEACTIVATE_ALL === false
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
isActive(path) {
|
isActive(path) {
|
||||||
return window.location.pathname === path
|
return window.location.pathname === path
|
||||||
@@ -432,10 +449,8 @@ window.app.component('username-password', {
|
|||||||
username: String,
|
username: String,
|
||||||
password_1: String,
|
password_1: String,
|
||||||
password_2: String,
|
password_2: String,
|
||||||
invitationCode: String,
|
|
||||||
resetKey: String
|
resetKey: String
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
oauth: [
|
oauth: [
|
||||||
@@ -448,10 +463,8 @@ window.app.component('username-password', {
|
|||||||
password: this.password_1,
|
password: this.password_1,
|
||||||
passwordRepeat: this.password_2,
|
passwordRepeat: this.password_2,
|
||||||
reset_key: this.resetKey,
|
reset_key: this.resetKey,
|
||||||
confirmationMethod: 'code',
|
keycloakOrg: LNBITS_AUTH_KEYCLOAK_ORG || 'Keycloak',
|
||||||
confirmationEmail: '',
|
keycloakIcon: LNBITS_AUTH_KEYCLOAK_ICON
|
||||||
confirmationCode: this.invitationCode || '',
|
|
||||||
showConfirmationCode: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -464,7 +477,6 @@ window.app.component('username-password', {
|
|||||||
this.$emit('update:userName', this.username)
|
this.$emit('update:userName', this.username)
|
||||||
this.$emit('update:password_1', this.password)
|
this.$emit('update:password_1', this.password)
|
||||||
this.$emit('update:password_2', this.passwordRepeat)
|
this.$emit('update:password_2', this.passwordRepeat)
|
||||||
this.$emit('update:invitationCode', this.confirmationCode)
|
|
||||||
this.$emit('register')
|
this.$emit('register')
|
||||||
},
|
},
|
||||||
reset() {
|
reset() {
|
||||||
@@ -556,25 +568,6 @@ window.app.component('username-password', {
|
|||||||
computed: {
|
computed: {
|
||||||
showOauth() {
|
showOauth() {
|
||||||
return this.oauth.some(m => this.authMethods.includes(m))
|
return this.oauth.some(m => this.authMethods.includes(m))
|
||||||
},
|
|
||||||
disableRegister() {
|
|
||||||
const usernameOK = !!this.username
|
|
||||||
const passwordOK = !!this.password && this.password.length >= 8
|
|
||||||
const passwordsMatch = this.password === this.passwordRepeat
|
|
||||||
const codeOk =
|
|
||||||
this.confirmationMethodsCount === 0 ||
|
|
||||||
this.confirmationMethod !== 'code' ||
|
|
||||||
this.confirmationCode.length > 0
|
|
||||||
|
|
||||||
return !usernameOK || !passwordOK || !passwordsMatch || !codeOk
|
|
||||||
},
|
|
||||||
confirmationMethodsCount() {
|
|
||||||
const methods = [
|
|
||||||
this.g.settings.userActivationByEmail,
|
|
||||||
this.g.settings.userActivationByPayment,
|
|
||||||
this.g.settings.userActivationByInvitationCode
|
|
||||||
]
|
|
||||||
return methods.filter(Boolean).length
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {}
|
created() {}
|
||||||
|
|||||||
@@ -229,38 +229,46 @@ window.app.component('lnbits-admin-funding-sources', {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'SparkL2Wallet',
|
'LightsparkSparkWallet',
|
||||||
'Spark (L2)',
|
'Spark (L2)',
|
||||||
{
|
{
|
||||||
spark_l2_external_endpoint: {
|
spark_l2_internal_sidecar_version: {
|
||||||
label: 'External Sidecar Endpoint',
|
label: 'Internal Sidecar Version (eg: 0.1.1).',
|
||||||
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.',
|
hint: 'If specified then the sidecar will be downloaded. Alternatively you can specify an External Sidecar endpoint in the Advanced section.',
|
||||||
value: ''
|
value: ''
|
||||||
},
|
},
|
||||||
spark_l2_external_api_key: {
|
spark_l2_mnemonic: {
|
||||||
label: 'External Sidecar API Key',
|
label: 'Mnemonic',
|
||||||
hint: 'API key for authenticating with the external sidecar if it requires authentication.',
|
hint: 'Only required if Interna Sidecar version is specified.'
|
||||||
value: ''
|
|
||||||
},
|
},
|
||||||
spark_l2_network: {
|
spark_l2_network: {
|
||||||
label: 'Network',
|
label: 'Network',
|
||||||
value: 'MAINNET',
|
value: 'MAINNET'
|
||||||
hint: 'The network to use for the Spark wallet.',
|
},
|
||||||
|
|
||||||
|
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: '',
|
||||||
advanced: true
|
advanced: true
|
||||||
},
|
},
|
||||||
|
spark_l2_external_api_key: {
|
||||||
|
label: 'External Sidecar API Key. ',
|
||||||
|
hint: 'API Key for authenticating with the external sidecar if it requires authentication.',
|
||||||
|
value: '',
|
||||||
|
advanced: true
|
||||||
|
},
|
||||||
|
|
||||||
spark_l2_pay_wait_ms: {
|
spark_l2_pay_wait_ms: {
|
||||||
label: 'Payment Wait Time (ms)',
|
label: 'Pay Wait Time (ms)',
|
||||||
hint: 'The time to wait for a payment to be processed before considering it failed.',
|
|
||||||
advanced: true
|
advanced: true
|
||||||
},
|
},
|
||||||
spark_l2_pay_poll_ms: {
|
spark_l2_pay_poll_ms: {
|
||||||
label: 'Payment Poll Time (ms)',
|
label: 'Pay Poll Time (ms)',
|
||||||
hint: 'The time to wait between polling for payment status updates.',
|
|
||||||
advanced: true
|
advanced: true
|
||||||
},
|
},
|
||||||
spark_l2_stream_keepalive_ms: {
|
spark_l2_stream_keepalive_ms: {
|
||||||
label: 'Stream Keepalive Time (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
|
advanced: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ window.app.component('lnbits-admin-users', {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
formAddUser: '',
|
formAddUser: '',
|
||||||
formAddAdmin: '',
|
formAddAdmin: ''
|
||||||
formAddActivationCode: '',
|
|
||||||
showReusableActivationCode: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -33,24 +31,6 @@ window.app.component('lnbits-admin-users', {
|
|||||||
removeAdminUser(user) {
|
removeAdminUser(user) {
|
||||||
let admin_users = this.formData.lnbits_admin_users
|
let admin_users = this.formData.lnbits_admin_users
|
||||||
this.formData.lnbits_admin_users = admin_users.filter(u => u !== user)
|
this.formData.lnbits_admin_users = admin_users.filter(u => u !== user)
|
||||||
},
|
|
||||||
addOneTimeActivationCode() {
|
|
||||||
const code = this.formAddActivationCode
|
|
||||||
const activationCodes =
|
|
||||||
this.formData.lnbits_register_one_time_activation_codes
|
|
||||||
if (code?.length && !activationCodes.includes(code)) {
|
|
||||||
this.formData.lnbits_register_one_time_activation_codes = [
|
|
||||||
...activationCodes,
|
|
||||||
code
|
|
||||||
]
|
|
||||||
this.formAddActivationCode = ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeOneTimeActivationCode(code) {
|
|
||||||
const codes = this.formData.lnbits_register_one_time_activation_codes
|
|
||||||
this.formData.lnbits_register_one_time_activation_codes = codes.filter(
|
|
||||||
u => u !== code
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
window.app.component('lnbits-footer', {
|
window.app.component('lnbits-footer', {
|
||||||
template: '#lnbits-footer',
|
template: '#lnbits-footer',
|
||||||
computed: {
|
computed: {
|
||||||
title() {
|
|
||||||
return `${this.g.settings.siteTitle}, ${this.g.settings.siteTagline}`
|
|
||||||
},
|
|
||||||
version() {
|
version() {
|
||||||
return this.$t('lnbits_version') + ': ' + this.g.settings.version
|
return this.LNBITS_VERSION || 'unknown version'
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return `${this.SITE_TITLE}, ${this.SITE_TAGLINE}`
|
||||||
|
},
|
||||||
|
showFooter() {
|
||||||
|
return this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,48 @@
|
|||||||
window.app.component('lnbits-header', {
|
window.app.component('lnbits-header', {
|
||||||
template: '#lnbits-header',
|
template: '#lnbits-header',
|
||||||
computed: {
|
computed: {
|
||||||
|
hasServiceFeeMax() {
|
||||||
|
return (
|
||||||
|
this.g.user &&
|
||||||
|
this.LNBITS_SERVICE_FEE_MAX &&
|
||||||
|
this.LNBITS_SERVICE_FEE_MAX > 0
|
||||||
|
)
|
||||||
|
},
|
||||||
|
serviceFeeMax() {
|
||||||
|
return this.LNBITS_SERVICE_FEE_MAX || 0
|
||||||
|
},
|
||||||
|
hasServiceFee() {
|
||||||
|
return (
|
||||||
|
this.g.user && this.LNBITS_SERVICE_FEE && this.LNBITS_SERVICE_FEE > 0
|
||||||
|
)
|
||||||
|
},
|
||||||
|
serviceFee() {
|
||||||
|
return this.LNBITS_SERVICE_FEE || 0
|
||||||
|
},
|
||||||
|
hasCustomBadge() {
|
||||||
|
return this.LNBITS_CUSTOM_BADGE && this.LNBITS_CUSTOM_BADGE != ''
|
||||||
|
},
|
||||||
|
customBadge() {
|
||||||
|
return this.LNBITS_CUSTOM_BADGE || ''
|
||||||
|
},
|
||||||
|
customBadgeColor() {
|
||||||
|
return this.LNBITS_CUSTOM_BADGE_COLOR || ''
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return this.SITE_TITLE
|
||||||
|
},
|
||||||
|
titleIsLnbits() {
|
||||||
|
return this.SITE_TITLE == 'LNbits'
|
||||||
|
},
|
||||||
|
customLogoUrl() {
|
||||||
|
return this.USE_CUSTOM_LOGO || null
|
||||||
|
},
|
||||||
showAdmin() {
|
showAdmin() {
|
||||||
return this.g.user && (this.g.user.super_user || this.g.user.admin)
|
return this.g.user && (this.g.user.super_user || this.g.user.admin)
|
||||||
},
|
},
|
||||||
|
showVoidwallet() {
|
||||||
|
return this.g.user && this.VOIDWALLET == true
|
||||||
|
},
|
||||||
displayName() {
|
displayName() {
|
||||||
return (
|
return (
|
||||||
this.g.user?.extra?.display_name ||
|
this.g.user?.extra?.display_name ||
|
||||||
@@ -30,17 +69,6 @@ window.app.component('lnbits-header', {
|
|||||||
window.location = '/users'
|
window.location = '/users'
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e)
|
console.warn(e)
|
||||||
}
|
|
||||||
},
|
|
||||||
async handleLanguageChanged(lang) {
|
|
||||||
try {
|
|
||||||
await LNbits.api.updateUiCustomization({locale: lang.locale})
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Language Updated',
|
|
||||||
caption: lang.locale
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,8 +95,8 @@ window.app.component('lnbits-home-logos', {
|
|||||||
showLogos() {
|
showLogos() {
|
||||||
return (
|
return (
|
||||||
this.g.isSatsDenomination &&
|
this.g.isSatsDenomination &&
|
||||||
this.g.settings.siteTitle == 'LNbits' &&
|
this.SITE_TITLE == 'LNbits' &&
|
||||||
this.g.settings.showHomePageElements == true
|
this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,5 @@
|
|||||||
window.app.component('lnbits-language-dropdown', {
|
window.app.component('lnbits-language-dropdown', {
|
||||||
template: '#lnbits-language-dropdown',
|
template: '#lnbits-language-dropdown',
|
||||||
computed: {
|
|
||||||
currentLanguage() {
|
|
||||||
return (
|
|
||||||
this.langs.find(lang => lang.value === window.i18n.global.locale) || {
|
|
||||||
value: 'en',
|
|
||||||
label: 'English',
|
|
||||||
display: '🇬🇧 EN'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
activeLanguage(lang) {
|
activeLanguage(lang) {
|
||||||
return window.i18n.global.locale === lang
|
return window.i18n.global.locale === lang
|
||||||
@@ -19,7 +8,6 @@ window.app.component('lnbits-language-dropdown', {
|
|||||||
this.g.locale = newValue
|
this.g.locale = newValue
|
||||||
window.i18n.global.locale = newValue
|
window.i18n.global.locale = newValue
|
||||||
this.$q.localStorage.set('lnbits.lang', newValue)
|
this.$q.localStorage.set('lnbits.lang', newValue)
|
||||||
this.$emit('language-changed', newValue)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ window.app.component('lnbits-qrcode', {
|
|||||||
},
|
},
|
||||||
logo: {
|
logo: {
|
||||||
type: String,
|
type: String,
|
||||||
default: window.g.settings.qrLogo || null
|
default: LNBITS_QR_LOGO
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -9,10 +9,6 @@ window.app.component('lnbits-theme', {
|
|||||||
'g.disclaimerShown'(val) {
|
'g.disclaimerShown'(val) {
|
||||||
this.$q.localStorage.setItem('lnbits.disclaimerShown', val)
|
this.$q.localStorage.setItem('lnbits.disclaimerShown', val)
|
||||||
},
|
},
|
||||||
'g.locale'(val) {
|
|
||||||
this.$q.localStorage.setItem('lnbits.lang', val)
|
|
||||||
window.i18n.global.locale = val
|
|
||||||
},
|
|
||||||
'g.isFiatPriority'(val) {
|
'g.isFiatPriority'(val) {
|
||||||
this.$q.localStorage.setItem('lnbits.isFiatPriority', val)
|
this.$q.localStorage.setItem('lnbits.isFiatPriority', val)
|
||||||
},
|
},
|
||||||
@@ -45,30 +41,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 +113,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(
|
||||||
@@ -160,13 +123,6 @@ window.app.component('lnbits-theme', {
|
|||||||
if (this.g.mobileSimple === true) {
|
if (this.g.mobileSimple === true) {
|
||||||
document.body.classList.add('mobile-simple')
|
document.body.classList.add('mobile-simple')
|
||||||
}
|
}
|
||||||
Object.entries(this.g.user?.uiCustomization || {}).forEach(
|
|
||||||
([key, value]) => {
|
|
||||||
if (key in this.g) {
|
|
||||||
this.g[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
this.checkUrlParams()
|
this.checkUrlParams()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -71,10 +71,6 @@ window.app.component('lnbits-wallet-extra', {
|
|||||||
'lnbits.exchangeRate.' + this.g.wallet.currency,
|
'lnbits.exchangeRate.' + this.g.wallet.currency,
|
||||||
this.g.exchangeRate
|
this.g.exchangeRate
|
||||||
)
|
)
|
||||||
if (this.g.exchangeRate <= 0) {
|
|
||||||
this.g.fiatTracking = false
|
|
||||||
this.g.isFiatPriority = false
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(e => console.error(e))
|
.catch(e => console.error(e))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ window.app.component('lnbits-wallet-new', {
|
|||||||
this.g.lastWalletId = res.data.id
|
this.g.lastWalletId = res.data.id
|
||||||
this.$router.push(`/wallet/${res.data.id}`)
|
this.$router.push(`/wallet/${res.data.id}`)
|
||||||
})
|
})
|
||||||
.catch(LNbits.utils.notifyApiError)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
|||||||
+32
-36
@@ -9,50 +9,17 @@ const localStore = (key, defaultValue) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.g = Vue.reactive({
|
window.g = Vue.reactive({
|
||||||
// vars from server
|
|
||||||
settings: SETTINGS,
|
|
||||||
currencies: CURRENCIES,
|
|
||||||
extensions: SETTINGS.extensions,
|
|
||||||
allowedCurrencies: SETTINGS.allowedCurrencies,
|
|
||||||
denomination: SETTINGS.denomination,
|
|
||||||
isSatsDenomination: SETTINGS.denomination == 'sats',
|
|
||||||
// local storage vars
|
|
||||||
themeChoice: localStore('lnbits.theme', SETTINGS.defaultTheme),
|
|
||||||
borderChoice: localStore('lnbits.border', SETTINGS.defaultBorder),
|
|
||||||
gradientChoice: localStore('lnbits.gradientBg', SETTINGS.defaultGradient),
|
|
||||||
cardRoundedChoice: localStore(
|
|
||||||
'lnbits.cardRounded',
|
|
||||||
SETTINGS.defaultCardRounded
|
|
||||||
),
|
|
||||||
cardGradientChoice: localStore(
|
|
||||||
'lnbits.cardGradient',
|
|
||||||
SETTINGS.defaultCardGradient
|
|
||||||
),
|
|
||||||
cardShadowChoice: localStore('lnbits.cardShadow', SETTINGS.defaultCardShadow),
|
|
||||||
reactionChoice: localStore('lnbits.reactions', SETTINGS.defaultReaction),
|
|
||||||
bgimageChoice: localStore(
|
|
||||||
'lnbits.backgroundImage',
|
|
||||||
SETTINGS.defaultBgimage || ''
|
|
||||||
),
|
|
||||||
locale: localStore('lnbits.lang', navigator.languages[1] ?? 'en'),
|
|
||||||
disclaimerShown: localStore('lnbits.disclaimerShown', false),
|
|
||||||
isFiatPriority: localStore('lnbits.isFiatPriority', false),
|
|
||||||
mobileSimple: localStore('lnbits.mobileSimple', true),
|
|
||||||
walletFlip: localStore('lnbits.walletFlip', false),
|
|
||||||
lastActiveWallet: localStore('lnbits.lastActiveWallet', null),
|
|
||||||
darkChoice: localStore('lnbits.darkMode', SETTINGS.defaultDark),
|
|
||||||
// cookie vars
|
|
||||||
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
|
|
||||||
isUserImpersonated: !!Quasar.Cookies.get('is_lnbits_user_impersonated'),
|
|
||||||
// frontend vars
|
|
||||||
errorCode: null,
|
errorCode: null,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
user: null,
|
user: null,
|
||||||
wallet: null,
|
wallet: null,
|
||||||
isPublicPage: true,
|
isPublicPage: true,
|
||||||
|
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
|
||||||
|
isUserImpersonated: !!Quasar.Cookies.get('is_lnbits_user_impersonated'),
|
||||||
offline: !navigator.onLine,
|
offline: !navigator.onLine,
|
||||||
hasCamera: false,
|
hasCamera: false,
|
||||||
visibleDrawer: false,
|
visibleDrawer: false,
|
||||||
|
extensions: WINDOW_SETTINGS.EXTENSIONS,
|
||||||
fiatBalance: 0,
|
fiatBalance: 0,
|
||||||
exchangeRate: 0,
|
exchangeRate: 0,
|
||||||
fiatTracking: false,
|
fiatTracking: false,
|
||||||
@@ -60,6 +27,35 @@ window.g = Vue.reactive({
|
|||||||
walletEventListeners: [],
|
walletEventListeners: [],
|
||||||
updatePayments: false, // used for updating the lnbits-payment-list
|
updatePayments: false, // used for updating the lnbits-payment-list
|
||||||
updatePaymentsHash: false, // used for closing the receive dialog
|
updatePaymentsHash: false, // used for closing the receive dialog
|
||||||
|
currencies: WINDOW_SETTINGS.LNBITS_CURRENCIES ?? [],
|
||||||
|
allowedCurrencies: WINDOW_SETTINGS.LNBITS_ALLOWED_CURRENCIES ?? [],
|
||||||
|
locale: localStore('lnbits.lang', navigator.languages[1] ?? 'en'),
|
||||||
|
disclaimerShown: localStore('lnbits.disclaimerShown', false),
|
||||||
|
isFiatPriority: localStore('lnbits.isFiatPriority', false),
|
||||||
|
mobileSimple: localStore('lnbits.mobileSimple', true),
|
||||||
|
walletFlip: localStore('lnbits.walletFlip', false),
|
||||||
|
lastActiveWallet: localStore('lnbits.lastActiveWallet', null),
|
||||||
|
darkChoice: localStore('lnbits.darkMode', true),
|
||||||
|
themeChoice: localStore('lnbits.theme', WINDOW_SETTINGS.LNBITS_DEFAULT_THEME),
|
||||||
|
borderChoice: localStore(
|
||||||
|
'lnbits.border',
|
||||||
|
WINDOW_SETTINGS.LNBITS_DEFAULT_BORDER || 'hard-border'
|
||||||
|
),
|
||||||
|
gradientChoice: localStore(
|
||||||
|
'lnbits.gradientBg',
|
||||||
|
WINDOW_SETTINGS.LNBITS_DEFAULT_GRADIENT || false
|
||||||
|
),
|
||||||
|
reactionChoice: localStore(
|
||||||
|
'lnbits.reactions',
|
||||||
|
WINDOW_SETTINGS.LNBITS_DEFAULT_REACTION || 'confettiBothSides'
|
||||||
|
),
|
||||||
|
bgimageChoice: localStore(
|
||||||
|
'lnbits.backgroundImage',
|
||||||
|
WINDOW_SETTINGS.LNBITS_DEFAULT_BGIMAGE || ''
|
||||||
|
),
|
||||||
|
ads: WINDOW_SETTINGS.AD_SPACE.split(',').map(ad => ad.split(';')),
|
||||||
|
denomination: WINDOW_SETTINGS.LNBITS_DENOMINATION,
|
||||||
|
isSatsDenomination: WINDOW_SETTINGS.LNBITS_DENOMINATION == 'sats',
|
||||||
scanner: null,
|
scanner: null,
|
||||||
newWalletType: null
|
newWalletType: null
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -157,7 +157,8 @@ window.app.mixin({
|
|||||||
return {
|
return {
|
||||||
api: window._lnbitsApi,
|
api: window._lnbitsApi,
|
||||||
utils: window._lnbitsUtils,
|
utils: window._lnbitsUtils,
|
||||||
g: window.g
|
g: window.g,
|
||||||
|
...WINDOW_SETTINGS
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// backwards compatibility for extensions, should not be used in the future
|
// backwards compatibility for extensions, should not be used in the future
|
||||||
|
|||||||
@@ -39,9 +39,6 @@ window.PageAccount = {
|
|||||||
color: 'pink-3'
|
color: 'pink-3'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
defaultSiteCustomisation: {
|
|
||||||
locale: 'en'
|
|
||||||
},
|
|
||||||
reactionOptions: [
|
reactionOptions: [
|
||||||
'None',
|
'None',
|
||||||
'confettiBothSides',
|
'confettiBothSides',
|
||||||
@@ -216,18 +213,26 @@ window.PageAccount = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
activeLanguage(lang) {
|
||||||
|
return window.i18n.global.locale === lang
|
||||||
|
},
|
||||||
changeLanguage(newValue) {
|
changeLanguage(newValue) {
|
||||||
window.i18n.global.locale = newValue
|
window.i18n.global.locale = newValue
|
||||||
this.$q.localStorage.set('lnbits.lang', newValue)
|
this.$q.localStorage.set('lnbits.lang', newValue)
|
||||||
},
|
},
|
||||||
async updateAccount() {
|
async updateAccount() {
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request('PATCH', '/api/v1/auth', null, {
|
const {data} = await LNbits.api.request(
|
||||||
user_id: this.g.user.id,
|
'PUT',
|
||||||
username: this.g.user.username,
|
'/api/v1/auth/update',
|
||||||
email: this.g.user.email,
|
null,
|
||||||
extra: this.g.user.extra
|
{
|
||||||
})
|
user_id: this.g.user.id,
|
||||||
|
username: this.g.user.username,
|
||||||
|
email: this.g.user.email,
|
||||||
|
extra: this.g.user.extra
|
||||||
|
}
|
||||||
|
)
|
||||||
this.untouchedUser = JSON.parse(JSON.stringify(this.g.user))
|
this.untouchedUser = JSON.parse(JSON.stringify(this.g.user))
|
||||||
this.hasUsername = !!data.username
|
this.hasUsername = !!data.username
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
@@ -675,37 +680,6 @@ window.PageAccount = {
|
|||||||
l => l.name !== label.name
|
l => l.name !== label.name
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
},
|
|
||||||
|
|
||||||
async siteCustomisationChanged(options = {}) {
|
|
||||||
try {
|
|
||||||
Object.entries(options || {}).forEach(([key, value]) => {
|
|
||||||
if (key in this.g) {
|
|
||||||
this.g[key] = value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await LNbits.api.updateUiCustomization(options)
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'UI Customization updated.'
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
LNbits.utils.notifyApiError(e)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resetThemeDefaults() {
|
|
||||||
const defaults = {
|
|
||||||
themeChoice: this.g.settings.defaultTheme,
|
|
||||||
borderChoice: this.g.settings.defaultBorder,
|
|
||||||
gradientChoice: this.g.settings.defaultGradient,
|
|
||||||
bgimageChoice: this.g.settings.defaultBgimage || '',
|
|
||||||
reactionChoice: this.g.settings.defaultReaction,
|
|
||||||
darkChoice: this.g.settings.defaultDark,
|
|
||||||
cardRoundedChoice: this.g.settings.defaultCardRounded,
|
|
||||||
cardGradientChoice: this.g.settings.defaultCardGradient,
|
|
||||||
cardShadowChoice: this.g.settings.defaultCardShadow
|
|
||||||
}
|
|
||||||
this.siteCustomisationChanged(defaults)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -719,7 +693,7 @@ window.PageAccount = {
|
|||||||
await this.getUserAssets()
|
await this.getUserAssets()
|
||||||
// filter out themes that are not allowed
|
// filter out themes that are not allowed
|
||||||
this.themeOptions = this.themeOptions.filter(theme =>
|
this.themeOptions = this.themeOptions.filter(theme =>
|
||||||
this.g.settings.themeOptions.includes(theme.name)
|
this.LNBITS_THEME_OPTIONS.includes(theme.name)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -842,8 +842,8 @@ window.PageExtensions = {
|
|||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
this.extensions = await this.fetchAllExtensions()
|
this.extensions = await this.fetchAllExtensions()
|
||||||
this.extbuilderEnabled = this.g.user.admin || this.g.settings.extBuilder
|
this.extbuilderEnabled = this.g.user.admin || this.LNBITS_EXT_BUILDER
|
||||||
this.reviewsUrl = this.g.settings.extensionsReviewsUrl
|
this.reviewsUrl = this.LNBITS_EXTENSIONS_REVIEWS_URL
|
||||||
|
|
||||||
if (this.g.user.extensions.length === 0) {
|
if (this.g.user.extensions.length === 0) {
|
||||||
this.tab = 'all'
|
this.tab = 'all'
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ window.PageFirstInstall = {
|
|||||||
isPwdRepeat: true,
|
isPwdRepeat: true,
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
passwordRepeat: '',
|
passwordRepeat: ''
|
||||||
firstInstallToken: ''
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -18,25 +17,20 @@ window.PageFirstInstall = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setPassword() {
|
async setPassword() {
|
||||||
LNbits.api
|
try {
|
||||||
.request('PUT', `/api/v1/auth/first_install`, null, {
|
await LNbits.api.request('PUT', '/api/v1/auth/first_install', null, {
|
||||||
username: this.loginData.username,
|
username: this.loginData.username,
|
||||||
password: this.loginData.password,
|
password: this.loginData.password,
|
||||||
password_repeat: this.loginData.passwordRepeat,
|
password_repeat: this.loginData.passwordRepeat
|
||||||
first_install_token: this.loginData.firstInstallToken
|
|
||||||
})
|
})
|
||||||
.then(async () => {
|
window.location.href = '/admin'
|
||||||
const res = await LNbits.api.getAuthUser()
|
} catch (e) {
|
||||||
this.g.user = LNbits.map.user(res.data)
|
LNbits.utils.notifyApiError(e)
|
||||||
this.g.isPublicPage = false
|
}
|
||||||
this.$router.push('/admin')
|
|
||||||
})
|
|
||||||
.catch(this.utils.notifyApiError)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
const params = new URLSearchParams(window.location.search)
|
document.title = 'First Install - LNbits'
|
||||||
this.loginData.firstInstallToken = params.get('token') || ''
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ window.PageHome = {
|
|||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
passwordRepeat: '',
|
passwordRepeat: '',
|
||||||
invitationCode: '',
|
|
||||||
walletName: '',
|
walletName: '',
|
||||||
signup: false
|
signup: false
|
||||||
}
|
}
|
||||||
@@ -20,15 +19,40 @@ window.PageHome = {
|
|||||||
showClaimLnurl() {
|
showClaimLnurl() {
|
||||||
return (
|
return (
|
||||||
this.lnurl !== '' &&
|
this.lnurl !== '' &&
|
||||||
this.g.settings.allowRegister &&
|
this.allowRegister &&
|
||||||
'user-id-only' in this.g.settings.authMethods
|
'user-id-only' in this.LNBITS_AUTH_METHODS
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
formatDescription() {
|
formatDescription() {
|
||||||
return LNbits.utils.convertMarkdown(this.g.settings.siteDescription)
|
return LNbits.utils.convertMarkdown(this.SITE_DESCRIPTION)
|
||||||
},
|
},
|
||||||
isAccessTokenExpired() {
|
isAccessTokenExpired() {
|
||||||
return this.$q.cookies.get('is_access_token_expired')
|
return this.$q.cookies.get('is_access_token_expired')
|
||||||
|
},
|
||||||
|
allowRegister() {
|
||||||
|
return this.LNBITS_NEW_ACCOUNTS_ALLOWED
|
||||||
|
},
|
||||||
|
// TODO: this makes no sense
|
||||||
|
hasCustomImage() {
|
||||||
|
return this.LNBITS_CUSTOM_IMAGE
|
||||||
|
},
|
||||||
|
showHomepageElements() {
|
||||||
|
return this.LNBITS_SHOW_HOME_PAGE_ELEMENTS === true
|
||||||
|
},
|
||||||
|
siteTitle() {
|
||||||
|
return this.SITE_TITLE || ''
|
||||||
|
},
|
||||||
|
siteTagline() {
|
||||||
|
return this.SITE_TAGLINE || ''
|
||||||
|
},
|
||||||
|
adsEnabled() {
|
||||||
|
return this.AD_SPACE_ENABLED && this.AD_SPACE && this.AD_SPACE.length > 0
|
||||||
|
},
|
||||||
|
adsTitle() {
|
||||||
|
return this.AD_SPACE_TITLE || ''
|
||||||
|
},
|
||||||
|
ads() {
|
||||||
|
return this.AD_SPACE.map(ad => ad.split(';'))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -41,7 +65,6 @@ window.PageHome = {
|
|||||||
this.username = null
|
this.username = null
|
||||||
this.password = null
|
this.password = null
|
||||||
this.passwordRepeat = null
|
this.passwordRepeat = null
|
||||||
this.invitationCode = null
|
|
||||||
|
|
||||||
this.authAction = 'register'
|
this.authAction = 'register'
|
||||||
this.authMethod = authMethod
|
this.authMethod = authMethod
|
||||||
@@ -53,8 +76,7 @@ window.PageHome = {
|
|||||||
this.username,
|
this.username,
|
||||||
this.email,
|
this.email,
|
||||||
this.password,
|
this.password,
|
||||||
this.passwordRepeat,
|
this.passwordRepeat
|
||||||
this.invitationCode
|
|
||||||
)
|
)
|
||||||
this.refreshAuthUser()
|
this.refreshAuthUser()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -70,10 +70,10 @@ window.PageUsers = {
|
|||||||
usersTable: {
|
usersTable: {
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: 'activated',
|
name: 'admin',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: this.$t('activated'),
|
label: 'Admin',
|
||||||
field: 'activated',
|
field: 'admin',
|
||||||
sortable: false
|
sortable: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -401,7 +401,7 @@ window.PageUsers = {
|
|||||||
|
|
||||||
toggleAdmin(userId) {
|
toggleAdmin(userId) {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('PUT', `/users/api/v1/user/${userId}/admin`)
|
.request('GET', `/users/api/v1/user/${userId}/admin`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.fetchUsers()
|
this.fetchUsers()
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
@@ -412,19 +412,6 @@ window.PageUsers = {
|
|||||||
})
|
})
|
||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(LNbits.utils.notifyApiError)
|
||||||
},
|
},
|
||||||
toggleUserActivated(userId) {
|
|
||||||
LNbits.api
|
|
||||||
.request('PUT', `/users/api/v1/user/${userId}/activate`)
|
|
||||||
.then(res => {
|
|
||||||
this.fetchUsers()
|
|
||||||
Quasar.Notify.create({
|
|
||||||
type: 'positive',
|
|
||||||
message: res.data.message,
|
|
||||||
icon: null
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(LNbits.utils.notifyApiError)
|
|
||||||
},
|
|
||||||
async showAccountPage(user_id) {
|
async showAccountPage(user_id) {
|
||||||
this.activeUser.showPassword = false
|
this.activeUser.showPassword = false
|
||||||
this.activeUser.showUserId = false
|
this.activeUser.showUserId = false
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
window._lnbitsUtils = {
|
window._lnbitsUtils = {
|
||||||
url_for(url) {
|
url_for(url) {
|
||||||
const _url = new URL(url, window.location.origin)
|
const _url = new URL(url, window.location.origin)
|
||||||
_url.searchParams.set('v', window.g.settings.cacheKey)
|
_url.searchParams.set('v', WINDOW_SETTINGS.CACHE_KEY)
|
||||||
return _url.toString()
|
return _url.toString()
|
||||||
},
|
},
|
||||||
loadScript(src) {
|
loadScript(src) {
|
||||||
@@ -144,7 +144,7 @@ window._lnbitsUtils = {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async notifyApiError(error) {
|
notifyApiError(error) {
|
||||||
if (!error.response) {
|
if (!error.response) {
|
||||||
return console.error(error)
|
return console.error(error)
|
||||||
}
|
}
|
||||||
@@ -154,10 +154,6 @@ window._lnbitsUtils = {
|
|||||||
500: 'negative'
|
500: 'negative'
|
||||||
}
|
}
|
||||||
let messages = error.response.data.detail
|
let messages = error.response.data.detail
|
||||||
if (!messages) {
|
|
||||||
const text = await error.response.data?.text()
|
|
||||||
messages = this.parseJSONSafe(text)?.detail
|
|
||||||
}
|
|
||||||
if (messages) {
|
if (messages) {
|
||||||
messages = Array.isArray(messages)
|
messages = Array.isArray(messages)
|
||||||
? messages.map(e => e.msg + ` (${e.loc?.join('/')})`)
|
? messages.map(e => e.msg + ` (${e.loc?.join('/')})`)
|
||||||
|
|||||||
@@ -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] {
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Vendored
+904
-786
File diff suppressed because it is too large
Load Diff
+1
-5
@@ -178,11 +178,7 @@ async def invoice_callback_dispatcher(checking_id: str, is_internal: bool = Fals
|
|||||||
logger.warning(f"Payment '{checking_id}' is not incoming, skipping.")
|
logger.warning(f"Payment '{checking_id}' is not incoming, skipping.")
|
||||||
return
|
return
|
||||||
|
|
||||||
from lnbits.core.services.payments import check_payment_status
|
status = await payment.check_status(skip_internal_payment_notifications=True)
|
||||||
|
|
||||||
status = await check_payment_status(
|
|
||||||
payment, skip_internal_payment_notifications=True
|
|
||||||
)
|
|
||||||
payment.fee = status.fee_msat or payment.fee
|
payment.fee = status.fee_msat or payment.fee
|
||||||
# only overwrite preimage if status.preimage provides it
|
# only overwrite preimage if status.preimage provides it
|
||||||
payment.preimage = status.preimage or payment.preimage
|
payment.preimage = status.preimage or payment.preimage
|
||||||
|
|||||||
@@ -70,10 +70,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const RENDERED_ROUTE = '{{ normalize_path(request.path) }}'
|
const RENDERED_ROUTE = '{{ normalize_path(request.path) }}'
|
||||||
const SETTINGS = {{ SETTINGS | tojson | safe }}
|
const WINDOW_SETTINGS = {{ WINDOW_SETTINGS | tojson }}
|
||||||
const CURRENCIES = {{ CURRENCIES | tojson | safe }}
|
Object.keys(WINDOW_SETTINGS).forEach(key => {
|
||||||
// backwards compatibility for extensions (tpos)
|
window[key] = WINDOW_SETTINGS[key]
|
||||||
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson | safe }}
|
})
|
||||||
</script>
|
</script>
|
||||||
<!-- vue templates -->
|
<!-- vue templates -->
|
||||||
{% include('components.vue') %} {% include('pages.vue') %}
|
{% include('components.vue') %} {% include('pages.vue') %}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ include('components/lnbits-error.vue') %}
|
|||||||
<q-list v-if="g.user" dense class="lnbits-drawer__q-list">
|
<q-list v-if="g.user" dense class="lnbits-drawer__q-list">
|
||||||
<q-item-label header v-text="$t('manage')"></q-item-label>
|
<q-item-label header v-text="$t('manage')"></q-item-label>
|
||||||
<div v-if="g.user.admin">
|
<div v-if="g.user.admin">
|
||||||
<q-item v-if="g.settings.showAdmin" to="/admin">
|
<q-item v-if="showAdmin" to="/admin">
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
<q-icon
|
<q-icon
|
||||||
name="settings"
|
name="settings"
|
||||||
@@ -51,7 +51,7 @@ include('components/lnbits-error.vue') %}
|
|||||||
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item v-if="g.settings.showNodemanager" to="/node">
|
<q-item v-if="showNode" to="/node">
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
<q-icon
|
<q-icon
|
||||||
name="developer_board"
|
name="developer_board"
|
||||||
@@ -66,7 +66,7 @@ include('components/lnbits-error.vue') %}
|
|||||||
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item v-if="g.settings.showAdmin" to="/users">
|
<q-item v-if="showUsers" to="/users">
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
<q-icon
|
<q-icon
|
||||||
name="groups"
|
name="groups"
|
||||||
@@ -81,7 +81,7 @@ include('components/lnbits-error.vue') %}
|
|||||||
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item v-if="g.settings.showAudit" to="/audit">
|
<q-item v-if="showAudit" to="/audit">
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
<q-icon
|
<q-icon
|
||||||
name="playlist_add_check_circle"
|
name="playlist_add_check_circle"
|
||||||
@@ -112,7 +112,7 @@ include('components/lnbits-error.vue') %}
|
|||||||
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item v-if="g.settings.showExtensions" to="/extensions">
|
<q-item v-if="showExtensions" to="/extensions">
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
<q-icon
|
<q-icon
|
||||||
name="extension"
|
name="extension"
|
||||||
@@ -686,7 +686,7 @@ include('components/lnbits-error.vue') %}
|
|||||||
dense
|
dense
|
||||||
filled
|
filled
|
||||||
v-model="walletName"
|
v-model="walletName"
|
||||||
:label="$t('name_your_wallet', {name: g.settings.siteTitle + ' *'})"
|
:label="$t('name_your_wallet', {name: SITE_TITLE + ' *'})"
|
||||||
></q-input>
|
></q-input>
|
||||||
<q-card-actions vertical align="center" class="q-pa-none">
|
<q-card-actions vertical align="center" class="q-pa-none">
|
||||||
<q-btn
|
<q-btn
|
||||||
@@ -814,90 +814,16 @@ include('components/lnbits-error.vue') %}
|
|||||||
type="password"
|
type="password"
|
||||||
:rules="[val => !val || val.length >= 8 || $t('invalid_password')]"
|
:rules="[val => !val || val.length >= 8 || $t('invalid_password')]"
|
||||||
></q-input>
|
></q-input>
|
||||||
<div
|
|
||||||
v-if="confirmationMethodsCount > 1"
|
|
||||||
class="row justify-center q-mb-md"
|
|
||||||
>
|
|
||||||
<q-tabs
|
|
||||||
v-model="confirmationMethod"
|
|
||||||
dense
|
|
||||||
active-color="primary"
|
|
||||||
indicator-color="primary"
|
|
||||||
>
|
|
||||||
<q-tab
|
|
||||||
v-if="g.settings.userActivationByInvitationCode"
|
|
||||||
name="code"
|
|
||||||
icon="confirmation_number"
|
|
||||||
label="Code"
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
v-if="g.settings.userActivationByPayment"
|
|
||||||
name="payment"
|
|
||||||
icon="bolt"
|
|
||||||
label="Payment"
|
|
||||||
></q-tab>
|
|
||||||
<q-tab
|
|
||||||
v-if="g.settings.userActivationByEmail"
|
|
||||||
name="email"
|
|
||||||
icon="email"
|
|
||||||
label="Email"
|
|
||||||
></q-tab>
|
|
||||||
</q-tabs>
|
|
||||||
</div>
|
|
||||||
<div v-if="confirmationMethodsCount > 0" class="q-mb-md">
|
|
||||||
<q-tab-panels v-model="confirmationMethod">
|
|
||||||
<q-tab-panel name="code" class="q-pa-none">
|
|
||||||
<div
|
|
||||||
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
|
|
||||||
>
|
|
||||||
<q-icon name="info" color="orange-4" class="q-mr-xs"></q-icon>
|
|
||||||
You need an invitation code to register.
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
v-model="confirmationCode"
|
|
||||||
:label="$t('invitation_code')"
|
|
||||||
:type="showConfirmationCode ? 'text' : 'password'"
|
|
||||||
:hint="$t('invitation_code_hint')"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
@click="showConfirmationCode = !showConfirmationCode"
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:icon="
|
|
||||||
showConfirmationCode ? 'visibility_off' : 'visibility'
|
|
||||||
"
|
|
||||||
color="grey"
|
|
||||||
></q-btn>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</q-tab-panel>
|
|
||||||
<q-tab-panel name="payment">
|
|
||||||
<div>payment</div>
|
|
||||||
</q-tab-panel>
|
|
||||||
|
|
||||||
<q-tab-panel name="email" class="q-pa-none">
|
|
||||||
<div>
|
|
||||||
<q-input
|
|
||||||
dense
|
|
||||||
filled
|
|
||||||
v-model="confirmationEmail"
|
|
||||||
:label="$t('email')"
|
|
||||||
:hint="$t('email_confirmation_hint')"
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</q-tab-panel>
|
|
||||||
</q-tab-panels>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row justify-end">
|
<div class="row justify-end">
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
color="primary"
|
color="primary"
|
||||||
:disable="disableRegister"
|
:disable="
|
||||||
|
!password ||
|
||||||
|
!passwordRepeat ||
|
||||||
|
!username ||
|
||||||
|
password !== passwordRepeat
|
||||||
|
"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="full-width"
|
class="full-width"
|
||||||
:label="$t('create_account')"
|
:label="$t('create_account')"
|
||||||
@@ -1034,9 +960,9 @@ include('components/lnbits-error.vue') %}
|
|||||||
<q-avatar size="32px" class="q-mr-md">
|
<q-avatar size="32px" class="q-mr-md">
|
||||||
<q-img
|
<q-img
|
||||||
:src="
|
:src="
|
||||||
g.settings.keycloakIcon
|
keycloakIcon
|
||||||
? g.settings.keycloakIcon
|
? keycloakIcon
|
||||||
: utils.url_for('lnbits/static/images/keycloak-logo.png')
|
: 'lnbits/static/images/keycloak-logo.png'
|
||||||
"
|
"
|
||||||
></q-img>
|
></q-img>
|
||||||
</q-avatar>
|
</q-avatar>
|
||||||
@@ -1044,7 +970,7 @@ include('components/lnbits-error.vue') %}
|
|||||||
<span
|
<span
|
||||||
v-text="
|
v-text="
|
||||||
$t('signin_with_custom_org', {
|
$t('signin_with_custom_org', {
|
||||||
custom_org: g.settings.keycloakOrg || 'Keycloak'
|
custom_org: keycloakOrg
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
></span>
|
></span>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div v-if="g.settings.hasNodemanager">
|
<div v-if="LNBITS_NODE_UI">
|
||||||
<p>
|
<p>
|
||||||
<span v-text="$t('node_management')"></span>
|
<span v-text="$t('node_management')"></span>
|
||||||
</p>
|
</p>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
v-model="formData.lnbits_node_ui_transactions"
|
v-model="formData.lnbits_node_ui_transactions"
|
||||||
></q-toggle>
|
></q-toggle>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!g.settings.hasNodemanager">
|
<p v-if="!LNBITS_NODE_UI">
|
||||||
<span v-text="$t('node_management_not_supported')"></span>
|
<span v-text="$t('node_management_not_supported')"></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -86,42 +86,6 @@
|
|||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
|
||||||
<div class="row q-col-gutter-md">
|
|
||||||
<div class="col-12 col-md-4">
|
|
||||||
<p>
|
|
||||||
<span v-text="$t('wallet_featured_button_label')"></span>
|
|
||||||
</p>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
type="text"
|
|
||||||
v-model="formData.lnbits_wallet_featured_button_label"
|
|
||||||
label="Loop to Onchain"
|
|
||||||
:hint="$t('wallet_featured_button_label_hint')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-4">
|
|
||||||
<p><span v-text="$t('wallet_featured_button_url')"></span></p>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
type="text"
|
|
||||||
v-model="formData.lnbits_wallet_featured_button_url"
|
|
||||||
label="/boltz"
|
|
||||||
:hint="$t('wallet_featured_button_url_hint')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-4">
|
|
||||||
<p><span v-text="$t('wallet_featured_button_icon')"></span></p>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
type="text"
|
|
||||||
v-model="formData.lnbits_wallet_featured_button_icon"
|
|
||||||
label="bolt"
|
|
||||||
:hint="$t('wallet_featured_button_icon_hint')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row q-col-gutter-md q-mt-md">
|
<div class="row q-col-gutter-md q-mt-md">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p><span v-text="$t('ui_custom_badge')"></span></p>
|
<p><span v-text="$t('ui_custom_badge')"></span></p>
|
||||||
@@ -257,50 +221,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>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
>
|
>
|
||||||
</q-chip>
|
</q-chip>
|
||||||
</div>
|
</div>
|
||||||
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p><span v-text="$t('allowed_users')"></span></p>
|
<p><span v-text="$t('allowed_users')"></span></p>
|
||||||
@@ -56,10 +57,7 @@
|
|||||||
>
|
>
|
||||||
</q-chip>
|
</q-chip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<br />
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 col-md-6 q-mt-sm">
|
|
||||||
<q-item tag="label" v-ripple>
|
<q-item tag="label" v-ripple>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label v-text="$t('allow_creation_user')"></q-item-label>
|
<q-item-label v-text="$t('allow_creation_user')"></q-item-label>
|
||||||
@@ -78,237 +76,8 @@
|
|||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</div>
|
<br />
|
||||||
<div class="col-12 col-md-6 q-mt-sm">
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label v-text="$t('require_user_activation')"></q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
v-text="$t('require_user_activation_desc')"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-toggle
|
|
||||||
size="md"
|
|
||||||
v-model="formData.lnbits_require_user_activation"
|
|
||||||
checked-icon="check"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<div v-if="formData.lnbits_require_user_activation" class="row">
|
|
||||||
<div class="col">
|
|
||||||
<q-list bordered class="rounded-borders">
|
|
||||||
<q-expansion-item header-class="text-primary text-bold">
|
|
||||||
<template v-slot:header>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="confirmation_number" size="md"></q-icon>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section> Invitation Code </q-item-section>
|
|
||||||
|
|
||||||
<q-item-section side>
|
|
||||||
<div class="row items-center">
|
|
||||||
<q-toggle
|
|
||||||
size="md"
|
|
||||||
:label="
|
|
||||||
formData.lnbits_user_activation_by_invitation_code
|
|
||||||
? $t('enabled')
|
|
||||||
: $t('disabled')
|
|
||||||
"
|
|
||||||
v-model="formData.lnbits_user_activation_by_invitation_code"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-item-section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<q-card class="q-pb-xl">
|
|
||||||
<q-card-section>
|
|
||||||
<div
|
|
||||||
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
|
|
||||||
>
|
|
||||||
<q-icon
|
|
||||||
name="info"
|
|
||||||
color="orange-4"
|
|
||||||
size="18px"
|
|
||||||
class="q-mr-xs"
|
|
||||||
></q-icon>
|
|
||||||
Users will need to provide a valid invitation code during
|
|
||||||
registration to activate their account.
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 col-md-6 q-pr-sm">
|
|
||||||
<p><span v-text="$t('reusable_activation_code')"></span></p>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_register_reusable_activation_code"
|
|
||||||
:type="showReusableActivationCode ? 'text' : 'password'"
|
|
||||||
:label="$t('reusable_activation_code_label')"
|
|
||||||
:hint="$t('reusable_activation_code_hint')"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
@click="
|
|
||||||
showReusableActivationCode = !showReusableActivationCode
|
|
||||||
"
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
:icon="
|
|
||||||
showReusableActivationCode
|
|
||||||
? 'visibility_off'
|
|
||||||
: 'visibility'
|
|
||||||
"
|
|
||||||
color="grey"
|
|
||||||
></q-btn>
|
|
||||||
<q-btn
|
|
||||||
@click="
|
|
||||||
utils.copyText(
|
|
||||||
formData.lnbits_register_reusable_activation_code
|
|
||||||
)
|
|
||||||
"
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
icon="content_copy"
|
|
||||||
color="grey"
|
|
||||||
></q-btn>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<p><span v-text="$t('one_time_activation_code')"></span></p>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-model="formAddActivationCode"
|
|
||||||
@keydown.enter="addOneTimeActivationCode"
|
|
||||||
type="text"
|
|
||||||
:label="$t('one_time_activation_code_label')"
|
|
||||||
:hint="$t('one_time_activation_code_hint')"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
@click="addOneTimeActivationCode"
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
icon="add"
|
|
||||||
></q-btn>
|
|
||||||
</q-input>
|
|
||||||
<div>
|
|
||||||
<q-chip
|
|
||||||
v-for="code in formData.lnbits_register_one_time_activation_codes"
|
|
||||||
:key="code"
|
|
||||||
removable
|
|
||||||
@remove="removeOneTimeActivationCode(code)"
|
|
||||||
color="primary"
|
|
||||||
text-color="white"
|
|
||||||
:label="code"
|
|
||||||
class="ellipsis"
|
|
||||||
>
|
|
||||||
</q-chip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
|
|
||||||
<q-expansion-item header-class="text-primary text-bold">
|
|
||||||
<template v-slot:header>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar>
|
|
||||||
<q-icon name="bolt" size="md"></q-icon>
|
|
||||||
</q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section> Payment </q-item-section>
|
|
||||||
|
|
||||||
<q-item-section side>
|
|
||||||
<div class="row items-center">
|
|
||||||
<q-toggle
|
|
||||||
size="md"
|
|
||||||
disable
|
|
||||||
:label="
|
|
||||||
formData.lnbits_user_activation_by_payment
|
|
||||||
? $t('enabled')
|
|
||||||
: $t('disabled')
|
|
||||||
"
|
|
||||||
v-model="formData.lnbits_user_activation_by_payment"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-item-section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<q-card class="q-pb-xl">
|
|
||||||
<q-card-section>
|
|
||||||
<div
|
|
||||||
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
|
|
||||||
>
|
|
||||||
<q-icon
|
|
||||||
name="info"
|
|
||||||
color="orange-4"
|
|
||||||
size="18px"
|
|
||||||
class="q-mr-xs"
|
|
||||||
></q-icon>
|
|
||||||
Users will need to make a payment during registration to
|
|
||||||
activate their account.
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<q-expansion-item header-class="text-primary text-bold">
|
|
||||||
<template v-slot:header>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar>
|
|
||||||
<q-icon name="email"></q-icon>
|
|
||||||
</q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section> Email </q-item-section>
|
|
||||||
|
|
||||||
<q-item-section side>
|
|
||||||
<div class="row items-center">
|
|
||||||
<q-toggle
|
|
||||||
disable
|
|
||||||
size="md"
|
|
||||||
:label="
|
|
||||||
formData.lnbits_user_activation_by_email
|
|
||||||
? $t('enabled')
|
|
||||||
: $t('disabled')
|
|
||||||
"
|
|
||||||
v-model="formData.lnbits_user_activation_by_email"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-item-section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<q-card class="q-pb-xl">
|
|
||||||
<q-card-section>
|
|
||||||
<div
|
|
||||||
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
|
|
||||||
>
|
|
||||||
<q-icon
|
|
||||||
name="info"
|
|
||||||
color="orange-4"
|
|
||||||
size="18px"
|
|
||||||
class="q-mr-xs"
|
|
||||||
></q-icon>
|
|
||||||
Users will receive a confirmation email.
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
</q-list>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template id="lnbits-footer">
|
<template id="lnbits-footer">
|
||||||
<q-footer
|
<q-footer
|
||||||
v-if="g.settings.showHomePageElements"
|
v-if="showFooter"
|
||||||
class="bg-transparent q-px-lg q-py-md"
|
class="bg-transparent q-px-lg q-py-md"
|
||||||
:class="{'text-dark': !$q.dark.isActive}"
|
:class="{'text-dark': !$q.dark.isActive}"
|
||||||
>
|
>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<q-toolbar-title class="text-caption">
|
<q-toolbar-title class="text-caption">
|
||||||
<span v-text="title"></span>
|
<span v-text="title"></span>
|
||||||
<br />
|
<br />
|
||||||
<small v-text="version"></small>
|
<small v-text="$t('lnbits_version') + ': ' + version"></small>
|
||||||
</q-toolbar-title>
|
</q-toolbar-title>
|
||||||
<q-space></q-space>
|
<q-space></q-space>
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
<template id="lnbits-header">
|
<template id="lnbits-header">
|
||||||
<q-header bordered class="bg-marginal-bg">
|
<q-header bordered class="bg-marginal-bg">
|
||||||
<q-banner
|
<q-banner v-if="showVoidwallet" class="bg-warning text-white" dense>
|
||||||
v-if="g.settings.showVoidwallet"
|
|
||||||
class="bg-warning text-white"
|
|
||||||
dense
|
|
||||||
>
|
|
||||||
<template v-slot:avatar>
|
<template v-slot:avatar>
|
||||||
<q-icon name="warning" color="white" />
|
<q-icon name="warning" color="white" />
|
||||||
</template>
|
</template>
|
||||||
@@ -28,15 +24,13 @@
|
|||||||
<q-toolbar-title>
|
<q-toolbar-title>
|
||||||
<q-btn flat no-caps dense class="q-mr-sm" size="lg" type="a" href="/">
|
<q-btn flat no-caps dense class="q-mr-sm" size="lg" type="a" href="/">
|
||||||
<q-img
|
<q-img
|
||||||
v-if="customLogo"
|
v-if="customLogoUrl"
|
||||||
height="30px"
|
height="30px"
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
:src="customLogo"
|
:src="customLogoUrl"
|
||||||
></q-img>
|
></q-img>
|
||||||
<span v-else-if="g.settings.siteTitle == 'LNbits'"
|
<span v-else-if="!titleIsLnbits"><strong>LN</strong>bits</span>
|
||||||
><strong>LN</strong>bits</span
|
<span v-else v-text="title"></span>
|
||||||
>
|
|
||||||
<span v-else v-text="g.settings.siteTitle"></span>
|
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-badge v-if="g.user && g.user.super_user">Super User</q-badge>
|
<q-badge v-if="g.user && g.user.super_user">Super User</q-badge>
|
||||||
<q-badge v-else-if="g.user && g.user.admin">Admin User</q-badge>
|
<q-badge v-else-if="g.user && g.user.admin">Admin User</q-badge>
|
||||||
@@ -44,31 +38,31 @@
|
|||||||
<q-badge
|
<q-badge
|
||||||
class="q-mr-md"
|
class="q-mr-md"
|
||||||
v-show="$q.screen.gt.sm"
|
v-show="$q.screen.gt.sm"
|
||||||
v-if="g.settings.customBadge"
|
v-if="hasCustomBadge"
|
||||||
:label="g.settings.customBadge"
|
:label="customBadge"
|
||||||
:color="g.settings.customBadgeColor"
|
:color="customBadgeColor"
|
||||||
>
|
>
|
||||||
</q-badge>
|
</q-badge>
|
||||||
|
|
||||||
<q-badge
|
<q-badge
|
||||||
v-show="$q.screen.gt.sm"
|
v-show="$q.screen.gt.sm"
|
||||||
v-if="g.user && g.settings.serviceFee > 0"
|
v-if="hasServiceFee"
|
||||||
color="green"
|
color="green"
|
||||||
class="q-mr-md"
|
class="q-mr-md"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="g.user && g.settings.serviceFeeMax > 0"
|
v-if="hasServiceFeeMax"
|
||||||
v-text="
|
v-text="
|
||||||
$t('service_fee_max_badge', {
|
$t('service_fee_max_badge', {
|
||||||
amount: g.settings.serviceFee,
|
amount: serviceFee,
|
||||||
max: g.settings.serviceFeeMax,
|
max: serviceFeeMax,
|
||||||
denom: g.denomination
|
denom: g.denomination
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
></span>
|
></span>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
v-text="$t('service_fee_badge', {amount: g.settings.serviceFee})"
|
v-text="$t('service_fee_badge', {amount: serviceFee})"
|
||||||
></span>
|
></span>
|
||||||
<q-tooltip><span v-text="$t('service_fee_tooltip')"></span></q-tooltip>
|
<q-tooltip><span v-text="$t('service_fee_tooltip')"></span></q-tooltip>
|
||||||
</q-badge>
|
</q-badge>
|
||||||
@@ -77,11 +71,8 @@
|
|||||||
<span>OFFLINE</span>
|
<span>OFFLINE</span>
|
||||||
</q-badge>
|
</q-badge>
|
||||||
|
|
||||||
<lnbits-language-dropdown
|
<lnbits-language-dropdown></lnbits-language-dropdown>
|
||||||
@language-changed="handleLanguageChanged({locale: $event})"
|
<q-btn-dropdown v-if="g.user" flat rounded size="sm" class="q-pl-sm">
|
||||||
></lnbits-language-dropdown>
|
|
||||||
|
|
||||||
<q-btn-dropdown v-if="g.user" flat rounded size="md" class="q-pl-sm">
|
|
||||||
<template v-slot:label>
|
<template v-slot:label>
|
||||||
<q-avatar
|
<q-avatar
|
||||||
v-if="g.user?.extra?.picture && g.user?.extra?.picture !== ''"
|
v-if="g.user?.extra?.picture && g.user?.extra?.picture !== ''"
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
<template id="lnbits-language-dropdown">
|
<template id="lnbits-language-dropdown">
|
||||||
<q-btn-dropdown dense flat rounded size="md" class="q-pl-md">
|
<q-btn-dropdown dense flat rounded size="sm" icon="language" class="q-pl-md">
|
||||||
<template v-slot:label>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label v-text="currentLanguage.display"></q-item-label>
|
|
||||||
<q-tooltip><span v-text="currentLanguage.label"></span></q-tooltip>
|
|
||||||
</q-item-section>
|
|
||||||
</template>
|
|
||||||
<q-list v-for="(lang, index) in langs" :key="index">
|
<q-list v-for="(lang, index) in langs" :key="index">
|
||||||
<q-item
|
<q-item
|
||||||
clickable
|
clickable
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
</q-list>
|
</q-list>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
v-if="!g.settings.hideApi"
|
v-if="!HIDE_API"
|
||||||
group="api"
|
group="api"
|
||||||
dense
|
dense
|
||||||
expand-separator
|
expand-separator
|
||||||
@@ -152,7 +152,7 @@
|
|||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
|
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
v-if="!g.settings.hideApi"
|
v-if="!HIDE_API"
|
||||||
group="api"
|
group="api"
|
||||||
dense
|
dense
|
||||||
expand-separator
|
expand-separator
|
||||||
@@ -197,7 +197,7 @@
|
|||||||
</q-card>
|
</q-card>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
v-if="!g.settings.hideApi"
|
v-if="!HIDE_API"
|
||||||
group="api"
|
group="api"
|
||||||
dense
|
dense
|
||||||
expand-separator
|
expand-separator
|
||||||
@@ -244,7 +244,7 @@
|
|||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
|
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
v-if="!g.settings.hideApi"
|
v-if="!HIDE_API"
|
||||||
group="api"
|
group="api"
|
||||||
dense
|
dense
|
||||||
expand-separator
|
expand-separator
|
||||||
@@ -273,7 +273,7 @@
|
|||||||
</q-card>
|
</q-card>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
<q-expansion-item
|
<q-expansion-item
|
||||||
v-if="!g.settings.hideApi"
|
v-if="!HIDE_API"
|
||||||
group="api"
|
group="api"
|
||||||
dense
|
dense
|
||||||
expand-separator
|
expand-separator
|
||||||
|
|||||||
@@ -258,20 +258,15 @@
|
|||||||
class="q-mb-md"
|
class="q-mb-md"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
<div v-if="!g.user.email" class="row"></div>
|
||||||
<div v-if="!g.user.email" class="row">
|
<div v-if="!g.user.email" class="row">
|
||||||
<div
|
{% if "google-auth" in LNBITS_AUTH_METHODS or
|
||||||
v-if="
|
"github-auth" in LNBITS_AUTH_METHODS %}
|
||||||
'google-auth' in g.settings.authMethods ||
|
<div class="col q-pa-sm text-h6">
|
||||||
'github-auth' in g.settings.authMethods
|
|
||||||
"
|
|
||||||
class="col q-pa-sm text-h6"
|
|
||||||
>
|
|
||||||
<span v-text="$t('verify_email')"></span>:
|
<span v-text="$t('verify_email')"></span>:
|
||||||
</div>
|
</div>
|
||||||
<div
|
{%endif%} {% if "google-auth" in LNBITS_AUTH_METHODS %}
|
||||||
v-if="'google-auth' in g.settings.authMethods"
|
<div class="col q-pa-sm">
|
||||||
class="col q-pa-sm"
|
|
||||||
>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
:href="`/api/v1/auth/google?user_id=${g.user.id}`"
|
:href="`/api/v1/auth/google?user_id=${g.user.id}`"
|
||||||
type="a"
|
type="a"
|
||||||
@@ -289,10 +284,8 @@
|
|||||||
<div>Google</div>
|
<div>Google</div>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
<div
|
{%endif%} {% if "github-auth" in LNBITS_AUTH_METHODS %}
|
||||||
v-if="'github-auth' in g.settings.authMethods"
|
<div class="col q-pa-sm">
|
||||||
class="col q-pa-sm"
|
|
||||||
>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
:href="`/api/v1/auth/github?user_id=${g.user.id}`"
|
:href="`/api/v1/auth/github?user_id=${g.user.id}`"
|
||||||
type="a"
|
type="a"
|
||||||
@@ -310,6 +303,7 @@
|
|||||||
<div>GitHub</div>
|
<div>GitHub</div>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
|
{%endif%}
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
@@ -339,16 +333,6 @@
|
|||||||
class="q-mb-md"
|
class="q-mb-md"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
||||||
<q-input
|
|
||||||
v-model="g.user.extra.visible_wallet_count"
|
|
||||||
:label="$t('visible_wallet_count')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
type="number"
|
|
||||||
class="q-mb-md"
|
|
||||||
></q-input>
|
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
v-model="g.user.external_id"
|
v-model="g.user.external_id"
|
||||||
:label="$t('external_id')"
|
:label="$t('external_id')"
|
||||||
@@ -396,11 +380,22 @@
|
|||||||
<span v-text="$t('language')"></span>
|
<span v-text="$t('language')"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<lnbits-language-dropdown
|
<lnbits-language-dropdown />
|
||||||
@language-changed="
|
</div>
|
||||||
siteCustomisationChanged({locale: $event})
|
</div>
|
||||||
"
|
<div class="row q-mb-md">
|
||||||
/>
|
<div class="col-4">
|
||||||
|
<span v-text="$t('visible_wallet_count')"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<q-input
|
||||||
|
v-model="g.user.extra.visible_wallet_count"
|
||||||
|
:label="$t('visible_wallet_count')"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
type="number"
|
||||||
|
class="q-mb-md"
|
||||||
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -412,9 +407,7 @@
|
|||||||
<q-btn
|
<q-btn
|
||||||
v-for="theme in themeOptions"
|
v-for="theme in themeOptions"
|
||||||
:key="theme.name"
|
:key="theme.name"
|
||||||
@click="
|
@click="g.themeChoice = theme.name"
|
||||||
siteCustomisationChanged({themeChoice: theme.name})
|
|
||||||
"
|
|
||||||
:color="theme.color"
|
:color="theme.color"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@@ -434,9 +427,6 @@
|
|||||||
<q-input
|
<q-input
|
||||||
v-model="g.bgimageChoice"
|
v-model="g.bgimageChoice"
|
||||||
:label="$t('background_image')"
|
:label="$t('background_image')"
|
||||||
@update:model-value="
|
|
||||||
siteCustomisationChanged({bgimageChoice: $event})
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('background_image')"></span
|
><span v-text="$t('background_image')"></span
|
||||||
@@ -455,9 +445,6 @@
|
|||||||
round
|
round
|
||||||
icon="gradient"
|
icon="gradient"
|
||||||
v-model="g.gradientChoice"
|
v-model="g.gradientChoice"
|
||||||
@update:model-value="
|
|
||||||
siteCustomisationChanged({gradientChoice: $event})
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('toggle_gradient')"></span
|
><span v-text="$t('toggle_gradient')"></span
|
||||||
@@ -465,69 +452,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">
|
||||||
@@ -539,9 +463,6 @@
|
|||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
v-model="g.darkChoice"
|
v-model="g.darkChoice"
|
||||||
@update:model-value="
|
|
||||||
siteCustomisationChanged({darkChoice: $event})
|
|
||||||
"
|
|
||||||
:icon="$q.dark.isActive ? 'brightness_3' : 'wb_sunny'"
|
:icon="$q.dark.isActive ? 'brightness_3' : 'wb_sunny'"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
@@ -560,9 +481,6 @@
|
|||||||
v-model="g.borderChoice"
|
v-model="g.borderChoice"
|
||||||
:options="borderOptions"
|
:options="borderOptions"
|
||||||
label="Borders"
|
label="Borders"
|
||||||
@update:model-value="
|
|
||||||
siteCustomisationChanged({borderChoice: $event})
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('border_choices')"></span
|
><span v-text="$t('border_choices')"></span
|
||||||
@@ -577,7 +495,7 @@
|
|||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<lnbits-notifications-btn
|
<lnbits-notifications-btn
|
||||||
v-if="g.user"
|
v-if="g.user"
|
||||||
pubkey="g.settings.webpushPubkey"
|
pubkey="{{ WEBPUSH_PUBKEY }}"
|
||||||
></lnbits-notifications-btn>
|
></lnbits-notifications-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -590,9 +508,6 @@
|
|||||||
v-model="g.reactionChoice"
|
v-model="g.reactionChoice"
|
||||||
:options="reactionOptions"
|
:options="reactionOptions"
|
||||||
label="Reactions"
|
label="Reactions"
|
||||||
@update:model-value="
|
|
||||||
siteCustomisationChanged({reactionChoice: $event})
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('payment_reactions')"></span
|
><span v-text="$t('payment_reactions')"></span
|
||||||
@@ -600,15 +515,6 @@
|
|||||||
</q-select>
|
</q-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-card-section>
|
|
||||||
<q-btn
|
|
||||||
@click="resetThemeDefaults"
|
|
||||||
:label="$t('reset_defaults')"
|
|
||||||
filled
|
|
||||||
color="primary"
|
|
||||||
class="float-right q-mb-md"
|
|
||||||
></q-btn>
|
|
||||||
</q-card-section>
|
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
<q-tab-panel name="notifications">
|
<q-tab-panel name="notifications">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
@@ -619,7 +525,7 @@
|
|||||||
></span>
|
></span>
|
||||||
<br />
|
<br />
|
||||||
<q-badge
|
<q-badge
|
||||||
v-if="!g.settings.nostrConfigured"
|
v-if="!LNBITS_NOSTR_CONFIGURED"
|
||||||
v-text="$t('not_connected')"
|
v-text="$t('not_connected')"
|
||||||
></q-badge>
|
></q-badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -639,7 +545,7 @@
|
|||||||
<span v-text="$t('notifications_chat_id')"></span>
|
<span v-text="$t('notifications_chat_id')"></span>
|
||||||
<br />
|
<br />
|
||||||
<q-badge
|
<q-badge
|
||||||
v-if="!g.settings.telegramConfigured"
|
v-if="!LNBITS_TELEGRAM_CONFIGURED"
|
||||||
v-text="$t('not_connected')"
|
v-text="$t('not_connected')"
|
||||||
></q-badge>
|
></q-badge>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -178,7 +178,7 @@
|
|||||||
"
|
"
|
||||||
:clickable="!!reviewsUrl"
|
:clickable="!!reviewsUrl"
|
||||||
@click="openReviews(extension)"
|
@click="openReviews(extension)"
|
||||||
></lnbits-extension-rating>
|
/>
|
||||||
<q-btn-group size="xs" style="margin: 5px 0">
|
<q-btn-group size="xs" style="margin: 5px 0">
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="extension.hasFreeRelease"
|
v-if="extension.hasFreeRelease"
|
||||||
|
|||||||
@@ -53,19 +53,6 @@
|
|||||||
@click="loginData.isPwdRepeat = !loginData.isPwdRepeat"
|
@click="loginData.isPwdRepeat = !loginData.isPwdRepeat"
|
||||||
/> </template
|
/> </template
|
||||||
></q-input>
|
></q-input>
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
v-if="g.settings.hasFirstInstallToken"
|
|
||||||
v-model.trim="loginData.firstInstallToken"
|
|
||||||
:type="loginData.isPwd ? 'password' : 'text'"
|
|
||||||
:label="$t('first_install_token')"
|
|
||||||
><template v-slot:append>
|
|
||||||
<q-icon
|
|
||||||
:name="loginData.isPwd ? 'visibility_off' : 'visibility'"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@click="loginData.isPwd = !loginData.isPwd"
|
|
||||||
/> </template
|
|
||||||
></q-input>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
@click="setPassword()"
|
@click="setPassword()"
|
||||||
unelevated
|
unelevated
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
<div class="home row justify-center items-center">
|
<div class="home row justify-center items-center">
|
||||||
<div
|
<div
|
||||||
class="full-width content-center"
|
class="full-width content-center"
|
||||||
:style="`max-width: ${g.settings.customImage ? '850' : '600'}px; min-height: 55vh;`"
|
:style="`max-width: ${hasCustomImage ? '850' : '600'}px; min-height: 55vh;`"
|
||||||
>
|
>
|
||||||
<div v-if="g.settings.showHomepageElements" class="row q-mb-md">
|
<div v-if="showHomepageElements" class="row q-mb-md">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div>
|
<div>
|
||||||
<h5 v-text="g.settings.siteTitle" class="q-my-none"></h5>
|
<h5 v-text="siteTitle" class="q-my-none"></h5>
|
||||||
<template v-if="$q.screen.gt.sm">
|
<template v-if="$q.screen.gt.sm">
|
||||||
<h6 class="q-my-sm" v-text="g.settings.siteTagline"></h6>
|
<h6 class="q-my-sm" v-text="siteTagline"></h6>
|
||||||
<p class="q-my-sm" v-html="formatDescription"></p>
|
<p class="q-my-sm" v-html="formatDescription"></p>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,10 +31,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div
|
<div
|
||||||
class="col-12"
|
class="col-12"
|
||||||
:class="{
|
:class="{'col-sm-7': hasCustomImage, 'col-lg-6': hasCustomImage}"
|
||||||
'col-sm-7': g.settings.customImage,
|
|
||||||
'col-lg-6': g.settings.customImage
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<div v-if="showClaimLnurl">
|
<div v-if="showClaimLnurl">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
@@ -57,13 +54,12 @@
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
<username-password
|
<username-password
|
||||||
v-if="authMethod != 'user-id-only'"
|
v-if="authMethod != 'user-id-only'"
|
||||||
:allowed_new_users="g.settings.allowRegister"
|
:allowed_new_users="allowRegister"
|
||||||
:auth-methods="g.settings.authMethods"
|
:auth-methods="LNBITS_AUTH_METHODS"
|
||||||
:auth-action="authAction"
|
:auth-action="authAction"
|
||||||
v-model:user-name="username"
|
v-model:user-name="username"
|
||||||
v-model:password_1="password"
|
v-model:password_1="password"
|
||||||
v-model:password_2="passwordRepeat"
|
v-model:password_2="passwordRepeat"
|
||||||
v-model:invitation-code="invitationCode"
|
|
||||||
v-model:reset-key="reset_key"
|
v-model:reset-key="reset_key"
|
||||||
@login="login"
|
@login="login"
|
||||||
@register="register"
|
@register="register"
|
||||||
@@ -74,7 +70,7 @@
|
|||||||
v-if="authAction !== 'reset'"
|
v-if="authAction !== 'reset'"
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
v-if="authAction === 'login' && g.settings.allowRegister"
|
v-if="authAction === 'login' && allowRegister"
|
||||||
class="q-mb-none"
|
class="q-mb-none"
|
||||||
>
|
>
|
||||||
Not registered?
|
Not registered?
|
||||||
@@ -86,9 +82,7 @@
|
|||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
v-else-if="
|
v-else-if="authAction === 'login' && !allowRegister"
|
||||||
authAction === 'login' && !g.settings.allowRegister
|
|
||||||
"
|
|
||||||
class="q-mb-none"
|
class="q-mb-none"
|
||||||
>
|
>
|
||||||
<span v-text="$t('new_user_not_allowed')"></span>
|
<span v-text="$t('new_user_not_allowed')"></span>
|
||||||
@@ -107,7 +101,7 @@
|
|||||||
</username-password>
|
</username-password>
|
||||||
<user-id-only
|
<user-id-only
|
||||||
v-if="authMethod == 'user-id-only'"
|
v-if="authMethod == 'user-id-only'"
|
||||||
:allowed_new_users="g.settings.allowRegister"
|
:allowed_new_users="allowRegister"
|
||||||
v-model:usr="usr"
|
v-model:usr="usr"
|
||||||
v-model:wallet="walletName"
|
v-model:wallet="walletName"
|
||||||
:auth-action="authAction"
|
:auth-action="authAction"
|
||||||
@@ -120,20 +114,16 @@
|
|||||||
</user-id-only>
|
</user-id-only>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="g.settings.customImage" class="col-sm-5 col-lg-6 gt-xs">
|
<div v-if="hasCustomImage" class="col-sm-5 col-lg-6 gt-xs">
|
||||||
<div class="full-height flex flex-center q-pa-lg">
|
<div class="full-height flex flex-center q-pa-lg">
|
||||||
<q-img
|
<q-img :src="hasCustomImage" :ratio="1" width="250px"></q-img>
|
||||||
:src="g.settings.customImage"
|
|
||||||
:ratio="1"
|
|
||||||
width="250px"
|
|
||||||
></q-img>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="g.settings.showHomepageElements">
|
<div v-if="showHomepageElements">
|
||||||
<div class="flex flex-center q-gutter-md q-py-md">
|
<div class="flex flex-center q-gutter-md q-py-md">
|
||||||
<q-btn
|
<q-btn
|
||||||
outline
|
outline
|
||||||
@@ -158,14 +148,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="g.settings.showAdSpace"
|
v-if="adsEnabled"
|
||||||
class="justify-center col-10 q-my-lg row q-col-gutter-sm"
|
class="justify-center col-10 q-my-lg row q-col-gutter-sm"
|
||||||
>
|
>
|
||||||
<a
|
<a :href="ad[0]" v-for="ad in g.ads" class="lnbits-ad col-12 col-md-4">
|
||||||
:href="ad[0]"
|
|
||||||
v-for="ad in g.settings.adSpace"
|
|
||||||
class="lnbits-ad col-12 col-md-4"
|
|
||||||
>
|
|
||||||
<q-img v-if="$q.dark.isActive" :src="ad[1]"></q-img>
|
<q-img v-if="$q.dark.isActive" :src="ad[1]"></q-img>
|
||||||
<q-img v-else :src="ad[2]"></q-img>
|
<q-img v-else :src="ad[2]"></q-img>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -218,26 +218,6 @@
|
|||||||
:label="$t('update_account')"
|
:label="$t('update_account')"
|
||||||
class="q-ml-md"
|
class="q-ml-md"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
|
|
||||||
<q-btn
|
|
||||||
v-if="activeUser.data.id"
|
|
||||||
outline
|
|
||||||
color="primary"
|
|
||||||
class="q-ml-md"
|
|
||||||
>
|
|
||||||
<q-toggle
|
|
||||||
size="xs"
|
|
||||||
color="secondary"
|
|
||||||
v-model="activeUser.data.admin"
|
|
||||||
@update:model-value="toggleAdmin(activeUser.data.id)"
|
|
||||||
:label="
|
|
||||||
activeUser.data.admin
|
|
||||||
? $t('revoke_admin')
|
|
||||||
: $t('make_user_admin')
|
|
||||||
"
|
|
||||||
>
|
|
||||||
</q-toggle>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-else
|
v-else
|
||||||
@click="createUser()"
|
@click="createUser()"
|
||||||
@@ -610,9 +590,9 @@
|
|||||||
<q-btn
|
<q-btn
|
||||||
@click="showAccountPage(props.row.id)"
|
@click="showAccountPage(props.row.id)"
|
||||||
round
|
round
|
||||||
:icon="props.row.is_admin ? 'admin_panel_settings' : 'edit'"
|
icon="edit"
|
||||||
size="sm"
|
size="sm"
|
||||||
:color="props.row.is_admin ? 'primary' : 'secondary'"
|
color="secondary"
|
||||||
class="q-ml-xs"
|
class="q-ml-xs"
|
||||||
>
|
>
|
||||||
<q-tooltip>
|
<q-tooltip>
|
||||||
@@ -625,18 +605,10 @@
|
|||||||
size="xs"
|
size="xs"
|
||||||
v-if="!props.row.is_super_user"
|
v-if="!props.row.is_super_user"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
v-model="props.row.activated"
|
v-model="props.row.is_admin"
|
||||||
@update:model-value="toggleUserActivated(props.row.id)"
|
@update:model-value="toggleAdmin(props.row.id)"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip>Toggle Admin</q-tooltip>
|
||||||
><span
|
|
||||||
v-text="
|
|
||||||
props.row.activated
|
|
||||||
? $t('deactivate')
|
|
||||||
: $t('activate')
|
|
||||||
"
|
|
||||||
></span
|
|
||||||
></q-tooltip>
|
|
||||||
</q-toggle>
|
</q-toggle>
|
||||||
<q-btn
|
<q-btn
|
||||||
round
|
round
|
||||||
|
|||||||
@@ -171,19 +171,6 @@
|
|||||||
><span v-text="$t('camera_tooltip')"></span
|
><span v-text="$t('camera_tooltip')"></span
|
||||||
></q-tooltip>
|
></q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<div
|
|
||||||
v-if="g.settings.walletFeaturedButtonUrl"
|
|
||||||
class="float-right q-mt-sm q-ml-sm"
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
:label="g.settings.walletFeaturedButtonLabel"
|
|
||||||
:icon="g.settings.walletFeaturedButtonIcon || undefined"
|
|
||||||
size="sm"
|
|
||||||
:to="g.settings.walletFeaturedButtonUrl"
|
|
||||||
>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
<lnbits-update-balance
|
<lnbits-update-balance
|
||||||
v-if="$q.screen.gt.md"
|
v-if="$q.screen.gt.md"
|
||||||
:wallet_id="this.g.wallet.id"
|
:wallet_id="this.g.wallet.id"
|
||||||
@@ -212,10 +199,10 @@
|
|||||||
@send-lnurl="handleSendLnurl"
|
@send-lnurl="handleSendLnurl"
|
||||||
:chart-config="chartConfig"
|
:chart-config="chartConfig"
|
||||||
></lnbits-wallet-extra>
|
></lnbits-wallet-extra>
|
||||||
<q-card class="lnbits-wallet-ads" v-if="g.settings.showAdSpace">
|
<q-card class="lnbits-wallet-ads" v-if="AD_SPACE_ENABLED">
|
||||||
<q-card-section class="text-subtitle1">
|
<q-card-section class="text-subtitle1">
|
||||||
<span v-text="g.settings.adSpaceTitle"></span>
|
<span v-text="AD_SPACE_TITLE"></span>
|
||||||
<a :href="ad[0]" class="lnbits-ad" v-for="ad in g.settings.adSpace">
|
<a :href="ad[0]" class="lnbits-ad" v-for="ad in g.ads">
|
||||||
<q-img class="q-mb-xs" v-if="$q.dark.isActive" :src="ad[1]"></q-img>
|
<q-img class="q-mb-xs" v-if="$q.dark.isActive" :src="ad[1]"></q-img>
|
||||||
<q-img class="q-mb-xs" v-else :src="ad[2]"></q-img>
|
<q-img class="q-mb-xs" v-else :src="ad[2]"></q-img>
|
||||||
</a>
|
</a>
|
||||||
@@ -289,7 +276,7 @@
|
|||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
<q-input
|
<q-input
|
||||||
v-if="g.settings.hasHoldinvoice"
|
v-if="has_holdinvoice"
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model="receive.data.payment_hash"
|
v-model="receive.data.payment_hash"
|
||||||
|
|||||||
@@ -45,8 +45,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const SETTINGS = {{ SETTINGS | tojson | safe }}
|
const WINDOW_SETTINGS = {{ WINDOW_SETTINGS | tojson }}
|
||||||
const CURRENCIES = {{ CURRENCIES | tojson | safe }}
|
Object.keys(WINDOW_SETTINGS).forEach(key => {
|
||||||
|
window[key] = WINDOW_SETTINGS[key]
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
{% include('components.vue') %}{% block vue_templates %}{% endblock %} {%
|
{% include('components.vue') %}{% block vue_templates %}{% endblock %} {%
|
||||||
for url in INCLUDED_JS %}
|
for url in INCLUDED_JS %}
|
||||||
|
|||||||
@@ -292,8 +292,7 @@ async def btc_rates(currency: str) -> list[tuple[str, float]]:
|
|||||||
async def btc_price(currency: str) -> float:
|
async def btc_price(currency: str) -> float:
|
||||||
rates = await btc_rates(currency)
|
rates = await btc_rates(currency)
|
||||||
if not rates:
|
if not rates:
|
||||||
logger.warning("Could not fetch any Bitcoin price.")
|
raise ValueError("Could not fetch any Bitcoin price.")
|
||||||
return 0.0
|
|
||||||
elif len(rates) == 1:
|
elif len(rates) == 1:
|
||||||
logger.warning("Could only fetch one Bitcoin price.")
|
logger.warning("Could only fetch one Bitcoin price.")
|
||||||
|
|
||||||
@@ -307,8 +306,7 @@ async def get_fiat_rate_and_price_satoshis(currency: str) -> tuple[float, float]
|
|||||||
f"btc-price-{currency}",
|
f"btc-price-{currency}",
|
||||||
settings.lnbits_exchange_rate_cache_seconds,
|
settings.lnbits_exchange_rate_cache_seconds,
|
||||||
)
|
)
|
||||||
rate = float(100_000_000 / price) if price > 0 else 0.0
|
return float(100_000_000 / price), price
|
||||||
return rate, price
|
|
||||||
|
|
||||||
|
|
||||||
async def get_fiat_rate_satoshis(currency: str) -> float:
|
async def get_fiat_rate_satoshis(currency: str) -> float:
|
||||||
@@ -318,13 +316,9 @@ async def get_fiat_rate_satoshis(currency: str) -> float:
|
|||||||
|
|
||||||
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
|
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
|
||||||
rate = await get_fiat_rate_satoshis(currency)
|
rate = await get_fiat_rate_satoshis(currency)
|
||||||
if rate > 0:
|
return int(amount * (rate))
|
||||||
return int(amount * rate)
|
|
||||||
raise ValueError(f"Could not get exchange rate for {currency}.")
|
|
||||||
|
|
||||||
|
|
||||||
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
|
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
|
||||||
rate = await get_fiat_rate_satoshis(currency)
|
rate = await get_fiat_rate_satoshis(currency)
|
||||||
if rate > 0:
|
return float(amount / rate)
|
||||||
return float(amount / rate)
|
|
||||||
raise ValueError(f"Could not get exchange rate for {currency}.")
|
|
||||||
|
|||||||
@@ -14,13 +14,6 @@ from lnbits.settings import settings
|
|||||||
|
|
||||||
def log_server_info():
|
def log_server_info():
|
||||||
logger.info("LNbits Info")
|
logger.info("LNbits Info")
|
||||||
if settings.first_install:
|
|
||||||
logger.success("This is a fresh install of LNbits.")
|
|
||||||
if settings.first_install_token:
|
|
||||||
logger.success(
|
|
||||||
f"FIRST_INSTALL_TOKEN: `{settings.first_install_token}`. "
|
|
||||||
"Please provide this token on /first_install."
|
|
||||||
)
|
|
||||||
logger.info(f"Version: {settings.version}")
|
logger.info(f"Version: {settings.version}")
|
||||||
logger.info(f"Baseurl: {settings.lnbits_baseurl}")
|
logger.info(f"Baseurl: {settings.lnbits_baseurl}")
|
||||||
logger.info(f"Host: {settings.host}")
|
logger.info(f"Host: {settings.host}")
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from .corelightning import CoreLightningWallet as CLightningWallet
|
|||||||
from .corelightningrest import CoreLightningRestWallet
|
from .corelightningrest import CoreLightningRestWallet
|
||||||
from .eclair import EclairWallet
|
from .eclair import EclairWallet
|
||||||
from .fake import FakeWallet
|
from .fake import FakeWallet
|
||||||
|
from .lightspark import LightsparkSparkWallet
|
||||||
from .lnbits import LNbitsWallet
|
from .lnbits import LNbitsWallet
|
||||||
from .lndgrpc import LndWallet
|
from .lndgrpc import LndWallet
|
||||||
from .lndrest import LndRestWallet
|
from .lndrest import LndRestWallet
|
||||||
@@ -29,7 +30,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
|
||||||
@@ -70,13 +70,13 @@ __all__ = [
|
|||||||
"FakeWallet",
|
"FakeWallet",
|
||||||
"LNPayWallet",
|
"LNPayWallet",
|
||||||
"LNbitsWallet",
|
"LNbitsWallet",
|
||||||
|
"LightsparkSparkWallet",
|
||||||
"LnTipsWallet",
|
"LnTipsWallet",
|
||||||
"LndRestWallet",
|
"LndRestWallet",
|
||||||
"LndWallet",
|
"LndWallet",
|
||||||
"NWCWallet",
|
"NWCWallet",
|
||||||
"OpenNodeWallet",
|
"OpenNodeWallet",
|
||||||
"PhoenixdWallet",
|
"PhoenixdWallet",
|
||||||
"SparkL2Wallet",
|
|
||||||
"SparkWallet",
|
"SparkWallet",
|
||||||
"StrikeWallet",
|
"StrikeWallet",
|
||||||
"VoidWallet",
|
"VoidWallet",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
import uuid
|
import uuid
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -10,7 +12,7 @@ import httpx
|
|||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.helpers import normalize_endpoint
|
from lnbits.helpers import download_url, normalize_endpoint
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from .base import (
|
from .base import (
|
||||||
@@ -29,7 +31,7 @@ class SparkSidecarError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SparkL2Wallet(Wallet):
|
class LightsparkSparkWallet(Wallet):
|
||||||
"""
|
"""
|
||||||
Spark L2 funding source via a local sidecar service.
|
Spark L2 funding source via a local sidecar service.
|
||||||
|
|
||||||
@@ -47,20 +49,21 @@ class SparkL2Wallet(Wallet):
|
|||||||
|
|
||||||
self.endpoint = "http://127.0.0.1:8765"
|
self.endpoint = "http://127.0.0.1:8765"
|
||||||
self._api_key = uuid.uuid4().hex
|
self._api_key = uuid.uuid4().hex
|
||||||
|
if settings.spark_l2_internal_sidecar_version:
|
||||||
if settings.spark_l2_external_endpoint:
|
self._sidecar_version = settings.spark_l2_internal_sidecar_version
|
||||||
|
self.sidecar_task = asyncio.create_task(self._start_sidecar())
|
||||||
|
logger.info(f"Internal Spark sidecar ({self._sidecar_version}).")
|
||||||
|
elif settings.spark_l2_external_endpoint:
|
||||||
self.endpoint = normalize_endpoint(
|
self.endpoint = normalize_endpoint(
|
||||||
cast(str, settings.spark_l2_external_endpoint)
|
cast(str, settings.spark_l2_external_endpoint)
|
||||||
)
|
)
|
||||||
|
self._api_key = settings.spark_l2_external_api_key
|
||||||
logger.info(f"Using external Spark sidecar endpoint: {self.endpoint}")
|
logger.info(f"Using external Spark sidecar endpoint: {self.endpoint}")
|
||||||
else:
|
else:
|
||||||
logger.error("No Spark sidecar endpoint configuration found.")
|
logger.error(
|
||||||
|
"No Spark sidecar configuration found. Please set either "
|
||||||
if settings.spark_l2_external_api_key:
|
"spark_l2_internal_sidecar_version or spark_l2_external_endpoint."
|
||||||
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}
|
headers = {"User-Agent": settings.user_agent, "X-Api-Key": self._api_key}
|
||||||
|
|
||||||
@@ -73,9 +76,38 @@ class SparkL2Wallet(Wallet):
|
|||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
try:
|
try:
|
||||||
await self.client.aclose()
|
await self.client.aclose()
|
||||||
|
self.sidecar_task.cancel()
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
logger.warning(f"Error closing wallet connection: {e}")
|
logger.warning(f"Error closing wallet connection: {e}")
|
||||||
|
|
||||||
|
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 status(self) -> StatusResponse:
|
async def status(self) -> StatusResponse:
|
||||||
try:
|
try:
|
||||||
res = await self._request("POST", "/v1/balance")
|
res = await self._request("POST", "/v1/balance")
|
||||||
@@ -87,7 +119,6 @@ class SparkL2Wallet(Wallet):
|
|||||||
return StatusResponse("Spark sidecar: missing balance.", 0)
|
return StatusResponse("Spark sidecar: missing balance.", 0)
|
||||||
return StatusResponse(None, int(balance_sats) * 1000)
|
return StatusResponse(None, int(balance_sats) * 1000)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(e)
|
|
||||||
return StatusResponse(f"Spark sidecar status error: {e}", 0)
|
return StatusResponse(f"Spark sidecar status error: {e}", 0)
|
||||||
|
|
||||||
async def create_invoice(
|
async def create_invoice(
|
||||||
@@ -118,9 +149,8 @@ class SparkL2Wallet(Wallet):
|
|||||||
bolt11 = res.get("payment_request")
|
bolt11 = res.get("payment_request")
|
||||||
checking_id = res.get("checking_id")
|
checking_id = res.get("checking_id")
|
||||||
if not bolt11 or not checking_id:
|
if not bolt11 or not checking_id:
|
||||||
return InvoiceResponse(
|
raise SparkSidecarError(
|
||||||
ok=False,
|
"Spark sidecar invoice response missing fields."
|
||||||
error_message="Spark sidecar invoice response missing fields.",
|
|
||||||
)
|
)
|
||||||
self.pending_invoices.append(checking_id)
|
self.pending_invoices.append(checking_id)
|
||||||
|
|
||||||
@@ -128,7 +158,7 @@ class SparkL2Wallet(Wallet):
|
|||||||
ok=True,
|
ok=True,
|
||||||
payment_request=bolt11,
|
payment_request=bolt11,
|
||||||
checking_id=checking_id,
|
checking_id=checking_id,
|
||||||
preimage=res.get("preimage", None),
|
preimage=res.get("preimage"),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return InvoiceResponse(ok=False, error_message=str(e))
|
return InvoiceResponse(ok=False, error_message=str(e))
|
||||||
@@ -140,8 +170,7 @@ class SparkL2Wallet(Wallet):
|
|||||||
payment_hash = None
|
payment_hash = None
|
||||||
try:
|
try:
|
||||||
payment_hash = bolt11_decode(bolt11).payment_hash
|
payment_hash = bolt11_decode(bolt11).payment_hash
|
||||||
except Exception as exc:
|
except Exception:
|
||||||
logger.warning(exc)
|
|
||||||
payment_hash = None
|
payment_hash = None
|
||||||
payload = {
|
payload = {
|
||||||
"bolt11": bolt11,
|
"bolt11": bolt11,
|
||||||
@@ -151,9 +180,8 @@ class SparkL2Wallet(Wallet):
|
|||||||
res = await self._request("POST", "/v1/payments", payload)
|
res = await self._request("POST", "/v1/payments", payload)
|
||||||
checking_id = payment_hash or res.get("checking_id")
|
checking_id = payment_hash or res.get("checking_id")
|
||||||
if not checking_id:
|
if not checking_id:
|
||||||
return PaymentResponse(
|
raise SparkSidecarError(
|
||||||
ok=False,
|
"Spark sidecar payment response missing checking_id."
|
||||||
error_message="Spark sidecar payment response missing checking_id.",
|
|
||||||
)
|
)
|
||||||
status = res.get("status")
|
status = res.get("status")
|
||||||
fee_msat = res.get("fee_msat")
|
fee_msat = res.get("fee_msat")
|
||||||
@@ -177,8 +205,7 @@ class SparkL2Wallet(Wallet):
|
|||||||
if not status:
|
if not status:
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
return self._map_invoice_status(status)
|
return self._map_invoice_status(status)
|
||||||
except Exception as exc:
|
except Exception:
|
||||||
logger.warning(exc)
|
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
|
|
||||||
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
||||||
@@ -198,82 +225,9 @@ class SparkL2Wallet(Wallet):
|
|||||||
if mapped.failed:
|
if mapped.failed:
|
||||||
return PaymentFailedStatus()
|
return PaymentFailedStatus()
|
||||||
return PaymentPendingStatus()
|
return PaymentPendingStatus()
|
||||||
except Exception as exc:
|
except Exception:
|
||||||
logger.warning(exc)
|
|
||||||
return PaymentPendingStatus()
|
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:
|
def _map_invoice_status(self, status: str) -> PaymentStatus:
|
||||||
success = {
|
success = {
|
||||||
"LIGHTNING_PAYMENT_RECEIVED",
|
"LIGHTNING_PAYMENT_RECEIVED",
|
||||||
@@ -319,3 +273,145 @@ class SparkL2Wallet(Wallet):
|
|||||||
if mapped.failed:
|
if mapped.failed:
|
||||||
return False
|
return False
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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 _start_sidecar(self):
|
||||||
|
logger.info("Starting Spark sidecar")
|
||||||
|
node_path = shutil.which("node")
|
||||||
|
if not node_path:
|
||||||
|
logger.error("Node.js not found in PATH, cannot start Spark sidecar")
|
||||||
|
return
|
||||||
|
logger.info(f"Node.js found: {node_path}")
|
||||||
|
|
||||||
|
repo, version = "spark_sidecar", self._sidecar_version
|
||||||
|
node_modules_path = Path(self._sidecar_path, f"{repo}-{version}")
|
||||||
|
await self._prepare_sidecar(repo, version, node_modules_path)
|
||||||
|
|
||||||
|
await self._start_sidecar_process(node_path, node_modules_path)
|
||||||
|
|
||||||
|
async def _prepare_sidecar(self, repo: str, version: str, node_modules_path: Path):
|
||||||
|
if not Path(node_modules_path, "package.json").is_file():
|
||||||
|
await self._download_sidecar(repo, version)
|
||||||
|
else:
|
||||||
|
logger.info("Spark sidecar already downloaded.")
|
||||||
|
|
||||||
|
if not Path(node_modules_path, "node_modules").is_dir():
|
||||||
|
self._install_sidecar_packages(node_modules_path)
|
||||||
|
else:
|
||||||
|
logger.info("Spark sidecar npm dependencies already installed.")
|
||||||
|
|
||||||
|
def _install_sidecar_packages(self, node_modules_path: Path):
|
||||||
|
logger.info(f"Installing Spark sidecar npm dependencies {node_modules_path}")
|
||||||
|
npm_path = shutil.which("npm")
|
||||||
|
if not npm_path:
|
||||||
|
logger.error("npm not found in PATH, cannot start Spark sidecar")
|
||||||
|
return
|
||||||
|
logger.info(f"npm found: {npm_path}")
|
||||||
|
|
||||||
|
result = subprocess.run( # noqa: S603
|
||||||
|
[npm_path, "install"],
|
||||||
|
cwd=str(node_modules_path),
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
shell=False,
|
||||||
|
check=True, # raises an exception if npm fails
|
||||||
|
)
|
||||||
|
logger.info("Spark sidecar npm dependencies installed.")
|
||||||
|
logger.info("npm install output:")
|
||||||
|
logger.info(result.stdout)
|
||||||
|
logger.error(result.stderr)
|
||||||
|
|
||||||
|
async def _start_sidecar_process(self, node_path: str, node_modules_path: Path):
|
||||||
|
logger.info("Starting Spark sidecar node process.")
|
||||||
|
|
||||||
|
env = {
|
||||||
|
"SPARK_NETWORK": settings.spark_l2_network,
|
||||||
|
"SPARK_SIDECAR_API_KEY": self._api_key or "",
|
||||||
|
"SPARK_PAY_WAIT_MS": str(settings.spark_l2_pay_wait_ms),
|
||||||
|
"SPARK_MNEMONIC": str(settings.spark_l2_mnemonic),
|
||||||
|
}
|
||||||
|
|
||||||
|
process = subprocess.Popen( # noqa: S603
|
||||||
|
[node_path, "server.mjs"],
|
||||||
|
env=env,
|
||||||
|
cwd=str(node_modules_path),
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
shell=False,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Started Spark sidecar node process.")
|
||||||
|
await asyncio.to_thread(self._log_process_output, process)
|
||||||
|
|
||||||
|
async def _download_sidecar(self, repo: str, version: str):
|
||||||
|
zip_path = Path(self._sidecar_path, f"{repo}.zip")
|
||||||
|
logger.info(f"⏳ Downloading Spark sidecar to {zip_path}")
|
||||||
|
Path(zip_path).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
await asyncio.to_thread(
|
||||||
|
download_url,
|
||||||
|
f"https://github.com/lnbits/{repo}/archive/refs/tags/v{version}.zip",
|
||||||
|
zip_path,
|
||||||
|
)
|
||||||
|
logger.info("✅ Downloaded Spark sidecar.")
|
||||||
|
|
||||||
|
logger.info("⏳ Extracting Spark sidecar.")
|
||||||
|
|
||||||
|
shutil.unpack_archive(
|
||||||
|
zip_path,
|
||||||
|
self._sidecar_path,
|
||||||
|
)
|
||||||
|
logger.info("✅ Extracted Spark sidecar.")
|
||||||
|
shutil.rmtree(zip_path, ignore_errors=True)
|
||||||
|
# todo: remove zip
|
||||||
|
|
||||||
|
def _log_process_output(self, process: subprocess.Popen):
|
||||||
|
if process.stdout:
|
||||||
|
for line in process.stdout:
|
||||||
|
logger.warning(f"[Lightspark]: {line}", end="")
|
||||||
|
else:
|
||||||
|
logger.error(" No output captured for Spark sidecar.")
|
||||||
Generated
+13
-19
@@ -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
@@ -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",
|
||||||
|
|||||||
Generated
+1
-1
@@ -4638,4 +4638,4 @@ migration = ["psycopg2-binary"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.10,<3.13"
|
python-versions = ">=3.10,<3.13"
|
||||||
content-hash = "f63a5be62359f837513e01a8b0e5282a3b4509ea97439ba0c8226ccf3c158ceb"
|
content-hash = "6c17d723baa390c36ecf6f861166878f2619acd47f878cf9253c5614ba25b9ea"
|
||||||
|
|||||||
+2
-1
@@ -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" }]
|
||||||
@@ -9,6 +9,7 @@ readme = "README.md"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bech32==1.2.0",
|
"bech32==1.2.0",
|
||||||
"click==8.3.1",
|
"click==8.3.1",
|
||||||
|
"ecdsa==0.19.1",
|
||||||
"fastapi==0.116.1",
|
"fastapi==0.116.1",
|
||||||
"starlette==0.47.1",
|
"starlette==0.47.1",
|
||||||
"httpx==0.27.2",
|
"httpx==0.27.2",
|
||||||
|
|||||||
+9
-145
@@ -297,148 +297,6 @@ async def test_register_ok(http_client: AsyncClient):
|
|||||||
), f"Expected 1 default wallet, not {len(user.wallets)}."
|
), f"Expected 1 default wallet, not {len(user.wallets)}."
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_register_no_activation_code(
|
|
||||||
http_client: AsyncClient, settings: Settings
|
|
||||||
):
|
|
||||||
settings.lnbits_require_user_activation = True
|
|
||||||
|
|
||||||
tiny_id = shortuuid.uuid()[:8]
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/register",
|
|
||||||
json={
|
|
||||||
"username": f"u21.{tiny_id}",
|
|
||||||
"password": "secret1234",
|
|
||||||
"password_repeat": "secret1234",
|
|
||||||
"email": f"u21.{tiny_id}@lnbits.com",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert response.json().get("detail") == "No activation method provided."
|
|
||||||
|
|
||||||
settings.lnbits_user_activation_by_invitation_code = True
|
|
||||||
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/register",
|
|
||||||
json={
|
|
||||||
"username": f"u21.{tiny_id}",
|
|
||||||
"password": "secret1234",
|
|
||||||
"password_repeat": "secret1234",
|
|
||||||
"email": f"u21.{tiny_id}@lnbits.com",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 400, "User creation blocked without activation code."
|
|
||||||
assert response.json().get("detail") == "Invitation code cannot be empty."
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_register_invalid_activation_code(
|
|
||||||
http_client: AsyncClient, settings: Settings
|
|
||||||
):
|
|
||||||
settings.lnbits_require_user_activation = True
|
|
||||||
settings.lnbits_user_activation_by_invitation_code = True
|
|
||||||
settings.lnbits_register_reusable_activation_code = "foo"
|
|
||||||
settings.lnbits_register_one_time_activation_codes = ["baz", "qux"]
|
|
||||||
|
|
||||||
tiny_id = shortuuid.uuid()[:8]
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/register",
|
|
||||||
json={
|
|
||||||
"username": f"u21.{tiny_id}",
|
|
||||||
"password": "secret1234",
|
|
||||||
"password_repeat": "secret1234",
|
|
||||||
"email": f"u21.{tiny_id}@lnbits.com",
|
|
||||||
"invitation_code": "bar",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 400
|
|
||||||
assert response.json().get("detail") == "Invalid invitation code."
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_register_reusable_activation_code(
|
|
||||||
http_client: AsyncClient, settings: Settings
|
|
||||||
):
|
|
||||||
settings.lnbits_require_user_activation = True
|
|
||||||
settings.lnbits_user_activation_by_invitation_code = True
|
|
||||||
settings.lnbits_register_reusable_activation_code = "foo"
|
|
||||||
|
|
||||||
tiny_id = shortuuid.uuid()[:8]
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/register",
|
|
||||||
json={
|
|
||||||
"username": f"u21.{tiny_id}",
|
|
||||||
"password": "secret1234",
|
|
||||||
"password_repeat": "secret1234",
|
|
||||||
"email": f"u21.{tiny_id}@lnbits.com",
|
|
||||||
"invitation_code": "foo",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200, "User created with reusable code."
|
|
||||||
assert response.json().get("access_token") is not None
|
|
||||||
|
|
||||||
# Register again with the same code
|
|
||||||
tiny_id = shortuuid.uuid()[:8]
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/register",
|
|
||||||
json={
|
|
||||||
"username": f"u21.{tiny_id}",
|
|
||||||
"password": "secret1234",
|
|
||||||
"password_repeat": "secret1234",
|
|
||||||
"email": f"u21.{tiny_id}@lnbits.com",
|
|
||||||
"invitation_code": "foo",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200, "User created with reusable code."
|
|
||||||
assert response.json().get("access_token") is not None
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_register_one_time_activation_code(
|
|
||||||
http_client: AsyncClient, settings: Settings
|
|
||||||
):
|
|
||||||
settings.lnbits_require_user_activation = True
|
|
||||||
settings.lnbits_user_activation_by_invitation_code = True
|
|
||||||
settings.lnbits_register_reusable_activation_code = "foo"
|
|
||||||
settings.lnbits_register_one_time_activation_codes = ["baz", "qux"]
|
|
||||||
|
|
||||||
tiny_id = shortuuid.uuid()[:8]
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/register",
|
|
||||||
json={
|
|
||||||
"username": f"u21.{tiny_id}",
|
|
||||||
"password": "secret1234",
|
|
||||||
"password_repeat": "secret1234",
|
|
||||||
"email": f"u21.{tiny_id}@lnbits.com",
|
|
||||||
"invitation_code": "baz",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200, "User created with one-time code."
|
|
||||||
assert response.json().get("access_token") is not None
|
|
||||||
|
|
||||||
# Register again with the same code
|
|
||||||
tiny_id = shortuuid.uuid()[:8]
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/register",
|
|
||||||
json={
|
|
||||||
"username": f"u21.{tiny_id}",
|
|
||||||
"password": "secret1234",
|
|
||||||
"password_repeat": "secret1234",
|
|
||||||
"email": f"u21.{tiny_id}@lnbits.com",
|
|
||||||
"invitation_code": "baz",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 400, "Invalid invitation code."
|
|
||||||
assert response.json().get("detail") == "Invalid invitation code."
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_register_email_twice(http_client: AsyncClient):
|
async def test_register_email_twice(http_client: AsyncClient):
|
||||||
tiny_id = shortuuid.uuid()[:8]
|
tiny_id = shortuuid.uuid()[:8]
|
||||||
@@ -2158,7 +2016,9 @@ async def test_api_update_user_labels(http_client: AsyncClient):
|
|||||||
]
|
]
|
||||||
data = UpdateUser(user_id=user.id, username=f"u{tiny_id}", extra=user.extra)
|
data = UpdateUser(user_id=user.id, username=f"u{tiny_id}", extra=user.extra)
|
||||||
assert data.extra
|
assert data.extra
|
||||||
response = await http_client.patch("/api/v1/auth?usr=" + user.id, json=data.dict())
|
response = await http_client.put(
|
||||||
|
"/api/v1/auth/update?usr=" + user.id, json=data.dict()
|
||||||
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
user_data = response.json()
|
user_data = response.json()
|
||||||
assert len(user_data["extra"]["labels"]) == 2
|
assert len(user_data["extra"]["labels"]) == 2
|
||||||
@@ -2168,14 +2028,18 @@ async def test_api_update_user_labels(http_client: AsyncClient):
|
|||||||
assert user_data["extra"]["labels"][1]["color"] == "#00FF00"
|
assert user_data["extra"]["labels"][1]["color"] == "#00FF00"
|
||||||
|
|
||||||
data.extra.labels = []
|
data.extra.labels = []
|
||||||
response = await http_client.patch("/api/v1/auth?usr=" + user.id, json=data.dict())
|
response = await http_client.put(
|
||||||
|
"/api/v1/auth/update?usr=" + user.id, json=data.dict()
|
||||||
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
user_data = response.json()
|
user_data = response.json()
|
||||||
assert len(user_data["extra"]["labels"]) == 0
|
assert len(user_data["extra"]["labels"]) == 0
|
||||||
|
|
||||||
json_data = data.dict()
|
json_data = data.dict()
|
||||||
json_data["extra"] = {"labels": [{"name": "label + 01", "color": "#FF0000"}]}
|
json_data["extra"] = {"labels": [{"name": "label + 01", "color": "#FF0000"}]}
|
||||||
response = await http_client.patch("/api/v1/auth?usr=" + user.id, json=json_data)
|
response = await http_client.put(
|
||||||
|
"/api/v1/auth/update?usr=" + user.id, json=json_data
|
||||||
|
)
|
||||||
|
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import jwt
|
|
||||||
import pytest
|
import pytest
|
||||||
import shortuuid
|
import shortuuid
|
||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
|
|
||||||
from lnbits.core.crud.wallets import get_wallets
|
from lnbits.core.crud.wallets import get_wallets
|
||||||
from lnbits.core.models import AccessTokenPayload, Payment
|
|
||||||
from lnbits.core.models.users import Account, User
|
from lnbits.core.models.users import Account, User
|
||||||
from lnbits.core.services.users import create_user_account
|
from lnbits.core.services.users import create_user_account
|
||||||
from lnbits.settings import Settings
|
from lnbits.settings import Settings
|
||||||
@@ -612,127 +610,3 @@ async def test_delete_and_undelete_wallet(http_client: AsyncClient, superuser_to
|
|||||||
undeleted_wallet = next((w for w in wallets if w.id == wallet_id), None)
|
undeleted_wallet = next((w for w in wallets if w.id == wallet_id), None)
|
||||||
assert undeleted_wallet is not None
|
assert undeleted_wallet is not None
|
||||||
assert undeleted_wallet.deleted is False
|
assert undeleted_wallet.deleted is False
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_user_activation(
|
|
||||||
http_client: AsyncClient, invoice: Payment, settings: Settings, superuser_token: str
|
|
||||||
):
|
|
||||||
|
|
||||||
# Register a new user
|
|
||||||
username = f"u21.{shortuuid.uuid()[:8]}"
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth/register",
|
|
||||||
json={
|
|
||||||
"username": username,
|
|
||||||
"password": "secret1234",
|
|
||||||
"password_repeat": "secret1234",
|
|
||||||
"email": f"{username}@lnbits.com",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
access_token = response.json().get("access_token")
|
|
||||||
assert response.status_code == 200, "User created."
|
|
||||||
assert response.json().get("access_token") is not None
|
|
||||||
|
|
||||||
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
|
|
||||||
access_token_payload = AccessTokenPayload(**payload)
|
|
||||||
user_id = access_token_payload.usr
|
|
||||||
assert user_id is not None
|
|
||||||
|
|
||||||
# Login works
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth", json={"username": username, "password": "secret1234"}
|
|
||||||
)
|
|
||||||
assert response.status_code == 200, "User logs in OK"
|
|
||||||
|
|
||||||
# Deactivate the user
|
|
||||||
respones = await http_client.put(
|
|
||||||
f"/users/api/v1/user/{user_id}/activate",
|
|
||||||
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert respones.status_code == 200, "User deactivated."
|
|
||||||
assert respones.json().get("message") == "User deactivated."
|
|
||||||
|
|
||||||
# Login should now fail
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth", json={"username": username, "password": "secret1234"}
|
|
||||||
)
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert response.json().get("detail") == "Invalid credentials."
|
|
||||||
|
|
||||||
response = await http_client.get(
|
|
||||||
"/api/v1/auth",
|
|
||||||
headers={"Authorization": f"Bearer {access_token}"},
|
|
||||||
)
|
|
||||||
assert response.status_code == 401
|
|
||||||
assert response.json().get("detail") == "User not found."
|
|
||||||
|
|
||||||
wallets = await get_wallets(user_id=user_id)
|
|
||||||
assert len(wallets) == 1, "User's wallet still exists."
|
|
||||||
wallet = wallets[0]
|
|
||||||
|
|
||||||
response = await http_client.get(
|
|
||||||
"/api/v1/payments/paginated",
|
|
||||||
params={"limit": 2},
|
|
||||||
headers={"x-Api-Key": wallet.inkey},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 404
|
|
||||||
assert response.json().get("detail") == "Wallet not found."
|
|
||||||
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/payments",
|
|
||||||
json={
|
|
||||||
"out": False,
|
|
||||||
"amount": 1000,
|
|
||||||
"memo": "test payment",
|
|
||||||
},
|
|
||||||
headers={"x-Api-Key": wallet.inkey},
|
|
||||||
)
|
|
||||||
assert response.status_code == 404
|
|
||||||
assert response.json().get("detail") == "Wallet not found."
|
|
||||||
|
|
||||||
data = {"out": True, "bolt11": invoice.bolt11}
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/payments",
|
|
||||||
json=data,
|
|
||||||
headers={"x-Api-Key": wallet.adminkey},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 404
|
|
||||||
assert response.json().get("detail") == "Wallet not found."
|
|
||||||
|
|
||||||
# Reactivate the user
|
|
||||||
response = await http_client.put(
|
|
||||||
f"/users/api/v1/user/{user_id}/activate",
|
|
||||||
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.json().get("message") == "User activated."
|
|
||||||
|
|
||||||
# Login should now pass
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/auth", json={"username": username, "password": "secret1234"}
|
|
||||||
)
|
|
||||||
assert response.status_code == 200, "User logs in OK again."
|
|
||||||
|
|
||||||
response = await http_client.get(
|
|
||||||
"/api/v1/payments/paginated",
|
|
||||||
params={"limit": 2},
|
|
||||||
headers={"x-Api-Key": wallet.inkey},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
|
|
||||||
response = await http_client.post(
|
|
||||||
"/api/v1/payments",
|
|
||||||
json={
|
|
||||||
"out": False,
|
|
||||||
"amount": 1000,
|
|
||||||
"memo": "test payment",
|
|
||||||
},
|
|
||||||
headers={"x-Api-Key": wallet.inkey},
|
|
||||||
)
|
|
||||||
assert response.status_code == 201
|
|
||||||
|
|||||||
@@ -341,7 +341,3 @@ def _settings_cleanup(settings: Settings):
|
|||||||
settings.lnbits_max_outgoing_payment_amount_sats = 10_000_000_100
|
settings.lnbits_max_outgoing_payment_amount_sats = 10_000_000_100
|
||||||
settings.lnbits_max_incoming_payment_amount_sats = 10_000_000_200
|
settings.lnbits_max_incoming_payment_amount_sats = 10_000_000_200
|
||||||
settings.stripe_limits = FiatProviderLimits()
|
settings.stripe_limits = FiatProviderLimits()
|
||||||
settings.lnbits_require_user_activation = False
|
|
||||||
settings.lnbits_user_activation_by_invitation_code = False
|
|
||||||
settings.lnbits_register_reusable_activation_code = ""
|
|
||||||
settings.lnbits_register_one_time_activation_codes = []
|
|
||||||
|
|||||||
@@ -7,11 +7,7 @@ from lnbits import bolt11
|
|||||||
from lnbits.core.crud import get_standalone_payment, update_payment
|
from lnbits.core.crud import get_standalone_payment, update_payment
|
||||||
from lnbits.core.crud.wallets import create_wallet, get_wallet
|
from lnbits.core.crud.wallets import create_wallet, get_wallet
|
||||||
from lnbits.core.models import CreateInvoice, Payment, PaymentState
|
from lnbits.core.models import CreateInvoice, Payment, PaymentState
|
||||||
from lnbits.core.services import (
|
from lnbits.core.services import fee_reserve_total, get_balance_delta
|
||||||
check_payment_status,
|
|
||||||
fee_reserve_total,
|
|
||||||
get_balance_delta,
|
|
||||||
)
|
|
||||||
from lnbits.core.services.payments import pay_invoice, update_wallet_balance
|
from lnbits.core.services.payments import pay_invoice, update_wallet_balance
|
||||||
from lnbits.core.services.users import create_user_account
|
from lnbits.core.services.users import create_user_account
|
||||||
from lnbits.exceptions import PaymentError
|
from lnbits.exceptions import PaymentError
|
||||||
@@ -359,7 +355,7 @@ async def test_pay_hold_invoice_check_pending_and_fail_cancel_payment_task_in_me
|
|||||||
assert payment_db_after_settlement is not None
|
assert payment_db_after_settlement is not None
|
||||||
|
|
||||||
# payment is failed
|
# payment is failed
|
||||||
status = await check_payment_status(payment_db_after_settlement)
|
status = await payment_db_after_settlement.check_status()
|
||||||
assert not status.paid
|
assert not status.paid
|
||||||
assert status.failed
|
assert status.failed
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user