Compare commits
114
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d95babc750 | ||
|
|
8b426efa3e | ||
|
|
9edc4786e1 | ||
|
|
93dc10fe94 | ||
|
|
6c8448d7a8 | ||
|
|
99e4f33142 | ||
|
|
f4f43ad361 | ||
|
|
f14ea6c577 | ||
|
|
07428ecf94 | ||
|
|
867e3d06f6 | ||
|
|
4b4b6d0bcd | ||
|
|
07b1521dad | ||
|
|
7a2ddd9826 | ||
|
|
0eb4b477b7 | ||
|
|
06a0ba58ce | ||
|
|
5399b36027 | ||
|
|
385fb4f9bc | ||
|
|
8db76b8864 | ||
|
|
9e3ab0ef26 | ||
|
|
04c9b67997 | ||
|
|
116f982aab | ||
|
|
63cc89e2b6 | ||
|
|
183e6e5661 | ||
|
|
6b3fd80e46 | ||
|
|
5999a773be | ||
|
|
7e0fadad3b | ||
|
|
b2d6243697 | ||
|
|
6b5a77fb3b | ||
|
|
cc8fb68b02 | ||
|
|
658da6b28e | ||
|
|
7d734ecb74 | ||
|
|
9177dd195b | ||
|
|
15faab4f38 | ||
|
|
2cce687865 | ||
|
|
313bd3f647 | ||
|
|
13a93836d9 | ||
|
|
4f76d0483e | ||
|
|
719d86aa9c | ||
|
|
bbad4a91ae | ||
|
|
fcebb7e28c | ||
|
|
ce5aa4c8a7 | ||
|
|
75bae67446 | ||
|
|
8c184356ef | ||
|
|
efc0547271 | ||
|
|
7a393b11fd | ||
|
|
fae3eca3c7 | ||
|
|
3398070dd5 | ||
|
|
7d98c6b080 | ||
|
|
5f4e889d3a | ||
|
|
fe774ae1a1 | ||
|
|
21e02cb20e | ||
|
|
a3d7b463ae | ||
|
|
e1e2112461 | ||
|
|
92fc9a130f | ||
|
|
e15f3cd207 | ||
|
|
041b358310 | ||
|
|
cbd3de88c4 | ||
|
|
156ab10ad6 | ||
|
|
b77c103020 | ||
|
|
1f34357ab7 | ||
|
|
d9eda48298 | ||
|
|
1be2d1a680 | ||
|
|
b55ae9daaf | ||
|
|
1f98aa2cdb | ||
|
|
55ec3f1812 | ||
|
|
7880044483 | ||
|
|
28b3f79d9b | ||
|
|
4c704305ac | ||
|
|
c47fd2a4d1 | ||
|
|
1255df43e6 | ||
|
|
6ccc10d327 | ||
|
|
8bcf5e4f70 | ||
|
|
04ec643c93 | ||
|
|
f93efbfb3a | ||
|
|
4429c0cdb6 | ||
|
|
2bdb89b4b9 | ||
|
|
75754598e1 | ||
|
|
b25a3c2bb4 | ||
|
|
e7c4de6506 | ||
|
|
c102f10fab | ||
|
|
7c28b58aaa | ||
|
|
2b27f13fa8 | ||
|
|
130f4fd0bc | ||
|
|
4894b70bad | ||
|
|
05769240d3 | ||
|
|
7bd127d22c | ||
|
|
6dc3df148d | ||
|
|
e286546b63 | ||
|
|
eae1be1db8 | ||
|
|
25ab676a3f | ||
|
|
2fd6648a9a | ||
|
|
8eda653e3f | ||
|
|
7e327b2c6a | ||
|
|
40c055e373 | ||
|
|
1d9808b269 | ||
|
|
c65ab2af87 | ||
|
|
15f093c136 | ||
|
|
5311b0a393 | ||
|
|
e72396fe31 | ||
|
|
d6d0ebb20e | ||
|
|
563dd5b499 | ||
|
|
9be894da74 | ||
|
|
87b66c554a | ||
|
|
314b533e39 | ||
|
|
af8f3382c1 | ||
|
|
67bb86c1ce | ||
|
|
f0f9e41f99 | ||
|
|
2fbaf57b5e | ||
|
|
87afba2abb | ||
|
|
c375bf1b15 | ||
|
|
dbcc920d0a | ||
|
|
2c61e219b8 | ||
|
|
6cb9b7da1f | ||
|
|
83334d67c8 |
+73
-4
@@ -6,11 +6,21 @@
|
|||||||
# The following settings are ONLY set in your .env file.
|
# The following settings are ONLY set in your .env file.
|
||||||
# They are NOT managed by the Admin UI and are not stored in the database.
|
# They are NOT managed by the Admin UI and are not stored in the database.
|
||||||
|
|
||||||
|
# === First Install Token ===
|
||||||
|
# If set the user is required to enter this token on the /first_install page
|
||||||
|
# FIRST_INSTALL_TOKEN="myaccesstoken"
|
||||||
|
|
||||||
|
# === Security ===
|
||||||
|
# When enabled (recommended), auth cookies require HTTPS and SSO will reject insecure HTTP.
|
||||||
|
AUTH_HTTPS_ONLY=true
|
||||||
|
|
||||||
# === Logging and Development ===
|
# === Logging and Development ===
|
||||||
|
|
||||||
DEBUG=False
|
DEBUG=False
|
||||||
DEBUG_DATABASE=False
|
DEBUG_DATABASE=False
|
||||||
BUNDLE_ASSETS=True
|
BUNDLE_ASSETS=True
|
||||||
|
# add `?profiler=true` to the url to enable the profiler for that request
|
||||||
|
PROFILER=False
|
||||||
|
|
||||||
# logging into LNBITS_DATA_FOLDER/logs/
|
# logging into LNBITS_DATA_FOLDER/logs/
|
||||||
ENABLE_LOG_TO_FILE=true
|
ENABLE_LOG_TO_FILE=true
|
||||||
@@ -20,6 +30,10 @@ LOG_ROTATION="100 MB"
|
|||||||
LOG_RETENTION="3 months"
|
LOG_RETENTION="3 months"
|
||||||
# for database cleanup commands
|
# for database cleanup commands
|
||||||
# CLEANUP_WALLETS_DAYS=90
|
# CLEANUP_WALLETS_DAYS=90
|
||||||
|
# Hard limit for total created users. Set to 0 to disable the limit.
|
||||||
|
# LNBITS_MAX_USERS=0
|
||||||
|
# Hard limit for total installed extensions. Set to 0 to disable the limit.
|
||||||
|
# LNBITS_MAX_EXTENSIONS=0
|
||||||
|
|
||||||
# === Admin Settings ===
|
# === Admin Settings ===
|
||||||
|
|
||||||
@@ -55,7 +69,7 @@ LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos"
|
|||||||
# LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx
|
# LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx
|
||||||
|
|
||||||
# which fundingsources are allowed in the admin ui
|
# which fundingsources are allowed in the admin ui
|
||||||
# LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet, NWCWallet, BreezSdkWallet, BoltzWallet, StrikeWallet, CLNRestWallet"
|
# 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
|
# uvicorn variable, allow https behind a proxy
|
||||||
# IMPORTANT: this also needs the webserver to be configured to forward the headers
|
# IMPORTANT: this also needs the webserver to be configured to forward the headers
|
||||||
@@ -90,7 +104,7 @@ AUTH_SECRET_KEY=""
|
|||||||
######################################
|
######################################
|
||||||
|
|
||||||
AUTH_TOKEN_EXPIRE_MINUTES=525600
|
AUTH_TOKEN_EXPIRE_MINUTES=525600
|
||||||
# Possible authorization methods: user-id-only, username-password, nostr-auth-nip98, google-auth, github-auth, keycloak-auth
|
# Possible authorization methods: user-id-only, username-password, nostr-auth-nip98, google-auth, github-auth, keycloak-auth, oidc-auth
|
||||||
AUTH_ALLOWED_METHODS="user-id-only, username-password"
|
AUTH_ALLOWED_METHODS="user-id-only, username-password"
|
||||||
# Set this flag if HTTP is used for OAuth
|
# Set this flag if HTTP is used for OAuth
|
||||||
# OAUTHLIB_INSECURE_TRANSPORT="1"
|
# OAUTHLIB_INSECURE_TRANSPORT="1"
|
||||||
@@ -187,6 +201,15 @@ BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon"
|
|||||||
# HEXSTRING instead of path also possible
|
# HEXSTRING instead of path also possible
|
||||||
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
|
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
|
||||||
|
|
||||||
|
# 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
|
# StrikeWallet
|
||||||
STRIKE_API_ENDPOINT=https://api.strike.me/v1
|
STRIKE_API_ENDPOINT=https://api.strike.me/v1
|
||||||
STRIKE_API_KEY=YOUR_STRIKE_API_KEY
|
STRIKE_API_KEY=YOUR_STRIKE_API_KEY
|
||||||
@@ -258,6 +281,50 @@ KEYCLOAK_DISCOVERY_URL=""
|
|||||||
KEYCLOAK_CLIENT_CUSTOM_ORG=""
|
KEYCLOAK_CLIENT_CUSTOM_ORG=""
|
||||||
KEYCLOAK_CLIENT_CUSTOM_ICON=""
|
KEYCLOAK_CLIENT_CUSTOM_ICON=""
|
||||||
|
|
||||||
|
# OIDC OAuth Config
|
||||||
|
# Generic OIDC provider configuration
|
||||||
|
# Make sure that the redirect URI in your OIDC provider is set to: https://{domain}/api/v1/auth/oidc/token
|
||||||
|
# Required scopes: openid, email, profile
|
||||||
|
# The discovery URL must be accessible from your LNbits server
|
||||||
|
# Always use HTTPS in production environments
|
||||||
|
# The CUSTOM_ORG and CUSTOM_ICON settings allow you to customize the login button
|
||||||
|
# For example: "Login via Zitadel" with the Zitadel logo
|
||||||
|
OIDC_DISCOVERY_URL=""
|
||||||
|
OIDC_CLIENT_ID=""
|
||||||
|
OIDC_CLIENT_SECRET=""
|
||||||
|
OIDC_CLIENT_CUSTOM_ORG=""
|
||||||
|
OIDC_CLIENT_CUSTOM_ICON=""
|
||||||
|
|
||||||
|
# Example OIDC configurations for various providers:
|
||||||
|
#
|
||||||
|
# ZITADEL:
|
||||||
|
# OIDC_DISCOVERY_URL=https://login.yourdomain.de/.well-known/openid-configuration
|
||||||
|
# OIDC_CLIENT_ID=your-zitadel-client-id@project-id
|
||||||
|
# OIDC_CLIENT_SECRET=your-zitadel-client-secret
|
||||||
|
# OIDC_CLIENT_CUSTOM_ORG=Zitadel
|
||||||
|
# OIDC_CLIENT_CUSTOM_ICON=/static/images/zitadel.png
|
||||||
|
#
|
||||||
|
# AUTHENTIK:
|
||||||
|
# OIDC_DISCOVERY_URL=https://authentik.yourdomain.com/application/o/lnbits/.well-known/openid-configuration
|
||||||
|
# OIDC_CLIENT_ID=your-authentik-client-id
|
||||||
|
# OIDC_CLIENT_SECRET=your-authentik-client-secret
|
||||||
|
# OIDC_CLIENT_CUSTOM_ORG=Authentik
|
||||||
|
# OIDC_CLIENT_CUSTOM_ICON=/static/images/authentik.png
|
||||||
|
#
|
||||||
|
# AUTHELIA:
|
||||||
|
# OIDC_DISCOVERY_URL=https://auth.yourdomain.com/.well-known/openid-configuration
|
||||||
|
# OIDC_CLIENT_ID=your-authelia-client-id
|
||||||
|
# OIDC_CLIENT_SECRET=your-authelia-client-secret
|
||||||
|
# OIDC_CLIENT_CUSTOM_ORG=Authelia
|
||||||
|
# OIDC_CLIENT_CUSTOM_ICON=/static/images/authelia.png
|
||||||
|
#
|
||||||
|
# OKTA:
|
||||||
|
# OIDC_DISCOVERY_URL=https://your-domain.okta.com/.well-known/openid-configuration
|
||||||
|
# OIDC_CLIENT_ID=your-okta-client-id
|
||||||
|
# OIDC_CLIENT_SECRET=your-okta-client-secret
|
||||||
|
# OIDC_CLIENT_CUSTOM_ORG=Okta
|
||||||
|
# OIDC_CLIENT_CUSTOM_ICON=/static/images/okta.png
|
||||||
|
|
||||||
|
|
||||||
######################################
|
######################################
|
||||||
|
|
||||||
@@ -313,10 +380,12 @@ LNBITS_SERVICE_FEE=0.0
|
|||||||
# disable fees for internal transactions
|
# disable fees for internal transactions
|
||||||
# LNBITS_SERVICE_FEE_IGNORE_INTERNAL=true
|
# LNBITS_SERVICE_FEE_IGNORE_INTERNAL=true
|
||||||
|
|
||||||
# value in millisats
|
# The minimum fee reserved per payment (millisats)
|
||||||
LNBITS_RESERVE_FEE_MIN=2000
|
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
|
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
|
# limit the maximum balance for each wallet
|
||||||
# throw an error if the wallet attempts to create a new invoice
|
# throw an error if the wallet attempts to create a new invoice
|
||||||
|
|||||||
@@ -69,7 +69,10 @@ jobs:
|
|||||||
--onefile \
|
--onefile \
|
||||||
--name lnbits \
|
--name lnbits \
|
||||||
--hidden-import=embit \
|
--hidden-import=embit \
|
||||||
|
--hidden-import=bitstring.bitstore_bitarray \
|
||||||
--collect-all embit \
|
--collect-all embit \
|
||||||
|
--collect-all bitstring \
|
||||||
|
--collect-all bitarray \
|
||||||
--collect-all lnbits \
|
--collect-all lnbits \
|
||||||
--collect-all sqlalchemy \
|
--collect-all sqlalchemy \
|
||||||
--collect-all breez_sdk \
|
--collect-all breez_sdk \
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: bundle
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bundle:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.head_ref }}
|
||||||
|
- uses: lnbits/lnbits/.github/actions/prepare@dev
|
||||||
|
with:
|
||||||
|
python-version: "3.10"
|
||||||
|
node-version: "24.x"
|
||||||
|
npm: true
|
||||||
|
- run: make bundle
|
||||||
|
- name: Commit and push bundle changes
|
||||||
|
run: |
|
||||||
|
git config user.name "alan"
|
||||||
|
git config user.email "alan@lnbits.com"
|
||||||
|
git add lnbits/static
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git commit -m "chore: make bundle [skip ci]"
|
||||||
|
git push
|
||||||
+10
-11
@@ -1,14 +1,9 @@
|
|||||||
name: LNbits CI
|
name: LNbits CI
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- dev
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
uses: ./.github/workflows/lint.yml
|
uses: ./.github/workflows/lint.yml
|
||||||
|
|
||||||
@@ -16,7 +11,7 @@ jobs:
|
|||||||
needs: [ lint ]
|
needs: [ lint ]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10", "3.11", "3.12"]
|
python-version: ["3.10", "3.12"]
|
||||||
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||||
uses: ./.github/workflows/tests.yml
|
uses: ./.github/workflows/tests.yml
|
||||||
with:
|
with:
|
||||||
@@ -30,7 +25,7 @@ jobs:
|
|||||||
needs: [ lint ]
|
needs: [ lint ]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10", "3.11", "3.12"]
|
python-version: ["3.10", "3.12"]
|
||||||
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||||
uses: ./.github/workflows/tests.yml
|
uses: ./.github/workflows/tests.yml
|
||||||
with:
|
with:
|
||||||
@@ -44,7 +39,7 @@ jobs:
|
|||||||
needs: [ lint ]
|
needs: [ lint ]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10", "3.11", "3.12"]
|
python-version: ["3.10", "3.12"]
|
||||||
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
|
||||||
uses: ./.github/workflows/tests.yml
|
uses: ./.github/workflows/tests.yml
|
||||||
with:
|
with:
|
||||||
@@ -58,7 +53,7 @@ jobs:
|
|||||||
needs: [ lint ]
|
needs: [ lint ]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10", "3.11", "3.12"]
|
python-version: ["3.10", "3.12"]
|
||||||
uses: ./.github/workflows/migration.yml
|
uses: ./.github/workflows/migration.yml
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
@@ -74,7 +69,7 @@ jobs:
|
|||||||
uses: ./.github/workflows/regtest.yml
|
uses: ./.github/workflows/regtest.yml
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10"]
|
python-version: ["3.12"]
|
||||||
backend-wallet-class:
|
backend-wallet-class:
|
||||||
- BoltzWallet
|
- BoltzWallet
|
||||||
- LndRestWallet
|
- LndRestWallet
|
||||||
@@ -94,7 +89,11 @@ jobs:
|
|||||||
needs: [ lint ]
|
needs: [ lint ]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10"]
|
python-version: ["3.12"]
|
||||||
uses: ./.github/workflows/jmeter.yml
|
uses: ./.github/workflows/jmeter.yml
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
bundle:
|
||||||
|
needs: [ lint, test-api, test-wallets, test-unit, migration, openapi, regtest, jmeter ]
|
||||||
|
uses: ./.github/workflows/bundle.yml
|
||||||
|
|||||||
@@ -55,11 +55,21 @@ jobs:
|
|||||||
- name: Build and push boltz
|
- name: Build and push boltz
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: docker/boltzclient
|
||||||
file: Dockerfile.boltz
|
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits-boltz:${{ inputs.tag }}
|
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits-boltz:${{ inputs.tag }}
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||||
build-args: LNBITS_TAG=${{ inputs.tag }}
|
build-args: LNBITS_TAG=${{ inputs.tag }}
|
||||||
|
|
||||||
|
- name: Build and push sparkl2
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: docker/sparkl2
|
||||||
|
push: true
|
||||||
|
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits-sparkl2:${{ inputs.tag }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||||
|
build-args: LNBITS_TAG=${{ inputs.tag }}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ jobs:
|
|||||||
- name: run LNbits
|
- name: run LNbits
|
||||||
env:
|
env:
|
||||||
LNBITS_ADMIN_UI: true
|
LNBITS_ADMIN_UI: true
|
||||||
|
AUTH_HTTPS_ONLY: false
|
||||||
LNBITS_EXTENSIONS_DEFAULT_INSTALL: "watchonly, satspay, tipjar, tpos, lnurlp, withdraw"
|
LNBITS_EXTENSIONS_DEFAULT_INSTALL: "watchonly, satspay, tipjar, tpos, lnurlp, withdraw"
|
||||||
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
|
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -6,53 +6,30 @@ jobs:
|
|||||||
|
|
||||||
black:
|
black:
|
||||||
uses: ./.github/workflows/make.yml
|
uses: ./.github/workflows/make.yml
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: ["3.10", "3.11", "3.12"]
|
|
||||||
with:
|
with:
|
||||||
make: checkblack
|
make: checkblack
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
ruff:
|
ruff:
|
||||||
uses: ./.github/workflows/make.yml
|
uses: ./.github/workflows/make.yml
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: ["3.10", "3.11", "3.12"]
|
|
||||||
with:
|
with:
|
||||||
make: checkruff
|
make: checkruff
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
mypy:
|
mypy:
|
||||||
uses: ./.github/workflows/make.yml
|
uses: ./.github/workflows/make.yml
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: ["3.10", "3.11", "3.12"]
|
|
||||||
with:
|
with:
|
||||||
make: mypy
|
make: mypy
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
pyright:
|
pyright:
|
||||||
uses: ./.github/workflows/make.yml
|
uses: ./.github/workflows/make.yml
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: ["3.10", "3.11", "3.12"]
|
|
||||||
with:
|
with:
|
||||||
make: pyright
|
make: pyright
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
npm: true
|
npm: true
|
||||||
|
|
||||||
|
|
||||||
prettier:
|
prettier:
|
||||||
uses: ./.github/workflows/make.yml
|
uses: ./.github/workflows/make.yml
|
||||||
with:
|
with:
|
||||||
make: checkprettier
|
make: checkprettier
|
||||||
npm: true
|
npm: true
|
||||||
|
|
||||||
bundle:
|
|
||||||
uses: ./.github/workflows/make.yml
|
|
||||||
with:
|
|
||||||
make: checkbundle
|
|
||||||
npm: true
|
|
||||||
|
|
||||||
poetry:
|
poetry:
|
||||||
uses: ./.github/workflows/poetry.yml
|
uses: ./.github/workflows/poetry.yml
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ on:
|
|||||||
python-version:
|
python-version:
|
||||||
description: "python version"
|
description: "python version"
|
||||||
type: string
|
type: string
|
||||||
default: "3.10"
|
default: "3.12"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
make:
|
make:
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os-version: ["ubuntu-24.04"]
|
os-version: ["ubuntu-24.04"]
|
||||||
node-version: ["18.x"]
|
node-version: ["24.x"]
|
||||||
runs-on: ${{ matrix.os-version }}
|
runs-on: ${{ matrix.os-version }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
python-version:
|
python-version:
|
||||||
default: "3.10"
|
default: "3.12"
|
||||||
type: string
|
type: string
|
||||||
os-version:
|
os-version:
|
||||||
default: "ubuntu-24.04"
|
default: "ubuntu-24.04"
|
||||||
@@ -38,8 +38,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup Regtest
|
- name: Setup Regtest
|
||||||
run: |
|
run: |
|
||||||
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
|
cd docker/regtest
|
||||||
cd docker
|
|
||||||
chmod +x ./start-regtest
|
chmod +x ./start-regtest
|
||||||
./start-regtest
|
./start-regtest
|
||||||
sudo chmod -R a+rwx .
|
sudo chmod -R a+rwx .
|
||||||
@@ -50,16 +49,16 @@ jobs:
|
|||||||
LNBITS_DATABASE_URL: ${{ inputs.db-url }}
|
LNBITS_DATABASE_URL: ${{ inputs.db-url }}
|
||||||
LNBITS_BACKEND_WALLET_CLASS: ${{ inputs.backend-wallet-class }}
|
LNBITS_BACKEND_WALLET_CLASS: ${{ inputs.backend-wallet-class }}
|
||||||
LND_REST_ENDPOINT: https://localhost:8081/
|
LND_REST_ENDPOINT: https://localhost:8081/
|
||||||
LND_REST_CERT: ./docker/data/lnd-3/tls.cert
|
LND_REST_CERT: ./docker/regtest/data/lnd-3/tls.cert
|
||||||
LND_REST_MACAROON: ./docker/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
|
LND_REST_MACAROON: ./docker/regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
|
||||||
LND_GRPC_ENDPOINT: localhost
|
LND_GRPC_ENDPOINT: localhost
|
||||||
LND_GRPC_PORT: 10009
|
LND_GRPC_PORT: 10009
|
||||||
LND_GRPC_CERT: docker/data/lnd-3/tls.cert
|
LND_GRPC_CERT: ./docker/regtest/data/lnd-3/tls.cert
|
||||||
LND_GRPC_MACAROON: docker/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
|
LND_GRPC_MACAROON: ./docker/regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
|
||||||
CORELIGHTNING_RPC: ./docker/data/clightning-1/regtest/lightning-rpc
|
CORELIGHTNING_RPC: ./docker/regtest/data/clightning-1/regtest/lightning-rpc
|
||||||
CORELIGHTNING_REST_URL: https://localhost:3001
|
CORELIGHTNING_REST_URL: https://localhost:3001
|
||||||
CORELIGHTNING_REST_MACAROON: ./docker/data/clightning-2-rest/access.macaroon
|
CORELIGHTNING_REST_MACAROON: ./docker/regtest/data/clightning-2-rest/access.macaroon
|
||||||
CORELIGHTNING_REST_CERT: ./docker/data/clightning-2-rest/certificate.pem
|
CORELIGHTNING_REST_CERT: ./docker/regtest/data/clightning-2-rest/certificate.pem
|
||||||
LNBITS_ENDPOINT: http://localhost:5001
|
LNBITS_ENDPOINT: http://localhost:5001
|
||||||
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
|
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
|
||||||
ECLAIR_URL: http://127.0.0.1:8082
|
ECLAIR_URL: http://127.0.0.1:8082
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ jobs:
|
|||||||
|
|
||||||
docker:
|
docker:
|
||||||
if: github.repository == 'lnbits/lnbits'
|
if: github.repository == 'lnbits/lnbits'
|
||||||
|
needs: [ release ]
|
||||||
uses: ./.github/workflows/docker.yml
|
uses: ./.github/workflows/docker.yml
|
||||||
with:
|
with:
|
||||||
tag: ${{ github.ref_name }}
|
tag: ${{ github.ref_name }}
|
||||||
@@ -40,19 +41,33 @@ jobs:
|
|||||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
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:
|
pypi:
|
||||||
if: github.repository == 'lnbits/lnbits'
|
if: github.repository == 'lnbits/lnbits'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Install dependencies for building secp256k1
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y build-essential automake libtool libffi-dev libgmp-dev
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Build and publish to pypi
|
- name: Set up Python 3.10
|
||||||
uses: JRubics/poetry-publish@v1.15
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
python-version: "3.10"
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
- name: Build the project
|
||||||
|
run: uv build
|
||||||
|
- name: Publish to pypi
|
||||||
|
env:
|
||||||
|
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_KEY }}
|
||||||
|
run: uv publish
|
||||||
|
|
||||||
appimage:
|
appimage:
|
||||||
needs: [ release ]
|
needs: [ release ]
|
||||||
|
|||||||
@@ -52,19 +52,33 @@ jobs:
|
|||||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
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:
|
pypi:
|
||||||
if: github.repository == 'lnbits/lnbits'
|
if: github.repository == 'lnbits/lnbits'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Install dependencies for building secp256k1
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y build-essential automake libtool libffi-dev libgmp-dev
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Build and publish to pypi
|
- name: Set up Python 3.10
|
||||||
uses: JRubics/poetry-publish@v1.15
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
python-version: "3.10"
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
- name: Build the project
|
||||||
|
run: uv build
|
||||||
|
- name: Publish to pypi
|
||||||
|
env:
|
||||||
|
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_KEY }}
|
||||||
|
run: uv publish
|
||||||
|
|
||||||
appimage:
|
appimage:
|
||||||
needs: [ release ]
|
needs: [ release ]
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
python-version:
|
python-version:
|
||||||
default: "3.10"
|
default: "3.12"
|
||||||
type: string
|
type: string
|
||||||
os-version:
|
os-version:
|
||||||
default: "ubuntu-24.04"
|
default: "ubuntu-24.04"
|
||||||
|
|||||||
+1
-1
@@ -6,6 +6,7 @@ __pycache__
|
|||||||
*$py.class
|
*$py.class
|
||||||
.mypy_cache
|
.mypy_cache
|
||||||
.vscode
|
.vscode
|
||||||
|
.codex
|
||||||
*-lock.json
|
*-lock.json
|
||||||
.python-version
|
.python-version
|
||||||
|
|
||||||
@@ -42,7 +43,6 @@ lnbits/static/bundle.min.js.old
|
|||||||
lnbits/static/bundle.min.css.old
|
lnbits/static/bundle.min.css.old
|
||||||
lnbits/static/bundle-components.min.js.old
|
lnbits/static/bundle-components.min.js.old
|
||||||
lnbits/upgrades
|
lnbits/upgrades
|
||||||
docker
|
|
||||||
|
|
||||||
# Nix
|
# Nix
|
||||||
*result*
|
*result*
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ repos:
|
|||||||
- id: mixed-line-ending
|
- id: mixed-line-ending
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 25.1.0
|
rev: 26.3.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.12.10
|
rev: v0.14.10
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: [ --fix, --exit-non-zero-on-fix ]
|
args: [ --fix, --exit-non-zero-on-fix ]
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Feature Spec: [FEAT-XXX] - Short Descriptive Title
|
||||||
|
|
||||||
|
**Milestone:** [e.g. MVP Core / Performance & Polish / Extension Framework]
|
||||||
|
**Priority:** Must-have / Should-have / Nice-to-have
|
||||||
|
**Spec Owner:** [Your Name / AI Agent Name]
|
||||||
|
**Status:** Draft → Under Review → Approved → Implemented → Verified
|
||||||
|
|
||||||
|
## 1. Purpose & User Story
|
||||||
|
|
||||||
|
As a [user type], I want [goal] so that [benefit].
|
||||||
|
|
||||||
|
_(One clear sentence. Keep it concise.)_
|
||||||
|
|
||||||
|
## 2. Functional Requirements
|
||||||
|
|
||||||
|
- [ ] REQ-1: [Clear, testable description]
|
||||||
|
- [ ] REQ-2: ...
|
||||||
|
- [ ] REQ-3: ...
|
||||||
|
|
||||||
|
_(List what the feature must do. Make each item verifiable.)_
|
||||||
|
|
||||||
|
## 3. Non-Functional Requirements
|
||||||
|
|
||||||
|
- **Performance:** [e.g. Latency < 800ms at p95, max 5k tokens, etc.]
|
||||||
|
- **Security / Safety:** [e.g. Input validation, no raw errors to user, etc.]
|
||||||
|
- **Compatibility:** [e.g. Works with all existing wallet backends, no breaking changes for extensions]
|
||||||
|
- **UI/UX:** [e.g. Follows existing Quasar/Vue patterns in wallet.js]
|
||||||
|
- **Other:** [cost, scalability, accessibility, etc.]
|
||||||
|
|
||||||
|
## 4. Technical Approach (Optional – recommended for complex features)
|
||||||
|
|
||||||
|
- Proposed solution: [e.g. Extend existing CRUD in lnbits/core/, new extension, middleware change, etc.]
|
||||||
|
- Key files to modify: [list expected files]
|
||||||
|
- New dependencies: [none / specific package + version]
|
||||||
|
- Migration / Database changes: [yes/no + description]
|
||||||
|
|
||||||
|
## 5. Success Criteria & Verification
|
||||||
|
|
||||||
|
**Must pass all of these to be accepted:**
|
||||||
|
|
||||||
|
- [ ] All functional requirements (REQ-\*) implemented and tested
|
||||||
|
- [ ] Non-functional requirements met (performance, security, etc.)
|
||||||
|
- [ ] Relevant tests pass: `make test-unit`, `make test-api`, `make test-regtest` (as applicable)
|
||||||
|
- [ ] `make check` passes (ruff, mypy, pyright, prettier, checkbundle)
|
||||||
|
- [ ] Constitution compliance: All changes respect CONSTITUTION.md
|
||||||
|
- [ ] Backward compatibility: No breakage for existing extensions or wallet backends
|
||||||
|
- [ ] Documentation updated (if applicable: README, OpenAPI, inline comments)
|
||||||
|
|
||||||
|
**Additional Tests / Edge Cases:**
|
||||||
|
|
||||||
|
- [ ] Test invalid inputs / error paths
|
||||||
|
- [ ] Test with FakeWallet and at least one real backend
|
||||||
|
- [ ] Test with multiple extensions installed
|
||||||
|
|
||||||
|
## 6. Safety & Risk Assessment
|
||||||
|
|
||||||
|
- Potential risks: [e.g. Payment flow impact, key exposure, extension conflicts]
|
||||||
|
- Mitigation: [how addressed]
|
||||||
|
- Security review needed: [yes/no]
|
||||||
|
|
||||||
|
## 7. Implementation Notes (for AI / Developer)
|
||||||
|
|
||||||
|
- Style to match: Existing code patterns in `lnbits/core/` and `wallet.js`
|
||||||
|
- Surgical changes only (per AGENTS.md)
|
||||||
|
- Any known gotchas or dependencies on other features:
|
||||||
|
|
||||||
|
## 8. Acceptance Checklist (Sign-off)
|
||||||
|
|
||||||
|
- [ ] Spec reviewed and approved by project owner
|
||||||
|
- [ ] Implementation completed
|
||||||
|
- [ ] Verification steps passed
|
||||||
|
- [ ] PR created with link to this spec
|
||||||
|
- [ ] Constitution & AGENTS.md compliance confirmed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Created:** [Date]
|
||||||
|
**Last Updated:** [Date]
|
||||||
|
**Approved By:** [Name / "Approved"]
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# AGENTS.md - Instructions for All AI Coding Agents
|
||||||
|
|
||||||
|
This file is the **master instruction manual** for any AI agent (Grok, Claude, Cursor, Aider, etc.) working on LNbits.
|
||||||
|
|
||||||
|
## 1. Core Rule (Never Break This)
|
||||||
|
|
||||||
|
**You MUST read and strictly follow `CONSTITUTION.md` before doing any planning, coding, refactoring, or suggesting changes.**
|
||||||
|
|
||||||
|
- Every single change, feature, extension, or fix **must comply** with the Constitution.
|
||||||
|
- If you detect a violation (in new code or existing code), you **must** flag it immediately and propose a fix or ask for clarification.
|
||||||
|
- Constitution > any other instruction (including this file, user prompts, or previous conversations).
|
||||||
|
|
||||||
|
## 2. Mandatory Development Workflow
|
||||||
|
|
||||||
|
For **any non-trivial task** (new feature, bug fix, refactor, extension change):
|
||||||
|
|
||||||
|
1. **Constitution Check** – Re-read relevant sections of `CONSTITUTION.md`
|
||||||
|
2. **Feature Spec Check** – If a spec exists in `.specify/`, follow it exactly. If none exists, ask the user for clarification or propose a minimal spec.
|
||||||
|
3. **Think Step-by-Step** – Follow the "Think Before Coding" and "Simplicity First" guidelines below.
|
||||||
|
4. **Surgical Changes** – Only touch what is necessary.
|
||||||
|
5. **Implement**
|
||||||
|
6. **Verify** – Run relevant tests (`make test-unit`, `make test-api`, etc.), `make check`, and confirm compliance.
|
||||||
|
7. **Report** – Always include a clear summary.
|
||||||
|
|
||||||
|
Use the following response format:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Constitution & Spec Compliance
|
||||||
|
|
||||||
|
- Relevant Constitution sections checked: [list or quote key rules]
|
||||||
|
- Feature Spec followed: [yes / no / proposed]
|
||||||
|
|
||||||
|
## Assumptions & Plan
|
||||||
|
|
||||||
|
- Assumptions: ...
|
||||||
|
- Plan:
|
||||||
|
1. ...
|
||||||
|
2. ...
|
||||||
|
- Tradeoffs considered: ...
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
- Files changed: ...
|
||||||
|
- Summary of modifications:
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- [ ] Passes `make check`
|
||||||
|
- [ ] Relevant tests pass (`make test-xxx`)
|
||||||
|
- [ ] Complies with Constitution
|
||||||
|
- [ ] Surgical & minimal (no unrelated changes)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Behavioral Guidelines (Merged & Project-Specific)
|
||||||
|
|
||||||
|
**Think Before Coding**
|
||||||
|
|
||||||
|
- Don't assume. Don't hide confusion. Surface tradeoffs.
|
||||||
|
- State assumptions explicitly. If uncertain, ask.
|
||||||
|
- If multiple interpretations exist, present them — don't pick silently.
|
||||||
|
- If something is unclear (especially regarding wallets, extensions, or funding sources), stop and ask.
|
||||||
|
|
||||||
|
**Simplicity First**
|
||||||
|
|
||||||
|
- Minimum code that solves the problem. Nothing speculative.
|
||||||
|
- No features beyond what was asked.
|
||||||
|
- No abstractions for single-use code.
|
||||||
|
- Respect LNbits' lean core philosophy: new functionality should preferably go into an **extension** unless it truly belongs in core.
|
||||||
|
|
||||||
|
**Surgical Changes**
|
||||||
|
|
||||||
|
- Touch only what you must. Clean up only your own mess.
|
||||||
|
- Match existing style (Python: Black + Ruff rules; JS: Prettier).
|
||||||
|
- Do not "improve" or refactor adjacent code unless explicitly asked.
|
||||||
|
- When editing, remove only imports/variables/functions made unused **by your changes**.
|
||||||
|
- Never delete pre-existing dead code unless instructed.
|
||||||
|
|
||||||
|
**Goal-Driven Execution**
|
||||||
|
|
||||||
|
- Transform tasks into verifiable goals.
|
||||||
|
- For tests: Write or update tests first when fixing bugs or adding behavior.
|
||||||
|
- Always consider impact on existing extensions and multiple wallet backends (LND, CLN, Boltz, VoidWallet, etc.).
|
||||||
|
|
||||||
|
## 4. LNbits-Specific Rules
|
||||||
|
|
||||||
|
- **Extensions First**: Core should remain lean. Prefer implementing new features as extensions unless they are fundamental to wallets, security, or the API.
|
||||||
|
- **Testing**: Use `FakeWallet` for unit/API tests. Regtest tests for full Lightning flows. Never break existing test targets in the Makefile.
|
||||||
|
- **Dependencies**: Never add new dependencies without updating `pyproject.toml` and getting approval.
|
||||||
|
- **Frontend**: JS/Vue code (e.g. `wallet.js`) must follow existing patterns and pass `make checkbundle` when static files are affected.
|
||||||
|
- **Database / Migrations**: Do not make raw SQL changes. Use existing CRUD/services and migration tooling.
|
||||||
|
- **Security**: Be extremely cautious with anything touching payments, keys, LNURL, Bolt11, or admin routes.
|
||||||
|
- **Tools**: Use `uv run` for all commands. Prefer Makefile targets (`make format`, `make check`, `make test-xxx`).
|
||||||
|
- **Generated Files**: Never modify gRPC files or other generated code.
|
||||||
|
|
||||||
|
## 5. Forbidden Behaviors
|
||||||
|
|
||||||
|
- Ignoring Constitution rules to "be helpful"
|
||||||
|
- Large refactors without a spec or explicit request
|
||||||
|
- Adding features "for future use"
|
||||||
|
- Breaking backward compatibility for extensions or existing wallet backends
|
||||||
|
- Committing code that fails `make check` or relevant tests
|
||||||
|
- Exposing raw errors/stack traces to users
|
||||||
|
- Using synchronous code in hot async paths without justification
|
||||||
|
|
||||||
|
## 6. How to Handle This File + Constitution
|
||||||
|
|
||||||
|
When the user gives you a task, start your response with:
|
||||||
|
|
||||||
|
> Following LNbits CONSTITUTION.md and AGENTS.md...
|
||||||
|
|
||||||
|
Then proceed with the structured format above.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**These guidelines are working if:**
|
||||||
|
|
||||||
|
- Fewer unnecessary changes appear in diffs
|
||||||
|
- Clarifying questions come **before** implementation
|
||||||
|
- All changes respect the lean, extension-first, security-first nature of LNbits
|
||||||
|
- Tests and `make check` continue to pass
|
||||||
|
|
||||||
|
Last Updated: April 2026
|
||||||
+136
@@ -0,0 +1,136 @@
|
|||||||
|
# CONSTITUTION.md
|
||||||
|
|
||||||
|
This is the immutable constitution of the LNbits project.
|
||||||
|
Every feature spec, code change, refactor, extension, or decision by humans or AI agents **must comply** with this document.
|
||||||
|
Changes to this file require explicit approval from the project owner/maintainers.
|
||||||
|
|
||||||
|
## 1. Project Overview
|
||||||
|
|
||||||
|
**Project Name:** LNbits
|
||||||
|
**Core Purpose:** Free and open-source Lightning wallet and accounts system. A lightweight Python server that sits on top of any Lightning funding source, providing safe isolated wallets, a clean REST API, and a powerful extension system for adding features rapidly.
|
||||||
|
**Target Users:** Individuals, communities, merchants, developers, and enterprises building on Bitcoin/Lightning (self-hosted or as part of larger stacks).
|
||||||
|
**High-Level Success Criteria:**
|
||||||
|
|
||||||
|
- Reliable multi-wallet Lightning accounting with any backend (20+ supported funding sources)
|
||||||
|
- Secure, extensible via 60+ extensions without bloating core
|
||||||
|
- High code quality, test coverage, and backward compatibility for extensions
|
||||||
|
- Production-ready performance and security for real Bitcoin value
|
||||||
|
|
||||||
|
**Version:** 1.5.4
|
||||||
|
|
||||||
|
## 2. Technology Stack (Strict)
|
||||||
|
|
||||||
|
- **Language:** Python >=3.10, <3.13 (strictly enforced via `pyproject.toml`)
|
||||||
|
- **Framework:** FastAPI + Starlette (backend API)
|
||||||
|
- **Frontend:** Vue.js + Quasar framework, with bundled static assets
|
||||||
|
- **Database:** SQLite (aiosqlite) by default, PostgreSQL (asyncpg/psycopg2) supported via `LNBITS_DATABASE_URL`
|
||||||
|
- **Async Runtime:** uvloop preferred
|
||||||
|
- **Dependency Management:** uv + pyproject.toml (Hatchling build backend). Use `uv run` for all commands.
|
||||||
|
- **Wallet Backends:** Abstracted via `lnbits.wallets` – support for LND, Core Lightning, Phoenixd, Boltz, Breez SDK, Liquid, VoidWallet fallback, etc. New backends must follow existing abstraction.
|
||||||
|
- **Other Key Libs:** SQLAlchemy, Pydantic (v1), Loguru, Jinja2, LNURL, Bolt11, etc. (see `pyproject.toml` for pinned versions)
|
||||||
|
- **Build/Frontend Tools:** npm for bundling (Quasar/Vue), Prettier for JS/CSS (check Makefile targets)
|
||||||
|
|
||||||
|
**Forbidden:**
|
||||||
|
|
||||||
|
- Adding new top-level dependencies without updating `pyproject.toml` **and** team approval
|
||||||
|
- Using synchronous blocking calls in async paths (except where explicitly justified)
|
||||||
|
- Direct database queries outside of CRUD layers or core services
|
||||||
|
- Modifying generated files (e.g., gRPC files in wallets/boltz_grpc_files or lnd_grpc_files)
|
||||||
|
|
||||||
|
## 3. Architecture & Code Organization (Mandatory Rules)
|
||||||
|
|
||||||
|
- **Core Principle:** Modular monolith with heavy emphasis on **extensions**. All non-core features must live in extensions (installed via `lnbits/extensions`). Core stays lean.
|
||||||
|
- **Backend Structure:**
|
||||||
|
- `lnbits/core/` for core models, CRUD, services, routers, tasks
|
||||||
|
- `lnbits/wallets/` for funding source abstractions
|
||||||
|
- `lnbits/extensions/` for installed/upgradeable extensions (do not commit large extensions to core repo)
|
||||||
|
- `lnbits/static/` for bundled frontend assets (managed via npm bundle)
|
||||||
|
- **Key Rules:**
|
||||||
|
- Use dependency injection and FastAPI routers properly
|
||||||
|
- Extensions register routes/tasks via `register_ext_routes` / `register_ext_tasks`
|
||||||
|
- Database migrations handled centrally (extension-specific migrations)
|
||||||
|
- All new endpoints must be under proper versioning/prefixing where applicable
|
||||||
|
- Frontend: Vue 2/Quasar components in `wallet.js` style (or updated) – keep reactive, use LNbits.utils helpers
|
||||||
|
- No circular imports; respect existing middleware order (e.g., InstalledExtensionMiddleware before ExtensionsRedirectMiddleware)
|
||||||
|
|
||||||
|
**Exclusions** (do not lint/format these):
|
||||||
|
|
||||||
|
- `lnbits/extensions/`, `lnbits/upgrades/`, generated gRPC files, static/vendor bundles
|
||||||
|
|
||||||
|
## 4. Code Quality & Style
|
||||||
|
|
||||||
|
- **Formatting & Linting:**
|
||||||
|
- Python: Black (line-length 88), Ruff (with selected rules: F, E, W, I, A, C, N, UP, RUF, B, S), MyPy (strict where possible), Pyright
|
||||||
|
- JS/Frontend: Prettier
|
||||||
|
- Run via Makefile: `make format` and `make check`
|
||||||
|
- **Type Checking:** MyPy + Pyright enforced on `lnbits/`, `tests/`, `tools/`
|
||||||
|
- **Testing Requirements:**
|
||||||
|
- Unit, API, wallet, and regtest tests via pytest (see Makefile targets)
|
||||||
|
- New core code or critical paths: high coverage expected (`--cov=lnbits`)
|
||||||
|
- Extensions should include their own tests where possible
|
||||||
|
- **Error Handling & Logging:** Use Loguru with structured context. Never expose raw stack traces to end users. Graceful fallbacks (e.g., VoidWallet on funding source failure).
|
||||||
|
- **Pre-commit:** Strongly recommended (`make install-pre-commit-hook`)
|
||||||
|
- **Bundle Integrity:** Frontend bundles must pass `make checkbundle` before commits affecting static files.
|
||||||
|
|
||||||
|
## 5. AI / LLM Usage Standards (if any agents or future AI features are added)
|
||||||
|
|
||||||
|
- Any new AI-powered features (e.g., via extensions) must use structured outputs (Pydantic/JSON mode)
|
||||||
|
- Store prompts/templates versioned in the extension
|
||||||
|
- Prefer deterministic behavior for financial/security paths (low temperature)
|
||||||
|
- All AI outputs involving value/money must be validated server-side
|
||||||
|
- Safety: Never allow untrusted model output to influence payments, wallet balances, or admin actions without guardrails
|
||||||
|
|
||||||
|
## 6. Safety, Security & Ethics
|
||||||
|
|
||||||
|
- **Critical:** Handle real Bitcoin/Lightning value → security-first mindset
|
||||||
|
- Per-wallet isolation with separate admin/invoice/read keys
|
||||||
|
- Rate limiting (SlowAPI) and IP blocking middleware mandatory
|
||||||
|
- Sanitize all user inputs; validate LNURL, Bolt11, etc.
|
||||||
|
- PII: Minimal collection; respect privacy (no unnecessary logging of sensitive data)
|
||||||
|
- Funding source failures: Graceful degradation to VoidWallet + clear logging
|
||||||
|
- Extensions: Hash-verified installs for vetted extensions; careful with custom extension paths
|
||||||
|
- Audit logging via AuditMiddleware
|
||||||
|
- Forbidden: Hard-coded secrets, insecure subprocess calls without review, SQL injection risks (use SQLAlchemy properly)
|
||||||
|
|
||||||
|
## 7. Performance & Cost Budgets
|
||||||
|
|
||||||
|
- Keep core lightweight – extensions handle heavy features
|
||||||
|
- Async-first (uvloop, asyncpg/aiosqlite)
|
||||||
|
- Reasonable retry logic for funding source connections (see `check_funding_source`)
|
||||||
|
- Frontend: Optimized bundles (checkbundle enforced)
|
||||||
|
- No unnecessary blocking operations in request paths
|
||||||
|
|
||||||
|
## 8. Development Workflow (Spec-Driven where possible)
|
||||||
|
|
||||||
|
- **All significant changes** should follow Spec-Driven Development:
|
||||||
|
- Create/update Feature Spec in `.specify/` folder (or equivalent)
|
||||||
|
- Reference this Constitution in every spec and PR
|
||||||
|
- Use Makefile targets for format/check/test
|
||||||
|
- Tests run with `FakeWallet` by default for unit/API; regtest for full flows
|
||||||
|
- PRs must:
|
||||||
|
- Pass `make check` and relevant tests
|
||||||
|
- Include Constitution compliance notes (via AGENTS.md/CLAUDE.md)
|
||||||
|
- Not break existing extensions or wallet backends
|
||||||
|
- Branching: Protect main; use feature branches
|
||||||
|
- Extensions: Develop separately; core repo focuses on framework stability
|
||||||
|
|
||||||
|
## 9. Decision Hierarchy (What Takes Precedence)
|
||||||
|
|
||||||
|
1. This Constitution
|
||||||
|
2. Approved Feature Spec / Milestone
|
||||||
|
3. Existing tests and backward compatibility (especially for extensions and wallet backends)
|
||||||
|
4. Project maintainers / owner decision
|
||||||
|
5. Everything else (including helpful AI suggestions)
|
||||||
|
|
||||||
|
If conflict: Stop, document the issue, and seek clarification from maintainers.
|
||||||
|
|
||||||
|
## 10. Amendment Process
|
||||||
|
|
||||||
|
- This Constitution can only be changed with explicit approval from project maintainers.
|
||||||
|
- All changes must be dated, versioned, and reflected in `AGENTS.md`.
|
||||||
|
- Minor clarifications can be proposed via PR with justification.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated:** April 2026 (based on v1.5.4)
|
||||||
|
**Owner/Maintainers Approval:** LNbits Team
|
||||||
+1
-1
@@ -43,4 +43,4 @@ ENV LNBITS_HOST="0.0.0.0"
|
|||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
||||||
CMD ["sh", "-c", "uv run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
|
CMD ["sh", "-c", "uv --offline run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ test-regtest:
|
|||||||
LNBITS_DATA_FOLDER="./tests/data" \
|
LNBITS_DATA_FOLDER="./tests/data" \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
DEBUG=true \
|
DEBUG=true \
|
||||||
|
rm -rf ./tests/data \
|
||||||
uv run pytest tests/regtest
|
uv run pytest tests/regtest
|
||||||
|
|
||||||
test-migration:
|
test-migration:
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
<source media="(prefers-color-scheme: dark)" srcset="docs/logos/lnbits-full-inverse.svg">
|
||||||
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:300px">
|
<img src="docs/logos/lnbits-full.svg" alt="LNbits" style="width:300px">
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
 [![license-badge]](LICENSE) [![docs-badge]][docs]  [](https://extensions.lnbits.com/) [](https://shop.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits) [<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
 [![license-badge]](LICENSE) [![docs-badge]][docs]  [](https://extensions.lnbits.com/) [](https://shop.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||||
<img width="2000" height="203" alt="lnbits_head" src="https://github.com/user-attachments/assets/77669718-ac10-43c7-ae95-6ce236c77401" />
|
<img alt="lnbits_head" src="docs/assets/header.jpg" />
|
||||||
[](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
|
[](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
|
||||||
|
|
||||||
# LNbits — The most powerful Bitcoin & Lightning toolkit
|
# LNbits — The most powerful Bitcoin & Lightning toolkit
|
||||||
@@ -46,7 +46,7 @@ Get yourself familiar and test on our demo server [demo.lnbits.com](https://demo
|
|||||||
|
|
||||||
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending, export to csv. Each wallet also comes with its own API keys, to help partition the exposure of your funding source.
|
LNbits is packaged with tools to help manage funds, such as a table of transactions, line chart of spending, export to csv. Each wallet also comes with its own API keys, to help partition the exposure of your funding source.
|
||||||
|
|
||||||
<img src="https://i.imgur.com/w8jdGpF.png" style="width:800px">
|
<img alt="lnbits_wallet" src="docs/assets/wallet.jpg" />
|
||||||
|
|
||||||
## LNbits extension universe
|
## LNbits extension universe
|
||||||
|
|
||||||
@@ -54,25 +54,25 @@ Extend YOUR LNbits to meet YOUR needs.
|
|||||||
|
|
||||||
All non-core features are installed as extensions, reducing your code base and making your LNbits unique to you. Extend your LNbits install in any direction, and even create and share your own extensions.
|
All non-core features are installed as extensions, reducing your code base and making your LNbits unique to you. Extend your LNbits install in any direction, and even create and share your own extensions.
|
||||||
|
|
||||||
<img src="https://i.imgur.com/aEBpwJF.png" style="width:800px">
|
<img alt="lnbits_extensions" src="docs/assets/extensions.jpg" />
|
||||||
|
|
||||||
## LNbits API
|
## LNbits API
|
||||||
|
|
||||||
LNbits has a powerful API, many projects use LNbits to do the heavy lifting for their bitcoin/lightning services.
|
LNbits has a powerful API, many projects use LNbits to do the heavy lifting for their bitcoin/lightning services.
|
||||||
|
|
||||||
<img src="https://i.imgur.com/V742sb9.png" style="width:800px">
|
<img alt="lnbits_api" src="docs/assets/api.jpg" />
|
||||||
|
|
||||||
## LNbits node manager
|
## LNbits node manager
|
||||||
|
|
||||||
LNbits comes packaged with a light node management UI, to make running your node that much easier.
|
LNbits comes packaged with a light node management UI, to make running your node that much easier.
|
||||||
|
|
||||||
<img src="https://i.imgur.com/TYqIK60.png" style="width:800px">
|
<img alt="lnbits_api" src="docs/assets/lightning_node.jpg" />
|
||||||
|
|
||||||
## LNbits across all your devices
|
## LNbits merchant tools
|
||||||
|
|
||||||
As well as working great in a browser, LNbits has native IoS and Android apps as well as a chrome extension. So you can enjoy the same UI across ALL your devices.
|
The LNbits stack can process both bitcoin and fiat payments, making it a turnkey, all-in-one solution for merchants. With orders and inventory shared across extensions, and built-in notifications for Nostr, Telegram, and email, LNbits keeps everything in sync, freeing merchants to focus on their business.
|
||||||
|
|
||||||
<img src="https://i.imgur.com/J96EbRf.png" style="width:800px">
|
<img alt="lnbits_merchants" src="docs/assets/merchants_small.webp" />
|
||||||
|
|
||||||
## Powered by LNbits
|
## Powered by LNbits
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ ENV PATH="/root/.local/bin:$PATH"
|
|||||||
# Reinstall dependencies just in case (needed for CMD usage)
|
# Reinstall dependencies just in case (needed for CMD usage)
|
||||||
RUN uv sync --all-extras
|
RUN uv sync --all-extras
|
||||||
|
|
||||||
# LNbits + boltzd configuration
|
# LNbits
|
||||||
ENV LNBITS_PORT="5000"
|
ENV LNBITS_PORT="5000"
|
||||||
ENV LNBITS_HOST="0.0.0.0"
|
ENV LNBITS_HOST="0.0.0.0"
|
||||||
ENV LNBITS_BACKEND_WALLET_CLASS="BoltzWallet"
|
ENV LNBITS_BACKEND_WALLET_CLASS="BoltzWallet"
|
||||||
ENV FUNDING_SOURCE_MAX_RETRIES=10
|
ENV FUNDING_SOURCE_MAX_RETRIES="10"
|
||||||
|
|
||||||
|
# Boltzd
|
||||||
ENV BOLTZ_CLIENT_ENDPOINT="127.0.0.1:9002"
|
ENV BOLTZ_CLIENT_ENDPOINT="127.0.0.1:9002"
|
||||||
ENV BOLTZ_CLIENT_MACAROON="/root/.boltz/macaroons/admin.macaroon"
|
ENV BOLTZ_CLIENT_MACAROON="/root/.boltz/macaroons/admin.macaroon"
|
||||||
ENV BOLTZ_CLIENT_CERT="/root/.boltz/tls.cert"
|
ENV BOLTZ_CLIENT_CERT="/root/.boltz/tls.cert"
|
||||||
@@ -26,8 +28,7 @@ ENV BOLTZ_CLIENT_WALLET="lnbits"
|
|||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
||||||
# Entrypoint to start boltzd and LNbits
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
COPY dockerboltz.sh /dockerboltz.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
RUN chmod +x /dockerboltz.sh
|
|
||||||
|
|
||||||
CMD ["/dockerboltz.sh"]
|
CMD ["/entrypoint.sh"]
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|

|
||||||
|
|
||||||
|
# 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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/
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Executable
+74
@@ -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
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
ARG LNBITS_TAG=latest
|
||||||
|
|
||||||
|
FROM lnbits/lnbits:${LNBITS_TAG}
|
||||||
|
|
||||||
|
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get -y upgrade
|
||||||
|
RUN apt-get install -y ca-certificates curl gnupg netcat-openbsd git nodejs
|
||||||
|
|
||||||
|
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 ci
|
||||||
|
|
||||||
|
# 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"]
|
||||||
@@ -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='*'
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 180 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 317 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 157 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 305 KiB |
@@ -14,7 +14,6 @@ nav_order: 1
|
|||||||

|

|
||||||

|

|
||||||
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||||
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
|
||||||
|
|
||||||
# LNBits Admin UI
|
# LNBits Admin UI
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ nav_order: 1
|
|||||||

|

|
||||||

|

|
||||||
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||||
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
|
||||||
|
|
||||||
# Backend Wallet Comparison Table
|
# Backend Wallet Comparison Table
|
||||||
|
|
||||||
@@ -50,6 +49,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. |
|
| **Blink** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; focuses on mobile integrations. |
|
||||||
| **ZBD** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Gaming-focused payment platform. |
|
| **ZBD** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Gaming-focused payment platform. |
|
||||||
| **Spark (CLN)** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | Web interface for CLN; requires Spark server setup. |
|
| **Spark (CLN)** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | Web interface for CLN; requires Spark server setup. |
|
||||||
|
| **Spark (L2)** | Self-custodial | ❌ | Medium | ❌ | 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. |
|
| **Cliche Wallet** | Self-custodial | ❌ | Medium | ❌ | Medium | Manual | Moderate | Moderate | Minimal fees | Medium | Lightweight wallet; suitable for embedded systems. |
|
||||||
| **Strike** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
|
| **Strike** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
|
||||||
| **LNPay** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
|
| **LNPay** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
|
||||||
@@ -57,7 +57,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. |
|
| **LN.tips** | Custodial/Self-Custodial | Depends on provider | Medium | ❌ | Low | Provider-managed | Moderate | Low | Transaction fees may apply | Medium | Simple hosted service; use LN.tips API as your backend. |
|
||||||
| **Fake Wallet** | Testing (simulated) | ❌ | Low | ❌ | N/A | N/A | Easy | Low | None (test only) | N/A | For testing only; mints accounting units in LNbits (no real sats, unit name configurable). |
|
| **Fake Wallet** | Testing (simulated) | ❌ | Low | ❌ | N/A | N/A | Easy | Low | None (test only) | N/A | For testing only; mints accounting units in LNbits (no real sats, unit name configurable). |
|
||||||
|
|
||||||
---
|
## Spark (L2)
|
||||||
|
|
||||||
|
Spark L2 uses a local Node.js sidecar to expose an HTTP API that LNbits can use as a funding source. It is self-custodial and secured by a standard mnemonic seed. Sidecar repo `https://github.com/lnbits/spark_sidecar`.
|
||||||
|
|
||||||
### Notes for readers
|
### Notes for readers
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ nav_order: 1
|
|||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
   [](https://extensions.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits) <img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">
|
   [](https://extensions.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||||
|
|
||||||
# Basic installation
|
# Basic installation
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# Generic OIDC Authentication Configuration
|
||||||
|
|
||||||
|
This document explains how to configure generic OIDC authentication for LNbits, which allows integration with various OIDC-compliant authentication providers such as Zitadel, Authentik, and others.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The generic OIDC provider (`oidc`) complements the existing Keycloak provider and allows you to integrate any OIDC-compliant authentication service. You can customize the login button with your own organization name and icon.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Add the following environment variables to your `.env` file or system environment:
|
||||||
|
|
||||||
|
### Required Settings
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable OIDC authentication
|
||||||
|
LNBITS_AUTH_ALLOWED_METHODS=oidc-auth
|
||||||
|
|
||||||
|
# OIDC Discovery URL (well-known endpoint)
|
||||||
|
LNBITS_OIDC_DISCOVERY_URL=https://your-oidc-provider-domain/.well-known/openid-configuration
|
||||||
|
|
||||||
|
# Client credentials from your OIDC provider
|
||||||
|
LNBITS_OIDC_CLIENT_ID=your-client-id
|
||||||
|
LNBITS_OIDC_CLIENT_SECRET=your-client-secret
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional Settings - Customize the Login Button
|
||||||
|
|
||||||
|
You can customize how the OIDC login button appears to your users:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Custom organization name (displayed on the login button)
|
||||||
|
# Example: "Login via Zitadel" or "Login via Authentik"
|
||||||
|
LNBITS_OIDC_CLIENT_CUSTOM_ORG="Zitadel"
|
||||||
|
|
||||||
|
# Custom icon URL (displayed on the login button)
|
||||||
|
# Can be a full URL or a path to a local image
|
||||||
|
LNBITS_OIDC_CLIENT_CUSTOM_ICON=https://zitadel.com/favicon.svg
|
||||||
|
```
|
||||||
|
|
||||||
|
If not set, the button will display "Login via OIDC" with a generic lock icon.
|
||||||
|
|
||||||
|
## Zitadel Configuration Example
|
||||||
|
|
||||||
|
For Zitadel, configure as follows:
|
||||||
|
|
||||||
|
1. Create a new application in Zitadel
|
||||||
|
2. Choose "Web" application type
|
||||||
|
3. Configure the redirect URI: `https://your-lnbits-domain/api/v1/auth/oidc/token`
|
||||||
|
4. Save the Client ID and Client Secret
|
||||||
|
5. Use these environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LNBITS_AUTH_ALLOWED_METHODS=oidc-auth
|
||||||
|
LNBITS_OIDC_DISCOVERY_URL=https://your-oidc-provider-domain/.well-known/openid-configuration
|
||||||
|
LNBITS_OIDC_CLIENT_ID=your-zitadel-client-id
|
||||||
|
LNBITS_OIDC_CLIENT_SECRET=your-zitadel-client-secret
|
||||||
|
# Customize the button to show "Login via Zitadel" with Zitadel's logo
|
||||||
|
LNBITS_OIDC_CLIENT_CUSTOM_ORG="Zitadel"
|
||||||
|
LNBITS_OIDC_CLIENT_CUSTOM_ICON="https://zitadel.com/favicon.svg"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result**: The login page will display a button with the text "Login via Zitadel" and the Zitadel logo.
|
||||||
|
|
||||||
|
## Authentik Configuration Example
|
||||||
|
|
||||||
|
For Authentik:
|
||||||
|
|
||||||
|
1. Create a new OAuth2/OpenID Provider
|
||||||
|
2. Set the redirect URI: `https://your-lnbits-domain/api/v1/auth/oidc/token`
|
||||||
|
3. Configure scopes: `openid`, `email`, `profile`
|
||||||
|
4. Get the Client ID and Client Secret
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LNBITS_AUTH_ALLOWED_METHODS=oidc-auth
|
||||||
|
LNBITS_OIDC_DISCOVERY_URL=https://authentik.yourdomain.com/application/o/your-app/.well-known/openid-configuration
|
||||||
|
LNBITS_OIDC_CLIENT_ID=your-authentik-client-id
|
||||||
|
LNBITS_OIDC_CLIENT_SECRET=your-authentik-client-secret
|
||||||
|
LNBITS_OIDC_CLIENT_CUSTOM_ORG="Authentik"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multiple Auth Methods
|
||||||
|
|
||||||
|
You can enable multiple authentication methods simultaneously:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LNBITS_AUTH_ALLOWED_METHODS=username-password,oidc-auth,keycloak-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
## Discovery Endpoint Requirements
|
||||||
|
|
||||||
|
Your OIDC provider must expose a standard discovery endpoint (`.well-known/openid-configuration`) that includes:
|
||||||
|
|
||||||
|
- `authorization_endpoint`
|
||||||
|
- `token_endpoint`
|
||||||
|
- `userinfo_endpoint`
|
||||||
|
- `jwks_uri` (JSON Web Key Set)
|
||||||
|
|
||||||
|
The OIDC implementation will automatically fetch these endpoints from the discovery URL.
|
||||||
|
|
||||||
|
## User Mapping
|
||||||
|
|
||||||
|
The OIDC provider maps user information from the OIDC userinfo endpoint:
|
||||||
|
|
||||||
|
- `sub` → User ID
|
||||||
|
- `email` → Email address
|
||||||
|
- `given_name` → First name
|
||||||
|
- `family_name` → Last name
|
||||||
|
- `name` or `preferred_username` → Display name
|
||||||
|
- `picture` → Profile picture URL
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Authentication fails
|
||||||
|
|
||||||
|
1. Verify the discovery URL is accessible
|
||||||
|
2. Check that Client ID and Client Secret are correct
|
||||||
|
3. Ensure redirect URI in your OIDC provider matches: `https://your-lnbits-domain/api/v1/auth/oidc/token`
|
||||||
|
4. Check LNbits logs for detailed error messages
|
||||||
|
|
||||||
|
### User info not populated
|
||||||
|
|
||||||
|
Some OIDC providers may use different claim names. If user information is not correctly populated, check your provider's userinfo endpoint response format and adjust the provider class if needed.
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Always use HTTPS in production
|
||||||
|
- Keep client secrets secure and never commit them to version control
|
||||||
|
- Use environment variables or secure configuration management
|
||||||
|
- Regularly rotate client secrets
|
||||||
|
- Review OIDC provider's security best practices
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
The OIDC provider is implemented in `lnbits/core/models/sso/oidc.py` and extends the `fastapi_sso` library's `SSOBase` class. It uses the standard OpenID Connect flow with:
|
||||||
|
|
||||||
|
- Scopes: `openid`, `email`, `profile`
|
||||||
|
- Response type: `code` (authorization code flow)
|
||||||
|
- Discovery document for automatic endpoint resolution
|
||||||
@@ -14,7 +14,6 @@ nav_order: 1
|
|||||||

|

|
||||||

|

|
||||||
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||||
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
|
||||||
|
|
||||||
# LNbits Super User (SU)
|
# LNbits Super User (SU)
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ nav_order: 1
|
|||||||

|

|
||||||

|

|
||||||
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||||
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
|
||||||
|
|
||||||
# LNbits Roles: A Quick Overview
|
# LNbits Roles: A Quick Overview
|
||||||
|
|
||||||
|
|||||||
+30
-3
@@ -14,7 +14,6 @@ nav_order: 3
|
|||||||

|

|
||||||

|

|
||||||
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||||
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
|
||||||
|
|
||||||
# Backend wallets
|
# Backend wallets
|
||||||
|
|
||||||
@@ -41,7 +40,8 @@ A backend wallet is selected and configured entirely through LNbits environment
|
|||||||
| [CoreLightning](#corelightning) | [LND (gRPC)](#lnd-grpc) | [Blink](#blink) |
|
| [CoreLightning](#corelightning) | [LND (gRPC)](#lnd-grpc) | [Blink](#blink) |
|
||||||
| [CoreLightning REST](#corelightning-rest) | [LNbits](#lnbits) | [Alby](#alby) |
|
| [CoreLightning REST](#corelightning-rest) | [LNbits](#lnbits) | [Alby](#alby) |
|
||||||
| [Spark (Core Lightning)](#spark-core-lightning) | [LNPay](#lnpay) | [Boltz](#boltz) |
|
| [Spark (Core Lightning)](#spark-core-lightning) | [LNPay](#lnpay) | [Boltz](#boltz) |
|
||||||
| [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) |
|
| [Breez SDK](#breez-sdk) | [Breez Liquid SDK](#breez-liquid-sdk) | [Nostr Wallet Connect](#nostr-wallet-connect-nwc) |
|
||||||
| [Strike](#strike) | [Eclair (ACINQ)](#eclair-acinq) | [LN.tips](#lntips) |
|
| [Strike](#strike) | [Eclair (ACINQ)](#eclair-acinq) | [LN.tips](#lntips) |
|
||||||
| [Fake Wallet](#fake-wallet) | | |
|
| [Fake Wallet](#fake-wallet) | | |
|
||||||
@@ -52,7 +52,7 @@ A backend wallet is selected and configured entirely through LNbits environment
|
|||||||
|
|
||||||
### CLNRest (using [runes](https://docs.corelightning.org/reference/lightning-createrune))
|
### CLNRest (using [runes](https://docs.corelightning.org/reference/lightning-createrune))
|
||||||
|
|
||||||
[Core Lightning REST API docs](https://docs.corelightning.org/docs/rest)
|
[Core Lightning REST API docs](https://docs.corelightning.org/docs/rest)
|
||||||
Should also work with the [Rust version of CLNRest](https://github.com/daywalker90/clnrest-rs)
|
Should also work with the [Rust version of CLNRest](https://github.com/daywalker90/clnrest-rs)
|
||||||
|
|
||||||
**Environment variables**
|
**Environment variables**
|
||||||
@@ -127,6 +127,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_URL`: `http://10.147.17.230:9737/rpc`
|
||||||
- `SPARK_TOKEN`: `secret_access_key`
|
- `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)
|
## LND (REST)
|
||||||
|
|
||||||
**Required env vars**
|
**Required env vars**
|
||||||
|
|||||||
+10
-1
@@ -65,6 +65,7 @@ from .middleware import (
|
|||||||
InstalledExtensionMiddleware,
|
InstalledExtensionMiddleware,
|
||||||
add_first_install_middleware,
|
add_first_install_middleware,
|
||||||
add_ip_block_middleware,
|
add_ip_block_middleware,
|
||||||
|
add_profiler_middleware,
|
||||||
add_ratelimit_middleware,
|
add_ratelimit_middleware,
|
||||||
)
|
)
|
||||||
from .tasks import internal_invoice_listener, invoice_listener, run_interval
|
from .tasks import internal_invoice_listener, invoice_listener, run_interval
|
||||||
@@ -196,6 +197,9 @@ def create_app() -> FastAPI:
|
|||||||
|
|
||||||
register_exception_handlers(app)
|
register_exception_handlers(app)
|
||||||
|
|
||||||
|
if settings.profiler:
|
||||||
|
add_profiler_middleware(app)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
@@ -468,7 +472,12 @@ def register_async_tasks() -> None:
|
|||||||
create_permanent_task(wait_for_audit_data)
|
create_permanent_task(wait_for_audit_data)
|
||||||
create_permanent_task(wait_notification_messages)
|
create_permanent_task(wait_notification_messages)
|
||||||
|
|
||||||
create_permanent_task(run_interval(30 * 60, check_pending_payments))
|
create_permanent_task(
|
||||||
|
run_interval(
|
||||||
|
settings.lnbits_funding_source_pending_interval_seconds,
|
||||||
|
check_pending_payments,
|
||||||
|
)
|
||||||
|
)
|
||||||
create_permanent_task(invoice_listener)
|
create_permanent_task(invoice_listener)
|
||||||
create_permanent_task(internal_invoice_listener)
|
create_permanent_task(internal_invoice_listener)
|
||||||
create_permanent_task(cache.invalidate_forever)
|
create_permanent_task(cache.invalidate_forever)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from .extensions import (
|
|||||||
drop_extension_db,
|
drop_extension_db,
|
||||||
get_installed_extension,
|
get_installed_extension,
|
||||||
get_installed_extensions,
|
get_installed_extensions,
|
||||||
|
get_installed_extensions_count,
|
||||||
get_user_active_extensions_ids,
|
get_user_active_extensions_ids,
|
||||||
get_user_extension,
|
get_user_extension,
|
||||||
get_user_extensions,
|
get_user_extensions,
|
||||||
@@ -58,6 +59,7 @@ from .users import (
|
|||||||
get_account_by_username,
|
get_account_by_username,
|
||||||
get_account_by_username_or_email,
|
get_account_by_username_or_email,
|
||||||
get_accounts,
|
get_accounts,
|
||||||
|
get_accounts_count,
|
||||||
get_user,
|
get_user,
|
||||||
get_user_access_control_lists,
|
get_user_access_control_lists,
|
||||||
get_user_from_account,
|
get_user_from_account,
|
||||||
@@ -117,11 +119,13 @@ __all__ = [
|
|||||||
"get_account_by_username",
|
"get_account_by_username",
|
||||||
"get_account_by_username_or_email",
|
"get_account_by_username_or_email",
|
||||||
"get_accounts",
|
"get_accounts",
|
||||||
|
"get_accounts_count",
|
||||||
"get_admin_settings",
|
"get_admin_settings",
|
||||||
"get_db_version",
|
"get_db_version",
|
||||||
"get_db_versions",
|
"get_db_versions",
|
||||||
"get_installed_extension",
|
"get_installed_extension",
|
||||||
"get_installed_extensions",
|
"get_installed_extensions",
|
||||||
|
"get_installed_extensions_count",
|
||||||
"get_latest_payments_by_extension",
|
"get_latest_payments_by_extension",
|
||||||
"get_payment",
|
"get_payment",
|
||||||
"get_payments",
|
"get_payments",
|
||||||
|
|||||||
@@ -90,6 +90,13 @@ async def get_installed_extensions(
|
|||||||
return all_extensions
|
return all_extensions
|
||||||
|
|
||||||
|
|
||||||
|
async def get_installed_extensions_count(conn: Connection | None = None) -> int:
|
||||||
|
row: dict | None = await (conn or db).fetchone(
|
||||||
|
"SELECT COUNT(*) as count FROM installed_extensions"
|
||||||
|
)
|
||||||
|
return int(row["count"]) if row else 0
|
||||||
|
|
||||||
|
|
||||||
async def get_user_extension(
|
async def get_user_extension(
|
||||||
user_id: str, extension: str, conn: Connection | None = None
|
user_id: str, extension: str, conn: Connection | None = None
|
||||||
) -> UserExtension | None:
|
) -> UserExtension | None:
|
||||||
|
|||||||
@@ -149,14 +149,12 @@ async def get_payments_paginated( # noqa: C901
|
|||||||
f"(status = '{PaymentState.SUCCESS}' OR status = '{PaymentState.PENDING}')"
|
f"(status = '{PaymentState.SUCCESS}' OR status = '{PaymentState.PENDING}')"
|
||||||
)
|
)
|
||||||
elif complete:
|
elif complete:
|
||||||
clause.append(
|
clause.append(f"""
|
||||||
f"""
|
|
||||||
(
|
(
|
||||||
status = '{PaymentState.SUCCESS}'
|
status = '{PaymentState.SUCCESS}'
|
||||||
OR (amount < 0 AND status = '{PaymentState.PENDING}')
|
OR (amount < 0 AND status = '{PaymentState.PENDING}')
|
||||||
)
|
)
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
elif pending:
|
elif pending:
|
||||||
clause.append(f"status = '{PaymentState.PENDING}'")
|
clause.append(f"status = '{PaymentState.PENDING}'")
|
||||||
elif failed:
|
elif failed:
|
||||||
@@ -346,14 +344,12 @@ async def get_payments_history(
|
|||||||
"wallet_id": wallet_id,
|
"wallet_id": wallet_id,
|
||||||
}
|
}
|
||||||
# count outgoing payments if they are still pending
|
# count outgoing payments if they are still pending
|
||||||
where = [
|
where = [f"""
|
||||||
f"""
|
|
||||||
wallet_id = :wallet_id AND (
|
wallet_id = :wallet_id AND (
|
||||||
status = '{PaymentState.SUCCESS}'
|
status = '{PaymentState.SUCCESS}'
|
||||||
OR (amount < 0 AND status = '{PaymentState.PENDING}')
|
OR (amount < 0 AND status = '{PaymentState.PENDING}')
|
||||||
)
|
)
|
||||||
"""
|
"""]
|
||||||
]
|
|
||||||
clause = filters.where(where)
|
clause = filters.where(where)
|
||||||
transactions: list[dict] = await db.fetchall(
|
transactions: list[dict] = await db.fetchall(
|
||||||
# This query is safe from SQL injection:
|
# This query is safe from SQL injection:
|
||||||
|
|||||||
@@ -105,7 +105,8 @@ async def get_settings_field(
|
|||||||
)
|
)
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
return SettingsField(id=row["id"], value=json.loads(row["value"]), tag=row["tag"])
|
value = json.loads(row["value"]) if row["value"] else None
|
||||||
|
return SettingsField(id=row["id"], value=value, tag=row["tag"])
|
||||||
|
|
||||||
|
|
||||||
async def set_settings_field(id_: str, value: Any | None, tag: str | None = "core"):
|
async def set_settings_field(id_: str, value: Any | None, tag: str | None = "core"):
|
||||||
|
|||||||
+103
-31
@@ -4,10 +4,17 @@ from typing import Any
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
||||||
from lnbits.core.crud.wallets import create_wallet, get_wallets
|
from lnbits.core.crud.wallets import (
|
||||||
|
clear_wallet_cache,
|
||||||
|
create_wallet,
|
||||||
|
get_standalone_wallet,
|
||||||
|
get_wallets,
|
||||||
|
)
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models import UserAcls
|
from lnbits.core.models import UserAcls
|
||||||
from lnbits.db import Connection, Filters, Page
|
from lnbits.db import Connection, Filters, Page
|
||||||
|
from lnbits.helpers import sha256s
|
||||||
|
from lnbits.utils.cache import cache
|
||||||
|
|
||||||
from ..models import (
|
from ..models import (
|
||||||
Account,
|
Account,
|
||||||
@@ -30,6 +37,13 @@ async def create_account(
|
|||||||
return account
|
return account
|
||||||
|
|
||||||
|
|
||||||
|
async def get_accounts_count(conn: Connection | None = None) -> int:
|
||||||
|
row: dict | None = await (conn or db).fetchone(
|
||||||
|
"SELECT COUNT(*) as count FROM accounts"
|
||||||
|
)
|
||||||
|
return int(row["count"]) if row else 0
|
||||||
|
|
||||||
|
|
||||||
async def update_account(account: Account, conn: Connection | None = None) -> Account:
|
async def update_account(account: Account, conn: Connection | None = None) -> Account:
|
||||||
account.updated_at = datetime.now(timezone.utc)
|
account.updated_at = datetime.now(timezone.utc)
|
||||||
await (conn or db).update("accounts", account)
|
await (conn or db).update("accounts", account)
|
||||||
@@ -41,6 +55,7 @@ async def delete_account(user_id: str, conn: Connection | None = None) -> None:
|
|||||||
"DELETE from accounts WHERE id = :user",
|
"DELETE from accounts WHERE id = :user",
|
||||||
{"user": user_id},
|
{"user": user_id},
|
||||||
)
|
)
|
||||||
|
await clear_user_id_cache(user_id)
|
||||||
|
|
||||||
|
|
||||||
async def get_accounts(
|
async def get_accounts(
|
||||||
@@ -49,17 +64,22 @@ async def get_accounts(
|
|||||||
) -> Page[AccountOverview]:
|
) -> Page[AccountOverview]:
|
||||||
where_clauses = []
|
where_clauses = []
|
||||||
values: dict[str, Any] = {}
|
values: dict[str, Any] = {}
|
||||||
|
filters = filters or Filters()
|
||||||
|
|
||||||
# Make wallet filter explicit
|
wallet_filter = filters.get_filter_by_field("wallet_id")
|
||||||
wallet_filter = (
|
|
||||||
next((f for f in filters.filters if f.field == "wallet_id"), None)
|
if wallet_filter and wallet_filter.values:
|
||||||
if filters
|
wallet_id_value = next(iter(wallet_filter.values.values()), None)
|
||||||
else None
|
wallet = (
|
||||||
)
|
await get_standalone_wallet(wallet_id_value, deleted=None, conn=conn)
|
||||||
if filters and wallet_filter and wallet_filter.values:
|
if wallet_id_value
|
||||||
where_clauses.append("wallets.id = :wallet_id")
|
else None
|
||||||
values = {**values, "wallet_id": next(iter(wallet_filter.values.values()))}
|
)
|
||||||
filters.filters = [f for f in filters.filters if f.field != "wallet_id"]
|
if not wallet:
|
||||||
|
return Page(data=[], total=0)
|
||||||
|
where_clauses.append("accounts.id = :account_id")
|
||||||
|
values = {**values, "account_id": wallet.user}
|
||||||
|
filters.remove_filter_by_field("wallet_id")
|
||||||
|
|
||||||
return await (conn or db).fetch_page(
|
return await (conn or db).fetch_page(
|
||||||
"""
|
"""
|
||||||
@@ -69,6 +89,7 @@ async def get_accounts(
|
|||||||
accounts.email,
|
accounts.email,
|
||||||
accounts.pubkey,
|
accounts.pubkey,
|
||||||
accounts.external_id,
|
accounts.external_id,
|
||||||
|
accounts.activated,
|
||||||
SUM(COALESCE((
|
SUM(COALESCE((
|
||||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
), 0)) as balance_msat,
|
), 0)) as balance_msat,
|
||||||
@@ -93,12 +114,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:
|
if len(user_id) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE id = :id",
|
"""
|
||||||
{"id": user_id},
|
SELECT * FROM accounts
|
||||||
|
WHERE id = :id AND (activated = true OR activated = :activated)
|
||||||
|
""",
|
||||||
|
{"id": user_id, "activated": active_only},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -124,55 +151,79 @@ async def delete_accounts_no_wallets(
|
|||||||
|
|
||||||
|
|
||||||
async def get_account_by_username(
|
async def get_account_by_username(
|
||||||
username: str, conn: Connection | None = None
|
username: str, active_only: bool = True, conn: Connection | None = None
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
if len(username) == 0:
|
if len(username) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE LOWER(username) = :username",
|
"""
|
||||||
{"username": username.lower()},
|
SELECT * FROM accounts
|
||||||
|
WHERE
|
||||||
|
LOWER(username) = :username
|
||||||
|
AND (activated = true OR activated = :activated)
|
||||||
|
""",
|
||||||
|
{"username": username.lower(), "activated": active_only},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account_by_pubkey(
|
async def get_account_by_pubkey(
|
||||||
pubkey: str, conn: Connection | None = None
|
pubkey: str, active_only: bool = True, conn: Connection | None = None
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts 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,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account_by_email(
|
async def get_account_by_email(
|
||||||
email: str, conn: Connection | None = None
|
email: str, active_only: bool = True, conn: Connection | None = None
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
if len(email) == 0:
|
if len(email) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"SELECT * FROM accounts WHERE LOWER(email) = :email",
|
"""
|
||||||
{"email": email.lower()},
|
SELECT * FROM accounts
|
||||||
|
WHERE
|
||||||
|
LOWER(email) = :email
|
||||||
|
AND (activated = true OR activated = :activated)
|
||||||
|
""",
|
||||||
|
{"email": email.lower(), "activated": active_only},
|
||||||
Account,
|
Account,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_account_by_username_or_email(
|
async def get_account_by_username_or_email(
|
||||||
username_or_email: str, conn: Connection | None = None
|
username_or_email: str,
|
||||||
|
active_only: bool = True,
|
||||||
|
conn: Connection | None = None,
|
||||||
) -> Account | None:
|
) -> Account | None:
|
||||||
|
|
||||||
return await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM accounts
|
SELECT * FROM accounts
|
||||||
WHERE 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,
|
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:
|
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:
|
if not account:
|
||||||
return None
|
return None
|
||||||
return await get_user_from_account(account, conn=conn)
|
return await get_user_from_account(account, conn=conn)
|
||||||
@@ -191,6 +242,7 @@ async def get_user_from_account(
|
|||||||
|
|
||||||
return User(
|
return User(
|
||||||
id=account.id,
|
id=account.id,
|
||||||
|
activated=account.activated,
|
||||||
email=account.email,
|
email=account.email,
|
||||||
username=account.username,
|
username=account.username,
|
||||||
pubkey=account.pubkey,
|
pubkey=account.pubkey,
|
||||||
@@ -204,6 +256,7 @@ async def get_user_from_account(
|
|||||||
super_user=account.is_super_user,
|
super_user=account.is_super_user,
|
||||||
fiat_providers=account.fiat_providers,
|
fiat_providers=account.fiat_providers,
|
||||||
has_password=account.password_hash is not None,
|
has_password=account.password_hash is not None,
|
||||||
|
ui_customization=account.ui_customization or {},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -215,12 +268,31 @@ async def update_user_access_control_list(
|
|||||||
|
|
||||||
|
|
||||||
async def get_user_access_control_lists(
|
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:
|
) -> UserAcls:
|
||||||
user_acls = await (conn or db).fetchone(
|
user_acls = await (conn or db).fetchone(
|
||||||
"SELECT id, access_control_list FROM accounts WHERE id = :id",
|
"""
|
||||||
{"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,
|
UserAcls,
|
||||||
)
|
)
|
||||||
|
|
||||||
return user_acls or UserAcls(id=user_id)
|
return user_acls or UserAcls(id=user_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def clear_user_id_cache(user_id: str):
|
||||||
|
user = await get_user(user_id, active_only=True)
|
||||||
|
if user:
|
||||||
|
clear_user_cache(user)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_user_cache(user: User):
|
||||||
|
user_cache_key: str | None = cache.pop(
|
||||||
|
f"auth:user:cache_key:{sha256s(user.id)}", None
|
||||||
|
)
|
||||||
|
if user_cache_key:
|
||||||
|
cache.pop(user_cache_key)
|
||||||
|
for wallet in user.wallets:
|
||||||
|
clear_wallet_cache(wallet)
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ async def delete_wallet(
|
|||||||
deleted: bool = True,
|
deleted: bool = True,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
_clear_wallet_cache(wallet_id)
|
clear_wallet_id_cache(wallet_id)
|
||||||
now = int(time())
|
now = int(time())
|
||||||
|
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
@@ -65,7 +65,7 @@ async def delete_wallet(
|
|||||||
|
|
||||||
|
|
||||||
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
|
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
|
||||||
_clear_wallet_cache(wallet_id)
|
clear_wallet_id_cache(wallet_id)
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"DELETE FROM wallets WHERE id = :wallet",
|
"DELETE FROM wallets WHERE id = :wallet",
|
||||||
{"wallet": wallet_id},
|
{"wallet": wallet_id},
|
||||||
@@ -75,7 +75,7 @@ async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) ->
|
|||||||
async def delete_wallet_by_id(
|
async def delete_wallet_by_id(
|
||||||
wallet_id: str, conn: Connection | None = None
|
wallet_id: str, conn: Connection | None = None
|
||||||
) -> int | None:
|
) -> int | None:
|
||||||
_clear_wallet_cache(wallet_id)
|
clear_wallet_id_cache(wallet_id)
|
||||||
now = int(time())
|
now = int(time())
|
||||||
result = await (conn or db).execute(
|
result = await (conn or db).execute(
|
||||||
# Timestamp placeholder is safe from SQL injection (not user input)
|
# Timestamp placeholder is safe from SQL injection (not user input)
|
||||||
@@ -226,11 +226,14 @@ async def get_wallet_for_key(
|
|||||||
) -> Wallet | None:
|
) -> Wallet | None:
|
||||||
wallet = await (conn or db).fetchone(
|
wallet = await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT *, COALESCE((
|
SELECT wallets.*, COALESCE((
|
||||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
), 0)
|
), 0)
|
||||||
AS balance_msat FROM wallets
|
AS balance_msat FROM wallets
|
||||||
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},
|
{"key": key},
|
||||||
Wallet,
|
Wallet,
|
||||||
@@ -250,8 +253,11 @@ async def get_base_wallet_for_key(
|
|||||||
) -> BaseWallet | None:
|
) -> BaseWallet | None:
|
||||||
wallet = await (conn or db).fetchone(
|
wallet = await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT id, "user", wallet_type, adminkey, inkey FROM wallets
|
SELECT wallets.id, "user", wallet_type, adminkey, inkey 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},
|
{"key": key},
|
||||||
BaseWallet,
|
BaseWallet,
|
||||||
@@ -294,8 +300,14 @@ async def get_total_balance(conn: Connection | None = None):
|
|||||||
return row.get("balance", 0) or 0
|
return row.get("balance", 0) or 0
|
||||||
|
|
||||||
|
|
||||||
def _clear_wallet_cache(wallet_id):
|
def clear_wallet_id_cache(wallet_id: str):
|
||||||
cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}")
|
cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}")
|
||||||
if cached_wallet:
|
if cached_wallet:
|
||||||
cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}")
|
cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}")
|
||||||
cache.pop(f"auth:x-api-key:{cached_wallet.inkey}")
|
cache.pop(f"auth:x-api-key:{cached_wallet.inkey}")
|
||||||
|
|
||||||
|
|
||||||
|
def clear_wallet_cache(wallet: Wallet):
|
||||||
|
cache.pop(f"auth:wallet:{wallet.id}")
|
||||||
|
cache.pop(f"auth:x-api-key:{wallet.adminkey}")
|
||||||
|
cache.pop(f"auth:x-api-key:{wallet.inkey}")
|
||||||
|
|||||||
+78
-124
@@ -10,31 +10,26 @@ from lnbits.db import Connection
|
|||||||
|
|
||||||
|
|
||||||
async def m000_create_migrations_table(db: Connection):
|
async def m000_create_migrations_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS dbversions (
|
CREATE TABLE IF NOT EXISTS dbversions (
|
||||||
db TEXT PRIMARY KEY,
|
db TEXT PRIMARY KEY,
|
||||||
version INT NOT NULL
|
version INT NOT NULL
|
||||||
)
|
)
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m001_initial(db: Connection):
|
async def m001_initial(db: Connection):
|
||||||
"""
|
"""
|
||||||
Initial LNbits tables.
|
Initial LNbits tables.
|
||||||
"""
|
"""
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS accounts (
|
CREATE TABLE IF NOT EXISTS accounts (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
email TEXT,
|
email TEXT,
|
||||||
pass TEXT
|
pass TEXT
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
await db.execute("""
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS extensions (
|
CREATE TABLE IF NOT EXISTS extensions (
|
||||||
"user" TEXT NOT NULL,
|
"user" TEXT NOT NULL,
|
||||||
extension TEXT NOT NULL,
|
extension TEXT NOT NULL,
|
||||||
@@ -42,10 +37,8 @@ async def m001_initial(db: Connection):
|
|||||||
|
|
||||||
UNIQUE ("user", extension)
|
UNIQUE ("user", extension)
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
await db.execute("""
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS wallets (
|
CREATE TABLE IF NOT EXISTS wallets (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
@@ -53,10 +46,8 @@ async def m001_initial(db: Connection):
|
|||||||
adminkey TEXT NOT NULL,
|
adminkey TEXT NOT NULL,
|
||||||
inkey TEXT
|
inkey TEXT
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
await db.execute(f"""
|
||||||
await db.execute(
|
|
||||||
f"""
|
|
||||||
CREATE TABLE IF NOT EXISTS apipayments (
|
CREATE TABLE IF NOT EXISTS apipayments (
|
||||||
payhash TEXT NOT NULL,
|
payhash TEXT NOT NULL,
|
||||||
amount {db.big_int} NOT NULL,
|
amount {db.big_int} NOT NULL,
|
||||||
@@ -67,11 +58,9 @@ async def m001_initial(db: Connection):
|
|||||||
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||||
UNIQUE (wallet, payhash)
|
UNIQUE (wallet, payhash)
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE VIEW balances AS
|
CREATE VIEW balances AS
|
||||||
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
|
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
|
||||||
SELECT wallet, SUM(amount) AS s -- incoming
|
SELECT wallet, SUM(amount) AS s -- incoming
|
||||||
@@ -85,8 +74,7 @@ async def m001_initial(db: Connection):
|
|||||||
GROUP BY wallet
|
GROUP BY wallet
|
||||||
)x
|
)x
|
||||||
GROUP BY wallet;
|
GROUP BY wallet;
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m002_add_fields_to_apipayments(db: Connection):
|
async def m002_add_fields_to_apipayments(db: Connection):
|
||||||
@@ -149,8 +137,7 @@ async def m004_ensure_fees_are_always_negative(db: Connection):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
await db.execute("DROP VIEW balances")
|
await db.execute("DROP VIEW balances")
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE VIEW balances AS
|
CREATE VIEW balances AS
|
||||||
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
|
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
|
||||||
SELECT wallet, SUM(amount) AS s -- incoming
|
SELECT wallet, SUM(amount) AS s -- incoming
|
||||||
@@ -164,8 +151,7 @@ async def m004_ensure_fees_are_always_negative(db: Connection):
|
|||||||
GROUP BY wallet
|
GROUP BY wallet
|
||||||
)x
|
)x
|
||||||
GROUP BY wallet;
|
GROUP BY wallet;
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m005_balance_check_balance_notify(db: Connection):
|
async def m005_balance_check_balance_notify(db: Connection):
|
||||||
@@ -174,8 +160,7 @@ async def m005_balance_check_balance_notify(db: Connection):
|
|||||||
LNbits wallet and of balanceNotify URLs supplied by users to empty their wallets.
|
LNbits wallet and of balanceNotify URLs supplied by users to empty their wallets.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS balance_check (
|
CREATE TABLE IF NOT EXISTS balance_check (
|
||||||
wallet TEXT NOT NULL REFERENCES wallets (id),
|
wallet TEXT NOT NULL REFERENCES wallets (id),
|
||||||
service TEXT NOT NULL,
|
service TEXT NOT NULL,
|
||||||
@@ -183,19 +168,16 @@ async def m005_balance_check_balance_notify(db: Connection):
|
|||||||
|
|
||||||
UNIQUE(wallet, service)
|
UNIQUE(wallet, service)
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS balance_notify (
|
CREATE TABLE IF NOT EXISTS balance_notify (
|
||||||
wallet TEXT NOT NULL REFERENCES wallets (id),
|
wallet TEXT NOT NULL REFERENCES wallets (id),
|
||||||
url TEXT NOT NULL,
|
url TEXT NOT NULL,
|
||||||
|
|
||||||
UNIQUE(wallet, url)
|
UNIQUE(wallet, url)
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m006_add_invoice_expiry_to_apipayments(db: Connection):
|
async def m006_add_invoice_expiry_to_apipayments(db: Connection):
|
||||||
@@ -262,19 +244,16 @@ async def m007_set_invoice_expiries(db: Connection):
|
|||||||
|
|
||||||
|
|
||||||
async def m008_create_admin_settings_table(db: Connection):
|
async def m008_create_admin_settings_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
super_user TEXT,
|
super_user TEXT,
|
||||||
editable_settings TEXT NOT NULL DEFAULT '{}'
|
editable_settings TEXT NOT NULL DEFAULT '{}'
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m009_create_tinyurl_table(db: Connection):
|
async def m009_create_tinyurl_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute(f"""
|
||||||
f"""
|
|
||||||
CREATE TABLE IF NOT EXISTS tiny_url (
|
CREATE TABLE IF NOT EXISTS tiny_url (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
url TEXT,
|
url TEXT,
|
||||||
@@ -282,13 +261,11 @@ async def m009_create_tinyurl_table(db: Connection):
|
|||||||
wallet TEXT,
|
wallet TEXT,
|
||||||
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m010_create_installed_extensions_table(db: Connection):
|
async def m010_create_installed_extensions_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS installed_extensions (
|
CREATE TABLE IF NOT EXISTS installed_extensions (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
version TEXT NOT NULL,
|
version TEXT NOT NULL,
|
||||||
@@ -299,8 +276,7 @@ async def m010_create_installed_extensions_table(db: Connection):
|
|||||||
active BOOLEAN DEFAULT false,
|
active BOOLEAN DEFAULT false,
|
||||||
meta TEXT NOT NULL DEFAULT '{}'
|
meta TEXT NOT NULL DEFAULT '{}'
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m011_optimize_balances_view(db: Connection):
|
async def m011_optimize_balances_view(db: Connection):
|
||||||
@@ -309,23 +285,19 @@ async def m011_optimize_balances_view(db: Connection):
|
|||||||
over the payments table instead of 2.
|
over the payments table instead of 2.
|
||||||
"""
|
"""
|
||||||
await db.execute("DROP VIEW balances")
|
await db.execute("DROP VIEW balances")
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE VIEW balances AS
|
CREATE VIEW balances AS
|
||||||
SELECT wallet, SUM(amount - abs(fee)) AS balance
|
SELECT wallet, SUM(amount - abs(fee)) AS balance
|
||||||
FROM apipayments
|
FROM apipayments
|
||||||
WHERE (pending = false AND amount > 0) OR amount < 0
|
WHERE (pending = false AND amount > 0) OR amount < 0
|
||||||
GROUP BY wallet
|
GROUP BY wallet
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m012_add_currency_to_wallet(db: Connection):
|
async def m012_add_currency_to_wallet(db: Connection):
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
ALTER TABLE wallets ADD COLUMN currency TEXT
|
ALTER TABLE wallets ADD COLUMN currency TEXT
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m013_add_deleted_to_wallets(db: Connection):
|
async def m013_add_deleted_to_wallets(db: Connection):
|
||||||
@@ -345,15 +317,13 @@ async def m014_set_deleted_wallets(db: Connection):
|
|||||||
Sets deleted column to wallets.
|
Sets deleted column to wallets.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
result = await db.execute(
|
result = await db.execute("""
|
||||||
"""
|
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM wallets
|
FROM wallets
|
||||||
WHERE user LIKE 'del:%'
|
WHERE user LIKE 'del:%'
|
||||||
AND adminkey LIKE 'del:%'
|
AND adminkey LIKE 'del:%'
|
||||||
AND inkey LIKE 'del:%'
|
AND inkey LIKE 'del:%'
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
rows = result.mappings().all()
|
rows = result.mappings().all()
|
||||||
|
|
||||||
for row in rows:
|
for row in rows:
|
||||||
@@ -386,8 +356,7 @@ async def m014_set_deleted_wallets(db: Connection):
|
|||||||
|
|
||||||
|
|
||||||
async def m015_create_push_notification_subscriptions_table(db: Connection):
|
async def m015_create_push_notification_subscriptions_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute(f"""
|
||||||
f"""
|
|
||||||
CREATE TABLE IF NOT EXISTS webpush_subscriptions (
|
CREATE TABLE IF NOT EXISTS webpush_subscriptions (
|
||||||
endpoint TEXT NOT NULL,
|
endpoint TEXT NOT NULL,
|
||||||
"user" TEXT NOT NULL,
|
"user" TEXT NOT NULL,
|
||||||
@@ -396,8 +365,7 @@ async def m015_create_push_notification_subscriptions_table(db: Connection):
|
|||||||
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
|
||||||
PRIMARY KEY (endpoint, "user")
|
PRIMARY KEY (endpoint, "user")
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m016_add_username_column_to_accounts(db: Connection):
|
async def m016_add_username_column_to_accounts(db: Connection):
|
||||||
@@ -484,8 +452,7 @@ async def m018_balances_view_exclude_deleted(db: Connection):
|
|||||||
Make deleted wallets not show up in the balances view.
|
Make deleted wallets not show up in the balances view.
|
||||||
"""
|
"""
|
||||||
await db.execute("DROP VIEW balances")
|
await db.execute("DROP VIEW balances")
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE VIEW balances AS
|
CREATE VIEW balances AS
|
||||||
SELECT apipayments.wallet,
|
SELECT apipayments.wallet,
|
||||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
||||||
@@ -495,8 +462,7 @@ async def m018_balances_view_exclude_deleted(db: Connection):
|
|||||||
AND ((apipayments.pending = false AND apipayments.amount > 0)
|
AND ((apipayments.pending = false AND apipayments.amount > 0)
|
||||||
OR apipayments.amount < 0)
|
OR apipayments.amount < 0)
|
||||||
GROUP BY wallet
|
GROUP BY wallet
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m019_balances_view_based_on_wallets(db: Connection):
|
async def m019_balances_view_based_on_wallets(db: Connection):
|
||||||
@@ -505,8 +471,7 @@ async def m019_balances_view_based_on_wallets(db: Connection):
|
|||||||
Important for querying whole lnbits balances.
|
Important for querying whole lnbits balances.
|
||||||
"""
|
"""
|
||||||
await db.execute("DROP VIEW balances")
|
await db.execute("DROP VIEW balances")
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE VIEW balances AS
|
CREATE VIEW balances AS
|
||||||
SELECT apipayments.wallet,
|
SELECT apipayments.wallet,
|
||||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
||||||
@@ -516,8 +481,7 @@ async def m019_balances_view_based_on_wallets(db: Connection):
|
|||||||
AND ((apipayments.pending = false AND apipayments.amount > 0)
|
AND ((apipayments.pending = false AND apipayments.amount > 0)
|
||||||
OR apipayments.amount < 0)
|
OR apipayments.amount < 0)
|
||||||
GROUP BY apipayments.wallet
|
GROUP BY apipayments.wallet
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m020_add_column_column_to_user_extensions(db: Connection):
|
async def m020_add_column_column_to_user_extensions(db: Connection):
|
||||||
@@ -536,8 +500,7 @@ async def m021_add_success_failed_to_apipayments(db: Connection):
|
|||||||
await db.execute("UPDATE apipayments SET status = 'success' WHERE NOT pending")
|
await db.execute("UPDATE apipayments SET status = 'success' WHERE NOT pending")
|
||||||
|
|
||||||
await db.execute("DROP VIEW balances")
|
await db.execute("DROP VIEW balances")
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE VIEW balances AS
|
CREATE VIEW balances AS
|
||||||
SELECT apipayments.wallet,
|
SELECT apipayments.wallet,
|
||||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
||||||
@@ -549,8 +512,7 @@ async def m021_add_success_failed_to_apipayments(db: Connection):
|
|||||||
OR (apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)
|
OR (apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)
|
||||||
)
|
)
|
||||||
GROUP BY apipayments.wallet
|
GROUP BY apipayments.wallet
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m022_add_pubkey_to_accounts(db: Connection):
|
async def m022_add_pubkey_to_accounts(db: Connection):
|
||||||
@@ -581,8 +543,7 @@ async def m024_drop_pending(db: Connection):
|
|||||||
|
|
||||||
async def m025_refresh_view(db: Connection):
|
async def m025_refresh_view(db: Connection):
|
||||||
await db.execute("DROP VIEW balances")
|
await db.execute("DROP VIEW balances")
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE VIEW balances AS
|
CREATE VIEW balances AS
|
||||||
SELECT apipayments.wallet_id,
|
SELECT apipayments.wallet_id,
|
||||||
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
|
||||||
@@ -594,8 +555,7 @@ async def m025_refresh_view(db: Connection):
|
|||||||
OR (apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)
|
OR (apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)
|
||||||
)
|
)
|
||||||
GROUP BY apipayments.wallet_id
|
GROUP BY apipayments.wallet_id
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m026_update_payment_table(db: Connection):
|
async def m026_update_payment_table(db: Connection):
|
||||||
@@ -658,8 +618,7 @@ async def m027_update_apipayments_data(db: Connection):
|
|||||||
|
|
||||||
async def m028_update_settings(db: Connection):
|
async def m028_update_settings(db: Connection):
|
||||||
|
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS system_settings (
|
CREATE TABLE IF NOT EXISTS system_settings (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
value TEXT,
|
value TEXT,
|
||||||
@@ -667,8 +626,7 @@ async def m028_update_settings(db: Connection):
|
|||||||
|
|
||||||
UNIQUE (id, tag)
|
UNIQUE (id, tag)
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
async def _insert_key_value(id_: str, value: Any):
|
async def _insert_key_value(id_: str, value: Any):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
@@ -691,8 +649,7 @@ async def m028_update_settings(db: Connection):
|
|||||||
|
|
||||||
|
|
||||||
async def m029_create_audit_table(db: Connection):
|
async def m029_create_audit_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute(f"""
|
||||||
f"""
|
|
||||||
CREATE TABLE IF NOT EXISTS audit (
|
CREATE TABLE IF NOT EXISTS audit (
|
||||||
component TEXT,
|
component TEXT,
|
||||||
ip_address TEXT,
|
ip_address TEXT,
|
||||||
@@ -706,16 +663,13 @@ async def m029_create_audit_table(db: Connection):
|
|||||||
delete_at TIMESTAMP,
|
delete_at TIMESTAMP,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m030_add_user_api_tokens_column(db: Connection):
|
async def m030_add_user_api_tokens_column(db: Connection):
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
ALTER TABLE accounts ADD COLUMN access_control_list TEXT
|
ALTER TABLE accounts ADD COLUMN access_control_list TEXT
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m031_add_color_and_icon_to_wallets(db: Connection):
|
async def m031_add_color_and_icon_to_wallets(db: Connection):
|
||||||
@@ -738,32 +692,25 @@ async def m033_update_payment_table(db: Connection):
|
|||||||
|
|
||||||
|
|
||||||
async def m034_add_stored_paylinks_to_wallet(db: Connection):
|
async def m034_add_stored_paylinks_to_wallet(db: Connection):
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
ALTER TABLE wallets ADD COLUMN stored_paylinks TEXT
|
ALTER TABLE wallets ADD COLUMN stored_paylinks TEXT
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m035_add_wallet_type_column(db: Connection):
|
async def m035_add_wallet_type_column(db: Connection):
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
ALTER TABLE wallets ADD COLUMN wallet_type TEXT DEFAULT 'lightning'
|
ALTER TABLE wallets ADD COLUMN wallet_type TEXT DEFAULT 'lightning'
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m036_add_shared_wallet_column(db: Connection):
|
async def m036_add_shared_wallet_column(db: Connection):
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
ALTER TABLE wallets ADD COLUMN shared_wallet_id TEXT
|
ALTER TABLE wallets ADD COLUMN shared_wallet_id TEXT
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m037_create_assets_table(db: Connection):
|
async def m037_create_assets_table(db: Connection):
|
||||||
await db.execute(
|
await db.execute(f"""
|
||||||
f"""
|
|
||||||
CREATE TABLE IF NOT EXISTS assets (
|
CREATE TABLE IF NOT EXISTS assets (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
user_id TEXT NOT NULL,
|
user_id TEXT NOT NULL,
|
||||||
@@ -776,16 +723,13 @@ async def m037_create_assets_table(db: Connection):
|
|||||||
data {db.blob} NOT NULL,
|
data {db.blob} NOT NULL,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||||
);
|
);
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m038_add_labels_for_payments(db: Connection):
|
async def m038_add_labels_for_payments(db: Connection):
|
||||||
await db.execute(
|
await db.execute("""
|
||||||
"""
|
|
||||||
ALTER TABLE apipayments ADD COLUMN labels TEXT
|
ALTER TABLE apipayments ADD COLUMN labels TEXT
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m039_index_payments(db: Connection):
|
async def m039_index_payments(db: Connection):
|
||||||
@@ -804,11 +748,9 @@ async def m039_index_payments(db: Connection):
|
|||||||
]
|
]
|
||||||
for index in indexes:
|
for index in indexes:
|
||||||
logger.debug(f"Creating index idx_payments_{index}...")
|
logger.debug(f"Creating index idx_payments_{index}...")
|
||||||
await db.execute(
|
await db.execute(f"""
|
||||||
f"""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_payments_{index} ON apipayments ({index});
|
CREATE INDEX IF NOT EXISTS idx_payments_{index} ON apipayments ({index});
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m040_index_wallets(db: Connection):
|
async def m040_index_wallets(db: Connection):
|
||||||
@@ -825,11 +767,9 @@ async def m040_index_wallets(db: Connection):
|
|||||||
|
|
||||||
for index in indexes:
|
for index in indexes:
|
||||||
logger.debug(f"Creating index idx_wallets_{index}...")
|
logger.debug(f"Creating index idx_wallets_{index}...")
|
||||||
await db.execute(
|
await db.execute(f"""
|
||||||
f"""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_wallets_{index} ON wallets ("{index}");
|
CREATE INDEX IF NOT EXISTS idx_wallets_{index} ON wallets ("{index}");
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m042_index_accounts(db: Connection):
|
async def m042_index_accounts(db: Connection):
|
||||||
@@ -843,8 +783,22 @@ async def m042_index_accounts(db: Connection):
|
|||||||
|
|
||||||
for index in indexes:
|
for index in indexes:
|
||||||
logger.debug(f"Creating index idx_wallets_{index}...")
|
logger.debug(f"Creating index idx_wallets_{index}...")
|
||||||
await db.execute(
|
await db.execute(f"""
|
||||||
f"""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_accounts_{index} ON accounts ("{index}");
|
CREATE INDEX IF NOT EXISTS idx_accounts_{index} ON accounts ("{index}");
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
async def m043_add_ui_customization_to_accounts(db: Connection):
|
||||||
|
"""
|
||||||
|
Adds ui_customization column to accounts.
|
||||||
|
Used for server side persistence of UI customization settings.
|
||||||
|
"""
|
||||||
|
await db.execute("ALTER TABLE accounts ADD COLUMN ui_customization TEXT")
|
||||||
|
|
||||||
|
|
||||||
|
async def m044_add_activated_to_accounts(db: Connection):
|
||||||
|
"""
|
||||||
|
Adds activated column to accounts.
|
||||||
|
Used for account activation status.
|
||||||
|
"""
|
||||||
|
await db.execute("ALTER TABLE accounts ADD COLUMN activated BOOLEAN DEFAULT true")
|
||||||
|
|||||||
@@ -269,10 +269,30 @@ class ExtensionRelease(BaseModel):
|
|||||||
async def get_github_releases(cls, org: str, repo: str) -> list[ExtensionRelease]:
|
async def get_github_releases(cls, org: str, repo: str) -> list[ExtensionRelease]:
|
||||||
try:
|
try:
|
||||||
github_releases = await cls.fetch_github_releases(org, repo)
|
github_releases = await cls.fetch_github_releases(org, repo)
|
||||||
return [
|
extension_releases = [
|
||||||
ExtensionRelease.from_github_release(f"{org}/{repo}", r)
|
ExtensionRelease.from_github_release(f"{org}/{repo}", r)
|
||||||
for r in github_releases
|
for r in github_releases
|
||||||
]
|
]
|
||||||
|
for release in extension_releases:
|
||||||
|
if not release.details_link:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
config = await ExtensionConfig.fetch_github_release_config(
|
||||||
|
org, repo, release.version
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
config = None
|
||||||
|
if not config:
|
||||||
|
continue
|
||||||
|
|
||||||
|
release.min_lnbits_version = config.min_lnbits_version
|
||||||
|
release.max_lnbits_version = config.max_lnbits_version
|
||||||
|
release.is_version_compatible = config.is_version_compatible()
|
||||||
|
|
||||||
|
release.icon = icon_to_github_url(f"{org}/{repo}", config.tile)
|
||||||
|
|
||||||
|
return extension_releases
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import random
|
||||||
|
import uuid
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
@@ -55,6 +57,29 @@ class DataField(BaseModel):
|
|||||||
field_type += ' = "sat"'
|
field_type += ' = "sat"'
|
||||||
return f"{field_name}: {field_type}"
|
return f"{field_name}: {field_type}"
|
||||||
|
|
||||||
|
def field_to_random_value(self) -> str:
|
||||||
|
field_name = camel_to_snake(self.name)
|
||||||
|
field_value: Any = f'"{self.type}"'
|
||||||
|
if self.type == "json":
|
||||||
|
field_value = '"{}"'
|
||||||
|
elif self.type == "wallet":
|
||||||
|
field_value = f'"{uuid.uuid4()}"'
|
||||||
|
elif self.type == "currency":
|
||||||
|
field_value = '"sat"'
|
||||||
|
elif self.type in ["str", "text"]:
|
||||||
|
field_value = f'"{field_name}_{urlsafe_short_hash()}"'
|
||||||
|
elif self.type == "int":
|
||||||
|
field_value = random.randint(1, 100) # noqa: S311
|
||||||
|
elif self.type == "float":
|
||||||
|
field_value = random.uniform(1.0, 100.0) # noqa: S311
|
||||||
|
elif self.type == "bool":
|
||||||
|
field_value = random.choice([True, False]) # noqa: S311
|
||||||
|
elif self.type == "datetime":
|
||||||
|
random_days = random.randint(-30, 30) # noqa: S311
|
||||||
|
random_date = datetime.now(timezone.utc) - timedelta(days=random_days)
|
||||||
|
field_value = f"""datetime.fromisoformat("{random_date.isoformat()}")"""
|
||||||
|
return f"{field_name} = {field_value},"
|
||||||
|
|
||||||
def field_to_js(self) -> str:
|
def field_to_js(self) -> str:
|
||||||
field_name = camel_to_snake(self.name)
|
field_name = camel_to_snake(self.name)
|
||||||
default_value = "null"
|
default_value = "null"
|
||||||
|
|||||||
@@ -6,23 +6,16 @@ from typing import Literal
|
|||||||
|
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
from lnurl import LnurlWithdrawResponse
|
from lnurl import LnurlWithdrawResponse
|
||||||
|
from loguru import logger
|
||||||
from pydantic import BaseModel, Field, validator
|
from pydantic import BaseModel, Field, validator
|
||||||
|
|
||||||
from lnbits.db import FilterModel
|
from lnbits.db import FilterModel
|
||||||
from lnbits.fiat import get_fiat_provider
|
|
||||||
from lnbits.fiat.base import (
|
from lnbits.fiat.base import (
|
||||||
FiatPaymentFailedStatus,
|
|
||||||
FiatPaymentPendingStatus,
|
|
||||||
FiatPaymentStatus,
|
FiatPaymentStatus,
|
||||||
FiatPaymentSuccessStatus,
|
|
||||||
)
|
)
|
||||||
from lnbits.utils.exchange_rates import allowed_currencies
|
from lnbits.utils.exchange_rates import allowed_currencies
|
||||||
from lnbits.wallets import get_funding_source
|
|
||||||
from lnbits.wallets.base import (
|
from lnbits.wallets.base import (
|
||||||
PaymentFailedStatus,
|
|
||||||
PaymentPendingStatus,
|
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
PaymentSuccessStatus,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -130,59 +123,23 @@ class Payment(BaseModel):
|
|||||||
"fiat_"
|
"fiat_"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# DEPRECATED: in v1.5.0, use service check_payment_status instead
|
||||||
async def check_status(
|
async def check_status(
|
||||||
self, skip_internal_payment_notifications: bool | None = False
|
self, skip_internal_payment_notifications: bool | None = False
|
||||||
) -> PaymentStatus:
|
) -> PaymentStatus:
|
||||||
if self.is_internal:
|
logger.warning("payment.check_status() is deprecated.")
|
||||||
if self.success:
|
from lnbits.core.services.payments import check_payment_status
|
||||||
return PaymentSuccessStatus()
|
|
||||||
if self.failed:
|
|
||||||
return PaymentFailedStatus()
|
|
||||||
if self.is_in and self.fiat_provider:
|
|
||||||
fiat_status = await self.check_fiat_status(
|
|
||||||
skip_internal_payment_notifications
|
|
||||||
)
|
|
||||||
return PaymentStatus(paid=fiat_status.paid)
|
|
||||||
return PaymentPendingStatus()
|
|
||||||
funding_source = get_funding_source()
|
|
||||||
if self.is_out:
|
|
||||||
status = await funding_source.get_payment_status(self.checking_id)
|
|
||||||
else:
|
|
||||||
status = await funding_source.get_invoice_status(self.checking_id)
|
|
||||||
return status
|
|
||||||
|
|
||||||
|
return await check_payment_status(self, skip_internal_payment_notifications)
|
||||||
|
|
||||||
|
# DEPRECATED: in v1.5.0, use service check_payment_status instead
|
||||||
async def check_fiat_status(
|
async def check_fiat_status(
|
||||||
self, skip_internal_payment_notifications: bool | None = False
|
self, skip_internal_payment_notifications: bool | None = False
|
||||||
) -> FiatPaymentStatus:
|
) -> FiatPaymentStatus:
|
||||||
if not self.is_internal:
|
logger.warning("payment.check_fiat_status() is deprecated.")
|
||||||
return FiatPaymentPendingStatus()
|
from lnbits.core.services.fiat_providers import check_fiat_status
|
||||||
if self.success:
|
|
||||||
return FiatPaymentSuccessStatus()
|
|
||||||
if self.failed:
|
|
||||||
return FiatPaymentFailedStatus()
|
|
||||||
|
|
||||||
if not self.fiat_provider:
|
return await check_fiat_status(self, skip_internal_payment_notifications)
|
||||||
return FiatPaymentPendingStatus()
|
|
||||||
|
|
||||||
checking_id = self.extra.get("fiat_checking_id")
|
|
||||||
if not checking_id:
|
|
||||||
return FiatPaymentPendingStatus()
|
|
||||||
|
|
||||||
fiat_provider = await get_fiat_provider(self.fiat_provider)
|
|
||||||
if not fiat_provider:
|
|
||||||
return FiatPaymentPendingStatus()
|
|
||||||
fiat_status = await fiat_provider.get_invoice_status(checking_id)
|
|
||||||
|
|
||||||
if skip_internal_payment_notifications:
|
|
||||||
return fiat_status
|
|
||||||
|
|
||||||
if fiat_status.success:
|
|
||||||
# notify receivers asynchronously
|
|
||||||
from lnbits.tasks import internal_invoice_queue
|
|
||||||
|
|
||||||
await internal_invoice_queue.put(self.checking_id)
|
|
||||||
|
|
||||||
return fiat_status
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentFilters(FilterModel):
|
class PaymentFilters(FilterModel):
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""SSO authentication providers for LNbits"""
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"""Generic OIDC SSO Login Helper"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from fastapi_sso.sso.base import DiscoveryDocument, OpenID, SSOBase
|
||||||
|
|
||||||
|
|
||||||
|
class OidcSSO(SSOBase):
|
||||||
|
"""Class providing login via Generic OIDC OAuth (e.g., Zitadel, Authentik, etc.)"""
|
||||||
|
|
||||||
|
provider = "oidc"
|
||||||
|
scope = ["openid", "email", "profile"]
|
||||||
|
discovery_url = ""
|
||||||
|
|
||||||
|
async def openid_from_response(
|
||||||
|
self, response: dict, session: Optional["httpx.AsyncClient"] = None
|
||||||
|
) -> OpenID:
|
||||||
|
"""Return OpenID from user information provided by OIDC provider"""
|
||||||
|
return OpenID(
|
||||||
|
email=response.get("email", ""),
|
||||||
|
provider=self.provider,
|
||||||
|
id=response.get("sub"),
|
||||||
|
first_name=response.get("given_name"),
|
||||||
|
last_name=response.get("family_name"),
|
||||||
|
display_name=response.get("name") or response.get("preferred_username"),
|
||||||
|
picture=response.get("picture"),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_discovery_document(self) -> DiscoveryDocument:
|
||||||
|
"""Get document containing handy urls"""
|
||||||
|
async with httpx.AsyncClient() as session:
|
||||||
|
response = await session.get(self.discovery_url)
|
||||||
|
content = response.json()
|
||||||
|
|
||||||
|
return content
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
from pydantic import BaseModel
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
class TinyURL(BaseModel):
|
class TinyURL(BaseModel):
|
||||||
@@ -6,4 +8,4 @@ class TinyURL(BaseModel):
|
|||||||
url: str
|
url: str
|
||||||
endless: bool
|
endless: bool
|
||||||
wallet: str
|
wallet: str
|
||||||
time: float
|
time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
|||||||
@@ -181,12 +181,14 @@ class AccountId(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class Account(AccountId):
|
class Account(AccountId):
|
||||||
|
activated: bool = True
|
||||||
external_id: str | None = None # for external account linking
|
external_id: str | None = None # for external account linking
|
||||||
username: str | None = None
|
username: str | None = None
|
||||||
password_hash: str | None = None
|
password_hash: str | None = None
|
||||||
pubkey: str | None = None
|
pubkey: str | None = None
|
||||||
email: str | None = None
|
email: str | None = None
|
||||||
extra: UserExtra = UserExtra()
|
extra: UserExtra = UserExtra()
|
||||||
|
ui_customization: dict = Field(default_factory=dict)
|
||||||
|
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
@@ -240,6 +242,7 @@ class Account(AccountId):
|
|||||||
|
|
||||||
|
|
||||||
class AccountOverview(Account):
|
class AccountOverview(Account):
|
||||||
|
activated: bool = True
|
||||||
transaction_count: int | None = 0
|
transaction_count: int | None = 0
|
||||||
wallet_count: int | None = 0
|
wallet_count: int | None = 0
|
||||||
balance_msat: int | None = 0
|
balance_msat: int | None = 0
|
||||||
@@ -275,6 +278,7 @@ class AccountFilters(FilterModel):
|
|||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
|
activated: bool = True
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
email: str | None = None
|
email: str | None = None
|
||||||
@@ -288,6 +292,7 @@ class User(BaseModel):
|
|||||||
fiat_providers: list[str] = []
|
fiat_providers: list[str] = []
|
||||||
has_password: bool = False
|
has_password: bool = False
|
||||||
extra: UserExtra = UserExtra()
|
extra: UserExtra = UserExtra()
|
||||||
|
ui_customization: dict = Field(default_factory=dict)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def wallet_ids(self) -> list[str]:
|
def wallet_ids(self) -> list[str]:
|
||||||
@@ -313,6 +318,7 @@ class RegisterUser(BaseModel):
|
|||||||
username: str = Query(default=..., min_length=2, max_length=20)
|
username: str = Query(default=..., min_length=2, max_length=20)
|
||||||
password: str = Query(default=..., min_length=8, max_length=50)
|
password: str = Query(default=..., min_length=8, max_length=50)
|
||||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||||
|
invitation_code: str | None = Query(default=None, max_length=256)
|
||||||
|
|
||||||
|
|
||||||
class CreateUser(BaseModel):
|
class CreateUser(BaseModel):
|
||||||
@@ -356,6 +362,7 @@ class UpdateSuperuserPassword(BaseModel):
|
|||||||
username: str = Query(default=..., min_length=2, max_length=20)
|
username: str = Query(default=..., min_length=2, max_length=20)
|
||||||
password: str = Query(default=..., min_length=8, max_length=50)
|
password: str = Query(default=..., min_length=8, max_length=50)
|
||||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||||
|
first_install_token: str | None = Query(None)
|
||||||
|
|
||||||
|
|
||||||
class LoginUsr(BaseModel):
|
class LoginUsr(BaseModel):
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from .fiat_providers import check_fiat_status
|
||||||
from .funding_source import (
|
from .funding_source import (
|
||||||
get_balance_delta,
|
get_balance_delta,
|
||||||
switch_to_voidwallet,
|
switch_to_voidwallet,
|
||||||
@@ -7,6 +8,7 @@ from .notifications import enqueue_admin_notification, send_payment_notification
|
|||||||
from .payments import (
|
from .payments import (
|
||||||
calculate_fiat_amounts,
|
calculate_fiat_amounts,
|
||||||
cancel_hold_invoice,
|
cancel_hold_invoice,
|
||||||
|
check_payment_status,
|
||||||
check_transaction_status,
|
check_transaction_status,
|
||||||
check_wallet_limits,
|
check_wallet_limits,
|
||||||
create_fiat_invoice,
|
create_fiat_invoice,
|
||||||
@@ -40,6 +42,8 @@ __all__ = [
|
|||||||
"calculate_fiat_amounts",
|
"calculate_fiat_amounts",
|
||||||
"cancel_hold_invoice",
|
"cancel_hold_invoice",
|
||||||
"check_admin_settings",
|
"check_admin_settings",
|
||||||
|
"check_fiat_status",
|
||||||
|
"check_payment_status",
|
||||||
"check_transaction_status",
|
"check_transaction_status",
|
||||||
"check_wallet_limits",
|
"check_wallet_limits",
|
||||||
"check_webpush_settings",
|
"check_webpush_settings",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import io
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from fastapi import UploadFile
|
from fastapi import UploadFile
|
||||||
|
from loguru import logger
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from lnbits.core.crud.assets import create_asset, get_user_assets_count
|
from lnbits.core.crud.assets import create_asset, get_user_assets_count
|
||||||
@@ -29,17 +30,7 @@ async def create_user_asset(user_id: str, file: UploadFile, is_public: bool) ->
|
|||||||
f"File limit of {settings.lnbits_max_asset_size_mb}MB exceeded."
|
f"File limit of {settings.lnbits_max_asset_size_mb}MB exceeded."
|
||||||
)
|
)
|
||||||
|
|
||||||
image = Image.open(io.BytesIO(contents))
|
thumb_buffer = thumbnail_from_bytes(contents)
|
||||||
|
|
||||||
thumbnail_width = min(256, settings.lnbits_asset_thumbnail_width)
|
|
||||||
thumbnail_height = min(256, settings.lnbits_asset_thumbnail_height)
|
|
||||||
image.thumbnail((thumbnail_width, thumbnail_height))
|
|
||||||
|
|
||||||
# Save thumbnail to an in-memory buffer
|
|
||||||
thumb_buffer = io.BytesIO()
|
|
||||||
thumbnail_format = settings.lnbits_asset_thumbnail_format or "PNG"
|
|
||||||
image.save(thumb_buffer, format=thumbnail_format)
|
|
||||||
thumb_buffer.seek(0)
|
|
||||||
|
|
||||||
asset = Asset(
|
asset = Asset(
|
||||||
id=uuid4().hex,
|
id=uuid4().hex,
|
||||||
@@ -48,9 +39,32 @@ async def create_user_asset(user_id: str, file: UploadFile, is_public: bool) ->
|
|||||||
is_public=is_public,
|
is_public=is_public,
|
||||||
name=file.filename or "unnamed",
|
name=file.filename or "unnamed",
|
||||||
size_bytes=len(contents),
|
size_bytes=len(contents),
|
||||||
thumbnail_base64=base64.b64encode(thumb_buffer.getvalue()).decode("utf-8"),
|
thumbnail_base64=(
|
||||||
|
base64.b64encode(thumb_buffer.getvalue()).decode("utf-8")
|
||||||
|
if thumb_buffer
|
||||||
|
else None
|
||||||
|
),
|
||||||
data=contents,
|
data=contents,
|
||||||
)
|
)
|
||||||
|
|
||||||
await create_asset(asset)
|
await create_asset(asset)
|
||||||
return asset
|
return asset
|
||||||
|
|
||||||
|
|
||||||
|
def thumbnail_from_bytes(contents: bytes) -> io.BytesIO | None:
|
||||||
|
try:
|
||||||
|
image = Image.open(io.BytesIO(contents))
|
||||||
|
|
||||||
|
thumbnail_width = min(256, settings.lnbits_asset_thumbnail_width)
|
||||||
|
thumbnail_height = min(256, settings.lnbits_asset_thumbnail_height)
|
||||||
|
image.thumbnail((thumbnail_width, thumbnail_height))
|
||||||
|
|
||||||
|
# Save thumbnail to an in-memory buffer
|
||||||
|
thumb_buffer = io.BytesIO()
|
||||||
|
thumbnail_format = settings.lnbits_asset_thumbnail_format or "PNG"
|
||||||
|
image.save(thumb_buffer, format=thumbnail_format)
|
||||||
|
thumb_buffer.seek(0)
|
||||||
|
return thumb_buffer
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(f"Failed to create thumbnail: {exc}")
|
||||||
|
return None
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from lnbits.core.crud import (
|
|||||||
delete_installed_extension,
|
delete_installed_extension,
|
||||||
get_db_version,
|
get_db_version,
|
||||||
get_installed_extension,
|
get_installed_extension,
|
||||||
|
get_installed_extensions_count,
|
||||||
update_installed_extension_state,
|
update_installed_extension_state,
|
||||||
)
|
)
|
||||||
from lnbits.core.crud.extensions import (
|
from lnbits.core.crud.extensions import (
|
||||||
@@ -38,6 +39,8 @@ async def install_extension(
|
|||||||
if installed_ext and installed_ext.meta:
|
if installed_ext and installed_ext.meta:
|
||||||
ext_info.meta.payments = installed_ext.meta.payments
|
ext_info.meta.payments = installed_ext.meta.payments
|
||||||
|
|
||||||
|
await check_extensions_limit(installed_ext)
|
||||||
|
|
||||||
if not skip_download:
|
if not skip_download:
|
||||||
await ext_info.download_archive()
|
await ext_info.download_archive()
|
||||||
|
|
||||||
@@ -63,6 +66,15 @@ async def install_extension(
|
|||||||
return extension
|
return extension
|
||||||
|
|
||||||
|
|
||||||
|
async def check_extensions_limit(installed_ext: InstallableExtension | None = None):
|
||||||
|
if settings.lnbits_max_extensions == 0 or installed_ext:
|
||||||
|
return
|
||||||
|
|
||||||
|
extensions_count = await get_installed_extensions_count()
|
||||||
|
if extensions_count >= settings.lnbits_max_extensions:
|
||||||
|
raise ValueError("Max amount of extensions have been installed")
|
||||||
|
|
||||||
|
|
||||||
async def uninstall_extension(ext_id: str):
|
async def uninstall_extension(ext_id: str):
|
||||||
await stop_extension_background_work(ext_id)
|
await stop_extension_background_work(ext_id)
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from lnbits.helpers import (
|
|||||||
camel_to_words,
|
camel_to_words,
|
||||||
download_url,
|
download_url,
|
||||||
lowercase_first_letter,
|
lowercase_first_letter,
|
||||||
|
snake_to_camel,
|
||||||
)
|
)
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
@@ -195,13 +196,29 @@ def _copy_ext_stub_to_build_dir(
|
|||||||
ext_build_dir = Path(working_dir, new_ext_id, working_dir_name, new_ext_id)
|
ext_build_dir = Path(working_dir, new_ext_id, working_dir_name, new_ext_id)
|
||||||
shutil.rmtree(ext_build_dir, True)
|
shutil.rmtree(ext_build_dir, True)
|
||||||
|
|
||||||
shutil.copytree(ext_stub_cache_dir, ext_build_dir)
|
shutil.copytree(
|
||||||
|
ext_stub_cache_dir,
|
||||||
|
ext_build_dir,
|
||||||
|
ignore=shutil.ignore_patterns(
|
||||||
|
"__pycache__",
|
||||||
|
".git",
|
||||||
|
".env",
|
||||||
|
".venv",
|
||||||
|
"*.env",
|
||||||
|
"*.log",
|
||||||
|
"node_modules",
|
||||||
|
".mypy_cache",
|
||||||
|
".pytest_cache",
|
||||||
|
".ruff_cache",
|
||||||
|
),
|
||||||
|
)
|
||||||
return ext_build_dir
|
return ext_build_dir
|
||||||
|
|
||||||
|
|
||||||
def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None:
|
def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None:
|
||||||
parsed_data = _parse_extension_data(data)
|
parsed_data = _parse_extension_data(data)
|
||||||
for py_file in py_files:
|
test_files = [f"tests/{p.name}" for p in Path(ext_stub_dir, "tests").glob("*.py")]
|
||||||
|
for py_file in py_files + test_files:
|
||||||
template_path = Path(ext_stub_dir, py_file).as_posix()
|
template_path = Path(ext_stub_dir, py_file).as_posix()
|
||||||
rederer = _render_file(template_path, parsed_data)
|
rederer = _render_file(template_path, parsed_data)
|
||||||
with open(template_path, "w", encoding="utf-8") as f:
|
with open(template_path, "w", encoding="utf-8") as f:
|
||||||
@@ -209,7 +226,7 @@ def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None
|
|||||||
|
|
||||||
_remove_lines_with_string(template_path, remove_line_marker)
|
_remove_lines_with_string(template_path, remove_line_marker)
|
||||||
|
|
||||||
template_path = Path(ext_stub_dir, "static", "js", "index.js").as_posix()
|
template_path = Path(ext_stub_dir, "static", "index.js").as_posix()
|
||||||
rederer = _render_file(
|
rederer = _render_file(
|
||||||
template_path, {"preview": data.preview_action, **parsed_data}
|
template_path, {"preview": data.preview_action, **parsed_data}
|
||||||
)
|
)
|
||||||
@@ -234,9 +251,7 @@ def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None
|
|||||||
"settingsFormDialog.data",
|
"settingsFormDialog.data",
|
||||||
ext_stub_dir,
|
ext_stub_dir,
|
||||||
)
|
)
|
||||||
template_path = Path(
|
template_path = Path(ext_stub_dir, "static", "index.vue").as_posix()
|
||||||
ext_stub_dir, "templates", "extension_builder_stub", "index.html"
|
|
||||||
).as_posix()
|
|
||||||
rederer = _render_file(
|
rederer = _render_file(
|
||||||
template_path,
|
template_path,
|
||||||
{
|
{
|
||||||
@@ -263,12 +278,12 @@ def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None
|
|||||||
"publicClientData",
|
"publicClientData",
|
||||||
ext_stub_dir,
|
ext_stub_dir,
|
||||||
)
|
)
|
||||||
public_template_path = Path(
|
public_template_path = Path(ext_stub_dir, "static", "public_page.vue")
|
||||||
ext_stub_dir, "templates", "extension_builder_stub", "public_page.html"
|
public_component_path = Path(ext_stub_dir, "static", "public_page.js")
|
||||||
)
|
|
||||||
template_path = public_template_path.as_posix()
|
template_path = public_template_path.as_posix()
|
||||||
if not data.public_page.has_public_page:
|
if not data.public_page.has_public_page:
|
||||||
public_template_path.unlink(missing_ok=True)
|
public_template_path.unlink(missing_ok=True)
|
||||||
|
public_component_path.unlink(missing_ok=True)
|
||||||
else:
|
else:
|
||||||
rederer = _render_file(
|
rederer = _render_file(
|
||||||
template_path,
|
template_path,
|
||||||
@@ -308,8 +323,10 @@ def zip_directory(source_dir, zip_path):
|
|||||||
|
|
||||||
def _rename_extension_builder_stub(data: ExtensionData, extension_dir: Path) -> None:
|
def _rename_extension_builder_stub(data: ExtensionData, extension_dir: Path) -> None:
|
||||||
extension_dir_path = extension_dir.as_posix()
|
extension_dir_path = extension_dir.as_posix()
|
||||||
|
# the order of fields is important, do not chage
|
||||||
rename_values = {
|
rename_values = {
|
||||||
"extension_builder_stub_name": data.name,
|
"extension_builder_stub_name": data.name,
|
||||||
|
"extension_builder_stub_camel_name": snake_to_camel(data.id, True),
|
||||||
"extension_builder_stub_short_description": data.short_description or "",
|
"extension_builder_stub_short_description": data.short_description or "",
|
||||||
"extension_builder_stub": data.id,
|
"extension_builder_stub": data.id,
|
||||||
"OwnerData": data.owner_data.name,
|
"OwnerData": data.owner_data.name,
|
||||||
@@ -328,7 +345,7 @@ def _rename_extension_builder_stub(data: ExtensionData, extension_dir: Path) ->
|
|||||||
directory=extension_dir_path,
|
directory=extension_dir_path,
|
||||||
old_text=old_text,
|
old_text=old_text,
|
||||||
new_text=new_text,
|
new_text=new_text,
|
||||||
file_extensions=[".py", ".js", ".html", ".md", ".json", ".toml"],
|
file_extensions=[".py", ".js", ".vue", ".html", ".md", ".json", ".toml"],
|
||||||
)
|
)
|
||||||
|
|
||||||
_rename_files_and_dirs_in_directory(
|
_rename_files_and_dirs_in_directory(
|
||||||
@@ -406,6 +423,7 @@ def _parse_extension_data(data: ExtensionData) -> dict:
|
|||||||
"owner_data": {
|
"owner_data": {
|
||||||
"name": data.owner_data.name,
|
"name": data.owner_data.name,
|
||||||
"editable": data.owner_data.editable,
|
"editable": data.owner_data.editable,
|
||||||
|
"fields": [field.name for field in data.owner_data.fields],
|
||||||
"js_fields": [
|
"js_fields": [
|
||||||
field.field_to_js()
|
field.field_to_js()
|
||||||
for field in data.owner_data.fields
|
for field in data.owner_data.fields
|
||||||
@@ -432,6 +450,18 @@ def _parse_extension_data(data: ExtensionData) -> dict:
|
|||||||
],
|
],
|
||||||
"db_fields": [field.field_to_db() for field in data.owner_data.fields],
|
"db_fields": [field.field_to_db() for field in data.owner_data.fields],
|
||||||
"all_fields": [field.field_to_py() for field in data.owner_data.fields],
|
"all_fields": [field.field_to_py() for field in data.owner_data.fields],
|
||||||
|
"random_fields_values": [
|
||||||
|
field.field_to_random_value() for field in data.owner_data.fields
|
||||||
|
],
|
||||||
|
"public_fields": [
|
||||||
|
field.field_to_py()
|
||||||
|
for field in data.owner_data.fields
|
||||||
|
if field.name
|
||||||
|
in [
|
||||||
|
data.public_page.owner_data_fields.name,
|
||||||
|
data.public_page.owner_data_fields.description,
|
||||||
|
]
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"client_data": {
|
"client_data": {
|
||||||
"name": data.client_data.name,
|
"name": data.client_data.name,
|
||||||
@@ -457,6 +487,9 @@ def _parse_extension_data(data: ExtensionData) -> dict:
|
|||||||
],
|
],
|
||||||
"db_fields": [field.field_to_db() for field in data.client_data.fields],
|
"db_fields": [field.field_to_db() for field in data.client_data.fields],
|
||||||
"all_fields": [field.field_to_py() for field in data.client_data.fields],
|
"all_fields": [field.field_to_py() for field in data.client_data.fields],
|
||||||
|
"random_fields_values": [
|
||||||
|
field.field_to_random_value() for field in data.client_data.fields
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"settings_data": {
|
"settings_data": {
|
||||||
"enabled": data.settings_data.enabled,
|
"enabled": data.settings_data.enabled,
|
||||||
@@ -536,7 +569,7 @@ def _rename_files_and_dirs_in_directory(directory, old_text, new_text):
|
|||||||
logger.warning(f"Failed to rename directory {old_dir_path}: {e}")
|
logger.warning(f"Failed to rename directory {old_dir_path}: {e}")
|
||||||
|
|
||||||
|
|
||||||
def _is_excluded_dir(path):
|
def _is_excluded_dir(path: str) -> bool:
|
||||||
for excluded_dir in excluded_dirs:
|
for excluded_dir in excluded_dirs:
|
||||||
if path.startswith(excluded_dir):
|
if path.startswith(excluded_dir):
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ from lnbits.core.models import CreatePayment, Payment, PaymentState
|
|||||||
from lnbits.core.models.misc import SimpleStatus
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
from lnbits.db import Connection
|
from lnbits.db import Connection
|
||||||
from lnbits.fiat import get_fiat_provider
|
from lnbits.fiat import get_fiat_provider
|
||||||
|
from lnbits.fiat.base import (
|
||||||
|
FiatPaymentFailedStatus,
|
||||||
|
FiatPaymentPendingStatus,
|
||||||
|
FiatPaymentStatus,
|
||||||
|
FiatPaymentSuccessStatus,
|
||||||
|
)
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
|
||||||
@@ -29,6 +35,40 @@ async def handle_fiat_payment_confirmation(
|
|||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
|
|
||||||
|
|
||||||
|
async def check_fiat_status(
|
||||||
|
payment: Payment, skip_internal_payment_notifications: bool | None = False
|
||||||
|
) -> FiatPaymentStatus:
|
||||||
|
if not payment.is_internal:
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
if payment.success:
|
||||||
|
return FiatPaymentSuccessStatus()
|
||||||
|
if payment.failed:
|
||||||
|
return FiatPaymentFailedStatus()
|
||||||
|
|
||||||
|
if not payment.fiat_provider:
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
|
||||||
|
checking_id = payment.extra.get("fiat_checking_id")
|
||||||
|
if not checking_id:
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
|
||||||
|
fiat_provider = await get_fiat_provider(payment.fiat_provider)
|
||||||
|
if not fiat_provider:
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
fiat_status = await fiat_provider.get_invoice_status(checking_id)
|
||||||
|
|
||||||
|
if skip_internal_payment_notifications:
|
||||||
|
return fiat_status
|
||||||
|
|
||||||
|
if fiat_status.success:
|
||||||
|
# notify receivers asynchronously
|
||||||
|
from lnbits.tasks import internal_invoice_queue
|
||||||
|
|
||||||
|
await internal_invoice_queue.put(payment.checking_id)
|
||||||
|
|
||||||
|
return fiat_status
|
||||||
|
|
||||||
|
|
||||||
def check_stripe_signature(
|
def check_stripe_signature(
|
||||||
payload: bytes,
|
payload: bytes,
|
||||||
sig_header: str | None,
|
sig_header: str | None,
|
||||||
@@ -78,8 +118,8 @@ async def verify_paypal_webhook(headers, payload: bytes):
|
|||||||
"""
|
"""
|
||||||
webhook_id = settings.paypal_webhook_id
|
webhook_id = settings.paypal_webhook_id
|
||||||
if not webhook_id:
|
if not webhook_id:
|
||||||
logger.warning("PayPal webhook ID not set; skipping verification.")
|
logger.warning("PayPal webhook ID not set.")
|
||||||
return
|
raise ValueError("PayPal webhook cannot be verified. Missing webhook ID.")
|
||||||
|
|
||||||
required_headers = {
|
required_headers = {
|
||||||
"PAYPAL-TRANSMISSION-ID": headers.get("PAYPAL-TRANSMISSION-ID"),
|
"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"),
|
"PAYPAL-AUTH-ALGO": headers.get("PAYPAL-AUTH-ALGO"),
|
||||||
}
|
}
|
||||||
if not all(required_headers.values()):
|
if not all(required_headers.values()):
|
||||||
logger.warning("Missing PayPal webhook headers; skipping verification.")
|
logger.warning("Missing PayPal webhook headers.")
|
||||||
return
|
raise ValueError("PayPal webhook cannot be verified. Missing headers.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(base_url=settings.paypal_api_endpoint) as client:
|
async with httpx.AsyncClient(base_url=settings.paypal_api_endpoint) as client:
|
||||||
|
|||||||
@@ -13,18 +13,19 @@ from lnbits.core.crud.payments import get_daily_stats
|
|||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
||||||
from lnbits.core.models.payments import CreateInvoice
|
from lnbits.core.models.payments import CreateInvoice
|
||||||
|
from lnbits.core.services.fiat_providers import handle_fiat_payment_confirmation
|
||||||
from lnbits.db import Connection, Filters
|
from lnbits.db import Connection, Filters
|
||||||
from lnbits.decorators import check_user_extension_access
|
from lnbits.decorators import check_user_extension_access
|
||||||
from lnbits.exceptions import InvoiceError, PaymentError, UnsupportedError
|
from lnbits.exceptions import InvoiceError, PaymentError, UnsupportedError
|
||||||
from lnbits.fiat import get_fiat_provider
|
from lnbits.fiat import get_fiat_provider
|
||||||
from lnbits.helpers import check_callback_url
|
from lnbits.helpers import check_callback_url
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.tasks import create_task, internal_invoice_queue_put
|
|
||||||
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash, verify_preimage
|
from lnbits.utils.crypto import fake_privkey, random_secret_and_hash, verify_preimage
|
||||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
|
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
|
||||||
from lnbits.wallets import fake_wallet, get_funding_source
|
from lnbits.wallets import fake_wallet, get_funding_source
|
||||||
from lnbits.wallets.base import (
|
from lnbits.wallets.base import (
|
||||||
InvoiceResponse,
|
InvoiceResponse,
|
||||||
|
PaymentFailedStatus,
|
||||||
PaymentPendingStatus,
|
PaymentPendingStatus,
|
||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
PaymentStatus,
|
PaymentStatus,
|
||||||
@@ -47,6 +48,7 @@ from ..models import (
|
|||||||
PaymentState,
|
PaymentState,
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
|
from .fiat_providers import check_fiat_status
|
||||||
from .notifications import send_payment_notification_in_background
|
from .notifications import send_payment_notification_in_background
|
||||||
|
|
||||||
payment_lock = asyncio.Lock()
|
payment_lock = asyncio.Lock()
|
||||||
@@ -364,7 +366,7 @@ async def update_pending_payments(wallet_id: str):
|
|||||||
async def update_pending_payment(
|
async def update_pending_payment(
|
||||||
payment: Payment, conn: Connection | None = None
|
payment: Payment, conn: Connection | None = None
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
status = await payment.check_status()
|
status = await check_payment_status(payment)
|
||||||
if status.failed:
|
if status.failed:
|
||||||
payment.status = PaymentState.FAILED
|
payment.status = PaymentState.FAILED
|
||||||
await update_payment(payment, conn=conn)
|
await update_payment(payment, conn=conn)
|
||||||
@@ -507,6 +509,8 @@ async def update_wallet_balance(
|
|||||||
)
|
)
|
||||||
payment.status = PaymentState.SUCCESS
|
payment.status = PaymentState.SUCCESS
|
||||||
await update_payment(payment, conn=conn)
|
await update_payment(payment, conn=conn)
|
||||||
|
from lnbits.tasks import internal_invoice_queue_put
|
||||||
|
|
||||||
await internal_invoice_queue_put(payment.checking_id)
|
await internal_invoice_queue_put(payment.checking_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -618,7 +622,29 @@ async def check_transaction_status(
|
|||||||
if payment.status == PaymentState.SUCCESS.value:
|
if payment.status == PaymentState.SUCCESS.value:
|
||||||
return PaymentSuccessStatus(fee_msat=payment.fee)
|
return PaymentSuccessStatus(fee_msat=payment.fee)
|
||||||
|
|
||||||
return await 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(
|
async def get_payments_daily_stats(
|
||||||
@@ -752,7 +778,7 @@ async def _pay_internal_invoice(
|
|||||||
await update_payment(internal_payment, conn=conn)
|
await update_payment(internal_payment, conn=conn)
|
||||||
logger.success(f"internal payment successful {internal_payment.checking_id}")
|
logger.success(f"internal payment successful {internal_payment.checking_id}")
|
||||||
|
|
||||||
send_payment_notification_in_background(wallet, payment)
|
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
|
||||||
|
|
||||||
# notify receiver asynchronously
|
# notify receiver asynchronously
|
||||||
from lnbits.tasks import internal_invoice_queue
|
from lnbits.tasks import internal_invoice_queue
|
||||||
@@ -795,6 +821,8 @@ async def _pay_external_invoice(
|
|||||||
|
|
||||||
fee_reserve_msat = fee_reserve(amount_msat, internal=False)
|
fee_reserve_msat = fee_reserve(amount_msat, internal=False)
|
||||||
|
|
||||||
|
from lnbits.tasks import create_task
|
||||||
|
|
||||||
task = create_task(
|
task = create_task(
|
||||||
_fundingsource_pay_invoice(checking_id, payment.bolt11, fee_reserve_msat)
|
_fundingsource_pay_invoice(checking_id, payment.bolt11, fee_reserve_msat)
|
||||||
)
|
)
|
||||||
@@ -825,7 +853,8 @@ async def _pay_external_invoice(
|
|||||||
payment = await update_payment_success_status(
|
payment = await update_payment_success_status(
|
||||||
payment, payment_response, conn=conn
|
payment, payment_response, conn=conn
|
||||||
)
|
)
|
||||||
send_payment_notification_in_background(wallet, payment)
|
|
||||||
|
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
|
||||||
logger.success(f"payment successful {payment_response.checking_id}")
|
logger.success(f"payment successful {payment_response.checking_id}")
|
||||||
|
|
||||||
payment.checking_id = payment_response.checking_id
|
payment.checking_id = payment_response.checking_id
|
||||||
@@ -868,7 +897,7 @@ async def _verify_external_payment(
|
|||||||
raise PaymentError("Payment already paid.", status="success")
|
raise PaymentError("Payment already paid.", status="success")
|
||||||
|
|
||||||
# payment failed
|
# payment failed
|
||||||
status = await payment.check_status()
|
status = await check_payment_status(payment)
|
||||||
if status.failed:
|
if status.failed:
|
||||||
raise PaymentError(
|
raise PaymentError(
|
||||||
"Payment is failed node, retrying is not possible.", status="failed"
|
"Payment is failed node, retrying is not possible.", status="failed"
|
||||||
@@ -1033,3 +1062,40 @@ async def cancel_hold_invoice(payment: Payment) -> InvoiceResponse:
|
|||||||
await update_payment(payment)
|
await update_payment(payment)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
async def _send_payment_notification_in_background(
|
||||||
|
wallet_id: str, payment: Payment, conn: Connection | None = None
|
||||||
|
):
|
||||||
|
# fetch balance again
|
||||||
|
wallet = await get_wallet(wallet_id, conn=conn)
|
||||||
|
if not wallet:
|
||||||
|
raise PaymentError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
||||||
|
send_payment_notification_in_background(wallet, payment)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_invoice_callback(checking_id: str) -> Payment | None:
|
||||||
|
"""
|
||||||
|
Takes a checking_id of an incoming payment, from either paid_invoices_stream()
|
||||||
|
or internal_invoice_queue. Checks its status, updates and returns it.
|
||||||
|
returns None if no payment was found or it not and incoming payment.
|
||||||
|
"""
|
||||||
|
payment = await get_standalone_payment(checking_id, incoming=True)
|
||||||
|
if not payment:
|
||||||
|
logger.warning(f"No payment found for '{checking_id}'.")
|
||||||
|
return None
|
||||||
|
if not payment.is_in:
|
||||||
|
logger.warning(f"Payment '{checking_id}' is not incoming, skipping.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
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
|
||||||
|
payment.status = PaymentState.SUCCESS
|
||||||
|
await update_payment(payment)
|
||||||
|
if payment.fiat_provider:
|
||||||
|
await handle_fiat_payment_confirmation(payment)
|
||||||
|
return payment
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ from uuid import uuid4
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.core.crud.settings import set_settings_field
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models.extensions import UserExtension
|
from lnbits.core.models.extensions import UserExtension
|
||||||
|
from lnbits.core.models.users import RegisterUser
|
||||||
from lnbits.db import Connection
|
from lnbits.db import Connection
|
||||||
from lnbits.settings import (
|
from lnbits.settings import (
|
||||||
EditableSettings,
|
EditableSettings,
|
||||||
@@ -21,6 +23,7 @@ from ..crud import (
|
|||||||
get_account_by_email,
|
get_account_by_email,
|
||||||
get_account_by_pubkey,
|
get_account_by_pubkey,
|
||||||
get_account_by_username,
|
get_account_by_username,
|
||||||
|
get_accounts_count,
|
||||||
get_super_settings,
|
get_super_settings,
|
||||||
get_user_extensions,
|
get_user_extensions,
|
||||||
get_user_from_account,
|
get_user_from_account,
|
||||||
@@ -53,6 +56,8 @@ async def create_user_account_no_ckeck(
|
|||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> User:
|
) -> User:
|
||||||
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||||
|
await check_users_limit(conn)
|
||||||
|
|
||||||
if account:
|
if account:
|
||||||
account.validate_fields()
|
account.validate_fields()
|
||||||
if account.username and await get_account_by_username(
|
if account.username and await get_account_by_username(
|
||||||
@@ -93,6 +98,15 @@ async def create_user_account_no_ckeck(
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
async def check_users_limit(conn: Connection | None = None):
|
||||||
|
if settings.lnbits_max_users == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
users_count = await get_accounts_count(conn=conn)
|
||||||
|
if users_count >= settings.lnbits_max_users:
|
||||||
|
raise ValueError("Max amount of users have been created")
|
||||||
|
|
||||||
|
|
||||||
async def update_user_account(account: Account) -> Account:
|
async def update_user_account(account: Account) -> Account:
|
||||||
account.validate_fields()
|
account.validate_fields()
|
||||||
|
|
||||||
@@ -171,6 +185,12 @@ async def check_admin_settings():
|
|||||||
if account and account.extra and account.extra.provider == "env":
|
if account and account.extra and account.extra.provider == "env":
|
||||||
settings.first_install = True
|
settings.first_install = True
|
||||||
|
|
||||||
|
if settings.has_first_install_token_changed():
|
||||||
|
logger.warning("First install token is changed. Resetting admin settings.")
|
||||||
|
new_settings = await init_admin_settings()
|
||||||
|
settings.super_user = new_settings.super_user
|
||||||
|
settings.first_install = True
|
||||||
|
|
||||||
logger.success(
|
logger.success(
|
||||||
"✔️ Admin UI is enabled. run `uv run lnbits-cli superuser` "
|
"✔️ Admin UI is enabled. run `uv run lnbits-cli superuser` "
|
||||||
"to get the superuser."
|
"to get the superuser."
|
||||||
@@ -192,3 +212,25 @@ async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
|
|||||||
|
|
||||||
editable_settings = EditableSettings.from_dict(settings.dict())
|
editable_settings = EditableSettings.from_dict(settings.dict())
|
||||||
return await create_admin_settings(account.id, editable_settings.dict())
|
return await create_admin_settings(account.id, editable_settings.dict())
|
||||||
|
|
||||||
|
|
||||||
|
async def check_register_activation_settings(data: RegisterUser):
|
||||||
|
if not settings.lnbits_require_user_activation:
|
||||||
|
return None
|
||||||
|
if settings.lnbits_user_activation_by_invitation_code:
|
||||||
|
code = data.invitation_code.strip() if data.invitation_code else ""
|
||||||
|
if len(code) == 0:
|
||||||
|
raise ValueError("Invitation code cannot be empty.")
|
||||||
|
|
||||||
|
if code == settings.lnbits_register_reusable_activation_code:
|
||||||
|
return None
|
||||||
|
if code in settings.lnbits_register_one_time_activation_codes:
|
||||||
|
settings.lnbits_register_one_time_activation_codes.remove(code)
|
||||||
|
await set_settings_field(
|
||||||
|
"lnbits_register_one_time_activation_codes",
|
||||||
|
settings.lnbits_register_one_time_activation_codes,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
raise ValueError("Invalid invitation code.")
|
||||||
|
|
||||||
|
raise ValueError("No activation method provided.")
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
|
||||||
%} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block page %}{%
|
|
||||||
endblock %}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{% extends "public.html" %} {% from "macros.jinja" import window_vars with
|
|
||||||
context %} {% block scripts %} {{ window_vars() }} {% endblock %} {% block page
|
|
||||||
%} {% endblock %}
|
|
||||||
@@ -57,11 +57,11 @@ async def api_get_asset(
|
|||||||
|
|
||||||
|
|
||||||
@asset_router.get(
|
@asset_router.get(
|
||||||
"/{asset_id}/binary",
|
"/{asset_id}/data",
|
||||||
name="Get user asset binary",
|
name="Get user asset data",
|
||||||
summary="Get user asset binary data by ID",
|
summary="Get user asset data data by ID",
|
||||||
)
|
)
|
||||||
async def api_get_asset_binary(
|
async def api_get_asset_data(
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
user_id: str | None = Depends(optional_user_id),
|
user_id: str | None = Depends(optional_user_id),
|
||||||
) -> Response:
|
) -> Response:
|
||||||
|
|||||||
+160
-14
@@ -4,13 +4,15 @@ import json
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from time import time
|
from time import time
|
||||||
|
from typing import Annotated
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
from fastapi import APIRouter, Cookie, Depends, HTTPException, Request
|
||||||
from fastapi.responses import JSONResponse, RedirectResponse
|
from fastapi.responses import JSONResponse, RedirectResponse
|
||||||
from fastapi_sso.sso.base import OpenID, SSOBase
|
from fastapi_sso.sso.base import OpenID, SSOBase
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.core.crud.settings import set_settings_field
|
||||||
from lnbits.core.crud.users import (
|
from lnbits.core.crud.users import (
|
||||||
get_user_access_control_lists,
|
get_user_access_control_lists,
|
||||||
update_user_access_control_list,
|
update_user_access_control_list,
|
||||||
@@ -25,10 +27,14 @@ from lnbits.core.models.users import (
|
|||||||
UpdateAccessControlList,
|
UpdateAccessControlList,
|
||||||
)
|
)
|
||||||
from lnbits.core.services import create_user_account
|
from lnbits.core.services import create_user_account
|
||||||
from lnbits.core.services.users import update_user_account
|
from lnbits.core.services.users import (
|
||||||
|
check_register_activation_settings,
|
||||||
|
update_user_account,
|
||||||
|
)
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
access_token_payload,
|
access_token_payload,
|
||||||
check_account_exists,
|
check_account_exists,
|
||||||
|
check_admin,
|
||||||
check_user_exists,
|
check_user_exists,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
@@ -84,6 +90,7 @@ async def login(data: LoginUsernamePassword) -> JSONResponse:
|
|||||||
account = await get_account_by_username_or_email(data.username)
|
account = await get_account_by_username_or_email(data.username)
|
||||||
if not account or not account.verify_password(data.password):
|
if not account or not account.verify_password(data.password):
|
||||||
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid credentials.")
|
||||||
|
|
||||||
return _auth_success_response(account.username, account.id, account.email)
|
return _auth_success_response(account.username, account.id, account.email)
|
||||||
|
|
||||||
|
|
||||||
@@ -92,7 +99,7 @@ async def nostr_login(request: Request) -> JSONResponse:
|
|||||||
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
|
if not settings.is_auth_method_allowed(AuthMethods.nostr_auth_nip98):
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "Login with Nostr Auth not allowed.")
|
||||||
event = _nostr_nip98_event(request)
|
event = _nostr_nip98_event(request)
|
||||||
account = await get_account_by_pubkey(event["pubkey"])
|
account = await get_account_by_pubkey(event["pubkey"], active_only=False)
|
||||||
if not account:
|
if not account:
|
||||||
account = Account(
|
account = Account(
|
||||||
id=uuid4().hex,
|
id=uuid4().hex,
|
||||||
@@ -100,6 +107,8 @@ async def nostr_login(request: Request) -> JSONResponse:
|
|||||||
extra=UserExtra(provider="nostr"),
|
extra=UserExtra(provider="nostr"),
|
||||||
)
|
)
|
||||||
await create_user_account(account)
|
await create_user_account(account)
|
||||||
|
if not account.activated:
|
||||||
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User is not activated.")
|
||||||
return _auth_success_response(account.username or "", account.id, account.email)
|
return _auth_success_response(account.username or "", account.id, account.email)
|
||||||
|
|
||||||
|
|
||||||
@@ -120,6 +129,79 @@ async def login_usr(data: LoginUsr) -> JSONResponse:
|
|||||||
return _auth_success_response(account.username, account.id, account.email)
|
return _auth_success_response(account.username, account.id, account.email)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_router.post("/impersonate", description="Login via the User ID of another user")
|
||||||
|
async def impersonate_user(
|
||||||
|
data: LoginUsr,
|
||||||
|
user: User = Depends(check_admin),
|
||||||
|
cookie_access_token: Annotated[str | None, Cookie()] = None,
|
||||||
|
) -> JSONResponse:
|
||||||
|
if not cookie_access_token:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.UNAUTHORIZED, "Only cookie based impersonation is allowed."
|
||||||
|
)
|
||||||
|
if data.usr == user.id:
|
||||||
|
raise HTTPException(HTTPStatus.FORBIDDEN, "You cannot impersonate yourself.")
|
||||||
|
if settings.is_admin_user(data.usr):
|
||||||
|
# this check includes the superuser
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.FORBIDDEN, "You cannot impersonate another admin user."
|
||||||
|
)
|
||||||
|
|
||||||
|
account = await get_account(data.usr)
|
||||||
|
if not account:
|
||||||
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "User ID does not exist.")
|
||||||
|
|
||||||
|
response = _auth_success_response(account.username, account.id, account.email)
|
||||||
|
|
||||||
|
max_age = settings.auth_token_expire_minutes * 60
|
||||||
|
response.set_cookie(
|
||||||
|
"admin_access_token",
|
||||||
|
cookie_access_token,
|
||||||
|
httponly=True,
|
||||||
|
secure=settings.auth_https_only,
|
||||||
|
samesite="lax",
|
||||||
|
max_age=max_age,
|
||||||
|
)
|
||||||
|
response.set_cookie(
|
||||||
|
"is_lnbits_user_impersonated",
|
||||||
|
"true",
|
||||||
|
secure=settings.auth_https_only,
|
||||||
|
samesite="lax",
|
||||||
|
max_age=max_age,
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@auth_router.delete(
|
||||||
|
"/impersonate", description="Stop impersonation and go back to admin"
|
||||||
|
)
|
||||||
|
async def stop_impersonate_user(
|
||||||
|
user: User = Depends(check_user_exists),
|
||||||
|
admin_access_token: Annotated[str | None, Cookie()] = None,
|
||||||
|
) -> JSONResponse:
|
||||||
|
if not admin_access_token:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.UNAUTHORIZED,
|
||||||
|
"No admin access token found to stop impersonation.",
|
||||||
|
)
|
||||||
|
response = JSONResponse(
|
||||||
|
{"access_token": admin_access_token, "token_type": "bearer"}
|
||||||
|
)
|
||||||
|
max_age = settings.auth_token_expire_minutes * 60
|
||||||
|
response.set_cookie(
|
||||||
|
"cookie_access_token",
|
||||||
|
admin_access_token,
|
||||||
|
httponly=True,
|
||||||
|
secure=settings.auth_https_only,
|
||||||
|
samesite="lax",
|
||||||
|
max_age=max_age,
|
||||||
|
)
|
||||||
|
response.delete_cookie("admin_access_token")
|
||||||
|
response.delete_cookie("is_access_token_expired")
|
||||||
|
response.delete_cookie("is_lnbits_user_impersonated")
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@auth_router.get("/acl")
|
@auth_router.get("/acl")
|
||||||
async def api_get_user_acls(
|
async def api_get_user_acls(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -298,12 +380,14 @@ async def register(data: RegisterUser) -> JSONResponse:
|
|||||||
if not is_valid_username(data.username):
|
if not is_valid_username(data.username):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
|
||||||
|
|
||||||
if await get_account_by_username(data.username):
|
if await get_account_by_username(data.username, active_only=False):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
||||||
|
|
||||||
if data.email and not is_valid_email_address(data.email):
|
if data.email and not is_valid_email_address(data.email):
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
||||||
|
|
||||||
|
await check_register_activation_settings(data)
|
||||||
|
|
||||||
account = Account(
|
account = Account(
|
||||||
id=uuid4().hex,
|
id=uuid4().hex,
|
||||||
email=data.email,
|
email=data.email,
|
||||||
@@ -408,7 +492,7 @@ async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
|||||||
return _auth_success_response(account.username, user_id, account.email)
|
return _auth_success_response(account.username, user_id, account.email)
|
||||||
|
|
||||||
|
|
||||||
@auth_router.put("/update")
|
@auth_router.patch("")
|
||||||
async def update(
|
async def update(
|
||||||
data: UpdateUser, account: Account = Depends(check_account_exists)
|
data: UpdateUser, account: Account = Depends(check_account_exists)
|
||||||
) -> User | None:
|
) -> User | None:
|
||||||
@@ -424,19 +508,54 @@ async def update(
|
|||||||
return await get_user_from_account(account)
|
return await get_user_from_account(account)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_router.patch("/ui")
|
||||||
|
async def update_ui_customization(
|
||||||
|
req: Request, account: Account = Depends(check_account_exists)
|
||||||
|
) -> Account:
|
||||||
|
ui_customization = await req.json()
|
||||||
|
account.ui_customization = {**(account.ui_customization or {}), **ui_customization}
|
||||||
|
|
||||||
|
if len(account.ui_customization or {}) > 1000 * 1024:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.BAD_REQUEST, "UI customization too large. Drop some fields."
|
||||||
|
)
|
||||||
|
|
||||||
|
await update_user_account(account)
|
||||||
|
return account
|
||||||
|
|
||||||
|
|
||||||
@auth_router.put("/first_install")
|
@auth_router.put("/first_install")
|
||||||
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
||||||
if not settings.first_install:
|
if not settings.first_install:
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "This is not your first install")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "This is not your first install")
|
||||||
|
|
||||||
|
if settings.first_install_token:
|
||||||
|
if not data.first_install_token:
|
||||||
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing first_install_token.")
|
||||||
|
if settings.first_install_token != data.first_install_token:
|
||||||
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid first_install_token.")
|
||||||
|
|
||||||
|
account = await get_account_by_username(data.username, False)
|
||||||
|
if account:
|
||||||
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
||||||
|
|
||||||
account = await get_account(settings.super_user)
|
account = await get_account(settings.super_user)
|
||||||
if not account:
|
if not account:
|
||||||
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
|
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
|
||||||
|
|
||||||
account.username = data.username
|
account.username = data.username
|
||||||
account.extra = account.extra or UserExtra()
|
account.extra = account.extra or UserExtra()
|
||||||
account.extra.provider = "lnbits"
|
account.extra.provider = "lnbits"
|
||||||
account.hash_password(data.password)
|
account.hash_password(data.password)
|
||||||
await update_account(account)
|
await update_account(account)
|
||||||
settings.first_install = False
|
settings.first_install = False
|
||||||
|
|
||||||
|
# only confrm it after the super user has been successfully updated
|
||||||
|
if settings.first_install_token:
|
||||||
|
settings.first_install_token_confirmed = data.first_install_token
|
||||||
|
await set_settings_field(
|
||||||
|
"first_install_token_confirmed", data.first_install_token
|
||||||
|
)
|
||||||
return _auth_success_response(account.username, account.id, account.email)
|
return _auth_success_response(account.username, account.id, account.email)
|
||||||
|
|
||||||
|
|
||||||
@@ -446,7 +565,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: str | None = Non
|
|||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid email.")
|
||||||
|
|
||||||
redirect_path = "/wallet"
|
redirect_path = "/wallet"
|
||||||
account = await get_account_by_email(email)
|
account = await get_account_by_email(email, active_only=False)
|
||||||
|
|
||||||
if verified_user_id:
|
if verified_user_id:
|
||||||
if account:
|
if account:
|
||||||
@@ -465,7 +584,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: str | None = Non
|
|||||||
id=uuid4().hex, email=email, extra=UserExtra(email_verified=True)
|
id=uuid4().hex, email=email, extra=UserExtra(email_verified=True)
|
||||||
)
|
)
|
||||||
await create_user_account(account)
|
await create_user_account(account)
|
||||||
return _auth_redirect_response(redirect_path, email)
|
return _auth_redirect_response(redirect_path, account.id, email)
|
||||||
|
|
||||||
|
|
||||||
def _auth_success_response(
|
def _auth_success_response(
|
||||||
@@ -480,9 +599,20 @@ def _auth_success_response(
|
|||||||
max_age = settings.auth_token_expire_minutes * 60
|
max_age = settings.auth_token_expire_minutes * 60
|
||||||
response = JSONResponse({"access_token": access_token, "token_type": "bearer"})
|
response = JSONResponse({"access_token": access_token, "token_type": "bearer"})
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
"cookie_access_token", access_token, httponly=True, max_age=max_age
|
"cookie_access_token",
|
||||||
|
access_token,
|
||||||
|
httponly=True,
|
||||||
|
secure=settings.auth_https_only,
|
||||||
|
samesite="lax",
|
||||||
|
max_age=max_age,
|
||||||
|
)
|
||||||
|
response.set_cookie(
|
||||||
|
"is_lnbits_user_authorized",
|
||||||
|
"true",
|
||||||
|
secure=settings.auth_https_only,
|
||||||
|
samesite="lax",
|
||||||
|
max_age=max_age,
|
||||||
)
|
)
|
||||||
response.set_cookie("is_lnbits_user_authorized", "true", max_age=max_age)
|
|
||||||
response.delete_cookie("is_access_token_expired")
|
response.delete_cookie("is_access_token_expired")
|
||||||
|
|
||||||
return response
|
return response
|
||||||
@@ -499,15 +629,28 @@ def _auth_api_token_response(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _auth_redirect_response(path: str, email: str) -> RedirectResponse:
|
def _auth_redirect_response(path: str, user_id: str, email: str) -> RedirectResponse:
|
||||||
payload = AccessTokenPayload(sub="" or "", email=email, auth_time=int(time()))
|
payload = AccessTokenPayload(
|
||||||
|
usr=user_id, sub="", email=email, auth_time=int(time())
|
||||||
|
)
|
||||||
access_token = create_access_token(data=payload.dict())
|
access_token = create_access_token(data=payload.dict())
|
||||||
max_age = settings.auth_token_expire_minutes * 60
|
max_age = settings.auth_token_expire_minutes * 60
|
||||||
response = RedirectResponse(path)
|
response = RedirectResponse(path)
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
"cookie_access_token", access_token, httponly=True, max_age=max_age
|
"cookie_access_token",
|
||||||
|
access_token,
|
||||||
|
httponly=True,
|
||||||
|
secure=settings.auth_https_only,
|
||||||
|
samesite="lax",
|
||||||
|
max_age=max_age,
|
||||||
|
)
|
||||||
|
response.set_cookie(
|
||||||
|
"is_lnbits_user_authorized",
|
||||||
|
"true",
|
||||||
|
secure=settings.auth_https_only,
|
||||||
|
samesite="lax",
|
||||||
|
max_age=max_age,
|
||||||
)
|
)
|
||||||
response.set_cookie("is_lnbits_user_authorized", "true", max_age=max_age)
|
|
||||||
response.delete_cookie("is_access_token_expired")
|
response.delete_cookie("is_access_token_expired")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -527,7 +670,10 @@ def _new_sso(provider: str) -> SSOBase | None:
|
|||||||
|
|
||||||
sso_provider_class = _find_auth_provider_class(provider)
|
sso_provider_class = _find_auth_provider_class(provider)
|
||||||
sso_provider = sso_provider_class(
|
sso_provider = sso_provider_class(
|
||||||
client_id, client_secret, None, allow_insecure_http=True
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
None,
|
||||||
|
allow_insecure_http=not settings.auth_https_only,
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
discovery_url
|
discovery_url
|
||||||
|
|||||||
@@ -9,10 +9,13 @@ from lnbits.core.crud.payments import (
|
|||||||
from lnbits.core.models.misc import SimpleStatus
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
from lnbits.core.models.payments import CreateInvoice
|
from lnbits.core.models.payments import CreateInvoice
|
||||||
from lnbits.core.services.fiat_providers import (
|
from lnbits.core.services.fiat_providers import (
|
||||||
|
check_fiat_status,
|
||||||
check_stripe_signature,
|
check_stripe_signature,
|
||||||
verify_paypal_webhook,
|
verify_paypal_webhook,
|
||||||
)
|
)
|
||||||
from lnbits.core.services.payments import create_fiat_invoice
|
from lnbits.core.services.payments import create_fiat_invoice
|
||||||
|
from lnbits.fiat import get_fiat_provider
|
||||||
|
from lnbits.fiat.paypal import PayPalWallet
|
||||||
from lnbits.fiat.base import FiatSubscriptionPaymentOptions
|
from lnbits.fiat.base import FiatSubscriptionPaymentOptions
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
@@ -23,7 +26,7 @@ callback_router = APIRouter(prefix="/api/v1/callback", tags=["callback"])
|
|||||||
async def api_generic_webhook_handler(
|
async def api_generic_webhook_handler(
|
||||||
provider_name: str, request: Request
|
provider_name: str, request: Request
|
||||||
) -> SimpleStatus:
|
) -> SimpleStatus:
|
||||||
|
logger.info(f"Received callback from provider: '{provider_name}'.")
|
||||||
if provider_name.lower() == "stripe":
|
if provider_name.lower() == "stripe":
|
||||||
payload = await request.body()
|
payload = await request.body()
|
||||||
sig_header = request.headers.get("Stripe-Signature")
|
sig_header = request.headers.get("Stripe-Signature")
|
||||||
@@ -87,7 +90,7 @@ async def _handle_stripe_intent_session_completed(event: dict):
|
|||||||
if not payment:
|
if not payment:
|
||||||
logger.warning(f"No payment found for hash: '{payment_hash}'.")
|
logger.warning(f"No payment found for hash: '{payment_hash}'.")
|
||||||
return
|
return
|
||||||
await payment.check_fiat_status()
|
await check_fiat_status(payment)
|
||||||
|
|
||||||
|
|
||||||
async def _handle_stripe_checkout_session_completed(event: dict):
|
async def _handle_stripe_checkout_session_completed(event: dict):
|
||||||
@@ -110,7 +113,7 @@ async def _handle_stripe_checkout_session_completed(event: dict):
|
|||||||
payment = await get_standalone_payment(payment_hash)
|
payment = await get_standalone_payment(payment_hash)
|
||||||
if not payment:
|
if not payment:
|
||||||
raise ValueError(f"No payment found for hash: '{payment_hash}'.")
|
raise ValueError(f"No payment found for hash: '{payment_hash}'.")
|
||||||
await payment.check_fiat_status()
|
await check_fiat_status(payment)
|
||||||
|
|
||||||
|
|
||||||
async def _handle_stripe_subscription_invoice_paid(event: dict):
|
async def _handle_stripe_subscription_invoice_paid(event: dict):
|
||||||
@@ -155,7 +158,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(
|
async def _get_stripe_subscription_payment_options(
|
||||||
@@ -180,10 +183,16 @@ async def _get_stripe_subscription_payment_options(
|
|||||||
|
|
||||||
|
|
||||||
async def handle_paypal_event(event: dict):
|
async def handle_paypal_event(event: dict):
|
||||||
|
event_id = event.get("id", "")
|
||||||
event_type = event.get("event_type", "")
|
event_type = event.get("event_type", "")
|
||||||
resource = event.get("resource", {})
|
resource = event.get("resource", {})
|
||||||
|
logger.info(f"Handling PayPal event: '{event_id}'. Type: '{event_type}'.")
|
||||||
|
|
||||||
if event_type in ("CHECKOUT.ORDER.APPROVED", "PAYMENT.CAPTURE.COMPLETED"):
|
if event_type == "CHECKOUT.ORDER.APPROVED":
|
||||||
|
await _handle_paypal_checkout_order_approved(resource)
|
||||||
|
return
|
||||||
|
|
||||||
|
if event_type == "PAYMENT.CAPTURE.COMPLETED":
|
||||||
payment_hash = _paypal_extract_payment_hash(resource)
|
payment_hash = _paypal_extract_payment_hash(resource)
|
||||||
if not payment_hash:
|
if not payment_hash:
|
||||||
logger.warning("PayPal event missing payment hash.")
|
logger.warning("PayPal event missing payment hash.")
|
||||||
@@ -192,23 +201,44 @@ async def handle_paypal_event(event: dict):
|
|||||||
if not payment:
|
if not payment:
|
||||||
logger.warning(f"No payment found for hash: '{payment_hash}'.")
|
logger.warning(f"No payment found for hash: '{payment_hash}'.")
|
||||||
return
|
return
|
||||||
await payment.check_fiat_status()
|
await check_fiat_status(payment)
|
||||||
return
|
return
|
||||||
|
|
||||||
if event_type in (
|
if event_type in ("PAYMENT.SALE.COMPLETED"):
|
||||||
"PAYMENT.SALE.COMPLETED",
|
|
||||||
"BILLING.SUBSCRIPTION.PAYMENT.SUCCEEDED",
|
|
||||||
):
|
|
||||||
await _handle_paypal_subscription_payment(resource)
|
await _handle_paypal_subscription_payment(resource)
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info(f"Unhandled PayPal event type: '{event_type}'.")
|
logger.warning(f"Unhandled PayPal event type: '{event_type}'.")
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_paypal_checkout_order_approved(resource: dict):
|
||||||
|
payment_hash = _paypal_extract_payment_hash(resource)
|
||||||
|
if not payment_hash:
|
||||||
|
logger.warning("PayPal approved event missing payment hash.")
|
||||||
|
return
|
||||||
|
|
||||||
|
payment = await get_standalone_payment(payment_hash)
|
||||||
|
if not payment:
|
||||||
|
logger.warning(f"No payment found for hash: '{payment_hash}'.")
|
||||||
|
return
|
||||||
|
|
||||||
|
fiat_provider = await get_fiat_provider("paypal")
|
||||||
|
if not isinstance(fiat_provider, PayPalWallet):
|
||||||
|
logger.warning("PayPal provider unavailable for approved order capture.")
|
||||||
|
return
|
||||||
|
|
||||||
|
capture_status = await fiat_provider.capture_order(
|
||||||
|
payment.extra.get("fiat_checking_id") or payment.checking_id
|
||||||
|
)
|
||||||
|
if capture_status.failed:
|
||||||
|
logger.warning(f"PayPal order capture failed for hash: '{payment_hash}'.")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
async def _handle_paypal_subscription_payment(resource: dict):
|
async def _handle_paypal_subscription_payment(resource: dict):
|
||||||
amount_info = resource.get("amount") or {}
|
amount_info = resource.get("amount") or {}
|
||||||
currency = (amount_info.get("currency_code") or "").upper()
|
currency = (amount_info.get("currency") or "").upper()
|
||||||
total = amount_info.get("value")
|
total = amount_info.get("total")
|
||||||
if not currency or total is None:
|
if not currency or total is None:
|
||||||
raise ValueError("PayPal subscription event missing amount.")
|
raise ValueError("PayPal subscription event missing amount.")
|
||||||
|
|
||||||
@@ -216,18 +246,14 @@ async def _handle_paypal_subscription_payment(resource: dict):
|
|||||||
if not custom_id:
|
if not custom_id:
|
||||||
raise ValueError("PayPal subscription event missing custom metadata.")
|
raise ValueError("PayPal subscription event missing custom metadata.")
|
||||||
|
|
||||||
try:
|
payment_options = _deserialize_paypal_metadata(custom_id)
|
||||||
metadata = json.loads(custom_id)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
metadata = {}
|
|
||||||
|
|
||||||
payment_options = FiatSubscriptionPaymentOptions(**metadata)
|
|
||||||
if not payment_options.wallet_id:
|
if not payment_options.wallet_id:
|
||||||
raise ValueError("PayPal subscription event missing wallet_id.")
|
raise ValueError("PayPal subscription event missing wallet_id.")
|
||||||
|
|
||||||
memo = payment_options.memo or ""
|
memo = payment_options.memo or ""
|
||||||
extra = {
|
extra = {
|
||||||
**(payment_options.extra or {}),
|
**(payment_options.extra or {}),
|
||||||
|
"subscription_request_id": resource.get("billing_agreement_id"),
|
||||||
"fiat_method": "subscription",
|
"fiat_method": "subscription",
|
||||||
"tag": payment_options.tag,
|
"tag": payment_options.tag,
|
||||||
"subscription": {
|
"subscription": {
|
||||||
@@ -247,7 +273,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:
|
def _paypal_extract_payment_hash(resource: dict) -> str | None:
|
||||||
@@ -258,3 +284,29 @@ def _paypal_extract_payment_hash(resource: dict) -> str | None:
|
|||||||
if pu.get("custom_id"):
|
if pu.get("custom_id"):
|
||||||
return pu.get("custom_id")
|
return pu.get("custom_id")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _deserialize_paypal_metadata(custom_id: str) -> FiatSubscriptionPaymentOptions:
|
||||||
|
try:
|
||||||
|
meta = json.loads(custom_id)
|
||||||
|
wallet_id = meta[0] if len(meta) > 0 else None
|
||||||
|
tag = meta[1] if len(meta) > 1 else None
|
||||||
|
subscription_request_id = meta[2] if len(meta) > 2 else None
|
||||||
|
extra_link = meta[3] if len(meta) > 3 else None
|
||||||
|
memo = meta[4] if len(meta) > 4 else None
|
||||||
|
|
||||||
|
extra = {
|
||||||
|
"link": extra_link,
|
||||||
|
"subscription_request_id": subscription_request_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
return FiatSubscriptionPaymentOptions(
|
||||||
|
wallet_id=wallet_id,
|
||||||
|
tag=tag,
|
||||||
|
subscription_request_id=subscription_request_id,
|
||||||
|
extra=extra,
|
||||||
|
memo=memo,
|
||||||
|
)
|
||||||
|
except (json.JSONDecodeError, IndexError) as e:
|
||||||
|
logger.warning(f"Failed to deserialize PayPal metadata: {e}")
|
||||||
|
return FiatSubscriptionPaymentOptions()
|
||||||
|
|||||||
@@ -98,12 +98,16 @@ async def api_install_extension(data: CreateExtension):
|
|||||||
ext_info.clean_extension_files()
|
ext_info.clean_extension_files()
|
||||||
detail = (
|
detail = (
|
||||||
str(exc)
|
str(exc)
|
||||||
if isinstance(exc, AssertionError)
|
if isinstance(exc, (AssertionError, ValueError))
|
||||||
else f"Failed to install extension '{ext_info.id}'."
|
else f"Failed to install extension '{ext_info.id}'."
|
||||||
f"({ext_info.installed_version})."
|
f"({ext_info.installed_version})."
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=(
|
||||||
|
HTTPStatus.BAD_REQUEST
|
||||||
|
if isinstance(exc, (AssertionError, ValueError))
|
||||||
|
else HTTPStatus.INTERNAL_SERVER_ERROR
|
||||||
|
),
|
||||||
detail=detail,
|
detail=detail,
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.models.misc import SimpleStatus
|
from lnbits.core.models.misc import SimpleStatus
|
||||||
from lnbits.core.models.wallets import WalletTypeInfo
|
from lnbits.core.models.wallets import WalletTypeInfo
|
||||||
@@ -30,6 +31,11 @@ async def create_subscription(
|
|||||||
data: CreateFiatSubscription,
|
data: CreateFiatSubscription,
|
||||||
key_type: WalletTypeInfo = Depends(require_admin_key),
|
key_type: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> FiatSubscriptionResponse:
|
) -> FiatSubscriptionResponse:
|
||||||
|
logger.debug(
|
||||||
|
f"Creating subscription with provider '{provider}. '"
|
||||||
|
f"Subscription ID '{data.subscription_id}'."
|
||||||
|
)
|
||||||
|
|
||||||
fiat_provider = await get_fiat_provider(provider)
|
fiat_provider = await get_fiat_provider(provider)
|
||||||
if not fiat_provider:
|
if not fiat_provider:
|
||||||
raise HTTPException(404, "Fiat provider not found")
|
raise HTTPException(404, "Fiat provider not found")
|
||||||
@@ -47,6 +53,12 @@ async def create_subscription(
|
|||||||
subscription_response = await fiat_provider.create_subscription(
|
subscription_response = await fiat_provider.create_subscription(
|
||||||
data.subscription_id, data.quantity, data.payment_options
|
data.subscription_id, data.quantity, data.payment_options
|
||||||
)
|
)
|
||||||
|
if subscription_response.error_message:
|
||||||
|
logger.warning(
|
||||||
|
f"Failed to create subscription with provider '{provider}': "
|
||||||
|
f"{subscription_response.error_message}"
|
||||||
|
)
|
||||||
|
|
||||||
return subscription_response
|
return subscription_response
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -42,54 +42,38 @@ async def robots():
|
|||||||
|
|
||||||
|
|
||||||
@generic_router.get(
|
@generic_router.get(
|
||||||
"/extensions/builder/preview/{ext_id}",
|
"/extensions/builder/preview/{ext_id}/{resource}",
|
||||||
name="extensions builder",
|
name="extensions builder",
|
||||||
dependencies=[Depends(check_extension_builder)],
|
dependencies=[Depends(check_extension_builder)],
|
||||||
)
|
)
|
||||||
async def extensions_builder_preview(
|
async def extensions_builder_preview(
|
||||||
request: Request,
|
|
||||||
ext_id: str,
|
ext_id: str,
|
||||||
|
resource: str | None = None,
|
||||||
page_name: str | None = None,
|
page_name: str | None = None,
|
||||||
user: User = Depends(check_user_exists),
|
user: User = Depends(check_user_exists),
|
||||||
) -> HTMLResponse:
|
) -> FileResponse:
|
||||||
working_dir_name = "preview_" + sha256(user.id.encode("utf-8")).hexdigest()
|
working_dir_name = "preview_" + sha256(user.id.encode("utf-8")).hexdigest()
|
||||||
html_file_name = "index.html"
|
|
||||||
if page_name == "public_page":
|
|
||||||
html_file_name = "public_page.html"
|
|
||||||
|
|
||||||
html_file_path = Path(
|
file_name = "index"
|
||||||
|
if page_name == "public_page":
|
||||||
|
file_name = "public_page"
|
||||||
|
|
||||||
|
resource_path = Path(
|
||||||
|
settings.extension_builder_working_dir_path,
|
||||||
"extension_builder_stub",
|
"extension_builder_stub",
|
||||||
ext_id,
|
ext_id,
|
||||||
working_dir_name,
|
working_dir_name,
|
||||||
ext_id,
|
ext_id,
|
||||||
"templates",
|
"static",
|
||||||
ext_id,
|
|
||||||
html_file_name,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
html_file_full_path = Path(
|
file_ext = ".vue" if resource == "template" else ".js"
|
||||||
settings.extension_builder_working_dir_path, html_file_path
|
file_full_path = Path(resource_path, file_name + file_ext)
|
||||||
)
|
|
||||||
|
|
||||||
if not html_file_full_path.is_file():
|
if not file_full_path.is_file():
|
||||||
return template_renderer().TemplateResponse(
|
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
|
||||||
request,
|
|
||||||
"error.html",
|
|
||||||
{
|
|
||||||
"status_code": 404,
|
|
||||||
"message": f"Extension {ext_id} not found, refresh Preview.",
|
|
||||||
},
|
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
|
||||||
)
|
|
||||||
|
|
||||||
response = template_renderer().TemplateResponse(
|
|
||||||
request,
|
|
||||||
html_file_path.as_posix(),
|
|
||||||
{
|
|
||||||
"user": user.json(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
response = FileResponse(file_full_path.absolute().as_posix())
|
||||||
response.headers["Content-Security-Policy"] = (
|
response.headers["Content-Security-Policy"] = (
|
||||||
"default-src 'self'; "
|
"default-src 'self'; "
|
||||||
"style-src 'self' 'unsafe-inline'; "
|
"style-src 'self' 'unsafe-inline'; "
|
||||||
@@ -208,12 +192,15 @@ admin_ui_checks = [Depends(check_admin), Depends(check_admin_ui)]
|
|||||||
@generic_router.get(
|
@generic_router.get(
|
||||||
"/extensions/builder", dependencies=[Depends(check_extension_builder)]
|
"/extensions/builder", dependencies=[Depends(check_extension_builder)]
|
||||||
)
|
)
|
||||||
|
@generic_router.get(
|
||||||
|
"/extensions/builder/preview", dependencies=[Depends(check_extension_builder)]
|
||||||
|
)
|
||||||
async def index(
|
async def index(
|
||||||
request: Request, user: User = Depends(check_user_exists)
|
request: Request, user: User = Depends(check_user_exists)
|
||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
return template_renderer().TemplateResponse(
|
return template_renderer().TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"index.html",
|
"base.html",
|
||||||
{
|
{
|
||||||
"user": user.json(),
|
"user": user.json(),
|
||||||
},
|
},
|
||||||
@@ -224,7 +211,7 @@ async def index(
|
|||||||
@generic_router.get("/node/public")
|
@generic_router.get("/node/public")
|
||||||
@generic_router.get("/first_install", dependencies=[Depends(check_first_install)])
|
@generic_router.get("/first_install", dependencies=[Depends(check_first_install)])
|
||||||
async def index_public(request: Request) -> HTMLResponse:
|
async def index_public(request: Request) -> HTMLResponse:
|
||||||
return template_renderer().TemplateResponse(request, "index.html", {"public": True})
|
return template_renderer().TemplateResponse(request, "base.html", {"public": True})
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/uuidv4/{hex_value}")
|
@generic_router.get("/uuidv4/{hex_value}")
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ lnurl_router = APIRouter(tags=["LNURL"])
|
|||||||
|
|
||||||
async def _handle(lnurl: str) -> LnurlResponseModel:
|
async def _handle(lnurl: str) -> LnurlResponseModel:
|
||||||
try:
|
try:
|
||||||
|
if "@" in lnurl: # lower case lightning addresses
|
||||||
|
lnurl = lnurl.lower()
|
||||||
res = await lnurl_handle(lnurl, user_agent=settings.user_agent, timeout=5)
|
res = await lnurl_handle(lnurl, user_agent=settings.user_agent, timeout=5)
|
||||||
if isinstance(res, LnurlErrorResponse):
|
if isinstance(res, LnurlErrorResponse):
|
||||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=res.reason)
|
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=res.reason)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from lnbits.core.crud import (
|
|||||||
update_admin_settings,
|
update_admin_settings,
|
||||||
update_wallet,
|
update_wallet,
|
||||||
)
|
)
|
||||||
|
from lnbits.core.crud.users import clear_user_id_cache, get_account, update_account
|
||||||
from lnbits.core.crud.wallets import delete_wallet_by_id
|
from lnbits.core.crud.wallets import delete_wallet_by_id
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
AccountFilters,
|
AccountFilters,
|
||||||
@@ -73,7 +74,7 @@ async def api_get_users(
|
|||||||
summary="Get user by Id",
|
summary="Get user by Id",
|
||||||
)
|
)
|
||||||
async def api_get_user(user_id: str) -> User:
|
async def api_get_user(user_id: str) -> User:
|
||||||
user = await get_user(user_id)
|
user = await get_user(user_id, active_only=False)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
|
raise HTTPException(HTTPStatus.NOT_FOUND, "User not found.")
|
||||||
return user
|
return user
|
||||||
@@ -197,7 +198,7 @@ async def api_users_reset_password(user_id: str) -> str:
|
|||||||
return f"reset_key_{reset_key_b64}"
|
return f"reset_key_{reset_key_b64}"
|
||||||
|
|
||||||
|
|
||||||
@users_router.get(
|
@users_router.put(
|
||||||
"/user/{user_id}/admin",
|
"/user/{user_id}/admin",
|
||||||
dependencies=[Depends(check_super_user)],
|
dependencies=[Depends(check_super_user)],
|
||||||
name="Give or revoke admin permsisions to a user",
|
name="Give or revoke admin permsisions to a user",
|
||||||
@@ -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")
|
@users_router.get("/user/{user_id}/wallet", name="Get wallets for user")
|
||||||
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
|
async def api_users_get_user_wallet(user_id: str) -> list[Wallet]:
|
||||||
return await get_wallets(user_id, deleted=None)
|
return await get_wallets(user_id, deleted=None)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from fastapi import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from lnbits.core.crud.wallets import (
|
from lnbits.core.crud.wallets import (
|
||||||
|
clear_wallet_cache,
|
||||||
create_wallet,
|
create_wallet,
|
||||||
get_wallets_paginated,
|
get_wallets_paginated,
|
||||||
)
|
)
|
||||||
@@ -37,7 +38,6 @@ from lnbits.decorators import (
|
|||||||
require_invoice_key,
|
require_invoice_key,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import generate_filter_params_openapi
|
from lnbits.helpers import generate_filter_params_openapi
|
||||||
from lnbits.utils.cache import cache
|
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
delete_wallet,
|
delete_wallet,
|
||||||
@@ -134,9 +134,7 @@ async def api_reset_wallet_keys(
|
|||||||
if not wallet or wallet.user != account_id.id:
|
if not wallet or wallet.user != account_id.id:
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
||||||
|
|
||||||
cache.pop(f"auth:wallet:{wallet.id}")
|
clear_wallet_cache(wallet)
|
||||||
cache.pop(f"auth:x-api-key:{wallet.adminkey}")
|
|
||||||
cache.pop(f"auth:x-api-key:{wallet.inkey}")
|
|
||||||
|
|
||||||
wallet.adminkey = uuid4().hex
|
wallet.adminkey = uuid4().hex
|
||||||
wallet.inkey = uuid4().hex
|
wallet.inkey = uuid4().hex
|
||||||
|
|||||||
+19
-39
@@ -12,7 +12,6 @@ from typing import Any, Generic, Literal, TypeVar, get_origin
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel, ValidationError, root_validator
|
from pydantic import BaseModel, ValidationError, root_validator
|
||||||
from sqlalchemy import event
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
|
||||||
from sqlalchemy.sql import text
|
from sqlalchemy.sql import text
|
||||||
|
|
||||||
@@ -36,7 +35,7 @@ if settings.lnbits_database_url:
|
|||||||
else:
|
else:
|
||||||
if not database_uri.startswith("postgres://"):
|
if not database_uri.startswith("postgres://"):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Please use the 'postgres://...' " "format for the database URL."
|
"Please use the 'postgres://...' format for the database URL."
|
||||||
)
|
)
|
||||||
DB_TYPE = POSTGRES
|
DB_TYPE = POSTGRES
|
||||||
|
|
||||||
@@ -56,14 +55,6 @@ def compat_timestamp_placeholder(key: str):
|
|||||||
return f":{key}"
|
return f":{key}"
|
||||||
|
|
||||||
|
|
||||||
def get_placeholder(model: Any, field: str) -> str:
|
|
||||||
type_ = model.__fields__[field].type_
|
|
||||||
if type_ == datetime:
|
|
||||||
return compat_timestamp_placeholder(field)
|
|
||||||
else:
|
|
||||||
return f":{field}"
|
|
||||||
|
|
||||||
|
|
||||||
class Compat:
|
class Compat:
|
||||||
type: str | None = "<inherited>"
|
type: str | None = "<inherited>"
|
||||||
schema: str | None = "<inherited>"
|
schema: str | None = "<inherited>"
|
||||||
@@ -326,30 +317,7 @@ class Database(Compat):
|
|||||||
self.engine: AsyncEngine = create_async_engine(
|
self.engine: AsyncEngine = create_async_engine(
|
||||||
database_uri, echo=settings.debug_database
|
database_uri, echo=settings.debug_database
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.type in {POSTGRES, COCKROACH}:
|
|
||||||
|
|
||||||
@event.listens_for(self.engine.sync_engine, "connect")
|
|
||||||
def register_custom_types(dbapi_connection, *_):
|
|
||||||
def _parse_date(value) -> datetime:
|
|
||||||
if value is None:
|
|
||||||
value = "1970-01-01 00:00:00"
|
|
||||||
f = "%Y-%m-%d %H:%M:%S.%f"
|
|
||||||
if "." not in value:
|
|
||||||
f = "%Y-%m-%d %H:%M:%S"
|
|
||||||
return datetime.strptime(value, f)
|
|
||||||
|
|
||||||
dbapi_connection.run_async(
|
|
||||||
lambda connection: connection.set_type_codec(
|
|
||||||
"TIMESTAMP",
|
|
||||||
encoder=datetime,
|
|
||||||
decoder=_parse_date,
|
|
||||||
schema="pg_catalog",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.lock = asyncio.Lock()
|
self.lock = asyncio.Lock()
|
||||||
|
|
||||||
logger.trace(f"database {self.type} added for {self.name}")
|
logger.trace(f"database {self.type} added for {self.name}")
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -592,7 +560,9 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
|
|
||||||
def pagination(self) -> str:
|
def pagination(self) -> str:
|
||||||
stmt = ""
|
stmt = ""
|
||||||
self.limit = self.limit or 10
|
if self.limit == 0:
|
||||||
|
self.limit = 1000
|
||||||
|
self.limit = 10 if self.limit is None else self.limit
|
||||||
stmt += f"LIMIT {min(1000, self.limit)} "
|
stmt += f"LIMIT {min(1000, self.limit)} "
|
||||||
if self.offset:
|
if self.offset:
|
||||||
stmt += f"OFFSET {self.offset}"
|
stmt += f"OFFSET {self.offset}"
|
||||||
@@ -645,6 +615,12 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
for page_filter in self.filters:
|
for page_filter in self.filters:
|
||||||
page_filter.table_name = table_name
|
page_filter.table_name = table_name
|
||||||
|
|
||||||
|
def get_filter_by_field(self, field: str) -> Filter[TFilterModel] | None:
|
||||||
|
return next((f for f in self.filters if f.field == field), None)
|
||||||
|
|
||||||
|
def remove_filter_by_field(self, field: str) -> None:
|
||||||
|
self.filters = [f for f in self.filters if f.field != field]
|
||||||
|
|
||||||
|
|
||||||
class DbJsonEncoder(json.JSONEncoder):
|
class DbJsonEncoder(json.JSONEncoder):
|
||||||
def default(self, o):
|
def default(self, o):
|
||||||
@@ -662,7 +638,7 @@ def insert_query(table_name: str, model: BaseModel) -> str:
|
|||||||
placeholders = []
|
placeholders = []
|
||||||
keys = model_to_dict(model).keys()
|
keys = model_to_dict(model).keys()
|
||||||
for field in keys:
|
for field in keys:
|
||||||
placeholders.append(get_placeholder(model, field))
|
placeholders.append(f":{field}")
|
||||||
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
||||||
fields = ", ".join([f'"{key}"' for key in keys])
|
fields = ", ".join([f'"{key}"' for key in keys])
|
||||||
values = ", ".join(placeholders)
|
values = ", ".join(placeholders)
|
||||||
@@ -680,9 +656,8 @@ def update_query(
|
|||||||
"""
|
"""
|
||||||
fields = []
|
fields = []
|
||||||
for field in model_to_dict(model).keys():
|
for field in model_to_dict(model).keys():
|
||||||
placeholder = get_placeholder(model, field)
|
|
||||||
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
|
||||||
fields.append(f'"{field}" = {placeholder}')
|
fields.append(f'"{field}" = :{field}')
|
||||||
query = ", ".join(fields)
|
query = ", ".join(fields)
|
||||||
return f"UPDATE {table_name} SET {query} {where}" # noqa: S608
|
return f"UPDATE {table_name} SET {query} {where}" # noqa: S608
|
||||||
|
|
||||||
@@ -700,7 +675,12 @@ def model_to_dict(model: BaseModel) -> dict:
|
|||||||
if model.__fields__[key].field_info.extra.get("no_database", False):
|
if model.__fields__[key].field_info.extra.get("no_database", False):
|
||||||
continue
|
continue
|
||||||
if isinstance(value, datetime):
|
if isinstance(value, datetime):
|
||||||
_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
|
continue
|
||||||
if (
|
if (
|
||||||
type(type_) is type(BaseModel)
|
type(type_) is type(BaseModel)
|
||||||
@@ -756,7 +736,7 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel: # noqa: C901
|
|||||||
if DB_TYPE == SQLITE:
|
if DB_TYPE == SQLITE:
|
||||||
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
|
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
|
||||||
else:
|
else:
|
||||||
_dict[key] = value
|
_dict[key] = value.replace(tzinfo=timezone.utc)
|
||||||
continue
|
continue
|
||||||
if issubclass(type_, BaseModel):
|
if issubclass(type_, BaseModel):
|
||||||
_dict[key] = dict_to_submodel(type_, value)
|
_dict[key] = dict_to_submodel(type_, value)
|
||||||
|
|||||||
@@ -261,6 +261,7 @@ async def check_account_id_exists(
|
|||||||
account_id,
|
account_id,
|
||||||
expiry=settings.auth_authentication_cache_minutes * 60,
|
expiry=settings.auth_authentication_cache_minutes * 60,
|
||||||
)
|
)
|
||||||
|
cache.set(f"auth:user:cache_key:{sha256s(account.id)}", cache_key)
|
||||||
|
|
||||||
return account_id
|
return account_id
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ def register_exception_handlers(app: FastAPI): # noqa: C901
|
|||||||
logger.error(f"RequestValidationError: {exc!s}")
|
logger.error(f"RequestValidationError: {exc!s}")
|
||||||
return render_html_error(request, exc) or JSONResponse(
|
return render_html_error(request, exc) or JSONResponse(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
content={"detail": str(exc)},
|
content={"detail": [dict(e) for e in exc.errors()]},
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.exception_handler(HTTPException)
|
@app.exception_handler(HTTPException)
|
||||||
|
|||||||
+88
-25
@@ -2,7 +2,7 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from typing import Any
|
from typing import Any, Literal
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -24,6 +24,8 @@ from .base import (
|
|||||||
FiatSubscriptionResponse,
|
FiatSubscriptionResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
FiatMethod = Literal["checkout", "subscription"]
|
||||||
|
|
||||||
|
|
||||||
class PayPalCheckoutOptions(BaseModel):
|
class PayPalCheckoutOptions(BaseModel):
|
||||||
class Config:
|
class Config:
|
||||||
@@ -34,11 +36,21 @@ class PayPalCheckoutOptions(BaseModel):
|
|||||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class PayPalSubscriptionOptions(BaseModel):
|
||||||
|
class Config:
|
||||||
|
extra = "ignore"
|
||||||
|
|
||||||
|
checking_id: str | None = None
|
||||||
|
payment_request: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class PayPalCreateInvoiceOptions(BaseModel):
|
class PayPalCreateInvoiceOptions(BaseModel):
|
||||||
class Config:
|
class Config:
|
||||||
extra = "ignore"
|
extra = "ignore"
|
||||||
|
|
||||||
|
fiat_method: FiatMethod = "checkout"
|
||||||
checkout: PayPalCheckoutOptions | None = None
|
checkout: PayPalCheckoutOptions | None = None
|
||||||
|
subscription: PayPalSubscriptionOptions | None = None
|
||||||
|
|
||||||
|
|
||||||
class PayPalWallet(FiatProvider):
|
class PayPalWallet(FiatProvider):
|
||||||
@@ -96,6 +108,14 @@ class PayPalWallet(FiatProvider):
|
|||||||
opts = self._parse_create_opts(extra or {})
|
opts = self._parse_create_opts(extra or {})
|
||||||
if opts is None:
|
if opts is None:
|
||||||
return FiatInvoiceResponse(ok=False, error_message="Invalid PayPal options")
|
return FiatInvoiceResponse(ok=False, error_message="Invalid PayPal options")
|
||||||
|
if opts.fiat_method == "subscription":
|
||||||
|
term = opts.subscription or PayPalSubscriptionOptions()
|
||||||
|
checking_id = term.checking_id or urlsafe_short_hash()
|
||||||
|
return FiatInvoiceResponse(
|
||||||
|
ok=True,
|
||||||
|
checking_id=f"subscription_{checking_id}",
|
||||||
|
payment_request=term.payment_request or "",
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self._ensure_access_token()
|
await self._ensure_access_token()
|
||||||
@@ -149,7 +169,7 @@ class PayPalWallet(FiatProvider):
|
|||||||
|
|
||||||
return FiatInvoiceResponse(
|
return FiatInvoiceResponse(
|
||||||
ok=True,
|
ok=True,
|
||||||
checking_id=f"fiat_paypal_{order_id}",
|
checking_id=order_id,
|
||||||
payment_request=approval_url,
|
payment_request=approval_url,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -171,6 +191,7 @@ class PayPalWallet(FiatProvider):
|
|||||||
or "https://lnbits.com"
|
or "https://lnbits.com"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.debug(f"Creating PayPal subscription with ID '{subscription_id}'")
|
||||||
if not payment_options.subscription_request_id:
|
if not payment_options.subscription_request_id:
|
||||||
payment_options.subscription_request_id = urlsafe_short_hash()
|
payment_options.subscription_request_id = urlsafe_short_hash()
|
||||||
payment_options.extra = payment_options.extra or {}
|
payment_options.extra = payment_options.extra or {}
|
||||||
@@ -182,7 +203,6 @@ class PayPalWallet(FiatProvider):
|
|||||||
await self._ensure_access_token()
|
await self._ensure_access_token()
|
||||||
payload = {
|
payload = {
|
||||||
"plan_id": subscription_id,
|
"plan_id": subscription_id,
|
||||||
"quantity": str(quantity),
|
|
||||||
"custom_id": self._serialize_metadata(payment_options),
|
"custom_id": self._serialize_metadata(payment_options),
|
||||||
"application_context": {
|
"application_context": {
|
||||||
"return_url": success_url,
|
"return_url": success_url,
|
||||||
@@ -205,7 +225,7 @@ class PayPalWallet(FiatProvider):
|
|||||||
return FiatSubscriptionResponse(
|
return FiatSubscriptionResponse(
|
||||||
ok=True,
|
ok=True,
|
||||||
checkout_session_url=approval_url,
|
checkout_session_url=approval_url,
|
||||||
subscription_request_id=payment_options.subscription_request_id,
|
subscription_request_id=data.get("id"),
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(exc)
|
logger.warning(exc)
|
||||||
@@ -219,6 +239,10 @@ class PayPalWallet(FiatProvider):
|
|||||||
correlation_id: str,
|
correlation_id: str,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> FiatSubscriptionResponse:
|
) -> FiatSubscriptionResponse:
|
||||||
|
logger.debug(
|
||||||
|
f"Cancelling PayPal subscription '{subscription_id}'. "
|
||||||
|
f"Correlation ID '{correlation_id}'."
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
await self._ensure_access_token()
|
await self._ensure_access_token()
|
||||||
r = await self.client.post(
|
r = await self.client.post(
|
||||||
@@ -240,12 +264,20 @@ class PayPalWallet(FiatProvider):
|
|||||||
async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
|
async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
|
||||||
try:
|
try:
|
||||||
await self._ensure_access_token()
|
await self._ensure_access_token()
|
||||||
order_id = self._normalize_paypal_id(checking_id)
|
paypal_id = self._normalize_paypal_id(checking_id)
|
||||||
r = await self.client.get(
|
if paypal_id.startswith("subscription_"):
|
||||||
f"/v2/checkout/orders/{order_id}", headers=self._auth_headers()
|
capture_id = paypal_id.replace("subscription_", "", 1)
|
||||||
)
|
r = await self.client.get(
|
||||||
r.raise_for_status()
|
f"v2/payments/captures/{capture_id}", headers=self._auth_headers()
|
||||||
return self._status_from_order(r.json())
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
return self._status_from_capture(r.json())
|
||||||
|
else:
|
||||||
|
r = await self.client.get(
|
||||||
|
f"/v2/checkout/orders/{paypal_id}", headers=self._auth_headers()
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
return self._status_from_order(r.json())
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug(f"Error getting PayPal order status: {exc}")
|
logger.debug(f"Error getting PayPal order status: {exc}")
|
||||||
return FiatPaymentPendingStatus()
|
return FiatPaymentPendingStatus()
|
||||||
@@ -253,6 +285,25 @@ class PayPalWallet(FiatProvider):
|
|||||||
async def get_payment_status(self, checking_id: str) -> FiatPaymentStatus:
|
async def get_payment_status(self, checking_id: str) -> FiatPaymentStatus:
|
||||||
raise NotImplementedError("PayPal does not support outgoing payments.")
|
raise NotImplementedError("PayPal does not support outgoing payments.")
|
||||||
|
|
||||||
|
async def capture_order(self, checking_id: str) -> FiatPaymentStatus:
|
||||||
|
try:
|
||||||
|
await self._ensure_access_token()
|
||||||
|
paypal_id = self._normalize_paypal_id(checking_id)
|
||||||
|
if paypal_id.startswith("subscription_"):
|
||||||
|
logger.warning("PayPal subscriptions do not support order capture.")
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
|
||||||
|
r = await self.client.post(
|
||||||
|
f"/v2/checkout/orders/{paypal_id}/capture",
|
||||||
|
json={},
|
||||||
|
headers=self._auth_headers(),
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
return self._status_from_order(r.json())
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(f"Error capturing PayPal order '{checking_id}': {exc}")
|
||||||
|
return await self.get_invoice_status(checking_id)
|
||||||
|
|
||||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"PayPal does not support paid invoices stream. Use webhooks instead."
|
"PayPal does not support paid invoices stream. Use webhooks instead."
|
||||||
@@ -264,31 +315,43 @@ class PayPalWallet(FiatProvider):
|
|||||||
|
|
||||||
def _status_from_order(self, order: dict[str, Any]) -> FiatPaymentStatus:
|
def _status_from_order(self, order: dict[str, Any]) -> FiatPaymentStatus:
|
||||||
status = (order.get("status") or "").upper()
|
status = (order.get("status") or "").upper()
|
||||||
if status in ["COMPLETED", "APPROVED"]:
|
if status == "COMPLETED":
|
||||||
return FiatPaymentSuccessStatus()
|
return FiatPaymentSuccessStatus()
|
||||||
if status in ["VOIDED", "CANCELLED", "CANCELED"]:
|
if status in ["VOIDED", "CANCELLED", "CANCELED"]:
|
||||||
return FiatPaymentFailedStatus()
|
return FiatPaymentFailedStatus()
|
||||||
return FiatPaymentPendingStatus()
|
return FiatPaymentPendingStatus()
|
||||||
|
|
||||||
|
def _status_from_capture(self, order: dict[str, Any]) -> FiatPaymentStatus:
|
||||||
|
status = (order.get("status") or "").upper()
|
||||||
|
if status in ["COMPLETED"]:
|
||||||
|
return FiatPaymentSuccessStatus()
|
||||||
|
if status in ["DECLINED", "FAILED", "CANCELLED", "CANCELED"]:
|
||||||
|
return FiatPaymentFailedStatus()
|
||||||
|
return FiatPaymentPendingStatus()
|
||||||
|
|
||||||
def _normalize_paypal_id(self, checking_id: str) -> str:
|
def _normalize_paypal_id(self, checking_id: str) -> str:
|
||||||
return (
|
normalized = checking_id
|
||||||
checking_id.replace("fiat_paypal_", "", 1)
|
while normalized.startswith("fiat_paypal_"):
|
||||||
if checking_id.startswith("fiat_paypal_")
|
normalized = normalized.replace("fiat_paypal_", "", 1)
|
||||||
else checking_id
|
return normalized
|
||||||
)
|
|
||||||
|
|
||||||
def _serialize_metadata(
|
def _serialize_metadata(
|
||||||
self, payment_options: FiatSubscriptionPaymentOptions
|
self, payment_options: FiatSubscriptionPaymentOptions
|
||||||
) -> str:
|
) -> str:
|
||||||
md = {
|
meta = [
|
||||||
"wallet_id": payment_options.wallet_id,
|
payment_options.wallet_id,
|
||||||
"memo": payment_options.memo,
|
payment_options.tag,
|
||||||
"extra": payment_options.extra,
|
payment_options.subscription_request_id,
|
||||||
"tag": payment_options.tag,
|
]
|
||||||
"subscription_request_id": payment_options.subscription_request_id,
|
if payment_options.extra:
|
||||||
}
|
meta.append(payment_options.extra.get("link", None))
|
||||||
raw = json.dumps(md)
|
|
||||||
return raw[:127] # PayPal custom_id limit
|
# 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(
|
def _parse_create_opts(
|
||||||
self, raw_opts: dict[str, Any]
|
self, raw_opts: dict[str, Any]
|
||||||
|
|||||||
+18
-55
@@ -7,15 +7,14 @@ from typing import Any
|
|||||||
from urllib import request
|
from urllib import request
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import jinja2
|
|
||||||
import jwt
|
import jwt
|
||||||
import shortuuid
|
import shortuuid
|
||||||
from fastapi.routing import APIRoute
|
from fastapi.routing import APIRoute
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from packaging import version
|
from packaging import version
|
||||||
from pydantic.schema import field_schema
|
from pydantic.schema import field_schema
|
||||||
|
from starlette.templating import Jinja2Templates
|
||||||
|
|
||||||
from lnbits.jinja2_templating import Jinja2Templates
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.crypto import AESCipher
|
from lnbits.utils.crypto import AESCipher
|
||||||
from lnbits.utils.exchange_rates import currencies
|
from lnbits.utils.exchange_rates import currencies
|
||||||
@@ -56,7 +55,6 @@ def static_url_for(static: str, path: str) -> str:
|
|||||||
def template_renderer(additional_folders: list | None = None) -> Jinja2Templates:
|
def template_renderer(additional_folders: list | None = None) -> Jinja2Templates:
|
||||||
folders = [
|
folders = [
|
||||||
"lnbits/templates",
|
"lnbits/templates",
|
||||||
"lnbits/core/templates",
|
|
||||||
settings.extension_builder_working_dir_path.as_posix(),
|
settings.extension_builder_working_dir_path.as_posix(),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -66,61 +64,15 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||||||
for f in additional_folders
|
for f in additional_folders
|
||||||
]
|
]
|
||||||
folders.extend(additional_folders)
|
folders.extend(additional_folders)
|
||||||
t = Jinja2Templates(loader=jinja2.FileSystemLoader(folders))
|
t = Jinja2Templates(directory=folders)
|
||||||
t.env.globals["static_url_for"] = static_url_for
|
t.env.globals["static_url_for"] = static_url_for
|
||||||
t.env.globals["normalize_path"] = normalize_path
|
t.env.globals["normalize_path"] = normalize_path
|
||||||
|
|
||||||
window_settings = {
|
# used in base.html
|
||||||
"AD_SPACE": settings.lnbits_ad_space,
|
t.env.globals["SITE_TITLE"] = settings.lnbits_site_title
|
||||||
"AD_SPACE_ENABLED": settings.lnbits_ad_space_enabled,
|
t.env.globals["LNBITS_APPLE_TOUCH_ICON"] = settings.lnbits_apple_touch_icon
|
||||||
"AD_SPACE_TITLE": settings.lnbits_ad_space_title,
|
t.env.globals["SETTINGS"] = settings.to_public().dict(by_alias=True)
|
||||||
"EXTENSIONS": list(settings.lnbits_installed_extensions_ids),
|
t.env.globals["CURRENCIES"] = list(currencies.keys())
|
||||||
"HIDE_API": settings.lnbits_hide_api,
|
|
||||||
"SITE_TITLE": settings.lnbits_site_title,
|
|
||||||
"SITE_TAGLINE": settings.lnbits_site_tagline,
|
|
||||||
"SITE_DESCRIPTION": settings.lnbits_site_description,
|
|
||||||
"LNBITS_ADMIN_UI": settings.lnbits_admin_ui,
|
|
||||||
"LNBITS_AUDIT_ENABLED": settings.lnbits_audit_enabled,
|
|
||||||
"LNBITS_AUTH_METHODS": settings.auth_allowed_methods,
|
|
||||||
"LNBITS_AUTH_KEYCLOAK_ORG": settings.keycloak_client_custom_org,
|
|
||||||
"LNBITS_AUTH_KEYCLOAK_ICON": settings.keycloak_client_custom_icon,
|
|
||||||
"LNBITS_CUSTOM_IMAGE": settings.lnbits_custom_image,
|
|
||||||
"LNBITS_CUSTOM_BADGE": settings.lnbits_custom_badge,
|
|
||||||
"LNBITS_CUSTOM_BADGE_COLOR": settings.lnbits_custom_badge_color,
|
|
||||||
"LNBITS_EXTENSIONS_DEACTIVATE_ALL": settings.lnbits_extensions_deactivate_all,
|
|
||||||
"LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed,
|
|
||||||
"LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager,
|
|
||||||
"LNBITS_NODE_UI_AVAILABLE": settings.has_nodemanager,
|
|
||||||
"LNBITS_QR_LOGO": settings.lnbits_qr_logo,
|
|
||||||
"LNBITS_APPLE_TOUCH_ICON": settings.lnbits_apple_touch_icon,
|
|
||||||
"LNBITS_SERVICE_FEE": settings.lnbits_service_fee,
|
|
||||||
"LNBITS_SERVICE_FEE_MAX": settings.lnbits_service_fee_max,
|
|
||||||
"LNBITS_SERVICE_FEE_WALLET": settings.lnbits_service_fee_wallet,
|
|
||||||
"LNBITS_SHOW_HOME_PAGE_ELEMENTS": settings.lnbits_show_home_page_elements,
|
|
||||||
"LNBITS_THEME_OPTIONS": settings.lnbits_theme_options,
|
|
||||||
"LNBITS_VERSION": settings.version,
|
|
||||||
"USE_CUSTOM_LOGO": settings.lnbits_custom_logo,
|
|
||||||
"LNBITS_DEFAULT_REACTION": settings.lnbits_default_reaction,
|
|
||||||
"LNBITS_DEFAULT_THEME": settings.lnbits_default_theme,
|
|
||||||
"LNBITS_DEFAULT_BORDER": settings.lnbits_default_border,
|
|
||||||
"LNBITS_DEFAULT_GRADIENT": settings.lnbits_default_gradient,
|
|
||||||
"LNBITS_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
|
||||||
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
|
||||||
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
|
||||||
"LNBITS_EXTENSIONS_REVIEWS_URL": settings.lnbits_extensions_reviews_url,
|
|
||||||
"LNBITS_DENOMINATION": settings.lnbits_denomination,
|
|
||||||
"has_holdinvoice": settings.has_holdinvoice,
|
|
||||||
"LNBITS_NOSTR_CONFIGURED": settings.is_nostr_notifications_configured(),
|
|
||||||
"LNBITS_TELEGRAM_CONFIGURED": settings.is_telegram_notifications_configured(),
|
|
||||||
"LNBITS_EXT_BUILDER": settings.lnbits_extensions_builder_activate_non_admins,
|
|
||||||
"LNBITS_CURRENCIES": list(currencies.keys()),
|
|
||||||
"LNBITS_ALLOWED_CURRENCIES": settings.lnbits_allowed_currencies,
|
|
||||||
"CACHE_KEY": settings.server_startup_time,
|
|
||||||
}
|
|
||||||
|
|
||||||
t.env.globals["WINDOW_SETTINGS"] = window_settings
|
|
||||||
for key, value in window_settings.items():
|
|
||||||
t.env.globals[key] = value
|
|
||||||
|
|
||||||
if settings.bundle_assets:
|
if settings.bundle_assets:
|
||||||
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
|
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
|
||||||
@@ -134,6 +86,9 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||||||
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
|
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
|
||||||
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
|
t.env.globals["INCLUDED_COMPONENTS"] = vendor_files["components"]
|
||||||
|
|
||||||
|
# backwards compatibility for extensions (tpos)
|
||||||
|
t.env.globals["LNBITS_DENOMINATION"] = settings.lnbits_denomination
|
||||||
|
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|
||||||
@@ -391,6 +346,14 @@ def camel_to_snake(name: str) -> str:
|
|||||||
return name.lower()
|
return name.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def snake_to_camel(name: str, capitalize_first: bool | None = False) -> str:
|
||||||
|
components = name.split("_")
|
||||||
|
camel = components[0] + "".join(x.capitalize() for x in components[1:])
|
||||||
|
if capitalize_first:
|
||||||
|
camel = camel[0].upper() + camel[1:]
|
||||||
|
return camel
|
||||||
|
|
||||||
|
|
||||||
def is_camel_case(v: str) -> bool:
|
def is_camel_case(v: str) -> bool:
|
||||||
return re.match(r"^[A-Z][a-z0-9]+([A-Z][a-z0-9]+)*$", v) is not None
|
return re.match(r"^[A-Z][a-z0-9]+([A-Z][a-z0-9]+)*$", v) is not None
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import typing
|
|
||||||
|
|
||||||
from jinja2 import BaseLoader, Environment, pass_context
|
|
||||||
from starlette.datastructures import QueryParams
|
|
||||||
from starlette.requests import Request
|
|
||||||
from starlette.templating import Jinja2Templates as SuperJinja2Templates
|
|
||||||
|
|
||||||
|
|
||||||
class Jinja2Templates(SuperJinja2Templates):
|
|
||||||
def __init__(self, loader: BaseLoader) -> None:
|
|
||||||
self.env = self.get_environment(loader)
|
|
||||||
super().__init__(env=self.env)
|
|
||||||
|
|
||||||
def get_environment(self, loader: BaseLoader) -> Environment:
|
|
||||||
@pass_context
|
|
||||||
def url_for(context: dict, name: str, **path_params: typing.Any) -> str:
|
|
||||||
request: Request = context["request"]
|
|
||||||
return request.app.url_path_for(name, **path_params)
|
|
||||||
|
|
||||||
def url_params_update(init: QueryParams, **new: typing.Any) -> QueryParams:
|
|
||||||
values = dict(init)
|
|
||||||
values.update(new)
|
|
||||||
return QueryParams(**values)
|
|
||||||
|
|
||||||
env = Environment(loader=loader, autoescape=True)
|
|
||||||
env.globals["url_for"] = url_for
|
|
||||||
env.globals["url_params_update"] = url_params_update
|
|
||||||
return env
|
|
||||||
@@ -7,6 +7,7 @@ from typing import Any
|
|||||||
from fastapi import FastAPI, Request, Response
|
from fastapi import FastAPI, Request, Response
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
from pyinstrument import Profiler
|
||||||
from slowapi import _rate_limit_exceeded_handler
|
from slowapi import _rate_limit_exceeded_handler
|
||||||
from slowapi.errors import RateLimitExceeded
|
from slowapi.errors import RateLimitExceeded
|
||||||
from slowapi.middleware import SlowAPIMiddleware
|
from slowapi.middleware import SlowAPIMiddleware
|
||||||
@@ -246,3 +247,16 @@ def add_first_install_middleware(app: FastAPI):
|
|||||||
):
|
):
|
||||||
return RedirectResponse("/first_install")
|
return RedirectResponse("/first_install")
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
|
|
||||||
|
|
||||||
|
def add_profiler_middleware(app: FastAPI):
|
||||||
|
@app.middleware("http")
|
||||||
|
async def profile_middleware(request: Request, call_next):
|
||||||
|
profiling = request.query_params.get("profiler", False)
|
||||||
|
if profiling:
|
||||||
|
profiler = Profiler(async_mode="enabled")
|
||||||
|
profiler.start()
|
||||||
|
_ = await call_next(request)
|
||||||
|
profiler.stop()
|
||||||
|
return HTMLResponse(profiler.output_html())
|
||||||
|
return await call_next(request)
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class LndRestNode(Node):
|
|||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
def get(self, path: str, **kwargs):
|
def get(self, path: str, **kwargs):
|
||||||
return self.request("GET", path, **kwargs)
|
return self.request("GET", path, timeout=30, **kwargs)
|
||||||
|
|
||||||
async def _get_id(self) -> str:
|
async def _get_id(self) -> str:
|
||||||
info = await self.get("/v1/getinfo")
|
info = await self.get("/v1/getinfo")
|
||||||
@@ -99,11 +99,12 @@ class LndRestNode(Node):
|
|||||||
"perm": True,
|
"perm": True,
|
||||||
"timeout": 30,
|
"timeout": 30,
|
||||||
},
|
},
|
||||||
|
timeout=30,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def disconnect_peer(self, peer_id: str):
|
async def disconnect_peer(self, peer_id: str):
|
||||||
try:
|
try:
|
||||||
await self.request("DELETE", "/v1/peers/" + peer_id)
|
await self.request("DELETE", "/v1/peers/" + peer_id, timeout=30)
|
||||||
except HTTPException as exc:
|
except HTTPException as exc:
|
||||||
if "unable to disconnect" in exc.detail:
|
if "unable to disconnect" in exc.detail:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -141,6 +142,7 @@ class LndRestNode(Node):
|
|||||||
"local_funding_amount": local_amount,
|
"local_funding_amount": local_amount,
|
||||||
"push_sat": push_amount,
|
"push_sat": push_amount,
|
||||||
},
|
},
|
||||||
|
timeout=30,
|
||||||
)
|
)
|
||||||
return ChannelPoint(
|
return ChannelPoint(
|
||||||
# WHY IS THIS REVERSED?!
|
# WHY IS THIS REVERSED?!
|
||||||
@@ -214,6 +216,7 @@ class LndRestNode(Node):
|
|||||||
# 'min_htlc_msat': <uint64>,
|
# 'min_htlc_msat': <uint64>,
|
||||||
# 'inbound_fee': <InboundFee>,
|
# 'inbound_fee': <InboundFee>,
|
||||||
},
|
},
|
||||||
|
timeout=30,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_channel(self, channel_id: str) -> NodeChannel | None:
|
async def get_channel(self, channel_id: str) -> NodeChannel | None:
|
||||||
|
|||||||
+195
-2
@@ -41,6 +41,14 @@ class UsersSettings(LNbitsSettings):
|
|||||||
lnbits_admin_users: list[str] = Field(default=[])
|
lnbits_admin_users: list[str] = Field(default=[])
|
||||||
lnbits_allowed_users: list[str] = Field(default=[])
|
lnbits_allowed_users: list[str] = Field(default=[])
|
||||||
lnbits_allow_new_accounts: bool = Field(default=True)
|
lnbits_allow_new_accounts: bool = Field(default=True)
|
||||||
|
lnbits_require_user_activation: bool = Field(default=False)
|
||||||
|
|
||||||
|
lnbits_user_activation_by_email: bool = Field(default=False)
|
||||||
|
lnbits_user_activation_by_payment: bool = Field(default=False)
|
||||||
|
lnbits_user_activation_by_invitation_code: bool = Field(default=False)
|
||||||
|
|
||||||
|
lnbits_register_reusable_activation_code: str = Field(default="")
|
||||||
|
lnbits_register_one_time_activation_codes: list[str] = Field(default=[])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def new_accounts_allowed(self) -> bool:
|
def new_accounts_allowed(self) -> bool:
|
||||||
@@ -252,6 +260,11 @@ class ThemesSettings(LNbitsSettings):
|
|||||||
)
|
)
|
||||||
lnbits_show_home_page_elements: bool = Field(default=True)
|
lnbits_show_home_page_elements: bool = Field(default=True)
|
||||||
lnbits_default_wallet_name: str = Field(default="LNbits wallet")
|
lnbits_default_wallet_name: str = Field(default="LNbits wallet")
|
||||||
|
|
||||||
|
lnbits_wallet_featured_button_label: str | None = Field(default=None)
|
||||||
|
lnbits_wallet_featured_button_url: str | None = Field(default=None)
|
||||||
|
lnbits_wallet_featured_button_icon: str | None = Field(default=None)
|
||||||
|
|
||||||
lnbits_custom_badge: str | None = Field(default=None)
|
lnbits_custom_badge: str | None = Field(default=None)
|
||||||
lnbits_custom_badge_color: str = Field(default="warning")
|
lnbits_custom_badge_color: str = Field(default="warning")
|
||||||
lnbits_theme_options: list[str] = Field(
|
lnbits_theme_options: list[str] = Field(
|
||||||
@@ -271,7 +284,7 @@ class ThemesSettings(LNbitsSettings):
|
|||||||
lnbits_custom_image: str | None = Field(default="/static/images/logos/lnbits.svg")
|
lnbits_custom_image: str | None = Field(default="/static/images/logos/lnbits.svg")
|
||||||
lnbits_ad_space_title: str = Field(default="Supported by")
|
lnbits_ad_space_title: str = Field(default="Supported by")
|
||||||
lnbits_ad_space: str = Field(
|
lnbits_ad_space: str = Field(
|
||||||
default="https://shop.lnbits.com/;/static/images/bitcoin-shop-banner.png;/static/images/bitcoin-shop-banner.png,https://affil.trezor.io/aff_c?offer_id=169&aff_id=33845;/static/images/bitcoin-hardware-wallet.png;/static/images/bitcoin-hardware-wallet.png,https://firefish.io/?ref=lnbits;/static/images/firefish.png;/static/images/firefish.png,https://opensats.org/;/static/images/open-sats.png;/static/images/open-sats.png"
|
default="https://shop.lnbits.com/;/static/images/bitcoin-shop-banner.png;/static/images/bitcoin-shop-banner.png,https://affil.trezor.io/aff_c?offer_id=169&aff_id=33845;/static/images/bitcoin-hardware-wallet.png;/static/images/bitcoin-hardware-wallet.png,https://firefish.io/?ref=lnbits;/static/images/firefish.png;/static/images/firefish.png"
|
||||||
) # sneaky sneaky
|
) # sneaky sneaky
|
||||||
lnbits_ad_space_enabled: bool = Field(default=False)
|
lnbits_ad_space_enabled: bool = Field(default=False)
|
||||||
lnbits_allowed_currencies: list[str] = Field(default=[])
|
lnbits_allowed_currencies: list[str] = Field(default=[])
|
||||||
@@ -279,10 +292,14 @@ class ThemesSettings(LNbitsSettings):
|
|||||||
lnbits_qr_logo: str = Field(default="/static/images/favicon_qr_logo.png")
|
lnbits_qr_logo: str = Field(default="/static/images/favicon_qr_logo.png")
|
||||||
lnbits_apple_touch_icon: str | None = Field(default=None)
|
lnbits_apple_touch_icon: str | None = Field(default=None)
|
||||||
lnbits_default_reaction: str = Field(default="confettiBothSides")
|
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_border: str = Field(default="hard-border")
|
||||||
lnbits_default_gradient: bool = Field(default=True)
|
lnbits_default_gradient: bool = Field(default=True)
|
||||||
lnbits_default_bgimage: str | None = Field(default=None)
|
lnbits_default_bgimage: str | None = Field(default=None)
|
||||||
|
lnbits_default_dark: bool = Field(default=True)
|
||||||
|
lnbits_default_card_rounded: bool = Field(default=True)
|
||||||
|
lnbits_default_card_gradient: bool = Field(default=True)
|
||||||
|
lnbits_default_card_shadow: bool = Field(default=False)
|
||||||
|
|
||||||
|
|
||||||
class OpsSettings(LNbitsSettings):
|
class OpsSettings(LNbitsSettings):
|
||||||
@@ -306,6 +323,11 @@ class AssetSettings(LNbitsSettings):
|
|||||||
"heic",
|
"heic",
|
||||||
"heif",
|
"heif",
|
||||||
"heics",
|
"heics",
|
||||||
|
"text/plain",
|
||||||
|
"text/json",
|
||||||
|
"text/xml",
|
||||||
|
"application/json",
|
||||||
|
"application/pdf",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
lnbits_asset_thumbnail_width: int = Field(default=128, ge=0)
|
lnbits_asset_thumbnail_width: int = Field(default=128, ge=0)
|
||||||
@@ -426,6 +448,7 @@ class SecuritySettings(LNbitsSettings):
|
|||||||
|
|
||||||
lnbits_max_outgoing_payment_amount_sats: int = Field(default=10_000_000, ge=0)
|
lnbits_max_outgoing_payment_amount_sats: int = Field(default=10_000_000, ge=0)
|
||||||
lnbits_max_incoming_payment_amount_sats: int = Field(default=10_000_000, ge=0)
|
lnbits_max_incoming_payment_amount_sats: int = Field(default=10_000_000, ge=0)
|
||||||
|
first_install_token_confirmed: str | None = Field(default=None)
|
||||||
|
|
||||||
def is_wallet_max_balance_exceeded(self, amount):
|
def is_wallet_max_balance_exceeded(self, amount):
|
||||||
return (
|
return (
|
||||||
@@ -566,6 +589,8 @@ class ZBDFundingSource(LNbitsSettings):
|
|||||||
class PhoenixdFundingSource(LNbitsSettings):
|
class PhoenixdFundingSource(LNbitsSettings):
|
||||||
phoenixd_api_endpoint: str | None = Field(default="http://localhost:9740/")
|
phoenixd_api_endpoint: str | None = Field(default="http://localhost:9740/")
|
||||||
phoenixd_api_password: str | None = Field(default=None)
|
phoenixd_api_password: str | None = Field(default=None)
|
||||||
|
phoenixd_data_dir: str | None = Field(default=None)
|
||||||
|
phoenixd_mnemonic: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class AlbyFundingSource(LNbitsSettings):
|
class AlbyFundingSource(LNbitsSettings):
|
||||||
@@ -585,6 +610,16 @@ class SparkFundingSource(LNbitsSettings):
|
|||||||
spark_token: str | None = Field(default=None)
|
spark_token: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
|
class SparkL2FundingSource(LNbitsSettings):
|
||||||
|
spark_l2_network: str = Field(default="MAINNET")
|
||||||
|
spark_l2_external_endpoint: str | None = Field(default="http://localhost:8765")
|
||||||
|
spark_l2_external_api_key: str | None = Field(default=None)
|
||||||
|
spark_l2_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):
|
class LnTipsFundingSource(LNbitsSettings):
|
||||||
lntips_api_endpoint: str | None = Field(default=None)
|
lntips_api_endpoint: str | None = Field(default=None)
|
||||||
lntips_api_key: str | None = Field(default=None)
|
lntips_api_key: str | None = Field(default=None)
|
||||||
@@ -690,6 +725,7 @@ class FundingSourcesSettings(
|
|||||||
PhoenixdFundingSource,
|
PhoenixdFundingSource,
|
||||||
OpenNodeFundingSource,
|
OpenNodeFundingSource,
|
||||||
SparkFundingSource,
|
SparkFundingSource,
|
||||||
|
SparkL2FundingSource,
|
||||||
LnTipsFundingSource,
|
LnTipsFundingSource,
|
||||||
NWCFundingSource,
|
NWCFundingSource,
|
||||||
BreezSdkFundingSource,
|
BreezSdkFundingSource,
|
||||||
@@ -700,6 +736,7 @@ class FundingSourcesSettings(
|
|||||||
# How long to wait for the payment to be confirmed before returning a pending status
|
# How long to wait for the payment to be confirmed before returning a pending status
|
||||||
# It will not fail the payment, it will make it return pending after the timeout
|
# It will not fail the payment, it will make it return pending after the timeout
|
||||||
lnbits_funding_source_pay_invoice_wait_seconds: int = Field(default=5, ge=0)
|
lnbits_funding_source_pay_invoice_wait_seconds: int = Field(default=5, ge=0)
|
||||||
|
lnbits_funding_source_pending_interval_seconds: int = Field(default=1800, ge=0)
|
||||||
funding_source_max_retries: int = Field(default=4, ge=0)
|
funding_source_max_retries: int = Field(default=4, ge=0)
|
||||||
|
|
||||||
|
|
||||||
@@ -764,6 +801,7 @@ class AuthMethods(Enum):
|
|||||||
google_auth = "google-auth"
|
google_auth = "google-auth"
|
||||||
github_auth = "github-auth"
|
github_auth = "github-auth"
|
||||||
keycloak_auth = "keycloak-auth"
|
keycloak_auth = "keycloak-auth"
|
||||||
|
oidc_auth = "oidc-auth"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def all(cls):
|
def all(cls):
|
||||||
@@ -774,6 +812,7 @@ class AuthMethods(Enum):
|
|||||||
AuthMethods.google_auth.value,
|
AuthMethods.google_auth.value,
|
||||||
AuthMethods.github_auth.value,
|
AuthMethods.github_auth.value,
|
||||||
AuthMethods.keycloak_auth.value,
|
AuthMethods.keycloak_auth.value,
|
||||||
|
AuthMethods.oidc_auth.value,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -819,6 +858,14 @@ class KeycloakAuthSettings(LNbitsSettings):
|
|||||||
keycloak_client_custom_icon: str | None = Field(default=None)
|
keycloak_client_custom_icon: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
|
class OidcAuthSettings(LNbitsSettings):
|
||||||
|
oidc_discovery_url: str = Field(default="")
|
||||||
|
oidc_client_id: str = Field(default="")
|
||||||
|
oidc_client_secret: str = Field(default="")
|
||||||
|
oidc_client_custom_org: str | None = Field(default=None)
|
||||||
|
oidc_client_custom_icon: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class AuditSettings(LNbitsSettings):
|
class AuditSettings(LNbitsSettings):
|
||||||
lnbits_audit_enabled: bool = Field(default=True)
|
lnbits_audit_enabled: bool = Field(default=True)
|
||||||
|
|
||||||
@@ -926,6 +973,7 @@ class EditableSettings(
|
|||||||
GoogleAuthSettings,
|
GoogleAuthSettings,
|
||||||
GitHubAuthSettings,
|
GitHubAuthSettings,
|
||||||
KeycloakAuthSettings,
|
KeycloakAuthSettings,
|
||||||
|
OidcAuthSettings,
|
||||||
):
|
):
|
||||||
@validator(
|
@validator(
|
||||||
"lnbits_admin_users",
|
"lnbits_admin_users",
|
||||||
@@ -961,6 +1009,9 @@ class EnvSettings(LNbitsSettings):
|
|||||||
debug: bool = Field(default=False)
|
debug: bool = Field(default=False)
|
||||||
debug_database: bool = Field(default=False)
|
debug_database: bool = Field(default=False)
|
||||||
bundle_assets: bool = Field(default=True)
|
bundle_assets: bool = Field(default=True)
|
||||||
|
profiler: bool = Field(default=False)
|
||||||
|
# When enabled, auth cookies require HTTPS and SSO will reject insecure HTTP.
|
||||||
|
auth_https_only: bool = Field(default=True)
|
||||||
host: str = Field(default="127.0.0.1")
|
host: str = Field(default="127.0.0.1")
|
||||||
port: int = Field(default=5000, gt=0)
|
port: int = Field(default=5000, gt=0)
|
||||||
forwarded_allow_ips: str = Field(default="*")
|
forwarded_allow_ips: str = Field(default="*")
|
||||||
@@ -974,14 +1025,24 @@ class EnvSettings(LNbitsSettings):
|
|||||||
enable_log_to_file: bool = Field(default=True)
|
enable_log_to_file: bool = Field(default=True)
|
||||||
log_rotation: str = Field(default="100 MB")
|
log_rotation: str = Field(default="100 MB")
|
||||||
log_retention: str = Field(default="3 months")
|
log_retention: str = Field(default="3 months")
|
||||||
|
first_install_token: str | None = Field(default=None)
|
||||||
|
|
||||||
cleanup_wallets_days: int = Field(default=90, ge=0)
|
cleanup_wallets_days: int = Field(default=90, ge=0)
|
||||||
funding_source_max_retries: int = Field(default=4, ge=0)
|
funding_source_max_retries: int = Field(default=4, ge=0)
|
||||||
|
lnbits_max_users: int = Field(default=0, ge=0)
|
||||||
|
lnbits_max_extensions: int = Field(default=0, ge=0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_default_extension_path(self) -> bool:
|
def has_default_extension_path(self) -> bool:
|
||||||
return self.lnbits_extensions_path == "lnbits"
|
return self.lnbits_extensions_path == "lnbits"
|
||||||
|
|
||||||
|
def has_first_install_token_changed(self) -> bool:
|
||||||
|
if not self.first_install_token:
|
||||||
|
return False
|
||||||
|
if not settings.first_install_token_confirmed:
|
||||||
|
return False
|
||||||
|
return self.first_install_token != settings.first_install_token_confirmed
|
||||||
|
|
||||||
def check_auth_secret_key(self):
|
def check_auth_secret_key(self):
|
||||||
if self.auth_secret_key:
|
if self.auth_secret_key:
|
||||||
return
|
return
|
||||||
@@ -1017,6 +1078,7 @@ class SuperUserSettings(LNbitsSettings):
|
|||||||
"FakeWallet",
|
"FakeWallet",
|
||||||
"LNPayWallet",
|
"LNPayWallet",
|
||||||
"LNbitsWallet",
|
"LNbitsWallet",
|
||||||
|
"SparkL2Wallet",
|
||||||
"LnTipsWallet",
|
"LnTipsWallet",
|
||||||
"LndRestWallet",
|
"LndRestWallet",
|
||||||
"LndWallet",
|
"LndWallet",
|
||||||
@@ -1122,6 +1184,137 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
|
|||||||
and ext_id in self.lnbits_all_extensions_ids
|
and ext_id in self.lnbits_all_extensions_ids
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_public(self) -> PublicSettings:
|
||||||
|
return PublicSettings.from_settings(self)
|
||||||
|
|
||||||
|
|
||||||
|
class PublicSettings(BaseModel):
|
||||||
|
"""
|
||||||
|
PublicSettings is used for templating and public information.
|
||||||
|
WARNING: do not expose private information, PublicSettings is exposed publicly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
allow_register: bool = Field(alias="allowRegister")
|
||||||
|
qr_logo: str = Field(alias="qrLogo")
|
||||||
|
site_title: str = Field(alias="siteTitle")
|
||||||
|
site_tagline: str = Field(alias="siteTagline")
|
||||||
|
site_description: str | None = Field(alias="siteDescription")
|
||||||
|
auth_methods: list[str] = Field(alias="authMethods")
|
||||||
|
keycloak_org: str | None = Field(alias="keycloakOrg")
|
||||||
|
keycloak_icon: str | None = Field(alias="keycloakIcon")
|
||||||
|
oidc_org: str | None = Field(alias="oidcOrg")
|
||||||
|
oidc_icon: str | None = Field(alias="oidcIcon")
|
||||||
|
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,
|
||||||
|
oidcOrg=settings.oidc_client_custom_org,
|
||||||
|
oidcIcon=settings.oidc_client_custom_icon,
|
||||||
|
hasHoldinvoice=settings.has_holdinvoice,
|
||||||
|
hasNodemanager=settings.has_nodemanager,
|
||||||
|
showNodemanager=settings.lnbits_node_ui and settings.has_nodemanager,
|
||||||
|
customLogo=settings.lnbits_custom_logo,
|
||||||
|
showVoidwallet=settings.lnbits_backend_wallet_class == "VoidWallet",
|
||||||
|
version=settings.version,
|
||||||
|
showHomePageElements=settings.lnbits_show_home_page_elements,
|
||||||
|
hideApi=settings.lnbits_hide_api,
|
||||||
|
cacheKey=str(settings.server_startup_time),
|
||||||
|
webpushPubkey=settings.lnbits_webpush_pubkey,
|
||||||
|
showExtensions=not settings.lnbits_extensions_deactivate_all,
|
||||||
|
showAudit=settings.lnbits_audit_enabled,
|
||||||
|
showAdmin=settings.lnbits_admin_ui,
|
||||||
|
customImage=settings.lnbits_custom_image,
|
||||||
|
customBadge=settings.lnbits_custom_badge,
|
||||||
|
customBadgeColor=settings.lnbits_custom_badge_color,
|
||||||
|
serviceFee=settings.lnbits_service_fee,
|
||||||
|
serviceFeeMax=settings.lnbits_service_fee_max,
|
||||||
|
serviceFeeWallet=settings.lnbits_service_fee_wallet,
|
||||||
|
themeOptions=settings.lnbits_theme_options,
|
||||||
|
defaultReaction=settings.lnbits_default_reaction,
|
||||||
|
defaultTheme=settings.lnbits_default_theme,
|
||||||
|
defaultBorder=settings.lnbits_default_border,
|
||||||
|
defaultGradient=settings.lnbits_default_gradient,
|
||||||
|
defaultBgimage=settings.lnbits_default_bgimage,
|
||||||
|
defaultDark=settings.lnbits_default_dark,
|
||||||
|
defaultCardRounded=settings.lnbits_default_card_rounded,
|
||||||
|
defaultCardGradient=settings.lnbits_default_card_gradient,
|
||||||
|
defaultCardShadow=settings.lnbits_default_card_shadow,
|
||||||
|
denomination=settings.lnbits_denomination,
|
||||||
|
extensions=list(settings.lnbits_installed_extensions_ids),
|
||||||
|
allowedCurrencies=settings.lnbits_allowed_currencies,
|
||||||
|
extensionsReviewsUrl=settings.lnbits_extensions_reviews_url,
|
||||||
|
extBuilder=settings.lnbits_extensions_builder_activate_non_admins,
|
||||||
|
nostrConfigured=settings.is_nostr_notifications_configured(),
|
||||||
|
telegramConfigured=settings.is_telegram_notifications_configured(),
|
||||||
|
walletFeaturedButtonLabel=settings.lnbits_wallet_featured_button_label,
|
||||||
|
walletFeaturedButtonUrl=settings.lnbits_wallet_featured_button_url,
|
||||||
|
walletFeaturedButtonIcon=settings.lnbits_wallet_featured_button_icon,
|
||||||
|
userActivationByEmail=settings.lnbits_user_activation_by_email,
|
||||||
|
userActivationByPayment=settings.lnbits_user_activation_by_payment,
|
||||||
|
userActivationByInvitationCode=settings.lnbits_user_activation_by_invitation_code,
|
||||||
|
hasFirstInstallToken=settings.first_install_token is not None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SuperSettings(EditableSettings):
|
class SuperSettings(EditableSettings):
|
||||||
super_user: str
|
super_user: str
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+13
-11
File diff suppressed because one or more lines are too long
@@ -220,6 +220,181 @@ body.body--dark .q-drawer {
|
|||||||
backdrop-filter: blur(6px) brightness(0.8);
|
backdrop-filter: blur(6px) brightness(0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.rounded-ui .q-card,
|
||||||
|
body.rounded-ui .q-btn {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=classic].card-gradient .q-card:not(.q-dialog .q-card,
|
||||||
|
.lnbits__dialog-card,
|
||||||
|
.q-dialog-plugin--dark) {
|
||||||
|
background-image: linear-gradient(135deg, rgba(103, 58, 183, 0.08), rgba(156, 39, 176, 0.06));
|
||||||
|
}
|
||||||
|
body[data-theme=classic].card-gradient .q-drawer {
|
||||||
|
background-image: linear-gradient(165deg, rgba(103, 58, 183, 0.06), rgba(156, 39, 176, 0.04));
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=classic].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
|
||||||
|
.lnbits__dialog-card,
|
||||||
|
.q-dialog-plugin--dark) {
|
||||||
|
background-image: linear-gradient(135deg, rgb(38.6192771084, 42.356626506, 64.7807228916), rgba(103, 58, 183, 0.07));
|
||||||
|
}
|
||||||
|
body[data-theme=classic].card-gradient.body--dark .q-drawer {
|
||||||
|
background-image: linear-gradient(165deg, rgb(36.7144578313, 40.2674698795, 61.5855421687), rgba(103, 58, 183, 0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=bitcoin].card-gradient .q-card:not(.q-dialog .q-card,
|
||||||
|
.lnbits__dialog-card,
|
||||||
|
.q-dialog-plugin--dark) {
|
||||||
|
background-image: linear-gradient(135deg, rgba(234, 97, 29, 0.08), rgba(229, 111, 53, 0.06));
|
||||||
|
}
|
||||||
|
body[data-theme=bitcoin].card-gradient .q-drawer {
|
||||||
|
background-image: linear-gradient(165deg, rgba(234, 97, 29, 0.06), rgba(229, 111, 53, 0.04));
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=bitcoin].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
|
||||||
|
.lnbits__dialog-card,
|
||||||
|
.q-dialog-plugin--dark) {
|
||||||
|
background-image: linear-gradient(135deg, rgb(54.18, 49.364, 71.036), rgba(234, 97, 29, 0.07));
|
||||||
|
}
|
||||||
|
body[data-theme=bitcoin].card-gradient.body--dark .q-drawer {
|
||||||
|
background-image: linear-gradient(165deg, rgb(51.885, 47.273, 68.027), rgba(234, 97, 29, 0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=freedom].card-gradient .q-card:not(.q-dialog .q-card,
|
||||||
|
.lnbits__dialog-card,
|
||||||
|
.q-dialog-plugin--dark) {
|
||||||
|
background-image: linear-gradient(135deg, rgba(226, 33, 86, 0.08), rgba(185, 26, 69, 0.06));
|
||||||
|
}
|
||||||
|
body[data-theme=freedom].card-gradient .q-drawer {
|
||||||
|
background-image: linear-gradient(165deg, rgba(226, 33, 86, 0.06), rgba(185, 26, 69, 0.04));
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=freedom].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
|
||||||
|
.lnbits__dialog-card,
|
||||||
|
.q-dialog-plugin--dark) {
|
||||||
|
background-image: linear-gradient(135deg, rgb(82.2051282051, 55.1948717949, 63.4153846154), rgba(226, 33, 86, 0.07));
|
||||||
|
}
|
||||||
|
body[data-theme=freedom].card-gradient.body--dark .q-drawer {
|
||||||
|
background-image: linear-gradient(165deg, rgb(79.1538461538, 53.1461538462, 61.0615384615), rgba(226, 33, 86, 0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=cyber].card-gradient .q-card:not(.q-dialog .q-card,
|
||||||
|
.lnbits__dialog-card,
|
||||||
|
.q-dialog-plugin--dark) {
|
||||||
|
background-image: linear-gradient(135deg, rgba(124, 179, 66, 0.08), rgba(85, 139, 47, 0.06));
|
||||||
|
}
|
||||||
|
body[data-theme=cyber].card-gradient .q-drawer {
|
||||||
|
background-image: linear-gradient(165deg, rgba(124, 179, 66, 0.06), rgba(85, 139, 47, 0.04));
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=cyber].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
|
||||||
|
.lnbits__dialog-card,
|
||||||
|
.q-dialog-plugin--dark) {
|
||||||
|
background-image: linear-gradient(135deg, rgb(10.2, 10.2, 10.2), rgba(124, 179, 66, 0.07));
|
||||||
|
}
|
||||||
|
body[data-theme=cyber].card-gradient.body--dark .q-drawer {
|
||||||
|
background-image: linear-gradient(165deg, rgb(7.65, 7.65, 7.65), rgba(124, 179, 66, 0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=mint].card-gradient .q-card:not(.q-dialog .q-card,
|
||||||
|
.lnbits__dialog-card,
|
||||||
|
.q-dialog-plugin--dark) {
|
||||||
|
background-image: linear-gradient(135deg, rgba(58, 183, 125, 0.08), rgba(39, 176, 101, 0.06));
|
||||||
|
}
|
||||||
|
body[data-theme=mint].card-gradient .q-drawer {
|
||||||
|
background-image: linear-gradient(165deg, rgba(58, 183, 125, 0.06), rgba(39, 176, 101, 0.04));
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=mint].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
|
||||||
|
.lnbits__dialog-card,
|
||||||
|
.q-dialog-plugin--dark) {
|
||||||
|
background-image: linear-gradient(135deg, rgb(38.6192771084, 64.7807228916, 53.5686746988), rgba(58, 183, 125, 0.07));
|
||||||
|
}
|
||||||
|
body[data-theme=mint].card-gradient.body--dark .q-drawer {
|
||||||
|
background-image: linear-gradient(165deg, rgb(36.7144578313, 61.5855421687, 50.9265060241), rgba(58, 183, 125, 0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=autumn].card-gradient .q-card:not(.q-dialog .q-card,
|
||||||
|
.lnbits__dialog-card,
|
||||||
|
.q-dialog-plugin--dark) {
|
||||||
|
background-image: linear-gradient(135deg, rgba(183, 118, 58, 0.08), rgba(176, 121, 39, 0.06));
|
||||||
|
}
|
||||||
|
body[data-theme=autumn].card-gradient .q-drawer {
|
||||||
|
background-image: linear-gradient(165deg, rgba(183, 118, 58, 0.06), rgba(176, 121, 39, 0.04));
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=autumn].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
|
||||||
|
.lnbits__dialog-card,
|
||||||
|
.q-dialog-plugin--dark) {
|
||||||
|
background-image: linear-gradient(135deg, rgb(64.7807228916, 51.0771084337, 38.6192771084), rgba(183, 118, 58, 0.07));
|
||||||
|
}
|
||||||
|
body[data-theme=autumn].card-gradient.body--dark .q-drawer {
|
||||||
|
background-image: linear-gradient(165deg, rgb(61.5855421687, 48.5578313253, 36.7144578313), rgba(183, 118, 58, 0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=flamingo].card-gradient .q-card:not(.q-dialog .q-card,
|
||||||
|
.lnbits__dialog-card,
|
||||||
|
.q-dialog-plugin--dark) {
|
||||||
|
background-image: linear-gradient(135deg, rgba(255, 0, 255, 0.08), rgba(253, 163, 253, 0.06));
|
||||||
|
}
|
||||||
|
body[data-theme=flamingo].card-gradient .q-drawer {
|
||||||
|
background-image: linear-gradient(165deg, rgba(255, 0, 255, 0.06), rgba(253, 163, 253, 0.04));
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=flamingo].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
|
||||||
|
.lnbits__dialog-card,
|
||||||
|
.q-dialog-plugin--dark) {
|
||||||
|
background-image: linear-gradient(135deg, rgb(66.176, 4.224, 66.176), rgba(255, 0, 255, 0.07));
|
||||||
|
}
|
||||||
|
body[data-theme=flamingo].card-gradient.body--dark .q-drawer {
|
||||||
|
background-image: linear-gradient(165deg, rgb(61.382, 3.918, 61.382), rgba(255, 0, 255, 0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=monochrome].card-gradient .q-card:not(.q-dialog .q-card,
|
||||||
|
.lnbits__dialog-card,
|
||||||
|
.q-dialog-plugin--dark) {
|
||||||
|
background-image: linear-gradient(135deg, rgba(73, 73, 73, 0.08), rgba(107, 107, 107, 0.06));
|
||||||
|
}
|
||||||
|
body[data-theme=monochrome].card-gradient .q-drawer {
|
||||||
|
background-image: linear-gradient(165deg, rgba(73, 73, 73, 0.06), rgba(107, 107, 107, 0.04));
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=monochrome].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
|
||||||
|
.lnbits__dialog-card,
|
||||||
|
.q-dialog-plugin--dark) {
|
||||||
|
background-image: linear-gradient(135deg, rgb(10.2, 10.2, 10.2), rgba(73, 73, 73, 0.07));
|
||||||
|
}
|
||||||
|
body[data-theme=monochrome].card-gradient.body--dark .q-drawer {
|
||||||
|
background-image: linear-gradient(165deg, rgb(7.65, 7.65, 7.65), rgba(73, 73, 73, 0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=salvador].card-gradient .q-card:not(.q-dialog .q-card,
|
||||||
|
.lnbits__dialog-card,
|
||||||
|
.q-dialog-plugin--dark) {
|
||||||
|
background-image: linear-gradient(135deg, rgba(25, 118, 210, 0.08), rgba(38, 166, 154, 0.06));
|
||||||
|
}
|
||||||
|
body[data-theme=salvador].card-gradient .q-drawer {
|
||||||
|
background-image: linear-gradient(165deg, rgba(25, 118, 210, 0.06), rgba(38, 166, 154, 0.04));
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme=salvador].card-gradient.body--dark .q-card:not(.q-dialog .q-card,
|
||||||
|
.lnbits__dialog-card,
|
||||||
|
.q-dialog-plugin--dark) {
|
||||||
|
background-image: linear-gradient(135deg, rgb(43.9888888889, 64.2, 84.4111111111), rgba(25, 118, 210, 0.07));
|
||||||
|
}
|
||||||
|
body[data-theme=salvador].card-gradient.body--dark .q-drawer {
|
||||||
|
background-image: linear-gradient(165deg, rgb(42.2416666667, 61.65, 81.0583333333), rgba(25, 118, 210, 0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
body.card-shadow .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
||||||
|
filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.18));
|
||||||
|
}
|
||||||
|
|
||||||
|
body.card-shadow.body--dark .q-card:not(.q-dialog .q-card, .lnbits__dialog-card, .q-dialog-plugin--dark) {
|
||||||
|
filter: drop-shadow(0 12px 28px rgba(0, 0, 0, 0.45));
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--size: 100px;
|
--size: 100px;
|
||||||
--gap: 25px;
|
--gap: 25px;
|
||||||
|
|||||||
+404
-15
@@ -6,6 +6,7 @@ window.localisation.br = {
|
|||||||
funding: 'Financiamento',
|
funding: 'Financiamento',
|
||||||
users: 'Usuários',
|
users: 'Usuários',
|
||||||
audit: 'Auditoria',
|
audit: 'Auditoria',
|
||||||
|
api_watch: 'Relógio da API',
|
||||||
apps: 'Aplicativos',
|
apps: 'Aplicativos',
|
||||||
channels: 'Canais',
|
channels: 'Canais',
|
||||||
transactions: 'Transações',
|
transactions: 'Transações',
|
||||||
@@ -21,13 +22,17 @@ window.localisation.br = {
|
|||||||
active_channels: 'Canais Ativos',
|
active_channels: 'Canais Ativos',
|
||||||
connect_peer: 'Conectar Par',
|
connect_peer: 'Conectar Par',
|
||||||
connect: 'Conectar',
|
connect: 'Conectar',
|
||||||
|
reconnect: 'Reconectar',
|
||||||
open_channel: 'Canal Aberto',
|
open_channel: 'Canal Aberto',
|
||||||
open: 'Abrir',
|
open: 'Abrir',
|
||||||
|
clear: 'Limpar',
|
||||||
close_channel: 'Fechar Canal',
|
close_channel: 'Fechar Canal',
|
||||||
close: 'Fechar',
|
close: 'Fechar',
|
||||||
restart: 'Reiniciar servidor',
|
restart: 'Reiniciar servidor',
|
||||||
|
image_library: 'Biblioteca de Imagens',
|
||||||
save: 'Salvar',
|
save: 'Salvar',
|
||||||
save_tooltip: 'Salvar suas alterações',
|
save_tooltip: 'Salvar suas alterações',
|
||||||
|
must_save: 'Você tem alterações não salvas',
|
||||||
credit_debit: 'Crédito / Débito',
|
credit_debit: 'Crédito / Débito',
|
||||||
credit_hint: 'Pressione Enter para creditar a conta',
|
credit_hint: 'Pressione Enter para creditar a conta',
|
||||||
credit_label: '{denomination} para creditar',
|
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: 'Exportar para o telefone com código QR',
|
||||||
export_to_phone_desc:
|
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.',
|
'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: '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',
|
wallets: 'Carteiras',
|
||||||
|
exclude_wallets: 'Excluir Carteiras',
|
||||||
add_wallet: 'Adicionar nova carteira',
|
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: 'Excluir carteira',
|
||||||
delete_wallet_desc:
|
delete_wallet_desc:
|
||||||
'Toda a carteira será excluída, os fundos serão IRRECUPERÁVEIS.',
|
'Toda a carteira será excluída, os fundos serão IRRECUPERÁVEIS.',
|
||||||
rename_wallet: 'Renomear carteira',
|
rename_wallet: 'Renomear carteira',
|
||||||
update_name: 'Atualizar nome',
|
update_name: 'Atualizar nome',
|
||||||
fiat_tracking: 'Rastreamento Fiat',
|
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',
|
currency: 'Moeda',
|
||||||
update_currency: 'Atualizar moeda',
|
update_currency: 'Atualizar moeda',
|
||||||
press_to_claim: 'Pressione para solicitar bitcoin',
|
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',
|
donate: 'Doar',
|
||||||
view_github: 'Ver no GitHub',
|
view_github: 'Ver no GitHub',
|
||||||
voidwallet_active: 'VoidWallet está ativo! Pagamentos desabilitados',
|
voidwallet_active: 'VoidWallet está ativo! Pagamentos desabilitados',
|
||||||
use_with_caution: 'USE COM CAUTELA - a carteira {name} ainda está em BETA',
|
voidwallet_active_user:
|
||||||
service_fee: 'Taxa de serviço: {amount} % por transação',
|
'Fonte de financiamento indisponível. Por favor, entre em contato com seu administrador para configurar.',
|
||||||
service_fee_max: 'Taxa de serviço: {amount} % por transação (máx {max} sats)',
|
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:
|
service_fee_tooltip:
|
||||||
'Taxa de serviço cobrada pelo administrador do servidor LNbits por transação de saída',
|
'Taxa de serviço cobrada pelo administrador do servidor LNbits por transação de saída',
|
||||||
toggle_darkmode: 'Alternar modo escuro',
|
toggle_darkmode: 'Alternar modo escuro',
|
||||||
@@ -79,6 +107,7 @@ window.localisation.br = {
|
|||||||
create_invoice: 'Criar Fatura',
|
create_invoice: 'Criar Fatura',
|
||||||
camera_tooltip: 'Usar a câmara para escanear uma fatura / QR',
|
camera_tooltip: 'Usar a câmara para escanear uma fatura / QR',
|
||||||
export_csv: 'Exportar para CSV',
|
export_csv: 'Exportar para CSV',
|
||||||
|
export_csv_details: 'Exportar para CSV com detalhes',
|
||||||
chart_tooltip: 'Mostrar gráfico',
|
chart_tooltip: 'Mostrar gráfico',
|
||||||
pending: 'Pendente',
|
pending: 'Pendente',
|
||||||
copy_invoice: 'Copiar fatura',
|
copy_invoice: 'Copiar fatura',
|
||||||
@@ -86,15 +115,27 @@ window.localisation.br = {
|
|||||||
cancel: 'Cancelar',
|
cancel: 'Cancelar',
|
||||||
scan: 'Escanear',
|
scan: 'Escanear',
|
||||||
read: 'Ler',
|
read: 'Ler',
|
||||||
|
write: 'Escrever',
|
||||||
pay: 'Pagar',
|
pay: 'Pagar',
|
||||||
memo: 'Memo',
|
memo: 'Memo',
|
||||||
date: 'Data',
|
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_processing: 'Processando pagamento...',
|
||||||
|
payment_successful: 'Pagamento bem-sucedido!',
|
||||||
|
payment_pending: 'Pagamento pendente...',
|
||||||
|
payment_check: 'Cheque pagamento',
|
||||||
not_enough_funds: 'Fundos insuficientes!',
|
not_enough_funds: 'Fundos insuficientes!',
|
||||||
search_by_tag_memo_amount: 'Pesquisar por tag, memo, quantidade',
|
search_by_tag_memo_amount: 'Pesquisar por tag, memo, quantidade',
|
||||||
|
search: 'Buscar',
|
||||||
invoice_waiting: 'Fatura aguardando pagamento',
|
invoice_waiting: 'Fatura aguardando pagamento',
|
||||||
payment_received: 'Pagamento Recebido',
|
payment_received: 'Pagamento Recebido',
|
||||||
payment_sent: 'Pagamento Enviado',
|
payment_sent: 'Pagamento Enviado',
|
||||||
|
payment_failed: 'Pagamento Falhou',
|
||||||
receive: 'receber',
|
receive: 'receber',
|
||||||
send: 'enviar',
|
send: 'enviar',
|
||||||
outgoing_payment_pending: 'Pagamento pendente de saída',
|
outgoing_payment_pending: 'Pagamento pendente de saída',
|
||||||
@@ -112,7 +153,10 @@ window.localisation.br = {
|
|||||||
extensions: 'Extensões',
|
extensions: 'Extensões',
|
||||||
no_extensions: 'Você não possui nenhuma extensão instalada :(',
|
no_extensions: 'Você não possui nenhuma extensão instalada :(',
|
||||||
created: 'Criado',
|
created: 'Criado',
|
||||||
|
created_at: 'Criado em',
|
||||||
|
updated_at: 'Atualizado em',
|
||||||
search_extensions: 'Extensões de pesquisa',
|
search_extensions: 'Extensões de pesquisa',
|
||||||
|
search_wallets: 'Pesquisar carteiras',
|
||||||
extension_sources: 'Fontes de Extensão',
|
extension_sources: 'Fontes de Extensão',
|
||||||
ext_sources_hint: 'Repositórios de onde as extensões podem ser baixadas',
|
ext_sources_hint: 'Repositórios de onde as extensões podem ser baixadas',
|
||||||
ext_sources_label:
|
ext_sources_label:
|
||||||
@@ -121,10 +165,13 @@ window.localisation.br = {
|
|||||||
repository: 'Repositório',
|
repository: 'Repositório',
|
||||||
confirm_continue: 'Você tem certeza de que deseja continuar?',
|
confirm_continue: 'Você tem certeza de que deseja continuar?',
|
||||||
manage_extension_details: 'Instalar/desinstalar extensão',
|
manage_extension_details: 'Instalar/desinstalar extensão',
|
||||||
|
upload: 'Enviar',
|
||||||
install: 'Instalar',
|
install: 'Instalar',
|
||||||
uninstall: 'Desinstalar',
|
uninstall: 'Desinstalar',
|
||||||
drop_db: 'Remover Dados',
|
drop_db: 'Remover Dados',
|
||||||
enable: 'Ativar',
|
enable: 'Ativar',
|
||||||
|
enabled: 'Habilitado',
|
||||||
|
disabled: 'Desativado',
|
||||||
pay_to_enable: 'Pague para Habilitar',
|
pay_to_enable: 'Pague para Habilitar',
|
||||||
enable_extension_details: 'Ativar extensão para o usuário atual',
|
enable_extension_details: 'Ativar extensão para o usuário atual',
|
||||||
disable: 'Desativar',
|
disable: 'Desativar',
|
||||||
@@ -132,6 +179,8 @@ window.localisation.br = {
|
|||||||
installed: 'Instalado',
|
installed: 'Instalado',
|
||||||
activated: 'Ativado',
|
activated: 'Ativado',
|
||||||
deactivated: 'Desativado',
|
deactivated: 'Desativado',
|
||||||
|
activate: 'Ativar',
|
||||||
|
deactivate: 'Desativar',
|
||||||
release_notes: 'Notas de Lançamento',
|
release_notes: 'Notas de Lançamento',
|
||||||
activate_extension_details:
|
activate_extension_details:
|
||||||
'Tornar a extensão disponível/indisponível para usuários',
|
'Tornar a extensão disponível/indisponível para usuários',
|
||||||
@@ -139,8 +188,33 @@ window.localisation.br = {
|
|||||||
all: 'Tudo',
|
all: 'Tudo',
|
||||||
only_admins_can_install:
|
only_admins_can_install:
|
||||||
'Apenas contas de administrador podem instalar extensões.',
|
'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',
|
admin_only: 'Apenas para Administração',
|
||||||
|
make_user_admin: 'Tornar usuário administrador',
|
||||||
|
revoke_admin: 'Revogar Admin',
|
||||||
new_version: 'Nova Versão',
|
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_depends_on: 'Depende de:',
|
||||||
extension_rating_soon: 'Avaliações estarão disponíveis em breve',
|
extension_rating_soon: 'Avaliações estarão disponíveis em breve',
|
||||||
extension_installed_version: 'Versão instalada',
|
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',
|
'Esta versão requer no mínimo a versão do LNbits',
|
||||||
min_version: 'Mínimo (incluído)',
|
min_version: 'Mínimo (incluído)',
|
||||||
max_version: 'Máximo (excluí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',
|
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',
|
fee: 'Taxa',
|
||||||
amount: 'Quantidade',
|
amount: 'Quantidade',
|
||||||
|
amount_limits: 'Limites de Quantia',
|
||||||
amount_sats: 'Quantidade (sats)',
|
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',
|
tag: 'Etiqueta',
|
||||||
unit: 'Unidade',
|
unit: 'Unidade',
|
||||||
description: 'Descrição',
|
description: 'Descrição',
|
||||||
expiry: 'Validade',
|
expiry: 'Validade',
|
||||||
webhook: 'Webhook',
|
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',
|
payment_proof: 'Comprovante de pagamento',
|
||||||
update: 'Atualizar',
|
update: 'Atualizar',
|
||||||
update_available: 'Atualização {version} disponível!',
|
update_available: 'Atualização {version} disponível!',
|
||||||
|
funding_sources: 'Fontes de Financiamento',
|
||||||
latest_update: 'Você está na versão mais recente {version}.',
|
latest_update: 'Você está na versão mais recente {version}.',
|
||||||
notifications: 'Notificações',
|
notifications: 'Notificações',
|
||||||
no_notifications: 'Sem notificações',
|
notifications_configure: 'Configurar Notificações',
|
||||||
notifications_disabled:
|
notifications_nostr_config: 'Configuração do Nostr',
|
||||||
'As notificações de status do LNbits estão desativadas.',
|
notifications_enable_nostr: 'Ativar Nostr',
|
||||||
enable_notifications: 'Ativar notificações',
|
notifications_enable_nostr_desc: 'Enviar notificações pelo Nostr',
|
||||||
enable_notifications_desc:
|
notifications_nostr_private_key: 'Chave Privada Nostr',
|
||||||
'Se ativado, ele buscará as últimas atualizações de status do LNbits, como incidentes de segurança e atualizações.',
|
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: 'Ativar Watchdog',
|
||||||
enable_watchdog_desc:
|
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.',
|
'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:
|
notification_source_label:
|
||||||
'URL de origem (use apenas a fonte de status oficial do LNbits e fontes de confiança)',
|
'URL de origem (use apenas a fonte de status oficial do LNbits e fontes de confiança)',
|
||||||
more: 'mais',
|
more: 'mais',
|
||||||
|
more_count: 'Mais {count}',
|
||||||
less: 'menos',
|
less: 'menos',
|
||||||
releases: 'Lançamentos',
|
releases: 'Lançamentos',
|
||||||
watchdog: 'Cão de guarda',
|
watchdog: 'Cão de guarda',
|
||||||
@@ -200,15 +371,24 @@ window.localisation.br = {
|
|||||||
allow_access_hint: 'Permitir acesso por IP (substituirá os IPs bloqueados)',
|
allow_access_hint: 'Permitir acesso por IP (substituirá os IPs bloqueados)',
|
||||||
enter_ip: 'Digite o IP e pressione enter',
|
enter_ip: 'Digite o IP e pressione enter',
|
||||||
rate_limiter: 'Limitador de Taxa',
|
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_limiter: 'Limitador de Carteira',
|
||||||
|
wallet_config: 'Configuração da Carteira',
|
||||||
|
wallet_charts: 'Gráficos da Carteira',
|
||||||
wallet_limit_max_withdraw_per_day:
|
wallet_limit_max_withdraw_per_day:
|
||||||
'Retirada máxima diária da carteira em sats (0 para desativar)',
|
'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_max_ballance: 'Saldo máximo da carteira em sats (0 para desativar)',
|
||||||
wallet_limit_secs_between_trans:
|
wallet_limit_secs_between_trans:
|
||||||
'Minutos e segundos entre transações por carteira (0 para desativar)',
|
'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',
|
number_of_requests: 'Número de solicitações',
|
||||||
time_unit: 'Unidade de tempo',
|
time_unit: 'Unidade de tempo',
|
||||||
minute: 'minuto',
|
minute: 'minuto',
|
||||||
|
settings: 'Configurações',
|
||||||
second: 'segundo',
|
second: 'segundo',
|
||||||
hour: 'hora',
|
hour: 'hora',
|
||||||
disable_server_log: 'Desativar Log do Servidor',
|
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',
|
login_with_user_id: 'Faça login com ID do usuário',
|
||||||
or: 'ou',
|
or: 'ou',
|
||||||
create_new_wallet: 'Criar Nova Carteira',
|
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',
|
login_to_account: 'Faça login na sua conta',
|
||||||
create_account: 'Criar conta',
|
create_account: 'Criar conta',
|
||||||
account_settings: 'Configurações da 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_nostr: 'Continuar com Nostr',
|
||||||
signin_with_google: 'Entrar com o Google',
|
signin_with_google: 'Entrar com o Google',
|
||||||
signin_with_github: 'Entrar com GitHub',
|
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',
|
username_or_email: 'Nome de usuário ou E-mail',
|
||||||
password: 'Senha',
|
password: 'Senha',
|
||||||
password_config: 'Configuração de Senha',
|
password_config: 'Configuração de Senha',
|
||||||
password_repeat: 'Repetição de senha',
|
password_repeat: 'Repetição de senha',
|
||||||
|
update_password: 'Atualizar Senha',
|
||||||
change_password: 'Alterar Senha',
|
change_password: 'Alterar Senha',
|
||||||
update_credentials: 'Atualizar credenciais',
|
update_credentials: 'Atualizar credenciais',
|
||||||
update_pubkey: 'Atualizar Chave Pública',
|
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: 'Definir Senha',
|
||||||
|
set_password_tooltip: 'Defina uma senha para este usuário',
|
||||||
invalid_password: 'A senha deve ter pelo menos 8 caracteres',
|
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',
|
login: 'Entrar',
|
||||||
register: 'Registrar',
|
register: 'Registrar',
|
||||||
username: 'Nome de usuário',
|
username: 'Nome de usuário',
|
||||||
pubkey: 'Chave Pública',
|
pubkey: 'Chave Pública',
|
||||||
user_id: 'ID do Usuário',
|
user_id: 'ID do Usuário',
|
||||||
|
id: 'ID',
|
||||||
email: 'E-mail',
|
email: 'E-mail',
|
||||||
first_name: 'Primeiro Nome',
|
first_name: 'Primeiro Nome',
|
||||||
last_name: 'Sobrenome',
|
last_name: 'Sobrenome',
|
||||||
picture: 'Foto',
|
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',
|
verify_email: 'Verifique o e-mail com',
|
||||||
account: 'Conta',
|
account: 'Conta',
|
||||||
update_account: 'Atualizar Conta',
|
update_account: 'Atualizar Conta',
|
||||||
invalid_username: 'Nome de usuário inválido',
|
invalid_username: 'Nome de usuário inválido',
|
||||||
auth_provider: 'Provedor de Autenticação',
|
auth_provider: 'Provedor de Autenticação',
|
||||||
|
external_id: 'ID Externo',
|
||||||
my_account: 'Minha Conta',
|
my_account: 'Minha Conta',
|
||||||
|
existing_account_question: 'Já tem uma conta?',
|
||||||
|
background_image: 'Imagem de Fundo',
|
||||||
back: 'Voltar',
|
back: 'Voltar',
|
||||||
logout: 'Sair',
|
logout: 'Sair',
|
||||||
look_and_feel: 'Aparência',
|
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',
|
toggle_gradient: 'Alternar Gradiente',
|
||||||
gradient_background: 'Fundo em Degradê',
|
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',
|
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',
|
color_scheme: 'Esquema de Cores',
|
||||||
|
visible_wallet_count: 'Contagem de Carteiras Visíveis',
|
||||||
admin_settings: 'Configurações do Administrador',
|
admin_settings: 'Configurações do Administrador',
|
||||||
extension_cost: 'Este lançamento requer um pagamento mínimo de {cost} sats.',
|
extension_cost: 'Este lançamento requer um pagamento mínimo de {cost} sats.',
|
||||||
extension_paid_sats: 'Você já pagou {paid_sats} 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.',
|
release_details_error: 'Não é possível obter os detalhes da versão.',
|
||||||
pay_from_wallet: 'Pagar com a Carteira',
|
pay_from_wallet: 'Pagar com a Carteira',
|
||||||
|
pay_with: 'Pague com {provider}',
|
||||||
|
select_payment_provider: 'Selecione o provedor de pagamento',
|
||||||
wallet_required: 'Carteira *',
|
wallet_required: 'Carteira *',
|
||||||
show_qr: 'Exibir QR',
|
show_qr: 'Exibir QR',
|
||||||
retry_install: 'Repetir Instalação',
|
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.',
|
'A extensão {name} requer um pagamento mínimo de {amount} sats para habilitar.',
|
||||||
hide_empty_wallets: 'Ocultar carteiras vazias',
|
hide_empty_wallets: 'Ocultar carteiras vazias',
|
||||||
recheck: 'Verificar novamente',
|
recheck: 'Verificar novamente',
|
||||||
|
check: 'Verificar',
|
||||||
|
check_connection: 'Verificar Conexão',
|
||||||
|
check_webhook: 'Verificar Webhook',
|
||||||
contributors: 'Contribuidores',
|
contributors: 'Contribuidores',
|
||||||
license: 'Licença',
|
license: 'Licença',
|
||||||
reset_key: 'Redefinir Chave',
|
reset_key: 'Redefinir Chave',
|
||||||
@@ -286,6 +528,7 @@ window.localisation.br = {
|
|||||||
hash: 'Hash:',
|
hash: 'Hash:',
|
||||||
welcome_lnbits: 'Bem-vindo ao LNbits',
|
welcome_lnbits: 'Bem-vindo ao LNbits',
|
||||||
setup_su_account: 'Configure a conta Superuser abaixo.',
|
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',
|
create_ticker_converter: 'Criar Conversor de Ticker de Moeda',
|
||||||
enable_audit: 'Habilitar Auditoria',
|
enable_audit: 'Habilitar Auditoria',
|
||||||
recommended: 'Recomendado',
|
recommended: 'Recomendado',
|
||||||
@@ -326,9 +569,16 @@ window.localisation.br = {
|
|||||||
user_default_extensions_label: 'Extensões do usuário',
|
user_default_extensions_label: 'Extensões do usuário',
|
||||||
user_default_extensions_hint:
|
user_default_extensions_hint:
|
||||||
'Extensões que serão ativadas por padrão para os usuários.',
|
'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',
|
miscellanous: 'Diversos',
|
||||||
misc_disable_extensions: 'Desativar extensões',
|
misc_disable_extensions: 'Desativar extensões',
|
||||||
misc_disable_extensions_label: 'Desativar todas as 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: 'Ocultar API',
|
||||||
misc_hide_api_label:
|
misc_hide_api_label:
|
||||||
'Oculta a API de carteira, extensões podem optar por honrar',
|
'Oculta a API de carteira, extensões podem optar por honrar',
|
||||||
@@ -346,16 +596,33 @@ window.localisation.br = {
|
|||||||
toggle_transactions_node_ui:
|
toggle_transactions_node_ui:
|
||||||
'Guia de Transações (Desativar em nós grandes CLN)',
|
'Guia de Transações (Desativar em nós grandes CLN)',
|
||||||
invoice_expiry: 'Expiração da Fatura',
|
invoice_expiry: 'Expiração da Fatura',
|
||||||
invoice_expiry_label: 'Validade da fatura (segundos)',
|
invoice_expiry_label: 'Validade da fatura',
|
||||||
fee_reserve: 'Reserva de Taxa',
|
routing_fee_reserve_calculations: 'Cálculos de Reserva de Taxa de Roteamento',
|
||||||
fee_reserve_msats: 'Taxa de reserva em msats',
|
routing_fee_reserve_calculations_desc:
|
||||||
fee_reserve_percent: 'Taxa de reserva em porcentagem',
|
'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',
|
server_management: 'Gerenciamento de Servidor',
|
||||||
base_url: 'URL base',
|
base_url: 'URL base',
|
||||||
base_url_label: 'URL estática/base para o servidor',
|
base_url_label: 'URL estática/base para o servidor',
|
||||||
authentication: 'Autenticação',
|
authentication: 'Autenticação',
|
||||||
auth_token_expiry_label: 'Minutos para expiração do token',
|
auth_token_expiry_label: 'Minutos para expiração do token',
|
||||||
auth_token_expiry_hint: 'Tempo em minutos até o token expirar',
|
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_label: 'Métodos de autorização permitidos',
|
||||||
auth_allowed_methods_hint: 'Selecione métodos de autorização',
|
auth_allowed_methods_hint: 'Selecione métodos de autorização',
|
||||||
auth_nostr_label: 'URL de Solicitação Nostr',
|
auth_nostr_label: 'URL de Solicitação Nostr',
|
||||||
@@ -373,13 +640,36 @@ window.localisation.br = {
|
|||||||
auth_keycloak_ci_hint:
|
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',
|
'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_cs_label: 'Segredo do Cliente Keycloak',
|
||||||
|
auth_keycloak_custom_org_label: 'Organização Personalizada do Keycloak',
|
||||||
|
auth_keycloak_custom_icon_label: 'Ícone Personalizado do Keycloak (URL)',
|
||||||
|
auth_oidc_label: 'URL de Descoberta do OIDC',
|
||||||
|
auth_oidc_ci_label: 'ID do Cliente OIDC',
|
||||||
|
auth_oidc_ci_hint:
|
||||||
|
'Certifique-se de que a URL de retorno de chamada de autorização esteja definida para https://{domain}/api/v1/auth/oidc/token',
|
||||||
|
auth_oidc_cs_label: 'Segredo do Cliente OIDC',
|
||||||
|
auth_oidc_custom_org_label:
|
||||||
|
'Nome da Organização Personalizada OIDC (ex. Zitadel, Authentik)',
|
||||||
|
auth_oidc_custom_icon_label: 'Ícone Personalizado do OIDC (URL)',
|
||||||
|
auth_keycloak_custom_org_label: 'Keycloak Custom Organization',
|
||||||
|
auth_keycloak_custom_icon_label: 'Ícone Personalizado do Keycloak (URL)',
|
||||||
currency_settings: 'Configurações de Moeda',
|
currency_settings: 'Configurações de Moeda',
|
||||||
allowed_currencies: 'Moedas Permitidas',
|
allowed_currencies: 'Moedas Permitidas',
|
||||||
allowed_currencies_hint: 'Limite o número de moedas fiduciárias disponíveis',
|
allowed_currencies_hint: 'Limite o número de moedas fiduciárias disponíveis',
|
||||||
default_account_currency: 'Moeda Padrão da Conta',
|
default_account_currency: 'Moeda Padrão da Conta',
|
||||||
default_account_currency_hint: 'Moeda padrão para contabilidade',
|
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_label: 'Taxa de serviço (%)',
|
||||||
service_fee_hint: 'Taxa cobrada por tx (%)',
|
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_label: 'Taxa de serviço máx (sats)',
|
||||||
service_fee_max_hint: 'Taxa máxima de serviço a cobrar em (sats)',
|
service_fee_max_hint: 'Taxa máxima de serviço a cobrar em (sats)',
|
||||||
fee_wallet: 'Carteira de Taxas',
|
fee_wallet: 'Carteira de Taxas',
|
||||||
@@ -391,6 +681,8 @@ window.localisation.br = {
|
|||||||
'Desativar Taxa de Serviço para Pagamentos Internos Lightning',
|
'Desativar Taxa de Serviço para Pagamentos Internos Lightning',
|
||||||
ui_management: 'Gerenciamento de UI',
|
ui_management: 'Gerenciamento de UI',
|
||||||
ui_site_title: 'Título do Site',
|
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_site_tagline: 'Tagline do site',
|
||||||
ui_elements_enable: 'Habilitar elementos na página inicial',
|
ui_elements_enable: 'Habilitar elementos na página inicial',
|
||||||
ui_elements_disable: 'Desativar elementos na página inicial',
|
ui_elements_disable: 'Desativar elementos na página inicial',
|
||||||
@@ -399,11 +691,27 @@ window.localisation.br = {
|
|||||||
ui_site_description: 'Descrição do Site',
|
ui_site_description: 'Descrição do Site',
|
||||||
ui_site_description_hint: 'Use texto simples, Markdown ou HTML bruto',
|
ui_site_description_hint: 'Use texto simples, Markdown ou HTML bruto',
|
||||||
ui_default_wallet_name: 'Nome Padrão da Carteira',
|
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',
|
lnbits_wallet: 'Carteira LNbits',
|
||||||
denomination: 'Denominação',
|
denomination: 'Denominação',
|
||||||
denomination_hint: 'O nome para o token FakeWallet',
|
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: 'Logo do QR Code',
|
||||||
ui_qr_code_logo_hint: 'URL para imagem de logo no código QR',
|
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: 'Distintivo Personalizado',
|
||||||
ui_custom_badge_label:
|
ui_custom_badge_label:
|
||||||
"Distintivo Personalizado 'USE COM CUIDADO - a carteira LNbits ainda está em BETA'",
|
"Distintivo Personalizado 'USE COM CUIDADO - a carteira LNbits ainda está em BETA'",
|
||||||
@@ -426,14 +734,95 @@ window.localisation.br = {
|
|||||||
admin_users_label: 'ID do Usuário',
|
admin_users_label: 'ID do Usuário',
|
||||||
allowed_users: 'Usuários Permitidos',
|
allowed_users: 'Usuários Permitidos',
|
||||||
allowed_users_hint: 'Somente esses usuários podem usar o LNbits',
|
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',
|
allowed_users_label: 'ID do Usuário',
|
||||||
allow_creation_user: 'Permitir a criação de novos usuários',
|
allow_creation_user: 'Permitir a criação de novos usuários',
|
||||||
allow_creation_user_desc:
|
allow_creation_user_desc:
|
||||||
'Permitir a criação de novos usuários na página de índice',
|
'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',
|
components: 'Componentes',
|
||||||
long_running_endpoints: 'Top 5 Endpoints de Longa Execução',
|
long_running_endpoints: 'Top 5 Endpoints de Longa Execução',
|
||||||
http_request_methods: 'Métodos de Requisição HTTP',
|
http_request_methods: 'Métodos de Requisição HTTP',
|
||||||
http_response_codes: 'Códigos de Resposta HTTP',
|
http_response_codes: 'Códigos de Resposta HTTP',
|
||||||
request_details: 'Detalhes do Pedido',
|
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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -353,6 +353,15 @@ window.localisation.cn = {
|
|||||||
auth_keycloak_ci_hint:
|
auth_keycloak_ci_hint:
|
||||||
'确保授权回调URL设置为https://{domain}/api/v1/auth/keycloak/token',
|
'确保授权回调URL设置为https://{domain}/api/v1/auth/keycloak/token',
|
||||||
auth_keycloak_cs_label: 'Keycloak客户端密钥',
|
auth_keycloak_cs_label: 'Keycloak客户端密钥',
|
||||||
|
auth_keycloak_custom_org_label: 'Keycloak 自定义组织',
|
||||||
|
auth_keycloak_custom_icon_label: 'Keycloak 自定义图标 (URL)',
|
||||||
|
auth_oidc_label: 'OIDC 发现 URL',
|
||||||
|
auth_oidc_ci_label: 'OIDC 客户端 ID',
|
||||||
|
auth_oidc_ci_hint:
|
||||||
|
'确保授权回调URL设置为https://{domain}/api/v1/auth/oidc/token',
|
||||||
|
auth_oidc_cs_label: 'OIDC客户端密钥',
|
||||||
|
auth_oidc_custom_org_label: 'OIDC 自定义组织名称(例如 Zitadel、Authentik)',
|
||||||
|
auth_oidc_custom_icon_label: 'OIDC 自定义图标 (URL)',
|
||||||
currency_settings: '货币设置',
|
currency_settings: '货币设置',
|
||||||
allowed_currencies: '允许的货币',
|
allowed_currencies: '允许的货币',
|
||||||
allowed_currencies_hint: '限制可用法定货币的数量',
|
allowed_currencies_hint: '限制可用法定货币的数量',
|
||||||
|
|||||||
@@ -367,6 +367,16 @@ window.localisation.cs = {
|
|||||||
auth_keycloak_ci_hint:
|
auth_keycloak_ci_hint:
|
||||||
'Ujistěte se, že je autorizace callback URL nastavena na https://{domain}/api/v1/auth/keycloak/token',
|
'Ujistěte se, že je autorizace callback URL nastavena na https://{domain}/api/v1/auth/keycloak/token',
|
||||||
auth_keycloak_cs_label: 'Klíč k aplikaci Keycloak tajemství',
|
auth_keycloak_cs_label: 'Klíč k aplikaci Keycloak tajemství',
|
||||||
|
auth_keycloak_custom_org_label: 'Vlastní organizace Keycloak',
|
||||||
|
auth_keycloak_custom_icon_label: 'Vlastní ikona Keycloak (URL)',
|
||||||
|
auth_oidc_label: 'URL pro zjištění OIDC',
|
||||||
|
auth_oidc_ci_label: 'ID klienta OIDC',
|
||||||
|
auth_oidc_ci_hint:
|
||||||
|
'Ujistěte se, že je autorizace callback URL nastavena na https://{domain}/api/v1/auth/oidc/token',
|
||||||
|
auth_oidc_cs_label: 'Klíč k aplikaci OIDC tajemství',
|
||||||
|
auth_oidc_custom_org_label:
|
||||||
|
'Název vlastní organizace OIDC (např. Zitadel, Authentik)',
|
||||||
|
auth_oidc_custom_icon_label: 'Vlastní ikona OIDC (URL)',
|
||||||
currency_settings: 'Nastavení měny',
|
currency_settings: 'Nastavení měny',
|
||||||
allowed_currencies: 'Povolené měny',
|
allowed_currencies: 'Povolené měny',
|
||||||
allowed_currencies_hint: 'Omezte počet dostupných fiat měn',
|
allowed_currencies_hint: 'Omezte počet dostupných fiat měn',
|
||||||
|
|||||||
@@ -377,6 +377,16 @@ window.localisation.de = {
|
|||||||
auth_keycloak_ci_hint:
|
auth_keycloak_ci_hint:
|
||||||
'Stellen Sie sicher, dass die Autorisierungs-Callback-URL auf https://{domain}/api/v1/auth/keycloak/token eingestellt ist.',
|
'Stellen Sie sicher, dass die Autorisierungs-Callback-URL auf https://{domain}/api/v1/auth/keycloak/token eingestellt ist.',
|
||||||
auth_keycloak_cs_label: 'Keycloak-Client-Geheimnis',
|
auth_keycloak_cs_label: 'Keycloak-Client-Geheimnis',
|
||||||
|
auth_keycloak_custom_org_label: 'Keycloak Benutzerdefinierte Organisation',
|
||||||
|
auth_keycloak_custom_icon_label: 'Keycloak Benutzerdefiniertes Symbol (URL)',
|
||||||
|
auth_oidc_label: 'OIDC Discovery-URL',
|
||||||
|
auth_oidc_ci_label: 'OIDC-Client-ID',
|
||||||
|
auth_oidc_ci_hint:
|
||||||
|
'Stellen Sie sicher, dass die Autorisierungs-Callback-URL auf https://{domain}/api/v1/auth/oidc/token eingestellt ist.',
|
||||||
|
auth_oidc_cs_label: 'OIDC-Client-Geheimnis',
|
||||||
|
auth_oidc_custom_org_label:
|
||||||
|
'OIDC Benutzerdefinierter Organisationsname (z.B. Zitadel, Authentik)',
|
||||||
|
auth_oidc_custom_icon_label: 'OIDC Benutzerdefiniertes Symbol (URL)',
|
||||||
currency_settings: 'Währungseinstellungen',
|
currency_settings: 'Währungseinstellungen',
|
||||||
allowed_currencies: 'Erlaubte Währungen',
|
allowed_currencies: 'Erlaubte Währungen',
|
||||||
allowed_currencies_hint:
|
allowed_currencies_hint:
|
||||||
|
|||||||
+159
-90
@@ -6,7 +6,7 @@ window.localisation.en = {
|
|||||||
funding: 'Funding',
|
funding: 'Funding',
|
||||||
users: 'Users',
|
users: 'Users',
|
||||||
audit: 'Audit',
|
audit: 'Audit',
|
||||||
api_watch: 'Api Watch',
|
api_watch: 'API Watch',
|
||||||
apps: 'Apps',
|
apps: 'Apps',
|
||||||
channels: 'Channels',
|
channels: 'Channels',
|
||||||
transactions: 'Transactions',
|
transactions: 'Transactions',
|
||||||
@@ -171,6 +171,7 @@ window.localisation.en = {
|
|||||||
drop_db: 'Remove Data',
|
drop_db: 'Remove Data',
|
||||||
enable: 'Enable',
|
enable: 'Enable',
|
||||||
enabled: 'Enabled',
|
enabled: 'Enabled',
|
||||||
|
disabled: 'Disabled',
|
||||||
pay_to_enable: 'Pay To Enable',
|
pay_to_enable: 'Pay To Enable',
|
||||||
enable_extension_details: 'Enable extension for current user',
|
enable_extension_details: 'Enable extension for current user',
|
||||||
disable: 'Disable',
|
disable: 'Disable',
|
||||||
@@ -178,6 +179,8 @@ window.localisation.en = {
|
|||||||
installed: 'Installed',
|
installed: 'Installed',
|
||||||
activated: 'Activated',
|
activated: 'Activated',
|
||||||
deactivated: 'Deactivated',
|
deactivated: 'Deactivated',
|
||||||
|
activate: 'Activate',
|
||||||
|
deactivate: 'Deactivate',
|
||||||
release_notes: 'Release Notes',
|
release_notes: 'Release Notes',
|
||||||
activate_extension_details: 'Make extension available/unavailable for users',
|
activate_extension_details: 'Make extension available/unavailable for users',
|
||||||
featured: 'Featured',
|
featured: 'Featured',
|
||||||
@@ -186,6 +189,8 @@ window.localisation.en = {
|
|||||||
only_admins_can_create_extensions:
|
only_admins_can_create_extensions:
|
||||||
'Only admin accounts can create extensions',
|
'Only admin accounts can create extensions',
|
||||||
admin_only: 'Admin Only',
|
admin_only: 'Admin Only',
|
||||||
|
make_user_admin: 'Make User Admin',
|
||||||
|
revoke_admin: 'Revoke Admin',
|
||||||
new_version: 'New Version',
|
new_version: 'New Version',
|
||||||
reviews_url: 'Reviews URL',
|
reviews_url: 'Reviews URL',
|
||||||
reviews_url_label: 'Reviews server URL',
|
reviews_url_label: 'Reviews server URL',
|
||||||
@@ -266,12 +271,18 @@ window.localisation.en = {
|
|||||||
update: 'Update',
|
update: 'Update',
|
||||||
update_available: 'Update {version} available!',
|
update_available: 'Update {version} available!',
|
||||||
funding_sources: 'Funding Sources',
|
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',
|
||||||
|
phoenixd_warning:
|
||||||
|
"Phoenixd mnemonic is only available if phoenixd data-dir is specified and is readable by LNbits. It's not indicative of phoenixd not running. It just means LNbits cannot access the mnemonic to display it here.",
|
||||||
latest_update: 'You are on the latest version {version}.',
|
latest_update: 'You are on the latest version {version}.',
|
||||||
notifications: 'Notifications',
|
notifications: 'Notifications',
|
||||||
notifications_configure: 'Configure Notifications',
|
notifications_configure: 'Configure Notifications',
|
||||||
notifications_nostr_config: 'Nostr Configuration',
|
notifications_nostr_config: 'Nostr Configuration',
|
||||||
notifications_enable_nostr: 'Enable Nostr',
|
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: 'Nostr Private Key',
|
||||||
notifications_nostr_private_key_desc:
|
notifications_nostr_private_key_desc:
|
||||||
'Private key (hex or nsec) to sign the messages sent to Nostr',
|
'Private key (hex or nsec) to sign the messages sent to Nostr',
|
||||||
@@ -280,11 +291,11 @@ window.localisation.en = {
|
|||||||
'Nip5 identifier to send notifications to',
|
'Nip5 identifier to send notifications to',
|
||||||
notifications_nostr_identifiers: 'Nostr Identifiers',
|
notifications_nostr_identifiers: 'Nostr Identifiers',
|
||||||
notifications_nostr_identifiers_desc:
|
notifications_nostr_identifiers_desc:
|
||||||
'List of identifiers to send notifications to',
|
'List of identifiers to send notifications to.',
|
||||||
|
|
||||||
notifications_telegram_config: 'Telegram Configuration',
|
notifications_telegram_config: 'Telegram Configuration',
|
||||||
notifications_enable_telegram: 'Enable Telegram',
|
notifications_enable_telegram: 'Enable Telegram',
|
||||||
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: 'Access Token',
|
||||||
notifications_telegram_access_token_desc: 'Access token for the bot',
|
notifications_telegram_access_token_desc: 'Access token for the bot',
|
||||||
notifications_chat_id: 'Telegram Chat ID',
|
notifications_chat_id: 'Telegram Chat ID',
|
||||||
@@ -313,15 +324,15 @@ window.localisation.en = {
|
|||||||
|
|
||||||
notification_settings_update: 'Settings updated',
|
notification_settings_update: 'Settings updated',
|
||||||
notification_settings_update_desc:
|
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: 'Server Start/Stop',
|
||||||
notification_server_start_stop_desc:
|
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: 'Watchdog Limit Notification',
|
||||||
notification_watchdog_limit_desc:
|
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: 'Server Status',
|
||||||
notification_server_status_desc:
|
notification_server_status_desc:
|
||||||
@@ -329,29 +340,31 @@ window.localisation.en = {
|
|||||||
|
|
||||||
notification_incoming_payment: 'Incoming Payments',
|
notification_incoming_payment: 'Incoming Payments',
|
||||||
notification_incoming_payment_desc:
|
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: 'Outgoing Payments',
|
||||||
notification_outgoing_payment_desc:
|
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: 'Credit / Debit',
|
||||||
notification_credit_debit_desc:
|
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: 'Balance Delta Changed',
|
||||||
notification_balance_delta_changed_desc:
|
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:
|
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.',
|
'You will need to re-enable this manually after an update.',
|
||||||
watchdog_interval: 'Watchdog Interval',
|
watchdog_interval: 'Watchdog Check Interval',
|
||||||
watchdog_interval_desc:
|
watchdog_interval_desc:
|
||||||
'How often the background task should check for a killswitch signal in the watchdog delta [node_balance - lnbits_balance] (in minutes).',
|
'How often LNbits should check for a killswitch signal in the watchdog threshold delta value [node_balance - lnbits_balance] (in minutes).',
|
||||||
watchdog_delta: 'Watchdog Delta',
|
watchdog_delta: 'Watchdog Threshold Delta',
|
||||||
watchdog_delta_desc:
|
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',
|
status: 'Status',
|
||||||
notification_source: 'Notification Source',
|
notification_source: 'Notification Source',
|
||||||
notification_source_label:
|
notification_source_label:
|
||||||
@@ -362,12 +375,12 @@ window.localisation.en = {
|
|||||||
releases: 'Releases',
|
releases: 'Releases',
|
||||||
watchdog: 'Watchdog',
|
watchdog: 'Watchdog',
|
||||||
server_logs: 'Server Logs',
|
server_logs: 'Server Logs',
|
||||||
ip_blocker: 'IP Blocker',
|
ip_blocker: 'IP Blacklist/Whitelist',
|
||||||
security: 'Security',
|
security: 'Security',
|
||||||
security_tools: 'Security tools',
|
security_tools: 'Security Tools',
|
||||||
block_access_hint: 'Block access by IP',
|
block_access_hint: 'Block access by IP',
|
||||||
allow_access_hint: 'Allow access by IP (will override blocked IPs)',
|
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',
|
rate_limiter: 'Rate Limiter',
|
||||||
callback_url_rules: 'Callback URL Rules',
|
callback_url_rules: 'Callback URL Rules',
|
||||||
enter_callback_url_rule: 'Enter URL rule as regex and hit enter',
|
enter_callback_url_rule: 'Enter URL rule as regex and hit enter',
|
||||||
@@ -381,14 +394,16 @@ window.localisation.en = {
|
|||||||
wallet_max_ballance: 'Wallet max balance in sats (0 to disable)',
|
wallet_max_ballance: 'Wallet max balance in sats (0 to disable)',
|
||||||
wallet_limit_secs_between_trans:
|
wallet_limit_secs_between_trans:
|
||||||
'Min secs between transactions per wallet (0 to disable)',
|
'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',
|
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',
|
time_unit: 'Time unit',
|
||||||
minute: 'minute',
|
minute: 'Minute',
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
second: 'second',
|
second: 'Second',
|
||||||
hour: 'hour',
|
hour: 'Hour',
|
||||||
disable_server_log: 'Disable Server Log',
|
disable_server_log: 'Disable Server Log',
|
||||||
enable_server_log: 'Enable Server Log',
|
enable_server_log: 'Enable Server Log',
|
||||||
coming_soon: 'Feature coming soon',
|
coming_soon: 'Feature coming soon',
|
||||||
@@ -460,6 +475,12 @@ window.localisation.en = {
|
|||||||
api_token_id: 'Token Id',
|
api_token_id: 'Token Id',
|
||||||
toggle_gradient: 'Toggle Gradient',
|
toggle_gradient: 'Toggle Gradient',
|
||||||
gradient_background: 'Gradient Background',
|
gradient_background: 'Gradient Background',
|
||||||
|
rounded_ui: 'Rounded Cards & Buttons',
|
||||||
|
toggle_rounded_ui: 'Toggle rounded corners for cards and buttons',
|
||||||
|
card_gradient: 'Card Gradient',
|
||||||
|
toggle_card_gradient: 'Toggle gradient on cards',
|
||||||
|
card_shadow: 'Card Shadow',
|
||||||
|
toggle_card_shadow: 'Toggle shadow on cards',
|
||||||
language: 'Language',
|
language: 'Language',
|
||||||
assets: 'Assets',
|
assets: 'Assets',
|
||||||
max_asset_size_mb: 'Max Asset Size (MB)',
|
max_asset_size_mb: 'Max Asset Size (MB)',
|
||||||
@@ -518,36 +539,34 @@ window.localisation.en = {
|
|||||||
hash: 'Hash: ',
|
hash: 'Hash: ',
|
||||||
welcome_lnbits: 'Welcome to LNbits',
|
welcome_lnbits: 'Welcome to LNbits',
|
||||||
setup_su_account: 'Set up the Superuser account below.',
|
setup_su_account: 'Set up the Superuser account below.',
|
||||||
|
first_install_token: 'First Install Token',
|
||||||
create_ticker_converter: 'Create Currency Ticker Converter',
|
create_ticker_converter: 'Create Currency Ticker Converter',
|
||||||
enable_audit: 'Enable Audit',
|
enable_audit: 'Enable Audit',
|
||||||
recommended: 'Recommended',
|
recommended: 'Recommended',
|
||||||
audit_desc: 'Record HTTP requests according with the specified filters',
|
audit_desc: 'Log HTTP requests according to the filters specified below.',
|
||||||
audit_record_req: 'Record Request Body',
|
audit_record_req: 'Log Request Body',
|
||||||
audit_record_warning: 'Warning: ',
|
audit_record_warning: 'Warning!',
|
||||||
audit_record_req_warning_1:
|
audit_record_req_warning_1: 'Sensitive data (like passwords) will be logged.',
|
||||||
'confidential data (like passwords) will be logged.',
|
audit_record_req_warning_2:
|
||||||
audit_record_req_warning_2: 'the request body can have large size.',
|
'The request body can be large. This can fill up your logs quickly.',
|
||||||
audit_record_use: 'Use it with caution.',
|
audit_record_use: 'Use this with caution!',
|
||||||
audit_ip: 'Record IP Address',
|
audit_ip: 'Log IP Address',
|
||||||
audit_ip_desc: 'Record the IP address of the client',
|
audit_ip_desc: 'Log the IP address of users making requests to LNbits.',
|
||||||
audit_path_params: 'Record Path Parameters',
|
audit_path_params: 'Log Path Parameters',
|
||||||
audit_query_params: 'Record Query Parameters',
|
audit_query_params: 'Log Query Parameters',
|
||||||
audit_http_methods: 'Include HTTP Methods',
|
audit_http_methods: 'Include HTTP Methods',
|
||||||
audit_http_methods_hint:
|
audit_http_methods_hint:
|
||||||
'List of HTTP methods to be included. Empty lists means all.',
|
'List of HTTP methods to be logged. No value means all methods will be logged.',
|
||||||
audit_http_methods_label: 'HTTP Methods',
|
audit_http_methods_label: 'HTTP Methods to Log',
|
||||||
audit_resp_codes: 'Include HTTP Response Codes',
|
|
||||||
audit_resp_codes_hint:
|
audit_resp_codes_hint:
|
||||||
'List of HTTP codes to be included (regex match). Empty lists means all. Eg: 4.*, 5.*',
|
'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_resp_codes_label: 'HTTP Response Codes to Log (regex)',
|
||||||
audit_paths: 'Include Paths',
|
|
||||||
audit_paths_hint:
|
audit_paths_hint:
|
||||||
'List of paths to be included (regex match). Empty list means all.',
|
'List of paths to be included (regex match). Empty list means all.',
|
||||||
audit_paths_label: 'HTTP Path (regex)',
|
audit_paths_label: 'HTTP Paths to Log (regex)',
|
||||||
audit_paths_exclude: 'Exclude Paths',
|
|
||||||
audit_paths_exclude_hint:
|
audit_paths_exclude_hint:
|
||||||
'List of paths to be excluded (regex match). Empty list means none.',
|
'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',
|
exchange_providers: 'Exchange Providers',
|
||||||
admin_extensions: 'Admin Extensions',
|
admin_extensions: 'Admin Extensions',
|
||||||
admin_extensions_label: 'Admin extensions',
|
admin_extensions_label: 'Admin extensions',
|
||||||
@@ -567,41 +586,51 @@ window.localisation.en = {
|
|||||||
misc_disable_extensions_builder_label:
|
misc_disable_extensions_builder_label:
|
||||||
'Enable Extensions Builder for non admin users.',
|
'Enable Extensions Builder for non admin users.',
|
||||||
misc_hide_api: 'Hide API',
|
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',
|
wallets_management: 'Wallets Management',
|
||||||
funding_source_info: 'Funding Source Info',
|
funding_source_info: 'Funding Source Information',
|
||||||
funding_source: 'Funding Source: {wallet_class}',
|
funding_source: 'Funding source: {wallet_class}',
|
||||||
node_balance: 'Node Balance: {balance} sats',
|
node_balance: 'Node balance: {balance} sats',
|
||||||
lnbits_balance: 'LNbits Balance: {balance} sats',
|
lnbits_balance: 'LNbits balance: {balance} sats',
|
||||||
funding_reserve_percent: 'Reserve Percent: {percent} %',
|
funding_reserve_percent: 'Funding reserve percentage: {percent} %',
|
||||||
node_management: 'Node Management',
|
node_management: 'Node Management',
|
||||||
node_management_not_supported:
|
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_node_ui: 'Node UI',
|
||||||
toggle_public_node_ui: 'Public Node UI',
|
toggle_public_node_ui: 'Public Node UI',
|
||||||
toggle_transactions_node_ui: 'Transactions Tab (Disable on large CLN nodes)',
|
toggle_transactions_node_ui: 'Transactions Tab (Disable on large CLN nodes)',
|
||||||
invoice_expiry: 'Invoice Expiry',
|
invoice_expiry: 'Invoice Expiry',
|
||||||
invoice_expiry_label: 'Invoice expiry (seconds)',
|
routing_fee_reserve_calculations: 'Routing Fee Reserve Calculations',
|
||||||
fee_reserve: 'Fee Reserve',
|
routing_fee_reserve_calculations_desc:
|
||||||
fee_reserve_percent: 'Fee Reserve Percent',
|
'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>.',
|
||||||
fee_reserve_msats: 'Reserve fee in msats',
|
millisats: 'millisats',
|
||||||
reserve_fee_in_percent: 'Reserve fee in percent',
|
fee_reserve: 'Minimum Routing Fee Reserve',
|
||||||
payment_wait_time: 'Payment Wait Time (sec)',
|
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_pending_interval: 'Check payment interval (sec)',
|
||||||
|
payment_pending_interval_desc: 'Interval to check pending payments',
|
||||||
|
payment_pending_interval_tooltip:
|
||||||
|
'Controls how often LNbits checks for pending payments to update their status. Higher values can reduce the load on the node and speed up the payment process, but it will take longer for pending payments to be updated.',
|
||||||
payment_wait_time_desc:
|
payment_wait_time_desc:
|
||||||
'Wait time before marking an outgoing payment as pending. Default: 5s; raise for slow-settling invoices.',
|
'Wait time before marking an outgoing payment as pending. Default: 5s; raise for slow-settling invoices.',
|
||||||
payment_wait_time_tooltip:
|
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.',
|
'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',
|
server_management: 'Server Management',
|
||||||
base_url: 'Base URL',
|
base_url_label: 'Base URL of the server',
|
||||||
base_url_label: 'Static/Base url for the server',
|
|
||||||
authentication: 'Authentication',
|
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_token_expiry_hint: 'Time in minutes until the token expires',
|
||||||
auth_authentication_cache_label: 'Cache time (minutes)',
|
auth_authentication_cache_label: 'Cache time (minutes)',
|
||||||
auth_authentication_cache_hint:
|
auth_authentication_cache_hint:
|
||||||
'Time in minutes to cache successful authentication (0 to disable)',
|
'Time in minutes to cache successful authentication (0 to disable)',
|
||||||
auth_allowed_methods_label: 'Allowed authorization methods',
|
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_label: 'Nostr Request URL',
|
||||||
auth_nostr_hint: 'Absolute URL that the clients will use to login.',
|
auth_nostr_hint: 'Absolute URL that the clients will use to login.',
|
||||||
auth_google_ci_label: 'Google Client ID',
|
auth_google_ci_label: 'Google Client ID',
|
||||||
@@ -619,33 +648,37 @@ window.localisation.en = {
|
|||||||
auth_keycloak_cs_label: 'Keycloak Client Secret',
|
auth_keycloak_cs_label: 'Keycloak Client Secret',
|
||||||
auth_keycloak_custom_org_label: 'Keycloak Custom Organization',
|
auth_keycloak_custom_org_label: 'Keycloak Custom Organization',
|
||||||
auth_keycloak_custom_icon_label: 'Keycloak Custom Icon (URL)',
|
auth_keycloak_custom_icon_label: 'Keycloak Custom Icon (URL)',
|
||||||
|
auth_oidc_label: 'OIDC Discovery URL',
|
||||||
|
auth_oidc_ci_label: 'OIDC Client ID',
|
||||||
|
auth_oidc_ci_hint:
|
||||||
|
'Make sure that the authorization callback URL is set to https://{domain}/api/v1/auth/oidc/token',
|
||||||
|
auth_oidc_cs_label: 'OIDC Client Secret',
|
||||||
|
auth_oidc_custom_org_label:
|
||||||
|
'OIDC Custom Organization Name (e.g., Zitadel, Authentik)',
|
||||||
|
auth_oidc_custom_icon_label: 'OIDC Custom Icon (URL)',
|
||||||
currency_settings: 'Currency Settings',
|
currency_settings: 'Currency Settings',
|
||||||
allowed_currencies: 'Allowed Currencies',
|
allowed_currencies: 'Allowed Currencies',
|
||||||
allowed_currencies_hint: 'Limit the number of available fiat currencies',
|
allowed_currencies_hint:
|
||||||
default_account_currency: 'Default Account Currency',
|
'Set the allowed fiat currencies for the exchange features',
|
||||||
default_account_currency_hint: 'Default currency for accounting',
|
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: 'Min Incoming Payment Amount',
|
||||||
min_incoming_payment_amount_desc:
|
min_incoming_payment_amount_desc:
|
||||||
'Minimum amount allowed for generating an invoice',
|
'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:
|
max_incoming_payment_amount_desc:
|
||||||
'Maximum amount allowed for generating an invoice',
|
'Maximum amount allowed when generating an invoice',
|
||||||
max_outgoing_payment_amount: 'Max Outgoing Payment Amount',
|
max_outgoing_payment_amount: 'Maximum Outgoing Payment Amount',
|
||||||
max_outgoing_payment_amount_desc:
|
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: 'Service Fee',
|
||||||
service_fee_label: 'Service fee (%)',
|
service_fee_label: 'Service Fee Charged Per Transaction',
|
||||||
service_fee_hint: 'Fee charged per tx (%)',
|
service_fee_max_label: 'Maximum Service Fee Limit',
|
||||||
service_fee_max: 'Service Fee Max',
|
fee_wallet_label: 'Service Fee Wallet ID',
|
||||||
service_fee_max_label: 'Service fee max (sats)',
|
fee_wallet_hint: 'The ID of the wallet to which to send service funds',
|
||||||
service_fee_max_hint: 'Max service fee to charge in (sats)',
|
disable_fee: 'Disable Service Fees for Internal Payments',
|
||||||
fee_wallet: 'Fee Wallet',
|
|
||||||
fee_wallet_label: 'Fee wallet (wallet ID)',
|
|
||||||
fee_wallet_hint: 'Wallet ID to send funds to',
|
|
||||||
disable_fee: 'Disable Fee',
|
|
||||||
disable_fee_internal: 'Disable Service Fee for Internal Payments',
|
|
||||||
disable_fee_internal_desc:
|
|
||||||
'Disable Service Fee for Internal Lightning Payments',
|
|
||||||
ui_management: 'UI Management',
|
ui_management: 'UI Management',
|
||||||
ui_site_title: 'Site Title',
|
ui_site_title: 'Site Title',
|
||||||
ui_changing_remove_lnbits_elements:
|
ui_changing_remove_lnbits_elements:
|
||||||
@@ -658,32 +691,47 @@ window.localisation.en = {
|
|||||||
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
|
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
|
||||||
ui_default_wallet_name: 'Default Wallet Name',
|
ui_default_wallet_name: 'Default Wallet Name',
|
||||||
ui_default_theme: 'Default Theme',
|
ui_default_theme: 'Default Theme',
|
||||||
|
wallet_featured_button_title: 'Wallet - Featured Button',
|
||||||
|
wallet_featured_button_label: 'Wallet Featured Button Label',
|
||||||
|
wallet_featured_button_label_hint:
|
||||||
|
'Show featured button on the wallet homepage',
|
||||||
|
wallet_featured_button_url: 'Featured Button URL',
|
||||||
|
wallet_featured_button_url_hint:
|
||||||
|
'On click the button will open this URL. Leave empty to hide the button.',
|
||||||
|
wallet_featured_button_icon: 'Featured Button Icon',
|
||||||
|
wallet_featured_button_icon_hint:
|
||||||
|
'Icon shown on the featured button (Quasar icon name e.g. "bolt")',
|
||||||
lnbits_wallet: 'LNbits wallet',
|
lnbits_wallet: 'LNbits wallet',
|
||||||
denomination: 'Denomination',
|
denomination: 'Denomination',
|
||||||
denomination_hint: 'The name for the FakeWallet token',
|
denomination_hint: 'The name for the FakeWallet token',
|
||||||
denomination_error: 'Denomination must be 3 characters, or `sats`',
|
denomination_error: 'Denomination must be 3 characters, or `sats`',
|
||||||
ui_qr_code_logo: 'QR Code/Favicon Logo',
|
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: '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: 'Custom Image',
|
||||||
ui_custom_image_label: 'URL to custom image',
|
ui_custom_image_label: 'URL to custom image',
|
||||||
ui_custom_image_hint: 'Image showed at homepage/login',
|
ui_custom_image_hint:
|
||||||
ui_custom_badge: 'Custom Badge',
|
'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_label: "Custom Badge 'USE WITH CAUTION'",
|
||||||
ui_custom_badge_color_label: 'Custom Badge Color',
|
ui_custom_badge_color_label: 'Custom Badge Color',
|
||||||
themes: 'Themes',
|
themes: 'Themes',
|
||||||
themes_hint: 'Choose themes available for users',
|
themes_hint: 'Choose themes available for users',
|
||||||
custom_logo: 'Custom Logo',
|
custom_logo: 'Custom Logo',
|
||||||
custom_logo_hint: 'URL to logo image',
|
custom_logo_hint: 'URL to logo image',
|
||||||
ad_space_title: 'Ad Space Title',
|
ad_space_section_title: 'Advertisement Space',
|
||||||
ad_space_title_label: 'Supported by',
|
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: 'Advertisement Slots',
|
||||||
ad_slots_hint:
|
ad_slots_hint:
|
||||||
'Ad url and image filepaths in CSV format, extensions can choose to honor',
|
'Advertisement image filepaths in CSV format, extensions can choose to honor. Format: url;img_light_url;img_dark_url, url..',
|
||||||
ad_slots_label: 'url;img_light_url;img_dark_url, url...',
|
ads_enabled: 'Enable Advertisement',
|
||||||
ads_enabled: 'Ads Enabled',
|
ads_disabled: 'Disabled Advertisement',
|
||||||
ads_disabled: 'Ads Disabled',
|
|
||||||
user_management: 'User Management',
|
user_management: 'User Management',
|
||||||
admin_users: 'Admin Users',
|
admin_users: 'Admin Users',
|
||||||
admin_users_hint: 'Users with admin privileges',
|
admin_users_hint: 'Users with admin privileges',
|
||||||
@@ -694,7 +742,28 @@ window.localisation.en = {
|
|||||||
allowed_users_label: 'User ID',
|
allowed_users_label: 'User ID',
|
||||||
allow_creation_user: 'Allow creation of new users',
|
allow_creation_user: 'Allow creation of new users',
|
||||||
allow_creation_user_desc: 'Allow creation of new users on the index page',
|
allow_creation_user_desc: 'Allow creation of new users on the index page',
|
||||||
|
require_user_activation: 'Require user activation',
|
||||||
|
require_user_activation_desc:
|
||||||
|
'New users will be activated only after they pass one of the confirmation methods. Admins can activate users manually from the admin panel.',
|
||||||
|
reusable_activation_code: 'Reusable activation code',
|
||||||
|
reusable_activation_code_label: 'Reusable activation code',
|
||||||
|
reusable_activation_code_hint:
|
||||||
|
'This activation code can be used multiple times by different users.',
|
||||||
|
one_time_activation_code: 'One-time activation codes',
|
||||||
|
one_time_activation_code_label: 'Add activation code',
|
||||||
|
one_time_activation_code_hint:
|
||||||
|
'List of one-time activation codes. Each code can be used only once, then will be reomved from the list.',
|
||||||
|
invitation_code: 'Invitation Code',
|
||||||
|
invitation_code_hint: 'The invitation code that you have received.',
|
||||||
|
email: 'Email',
|
||||||
|
email_confirmation_hint: 'Email address to send the confirmation code to.',
|
||||||
|
nostr_identifier: 'Nostr Identifier',
|
||||||
|
nostr_identifier_hint:
|
||||||
|
'Nostr nip5 identifier or <npub> to send the confirmation code to.',
|
||||||
|
|
||||||
new_user_not_allowed: 'Registration is disabled.',
|
new_user_not_allowed: 'Registration is disabled.',
|
||||||
|
start_user_impersonation: 'Impersonate this user',
|
||||||
|
stop_user_impersonation: 'Stop User Impersonation',
|
||||||
components: 'Components',
|
components: 'Components',
|
||||||
long_running_endpoints: 'Top 5 Long Running Endpoints',
|
long_running_endpoints: 'Top 5 Long Running Endpoints',
|
||||||
http_request_methods: 'HTTP Request Methods',
|
http_request_methods: 'HTTP Request Methods',
|
||||||
|
|||||||
@@ -379,6 +379,16 @@ window.localisation.es = {
|
|||||||
auth_keycloak_ci_hint:
|
auth_keycloak_ci_hint:
|
||||||
'Asegúrate de que la URL de devolución de llamada de autorización esté configurada en https://{domain}/api/v1/auth/keycloak/token',
|
'Asegúrate de que la URL de devolución de llamada de autorización esté configurada en https://{domain}/api/v1/auth/keycloak/token',
|
||||||
auth_keycloak_cs_label: 'Secreto del Cliente de Keycloak',
|
auth_keycloak_cs_label: 'Secreto del Cliente de Keycloak',
|
||||||
|
auth_keycloak_custom_org_label: 'Organización personalizada de Keycloak',
|
||||||
|
auth_keycloak_custom_icon_label: 'Icono personalizado de Keycloak (URL)',
|
||||||
|
auth_oidc_label: 'URL de descubrimiento de OIDC',
|
||||||
|
auth_oidc_ci_label: 'ID de cliente de OIDC',
|
||||||
|
auth_oidc_ci_hint:
|
||||||
|
'Asegúrate de que la URL de devolución de llamada de autorización esté configurada en https://{domain}/api/v1/auth/oidc/token',
|
||||||
|
auth_oidc_cs_label: 'Secreto del Cliente de OIDC',
|
||||||
|
auth_oidc_custom_org_label:
|
||||||
|
'Nombre de organización personalizada OIDC (ej. Zitadel, Authentik)',
|
||||||
|
auth_oidc_custom_icon_label: 'Icono personalizado de OIDC (URL)',
|
||||||
currency_settings: 'Configuración de moneda',
|
currency_settings: 'Configuración de moneda',
|
||||||
allowed_currencies: 'Monedas permitidas',
|
allowed_currencies: 'Monedas permitidas',
|
||||||
allowed_currencies_hint:
|
allowed_currencies_hint:
|
||||||
|
|||||||
@@ -520,6 +520,14 @@ window.localisation.fi = {
|
|||||||
auth_keycloak_cs_label: 'Keycloak-asiakassalasana',
|
auth_keycloak_cs_label: 'Keycloak-asiakassalasana',
|
||||||
auth_keycloak_custom_org_label: 'Valinnainen Keycloak-organisaatio',
|
auth_keycloak_custom_org_label: 'Valinnainen Keycloak-organisaatio',
|
||||||
auth_keycloak_custom_icon_label: 'Valinnainen Keycloak-kuvake (URL)',
|
auth_keycloak_custom_icon_label: 'Valinnainen Keycloak-kuvake (URL)',
|
||||||
|
auth_oidc_label: 'OIDC-discovery-URL',
|
||||||
|
auth_oidc_ci_label: 'OIDC-asiakastunnus',
|
||||||
|
auth_oidc_ci_hint:
|
||||||
|
'Varmista, että valtuutuksen palautus-URL on asetettu muotoon https://{domain}/api/v1/auth/oidc/token',
|
||||||
|
auth_oidc_cs_label: 'OIDC-asiakassalasana',
|
||||||
|
auth_oidc_custom_org_label:
|
||||||
|
'OIDC mukautetun organisaation nimi (esim. Zitadel, Authentik)',
|
||||||
|
auth_oidc_custom_icon_label: 'Valinnainen OIDC-kuvake (URL)',
|
||||||
currency_settings: 'Valuutta-asetukset',
|
currency_settings: 'Valuutta-asetukset',
|
||||||
allowed_currencies: 'Käytettävät valuutat',
|
allowed_currencies: 'Käytettävät valuutat',
|
||||||
allowed_currencies_hint: 'Valitse käytettävissä olevat fiat-valuutat',
|
allowed_currencies_hint: 'Valitse käytettävissä olevat fiat-valuutat',
|
||||||
|
|||||||
@@ -381,6 +381,16 @@ window.localisation.fr = {
|
|||||||
auth_keycloak_ci_hint:
|
auth_keycloak_ci_hint:
|
||||||
"Assurez-vous que l'URL de rappel d'autorisation est définie sur https://{domain}/api/v1/auth/keycloak/token",
|
"Assurez-vous que l'URL de rappel d'autorisation est définie sur https://{domain}/api/v1/auth/keycloak/token",
|
||||||
auth_keycloak_cs_label: 'Secret client Keycloak',
|
auth_keycloak_cs_label: 'Secret client Keycloak',
|
||||||
|
auth_keycloak_custom_org_label: 'Organisation personnalisée Keycloak',
|
||||||
|
auth_keycloak_custom_icon_label: 'Icône personnalisée Keycloak (URL)',
|
||||||
|
auth_oidc_label: 'URL de découverte OIDC',
|
||||||
|
auth_oidc_ci_label: 'ID Client OIDC',
|
||||||
|
auth_oidc_ci_hint:
|
||||||
|
"Assurez-vous que l'URL de rappel d'autorisation est définie sur https://{domain}/api/v1/auth/oidc/token",
|
||||||
|
auth_oidc_cs_label: 'Secret client OIDC',
|
||||||
|
auth_oidc_custom_org_label:
|
||||||
|
"Nom de l'organisation personnalisée OIDC (par ex. Zitadel, Authentik)",
|
||||||
|
auth_oidc_custom_icon_label: 'Icône personnalisée OIDC (URL)',
|
||||||
currency_settings: 'Paramètres de devise',
|
currency_settings: 'Paramètres de devise',
|
||||||
allowed_currencies: 'Devises autorisées',
|
allowed_currencies: 'Devises autorisées',
|
||||||
allowed_currencies_hint:
|
allowed_currencies_hint:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user