Compare commits

..
Author SHA1 Message Date
dni ⚡andGitHub b77c103020 chore: update to v1.5.2-rc1 (#3876) 2026-03-18 12:39:22 +01:00
1f34357ab7 fix: docker sparkl2 and ad multiplicity (#3875)
Co-authored-by: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com>
2026-03-18 12:36:20 +01:00
d9eda48298 feat: Improvements to settings > routing fee reserve settings UX (#3857)
Co-authored-by: dni  <office@dnilabs.com>
2026-03-18 11:29:29 +00:00
dni ⚡andGitHub 1be2d1a680 feat: docker release-rc add tag for latest rc release latest-rc (#3872) 2026-03-18 10:25:22 +00:00
dni ⚡andGitHub b55ae9daaf chore: update to v1.5.1 (#3874) 2026-03-18 11:14:17 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1f98aa2cdb chore(deps): bump werkzeug from 3.1.5 to 3.1.6 (#3807)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-10 12:16:20 +02:00
blackcoffeexbtandGitHub 55ec3f1812 feat: admin settings UX improvements (#3859) 2026-03-10 11:57:45 +02:00
dni ⚡andGitHub 7880044483 chore: update to v1.5.1-rc1 (#3858) 2026-03-09 09:10:48 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
28b3f79d9b chore(deps-dev): bump minimatch from 3.1.2 to 3.1.5 (#3815)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 08:29:20 +01:00
Tiago VasconcelosandGitHub 4c704305ac Fix: mobile view wallet issues (#3850) 2026-03-05 10:36:52 +00:00
c47fd2a4d1 SparkL2 updated docs (#3810)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-03-05 10:19:19 +02:00
ArcandGitHub 1255df43e6 fix: default theme change for 1.5.1 (#3838) 2026-03-05 10:09:54 +02:00
ArcandGitHub 6ccc10d327 feat: currency helper (#3849) 2026-03-05 10:09:23 +02:00
dni ⚡andGitHub 8bcf5e4f70 CI: pin boltz to version (#3852) 2026-03-05 09:54:07 +02:00
Vlad StanandGitHub 04ec643c93 feat: set sidecar mnemonic if missing (#3839) 2026-03-04 16:13:46 +02:00
dni ⚡ f93efbfb3a chore: update to version 1.5.0 (#3813) 2026-02-25 07:54:35 +01:00
dni ⚡ 4429c0cdb6 chore: update to v1.5.0-rc2 (#3806) 2026-02-25 07:54:33 +01:00
Vlad Stananddni ⚡ 2bdb89b4b9 fix: paypal subscriptions (#3797) 2026-02-25 07:54:31 +01:00
dni ⚡ 75754598e1 feat: restructure docker images, add sparkl2 docker image (#3794) 2026-02-25 07:54:30 +01:00
dni ⚡ b25a3c2bb4 feat: hide first_install_token if is not required (#3795) 2026-02-25 07:54:28 +01:00
dni ⚡ e7c4de6506 chore: update lnbits to version v1.5.0-rc1 (#3793) 2026-02-25 07:54:27 +01:00
c102f10fab feat: Spark L2 (#3715)
Co-authored-by: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-02-25 07:54:18 +01:00
dependabot[bot]dni ⚡dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7c28b58aaa chore(deps): bump pillow from 12.1.0 to 12.1.1 (#3785)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 07:54:16 +01:00
dependabot[bot]dni ⚡dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Vlad Stan
2b27f13fa8 chore(deps): bump axios from 1.13.2 to 1.13.5 (#3786)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-02-25 07:54:15 +01:00
130f4fd0bc feat: modern theme stuff like rounded corners and card gradient (#3766)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-02-25 07:54:14 +01:00
dni ⚡ 4894b70bad fix: /first_install new superuser may duplicate an username (#3787) 2026-02-25 07:54:13 +01:00
Arcanddni ⚡ 05769240d3 fix: fail closed on PayPal webhook verification errors (#3769) 2026-02-25 07:54:11 +01:00
krvianddni ⚡ 7bd127d22c fix: use canonical values in rate limit unit select (#3753) 2026-02-25 07:54:10 +01:00
Vlad Stananddni ⚡ 6dc3df148d fix: validation of invitation code (#3767) 2026-02-25 07:54:09 +01:00
Vlad Stananddni ⚡ e286546b63 [feat] User invitation code (#3761) 2026-02-25 07:54:07 +01:00
Vlad Stananddni ⚡ eae1be1db8 [feat] Allow no exchange providers (#3763) 2026-02-25 07:54:06 +01:00
dni ⚡ 25ab676a3f fix: datetime in db.py added utc offset (#3762) 2026-02-25 07:54:05 +01:00
Vlad Stananddni ⚡ 2fd6648a9a fix: handle validation errors when the response is binary (#3765) 2026-02-25 07:54:03 +01:00
Vlad Stananddni ⚡ 8eda653e3f fix: extension paid/free label (#3764) 2026-02-25 07:54:01 +01:00
Vlad Stananddni ⚡ 7e327b2c6a [feat] User activation (#3749) 2026-02-25 07:54:00 +01:00
Arcanddni ⚡ 40c055e373 feat: FIRST_INSTALL_TOKEN to help services like Umbrel secure the first_install endpoint (#3751)
Co-authored-by: dni  <office@dnilabs.com>
2026-02-25 07:53:59 +01:00
dni ⚡andVlad Stan 1d9808b269 refactor: payment.check_status() into check_payment_status(payment) (#3747)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2026-02-25 07:53:57 +01:00
dni ⚡ c65ab2af87 fix: uiCustomisation JS Error (#3752) 2026-02-25 07:53:56 +01:00
dni ⚡ 15f093c136 refactor: g.settings with a PublicSettings class for mapping, for reactive WINDOW_SETTINGS (#3644) 2026-02-25 07:53:55 +01:00
Vlad Stananddni ⚡ 5311b0a393 feat: wallet featured button (#3740) 2026-02-25 07:53:54 +01:00
Vlad Stananddni ⚡ e72396fe31 [feat] persist user ui customization (#3743) 2026-02-25 07:53:52 +01:00
dni ⚡ d6d0ebb20e chore: remove ecdsa in favor of coincurve (#3746) 2026-02-25 07:53:51 +01:00
dni ⚡ 563dd5b499 CI: use uv for publishing on pypi (#3745) 2026-02-25 07:53:49 +01:00
Vlad Stananddni ⚡ 9be894da74 [feat] admin impersonate user (#3741) 2026-02-25 07:53:47 +01:00
Vlad Stananddni ⚡ 87b66c554a fix: DB migration conflict (#3739) 2026-02-25 07:53:46 +01:00
dni ⚡ 314b533e39 chore: update python packages (#3707) 2026-02-25 07:53:44 +01:00
Vlad Stananddni ⚡ af8f3382c1 feat: allow text upload (#3738) 2026-02-25 07:53:42 +01:00
Vlad Stananddni ⚡ 67bb86c1ce fix: better validation error messages (#3736) 2026-02-25 07:53:41 +01:00
dni ⚡ f0f9e41f99 chore: remove windowMixin from components and pages (#3632) 2026-02-25 07:53:39 +01:00
dependabot[bot]dni ⚡dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2fbaf57b5e chore(deps): bump cryptography from 42.0.8 to 44.0.1 (#3735)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 07:53:37 +01:00
dependabot[bot]dni ⚡dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
87afba2abb chore(deps): bump aiohttp from 3.12.15 to 3.13.3 (#3734)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 07:53:36 +01:00
dependabot[bot]dni ⚡dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c375bf1b15 chore(deps): bump urllib3 from 2.5.0 to 2.6.3 (#3733)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 07:53:34 +01:00
Djuri Baarsanddni ⚡ dbcc920d0a [fix] Fix missing timezone information 2026-02-25 07:53:32 +01:00
Vlad Stananddni ⚡ 2c61e219b8 [feat] Update extension builder for 1.4.1 (#3725)
Co-authored-by: dni  <office@dnilabs.com>
2026-02-25 07:53:31 +01:00
dependabot[bot]dni ⚡dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6cb9b7da1f chore(deps): bump werkzeug from 3.1.3 to 3.1.5 (#3732)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 07:53:30 +01:00
dni ⚡andGitHub 83334d67c8 HOTFIX: websocket with old balance was sent. (#3758) 2026-02-03 12:12:33 +01:00
112 changed files with 5783 additions and 2521 deletions
+18 -3
View File
@@ -6,6 +6,10 @@
# The following settings are ONLY set in your .env file.
# They are NOT managed by the Admin UI and are not stored in the database.
# === First Install Token ===
# if set the user is required to enter this token on the /first_install page
# FIRST_INSTALL_TOKEN="myaccesstoken"
# === Logging and Development ===
DEBUG=False
@@ -55,7 +59,7 @@ LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos"
# LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx
# which fundingsources are allowed in the admin ui
# LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet, NWCWallet, BreezSdkWallet, BoltzWallet, StrikeWallet, CLNRestWallet"
# 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"
# uvicorn variable, allow https behind a proxy
# IMPORTANT: this also needs the webserver to be configured to forward the headers
@@ -187,6 +191,15 @@ BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon"
# HEXSTRING instead of path also possible
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
# SparkL2Wallet (external sidecar: https://github.com/lnbits/spark_sidecar)
SPARK_L2_NETWORK=MAINNET
SPARK_L2_EXTERNAL_ENDPOINT=http://127.0.0.1:8765
SPARK_L2_EXTERNAL_API_KEY=
# optional tuning
# SPARK_L2_PAY_WAIT_MS=4000
# SPARK_L2_PAY_POLL_MS=500
# SPARK_L2_STREAM_KEEPALIVE_MS=15000
# StrikeWallet
STRIKE_API_ENDPOINT=https://api.strike.me/v1
STRIKE_API_KEY=YOUR_STRIKE_API_KEY
@@ -313,10 +326,12 @@ LNBITS_SERVICE_FEE=0.0
# disable fees for internal transactions
# LNBITS_SERVICE_FEE_IGNORE_INTERNAL=true
# value in millisats
# The minimum fee reserved per payment (millisats)
LNBITS_RESERVE_FEE_MIN=2000
# value in percent
# The percentage of the payment amount to reserve for routing fees (percent)
LNBITS_RESERVE_FEE_PERCENT=1.0
# The default time to wait to for status response from the funding source when paying an invoice
LNBITS_FUNDING_SOURCE_PAY_INVOICE_WAIT_SECONDS=5
# limit the maximum balance for each wallet
# throw an error if the wallet attempts to create a new invoice
+12 -2
View File
@@ -55,11 +55,21 @@ jobs:
- name: Build and push boltz
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile.boltz
context: docker/boltzclient
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits-boltz:${{ inputs.tag }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
build-args: LNBITS_TAG=${{ inputs.tag }}
- name: Build and push sparkl2
uses: docker/build-push-action@v5
with:
context: docker/sparkl2
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits-sparkl2:${{ inputs.tag }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
build-args: LNBITS_TAG=${{ inputs.tag }}
+8 -9
View File
@@ -38,8 +38,7 @@ jobs:
- name: Setup Regtest
run: |
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
cd docker
cd docker/regtest
chmod +x ./start-regtest
./start-regtest
sudo chmod -R a+rwx .
@@ -50,16 +49,16 @@ jobs:
LNBITS_DATABASE_URL: ${{ inputs.db-url }}
LNBITS_BACKEND_WALLET_CLASS: ${{ inputs.backend-wallet-class }}
LND_REST_ENDPOINT: https://localhost:8081/
LND_REST_CERT: ./docker/data/lnd-3/tls.cert
LND_REST_MACAROON: ./docker/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
LND_REST_CERT: ./docker/regtest/data/lnd-3/tls.cert
LND_REST_MACAROON: ./docker/regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
LND_GRPC_ENDPOINT: localhost
LND_GRPC_PORT: 10009
LND_GRPC_CERT: docker/data/lnd-3/tls.cert
LND_GRPC_MACAROON: docker/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
CORELIGHTNING_RPC: ./docker/data/clightning-1/regtest/lightning-rpc
LND_GRPC_CERT: ./docker/regtest/data/lnd-3/tls.cert
LND_GRPC_MACAROON: ./docker/regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
CORELIGHTNING_RPC: ./docker/regtest/data/clightning-1/regtest/lightning-rpc
CORELIGHTNING_REST_URL: https://localhost:3001
CORELIGHTNING_REST_MACAROON: ./docker/data/clightning-2-rest/access.macaroon
CORELIGHTNING_REST_CERT: ./docker/data/clightning-2-rest/certificate.pem
CORELIGHTNING_REST_MACAROON: ./docker/regtest/data/clightning-2-rest/access.macaroon
CORELIGHTNING_REST_CERT: ./docker/regtest/data/clightning-2-rest/certificate.pem
LNBITS_ENDPOINT: http://localhost:5001
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
ECLAIR_URL: http://127.0.0.1:8082
+11
View File
@@ -33,6 +33,7 @@ jobs:
docker:
if: github.repository == 'lnbits/lnbits'
needs: [ release ]
uses: ./.github/workflows/docker.yml
with:
tag: ${{ github.ref_name }}
@@ -40,6 +41,16 @@ jobs:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
docker-latest-rc:
if: github.repository == 'lnbits/lnbits'
needs: [ release ]
uses: ./.github/workflows/docker.yml
with:
tag: latest-rc
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
pypi:
if: github.repository == 'lnbits/lnbits'
runs-on: ubuntu-24.04
+10
View File
@@ -52,6 +52,16 @@ jobs:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
docker-latest-rc:
if: github.repository == 'lnbits/lnbits'
needs: [ release ]
uses: ./.github/workflows/docker.yml
with:
tag: latest-rc
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
pypi:
if: github.repository == 'lnbits/lnbits'
runs-on: ubuntu-24.04
-1
View File
@@ -42,7 +42,6 @@ lnbits/static/bundle.min.js.old
lnbits/static/bundle.min.css.old
lnbits/static/bundle-components.min.js.old
lnbits/upgrades
docker
# Nix
*result*
@@ -14,11 +14,13 @@ ENV PATH="/root/.local/bin:$PATH"
# Reinstall dependencies just in case (needed for CMD usage)
RUN uv sync --all-extras
# LNbits + boltzd configuration
# LNbits
ENV LNBITS_PORT="5000"
ENV LNBITS_HOST="0.0.0.0"
ENV LNBITS_BACKEND_WALLET_CLASS="BoltzWallet"
ENV FUNDING_SOURCE_MAX_RETRIES=10
# Boltzd
ENV BOLTZ_CLIENT_ENDPOINT="127.0.0.1:9002"
ENV BOLTZ_CLIENT_MACAROON="/root/.boltz/macaroons/admin.macaroon"
ENV BOLTZ_CLIENT_CERT="/root/.boltz/tls.cert"
@@ -26,8 +28,7 @@ ENV BOLTZ_CLIENT_WALLET="lnbits"
EXPOSE 5000
# Entrypoint to start boltzd and LNbits
COPY dockerboltz.sh /dockerboltz.sh
RUN chmod +x /dockerboltz.sh
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
CMD ["/dockerboltz.sh"]
CMD ["/entrypoint.sh"]
+18
View File
@@ -0,0 +1,18 @@
!data
data/*
!data/boltz
data/boltz/*
!data/boltz/boltz.conf
!data/boltz-client
data/boltz-client/*
!data/boltz-client/boltz.toml
!data/boltz-nginx
data/boltz-nginx/*
!data/boltz-nginx/default.conf
!data/eclair
data/eclair/*
!data/eclair/eclair.conf
+99
View File
@@ -0,0 +1,99 @@
![TESTS](https://github.com/lnbits/legend-regtest-enviroment/actions/workflows/ci.yml/badge.svg)
# nodes
- lnd-1: for locally testing your current lnbits
- lnd-2: used for boltz backend
- lnd-3: used for lnbits inside docker
- cln-1: for locally testing your current lnbits
- cln-2: used for clightning-REST
- eclair-1: for locally testing your current lnbits
# Installing regtest
get the regtest enviroment ready
```sh
# Install docker https://docs.docker.com/engine/install/
# Make sure your user has permission to use docker 'sudo usermod -aG docker ${USER}' then reboot
# Stop/start docker 'sudo systemctl stop docker' 'sudo systemctl start docker'
sudo apt install jq
git clone https://github.com/lnbits/lnbits.git
cd lnbits
docker build -t lnbits/lnbits .
mkdir docker
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
cd docker
chmod +x ./start-regtest
./start-regtest # start the regtest and also run tests
sudo chown -R $USER ./data # Give the data file permissions for user
```
# Running LNbits on regtest
add this ENV variables to your `.env` file
```sh
DEBUG=true
# LND
LNBITS_BACKEND_WALLET_CLASS="LndRestWallet"
LND_REST_ENDPOINT=https://127.0.0.1:8081/
LND_REST_CERT=/home/user/repos/lnbits/docker/data/lnd-1/tls.cert
LND_REST_MACAROON=/home/user/repos/lnbits/docker/data/lnd-1/data/chain/bitcoin/regtest/admin.macaroon
# CLN
LNBITS_BACKEND_WALLET_CLASS="CoreLightningWallet"
CORELIGHTNING_RPC=./docker/data/clightning-1/regtest/lightning-rpc
# Run LNbits
uv run lnbits
# Run LNbits with hot reload
make dev
```
# testing
```sh
chmod +x ./start-regtest
./start-regtest
# short answer :)
./start-regtest && echo "PASSED" || echo "FAILED" > /dev/null
```
usage of the `bitcoin-cli-sim`, `lightning-cli-sim` and `lncli-sim` aliases
```sh
cd ~/lnbits/docker
source docker-scripts.sh
# use bitcoin core, mine a block
bitcoin-cli-sim -generate 1
# use c-lightning nodes
lightning-cli-sim 1 newaddr | jq -r '.bech32' # use node 1
lightning-cli-sim 2 getinfo # use node 2
lightning-cli-sim 3 getinfo # use node 3
# use lnd nodes
lncli-sim 1 newaddr p2wsh
lncli-sim 2 listpeers
```
# urls
- mempool: http://localhost:8080/
- boltz api: http://localhost:9001/
- lnd-1 rest: http://localhost:8081/
- lnbits: http://localhost:5001/
# debugging docker logs
```sh
docker logs lnbits-lnbits-1 -f
docker logs lnbits-boltz-1 -f
docker logs lnbits-clightning-1-1 -f
docker logs lnbits-lnd-2-1 -f
```
@@ -0,0 +1,49 @@
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 = ""
@@ -0,0 +1,49 @@
upstream boltz {
server boltz:9001;
}
upstream boltzr {
server boltz:9005;
}
upstream ws {
server boltz:9004;
}
server {
listen 9001;
listen [::]:9001;
server_name localhost;
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods 'GET, PATCH, DELETE, POST, OPTIONS' always;
add_header Access-Control-Allow-Headers "*" always;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
if ($request_method = OPTIONS) {
return 204;
}
location /v2/ws {
proxy_pass http://ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location ~ ^/v2/(lightning|swap/rescue|swap/restore) {
proxy_pass http://boltzr;
}
location /streamswapstatus {
proxy_pass http://boltzr;
}
location / {
proxy_pass http://boltz;
}
}
+114
View File
@@ -0,0 +1,114 @@
[api]
host = "0.0.0.0"
port = 9_001
[grpc]
host = "0.0.0.0"
port = 9_000
[postgres]
host = "boltz-postgres"
port = 5432
database = "boltz"
username = "boltz"
password = "boltz"
[sidecar]
[sidecar.grpc]
host = "127.0.0.1"
port = 9003
[sidecar.ws]
host = "0.0.0.0"
port = 9004
[sidecar.api]
host = "0.0.0.0"
port = 9005
[swap]
deferredClaimSymbols = ["BTC", "L-BTC"]
[[pairs]]
base = "BTC"
quote = "BTC"
rate = 1
fee = 0.5
swapInFee = 0.1
maxSwapAmount = 40_294_967
minSwapAmount = 50_000
[pairs.timeoutDelta]
chain = 1440
reverse = 1440
swapMinimal = 1440
swapMaximal = 2880
swapTaproot = 10080
[[pairs]]
base = "L-BTC"
quote = "BTC"
fee = 0.25
swapInFee = 0.1
rate = 1
maxSwapAmount = 40_294_967
minSwapAmount = 100
[pairs.submarineSwap]
minSwapAmount = 1_000
minBatchedAmount = 21
[pairs.chainSwap]
minSwapAmount = 25_000
[pairs.timeoutDelta]
chain = 1440
reverse = 1440
swapMinimal = 1440
swapMaximal = 2880
swapTaproot = 10080
[[currencies]]
symbol = "BTC"
network = "bitcoinRegtest"
minWalletBalance = 10_000_000
minChannelBalance = 10_000_000
maxSwapAmount = 40_294_967
minSwapAmount = 10_000
maxZeroConfAmount = 0
[currencies.chain]
# mempoolSpace = "http://mempool-web:8090/api"
host = "bitcoind"
zmqpubrawtx = "tcp://bitcoind:29000"
zmqpubrawblock = "tcp://bitcoind:29001"
port = 18_443
cookie = "/root/.bitcoin/regtest/.cookie"
feeFloor = 0.2
wallet = "lnbits"
[currencies.lnd]
host = "lnd-2"
port = 10_009
certpath = "/data/lnd/tls.cert"
macaroonpath = "/data/lnd/data/chain/bitcoin/regtest/admin.macaroon"
[liquid]
symbol = "L-BTC"
network = "liquidRegtest"
maxSwapAmount = 40_294_967
minSwapAmount = 10_000
maxZeroConfAmount = 40_294_967
[liquid.chain]
host = "elementsd"
port = 18884
cookie = "/root/.elements/liquidregtest/.cookie"
zmqpubrawtx = "tcp://elementsd:31000"
zmqpubhashblock = "tcp://elementsd:31002"
wallet = "lnbits"
+28
View File
@@ -0,0 +1,28 @@
eclair {
chain = "regtest"
api {
binding-ip = "0.0.0.0"
enabled = true
port = 8080
password = "lnbits"
}
bitcoind {
host = "bitcoind"
rpcport = 18443
auth = "safecookie"
cookie = "/root/.bitcoin/regtest/.cookie"
zmqblock = "tcp://bitcoind:29002"
zmqtx = "tcp://bitcoind:29000"
}
channel {
max-funding-satoshis = 10000000000
}
features {
option_support_large_channel = mandatory
}
}
+434
View File
@@ -0,0 +1,434 @@
services:
lnbits:
hostname: lnbits
depends_on:
- lnd-3
image: lnbits/lnbits
restart: on-failure
user: "0:0"
environment:
LNBITS_PORT: 5001
DEBUG: true
LNBITS_ADMIN_UI: false
LNBITS_BACKEND_WALLET_CLASS: "LndRestWallet"
LNBITS_DATA_FOLDER: "./data"
LND_REST_ENDPOINT: "https://lnd-3:8081/"
LND_REST_CERT: "./lnd/tls.cert"
LND_REST_MACAROON: "./lnd/data/chain/bitcoin/regtest/admin.macaroon"
ports:
- 5001:5001
volumes:
- lnbits-data:/app/data
- ./data/lnd-3:/app/lnd:uid=1000,gid=1000
boltz:
hostname: boltz
depends_on:
- lnd-2
- boltz-postgres
restart: always
image: boltz/boltz:v3.12.1
ports:
- 9000:9000
entrypoint: "sh -c 'sleep 30; /boltz-backend/bin/boltzd'"
volumes:
- ./data/lnd-2:/data/lnd/
- ./data/boltz/:/root/.boltz/
- elements-data:/root/.elements
- bitcoin-data:/root/.bitcoin
boltz-client:
hostname: boltz-client
depends_on:
- boltz
restart: always
image: boltz/boltz-client:latest
ports:
- 9002:9002
- 9003:9003
expose:
- 9002
healthcheck:
test: ['CMD', 'boltzcli', '--host', 'boltz-client', 'getinfo']
interval: 5s
timeout: 3s
retries: 10
start_period: 0s
volumes:
- elements-data:/root/.elements
- ./data/boltz-client:/root/.boltz
boltz-backend-nginx:
hostname: boltz-nginx
restart: always
image: nginx:latest
ports:
- 9001:9001
volumes:
- nginx-data:/etc/nginx/conf.d
healthcheck:
test: ['CMD-SHELL', 'curl http://localhost:9001/version']
timeout: 1s
retries: 10
interval: 1s
start_period: 0s
boltz-postgres:
hostname: boltz-postgres
restart: always
image: postgres:14-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready --dbname boltz --username boltz"]
interval: 5s
timeout: 30s
retries: 10
start_period: 5s
environment:
- POSTGRES_DB=boltz
- POSTGRES_USER=boltz
- POSTGRES_PASSWORD=boltz
expose:
- 5432
bitcoind:
hostname: bitcoind
image: boltz/bitcoin-core:25.0
command:
- -regtest
- -fallbackfee=0.00000253
- -zmqpubrawtx=tcp://0.0.0.0:29000
- -zmqpubrawblock=tcp://0.0.0.0:29001
- -zmqpubhashblock=tcp://0.0.0.0:29002
- -txindex
- -rpcallowip=0.0.0.0/0
- -rpcbind=0.0.0.0
- -addresstype=bech32
- -changetype=bech32
- -dbcache=2048
- -rpcworkqueue=256
volumes:
- bitcoin-data:/root/.bitcoin
expose:
- 29000
- 29001
- 29002
- 18443
- 18444
healthcheck:
test:
[
"CMD",
"bitcoin-cli",
"--rpccookiefile=/root/.bitcoin/regtest/.cookie",
"-regtest",
"getblockchaininfo",
]
timeout: 1s
retries: 1
interval: 1s
start_period: 0s
clightning-1:
hostname: clightning-1
depends_on:
- bitcoind
image: boltz/c-lightning:24.11
command:
- --large-channels
- --network=regtest
- --grpc-port=9736
- --bind-addr=0.0.0.0:9735
- --bitcoin-rpcconnect=bitcoind
- --bitcoin-rpcport=18443
- --clnrest-host=0.0.0.0
- --clnrest-port=3010
expose:
- 9735
ports:
- 9736:9736
- 3010:3010
volumes:
- ./data/clightning-1:/root/.lightning/
- bitcoin-data:/root/.bitcoin
clightning-2:
hostname: clightning-2
depends_on:
- bitcoind
image: boltz/c-lightning:22.11.1
command:
- --large-channels
- --network=regtest
- --grpc-port=9737
- --bind-addr=0.0.0.0:9735
- --bitcoin-rpcconnect=bitcoind
- --bitcoin-rpcport=18443
expose:
- 9735
ports:
- 9737:9737
volumes:
- ./data/clightning-2:/root/.lightning/
- bitcoin-data:/root/.bitcoin
clightning-2-rest:
hostname: clightning-2-rest
depends_on:
- clightning-2
image: saubyk/c-lightning-rest:0.10.7
entrypoint: "sh -c 'sleep 35 && /sbin/tini -g -- ./docker-entrypoint.sh'"
ports:
- 3001:3001
expose:
- 3001
volumes:
- ./data/clightning-2:/root/.lightning/:uid=1000,gid=1000
- ./data/clightning-2-rest:/usr/src/app/certs/
- bitcoin-data:/root/.bitcoin
clightning-3:
hostname: clightning-3
depends_on:
- bitcoind
image: boltz/c-lightning:24.11
command:
- --large-channels
- --network=regtest
- --grpc-port=9738
- --bind-addr=0.0.0.0:9735
- --bitcoin-rpcconnect=bitcoind
- --bitcoin-rpcport=18443
expose:
- 9735
ports:
- 9738:9738
volumes:
- ./data/clightning-3:/root/.lightning/
- bitcoin-data:/root/.bitcoin
lnd-1:
hostname: lnd-1
depends_on:
- bitcoind
image: boltz/lnd:0.18.4-beta
restart: on-failure
command:
- --listen=lnd-1:9735
- --rpclisten=lnd-1:10009
- --restlisten=lnd-1:8081
- --bitcoin.active
- --bitcoin.regtest
- --bitcoin.node=bitcoind
- --bitcoind.rpchost=bitcoind
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
- --bitcoind.zmqpubrawtx=bitcoind:29000
- --bitcoind.zmqpubrawblock=bitcoind:29001
- --noseedbackup
- --protocol.wumbo-channels
expose:
- 8081
- 9735
- 10009
volumes:
- ./data/lnd-1:/root/.lnd/
- bitcoin-data:/root/.bitcoin
lnd-2:
hostname: lnd-2
depends_on:
- bitcoind
image: boltz/lnd:0.19.3-beta
restart: on-failure
command:
- --listen=lnd-2:9735
- --rpclisten=lnd-2:10009
- --restlisten=lnd-2:8081
- --bitcoin.active
- --bitcoin.regtest
- --bitcoin.node=bitcoind
- --bitcoind.rpchost=bitcoind
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
- --bitcoind.zmqpubrawtx=bitcoind:29000
- --bitcoind.zmqpubrawblock=bitcoind:29001
- --noseedbackup
- --protocol.wumbo-channels
expose:
- 8081
- 9735
- 10009
volumes:
- ./data/lnd-2:/root/.lnd/
- bitcoin-data:/root/.bitcoin
lnd-3:
hostname: lnd-3
depends_on:
- bitcoind
image: boltz/lnd:0.18.4-beta
restart: on-failure
command:
- --listen=lnd-3:9735
- --rpclisten=lnd-3:10009
- --restlisten=lnd-3:8081
- --bitcoin.active
- --bitcoin.regtest
- --bitcoin.node=bitcoind
- --bitcoind.rpchost=bitcoind
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
- --bitcoind.zmqpubrawtx=bitcoind:29000
- --bitcoind.zmqpubrawblock=bitcoind:29001
- --noseedbackup
- --protocol.wumbo-channels
ports:
- 8081:8081
- 10009:10009
expose:
- 8081
- 9735
- 10009
volumes:
- ./data/lnd-3:/root/.lnd/
- bitcoin-data:/root/.bitcoin
lnd-4:
hostname: lnd-4
depends_on:
- bitcoind
image: boltz/lnd:0.18.4-beta
restart: on-failure
command:
- --listen=lnd-4:9735
- --rpclisten=lnd-4:10009
- --restlisten=lnd-4:8081
- --bitcoin.active
- --bitcoin.regtest
- --bitcoin.node=bitcoind
- --bitcoind.rpchost=bitcoind
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
- --bitcoind.zmqpubrawtx=bitcoind:29000
- --bitcoind.zmqpubrawblock=bitcoind:29001
- --noseedbackup
- --protocol.wumbo-channels
expose:
- 8081
- 9735
- 10009
volumes:
- ./data/lnd-4:/root/.lnd/
- bitcoin-data:/root/.bitcoin
eclair:
hostname: eclair
depends_on:
- bitcoind
image: boltz/eclair:0.8.0
restart: on-failure
entrypoint: "sh -c 'JAVA_OPTS=-Xmx512m /eclair-node/bin/eclair-node.sh -Declair.datadir=/root/eclair -Declair.printToConsole'"
ports:
- 8082:8080
expose:
- 9735
- 8080
volumes:
- ./data/eclair:/root/eclair
- bitcoin-data:/root/.bitcoin
electrs:
hostname: electrs
restart: always
image: boltz/electrs:latest
entrypoint: ["electrs-bitcoin"]
command:
- --electrum-rpc-addr
- electrs:19001
- --http-addr
- electrs:3002
- --daemon-rpc-addr
- bitcoind:18443
- --network
- regtest
- --jsonrpc-import
healthcheck:
test: ["CMD-SHELL", "curl -s $(hostname):3002/blocks/tip/height"]
timeout: 1s
retries: 20
interval: 2s
start_period: 5s
ports:
- 19001:19001
- 3002:3002
volumes:
- bitcoin-data:/root/.bitcoin
elementsd:
hostname: elementsd
restart: always
image: boltz/elements:latest
expose:
- 31001
ports:
- 31000:31000
- 31002:31002
- 18884:18884
command:
- -chain=liquidregtest
- -txindex=1
- -rest=1
- -server=1
- -rpcallowip=0.0.0.0/0
- -validatepegin=0
- -initialfreecoins=2100000000000000
- -fallbackfee=0.000001
- -rpcbind=0.0.0.0
- -rpcport=18884
- -zmqpubrawtx=tcp://0.0.0.0:31000
- -zmqpubrawblock=tcp://0.0.0.0:31001
- -zmqpubhashblock=tcp://0.0.0.0:31002
- -acceptdiscountct=1
- -creatediscountct=1
volumes:
- elements-data:/root/.elements
electrs-liquid:
hostname: electrs-liquid
restart: always
image: boltz/electrs:latest
entrypoint: ["electrs-liquid"]
command:
- --electrum-rpc-addr
- electrs-liquid:19002
- --http-addr
- electrs-liquid:3003
- --daemon-rpc-addr
- elementsd:18884
- --daemon-dir
- /root/.elements
- --network
- liquidregtest
- --parent-network
- regtest
- --jsonrpc-import
healthcheck:
test: ["CMD-SHELL", "curl -s $(hostname):3003/blocks/tip/height"]
timeout: 15s
retries: 20
interval: 2s
start_period: 5s
ports:
- 19002:19002
- 3003:3003
volumes:
- elements-data:/root/.elements
volumes:
lnbits-data:
bitcoin-data:
elements-data:
nginx-data:
name: nginx-data
driver: local
driver_opts:
type: none
o: bind
device: ./data/boltz-nginx/
+322
View File
@@ -0,0 +1,322 @@
#!/bin/sh
export COMPOSE_PROJECT_NAME=lnbits
boltzcli-sim() {
docker exec -it lnbits-boltz-client-1 boltzcli "$@"
}
bitcoin-cli-sim() {
docker exec lnbits-bitcoind-1 bitcoin-cli -regtest "$@"
}
elements-cli-sim() {
docker exec lnbits-elementsd-1 elements-cli -rpcport=18884 -chain=liquidregtest "$@"
}
# args(i, cmd)
lightning-cli-sim() {
i=$1
shift # shift first argument so we can use $@
docker exec lnbits-clightning-$i-1 lightning-cli --network regtest "$@"
}
# args(i, cmd)
lncli-sim() {
i=$1
shift # shift first argument so we can use $@
docker exec lnbits-lnd-$i-1 lncli --network regtest --rpcserver=lnd-$i:10009 "$@"
}
get-eclair-pubkey() {
while true; do
pubkey=$(docker exec lnbits-eclair-1 curl http://localhost:8080/getinfo -X POST -s -u :lnbits | jq -r .nodeId 2> /dev/null)
pubkeyPrefix=$(echo $pubkey | cut -c1,2)
if [[ "$pubkeyPrefix" == "02" || "$pubkeyPrefix" == "03" ]]; then
echo $pubkey
break
fi
sleep 1
done
}
wait-for-eclair-channel() {
while true; do
state=$(docker exec lnbits-eclair-1 curl http://localhost:8080/channels -X POST -s -u :lnbits | jq -r ".[0].state")
pending=$(docker exec lnbits-eclair-1 curl -s http://localhost:8080/channels -X POST -u :lnbits| jq '. | length')
echo "eclair-1 pendingchannels: $pending, current state: $state"
if [[ "$state" == "NORMAL" ]]; then
break
fi
sleep 1
done
}
# args(i)
fund_boltz_client() {
# first address of seed: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
address="el1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f3mmz5l7uw5pqmx6xf5xy50hsn6vhkm5euwt72x878eq6zxx2z0z676mna6kdq"
echo "funding: $address on boltz-client"
elements-cli-sim -named sendtoaddress address=$address amount=30 fee_rate=100 > /dev/null
}
# args(i)
fund_clightning_node() {
address=$(lightning-cli-sim $1 newaddr | jq -r .bech32)
echo "funding: $address on clightning-node: $1"
bitcoin-cli-sim -named sendtoaddress address=$address amount=30 fee_rate=100 > /dev/null
}
# args(i)
fund_lnd_node() {
address=$(lncli-sim $1 newaddress p2wkh | jq -r .address)
echo "funding: $address on lnd-node: $1"
bitcoin-cli-sim -named sendtoaddress address=$address amount=30 fee_rate=100 > /dev/null
}
# args(i, j)
connect_clightning_node() {
pubkey=$(lightning-cli-sim $2 getinfo | jq -r '.id')
lightning-cli-sim $1 connect $pubkey@lnbits-clightning-$2-1:9735 | jq -r '.id'
}
lnbits-regtest-start(){
if ! command -v jq &> /dev/null
then
echo "jq is not installed"
exit
fi
if ! command -v docker &> /dev/null
then
echo "docker is not installed"
exit
fi
if ! command -v docker version &> /dev/null
then
echo "dockerd is not running"
exit
fi
lnbits-regtest-stop
docker compose up -d --remove-orphans
lnbits-regtest-init
}
lnbits-regtest-start-log(){
lnbits-regtest-stop
docker compose up --remove-orphans
lnbits-regtest-init
}
lnbits-regtest-stop(){
docker compose down --volumes
# clean up lightning node data
sudo rm -rf ./data/clightning-1 ./data/clightning-2 ./data/clightning-3 ./data/lnd-1 ./data/lnd-2 ./data/lnd-3 ./data/lnd-4 ./data/boltz/boltz.db ./data/eclair/regtest ./data/boltz-client/liquid-wallet ./data/boltz-client/bitcoin-wallet ./data/boltz-client/wallet ./data/boltz-client/boltz.db
# recreate lightning node data folders preventing permission errors
mkdir ./data/clightning-1 ./data/clightning-2 ./data/clightning-3 ./data/lnd-1 ./data/lnd-2 ./data/lnd-3 ./data/lnd-4
}
lnbits-regtest-restart(){
lnbits-regtest-stop
lnbits-regtest-start
}
boltz-client-init(){
for i in 0 1 2; do
fund_boltz_client
done
elements-cli-sim -generate 3 > /dev/null
}
lnbits-bitcoin-init(){
echo "init_bitcoin_wallet..."
bitcoin-cli-sim createwallet lnbits || bitcoin-cli-sim loadwallet lnbits
echo "mining 150 blocks..."
bitcoin-cli-sim -generate 150 > /dev/null
}
lnbits-elements-init(){
echo "init_elements_wallet..."
elements-cli-sim createwallet lnbits || elements-cli-sim loadwallet lnbits
echo "mining 150 blocks..."
elements-cli-sim -generate 150 > /dev/null
elements-cli-sim rescanblockchain
}
lnbits-init(){
echo "init_lnbits..."
docker exec lnbits-lnbits-1 uv run python tools/create_fake_admin.py
}
lnbits-regtest-init(){
lnbits-bitcoin-init
lnbits-elements-init
lnbits-lightning-sync
lnbits-lightning-init
boltz-client-init
lnbits-init
}
lnbits-lightning-sync(){
wait-for-clightning-sync 1
wait-for-clightning-sync 2
wait-for-clightning-sync 3
wait-for-lnd-sync 1
wait-for-lnd-sync 2
wait-for-lnd-sync 3
wait-for-lnd-sync 4
}
lnbits-lightning-init(){
# create 10 UTXOs for each node
for i in 0 1 2; do
fund_clightning_node 1
fund_clightning_node 2
fund_clightning_node 3
fund_lnd_node 1
fund_lnd_node 2
fund_lnd_node 3
fund_lnd_node 4
done
echo "mining 3 blocks..."
bitcoin-cli-sim -generate 3 > /dev/null
lnbits-lightning-sync
channel_confirms=6
channel_size=24000000 # 0.024 btc
balance_size=12000000 # 0.12 btc
balance_size_msat=12000000000 # 0.12 btc
# lnd-1 -> lnd-2
lncli-sim 1 connect $(lncli-sim 2 getinfo | jq -r '.identity_pubkey')@lnbits-lnd-2-1 > /dev/null
echo "open channel from lnd-1 to lnd-2"
lncli-sim 1 openchannel $(lncli-sim 2 getinfo | jq -r '.identity_pubkey') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 1
# lnd-1 -> lnd-3
lncli-sim 1 connect $(lncli-sim 3 getinfo | jq -r '.identity_pubkey')@lnbits-lnd-3-1 > /dev/null
echo "open channel from lnd-1 to lnd-3"
lncli-sim 1 openchannel $(lncli-sim 3 getinfo | jq -r '.identity_pubkey') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 1
# lnd-1 -> cln-1
lncli-sim 1 connect $(lightning-cli-sim 1 getinfo | jq -r '.id')@lnbits-clightning-1-1 > /dev/null
echo "open channel from lnd-1 to cln-1"
lncli-sim 1 openchannel $(lightning-cli-sim 1 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 1
# lnd-1 -> cln-2
lncli-sim 1 connect $(lightning-cli-sim 2 getinfo | jq -r '.id')@lnbits-clightning-2-1 > /dev/null
echo "open channel from lnd-1 to cln-2"
lncli-sim 1 openchannel $(lightning-cli-sim 2 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 1
# lnd-1 -> cln-3
lncli-sim 1 connect $(lightning-cli-sim 3 getinfo | jq -r '.id')@lnbits-clightning-3-1 > /dev/null
echo "open channel from lnd-1 to cln-3"
lncli-sim 1 openchannel $(lightning-cli-sim 3 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 1
# lnd-2 -> cln-2
lncli-sim 2 connect $(lightning-cli-sim 2 getinfo | jq -r '.id')@lnbits-clightning-2-1 > /dev/null
echo "open channel from lnd-2 to cln-2"
lncli-sim 2 openchannel $(lightning-cli-sim 2 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 2
# lnd-3 -> cln-3
lncli-sim 3 connect $(lightning-cli-sim 3 getinfo | jq -r '.id')@lnbits-clightning-3-1 > /dev/null
echo "open channel from lnd-3 to cln-1"
lncli-sim 3 openchannel $(lightning-cli-sim 3 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 3
# lnd-3 -> cln-1
lncli-sim 3 connect $(lightning-cli-sim 1 getinfo | jq -r '.id')@lnbits-clightning-1-1 > /dev/null
echo "open channel from lnd-3 to cln-1"
lncli-sim 3 openchannel $(lightning-cli-sim 1 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 3
# lnd-1 -> eclair-1
lncli-sim 1 connect $(get-eclair-pubkey)@lnbits-eclair-1 > /dev/null
echo "open channel from lnd-2 to eclair-1"
lncli-sim 1 openchannel $(get-eclair-pubkey) $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 1
# lnd-2 -> eclair-1
lncli-sim 2 connect $(get-eclair-pubkey)@lnbits-eclair-1 > /dev/null
echo "open channel from lnd-2 to eclair-1"
lncli-sim 2 openchannel $(get-eclair-pubkey) $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 2
wait-for-clightning-channel 1
wait-for-clightning-channel 2
wait-for-clightning-channel 3
wait-for-eclair-channel
lnbits-lightning-sync
}
wait-for-lnd-channel(){
while true; do
pending=$(lncli-sim $1 pendingchannels | jq -r '.pending_open_channels | length')
echo "lnd-$1 pendingchannels: $pending"
if [[ "$pending" == "0" ]]; then
break
fi
sleep 1
done
}
wait-for-lnd-sync(){
while true; do
if [[ "$(lncli-sim $1 getinfo 2>&1 | jq -r '.synced_to_chain' 2> /dev/null)" == "true" ]]; then
echo "lnd-$1 is synced!"
break
fi
echo "waiting for lnd-$1 to sync..."
sleep 1
done
}
wait-for-clightning-channel(){
while true; do
pending=$(lightning-cli-sim $1 getinfo | jq -r '.num_pending_channels | length')
echo "cln-$1 pendingchannels: $pending"
if [[ "$pending" == "0" ]]; then
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_bitcoind_sync' 2> /dev/null)" == "null" ]]; then
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_lightningd_sync' 2> /dev/null)" == "null" ]]; then
break
fi
fi
fi
sleep 1
done
}
wait-for-clightning-sync(){
while true; do
if [[ ! "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.id' 2> /dev/null)" == "null" ]]; then
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_bitcoind_sync' 2> /dev/null)" == "null" ]]; then
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_lightningd_sync' 2> /dev/null)" == "null" ]]; then
echo "cln-$1 is synced!"
break
fi
fi
fi
echo "waiting for cln-$1 to sync..."
sleep 1
done
}
+74
View File
@@ -0,0 +1,74 @@
#!/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
+39
View File
@@ -0,0 +1,39 @@
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 LNBITS_RESERVE_FEE_MIN="20000"
ENV LNBITS_RESERVE_FEE_PERCENT="1"
ENV LNBITS_FUNDING_SOURCE_PAY_INVOICE_WAIT_SECONDS="20"
ENV FUNDING_SOURCE_MAX_RETRIES=10
# spark sidecar
ENV SPARK_NETWORK="MAINNET"
ENV SPARK_SIDECAR_PORT="8765"
ENV SPARK_PAY_WAIT_MS="20000"
ENV SPARK_MULTIPLICITY="3"
EXPOSE 5000
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Entrypoint to start sparksidebar and LNbits
CMD ["/entrypoint.sh"]
+25
View File
@@ -0,0 +1,25 @@
#!/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='*'
+4 -1
View File
@@ -50,6 +50,7 @@ Below is a side-by-side comparison of Lightning funding sources you can use with
| **Blink** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; focuses on mobile integrations. |
| **ZBD** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Gaming-focused payment platform. |
| **Spark (CLN)** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | Web interface for CLN; requires Spark server setup. |
| **Spark (L2)** | Self-custodial | ❌ | Medium | ❌ | High | Automatic | Easy | Low | Minimal fees | Medium | Runs via Spark sidecar; seed-based self-custody. |
| **Cliche Wallet** | Self-custodial | ❌ | Medium | ❌ | Medium | Manual | Moderate | Moderate | Minimal fees | Medium | Lightweight wallet; suitable for embedded systems. |
| **Strike** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
| **LNPay** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
@@ -57,7 +58,9 @@ Below is a side-by-side comparison of Lightning funding sources you can use with
| **LN.tips** | Custodial/Self-Custodial | Depends on provider | Medium | ❌ | Low | Provider-managed | Moderate | Low | Transaction fees may apply | Medium | Simple hosted service; use LN.tips API as your backend. |
| **Fake Wallet** | Testing (simulated) | ❌ | Low | ❌ | N/A | N/A | Easy | Low | None (test only) | N/A | For testing only; mints accounting units in LNbits (no real sats, unit name configurable). |
---
## Spark (L2)
Spark L2 uses a local Node.js sidecar to expose an HTTP API that LNbits can use as a funding source. It is self-custodial and secured by a standard mnemonic seed. Sidecar repo `https://github.com/lnbits/spark_sidecar`.
### Notes for readers
+30 -2
View File
@@ -41,7 +41,8 @@ A backend wallet is selected and configured entirely through LNbits environment
| [CoreLightning](#corelightning) | [LND (gRPC)](#lnd-grpc) | [Blink](#blink) |
| [CoreLightning REST](#corelightning-rest) | [LNbits](#lnbits) | [Alby](#alby) |
| [Spark (Core Lightning)](#spark-core-lightning) | [LNPay](#lnpay) | [Boltz](#boltz) |
| [Cliche Wallet](#cliche-wallet) | [ZBD](#zbd) | [Phoenixd](#phoenixd) |
| [Spark L2](#spark-l2) | [ZBD](#zbd) | [Phoenixd](#phoenixd) |
| [Cliche Wallet](#cliche-wallet) | | |
| [Breez SDK](#breez-sdk) | [Breez Liquid SDK](#breez-liquid-sdk) | [Nostr Wallet Connect](#nostr-wallet-connect-nwc) |
| [Strike](#strike) | [Eclair (ACINQ)](#eclair-acinq) | [LN.tips](#lntips) |
| [Fake Wallet](#fake-wallet) | | |
@@ -52,7 +53,7 @@ A backend wallet is selected and configured entirely through LNbits environment
### CLNRest (using [runes](https://docs.corelightning.org/reference/lightning-createrune))
[Core Lightning REST API docs](https://docs.corelightning.org/docs/rest)
[Core Lightning REST API docs](https://docs.corelightning.org/docs/rest)
Should also work with the [Rust version of CLNRest](https://github.com/daywalker90/clnrest-rs)
**Environment variables**
@@ -127,6 +128,33 @@ Old REST interface using [RTL c-lightning-REST](https://github.com/Ride-The-Ligh
- `SPARK_URL`: `http://10.147.17.230:9737/rpc`
- `SPARK_TOKEN`: `secret_access_key`
## Spark L2
Self-custodial funding source using the [Spark L2](https://docs.spark.money/start/overview) network. Requires a Node.js [sidecar](https://github.com/lnbits/spark_sidecar) that bridges lnbits talking to Spark. Works in addition with any Spark-compatible seed (Wallet of Satoshi, BuhoGO, BlitzWallet).
If the sidecar is started with a `mnemonic` then that mnemonic will be used. Otherwhise if a mnemonic is set for the `Spark L2` LNbits funding source then that mnemonic will be used.
### Optional tuning
- `SPARK_L2_PAY_WAIT_MS`: `4000` _(payment timeout in ms)_
- `SPARK_L2_PAY_POLL_MS`: `500` _(polling interval in ms)_
- `SPARK_L2_STREAM_KEEPALIVE_MS`: `15000` _(SSE keepalive in ms)_
### Example: run the sidecar
```bash
git clone https://github.com/lnbits/spark_sidecar.git
cd spark_sidecar
npm install
SPARK_MNEMONIC="bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom" \
SPARK_NETWORK=MAINNET \
SPARK_SIDECAR_PORT=8765 \
SPARK_PAY_WAIT_MS=20000 \
node server.mjs
```
For testing, you can generate a 12-word mnemonic at https://iancoleman.io/bip39/. Store it securely — it controls your funds. Then select Spark (L2) as the funding source in LNbits.
## LND (REST)
**Required env vars**
-2
View File
@@ -66,7 +66,6 @@ from .middleware import (
add_first_install_middleware,
add_ip_block_middleware,
add_ratelimit_middleware,
add_route_access_middleware,
)
from .tasks import internal_invoice_listener, invoice_listener, run_interval
@@ -193,7 +192,6 @@ def create_app() -> FastAPI:
# adds security middleware
add_ip_block_middleware(app)
add_route_access_middleware(app)
add_ratelimit_middleware(app)
register_exception_handlers(app)
+75 -21
View File
@@ -4,10 +4,12 @@ from typing import Any
from uuid import uuid4
from lnbits.core.crud.extensions import get_user_active_extensions_ids
from lnbits.core.crud.wallets import create_wallet, get_wallets
from lnbits.core.crud.wallets import clear_wallet_cache, create_wallet, get_wallets
from lnbits.core.db import db
from lnbits.core.models import UserAcls
from lnbits.db import Connection, Filters, Page
from lnbits.helpers import sha256s
from lnbits.utils.cache import cache
from ..models import (
Account,
@@ -41,6 +43,7 @@ async def delete_account(user_id: str, conn: Connection | None = None) -> None:
"DELETE from accounts WHERE id = :user",
{"user": user_id},
)
await clear_user_id_cache(user_id)
async def get_accounts(
@@ -69,6 +72,7 @@ async def get_accounts(
accounts.email,
accounts.pubkey,
accounts.external_id,
accounts.activated,
SUM(COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0)) as balance_msat,
@@ -93,12 +97,18 @@ async def get_accounts(
)
async def get_account(user_id: str, conn: Connection | None = None) -> Account | None:
async def get_account(
user_id: str, active_only: bool = True, conn: Connection | None = None
) -> Account | None:
if len(user_id) == 0:
return None
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE id = :id",
{"id": user_id},
"""
SELECT * FROM accounts
WHERE id = :id AND (activated = true OR activated = :activated)
""",
{"id": user_id, "activated": active_only},
Account,
)
@@ -124,55 +134,79 @@ async def delete_accounts_no_wallets(
async def get_account_by_username(
username: str, conn: Connection | None = None
username: str, active_only: bool = True, conn: Connection | None = None
) -> Account | None:
if len(username) == 0:
return None
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE LOWER(username) = :username",
{"username": username.lower()},
"""
SELECT * FROM accounts
WHERE
LOWER(username) = :username
AND (activated = true OR activated = :activated)
""",
{"username": username.lower(), "activated": active_only},
Account,
)
async def get_account_by_pubkey(
pubkey: str, conn: Connection | None = None
pubkey: str, active_only: bool = True, conn: Connection | None = None
) -> Account | None:
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
{"pubkey": pubkey.lower()},
"""
SELECT * FROM accounts
WHERE
LOWER(pubkey) = :pubkey
AND (activated = true OR activated = :activated)
""",
{"pubkey": pubkey.lower(), "activated": active_only},
Account,
)
async def get_account_by_email(
email: str, conn: Connection | None = None
email: str, active_only: bool = True, conn: Connection | None = None
) -> Account | None:
if len(email) == 0:
return None
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE LOWER(email) = :email",
{"email": email.lower()},
"""
SELECT * FROM accounts
WHERE
LOWER(email) = :email
AND (activated = true OR activated = :activated)
""",
{"email": email.lower(), "activated": active_only},
Account,
)
async def get_account_by_username_or_email(
username_or_email: str, conn: Connection | None = None
username_or_email: str,
active_only: bool = True,
conn: Connection | None = None,
) -> Account | None:
return await (conn or db).fetchone(
"""
SELECT * FROM accounts
WHERE LOWER(email) = :value or LOWER(username) = :value
WHERE
(LOWER(email) = :value or LOWER(username) = :value)
AND (activated = true OR activated = :activated)
""",
{"value": username_or_email.lower()},
{"value": username_or_email.lower(), "activated": active_only},
Account,
)
async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
async def get_user(
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:
account = await get_account(user_id, conn=conn)
account = await get_account(user_id, active_only, conn=conn)
if not account:
return None
return await get_user_from_account(account, conn=conn)
@@ -191,6 +225,7 @@ async def get_user_from_account(
return User(
id=account.id,
activated=account.activated,
email=account.email,
username=account.username,
pubkey=account.pubkey,
@@ -216,12 +251,31 @@ async def update_user_access_control_list(
async def get_user_access_control_lists(
user_id: str, conn: Connection | None = None
user_id: str, active_only: bool = True, conn: Connection | None = None
) -> UserAcls:
user_acls = await (conn or db).fetchone(
"SELECT id, access_control_list FROM accounts WHERE id = :id",
{"id": user_id},
"""
SELECT id, access_control_list FROM accounts
WHERE id = :user_id AND (activated = true OR activated = :activated)
""",
{"user_id": user_id, "activated": active_only},
UserAcls,
)
return user_acls or UserAcls(id=user_id)
async def clear_user_id_cache(user_id: str):
user = await get_user(user_id, active_only=True)
if user:
clear_user_cache(user)
def clear_user_cache(user: User):
user_cache_key: str | None = cache.pop(
f"auth:user:cache_key:{sha256s(user.id)}", None
)
if user_cache_key:
cache.pop(user_cache_key)
for wallet in user.wallets:
clear_wallet_cache(wallet)
+20 -8
View File
@@ -50,7 +50,7 @@ async def delete_wallet(
deleted: bool = True,
conn: Connection | None = None,
) -> None:
_clear_wallet_cache(wallet_id)
clear_wallet_id_cache(wallet_id)
now = int(time())
await (conn or db).execute(
@@ -65,7 +65,7 @@ async def delete_wallet(
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
_clear_wallet_cache(wallet_id)
clear_wallet_id_cache(wallet_id)
await (conn or db).execute(
"DELETE FROM wallets WHERE id = :wallet",
{"wallet": wallet_id},
@@ -75,7 +75,7 @@ async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) ->
async def delete_wallet_by_id(
wallet_id: str, conn: Connection | None = None
) -> int | None:
_clear_wallet_cache(wallet_id)
clear_wallet_id_cache(wallet_id)
now = int(time())
result = await (conn or db).execute(
# Timestamp placeholder is safe from SQL injection (not user input)
@@ -226,11 +226,14 @@ async def get_wallet_for_key(
) -> Wallet | None:
wallet = await (conn or db).fetchone(
"""
SELECT *, COALESCE((
SELECT wallets.*, COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0)
AS balance_msat FROM wallets
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
INNER JOIN accounts ON wallets.user = accounts.id
WHERE (adminkey = :key OR inkey = :key)
AND deleted = false
AND accounts.activated = true
""",
{"key": key},
Wallet,
@@ -250,8 +253,11 @@ async def get_base_wallet_for_key(
) -> BaseWallet | None:
wallet = await (conn or db).fetchone(
"""
SELECT id, "user", wallet_type, adminkey, inkey FROM wallets
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
SELECT wallets.id, "user", wallet_type, adminkey, inkey FROM wallets
INNER JOIN accounts ON wallets.user = accounts.id
WHERE (adminkey = :key OR inkey = :key)
AND deleted = false
AND accounts.activated = true
""",
{"key": key},
BaseWallet,
@@ -294,8 +300,14 @@ async def get_total_balance(conn: Connection | None = None):
return row.get("balance", 0) or 0
def _clear_wallet_cache(wallet_id):
def clear_wallet_id_cache(wallet_id: str):
cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}")
if cached_wallet:
cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}")
cache.pop(f"auth:x-api-key:{cached_wallet.inkey}")
def clear_wallet_cache(wallet: Wallet):
cache.pop(f"auth:wallet:{wallet.id}")
cache.pop(f"auth:x-api-key:{wallet.adminkey}")
cache.pop(f"auth:x-api-key:{wallet.inkey}")
+8
View File
@@ -856,3 +856,11 @@ async def m043_add_ui_customization_to_accounts(db: Connection):
Used for server side persistence of UI customization settings.
"""
await db.execute("ALTER TABLE accounts ADD COLUMN ui_customization TEXT")
async def m044_add_activated_to_accounts(db: Connection):
"""
Adds activated column to accounts.
Used for account activation status.
"""
await db.execute("ALTER TABLE accounts ADD COLUMN activated BOOLEAN DEFAULT true")
+10 -53
View File
@@ -6,23 +6,16 @@ from typing import Literal
from fastapi import Query
from lnurl import LnurlWithdrawResponse
from loguru import logger
from pydantic import BaseModel, Field, validator
from lnbits.db import FilterModel
from lnbits.fiat import get_fiat_provider
from lnbits.fiat.base import (
FiatPaymentFailedStatus,
FiatPaymentPendingStatus,
FiatPaymentStatus,
FiatPaymentSuccessStatus,
)
from lnbits.utils.exchange_rates import allowed_currencies
from lnbits.wallets import get_funding_source
from lnbits.wallets.base import (
PaymentFailedStatus,
PaymentPendingStatus,
PaymentStatus,
PaymentSuccessStatus,
)
@@ -130,59 +123,23 @@ class Payment(BaseModel):
"fiat_"
)
# DEPRECATED: in v1.5.0, use service check_payment_status instead
async def check_status(
self, skip_internal_payment_notifications: bool | None = False
) -> PaymentStatus:
if self.is_internal:
if self.success:
return PaymentSuccessStatus()
if self.failed:
return PaymentFailedStatus()
if self.is_in and self.fiat_provider:
fiat_status = await self.check_fiat_status(
skip_internal_payment_notifications
)
return PaymentStatus(paid=fiat_status.paid)
return PaymentPendingStatus()
funding_source = get_funding_source()
if self.is_out:
status = await funding_source.get_payment_status(self.checking_id)
else:
status = await funding_source.get_invoice_status(self.checking_id)
return status
logger.warning("payment.check_status() is deprecated.")
from lnbits.core.services.payments import check_payment_status
return await check_payment_status(self, skip_internal_payment_notifications)
# DEPRECATED: in v1.5.0, use service check_payment_status instead
async def check_fiat_status(
self, skip_internal_payment_notifications: bool | None = False
) -> FiatPaymentStatus:
if not self.is_internal:
return FiatPaymentPendingStatus()
if self.success:
return FiatPaymentSuccessStatus()
if self.failed:
return FiatPaymentFailedStatus()
logger.warning("payment.check_fiat_status() is deprecated.")
from lnbits.core.services.fiat_providers import check_fiat_status
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
return await check_fiat_status(self, skip_internal_payment_notifications)
class PaymentFilters(FilterModel):
+5
View File
@@ -181,6 +181,7 @@ class AccountId(BaseModel):
class Account(AccountId):
activated: bool = True
external_id: str | None = None # for external account linking
username: str | None = None
password_hash: str | None = None
@@ -241,6 +242,7 @@ class Account(AccountId):
class AccountOverview(Account):
activated: bool = True
transaction_count: int | None = 0
wallet_count: int | None = 0
balance_msat: int | None = 0
@@ -276,6 +278,7 @@ class AccountFilters(FilterModel):
class User(BaseModel):
id: str
activated: bool = True
created_at: datetime
updated_at: datetime
email: str | None = None
@@ -315,6 +318,7 @@ class RegisterUser(BaseModel):
username: str = Query(default=..., min_length=2, max_length=20)
password: str = Query(default=..., min_length=8, max_length=50)
password_repeat: str = Query(default=..., min_length=8, max_length=50)
invitation_code: str | None = Query(default=None, max_length=256)
class CreateUser(BaseModel):
@@ -358,6 +362,7 @@ class UpdateSuperuserPassword(BaseModel):
username: str = Query(default=..., min_length=2, max_length=20)
password: str = Query(default=..., min_length=8, max_length=50)
password_repeat: str = Query(default=..., min_length=8, max_length=50)
first_install_token: str | None = Query(None)
class LoginUsr(BaseModel):
+4
View File
@@ -1,3 +1,4 @@
from .fiat_providers import check_fiat_status
from .funding_source import (
get_balance_delta,
switch_to_voidwallet,
@@ -7,6 +8,7 @@ from .notifications import enqueue_admin_notification, send_payment_notification
from .payments import (
calculate_fiat_amounts,
cancel_hold_invoice,
check_payment_status,
check_transaction_status,
check_wallet_limits,
create_fiat_invoice,
@@ -40,6 +42,8 @@ __all__ = [
"calculate_fiat_amounts",
"cancel_hold_invoice",
"check_admin_settings",
"check_fiat_status",
"check_payment_status",
"check_transaction_status",
"check_wallet_limits",
"check_webpush_settings",
+44 -4
View File
@@ -12,6 +12,12 @@ from lnbits.core.models import CreatePayment, Payment, PaymentState
from lnbits.core.models.misc import SimpleStatus
from lnbits.db import Connection
from lnbits.fiat import get_fiat_provider
from lnbits.fiat.base import (
FiatPaymentFailedStatus,
FiatPaymentPendingStatus,
FiatPaymentStatus,
FiatPaymentSuccessStatus,
)
from lnbits.settings import settings
@@ -29,6 +35,40 @@ async def handle_fiat_payment_confirmation(
logger.warning(e)
async def check_fiat_status(
payment: Payment, skip_internal_payment_notifications: bool | None = False
) -> FiatPaymentStatus:
if not payment.is_internal:
return FiatPaymentPendingStatus()
if payment.success:
return FiatPaymentSuccessStatus()
if payment.failed:
return FiatPaymentFailedStatus()
if not payment.fiat_provider:
return FiatPaymentPendingStatus()
checking_id = payment.extra.get("fiat_checking_id")
if not checking_id:
return FiatPaymentPendingStatus()
fiat_provider = await get_fiat_provider(payment.fiat_provider)
if not fiat_provider:
return FiatPaymentPendingStatus()
fiat_status = await fiat_provider.get_invoice_status(checking_id)
if skip_internal_payment_notifications:
return fiat_status
if fiat_status.success:
# notify receivers asynchronously
from lnbits.tasks import internal_invoice_queue
await internal_invoice_queue.put(payment.checking_id)
return fiat_status
def check_stripe_signature(
payload: bytes,
sig_header: str | None,
@@ -78,8 +118,8 @@ async def verify_paypal_webhook(headers, payload: bytes):
"""
webhook_id = settings.paypal_webhook_id
if not webhook_id:
logger.warning("PayPal webhook ID not set; skipping verification.")
return
logger.warning("PayPal webhook ID not set.")
raise ValueError("PayPal webhook cannot be verified. Missing webhook ID.")
required_headers = {
"PAYPAL-TRANSMISSION-ID": headers.get("PAYPAL-TRANSMISSION-ID"),
@@ -89,8 +129,8 @@ async def verify_paypal_webhook(headers, payload: bytes):
"PAYPAL-AUTH-ALGO": headers.get("PAYPAL-AUTH-ALGO"),
}
if not all(required_headers.values()):
logger.warning("Missing PayPal webhook headers; skipping verification.")
return
logger.warning("Missing PayPal webhook headers.")
raise ValueError("PayPal webhook cannot be verified. Missing headers.")
try:
async with httpx.AsyncClient(base_url=settings.paypal_api_endpoint) as client:
+40 -5
View File
@@ -25,6 +25,7 @@ from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount
from lnbits.wallets import fake_wallet, get_funding_source
from lnbits.wallets.base import (
InvoiceResponse,
PaymentFailedStatus,
PaymentPendingStatus,
PaymentResponse,
PaymentStatus,
@@ -47,6 +48,7 @@ from ..models import (
PaymentState,
Wallet,
)
from .fiat_providers import check_fiat_status
from .notifications import send_payment_notification_in_background
payment_lock = asyncio.Lock()
@@ -364,7 +366,7 @@ async def update_pending_payments(wallet_id: str):
async def update_pending_payment(
payment: Payment, conn: Connection | None = None
) -> Payment:
status = await payment.check_status()
status = await check_payment_status(payment)
if status.failed:
payment.status = PaymentState.FAILED
await update_payment(payment, conn=conn)
@@ -618,7 +620,29 @@ async def check_transaction_status(
if payment.status == PaymentState.SUCCESS.value:
return PaymentSuccessStatus(fee_msat=payment.fee)
return await payment.check_status()
return await check_payment_status(payment)
async def check_payment_status(
payment: Payment, skip_internal_payment_notifications: bool | None = False
) -> PaymentStatus:
if payment.is_internal:
if payment.success:
return PaymentSuccessStatus()
if payment.failed:
return PaymentFailedStatus()
if payment.is_in and payment.fiat_provider:
fiat_status = await check_fiat_status(
payment, skip_internal_payment_notifications
)
return PaymentStatus(paid=fiat_status.paid)
return PaymentPendingStatus()
funding_source = get_funding_source()
if payment.is_out:
status = await funding_source.get_payment_status(payment.checking_id)
else:
status = await funding_source.get_invoice_status(payment.checking_id)
return status
async def get_payments_daily_stats(
@@ -752,7 +776,7 @@ async def _pay_internal_invoice(
await update_payment(internal_payment, conn=conn)
logger.success(f"internal payment successful {internal_payment.checking_id}")
send_payment_notification_in_background(wallet, payment)
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
# notify receiver asynchronously
from lnbits.tasks import internal_invoice_queue
@@ -825,7 +849,8 @@ async def _pay_external_invoice(
payment = await update_payment_success_status(
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}")
payment.checking_id = payment_response.checking_id
@@ -868,7 +893,7 @@ async def _verify_external_payment(
raise PaymentError("Payment already paid.", status="success")
# payment failed
status = await payment.check_status()
status = await check_payment_status(payment)
if status.failed:
raise PaymentError(
"Payment is failed node, retrying is not possible.", status="failed"
@@ -1033,3 +1058,13 @@ async def cancel_hold_invoice(payment: Payment) -> InvoiceResponse:
await update_payment(payment)
return response
async def _send_payment_notification_in_background(
wallet_id: str, payment: Payment, conn: Connection | None = None
):
# fetch balance again
wallet = await get_wallet(wallet_id, conn=conn)
if not wallet:
raise PaymentError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
send_payment_notification_in_background(wallet, payment)
+24
View File
@@ -3,8 +3,10 @@ from uuid import uuid4
from loguru import logger
from lnbits.core.crud.settings import set_settings_field
from lnbits.core.db import db
from lnbits.core.models.extensions import UserExtension
from lnbits.core.models.users import RegisterUser
from lnbits.db import Connection
from lnbits.settings import (
EditableSettings,
@@ -192,3 +194,25 @@ async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
editable_settings = EditableSettings.from_dict(settings.dict())
return await create_admin_settings(account.id, editable_settings.dict())
async def check_register_activation_settings(data: RegisterUser):
if not settings.lnbits_require_user_activation:
return None
if settings.lnbits_user_activation_by_invitation_code:
code = data.invitation_code.strip() if data.invitation_code else ""
if len(code) == 0:
raise ValueError("Invitation code cannot be empty.")
if code == settings.lnbits_register_reusable_activation_code:
return None
if code in settings.lnbits_register_one_time_activation_codes:
settings.lnbits_register_one_time_activation_codes.remove(code)
await set_settings_field(
"lnbits_register_one_time_activation_codes",
settings.lnbits_register_one_time_activation_codes,
)
return None
raise ValueError("Invalid invitation code.")
raise ValueError("No activation method provided.")
+24 -4
View File
@@ -26,7 +26,10 @@ from lnbits.core.models.users import (
UpdateAccessControlList,
)
from lnbits.core.services import create_user_account
from lnbits.core.services.users import update_user_account
from lnbits.core.services.users import (
check_register_activation_settings,
update_user_account,
)
from lnbits.decorators import (
access_token_payload,
check_account_exists,
@@ -86,6 +89,7 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
account = await get_account_by_username_or_email(data.username)
if not account or not account.verify_password(data.password):
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
return _auth_success_response(account.username, account.id, account.email)
@@ -94,7 +98,7 @@ async def nostr_login(request: Request) -> JSONResponse:
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
event = _nostr_nip98_event(request)
account = await get_account_by_pubkey(event["pubkey"])
account = await get_account_by_pubkey(event["pubkey"], active_only=False)
if not account:
account = Account(
id=uuid4().hex,
@@ -102,6 +106,8 @@ async def nostr_login(request: Request) -> JSONResponse:
extra=UserExtra(provider="nostr"),
)
await create_user_account(account)
if not account.activated:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User is not activated.")
return _auth_success_response(account.username or "", account.id, account.email)
@@ -357,12 +363,14 @@ async def register(data: RegisterUser) -> JSONResponse:
if not is_valid_username(data.username):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
if await get_account_by_username(data.username):
if await get_account_by_username(data.username, active_only=False):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
if data.email and not is_valid_email_address(data.email):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
await check_register_activation_settings(data)
account = Account(
id=uuid4().hex,
email=data.email,
@@ -503,9 +511,21 @@ async def update_ui_customization(
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
if not settings.first_install:
raise HTTPException(HTTPStatus.FORBIDDEN, "This is not your first install")
if settings.first_install_token:
if not data.first_install_token:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing first_install_token.")
if settings.first_install_token != data.first_install_token:
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid first_install_token.")
account = await get_account_by_username(data.username, False)
if account:
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
account = await get_account(settings.super_user)
if not account:
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
account.username = data.username
account.extra = account.extra or UserExtra()
account.extra.provider = "lnbits"
@@ -521,7 +541,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: str | None = Non
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
redirect_path = "/wallet"
account = await get_account_by_email(email)
account = await get_account_by_email(email, active_only=False)
if verified_user_id:
if account:
+41 -19
View File
@@ -9,6 +9,7 @@ from lnbits.core.crud.payments import (
from lnbits.core.models.misc import SimpleStatus
from lnbits.core.models.payments import CreateInvoice
from lnbits.core.services.fiat_providers import (
check_fiat_status,
check_stripe_signature,
verify_paypal_webhook,
)
@@ -23,7 +24,7 @@ callback_router = APIRouter(prefix="/api/v1/callback", tags=["callback"])
async def api_generic_webhook_handler(
provider_name: str, request: Request
) -> SimpleStatus:
logger.info(f"Received callback from provider: '{provider_name}'.")
if provider_name.lower() == "stripe":
payload = await request.body()
sig_header = request.headers.get("Stripe-Signature")
@@ -87,7 +88,7 @@ async def _handle_stripe_intent_session_completed(event: dict):
if not payment:
logger.warning(f"No payment found for hash: '{payment_hash}'.")
return
await payment.check_fiat_status()
await check_fiat_status(payment)
async def _handle_stripe_checkout_session_completed(event: dict):
@@ -110,7 +111,7 @@ async def _handle_stripe_checkout_session_completed(event: dict):
payment = await get_standalone_payment(payment_hash)
if not payment:
raise ValueError(f"No payment found for hash: '{payment_hash}'.")
await payment.check_fiat_status()
await check_fiat_status(payment)
async def _handle_stripe_subscription_invoice_paid(event: dict):
@@ -155,7 +156,7 @@ async def _handle_stripe_subscription_invoice_paid(event: dict):
),
)
await payment.check_fiat_status()
await check_fiat_status(payment)
async def _get_stripe_subscription_payment_options(
@@ -180,8 +181,10 @@ async def _get_stripe_subscription_payment_options(
async def handle_paypal_event(event: dict):
event_id = event.get("id", "")
event_type = event.get("event_type", "")
resource = event.get("resource", {})
logger.info(f"Handling PayPal event: '{event_id}'. Type: '{event_type}'.")
if event_type in ("CHECKOUT.ORDER.APPROVED", "PAYMENT.CAPTURE.COMPLETED"):
payment_hash = _paypal_extract_payment_hash(resource)
@@ -192,23 +195,20 @@ async def handle_paypal_event(event: dict):
if not payment:
logger.warning(f"No payment found for hash: '{payment_hash}'.")
return
await payment.check_fiat_status()
await check_fiat_status(payment)
return
if event_type in (
"PAYMENT.SALE.COMPLETED",
"BILLING.SUBSCRIPTION.PAYMENT.SUCCEEDED",
):
if event_type in ("PAYMENT.SALE.COMPLETED"):
await _handle_paypal_subscription_payment(resource)
return
logger.info(f"Unhandled PayPal event type: '{event_type}'.")
logger.warning(f"Unhandled PayPal event type: '{event_type}'.")
async def _handle_paypal_subscription_payment(resource: dict):
amount_info = resource.get("amount") or {}
currency = (amount_info.get("currency_code") or "").upper()
total = amount_info.get("value")
currency = (amount_info.get("currency") or "").upper()
total = amount_info.get("total")
if not currency or total is None:
raise ValueError("PayPal subscription event missing amount.")
@@ -216,18 +216,14 @@ async def _handle_paypal_subscription_payment(resource: dict):
if not custom_id:
raise ValueError("PayPal subscription event missing custom metadata.")
try:
metadata = json.loads(custom_id)
except json.JSONDecodeError:
metadata = {}
payment_options = FiatSubscriptionPaymentOptions(**metadata)
payment_options = _deserialize_paypal_metadata(custom_id)
if not payment_options.wallet_id:
raise ValueError("PayPal subscription event missing wallet_id.")
memo = payment_options.memo or ""
extra = {
**(payment_options.extra or {}),
"subscription_request_id": resource.get("billing_agreement_id"),
"fiat_method": "subscription",
"tag": payment_options.tag,
"subscription": {
@@ -247,7 +243,7 @@ async def _handle_paypal_subscription_payment(resource: dict):
),
)
await payment.check_fiat_status()
await check_fiat_status(payment)
def _paypal_extract_payment_hash(resource: dict) -> str | None:
@@ -258,3 +254,29 @@ def _paypal_extract_payment_hash(resource: dict) -> str | None:
if pu.get("custom_id"):
return pu.get("custom_id")
return None
def _deserialize_paypal_metadata(custom_id: str) -> FiatSubscriptionPaymentOptions:
try:
meta = json.loads(custom_id)
wallet_id = meta[0] if len(meta) > 0 else None
tag = meta[1] if len(meta) > 1 else None
subscription_request_id = meta[2] if len(meta) > 2 else None
extra_link = meta[3] if len(meta) > 3 else None
memo = meta[4] if len(meta) > 4 else None
extra = {
"link": extra_link,
"subscription_request_id": subscription_request_id,
}
return FiatSubscriptionPaymentOptions(
wallet_id=wallet_id,
tag=tag,
subscription_request_id=subscription_request_id,
extra=extra,
memo=memo,
)
except (json.JSONDecodeError, IndexError) as e:
logger.warning(f"Failed to deserialize PayPal metadata: {e}")
return FiatSubscriptionPaymentOptions()
+12
View File
@@ -1,6 +1,7 @@
from http import HTTPStatus
from fastapi import APIRouter, Depends, HTTPException
from loguru import logger
from lnbits.core.models.misc import SimpleStatus
from lnbits.core.models.wallets import WalletTypeInfo
@@ -30,6 +31,11 @@ async def create_subscription(
data: CreateFiatSubscription,
key_type: WalletTypeInfo = Depends(require_admin_key),
) -> FiatSubscriptionResponse:
logger.debug(
f"Creating subscription with provider '{provider}. '"
f"Subscription ID '{data.subscription_id}'."
)
fiat_provider = await get_fiat_provider(provider)
if not fiat_provider:
raise HTTPException(404, "Fiat provider not found")
@@ -47,6 +53,12 @@ async def create_subscription(
subscription_response = await fiat_provider.create_subscription(
data.subscription_id, data.quantity, data.payment_options
)
if subscription_response.error_message:
logger.warning(
f"Failed to create subscription with provider '{provider}': "
f"{subscription_response.error_message}"
)
return subscription_response
+40 -2
View File
@@ -20,6 +20,7 @@ from lnbits.core.crud import (
update_admin_settings,
update_wallet,
)
from lnbits.core.crud.users import clear_user_id_cache, get_account, update_account
from lnbits.core.crud.wallets import delete_wallet_by_id
from lnbits.core.models import (
AccountFilters,
@@ -73,7 +74,7 @@ async def api_get_users(
summary="Get user by Id",
)
async def api_get_user(user_id: str) -> User:
user = await get_user(user_id)
user = await get_user(user_id, active_only=False)
if not user:
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
return user
@@ -197,7 +198,7 @@ async def api_users_reset_password(user_id: str) -> str:
return f"reset_key_{reset_key_b64}"
@users_router.get(
@users_router.put(
"/user/{user_id}/admin",
dependencies=[Depends(check_super_user)],
name="Give or revoke admin permsisions to a user",
@@ -220,6 +221,43 @@ async def api_users_toggle_admin(user_id: str) -> SimpleStatus:
)
@users_router.put(
"/user/{user_id}/activate",
name="Activate or deactivate a user",
)
async def api_users_toggle_activated(
user_id: str, admin_account: Account = Depends(check_admin)
) -> SimpleStatus:
if user_id == settings.super_user:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Cannot deactivate super user.",
)
if user_id == admin_account.id:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="You cannot deactivate yourself.",
)
if settings.is_admin_user(user_id):
settings.lnbits_admin_users.remove(user_id)
user_account = await get_account(user_id, active_only=False)
if not user_account:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="User not found.",
)
user_account.activated = not user_account.activated
await update_account(user_account)
await clear_user_id_cache(user_id)
return SimpleStatus(
success=True,
message=f"User {'activated' if user_account.activated else 'deactivated'}.",
)
@users_router.get("/user/{user_id}/wallet", name="Get wallets for user")
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
return await get_wallets(user_id, deleted=None)
+2 -4
View File
@@ -9,6 +9,7 @@ from fastapi import (
)
from lnbits.core.crud.wallets import (
clear_wallet_cache,
create_wallet,
get_wallets_paginated,
)
@@ -37,7 +38,6 @@ from lnbits.decorators import (
require_invoice_key,
)
from lnbits.helpers import generate_filter_params_openapi
from lnbits.utils.cache import cache
from ..crud import (
delete_wallet,
@@ -134,9 +134,7 @@ async def api_reset_wallet_keys(
if not wallet or wallet.user != account_id.id:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
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}")
clear_wallet_cache(wallet)
wallet.adminkey = uuid4().hex
wallet.inkey = uuid4().hex
+9 -42
View File
@@ -12,7 +12,6 @@ from typing import Any, Generic, Literal, TypeVar, get_origin
from loguru import logger
from pydantic import BaseModel, ValidationError, root_validator
from sqlalchemy import event
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
from sqlalchemy.sql import text
@@ -56,14 +55,6 @@ def compat_timestamp_placeholder(key: str):
return f":{key}"
def get_placeholder(model: Any, field: str) -> str:
type_ = model.__fields__[field].type_
if type_ == datetime:
return compat_timestamp_placeholder(field)
else:
return f":{field}"
class Compat:
type: str | None = "<inherited>"
schema: str | None = "<inherited>"
@@ -326,31 +317,7 @@ class Database(Compat):
self.engine: AsyncEngine = create_async_engine(
database_uri, echo=settings.debug_database
)
if self.type in {POSTGRES, COCKROACH}:
@event.listens_for(self.engine.sync_engine, "connect")
def register_custom_types(dbapi_connection, *_):
def _parse_date(value) -> datetime:
if value is None:
value = "1970-01-01 00:00:00"
f = "%Y-%m-%d %H:%M:%S.%f"
if "." not in value:
f = "%Y-%m-%d %H:%M:%S"
# Parse and add UTC timezone info
return datetime.strptime(value, f).replace(tzinfo=timezone.utc)
dbapi_connection.run_async(
lambda connection: connection.set_type_codec(
"TIMESTAMP",
encoder=datetime,
decoder=_parse_date,
schema="pg_catalog",
)
)
self.lock = asyncio.Lock()
logger.trace(f"database {self.type} added for {self.name}")
@asynccontextmanager
@@ -663,7 +630,7 @@ def insert_query(table_name: str, model: BaseModel) -> str:
placeholders = []
keys = model_to_dict(model).keys()
for field in keys:
placeholders.append(get_placeholder(model, field))
placeholders.append(f":{field}")
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
fields = ", ".join([f'"{key}"' for key in keys])
values = ", ".join(placeholders)
@@ -681,9 +648,8 @@ def update_query(
"""
fields = []
for field in model_to_dict(model).keys():
placeholder = get_placeholder(model, field)
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
fields.append(f'"{field}" = {placeholder}')
fields.append(f'"{field}" = :{field}')
query = ", ".join(fields)
return f"UPDATE {table_name} SET {query} {where}" # noqa: S608
@@ -701,7 +667,12 @@ def model_to_dict(model: BaseModel) -> dict:
if model.__fields__[key].field_info.extra.get("no_database", False):
continue
if isinstance(value, datetime):
_dict[key] = value.timestamp()
if DB_TYPE == SQLITE:
_dict[key] = value.timestamp()
else:
# remove tz. postgres and cockroach TIMESTAMP is not tz aware
# so it will throw if we dont remove the UTC.
_dict[key] = value.replace(tzinfo=None)
continue
if (
type(type_) is type(BaseModel)
@@ -757,11 +728,7 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel: # noqa: C901
if DB_TYPE == SQLITE:
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
else:
# 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
_dict[key] = value.replace(tzinfo=timezone.utc)
continue
if issubclass(type_, BaseModel):
_dict[key] = dict_to_submodel(type_, value)
+1
View File
@@ -261,6 +261,7 @@ async def check_account_id_exists(
account_id,
expiry=settings.auth_authentication_cache_minutes * 60,
)
cache.set(f"auth:user:cache_key:{sha256s(account.id)}", cache_key)
return account_id
+63 -18
View File
@@ -2,7 +2,7 @@ import asyncio
import json
import time
from collections.abc import AsyncGenerator
from typing import Any
from typing import Any, Literal
import httpx
from loguru import logger
@@ -24,6 +24,8 @@ from .base import (
FiatSubscriptionResponse,
)
FiatMethod = Literal["checkout", "subscription"]
class PayPalCheckoutOptions(BaseModel):
class Config:
@@ -34,11 +36,21 @@ class PayPalCheckoutOptions(BaseModel):
metadata: dict[str, Any] = Field(default_factory=dict)
class PayPalSubscriptionOptions(BaseModel):
class Config:
extra = "ignore"
checking_id: str | None = None
payment_request: str | None = None
class PayPalCreateInvoiceOptions(BaseModel):
class Config:
extra = "ignore"
fiat_method: FiatMethod = "checkout"
checkout: PayPalCheckoutOptions | None = None
subscription: PayPalSubscriptionOptions | None = None
class PayPalWallet(FiatProvider):
@@ -96,6 +108,14 @@ class PayPalWallet(FiatProvider):
opts = self._parse_create_opts(extra or {})
if opts is None:
return FiatInvoiceResponse(ok=False, error_message="Invalid PayPal options")
if opts.fiat_method == "subscription":
term = opts.subscription or PayPalSubscriptionOptions()
checking_id = term.checking_id or urlsafe_short_hash()
return FiatInvoiceResponse(
ok=True,
checking_id=f"subscription_{checking_id}",
payment_request=term.payment_request or "",
)
try:
await self._ensure_access_token()
@@ -171,6 +191,7 @@ class PayPalWallet(FiatProvider):
or "https://lnbits.com"
)
logger.debug(f"Creating PayPal subscription with ID '{subscription_id}'")
if not payment_options.subscription_request_id:
payment_options.subscription_request_id = urlsafe_short_hash()
payment_options.extra = payment_options.extra or {}
@@ -182,7 +203,6 @@ class PayPalWallet(FiatProvider):
await self._ensure_access_token()
payload = {
"plan_id": subscription_id,
"quantity": str(quantity),
"custom_id": self._serialize_metadata(payment_options),
"application_context": {
"return_url": success_url,
@@ -205,7 +225,7 @@ class PayPalWallet(FiatProvider):
return FiatSubscriptionResponse(
ok=True,
checkout_session_url=approval_url,
subscription_request_id=payment_options.subscription_request_id,
subscription_request_id=data.get("id"),
)
except Exception as exc:
logger.warning(exc)
@@ -219,6 +239,10 @@ class PayPalWallet(FiatProvider):
correlation_id: str,
**kwargs,
) -> FiatSubscriptionResponse:
logger.debug(
f"Cancelling PayPal subscription '{subscription_id}'. "
f"Correlation ID '{correlation_id}'."
)
try:
await self._ensure_access_token()
r = await self.client.post(
@@ -240,12 +264,20 @@ class PayPalWallet(FiatProvider):
async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
try:
await self._ensure_access_token()
order_id = self._normalize_paypal_id(checking_id)
r = await self.client.get(
f"/v2/checkout/orders/{order_id}", headers=self._auth_headers()
)
r.raise_for_status()
return self._status_from_order(r.json())
paypal_id = self._normalize_paypal_id(checking_id)
if paypal_id.startswith("subscription_"):
capture_id = paypal_id.replace("subscription_", "", 1)
r = await self.client.get(
f"v2/payments/captures/{capture_id}", headers=self._auth_headers()
)
r.raise_for_status()
return self._status_from_capture(r.json())
else:
r = await self.client.get(
f"/v2/checkout/orders/{paypal_id}", headers=self._auth_headers()
)
r.raise_for_status()
return self._status_from_order(r.json())
except Exception as exc:
logger.debug(f"Error getting PayPal order status: {exc}")
return FiatPaymentPendingStatus()
@@ -270,6 +302,14 @@ class PayPalWallet(FiatProvider):
return FiatPaymentFailedStatus()
return FiatPaymentPendingStatus()
def _status_from_capture(self, order: dict[str, Any]) -> FiatPaymentStatus:
status = (order.get("status") or "").upper()
if status in ["COMPLETED"]:
return FiatPaymentSuccessStatus()
if status in ["DECLINED", "FAILED", "CANCELLED", "CANCELED"]:
return FiatPaymentFailedStatus()
return FiatPaymentPendingStatus()
def _normalize_paypal_id(self, checking_id: str) -> str:
return (
checking_id.replace("fiat_paypal_", "", 1)
@@ -280,15 +320,20 @@ class PayPalWallet(FiatProvider):
def _serialize_metadata(
self, payment_options: FiatSubscriptionPaymentOptions
) -> str:
md = {
"wallet_id": payment_options.wallet_id,
"memo": payment_options.memo,
"extra": payment_options.extra,
"tag": payment_options.tag,
"subscription_request_id": payment_options.subscription_request_id,
}
raw = json.dumps(md)
return raw[:127] # PayPal custom_id limit
meta = [
payment_options.wallet_id,
payment_options.tag,
payment_options.subscription_request_id,
]
if payment_options.extra:
meta.append(payment_options.extra.get("link", None))
# Keep custom_id within PayPal's 127-char limit
memo_limit = 120 - len(json.dumps(meta))
if memo_limit > 0 and payment_options.memo:
meta.append(payment_options.memo[:memo_limit])
return json.dumps(meta)
def _parse_create_opts(
self, raw_opts: dict[str, Any]
+8 -54
View File
@@ -70,60 +70,11 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
t.env.globals["static_url_for"] = static_url_for
t.env.globals["normalize_path"] = normalize_path
window_settings = {
"AD_SPACE": settings.lnbits_ad_space,
"AD_SPACE_ENABLED": settings.lnbits_ad_space_enabled,
"AD_SPACE_TITLE": settings.lnbits_ad_space_title,
"EXTENSIONS": list(settings.lnbits_installed_extensions_ids),
"HIDE_API": settings.lnbits_hide_api,
"SITE_TITLE": settings.lnbits_site_title,
"SITE_TAGLINE": settings.lnbits_site_tagline,
"SITE_DESCRIPTION": settings.lnbits_site_description,
"LNBITS_ADMIN_UI": settings.lnbits_admin_ui,
"LNBITS_AUDIT_ENABLED": settings.lnbits_audit_enabled,
"LNBITS_AUTH_METHODS": settings.auth_allowed_methods,
"LNBITS_AUTH_KEYCLOAK_ORG": settings.keycloak_client_custom_org,
"LNBITS_AUTH_KEYCLOAK_ICON": settings.keycloak_client_custom_icon,
"LNBITS_CUSTOM_IMAGE": settings.lnbits_custom_image,
"LNBITS_CUSTOM_BADGE": settings.lnbits_custom_badge,
"LNBITS_CUSTOM_BADGE_COLOR": settings.lnbits_custom_badge_color,
"LNBITS_EXTENSIONS_DEACTIVATE_ALL": settings.lnbits_extensions_deactivate_all,
"LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed,
"LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager,
"LNBITS_NODE_UI_AVAILABLE": settings.has_nodemanager,
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
"LNBITS_APPLE_TOUCH_ICON": settings.lnbits_apple_touch_icon,
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
"LNBITS_SERVICE_FEE_WALLET": settings.lnbits_service_fee_wallet,
"LNBITS_SHOW_HOME_PAGE_ELEMENTS": settings.lnbits_show_home_page_elements,
"LNBITS_THEME_OPTIONS": settings.lnbits_theme_options,
"WALLET_FEATURED_BUTTON_LABEL": settings.lnbits_wallet_featured_button_label,
"WALLET_FEATURED_BUTTON_URL": settings.lnbits_wallet_featured_button_url,
"WALLET_FEATURED_BUTTON_ICON": settings.lnbits_wallet_featured_button_icon,
"LNBITS_VERSION": settings.version,
"USE_CUSTOM_LOGO": settings.lnbits_custom_logo,
"LNBITS_DEFAULT_REACTION": settings.lnbits_default_reaction,
"LNBITS_DEFAULT_THEME": settings.lnbits_default_theme,
"LNBITS_DEFAULT_BORDER": settings.lnbits_default_border,
"LNBITS_DEFAULT_GRADIENT": settings.lnbits_default_gradient,
"LNBITS_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
"LNBITS_EXTENSIONS_REVIEWS_URL": settings.lnbits_extensions_reviews_url,
"LNBITS_DENOMINATION": settings.lnbits_denomination,
"has_holdinvoice": settings.has_holdinvoice,
"LNBITS_NOSTR_CONFIGURED": settings.is_nostr_notifications_configured(),
"LNBITS_TELEGRAM_CONFIGURED": settings.is_telegram_notifications_configured(),
"LNBITS_EXT_BUILDER": settings.lnbits_extensions_builder_activate_non_admins,
"LNBITS_CURRENCIES": list(currencies.keys()),
"LNBITS_ALLOWED_CURRENCIES": settings.lnbits_allowed_currencies,
"CACHE_KEY": settings.server_startup_time,
}
t.env.globals["WINDOW_SETTINGS"] = window_settings
for key, value in window_settings.items():
t.env.globals[key] = value
# used in base.html
t.env.globals["SITE_TITLE"] = settings.lnbits_site_title
t.env.globals["LNBITS_APPLE_TOUCH_ICON"] = settings.lnbits_apple_touch_icon
t.env.globals["SETTINGS"] = settings.to_public().dict(by_alias=True)
t.env.globals["CURRENCIES"] = list(currencies.keys())
if settings.bundle_assets:
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
@@ -137,6 +88,9 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
# backwards compatibility for extensions (tpos)
t.env.globals["LNBITS_DENOMINATION"] = settings.lnbits_denomination
return t
-84
View File
@@ -1,6 +1,5 @@
import asyncio
import json
import re
from datetime import datetime, timezone
from http import HTTPStatus
from typing import Any
@@ -19,53 +18,6 @@ from lnbits.core.models import AuditEntry
from lnbits.helpers import normalize_path, template_renderer
from lnbits.settings import settings
_LOCALHOST_IPS = {"127.0.0.1", "::1"}
_PUBLIC_ASSET_PATHS = {
"/favicon.ico",
"/service-worker.js",
}
def _normalize_match_path(path: str) -> str:
if not path:
return "/"
if path != "/" and path.endswith("/"):
return path.rstrip("/")
return path
def _route_pattern_matches(pattern: str, path: str) -> bool:
if not pattern:
return False
if not pattern.startswith("/"):
pattern = f"/{pattern}"
pattern = _normalize_match_path(pattern)
path = _normalize_match_path(path)
if pattern.endswith("*"):
prefix = pattern.rstrip("*")
return path.startswith(prefix)
if pattern == path:
return True
escaped = re.escape(pattern)
escaped = re.sub(r"\\\{[^/]+\\\}", r"[^/]+", escaped)
return re.fullmatch(escaped, path) is not None
def _response_by_accepted_type(request: Request, msg: str, status_code: HTTPStatus):
accept_header = request.headers.get("accept", "")
if "text/html" in accept_header.split(","):
return HTMLResponse(
status_code=status_code,
content=template_renderer()
.TemplateResponse(
request,
"error.html",
{"err": msg, "status_code": status_code, "message": msg},
)
.body,
)
return JSONResponse(status_code=status_code, content={"detail": msg})
class InstalledExtensionMiddleware:
# This middleware class intercepts calls made to the extensions API and:
@@ -283,42 +235,6 @@ def add_ip_block_middleware(app: FastAPI):
return await call_next(request)
def add_route_access_middleware(app: FastAPI):
@app.middleware("http")
async def route_access_middleware(request: Request, call_next):
if not settings.lnbits_route_access_control_enabled:
return await call_next(request)
if not request.client:
return JSONResponse(
status_code=HTTPStatus.FORBIDDEN,
content={"detail": "No request client"},
)
if request.client.host in _LOCALHOST_IPS:
return await call_next(request)
path = request.url.path or "/"
if "/static/" in path or path in _PUBLIC_ASSET_PATHS:
return await call_next(request)
whitelist = settings.lnbits_route_access_whitelist
blacklist = settings.lnbits_route_access_blacklist
if whitelist:
if any(_route_pattern_matches(route, path) for route in whitelist):
return await call_next(request)
return _response_by_accepted_type(
request, f"Route not whitelisted: {path}", HTTPStatus.FORBIDDEN
)
if blacklist and any(
_route_pattern_matches(route, path) for route in blacklist
):
return _response_by_accepted_type(
request, f"Route is blacklisted: {path}", HTTPStatus.FORBIDDEN
)
return await call_next(request)
def add_first_install_middleware(app: FastAPI):
@app.middleware("http")
async def first_install_middleware(request: Request, call_next):
+153 -4
View File
@@ -41,6 +41,14 @@ class UsersSettings(LNbitsSettings):
lnbits_admin_users: list[str] = Field(default=[])
lnbits_allowed_users: list[str] = Field(default=[])
lnbits_allow_new_accounts: bool = Field(default=True)
lnbits_require_user_activation: bool = Field(default=False)
lnbits_user_activation_by_email: bool = Field(default=False)
lnbits_user_activation_by_payment: bool = Field(default=False)
lnbits_user_activation_by_invitation_code: bool = Field(default=False)
lnbits_register_reusable_activation_code: str = Field(default="")
lnbits_register_one_time_activation_codes: list[str] = Field(default=[])
@property
def new_accounts_allowed(self) -> bool:
@@ -284,10 +292,14 @@ class ThemesSettings(LNbitsSettings):
lnbits_qr_logo: str = Field(default="/static/images/favicon_qr_logo.png")
lnbits_apple_touch_icon: str | None = Field(default=None)
lnbits_default_reaction: str = Field(default="confettiBothSides")
lnbits_default_theme: str = Field(default="salvador")
lnbits_default_theme: str = Field(default="bitcoin")
lnbits_default_border: str = Field(default="hard-border")
lnbits_default_gradient: bool = Field(default=True)
lnbits_default_bgimage: str | None = Field(default=None)
lnbits_default_dark: bool = Field(default=True)
lnbits_default_card_rounded: bool = Field(default=True)
lnbits_default_card_gradient: bool = Field(default=True)
lnbits_default_card_shadow: bool = Field(default=False)
class OpsSettings(LNbitsSettings):
@@ -421,9 +433,6 @@ class SecuritySettings(LNbitsSettings):
lnbits_rate_limit_unit: str = Field(default="minute")
lnbits_allowed_ips: list[str] = Field(default=[])
lnbits_blocked_ips: list[str] = Field(default=[])
lnbits_route_access_control_enabled: bool = Field(default=False)
lnbits_route_access_whitelist: list[str] = Field(default=[])
lnbits_route_access_blacklist: list[str] = Field(default=[])
lnbits_callback_url_rules: list[str] = Field(
default=["https?://([a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})(:\\d+)?"]
)
@@ -597,6 +606,16 @@ class SparkFundingSource(LNbitsSettings):
spark_token: str | None = Field(default=None)
class SparkL2FundingSource(LNbitsSettings):
spark_l2_network: str = Field(default="MAINNET")
spark_l2_external_endpoint: str | None = Field(default="http://localhost:8765")
spark_l2_external_api_key: str | None = Field(default=None)
spark_l2_mnemonic: str | None = Field(default=None)
spark_l2_pay_wait_ms: int = Field(default=4000, ge=0)
spark_l2_pay_poll_ms: int = Field(default=500, ge=0)
spark_l2_stream_keepalive_ms: int = Field(default=15000, ge=0)
class LnTipsFundingSource(LNbitsSettings):
lntips_api_endpoint: str | None = Field(default=None)
lntips_api_key: str | None = Field(default=None)
@@ -702,6 +721,7 @@ class FundingSourcesSettings(
PhoenixdFundingSource,
OpenNodeFundingSource,
SparkFundingSource,
SparkL2FundingSource,
LnTipsFundingSource,
NWCFundingSource,
BreezSdkFundingSource,
@@ -986,6 +1006,7 @@ class EnvSettings(LNbitsSettings):
enable_log_to_file: bool = Field(default=True)
log_rotation: str = Field(default="100 MB")
log_retention: str = Field(default="3 months")
first_install_token: str | None = Field(default=None)
cleanup_wallets_days: int = Field(default=90, ge=0)
funding_source_max_retries: int = Field(default=4, ge=0)
@@ -1029,6 +1050,7 @@ class SuperUserSettings(LNbitsSettings):
"FakeWallet",
"LNPayWallet",
"LNbitsWallet",
"SparkL2Wallet",
"LnTipsWallet",
"LndRestWallet",
"LndWallet",
@@ -1134,6 +1156,133 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
and ext_id in self.lnbits_all_extensions_ids
)
def to_public(self) -> PublicSettings:
return PublicSettings.from_settings(self)
class PublicSettings(BaseModel):
"""
PublicSettings is used for templating and public information.
WARNING: do not expose private information, PublicSettings is exposed publicly.
"""
allow_register: bool = Field(alias="allowRegister")
qr_logo: str = Field(alias="qrLogo")
site_title: str = Field(alias="siteTitle")
site_tagline: str = Field(alias="siteTagline")
site_description: str | None = Field(alias="siteDescription")
auth_methods: list[str] = Field(alias="authMethods")
keycloak_org: str | None = Field(alias="keycloakOrg")
keycloak_icon: str | None = Field(alias="keycloakIcon")
has_holdinvoice: bool = Field(alias="hasHoldinvoice")
has_nodemanager: bool = Field(alias="hasNodemanager")
show_nodemanager: bool = Field(alias="showNodemanager")
custom_logo: str | None = Field(alias="customLogo")
show_voidwallet: bool = Field(alias="showVoidwallet")
version: str = Field(alias="version")
show_home_page_elements: bool = Field(alias="showHomePageElements")
hide_api: bool = Field(alias="hideApi")
cache_key: str = Field(alias="cacheKey")
webpush_pubkey: str | None = Field(alias="webpushPubkey")
show_extensions: bool = Field(alias="showExtensions")
show_audit: bool = Field(alias="showAudit")
show_admin: bool = Field(alias="showAdmin")
ad_space: list[list[str]] = Field(alias="adSpace")
ad_space_title: str = Field(alias="adSpaceTitle")
show_ad_space: bool = Field(alias="showAdSpace")
custom_image: str | None = Field(alias="customImage")
custom_badge: str | None = Field(alias="customBadge")
custom_badge_color: str | None = Field(alias="customBadgeColor")
service_fee: float = Field(alias="serviceFee")
service_fee_max: int = Field(alias="serviceFeeMax")
service_fee_wallet: str | None = Field(alias="serviceFeeWallet")
theme_options: list[str] = Field(alias="themeOptions")
default_reaction: str = Field(alias="defaultReaction")
default_theme: str = Field(alias="defaultTheme")
default_border: str = Field(alias="defaultBorder")
default_bgimage: str | None = Field(alias="defaultBgimage")
default_gradient: bool = Field(alias="defaultGradient")
default_dark: bool = Field(alias="defaultDark")
default_card_rounded: bool = Field(alias="defaultCardRounded")
default_card_gradient: bool = Field(alias="defaultCardGradient")
default_card_shadow: bool = Field(alias="defaultCardShadow")
denomination: str | None = Field()
extensions: list[str] = Field()
allowed_currencies: list[str] = Field(alias="allowedCurrencies")
extensions_reviews_url: str = Field(alias="extensionsReviewsUrl")
ext_builder: bool = Field(alias="extBuilder")
nostr_configured: bool = Field(alias="nostrConfigured")
telegram_configured: bool = Field(alias="telegramConfigured")
wallet_featured_button_label: str | None = Field(alias="walletFeaturedButtonLabel")
wallet_featured_button_url: str | None = Field(alias="walletFeaturedButtonUrl")
wallet_featured_button_icon: str | None = Field(alias="walletFeaturedButtonIcon")
lnbits_user_activation_by_email: bool = Field(alias="userActivationByEmail")
lnbits_user_activation_by_payment: bool = Field(alias="userActivationByPayment")
lnbits_user_activation_by_invitation_code: bool = Field(
alias="userActivationByInvitationCode"
)
has_first_install_token: bool = Field(alias="hasFirstInstallToken")
@classmethod
def from_settings(cls, settings: Settings):
ads = [x.split(";") for x in settings.lnbits_ad_space.split(",")]
return cls(
adSpace=ads,
adSpaceTitle=settings.lnbits_ad_space_title,
showAdSpace=settings.lnbits_ad_space_enabled and len(ads) > 0,
allowRegister=settings.new_accounts_allowed,
qrLogo=settings.lnbits_qr_logo,
siteDescription=settings.lnbits_site_description,
siteTitle=settings.lnbits_site_title,
siteTagline=settings.lnbits_site_tagline,
authMethods=settings.auth_allowed_methods,
keycloakOrg=settings.keycloak_client_custom_org,
keycloakIcon=settings.keycloak_client_custom_icon,
hasHoldinvoice=settings.has_holdinvoice,
hasNodemanager=settings.has_nodemanager,
showNodemanager=settings.lnbits_node_ui and settings.has_nodemanager,
customLogo=settings.lnbits_custom_logo,
showVoidwallet=settings.lnbits_backend_wallet_class == "VoidWallet",
version=settings.version,
showHomePageElements=settings.lnbits_show_home_page_elements,
hideApi=settings.lnbits_hide_api,
cacheKey=str(settings.server_startup_time),
webpushPubkey=settings.lnbits_webpush_pubkey,
showExtensions=not settings.lnbits_extensions_deactivate_all,
showAudit=settings.lnbits_audit_enabled,
showAdmin=settings.lnbits_admin_ui,
customImage=settings.lnbits_custom_image,
customBadge=settings.lnbits_custom_badge,
customBadgeColor=settings.lnbits_custom_badge_color,
serviceFee=settings.lnbits_service_fee,
serviceFeeMax=settings.lnbits_service_fee_max,
serviceFeeWallet=settings.lnbits_service_fee_wallet,
themeOptions=settings.lnbits_theme_options,
defaultReaction=settings.lnbits_default_reaction,
defaultTheme=settings.lnbits_default_theme,
defaultBorder=settings.lnbits_default_border,
defaultGradient=settings.lnbits_default_gradient,
defaultBgimage=settings.lnbits_default_bgimage,
defaultDark=settings.lnbits_default_dark,
defaultCardRounded=settings.lnbits_default_card_rounded,
defaultCardGradient=settings.lnbits_default_card_gradient,
defaultCardShadow=settings.lnbits_default_card_shadow,
denomination=settings.lnbits_denomination,
extensions=list(settings.lnbits_installed_extensions_ids),
allowedCurrencies=settings.lnbits_allowed_currencies,
extensionsReviewsUrl=settings.lnbits_extensions_reviews_url,
extBuilder=settings.lnbits_extensions_builder_activate_non_admins,
nostrConfigured=settings.is_nostr_notifications_configured(),
telegramConfigured=settings.is_telegram_notifications_configured(),
walletFeaturedButtonLabel=settings.lnbits_wallet_featured_button_label,
walletFeaturedButtonUrl=settings.lnbits_wallet_featured_button_url,
walletFeaturedButtonIcon=settings.lnbits_wallet_featured_button_icon,
userActivationByEmail=settings.lnbits_user_activation_by_email,
userActivationByPayment=settings.lnbits_user_activation_by_payment,
userActivationByInvitationCode=settings.lnbits_user_activation_by_invitation_code,
hasFirstInstallToken=settings.first_install_token is not None,
)
class SuperSettings(EditableSettings):
super_user: str
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+13 -11
View File
File diff suppressed because one or more lines are too long
+175
View File
@@ -220,6 +220,181 @@ body.body--dark .q-drawer {
backdrop-filter: blur(6px) brightness(0.8);
}
body.rounded-ui .q-card,
body.rounded-ui .q-btn {
border-radius: 10px;
}
body[data-theme=classic].card-gradient .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgba(103, 58, 183, 0.08), rgba(156, 39, 176, 0.06));
}
body[data-theme=classic].card-gradient .q-drawer {
background-image: linear-gradient(165deg, rgba(103, 58, 183, 0.06), rgba(156, 39, 176, 0.04));
}
body[data-theme=classic].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgb(38.6192771084, 42.356626506, 64.7807228916), rgba(103, 58, 183, 0.07));
}
body[data-theme=classic].card-gradient.body--dark .q-drawer {
background-image: linear-gradient(165deg, rgb(36.7144578313, 40.2674698795, 61.5855421687), rgba(103, 58, 183, 0.05));
}
body[data-theme=bitcoin].card-gradient .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgba(234, 97, 29, 0.08), rgba(229, 111, 53, 0.06));
}
body[data-theme=bitcoin].card-gradient .q-drawer {
background-image: linear-gradient(165deg, rgba(234, 97, 29, 0.06), rgba(229, 111, 53, 0.04));
}
body[data-theme=bitcoin].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgb(54.18, 49.364, 71.036), rgba(234, 97, 29, 0.07));
}
body[data-theme=bitcoin].card-gradient.body--dark .q-drawer {
background-image: linear-gradient(165deg, rgb(51.885, 47.273, 68.027), rgba(234, 97, 29, 0.05));
}
body[data-theme=freedom].card-gradient .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgba(226, 33, 86, 0.08), rgba(185, 26, 69, 0.06));
}
body[data-theme=freedom].card-gradient .q-drawer {
background-image: linear-gradient(165deg, rgba(226, 33, 86, 0.06), rgba(185, 26, 69, 0.04));
}
body[data-theme=freedom].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgb(82.2051282051, 55.1948717949, 63.4153846154), rgba(226, 33, 86, 0.07));
}
body[data-theme=freedom].card-gradient.body--dark .q-drawer {
background-image: linear-gradient(165deg, rgb(79.1538461538, 53.1461538462, 61.0615384615), rgba(226, 33, 86, 0.05));
}
body[data-theme=cyber].card-gradient .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgba(124, 179, 66, 0.08), rgba(85, 139, 47, 0.06));
}
body[data-theme=cyber].card-gradient .q-drawer {
background-image: linear-gradient(165deg, rgba(124, 179, 66, 0.06), rgba(85, 139, 47, 0.04));
}
body[data-theme=cyber].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgb(10.2, 10.2, 10.2), rgba(124, 179, 66, 0.07));
}
body[data-theme=cyber].card-gradient.body--dark .q-drawer {
background-image: linear-gradient(165deg, rgb(7.65, 7.65, 7.65), rgba(124, 179, 66, 0.05));
}
body[data-theme=mint].card-gradient .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgba(58, 183, 125, 0.08), rgba(39, 176, 101, 0.06));
}
body[data-theme=mint].card-gradient .q-drawer {
background-image: linear-gradient(165deg, rgba(58, 183, 125, 0.06), rgba(39, 176, 101, 0.04));
}
body[data-theme=mint].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgb(38.6192771084, 64.7807228916, 53.5686746988), rgba(58, 183, 125, 0.07));
}
body[data-theme=mint].card-gradient.body--dark .q-drawer {
background-image: linear-gradient(165deg, rgb(36.7144578313, 61.5855421687, 50.9265060241), rgba(58, 183, 125, 0.05));
}
body[data-theme=autumn].card-gradient .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgba(183, 118, 58, 0.08), rgba(176, 121, 39, 0.06));
}
body[data-theme=autumn].card-gradient .q-drawer {
background-image: linear-gradient(165deg, rgba(183, 118, 58, 0.06), rgba(176, 121, 39, 0.04));
}
body[data-theme=autumn].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgb(64.7807228916, 51.0771084337, 38.6192771084), rgba(183, 118, 58, 0.07));
}
body[data-theme=autumn].card-gradient.body--dark .q-drawer {
background-image: linear-gradient(165deg, rgb(61.5855421687, 48.5578313253, 36.7144578313), rgba(183, 118, 58, 0.05));
}
body[data-theme=flamingo].card-gradient .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgba(255, 0, 255, 0.08), rgba(253, 163, 253, 0.06));
}
body[data-theme=flamingo].card-gradient .q-drawer {
background-image: linear-gradient(165deg, rgba(255, 0, 255, 0.06), rgba(253, 163, 253, 0.04));
}
body[data-theme=flamingo].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgb(66.176, 4.224, 66.176), rgba(255, 0, 255, 0.07));
}
body[data-theme=flamingo].card-gradient.body--dark .q-drawer {
background-image: linear-gradient(165deg, rgb(61.382, 3.918, 61.382), rgba(255, 0, 255, 0.05));
}
body[data-theme=monochrome].card-gradient .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgba(73, 73, 73, 0.08), rgba(107, 107, 107, 0.06));
}
body[data-theme=monochrome].card-gradient .q-drawer {
background-image: linear-gradient(165deg, rgba(73, 73, 73, 0.06), rgba(107, 107, 107, 0.04));
}
body[data-theme=monochrome].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgb(10.2, 10.2, 10.2), rgba(73, 73, 73, 0.07));
}
body[data-theme=monochrome].card-gradient.body--dark .q-drawer {
background-image: linear-gradient(165deg, rgb(7.65, 7.65, 7.65), rgba(73, 73, 73, 0.05));
}
body[data-theme=salvador].card-gradient .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgba(25, 118, 210, 0.08), rgba(38, 166, 154, 0.06));
}
body[data-theme=salvador].card-gradient .q-drawer {
background-image: linear-gradient(165deg, rgba(25, 118, 210, 0.06), rgba(38, 166, 154, 0.04));
}
body[data-theme=salvador].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
.lnbits__dialog-card,
.q-dialog-plugin--dark) {
background-image: linear-gradient(135deg, rgb(43.9888888889, 64.2, 84.4111111111), rgba(25, 118, 210, 0.07));
}
body[data-theme=salvador].card-gradient.body--dark .q-drawer {
background-image: linear-gradient(165deg, rgb(42.2416666667, 61.65, 81.0583333333), rgba(25, 118, 210, 0.05));
}
body.card-shadow .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.18));
}
body.card-shadow.body--dark .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.45));
}
:root {
--size: 100px;
--gap: 25px;
+394 -15
View File
@@ -6,6 +6,7 @@ window.localisation.br = {
funding: 'Financiamento',
users: 'Usuários',
audit: 'Auditoria',
api_watch: 'Relógio da API',
apps: 'Aplicativos',
channels: 'Canais',
transactions: 'Transações',
@@ -21,13 +22,17 @@ window.localisation.br = {
active_channels: 'Canais Ativos',
connect_peer: 'Conectar Par',
connect: 'Conectar',
reconnect: 'Reconectar',
open_channel: 'Canal Aberto',
open: 'Abrir',
clear: 'Limpar',
close_channel: 'Fechar Canal',
close: 'Fechar',
restart: 'Reiniciar servidor',
image_library: 'Biblioteca de Imagens',
save: 'Salvar',
save_tooltip: 'Salvar suas alterações',
must_save: 'Você tem alterações não salvas',
credit_debit: 'Crédito / Débito',
credit_hint: 'Pressione Enter para creditar a conta',
credit_label: '{denomination} para creditar',
@@ -46,24 +51,47 @@ window.localisation.br = {
export_to_phone: 'Exportar para o telefone com código QR',
export_to_phone_desc:
'Este código QR contém a URL da sua carteira com acesso total. Você pode escaneá-lo do seu telefone para abrir sua carteira a partir dele.',
access_wallet_on_mobile: 'Acesso Móvel',
stored_paylinks: 'Links LNURL de pagamento armazenados',
wallet: 'Carteira:',
wallet_name: 'Nome da carteira',
wallet_type: 'Tipo de carteira',
shared_wallet: 'Carteira Compartilhada',
share_wallet: 'Compartilhar Carteira',
update_permissions: 'Atualizar Permissões',
shared_wallet_id: 'ID da Carteira Compartilhada',
shared_wallet_desc:
'Você foi convidado(a) para ter acesso à carteira de outra pessoa.',
wallets: 'Carteiras',
exclude_wallets: 'Excluir Carteiras',
add_wallet: 'Adicionar nova carteira',
reject_wallet: 'Rejeitar carteira',
add_new_wallet: 'Adicionar uma nova carteira',
pin_wallet: 'Fixar carteira',
delete_wallet: 'Excluir carteira',
delete_wallet_desc:
'Toda a carteira será excluída, os fundos serão IRRECUPERÁVEIS.',
rename_wallet: 'Renomear carteira',
update_name: 'Atualizar nome',
fiat_tracking: 'Rastreamento Fiat',
fiat_providers: 'Provedores fiat',
fiat_warning_bitcoin:
'Provedores Fiat podem ficar nervosos com qualquer coisa relacionada ao bitcoin, portanto, evite usar a palavra "bitcoin" nos seus memorandos!',
currency: 'Moeda',
update_currency: 'Atualizar moeda',
press_to_claim: 'Pressione para solicitar bitcoin',
claim_desc:
'Parece que você tem um valor resgatável de bitcoin, mas ainda não tem uma carteira. Pressione o botão abaixo para reivindicá-lo. Isso criará uma nova carteira para você.',
donate: 'Doar',
view_github: 'Ver no GitHub',
voidwallet_active: 'VoidWallet está ativo! Pagamentos desabilitados',
use_with_caution: 'USE COM CAUTELA - a carteira {name} ainda está em BETA',
service_fee: 'Taxa de serviço: {amount} % por transação',
service_fee_max: 'Taxa de serviço: {amount} % por transação (máx {max} sats)',
voidwallet_active_user:
'Fonte de financiamento indisponível. Por favor, entre em contato com seu administrador para configurar.',
voidwallet_active_admin:
'Fonte de financiamento indisponível. Clique aqui para configurar.',
service_fee_badge: 'Taxa de serviço: {amount} % por transação',
service_fee_max_badge:
'Taxa de serviço: {amount} % por transação (máximo {max} {denom})',
service_fee_tooltip:
'Taxa de serviço cobrada pelo administrador do servidor LNbits por transação de saída',
toggle_darkmode: 'Alternar modo escuro',
@@ -79,6 +107,7 @@ window.localisation.br = {
create_invoice: 'Criar Fatura',
camera_tooltip: 'Usar a câmara para escanear uma fatura / QR',
export_csv: 'Exportar para CSV',
export_csv_details: 'Exportar para CSV com detalhes',
chart_tooltip: 'Mostrar gráfico',
pending: 'Pendente',
copy_invoice: 'Copiar fatura',
@@ -86,15 +115,27 @@ window.localisation.br = {
cancel: 'Cancelar',
scan: 'Escanear',
read: 'Ler',
write: 'Escrever',
pay: 'Pagar',
memo: 'Memo',
date: 'Data',
path: 'Caminho',
internal_memo: 'Memorando interno (opcional)',
internal_memo_hint_receive:
'Este memorando não é mostrado ao pagador, mas é armazenado na fatura para sua referência.',
internal_memo_hint_pay:
'Este memorando não é exibido ao beneficiário, mas é armazenado no pagamento para sua referência.',
payment_processing: 'Processando pagamento...',
payment_successful: 'Pagamento bem-sucedido!',
payment_pending: 'Pagamento pendente...',
payment_check: 'Cheque pagamento',
not_enough_funds: 'Fundos insuficientes!',
search_by_tag_memo_amount: 'Pesquisar por tag, memo, quantidade',
search: 'Buscar',
invoice_waiting: 'Fatura aguardando pagamento',
payment_received: 'Pagamento Recebido',
payment_sent: 'Pagamento Enviado',
payment_failed: 'Pagamento Falhou',
receive: 'receber',
send: 'enviar',
outgoing_payment_pending: 'Pagamento pendente de saída',
@@ -112,7 +153,10 @@ window.localisation.br = {
extensions: 'Extensões',
no_extensions: 'Você não possui nenhuma extensão instalada :(',
created: 'Criado',
created_at: 'Criado em',
updated_at: 'Atualizado em',
search_extensions: 'Extensões de pesquisa',
search_wallets: 'Pesquisar carteiras',
extension_sources: 'Fontes de Extensão',
ext_sources_hint: 'Repositórios de onde as extensões podem ser baixadas',
ext_sources_label:
@@ -121,10 +165,13 @@ window.localisation.br = {
repository: 'Repositório',
confirm_continue: 'Você tem certeza de que deseja continuar?',
manage_extension_details: 'Instalar/desinstalar extensão',
upload: 'Enviar',
install: 'Instalar',
uninstall: 'Desinstalar',
drop_db: 'Remover Dados',
enable: 'Ativar',
enabled: 'Habilitado',
disabled: 'Desativado',
pay_to_enable: 'Pague para Habilitar',
enable_extension_details: 'Ativar extensão para o usuário atual',
disable: 'Desativar',
@@ -132,6 +179,8 @@ window.localisation.br = {
installed: 'Instalado',
activated: 'Ativado',
deactivated: 'Desativado',
activate: 'Ativar',
deactivate: 'Desativar',
release_notes: 'Notas de Lançamento',
activate_extension_details:
'Tornar a extensão disponível/indisponível para usuários',
@@ -139,8 +188,33 @@ window.localisation.br = {
all: 'Tudo',
only_admins_can_install:
'Apenas contas de administrador podem instalar extensões.',
only_admins_can_create_extensions:
'Apenas contas de administrador podem criar extensões',
admin_only: 'Apenas para Administração',
make_user_admin: 'Tornar usuário administrador',
revoke_admin: 'Revogar Admin',
new_version: 'Nova Versão',
reviews_url: 'URL de Avaliações',
reviews_url_label: 'URL do servidor de avaliações',
reviews_url_hint:
'URL completa do PaidReviews incluindo o id das configurações (por exemplo, https://example.com/paidreviews/SETTINGS_ID)',
reviews_open: 'Ver avaliações',
reviews_leave: 'Deixe uma avaliação',
reviews_name: 'Seu nome',
reviews_comment: 'Sua avaliação',
reviews_rating: 'Avaliação',
reviews_submit: 'Enviar avaliação',
reviews_loading: 'Carregando avaliações...',
reviews_refresh: 'Atualizar avaliações',
reviews_error_load: 'Não foi possível carregar as avaliações',
reviews_url_not_configured: 'URL de avaliações não configurada',
reviews_pay_invoice: 'Pagar fatura',
reviews_invoice_paid: 'Fatura paga',
reviews_invoice_title: 'Pague esta fatura para enviar sua avaliação',
reviews_count: 'Avaliações',
no_reviews: 'Ainda não há avaliações',
extension_has_free_release: 'Tem lançamentos gratuitos',
extension_has_paid_release: 'Tem lançamentos pagos',
extension_depends_on: 'Depende de:',
extension_rating_soon: 'Avaliações estarão disponíveis em breve',
extension_installed_version: 'Versão instalada',
@@ -155,26 +229,122 @@ window.localisation.br = {
'Esta versão requer no mínimo a versão do LNbits',
min_version: 'Mínimo (incluído)',
max_version: 'Máximo (excluído)',
preimage: 'Pré-imagem',
preimage_hint: 'Pré-imagem para liquidar a fatura de retenção',
hold_invoice: 'Reter Fatura',
hold_invoice_description:
'Esta fatura está em espera e requer uma pré-imagem para ser liquidada.',
payment_hash: 'Hash de pagamento',
invoice_cancelled: 'Fatura Cancelada',
invoice_settled: 'Fatura Liquidada',
hold_invoice_payment_hash:
'Hash de pagamento para fatura em espera (opcional)',
settle_invoice: 'Liquidar Fatura',
cancel_invoice: 'Cancelar Fatura',
fee: 'Taxa',
amount: 'Quantidade',
amount_limits: 'Limites de Quantia',
amount_sats: 'Quantidade (sats)',
faucest_wallet: 'Carteira de Torneira',
faucest_wallet_desc_1:
'Toda vez que um pagamento for confirmado pelo provedor {provider}, os fundos serão subtraídos desta carteira.',
faucest_wallet_desc_2:
'Isso ajuda a monitorar todos os pagamentos do {provider} e seu status.',
faucest_wallet_desc_3:
'Esta carteira deve ser recarregada com a quantia de sats que o administrador está disposto a oferecer em troca da moeda fiduciária.',
faucest_wallet_desc_4:
'Se esta carteira estiver configurada, mas estiver vazia, os pagamentos de {provider} não serão processados.',
faucest_wallet_desc_5:
'Esta carteira pode eventualmente ficar com saldo negativo se pagamentos fiduciários paralelos forem feitos.',
faucest_wallet_id: 'ID da Carteira de Torneira (opcional)',
faucest_wallet_id_hint:
'ID da carteira a ser usado para a torneira. Será usado para enviar os fundos ao usuário.',
tag: 'Etiqueta',
unit: 'Unidade',
description: 'Descrição',
expiry: 'Validade',
webhook: 'Webhook',
webhook_url: 'URL do Webhook',
webhook_url_hint:
'URL de Webhook para enviar os detalhes do pagamento. Será chamada quando o pagamento for concluído.',
copy_webhook_url: 'Copiar URL do webhook',
webhook_events_list:
'Os seguintes eventos devem ser suportados pelo webhook:',
webhook_stripe_description:
'No lado do Stripe, você deve configurar um webhook com um URL que aponta para o seu servidor LNbits.',
payment_proof: 'Comprovante de pagamento',
update: 'Atualizar',
update_available: 'Atualização {version} disponível!',
funding_sources: 'Fontes de Financiamento',
latest_update: 'Você está na versão mais recente {version}.',
notifications: 'Notificações',
no_notifications: 'Sem notificações',
notifications_disabled:
'As notificações de status do LNbits estão desativadas.',
enable_notifications: 'Ativar notificações',
enable_notifications_desc:
'Se ativado, ele buscará as últimas atualizações de status do LNbits, como incidentes de segurança e atualizações.',
notifications_configure: 'Configurar Notificações',
notifications_nostr_config: 'Configuração do Nostr',
notifications_enable_nostr: 'Ativar Nostr',
notifications_enable_nostr_desc: 'Enviar notificações pelo Nostr',
notifications_nostr_private_key: 'Chave Privada Nostr',
notifications_nostr_private_key_desc:
'Chave privada (hex ou nsec) para assinar as mensagens enviadas para Nostr',
notifications_nostr_identifier: 'Identificador Nostr',
notifications_nostr_identifier_desc:
'Identificador Nip5 para enviar notificações para',
notifications_nostr_identifiers: 'Identificadores Nostr',
notifications_nostr_identifiers_desc:
'Lista de identificadores para enviar notificações.',
notifications_telegram_config: 'Configuração do Telegram',
notifications_enable_telegram: 'Ativar Telegram',
notifications_enable_telegram_desc: 'Enviar notificações pelo Telegram',
notifications_telegram_access_token: 'Token de Acesso',
notifications_telegram_access_token_desc: 'Token de acesso para o bot',
notifications_chat_id: 'ID de bate-papo do Telegram',
notifications_chat_id_desc:
'ID do chat do Telegram para enviar as notificações para',
notifications_excluded_wallets_desc:
'Não envie notificações para essas carteiras',
notifications_email_config: 'Configuração de Email',
notifications_enable_email: 'Habilitar Email',
notifications_enable_email_desc: 'Enviar notificações por e-mail',
notifications_send_test_email: 'Enviar e-mail de teste',
notifications_send_email: 'Enviar e-mail',
notifications_send_email_desc: 'Email que você enviará de',
notifications_send_email_username: 'Nome de usuário',
notifications_send_email_username_desc:
'Nome de usuário, usará o e-mail se não estiver definido',
notifications_send_email_password: 'Enviar senha de e-mail',
notifications_send_email_password_desc:
'Senha para o e-mail que você enviará de',
notifications_send_email_server_port: 'Enviar e-mail porta SMTP',
notifications_send_email_server_port_desc: 'Porta para o servidor SMTP',
notifications_send_email_server: 'Enviar e-mail servidor SMTP',
notifications_send_email_server_desc:
'Servidor SMTP para o e-mail de que você enviará',
notifications_send_to_emails: 'Emails para enviar para',
notifications_send_to_emails_desc:
'Notificações de e-mails serão enviadas para',
notification_settings_update: 'Configurações atualizadas',
notification_settings_update_desc:
'Notificar quando as configurações do servidor forem atualizadas',
notification_server_start_stop: 'Iniciar/Parar Servidor',
notification_server_start_stop_desc:
'Notificar quando o servidor tiver sido iniciado/parado',
notification_watchdog_limit: 'Notificação de Limite do Watchdog',
notification_watchdog_limit_desc:
'Notifique quando o limite do watchdog for alcançado (não afeta a fonte de financiamento)',
notification_server_status: 'Status do Servidor',
notification_server_status_desc:
'Enviar notificações regulares sobre o status do servidor (valor do intervalo em horas)',
notification_incoming_payment: 'Pagamentos Recebidos',
notification_incoming_payment_desc:
'Notificar quando uma carteira tiver recebido um pagamento acima do valor especificado (sats)',
notification_outgoing_payment: 'Pagamentos Saída',
notification_outgoing_payment_desc:
'Notificar quando uma carteira tiver enviado um pagamento acima do valor especificado (sats)',
notification_credit_debit: 'Crédito / Débito',
notification_credit_debit_desc:
'Notificar quando uma carteira tiver sido creditada/debitada pelo superusuário',
notification_balance_delta_changed: 'Mudança no Delta de Saldo',
notification_balance_delta_changed_desc:
'Notifique quando a diferença entre o saldo do nó e o saldo do LNbits tiver mudado mais do que a quantidade especificada (em sats). Defina como 0 para desativar. Isso é executado a cada minuto.',
enable_watchdog: 'Ativar Watchdog',
enable_watchdog_desc:
'Se ativado, ele mudará automaticamente sua fonte de financiamento para VoidWallet se o seu saldo for inferior ao saldo do LNbits. Você precisará ativar manualmente após uma atualização.',
@@ -189,6 +359,7 @@ window.localisation.br = {
notification_source_label:
'URL de origem (use apenas a fonte de status oficial do LNbits e fontes de confiança)',
more: 'mais',
more_count: 'Mais {count}',
less: 'menos',
releases: 'Lançamentos',
watchdog: 'Cão de guarda',
@@ -200,15 +371,24 @@ window.localisation.br = {
allow_access_hint: 'Permitir acesso por IP (substituirá os IPs bloqueados)',
enter_ip: 'Digite o IP e pressione enter',
rate_limiter: 'Limitador de Taxa',
callback_url_rules: 'Regras de URL de Retorno',
enter_callback_url_rule: 'Digite a regra de URL como regex e pressione enter',
callback_url_rule_hint:
'URLs de retorno de chamada (como a de LNURL) serão validados conforme estas regras. Pelo menos uma regra deve corresponder. Nenhuma regra significa que todas as URLs são permitidas.',
wallet_limiter: 'Limitador de Carteira',
wallet_config: 'Configuração da Carteira',
wallet_charts: 'Gráficos da Carteira',
wallet_limit_max_withdraw_per_day:
'Retirada máxima diária da carteira em sats (0 para desativar)',
wallet_max_ballance: 'Saldo máximo da carteira em sats (0 para desativar)',
wallet_limit_secs_between_trans:
'Minutos e segundos entre transações por carteira (0 para desativar)',
only_incoming_payments_allowed: 'Apenas pagamentos recebidos são permitidos',
disable_outgoing_payments: 'Desativar pagamentos de saída',
number_of_requests: 'Número de solicitações',
time_unit: 'Unidade de tempo',
minute: 'minuto',
settings: 'Configurações',
second: 'segundo',
hour: 'hora',
disable_server_log: 'Desativar Log do Servidor',
@@ -219,49 +399,108 @@ window.localisation.br = {
login_with_user_id: 'Faça login com ID do usuário',
or: 'ou',
create_new_wallet: 'Criar Nova Carteira',
delete_all_wallets: 'Excluir Todas as Carteiras',
confirm_delete_all_wallets:
'Tem certeza de que deseja excluir TODAS as carteiras deste usuário?',
login_to_account: 'Faça login na sua conta',
create_account: 'Criar conta',
account_settings: 'Configurações da Conta',
signin_with_oauth: 'Entrar com',
signin_with_oauth_or: 'ou entre com',
signin_with_nostr: 'Continuar com Nostr',
signin_with_google: 'Entrar com o Google',
signin_with_github: 'Entrar com GitHub',
signin_with_keycloak: 'Entrar com Keycloak',
signin_with_custom_org: 'Entrar com {custom_org}',
username_or_email: 'Nome de usuário ou E-mail',
password: 'Senha',
password_config: 'Configuração de Senha',
password_repeat: 'Repetição de senha',
update_password: 'Atualizar Senha',
change_password: 'Alterar Senha',
update_credentials: 'Atualizar credenciais',
update_pubkey: 'Atualizar Chave Pública',
nostr_pubkey_tooltip:
'Insira a chave pública Nostr deste usuário (valor hexadecimal)',
set_password: 'Definir Senha',
set_password_tooltip: 'Defina uma senha para este usuário',
invalid_password: 'A senha deve ter pelo menos 8 caracteres',
invalid_password_repeat: 'As senhas não coincidem',
reset_key_generated: 'Uma chave de reinicialização foi gerada.',
reset_key_copy:
'Clique em OK para copiar o URL de redefinição para sua área de transferência.',
login: 'Entrar',
register: 'Registrar',
username: 'Nome de usuário',
pubkey: 'Chave Pública',
user_id: 'ID do Usuário',
id: 'ID',
email: 'E-mail',
first_name: 'Primeiro Nome',
last_name: 'Sobrenome',
picture: 'Foto',
user_picture_desc:
'URL para uma imagem a ser usada como foto de perfil. Você pode carregá-la como um ativo.',
verify_email: 'Verifique o e-mail com',
account: 'Conta',
update_account: 'Atualizar Conta',
invalid_username: 'Nome de usuário inválido',
auth_provider: 'Provedor de Autenticação',
external_id: 'ID Externo',
my_account: 'Minha Conta',
existing_account_question: 'Já tem uma conta?',
background_image: 'Imagem de Fundo',
back: 'Voltar',
logout: 'Sair',
look_and_feel: 'Aparência',
endpoint: 'Ponto de extremidade',
api: 'API',
api_stripe: 'API',
api_token: 'Token de API',
api_tokens: 'Tokens da API',
access_control_list: 'Lista de Controle de Acesso',
access_control_list_admin_warning:
'Esta é uma conta de administrador. Os tokens gerados terão privilégios de administrador.',
new_api_acl: 'Nova Lista de Controle de Acesso',
api_token_id: 'Id do Token',
toggle_gradient: 'Alternar Gradiente',
gradient_background: 'Fundo em Degradê',
rounded_ui: 'Cartões e Botões Arredondados',
toggle_rounded_ui: 'Alternar cantos arredondados para cartões e botões',
card_gradient: 'Gradiente do Cartão',
toggle_card_gradient: 'Alternar gradiente nos cartões',
card_shadow: 'Sombra do Cartão',
toggle_card_shadow: 'Alternar sombra nas cartas',
language: 'Idioma',
assets: 'Ativos',
max_asset_size_mb: 'Tamanho Máximo do Ativo (MB)',
max_asset_size_mb_desc:
'O tamanho máximo permitido para uploads de ativos em megabytes (pode usar valores decimais).',
assets_allowed_mime_types: 'Tipos MIME permitidos',
assets_allowed_mime_types_desc:
'Os tipos MIME permitidos para uploads de ativos. Nenhum valor significa que todos os uploads são permitidos.',
thumbnail_width: 'Largura da Miniatura',
thumbnail_width_desc: 'Largura da miniatura gerada em pixels.',
thumbnail_height: 'Altura da miniatura',
thumbnail_height_desc: 'Altura da miniatura gerada em pixels.',
thumbnail_format: 'Formato da Miniatura',
thumbnail_format_desc:
'Formato de imagem da miniatura gerada (PNG, JPEG, etc.).',
max_assets_per_user: 'Máximo de ativos por usuário',
max_assets_per_user_desc:
'O número máximo de ativos que um usuário pode fazer upload. Zero significa que o upload está proibido.',
assets_no_limit_users: 'Usuários sem Limites de Ativos',
assets_no_limit_users_desc:
'Esses usuários podem enviar um número ilimitado de ativos (com base no ID do usuário).',
color_scheme: 'Esquema de Cores',
visible_wallet_count: 'Contagem de Carteiras Visíveis',
admin_settings: 'Configurações do Administrador',
extension_cost: 'Este lançamento requer um pagamento mínimo de {cost} sats.',
extension_paid_sats: 'Você já pagou {paid_sats} sats.',
create_extension: 'Criar Extensão',
release_details_error: 'Não é possível obter os detalhes da versão.',
pay_from_wallet: 'Pagar com a Carteira',
pay_with: 'Pague com {provider}',
select_payment_provider: 'Selecione o provedor de pagamento',
wallet_required: 'Carteira *',
show_qr: 'Exibir QR',
retry_install: 'Repetir Instalação',
@@ -274,6 +513,9 @@ window.localisation.br = {
'A extensão {name} requer um pagamento mínimo de {amount} sats para habilitar.',
hide_empty_wallets: 'Ocultar carteiras vazias',
recheck: 'Verificar novamente',
check: 'Verificar',
check_connection: 'Verificar Conexão',
check_webhook: 'Verificar Webhook',
contributors: 'Contribuidores',
license: 'Licença',
reset_key: 'Redefinir Chave',
@@ -286,6 +528,7 @@ window.localisation.br = {
hash: 'Hash:',
welcome_lnbits: 'Bem-vindo ao LNbits',
setup_su_account: 'Configure a conta Superuser abaixo.',
first_install_token: 'Primeiro Token de Instalação',
create_ticker_converter: 'Criar Conversor de Ticker de Moeda',
enable_audit: 'Habilitar Auditoria',
recommended: 'Recomendado',
@@ -326,9 +569,16 @@ window.localisation.br = {
user_default_extensions_label: 'Extensões do usuário',
user_default_extensions_hint:
'Extensões que serão ativadas por padrão para os usuários.',
extension_builder: 'Construtor de Extensão',
extension_builder_manifest_url: 'URL do Manifesto do Criador de Extensões',
extension_builder_manifest_url_hint:
'URL para um arquivo JSON manifest com detalhes do extension builder',
miscellanous: 'Diversos',
misc_disable_extensions: 'Desativar extensões',
misc_disable_extensions_label: 'Desativar todas as extensões',
misc_disable_extensions_builder: 'Habilitar Extensions Builder',
misc_disable_extensions_builder_label:
'Habilitar Extensions Builder para usuários não administradores.',
misc_hide_api: 'Ocultar API',
misc_hide_api_label:
'Oculta a API de carteira, extensões podem optar por honrar',
@@ -346,16 +596,33 @@ window.localisation.br = {
toggle_transactions_node_ui:
'Guia de Transações (Desativar em nós grandes CLN)',
invoice_expiry: 'Expiração da Fatura',
invoice_expiry_label: 'Validade da fatura (segundos)',
fee_reserve: 'Reserva de Taxa',
fee_reserve_msats: 'Taxa de reserva em msats',
fee_reserve_percent: 'Taxa de reserva em porcentagem',
invoice_expiry_label: 'Validade da fatura',
routing_fee_reserve_calculations: 'Cálculos de Reserva de Taxa de Roteamento',
routing_fee_reserve_calculations_desc:
'LNbits reserva um "valor de reserva" para cada pagamento para cobrir as taxas de roteamento. A taxa de roteamento máxima passada para a fonte de financiamento é a que for maior: a <strong>reserva mínima de taxa de roteamento</strong> ou a <strong>percentagem de reserva de taxa de roteamento</strong>.',
millisats: 'milissats',
fee_reserve: 'Reserva Mínima de Taxa de Encaminhamento',
fee_reserve_percent: 'Porcentagem da Reserva de Taxa de Roteamento',
fee_reserve_min_hint:
'A taxa mínima reservada por pagamento.<br>Isso atua como um piso - a taxa de roteamento máxima nunca será inferior a este valor, independentemente do tamanho do pagamento.',
fee_reserve_percent_hint:
'A porcentagem do valor do pagamento a reservar para taxas de roteamento.',
payment_timeouts: 'Tempos de Espera de Pagamento',
payment_wait_time: 'Tempo de Espera do Pagamento',
seconds: 'segundos',
payment_wait_time_desc:
'Tempo de espera antes de marcar um pagamento de saída como pendente. Padrão: 5s; aumentar para faturas de liquidação lenta.',
payment_wait_time_tooltip:
'Controla quanto tempo o LNbits espera para uma tentativa de pagamento de saída ser confirmada antes de marcá-la como pendente. Valores mais altos ajudam ao pagar faturas de liquidação lenta (por exemplo, faturas HODL, Boltz). O pagamento será verificado novamente mais tarde e atualizado automaticamente ou manualmente.',
server_management: 'Gerenciamento de Servidor',
base_url: 'URL base',
base_url_label: 'URL estática/base para o servidor',
authentication: 'Autenticação',
auth_token_expiry_label: 'Minutos para expiração do token',
auth_token_expiry_hint: 'Tempo em minutos até o token expirar',
auth_authentication_cache_label: 'Tempo de cache (minutos)',
auth_authentication_cache_hint:
'Tempo em minutos para armazenar em cache a autenticação bem-sucedida (0 para desativar)',
auth_allowed_methods_label: 'Métodos de autorização permitidos',
auth_allowed_methods_hint: 'Selecione métodos de autorização',
auth_nostr_label: 'URL de Solicitação Nostr',
@@ -373,13 +640,26 @@ window.localisation.br = {
auth_keycloak_ci_hint:
'Certifique-se de que a URL de retorno de chamada de autorização esteja definida para https://{domain}/api/v1/auth/keycloak/token',
auth_keycloak_cs_label: 'Segredo do Cliente Keycloak',
auth_keycloak_custom_org_label: 'Keycloak Custom Organization',
auth_keycloak_custom_icon_label: 'Ícone Personalizado do Keycloak (URL)',
currency_settings: 'Configurações de Moeda',
allowed_currencies: 'Moedas Permitidas',
allowed_currencies_hint: 'Limite o número de moedas fiduciárias disponíveis',
default_account_currency: 'Moeda Padrão da Conta',
default_account_currency_hint: 'Moeda padrão para contabilidade',
min_incoming_payment_amount: 'Quantia Mínima de Pagamento de Entrada',
min_incoming_payment_amount_desc:
'Quantidade mínima permitida para gerar uma fatura',
max_incoming_payment_amount: 'Valor Máximo do Pagamento Recebido',
max_incoming_payment_amount_desc:
'Quantidade máxima permitida para gerar uma fatura',
max_outgoing_payment_amount: 'Valor Máximo de Pagamento de Saída',
max_outgoing_payment_amount_desc:
'Valor máximo permitido para efetuar um pagamento',
service_fee: 'Taxa de serviço: {amount} % por transação',
service_fee_label: 'Taxa de serviço (%)',
service_fee_hint: 'Taxa cobrada por tx (%)',
service_fee_max: 'Taxa de serviço: {amount} % por transação (máx {max} sats)',
service_fee_max_label: 'Taxa de serviço máx (sats)',
service_fee_max_hint: 'Taxa máxima de serviço a cobrar em (sats)',
fee_wallet: 'Carteira de Taxas',
@@ -391,6 +671,8 @@ window.localisation.br = {
'Desativar Taxa de Serviço para Pagamentos Internos Lightning',
ui_management: 'Gerenciamento de UI',
ui_site_title: 'Título do Site',
ui_changing_remove_lnbits_elements:
'(alterar removerá os elementos LNbits na página inicial e rodapé)',
ui_site_tagline: 'Tagline do site',
ui_elements_enable: 'Habilitar elementos na página inicial',
ui_elements_disable: 'Desativar elementos na página inicial',
@@ -399,11 +681,27 @@ window.localisation.br = {
ui_site_description: 'Descrição do Site',
ui_site_description_hint: 'Use texto simples, Markdown ou HTML bruto',
ui_default_wallet_name: 'Nome Padrão da Carteira',
ui_default_theme: 'Tema Padrão',
wallet_featured_button_label: 'Carteira Rótulo do Botão em Destaque',
wallet_featured_button_label_hint:
'Mostrar botão em destaque na página inicial da carteira',
wallet_featured_button_url: 'URL do Botão em Destaque',
wallet_featured_button_url_hint:
'Ao clicar, o botão abrirá este URL. Deixe em branco para ocultar o botão.',
wallet_featured_button_icon: 'Ícone do Botão em Destaque',
wallet_featured_button_icon_hint:
'Ícone mostrado no botão de destaque (verifique os ícones quasar)',
lnbits_wallet: 'Carteira LNbits',
denomination: 'Denominação',
denomination_hint: 'O nome para o token FakeWallet',
denomination_error: 'A denominação deve ter 3 caracteres ou `sats`.',
ui_qr_code_logo: 'Logo do QR Code',
ui_qr_code_logo_hint: 'URL para imagem de logo no código QR',
ui_apple_touch_icon: 'Ícone de Toque da Apple',
ui_apple_touch_icon_hint: 'URL do ícone de toque da Apple',
ui_custom_image: 'Imagem Personalizada',
ui_custom_image_label: 'URL para imagem personalizada',
ui_custom_image_hint: 'Imagem exibida na página inicial/login',
ui_custom_badge: 'Distintivo Personalizado',
ui_custom_badge_label:
"Distintivo Personalizado 'USE COM CUIDADO - a carteira LNbits ainda está em BETA'",
@@ -426,14 +724,95 @@ window.localisation.br = {
admin_users_label: 'ID do Usuário',
allowed_users: 'Usuários Permitidos',
allowed_users_hint: 'Somente esses usuários podem usar o LNbits',
allowed_users_hint_feature: 'Somente estes usuários podem usar {feature}',
allowed_users_label: 'ID do Usuário',
allow_creation_user: 'Permitir a criação de novos usuários',
allow_creation_user_desc:
'Permitir a criação de novos usuários na página de índice',
require_user_activation: 'Requer ativação do usuário',
require_user_activation_desc:
'Novos usuários serão ativados somente após passarem por um dos métodos de confirmação. Administradores podem ativar usuários manualmente a partir do painel de administração.',
reusable_activation_code: 'Código de ativação reutilizável',
reusable_activation_code_label: 'Código de ativação reutilizável',
reusable_activation_code_hint:
'Este código de ativação pode ser usado várias vezes por diferentes usuários.',
one_time_activation_code: 'Códigos de ativação única',
one_time_activation_code_label: 'Adicionar código de ativação',
one_time_activation_code_hint:
'Lista de códigos de ativação únicos. Cada código pode ser usado apenas uma vez, depois será removido da lista.',
invitation_code: 'Código de Convite',
invitation_code_hint: 'O código de convite que você recebeu.',
email_confirmation_hint:
'Endereço de e-mail para enviar o código de confirmação.',
nostr_identifier: 'Identificador Nostr',
nostr_identifier_hint:
'Identificador nostr nip5 ou <npub> para enviar o código de confirmação.',
new_user_not_allowed: 'O registro está desativado.',
start_user_impersonation: 'Personificar este usuário',
stop_user_impersonation: 'Parar a Personificação do Usuário',
components: 'Componentes',
long_running_endpoints: 'Top 5 Endpoints de Longa Execução',
http_request_methods: 'Métodos de Requisição HTTP',
http_response_codes: 'Códigos de Resposta HTTP',
request_details: 'Detalhes do Pedido',
http_request_details: 'Detalhes da Requisição HTTP'
http_request_details: 'Detalhes da Requisição HTTP',
payment_details: 'Detalhes do Pagamento',
payment_details_desc: 'Informações detalhadas sobre o pagamento',
payments: 'Pagamentos',
payment_show_internal: 'Mostrar Pagamentos Internos',
payment_chart_flow: 'Fluxo de Pagamento Mensal',
payment_chart_status: 'Status do Pagamento',
payment_chart_tx_per_wallet: 'Transações por Carteira (saldo/contagem)',
payment_details_back: 'Voltar para Pagamentos',
payment_chart_tags: 'Pagamentos por Tags',
payments_balance_in_out: 'Entradas/Saídas de Saldo',
payments_count_in_out: 'Contar Entrada/Saída',
payments_status_chart: 'Gráfico de Status',
payments_tag_chart: 'Gráfico de Marcadores',
payments_balance_chart: 'Gráfico de Saldo',
payments_wallets_chart: 'Gráfico de Carteiras',
payments_balance_in_out_chart: 'Gráfico de Entrada/Saída de Saldo',
payments_count_in_out_chart: 'Gráfico de Entrada/Saída',
reset_wallet_keys: 'Redefinir Chaves',
reset_wallet_keys_desc:
'Redefina as chaves de API para esta carteira. Isso invalidará as chaves atuais e gerará novas.',
view_list: 'Visualizar carteiras como lista',
view_column: 'Visualizar carteiras como linhas',
filter_payments: 'Filtrar pagamentos',
filter_labels: 'Rótulos de filtro',
filter_date: 'Filtrar por data',
websocket_example: 'Exemplo de Websocket',
client_id: 'ID do Cliente',
secret_key: 'Chave Secreta',
signing_secret: 'Segredo de Assinatura',
signing_secret_hint:
'Segredo de assinatura para o webhook. As mensagens serão assinadas com este segredo.',
webhook_id: 'ID do Webhook',
webhook_id_hint:
'ID do webhook do PayPal usado para verificar eventos recebidos.',
webhook_paypal_description:
'No lado do PayPal, configure um webhook apontando para o seu servidor LNbits.',
callback_success_url: 'URL de Sucesso de Callback',
callback_success_url_hint:
'O usuário será redirecionado para este URL após o pagamento ser bem-sucedido.',
connected: 'Conectado',
not_connected: 'Não Conectado',
free: 'Grátis',
paid: 'Pago',
funding_source_retries: 'Máximo de tentativas',
funding_source_retries_desc:
'Número máximo de tentativas para fontes de financiamento, antes de voltar para VoidWallet.',
add_label: 'Adicionar Rótulo',
label: 'Etiqueta',
labels: 'Rótulos',
label_filter: 'Filtro de Rótulo',
no_labels_defined: 'Ainda não há rótulos definidos',
manage_labels: 'Gerenciar Etiquetas',
update_label: 'Atualizar Rótulo',
delete_label: 'Excluir Rótulo',
add_remove_labels: 'Adicionar ou Remover Rótulos',
payment_labels_updated: 'Rótulos de pagamento atualizados',
color: 'Cor',
sort: 'Ordenar',
sort_by: 'Ordenar por'
}
+135 -103
View File
@@ -6,7 +6,7 @@ window.localisation.en = {
funding: 'Funding',
users: 'Users',
audit: 'Audit',
api_watch: 'Api Watch',
api_watch: 'API Watch',
apps: 'Apps',
channels: 'Channels',
transactions: 'Transactions',
@@ -171,6 +171,7 @@ window.localisation.en = {
drop_db: 'Remove Data',
enable: 'Enable',
enabled: 'Enabled',
disabled: 'Disabled',
pay_to_enable: 'Pay To Enable',
enable_extension_details: 'Enable extension for current user',
disable: 'Disable',
@@ -178,6 +179,8 @@ window.localisation.en = {
installed: 'Installed',
activated: 'Activated',
deactivated: 'Deactivated',
activate: 'Activate',
deactivate: 'Deactivate',
release_notes: 'Release Notes',
activate_extension_details: 'Make extension available/unavailable for users',
featured: 'Featured',
@@ -186,6 +189,8 @@ window.localisation.en = {
only_admins_can_create_extensions:
'Only admin accounts can create extensions',
admin_only: 'Admin Only',
make_user_admin: 'Make User Admin',
revoke_admin: 'Revoke Admin',
new_version: 'New Version',
reviews_url: 'Reviews URL',
reviews_url_label: 'Reviews server URL',
@@ -266,12 +271,16 @@ window.localisation.en = {
update: 'Update',
update_available: 'Update {version} available!',
funding_sources: 'Funding Sources',
funding_source: 'Funding Source',
requires_server_restart:
'Changing these settings requires a server restart to take effect.',
funding_source_info: 'Select the active funding wallet',
latest_update: 'You are on the latest version {version}.',
notifications: 'Notifications',
notifications_configure: 'Configure Notifications',
notifications_nostr_config: 'Nostr Configuration',
notifications_enable_nostr: 'Enable Nostr',
notifications_enable_nostr_desc: 'Send notfications over Nostr',
notifications_enable_nostr_desc: 'Send notifications over Nostr',
notifications_nostr_private_key: 'Nostr Private Key',
notifications_nostr_private_key_desc:
'Private key (hex or nsec) to sign the messages sent to Nostr',
@@ -280,11 +289,11 @@ window.localisation.en = {
'Nip5 identifier to send notifications to',
notifications_nostr_identifiers: 'Nostr Identifiers',
notifications_nostr_identifiers_desc:
'List of identifiers to send notifications to',
'List of identifiers to send notifications to.',
notifications_telegram_config: 'Telegram Configuration',
notifications_enable_telegram: 'Enable Telegram',
notifications_enable_telegram_desc: 'Send notfications over Telegram',
notifications_enable_telegram_desc: 'Send notifications over Telegram',
notifications_telegram_access_token: 'Access Token',
notifications_telegram_access_token_desc: 'Access token for the bot',
notifications_chat_id: 'Telegram Chat ID',
@@ -313,15 +322,15 @@ window.localisation.en = {
notification_settings_update: 'Settings updated',
notification_settings_update_desc:
'Notify when server settings have been updated',
'Send a notification when server settings have been updated',
notification_server_start_stop: 'Server Start/Stop',
notification_server_start_stop_desc:
'Notify when the server has been started/stopped',
'Send a notification when the server has been started/stopped',
notification_watchdog_limit: 'Watchdog Limit Notification',
notification_watchdog_limit_desc:
'Notify when the watchdog limit has been reached (does not affect the funding source)',
'Send a notification when the watchdog limit has been reached (does not affect the funding source)',
notification_server_status: 'Server Status',
notification_server_status_desc:
@@ -329,29 +338,31 @@ window.localisation.en = {
notification_incoming_payment: 'Incoming Payments',
notification_incoming_payment_desc:
'Notify when a wallet has received a payment above the specified amount (sats)',
'Send a notification when a wallet has received a payment above the specified amount (sats)',
notification_outgoing_payment: 'Outgoing Payments',
notification_outgoing_payment_desc:
'Notify when a wallet has sent a payment above the specified amount (sats)',
'Send a notification when a wallet has sent a payment above the specified amount (sats)',
notification_credit_debit: 'Credit / Debit',
notification_credit_debit_desc:
'Notify when a wallet has been credited/debited by the superuser',
'Send a notification when a wallet has been credited/debited by the superuser',
notification_balance_delta_changed: 'Balance Delta Changed',
notification_balance_delta_changed_desc:
'Notify when the difference between the node balance and the LNbits balance has changed by more than the specified amount (in sats). Set to 0 to disable. This runs every minute.',
'Send a notification when the difference between the node balance and the LNbits balance has changed by more than the specified amount (in sats). Set to 0 to disable. This runs every minute.',
enable_watchdog: 'Enable Watchdog Switch',
watchdog_introduction:
"Watchdog is a feature that allows you to automatically switch the LNbits funding source to VoidWallet if your node balance is lower than the LNbits balance by a certain threshold. This can help prevent overspending and keep your node's funds safe.",
enable_watchdog: 'Enable Watchdog',
enable_watchdog_desc:
'If enabled it will change your funding source to VoidWallet automatically if your balance is lower than the LNbits balance. You will need to enable manually after an update.',
watchdog_interval: 'Watchdog Interval',
'You will need to re-enable this manually after an update.',
watchdog_interval: 'Watchdog Check Interval',
watchdog_interval_desc:
'How often the background task should check for a killswitch signal in the watchdog delta [node_balance - lnbits_balance] (in minutes).',
watchdog_delta: 'Watchdog Delta',
'How often LNbits should check for a killswitch signal in the watchdog threshold delta value [node_balance - lnbits_balance] (in minutes).',
watchdog_delta: 'Watchdog Threshold Delta',
watchdog_delta_desc:
'Limit before killswitch changes funding source to VoidWallet [lnbits_balance - node_balance > delta]',
"The LNbit's > Node balance delta threshold. If this threshold is exceeded, the funding source is changed to VoidWallet.",
status: 'Status',
notification_source: 'Notification Source',
notification_source_label:
@@ -362,24 +373,12 @@ window.localisation.en = {
releases: 'Releases',
watchdog: 'Watchdog',
server_logs: 'Server Logs',
ip_blocker: 'IP Blocker',
route_access_control: 'Route Access Control',
route_access_control_enable: 'Enable route access control',
route_access_control_hint:
'When enabled, non-local requests are restricted. Requests from 127.0.0.1 (and ::1) are always allowed.',
route_access_whitelist_label: 'Route Whitelist',
route_access_whitelist_hint:
'Only allow non-local access to these routes (leave empty to disable whitelist mode).',
route_access_blacklist_label: 'Route Blacklist',
route_access_blacklist_hint:
'Block non-local access to these routes (ignored when a whitelist is set).',
route_access_save_confirm:
'Are you sure you want to save? This can impact access to LNbits if you are not accessing locally.',
ip_blocker: 'IP Blacklist/Whitelist',
security: 'Security',
security_tools: 'Security tools',
security_tools: 'Security Tools',
block_access_hint: 'Block access by IP',
allow_access_hint: 'Allow access by IP (will override blocked IPs)',
enter_ip: 'Enter IP and hit enter',
enter_ip: 'Enter an IP address and press enter',
rate_limiter: 'Rate Limiter',
callback_url_rules: 'Callback URL Rules',
enter_callback_url_rule: 'Enter URL rule as regex and hit enter',
@@ -393,14 +392,16 @@ window.localisation.en = {
wallet_max_ballance: 'Wallet max balance in sats (0 to disable)',
wallet_limit_secs_between_trans:
'Min secs between transactions per wallet (0 to disable)',
only_incoming_payments_allowed: 'Only incoming payments allowed',
only_incoming_payments_allowed: 'Allow incoming payments only',
disable_outgoing_payments: 'Disable outgoing payments',
number_of_requests: 'Number of requests',
number_of_requests: 'Number of requests to allow',
number_of_requests_hint:
'Number of requests to allow per "time unit" for the rate limiter. Set to 0 to disable.',
time_unit: 'Time unit',
minute: 'minute',
minute: 'Minute',
settings: 'Settings',
second: 'second',
hour: 'hour',
second: 'Second',
hour: 'Hour',
disable_server_log: 'Disable Server Log',
enable_server_log: 'Enable Server Log',
coming_soon: 'Feature coming soon',
@@ -472,6 +473,12 @@ window.localisation.en = {
api_token_id: 'Token Id',
toggle_gradient: 'Toggle Gradient',
gradient_background: 'Gradient Background',
rounded_ui: 'Rounded Cards & Buttons',
toggle_rounded_ui: 'Toggle rounded corners for cards and buttons',
card_gradient: 'Card Gradient',
toggle_card_gradient: 'Toggle gradient on cards',
card_shadow: 'Card Shadow',
toggle_card_shadow: 'Toggle shadow on cards',
language: 'Language',
assets: 'Assets',
max_asset_size_mb: 'Max Asset Size (MB)',
@@ -530,36 +537,34 @@ window.localisation.en = {
hash: 'Hash: ',
welcome_lnbits: 'Welcome to LNbits',
setup_su_account: 'Set up the Superuser account below.',
first_install_token: 'First Install Token',
create_ticker_converter: 'Create Currency Ticker Converter',
enable_audit: 'Enable Audit',
recommended: 'Recommended',
audit_desc: 'Record HTTP requests according with the specified filters',
audit_record_req: 'Record Request Body',
audit_record_warning: 'Warning: ',
audit_record_req_warning_1:
'confidential data (like passwords) will be logged.',
audit_record_req_warning_2: 'the request body can have large size.',
audit_record_use: 'Use it with caution.',
audit_ip: 'Record IP Address',
audit_ip_desc: 'Record the IP address of the client',
audit_path_params: 'Record Path Parameters',
audit_query_params: 'Record Query Parameters',
audit_desc: 'Log HTTP requests according to the filters specified below.',
audit_record_req: 'Log Request Body',
audit_record_warning: 'Warning!',
audit_record_req_warning_1: 'Sensitive data (like passwords) will be logged.',
audit_record_req_warning_2:
'The request body can be large. This can fill up your logs quickly.',
audit_record_use: 'Use this with caution!',
audit_ip: 'Log IP Address',
audit_ip_desc: 'Log the IP address of users making requests to LNbits.',
audit_path_params: 'Log Path Parameters',
audit_query_params: 'Log Query Parameters',
audit_http_methods: 'Include HTTP Methods',
audit_http_methods_hint:
'List of HTTP methods to be included. Empty lists means all.',
audit_http_methods_label: 'HTTP Methods',
audit_resp_codes: 'Include HTTP Response Codes',
'List of HTTP methods to be logged. No value means all methods will be logged.',
audit_http_methods_label: 'HTTP Methods to Log',
audit_resp_codes_hint:
'List of HTTP codes to be included (regex match). Empty lists means all. Eg: 4.*, 5.*',
audit_resp_codes_label: 'HTTP Response code (regex)',
audit_paths: 'Include Paths',
audit_resp_codes_label: 'HTTP Response Codes to Log (regex)',
audit_paths_hint:
'List of paths to be included (regex match). Empty list means all.',
audit_paths_label: 'HTTP Path (regex)',
audit_paths_exclude: 'Exclude Paths',
audit_paths_label: 'HTTP Paths to Log (regex)',
audit_paths_exclude_hint:
'List of paths to be excluded (regex match). Empty list means none.',
audit_paths_exclude_label: 'HTTP Path (regex)',
audit_paths_exclude_label: 'HTTP Paths to Exclude from Logging (regex)',
exchange_providers: 'Exchange Providers',
admin_extensions: 'Admin Extensions',
admin_extensions_label: 'Admin extensions',
@@ -579,41 +584,47 @@ window.localisation.en = {
misc_disable_extensions_builder_label:
'Enable Extensions Builder for non admin users.',
misc_hide_api: 'Hide API',
misc_hide_api_label: 'Hides wallet api, extensions can choose to honor',
misc_hide_api_label: 'Hides wallet API, extensions can choose to honor',
wallets_management: 'Wallets Management',
funding_source_info: 'Funding Source Info',
funding_source: 'Funding Source: {wallet_class}',
node_balance: 'Node Balance: {balance} sats',
lnbits_balance: 'LNbits Balance: {balance} sats',
funding_reserve_percent: 'Reserve Percent: {percent} %',
funding_source_info: 'Funding Source Information',
funding_source: 'Funding source: {wallet_class}',
node_balance: 'Node balance: {balance} sats',
lnbits_balance: 'LNbits balance: {balance} sats',
funding_reserve_percent: 'Funding reserve percentage: {percent} %',
node_management: 'Node Management',
node_management_not_supported:
'Node Management not supported by active funding source',
'Node management is not supported by the active funding source',
toggle_node_ui: 'Node UI',
toggle_public_node_ui: 'Public Node UI',
toggle_transactions_node_ui: 'Transactions Tab (Disable on large CLN nodes)',
invoice_expiry: 'Invoice Expiry',
invoice_expiry_label: 'Invoice expiry (seconds)',
fee_reserve: 'Fee Reserve',
fee_reserve_percent: 'Fee Reserve Percent',
fee_reserve_msats: 'Reserve fee in msats',
reserve_fee_in_percent: 'Reserve fee in percent',
payment_wait_time: 'Payment Wait Time (sec)',
routing_fee_reserve_calculations: 'Routing Fee Reserve Calculations',
routing_fee_reserve_calculations_desc:
'LNbits sets aside a “reserve amount” for each payment to cover routing fees. The maximum routing fee passed to the funding source is whichever is higher: the <strong>minimum routing fee reserve</strong> or the <strong>routing fee reserve percentage</strong>.',
millisats: 'millisats',
fee_reserve: 'Minimum Routing Fee Reserve',
fee_reserve_percent: 'Routing Fee Reserve Percentage',
fee_reserve_min_hint:
'The minimum fee reserved per payment.<br>This acts as a floor - the maximum allowed routing fee will never be lower than this value regardless of payment size.',
fee_reserve_percent_hint:
'The percentage of the payment amount to reserve for routing fees.',
payment_timeouts: 'Payment Timeouts',
payment_wait_time: 'Payment Wait Time',
seconds: 'seconds',
payment_wait_time_desc:
'Wait time before marking an outgoing payment as pending. Default: 5s; raise for slow-settling invoices.',
payment_wait_time_tooltip:
'Controls how long LNbits waits for an outgoing payment attempt to confirm before marking it as pending. Higher values help when paying slow-settling invoices (e.g., HODL invoices, Boltz). The payment will be rechecked later and updated automatically or manually.',
server_management: 'Server Management',
base_url: 'Base URL',
base_url_label: 'Static/Base url for the server',
base_url_label: 'Base URL of the server',
authentication: 'Authentication',
auth_token_expiry_label: 'Token expire minutes',
auth_token_expiry_label: 'Token expiry (minutes)',
auth_token_expiry_hint: 'Time in minutes until the token expires',
auth_authentication_cache_label: 'Cache time (minutes)',
auth_authentication_cache_hint:
'Time in minutes to cache successful authentication (0 to disable)',
auth_allowed_methods_label: 'Allowed authorization methods',
auth_allowed_methods_hint: 'Select authorization methods',
auth_allowed_methods_hint: 'Select allowed authorization methods',
auth_nostr_label: 'Nostr Request URL',
auth_nostr_hint: 'Absolute URL that the clients will use to login.',
auth_google_ci_label: 'Google Client ID',
@@ -633,31 +644,27 @@ window.localisation.en = {
auth_keycloak_custom_icon_label: 'Keycloak Custom Icon (URL)',
currency_settings: 'Currency Settings',
allowed_currencies: 'Allowed Currencies',
allowed_currencies_hint: 'Limit the number of available fiat currencies',
default_account_currency: 'Default Account Currency',
default_account_currency_hint: 'Default currency for accounting',
allowed_currencies_hint:
'Set the allowed fiat currencies for the exchange features',
default_account_currency: 'Default Accounting Currency',
default_account_currency_hint:
'The default currency to use for accounting features.',
min_incoming_payment_amount: 'Min Incoming Payment Amount',
min_incoming_payment_amount_desc:
'Minimum amount allowed for generating an invoice',
max_incoming_payment_amount: 'Max Incoming Payment Amount',
max_incoming_payment_amount: 'Maximum Incoming Payment Amount',
max_incoming_payment_amount_desc:
'Maximum amount allowed for generating an invoice',
max_outgoing_payment_amount: 'Max Outgoing Payment Amount',
'Maximum amount allowed when generating an invoice',
max_outgoing_payment_amount: 'Maximum Outgoing Payment Amount',
max_outgoing_payment_amount_desc:
'Maximum amount allowed for making a payment',
'Maximum amount allowed when making a payment',
service_fees: 'Service Fees',
service_fee: 'Service Fee',
service_fee_label: 'Service fee (%)',
service_fee_hint: 'Fee charged per tx (%)',
service_fee_max: 'Service Fee Max',
service_fee_max_label: 'Service fee max (sats)',
service_fee_max_hint: 'Max service fee to charge in (sats)',
fee_wallet: 'Fee Wallet',
fee_wallet_label: 'Fee wallet (wallet ID)',
fee_wallet_hint: 'Wallet ID to send funds to',
disable_fee: 'Disable Fee',
disable_fee_internal: 'Disable Service Fee for Internal Payments',
disable_fee_internal_desc:
'Disable Service Fee for Internal Lightning Payments',
service_fee_label: 'Service Fee Charged Per Transaction',
service_fee_max_label: 'Maximum Service Fee Limit',
fee_wallet_label: 'Service Fee Wallet ID',
fee_wallet_hint: 'The ID of the wallet to which to send service funds',
disable_fee: 'Disable Service Fees for Internal Payments',
ui_management: 'UI Management',
ui_site_title: 'Site Title',
ui_changing_remove_lnbits_elements:
@@ -670,6 +677,7 @@ window.localisation.en = {
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
ui_default_wallet_name: 'Default Wallet Name',
ui_default_theme: 'Default Theme',
wallet_featured_button_title: 'Wallet - Featured Button',
wallet_featured_button_label: 'Wallet Featured Button Label',
wallet_featured_button_label_hint:
'Show featured button on the wallet homepage',
@@ -678,33 +686,38 @@ window.localisation.en = {
'On click the button will open this URL. Leave empty to hide the button.',
wallet_featured_button_icon: 'Featured Button Icon',
wallet_featured_button_icon_hint:
'Icon shown on the featured button (check quasar icons)',
'Icon shown on the featured button (Quasar icon name e.g. "bolt")',
lnbits_wallet: 'LNbits wallet',
denomination: 'Denomination',
denomination_hint: 'The name for the FakeWallet token',
denomination_error: 'Denomination must be 3 characters, or `sats`',
ui_qr_code_logo: 'QR Code/Favicon Logo',
ui_qr_code_logo_hint: 'QR code and favicon logo url',
ui_qr_code_logo_hint: 'QR code icon and favicon logo URL',
ui_apple_touch_icon: 'Apple Touch Icon',
ui_apple_touch_icon_hint: 'Apple touch icon url',
ui_apple_touch_icon_hint: 'Apple touch icon URL',
ui_custom_image: 'Custom Image',
ui_custom_image_label: 'URL to custom image',
ui_custom_image_hint: 'Image showed at homepage/login',
ui_custom_badge: 'Custom Badge',
ui_custom_image_hint:
'This image is shown on the LNbits homepage and login screen.',
ui_custom_badge_title: 'Custom Badge Settings',
ui_custom_badge_desc: 'Show a custom badge in the header of LNbits',
ui_custom_badge: 'Custom Badge Text',
ui_custom_badge_label: "Custom Badge 'USE WITH CAUTION'",
ui_custom_badge_color_label: 'Custom Badge Color',
themes: 'Themes',
themes_hint: 'Choose themes available for users',
custom_logo: 'Custom Logo',
custom_logo_hint: 'URL to logo image',
ad_space_title: 'Ad Space Title',
ad_space_title_label: 'Supported by',
ad_space_section_title: 'Advertisement Space',
ad_space_section_desc:
'Configure the advertisement space on the wallet sidebar.',
ad_space_title: 'Advertisement Space Title',
ad_space_title_hint: 'Title shown above the advertisement space',
ad_slots: 'Advertisement Slots',
ad_slots_hint:
'Ad url and image filepaths in CSV format, extensions can choose to honor',
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
ads_enabled: 'Ads Enabled',
ads_disabled: 'Ads Disabled',
'Advertisement image filepaths in CSV format, extensions can choose to honor. Format: url;img_light_url;img_dark_url, url..',
ads_enabled: 'Enable Advertisement',
ads_disabled: 'Disabled Advertisement',
user_management: 'User Management',
admin_users: 'Admin Users',
admin_users_hint: 'Users with admin privileges',
@@ -715,6 +728,25 @@ window.localisation.en = {
allowed_users_label: 'User ID',
allow_creation_user: 'Allow creation of new users',
allow_creation_user_desc: 'Allow creation of new users on the index page',
require_user_activation: 'Require user activation',
require_user_activation_desc:
'New users will be activated only after they pass one of the confirmation methods. Admins can activate users manually from the admin panel.',
reusable_activation_code: 'Reusable activation code',
reusable_activation_code_label: 'Reusable activation code',
reusable_activation_code_hint:
'This activation code can be used multiple times by different users.',
one_time_activation_code: 'One-time activation codes',
one_time_activation_code_label: 'Add activation code',
one_time_activation_code_hint:
'List of one-time activation codes. Each code can be used only once, then will be reomved from the list.',
invitation_code: 'Invitation Code',
invitation_code_hint: 'The invitation code that you have received.',
email: 'Email',
email_confirmation_hint: 'Email address to send the confirmation code to.',
nostr_identifier: 'Nostr Identifier',
nostr_identifier_hint:
'Nostr nip5 identifier or <npub> to send the confirmation code to.',
new_user_not_allowed: 'Registration is disabled.',
start_user_impersonation: 'Impersonate this user',
stop_user_impersonation: 'Stop User Impersonation',
+4 -3
View File
@@ -66,7 +66,7 @@ window._lnbitsApi = {
name: name
})
},
register(username, email, password, password_repeat) {
register(username, email, password, password_repeat, invitation_code) {
return axios({
method: 'POST',
url: '/api/v1/auth/register',
@@ -74,7 +74,8 @@ window._lnbitsApi = {
username,
email,
password,
password_repeat
password_repeat,
invitation_code
}
})
},
@@ -147,7 +148,7 @@ window._lnbitsApi = {
name: name,
wallet_type: walletType,
...opts
}).catch(LNbits.utils.notifyApiError)
})
},
updateWallet(name, wallet) {
return this.request('patch', '/api/v1/wallet', wallet.adminkey, {
+26 -19
View File
@@ -35,23 +35,6 @@ window.app.component('lnbits-extension-rating', {
window.app.component('lnbits-manage', {
template: '#lnbits-manage',
computed: {
showAdmin() {
return this.LNBITS_ADMIN_UI
},
showUsers() {
return this.LNBITS_ADMIN_UI
},
showNode() {
return this.LNBITS_NODE_UI
},
showAudit() {
return this.LNBITS_AUDIT_ENABLED
},
showExtensions() {
return this.LNBITS_EXTENSIONS_DEACTIVATE_ALL === false
}
},
methods: {
isActive(path) {
return window.location.pathname === path
@@ -449,8 +432,10 @@ window.app.component('username-password', {
username: String,
password_1: String,
password_2: String,
invitationCode: String,
resetKey: String
},
data() {
return {
oauth: [
@@ -463,8 +448,10 @@ window.app.component('username-password', {
password: this.password_1,
passwordRepeat: this.password_2,
reset_key: this.resetKey,
keycloakOrg: LNBITS_AUTH_KEYCLOAK_ORG || 'Keycloak',
keycloakIcon: LNBITS_AUTH_KEYCLOAK_ICON
confirmationMethod: 'code',
confirmationEmail: '',
confirmationCode: this.invitationCode || '',
showConfirmationCode: false
}
},
methods: {
@@ -477,6 +464,7 @@ window.app.component('username-password', {
this.$emit('update:userName', this.username)
this.$emit('update:password_1', this.password)
this.$emit('update:password_2', this.passwordRepeat)
this.$emit('update:invitationCode', this.confirmationCode)
this.$emit('register')
},
reset() {
@@ -568,6 +556,25 @@ window.app.component('username-password', {
computed: {
showOauth() {
return this.oauth.some(m => this.authMethods.includes(m))
},
disableRegister() {
const usernameOK = !!this.username
const passwordOK = !!this.password && this.password.length >= 8
const passwordsMatch = this.password === this.passwordRepeat
const codeOk =
this.confirmationMethodsCount === 0 ||
this.confirmationMethod !== 'code' ||
this.confirmationCode.length > 0
return !usernameOK || !passwordOK || !passwordsMatch || !codeOk
},
confirmationMethodsCount() {
const methods = [
this.g.settings.userActivationByEmail,
this.g.settings.userActivationByPayment,
this.g.settings.userActivationByInvitationCode
]
return methods.filter(Boolean).length
}
},
created() {}
@@ -228,6 +228,48 @@ window.app.component('lnbits-admin-funding-sources', {
spark_token: 'Token'
}
],
[
'SparkL2Wallet',
'Spark (L2)',
{
spark_l2_external_endpoint: {
label: 'External Sidecar Endpoint',
hint: 'Make sure to also specify the API key if your sidecar requires authentication.',
value: ''
},
spark_l2_mnemonic: {
label: 'External Sidecar Mnemonic',
hint: 'Mnemonic for the Spark wallet on the external sidecar. Required if the side car does not have its own mnemonic.',
value: ''
},
spark_l2_external_api_key: {
label: 'External Sidecar API Key',
hint: 'API key for authenticating with the external sidecar if it requires authentication.',
value: ''
},
spark_l2_network: {
label: 'Network',
value: 'MAINNET',
hint: 'The network to use for the Spark wallet.',
advanced: true
},
spark_l2_pay_wait_ms: {
label: 'Payment Wait Time (ms)',
hint: 'The time to wait for a payment to be processed before considering it failed.',
advanced: true
},
spark_l2_pay_poll_ms: {
label: 'Payment Poll Time (ms)',
hint: 'The time to wait between polling for payment status updates.',
advanced: true
},
spark_l2_stream_keepalive_ms: {
label: 'Stream Keepalive Time (ms)',
hint: 'The time to wait between sending keepalive messages to the Spark sidecar to keep the connection open.',
advanced: true
}
}
],
[
'NWCWallet',
'Nostr Wallet Connect',
@@ -8,58 +8,11 @@ window.app.component('lnbits-admin-security', {
serverlogEnabled: false,
nostrAcceptedUrl: '',
formAllowedIPs: '',
formCallbackUrlRule: '',
routeOptions: [],
routeOptionsFiltered: [],
routeOptionsLoading: false
formCallbackUrlRule: ''
}
},
created() {
this.loadOpenApiRoutes()
},
created() {},
methods: {
async loadOpenApiRoutes() {
this.routeOptionsLoading = true
try {
const response = await fetch('/openapi.json')
if (!response.ok) {
throw new Error('Failed to load OpenAPI spec')
}
const data = await response.json()
const paths = Object.keys(data.paths || {}).sort()
this.routeOptions = paths
this.routeOptionsFiltered = paths
} catch (error) {
console.warn(error)
} finally {
this.routeOptionsLoading = false
}
},
addRouteOption(value, done) {
const route = value.trim()
if (!route) {
done()
return
}
if (!this.routeOptions.includes(route)) {
this.routeOptions.push(route)
this.routeOptions.sort()
}
this.routeOptionsFiltered = this.routeOptions
done(route)
},
filterRouteOptions(val, update) {
update(() => {
if (!val) {
this.routeOptionsFiltered = this.routeOptions
return
}
const needle = val.toLowerCase()
this.routeOptionsFiltered = this.routeOptions.filter(route =>
route.toLowerCase().includes(needle)
)
})
},
addAllowedIPs() {
const allowedIPs = this.formAllowedIPs.trim()
const allowed_ips = this.formData.lnbits_allowed_ips
@@ -4,7 +4,9 @@ window.app.component('lnbits-admin-users', {
data() {
return {
formAddUser: '',
formAddAdmin: ''
formAddAdmin: '',
formAddActivationCode: '',
showReusableActivationCode: false
}
},
methods: {
@@ -31,6 +33,24 @@ window.app.component('lnbits-admin-users', {
removeAdminUser(user) {
let admin_users = this.formData.lnbits_admin_users
this.formData.lnbits_admin_users = admin_users.filter(u => u !== user)
},
addOneTimeActivationCode() {
const code = this.formAddActivationCode
const activationCodes =
this.formData.lnbits_register_one_time_activation_codes
if (code?.length && !activationCodes.includes(code)) {
this.formData.lnbits_register_one_time_activation_codes = [
...activationCodes,
code
]
this.formAddActivationCode = ''
}
},
removeOneTimeActivationCode(code) {
const codes = this.formData.lnbits_register_one_time_activation_codes
this.formData.lnbits_register_one_time_activation_codes = codes.filter(
u => u !== code
)
}
}
})
+3 -6
View File
@@ -1,14 +1,11 @@
window.app.component('lnbits-footer', {
template: '#lnbits-footer',
computed: {
version() {
return this.LNBITS_VERSION || 'unknown version'
},
title() {
return `${this.SITE_TITLE}, ${this.SITE_TAGLINE}`
return `${this.g.settings.siteTitle}, ${this.g.settings.siteTagline}`
},
showFooter() {
return this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
version() {
return this.$t('lnbits_version') + ': ' + this.g.settings.version
}
}
})
@@ -1,48 +1,9 @@
window.app.component('lnbits-header', {
template: '#lnbits-header',
computed: {
hasServiceFeeMax() {
return (
this.g.user &&
this.LNBITS_SERVICE_FEE_MAX &&
this.LNBITS_SERVICE_FEE_MAX > 0
)
},
serviceFeeMax() {
return this.LNBITS_SERVICE_FEE_MAX || 0
},
hasServiceFee() {
return (
this.g.user && this.LNBITS_SERVICE_FEE && this.LNBITS_SERVICE_FEE > 0
)
},
serviceFee() {
return this.LNBITS_SERVICE_FEE || 0
},
hasCustomBadge() {
return this.LNBITS_CUSTOM_BADGE && this.LNBITS_CUSTOM_BADGE != ''
},
customBadge() {
return this.LNBITS_CUSTOM_BADGE || ''
},
customBadgeColor() {
return this.LNBITS_CUSTOM_BADGE_COLOR || ''
},
title() {
return this.SITE_TITLE
},
titleIsLnbits() {
return this.SITE_TITLE == 'LNbits'
},
customLogoUrl() {
return this.USE_CUSTOM_LOGO || null
},
showAdmin() {
return this.g.user && (this.g.user.super_user || this.g.user.admin)
},
showVoidwallet() {
return this.g.user && this.VOIDWALLET == true
},
displayName() {
return (
this.g.user?.extra?.display_name ||
@@ -95,8 +95,8 @@ window.app.component('lnbits-home-logos', {
showLogos() {
return (
this.g.isSatsDenomination &&
this.SITE_TITLE == 'LNbits' &&
this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
this.g.settings.siteTitle == 'LNbits' &&
this.g.settings.showHomePageElements == true
)
}
}
@@ -17,6 +17,13 @@ window.app.component('lnbits-manage-wallet-list', {
} else {
this.activeWalletId = null
}
},
'g.user.wallets': {
handler() {
this.paymentEvents()
},
deep: true,
immediate: true
}
},
created() {
+1 -1
View File
@@ -34,7 +34,7 @@ window.app.component('lnbits-qrcode', {
},
logo: {
type: String,
default: LNBITS_QR_LOGO
default: window.g.settings.qrLogo || null
}
},
data() {
+34 -1
View File
@@ -45,6 +45,30 @@ window.app.component('lnbits-theme', {
document.body.classList.remove('gradient-bg')
}
},
'g.cardRoundedChoice'(val) {
this.$q.localStorage.set('lnbits.cardRounded', val)
if (val === true) {
document.body.classList.add('rounded-ui')
} else {
document.body.classList.remove('rounded-ui')
}
},
'g.cardGradientChoice'(val) {
this.$q.localStorage.set('lnbits.cardGradient', val)
if (val === true) {
document.body.classList.add('card-gradient')
} else {
document.body.classList.remove('card-gradient')
}
},
'g.cardShadowChoice'(val) {
this.$q.localStorage.set('lnbits.cardShadow', val)
if (val === true) {
document.body.classList.add('card-shadow')
} else {
document.body.classList.remove('card-shadow')
}
},
'g.mobileSimple'(val) {
this.$q.localStorage.set('lnbits.mobileSimple', val)
if (val === true) {
@@ -117,6 +141,15 @@ window.app.component('lnbits-theme', {
if (this.g.gradientChoice === true) {
document.body.classList.add('gradient-bg')
}
if (this.g.cardRoundedChoice === true) {
document.body.classList.add('rounded-ui')
}
if (this.g.cardGradientChoice === true) {
document.body.classList.add('card-gradient')
}
if (this.g.cardShadowChoice === true) {
document.body.classList.add('card-shadow')
}
if (this.g.bgimageChoice !== '') {
document.body.classList.add('bg-image')
document.body.style.setProperty(
@@ -127,7 +160,7 @@ window.app.component('lnbits-theme', {
if (this.g.mobileSimple === true) {
document.body.classList.add('mobile-simple')
}
Object.entries(this.g.user.uiCustomization || {}).forEach(
Object.entries(this.g.user?.uiCustomization || {}).forEach(
([key, value]) => {
if (key in this.g) {
this.g[key] = value
@@ -71,6 +71,10 @@ window.app.component('lnbits-wallet-extra', {
'lnbits.exchangeRate.' + this.g.wallet.currency,
this.g.exchangeRate
)
if (this.g.exchangeRate <= 0) {
this.g.fiatTracking = false
this.g.isFiatPriority = false
}
})
.catch(e => console.error(e))
}
@@ -97,6 +97,7 @@ window.app.component('lnbits-wallet-new', {
this.g.lastWalletId = res.data.id
this.$router.push(`/wallet/${res.data.id}`)
})
.catch(LNbits.utils.notifyApiError)
}
},
created() {
+36 -32
View File
@@ -9,17 +9,50 @@ const localStore = (key, defaultValue) => {
}
window.g = Vue.reactive({
// vars from server
settings: SETTINGS,
currencies: CURRENCIES,
extensions: SETTINGS.extensions,
allowedCurrencies: SETTINGS.allowedCurrencies,
denomination: SETTINGS.denomination,
isSatsDenomination: SETTINGS.denomination == 'sats',
// local storage vars
themeChoice: localStore('lnbits.theme', SETTINGS.defaultTheme),
borderChoice: localStore('lnbits.border', SETTINGS.defaultBorder),
gradientChoice: localStore('lnbits.gradientBg', SETTINGS.defaultGradient),
cardRoundedChoice: localStore(
'lnbits.cardRounded',
SETTINGS.defaultCardRounded
),
cardGradientChoice: localStore(
'lnbits.cardGradient',
SETTINGS.defaultCardGradient
),
cardShadowChoice: localStore('lnbits.cardShadow', SETTINGS.defaultCardShadow),
reactionChoice: localStore('lnbits.reactions', SETTINGS.defaultReaction),
bgimageChoice: localStore(
'lnbits.backgroundImage',
SETTINGS.defaultBgimage || ''
),
locale: localStore('lnbits.lang', navigator.languages[1] ?? 'en'),
disclaimerShown: localStore('lnbits.disclaimerShown', false),
isFiatPriority: localStore('lnbits.isFiatPriority', false),
mobileSimple: localStore('lnbits.mobileSimple', true),
walletFlip: localStore('lnbits.walletFlip', false),
lastActiveWallet: localStore('lnbits.lastActiveWallet', null),
darkChoice: localStore('lnbits.darkMode', SETTINGS.defaultDark),
// cookie vars
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
isUserImpersonated: !!Quasar.Cookies.get('is_lnbits_user_impersonated'),
// frontend vars
errorCode: null,
errorMessage: null,
user: null,
wallet: null,
isPublicPage: true,
isUserAuthorized: !!Quasar.Cookies.get('is_lnbits_user_authorized'),
isUserImpersonated: !!Quasar.Cookies.get('is_lnbits_user_impersonated'),
offline: !navigator.onLine,
hasCamera: false,
visibleDrawer: false,
extensions: WINDOW_SETTINGS.EXTENSIONS,
fiatBalance: 0,
exchangeRate: 0,
fiatTracking: false,
@@ -27,35 +60,6 @@ window.g = Vue.reactive({
walletEventListeners: [],
updatePayments: false, // used for updating the lnbits-payment-list
updatePaymentsHash: false, // used for closing the receive dialog
currencies: WINDOW_SETTINGS.LNBITS_CURRENCIES ?? [],
allowedCurrencies: WINDOW_SETTINGS.LNBITS_ALLOWED_CURRENCIES ?? [],
locale: localStore('lnbits.lang', navigator.languages[1] ?? 'en'),
disclaimerShown: localStore('lnbits.disclaimerShown', false),
isFiatPriority: localStore('lnbits.isFiatPriority', false),
mobileSimple: localStore('lnbits.mobileSimple', true),
walletFlip: localStore('lnbits.walletFlip', false),
lastActiveWallet: localStore('lnbits.lastActiveWallet', null),
darkChoice: localStore('lnbits.darkMode', true),
themeChoice: localStore('lnbits.theme', WINDOW_SETTINGS.LNBITS_DEFAULT_THEME),
borderChoice: localStore(
'lnbits.border',
WINDOW_SETTINGS.LNBITS_DEFAULT_BORDER || 'hard-border'
),
gradientChoice: localStore(
'lnbits.gradientBg',
WINDOW_SETTINGS.LNBITS_DEFAULT_GRADIENT || false
),
reactionChoice: localStore(
'lnbits.reactions',
WINDOW_SETTINGS.LNBITS_DEFAULT_REACTION || 'confettiBothSides'
),
bgimageChoice: localStore(
'lnbits.backgroundImage',
WINDOW_SETTINGS.LNBITS_DEFAULT_BGIMAGE || ''
),
ads: WINDOW_SETTINGS.AD_SPACE.split(',').map(ad => ad.split(';')),
denomination: WINDOW_SETTINGS.LNBITS_DENOMINATION,
isSatsDenomination: WINDOW_SETTINGS.LNBITS_DENOMINATION == 'sats',
scanner: null,
newWalletType: null
})
+1 -2
View File
@@ -157,8 +157,7 @@ window.app.mixin({
return {
api: window._lnbitsApi,
utils: window._lnbitsUtils,
g: window.g,
...WINDOW_SETTINGS
g: window.g
}
},
// backwards compatibility for extensions, should not be used in the future
+16 -8
View File
@@ -40,13 +40,7 @@ window.PageAccount = {
}
],
defaultSiteCustomisation: {
locale: 'en',
themeChoice: 'salvador',
bgimageChoice: '',
gradientChoice: true,
darkChoice: true,
borderChoice: 'hard-border',
reactionChoice: 'confettiBothSides'
locale: 'en'
},
reactionOptions: [
'None',
@@ -698,6 +692,20 @@ window.PageAccount = {
} catch (e) {
LNbits.utils.notifyApiError(e)
}
},
resetThemeDefaults() {
const defaults = {
themeChoice: this.g.settings.defaultTheme,
borderChoice: this.g.settings.defaultBorder,
gradientChoice: this.g.settings.defaultGradient,
bgimageChoice: this.g.settings.defaultBgimage || '',
reactionChoice: this.g.settings.defaultReaction,
darkChoice: this.g.settings.defaultDark,
cardRoundedChoice: this.g.settings.defaultCardRounded,
cardGradientChoice: this.g.settings.defaultCardGradient,
cardShadowChoice: this.g.settings.defaultCardShadow
}
this.siteCustomisationChanged(defaults)
}
},
@@ -711,7 +719,7 @@ window.PageAccount = {
await this.getUserAssets()
// filter out themes that are not allowed
this.themeOptions = this.themeOptions.filter(theme =>
this.LNBITS_THEME_OPTIONS.includes(theme.name)
this.g.settings.themeOptions.includes(theme.name)
)
}
}
+1 -22
View File
@@ -8,9 +8,7 @@ window.PageAdmin = {
lnbits_exchange_rate_providers: [],
lnbits_audit_exclude_paths: [],
lnbits_audit_include_paths: [],
lnbits_audit_http_response_codes: [],
lnbits_route_access_whitelist: [],
lnbits_route_access_blacklist: []
lnbits_audit_http_response_codes: []
},
isSuperUser: false,
needsRestart: false
@@ -71,25 +69,6 @@ window.PageAdmin = {
.catch(LNbits.utils.notifyApiError)
},
updateSettings() {
if (this.shouldConfirmRouteAccess()) {
LNbits.utils
.confirmDialog(this.$t('route_access_save_confirm'))
.onOk(() => this.persistSettings())
return
}
this.persistSettings()
},
shouldConfirmRouteAccess() {
const fields = [
'lnbits_route_access_control_enabled',
'lnbits_route_access_whitelist',
'lnbits_route_access_blacklist'
]
return fields.some(
field => !_.isEqual(this.settings[field], this.formData[field])
)
},
persistSettings() {
const data = _.omit(this.formData, [
'is_super_user',
'lnbits_allowed_funding_sources',
+2 -2
View File
@@ -842,8 +842,8 @@ window.PageExtensions = {
},
async created() {
this.extensions = await this.fetchAllExtensions()
this.extbuilderEnabled = this.g.user.admin || this.LNBITS_EXT_BUILDER
this.reviewsUrl = this.LNBITS_EXTENSIONS_REVIEWS_URL
this.extbuilderEnabled = this.g.user.admin || this.g.settings.extBuilder
this.reviewsUrl = this.g.settings.extensionsReviewsUrl
if (this.g.user.extensions.length === 0) {
this.tab = 'all'
+16 -10
View File
@@ -7,7 +7,8 @@ window.PageFirstInstall = {
isPwdRepeat: true,
username: '',
password: '',
passwordRepeat: ''
passwordRepeat: '',
firstInstallToken: ''
}
}
},
@@ -17,20 +18,25 @@ window.PageFirstInstall = {
}
},
methods: {
async setPassword() {
try {
await LNbits.api.request('PUT', '/api/v1/auth/first_install', null, {
setPassword() {
LNbits.api
.request('PUT', `/api/v1/auth/first_install`, null, {
username: this.loginData.username,
password: this.loginData.password,
password_repeat: this.loginData.passwordRepeat
password_repeat: this.loginData.passwordRepeat,
first_install_token: this.loginData.firstInstallToken
})
window.location.href = '/admin'
} catch (e) {
LNbits.utils.notifyApiError(e)
}
.then(async () => {
const res = await LNbits.api.getAuthUser()
this.g.user = LNbits.map.user(res.data)
this.g.isPublicPage = false
this.$router.push('/admin')
})
.catch(this.utils.notifyApiError)
}
},
created() {
document.title = 'First Install - LNbits'
const params = new URLSearchParams(window.location.search)
this.loginData.firstInstallToken = params.get('token') || ''
}
}
+7 -29
View File
@@ -11,6 +11,7 @@ window.PageHome = {
email: '',
password: '',
passwordRepeat: '',
invitationCode: '',
walletName: '',
signup: false
}
@@ -19,40 +20,15 @@ window.PageHome = {
showClaimLnurl() {
return (
this.lnurl !== '' &&
this.allowRegister &&
'user-id-only' in this.LNBITS_AUTH_METHODS
this.g.settings.allowRegister &&
'user-id-only' in this.g.settings.authMethods
)
},
formatDescription() {
return LNbits.utils.convertMarkdown(this.SITE_DESCRIPTION)
return LNbits.utils.convertMarkdown(this.g.settings.siteDescription)
},
isAccessTokenExpired() {
return this.$q.cookies.get('is_access_token_expired')
},
allowRegister() {
return this.LNBITS_NEW_ACCOUNTS_ALLOWED
},
// TODO: this makes no sense
hasCustomImage() {
return this.LNBITS_CUSTOM_IMAGE
},
showHomepageElements() {
return this.LNBITS_SHOW_HOME_PAGE_ELEMENTS === true
},
siteTitle() {
return this.SITE_TITLE || ''
},
siteTagline() {
return this.SITE_TAGLINE || ''
},
adsEnabled() {
return this.AD_SPACE_ENABLED && this.AD_SPACE && this.AD_SPACE.length > 0
},
adsTitle() {
return this.AD_SPACE_TITLE || ''
},
ads() {
return this.AD_SPACE.map(ad => ad.split(';'))
}
},
methods: {
@@ -65,6 +41,7 @@ window.PageHome = {
this.username = null
this.password = null
this.passwordRepeat = null
this.invitationCode = null
this.authAction = 'register'
this.authMethod = authMethod
@@ -76,7 +53,8 @@ window.PageHome = {
this.username,
this.email,
this.password,
this.passwordRepeat
this.passwordRepeat,
this.invitationCode
)
this.refreshAuthUser()
} catch (e) {
+17 -4
View File
@@ -70,10 +70,10 @@ window.PageUsers = {
usersTable: {
columns: [
{
name: 'admin',
name: 'activated',
align: 'left',
label: 'Admin',
field: 'admin',
label: this.$t('activated'),
field: 'activated',
sortable: false
},
{
@@ -401,7 +401,7 @@ window.PageUsers = {
toggleAdmin(userId) {
LNbits.api
.request('GET', `/users/api/v1/user/${userId}/admin`)
.request('PUT', `/users/api/v1/user/${userId}/admin`)
.then(() => {
this.fetchUsers()
Quasar.Notify.create({
@@ -412,6 +412,19 @@ window.PageUsers = {
})
.catch(LNbits.utils.notifyApiError)
},
toggleUserActivated(userId) {
LNbits.api
.request('PUT', `/users/api/v1/user/${userId}/activate`)
.then(res => {
this.fetchUsers()
Quasar.Notify.create({
type: 'positive',
message: res.data.message,
icon: null
})
})
.catch(LNbits.utils.notifyApiError)
},
async showAccountPage(user_id) {
this.activeUser.showPassword = false
this.activeUser.showUserId = false
+22 -2
View File
@@ -1,7 +1,7 @@
window._lnbitsUtils = {
url_for(url) {
const _url = new URL(url, window.location.origin)
_url.searchParams.set('v', WINDOW_SETTINGS.CACHE_KEY)
_url.searchParams.set('v', window.g.settings.cacheKey)
return _url.toString()
},
loadScript(src) {
@@ -131,6 +131,22 @@ window._lnbitsUtils = {
currency: currency || 'sat'
}).format(value)
},
getCurrencySymbol(currency) {
const code = (currency || '').toUpperCase()
if (code === 'BTC' || code === 'XBT' || code === 'SAT' || code === 'SATS') {
return '₿'
}
try {
const parts = new Intl.NumberFormat(window.i18n.global.locale, {
style: 'currency',
currency: code
}).formatToParts(0)
const symbolPart = parts.find(part => part.type === 'currency')
return symbolPart?.value || code || '¤'
} catch (e) {
return code || '¤'
}
},
formatSat(value) {
return new Intl.NumberFormat(window.i18n.global.locale).format(value)
},
@@ -144,7 +160,7 @@ window._lnbitsUtils = {
return null
}
},
notifyApiError(error) {
async notifyApiError(error) {
if (!error.response) {
return console.error(error)
}
@@ -154,6 +170,10 @@ window._lnbitsUtils = {
500: 'negative'
}
let messages = error.response.data.detail
if (!messages) {
const text = await error.response.data?.text()
messages = this.parseJSONSafe(text)?.detail
}
if (messages) {
messages = Array.isArray(messages)
? messages.map(e => e.msg + ` (${e.loc?.join('/')})`)
+1
View File
@@ -1,6 +1,7 @@
@use 'themes';
@use 'borders';
@use 'background';
@use 'cards';
@use 'home';
[v-cloak] {
+72
View File
@@ -0,0 +1,72 @@
@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));
}
}
+782 -900
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -178,7 +178,11 @@ async def invoice_callback_dispatcher(checking_id: str, is_internal: bool = Fals
logger.warning(f"Payment '{checking_id}' is not incoming, skipping.")
return
status = await payment.check_status(skip_internal_payment_notifications=True)
from lnbits.core.services.payments import check_payment_status
status = await check_payment_status(
payment, skip_internal_payment_notifications=True
)
payment.fee = status.fee_msat or payment.fee
# only overwrite preimage if status.preimage provides it
payment.preimage = status.preimage or payment.preimage
+4 -4
View File
@@ -70,10 +70,10 @@
</div>
<script type="text/javascript">
const RENDERED_ROUTE = '{{ normalize_path(request.path) }}'
const WINDOW_SETTINGS = {{ WINDOW_SETTINGS | tojson }}
Object.keys(WINDOW_SETTINGS).forEach(key => {
window[key] = WINDOW_SETTINGS[key]
})
const SETTINGS = {{ SETTINGS | tojson | safe }}
const CURRENCIES = {{ CURRENCIES | tojson | safe }}
// backwards compatibility for extensions (tpos)
const LNBITS_DENOMINATION = {{ LNBITS_DENOMINATION | tojson | safe }}
</script>
<!-- vue templates -->
{% include('components.vue') %} {% include('pages.vue') %}
+90 -16
View File
@@ -36,7 +36,7 @@ include('components/lnbits-error.vue') %}
<q-list v-if="g.user" dense class="lnbits-drawer__q-list">
<q-item-label header v-text="$t('manage')"></q-item-label>
<div v-if="g.user.admin">
<q-item v-if="showAdmin" to="/admin">
<q-item v-if="g.settings.showAdmin" to="/admin">
<q-item-section side>
<q-icon
name="settings"
@@ -51,7 +51,7 @@ include('components/lnbits-error.vue') %}
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
</q-item-section>
</q-item>
<q-item v-if="showNode" to="/node">
<q-item v-if="g.settings.showNodemanager" to="/node">
<q-item-section side>
<q-icon
name="developer_board"
@@ -66,7 +66,7 @@ include('components/lnbits-error.vue') %}
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
</q-item-section>
</q-item>
<q-item v-if="showUsers" to="/users">
<q-item v-if="g.settings.showAdmin" to="/users">
<q-item-section side>
<q-icon
name="groups"
@@ -81,7 +81,7 @@ include('components/lnbits-error.vue') %}
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
</q-item-section>
</q-item>
<q-item v-if="showAudit" to="/audit">
<q-item v-if="g.settings.showAudit" to="/audit">
<q-item-section side>
<q-icon
name="playlist_add_check_circle"
@@ -112,7 +112,7 @@ include('components/lnbits-error.vue') %}
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
</q-item-section>
</q-item>
<q-item v-if="showExtensions" to="/extensions">
<q-item v-if="g.settings.showExtensions" to="/extensions">
<q-item-section side>
<q-icon
name="extension"
@@ -686,7 +686,7 @@ include('components/lnbits-error.vue') %}
dense
filled
v-model="walletName"
:label="$t('name_your_wallet', {name: SITE_TITLE + ' *'})"
:label="$t('name_your_wallet', {name: g.settings.siteTitle + ' *'})"
></q-input>
<q-card-actions vertical align="center" class="q-pa-none">
<q-btn
@@ -814,16 +814,90 @@ include('components/lnbits-error.vue') %}
type="password"
:rules="[val => !val || val.length >= 8 || $t('invalid_password')]"
></q-input>
<div
v-if="confirmationMethodsCount > 1"
class="row justify-center q-mb-md"
>
<q-tabs
v-model="confirmationMethod"
dense
active-color="primary"
indicator-color="primary"
>
<q-tab
v-if="g.settings.userActivationByInvitationCode"
name="code"
icon="confirmation_number"
label="Code"
></q-tab>
<q-tab
v-if="g.settings.userActivationByPayment"
name="payment"
icon="bolt"
label="Payment"
></q-tab>
<q-tab
v-if="g.settings.userActivationByEmail"
name="email"
icon="email"
label="Email"
></q-tab>
</q-tabs>
</div>
<div v-if="confirmationMethodsCount > 0" class="q-mb-md">
<q-tab-panels v-model="confirmationMethod">
<q-tab-panel name="code" class="q-pa-none">
<div
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
>
<q-icon name="info" color="orange-4" class="q-mr-xs"></q-icon>
You need an invitation code to register.
</div>
<div>
<q-input
dense
filled
v-model="confirmationCode"
:label="$t('invitation_code')"
:type="showConfirmationCode ? 'text' : 'password'"
:hint="$t('invitation_code_hint')"
>
<q-btn
@click="showConfirmationCode = !showConfirmationCode"
dense
flat
:icon="
showConfirmationCode ? 'visibility_off' : 'visibility'
"
color="grey"
></q-btn>
</q-input>
</div>
</q-tab-panel>
<q-tab-panel name="payment">
<div>payment</div>
</q-tab-panel>
<q-tab-panel name="email" class="q-pa-none">
<div>
<q-input
dense
filled
v-model="confirmationEmail"
:label="$t('email')"
:hint="$t('email_confirmation_hint')"
>
</q-input>
</div>
</q-tab-panel>
</q-tab-panels>
</div>
<div class="row justify-end">
<q-btn
unelevated
color="primary"
:disable="
!password ||
!passwordRepeat ||
!username ||
password !== passwordRepeat
"
:disable="disableRegister"
type="submit"
class="full-width"
:label="$t('create_account')"
@@ -960,9 +1034,9 @@ include('components/lnbits-error.vue') %}
<q-avatar size="32px" class="q-mr-md">
<q-img
:src="
keycloakIcon
? keycloakIcon
: 'lnbits/static/images/keycloak-logo.png'
g.settings.keycloakIcon
? g.settings.keycloakIcon
: utils.url_for('lnbits/static/images/keycloak-logo.png')
"
></q-img>
</q-avatar>
@@ -970,7 +1044,7 @@ include('components/lnbits-error.vue') %}
<span
v-text="
$t('signin_with_custom_org', {
custom_org: keycloakOrg
custom_org: g.settings.keycloakOrg || 'Keycloak'
})
"
></span>
@@ -3,9 +3,6 @@
<h6 class="q-my-none q-mb-sm">Assets</h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-3">
<p>
<span v-text="$t('max_asset_size_mb')"></span>
</p>
<q-input
filled
type="number"
@@ -18,9 +15,6 @@
</div>
<div class="col-12 col-md-9">
<p>
<span v-text="$t('assets_allowed_mime_types')"></span>
</p>
<q-input
filled
v-model.number="newAllowedAssetMimeType"
@@ -56,11 +50,8 @@
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none q-mb-sm">Thumbnails</h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-3">
<p>
<span v-text="$t('thumbnail_width')"></span>
</p>
<div class="row q-col-gutter-md q-my-lg">
<div class="col-12 col-md-4">
<q-input
filled
type="number"
@@ -71,10 +62,7 @@
:hint="$t('thumbnail_width_desc')"
></q-input>
</div>
<div class="col-12 col-md-3">
<p>
<span v-text="$t('thumbnail_height')"></span>
</p>
<div class="col-12 col-md-4">
<q-input
filled
type="number"
@@ -85,10 +73,7 @@
:hint="$t('thumbnail_height_desc')"
></q-input>
</div>
<div class="col-12 col-md-3">
<p>
<span v-text="$t('thumbnail_format')"></span>
</p>
<div class="col-12 col-md-4">
<q-input
filled
v-model.number="formData.lnbits_asset_thumbnail_format"
@@ -101,9 +86,6 @@
<h6 class="q-my-none q-mb-sm">Users</h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-3">
<p>
<span v-text="$t('max_assets_per_user')"></span>
</p>
<q-input
filled
type="number"
@@ -115,9 +97,6 @@
></q-input>
</div>
<div class="col-12 col-md-9">
<p>
<span v-text="$t('assets_no_limit_users')"></span>
</p>
<q-input
filled
v-model="newNoLimitUser"
+4 -10
View File
@@ -32,7 +32,7 @@
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<div class="row">
<div class="col-md-3 col-sm-12 q-pr-sm">
<div class="col-md-6 col-sm-12 q-pr-sm">
<q-item tag="label" v-ripple>
<q-item-section avatar>
<q-toggle
@@ -62,7 +62,7 @@
</q-item-section>
</q-item>
</div>
<div class="col-md-3 col-sm-12 q-pr-sm">
<div class="col-md-6 col-sm-12 q-pr-sm">
<q-item tag="label" v-ripple>
<q-item-section avatar>
<q-toggle
@@ -83,7 +83,7 @@
</q-item-section>
</q-item>
</div>
<div class="col-md-3 col-sm-12 q-pr-sm">
<div class="col-md-6 col-sm-12 q-pr-sm">
<q-item tag="label" v-ripple>
<q-item-section avatar>
<q-toggle
@@ -104,7 +104,7 @@
</q-item-section>
</q-item>
</div>
<div class="col-md-3 col-sm-12 q-pr-sm">
<div class="col-md-6 col-sm-12 q-pr-sm">
<q-item tag="label" v-ripple>
<q-item-section avatar>
<q-toggle
@@ -129,7 +129,6 @@
<q-separator class="q-mb-xl q-mt-sm"></q-separator>
<div class="row q-mb-lg">
<div class="col-md-6 col-sm-12 q-pr-sm">
<p><span v-text="$t('audit_http_methods')"></span></p>
<q-select
filled
v-model="formData.lnbits_audit_http_methods"
@@ -148,7 +147,6 @@
></q-select>
</div>
<div class="col-md-6 col-sm-12 q-pr-sm">
<p><span v-text="$t('audit_resp_codes')"></span></p>
<q-input
class="q-mb-md"
filled
@@ -177,7 +175,6 @@
<div class="row">
<div class="col-md-6 col-sm-12 q-pr-sm">
<p><span v-text="$t('audit_paths')"></span></p>
<q-input
class="q-mb-md"
filled
@@ -204,9 +201,6 @@
<br />
</div>
<div class="col-md-6 col-sm-12">
<p>
<span v-text="$t('audit_paths_exclude')"></span>
</p>
<q-input
class="q-mb-md"
filled
@@ -19,6 +19,7 @@
</div>
<div class="col-12 col-md-4">
<q-input
class="q-mb-md"
filled
v-model="formData.lnbits_exchange_history_refresh_interval_seconds"
type="number"
@@ -137,7 +137,7 @@
:hint="$t('extension_builder_manifest_url_hint')"
></q-input>
</div>
<div class="col-12 col-md-6">
<div class="col-12 col-md-6 q-pb-md">
<p>
<span v-text="$t('reviews_url')"></span>
</p>
@@ -128,7 +128,7 @@
</ul>
</q-card-section>
</q-expansion-item>
<q-expansion-item :label="$t('service_fee')">
<q-expansion-item :label="$t('service_fees')">
<q-card-section>
<div class="row">
<div class="col-md-4 col-sm-12">
@@ -140,7 +140,6 @@
v-model="formData.stripe_limits.service_fee_percent"
@update:model-value="formData.touch = null"
:label="$t('service_fee_label')"
:hint="$t('service_fee_hint')"
></q-input>
</div>
<div class="col-md-4 col-sm-12">
@@ -151,8 +150,7 @@
min="0"
v-model="formData.stripe_limits.service_max_fee_sats"
@update:model-value="formData.touch = null"
:label="$t('service_fee_max')"
:hint="$t('service_fee_max_hint')"
:label="$t('service_fee_max_label')"
></q-input>
</div>
<div class="col-md-4 col-sm-12">
@@ -408,7 +406,6 @@
<li><code>CHECKOUT.ORDER.APPROVED</code></li>
<li><code>PAYMENT.CAPTURE.COMPLETED</code></li>
<li><code>PAYMENT.SALE.COMPLETED</code></li>
<li><code>BILLING.SUBSCRIPTION.PAYMENT.SUCCEEDED</code></li>
</ul>
</q-card-section>
</q-expansion-item>
+47 -13
View File
@@ -50,7 +50,7 @@
<br />
</div>
<div class="col">
<div v-if="LNBITS_NODE_UI">
<div v-if="g.settings.hasNodemanager">
<p>
<span v-text="$t('node_management')"></span>
</p>
@@ -70,53 +70,81 @@
v-model="formData.lnbits_node_ui_transactions"
></q-toggle>
</div>
<p v-if="!LNBITS_NODE_UI">
<p v-if="!g.settings.hasNodemanager">
<span v-text="$t('node_management_not_supported')"></span>
</p>
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-3">
<p><span v-text="$t('fee_reserve')"></span></p>
<div class="col-12">
<h6 class="q-my-none">
<span v-text="$t('routing_fee_reserve_calculations')"></span>
</h6>
<p class="q-mt-sm q-mb-none">
<span v-html="$t('routing_fee_reserve_calculations_desc')"></span>
</p>
</div>
<div class="col-12 col-md-4">
<p>
<span v-text="$t('fee_reserve')"></span>
<sup>
<q-icon name="info" size="16px" class="q-ml-xs"></q-icon>
<q-tooltip max-width="300px">
<span v-html="$t('fee_reserve_min_hint')"></span>
</q-tooltip>
</sup>
</p>
<q-input
type="number"
filled
v-model="formData.lnbits_reserve_fee_min"
:label="$t('fee_reserve_msats')"
:suffix="$t('millisats')"
>
</q-input>
</div>
<div class="col-12 col-md-3">
<div class="col-12 col-md-4">
<p>
<span v-text="$t('fee_reserve_percent')"></span>
<sup>
<q-icon name="info" size="16px" class="q-ml-xs"></q-icon>
<q-tooltip max-width="300px">
<span v-html="$t('fee_reserve_percent_hint')"></span>
</q-tooltip>
</sup>
</p>
<q-input
type="number"
filled
name="lnbits_reserve_fee_percent"
v-model="formData.lnbits_reserve_fee_percent"
:label="$t('reserve_fee_in_percent')"
step="0.1"
suffix="%"
></q-input>
</div>
<div class="col-12 col-md-3">
</div>
<div class="row q-col-gutter-md q-mt-sm">
<div class="col-12">
<h6 class="q-my-none">
<span v-text="$t('payment_timeouts')"></span>
</h6>
</div>
<div class="col-12 col-md-4">
<p><span v-text="$t('invoice_expiry')"></span></p>
<q-input
filled
v-model.number="formData.lightning_invoice_expiry"
type="number"
:label="$t('invoice_expiry_label')"
:suffix="$t('seconds')"
mask="#######"
>
</q-input>
</div>
<div class="col-12 col-md-3">
<div class="col-12 col-md-4">
<p>
<span v-text="$t('payment_wait_time')"></span>
<sup>
<q-icon name="info" size="16px" class="q-ml-xs"></q-icon>
<q-tooltip max-width="150px">
<q-tooltip max-width="300px">
<span v-text="$t('payment_wait_time_tooltip')"></span>
</q-tooltip>
</sup>
@@ -126,8 +154,8 @@
filled
name="lnbits_funding_source_pay_invoice_wait_seconds"
v-model="formData.lnbits_funding_source_pay_invoice_wait_seconds"
:label="$t('payment_wait_time')"
:hint="$t('payment_wait_time_desc')"
:suffix="$t('seconds')"
step="1"
min="0"
></q-input>
@@ -165,6 +193,11 @@
<h6 class="q-mt-lg q-mb-sm">
<p v-text="$t('watchdog')"></p>
</h6>
<div class="row q-col-gutter-md">
<div class="col-12">
<p v-text="$t('watchdog_introduction')"></p>
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<q-item tag="label" v-ripple>
@@ -239,6 +272,7 @@
<q-input
filled
v-model="formData.lnbits_watchdog_delta"
:suffix="$t('sats')"
type="number"
/>
</q-item-section>
@@ -12,11 +12,14 @@
<div class="row">
<div class="col-12">
<p>Active Funding<small> (Requires server restart)</small></p>
<p>
<span v-text="$t('funding_source')"></span>
<small><span v-text="$t('requires_server_restart')"></span></small>
</p>
<q-select
filled
v-model="formData.lnbits_backend_wallet_class"
hint="Select the active funding wallet"
:hint="$t('funding_source_info')"
:options="sortedAllowedFundingSources"
:option-label="item => getFundingSourceLabel(item)"
></q-select>
+10 -54
View File
@@ -1,11 +1,10 @@
<template id="lnbits-admin-security">
<q-card-section class="q-pa-none">
<h6 class="q-my-none">
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('server_management')"></span>
</h6>
<div class="row">
<div class="col-12 col-md-6">
<p><span v-text="$t('base_url')"></span></p>
<q-input
filled
v-model.number="formData.lnbits_baseurl"
@@ -17,7 +16,7 @@
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('authentication')"></span>
</h6>
<div class="row q-col-gutter-sm">
<div class="row q-col-gutter-sm q-mb-md">
<div class="col-12 col-md-6">
<q-input
filled
@@ -296,56 +295,6 @@
</div>
</div>
<div class="col-12 col-md-12">
<p v-text="$t('route_access_control')"></p>
<div class="row q-col-gutter-md">
<div class="col-12">
<q-toggle
v-model="formData.lnbits_route_access_control_enabled"
:label="$t('route_access_control_enable')"
></q-toggle>
<div
class="text-caption text-grey-6 q-mt-xs"
v-text="$t('route_access_control_hint')"
></div>
</div>
<div class="col-12 col-md-6">
<q-select
filled
multiple
use-chips
use-input
input-debounce="0"
new-value-mode="add-unique"
@new-value="addRouteOption"
:options="routeOptionsFiltered"
:loading="routeOptionsLoading"
@filter="filterRouteOptions"
v-model="formData.lnbits_route_access_whitelist"
:label="$t('route_access_whitelist_label')"
:hint="$t('route_access_whitelist_hint')"
></q-select>
</div>
<div class="col-12 col-md-6">
<q-select
filled
multiple
use-chips
use-input
input-debounce="0"
new-value-mode="add-unique"
@new-value="addRouteOption"
:options="routeOptionsFiltered"
:loading="routeOptionsLoading"
@filter="filterRouteOptions"
v-model="formData.lnbits_route_access_blacklist"
:label="$t('route_access_blacklist_label')"
:hint="$t('route_access_blacklist_hint')"
></q-select>
</div>
</div>
</div>
<div class="col-12 col-md-12">
<p v-text="$t('rate_limiter')"></p>
<div class="row q-col-gutter-md">
@@ -355,12 +304,19 @@
type="number"
v-model.number="formData.lnbits_rate_limit_no"
:label="$t('number_of_requests')"
:hint="$t('number_of_requests_hint')"
></q-input>
</div>
<div class="col-12 col-md-6">
<q-select
filled
:options="[$t('second'), $t('minute'), $t('hour')]"
:options="[
{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"
:label="$t('time_unit')"
></q-select>
+10 -26
View File
@@ -38,14 +38,11 @@
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none">
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('payments')"></span>
</h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-3">
<p>
<span v-text="$t('max_outgoing_payment_amount')"></span>
</p>
<div class="col-12 col-md-4">
<q-input
filled
type="number"
@@ -57,10 +54,7 @@
></q-input>
</div>
<div class="col-12 col-md-3">
<p>
<span v-text="$t('max_incoming_payment_amount')"></span>
</p>
<div class="col-12 col-md-4">
<q-input
filled
type="number"
@@ -75,7 +69,7 @@
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none">
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('wallet_limiter')"></span>
</h6>
<div class="row q-col-gutter-md">
@@ -113,35 +107,32 @@
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none">
<span v-text="$t('service_fee')"></span>
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('service_fees')"></span>
</h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p><span v-text="$t('service_fee')"></span></p>
<div class="col-12 col-md-4">
<q-input
filled
type="number"
v-model.number="formData.lnbits_service_fee"
:label="$t('service_fee_label')"
step="0.1"
:hint="$t('service_fee_hint')"
:suffix="'%'"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('service_fee_max')"></span></p>
<div class="col-12 col-md-4">
<q-input
filled
type="number"
v-model.number="formData.lnbits_service_fee_max"
:label="$t('service_fee_max_label')"
:hint="$t('service_fee_max_hint')"
:suffix="$t('sats')"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('fee_wallet')"></span></p>
<q-input
filled
v-model="formData.lnbits_service_fee_wallet"
@@ -151,16 +142,9 @@
<br />
</div>
<div class="col-12 col-md-6">
<p>
<span v-text="$t('disable_fee_internal')"></span>
</p>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('disable_fee')"></q-item-label>
<q-item-label
caption
v-text="$t('disable_fee_internal_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
@@ -6,8 +6,7 @@
<br />
<div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-5">
<p><span v-text="$t('ui_site_title')"></span></p>
<div class="col-12 col-md-6">
<q-input
filled
type="text"
@@ -18,8 +17,7 @@
></q-input>
<br />
</div>
<div class="col-12 col-md-5">
<p><span v-text="$t('ui_site_tagline')"></span></p>
<div class="col-12 col-md-6">
<q-input
filled
type="text"
@@ -28,7 +26,9 @@
></q-input>
<br />
</div>
<div class="col-12 col-md-2 q-mt-xl">
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-12">
<q-toggle
:tip="$t('ui_toggle_elements_tip')"
v-model="formData.lnbits_show_home_page_elements"
@@ -40,78 +40,66 @@
></q-toggle>
</div>
</div>
<div>
<p>
<span v-text="$t('ui_site_description')"></span>
</p>
<q-input
v-model="formData.lnbits_site_description"
filled
type="textarea"
:hint="$t('ui_site_description_hint')"
/>
</div>
<br />
<div class="row q-col-gutter-md">
<div class="col-12">
<q-input
v-model="formData.lnbits_site_description"
filled
type="textarea"
:label="$t('ui_site_description')"
:hint="$t('ui_site_description_hint')"
/>
</div>
<div class="col-12 col-md-4">
<p>
<span v-text="$t('ui_default_wallet_name')"></span>
</p>
<q-input
filled
type="text"
v-model="formData.lnbits_default_wallet_name"
:label="$t('lnbits_wallet')"
:label="$t('ui_default_wallet_name')"
></q-input>
</div>
<div class="col-12 col-md-4">
<p><span v-text="$t('ui_qr_code_logo')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_qr_logo"
label="https://example.com/image.svg"
:label="$t('ui_qr_code_logo')"
:hint="$t('ui_qr_code_logo_hint')"
></q-input>
</div>
<div class="col-12 col-md-4">
<p><span v-text="$t('ui_apple_touch_icon')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_apple_touch_icon"
label="https://example.com/image.png"
:label="$t('ui_apple_touch_icon')"
:hint="$t('ui_apple_touch_icon_hint')"
></q-input>
</div>
</div>
<br />
<div class="row q-col-gutter-md">
<div class="col-12 col-md-12">
<p><span v-text="$t('wallet_featured_button_title')"></span></p>
</div>
<div class="col-12 col-md-4">
<p>
<span v-text="$t('wallet_featured_button_label')"></span>
</p>
<q-input
filled
type="text"
v-model="formData.lnbits_wallet_featured_button_label"
label="Loop to Onchain"
:label="$t('wallet_featured_button_label')"
: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"
:label="$t('wallet_featured_button_url')"
: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"
@@ -121,21 +109,27 @@
></q-input>
</div>
</div>
<div class="row q-col-gutter-md q-mt-md">
<div class="col-12 col-md-6">
<p><span v-text="$t('ui_custom_badge')"></span></p>
<div class="col-12 col-md-12">
<h6 class="q-my-sm">
<span v-text="$t('ui_custom_badge_title')"></span>
</h6>
<p class="q-mb-none">
<span v-text="$t('ui_custom_badge_desc')"></span>
</p>
</div>
<div class="col-12">
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8">
<div class="col-12 col-md-6">
<q-input
filled
type="text"
tip="Custom Badge"
v-model.trim="formData.lnbits_custom_badge"
:label="$t('ui_custom_badge_label')"
:label="$t('ui_custom_badge')"
></q-input>
</div>
<div class="col-12 col-md-4">
<div class="col-12 col-md-6">
<q-select
filled
v-model="formData.lnbits_custom_badge_color"
@@ -145,8 +139,8 @@
</div>
</div>
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('ui_custom_image')"></span></p>
<div class="col-12">
<h6 class="q-my-sm"><span v-text="$t('ui_custom_image')"></span></h6>
<q-input
filled
type="text"
@@ -157,10 +151,8 @@
></q-input>
</div>
</div>
<br />
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p><span v-text="$t('themes')"></span></p>
<q-select
filled
v-model="formData.lnbits_theme_options"
@@ -172,41 +164,26 @@
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('custom_logo')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_custom_logo"
label="https://example.com/image.png"
:label="$t('custom_logo')"
:hint="$t('custom_logo_hint')"
></q-input>
<br />
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p><span v-text="$t('ad_space_title')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_ad_space_title"
:label="$t('ad_space_title_label')"
></q-input>
<br />
<div class="col-12 col-md-12">
<h6 class="q-my-sm">
<span v-text="$t('ad_space_section_title')"></span>
</h6>
<p class="q-my-none">
<span v-text="$t('ad_space_section_desc')"></span>
</p>
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('ad_slots')"></span></p>
<q-input
class="q-mb-md"
filled
v-model="formData.lnbits_ad_space"
type="text"
:label="$t('ad_slots_label')"
:hint="$t('ad_slots_hint')"
><q-tooltip>
format {url};{img-light};{img-dark},{url};{img-light};{img-dark}"
</q-tooltip>
</q-input>
<div class="col-12 col-md-12">
<q-toggle
v-model="formData.lnbits_ad_space_enabled"
:label="
@@ -215,8 +192,30 @@
: $t('ads_disabled')
"
/>
</div>
<div class="col-12 col-md-6">
<q-input
filled
type="text"
v-model="formData.lnbits_ad_space_title"
:label="$t('ad_space_title')"
:hint="$t('ad_space_title_hint')"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<q-input
class="q-mb-md"
filled
v-model="formData.lnbits_ad_space"
type="text"
:label="$t('ad_slots')"
:hint="$t('ad_slots_hint')"
><q-tooltip>
format {url};{img-light};{img-dark},{url};{img-light};{img-dark}"
</q-tooltip>
</q-input>
</div>
</div>
<div class="row q-col-gutter-md q-my-md">
<div class="col-12" v-text="$t('ui_default_theme')"></div>
@@ -257,12 +256,50 @@
>
</q-input>
</div>
<div class="col-12 col-sm-6 col-lg-3">
</div>
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12 col-sm-6 col-lg-2">
<q-toggle
type="bool"
v-model="formData.lnbits_default_gradient"
color="primary"
label="Gradient Background"
:label="$t('gradient_background')"
>
</q-toggle>
</div>
<div class="col-12 col-sm-6 col-lg-2">
<q-toggle
type="bool"
v-model="formData.lnbits_default_dark"
color="primary"
:label="$t('toggle_darkmode')"
>
</q-toggle>
</div>
<div class="col-12 col-sm-6 col-lg-2">
<q-toggle
type="bool"
v-model="formData.lnbits_default_card_rounded"
color="primary"
:label="$t('rounded_ui')"
>
</q-toggle>
</div>
<div class="col-12 col-sm-6 col-lg-2">
<q-toggle
type="bool"
v-model="formData.lnbits_default_card_gradient"
color="primary"
:label="$t('card_gradient')"
>
</q-toggle>
</div>
<div class="col-12 col-sm-6 col-lg-2">
<q-toggle
type="bool"
v-model="formData.lnbits_default_card_shadow"
color="primary"
:label="$t('card_shadow')"
>
</q-toggle>
</div>
+234 -3
View File
@@ -30,7 +30,6 @@
>
</q-chip>
</div>
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('allowed_users')"></span></p>
@@ -57,7 +56,10 @@
>
</q-chip>
</div>
<br />
</div>
</div>
<div class="row">
<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('allow_creation_user')"></q-item-label>
@@ -76,8 +78,237 @@
/>
</q-item-section>
</q-item>
<br />
</div>
<div class="col-12 col-md-6 q-mt-sm">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('require_user_activation')"></q-item-label>
<q-item-label
caption
v-text="$t('require_user_activation_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_require_user_activation"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
</div>
</div>
</q-card-section>
<div v-if="formData.lnbits_require_user_activation" class="row">
<div class="col">
<q-list bordered class="rounded-borders">
<q-expansion-item header-class="text-primary text-bold">
<template v-slot:header>
<q-item-section avatar>
<q-icon name="confirmation_number" size="md"></q-icon>
</q-item-section>
<q-item-section> Invitation Code </q-item-section>
<q-item-section side>
<div class="row items-center">
<q-toggle
size="md"
:label="
formData.lnbits_user_activation_by_invitation_code
? $t('enabled')
: $t('disabled')
"
v-model="formData.lnbits_user_activation_by_invitation_code"
color="green"
unchecked-icon="clear"
/>
</div>
</q-item-section>
</template>
<q-card class="q-pb-xl">
<q-card-section>
<div
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
>
<q-icon
name="info"
color="orange-4"
size="18px"
class="q-mr-xs"
></q-icon>
Users will need to provide a valid invitation code during
registration to activate their account.
</div>
</q-card-section>
<q-card-section>
<div class="row">
<div class="col-12 col-md-6 q-pr-sm">
<p><span v-text="$t('reusable_activation_code')"></span></p>
<q-input
filled
v-model="formData.lnbits_register_reusable_activation_code"
:type="showReusableActivationCode ? 'text' : 'password'"
:label="$t('reusable_activation_code_label')"
:hint="$t('reusable_activation_code_hint')"
>
<q-btn
@click="
showReusableActivationCode = !showReusableActivationCode
"
dense
flat
:icon="
showReusableActivationCode
? 'visibility_off'
: 'visibility'
"
color="grey"
></q-btn>
<q-btn
@click="
utils.copyText(
formData.lnbits_register_reusable_activation_code
)
"
dense
flat
icon="content_copy"
color="grey"
></q-btn>
</q-input>
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('one_time_activation_code')"></span></p>
<q-input
filled
v-model="formAddActivationCode"
@keydown.enter="addOneTimeActivationCode"
type="text"
:label="$t('one_time_activation_code_label')"
:hint="$t('one_time_activation_code_hint')"
>
<q-btn
@click="addOneTimeActivationCode"
dense
flat
icon="add"
></q-btn>
</q-input>
<div>
<q-chip
v-for="code in formData.lnbits_register_one_time_activation_codes"
:key="code"
removable
@remove="removeOneTimeActivationCode(code)"
color="primary"
text-color="white"
:label="code"
class="ellipsis"
>
</q-chip>
</div>
</div>
</div>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item header-class="text-primary text-bold">
<template v-slot:header>
<q-item-section avatar>
<q-avatar>
<q-icon name="bolt" size="md"></q-icon>
</q-avatar>
</q-item-section>
<q-item-section> Payment </q-item-section>
<q-item-section side>
<div class="row items-center">
<q-toggle
size="md"
disable
:label="
formData.lnbits_user_activation_by_payment
? $t('enabled')
: $t('disabled')
"
v-model="formData.lnbits_user_activation_by_payment"
color="green"
unchecked-icon="clear"
/>
</div>
</q-item-section>
</template>
<q-card class="q-pb-xl">
<q-card-section>
<div
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
>
<q-icon
name="info"
color="orange-4"
size="18px"
class="q-mr-xs"
></q-icon>
Users will need to make a payment during registration to
activate their account.
</div>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item header-class="text-primary text-bold">
<template v-slot:header>
<q-item-section avatar>
<q-avatar>
<q-icon name="email"></q-icon>
</q-avatar>
</q-item-section>
<q-item-section> Email </q-item-section>
<q-item-section side>
<div class="row items-center">
<q-toggle
disable
size="md"
:label="
formData.lnbits_user_activation_by_email
? $t('enabled')
: $t('disabled')
"
v-model="formData.lnbits_user_activation_by_email"
color="green"
unchecked-icon="clear"
/>
</div>
</q-item-section>
</template>
<q-card class="q-pb-xl">
<q-card-section>
<div
class="q-my-md q-pa-sm text-body2 text-grey-4 bg-grey-9 rounded-borders"
>
<q-icon
name="info"
color="orange-4"
size="18px"
class="q-mr-xs"
></q-icon>
Users will receive a confirmation email.
</div>
</q-card-section>
</q-card>
</q-expansion-item>
</q-list>
</div>
</div>
</template>
@@ -1,6 +1,6 @@
<template id="lnbits-footer">
<q-footer
v-if="showFooter"
v-if="g.settings.showHomePageElements"
class="bg-transparent q-px-lg q-py-md"
:class="{'text-dark': !$q.dark.isActive}"
>
@@ -9,7 +9,7 @@
<q-toolbar-title class="text-caption">
<span v-text="title"></span>
<br />
<small v-text="$t('lnbits_version') + ': ' + version"></small>
<small v-text="version"></small>
</q-toolbar-title>
<q-space></q-space>
<q-btn
+20 -17
View File
@@ -1,6 +1,10 @@
<template id="lnbits-header">
<q-header bordered class="bg-marginal-bg">
<q-banner v-if="showVoidwallet" class="bg-warning text-white" dense>
<q-banner
v-if="g.settings.showVoidwallet"
class="bg-warning text-white"
dense
>
<template v-slot:avatar>
<q-icon name="warning" color="white" />
</template>
@@ -23,14 +27,13 @@
></q-btn>
<q-toolbar-title>
<q-btn flat no-caps dense class="q-mr-sm" size="lg" type="a" href="/">
<q-img
v-if="customLogoUrl"
height="30px"
alt="Logo"
:src="customLogoUrl"
></q-img>
<span v-else-if="!titleIsLnbits"><strong>LN</strong>bits</span>
<span v-else v-text="title"></span>
<q-avatar v-if="g.settings.customLogo" height="30px">
<img alt="Logo" :src="g.settings.customLogo" />
</q-avatar>
<span v-else-if="g.settings.siteTitle == 'LNbits'"
><strong>LN</strong>bits</span
>
<span v-else v-text="g.settings.siteTitle"></span>
</q-btn>
<q-badge v-if="g.user && g.user.super_user">Super User</q-badge>
<q-badge v-else-if="g.user && g.user.admin">Admin User</q-badge>
@@ -38,31 +41,31 @@
<q-badge
class="q-mr-md"
v-show="$q.screen.gt.sm"
v-if="hasCustomBadge"
:label="customBadge"
:color="customBadgeColor"
v-if="g.settings.customBadge"
:label="g.settings.customBadge"
:color="g.settings.customBadgeColor"
>
</q-badge>
<q-badge
v-show="$q.screen.gt.sm"
v-if="hasServiceFee"
v-if="g.user && g.settings.serviceFee > 0"
color="green"
class="q-mr-md"
>
<span
v-if="hasServiceFeeMax"
v-if="g.user && g.settings.serviceFeeMax > 0"
v-text="
$t('service_fee_max_badge', {
amount: serviceFee,
max: serviceFeeMax,
amount: g.settings.serviceFee,
max: g.settings.serviceFeeMax,
denom: g.denomination
})
"
></span>
<span
v-else
v-text="$t('service_fee_badge', {amount: serviceFee})"
v-text="$t('service_fee_badge', {amount: g.settings.serviceFee})"
></span>
<q-tooltip><span v-text="$t('service_fee_tooltip')"></span></q-tooltip>
</q-badge>
File diff suppressed because it is too large Load Diff
@@ -118,7 +118,7 @@
</q-list>
</q-card-section>
<q-expansion-item
v-if="!HIDE_API"
v-if="!g.settings.hideApi"
group="api"
dense
expand-separator
@@ -152,7 +152,7 @@
</q-expansion-item>
<q-expansion-item
v-if="!HIDE_API"
v-if="!g.settings.hideApi"
group="api"
dense
expand-separator
@@ -197,7 +197,7 @@
</q-card>
</q-expansion-item>
<q-expansion-item
v-if="!HIDE_API"
v-if="!g.settings.hideApi"
group="api"
dense
expand-separator
@@ -244,7 +244,7 @@
</q-expansion-item>
<q-expansion-item
v-if="!HIDE_API"
v-if="!g.settings.hideApi"
group="api"
dense
expand-separator
@@ -273,7 +273,7 @@
</q-card>
</q-expansion-item>
<q-expansion-item
v-if="!HIDE_API"
v-if="!g.settings.hideApi"
group="api"
dense
expand-separator
+82 -15
View File
@@ -258,15 +258,20 @@
class="q-mb-md"
>
</q-input>
<div v-if="!g.user.email" class="row"></div>
<div v-if="!g.user.email" class="row">
{% if "google-auth" in LNBITS_AUTH_METHODS or
"github-auth" in LNBITS_AUTH_METHODS %}
<div class="col q-pa-sm text-h6">
<div
v-if="
'google-auth' in g.settings.authMethods ||
'github-auth' in g.settings.authMethods
"
class="col q-pa-sm text-h6"
>
<span v-text="$t('verify_email')"></span>:
</div>
{%endif%} {% if "google-auth" in LNBITS_AUTH_METHODS %}
<div class="col q-pa-sm">
<div
v-if="'google-auth' in g.settings.authMethods"
class="col q-pa-sm"
>
<q-btn
:href="`/api/v1/auth/google?user_id=${g.user.id}`"
type="a"
@@ -284,8 +289,10 @@
<div>Google</div>
</q-btn>
</div>
{%endif%} {% if "github-auth" in LNBITS_AUTH_METHODS %}
<div class="col q-pa-sm">
<div
v-if="'github-auth' in g.settings.authMethods"
class="col q-pa-sm"
>
<q-btn
:href="`/api/v1/auth/github?user_id=${g.user.id}`"
type="a"
@@ -303,7 +310,6 @@
<div>GitHub</div>
</q-btn>
</div>
{%endif%}
</div>
</q-card-section>
@@ -459,6 +465,69 @@
</q-toggle>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('rounded_ui')"></span>
</div>
<div class="col-8">
<q-toggle
dense
flat
round
icon="rounded_corner"
v-model="g.cardRoundedChoice"
@update:model-value="
siteCustomisationChanged({cardRoundedChoice: $event})
"
>
<q-tooltip
><span v-text="$t('toggle_rounded_ui')"></span
></q-tooltip>
</q-toggle>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('card_gradient')"></span>
</div>
<div class="col-8">
<q-toggle
dense
flat
round
icon="gradient"
v-model="g.cardGradientChoice"
@update:model-value="
siteCustomisationChanged({cardGradientChoice: $event})
"
>
<q-tooltip
><span v-text="$t('toggle_card_gradient')"></span
></q-tooltip>
</q-toggle>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('card_shadow')"></span>
</div>
<div class="col-8">
<q-toggle
dense
flat
round
icon="blur_on"
v-model="g.cardShadowChoice"
@update:model-value="
siteCustomisationChanged({cardShadowChoice: $event})
"
>
<q-tooltip
><span v-text="$t('toggle_card_shadow')"></span
></q-tooltip>
</q-toggle>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
@@ -508,7 +577,7 @@
<div class="col-8">
<lnbits-notifications-btn
v-if="g.user"
pubkey="{{ WEBPUSH_PUBKEY }}"
pubkey="g.settings.webpushPubkey"
></lnbits-notifications-btn>
</div>
</div>
@@ -533,9 +602,7 @@
</div>
<q-card-section>
<q-btn
@click="
siteCustomisationChanged(defaultSiteCustomisation)
"
@click="resetThemeDefaults"
:label="$t('reset_defaults')"
filled
color="primary"
@@ -552,7 +619,7 @@
></span>
<br />
<q-badge
v-if="!LNBITS_NOSTR_CONFIGURED"
v-if="!g.settings.nostrConfigured"
v-text="$t('not_connected')"
></q-badge>
</div>
@@ -572,7 +639,7 @@
<span v-text="$t('notifications_chat_id')"></span>
<br />
<q-badge
v-if="!LNBITS_TELEGRAM_CONFIGURED"
v-if="!g.settings.telegramConfigured"
v-text="$t('not_connected')"
></q-badge>
</div>
+1 -1
View File
@@ -178,7 +178,7 @@
"
:clickable="!!reviewsUrl"
@click="openReviews(extension)"
/>
></lnbits-extension-rating>
<q-btn-group size="xs" style="margin: 5px 0">
<q-btn
v-if="extension.hasFreeRelease"
+13
View File
@@ -53,6 +53,19 @@
@click="loginData.isPwdRepeat = !loginData.isPwdRepeat"
/> </template
></q-input>
<q-input
filled
v-if="g.settings.hasFirstInstallToken"
v-model.trim="loginData.firstInstallToken"
:type="loginData.isPwd ? 'password' : 'text'"
:label="$t('first_install_token')"
><template v-slot:append>
<q-icon
:name="loginData.isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="loginData.isPwd = !loginData.isPwd"
/> </template
></q-input>
<q-btn
@click="setPassword()"
unelevated
+29 -15
View File
@@ -2,14 +2,14 @@
<div class="home row justify-center items-center">
<div
class="full-width content-center"
:style="`max-width: ${hasCustomImage ? '850' : '600'}px; min-height: 55vh;`"
:style="`max-width: ${g.settings.customImage ? '850' : '600'}px; min-height: 55vh;`"
>
<div v-if="showHomepageElements" class="row q-mb-md">
<div v-if="g.settings.showHomepageElements" class="row q-mb-md">
<div class="col-12">
<div>
<h5 v-text="siteTitle" class="q-my-none"></h5>
<h5 v-text="g.settings.siteTitle" class="q-my-none"></h5>
<template v-if="$q.screen.gt.sm">
<h6 class="q-my-sm" v-text="siteTagline"></h6>
<h6 class="q-my-sm" v-text="g.settings.siteTagline"></h6>
<p class="q-my-sm" v-html="formatDescription"></p>
</template>
</div>
@@ -31,7 +31,10 @@
<div class="row">
<div
class="col-12"
:class="{'col-sm-7': hasCustomImage, 'col-lg-6': hasCustomImage}"
:class="{
'col-sm-7': g.settings.customImage,
'col-lg-6': g.settings.customImage
}"
>
<div v-if="showClaimLnurl">
<q-card-section>
@@ -54,12 +57,13 @@
<div v-else>
<username-password
v-if="authMethod != 'user-id-only'"
:allowed_new_users="allowRegister"
:auth-methods="LNBITS_AUTH_METHODS"
:allowed_new_users="g.settings.allowRegister"
:auth-methods="g.settings.authMethods"
:auth-action="authAction"
v-model:user-name="username"
v-model:password_1="password"
v-model:password_2="passwordRepeat"
v-model:invitation-code="invitationCode"
v-model:reset-key="reset_key"
@login="login"
@register="register"
@@ -70,7 +74,7 @@
v-if="authAction !== 'reset'"
>
<p
v-if="authAction === 'login' && allowRegister"
v-if="authAction === 'login' && g.settings.allowRegister"
class="q-mb-none"
>
Not registered?
@@ -82,7 +86,9 @@
>
</p>
<p
v-else-if="authAction === 'login' && !allowRegister"
v-else-if="
authAction === 'login' && !g.settings.allowRegister
"
class="q-mb-none"
>
<span v-text="$t('new_user_not_allowed')"></span>
@@ -101,7 +107,7 @@
</username-password>
<user-id-only
v-if="authMethod == 'user-id-only'"
:allowed_new_users="allowRegister"
:allowed_new_users="g.settings.allowRegister"
v-model:usr="usr"
v-model:wallet="walletName"
:auth-action="authAction"
@@ -114,16 +120,20 @@
</user-id-only>
</div>
</div>
<div v-if="hasCustomImage" class="col-sm-5 col-lg-6 gt-xs">
<div v-if="g.settings.customImage" class="col-sm-5 col-lg-6 gt-xs">
<div class="full-height flex flex-center q-pa-lg">
<q-img :src="hasCustomImage" :ratio="1" width="250px"></q-img>
<q-img
:src="g.settings.customImage"
:ratio="1"
width="250px"
></q-img>
</div>
</div>
</div>
</q-card>
</div>
<div v-if="showHomepageElements">
<div v-if="g.settings.showHomepageElements">
<div class="flex flex-center q-gutter-md q-py-md">
<q-btn
outline
@@ -148,10 +158,14 @@
</div>
<div
v-if="adsEnabled"
v-if="g.settings.showAdSpace"
class="justify-center col-10 q-my-lg row q-col-gutter-sm"
>
<a :href="ad[0]" v-for="ad in g.ads" class="lnbits-ad col-12 col-md-4">
<a
: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-else :src="ad[2]"></q-img>
</a>
+33 -5
View File
@@ -218,6 +218,26 @@
:label="$t('update_account')"
class="q-ml-md"
></q-btn>
<q-btn
v-if="activeUser.data.id"
outline
color="primary"
class="q-ml-md"
>
<q-toggle
size="xs"
color="secondary"
v-model="activeUser.data.admin"
@update:model-value="toggleAdmin(activeUser.data.id)"
:label="
activeUser.data.admin
? $t('revoke_admin')
: $t('make_user_admin')
"
>
</q-toggle>
</q-btn>
<q-btn
v-else
@click="createUser()"
@@ -590,9 +610,9 @@
<q-btn
@click="showAccountPage(props.row.id)"
round
icon="edit"
:icon="props.row.is_admin ? 'admin_panel_settings' : 'edit'"
size="sm"
color="secondary"
:color="props.row.is_admin ? 'primary' : 'secondary'"
class="q-ml-xs"
>
<q-tooltip>
@@ -605,10 +625,18 @@
size="xs"
v-if="!props.row.is_super_user"
color="secondary"
v-model="props.row.is_admin"
@update:model-value="toggleAdmin(props.row.id)"
v-model="props.row.activated"
@update:model-value="toggleUserActivated(props.row.id)"
>
<q-tooltip>Toggle Admin</q-tooltip>
<q-tooltip
><span
v-text="
props.row.activated
? $t('deactivate')
: $t('activate')
"
></span
></q-tooltip>
</q-toggle>
<q-btn
round
+9 -9
View File
@@ -172,15 +172,15 @@
></q-tooltip>
</q-btn>
<div
v-if="WALLET_FEATURED_BUTTON_URL"
v-if="g.settings.walletFeaturedButtonUrl"
class="float-right q-mt-sm q-ml-sm"
>
<q-btn
color="primary"
:label="WALLET_FEATURED_BUTTON_LABEL"
:icon="WALLET_FEATURED_BUTTON_ICON || undefined"
:label="g.settings.walletFeaturedButtonLabel"
:icon="g.settings.walletFeaturedButtonIcon || undefined"
size="sm"
:to="WALLET_FEATURED_BUTTON_URL"
:to="g.settings.walletFeaturedButtonUrl"
>
</q-btn>
</div>
@@ -204,7 +204,7 @@
</q-card>
</div>
<div
v-if="!g.mobileSimple || !$q.screen.lt.md"
v-show="!g.mobileSimple || !$q.screen.lt.md"
class="col-12 col-md-5 q-gutter-y-md"
>
<lnbits-wallet-extra
@@ -212,10 +212,10 @@
@send-lnurl="handleSendLnurl"
:chart-config="chartConfig"
></lnbits-wallet-extra>
<q-card class="lnbits-wallet-ads" v-if="AD_SPACE_ENABLED">
<q-card class="lnbits-wallet-ads" v-if="g.settings.showAdSpace">
<q-card-section class="text-subtitle1">
<span v-text="AD_SPACE_TITLE"></span>
<a :href="ad[0]" class="lnbits-ad" v-for="ad in g.ads">
<span v-text="g.settings.adSpaceTitle"></span>
<a :href="ad[0]" class="lnbits-ad" v-for="ad in g.settings.adSpace">
<q-img class="q-mb-xs" v-if="$q.dark.isActive" :src="ad[1]"></q-img>
<q-img class="q-mb-xs" v-else :src="ad[2]"></q-img>
</a>
@@ -289,7 +289,7 @@
></q-input>
</div>
<q-input
v-if="has_holdinvoice"
v-if="g.settings.hasHoldinvoice"
filled
dense
v-model="receive.data.payment_hash"
+2 -4
View File
@@ -45,10 +45,8 @@
</div>
<script type="text/javascript">
const WINDOW_SETTINGS = {{ WINDOW_SETTINGS | tojson }}
Object.keys(WINDOW_SETTINGS).forEach(key => {
window[key] = WINDOW_SETTINGS[key]
})
const SETTINGS = {{ SETTINGS | tojson | safe }}
const CURRENCIES = {{ CURRENCIES | tojson | safe }}
</script>
{% include('components.vue') %}{% block vue_templates %}{% endblock %} {%
for url in INCLUDED_JS %}
+10 -4
View File
@@ -292,7 +292,8 @@ async def btc_rates(currency: str) -> list[tuple[str, float]]:
async def btc_price(currency: str) -> float:
rates = await btc_rates(currency)
if not rates:
raise ValueError("Could not fetch any Bitcoin price.")
logger.warning("Could not fetch any Bitcoin price.")
return 0.0
elif len(rates) == 1:
logger.warning("Could only fetch one Bitcoin price.")
@@ -306,7 +307,8 @@ async def get_fiat_rate_and_price_satoshis(currency: str) -> tuple[float, float]
f"btc-price-{currency}",
settings.lnbits_exchange_rate_cache_seconds,
)
return float(100_000_000 / price), price
rate = float(100_000_000 / price) if price > 0 else 0.0
return rate, price
async def get_fiat_rate_satoshis(currency: str) -> float:
@@ -316,9 +318,13 @@ async def get_fiat_rate_satoshis(currency: str) -> float:
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
rate = await get_fiat_rate_satoshis(currency)
return int(amount * (rate))
if rate > 0:
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:
rate = await get_fiat_rate_satoshis(currency)
return float(amount / rate)
if rate > 0:
return float(amount / rate)
raise ValueError(f"Could not get exchange rate for {currency}.")
+7
View File
@@ -14,6 +14,13 @@ from lnbits.settings import settings
def log_server_info():
logger.info("LNbits Info")
if settings.first_install:
logger.success("This is a fresh install of LNbits.")
if settings.first_install_token:
logger.success(
f"FIRST_INSTALL_TOKEN: `{settings.first_install_token}`. "
"Please provide this token on /first_install."
)
logger.info(f"Version: {settings.version}")
logger.info(f"Baseurl: {settings.lnbits_baseurl}")
logger.info(f"Host: {settings.host}")

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