Compare commits

..
Author SHA1 Message Date
Arc 1f46545dea Reapply "Merge remote-tracking branch 'origin/dev' into stripe_recurring"
This reverts commit 37d2a695a7.
2025-09-26 11:49:42 +01:00
Arc 37d2a695a7 Revert "Merge remote-tracking branch 'origin/dev' into stripe_recurring"
This reverts commit 63458ced1e, reversing
changes made to cb3e340a78.
2025-09-26 11:47:50 +01:00
Arc 63458ced1e Merge remote-tracking branch 'origin/dev' into stripe_recurring 2025-09-26 11:44:04 +01:00
Arc cb3e340a78 mypy fix 2025-09-26 11:42:18 +01:00
arcbtc ae24f7e43c Merge branch 'or_stripe' into stripe_recurring 2025-09-25 05:18:26 +01:00
arcbtc 609808f6a2 added helper for getting subscription id 2025-09-25 05:17:08 +01:00
arcbtc c760e6f63d added a helper to get the subscription id
if returned that can be used for the checking id
2025-09-25 05:13:04 +01:00
ArcandArc 991ac4d7fe make 2025-09-25 03:58:14 +01:00
ArcandArc c1c622524e Working 2025-09-25 03:58:14 +01:00
arcbtcandArc ccc784c8fc recuuring payments 2025-09-25 03:58:14 +01:00
Arc 7c72766bbd make 2025-09-16 22:49:55 +01:00
Arc 416d170996 Working 2025-09-16 22:45:55 +01:00
arcbtc 94ebc22dcc recuuring payments 2025-09-15 21:13:28 +01:00
396 changed files with 53371 additions and 69308 deletions
-7
View File
@@ -23,10 +23,3 @@ mypy.ini
package-lock.json
package.json
pytest.ini
.mypy_cache
.github
.pytest_cache
.vscode
bin
dist
+7 -75
View File
@@ -6,21 +6,11 @@
# The following settings are ONLY set in your .env file.
# They are NOT managed by the Admin UI and are not stored in the database.
# === First Install Token ===
# If set the user is required to enter this token on the /first_install page
# FIRST_INSTALL_TOKEN="myaccesstoken"
# === Security ===
# When enabled (recommended), auth cookies require HTTPS and SSO will reject insecure HTTP.
AUTH_HTTPS_ONLY=true
# === Logging and Development ===
DEBUG=False
DEBUG_DATABASE=False
BUNDLE_ASSETS=True
# add `?profiler=true` to the url to enable the profiler for that request
PROFILER=False
# logging into LNBITS_DATA_FOLDER/logs/
ENABLE_LOG_TO_FILE=true
@@ -28,12 +18,9 @@ ENABLE_LOG_TO_FILE=true
# https://loguru.readthedocs.io/en/stable/api/logger.html#file
LOG_ROTATION="100 MB"
LOG_RETENTION="3 months"
# for database cleanup commands
# 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 ===
@@ -69,7 +56,7 @@ LNBITS_EXTENSIONS_DEFAULT_INSTALL="tpos"
# LNBITS_EXT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxxxx
# which fundingsources are allowed in the admin ui
# LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet, NWCWallet, BreezSdkWallet, BoltzWallet, StrikeWallet, CLNRestWallet, SparkWallet, SparkL2Wallet"
# LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, BlinkWallet, AlbyWallet, ZBDWallet, PhoenixdWallet, OpenNodeWallet, NWCWallet, BreezSdkWallet, BoltzWallet, StrikeWallet, CLNRestWallet"
# uvicorn variable, allow https behind a proxy
# IMPORTANT: this also needs the webserver to be configured to forward the headers
@@ -104,7 +91,7 @@ AUTH_SECRET_KEY=""
######################################
AUTH_TOKEN_EXPIRE_MINUTES=525600
# Possible authorization methods: user-id-only, username-password, nostr-auth-nip98, google-auth, github-auth, keycloak-auth, oidc-auth
# Possible authorization methods: user-id-only, username-password, nostr-auth-nip98, google-auth, github-auth, keycloak-auth
AUTH_ALLOWED_METHODS="user-id-only, username-password"
# Set this flag if HTTP is used for OAuth
# OAUTHLIB_INSECURE_TRANSPORT="1"
@@ -119,8 +106,6 @@ LNBITS_SITE_TAGLINE="Open Source Lightning Payments Platform"
LNBITS_SITE_DESCRIPTION="The world's most powerful suite of bitcoin tools. Run for yourself, for others, or as part of a stack."
# Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic, cyber
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber"
# Toggle the background styling on burger menus / drawers
# LNBITS_DEFAULT_BURGER_MENU_BACKGROUND=true
# LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg"
######################################
@@ -202,15 +187,7 @@ BOLTZ_CLIENT_ENDPOINT=127.0.0.1:9002
BOLTZ_CLIENT_MACAROON="/home/bob/.boltz/macaroons/admin.macaroon"
# HEXSTRING instead of path also possible
BOLTZ_CLIENT_CERT="/home/bob/.boltz/tls.cert"
# SparkL2Wallet (external sidecar: https://github.com/lnbits/spark_sidecar)
SPARK_L2_NETWORK=MAINNET
SPARK_L2_EXTERNAL_ENDPOINT=http://127.0.0.1:8765
SPARK_L2_EXTERNAL_API_KEY=
# optional tuning
# SPARK_L2_PAY_WAIT_MS=4000
# SPARK_L2_PAY_POLL_MS=500
# SPARK_L2_STREAM_KEEPALIVE_MS=15000
BOLTZ_CLIENT_WALLET="lnbits"
# StrikeWallet
STRIKE_API_ENDPOINT=https://api.strike.me/v1
@@ -283,50 +260,6 @@ KEYCLOAK_DISCOVERY_URL=""
KEYCLOAK_CLIENT_CUSTOM_ORG=""
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
######################################
@@ -382,12 +315,10 @@ LNBITS_SERVICE_FEE=0.0
# disable fees for internal transactions
# LNBITS_SERVICE_FEE_IGNORE_INTERNAL=true
# The minimum fee reserved per payment (millisats)
# value in millisats
LNBITS_RESERVE_FEE_MIN=2000
# The percentage of the payment amount to reserve for routing fees (percent)
# value in percent
LNBITS_RESERVE_FEE_PERCENT=1.0
# The default time to wait to for status response from the funding source when paying an invoice
LNBITS_FUNDING_SOURCE_PAY_INVOICE_WAIT_SECONDS=5
# limit the maximum balance for each wallet
# throw an error if the wallet attempts to create a new invoice
@@ -402,3 +333,4 @@ LNBITS_FUNDING_SOURCE_PAY_INVOICE_WAIT_SECONDS=5
######################################
###### Logging and Development #######
######################################
+1 -1
View File
@@ -1 +1 @@
custom: https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg
custom: https://demo.lnbits.com/lnurlp/link/fH59GD
-29
View File
@@ -1,29 +0,0 @@
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
+12 -18
View File
@@ -1,9 +1,14 @@
name: LNbits CI
on:
push:
branches:
- main
- dev
pull_request:
jobs:
lint:
uses: ./.github/workflows/lint.yml
@@ -11,7 +16,7 @@ jobs:
needs: [ lint ]
strategy:
matrix:
python-version: ["3.10", "3.12"]
python-version: ["3.10", "3.11", "3.12"]
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
uses: ./.github/workflows/tests.yml
with:
@@ -25,7 +30,7 @@ jobs:
needs: [ lint ]
strategy:
matrix:
python-version: ["3.10", "3.12"]
python-version: ["3.10", "3.11", "3.12"]
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
uses: ./.github/workflows/tests.yml
with:
@@ -39,7 +44,7 @@ jobs:
needs: [ lint ]
strategy:
matrix:
python-version: ["3.10", "3.12"]
python-version: ["3.10", "3.11", "3.12"]
db-url: ["", "postgres://lnbits:lnbits@0.0.0.0:5432/lnbits"]
uses: ./.github/workflows/tests.yml
with:
@@ -53,7 +58,7 @@ jobs:
needs: [ lint ]
strategy:
matrix:
python-version: ["3.10", "3.12"]
python-version: ["3.10", "3.11", "3.12"]
uses: ./.github/workflows/migration.yml
with:
python-version: ${{ matrix.python-version }}
@@ -69,15 +74,8 @@ jobs:
uses: ./.github/workflows/regtest.yml
strategy:
matrix:
python-version: ["3.12"]
backend-wallet-class:
- BoltzWallet
- LndRestWallet
- LndWallet
- CoreLightningWallet
- CoreLightningRestWallet
- LNbitsWallet
- EclairWallet
python-version: ["3.10"]
backend-wallet-class: ["LndRestWallet", "LndWallet", "CoreLightningWallet", "CoreLightningRestWallet", "LNbitsWallet", "EclairWallet"]
with:
custom-pytest: "uv run pytest tests/regtest"
python-version: ${{ matrix.python-version }}
@@ -89,11 +87,7 @@ jobs:
needs: [ lint ]
strategy:
matrix:
python-version: ["3.12"]
python-version: ["3.10"]
uses: ./.github/workflows/jmeter.yml
with:
python-version: ${{ matrix.python-version }}
bundle:
needs: [ lint, test-api, test-wallets, test-unit, migration, openapi, regtest, jmeter ]
uses: ./.github/workflows/bundle.yml
+2 -13
View File
@@ -55,21 +55,10 @@ jobs:
- name: Build and push boltz
uses: docker/build-push-action@v5
with:
context: docker/boltzclient
context: .
file: Dockerfile.boltz
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits-boltz:${{ inputs.tag }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
build-args: LNBITS_TAG=${{ inputs.tag }}
- name: Build and push sparkl2
uses: docker/build-push-action@v5
with:
context: docker/sparkl2
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/lnbits-sparkl2:${{ inputs.tag }}
platforms: linux/amd64,linux/arm64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
build-args: LNBITS_TAG=${{ inputs.tag }}
-1
View File
@@ -22,7 +22,6 @@ jobs:
- name: run LNbits
env:
LNBITS_ADMIN_UI: true
AUTH_HTTPS_ONLY: false
LNBITS_EXTENSIONS_DEFAULT_INSTALL: "watchonly, satspay, tipjar, tpos, lnurlp, withdraw"
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
run: |
+23
View File
@@ -6,30 +6,53 @@ jobs:
black:
uses: ./.github/workflows/make.yml
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
with:
make: checkblack
python-version: ${{ matrix.python-version }}
ruff:
uses: ./.github/workflows/make.yml
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
with:
make: checkruff
python-version: ${{ matrix.python-version }}
mypy:
uses: ./.github/workflows/make.yml
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
with:
make: mypy
python-version: ${{ matrix.python-version }}
pyright:
uses: ./.github/workflows/make.yml
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
with:
make: pyright
python-version: ${{ matrix.python-version }}
npm: true
prettier:
uses: ./.github/workflows/make.yml
with:
make: checkprettier
npm: true
bundle:
uses: ./.github/workflows/make.yml
with:
make: checkbundle
npm: true
poetry:
uses: ./.github/workflows/poetry.yml
+2 -2
View File
@@ -14,7 +14,7 @@ on:
python-version:
description: "python version"
type: string
default: "3.12"
default: "3.10"
jobs:
make:
@@ -22,7 +22,7 @@ jobs:
strategy:
matrix:
os-version: ["ubuntu-24.04"]
node-version: ["24.x"]
node-version: ["18.x"]
runs-on: ${{ matrix.os-version }}
steps:
- uses: actions/checkout@v4
@@ -1,42 +1,27 @@
name: Build LNbits AppImage
on:
workflow_call:
inputs:
tag_name:
description: 'The tag name for the release'
required: true
type: string
upload_url:
description: 'The upload URL for the release'
required: true
type: string
workflow_dispatch:
inputs:
tag_name:
description: 'The tag name for the release'
required: true
type: string
upload_url:
description: 'The upload URL for the release'
required: true
type: string
release:
types: [published]
jobs:
build-linux-package:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
# Step 1: Checkout the repository
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
# Step 2: Set up Python (uv will still use this toolchain)
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
# Step 3: Install system deps (fuse) + uv
- name: Install system deps and uv
run: |
sudo apt-get update
@@ -45,6 +30,7 @@ jobs:
echo "$HOME/.local/bin" >> $GITHUB_PATH
shell: bash
# Optional: Cache uv + venv to speed up CI
- name: Cache uv and venv
uses: actions/cache@v4
with:
@@ -53,6 +39,7 @@ jobs:
.venv
key: ${{ runner.os }}-uv-${{ hashFiles('uv.lock', 'pyproject.toml') }}
# Step 4: Prepare packaging tree and clone LNbits
- name: Prepare packaging & clone LNbits
run: |
mv .github/packaging packaging
@@ -60,25 +47,25 @@ jobs:
git clone https://github.com/lnbits/lnbits.git packaging/linux/AppDir/usr/lnbits
shell: bash
# Step 5: Build the LNbits binary with uv + PyInstaller
- name: Build LNbits binary (uv + PyInstaller)
run: |
cd packaging/linux/AppDir/usr/lnbits
uv sync --all-extras --no-dev
# Install project deps into .venv using uv
uv sync
# Install PyInstaller into the same environment
uv pip install pyinstaller
# Build the LNbits binary
uv run pyinstaller \
--onefile \
--name lnbits \
--hidden-import=embit \
--hidden-import=bitstring.bitstore_bitarray \
--collect-all embit \
--collect-all bitstring \
--collect-all bitarray \
--collect-all lnbits \
--collect-all sqlalchemy \
--collect-all breez_sdk \
--collect-binaries breez_sdk \
--collect-all breez_sdk_liquid \
--collect-binaries breez_sdk_liquid \
--collect-all aiosqlite \
--hidden-import=passlib.handlers.bcrypt \
"$(uv run which lnbits)"
@@ -88,7 +75,7 @@ jobs:
chmod +x packaging/linux/AppDir/lnbits.desktop
chmod +x packaging/linux/AppDir/usr/lnbits/dist/lnbits
# keep AppDir slim
# Clean out non-dist content from the app dir to keep AppImage slim
find packaging/linux/AppDir/usr/lnbits -mindepth 1 -maxdepth 1 \
! -name 'dist' \
! -name 'lnbits' \
@@ -97,25 +84,20 @@ jobs:
# Build AppImage
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
TAG_NAME=${{ inputs.tag_name }}
TAG_NAME=${{ github.event.release.tag_name }}
APPIMAGE_NAME="LNbits-${TAG_NAME}.AppImage"
./appimagetool-x86_64.AppImage \
--updateinformation "gh-releases-zsync|lnbits|lnbits|latest|*.AppImage.zsync" \
packaging/linux/AppDir "$APPIMAGE_NAME"
chmod +x "$APPIMAGE_NAME"
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
# 🔎 quick audit: show glibc versions referenced by the binary
echo "Runner glibc:"
ldd --version | head -n1 || true
echo "Symbols needed by lnbits:"
strings "$APPIMAGE_NAME" | grep -o 'GLIBC_[0-9.]*' | sort -u || true
shell: bash
# Step 6: Upload Linux Release Asset
- name: Upload Linux Release Asset
uses: actions/upload-release-asset@v1
with:
upload_url: ${{ inputs.upload_url }}
upload_url: ${{ github.event.release.upload_url }}
asset_path: ${{ env.APPIMAGE_NAME }}
asset_name: ${{ env.APPIMAGE_NAME }}
asset_content_type: application/octet-stream
+12 -13
View File
@@ -8,7 +8,7 @@ on:
required: true
type: string
python-version:
default: "3.12"
default: "3.10"
type: string
os-version:
default: "ubuntu-24.04"
@@ -38,9 +38,10 @@ jobs:
- name: Setup Regtest
run: |
cd docker/regtest
chmod +x ./start-regtest
./start-regtest
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
cd docker
chmod +x ./tests
./tests
sudo chmod -R a+rwx .
- name: Run pytest
@@ -49,21 +50,19 @@ jobs:
LNBITS_DATABASE_URL: ${{ inputs.db-url }}
LNBITS_BACKEND_WALLET_CLASS: ${{ inputs.backend-wallet-class }}
LND_REST_ENDPOINT: https://localhost:8081/
LND_REST_CERT: ./docker/regtest/data/lnd-3/tls.cert
LND_REST_MACAROON: ./docker/regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
LND_REST_CERT: ./docker/data/lnd-3/tls.cert
LND_REST_MACAROON: ./docker/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
LND_GRPC_ENDPOINT: localhost
LND_GRPC_PORT: 10009
LND_GRPC_CERT: ./docker/regtest/data/lnd-3/tls.cert
LND_GRPC_MACAROON: ./docker/regtest/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
CORELIGHTNING_RPC: ./docker/regtest/data/clightning-1/regtest/lightning-rpc
LND_GRPC_CERT: docker/data/lnd-3/tls.cert
LND_GRPC_MACAROON: docker/data/lnd-3/data/chain/bitcoin/regtest/admin.macaroon
CORELIGHTNING_RPC: ./docker/data/clightning-1/regtest/lightning-rpc
CORELIGHTNING_REST_URL: https://localhost:3001
CORELIGHTNING_REST_MACAROON: ./docker/regtest/data/clightning-2-rest/access.macaroon
CORELIGHTNING_REST_CERT: ./docker/regtest/data/clightning-2-rest/certificate.pem
CORELIGHTNING_REST_MACAROON: ./docker/data/clightning-2-rest/access.macaroon
CORELIGHTNING_REST_CERT: ./docker/data/clightning-2-rest/certificate.pem
LNBITS_ENDPOINT: http://localhost:5001
LNBITS_KEY: "d08a3313322a4514af75d488bcc27eee"
ECLAIR_URL: http://127.0.0.1:8082
BOLTZ_CLIENT_ENDPOINT: 127.0.0.1:9002
BOLTZ_MNEMONIC: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
LNBITS_MAX_OUTGOING_PAYMENT_AMOUNT_SATS: 1000000000
LNBITS_MAX_INCOMING_PAYMENT_AMOUNT_SATS: 1000000000
ECLAIR_PASS: lnbits
+7 -52
View File
@@ -10,30 +10,7 @@ permissions:
jobs:
release:
runs-on: ubuntu-24.04
outputs:
upload_url: ${{ steps.get_upload_url.outputs.upload_url }}
steps:
- uses: actions/checkout@v4
- name: Create github pre-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }}
run: |
gh release create "$tag" --prerelease --generate-notes --draft
- id: get_upload_url
name: Get upload url of Github release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }}
run: |
upload_url=$(gh release view "$tag" --json uploadUrl -q ".uploadUrl")
echo "upload_url=$upload_url" >> "$GITHUB_OUTPUT"
docker:
if: github.repository == 'lnbits/lnbits'
needs: [ release ]
uses: ./.github/workflows/docker.yml
with:
tag: ${{ github.ref_name }}
@@ -41,37 +18,15 @@ jobs:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
docker-latest-rc:
if: github.repository == 'lnbits/lnbits'
needs: [ release ]
uses: ./.github/workflows/docker.yml
with:
tag: latest-rc
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
pypi:
if: github.repository == 'lnbits/lnbits'
runs-on: ubuntu-24.04
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
- name: Set up Python 3.10
uses: actions/setup-python@v5
- name: Build and publish to pypi
uses: JRubics/poetry-publish@v1.15
with:
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:
needs: [ release ]
uses: ./.github/workflows/appimage.yml
with:
tag_name: ${{ github.ref_name }}
upload_url: ${{ needs.release.outputs.upload_url }}
pypi_token: ${{ secrets.PYPI_API_KEY }}
+7 -41
View File
@@ -13,8 +13,6 @@ jobs:
release:
runs-on: ubuntu-24.04
outputs:
upload_url: ${{ steps.get_upload_url.outputs.upload_url }}
steps:
- uses: actions/checkout@v4
- name: Create github release
@@ -23,17 +21,8 @@ jobs:
tag: ${{ github.ref_name }}
run: |
gh release create "$tag" --generate-notes --draft
- id: get_upload_url
name: Get upload url of Github release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }}
run: |
upload_url=$(gh release view "$tag" --json uploadUrl -q ".uploadUrl")
echo "upload_url=$upload_url" >> "$GITHUB_OUTPUT"
docker:
if: github.repository == 'lnbits/lnbits'
needs: [ release ]
uses: ./.github/workflows/docker.yml
with:
@@ -43,7 +32,6 @@ jobs:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
docker-latest:
if: github.repository == 'lnbits/lnbits'
needs: [ release ]
uses: ./.github/workflows/docker.yml
with:
@@ -52,37 +40,15 @@ jobs:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
docker-latest-rc:
if: github.repository == 'lnbits/lnbits'
needs: [ release ]
uses: ./.github/workflows/docker.yml
with:
tag: latest-rc
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
pypi:
if: github.repository == 'lnbits/lnbits'
runs-on: ubuntu-24.04
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
- name: Set up Python 3.10
uses: actions/setup-python@v5
- name: Build and publish to pypi
uses: JRubics/poetry-publish@v1.15
with:
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:
needs: [ release ]
uses: ./.github/workflows/appimage.yml
with:
tag_name: ${{ github.ref_name }}
upload_url: ${{ needs.release.outputs.upload_url }}
pypi_token: ${{ secrets.PYPI_API_KEY }}
+1 -1
View File
@@ -8,7 +8,7 @@ on:
required: true
type: string
python-version:
default: "3.12"
default: "3.10"
type: string
os-version:
default: "ubuntu-24.04"
+1 -1
View File
@@ -6,7 +6,6 @@ __pycache__
*$py.class
.mypy_cache
.vscode
.codex
*-lock.json
.python-version
@@ -43,6 +42,7 @@ lnbits/static/bundle.min.js.old
lnbits/static/bundle.min.css.old
lnbits/static/bundle-components.min.js.old
lnbits/upgrades
docker
# Nix
*result*
-1
View File
@@ -1 +0,0 @@
min-release-age=7
+3 -3
View File
@@ -14,16 +14,16 @@ repos:
- id: mixed-line-ending
- id: check-case-conflict
- repo: https://github.com/psf/black
rev: 26.3.1
rev: 25.1.0
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.10
rev: v0.12.10
hooks:
- id: ruff
args: [ --fix, --exit-non-zero-on-fix ]
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.7.4
rev: v3.6.2
hooks:
- id: prettier
types_or: [css, javascript, html, json]
-60
View File
@@ -1,60 +0,0 @@
# AGENTS.md - AI Coding Agent Guide for LNbits
This file guides AI coding agents working on LNbits. Keep changes small, verified, and aligned with existing project patterns.
## Core Behavior
- Think before coding. State material assumptions. Ask when ambiguity affects correctness, security, payments, wallets, or data migrations.
- Prefer the simplest implementation that solves the request.
- Make surgical changes. Every changed line should trace back to the task.
- Do not refactor, reformat, rename, or clean adjacent code unless required.
- Remove only dead code or imports created by your own changes.
- Define success criteria for non-trivial work and verify them before reporting done.
## LNbits Architecture
- Keep core lean. Prefer/assess extensions for non-core features.
- Preserve compatibility with existing extensions and wallet backends.
- Follow existing patterns in `lnbits/core`, `lnbits/wallets`, `lnbits/extensions`, and frontend code.
- Use existing CRUD, services, settings, and migration patterns.
- Do not edit generated files, bundled vendor files, or unrelated extension code.
## Security-Sensitive Areas
Be extra cautious with payments, wallet balances, admin routes, keys, LNURL, Bolt11, funding sources, migrations, and authentication.
Do not expose raw stack traces or sensitive values. Do not add synchronous blocking work in hot async paths without justification.
## Commands and Verification
Read `Makefile` before running project commands.
Use Makefile targets instead of hand-written commands when available:
- `make check` for full checks.
- `make test-unit` for unit tests.
- `make test-api` for API tests.
- `make test-wallets` for wallet tests.
- `make checkbundle` when bundled frontend assets may be affected.
- `make format` only when formatting is intended.
Do not run `make test` by default. Use the targeted tests available in the Makefile that are related to the work done, unless the user explicitly asks for broader test coverage.
## Dependencies
Do not add dependencies without approval. If approved, update the correct project files and explain why the dependency is necessary.
## Maintenance
LNbits maintainers own this file. They should update it when the development workflow, architecture, or verification commands materially change.
Do not edit, commit, push, or include changes to this file in a PR as part of normal feature work unless the user explicitly asks for `AGENTS.md` changes.
## Reporting
When finished, report:
- Summary of what changed.
- Files touched.
- Makefile targets or checks run.
- Anything not verified and why.
+1 -1
View File
@@ -43,4 +43,4 @@ ENV LNBITS_HOST="0.0.0.0"
EXPOSE 5000
CMD ["sh", "-c", "uv --offline run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
CMD ["sh", "-c", "uv run lnbits --port $LNBITS_PORT --host $LNBITS_HOST --forwarded-allow-ips='*'"]
@@ -1,7 +1,6 @@
ARG LNBITS_TAG=latest
FROM boltz/boltz-client:latest AS boltz
FROM lnbits/lnbits:${LNBITS_TAG}
FROM lnbits/lnbits:latest
COPY --from=boltz /bin/boltzd /bin/boltzcli /usr/local/bin/
RUN ls -l /usr/local/bin/boltzd
@@ -14,13 +13,11 @@ ENV PATH="/root/.local/bin:$PATH"
# Reinstall dependencies just in case (needed for CMD usage)
RUN uv sync --all-extras
# LNbits
# LNbits + boltzd configuration
ENV LNBITS_PORT="5000"
ENV LNBITS_HOST="0.0.0.0"
ENV LNBITS_BACKEND_WALLET_CLASS="BoltzWallet"
ENV FUNDING_SOURCE_MAX_RETRIES="10"
# Boltzd
ENV FUNDING_SOURCE_MAX_RETRIES=10
ENV BOLTZ_CLIENT_ENDPOINT="127.0.0.1:9002"
ENV BOLTZ_CLIENT_MACAROON="/root/.boltz/macaroons/admin.macaroon"
ENV BOLTZ_CLIENT_CERT="/root/.boltz/tls.cert"
@@ -28,7 +25,8 @@ ENV BOLTZ_CLIENT_WALLET="lnbits"
EXPOSE 5000
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Entrypoint to start boltzd and LNbits
COPY dockerboltz.sh /dockerboltz.sh
RUN chmod +x /dockerboltz.sh
CMD ["/entrypoint.sh"]
CMD ["/dockerboltz.sh"]
-1
View File
@@ -66,7 +66,6 @@ test-regtest:
LNBITS_DATA_FOLDER="./tests/data" \
PYTHONUNBUFFERED=1 \
DEBUG=true \
rm -rf ./tests/data \
uv run pytest tests/regtest
test-migration:
+30 -44
View File
@@ -1,52 +1,44 @@
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/logos/lnbits-full-inverse.svg">
<img src="docs/logos/lnbits-full.svg" alt="LNbits" style="width:300px">
</picture>
</a>
<picture >
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png" style="width:300px">
<img src="https://i.imgur.com/fyKPgVT.png" style="width:300px">
</picture>
![phase: stable](https://img.shields.io/badge/phase-stable-2EA043) [![license-badge]](LICENSE) [![docs-badge]][docs] ![PRs: welcome](https://img.shields.io/badge/PRs-Welcome-yellow) [![explore: LNbits extensions](https://img.shields.io/badge/explore-LNbits%20extensions-10B981)](https://extensions.lnbits.com/) [![hardware: LNBitsShop](https://img.shields.io/badge/hardware-LNBitsShop-7C3AED)](https://shop.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
<img alt="lnbits_head" src="docs/assets/header.jpg" />
[![tip-hero](https://img.shields.io/badge/TipJar-LNBits%20Hero-9b5cff?labelColor=6b7280&logo=lightning&logoColor=white)](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
![phase: beta](https://img.shields.io/badge/phase-beta-C41E3A) [![license-badge]](LICENSE) [![docs-badge]][docs] ![PRs: welcome](https://img.shields.io/badge/PRs-Welcome-08A04B) [<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)
![Lightning network wallet](https://i.imgur.com/DeIiO0y.png)
# LNbits — The most powerful Bitcoin & Lightning toolkit
# The world's most powerful suite of bitcoin tools.
> Run it for yourself, for your community, or as part of a larger stack.
## Run for yourself, for others, or as part of a stack.
## What is LNbits?
LNbits is beta, for responsible disclosure of any concerns please contact an admin in the community chat.
LNbits is a lightweight Python server that sits on top of your Lightning funding source. It gives you safe, isolated wallets, a clean API, and an extension system for rapidly adding features - without locking you into a single node implementation. The Inspiration for LNBits came from ideas pioneered by **OpenNode** and **LNPay** — both today work as funding sources for LNbits.
LNbits is a Python server that sits on top of any funding source. It can be used as:
## What you can do with LNbits
- Accounts system to mitigate the risk of exposing applications to your full balance via unique API keys for each wallet
- Extendable platform for exploring Lightning network functionality via the LNbits extension framework
- Part of a development stack via LNbits API
- Fallback wallet for the LNURL scheme
- Instant wallet for LN demonstrations
- **Harden app security:** Create per-wallet API keys so individual apps never touch your full balance.
- **Extend functionality fast:** Install extensions to explore and ship Lightning features with minimal code.
- **Build into your stack:** Use the LNbits HTTP API to integrate payments, wallets, and accounting.
- **Cover LNURL flows:** Use LNbits as a reliable fallback wallet for LNURL.
- **Demo in minutes:** Spin up instant wallets for workshops, proofs-of-concept, and user testing.
LNbits can run on top of almost all Lightning funding sources.
## Funding sources
See [LNbits manual](https://docs.lnbits.org/guide/wallets.html) for more detailed documentation about each funding source.
LNbits runs on top of most Lightning backends. Choose the one you already operate - or swap later without changing your app architecture.
Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series.
- Read the [funding source guide](https://docs.lnbits.org/guide/wallets.html)
## Learn more
- Video series on [Youtube](https://www.youtube.com/@lnbits)
- Introduction Video [LNBits V1](https://www.youtube.com/watch?v=PFAHKxvgI9Y&t=19s)
LNbits is inspired by all the great work of [opennode.com](https://www.opennode.com/), and in particular [lnpay.co](https://lnpay.co/). Both work as funding sources for LNbits.
## Running LNbits
See the [install guide](https://github.com/lnbits/lnbits/blob/main/docs/guide/installation.md) for details on installation and setup.
Test on our demo server [demo.lnbits.com](https://demo.lnbits.com), or on [lnbits.com](https://lnbits.com) software as a service, where you can spin up an LNbits instance for 21sats per hr.
Get yourself familiar and test on our demo server [demo.lnbits.com](https://demo.lnbits.com), or on [lnbits.com](https://lnbits.com) software as a service, where you can spin up an LNbits instance for 21sats per hr.
See the [install guide](https://github.com/lnbits/lnbits/blob/main/docs/guide/installation.md) for details on installation and setup.
## LNbits account system
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 alt="lnbits_wallet" src="docs/assets/wallet.jpg" />
<img src="https://i.imgur.com/w8jdGpF.png" style="width:800px">
## LNbits extension universe
@@ -54,35 +46,29 @@ 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.
<img alt="lnbits_extensions" src="docs/assets/extensions.jpg" />
<img src="https://i.imgur.com/aEBpwJF.png" style="width:800px">
## LNbits API
LNbits has a powerful API, many projects use LNbits to do the heavy lifting for their bitcoin/lightning services.
<img alt="lnbits_api" src="docs/assets/api.jpg" />
<img src="https://i.imgur.com/V742sb9.png" style="width:800px">
## LNbits node manager
LNbits comes packaged with a light node management UI, to make running your node that much easier.
<img alt="lnbits_api" src="docs/assets/lightning_node.jpg" />
<img src="https://i.imgur.com/TYqIK60.png" style="width:800px">
## LNbits merchant tools
## LNbits 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.
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.
<img alt="lnbits_merchants" src="docs/assets/merchants_small.webp" />
<img src="https://i.imgur.com/J96EbRf.png" style="width:800px">
## Powered by LNbits
## Tip us
LNbits empowers everyone with modular, open-source tools for building Bitcoin-based systems — fast, free, and extendable.
[![LNbits Shop](https://demo.lnbits.com/static/images/bitcoin-shop-banner.png)](https://shop.lnbits.com/)
[![Visit LNbits Shop](https://img.shields.io/badge/Visit-LNbits%20Shop-7C3AED?logo=shopping-cart&logoColor=white&labelColor=5B21B6)](https://shop.lnbits.com/)
[![Try myLNbits SaaS](https://img.shields.io/badge/Try-myLNbits%20SaaS-2563EB?logo=lightning&logoColor=white&labelColor=1E40AF)](https://my.lnbits.com/login)
[![Read LNbits News](https://img.shields.io/badge/Read-LNbits%20News-F97316?logo=rss&logoColor=white&labelColor=C2410C)](https://news.lnbits.com/)
[![Explore LNbits Extensions](https://img.shields.io/badge/Explore-LNbits%20Extensions-10B981?logo=puzzle-piece&logoColor=white&labelColor=065F46)](https://extensions.lnbits.com/) [![tip-hero](https://img.shields.io/badge/TipJar-LNBits%20Hero-9b5cff?labelColor=7c3aed&logo=lightning&logoColor=white)](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
If you like this project [send some tip love](https://demo.lnbits.com/lnurlp/link/fH59GD)!
[docs]: https://github.com/lnbits/lnbits/wiki
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
-18
View File
@@ -1,18 +0,0 @@
!data
data/*
!data/boltz
data/boltz/*
!data/boltz/boltz.conf
!data/boltz-client
data/boltz-client/*
!data/boltz-client/boltz.toml
!data/boltz-nginx
data/boltz-nginx/*
!data/boltz-nginx/default.conf
!data/eclair
data/eclair/*
!data/eclair/eclair.conf
-99
View File
@@ -1,99 +0,0 @@
![TESTS](https://github.com/lnbits/legend-regtest-enviroment/actions/workflows/ci.yml/badge.svg)
# nodes
- lnd-1: for locally testing your current lnbits
- lnd-2: used for boltz backend
- lnd-3: used for lnbits inside docker
- cln-1: for locally testing your current lnbits
- cln-2: used for clightning-REST
- eclair-1: for locally testing your current lnbits
# Installing regtest
get the regtest enviroment ready
```sh
# Install docker https://docs.docker.com/engine/install/
# Make sure your user has permission to use docker 'sudo usermod -aG docker ${USER}' then reboot
# Stop/start docker 'sudo systemctl stop docker' 'sudo systemctl start docker'
sudo apt install jq
git clone https://github.com/lnbits/lnbits.git
cd lnbits
docker build -t lnbits/lnbits .
mkdir docker
git clone https://github.com/lnbits/legend-regtest-enviroment.git docker
cd docker
chmod +x ./start-regtest
./start-regtest # start the regtest and also run tests
sudo chown -R $USER ./data # Give the data file permissions for user
```
# Running LNbits on regtest
add this ENV variables to your `.env` file
```sh
DEBUG=true
# LND
LNBITS_BACKEND_WALLET_CLASS="LndRestWallet"
LND_REST_ENDPOINT=https://127.0.0.1:8081/
LND_REST_CERT=/home/user/repos/lnbits/docker/data/lnd-1/tls.cert
LND_REST_MACAROON=/home/user/repos/lnbits/docker/data/lnd-1/data/chain/bitcoin/regtest/admin.macaroon
# CLN
LNBITS_BACKEND_WALLET_CLASS="CoreLightningWallet"
CORELIGHTNING_RPC=./docker/data/clightning-1/regtest/lightning-rpc
# Run LNbits
uv run lnbits
# Run LNbits with hot reload
make dev
```
# testing
```sh
chmod +x ./start-regtest
./start-regtest
# short answer :)
./start-regtest && echo "PASSED" || echo "FAILED" > /dev/null
```
usage of the `bitcoin-cli-sim`, `lightning-cli-sim` and `lncli-sim` aliases
```sh
cd ~/lnbits/docker
source docker-scripts.sh
# use bitcoin core, mine a block
bitcoin-cli-sim -generate 1
# use c-lightning nodes
lightning-cli-sim 1 newaddr | jq -r '.bech32' # use node 1
lightning-cli-sim 2 getinfo # use node 2
lightning-cli-sim 3 getinfo # use node 3
# use lnd nodes
lncli-sim 1 newaddr p2wsh
lncli-sim 2 listpeers
```
# urls
- mempool: http://localhost:8080/
- boltz api: http://localhost:9001/
- lnd-1 rest: http://localhost:8081/
- lnbits: http://localhost:5001/
# debugging docker logs
```sh
docker logs lnbits-lnbits-1 -f
docker logs lnbits-boltz-1 -f
docker logs lnbits-clightning-1-1 -f
docker logs lnbits-lnd-2-1 -f
```
@@ -1,49 +0,0 @@
standalone = true
network = "regtest"
# Path the the log file
logfile = ""
electrumUrl = "electrs:19001"
electrumLiquidUrl = "electrs-liquid:19002"
[BOLTZ]
# By default the daemon automatically connects to the official Boltz instance for the network LND is on
# This value is used to override that
url = "http://boltz-nginx:9001"
[DATABASE]
# Path to the SQLite database file
# path = "/home/michael/test.db"
[RPC]
# Host of
host = "0.0.0.0"
# Port of the gRPC interface
port = 9002
# Whether the REST proxy for the gRPC interface should be disabled
restDisabled = false
# Host of the REST proxy
restHost = "0.0.0.0"
# Port of the REST proxy
restPort = 9003
# Path to the TLS cert for the gRPC and REST interface
tlsCert = ""
# Path to the TLS private key for the gRPC and REST interface
tlsKey = ""
noTls = true
# Whether the macaroon authentication for the gRPC and REST interface should be disabled
noMacaroons = true
# Path to the admin macaroon for the gRPC and REST interface
adminMacaroonPath = ""
# Path to the read only macaroon for the gRPC and REST interface
readOnlyMacaroonPath = ""
@@ -1,49 +0,0 @@
upstream boltz {
server boltz:9001;
}
upstream boltzr {
server boltz:9005;
}
upstream ws {
server boltz:9004;
}
server {
listen 9001;
listen [::]:9001;
server_name localhost;
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods 'GET, PATCH, DELETE, POST, OPTIONS' always;
add_header Access-Control-Allow-Headers "*" always;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
if ($request_method = OPTIONS) {
return 204;
}
location /v2/ws {
proxy_pass http://ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location ~ ^/v2/(lightning|swap/rescue|swap/restore) {
proxy_pass http://boltzr;
}
location /streamswapstatus {
proxy_pass http://boltzr;
}
location / {
proxy_pass http://boltz;
}
}
-114
View File
@@ -1,114 +0,0 @@
[api]
host = "0.0.0.0"
port = 9_001
[grpc]
host = "0.0.0.0"
port = 9_000
[postgres]
host = "boltz-postgres"
port = 5432
database = "boltz"
username = "boltz"
password = "boltz"
[sidecar]
[sidecar.grpc]
host = "127.0.0.1"
port = 9003
[sidecar.ws]
host = "0.0.0.0"
port = 9004
[sidecar.api]
host = "0.0.0.0"
port = 9005
[swap]
deferredClaimSymbols = ["BTC", "L-BTC"]
[[pairs]]
base = "BTC"
quote = "BTC"
rate = 1
fee = 0.5
swapInFee = 0.1
maxSwapAmount = 40_294_967
minSwapAmount = 50_000
[pairs.timeoutDelta]
chain = 1440
reverse = 1440
swapMinimal = 1440
swapMaximal = 2880
swapTaproot = 10080
[[pairs]]
base = "L-BTC"
quote = "BTC"
fee = 0.25
swapInFee = 0.1
rate = 1
maxSwapAmount = 40_294_967
minSwapAmount = 100
[pairs.submarineSwap]
minSwapAmount = 1_000
minBatchedAmount = 21
[pairs.chainSwap]
minSwapAmount = 25_000
[pairs.timeoutDelta]
chain = 1440
reverse = 1440
swapMinimal = 1440
swapMaximal = 2880
swapTaproot = 10080
[[currencies]]
symbol = "BTC"
network = "bitcoinRegtest"
minWalletBalance = 10_000_000
minChannelBalance = 10_000_000
maxSwapAmount = 40_294_967
minSwapAmount = 10_000
maxZeroConfAmount = 0
[currencies.chain]
# mempoolSpace = "http://mempool-web:8090/api"
host = "bitcoind"
zmqpubrawtx = "tcp://bitcoind:29000"
zmqpubrawblock = "tcp://bitcoind:29001"
port = 18_443
cookie = "/root/.bitcoin/regtest/.cookie"
feeFloor = 0.2
wallet = "lnbits"
[currencies.lnd]
host = "lnd-2"
port = 10_009
certpath = "/data/lnd/tls.cert"
macaroonpath = "/data/lnd/data/chain/bitcoin/regtest/admin.macaroon"
[liquid]
symbol = "L-BTC"
network = "liquidRegtest"
maxSwapAmount = 40_294_967
minSwapAmount = 10_000
maxZeroConfAmount = 40_294_967
[liquid.chain]
host = "elementsd"
port = 18884
cookie = "/root/.elements/liquidregtest/.cookie"
zmqpubrawtx = "tcp://elementsd:31000"
zmqpubhashblock = "tcp://elementsd:31002"
wallet = "lnbits"
-28
View File
@@ -1,28 +0,0 @@
eclair {
chain = "regtest"
api {
binding-ip = "0.0.0.0"
enabled = true
port = 8080
password = "lnbits"
}
bitcoind {
host = "bitcoind"
rpcport = 18443
auth = "safecookie"
cookie = "/root/.bitcoin/regtest/.cookie"
zmqblock = "tcp://bitcoind:29002"
zmqtx = "tcp://bitcoind:29000"
}
channel {
max-funding-satoshis = 10000000000
}
features {
option_support_large_channel = mandatory
}
}
-434
View File
@@ -1,434 +0,0 @@
services:
lnbits:
hostname: lnbits
depends_on:
- lnd-3
image: lnbits/lnbits
restart: on-failure
user: "0:0"
environment:
LNBITS_PORT: 5001
DEBUG: true
LNBITS_ADMIN_UI: false
LNBITS_BACKEND_WALLET_CLASS: "LndRestWallet"
LNBITS_DATA_FOLDER: "./data"
LND_REST_ENDPOINT: "https://lnd-3:8081/"
LND_REST_CERT: "./lnd/tls.cert"
LND_REST_MACAROON: "./lnd/data/chain/bitcoin/regtest/admin.macaroon"
ports:
- 5001:5001
volumes:
- lnbits-data:/app/data
- ./data/lnd-3:/app/lnd:uid=1000,gid=1000
boltz:
hostname: boltz
depends_on:
- lnd-2
- boltz-postgres
restart: always
image: boltz/boltz:v3.12.1
ports:
- 9000:9000
entrypoint: "sh -c 'sleep 30; /boltz-backend/bin/boltzd'"
volumes:
- ./data/lnd-2:/data/lnd/
- ./data/boltz/:/root/.boltz/
- elements-data:/root/.elements
- bitcoin-data:/root/.bitcoin
boltz-client:
hostname: boltz-client
depends_on:
- boltz
restart: always
image: boltz/boltz-client:latest
ports:
- 9002:9002
- 9003:9003
expose:
- 9002
healthcheck:
test: ['CMD', 'boltzcli', '--host', 'boltz-client', 'getinfo']
interval: 5s
timeout: 3s
retries: 10
start_period: 0s
volumes:
- elements-data:/root/.elements
- ./data/boltz-client:/root/.boltz
boltz-backend-nginx:
hostname: boltz-nginx
restart: always
image: nginx:latest
ports:
- 9001:9001
volumes:
- nginx-data:/etc/nginx/conf.d
healthcheck:
test: ['CMD-SHELL', 'curl http://localhost:9001/version']
timeout: 1s
retries: 10
interval: 1s
start_period: 0s
boltz-postgres:
hostname: boltz-postgres
restart: always
image: postgres:14-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready --dbname boltz --username boltz"]
interval: 5s
timeout: 30s
retries: 10
start_period: 5s
environment:
- POSTGRES_DB=boltz
- POSTGRES_USER=boltz
- POSTGRES_PASSWORD=boltz
expose:
- 5432
bitcoind:
hostname: bitcoind
image: boltz/bitcoin-core:25.0
command:
- -regtest
- -fallbackfee=0.00000253
- -zmqpubrawtx=tcp://0.0.0.0:29000
- -zmqpubrawblock=tcp://0.0.0.0:29001
- -zmqpubhashblock=tcp://0.0.0.0:29002
- -txindex
- -rpcallowip=0.0.0.0/0
- -rpcbind=0.0.0.0
- -addresstype=bech32
- -changetype=bech32
- -dbcache=2048
- -rpcworkqueue=256
volumes:
- bitcoin-data:/root/.bitcoin
expose:
- 29000
- 29001
- 29002
- 18443
- 18444
healthcheck:
test:
[
"CMD",
"bitcoin-cli",
"--rpccookiefile=/root/.bitcoin/regtest/.cookie",
"-regtest",
"getblockchaininfo",
]
timeout: 1s
retries: 1
interval: 1s
start_period: 0s
clightning-1:
hostname: clightning-1
depends_on:
- bitcoind
image: boltz/c-lightning:24.11
command:
- --large-channels
- --network=regtest
- --grpc-port=9736
- --bind-addr=0.0.0.0:9735
- --bitcoin-rpcconnect=bitcoind
- --bitcoin-rpcport=18443
- --clnrest-host=0.0.0.0
- --clnrest-port=3010
expose:
- 9735
ports:
- 9736:9736
- 3010:3010
volumes:
- ./data/clightning-1:/root/.lightning/
- bitcoin-data:/root/.bitcoin
clightning-2:
hostname: clightning-2
depends_on:
- bitcoind
image: boltz/c-lightning:22.11.1
command:
- --large-channels
- --network=regtest
- --grpc-port=9737
- --bind-addr=0.0.0.0:9735
- --bitcoin-rpcconnect=bitcoind
- --bitcoin-rpcport=18443
expose:
- 9735
ports:
- 9737:9737
volumes:
- ./data/clightning-2:/root/.lightning/
- bitcoin-data:/root/.bitcoin
clightning-2-rest:
hostname: clightning-2-rest
depends_on:
- clightning-2
image: saubyk/c-lightning-rest:0.10.7
entrypoint: "sh -c 'sleep 35 && /sbin/tini -g -- ./docker-entrypoint.sh'"
ports:
- 3001:3001
expose:
- 3001
volumes:
- ./data/clightning-2:/root/.lightning/:uid=1000,gid=1000
- ./data/clightning-2-rest:/usr/src/app/certs/
- bitcoin-data:/root/.bitcoin
clightning-3:
hostname: clightning-3
depends_on:
- bitcoind
image: boltz/c-lightning:24.11
command:
- --large-channels
- --network=regtest
- --grpc-port=9738
- --bind-addr=0.0.0.0:9735
- --bitcoin-rpcconnect=bitcoind
- --bitcoin-rpcport=18443
expose:
- 9735
ports:
- 9738:9738
volumes:
- ./data/clightning-3:/root/.lightning/
- bitcoin-data:/root/.bitcoin
lnd-1:
hostname: lnd-1
depends_on:
- bitcoind
image: boltz/lnd:0.18.4-beta
restart: on-failure
command:
- --listen=lnd-1:9735
- --rpclisten=lnd-1:10009
- --restlisten=lnd-1:8081
- --bitcoin.active
- --bitcoin.regtest
- --bitcoin.node=bitcoind
- --bitcoind.rpchost=bitcoind
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
- --bitcoind.zmqpubrawtx=bitcoind:29000
- --bitcoind.zmqpubrawblock=bitcoind:29001
- --noseedbackup
- --protocol.wumbo-channels
expose:
- 8081
- 9735
- 10009
volumes:
- ./data/lnd-1:/root/.lnd/
- bitcoin-data:/root/.bitcoin
lnd-2:
hostname: lnd-2
depends_on:
- bitcoind
image: boltz/lnd:0.19.3-beta
restart: on-failure
command:
- --listen=lnd-2:9735
- --rpclisten=lnd-2:10009
- --restlisten=lnd-2:8081
- --bitcoin.active
- --bitcoin.regtest
- --bitcoin.node=bitcoind
- --bitcoind.rpchost=bitcoind
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
- --bitcoind.zmqpubrawtx=bitcoind:29000
- --bitcoind.zmqpubrawblock=bitcoind:29001
- --noseedbackup
- --protocol.wumbo-channels
expose:
- 8081
- 9735
- 10009
volumes:
- ./data/lnd-2:/root/.lnd/
- bitcoin-data:/root/.bitcoin
lnd-3:
hostname: lnd-3
depends_on:
- bitcoind
image: boltz/lnd:0.18.4-beta
restart: on-failure
command:
- --listen=lnd-3:9735
- --rpclisten=lnd-3:10009
- --restlisten=lnd-3:8081
- --bitcoin.active
- --bitcoin.regtest
- --bitcoin.node=bitcoind
- --bitcoind.rpchost=bitcoind
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
- --bitcoind.zmqpubrawtx=bitcoind:29000
- --bitcoind.zmqpubrawblock=bitcoind:29001
- --noseedbackup
- --protocol.wumbo-channels
ports:
- 8081:8081
- 10009:10009
expose:
- 8081
- 9735
- 10009
volumes:
- ./data/lnd-3:/root/.lnd/
- bitcoin-data:/root/.bitcoin
lnd-4:
hostname: lnd-4
depends_on:
- bitcoind
image: boltz/lnd:0.18.4-beta
restart: on-failure
command:
- --listen=lnd-4:9735
- --rpclisten=lnd-4:10009
- --restlisten=lnd-4:8081
- --bitcoin.active
- --bitcoin.regtest
- --bitcoin.node=bitcoind
- --bitcoind.rpchost=bitcoind
- --bitcoind.rpccookie=/root/.bitcoin/regtest/.cookie
- --bitcoind.zmqpubrawtx=bitcoind:29000
- --bitcoind.zmqpubrawblock=bitcoind:29001
- --noseedbackup
- --protocol.wumbo-channels
expose:
- 8081
- 9735
- 10009
volumes:
- ./data/lnd-4:/root/.lnd/
- bitcoin-data:/root/.bitcoin
eclair:
hostname: eclair
depends_on:
- bitcoind
image: boltz/eclair:0.8.0
restart: on-failure
entrypoint: "sh -c 'JAVA_OPTS=-Xmx512m /eclair-node/bin/eclair-node.sh -Declair.datadir=/root/eclair -Declair.printToConsole'"
ports:
- 8082:8080
expose:
- 9735
- 8080
volumes:
- ./data/eclair:/root/eclair
- bitcoin-data:/root/.bitcoin
electrs:
hostname: electrs
restart: always
image: boltz/electrs:latest
entrypoint: ["electrs-bitcoin"]
command:
- --electrum-rpc-addr
- electrs:19001
- --http-addr
- electrs:3002
- --daemon-rpc-addr
- bitcoind:18443
- --network
- regtest
- --jsonrpc-import
healthcheck:
test: ["CMD-SHELL", "curl -s $(hostname):3002/blocks/tip/height"]
timeout: 1s
retries: 20
interval: 2s
start_period: 5s
ports:
- 19001:19001
- 3002:3002
volumes:
- bitcoin-data:/root/.bitcoin
elementsd:
hostname: elementsd
restart: always
image: boltz/elements:latest
expose:
- 31001
ports:
- 31000:31000
- 31002:31002
- 18884:18884
command:
- -chain=liquidregtest
- -txindex=1
- -rest=1
- -server=1
- -rpcallowip=0.0.0.0/0
- -validatepegin=0
- -initialfreecoins=2100000000000000
- -fallbackfee=0.000001
- -rpcbind=0.0.0.0
- -rpcport=18884
- -zmqpubrawtx=tcp://0.0.0.0:31000
- -zmqpubrawblock=tcp://0.0.0.0:31001
- -zmqpubhashblock=tcp://0.0.0.0:31002
- -acceptdiscountct=1
- -creatediscountct=1
volumes:
- elements-data:/root/.elements
electrs-liquid:
hostname: electrs-liquid
restart: always
image: boltz/electrs:latest
entrypoint: ["electrs-liquid"]
command:
- --electrum-rpc-addr
- electrs-liquid:19002
- --http-addr
- electrs-liquid:3003
- --daemon-rpc-addr
- elementsd:18884
- --daemon-dir
- /root/.elements
- --network
- liquidregtest
- --parent-network
- regtest
- --jsonrpc-import
healthcheck:
test: ["CMD-SHELL", "curl -s $(hostname):3003/blocks/tip/height"]
timeout: 15s
retries: 20
interval: 2s
start_period: 5s
ports:
- 19002:19002
- 3003:3003
volumes:
- elements-data:/root/.elements
volumes:
lnbits-data:
bitcoin-data:
elements-data:
nginx-data:
name: nginx-data
driver: local
driver_opts:
type: none
o: bind
device: ./data/boltz-nginx/
-322
View File
@@ -1,322 +0,0 @@
#!/bin/sh
export COMPOSE_PROJECT_NAME=lnbits
boltzcli-sim() {
docker exec -it lnbits-boltz-client-1 boltzcli "$@"
}
bitcoin-cli-sim() {
docker exec lnbits-bitcoind-1 bitcoin-cli -regtest "$@"
}
elements-cli-sim() {
docker exec lnbits-elementsd-1 elements-cli -rpcport=18884 -chain=liquidregtest "$@"
}
# args(i, cmd)
lightning-cli-sim() {
i=$1
shift # shift first argument so we can use $@
docker exec lnbits-clightning-$i-1 lightning-cli --network regtest "$@"
}
# args(i, cmd)
lncli-sim() {
i=$1
shift # shift first argument so we can use $@
docker exec lnbits-lnd-$i-1 lncli --network regtest --rpcserver=lnd-$i:10009 "$@"
}
get-eclair-pubkey() {
while true; do
pubkey=$(docker exec lnbits-eclair-1 curl http://localhost:8080/getinfo -X POST -s -u :lnbits | jq -r .nodeId 2> /dev/null)
pubkeyPrefix=$(echo $pubkey | cut -c1,2)
if [[ "$pubkeyPrefix" == "02" || "$pubkeyPrefix" == "03" ]]; then
echo $pubkey
break
fi
sleep 1
done
}
wait-for-eclair-channel() {
while true; do
state=$(docker exec lnbits-eclair-1 curl http://localhost:8080/channels -X POST -s -u :lnbits | jq -r ".[0].state")
pending=$(docker exec lnbits-eclair-1 curl -s http://localhost:8080/channels -X POST -u :lnbits| jq '. | length')
echo "eclair-1 pendingchannels: $pending, current state: $state"
if [[ "$state" == "NORMAL" ]]; then
break
fi
sleep 1
done
}
# args(i)
fund_boltz_client() {
# first address of seed: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
address="el1qq2xvpcvfup5j8zscjq05u2wxxjcyewk7979f3mmz5l7uw5pqmx6xf5xy50hsn6vhkm5euwt72x878eq6zxx2z0z676mna6kdq"
echo "funding: $address on boltz-client"
elements-cli-sim -named sendtoaddress address=$address amount=30 fee_rate=100 > /dev/null
}
# args(i)
fund_clightning_node() {
address=$(lightning-cli-sim $1 newaddr | jq -r .bech32)
echo "funding: $address on clightning-node: $1"
bitcoin-cli-sim -named sendtoaddress address=$address amount=30 fee_rate=100 > /dev/null
}
# args(i)
fund_lnd_node() {
address=$(lncli-sim $1 newaddress p2wkh | jq -r .address)
echo "funding: $address on lnd-node: $1"
bitcoin-cli-sim -named sendtoaddress address=$address amount=30 fee_rate=100 > /dev/null
}
# args(i, j)
connect_clightning_node() {
pubkey=$(lightning-cli-sim $2 getinfo | jq -r '.id')
lightning-cli-sim $1 connect $pubkey@lnbits-clightning-$2-1:9735 | jq -r '.id'
}
lnbits-regtest-start(){
if ! command -v jq &> /dev/null
then
echo "jq is not installed"
exit
fi
if ! command -v docker &> /dev/null
then
echo "docker is not installed"
exit
fi
if ! command -v docker version &> /dev/null
then
echo "dockerd is not running"
exit
fi
lnbits-regtest-stop
docker compose up -d --remove-orphans
lnbits-regtest-init
}
lnbits-regtest-start-log(){
lnbits-regtest-stop
docker compose up --remove-orphans
lnbits-regtest-init
}
lnbits-regtest-stop(){
docker compose down --volumes
# clean up lightning node data
sudo rm -rf ./data/clightning-1 ./data/clightning-2 ./data/clightning-3 ./data/lnd-1 ./data/lnd-2 ./data/lnd-3 ./data/lnd-4 ./data/boltz/boltz.db ./data/eclair/regtest ./data/boltz-client/liquid-wallet ./data/boltz-client/bitcoin-wallet ./data/boltz-client/wallet ./data/boltz-client/boltz.db
# recreate lightning node data folders preventing permission errors
mkdir ./data/clightning-1 ./data/clightning-2 ./data/clightning-3 ./data/lnd-1 ./data/lnd-2 ./data/lnd-3 ./data/lnd-4
}
lnbits-regtest-restart(){
lnbits-regtest-stop
lnbits-regtest-start
}
boltz-client-init(){
for i in 0 1 2; do
fund_boltz_client
done
elements-cli-sim -generate 3 > /dev/null
}
lnbits-bitcoin-init(){
echo "init_bitcoin_wallet..."
bitcoin-cli-sim createwallet lnbits || bitcoin-cli-sim loadwallet lnbits
echo "mining 150 blocks..."
bitcoin-cli-sim -generate 150 > /dev/null
}
lnbits-elements-init(){
echo "init_elements_wallet..."
elements-cli-sim createwallet lnbits || elements-cli-sim loadwallet lnbits
echo "mining 150 blocks..."
elements-cli-sim -generate 150 > /dev/null
elements-cli-sim rescanblockchain
}
lnbits-init(){
echo "init_lnbits..."
docker exec lnbits-lnbits-1 uv run python tools/create_fake_admin.py
}
lnbits-regtest-init(){
lnbits-bitcoin-init
lnbits-elements-init
lnbits-lightning-sync
lnbits-lightning-init
boltz-client-init
lnbits-init
}
lnbits-lightning-sync(){
wait-for-clightning-sync 1
wait-for-clightning-sync 2
wait-for-clightning-sync 3
wait-for-lnd-sync 1
wait-for-lnd-sync 2
wait-for-lnd-sync 3
wait-for-lnd-sync 4
}
lnbits-lightning-init(){
# create 10 UTXOs for each node
for i in 0 1 2; do
fund_clightning_node 1
fund_clightning_node 2
fund_clightning_node 3
fund_lnd_node 1
fund_lnd_node 2
fund_lnd_node 3
fund_lnd_node 4
done
echo "mining 3 blocks..."
bitcoin-cli-sim -generate 3 > /dev/null
lnbits-lightning-sync
channel_confirms=6
channel_size=24000000 # 0.024 btc
balance_size=12000000 # 0.12 btc
balance_size_msat=12000000000 # 0.12 btc
# lnd-1 -> lnd-2
lncli-sim 1 connect $(lncli-sim 2 getinfo | jq -r '.identity_pubkey')@lnbits-lnd-2-1 > /dev/null
echo "open channel from lnd-1 to lnd-2"
lncli-sim 1 openchannel $(lncli-sim 2 getinfo | jq -r '.identity_pubkey') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 1
# lnd-1 -> lnd-3
lncli-sim 1 connect $(lncli-sim 3 getinfo | jq -r '.identity_pubkey')@lnbits-lnd-3-1 > /dev/null
echo "open channel from lnd-1 to lnd-3"
lncli-sim 1 openchannel $(lncli-sim 3 getinfo | jq -r '.identity_pubkey') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 1
# lnd-1 -> cln-1
lncli-sim 1 connect $(lightning-cli-sim 1 getinfo | jq -r '.id')@lnbits-clightning-1-1 > /dev/null
echo "open channel from lnd-1 to cln-1"
lncli-sim 1 openchannel $(lightning-cli-sim 1 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 1
# lnd-1 -> cln-2
lncli-sim 1 connect $(lightning-cli-sim 2 getinfo | jq -r '.id')@lnbits-clightning-2-1 > /dev/null
echo "open channel from lnd-1 to cln-2"
lncli-sim 1 openchannel $(lightning-cli-sim 2 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 1
# lnd-1 -> cln-3
lncli-sim 1 connect $(lightning-cli-sim 3 getinfo | jq -r '.id')@lnbits-clightning-3-1 > /dev/null
echo "open channel from lnd-1 to cln-3"
lncli-sim 1 openchannel $(lightning-cli-sim 3 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 1
# lnd-2 -> cln-2
lncli-sim 2 connect $(lightning-cli-sim 2 getinfo | jq -r '.id')@lnbits-clightning-2-1 > /dev/null
echo "open channel from lnd-2 to cln-2"
lncli-sim 2 openchannel $(lightning-cli-sim 2 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 2
# lnd-3 -> cln-3
lncli-sim 3 connect $(lightning-cli-sim 3 getinfo | jq -r '.id')@lnbits-clightning-3-1 > /dev/null
echo "open channel from lnd-3 to cln-1"
lncli-sim 3 openchannel $(lightning-cli-sim 3 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 3
# lnd-3 -> cln-1
lncli-sim 3 connect $(lightning-cli-sim 1 getinfo | jq -r '.id')@lnbits-clightning-1-1 > /dev/null
echo "open channel from lnd-3 to cln-1"
lncli-sim 3 openchannel $(lightning-cli-sim 1 getinfo | jq -r '.id') $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 3
# lnd-1 -> eclair-1
lncli-sim 1 connect $(get-eclair-pubkey)@lnbits-eclair-1 > /dev/null
echo "open channel from lnd-2 to eclair-1"
lncli-sim 1 openchannel $(get-eclair-pubkey) $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 1
# lnd-2 -> eclair-1
lncli-sim 2 connect $(get-eclair-pubkey)@lnbits-eclair-1 > /dev/null
echo "open channel from lnd-2 to eclair-1"
lncli-sim 2 openchannel $(get-eclair-pubkey) $channel_size $balance_size > /dev/null
bitcoin-cli-sim -generate $channel_confirms > /dev/null
wait-for-lnd-channel 2
wait-for-clightning-channel 1
wait-for-clightning-channel 2
wait-for-clightning-channel 3
wait-for-eclair-channel
lnbits-lightning-sync
}
wait-for-lnd-channel(){
while true; do
pending=$(lncli-sim $1 pendingchannels | jq -r '.pending_open_channels | length')
echo "lnd-$1 pendingchannels: $pending"
if [[ "$pending" == "0" ]]; then
break
fi
sleep 1
done
}
wait-for-lnd-sync(){
while true; do
if [[ "$(lncli-sim $1 getinfo 2>&1 | jq -r '.synced_to_chain' 2> /dev/null)" == "true" ]]; then
echo "lnd-$1 is synced!"
break
fi
echo "waiting for lnd-$1 to sync..."
sleep 1
done
}
wait-for-clightning-channel(){
while true; do
pending=$(lightning-cli-sim $1 getinfo | jq -r '.num_pending_channels | length')
echo "cln-$1 pendingchannels: $pending"
if [[ "$pending" == "0" ]]; then
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_bitcoind_sync' 2> /dev/null)" == "null" ]]; then
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_lightningd_sync' 2> /dev/null)" == "null" ]]; then
break
fi
fi
fi
sleep 1
done
}
wait-for-clightning-sync(){
while true; do
if [[ ! "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.id' 2> /dev/null)" == "null" ]]; then
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_bitcoind_sync' 2> /dev/null)" == "null" ]]; then
if [[ "$(lightning-cli-sim $1 getinfo 2>&1 | jq -r '.warning_lightningd_sync' 2> /dev/null)" == "null" ]]; then
echo "cln-$1 is synced!"
break
fi
fi
fi
echo "waiting for cln-$1 to sync..."
sleep 1
done
}
-74
View File
@@ -1,74 +0,0 @@
#!/bin/bash
print_success() {
printf "\033[;1;32mPASSED\033[;0m $1\n"
}
print_error() {
printf "\033[;1;31mFAILED\033[;0m $1\n"
}
run(){
label=$1
value=$2
cmd=$3
if [[ "$cmd" == "$value" ]]; then
print_success "$label is $cmd"
else
print_error "$label is $cmd, should be $value"
failed="true"
fi
}
failed="false"
blockheight=213
utxos=3
channel_size=24000000 # 0.024 btc
balance_size=12000000 # 0.012 btc
source $(pwd)/docker-scripts.sh
lnbits-regtest-start
echo "=================================="
printf "\033[;1;36mregtest started! starting tests...\033[;0m\n"
echo "=================================="
echo ""
for i in 1 2 3; do
run "lnd-$i .synced_to_chain" "true" $(lncli-sim $i getinfo | jq -r ".synced_to_chain")
run "lnd-$i utxo count" $utxos $(lncli-sim $i listunspent | jq -r ".utxos | length")
run "lnd-$i .block_height" $blockheight $(lncli-sim $i getinfo | jq -r ".block_height")
if [[ "$i" == "1" ]]; then
channel_count=6
else
channel_count=3
fi
run "lnd-$i openchannels" $channel_count $(lncli-sim $i listchannels | jq -r ".channels | length")
run "lnd-$i .channels[0].capacity" $channel_size $(lncli-sim $i listchannels | jq -r ".channels[0].capacity")
run "lnd-$i .channels[0].push_amount_sat" $balance_size $(lncli-sim $i listchannels | jq -r ".channels[0].push_amount_sat")
done
for i in 1 2 3; do
# run "cln-$i blockheight" $blockheight $(lightning-cli-sim $i getinfo | jq -r ".blockheight")
run "cln-$i utxo count" $utxos $(lightning-cli-sim $i listfunds | jq -r ".outputs | length")
run "cln-$i openchannels" 2 $(lightning-cli-sim $i getinfo | jq -r ".num_active_channels")
run "cln-$i channel[0].state" "CHANNELD_NORMAL" $(lightning-cli-sim $i listfunds | jq -r ".channels[0].state")
run "cln-$i channel[0].amount_msat" $(($channel_size * 1000)) $(lightning-cli-sim $i listfunds | jq -r ".channels[0].amount_msat" | sed 's/msat//g')
run "cln-$i channel[0].our_amount_msat" $(($balance_size * 1000)) $(lightning-cli-sim $i listfunds | jq -r ".channels[0].our_amount_msat" | sed 's/msat//g')
done
run "eclair-1 openchannels" 2 $(docker exec lnbits-eclair-1 curl -s http://localhost:8080/channels -X POST -u :lnbits| jq '. | length')
run "eclair-1 blockHeight" $blockheight $(docker exec lnbits-eclair-1 curl -s http://localhost:8080/getinfo -X POST -u :lnbits| jq '.blockHeight')
run "lnbits service status" "200" $(curl -s -o /dev/null -w "%{http_code}" "http://localhost:5001/")
run "boltz service status" "200" $(curl -s -o /dev/null --head -w "%{http_code}" "http://localhost:9001/version")
# return non-zero exit code if a test fails
if [[ "$failed" == "true" ]]; then
echo ""
echo "=================================="
print_error "one more more tests failed"
echo "=================================="
exit 1
else
echo ""
echo "=================================="
print_success "all tests passed! yay!"
echo "=================================="
fi
-41
View File
@@ -1,41 +0,0 @@
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"]
-25
View File
@@ -1,25 +0,0 @@
#!/bin/bash
set -e
node spark_sidecar/server.mjs &
SIDECAR_PID=$!
# Wait for startup
for i in {1..10}; do
if nc -z localhost $SPARK_SIDECAR_PORT; then
echo "sparksidebar is up!"
break
fi
echo "Waiting for sparksidebar to start..."
sleep 1
done
# Optional: check if still not up
if ! nc -z localhost $SPARK_SIDECAR_PORT; then
echo "sparksidebar did not start successfully."
exit 1
fi
echo "Starting LNbits on $LNBITS_HOST:$LNBITS_PORT..."
exec uv run lnbits --port "$LNBITS_PORT" --host "$LNBITS_HOST" --forwarded-allow-ips='*'
-1
View File
@@ -4,7 +4,6 @@ color_scheme: dark
logo: "/logos/lnbits-full-inverse.png"
search_enabled: true
url: https://docs.lnbits.org
markdown: gfm
aux_links:
"LNbits on GitHub":
- "//github.com/lnbits/lnbits"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 KiB

+49 -104
View File
@@ -1,133 +1,78 @@
---
layout: default
title: Admin UI
nav_order: 1
nav_order: 4
---
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:300px">
</picture>
</a>
# Admin UI
![phase: stable](https://img.shields.io/badge/phase-stable-2EA043)
![PRs: welcome](https://img.shields.io/badge/PRs-Welcome-yellow)
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
The LNbits Admin UI lets you change LNbits settings via the LNbits frontend.
It is disabled by default and the first time you set the environment variable `LNBITS_ADMIN_UI=true`
the settings are initialized and saved to the database and will be used from there as long the UI is enabled.
From there on the settings from the database are used.
# LNBits Admin UI
# Super User
[What you can do](#what-you-can-do-with-the-admin-ui) · [First Run](#first-run-and-super-user-id) · [Enable/Disable](#enabling-or-disabling-the-admin-ui) · [Reset](#reset-to-defaults) · [Allowed Users](#allowed-users) · [Guides](#additional-guides)
With the Admin UI we introduced the super user, it is created with the initialisation of the Admin UI and will be shown with a success message in the server logs.
The super user has access to the server and can change settings that may crash the server and make it unresponsive via the frontend and api, like changing funding sources.
**We introduced the Admin UI as the new default to make setup simpler and more straightforward**. Instead of hand editing the `.env` file, you configure key server settings directly in the frontend with clear labels and guardrails.
Also only the super user can brrrr satoshis to different wallets.
<ins>On a fresh install the Admin UI is enabled by default</ins>, and at first launch you are prompted to create **Super User** credentials so that sensitive operations, such as switching funding sources, remain in trusted hands. When the Admin UI is enabled, configuration is written to and read from the database; for all settings managed by the UI, the parameters in `.env` are largely no longer used. If you disable the Admin UI, the `.env` file becomes the single source of truth again.
The super user is only stored inside the settings table of the database and after the settings are "reset to defaults" and a restart happened,
a new super user is created.
For privileged actions and role details see **[Super User](./super_user.md)** & [User Roles](./user_roles.md)
For a complete reference of legacy variables consult **[.env.example](../../.env.example)**.
The super user is never sent over the api and the frontend only receives a bool if you are super user or not.
<img width="900" height="640" alt="grafik" src="https://github.com/user-attachments/assets/d8852b4b-21be-446f-a1e7-d3eb794d3505" />
We also added a decorator for the API routes to check for super user.
> [!WARNING]
> Some settings remain `.env` only. Use **[.env.example](../../.env.example#L3-L87)** as the authoritative reference for those variables.
There is also the possibility of posting the super user via webhook to another service when it is created. you can look it up here https://github.com/lnbits/lnbits/blob/main/lnbits/settings.py `class SaaSSettings`
## What you can do with the Admin UI
# Admin Users
- Switch funding sources and other server level settings
- Manage who can access LNbits (**[Allowed Users](#allowed-users)**)
- Promote or demote Admin Users
- Gate extensions to Admins only or disable them globally
- Adjust balances with credit or debit
- Adjust site customization
environment variable: `LNBITS_ADMIN_USERS`, comma-separated list of user ids
Admin Users can change settings in the admin ui as well, with the exception of funding source settings, because they require e server restart and could potentially make the server inaccessible. Also they have access to all the extension defined in `LNBITS_ADMIN_EXTENSIONS`.
> [!NOTE]
> See **[Super User](./super_user.md)** for the role and permission differences compared to Admin Users.
# Allowed Users
## First run and Super User ID
environment variable: `LNBITS_ALLOWED_USERS`, comma-separated list of user ids
By defining this users, LNbits will no longer be usable by the public, only defined users and admins can then access the LNbits frontend.
On first start with the Admin UI enabled you will be prompted to generate a Super User.
Setting this environment variable also disables account creation.
Account creation can be also disabled by setting `LNBITS_ALLOW_NEW_ACCOUNTS=false`
<img width="1573" height="976" alt="Admin_UI_first_install" src="https://github.com/user-attachments/assets/05aa634f-06ec-4a4d-a5c6-d90927c90991" />
# How to activate
If you need to read it from disk later:
```
$ sudo systemctl stop lnbits.service
$ cd ~/lnbits
$ sudo nano .env
```
```bash
cat /lnbits/data/.super_user
# example
-> set: `LNBITS_ADMIN_UI=true`
Now start LNbits once in the terminal window
```
$ uv run lnbits
```
You can now `cat` the Super User ID:
```
$ cat data/.super_user
123de4bfdddddbbeb48c8bc8382fe123
```
> [!WARNING]
> For security reasons, Super Users and Admin users must authenticate with credentials (username and password).
You can access your super user account at `/wallet?usr=super_user_id`. You just have to append it to your normal LNbits web domain.
After login you will see **Settings** and **Users** in the sidebar between **Wallets** and **Extensions**, plus a role badge in the top left.
After that you will find the **`Admin` / `Manage Server`** between `Wallets` and `Extensions`
<img width="1353" height="914" alt="grafik" src="https://github.com/user-attachments/assets/06bb4f36-a23a-4058-87ec-60440d322c25" />
Here you can design the interface, it has credit/debit to change wallets balances and you can restrict access rights to extensions only for admins or generally deactivated for everyone. You can make users admins or set up Allowed Users if you want to restrict access. And of course the classic settings of the .env file, e.g. to change the funding source wallet or set a charge fee.
## Enabling or disabling the Admin UI
Do not forget
The Admin UI is enabled by default on new installs. To change the state:
```
sudo systemctl start lnbits.service
```
1. Stop LNbits
```bash
sudo systemctl stop lnbits.service
```
2. Edit your `.env`
```
cd ~/lnbits
sudo nano .env
```
3. Set one of
```
# Enable Admin UI
LNBITS_ADMIN_UI=true
# Disable Admin UI
LNBITS_ADMIN_UI=false
```
4. Start LNbits
```
sudo systemctl start lnbits.service
```
> [!NOTE]
> With the Admin UI enabled, config is DB-backed and UI-managed settings ignore .env. Disable it to revert to [.env](../../.env.example) as the single source of truth.
## Reset to defaults
Using `Reset to defaults` in the Admin UI wipes stored settings. After a restart, a new `Super User` is created and the old one is no longer valid.
## Allowed Users
When set **at least one**, LNbits becomes private: only the listed users and Admins can access the frontend. Account creation is disabled automatically. You can also disable account creation explicitly.
<img width="1889" height="870" alt="grafik" src="https://github.com/user-attachments/assets/89011b75-a267-44ea-971a-1517968b7af5" />
> [!WARNING]
> Assign your own account first when enabling **Allowed Users** to avoid locking yourself out. If you do get locked out, use your Super User to recover access.
## Additional Guides
- **[Backend Wallets](./wallets.md)** — Explore options to fund your LNbits instance.
- **[User Roles](./user_roles.md)** — Overview of existing roles in LNbits.
- **[Funding sources](./funding-sources-table.md)** — What is available and how to configure each.
- **[Install LNBits](./installation.md)** — Choose your prefared way to install LNBits.
## Powered by LNbits
LNbits empowers everyone with modular, open source tools for building Bitcoin based systems — fast, free, and extendable.
If you like this project, [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
[![LNbits Shop](https://demo.lnbits.com/static/images/bitcoin-shop-banner.png)](https://shop.lnbits.com/)
[![Visit LNbits Shop](https://img.shields.io/badge/Visit-LNbits%20Shop-7C3AED?logo=shopping-cart&logoColor=white&labelColor=5B21B6)](https://shop.lnbits.com/)
[![Try myLNbits SaaS](https://img.shields.io/badge/Try-myLNbits%20SaaS-2563EB?logo=lightning&logoColor=white&labelColor=1E40AF)](https://my.lnbits.com/login)
[![Read LNbits News](https://img.shields.io/badge/Read-LNbits%20News-F97316?logo=rss&logoColor=white&labelColor=C2410C)](https://news.lnbits.com/)
[![Explore LNbits Extensions](https://img.shields.io/badge/Explore-LNbits%20Extensions-10B981?logo=puzzle-piece&logoColor=white&labelColor=065F46)](https://extensions.lnbits.com/)
A little hint, if you set `RESET TO DEFAULTS`, then a new Super User Account will also be created. The old one is then no longer valid.
-6
View File
@@ -1,9 +1,3 @@
---
layout: default
title: Extension Install
nav_order: 1
---
# Extension Install
Anyone can create an extension by following the [example extension](https://github.com/lnbits/example) and [making extensions](https://github.com/lnbits/lnbits/blob/main/docs/devs/extensions.md) dev guide.
@@ -1,9 +1,3 @@
---
layout: default
title: FastAPI extension upgrade
nav_order: 1
---
## Defining a route with path parameters
**old:**
+24 -81
View File
@@ -1,87 +1,30 @@
---
layout: default
title: Wallet comparison
nav_order: 1
---
# LNbits Funding Sources Comparison Table
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:300px">
</picture>
</a>
LNbits can use a number of different Lightning Network funding source.
![phase: stable](https://img.shields.io/badge/phase-stable-2EA043)
![PRs: welcome](https://img.shields.io/badge/PRs-Welcome-yellow)
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
There may be trade-offs between the funding sources used, for example funding LNbits using Strike requires the user to KYC themselves and has some
privacy compromises versus running your own LND node. However the technical barrier to entry of using Strike is lower than using LND.
# Backend Wallet Comparison Table
LNbits lets you choose **how your wallets are funded** — from fully self-custodial nodes to simple hosted services. You can switch the funding source **without touching your apps, users, or extensions**. That means you can start fast, learn, and later upgrade to more control and privacy when you are ready.
**Why this matters**
- **Flexibility:** Pick the backend that fits your skills and constraints today, change it later with minimal friction.
- **Speed to ship:** Use a hosted option to get live quickly; move to a node when you need more control.
- **Scalability:** Match cost and maintenance to your stage — from hobby to production.
- **Privacy and compliance:** Choose between self-custody and provider-managed options depending on your requirements.
Below is a side-by-side comparison of Lightning funding sources you can use with LNbits.
> [!NOTE]
> “Backend Wallet” and “Funding Source” mean the same thing — the wallet or service that funds your LNbits.
The table below offers a comparison of the different Lightning Network funding sources that can be used with LNbits.
## LNbits Lightning Network Funding Sources Comparison Table
| **Funding Source** | **Custodial Type** | **KYC Required** | **Technical Knowledge Needed** | **Node Hosting Required** | **Privacy Level** | **Liquidity Management** | **Ease of Setup** | **Maintenance Effort** | **Cost Implications** | **Scalability** | **Notes** |
| ------------------------------ | ------------------------ | ------------------- | ------------------------------ | ------------------------- | ----------------- | ------------------------ | ----------------- | ---------------------- | -------------------------------------------- | --------------- | ------------------------------------------------------------------------------------------ |
| **LND (gRPC)** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | gRPC interface for LND; suitable for advanced integrations. |
| **CoreLightning (CLN)** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | Requires setting up and managing your own CLN node. |
| **Phoenixd** | Self-custodial | ❌ | Medium | ❌ | Medium | Automatic | Moderate | Low | Minimal fees | Medium | Mobile wallet backend; suitable for mobile integrations. |
| **Nostr Wallet Connect (NWC)** | Custodial | Depends on provider | Low | ❌ | Variable | Provider-managed | Easy | Low | May incur fees | Medium | Connects via Nostr protocol; depends on provider's policies. |
| **Boltz** | Self-custodial | ❌ | Medium | ❌ | Medium | Provider-managed | Moderate | Moderate | Minimal fees | Medium | Uses submarine swaps; connects to Boltz client. |
| **LND (REST)** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | REST interface for LND; suitable for web integrations. |
| **CoreLightning REST** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | REST interface for CLN; suitable for web integrations. |
| **LNbits (another instance)** | Custodial | Depends on host | Low | ❌ | Variable | Provider-managed | Easy | Low | May incur hosting fees | Medium | Connects to another LNbits instance; depends on host's policies. |
| **Alby** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Browser extension wallet; suitable for web users. |
| **Breez SDK** | Self-custodial | ❌ | Medium | ❌ | High | Automatic | Moderate | Low | Minimal fees | Medium | SDK for integrating Breez wallet functionalities. |
| **OpenNode** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for merchants. |
| **Blink** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; focuses on mobile integrations. |
| **ZBD** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Gaming-focused payment platform. |
| **Spark (CLN)** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | Web interface for CLN; requires Spark server setup. |
| **Spark (L2)** | Self-custodial | ❌ | Medium | ❌ | High | Automatic | Easy | Low | Minimal fees | Medium | Runs via Spark sidecar; seed-based self-custody. |
| **Cliche Wallet** | Self-custodial | | Medium | ❌ | Medium | Manual | Moderate | Moderate | Minimal fees | Medium | Lightweight wallet; suitable for embedded systems. |
| **Strike** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
| **LNPay** | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
| **Eclair (ACINQ)** | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | Connects via API; you run and manage your Eclair node. |
| **LN.tips** | Custodial/Self-Custodial | Depends on provider | Medium | ❌ | Low | Provider-managed | Moderate | Low | Transaction fees may apply | Medium | Simple hosted service; use LN.tips API as your backend. |
| **Fake Wallet** | Testing (simulated) | ❌ | Low | ❌ | N/A | N/A | Easy | Low | None (test only) | N/A | For testing only; mints accounting units in LNbits (no real sats, unit name configurable). |
## Spark (L2)
Spark L2 uses a local Node.js sidecar to expose an HTTP API that LNbits can use as a funding source. It is self-custodial and secured by a standard mnemonic seed. Sidecar repo `https://github.com/lnbits/spark_sidecar`.
### Notes for readers
- These are typical characteristics; your exact experience may vary by configuration and provider policy.
- Pick based on your constraints: compliance (KYC), privacy, ops effort, and time-to-ship.
---
## Additional Guides
- **[Admin UI](./admin_ui.md)** — Manage server settings via a clean UI (avoid editing `.env` by hand).
- **[User Roles](./User_Roles.md)** — Quick Overview of existing Roles in LNBits.
- **[Funding sources](./funding-sources_table.md)** — Whats available and how to enable/configure each.
## Powered by LNbits
LNbits empowers everyone with modular, open-source tools for building Bitcoin-based systems — fast, free, and extendable.
If you like this project, [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
[![LNbits Shop](https://demo.lnbits.com/static/images/bitcoin-shop-banner.png)](https://shop.lnbits.com/)
[![Visit LNbits Shop](https://img.shields.io/badge/Visit-LNbits%20Shop-7C3AED?logo=shopping-cart&logoColor=white&labelColor=5B21B6)](https://shop.lnbits.com/)
[![Try myLNbits SaaS](https://img.shields.io/badge/Try-myLNbits%20SaaS-2563EB?logo=lightning&logoColor=white&labelColor=1E40AF)](https://my.lnbits.com/login)
[![Read LNbits News](https://img.shields.io/badge/Read-LNbits%20News-F97316?logo=rss&logoColor=white&labelColor=C2410C)](https://news.lnbits.com/)
[![Explore LNbits Extensions](https://img.shields.io/badge/Explore-LNbits%20Extensions-10B981?logo=puzzle-piece&logoColor=white&labelColor=065F46)](https://extensions.lnbits.com/)
| **Funding Source** | **Custodial Type** | **KYC Required** | **Technical Knowledge Needed** | **Node Hosting Required** | **Privacy Level** | **Liquidity Management** | **Ease of Setup** | **Maintenance Effort** | **Cost Implications** | **Scalability** | **Notes** |
| -------------------------- | ------------------ | ------------------- | ------------------------------ | ------------------------- | ----------------- | ------------------------ | ----------------- | ---------------------- | -------------------------------------------- | --------------- | ---------------------------------------------------------------- |
| LND (gRPC) | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | gRPC interface for LND; suitable for advanced integrations. |
| CoreLightning (CLN) | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | Requires setting up and managing your own CLN node. |
| Phoenixd | Self-custodial | ❌ | Medium | ❌ | Medium | Automatic | Moderate | Low | Minimal fees | Medium | Mobile wallet backend; suitable for mobile integrations. |
| Nostr Wallet Connect (NWC) | Custodial | Depends on provider | Low | ❌ | Variable | Provider-managed | Easy | Low | May incur fees | Medium | Connects via Nostr protocol; depends on provider's policies. |
| Boltz | Self-custodial | ❌ | Medium | ❌ | Medium | Provider-managed | Moderate | Moderate | Minimal fees | Medium | Uses submarine swaps; connects to Boltz client. |
| LND (REST) | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | REST interface for LND; suitable for web integrations. |
| CoreLightning REST | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | REST interface for CLN; suitable for web integrations. |
| LNbits (another instance) | Custodial | Depends on host | Low | ❌ | Variable | Provider-managed | Easy | Low | May incur hosting fees | Medium | Connects to another LNbits instance; depends on host's policies. |
| Alby | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Browser extension wallet; suitable for web users. |
| Breez SDK | Self-custodial | ❌ | Medium | ❌ | High | Automatic | Moderate | Low | Minimal fees | Medium | SDK for integrating Breez wallet functionalities. |
| OpenNode | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for merchants. |
| Blink | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; focuses on mobile integrations. |
| ZBD | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Gaming-focused payment platform. |
| Spark (CLN) | Self-custodial | ❌ | Higher | ✅ | High | Manual | Moderate | High | Infrastructure cost and channel opening fees | High | Web interface for CLN; requires Spark server setup. |
| Cliche Wallet | Self-custodial | ❌ | Medium | ❌ | Medium | Manual | Moderate | Moderate | Minimal fees | Medium | Lightweight wallet; suitable for embedded systems. |
| Strike | Custodial | | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
| LNPay | Custodial | ✅ | Low | ❌ | Low | Provider-managed | Easy | Low | Transaction fees apply | Medium | Third-party service; suitable for quick setups. |
+192 -458
View File
@@ -1,72 +1,39 @@
---
layout: default
title: Installation
nav_order: 1
title: Basic installation
nav_order: 2
---
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:300px">
</picture>
</a>
![phase: stable](https://img.shields.io/badge/phase-stable-2EA043) ![License: MIT](https://img.shields.io/badge/License-MIT-blue) ![PRs: welcome](https://img.shields.io/badge/PRs-Welcome-yellow) [![explore: LNbits extensions](https://img.shields.io/badge/explore-LNbits%20extensions-10B981)](https://extensions.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
# Basic installation
> [!NOTE]
> **Default DB:** LNbits uses SQLite by default (simple & effective). You can switch to PostgreSQL — see the section below.
Note that by default LNbits uses SQLite as its database, which is simple and effective but you can configure it to use PostgreSQL instead which is also described in a section below.
## Table of contents
## Option 1: AppImage (LInux)
- [Option 1: AppImage (Linux)](#option-1-appimage-linux)
- [Option 2: UV (recommended for developers)](#option-2-uv-recommended-for-developers)
- [Option 2a (Legacy): Poetry — Replaced by UV](#option-2a-legacy-poetry--replaced-by-uv)
- [Option 3: Install script (Debian/Ubuntu)](#option-3-install-script-debianubuntu)
- [Option 4: Nix](#option-4-nix)
- [Option 5: Docker](#option-5-docker)
- [Option 6: Fly.io](#option-6-flyio)
- [Troubleshooting](#troubleshooting)
- [Optional: PostgreSQL database](#optional-postgresql-database)
- [Using LNbits](#using-lnbits)
- [Additional guides](#additional-guides)
- [Update LNbits (all methods)](#update-lnbits-all-methods)
- [SQLite → PostgreSQL migration](#sqlite--postgresql-migration)
- [LNbits as a systemd service](#lnbits-as-a-systemd-service)
- [Reverse proxy with automatic HTTPS (Caddy)](#reverse-proxy-with-automatic-https-caddy)
- [Apache2 reverse proxy over HTTPS](#apache2-reverse-proxy-over-https)
- [Nginx reverse proxy over HTTPS](#nginx-reverse-proxy-over-https)
- [HTTPS without a reverse proxy (self-signed)](#https-without-a-reverse-proxy-self-signed)
- [LNbits on Umbrel behind Tor](#lnbits-on-umbrel-behind-tor)
- [FreeBSD notes](#freebsd-notes)
### AppImage (Linux)
## Option 1: AppImage (Linux)
**Quickstart**
1. Download latest AppImage from [releases](https://github.com/lnbits/lnbits/releases) **or** run:
Go to [releases](https://github.com/lnbits/lnbits/releases) and pull latest AppImage, or:
```sh
sudo apt-get install jq libfuse2
sudo apt-get install libfuse2
wget $(curl -s https://api.github.com/repos/lnbits/lnbits/releases/latest | jq -r '.assets[] | select(.name | endswith(".AppImage")) | .browser_download_url') -O LNbits-latest.AppImage
chmod +x LNbits-latest.AppImage
LNBITS_ADMIN_UI=true HOST=0.0.0.0 PORT=5000 ./LNbits-latest.AppImage # most system settings are now in the admin UI, but pass additional .env variables here
```
- LNbits will create a folder for DB and extension files **in the same directory** as the AppImage.
> [!NOTE]
> **Next steps**
> Install complete → **[Running LNbits](#run-the-server)**
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
LNbits will create a folder for db and extension files in the folder the AppImage runs from.
## Option 2: UV (recommended for developers)
> [!IMPORTANT]
> **It is recommended to use the latest version of UV & Make sure you have Python version 3.12 installed.**
It is recommended to use the latest version of UV. Make sure you have Python version `3.12` installed.
### Verify Python
### Install Python 3.12
## Option 2 (recommended): UV
It is recommended to use the latest version of UV. Make sure you have Python version 3.10 or higher installed.
### Verify Python version
```sh
python3 --version
@@ -79,86 +46,26 @@ curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
```
### Install LNbits
```sh
git clone https://github.com/lnbits/lnbits.git
cd lnbits
git checkout main
uv sync --all-extras
cp .env.example .env
# Optional: set funding source and other options in .env (e.g., `nano .env`)
```
### Run the server
```sh
uv run lnbits
# To change port/host: uv run lnbits --port 9000 --host 0.0.0.0
# Add --debug to the command above and set DEBUG=true in .env for verbose output
```
### LNbits CLI
```sh
# Useful for superuser ID, updating extensions, etc.
uv run lnbits-cli --help
```
### Update LNbits
```sh
cd lnbits
# Stop LNbits with Ctrl + X or your service manager
# sudo systemctl stop lnbits
# Update code
git pull --rebase
uv sync --all-extras
uv run lnbits
```
#### Use Admin UI → Extensions → "Update All" to bring extensions up to the proper level
> [!NOTE]
> **Next steps**
> Install complete → **[Running LNbits](#run-the-server)**
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
## Option 2a (Legacy): Poetry — _Replaced by UV_
<details>
<summary><strong>Poetry install and update (legacy workflow)</summary>
This legacy section is preserved for older environments.
**UV is the recommended (and faster) tool** for new installs. Use Poetry only if you have personal preferences or must support an older workflow.
> ![IMPORTANT](https://img.shields.io/badge/IMPORTANT-7c3aed?labelColor=494949)
> **It is recommended to use the latest version of Poetry & Make sure you have Python version 3.12 installed.**
### Verify Python version
```sh
python3 --version
```
### Install Poetry
### (old) Install Poetry
```sh
# If path 'export PATH="$HOME/.local/bin:$PATH"' fails, use the path echoed by the install
curl -sSL https://install.python-poetry.org | python3 - && export PATH="$HOME/.local/bin:$PATH"
curl -sSL https://install.python-poetry.org | python3 -
export PATH="$HOME/.local/bin:$PATH"
```
### Install LNbits
### install LNbits
```sh
git clone https://github.com/lnbits/lnbits.git
cd lnbits
poetry env use 3.12
git checkout main
poetry install --only main
uv sync --all-extras
# or poetry
# poetry env use 3.12
# poetry install --only main
cp .env.example .env
# Optional: to set funding source amongst other options via the env `nano .env`
```
@@ -166,53 +73,50 @@ cp .env.example .env
#### Running the server
```sh
poetry run lnbits
# To change port/host: poetry run lnbits --port 9000 --host 0.0.0.0
# Add --debug to help troubleshooting (also set DEBUG=true in .env)
uv run lnbits
# To change port/host pass 'uv run lnbits --port 9000 --host 0.0.0.0'
# or poetry
# poetry run lnbits
# adding --debug in the start-up command above to help your troubleshooting and generate a more verbose output
# Note that you have to add the line DEBUG=true in your .env file, too.
```
#### LNbits CLI
#### LNbits-cli
```sh
# A very useful terminal client for getting the superuser ID, updating extensions, etc.
poetry run lnbits-cli --help
# A very useful terminal client for getting the supersuer ID, updating extensions, etc
uv run lnbits-cli --help
```
#### Updating the server
```sh
cd lnbits
# Stop LNbits with Ctrl + X or with your service manager
# Stop LNbits with `ctrl + x` or with service manager
# sudo systemctl stop lnbits
# Update LNbits
git pull --rebase
# Check your Poetry Python version
poetry env list
# If version is less than 3.12, update it:
poetry env use python3.12
poetry env remove python3.X
poetry env list
# Check your poetry version with
# poetry env list
# If version is less 3.12, update it by running
# poetry env use python3.12
# poetry env remove python3.9
# poetry env list
# Reinstall and start
poetry install --only main
poetry run lnbits
# Run install and start LNbits with
# poetry install --only main
# poetry run lnbits
uv sync --all-extras
uv run lnbits
# use LNbits admin UI Extensions page function "Update All" do get extensions onto proper level
```
#### Use Admin UI → Extensions → "Update All" to bring extensions up to the proper level
> ![NOTE](https://img.shields.io/badge/NOTE-3b82f6?labelColor=494949)
> **Next steps**
> Install complete → **[Running LNbits](#run-the-server)**
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
</details>
## Option 3: Install script (Debian/Ubuntu)
<details>
<summary><strong>Show install script</strong> (one-line setup)</summary>
## Option 2: Install script (on Debian/Ubuntu)
```sh
wget https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits.sh &&
@@ -220,23 +124,15 @@ chmod +x lnbits.sh &&
./lnbits.sh
```
- You can use `./lnbits.sh` to run, but for more control: `cd lnbits` and use `uv run lnbits` (see Option 2).
Now visit `0.0.0.0:5000` to make a super-user account.
> ![NOTE](https://img.shields.io/badge/NOTE-3b82f6?labelColor=494949)
> **Next steps**
> Install complete → **[Running LNbits](#run-the-server)**
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
`./lnbits.sh` can be used to run, but for more control `cd lnbits` and use `uv run lnbits` (see previous option).
</details>
## Option 4: Nix
<details>
<summary><strong>Show Nix instructions</strong> (flakes, cachix, run)</summary>
## Option 3: Nix
```sh
# Install nix. If you have installed via another manager, remove and use this install (from https://nixos.org/download)
sh <(curl --proto '=https' --tlsv1.2 -L https://nixos.org/nix/install) --daemon --yes
sh <(curl --proto '=https' --tlsv1.2 -L https://nixos.org/nix/install) --daemon
# Enable nix-command and flakes experimental features for nix:
grep -qxF 'experimental-features = nix-command flakes' /etc/nix/nix.conf || \
@@ -249,29 +145,16 @@ echo "trusted-users = root $USER" | sudo tee -a /etc/nix/nix.conf
# Restart daemon so changes apply
sudo systemctl restart nix-daemon
# Clone and build LNbits
git clone https://github.com/lnbits/lnbits.git
cd lnbits
# Make data directory and persist data/extension folders
mkdir data
PROJECT_DIR="$(pwd)"
{
echo "export PYTHONPATH=\"$PROJECT_DIR/ns:\$PYTHONPATH\""
echo "export LNBITS_DATA_FOLDER=\"$PROJECT_DIR/data\""
echo "export LNBITS_EXTENSIONS_PATH=\"$PROJECT_DIR\""
} >> ~/.bashrc
grep -qxF '. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' ~/.bashrc || \
echo '. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' >> ~/.bashrc
. ~/.bashrc
# Add cachix for cached binaries
nix-env -iA cachix -f https://cachix.org/api/v1/install
cachix use lnbits
# Build LNbits
# Clone and build LNbits
git clone https://github.com/lnbits/lnbits.git
cd lnbits
nix build
mkdir data
```
#### Running the server
@@ -291,36 +174,26 @@ LNBITS_ADMIN_UI=true ./result/bin/lnbits --port 9000 --host 0.0.0.0
SUPER_USER=be54db7f245346c8833eaa430e1e0405 LNBITS_ADMIN_UI=true ./result/bin/lnbits --port 9000
```
> ![NOTE](https://img.shields.io/badge/NOTE-3b82f6?labelColor=494949)
> **Next steps**
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
## Option 4: Docker
</details>
## Option 5: Docker
<details>
<summary><strong>Show Docker instructions</strong> (official image, volumes, extensions)</summary>
**Use latest image**
Use latest version from Docker Hub.
```sh
docker pull lnbits/lnbits
wget https://raw.githubusercontent.com/lnbits/lnbits/main/.env.example -O .env
mkdir data
docker run --detach --publish 5000:5000 --name lnbits \
--volume ${PWD}/.env:/app/.env \
--volume ${PWD}/data/:/app/data \
lnbits/lnbits
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
```
- The LNbits Docker image ships **without any extensions**; by default, any extensions you install are stored **inside the container** and will be **lost** when the container is removed, so you should set `LNBITS_EXTENSIONS_PATH` to a directory thats **mapped to a persistent host volume** so extensions **survive rebuilds/recreates**—for example:
The LNbits Docker image comes with no extensions installed. User-installed extensions will be stored by default in a container directory.
It is recommended to point the `LNBITS_EXTENSIONS_PATH` environment variable to a directory that is mapped to a Docker volume. This way, the extensions will not be reinstalled when the container is destroyed.
Example:
```sh
docker run ... -e "LNBITS_EXTENSIONS_PATH='/app/data/extensions'" --volume ${PWD}/data/:/app/data ...
```
**Build image yourself**
Build the image yourself.
```sh
git clone https://github.com/lnbits/lnbits.git
@@ -328,39 +201,24 @@ cd lnbits
docker build -t lnbits/lnbits .
cp .env.example .env
mkdir data
docker run --detach --publish 5000:5000 --name lnbits \
--volume ${PWD}/.env:/app/.env \
--volume ${PWD}/data/:/app/data \
lnbits/lnbits
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits/lnbits
```
You can optionally override the install extras for both **Poetry** and **UV** to include optional features during build or setup:
- with Poetry, pass extras via the `POETRY_INSTALL_ARGS` Docker build-arg (e.g., to enable the **Breez** funding source: `docker build --build-arg POETRY_INSTALL_ARGS="-E breez" -t lnbits/lnbits .`);
- with UV, enable extras during environment sync (e.g., locally run `uv sync --extra breez` or `uv sync --all-extras`), and—**if your Dockerfile supports it**—you can mirror the same at build time via a build-arg such as `UV_SYNC_ARGS` (example pattern: `docker build --build-arg UV_SYNC_ARGS="--extra breez" -t lnbits/lnbits .`).
**Enable Breez funding source at build**
You can optionally override the arguments that are passed to `poetry install` during the build process by setting the Docker build argument named `POETRY_INSTALL_ARGS`. For example, to enable the Breez funding source, build the Docker image with the command:
```sh
docker build --build-arg POETRY_INSTALL_ARGS="-E breez" -t lnbits/lnbits .
```
> ![NOTE](https://img.shields.io/badge/NOTE-3b82f6?labelColor=494949)
> **Next steps**
> Install complete → **[Running LNbits](#run-the-server)**
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
## Option 5: Fly.io
</details>
Fly.io is a docker container hosting platform that has a generous free tier. You can host LNbits for free on Fly.io for personal use.
## Option 6: Fly.io
First, sign up for an account at [Fly.io](https://fly.io) (no credit card required).
<details>
<summary><strong>Deploy LNbits on Fly.io (free tier friendly)</summary>
Then, install the Fly.io CLI onto your device [here](https://fly.io/docs/getting-started/installing-flyctl/).
**Fly.io is a docker container hosting platform that has a generous free tier. You can host LNbits for free on Fly.io for personal use.**
1. Create an account at [Fly.io](https://fly.io).
2. Install the Fly.io CLI ([guide](https://fly.io/docs/getting-started/installing-flyctl/)).
After install is complete, the command will output a command you should copy/paste/run to get `fly` into your `$PATH`. Something like:
```
flyctl was installed successfully to /home/ubuntu/.fly/bin/flyctl
@@ -369,9 +227,9 @@ Manually add the directory to your $HOME/.bash_profile (or similar)
export PATH="$FLYCTL_INSTALL/bin:$PATH"
```
3. You can either run those commands, then `source ~/.bash_profile` or, if you don't, you'll have to call Fly from `~/.fly/bin/flyctl`.
You can either run those commands, then `source ~/.bash_profile` or, if you don't, you'll have to call Fly from `~/.fly/bin/flyctl`.
- Once installed, run the following commands.
Once installed, run the following commands.
```
git clone https://github.com/lnbits/lnbits.git
@@ -385,16 +243,9 @@ You'll be prompted to enter an app name, region, postgres (choose no), deploy no
You'll now find a file in the directory called `fly.toml`. Open that file and modify/add the following settings.
> ![IMPORTANT](https://img.shields.io/badge/IMPORTANT-7c3aed?labelColor=494949)
> Be sure to replace `${PUT_YOUR_LNBITS_ENV_VARS_HERE}` with all relevant environment variables in `.env` or `.env.example`.
> Environment variable strings should be quoted here. For example, if `.env` has
> `LNBITS_ENDPOINT=https://demo.lnbits.com`, then in `fly.toml` use
> `LNBITS_ENDPOINT="https://demo.lnbits.com"`.
Note: Be sure to replace `${PUT_YOUR_LNBITS_ENV_VARS_HERE}` with all relevant environment variables in `.env` or `.env.example`. Environment variable strings should be quoted here, so if in `.env` you have `LNBITS_ENDPOINT=https://demo.lnbits.com` in `fly.toml` you should have `LNBITS_ENDPOINT="https://demo.lnbits.com"`.
> ![WARNING](https://img.shields.io/badge/WARNING-ea580c?labelColor=494949)
> Don't enter secret environment variables here. Fly.io offers **secrets** (via `fly secrets`) that are exposed as env vars at runtime.
> Example (LND REST funding source):
> `fly secrets set LND_REST_MACAROON=<hex_macaroon_data>`
Note: Don't enter secret environment variables here. Fly.io offers secrets (via the `fly secrets` command) that are exposed as environment variables in your runtime. So, for example, if using the LND_REST funding source, you can run `fly secrets set LND_REST_MACAROON=<hex_macaroon_data>`.
```
...
@@ -449,49 +300,26 @@ sudo apt install python3.10-dev gcc build-essential
poetry add setuptools wheel
```
> ![NOTE](https://img.shields.io/badge/NOTE-3b82f6?labelColor=0b0b0b)
>
> **Next steps**
> Install complete → **[Running LNbits](#run-the-server)**
> Update LNbits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
### Optional: PostgreSQL database
## Troubleshooting
If you want to use LNbits at scale, we recommend using PostgreSQL as the backend database. Install Postgres and setup a database for LNbits:
```sh
sudo apt install pkg-config libffi-dev libpq-dev
# on debian/ubuntu 'sudo apt-get -y install postgresql'
# or follow instructions at https://www.postgresql.org/download/linux/
# build essentials (Debian/Ubuntu)
sudo apt install python3.10-dev gcc build-essential
# if secp256k1 build fails and you used poetry
poetry add setuptools wheel
```
</details>
</details>
## Optional: PostgreSQL database
> [!TIP]
> If you want to use LNbits at scale, we recommend using PostgreSQL as the backend database. Install Postgres and set up a database for LNbits.
```sh
# Debian/Ubuntu: sudo apt-get -y install postgresql
# or see https://www.postgresql.org/download/linux/
# Create a password for the postgres user
# Postgres doesn't have a default password, so we'll create one.
sudo -i -u postgres
psql
# in psql
ALTER USER postgres PASSWORD 'myPassword';
# on psql
ALTER USER postgres PASSWORD 'myPassword'; # choose whatever password you want
\q
# back as postgres user
# on postgres user
createdb lnbits
exit
```
**Configure LNbits**
You need to edit the `.env` file.
```sh
# add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL=
@@ -502,189 +330,44 @@ LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost:5432/lnbits"
# Using LNbits
Visit **[http://localhost:5000/](http://localhost:5000/)** (or `0.0.0.0:5000`).
Now you can visit your LNbits at http://localhost:5000/.
### Option A — First-run setup in the Browser (UI)
Now modify the `.env` file with any settings you prefer and add a proper [funding source](./wallets.md) by modifying the value of `LNBITS_BACKEND_WALLET_CLASS` and providing the extra information and credentials related to the chosen funding source.
1. On the **first start**, LNbits will **prompt you to Setup a SuperUser**.
2. After creating it, youll be **redirected to the Admin UI as SuperUser**.
3. In the Admin UI, **set your funding source** (backend wallet) and other preferences.
4. **Restart LNbits** if prompted or after changing critical settings.
Then you can restart it and it will be using the new settings.
> [!IMPORTANT]
> Use the **SuperUser only** for initial setup and instance settings (funding source, configuration, Topup).
> For maintenance, create a separate **Admin** account. For everyday usage (payments, wallets, etc.), **do not use the SuperUser** — use admin or regular user accounts instead. Its a bad behaviour.
> Read more about [SuperUser](./super_user.md) and [Admin UI](./admin_ui.md)
You might also need to install additional packages or perform additional setup steps, depending on the chosen backend. See [the short guide](./wallets.md) on each different funding source.
### Option B — Configure via `.env`
1. Edit your `.env` with preferred settings (funding, base URL, etc.).
2. Set a funding source by configuring:
- `LNBITS_BACKEND_WALLET_CLASS`
- plus the required credentials for your chosen backend (see **[wallets.md](./wallets.md)**).
3. **Restart LNbits** to apply changes.
---
> [!NOTE]
> **Paths overview**
>
> - **SuperUser file:** `<lnbits_root>/data/.super_user`
> Example: `~/lnbits/data/.super_user` • View: `cat ~/lnbits/data/.super_user`
> - **Environment file:** `<lnbits_root>/.env` (for bare-metal installs)
> - **Docker:** bind a host directory to `/app/data`.
> On the host the SuperUser file is at `<host_data_dir>/.super_user`.
> The container reads `/app/.env` (usually bind-mounted from your project root).
> [!TIP]
> **Local Lightning test network**
> Use **Polar** to spin up a safe local Lightning environment and test LNbits without touching your live setup.
> https://lightningpolar.com/
> [!TIP]
> **API comparison before updates**
> Use **TableTown** to diff your LNbits instance against another (dev vs prod) or the upstream dev branch. Spot endpoint changes before updating.
> Crafted by [Arbadacarbayk](https://github.com/arbadacarbaYK) - a standout contribution that makes pre-release reviews fast and reliable.
> https://arbadacarbayk.github.io/LNbits_TableTown/
Take a look at [Polar](https://lightningpolar.com/) for an excellent way of spinning up a Lightning Network dev environment.
# Additional guides
## Update LNbits (all methods)
## SQLite to PostgreSQL migration
> After updating, open **Admin UI → Extensions → “Update All”** to make sure extensions match the core version.
If you already have LNbits installed and running, on an SQLite database, we **highly** recommend you migrate to postgres if you are planning to run LNbits on scale.
<details>
<summary><strong>UV (recommended)</strong></summary>
```sh
cd lnbits
git pull --rebase
uv sync --all-extras
# restart (dev)
uv run lnbits
```
</details>
<details>
<summary><strong>Poetry (legacy)</strong></summary>
```sh
cd lnbits
git pull --rebase
# Optional: ensure Python 3.12
poetry env list
poetry env use python3.12
poetry install --only main
# restart (dev)
poetry run lnbits
```
</details>
<details>
<summary><strong>AppImage</strong></summary>
Download the latest AppImage from Releases and replace your old file **in the same directory** to keep the `./data` folder (DB, extensions).
</details>
<details>
<summary><strong>Install script (Debian/Ubuntu)</strong></summary>
```sh
# If you installed via lnbits.sh:
cd lnbits
git pull --rebase
# then use your chosen runner (UV recommended)
uv sync --all-extras
uv run lnbits
```
</details>
<details>
<summary><strong>Nix</strong></summary>
```sh
cd lnbits
git pull --rebase
nix build
# restart
nix run
```
</details>
<details>
<summary><strong>Docker (official image)</strong></summary>
```sh
docker pull lnbits/lnbits
docker stop lnbits && docker rm lnbits
docker run --detach --publish 5000:5000 --name lnbits \
--volume ${PWD}/.env:/app/.env \
--volume ${PWD}/data/:/app/data \
lnbits/lnbits
```
</details>
<details>
<summary><strong>Docker (build yourself)</strong></summary>
```sh
cd lnbits
git pull --rebase
docker build -t lnbits/lnbits .
docker stop lnbits && docker rm lnbits
docker run --detach --publish 5000:5000 --name lnbits \
--volume ${PWD}/.env:/app/.env \
--volume ${PWD}/data/:/app/data \
lnbits/lnbits
```
</details>
<details>
<summary><strong>Fly.io</strong></summary>
```sh
# If using Dockerfile in repo (recommended)
cd lnbits
git pull --rebase
fly deploy
# Logs & shell if needed
fly logs
fly ssh console
```
</details>
## SQLite → PostgreSQL migration
> [!TIP]
> If you run on SQLite and plan to scale, migrate to Postgres.
There's a script included that can do the migration easy. You should have Postgres already installed and there should be a password for the user (see Postgres install guide above). Additionally, your LNbits instance should run once on postgres to implement the database schema before the migration works:
```sh
# STOP LNbits
# Edit .env with Postgres URL
# add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL=
# postgres://<user>:<password>@<host>/<database> - alter line bellow with your user, password and db name
LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
# save and exit
# START then STOP LNbits once to apply schema
# START LNbits
# STOP LNbits
uv run python tools/conv.py
# or
make migration
```
- Launch LNbits again and verify.
Hopefully, everything works and get migrated... Launch LNbits again and check if everything is working properly.
## LNbits as a systemd service
Create `/etc/systemd/system/lnbits.service`:
Systemd is great for taking care of your LNbits instance. It will start it on boot and restart it in case it crashes. If you want to run LNbits as a systemd service on your Debian/Ubuntu/Raspbian server, create a file at `/etc/systemd/system/lnbits.service` with the following content:
```
# Systemd unit for lnbits
@@ -692,14 +375,17 @@ Create `/etc/systemd/system/lnbits.service`:
[Unit]
Description=LNbits
# Optional: start after your backend
# you can uncomment these lines if you know what you're doing
# it will make sure that lnbits starts after lnd (replace with your own backend service)
#Wants=lnd.service
#After=lnd.service
[Service]
# replace with the absolute path of your lnbits installation
WorkingDirectory=/home/lnbits/lnbits
# Find uv path via `which uv`
# same here. run `which uv` if you can't find the poetry binary
ExecStart=/home/lnbits/.local/bin/uv run lnbits
# replace with the user that you're running lnbits on
User=lnbits
Restart=always
TimeoutSec=120
@@ -710,23 +396,33 @@ Environment=PYTHONUNBUFFERED=1
WantedBy=multi-user.target
```
Enable & start:
Save the file and run the following commands:
```sh
sudo systemctl enable lnbits.service
sudo systemctl start lnbits.service
```
## Reverse proxy with automatic HTTPS (Caddy)
## Reverse proxy with automatic HTTPS using Caddy
Point your domain A-record to your server IP. Install Caddy: [Caddy install guide](https://caddyserver.com/docs/install#debian-ubuntu-raspbian)
Use Caddy to make your LNbits install accessible over clearnet with a domain and https cert.
```sh
Point your domain at the IP of the server you're running LNbits on, by making an `A` record.
Install Caddy on the server
https://caddyserver.com/docs/install#debian-ubuntu-raspbian
```
sudo caddy stop
```
Create a Caddyfile
```
sudo nano Caddyfile
```
Add:
Assuming your LNbits is running on port `5000` add:
```
yourdomain.com {
@@ -736,21 +432,28 @@ yourdomain.com {
}
```
Save (Ctrl+X) and start:
Save and exit `CTRL + x`
```sh
```
sudo caddy start
```
## Apache2 reverse proxy over HTTPS
## Running behind an Apache2 reverse proxy over HTTPS
Install Apache2 and enable Apache2 mods:
```sh
apt-get install apache2 certbot
a2enmod headers ssl proxy proxy_http
```
Create a SSL certificate with LetsEncrypt:
```sh
certbot certonly --webroot --agree-tos --non-interactive --webroot-path /var/www/html -d lnbits.org
```
Create `/etc/apache2/sites-enabled/lnbits.conf`:
Create an Apache2 vhost at: `/etc/apache2/sites-enabled/lnbits.conf`:
```sh
cat <<EOF > /etc/apache2/sites-enabled/lnbits.conf
@@ -777,20 +480,27 @@ cat <<EOF > /etc/apache2/sites-enabled/lnbits.conf
EOF
```
Restart:
Restart Apache2:
```sh
service apache2 restart
```
## Nginx reverse proxy over HTTPS
## Running behind an Nginx reverse proxy over HTTPS
Install nginx:
```sh
apt-get install nginx certbot
```
Create a SSL certificate with LetsEncrypt:
```sh
certbot certonly --nginx --agree-tos -d lnbits.org
```
Create `/etc/nginx/sites-enabled/lnbits.org`:
Create an nginx vhost at `/etc/nginx/sites-enabled/lnbits.org`:
```sh
cat <<EOF > /etc/nginx/sites-enabled/lnbits.org
@@ -824,22 +534,23 @@ server {
EOF
```
Restart:
Restart nginx:
```sh
service nginx restart
```
---
## Using https without reverse proxy
## HTTPS without a reverse proxy (self-signed)
The most common way of using LNbits via https is to use a reverse proxy such as Caddy, nginx, or ngriok. However, you can also run LNbits via https without additional software. This is useful for development purposes or if you want to use LNbits in your local network.
Create a self-signed cert (useful for local/dev). Browsers wont trust it by default.
We have to create a self-signed certificate using `mkcert`. Note that this certificate is not "trusted" by most browsers but that's fine (since you know that you have created it) and encryption is always better than clear text.
### Install mkcert
#### Install mkcert
- Install instructions: [mkcert README](https://github.com/FiloSottile/mkcert)
- Ubuntu example:
You can find the install instructions for `mkcert` [here](https://github.com/FiloSottile/mkcert).
Install mkcert on Ubuntu:
```sh
sudo apt install libnss3-tools
@@ -848,47 +559,70 @@ chmod +x mkcert-v*-linux-amd64
sudo cp mkcert-v*-linux-amd64 /usr/local/bin/mkcert
```
### Create certificate
#### Create certificate
**OpenSSL**
To create a certificate, first `cd` into your LNbits folder and execute the following command on Linux:
```sh
openssl req -new -newkey rsa:4096 -x509 -sha256 -days 3650 -nodes -out cert.pem -keyout key.pem
```
**mkcert** (alternative)
This will create two new files (`key.pem` and `cert.pem `).
Alternatively, you can use mkcert ([more info](https://kifarunix.com/how-to-create-self-signed-ssl-certificate-with-mkcert-on-ubuntu-18-04/)):
```sh
# include your local IP (e.g., 192.x.x.x) if needed
# add your local IP (192.x.x.x) as well if you want to use it in your local network
mkcert localhost 127.0.0.1 ::1
```
**Run with certs**
You can then pass the certificate files to uvicorn when you start LNbits:
```sh
poetry run uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5000 --ssl-keyfile ./key.pem --ssl-certfile ./cert.pem
```
## LNbits on Umbrel behind Tor
## LNbits running on Umbrel behind Tor
See this community [guide](https://community.getumbrel.com/t/guide-lnbits-without-tor/604).
If you want to run LNbits on your Umbrel but want it to be reached through clearnet, _Uxellodunum_ made an extensive [guide](https://community.getumbrel.com/t/guide-lnbits-without-tor/604) on how to do it.
## FreeBSD notes
## Docker installation
Issue with secp256k1 0.14.0 on FreeBSD (thanks @GitKalle):
To install using docker you first need to build the docker image as:
1. Install `py311-secp256k1` with `pkg install py311-secp256k1`.
2. Change version in `pyproject.toml` from `0.14.0` to `0.13.2`.
3. Rewrite `poetry.lock` with `poetry lock`.
4. Follow install instructions with Poetry.
```
git clone https://github.com/lnbits/lnbits.git
cd lnbits
docker build -t lnbits/lnbits .
```
---
You can launch the docker in a different directory, but make sure to copy `.env.example` from lnbits there
## Powered by LNbits
```
cp <lnbits_repo>/.env.example .env
```
LNbits empowers everyone with modular, open-source tools for building Bitcoin-based systems — fast, free, and extendable.
and change the configuration in `.env` as required.
If you like this project [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visiting our [Shop](https://shop.lnbits.com)
Then create the data directory
[![LNbits Shop](https://demo.lnbits.com/static/images/bitcoin-shop-banner.png)](https://shop.lnbits.com/)
[![Visit LNbits Shop](https://img.shields.io/badge/Visit-LNbits%20Shop-7C3AED?logo=shopping-cart&logoColor=white&labelColor=5B21B6)](https://shop.lnbits.com/) [![Try myLNbits SaaS](https://img.shields.io/badge/Try-myLNbits%20SaaS-2563EB?logo=lightning&logoColor=white&labelColor=1E40AF)](https://my.lnbits.com/login) [![Read LNbits News](https://img.shields.io/badge/Read-LNbits%20News-F97316?logo=rss&logoColor=white&labelColor=C2410C)](https://news.lnbits.com/) [![Explore LNbits Extensions](https://img.shields.io/badge/Explore-LNbits%20Extensions-10B981?logo=puzzle-piece&logoColor=white&labelColor=065F46)](https://extensions.lnbits.com/)
```
mkdir data
```
Then the image can be run as:
```
docker run --detach --publish 5000:5000 --name lnbits -e "LNBITS_BACKEND_WALLET_CLASS='FakeWallet'" --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits
```
Finally you can access your lnbits on your machine at port 5000.
### FreeBSD notes
Currently there is an issue with secp256k1 0.14.0 on FreeBSD. Thanks to @GitKalle
1. Install package `py311-secp256k1` with `pkg install py311-secp256k1`
2. Change version in `pyproject.toml` from 0.14.0 to 0.13.2
3. Rewrite `poetry.lock` file with command `poetry lock`
4. Follow install instruction with Poetry
-139
View File
@@ -1,139 +0,0 @@
# 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
-132
View File
@@ -1,132 +0,0 @@
---
layout: default
title: Super user
nav_order: 1
---
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:300px">
</picture>
</a>
![phase: stable](https://img.shields.io/badge/phase-stable-2EA043)
![PRs: welcome](https://img.shields.io/badge/PRs-Welcome-yellow)
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
# LNbits Super User (SU)
**Table of Contents**
- [What is the Super User?](#what-is-the-super-user)
- [When is the Super User created?](#when-is-the-super-user-created)
- [Disabeling the Admin UI](#disabeling-the-admin-ui)
- [Super User identity and storage](#super-user-identity-and-storage)
- [Security model since v1](#security-model-since-v1)
- [Admin vs Super User](#admin-vs-super-user)
- [Operational guidance](#operational-guidance)
- [Additional guides](#additional-guides)
<details>
<summary><strong>TLDR</strong></summary>
- **No Admin UI → No Super User.** The Super User (SU) exists only when `LNBITS_ADMIN_UI=true`.
- **Why SU exists:** SU can do a few high impact actions regular admins cannot, like **changing the funding source**, **restarting the server from the UI**, and **crediting or debiting accounts**.
- **Login changes since v1:** Logging in by **user ID** for SU and admins is **disabled**. On first visit after enabling the Admin UI you will be prompted to set a **username and password** for the SU.
- **Trust model:** Admins and the SU share about **99 percent of the same powers**, but the SU is the one trusted with funding source control and cannot be demoted by regular admins.
</details>
## What is the Super User?
The **Super User** is the owner-operator account of an LNbits instance. Think of it as your “break glass” operator with a few capabilities that are intentionally reserved for the person ultimately responsible for the server and the funding rails.
The SU is created alongside the [Admin UI](./admin_ui.md) and is meant to keep enviroment operations pleasant in the UI while keeping the most sensitive knobs in trusted hands.
**Key SU capabilities**
- **Change the funding source** for the instance
- **Restart the LNbits server** from the web UI
- **Credit or debit accounts** for operational corrections
> Note
> These are separated from regular admin tasks on purpose. It helps maintain least privilege and reduces the chance of accidental or malicious changes.
## Admin vs Super User
| Capability | Admin | Super User |
| ------------------------ | ---------- | ---------- |
| View Admin UI | If enabled | If enabled |
| Change funding source | — | ✓ |
| Credit or debit accounts | — | ✓ |
| Restart server from UI | — | ✓ |
| Manage users and wallets | ✓ | ✓ |
| Instance-level settings | ✓ | ✓ |
| Manage notifications | ✓ | ✓ |
| Exchange rates | ✓ | ✓ |
| View all Payments | ✓ | ✓ |
**Why both roles?**
In many teams the person running the server prefers to **delegate day-to-day admin work** while keeping funding and final authority safe. Admins can do almost everything; the SU retains the last few high risk powers.
## When is the Super User created?
- The SU is created **only** when you enable the Admin UI: `LNBITS_ADMIN_UI=true`.
- If the Admin UI is **disabled**, there is **no SU** and all SU-only UI is hidden.
## Disabeling the Admin UI
> [!IMPORTANT]
> Read the [Admin UI guide](./admin_ui.md) before Disabeling. You are turning on a management surface; do it deliberately.
Set the environment variable in your deployment:
```bash
# .env
LNBITS_ADMIN_UI=false
```
## Super User identity and storage
LNbits stores the **Super User ID** at:
```
/lnbits/data/.super_user
```
- Back this up along with the rest of `/lnbits/data` as part of your secure backup routine.
- **Changing who is the SU** can only be done by someone with **CLI access to the host OS** where LNbits runs. **Regular admins cannot revoke or replace the SU in the Admin UI.**
## Security model since v1
- **User-ID logins are disabled** for SU and admin roles.
- **Credentialed login is required:** set a **username and password** for the SU at first run of the Admin UI.
- **SU secrecy:** Regular users and admins **cannot discover the SU user ID** through normal UI flows.
## Operational guidance
These are practical tips for running a safe and friendly instance.
- It is normal to **delegate admin** duties to trusted people. Admins have about **99 percent** of SU powers for day-to-day work.
- Keep the **SU** reserved for the person legally or operationally responsible for the **funding source**.
- Use admin roles for regular day-to-day management and keep the SU for reserved SU tasks only.
## Additional guides
- **[Admin UI](./admin_ui.md)** — Manage server settings in the browser instead of editing `.env` or using the CLI for routine tasks.
- **[User Roles](./user_roles.md)** — Overview of roles and what they can do.
- **[Funding sources](./funding-sources-table.md)** — Available options and how to enable and configure them.
- **[Install LNBits](./installation.md)** — Choose your prefared way to install LNBits.
## Powered by LNbits
LNbits empowers everyone with modular, open source tools for building Bitcoin-based systems — fast, free, and extendable.
If you like this project, [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
[![LNbits Shop](https://demo.lnbits.com/static/images/bitcoin-shop-banner.png)](https://shop.lnbits.com/)
[![Visit LNbits Shop](https://img.shields.io/badge/Visit-LNbits%20Shop-7C3AED?logo=shopping-cart&logoColor=white&labelColor=5B21B6)](https://shop.lnbits.com/)
[![Try myLNbits SaaS](https://img.shields.io/badge/Try-myLNbits%20SaaS-2563EB?logo=lightning&logoColor=white&labelColor=1E40AF)](https://my.lnbits.com/login)
[![Read LNbits News](https://img.shields.io/badge/Read-LNbits%20News-F97316?logo=rss&logoColor=white&labelColor=C2410C)](https://news.lnbits.com/)
[![Explore LNbits Extensions](https://img.shields.io/badge/Explore-LNbits%20Extensions-10B981?logo=puzzle-piece&logoColor=white&labelColor=065F46)](https://extensions.lnbits.com/)
-101
View File
@@ -1,101 +0,0 @@
---
layout: default
title: User Roles
nav_order: 1
---
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:300px">
</picture>
</a>
![phase: stable](https://img.shields.io/badge/phase-stable-2EA043)
![PRs: welcome](https://img.shields.io/badge/PRs-Welcome-yellow)
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
# LNbits Roles: A Quick Overview
### Understand **who can do what** in seconds: `Super User`, `Admin`, and `Regular User`.
**Jump to:**
[Roles at a Glance](#roles-at-a-glance) •
[Super User](#super-user--master-control) •
[Admin](#admin--day-to-day-manager) •
[Regular User](#regular-user--everyday-use) •
[Best Practices](#best-practices) •
[Additional Guides](#additional-guides)
---
## Roles at a Glance
| Capability | **Super User** (owner) | **Admin** (manager) | **Regular User** (end user) |
| -------------------------------- | :--------------------: | :-----------------: | :-------------------------: |
| Change **funding source** | ✅ | ❌ | ❌ |
| Credit/Debit any wallet | ✅ | ❌ | ❌ |
| Manage Admins & Users | ✅ | ✅ | ❌ |
| Enable/disable extensions | ✅ | ✅ | ❌ |
| Use wallets & allowed extensions | ✅ | ✅ | ✅ |
> **Plain talk:** **Super User** = Owner • **Admin** = Trusted manager • **Regular User** = End user
## Role Snapshots
### Super User — Master Control
For initial setup and rare, high-impact changes.
- Configure **server-level settings** (e.g., funding source).
- **Credit/Debit** any wallet.
- Create and manage initial **Admin(s)**.
> **Sign-in:** username + password (v1+). The old query-string login is retired.
### Admin — Day-to-Day Manager
For running the service without touching the most sensitive knobs.
- Manage **Users**, **Admins**, and **Extensions** in the Admin UI.
- Adjust security-related settings (e.g., `rate_limiter`, `ip_blocker`).
- Handle operations settings (e.g., `service_fee`, `invoice_expiry`).
- Build brand design in **Site Customization**.
- Update user accounts.
**Typical tasks:** onboarding users, enabling extensions, tidying wallets, reviewing activity.
> **Sign-in:** username + password (v1+). The old query-string login is retired.
### Regular User — Everyday Use
For using LNbits, not administering it.
- Access **personal wallets** and **allowed extensions**.
- No server/admin privileges.
**Typical tasks:** receive and send payments, use enabled extensions.
## Best Practices
- **Minimize risk:** Reserve **Super User** for rare, sensitive actions (funding source, debit/credit). Use **Admin** for daily operations.
- **Keep access tidy:** Review your Admin list occasionally; remove unused accounts.
- **Change management:** Test risky changes (like funding) in a staging setup first.
## Additional Guides
- **[Admin UI](./admin_ui.md)** — Manage server settings via a clean UI (avoid editing `.env` by hand).
- **[Super User](./super_user.md)** — Deep dive on responsibilities and safe usage patterns.
- **[Funding sources](./funding-sources-table.md)** — Whats available and how to enable/configure each.
## Powered by LNbits
LNbits empowers everyone with modular, open-source tools for building Bitcoin-based systems—fast, free, and extendable.
If you like this project, [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
[![LNbits Shop](https://demo.lnbits.com/static/images/bitcoin-shop-banner.png)](https://shop.lnbits.com/)
[![Visit LNbits Shop](https://img.shields.io/badge/Visit-LNbits%20Shop-7C3AED?logo=shopping-cart&logoColor=white&labelColor=5B21B6)](https://shop.lnbits.com/)
[![Try myLNbits SaaS](https://img.shields.io/badge/Try-myLNbits%20SaaS-2563EB?logo=lightning&logoColor=white&labelColor=1E40AF)](https://my.lnbits.com/login)
[![Read LNbits News](https://img.shields.io/badge/Read-LNbits%20News-F97316?logo=rss&logoColor=white&labelColor=C2410C)](https://news.lnbits.com/)
[![Explore LNbits Extensions](https://img.shields.io/badge/Explore-LNbits%20Extensions-10B981?logo=puzzle-piece&logoColor=white&labelColor=065F46)](https://extensions.lnbits.com/)
+110 -327
View File
@@ -4,394 +4,177 @@ title: Backend wallets
nav_order: 3
---
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:300px">
</picture>
</a>
![phase: stable](https://img.shields.io/badge/phase-stable-2EA043)
![PRs: welcome](https://img.shields.io/badge/PRs-Welcome-yellow)
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
# Backend wallets
**LNbits is modular**: You can switch the funding source (backend wallet) **without changing anything else** in your setup. Keep your extensions, apps, users, and config as-is — just point LNbits to a different backend via environment variables.
LNbits can run on top of many Lightning Network funding sources with more being added regularly.
**What stays the same when you switch backends**
A backend wallet can be configured using the following LNbits environment variables:
- Your LNbits setup and extensions
- Your API keys and endpoints
- Your server and deployment setup
A backend wallet is selected and configured entirely through LNbits environment variables. See the options and variables below, and compare them here: [Funding-Source-Table.md](funding-sources-table.md)
> [!NOTE]
> **Terminology:** “Backend Wallet” and “Funding Source” mean the same thing — the wallet or service that funds your LNbits.
## Funding Sources
## Funding Sources
| | | |
| ----------------------------------------------- | ------------------------------------- | ------------------------------------------------- |
| [CLNRest (runes)](#clnrest-runes) | [LND (REST)](#lnd-rest) | [OpenNode](#opennode) |
| [CoreLightning](#corelightning) | [LND (gRPC)](#lnd-grpc) | [Blink](#blink) |
| [CoreLightning REST](#corelightning-rest) | [LNbits](#lnbits) | [Alby](#alby) |
| [Spark (Core Lightning)](#spark-core-lightning) | [LNPay](#lnpay) | [Boltz](#boltz) |
| [Spark L2](#spark-l2) | [ZBD](#zbd) | [Phoenixd](#phoenixd) |
| [Cliche Wallet](#cliche-wallet) | | |
| [Breez SDK](#breez-sdk) | [Breez Liquid SDK](#breez-liquid-sdk) | [Nostr Wallet Connect](#nostr-wallet-connect-nwc) |
| [Strike](#strike) | [Eclair (ACINQ)](#eclair-acinq) | [LN.tips](#lntips) |
| [Fake Wallet](#fake-wallet) | | |
---
<a id="clnrest-runes"></a>
You can [compare the LNbits compatible Lightning Network funding sources here](wallets.md).
### CLNRest (using [runes](https://docs.corelightning.org/reference/lightning-createrune))
[Core Lightning REST API docs](https://docs.corelightning.org/docs/rest)
[Core lightning Rest API docs](https://docs.corelightning.org/docs/rest)
Should also work with the [Rust version of CLNRest](https://github.com/daywalker90/clnrest-rs)
**Environment variables**
- `LNBITS_BACKEND_WALLET_CLASS`: `CLNRestWallet`
- `LNBITS_BACKEND_WALLET_CLASS`: **CLNRestWallet**
- `CLNREST_URL`: `https://127.0.0.1:3010`
- `CLNREST_CA`: `/home/lightningd/.lightning/bitcoin/ca.pem` (or the content of the file)
- `CLNREST_CERT`: `/home/lightningd/.lightning/bitcoin/server.pem` (or the content of the file)
- `CLNREST_LAST_PAY_INDEX`: `lightning-cli listinvoices | jq -r '.invoices | map(.created_index) | max'`
- `CLNREST_CA`: `/home/lightningd/.lightning/bitcoin/ca.pem` (or the content of the `ca.pem` file)
- `CLNREST_CERT`: `/home/lightningd/.lightning/bitcoin/server.pem` (or the content of the `server.pem` file)
- `CLNREST_READONLY_RUNE`: `lightning-cli createrune restrictions='[["method=listfunds", "method=listpays", "method=listinvoices", "method=getinfo", "method=summary", "method=waitanyinvoice"]]' | jq -r .rune`
- `CLNREST_INVOICE_RUNE`: `lightning-cli createrune restrictions='[["method=invoice"], ["pnameamount_msat<1000001"], ["pnamelabel^LNbits"], ["rate=60"]]' | jq -r .rune`
- `CLNREST_PAY_RUNE`: `lightning-cli createrune restrictions='[["method=pay"], ["pinvbolt11_amount<1001"], ["pnamelabel^LNbits"], ["rate=1"]]' | jq -r .rune`
- `CLNREST_RENEPAY_RUNE`: `lightning-cli createrune restrictions='[["method=renepay"], ["pinvinvstring_amount<1001"], ["pnamelabel^LNbits"], ["rate=1"]]' | jq -r .rune`
- `CLNREST_LAST_PAY_INDEX`: `lightning-cli listinvoices | jq -r '.invoices | map(.created_index) | max' `
- `CLNREST_NODEID`: `lightning-cli getinfo | jq -r .id` (only required for v23.08)
**Create runes (copy/paste)**
### CoreLightning
```bash
# Read-only: funds, pays, invoices, info, summary, and invoice listener
lightning-cli createrune \
restrictions='[["method=listfunds","method=listpays","method=listinvoices","method=getinfo","method=summary","method=waitanyinvoice"]]' \
| jq -r .rune
```
- `LNBITS_BACKEND_WALLET_CLASS`: **CoreLightningWallet**
- `CORELIGHTNING_RPC`: /file/path/lightning-rpc
```bash
# Invoice: max 1,000,001 msat, label must start with "LNbits", 60 req/min
lightning-cli createrune \
restrictions='[["method=invoice"], ["pnameamount_msat<1000001"], ["pnamelabel^LNbits"], ["rate=60"]]' \
| jq -r .rune
```
### CoreLightning REST
```bash
# Pay: bolt11 amount < 1001 (msat), label must start with "LNbits", 1 req/min
lightning-cli createrune \
restrictions='[["method=pay"], ["pinvbolt11_amount<1001"], ["pnamelabel^LNbits"], ["rate=1"]]' \
| jq -r .rune
```
This is the old REST interface that uses [Ride The Lightning/c-lightning-REST](https://github.com/Ride-The-Lightning/c-lightning-REST)
```bash
# Renepay: invstring amount < 1001 (msat), label must start with "LNbits", 1 req/min
lightning-cli createrune \
restrictions='[["method=renepay"], ["pinvinvstring_amount<1001"], ["pnamelabel^LNbits"], ["rate=1"]]' \
| jq -r .rune
```
- `LNBITS_BACKEND_WALLET_CLASS`: **CoreLightningRestWallet**
- `CORELIGHTNING_REST_URL`: http://127.0.0.1:8185/
- `CORELIGHTNING_REST_MACAROON`: /file/path/admin.macaroon or Base64/Hex
- `CORELIGHTNING_REST_CERT`: /home/lightning/clnrest/tls.cert
Set the resulting values into:
### Spark (Core Lightning)
- `CLNREST_READONLY_RUNE`
- `CLNREST_INVOICE_RUNE`
- `CLNREST_PAY_RUNE`
- `CLNREST_RENEPAY_RUNE`
- `LNBITS_BACKEND_WALLET_CLASS`: **SparkWallet**
- `SPARK_URL`: http://10.147.17.230:9737/rpc
- `SPARK_TOKEN`: secret_access_key
## CoreLightning
### LND (REST)
**Required env vars**
- `LNBITS_BACKEND_WALLET_CLASS`: **LndRestWallet**
- `LND_REST_ENDPOINT`: http://10.147.17.230:8080/
- `LND_REST_CERT`: /file/path/tls.cert
- `LND_REST_MACAROON`: /file/path/admin.macaroon or Base64/Hex
- `LNBITS_BACKEND_WALLET_CLASS`: `CoreLightningWallet`
- `CORELIGHTNING_RPC`: `/file/path/lightning-rpc`
or
## CoreLightning REST
- `LND_REST_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
Old REST interface using [RTL c-lightning-REST](https://github.com/Ride-The-Lightning/c-lightning-REST)
### LND (gRPC)
**Required env vars**
- `LNBITS_BACKEND_WALLET_CLASS`: **LndWallet**
- `LND_GRPC_ENDPOINT`: ip_address
- `LND_GRPC_PORT`: port
- `LND_GRPC_CERT`: /file/path/tls.cert
- `LND_GRPC_MACAROON`: /file/path/admin.macaroon or Base64/Hex
- `LNBITS_BACKEND_WALLET_CLASS`: `CoreLightningRestWallet`
- `CORELIGHTNING_REST_URL`: `http://127.0.0.1:8185/`
- `CORELIGHTNING_REST_MACAROON`: `/file/path/admin.macaroon` or Base64/Hex
- `CORELIGHTNING_REST_CERT`: `/home/lightning/clnrest/tls.cert`
You can also use an AES-encrypted macaroon (more info) instead by using
## Spark (Core Lightning)
- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
**Required env vars**
To encrypt your macaroon, run `uv run lnbits-cli encrypt macaroon`.
- `LNBITS_BACKEND_WALLET_CLASS`: `SparkWallet`
- `SPARK_URL`: `http://10.147.17.230:9737/rpc`
- `SPARK_TOKEN`: `secret_access_key`
### LNbits
## Spark L2
- `LNBITS_BACKEND_WALLET_CLASS`: **LNbitsWallet**
- `LNBITS_ENDPOINT`: e.g. https://lnbits.com
- `LNBITS_KEY`: lnbitsAdminKey
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.
### LNPay
### Optional tuning
For the invoice listener to work you have a publicly accessible URL in your LNbits and must set up [LNPay webhooks](https://dashboard.lnpay.co/webhook/) pointing to `<your LNbits host>/wallet/webhook` with the "Wallet Receive" event and no secret. For example, `https://mylnbits/wallet/webhook` will be the Endpoint Url that gets notified about the payment.
- `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)_
- `LNBITS_BACKEND_WALLET_CLASS`: **LNPayWallet**
- `LNPAY_API_ENDPOINT`: https://api.lnpay.co/v1/
- `LNPAY_API_KEY`: sak_apiKey
- `LNPAY_WALLET_KEY`: waka_apiKey
### Example: run the sidecar
### OpenNode
```bash
git clone https://github.com/lnbits/spark_sidecar.git
cd spark_sidecar
npm install
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary.
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
```
- `LNBITS_BACKEND_WALLET_CLASS`: **OpenNodeWallet**
- `OPENNODE_API_ENDPOINT`: https://api.opennode.com/
- `OPENNODE_KEY`: opennodeAdminApiKey
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.
### Blink
## LND (REST)
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary. You can generate a Blink API key after logging in or creating a new Blink account at: https://dashboard.blink.sv. For more info visit: https://dev.blink.sv/api/auth#create-an-api-key```
**Required env vars**
- `LNBITS_BACKEND_WALLET_CLASS`: **BlinkWallet**
- `BLINK_API_ENDPOINT`: https://api.blink.sv/graphql
- `BLINK_WS_ENDPOINT`: wss://ws.blink.sv/graphql
- `BLINK_TOKEN`: BlinkToken
- `LNBITS_BACKEND_WALLET_CLASS`: `LndRestWallet`
- `LND_REST_ENDPOINT`: `http://10.147.17.230:8080/`
- `LND_REST_CERT`: `/file/path/tls.cert`
- `LND_REST_MACAROON`: `/file/path/admin.macaroon` or Base64/Hex
### Alby
or:
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary. You can generate an alby access token here: https://getalby.com/developer/access_tokens/new
- `LND_REST_MACAROON_ENCRYPTED`: `eNcRyPtEdMaCaRoOn`
- `LNBITS_BACKEND_WALLET_CLASS`: **AlbyWallet**
- `ALBY_API_ENDPOINT`: https://api.getalby.com/
- `ALBY_ACCESS_TOKEN`: AlbyAccessToken
## LND (gRPC)
### Boltz
**Required env vars**
This funding source connects to a running [boltz-client](https://docs.boltz.exchange/v/boltz-client) and handles all lightning payments through submarine swaps on the liquid network.
You can configure the daemon to run in standalone mode by `standalone = True` in the config file or using the cli flag (`boltzd --standalone`).
Once running, you can create a liquid wallet using `boltzcli wallet create lnbits lbtc`.
- `LNBITS_BACKEND_WALLET_CLASS`: `LndWallet`
- `LND_GRPC_ENDPOINT`: `ip_address`
- `LND_GRPC_PORT`: `port`
- `LND_GRPC_CERT`: `/file/path/tls.cert`
- `LND_GRPC_MACAROON`: `/file/path/admin.macaroon` or Base64/Hex
- `LNBITS_BACKEND_WALLET_CLASS`: **BoltzWallet**
- `BOLTZ_CLIENT_ENDPOINT`: 127.0.0.1:9002
- `BOLTZ_CLIENT_MACAROON`: /home/bob/.boltz/macaroons/admin.macaroon or Base64/Hex
- `BOLTZ_CLIENT_CERT`: /home/bob/.boltz/tls.cert or Base64/Hex
- `BOLTZ_CLIENT_WALLET`: lnbits
You can also use an AES-encrypted macaroon instead:
### ZBD
- `LND_GRPC_MACAROON_ENCRYPTED`: `eNcRyPtEdMaCaRoOn`
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary. You can generate an ZBD API Key here: https://zbd.dev/docs/dashboard/projects/api
To encrypt your macaroon:
- `LNBITS_BACKEND_WALLET_CLASS`: **ZBDWallet**
- `ZBD_API_ENDPOINT`: https://api.zebedee.io/v0/
- `ZBD_API_KEY`: ZBDApiKey
```bash
uv run lnbits-cli encrypt macaroon
```
### Phoenixd
## LNbits
For the invoice to work you must have a publicly accessible URL in your LNbits. You can get a phoenixd API key from the install
~/.phoenix/phoenix.conf, also see the documentation for phoenixd.
**Required env vars**
- `LNBITS_BACKEND_WALLET_CLASS`: **PhoenixdWallet**
- `PHOENIXD_API_ENDPOINT`: http://localhost:9740/
- `PHOENIXD_API_PASSWORD`: PhoenixdApiPassword
- `LNBITS_BACKEND_WALLET_CLASS`: `LNbitsWallet`
- `LNBITS_ENDPOINT`: for example `https://lnbits.com`
- `LNBITS_KEY`: `lnbitsAdminKey`
### Breez SDK
## LNPay
A Greenlight invite code or Greenlight partner certificate/key can be used to register a new node with Greenlight. If the Greenlight node already exists, neither are required.
For the invoice listener to work you must have a publicly accessible URL in your LNbits and set up [LNPay webhooks](https://dashboard.lnpay.co/webhook/) pointing to `<your LNbits host>/wallet/webhook` with the event **Wallet Receive** and no secret. Example: [https://mylnbits/wallet/webhook](`https://mylnbits/wallet/webhook).
- `LNBITS_BACKEND_WALLET_CLASS`: **BreezSdkWallet**
- `BREEZ_API_KEY`: ...
- `BREEZ_GREENLIGHT_SEED`: ...
- `BREEZ_GREENLIGHT_INVITE_CODE`: ...
- `BREEZ_GREENLIGHT_DEVICE_KEY`: /path/to/breezsdk/device.pem or Base64/Hex
- `BREEZ_GREENLIGHT_DEVICE_CERT`: /path/to/breezsdk/device.crt or Base64/Hex
**Required env vars**
### Breez Liquid SDK
- `LNBITS_BACKEND_WALLET_CLASS`: `LNPayWallet`
- `LNPAY_API_ENDPOINT`: `https://api.lnpay.co/v1/`
- `LNPAY_API_KEY`: `sak_apiKey`
- `LNPAY_WALLET_KEY`: `waka_apiKey`
This funding source leverages the [Breez SDK - Liquid](https://sdk-doc-liquid.breez.technology/) to manage all Lightning payments via submarine swaps on the Liquid network. To get started, simply provide a mnemonic seed phrase. The easiest way to generate one is by using a liquid wallet, such as [Blockstream Green](https://blockstream.com/green/). Once generated, you can copy the seed to your environment variable or enter it in the admin UI.
## OpenNode
- `LNBITS_BACKEND_WALLET_CLASS`: **BreezLiquidSdkWallet**
- `BREEZ_LIQUID_SEED`: ...
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook configuration required.
Each submarine swap incurs service and on-chain fees. To account for these, you may need to increase the reserve fee in the admin UI by navigating to **Settings -> Funding**, or by setting the following environment variables:
**Required env vars**
- `LNBITS_RESERVE_FEE_MIN`: ...
- `LNBITS_RESERVE_FEE_PERCENT`: ...
- `LNBITS_BACKEND_WALLET_CLASS`: `OpenNodeWallet`
- `OPENNODE_API_ENDPOINT`: `https://api.opennode.com/`
- `OPENNODE_KEY`: `opennodeAdminApiKey`
### Cliche Wallet
## Blink
- `CLICHE_ENDPOINT`: ws://127.0.0.1:12000
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook configuration required.
### Nostr Wallet Connect (NWC)
You can generate a Blink API key at [https://dashboard.blink.sv](https://dashboard.blink.sv). More info: [https://dev.blink.sv/api/auth#create-an-api-key](https://dev.blink.sv/api/auth#create-an-api-key)
To use NWC as funding source in LNbits you'll need a pairing URL (also known as pairing secret) from a NWC service provider. You can find a list of providers [here](https://github.com/getAlby/awesome-nwc?tab=readme-ov-file#nwc-wallets).
**Required env vars**
You can configure Nostr Wallet Connect in the admin ui or using the following environment variables:
- `LNBITS_BACKEND_WALLET_CLASS`: `BlinkWallet`
- `BLINK_API_ENDPOINT`: `https://api.blink.sv/graphql`
- `BLINK_WS_ENDPOINT`: `wss://ws.blink.sv/graphql`
- `BLINK_TOKEN`: `BlinkToken`
## Alby
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook configuration required.
Generate an Alby access token here: [https://getalby.com/developer/access_tokens/new](https://getalby.com/developer/access_tokens/new)
**Required env vars**
- `LNBITS_BACKEND_WALLET_CLASS`: `AlbyWallet`
- `ALBY_API_ENDPOINT`: `https://api.getalby.com/`
- `ALBY_ACCESS_TOKEN`: `AlbyAccessToken`
## Boltz
This connects to a running [boltz-client](https://docs.boltz.exchange/v/boltz-client) and handles Lightning payments through submarine swaps on the Liquid network.
You can run the daemon in standalone mode via `standalone = True` in the config or `boltzd --standalone`. Create a Liquid wallet with:
```bash
boltzcli wallet create lnbits lbtc
```
**Required env vars**
- `LNBITS_BACKEND_WALLET_CLASS`: `BoltzWallet`
- `BOLTZ_CLIENT_ENDPOINT`: `127.0.0.1:9002`
- `BOLTZ_CLIENT_MACAROON`: `/home/bob/.boltz/macaroons/admin.macaroon` or Base64/Hex
- `BOLTZ_CLIENT_CERT`: `/home/bob/.boltz/tls.cert` or Base64/Hex
- `BOLTZ_CLIENT_WALLET`: `lnbits`
## ZBD
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook configuration required.
Generate a ZBD API key here: [https://zbd.dev/docs/dashboard/projects/api](https://zbd.dev/docs/dashboard/projects/api)
**Required env vars**
- `LNBITS_BACKEND_WALLET_CLASS`: `ZBDWallet`
- `ZBD_API_ENDPOINT`: `https://api.zebedee.io/v0/`
- `ZBD_API_KEY`: `ZBDApiKey`
## Phoenixd
For the invoice to work you must have a publicly accessible URL in your LNbits.
You can get a phoenixd API key from `~/.phoenix/phoenix.conf`. See the phoenixd documentation for details.
**Required env vars**
- `LNBITS_BACKEND_WALLET_CLASS`: `PhoenixdWallet`
- `PHOENIXD_API_ENDPOINT`: `http://localhost:9740/`
- `PHOENIXD_API_PASSWORD`: `PhoenixdApiPassword`
## Eclair (ACINQ)
<a id="eclair-acinq"></a>
Connect to an existing Eclair node so your backend handles invoices and payments.
**Required env vars**
- `LNBITS_BACKEND_WALLET_CLASS`: `EclairWallet`
- `ECLAIR_URL`: `http://127.0.0.1:8283`
- `ECLAIR_PASSWORD`: `eclairpw`
## Fake Wallet
<a id="fake-wallet"></a>
A testing-only backend that mints accounting units inside LNbits accounting (no real sats).
**Required env vars**
- `LNBITS_BACKEND_WALLET_CLASS`: `FakeWallet`
- `FAKE_SECRET`: `ToTheMoon1`
- `FAKE_UNIT`: `sats`
## LN.tips
<a id="lntips"></a>
As the initinal LN.tips bot is no longer active the code still exists and is widly used. Connect one of custodial services as your backend to create and pay Lightning invoices through their API or selfhost this service and run it as funding source.
Resources: https://github.com/massmux/SatsMobiBot
**Required env vars**
- `LNBITS_BACKEND_WALLET_CLASS`: `LNTipsWallet`
- `LNTIPS_API_ENDPOINT`: `https://ln.tips`
- `LNTIPS_API_KEY`: `LNTIPS_ADMIN_KEY`
## Breez SDK
A Greenlight invite code or Greenlight partner certificate/key can register a new node with Greenlight. If the Greenlight node already exists, neither is required.
**Required env vars**
- `LNBITS_BACKEND_WALLET_CLASS`: `BreezSdkWallet`
- `BREEZ_API_KEY`: `...`
- `BREEZ_GREENLIGHT_SEED`: `...`
- `BREEZ_GREENLIGHT_INVITE_CODE`: `...`
- `BREEZ_GREENLIGHT_DEVICE_KEY`: `/path/to/breezsdk/device.pem` or Base64/Hex
- `BREEZ_GREENLIGHT_DEVICE_CERT`: `/path/to/breezsdk/device.crt` or Base64/Hex
## Breez Liquid SDK
This uses the [Breez SDK - Liquid](https://sdk-doc-liquid.breez.technology/) to manage Lightning payments via submarine swaps on the Liquid network. Provide a mnemonic seed phrase (for example, generate one with a Liquid wallet like [Blockstream Green](https://blockstream.com/green/)) and set it in the environment or admin UI.
**Required env vars**
- `LNBITS_BACKEND_WALLET_CLASS`: `BreezLiquidSdkWallet`
- `BREEZ_LIQUID_SEED`: `...`
Fees apply for each submarine swap. You may need to increase the reserve fee under **Settings → Funding** or via:
- `LNBITS_RESERVE_FEE_MIN`: `...`
- `LNBITS_RESERVE_FEE_PERCENT`: `...`
## Cliche Wallet
**Required env vars**
- `CLICHE_ENDPOINT`: `ws://127.0.0.1:12000`
## Nostr Wallet Connect (NWC)
To use NWC as a funding source you need a pairing URL (pairing secret) from an NWC provider. See providers here:
[https://github.com/getAlby/awesome-nwc?tab=readme-ov-file#nwc-wallets](https://github.com/getAlby/awesome-nwc?tab=readme-ov-file#nwc-wallets)
Configure in the admin UI or via env vars:
- `LNBITS_BACKEND_WALLET_CLASS`: `NWCWallet`
- `NWC_PAIRING_URL`: `nostr+walletconnect://...your...pairing...secret...`
<a id="strike"></a>
## Strike (alpha)
Custodial provider integrated via **Strike OAuth Connect** (OAuth 2.0 / OIDC). Authenticate a Strike user in your app, then call Strike APIs on the users behalf once scopes are granted. Requires a Strike business account, registered OAuth client, minimal scopes, and login/logout redirect URLs.
Get more info here [https://docs.strike.me/strike-oauth-connect/](https://docs.strike.me/strike-oauth-connect/)
**Integration endpoints**
- `STRIKE_API_ENDPOINT`: `https://api.strike.me/v1`
- `STRIKE_API_KEY`: `YOUR_STRIKE_API_KEY`
---
## Additional Guides
- **[Admin UI](./admin_ui.md)** — Manage server settings via a clean UI (avoid editing `.env` by hand).
- **[User Roles](./user_roles.md)** — Quick Overview of existing Roles in LNBits.
- **[Funding sources](./funding-sources-table.md)** — Whats available and how to enable/configure each.
## Powered by LNbits
LNbits empowers everyone with modular, open-source tools for building Bitcoin-based systems — fast, free, and extendable.
If you like this project, [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
[![LNbits Shop](https://demo.lnbits.com/static/images/bitcoin-shop-banner.png)](https://shop.lnbits.com/)
[![Visit LNbits Shop](https://img.shields.io/badge/Visit-LNbits%20Shop-7C3AED?logo=shopping-cart&logoColor=white&labelColor=5B21B6)](https://shop.lnbits.com/)
[![Try myLNbits SaaS](https://img.shields.io/badge/Try-myLNbits%20SaaS-2563EB?logo=lightning&logoColor=white&labelColor=1E40AF)](https://my.lnbits.com/login)
[![Read LNbits News](https://img.shields.io/badge/Read-LNbits%20News-F97316?logo=rss&logoColor=white&labelColor=C2410C)](https://news.lnbits.com/)
[![Explore LNbits Extensions](https://img.shields.io/badge/Explore-LNbits%20Extensions-10B981?logo=puzzle-piece&logoColor=white&labelColor=065F46)](https://extensions.lnbits.com/)
- `LNBITS_BACKEND_WALLET_CLASS`: **NWCWallet**
- `NWC_PAIRING_URL`: **nostr+walletconnect://...your...pairing...secret...**
+1 -2
View File
@@ -145,7 +145,6 @@
nixpkgs.overlays = [ self.overlays.${system}.default ];
};
checks =
import ./nix/tests { inherit pkgs; flake = self; };
checks = { };
});
}
+42 -64
View File
@@ -1,75 +1,53 @@
#!/usr/bin/env bash
set -euo pipefail
#!/bin/bash
# --- Config you might tweak ---
REPO_URL="https://github.com/lnbits/lnbits.git"
BRANCH="main"
APP_DIR="${PWD}/lnbits"
HOST="${HOST:-0.0.0.0}"
PORT="${PORT:-5000}"
ADMIN_UI="${LNBITS_ADMIN_UI:-true}"
# -------------------------------
# Check install has not already run
if [ ! -d lnbits/data ]; then
export DEBIAN_FRONTEND=noninteractive
# Update package list and install prerequisites non-interactively
sudo apt update -y
sudo apt install -y software-properties-common
# Ensure basic tooling
if ! command -v curl >/dev/null 2>&1 || ! command -v git >/dev/null 2>&1; then
sudo apt-get update -y
sudo apt-get install -y curl git
fi
# Add the deadsnakes PPA repository non-interactively
sudo add-apt-repository -y ppa:deadsnakes/ppa
# System build deps and secp headers
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -y
sudo apt-get install -y \
pkg-config \
build-essential \
libsecp256k1-dev \
automake \
autoconf \
libtool \
m4
fi
# Install Python 3.10 and distutils non-interactively
sudo apt install -y python3.10 python3.10-distutils
# Install uv (if missing)
if ! command -v uv >/dev/null 2>&1; then
# Install UV
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
fi
# Ensure PATH for current session
if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
export PATH="$HOME/.local/bin:$PATH"
export PATH="/home/$USER/.local/bin:$PATH"
if [ ! -d lnbits/wallets ]; then
# Clone the LNbits repository
git clone https://github.com/lnbits/lnbits.git
if [ $? -ne 0 ]; then
echo "Failed to clone the repository ... FAIL"
exit 1
fi
# Ensure we are in the lnbits directory
cd lnbits || { echo "Failed to cd into lnbits ... FAIL"; exit 1; }
fi
git checkout main
# Make data folder
mkdir data
# Copy the .env.example to .env
cp .env.example .env
elif [ ! -d lnbits/wallets ]; then
# cd into lnbits
cd lnbits || { echo "Failed to cd into lnbits ... FAIL"; exit 1; }
fi
# Clone or reuse repo
if [[ ! -d "$APP_DIR/.git" ]]; then
git clone "$REPO_URL" "$APP_DIR"
fi
# Install the dependencies using UV
uv sync --all-extras
cd "$APP_DIR"
git fetch --all --prune
git checkout "$BRANCH"
git pull --ff-only || true
# First-run setup
mkdir -p data
[[ -f .env ]] || cp .env.example .env || true
# Set environment variables for LNbits
export LNBITS_ADMIN_UI=true
export HOST=0.0.0.0
# Prefer system libsecp256k1 (avoid autotools path)
export SECP_BUNDLED=0
# Sync dependencies with Python 3.12
uv sync --python 3.12 --all-extras --no-dev
# Environment
export LNBITS_ADMIN_UI="$ADMIN_UI"
export HOST="$HOST"
export PORT="$PORT"
# Open firewall (optional)
if command -v ufw >/dev/null 2>&1; then
sudo ufw allow "$PORT"/tcp || true
fi
# Run LNbits with Python 3.12 via uv
exec uv run --python 3.12 lnbits
# Run LNbits
uv run lnbits
-2
View File
@@ -1,6 +1,5 @@
from .core.services import create_invoice, pay_invoice
from .decorators import (
check_account_exists,
check_admin,
check_super_user,
check_user_exists,
@@ -12,7 +11,6 @@ from .exceptions import InvoiceError, PaymentError
__all__ = [
"InvoiceError",
"PaymentError",
"check_account_exists",
"check_admin",
"check_super_user",
"check_user_exists",
+1 -10
View File
@@ -65,7 +65,6 @@ from .middleware import (
InstalledExtensionMiddleware,
add_first_install_middleware,
add_ip_block_middleware,
add_profiler_middleware,
add_ratelimit_middleware,
)
from .tasks import internal_invoice_listener, invoice_listener, run_interval
@@ -197,9 +196,6 @@ def create_app() -> FastAPI:
register_exception_handlers(app)
if settings.profiler:
add_profiler_middleware(app)
return app
@@ -472,12 +468,7 @@ def register_async_tasks() -> None:
create_permanent_task(wait_for_audit_data)
create_permanent_task(wait_notification_messages)
create_permanent_task(
run_interval(
settings.lnbits_funding_source_pending_interval_seconds,
check_pending_payments,
)
)
create_permanent_task(run_interval(30 * 60, check_pending_payments))
create_permanent_task(invoice_listener)
create_permanent_task(internal_invoice_listener)
create_permanent_task(cache.invalidate_forever)
-2
View File
@@ -3,7 +3,6 @@ from fastapi import APIRouter, FastAPI
from .db import core_app_extra, db
from .views.admin_api import admin_router
from .views.api import api_router
from .views.asset_api import asset_router
from .views.audit_api import audit_router
from .views.auth_api import auth_router
from .views.callback_api import callback_router
@@ -45,7 +44,6 @@ def init_core_routers(app: FastAPI):
app.include_router(webpush_router)
app.include_router(users_router)
app.include_router(audit_router)
app.include_router(asset_router)
app.include_router(fiat_router)
app.include_router(lnurl_router)
-4
View File
@@ -12,7 +12,6 @@ from .extensions import (
drop_extension_db,
get_installed_extension,
get_installed_extensions,
get_installed_extensions_count,
get_user_active_extensions_ids,
get_user_extension,
get_user_extensions,
@@ -59,7 +58,6 @@ from .users import (
get_account_by_username,
get_account_by_username_or_email,
get_accounts,
get_accounts_count,
get_user,
get_user_access_control_lists,
get_user_from_account,
@@ -119,13 +117,11 @@ __all__ = [
"get_account_by_username",
"get_account_by_username_or_email",
"get_accounts",
"get_accounts_count",
"get_admin_settings",
"get_db_version",
"get_db_versions",
"get_installed_extension",
"get_installed_extensions",
"get_installed_extensions_count",
"get_latest_payments_by_extension",
"get_payment",
"get_payments",
-108
View File
@@ -1,108 +0,0 @@
from lnbits.core.db import db
from lnbits.core.models.assets import Asset, AssetFilters, AssetInfo
from lnbits.db import Connection, Filters, Page
async def create_asset(
entry: Asset,
conn: Connection | None = None,
) -> None:
await (conn or db).insert("assets", entry)
async def get_user_asset_info(
user_id: str,
asset_id: str,
conn: Connection | None = None,
) -> AssetInfo | None:
return await (conn or db).fetchone(
query="SELECT * from assets WHERE id = :asset_id AND user_id = :user_id",
values={"asset_id": asset_id, "user_id": user_id},
model=AssetInfo,
)
async def get_asset_info(
asset_id: str, conn: Connection | None = None
) -> AssetInfo | None:
return await (conn or db).fetchone(
query="SELECT * from assets WHERE id = :asset_id",
values={"asset_id": asset_id},
model=AssetInfo,
)
async def get_user_asset(
user_id: str,
asset_id: str,
conn: Connection | None = None,
) -> Asset | None:
return await (conn or db).fetchone(
query="SELECT * from assets WHERE id = :asset_id AND user_id = :user_id",
values={"asset_id": asset_id, "user_id": user_id},
model=Asset,
)
async def get_public_asset(
asset_id: str,
conn: Connection | None = None,
) -> Asset | None:
return await (conn or db).fetchone(
query="SELECT * from assets WHERE id = :asset_id AND is_public = true",
values={"asset_id": asset_id},
model=Asset,
)
async def get_public_asset_info(
asset_id: str,
conn: Connection | None = None,
) -> AssetInfo | None:
return await (conn or db).fetchone(
query="SELECT * from assets WHERE id = :asset_id AND is_public = true",
values={"asset_id": asset_id},
model=AssetInfo,
)
async def update_user_asset_info(
asset: AssetInfo,
) -> AssetInfo:
await db.update("assets", asset)
return asset
async def delete_user_asset(
user_id: str, asset_id: str, conn: Connection | None = None
) -> None:
await (conn or db).execute(
query="DELETE FROM assets WHERE id = :asset_id AND user_id = :user_id",
values={"asset_id": asset_id, "user_id": user_id},
)
async def get_user_assets(
user_id: str,
filters: Filters[AssetFilters] | None = None,
conn: Connection | None = None,
) -> Page[AssetInfo]:
filters = filters or Filters()
filters.sortby = filters.sortby or "created_at"
return await (conn or db).fetch_page(
query="SELECT * FROM assets",
where=["user_id = :user_id"],
values={"user_id": user_id},
filters=filters,
model=AssetInfo,
table_name="assets",
)
async def get_user_assets_count(user_id: str) -> int:
result = await db.execute(
query="SELECT COUNT(*) as count FROM assets WHERE user_id = :user_id",
values={"user_id": user_id},
)
row = result.mappings().first()
return row.get("count", 0)
+1 -2
View File
@@ -16,12 +16,11 @@ async def get_audit_entries(
conn: Connection | None = None,
) -> Page[AuditEntry]:
return await (conn or db).fetch_page(
"SELECT * FROM audit",
"SELECT * from audit",
[],
{},
filters=filters,
model=AuditEntry,
table_name="audit",
)
-7
View File
@@ -90,13 +90,6 @@ async def get_installed_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(
user_id: str, extension: str, conn: Connection | None = None
) -> UserExtension | None:
+22 -41
View File
@@ -1,4 +1,3 @@
from datetime import datetime, timezone
from time import time
from typing import Any
@@ -34,12 +33,13 @@ async def get_payment(checking_id: str, conn: Connection | None = None) -> Payme
async def get_standalone_payment(
checking_id_or_hash: str,
conn: Connection | None = None,
incoming: bool | None = False,
wallet_id: str | None = None,
conn: Connection | None = None,
) -> Payment | None:
clause: str = "checking_id = :checking_id OR payment_hash = :hash"
values = {
"wallet_id": wallet_id,
"checking_id": checking_id_or_hash,
"hash": checking_id_or_hash,
}
@@ -47,10 +47,6 @@ async def get_standalone_payment(
clause = f"({clause}) AND amount > 0"
if wallet_id:
wallet = await get_wallet(wallet_id, conn=conn)
if not wallet or not wallet.can_view_payments:
return None
values["wallet_id"] = wallet.source_wallet_id
clause = f"({clause}) AND wallet_id = :wallet_id"
row = await (conn or db).fetchone(
@@ -70,16 +66,13 @@ async def get_standalone_payment(
async def get_wallet_payment(
wallet_id: str, payment_hash: str, conn: Connection | None = None
) -> Payment | None:
wallet = await get_wallet(wallet_id, conn=conn)
if not wallet or not wallet.can_view_payments:
return None
payment = await (conn or db).fetchone(
"""
SELECT *
FROM apipayments
WHERE wallet_id = :wallet AND payment_hash = :hash
""",
{"wallet": wallet.source_wallet_id, "hash": payment_hash},
{"wallet": wallet_id, "hash": payment_hash},
Payment,
)
return payment
@@ -125,6 +118,7 @@ async def get_payments_paginated( # noqa: C901
Filters payments to be returned by:
- complete | pending | failed | outgoing | incoming.
"""
values: dict[str, Any] = {
"time": since,
}
@@ -134,11 +128,7 @@ async def get_payments_paginated( # noqa: C901
clause.append(f"time > {db.timestamp_placeholder('time')}")
if wallet_id:
wallet = await get_wallet(wallet_id, conn=conn)
if not wallet or not wallet.can_view_payments:
return Page(data=[], total=0)
values["wallet_id"] = wallet.source_wallet_id
values["wallet_id"] = wallet_id
clause.append("wallet_id = :wallet_id")
elif user_id:
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
@@ -149,12 +139,14 @@ async def get_payments_paginated( # noqa: C901
f"(status = '{PaymentState.SUCCESS}' OR status = '{PaymentState.PENDING}')"
)
elif complete:
clause.append(f"""
clause.append(
f"""
(
status = '{PaymentState.SUCCESS}'
OR (amount < 0 AND status = '{PaymentState.PENDING}')
)
""")
"""
)
elif pending:
clause.append(f"status = '{PaymentState.PENDING}'")
elif failed:
@@ -179,7 +171,6 @@ async def get_payments_paginated( # noqa: C901
values,
filters=filters,
model=Payment,
table_name="apipayments",
)
@@ -291,8 +282,6 @@ async def create_payment(
fee=-abs(data.fee),
tag=extra.get("tag", None),
extra=extra,
labels=data.labels or [],
external_id=data.external_id,
)
await (conn or db).insert("apipayments", payment)
@@ -304,16 +293,8 @@ async def update_payment_checking_id(
checking_id: str, new_checking_id: str, conn: Connection | None = None
) -> None:
await (conn or db).execute(
f"""
UPDATE apipayments
SET checking_id = :new_id, updated_at = {db.timestamp_placeholder('now')}
WHERE checking_id = :old_id
""", # noqa: S608
{
"new_id": new_checking_id,
"old_id": checking_id,
"now": int(time()),
},
"UPDATE apipayments SET checking_id = :new_id WHERE checking_id = :old_id",
{"new_id": new_checking_id, "old_id": checking_id},
)
@@ -322,7 +303,6 @@ async def update_payment(
new_checking_id: str | None = None,
conn: Connection | None = None,
) -> None:
payment.updated_at = datetime.now(timezone.utc)
await (conn or db).update(
"apipayments", payment, "WHERE checking_id = :checking_id"
)
@@ -334,23 +314,24 @@ async def get_payments_history(
wallet_id: str | None = None,
group: DateTrunc = "day",
filters: Filters | None = None,
conn: Connection | None = None,
) -> list[PaymentHistoryPoint]:
if not filters:
filters = Filters()
date_trunc = db.datetime_grouping(group)
values: dict[str, Any] = {
values = {
"wallet_id": wallet_id,
}
# count outgoing payments if they are still pending
where = [f"""
where = [
f"""
wallet_id = :wallet_id AND (
status = '{PaymentState.SUCCESS}'
OR (amount < 0 AND status = '{PaymentState.PENDING}')
)
"""]
"""
]
clause = filters.where(where)
transactions: list[dict] = await db.fetchall(
# This query is safe from SQL injection:
@@ -368,13 +349,13 @@ async def get_payments_history(
filters.values(values),
)
if wallet_id:
wallet = await get_wallet(wallet_id, conn=conn)
if not wallet or not wallet.can_view_payments:
return []
balance = wallet.balance_msat
values["wallet_id"] = wallet.source_wallet_id
wallet = await get_wallet(wallet_id)
if wallet:
balance = wallet.balance_msat
else:
raise ValueError("Unknown wallet")
else:
balance = await get_total_balance(conn=conn)
balance = await get_total_balance()
# since we dont know the balance at the starting point,
# we take the current balance and walk backwards
+1 -2
View File
@@ -105,8 +105,7 @@ async def get_settings_field(
)
if not row:
return None
value = json.loads(row["value"]) if row["value"] else None
return SettingsField(id=row["id"], value=value, tag=row["tag"])
return SettingsField(id=row["id"], value=json.loads(row["value"]), tag=row["tag"])
async def set_settings_field(id_: str, value: Any | None, tag: str | None = "core"):
+40 -122
View File
@@ -4,17 +4,10 @@ from typing import Any
from uuid import uuid4
from lnbits.core.crud.extensions import get_user_active_extensions_ids
from lnbits.core.crud.wallets import (
clear_wallet_cache,
create_wallet,
get_standalone_wallet,
get_wallets,
)
from lnbits.core.crud.wallets import get_wallets
from lnbits.core.db import db
from lnbits.core.models import UserAcls
from lnbits.db import Connection, Filters, Page
from lnbits.helpers import sha256s
from lnbits.utils.cache import cache
from ..models import (
Account,
@@ -37,16 +30,9 @@ async def create_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) -> Account:
account.updated_at = datetime.now(timezone.utc)
await (conn or db).update("accounts", account)
await db.update("accounts", account)
return account
@@ -55,7 +41,6 @@ async def delete_account(user_id: str, conn: Connection | None = None) -> None:
"DELETE from accounts WHERE id = :user",
{"user": user_id},
)
await clear_user_id_cache(user_id)
async def get_accounts(
@@ -64,22 +49,17 @@ async def get_accounts(
) -> Page[AccountOverview]:
where_clauses = []
values: dict[str, Any] = {}
filters = filters or Filters()
wallet_filter = filters.get_filter_by_field("wallet_id")
if wallet_filter and wallet_filter.values:
wallet_id_value = next(iter(wallet_filter.values.values()), None)
wallet = (
await get_standalone_wallet(wallet_id_value, deleted=None, conn=conn)
if wallet_id_value
else None
)
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")
# Make wallet filter explicit
wallet_filter = (
next((f for f in filters.filters if f.field == "wallet_id"), None)
if filters
else None
)
if filters and wallet_filter and wallet_filter.values:
where_clauses.append("wallets.id = :wallet_id")
values = {**values, "wallet_id": next(iter(wallet_filter.values.values()))}
filters.filters = [f for f in filters.filters if f.field != "wallet_id"]
return await (conn or db).fetch_page(
"""
@@ -89,7 +69,6 @@ async def get_accounts(
accounts.email,
accounts.pubkey,
accounts.external_id,
accounts.activated,
SUM(COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0)) as balance_msat,
@@ -110,22 +89,15 @@ async def get_accounts(
filters=filters,
model=AccountOverview,
group_by=["accounts.id"],
table_name="accounts",
)
async def get_account(
user_id: str, active_only: bool = True, conn: Connection | None = None
) -> Account | None:
async def get_account(user_id: str, conn: Connection | None = None) -> Account | None:
if len(user_id) == 0:
return None
return await (conn or db).fetchone(
"""
SELECT * FROM accounts
WHERE id = :id AND (activated = true OR activated = :activated)
""",
{"id": user_id, "activated": active_only},
"SELECT * FROM accounts WHERE id = :id",
{"id": user_id},
Account,
)
@@ -151,98 +123,66 @@ async def delete_accounts_no_wallets(
async def get_account_by_username(
username: str, active_only: bool = True, conn: Connection | None = None
username: str, conn: Connection | None = None
) -> Account | None:
if len(username) == 0:
return None
return await (conn or db).fetchone(
"""
SELECT * FROM accounts
WHERE
LOWER(username) = :username
AND (activated = true OR activated = :activated)
""",
{"username": username.lower(), "activated": active_only},
"SELECT * FROM accounts WHERE LOWER(username) = :username",
{"username": username.lower()},
Account,
)
async def get_account_by_pubkey(
pubkey: str, active_only: bool = True, conn: Connection | None = None
pubkey: str, conn: Connection | None = None
) -> Account | None:
return await (conn or db).fetchone(
"""
SELECT * FROM accounts
WHERE
LOWER(pubkey) = :pubkey
AND (activated = true OR activated = :activated)
""",
{"pubkey": pubkey.lower(), "activated": active_only},
"SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
{"pubkey": pubkey.lower()},
Account,
)
async def get_account_by_email(
email: str, active_only: bool = True, conn: Connection | None = None
email: str, conn: Connection | None = None
) -> Account | None:
if len(email) == 0:
return None
return await (conn or db).fetchone(
"""
SELECT * FROM accounts
WHERE
LOWER(email) = :email
AND (activated = true OR activated = :activated)
""",
{"email": email.lower(), "activated": active_only},
"SELECT * FROM accounts WHERE LOWER(email) = :email",
{"email": email.lower()},
Account,
)
async def get_account_by_username_or_email(
username_or_email: str,
active_only: bool = True,
conn: Connection | None = None,
username_or_email: str, conn: Connection | None = None
) -> Account | None:
return await (conn or db).fetchone(
"""
SELECT * FROM accounts
WHERE
(LOWER(email) = :value or LOWER(username) = :value)
AND (activated = true OR activated = :activated)
WHERE LOWER(email) = :value or LOWER(username) = :value
""",
{"value": username_or_email.lower(), "activated": active_only},
{"value": username_or_email.lower()},
Account,
)
async def get_user(
user_id: str, active_only: bool = True, conn: Connection | None = None
) -> User | None:
async with db.reuse_conn(conn) if conn else db.connect() as conn:
account = await get_account(user_id, active_only, conn=conn)
if not account:
return None
return await get_user_from_account(account, conn=conn)
async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
account = await get_account(user_id, conn)
if not account:
return None
return await get_user_from_account(account, conn)
async def get_user_from_account(
account: Account, conn: Connection | None = None
) -> User | None:
async with db.reuse_conn(conn) if conn else db.connect() as conn:
extensions = await get_user_active_extensions_ids(account.id, conn=conn)
wallets = await get_wallets(account.id, deleted=False, conn=conn)
if len(wallets) == 0:
wallet = await create_wallet(user_id=account.id, conn=conn)
wallets.append(wallet)
extensions = await get_user_active_extensions_ids(account.id, conn)
wallets = await get_wallets(account.id, False, conn=conn)
return User(
id=account.id,
activated=account.activated,
email=account.email,
username=account.username,
pubkey=account.pubkey,
@@ -256,43 +196,21 @@ async def get_user_from_account(
super_user=account.is_super_user,
fiat_providers=account.fiat_providers,
has_password=account.password_hash is not None,
ui_customization=account.ui_customization or {},
)
async def update_user_access_control_list(
user_acls: UserAcls, conn: Connection | None = None
):
async def update_user_access_control_list(user_acls: UserAcls):
user_acls.updated_at = datetime.now(timezone.utc)
await (conn or db).update("accounts", user_acls)
await db.update("accounts", user_acls)
async def get_user_access_control_lists(
user_id: str, active_only: bool = True, conn: Connection | None = None
user_id: str, conn: Connection | None = None
) -> UserAcls:
user_acls = await (conn or db).fetchone(
"""
SELECT id, access_control_list FROM accounts
WHERE id = :user_id AND (activated = true OR activated = :activated)
""",
{"user_id": user_id, "activated": active_only},
"SELECT id, access_control_list FROM accounts WHERE id = :id",
{"id": user_id},
UserAcls,
)
return user_acls or UserAcls(id=user_id)
async def clear_user_id_cache(user_id: str):
user = await get_user(user_id, active_only=True)
if user:
clear_user_cache(user)
def clear_user_cache(user: User):
user_cache_key: str | None = cache.pop(
f"auth:user:cache_key:{sha256s(user.id)}", None
)
if user_cache_key:
cache.pop(user_cache_key)
for wallet in user.wallets:
clear_wallet_cache(wallet)
+16 -125
View File
@@ -3,10 +3,9 @@ from time import time
from uuid import uuid4
from lnbits.core.db import db
from lnbits.core.models.wallets import BaseWallet, WalletsFilters, WalletType
from lnbits.core.models.wallets import WalletsFilters
from lnbits.db import Connection, Filters, Page
from lnbits.settings import settings
from lnbits.utils.cache import cache
from ..models import Wallet
@@ -15,22 +14,17 @@ async def create_wallet(
*,
user_id: str,
wallet_name: str | None = None,
wallet_type: WalletType = WalletType.LIGHTNING,
shared_wallet_id: str | None = None,
conn: Connection | None = None,
) -> Wallet:
wallet_id = uuid4().hex
wallet = Wallet(
id=wallet_id,
name=wallet_name or settings.lnbits_default_wallet_name,
wallet_type=wallet_type.value,
shared_wallet_id=shared_wallet_id,
user=user_id,
adminkey=uuid4().hex,
inkey=uuid4().hex,
currency=settings.lnbits_default_accounting_currency or "USD",
)
await (conn or db).insert("wallets", wallet)
return wallet
@@ -45,14 +39,13 @@ async def update_wallet(
async def delete_wallet(
*,
user_id: str,
wallet_id: str,
deleted: bool = True,
conn: Connection | None = None,
) -> None:
clear_wallet_id_cache(wallet_id)
now = int(time())
await (conn or db).execute(
# Timestamp placeholder is safe from SQL injection (not user input)
f"""
@@ -65,7 +58,6 @@ async def delete_wallet(
async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) -> None:
clear_wallet_id_cache(wallet_id)
await (conn or db).execute(
"DELETE FROM wallets WHERE id = :wallet",
{"wallet": wallet_id},
@@ -75,7 +67,6 @@ async def force_delete_wallet(wallet_id: str, conn: Connection | None = None) ->
async def delete_wallet_by_id(
wallet_id: str, conn: Connection | None = None
) -> int | None:
clear_wallet_id_cache(wallet_id)
now = int(time())
result = await (conn or db).execute(
# Timestamp placeholder is safe from SQL injection (not user input)
@@ -112,8 +103,8 @@ async def delete_unused_wallets(
)
async def get_standalone_wallet(
wallet_id: str, deleted: bool | None = False, conn: Connection | None = None
async def get_wallet(
wallet_id: str, deleted: bool | None = None, conn: Connection | None = None
) -> Wallet | None:
query = """
SELECT *, COALESCE((
@@ -130,23 +121,8 @@ async def get_standalone_wallet(
)
async def get_wallet(
wallet_id: str, deleted: bool | None = False, conn: Connection | None = None
) -> Wallet | None:
wallet = await get_standalone_wallet(wallet_id, deleted, conn)
if not wallet:
return None
if wallet.is_lightning_shared_wallet:
return await get_source_wallet(wallet, conn)
return wallet
async def get_wallets(
user_id: str,
deleted: bool | None = False,
wallet_type: WalletType | None = None,
conn: Connection | None = None,
user_id: str, deleted: bool | None = None, conn: Connection | None = None
) -> list[Wallet]:
query = """
SELECT *, COALESCE((
@@ -156,20 +132,12 @@ async def get_wallets(
"""
if deleted is not None:
query += " AND deleted = :deleted "
if wallet_type is not None:
query += " AND wallet_type = :wallet_type "
wallets = await (conn or db).fetchall(
return await (conn or db).fetchall(
query,
{
"user": user_id,
"deleted": deleted,
"wallet_type": wallet_type.value if wallet_type else None,
},
{"user": user_id, "deleted": deleted},
Wallet,
)
return await get_source_wallets(wallets, conn)
async def get_wallets_paginated(
user_id: str,
@@ -181,7 +149,7 @@ async def get_wallets_paginated(
deleted = False
where: list[str] = [""" "user" = :user AND deleted = :deleted """]
wallets = await (conn or db).fetch_page(
return await (conn or db).fetch_page(
"""
SELECT *, COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
@@ -191,27 +159,20 @@ async def get_wallets_paginated(
values={"user": user_id, "deleted": deleted},
filters=filters,
model=Wallet,
table_name="wallets",
)
wallets.data = await get_source_wallets(wallets.data, conn)
return wallets
async def get_wallets_ids(
user_id: str, deleted: bool | None = False, conn: Connection | None = None
user_id: str, deleted: bool | None = None, conn: Connection | None = None
) -> list[str]:
query = """SELECT * FROM wallets WHERE "user" = :user"""
query = """SELECT id FROM wallets WHERE "user" = :user"""
if deleted is not None:
query += " AND deleted = :deleted "
wallets = await (conn or db).fetchall(
query += "AND deleted = :deleted"
result: list[dict] = await (conn or db).fetchall(
query,
{"user": user_id, "deleted": deleted},
Wallet,
)
wallets = await get_source_wallets(wallets, conn)
return [w.source_wallet_id for w in wallets if w.can_view_payments]
return [row["id"] for row in result]
async def get_wallets_count():
@@ -224,90 +185,20 @@ async def get_wallet_for_key(
key: str,
conn: Connection | None = None,
) -> Wallet | None:
wallet = await (conn or db).fetchone(
return await (conn or db).fetchone(
"""
SELECT wallets.*, COALESCE((
SELECT *, COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0)
AS balance_msat FROM wallets
INNER JOIN accounts ON wallets.user = accounts.id
WHERE (adminkey = :key OR inkey = :key)
AND deleted = false
AND accounts.activated = true
WHERE (adminkey = :key OR inkey = :key) AND deleted = false
""",
{"key": key},
Wallet,
)
if not wallet:
return None
if wallet.is_lightning_shared_wallet:
mw = await get_source_wallet(wallet, conn)
return mw
return wallet
async def get_base_wallet_for_key(
key: str,
conn: Connection | None = None,
) -> BaseWallet | None:
wallet = await (conn or db).fetchone(
"""
SELECT wallets.id, "user", wallet_type, adminkey, inkey FROM wallets
INNER JOIN accounts ON wallets.user = accounts.id
WHERE (adminkey = :key OR inkey = :key)
AND deleted = false
AND accounts.activated = true
""",
{"key": key},
BaseWallet,
)
if not wallet:
return None
return wallet
async def get_source_wallet(
wallet: Wallet, conn: Connection | None = None
) -> Wallet | None:
if not wallet.is_lightning_shared_wallet:
return wallet
if not wallet.shared_wallet_id:
return None
shared_wallet = await get_standalone_wallet(wallet.shared_wallet_id, False, conn)
if not shared_wallet:
return None
wallet.mirror_shared_wallet(shared_wallet)
return wallet
async def get_source_wallets(
wallet: list[Wallet], conn: Connection | None = None
) -> list[Wallet]:
source_wallets = []
for w in wallet:
source_wallet = await get_source_wallet(w, conn)
if source_wallet:
source_wallets.append(source_wallet)
return source_wallets
async def get_total_balance(conn: Connection | None = None):
result = await (conn or db).execute("SELECT SUM(balance) as balance FROM balances")
row = result.mappings().first()
return row.get("balance", 0) or 0
def clear_wallet_id_cache(wallet_id: str):
cached_wallet: BaseWallet | None = cache.pop(f"auth:wallet:{wallet_id}")
if cached_wallet:
cache.pop(f"auth:x-api-key:{cached_wallet.adminkey}")
cache.pop(f"auth:x-api-key:{cached_wallet.inkey}")
def clear_wallet_cache(wallet: Wallet):
cache.pop(f"auth:wallet:{wallet.id}")
cache.pop(f"auth:x-api-key:{wallet.adminkey}")
cache.pop(f"auth:x-api-key:{wallet.inkey}")
+96 -168
View File
@@ -10,26 +10,31 @@ from lnbits.db import Connection
async def m000_create_migrations_table(db: Connection):
await db.execute("""
await db.execute(
"""
CREATE TABLE IF NOT EXISTS dbversions (
db TEXT PRIMARY KEY,
version INT NOT NULL
)
""")
"""
)
async def m001_initial(db: Connection):
"""
Initial LNbits tables.
"""
await db.execute("""
await db.execute(
"""
CREATE TABLE IF NOT EXISTS accounts (
id TEXT PRIMARY KEY,
email TEXT,
pass TEXT
);
""")
await db.execute("""
"""
)
await db.execute(
"""
CREATE TABLE IF NOT EXISTS extensions (
"user" TEXT NOT NULL,
extension TEXT NOT NULL,
@@ -37,8 +42,10 @@ async def m001_initial(db: Connection):
UNIQUE ("user", extension)
);
""")
await db.execute("""
"""
)
await db.execute(
"""
CREATE TABLE IF NOT EXISTS wallets (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
@@ -46,8 +53,10 @@ async def m001_initial(db: Connection):
adminkey TEXT NOT NULL,
inkey TEXT
);
""")
await db.execute(f"""
"""
)
await db.execute(
f"""
CREATE TABLE IF NOT EXISTS apipayments (
payhash TEXT NOT NULL,
amount {db.big_int} NOT NULL,
@@ -58,9 +67,11 @@ async def m001_initial(db: Connection):
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
UNIQUE (wallet, payhash)
);
""")
"""
)
await db.execute("""
await db.execute(
"""
CREATE VIEW balances AS
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
SELECT wallet, SUM(amount) AS s -- incoming
@@ -74,7 +85,8 @@ async def m001_initial(db: Connection):
GROUP BY wallet
)x
GROUP BY wallet;
""")
"""
)
async def m002_add_fields_to_apipayments(db: Connection):
@@ -137,7 +149,8 @@ async def m004_ensure_fees_are_always_negative(db: Connection):
"""
await db.execute("DROP VIEW balances")
await db.execute("""
await db.execute(
"""
CREATE VIEW balances AS
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
SELECT wallet, SUM(amount) AS s -- incoming
@@ -151,7 +164,8 @@ async def m004_ensure_fees_are_always_negative(db: Connection):
GROUP BY wallet
)x
GROUP BY wallet;
""")
"""
)
async def m005_balance_check_balance_notify(db: Connection):
@@ -160,7 +174,8 @@ async def m005_balance_check_balance_notify(db: Connection):
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 (
wallet TEXT NOT NULL REFERENCES wallets (id),
service TEXT NOT NULL,
@@ -168,16 +183,19 @@ async def m005_balance_check_balance_notify(db: Connection):
UNIQUE(wallet, service)
);
""")
"""
)
await db.execute("""
await db.execute(
"""
CREATE TABLE IF NOT EXISTS balance_notify (
wallet TEXT NOT NULL REFERENCES wallets (id),
url TEXT NOT NULL,
UNIQUE(wallet, url)
);
""")
"""
)
async def m006_add_invoice_expiry_to_apipayments(db: Connection):
@@ -244,16 +262,19 @@ async def m007_set_invoice_expiries(db: Connection):
async def m008_create_admin_settings_table(db: Connection):
await db.execute("""
await db.execute(
"""
CREATE TABLE IF NOT EXISTS settings (
super_user TEXT,
editable_settings TEXT NOT NULL DEFAULT '{}'
);
""")
"""
)
async def m009_create_tinyurl_table(db: Connection):
await db.execute(f"""
await db.execute(
f"""
CREATE TABLE IF NOT EXISTS tiny_url (
id TEXT PRIMARY KEY,
url TEXT,
@@ -261,11 +282,13 @@ async def m009_create_tinyurl_table(db: Connection):
wallet TEXT,
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
""")
"""
)
async def m010_create_installed_extensions_table(db: Connection):
await db.execute("""
await db.execute(
"""
CREATE TABLE IF NOT EXISTS installed_extensions (
id TEXT PRIMARY KEY,
version TEXT NOT NULL,
@@ -276,7 +299,8 @@ async def m010_create_installed_extensions_table(db: Connection):
active BOOLEAN DEFAULT false,
meta TEXT NOT NULL DEFAULT '{}'
);
""")
"""
)
async def m011_optimize_balances_view(db: Connection):
@@ -285,19 +309,23 @@ async def m011_optimize_balances_view(db: Connection):
over the payments table instead of 2.
"""
await db.execute("DROP VIEW balances")
await db.execute("""
await db.execute(
"""
CREATE VIEW balances AS
SELECT wallet, SUM(amount - abs(fee)) AS balance
FROM apipayments
WHERE (pending = false AND amount > 0) OR amount < 0
GROUP BY wallet
""")
"""
)
async def m012_add_currency_to_wallet(db: Connection):
await db.execute("""
await db.execute(
"""
ALTER TABLE wallets ADD COLUMN currency TEXT
""")
"""
)
async def m013_add_deleted_to_wallets(db: Connection):
@@ -317,13 +345,15 @@ async def m014_set_deleted_wallets(db: Connection):
Sets deleted column to wallets.
"""
try:
result = await db.execute("""
result = await db.execute(
"""
SELECT *
FROM wallets
WHERE user LIKE 'del:%'
AND adminkey LIKE 'del:%'
AND inkey LIKE 'del:%'
""")
"""
)
rows = result.mappings().all()
for row in rows:
@@ -356,7 +386,8 @@ async def m014_set_deleted_wallets(db: Connection):
async def m015_create_push_notification_subscriptions_table(db: Connection):
await db.execute(f"""
await db.execute(
f"""
CREATE TABLE IF NOT EXISTS webpush_subscriptions (
endpoint TEXT NOT NULL,
"user" TEXT NOT NULL,
@@ -365,7 +396,8 @@ async def m015_create_push_notification_subscriptions_table(db: Connection):
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
PRIMARY KEY (endpoint, "user")
);
""")
"""
)
async def m016_add_username_column_to_accounts(db: Connection):
@@ -452,7 +484,8 @@ async def m018_balances_view_exclude_deleted(db: Connection):
Make deleted wallets not show up in the balances view.
"""
await db.execute("DROP VIEW balances")
await db.execute("""
await db.execute(
"""
CREATE VIEW balances AS
SELECT apipayments.wallet,
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
@@ -462,7 +495,8 @@ async def m018_balances_view_exclude_deleted(db: Connection):
AND ((apipayments.pending = false AND apipayments.amount > 0)
OR apipayments.amount < 0)
GROUP BY wallet
""")
"""
)
async def m019_balances_view_based_on_wallets(db: Connection):
@@ -471,7 +505,8 @@ async def m019_balances_view_based_on_wallets(db: Connection):
Important for querying whole lnbits balances.
"""
await db.execute("DROP VIEW balances")
await db.execute("""
await db.execute(
"""
CREATE VIEW balances AS
SELECT apipayments.wallet,
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
@@ -481,7 +516,8 @@ async def m019_balances_view_based_on_wallets(db: Connection):
AND ((apipayments.pending = false AND apipayments.amount > 0)
OR apipayments.amount < 0)
GROUP BY apipayments.wallet
""")
"""
)
async def m020_add_column_column_to_user_extensions(db: Connection):
@@ -500,7 +536,8 @@ async def m021_add_success_failed_to_apipayments(db: Connection):
await db.execute("UPDATE apipayments SET status = 'success' WHERE NOT pending")
await db.execute("DROP VIEW balances")
await db.execute("""
await db.execute(
"""
CREATE VIEW balances AS
SELECT apipayments.wallet,
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
@@ -512,7 +549,8 @@ async def m021_add_success_failed_to_apipayments(db: Connection):
OR (apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)
)
GROUP BY apipayments.wallet
""")
"""
)
async def m022_add_pubkey_to_accounts(db: Connection):
@@ -543,7 +581,8 @@ async def m024_drop_pending(db: Connection):
async def m025_refresh_view(db: Connection):
await db.execute("DROP VIEW balances")
await db.execute("""
await db.execute(
"""
CREATE VIEW balances AS
SELECT apipayments.wallet_id,
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
@@ -555,7 +594,8 @@ async def m025_refresh_view(db: Connection):
OR (apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)
)
GROUP BY apipayments.wallet_id
""")
"""
)
async def m026_update_payment_table(db: Connection):
@@ -618,7 +658,8 @@ async def m027_update_apipayments_data(db: Connection):
async def m028_update_settings(db: Connection):
await db.execute("""
await db.execute(
"""
CREATE TABLE IF NOT EXISTS system_settings (
id TEXT PRIMARY KEY,
value TEXT,
@@ -626,7 +667,8 @@ async def m028_update_settings(db: Connection):
UNIQUE (id, tag)
);
""")
"""
)
async def _insert_key_value(id_: str, value: Any):
await db.execute(
@@ -649,7 +691,8 @@ async def m028_update_settings(db: Connection):
async def m029_create_audit_table(db: Connection):
await db.execute(f"""
await db.execute(
f"""
CREATE TABLE IF NOT EXISTS audit (
component TEXT,
ip_address TEXT,
@@ -663,13 +706,16 @@ async def m029_create_audit_table(db: Connection):
delete_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
""")
"""
)
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
""")
"""
)
async def m031_add_color_and_icon_to_wallets(db: Connection):
@@ -692,126 +738,8 @@ async def m033_update_payment_table(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
""")
async def m035_add_wallet_type_column(db: Connection):
await db.execute("""
ALTER TABLE wallets ADD COLUMN wallet_type TEXT DEFAULT 'lightning'
""")
async def m036_add_shared_wallet_column(db: Connection):
await db.execute("""
ALTER TABLE wallets ADD COLUMN shared_wallet_id TEXT
""")
async def m037_create_assets_table(db: Connection):
await db.execute(f"""
CREATE TABLE IF NOT EXISTS assets (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
mime_type TEXT NOT NULL,
is_public BOOLEAN NOT NULL DEFAULT false,
name TEXT NOT NULL,
size_bytes INT NOT NULL,
thumbnail_base64 TEXT,
thumbnail {db.blob},
data {db.blob} NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
""")
async def m038_add_labels_for_payments(db: Connection):
await db.execute("""
ALTER TABLE apipayments ADD COLUMN labels TEXT
""")
async def m039_index_payments(db: Connection):
indexes = [
"wallet_id",
"checking_id",
"payment_hash",
"amount",
"fee",
"labels",
"time",
"status",
"memo",
"created_at",
"updated_at",
]
for index in indexes:
logger.debug(f"Creating index idx_payments_{index}...")
await db.execute(f"""
CREATE INDEX IF NOT EXISTS idx_payments_{index} ON apipayments ({index});
""")
async def m040_index_wallets(db: Connection):
indexes = [
"id",
"user",
"deleted",
"adminkey",
"inkey",
"wallet_type",
"created_at",
"updated_at",
]
for index in indexes:
logger.debug(f"Creating index idx_wallets_{index}...")
await db.execute(f"""
CREATE INDEX IF NOT EXISTS idx_wallets_{index} ON wallets ("{index}");
""")
async def m042_index_accounts(db: Connection):
indexes = [
"id",
"email",
"username",
"pubkey",
"external_id",
]
for index in indexes:
logger.debug(f"Creating index idx_wallets_{index}...")
await db.execute(f"""
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")
async def m045_add_external_id_to_payments(db: Connection):
"""
Adds external_id column to apipayments.
Used for external payment references.
"""
await db.execute("ALTER TABLE apipayments ADD COLUMN external_id TEXT")
logger.debug("Creating index idx_payments_external_id...")
await db.execute("""
CREATE INDEX IF NOT EXISTS idx_payments_external_id
ON apipayments (external_id);
""")
"""
)
+2 -2
View File
@@ -46,7 +46,7 @@ from .users import (
UserAcls,
UserExtra,
)
from .wallets import CreateWallet, KeyType, Wallet, WalletInfo, WalletTypeInfo
from .wallets import BaseWallet, CreateWallet, KeyType, Wallet, WalletTypeInfo
from .webpush import CreateWebPushSubscription, WebPushSubscription
__all__ = [
@@ -57,6 +57,7 @@ __all__ = [
"AuditEntry",
"AuditFilters",
"BalanceDelta",
"BaseWallet",
"Callback",
"CancelInvoice",
"ConversionData",
@@ -98,7 +99,6 @@ __all__ = [
"UserAcls",
"UserExtra",
"Wallet",
"WalletInfo",
"WalletTypeInfo",
"WebPushSubscription",
]
-37
View File
@@ -1,37 +0,0 @@
from __future__ import annotations
from datetime import datetime, timezone
from pydantic import BaseModel, Field
from lnbits.db import FilterModel
class AssetInfo(BaseModel):
id: str
mime_type: str
name: str
is_public: bool = False
size_bytes: int
thumbnail_base64: str | None = None
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class Asset(AssetInfo):
user_id: str
data: bytes
class AssetUpdate(BaseModel):
name: str | None = None
is_public: bool | None = None
class AssetFilters(FilterModel):
__search_fields__ = ["name"]
__sort_fields__ = [
"created_at",
"name",
]
name: str | None = None
+13 -118
View File
@@ -6,13 +6,12 @@ import json
import os
import shutil
import zipfile
from asyncio.tasks import create_task
from pathlib import Path
from typing import Any
import httpx
from loguru import logger
from pydantic import BaseModel, Field
from pydantic import BaseModel
from lnbits.helpers import (
download_url,
@@ -21,7 +20,6 @@ from lnbits.helpers import (
version_parse,
)
from lnbits.settings import settings
from lnbits.utils.cache import cache
class ExplicitRelease(BaseModel):
@@ -41,7 +39,6 @@ class ExplicitRelease(BaseModel):
info_notification: str | None
critical_notification: str | None
details_link: str | None
paid_features: str | None
pay_link: str | None
def is_version_compatible(self):
@@ -190,7 +187,6 @@ class ExtensionRelease(BaseModel):
icon: str | None = None
details_link: str | None = None
paid_features: str | None = None
pay_link: str | None = None
cost_sats: int | None = None
paid_sats: int | None = 0
@@ -260,7 +256,6 @@ class ExtensionRelease(BaseModel):
html_url=e.html_url,
details_link=e.details_link,
pay_link=e.pay_link,
paid_features=e.paid_features,
repo=e.repo,
icon=e.icon,
)
@@ -269,30 +264,10 @@ class ExtensionRelease(BaseModel):
async def get_github_releases(cls, org: str, repo: str) -> list[ExtensionRelease]:
try:
github_releases = await cls.fetch_github_releases(org, repo)
extension_releases = [
return [
ExtensionRelease.from_github_release(f"{org}/{repo}", r)
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:
logger.warning(e)
return []
@@ -333,9 +308,6 @@ class ExtensionMeta(BaseModel):
dependencies: list[str] = []
archive: str | None = None
featured: bool = False
paid_features: str | None = None
has_paid_release: bool = False
has_free_release: bool = False
class InstallableExtension(BaseModel):
@@ -479,23 +451,9 @@ class InstallableExtension(BaseModel):
shutil.rmtree(self.ext_upgrade_dir, True)
def check_release_updates(self, release: ExtensionRelease | None):
self._check_latest_version(release)
self._check_payment_link(release)
def find_existing_payment(self, pay_link: str | None) -> ReleasePaymentInfo | None:
if not pay_link or not self.meta or not self.meta.payments:
return None
return next(
(p for p in self.meta.payments if p.pay_link == pay_link),
None,
)
def _check_latest_version(self, release: ExtensionRelease | None):
def check_latest_version(self, release: ExtensionRelease | None):
if not release:
return
if not release.is_version_compatible:
return
if not self.meta or not self.meta.latest_release:
meta = self.meta or ExtensionMeta()
meta.latest_release = release
@@ -506,19 +464,13 @@ class InstallableExtension(BaseModel):
):
self.meta.latest_release = release
def _check_payment_link(self, release: ExtensionRelease | None):
if not release:
return
if not release.is_version_compatible:
return
if not self.meta:
self.meta = ExtensionMeta()
if release.pay_link:
self.meta.has_paid_release = True
else:
self.meta.has_free_release = True
if release.paid_features:
self.meta.paid_features = release.paid_features
def find_existing_payment(self, pay_link: str | None) -> ReleasePaymentInfo | None:
if not pay_link or not self.meta or not self.meta.payments:
return None
return next(
(p for p in self.meta.payments if p.pay_link == pay_link),
None,
)
def _restore_payment_info(self):
if (
@@ -628,37 +580,6 @@ class InstallableExtension(BaseModel):
@classmethod
async def get_installable_extensions(
cls, post_refresh_cache: bool = False
) -> list[InstallableExtension]:
extension_list: list[InstallableExtension] = []
cache_key = "extensions:installable"
cache_value = cache.value(cache_key)
if not cache_value:
extension_list = await cls._get_installable_extensions()
cache.set(cache_key, extension_list, expiry=3600) # one hour
return extension_list
if cache_value.older_than(10 * 60) or post_refresh_cache:
# refresh cache in background if older than 10 minutes or requested
create_task(cls._refresh_installable_extensions_cache())
extension_list = cache_value.value # type: ignore
return extension_list
@classmethod
async def _refresh_installable_extensions_cache(
cls,
) -> None:
cache_key = "extensions:installable"
extension_list: list[InstallableExtension] = (
await cls._get_installable_extensions()
)
cache.set(cache_key, extension_list, expiry=3600)
@classmethod
async def _get_installable_extensions(
cls,
) -> list[InstallableExtension]:
extension_list: list[InstallableExtension] = []
@@ -675,7 +596,7 @@ class InstallableExtension(BaseModel):
(ee for ee in extension_list if ee.id == r.id), None
)
if existing_ext and ext.meta:
existing_ext.check_release_updates(ext.meta.latest_release)
existing_ext.check_latest_version(ext.meta.latest_release)
continue
meta = ext.meta or ExtensionMeta()
@@ -689,10 +610,10 @@ class InstallableExtension(BaseModel):
(ee for ee in extension_list if ee.id == e.id), None
)
if existing_ext:
existing_ext.check_release_updates(release)
existing_ext.check_latest_version(release)
continue
ext = InstallableExtension.from_explicit_release(e)
ext.check_release_updates(release)
ext.check_latest_version(release)
meta = ext.meta or ExtensionMeta()
meta.featured = ext.id in manifest.featured
ext.meta = meta
@@ -797,32 +718,6 @@ class ExtensionDetailsRequest(BaseModel):
version: str
class ExtensionReviewsStatus(BaseModel):
tag: str
avg_rating: float
review_count: int
class CreateExtensionReview(BaseModel):
tag: str
name: str | None = Field(None)
rating: int = Field(..., ge=0, le=1000)
comment: str | None = Field(None)
class ExtensionReviewPaymentRequest(BaseModel):
payment_hash: str
payment_request: str
class ExtensionReview(BaseModel):
id: str
name: str | None = Field(default=None)
tag: str | None = Field(default=None)
rating: int = Field(default=0, ge=0, le=1000)
comment: str | None = Field(default=None)
async def github_api_get(url: str, error_msg: str | None) -> Any:
headers = {"User-Agent": settings.user_agent}
if settings.lnbits_ext_github_token:
-25
View File
@@ -1,8 +1,6 @@
from __future__ import annotations
import json
import random
import uuid
from datetime import datetime, timedelta, timezone
from typing import Any, Literal
@@ -57,29 +55,6 @@ class DataField(BaseModel):
field_type += ' = "sat"'
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:
field_name = camel_to_snake(self.name)
default_value = "null"
+56 -72
View File
@@ -6,17 +6,23 @@ from typing import Literal
from fastapi import Query
from lnurl import LnurlWithdrawResponse
from loguru import logger
from pydantic import BaseModel, Field, validator
from lnbits.db import FilterModel
from lnbits.fiat import get_fiat_provider
from lnbits.fiat.base import (
FiatPaymentFailedStatus,
FiatPaymentPendingStatus,
FiatPaymentStatus,
FiatPaymentSuccessStatus,
)
from lnbits.helpers import is_valid_external_id
from lnbits.utils.exchange_rates import allowed_currencies
from lnbits.wallets import get_funding_source
from lnbits.wallets.base import (
PaymentFailedStatus,
PaymentPendingStatus,
PaymentStatus,
PaymentSuccessStatus,
)
@@ -53,12 +59,6 @@ class CreatePayment(BaseModel):
expiry: datetime | None = None
webhook: str | None = None
fee: int = 0
labels: list[str] | None = None
external_id: str | None = None
@validator("external_id")
def validate_external_id(cls, external_id):
return _validate_external_id(external_id)
class Payment(BaseModel):
@@ -68,7 +68,7 @@ class Payment(BaseModel):
amount: int
fee: int
bolt11: str
payment_request: str | None = Field(default=None, no_database=True)
# payment_request: str | None
fiat_provider: str | None = None
status: str = PaymentState.PENDING
memo: str | None = None
@@ -81,20 +81,7 @@ class Payment(BaseModel):
time: 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))
labels: list[str] = []
extra: dict = {}
external_id: str | None = None
@validator("external_id")
def validate_external_id(cls, external_id):
return _validate_external_id(external_id)
def __init__(self, **data):
super().__init__(**data)
if "fiat_payment_request" in self.extra:
self.payment_request = self.extra["fiat_payment_request"]
else:
self.payment_request = self.bolt11
@property
def pending(self) -> bool:
@@ -134,52 +121,69 @@ class Payment(BaseModel):
"fiat_"
)
# DEPRECATED: in v1.5.0, use service check_payment_status instead
async def check_status(
self, skip_internal_payment_notifications: bool | None = False
) -> PaymentStatus:
logger.warning("payment.check_status() is deprecated.")
from lnbits.core.services.payments import check_payment_status
if self.is_internal:
if self.success:
return PaymentSuccessStatus()
if self.failed:
return PaymentFailedStatus()
if self.is_in and self.fiat_provider:
fiat_status = await self.check_fiat_status(
skip_internal_payment_notifications
)
return PaymentStatus(paid=fiat_status.paid)
return PaymentPendingStatus()
funding_source = get_funding_source()
if self.is_out:
status = await funding_source.get_payment_status(self.checking_id)
else:
status = await funding_source.get_invoice_status(self.checking_id)
return status
return await check_payment_status(self, skip_internal_payment_notifications)
# DEPRECATED: in v1.5.0, use service check_payment_status instead
async def check_fiat_status(
self, skip_internal_payment_notifications: bool | None = False
) -> FiatPaymentStatus:
logger.warning("payment.check_fiat_status() is deprecated.")
from lnbits.core.services.fiat_providers import check_fiat_status
if not self.is_internal:
return FiatPaymentPendingStatus()
if self.success:
return FiatPaymentSuccessStatus()
if self.failed:
return FiatPaymentFailedStatus()
return await check_fiat_status(self, skip_internal_payment_notifications)
if not self.fiat_provider:
return FiatPaymentPendingStatus()
checking_id = self.extra.get("fiat_checking_id")
if not checking_id:
return FiatPaymentPendingStatus()
fiat_provider = await get_fiat_provider(self.fiat_provider)
if not fiat_provider:
return FiatPaymentPendingStatus()
fiat_status = await fiat_provider.get_invoice_status(checking_id)
if skip_internal_payment_notifications:
return fiat_status
if fiat_status.success:
# notify receivers asynchronously
from lnbits.tasks import internal_invoice_queue
await internal_invoice_queue.put(self.checking_id)
return fiat_status
class PaymentFilters(FilterModel):
__search_fields__ = [
"memo",
"amount",
"wallet_id",
"tag",
"status",
"time",
"labels",
"external_id",
]
__search_fields__ = ["memo", "amount", "wallet_id", "tag", "status", "time"]
__sort_fields__ = [
"created_at",
"updated_at",
"amount",
"fee",
"memo",
"time",
"tag",
"external_id",
]
__sort_fields__ = ["created_at", "amount", "fee", "memo", "time", "tag"]
status: str | None
tag: str | None
checking_id: str | None
external_id: str | None
amount: int
fee: int
memo: str | None
@@ -187,7 +191,6 @@ class PaymentFilters(FilterModel):
preimage: str | None
payment_hash: str | None
wallet_id: str | None
labels: str | None
class PaymentDataPoint(BaseModel):
@@ -262,8 +265,6 @@ class CreateInvoice(BaseModel):
bolt11: str | None = None
lnurl_withdraw: LnurlWithdrawResponse | None = None
fiat_provider: str | None = None
labels: list[str] = []
external_id: str | None = Query(default=None, max_length=256)
@validator("payment_hash")
def check_hex(cls, v):
@@ -278,10 +279,6 @@ class CreateInvoice(BaseModel):
raise ValueError("The provided unit is not supported")
return v
@validator("external_id")
def validate_external_id(cls, external_id):
return _validate_external_id(external_id)
class PaymentsStatusCount(BaseModel):
incoming: int = 0
@@ -316,16 +313,3 @@ class CancelInvoice(BaseModel):
def check_hex(cls, v):
_ = bytes.fromhex(v)
return v
class UpdatePaymentLabels(BaseModel):
labels: list[str] = []
def _validate_external_id(external_id: str | None) -> str | None:
if external_id and not is_valid_external_id(external_id):
raise ValueError(
"Invalid external id. Max length is 256 characters. "
"Space and newlines are not allowed."
)
return external_id
-1
View File
@@ -1 +0,0 @@
"""SSO authentication providers for LNbits"""
-36
View File
@@ -1,36 +0,0 @@
"""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
+2 -4
View File
@@ -1,6 +1,4 @@
from datetime import datetime, timezone
from pydantic import BaseModel, Field
from pydantic import BaseModel
class TinyURL(BaseModel):
@@ -8,4 +6,4 @@ class TinyURL(BaseModel):
url: str
endless: bool
wallet: str
time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
time: float
+8 -94
View File
@@ -12,7 +12,6 @@ from lnbits.db import FilterModel
from lnbits.helpers import (
is_valid_email_address,
is_valid_external_id,
is_valid_label,
is_valid_pubkey,
is_valid_username,
)
@@ -30,21 +29,6 @@ class UserNotifications(BaseModel):
incoming_payments_sats: int = 0
class WalletInviteRequest(BaseModel):
request_id: str
from_user_name: str | None = None
to_wallet_id: str
to_wallet_name: str
class UserLabel(BaseModel):
name: str = Field(regex=r"([A-Za-z0-9 ._-]{1,100}$)")
description: str | None = Field(default=None, max_length=250)
color: str | None = Field(
default=None, regex=r"^#[0-9A-Fa-f]{6}$"
) # e.g., "#RRGGBB"
class UserExtra(BaseModel):
email_verified: bool | None = False
first_name: str | None = None
@@ -62,55 +46,6 @@ class UserExtra(BaseModel):
notifications: UserNotifications = UserNotifications()
wallet_invite_requests: list[WalletInviteRequest] = []
labels: list[UserLabel] = []
def add_wallet_invite_request(
self,
request_id: str,
to_wallet_id: str,
to_wallet_name: str,
from_user_name: str | None = None,
) -> WalletInviteRequest:
self.remove_wallet_invite_request(request_id)
invite = WalletInviteRequest(
request_id=request_id,
from_user_name=from_user_name,
to_wallet_id=to_wallet_id,
to_wallet_name=to_wallet_name,
)
self.wallet_invite_requests.append(invite)
return invite
def find_wallet_invite_request(self, request_id: str) -> WalletInviteRequest | None:
for invite in self.wallet_invite_requests:
if invite.request_id == request_id:
return invite
return None
def validate_labels(self):
seen_labels = set()
for label in self.labels:
if not label.name:
raise ValueError("Label name cannot be empty.")
# apply the same rule for labels as for usernames
if not is_valid_label(label.name):
raise ValueError(f"Invalid label name: {label.name}")
if label.name in seen_labels:
raise ValueError(f"Duplicate label name: {label.name}")
seen_labels.add(label.name)
def remove_wallet_invite_request(
self,
request_id: str,
):
self.wallet_invite_requests = [
invite
for invite in self.wallet_invite_requests
if invite.request_id != request_id
]
class EndpointAccess(BaseModel):
path: str
@@ -172,23 +107,14 @@ class UserAcls(BaseModel):
return None
class AccountId(BaseModel):
class Account(BaseModel):
id: str
@property
def is_admin_id(self) -> bool:
return settings.is_admin_user(self.id)
class Account(AccountId):
activated: bool = True
external_id: str | None = None # for external account linking
username: str | None = None
password_hash: str | None = None
pubkey: str | None = None
email: str | None = None
extra: UserExtra = UserExtra()
ui_customization: dict = Field(default_factory=dict)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
@@ -203,10 +129,6 @@ class Account(AccountId):
self.is_admin = settings.is_admin_user(self.id)
self.fiat_providers = settings.get_fiat_providers_for_user(self.id)
@property
def has_password(self) -> bool:
return self.password_hash is not None
def hash_password(self, password: str) -> str:
"""sets and returns the hashed password"""
salt = gensalt()
@@ -238,11 +160,8 @@ class Account(AccountId):
if user_uuid4.hex != self.id:
raise ValueError("User ID is not valid UUID4 hex string.")
self.extra.validate_labels()
class AccountOverview(Account):
activated: bool = True
transaction_count: int | None = 0
wallet_count: int | None = 0
balance_msat: int | None = 0
@@ -251,7 +170,7 @@ class AccountOverview(Account):
class AccountFilters(FilterModel):
__search_fields__ = [
"id",
"user",
"email",
"username",
"pubkey",
@@ -259,18 +178,17 @@ class AccountFilters(FilterModel):
"wallet_id",
]
__sort_fields__ = [
"id",
"balance_msat",
"email",
"username",
"pubkey",
"external_id",
"created_at",
"updated_at",
"transaction_count",
"wallet_count",
"last_payment",
]
id: str | None = None
username: str | None = None
email: str | None = None
user: str | None = None
username: str | None = None
pubkey: str | None = None
external_id: str | None = None
wallet_id: str | None = None
@@ -278,7 +196,6 @@ class AccountFilters(FilterModel):
class User(BaseModel):
id: str
activated: bool = True
created_at: datetime
updated_at: datetime
email: str | None = None
@@ -292,7 +209,6 @@ class User(BaseModel):
fiat_providers: list[str] = []
has_password: bool = False
extra: UserExtra = UserExtra()
ui_customization: dict = Field(default_factory=dict)
@property
def wallet_ids(self) -> list[str]:
@@ -318,7 +234,6 @@ class RegisterUser(BaseModel):
username: str = Query(default=..., min_length=2, max_length=20)
password: str = Query(default=..., min_length=8, max_length=50)
password_repeat: str = Query(default=..., min_length=8, max_length=50)
invitation_code: str | None = Query(default=None, max_length=256)
class CreateUser(BaseModel):
@@ -362,7 +277,6 @@ class UpdateSuperuserPassword(BaseModel):
username: str = Query(default=..., min_length=2, max_length=20)
password: str = Query(default=..., min_length=8, max_length=50)
password_repeat: str = Query(default=..., min_length=8, max_length=50)
first_install_token: str | None = Query(None)
class LoginUsr(BaseModel):
+11 -172
View File
@@ -4,14 +4,16 @@ from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum
from lnurl import encode as lnurl_encode
from pydantic import BaseModel, Field
from lnbits.core.models.lnurl import StoredPayLinks
from lnbits.db import FilterModel
from lnbits.helpers import url_for
from lnbits.settings import settings
class WalletInfo(BaseModel):
class BaseWallet(BaseModel):
id: str
name: str
adminkey: str
@@ -19,109 +21,18 @@ class WalletInfo(BaseModel):
balance_msat: int
class WalletType(Enum):
LIGHTNING = "lightning"
LIGHTNING_SHARED = "lightning-shared"
class WalletPermission(Enum):
VIEW_PAYMENTS = "view-payments"
RECEIVE_PAYMENTS = "receive-payments"
SEND_PAYMENTS = "send-payments"
def __str__(self):
return self.value
class WalletShareStatus(Enum):
INVITE_SENT = "invite_sent"
APPROVED = "approved"
class WalletSharePermission(BaseModel):
# unique identifier for this share request
request_id: str | None = None
# username of the invited user
username: str
# ID of the wallet being shared with
shared_with_wallet_id: str | None = None
# permissions being granted
permissions: list[WalletPermission] = []
# status of the share request
status: WalletShareStatus
comment: str | None = None
def approve(
self,
permissions: list[WalletPermission] | None = None,
shared_with_wallet_id: str | None = None,
):
self.status = WalletShareStatus.APPROVED
if permissions is not None:
self.permissions = permissions
if shared_with_wallet_id is not None:
self.shared_with_wallet_id = shared_with_wallet_id
@property
def is_approved(self) -> bool:
return self.status == WalletShareStatus.APPROVED
class WalletExtra(BaseModel):
icon: str = "flash_on"
color: str = "primary"
pinned: bool = False
# What permissions this wallet grants when it's shared with other users
shared_with: list[WalletSharePermission] = []
def invite_user_to_shared_wallet(
self,
request_id: str,
request_type: WalletShareStatus,
username: str,
permissions: list[WalletPermission] | None = None,
) -> WalletSharePermission:
share = WalletSharePermission(
request_id=request_id,
username=username,
status=request_type,
permissions=permissions or [],
)
self.shared_with.append(share)
return share
def find_share_by_id(self, request_id: str) -> WalletSharePermission | None:
for share in self.shared_with:
if share.request_id == request_id:
return share
return None
def find_share_for_wallet(
self, shared_with_wallet_id: str
) -> WalletSharePermission | None:
for share in self.shared_with:
if share.shared_with_wallet_id == shared_with_wallet_id:
return share
return None
def remove_share_by_id(self, request_id: str):
self.shared_with = [
share for share in self.shared_with if share.request_id != request_id
]
class BaseWallet(BaseModel):
class Wallet(BaseModel):
id: str
user: str
wallet_type: str = WalletType.LIGHTNING.value
name: str
adminkey: str
inkey: str
class Wallet(BaseWallet):
name: str
# Must be set only for shared wallets
shared_wallet_id: str | None = None
deleted: bool = False
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
@@ -129,65 +40,6 @@ class Wallet(BaseWallet):
balance_msat: int = Field(default=0, no_database=True)
extra: WalletExtra = WalletExtra()
stored_paylinks: StoredPayLinks = StoredPayLinks()
# What permission this wallet has when it's a shared wallet
share_permissions: list[WalletPermission] = Field(default=[], no_database=True)
def __init__(self, **data):
super().__init__(**data)
self._validate_data()
def mirror_shared_wallet(
self,
shared_wallet: Wallet,
):
if not shared_wallet.is_lightning_wallet:
return None
self.wallet_type = WalletType.LIGHTNING_SHARED.value
self.shared_wallet_id = shared_wallet.id
self.name = shared_wallet.name
self.share_permissions = shared_wallet.get_share_permissions(self.id)
if len(self.share_permissions):
self.currency = shared_wallet.currency
self.balance_msat = shared_wallet.balance_msat
self.stored_paylinks = shared_wallet.stored_paylinks
self.extra.icon = shared_wallet.extra.icon
self.extra.color = shared_wallet.extra.color
def get_share_permissions(self, wallet_id: str) -> list[WalletPermission]:
for share in self.extra.shared_with:
if share.shared_with_wallet_id == wallet_id and share.is_approved:
return share.permissions
return []
def has_permission(self, permission: WalletPermission) -> bool:
if self.is_lightning_wallet:
return True
if self.is_lightning_shared_wallet:
return permission in self.share_permissions
return False
@property
def source_wallet_id(self) -> str:
"""For shared wallets return the original wallet ID, else return own ID."""
if self.is_lightning_shared_wallet and len(self.share_permissions):
return self.shared_wallet_id or self.id
return self.id
@property
def can_receive_payments(self) -> bool:
return self.has_permission(WalletPermission.RECEIVE_PAYMENTS)
@property
def can_send_payments(self) -> bool:
return self.has_permission(WalletPermission.SEND_PAYMENTS)
@property
def can_view_payments(self) -> bool:
return self.has_permission(WalletPermission.VIEW_PAYMENTS)
@property
def balance(self) -> int:
@@ -198,23 +50,16 @@ class Wallet(BaseWallet):
return self.balance_msat - settings.fee_reserve(self.balance_msat)
@property
def is_lightning_wallet(self) -> bool:
return self.wallet_type == WalletType.LIGHTNING.value
@property
def is_lightning_shared_wallet(self) -> bool:
return self.wallet_type == WalletType.LIGHTNING_SHARED.value
def _validate_data(self):
if self.is_lightning_shared_wallet:
if not self.shared_wallet_id:
raise ValueError("Shared wallet ID must be set for shared wallets.")
def lnurlwithdraw_full(self) -> str:
url = url_for("/withdraw", external=True, usr=self.user, wal=self.id)
try:
return lnurl_encode(url)
except Exception:
return ""
class CreateWallet(BaseModel):
name: str | None = None
wallet_type: WalletType = WalletType.LIGHTNING
shared_wallet_id: str | None = None
class KeyType(Enum):
@@ -233,12 +78,6 @@ class WalletTypeInfo:
wallet: Wallet
@dataclass
class BaseWalletTypeInfo:
key_type: KeyType
wallet: BaseWallet
class WalletsFilters(FilterModel):
__search_fields__ = ["id", "name", "currency"]
-4
View File
@@ -1,4 +1,3 @@
from .fiat_providers import check_fiat_status
from .funding_source import (
get_balance_delta,
switch_to_voidwallet,
@@ -8,7 +7,6 @@ from .notifications import enqueue_admin_notification, send_payment_notification
from .payments import (
calculate_fiat_amounts,
cancel_hold_invoice,
check_payment_status,
check_transaction_status,
check_wallet_limits,
create_fiat_invoice,
@@ -42,8 +40,6 @@ __all__ = [
"calculate_fiat_amounts",
"cancel_hold_invoice",
"check_admin_settings",
"check_fiat_status",
"check_payment_status",
"check_transaction_status",
"check_wallet_limits",
"check_webpush_settings",
-194
View File
@@ -1,194 +0,0 @@
import base64
import io
from urllib.parse import quote
from uuid import uuid4
import filetype
from fastapi import UploadFile
from loguru import logger
from PIL import Image
from lnbits.core.crud.assets import create_asset, get_user_assets_count
from lnbits.core.models.assets import Asset
from lnbits.settings import settings
IMAGE_MIME_TYPE_ALIASES = {
"heic": "image/heic",
"heics": "image/heics",
"heif": "image/heif",
"image/jpg": "image/jpeg",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"png": "image/png",
}
PIL_IMAGE_FORMAT_MIME_TYPES = {
"JPEG": "image/jpeg",
"PNG": "image/png",
}
INLINE_ASSET_MIME_TYPES = {
"image/heic",
"image/heics",
"image/heif",
"image/jpeg",
"image/png",
}
ASSET_SECURITY_HEADERS = {
"X-Content-Type-Options": "nosniff",
"Content-Security-Policy": (
"sandbox; default-src 'none'; script-src 'none'; "
"object-src 'none'; base-uri 'none'"
),
}
THUMBNAIL_FORMAT_MIME_TYPES = {
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"png": "image/png",
}
async def create_user_asset(user_id: str, file: UploadFile, is_public: bool) -> Asset:
if not file.content_type:
raise ValueError("File must have a content type.")
content_type = normalize_asset_mime_type(file.content_type)
filename = file.filename or "unnamed"
if content_type not in allowed_asset_mime_types():
raise ValueError(f"File type '{file.content_type}' not allowed.")
if not settings.is_unlimited_assets_user(user_id):
user_assets_count = await get_user_assets_count(user_id)
if user_assets_count >= settings.lnbits_max_assets_per_user:
raise ValueError(
f"Max upload count of {settings.lnbits_max_assets_per_user} exceeded."
)
contents = await file.read()
if len(contents) > settings.lnbits_max_asset_size_mb * 1024 * 1024:
raise ValueError(
f"File limit of {settings.lnbits_max_asset_size_mb}MB exceeded."
)
stored_mime_type = detect_image_mime_type(contents)
if stored_mime_type != content_type:
logger.warning(
"Image MIME type mismatch: declared={}, detected={}",
content_type,
stored_mime_type,
)
raise ValueError(
"Image file content does not match declared file type. "
f"Declared: '{content_type}', detected: '{stored_mime_type}'."
)
thumb_buffer = thumbnail_from_bytes(contents)
asset = Asset(
id=uuid4().hex,
user_id=user_id,
mime_type=stored_mime_type,
is_public=is_public,
name=filename,
size_bytes=len(contents),
thumbnail_base64=(
base64.b64encode(thumb_buffer.getvalue()).decode("utf-8")
if thumb_buffer
else None
),
data=contents,
)
await create_asset(asset)
return asset
def normalize_asset_mime_type(content_type: str) -> str:
content_type = content_type.split(";", 1)[0].strip().lower()
return IMAGE_MIME_TYPE_ALIASES.get(content_type, content_type)
def normalize_media_type(media_type: str) -> str:
return media_type.split(";", 1)[0].strip().lower() or "application/octet-stream"
def thumbnail_media_type() -> str:
thumbnail_format = (settings.lnbits_asset_thumbnail_format or "png").strip().lower()
return THUMBNAIL_FORMAT_MIME_TYPES.get(thumbnail_format, "application/octet-stream")
def content_disposition(disposition: str, filename: str) -> str:
safe_filename = filename or "unnamed"
quoted_filename = quote(safe_filename, safe="")
if quoted_filename == safe_filename:
return f'{disposition}; filename="{safe_filename}"'
return f"{disposition}; filename*=utf-8''{quoted_filename}"
def allowed_asset_mime_types() -> set[str]:
return {
mime_type
for mime_type in (
normalize_asset_mime_type(mime_type)
for mime_type in settings.lnbits_assets_allowed_mime_types
)
if mime_type.startswith("image/")
}
def detect_image_mime_type(contents: bytes) -> str:
kind = filetype.guess(contents)
mime_type = normalize_asset_mime_type(kind.mime) if kind else None
if mime_type and mime_type in PIL_IMAGE_FORMAT_MIME_TYPES.values():
verify_pil_image(contents, mime_type)
return mime_type
if mime_type and mime_type.startswith("image/"):
return mime_type
try:
with Image.open(io.BytesIO(contents)) as image:
image.verify()
mime_type = PIL_IMAGE_FORMAT_MIME_TYPES.get(image.format or "")
except Exception as exc:
raise ValueError(
"Image file content does not match declared file type."
) from exc
if not mime_type:
raise ValueError("Image file content does not match declared file type.")
return mime_type
def verify_pil_image(contents: bytes, mime_type: str) -> None:
try:
with Image.open(io.BytesIO(contents)) as image:
image.verify()
detected_mime_type = PIL_IMAGE_FORMAT_MIME_TYPES.get(image.format or "")
except Exception as exc:
raise ValueError(
"Image file content does not match declared file type."
) from exc
if detected_mime_type != mime_type:
raise ValueError("Image file content does not match declared file type.")
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
+2 -15
View File
@@ -9,7 +9,6 @@ from lnbits.core.crud import (
delete_installed_extension,
get_db_version,
get_installed_extension,
get_installed_extensions_count,
update_installed_extension_state,
)
from lnbits.core.crud.extensions import (
@@ -17,7 +16,6 @@ from lnbits.core.crud.extensions import (
update_installed_extension,
)
from lnbits.core.helpers import migrate_extension_database
from lnbits.db import Connection
from lnbits.settings import settings
from ..models.extensions import Extension, ExtensionMeta, InstallableExtension
@@ -39,8 +37,6 @@ async def install_extension(
if installed_ext and installed_ext.meta:
ext_info.meta.payments = installed_ext.meta.payments
await check_extensions_limit(installed_ext)
if not skip_download:
await ext_info.download_archive()
@@ -66,15 +62,6 @@ async def install_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):
await stop_extension_background_work(ext_id)
@@ -162,9 +149,9 @@ async def start_extension_background_work(ext_id: str) -> bool:
async def get_valid_extensions(
include_deactivated: bool | None = True, conn: Connection | None = None
include_deactivated: bool | None = True,
) -> list[Extension]:
installed_extensions = await get_installed_extensions(conn=conn)
installed_extensions = await get_installed_extensions()
valid_extensions = [Extension.from_installable_ext(e) for e in installed_extensions]
if include_deactivated:
+11 -44
View File
@@ -20,7 +20,6 @@ from lnbits.helpers import (
camel_to_words,
download_url,
lowercase_first_letter,
snake_to_camel,
)
from lnbits.settings import settings
@@ -196,29 +195,13 @@ def _copy_ext_stub_to_build_dir(
ext_build_dir = Path(working_dir, new_ext_id, working_dir_name, new_ext_id)
shutil.rmtree(ext_build_dir, True)
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",
),
)
shutil.copytree(ext_stub_cache_dir, ext_build_dir)
return ext_build_dir
def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None:
parsed_data = _parse_extension_data(data)
test_files = [f"tests/{p.name}" for p in Path(ext_stub_dir, "tests").glob("*.py")]
for py_file in py_files + test_files:
for py_file in py_files:
template_path = Path(ext_stub_dir, py_file).as_posix()
rederer = _render_file(template_path, parsed_data)
with open(template_path, "w", encoding="utf-8") as f:
@@ -226,7 +209,7 @@ def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None
_remove_lines_with_string(template_path, remove_line_marker)
template_path = Path(ext_stub_dir, "static", "index.js").as_posix()
template_path = Path(ext_stub_dir, "static", "js", "index.js").as_posix()
rederer = _render_file(
template_path, {"preview": data.preview_action, **parsed_data}
)
@@ -251,7 +234,9 @@ def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None
"settingsFormDialog.data",
ext_stub_dir,
)
template_path = Path(ext_stub_dir, "static", "index.vue").as_posix()
template_path = Path(
ext_stub_dir, "templates", "extension_builder_stub", "index.html"
).as_posix()
rederer = _render_file(
template_path,
{
@@ -278,12 +263,12 @@ def _replace_jinja_placeholders(data: ExtensionData, ext_stub_dir: Path) -> None
"publicClientData",
ext_stub_dir,
)
public_template_path = Path(ext_stub_dir, "static", "public_page.vue")
public_component_path = Path(ext_stub_dir, "static", "public_page.js")
public_template_path = Path(
ext_stub_dir, "templates", "extension_builder_stub", "public_page.html"
)
template_path = public_template_path.as_posix()
if not data.public_page.has_public_page:
public_template_path.unlink(missing_ok=True)
public_component_path.unlink(missing_ok=True)
else:
rederer = _render_file(
template_path,
@@ -323,10 +308,8 @@ def zip_directory(source_dir, zip_path):
def _rename_extension_builder_stub(data: ExtensionData, extension_dir: Path) -> None:
extension_dir_path = extension_dir.as_posix()
# the order of fields is important, do not chage
rename_values = {
"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": data.id,
"OwnerData": data.owner_data.name,
@@ -345,7 +328,7 @@ def _rename_extension_builder_stub(data: ExtensionData, extension_dir: Path) ->
directory=extension_dir_path,
old_text=old_text,
new_text=new_text,
file_extensions=[".py", ".js", ".vue", ".html", ".md", ".json", ".toml"],
file_extensions=[".py", ".js", ".html", ".md", ".json", ".toml"],
)
_rename_files_and_dirs_in_directory(
@@ -423,7 +406,6 @@ def _parse_extension_data(data: ExtensionData) -> dict:
"owner_data": {
"name": data.owner_data.name,
"editable": data.owner_data.editable,
"fields": [field.name for field in data.owner_data.fields],
"js_fields": [
field.field_to_js()
for field in data.owner_data.fields
@@ -450,18 +432,6 @@ def _parse_extension_data(data: ExtensionData) -> dict:
],
"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],
"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": {
"name": data.client_data.name,
@@ -487,9 +457,6 @@ def _parse_extension_data(data: ExtensionData) -> dict:
],
"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],
"random_fields_values": [
field.field_to_random_value() for field in data.client_data.fields
],
},
"settings_data": {
"enabled": data.settings_data.enabled,
@@ -569,7 +536,7 @@ def _rename_files_and_dirs_in_directory(directory, old_text, new_text):
logger.warning(f"Failed to rename directory {old_dir_path}: {e}")
def _is_excluded_dir(path: str) -> bool:
def _is_excluded_dir(path):
for excluded_dir in excluded_dirs:
if path.startswith(excluded_dir):
return True
+88 -244
View File
@@ -1,24 +1,15 @@
import hashlib
import hmac
import json
import time
from base64 import b64encode
import httpx
from loguru import logger
from lnbits.core.crud import get_wallet
from lnbits.core.crud.payments import create_payment
from lnbits.core.crud.payments import create_payment, get_standalone_payment
from lnbits.core.models import CreatePayment, Payment, PaymentState
from lnbits.core.models.misc import SimpleStatus
from lnbits.db import Connection
from lnbits.fiat import get_fiat_provider
from lnbits.fiat.base import (
FiatPaymentFailedStatus,
FiatPaymentPendingStatus,
FiatPaymentStatus,
FiatPaymentSuccessStatus,
)
from lnbits.settings import settings
@@ -36,240 +27,6 @@ async def handle_fiat_payment_confirmation(
logger.warning(e)
async def check_fiat_status(
payment: Payment, skip_internal_payment_notifications: bool | None = False
) -> FiatPaymentStatus:
if not payment.is_internal:
return FiatPaymentPendingStatus()
if payment.success:
return FiatPaymentSuccessStatus()
if payment.failed:
return FiatPaymentFailedStatus()
if not payment.fiat_provider:
return FiatPaymentPendingStatus()
checking_id = payment.extra.get("fiat_checking_id")
if not checking_id:
return FiatPaymentPendingStatus()
fiat_provider = await get_fiat_provider(payment.fiat_provider)
if not fiat_provider:
return FiatPaymentPendingStatus()
fiat_status = await fiat_provider.get_invoice_status(checking_id)
if skip_internal_payment_notifications:
return fiat_status
if fiat_status.success:
# notify receivers asynchronously
from lnbits.tasks import internal_invoice_queue
await internal_invoice_queue.put(payment.checking_id)
return fiat_status
def check_stripe_signature(
payload: bytes,
sig_header: str | None,
secret: str | None,
tolerance_seconds=300,
):
if not sig_header:
logger.warning("Stripe-Signature header is missing.")
raise ValueError("Stripe-Signature header is missing.")
if not secret:
logger.warning("Stripe webhook signing secret is not set.")
raise ValueError("Stripe webhook cannot be verified.")
# Split the Stripe-Signature header
items = dict(i.split("=") for i in sig_header.split(","))
timestamp = int(items["t"])
signature = items["v1"]
# Check timestamp tolerance
if abs(time.time() - timestamp) > tolerance_seconds:
logger.warning("Timestamp outside tolerance.")
logger.debug(
f"Current time: {time.time()}, "
f"Timestamp: {timestamp}, "
f"Tolerance: {tolerance_seconds} seconds"
)
raise ValueError("Timestamp outside tolerance." f"Timestamp: {timestamp}")
signed_payload = f"{timestamp}.{payload.decode()}"
# Compute HMAC SHA256 using the webhook secret
computed_signature = hmac.new(
key=secret.encode(), msg=signed_payload.encode(), digestmod=hashlib.sha256
).hexdigest()
# Compare signatures using constant time comparison
if hmac.compare_digest(computed_signature, signature) is not True:
logger.warning("Stripe signature verification failed.")
raise ValueError("Stripe signature verification failed.")
async def verify_paypal_webhook(headers, payload: bytes):
"""
Validate PayPal webhook signatures using the PayPal verify API.
"""
webhook_id = settings.paypal_webhook_id
if not webhook_id:
logger.warning("PayPal webhook ID not set.")
raise ValueError("PayPal webhook cannot be verified. Missing webhook ID.")
required_headers = {
"PAYPAL-TRANSMISSION-ID": headers.get("PAYPAL-TRANSMISSION-ID"),
"PAYPAL-TRANSMISSION-TIME": headers.get("PAYPAL-TRANSMISSION-TIME"),
"PAYPAL-TRANSMISSION-SIG": headers.get("PAYPAL-TRANSMISSION-SIG"),
"PAYPAL-CERT-URL": headers.get("PAYPAL-CERT-URL"),
"PAYPAL-AUTH-ALGO": headers.get("PAYPAL-AUTH-ALGO"),
}
if not all(required_headers.values()):
logger.warning("Missing PayPal webhook headers.")
raise ValueError("PayPal webhook cannot be verified. Missing headers.")
try:
async with httpx.AsyncClient(base_url=settings.paypal_api_endpoint) as client:
token_resp = await client.post(
"/v1/oauth2/token",
data={"grant_type": "client_credentials"},
auth=(
settings.paypal_client_id or "",
settings.paypal_client_secret or "",
),
)
token_resp.raise_for_status()
access_token = token_resp.json().get("access_token")
if not access_token:
raise ValueError("PayPal token missing in verification flow.")
verify_resp = await client.post(
"/v1/notifications/verify-webhook-signature",
json={
"auth_algo": required_headers["PAYPAL-AUTH-ALGO"],
"cert_url": required_headers["PAYPAL-CERT-URL"],
"transmission_id": required_headers["PAYPAL-TRANSMISSION-ID"],
"transmission_sig": required_headers["PAYPAL-TRANSMISSION-SIG"],
"transmission_time": required_headers["PAYPAL-TRANSMISSION-TIME"],
"webhook_id": webhook_id,
"webhook_event": json.loads(payload.decode()),
},
headers={"Authorization": f"Bearer {access_token}"},
)
verify_resp.raise_for_status()
verification_status = verify_resp.json().get("verification_status")
if verification_status != "SUCCESS":
raise ValueError("PayPal webhook verification failed.")
except Exception as exc:
logger.warning(exc)
raise ValueError("PayPal webhook cannot be verified.") from exc
def check_square_signature(
payload: bytes,
sig_header: str | None,
secret: str | None,
notification_url: str | None,
):
if not sig_header:
logger.warning("Square signature header is missing.")
raise ValueError("Square signature header is missing.")
if not secret:
logger.warning("Square webhook signature key is not set.")
raise ValueError("Square webhook cannot be verified.")
if not notification_url:
logger.warning("Square webhook notification URL is not set.")
raise ValueError("Square webhook cannot be verified.")
signed_payload = notification_url.encode() + payload
computed_signature = b64encode(
hmac.new(
key=secret.encode(), msg=signed_payload, digestmod=hashlib.sha256
).digest()
).decode()
if hmac.compare_digest(computed_signature, sig_header) is not True:
logger.warning("Square signature verification failed.")
raise ValueError("Square signature verification failed.")
def check_revolut_signature(
payload: bytes,
sig_header: str | None,
timestamp_header: str | None,
secret: str | None,
tolerance_seconds=300,
):
if not sig_header:
logger.warning("Revolut signature header is missing.")
raise ValueError("Revolut signature header is missing.")
if not timestamp_header:
logger.warning("Revolut timestamp header is missing.")
raise ValueError("Revolut timestamp header is missing.")
if not secret:
logger.warning("Revolut webhook signing secret is not set.")
raise ValueError("Revolut webhook cannot be verified.")
try:
timestamp = int(timestamp_header)
except ValueError as exc:
logger.warning("Invalid Revolut timestamp.")
raise ValueError("Invalid Revolut timestamp.") from exc
timestamp_seconds = timestamp / 1000 if timestamp > 9999999999 else timestamp
if abs(time.time() - timestamp_seconds) > tolerance_seconds:
logger.warning("Timestamp outside tolerance.")
raise ValueError("Timestamp outside tolerance." f"Timestamp: {timestamp}")
signed_payload = b"v1." + timestamp_header.encode() + b"." + payload
digest = hmac.new(
key=secret.encode(), msg=signed_payload, digestmod=hashlib.sha256
).hexdigest()
expected_signature = f"v1={digest}"
provided_signatures = [sig.strip() for sig in sig_header.split(",") if sig.strip()]
if not any(
hmac.compare_digest(expected_signature, provided)
for provided in provided_signatures
):
logger.warning("Revolut signature verification failed.")
raise ValueError("Revolut signature verification failed.")
async def test_connection(provider: str) -> SimpleStatus:
"""
Test the connection to Stripe by checking if the API key is valid.
This function should be called when setting up or testing the Stripe integration.
"""
fiat_provider = await get_fiat_provider(provider)
if not fiat_provider:
return SimpleStatus(
success=False,
message=f"Fiat provider '{provider}' not found.",
)
status = await fiat_provider.status()
if status.error_message:
return SimpleStatus(
success=False,
message=f"Cconnection test failed: {status.error_message}",
)
return SimpleStatus(
success=True,
message="Connection test successful." f" Balance: {status.balance}.",
)
async def _credit_fiat_service_fee_wallet(
payment: Payment, conn: Connection | None = None
):
@@ -347,3 +104,90 @@ async def _debit_fiat_service_faucet_wallet(
status=PaymentState.SUCCESS,
conn=conn,
)
async def handle_stripe_event(event: dict):
event_id = event.get("id")
event_object = event.get("data", {}).get("object", {})
object_type = event_object.get("object")
payment_hash = event_object.get("metadata", {}).get("payment_hash")
logger.debug(
f"Handling Stripe event: '{event_id}'. Type: '{object_type}'."
f" Payment hash: '{payment_hash}'."
)
if not payment_hash:
logger.warning("Stripe event does not contain a payment hash.")
return
payment = await get_standalone_payment(payment_hash)
if not payment:
logger.warning(f"No payment found for hash: '{payment_hash}'.")
return
await payment.check_fiat_status()
def check_stripe_signature(
payload: bytes,
sig_header: str | None,
secret: str | None,
tolerance_seconds=300,
):
if not sig_header:
logger.warning("Stripe-Signature header is missing.")
raise ValueError("Stripe-Signature header is missing.")
if not secret:
logger.warning("Stripe webhook signing secret is not set.")
raise ValueError("Stripe webhook cannot be verified.")
# Split the Stripe-Signature header
items = dict(i.split("=") for i in sig_header.split(","))
timestamp = int(items["t"])
signature = items["v1"]
# Check timestamp tolerance
if abs(time.time() - timestamp) > tolerance_seconds:
logger.warning("Timestamp outside tolerance.")
logger.debug(
f"Current time: {time.time()}, "
f"Timestamp: {timestamp}, "
f"Tolerance: {tolerance_seconds} seconds"
)
raise ValueError("Timestamp outside tolerance." f"Timestamp: {timestamp}")
signed_payload = f"{timestamp}.{payload.decode()}"
# Compute HMAC SHA256 using the webhook secret
computed_signature = hmac.new(
key=secret.encode(), msg=signed_payload.encode(), digestmod=hashlib.sha256
).hexdigest()
# Compare signatures using constant time comparison
if hmac.compare_digest(computed_signature, signature) is not True:
logger.warning("Stripe signature verification failed.")
raise ValueError("Stripe signature verification failed.")
async def test_connection(provider: str) -> SimpleStatus:
"""
Test the connection to Stripe by checking if the API key is valid.
This function should be called when setting up or testing the Stripe integration.
"""
fiat_provider = await get_fiat_provider(provider)
if not fiat_provider:
return SimpleStatus(
success=False,
message=f"Fiat provider '{provider}' not found.",
)
status = await fiat_provider.status()
if status.error_message:
return SimpleStatus(
success=False,
message=f"Cconnection test failed: {status.error_message}",
)
return SimpleStatus(
success=True,
message="Connection test successful." f" Balance: {status.balance}.",
)
+1 -2
View File
@@ -70,8 +70,7 @@ async def check_balance_delta_changed():
if settings.latest_balance_delta_sats is None:
settings.latest_balance_delta_sats = status.delta_sats
return
delta_change = abs(status.delta_sats - settings.latest_balance_delta_sats)
if delta_change >= settings.notification_balance_delta_threshold_sats:
if status.delta_sats != settings.latest_balance_delta_sats:
enqueue_admin_notification(
NotificationType.balance_delta,
{
+1 -20
View File
@@ -1,7 +1,6 @@
import asyncio
import json
import smtplib
from asyncio.tasks import create_task
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from http import HTTPStatus
@@ -17,7 +16,6 @@ from lnbits.core.crud import (
mark_webhook_sent,
)
from lnbits.core.crud.users import get_user
from lnbits.core.crud.wallets import get_wallet
from lnbits.core.models import Payment, Wallet
from lnbits.core.models.notifications import (
NOTIFICATION_TEMPLATES,
@@ -242,10 +240,6 @@ async def dispatch_webhook(payment: Payment):
async with httpx.AsyncClient(headers=headers) as client:
try:
check_callback_url(payment.webhook)
except ValueError as exc:
await mark_webhook_sent(payment.payment_hash, "-1")
logger.warning(f"Invalid webhook URL {payment.webhook}: {exc!s}")
try:
r = await client.post(payment.webhook, json=payment.json(), timeout=40)
r.raise_for_status()
await mark_webhook_sent(payment.payment_hash, str(r.status_code))
@@ -263,12 +257,6 @@ async def dispatch_webhook(payment: Payment):
async def send_payment_notification(wallet: Wallet, payment: Payment):
try:
await send_ws_payment_notification(wallet, payment)
for shared in wallet.extra.shared_with:
if not shared.shared_with_wallet_id:
continue
shared_wallet = await get_wallet(shared.shared_with_wallet_id)
if shared_wallet and shared_wallet.can_view_payments:
await send_ws_payment_notification(shared_wallet, payment)
except Exception as e:
logger.error(f"Error sending websocket payment notification {e!s}")
try:
@@ -287,13 +275,6 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
logger.error(f"Error dispatching webhook: {e!s}")
def send_payment_notification_in_background(wallet: Wallet, payment: Payment):
try:
create_task(send_payment_notification(wallet, payment))
except Exception as e:
logger.warning(f"Error sending payment notification: {e}")
async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
# TODO: websocket message should be a clean payment model
# await websocket_manager.send(wallet.inkey, payment.json())
@@ -400,7 +381,7 @@ def _is_message_type_enabled(message_type: NotificationType) -> bool:
if message_type == NotificationType.watchdog_check:
return settings.lnbits_notification_watchdog
if message_type == NotificationType.balance_delta:
return settings.notification_balance_delta_threshold_sats > 0
return settings.notification_balance_delta_changed
if message_type == NotificationType.server_start_stop:
return settings.lnbits_notification_server_start_stop
if message_type == NotificationType.server_status:
+32 -130
View File
@@ -13,19 +13,18 @@ from lnbits.core.crud.payments import get_daily_stats
from lnbits.core.db import db
from lnbits.core.models import PaymentDailyStats, PaymentFilters
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.decorators import check_user_extension_access
from lnbits.exceptions import InvoiceError, PaymentError, UnsupportedError
from lnbits.fiat import get_fiat_provider
from lnbits.helpers import check_callback_url
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.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
from lnbits.wallets import fake_wallet, get_funding_source
from lnbits.wallets.base import (
InvoiceResponse,
PaymentFailedStatus,
PaymentPendingStatus,
PaymentResponse,
PaymentStatus,
@@ -48,8 +47,7 @@ from ..models import (
PaymentState,
Wallet,
)
from .fiat_providers import check_fiat_status
from .notifications import send_payment_notification_in_background
from .notifications import send_payment_notification
payment_lock = asyncio.Lock()
wallets_payments_lock: dict[str, asyncio.Lock] = {}
@@ -63,14 +61,11 @@ async def pay_invoice(
extra: dict | None = None,
description: str = "",
tag: str = "",
labels: list[str] | None = None,
external_id: str | None = None,
conn: Connection | None = None,
) -> Payment:
if settings.lnbits_only_allow_incoming_payments:
raise PaymentError("Only incoming payments allowed.", status="failed")
invoice = _validate_payment_request(payment_request, max_sat)
if not invoice.amount_msat:
raise ValueError("Missig invoice amount.")
@@ -78,35 +73,25 @@ async def pay_invoice(
amount_msat = invoice.amount_msat
wallet = await _check_wallet_for_payment(wallet_id, tag, amount_msat, new_conn)
if not wallet.can_send_payments:
raise PaymentError(
"Wallet does not have permission to pay invoices.",
status="failed",
)
if await is_internal_status_success(invoice.payment_hash, new_conn):
raise PaymentError("Internal invoice already paid.", status="failed")
_, extra = await calculate_fiat_amounts(amount_msat / 1000, wallet, extra=extra)
create_payment_model = CreatePayment(
wallet_id=wallet.source_wallet_id,
wallet_id=wallet_id,
bolt11=payment_request,
payment_hash=invoice.payment_hash,
amount_msat=-amount_msat,
expiry=invoice.expiry_date,
memo=description or invoice.description or "",
extra=extra,
labels=labels,
external_id=external_id,
)
payment = await _pay_invoice(wallet.id, create_payment_model, conn)
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
payment = await _pay_invoice(
wallet.source_wallet_id, create_payment_model, conn=new_conn
)
await _credit_service_fee_wallet(wallet, payment, conn=new_conn)
await _credit_service_fee_wallet(wallet, payment, new_conn)
return payment
@@ -125,7 +110,7 @@ async def create_payment_request(
async def create_fiat_invoice(
wallet_id: str, invoice_data: CreateInvoice, conn: Connection | None = None
) -> Payment:
):
fiat_provider_name = invoice_data.fiat_provider
if not fiat_provider_name:
raise ValueError("Fiat provider is required for fiat invoices.")
@@ -205,23 +190,19 @@ async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
# do not save memo if description_hash or unhashed_description is set
memo = ""
async with db.connect() as conn:
payment = await create_invoice(
wallet_id=wallet_id,
amount=data.amount,
memo=memo,
currency=data.unit,
description_hash=description_hash,
unhashed_description=unhashed_description,
expiry=data.expiry,
extra=data.extra,
webhook=data.webhook,
internal=data.internal,
payment_hash=data.payment_hash,
labels=data.labels,
external_id=data.external_id,
conn=conn,
)
payment = await create_invoice(
wallet_id=wallet_id,
amount=data.amount,
memo=memo,
currency=data.unit,
description_hash=description_hash,
unhashed_description=unhashed_description,
expiry=data.expiry,
extra=data.extra,
webhook=data.webhook,
internal=data.internal,
payment_hash=data.payment_hash,
)
if data.lnurl_withdraw:
try:
@@ -260,8 +241,6 @@ async def create_invoice(
webhook: str | None = None,
internal: bool | None = False,
payment_hash: str | None = None,
labels: list[str] | None = None,
external_id: str | None = None,
conn: Connection | None = None,
) -> Payment:
if not amount > 0:
@@ -271,12 +250,6 @@ async def create_invoice(
if not user_wallet:
raise InvoiceError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
if not user_wallet.can_receive_payments:
raise InvoiceError(
"Wallet does not have permission to create invoices.",
status="failed",
)
invoice_memo = None if description_hash else memo[:640]
# use the fake wallet if the invoice is for internal use only
@@ -335,7 +308,7 @@ async def create_invoice(
invoice = bolt11_decode(invoice_response.payment_request)
create_payment_model = CreatePayment(
wallet_id=user_wallet.source_wallet_id,
wallet_id=wallet_id,
bolt11=invoice_response.payment_request,
payment_hash=invoice.payment_hash,
preimage=invoice_response.preimage,
@@ -345,8 +318,6 @@ async def create_invoice(
extra=extra,
webhook=webhook,
fee=invoice_response.fee_msat or 0,
labels=labels,
external_id=external_id,
)
payment = await create_payment(
@@ -368,15 +339,13 @@ async def update_pending_payments(wallet_id: str):
await update_pending_payment(payment)
async def update_pending_payment(
payment: Payment, conn: Connection | None = None
) -> Payment:
status = await check_payment_status(payment)
async def update_pending_payment(payment: Payment) -> Payment:
status = await payment.check_status()
if status.failed:
payment.status = PaymentState.FAILED
await update_payment(payment, conn=conn)
await update_payment(payment)
elif status.success:
payment = await update_payment_success_status(payment, status, conn=conn)
payment = await update_payment_success_status(payment, status)
return payment
@@ -487,7 +456,7 @@ async def update_wallet_balance(
await create_payment(
checking_id=f"internal_{payment_hash}",
data=CreatePayment(
wallet_id=wallet.source_wallet_id,
wallet_id=wallet.id,
bolt11=bolt11,
payment_hash=payment_hash,
amount_msat=amount * 1000,
@@ -506,7 +475,7 @@ async def update_wallet_balance(
raise ValueError("Balance change failed, amount exceeds maximum balance.")
async with db.reuse_conn(conn) if conn else db.connect() as conn:
payment = await create_invoice(
wallet_id=wallet.source_wallet_id,
wallet_id=wallet.id,
amount=amount,
memo="Admin credit",
internal=True,
@@ -514,8 +483,6 @@ async def update_wallet_balance(
)
payment.status = PaymentState.SUCCESS
await update_payment(payment, conn=conn)
from lnbits.tasks import internal_invoice_queue_put
await internal_invoice_queue_put(payment.checking_id)
@@ -627,29 +594,7 @@ async def check_transaction_status(
if payment.status == PaymentState.SUCCESS.value:
return PaymentSuccessStatus(fee_msat=payment.fee)
return await check_payment_status(payment)
async def check_payment_status(
payment: Payment, skip_internal_payment_notifications: bool | None = False
) -> PaymentStatus:
if payment.is_internal:
if payment.success:
return PaymentSuccessStatus()
if payment.failed:
return PaymentFailedStatus()
if payment.is_in and payment.fiat_provider:
fiat_status = await check_fiat_status(
payment, skip_internal_payment_notifications
)
return PaymentStatus(paid=fiat_status.paid)
return PaymentPendingStatus()
funding_source = get_funding_source()
if payment.is_out:
status = await funding_source.get_payment_status(payment.checking_id)
else:
status = await funding_source.get_invoice_status(payment.checking_id)
return status
return await payment.check_status()
async def get_payments_daily_stats(
@@ -737,7 +682,6 @@ async def _pay_internal_invoice(
internal_payment = await check_internal(
create_payment_model.payment_hash, conn=conn
)
if not internal_payment:
return None
@@ -746,7 +690,6 @@ async def _pay_internal_invoice(
internal_invoice = await get_standalone_payment(
internal_payment.checking_id, incoming=True, conn=conn
)
if not internal_invoice:
raise PaymentError("Internal payment not found.", status="failed")
@@ -768,7 +711,6 @@ async def _pay_internal_invoice(
internal_id = f"internal_{create_payment_model.payment_hash}"
logger.debug(f"creating temporary internal payment with id {internal_id}")
payment = await create_payment(
checking_id=internal_id,
data=create_payment_model,
@@ -783,7 +725,7 @@ async def _pay_internal_invoice(
await update_payment(internal_payment, conn=conn)
logger.success(f"internal payment successful {internal_payment.checking_id}")
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
await send_payment_notification(wallet, payment)
# notify receiver asynchronously
from lnbits.tasks import internal_invoice_queue
@@ -826,8 +768,6 @@ async def _pay_external_invoice(
fee_reserve_msat = fee_reserve(amount_msat, internal=False)
from lnbits.tasks import create_task
task = create_task(
_fundingsource_pay_invoice(checking_id, payment.bolt11, fee_reserve_msat)
)
@@ -858,8 +798,7 @@ async def _pay_external_invoice(
payment = await update_payment_success_status(
payment, payment_response, conn=conn
)
await _send_payment_notification_in_background(wallet.id, payment, conn=conn)
await send_payment_notification(wallet, payment)
logger.success(f"payment successful {payment_response.checking_id}")
payment.checking_id = payment_response.checking_id
@@ -902,7 +841,7 @@ async def _verify_external_payment(
raise PaymentError("Payment already paid.", status="success")
# payment failed
status = await check_payment_status(payment)
status = await payment.check_status()
if status.failed:
raise PaymentError(
"Payment is failed node, retrying is not possible.", status="failed"
@@ -971,7 +910,7 @@ async def _credit_service_fee_wallet(
memo = f"""
Service fee for payment of {abs(payment.sat)} sats.
Wallet: '{wallet.name}' ({wallet.source_wallet_id})."""
Wallet: '{wallet.name}' ({wallet.id})."""
create_payment_model = CreatePayment(
wallet_id=settings.lnbits_service_fee_wallet,
@@ -1067,40 +1006,3 @@ async def cancel_hold_invoice(payment: Payment) -> InvoiceResponse:
await update_payment(payment)
return response
async def _send_payment_notification_in_background(
wallet_id: str, payment: Payment, conn: Connection | None = None
):
# fetch balance again
wallet = await get_wallet(wallet_id, conn=conn)
if not wallet:
raise PaymentError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
send_payment_notification_in_background(wallet, payment)
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
+23 -73
View File
@@ -3,11 +3,7 @@ from uuid import uuid4
from loguru import logger
from lnbits.core.crud.settings import set_settings_field
from lnbits.core.db import db
from lnbits.core.models.extensions import UserExtension
from lnbits.core.models.users import RegisterUser
from lnbits.db import Connection
from lnbits.settings import (
EditableSettings,
SuperSettings,
@@ -23,7 +19,6 @@ from ..crud import (
get_account_by_email,
get_account_by_pubkey,
get_account_by_username,
get_accounts_count,
get_super_settings,
get_user_extensions,
get_user_from_account,
@@ -53,60 +48,43 @@ async def create_user_account_no_ckeck(
account: Account | None = None,
wallet_name: str | None = None,
default_exts: list[str] | None = None,
conn: Connection | None = None,
) -> User:
async with db.reuse_conn(conn) if conn else db.connect() as conn:
await check_users_limit(conn)
if account:
account.validate_fields()
if account.username and await get_account_by_username(
account.username, conn=conn
):
raise ValueError("Username already exists.")
if account:
account.validate_fields()
if account.username and await get_account_by_username(account.username):
raise ValueError("Username already exists.")
if account.email and await get_account_by_email(account.email, conn=conn):
raise ValueError("Email already exists.")
if account.email and await get_account_by_email(account.email):
raise ValueError("Email already exists.")
if account.pubkey and await get_account_by_pubkey(
account.pubkey, conn=conn
):
raise ValueError("Pubkey already exists.")
if account.pubkey and await get_account_by_pubkey(account.pubkey):
raise ValueError("Pubkey already exists.")
if not account.id:
account.id = uuid4().hex
if not account.id:
account.id = uuid4().hex
account = await create_account(account, conn=conn)
await create_wallet(
user_id=account.id,
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
conn=conn,
)
account = await create_account(account)
await create_wallet(
user_id=account.id,
wallet_name=wallet_name or settings.lnbits_default_wallet_name,
)
user_extensions = (default_exts or []) + settings.lnbits_user_default_extensions
for ext_id in user_extensions:
try:
user_ext = UserExtension(user=account.id, extension=ext_id, active=True)
await create_user_extension(user_ext, conn=conn)
except Exception as e:
logger.error(f"Error enabeling default extension {ext_id}: {e}")
user_extensions = (default_exts or []) + settings.lnbits_user_default_extensions
for ext_id in user_extensions:
try:
user_ext = UserExtension(user=account.id, extension=ext_id, active=True)
await create_user_extension(user_ext)
except Exception as e:
logger.error(f"Error enabeling default extension {ext_id}: {e}")
user = await get_user_from_account(account, conn=conn)
user = await get_user_from_account(account)
if not user:
raise ValueError("Cannot find user for account.")
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:
account.validate_fields()
@@ -185,12 +163,6 @@ async def check_admin_settings():
if account and account.extra and account.extra.provider == "env":
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(
"✔️ Admin UI is enabled. run `uv run lnbits-cli superuser` "
"to get the superuser."
@@ -212,25 +184,3 @@ async def init_admin_settings(super_user: str | None = None) -> SuperSettings:
editable_settings = EditableSettings.from_dict(settings.dict())
return await create_admin_settings(account.id, editable_settings.dict())
async def check_register_activation_settings(data: RegisterUser):
if not settings.lnbits_require_user_activation:
return None
if settings.lnbits_user_activation_by_invitation_code:
code = data.invitation_code.strip() if data.invitation_code else ""
if len(code) == 0:
raise ValueError("Invitation code cannot be empty.")
if code == settings.lnbits_register_reusable_activation_code:
return None
if code in settings.lnbits_register_one_time_activation_codes:
settings.lnbits_register_one_time_activation_codes.remove(code)
await set_settings_field(
"lnbits_register_one_time_activation_codes",
settings.lnbits_register_one_time_activation_codes,
)
return None
raise ValueError("Invalid invitation code.")
raise ValueError("No activation method provided.")
-202
View File
@@ -1,202 +0,0 @@
from lnbits.core.crud.users import (
get_account,
get_account_by_username_or_email,
update_account,
)
from lnbits.core.crud.wallets import (
create_wallet,
force_delete_wallet,
get_standalone_wallet,
get_wallet,
get_wallets,
update_wallet,
)
from lnbits.core.models.misc import SimpleStatus
from lnbits.core.models.users import Account
from lnbits.core.models.wallets import (
Wallet,
WalletSharePermission,
WalletShareStatus,
WalletType,
)
from lnbits.db import Connection
from lnbits.helpers import sha256s
async def invite_to_wallet(
source_wallet: Wallet, data: WalletSharePermission
) -> WalletSharePermission:
if not source_wallet.is_lightning_wallet:
raise ValueError("Only lightning wallets can be shared.")
if not data.username:
raise ValueError("Username or email missing.")
invited_user = await get_account_by_username_or_email(data.username)
if not invited_user:
raise ValueError("Invited user not found.")
request_id = sha256s(invited_user.id + source_wallet.id)
share = source_wallet.extra.find_share_by_id(request_id)
if share:
raise ValueError("User already invited to this wallet.")
invite_request = source_wallet.extra.invite_user_to_shared_wallet(
request_id=request_id,
request_type=WalletShareStatus.INVITE_SENT,
username=data.username,
permissions=data.permissions,
)
await update_wallet(source_wallet)
wallet_owner = await get_account(source_wallet.user)
if not wallet_owner:
raise ValueError("Cannot find wallet owner.")
invited_user.extra.add_wallet_invite_request(
request_id=request_id,
from_user_name=wallet_owner.username or wallet_owner.email,
to_wallet_id=source_wallet.id,
to_wallet_name=source_wallet.name,
)
await update_account(invited_user)
return invite_request
async def reject_wallet_invitation(invited_user_id: str, share_request_id: str):
invited_user = await get_account(invited_user_id)
if not invited_user:
raise ValueError("Invited user not found.")
existing_request = invited_user.extra.find_wallet_invite_request(share_request_id)
if not existing_request:
raise ValueError("Invitation not found.")
invited_user.extra.remove_wallet_invite_request(share_request_id)
await update_account(invited_user)
async def update_wallet_share_permissions(
source_wallet: Wallet, data: WalletSharePermission
) -> WalletSharePermission:
if not source_wallet.is_lightning_wallet:
raise ValueError("Only lightning wallets can be shared.")
if not data.shared_with_wallet_id:
raise ValueError("Wallet ID missing.")
share = source_wallet.extra.find_share_for_wallet(data.shared_with_wallet_id)
if not share:
raise ValueError("Share not found")
if not share.shared_with_wallet_id:
raise ValueError("Share does not have a mirror wallet ID.")
mirror_wallet = await get_wallet(share.shared_with_wallet_id)
if not mirror_wallet:
raise ValueError("Target wallet not found")
if not mirror_wallet.is_lightning_shared_wallet:
raise ValueError("Target wallet is not a shared wallet.")
if mirror_wallet.shared_wallet_id != source_wallet.id:
raise ValueError("Not the owner of the shared wallet.")
share.approve(permissions=data.permissions)
await update_wallet(source_wallet)
return share
async def delete_wallet_share(source_wallet: Wallet, request_id: str) -> SimpleStatus:
if not source_wallet.is_lightning_wallet:
raise ValueError("Source wallet is not a lightning wallet.")
share = source_wallet.extra.find_share_by_id(request_id)
if not share:
raise ValueError("Wallet share not found.")
source_wallet.extra.remove_share_by_id(request_id)
invited_user = await get_account_by_username_or_email(share.username)
if not invited_user:
await update_wallet(source_wallet)
return SimpleStatus(
success=True, message="Permission removed. Invited user not found."
)
if invited_user.extra.find_wallet_invite_request(request_id):
invited_user.extra.remove_wallet_invite_request(request_id)
await update_account(invited_user)
mirror_wallets = await get_wallets(
invited_user.id, wallet_type=WalletType.LIGHTNING_SHARED
)
mirror_wallet = next(
(w for w in mirror_wallets if w.shared_wallet_id == source_wallet.id), None
)
if not mirror_wallet:
await update_wallet(source_wallet)
return SimpleStatus(
success=True, message="Permission removed. Target wallet not found."
)
if not mirror_wallet.is_lightning_shared_wallet:
raise ValueError("Target wallet is not a shared lightning wallet.")
if mirror_wallet.shared_wallet_id != source_wallet.id:
raise ValueError("Not the owner of the shared wallet.")
await force_delete_wallet(mirror_wallet.id)
await update_wallet(source_wallet)
return SimpleStatus(success=True, message="Permission removed.")
async def create_lightning_shared_wallet(
user_id: str,
source_wallet_id: str,
conn: Connection | None = None,
) -> Wallet:
source_wallet = await get_standalone_wallet(source_wallet_id, conn=conn)
if not source_wallet:
raise ValueError("Shared wallet does not exist.")
if not source_wallet.is_lightning_wallet:
raise ValueError("Shared wallet is not a lightning wallet.")
if source_wallet.user == user_id:
raise ValueError("Cannot mirror your own wallet.")
invited_user = await get_account(user_id, conn=conn)
if not invited_user:
raise ValueError("Cannot find invited user.")
return await _accept_invitation_to_shared_wallet(
invited_user, source_wallet, conn=conn
)
async def _accept_invitation_to_shared_wallet(
invited_user: Account,
source_wallet: Wallet,
conn: Connection | None = None,
) -> Wallet:
request_id = sha256s(invited_user.id + source_wallet.id)
existing_request = source_wallet.extra.find_share_by_id(request_id)
if not existing_request:
raise ValueError("No invitation found for this invited user.")
if existing_request.status == WalletShareStatus.APPROVED:
raise ValueError("This wallet is already shared with you.")
if existing_request.status != WalletShareStatus.INVITE_SENT:
raise ValueError("Unknown request type.")
invited_user.extra.remove_wallet_invite_request(request_id)
await update_account(invited_user)
# todo: double check if user already has a mirror wallet for this source wallet
mirror_wallet = await create_wallet(
user_id=invited_user.id,
wallet_name=source_wallet.name,
wallet_type=WalletType.LIGHTNING_SHARED,
shared_wallet_id=source_wallet.id,
conn=conn,
)
existing_request.approve(shared_with_wallet_id=mirror_wallet.id)
await update_wallet(source_wallet, conn=conn)
mirror_wallet.mirror_shared_wallet(source_wallet)
return mirror_wallet
+16 -4
View File
@@ -1,4 +1,6 @@
import asyncio
import traceback
from collections.abc import Callable, Coroutine
from loguru import logger
@@ -25,6 +27,7 @@ from lnbits.core.services.notifications import (
)
from lnbits.db import Filters
from lnbits.settings import settings
from lnbits.tasks import create_unique_task
from lnbits.utils.exchange_rates import btc_rates
audit_queue: asyncio.Queue[AuditEntry] = asyncio.Queue()
@@ -35,7 +38,7 @@ async def run_by_the_minute_tasks() -> None:
while settings.lnbits_running:
status_minutes = settings.lnbits_notification_server_status_hours * 60
if settings.notification_balance_delta_threshold_sats > 0:
if settings.notification_balance_delta_changed:
try:
# runs by default every minute, the delta should not change that often
await check_balance_delta_changed()
@@ -57,9 +60,7 @@ async def run_by_the_minute_tasks() -> None:
if minute_counter % 60 == 0:
try:
# initialize the list of all extensions
await InstallableExtension.get_installable_extensions(
post_refresh_cache=True
)
await InstallableExtension.get_installable_extensions()
except Exception as ex:
logger.error(ex)
@@ -161,3 +162,14 @@ async def collect_exchange_rates_data() -> None:
else:
sleep_time = 60
await asyncio.sleep(sleep_time)
def _create_unique_task(name: str, func: Callable):
async def _to_coro(func: Callable[[], Coroutine]) -> Coroutine:
return await func()
try:
create_unique_task(name, _to_coro(func))
except Exception as e:
logger.error(f"Error in {name} task", e)
logger.error(traceback.format_exc())
@@ -1,6 +1,7 @@
<template id="lnbits-admin-audit">
<q-tab-panel name="audit">
<q-card-section class="q-pa-none">
<h6 class="q-my-none q-mb-sm">Audit</h6>
<div class="row q-mb-lg">
<div class="col-md-6 col-sm-12 q-pr-sm">
<q-item tag="label" v-ripple>
@@ -32,7 +33,7 @@
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<div class="row">
<div class="col-md-6 col-sm-12 q-pr-sm">
<div class="col-md-3 col-sm-12 q-pr-sm">
<q-item tag="label" v-ripple>
<q-item-section avatar>
<q-toggle
@@ -49,12 +50,8 @@
<span v-text="$t('audit_record_warning')"></span>
<br />
<ul>
<li>
<span v-text="$t('audit_record_req_warning_1')"></span>
</li>
<li>
<span v-text="$t('audit_record_req_warning_2')"></span>
</li>
<li><span v-text="$t('audit_record_req_warning_1')"></span></li>
<li><span v-text="$t('audit_record_req_warning_2')"></span></li>
</ul>
<br />
<span v-text="$t('audit_record_use')"></span>
@@ -62,7 +59,7 @@
</q-item-section>
</q-item>
</div>
<div class="col-md-6 col-sm-12 q-pr-sm">
<div class="col-md-3 col-sm-12 q-pr-sm">
<q-item tag="label" v-ripple>
<q-item-section avatar>
<q-toggle
@@ -83,7 +80,7 @@
</q-item-section>
</q-item>
</div>
<div class="col-md-6 col-sm-12 q-pr-sm">
<div class="col-md-3 col-sm-12 q-pr-sm">
<q-item tag="label" v-ripple>
<q-item-section avatar>
<q-toggle
@@ -104,7 +101,7 @@
</q-item-section>
</q-item>
</div>
<div class="col-md-6 col-sm-12 q-pr-sm">
<div class="col-md-3 col-sm-12 q-pr-sm">
<q-item tag="label" v-ripple>
<q-item-section avatar>
<q-toggle
@@ -129,24 +126,18 @@
<q-separator class="q-mb-xl q-mt-sm"></q-separator>
<div class="row q-mb-lg">
<div class="col-md-6 col-sm-12 q-pr-sm">
<p><span v-text="$t('audit_http_methods')"></span></p>
<q-select
filled
v-model="formData.lnbits_audit_http_methods"
multiple
:hint="$t('audit_http_methods_hint')"
:label="$t('audit_http_methods_label')"
:options="[
'GET',
'POST',
'PUT',
'PATCH',
'DELETE',
'HEAD',
'OPTIONS'
]"
:options="['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']"
></q-select>
</div>
<div class="col-md-6 col-sm-12 q-pr-sm">
<p><span v-text="$t('audit_resp_codes')"></span></p>
<q-input
class="q-mb-md"
filled
@@ -175,6 +166,7 @@
<div class="row">
<div class="col-md-6 col-sm-12 q-pr-sm">
<p><span v-text="$t('audit_paths')"></span></p>
<q-input
class="q-mb-md"
filled
@@ -201,6 +193,9 @@
<br />
</div>
<div class="col-md-6 col-sm-12">
<p>
<span v-text="$t('audit_paths_exclude')"></span>
</p>
<q-input
class="q-mb-md"
filled
@@ -230,4 +225,4 @@
</div>
</div>
</q-card-section>
</template>
</q-tab-panel>
@@ -0,0 +1,197 @@
<q-tab-panel name="exchange_providers">
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('exchange_providers')"></span>
</h6>
<div class="row">
<div class="col-md-8 col-sm-12">
<div class="q-pa-sm">
<canvas
style="
width: 100% !important;
height: auto !important;
min-height: 350px;
max-height: 50vh;
"
ref="exchangeRatesChart"
></canvas>
</div>
</div>
<div class="col-md-4 col-sm-12">
<q-input
filled
v-model="formData.lnbits_exchange_history_refresh_interval_seconds"
type="number"
label="Refresh Interval (seconds)"
hint="How often should the exchange rates be fetched. Set to zero to disable."
>
</q-input>
<q-input
filled
v-model="formData.lnbits_exchange_history_size"
type="number"
label="History Size"
hint="How many data points should be kept in memory."
>
</q-input>
<ul>
<li>
<code>Refresh Interval</code> and <code> History Size </code>are for
historical purposes only.
</li>
<li>These two settings do not affect the live price computation.</li>
<li>
Chart currency:
<strong
><span
v-text="formData.lnbits_default_accounting_currency || 'USD'"
></span
></strong>
</li>
</ul>
</div>
</div>
<div class="row q-mt-md">
<div class="col-6">
<q-btn
@click="addExchangeProvider()"
label="Add Exchange Provider"
color="primary"
class="q-mb-md"
>
</q-btn>
</div>
<div class="col-6">
<q-btn
@click="getDefaultSetting('lnbits_exchange_rate_providers')"
flat
:label="$t('reset_defaults')"
color="primary"
class="float-right"
>
</q-btn>
</div>
</div>
<q-table
row-key="name"
:rows="formData.lnbits_exchange_rate_providers"
:columns="exchangesTable.columns"
v-model:pagination="exchangesTable.pagination"
>
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
<span v-text="col.label"></span>
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td>
<q-btn
@click="removeExchangeProvider(props.row)"
round
icon="delete"
size="sm"
color="negative"
class="q-ml-xs"
>
</q-btn>
</q-td>
<q-td>
<q-input
dense
filled
v-model="props.row.name"
@update:model-value="touchSettings()"
type="text"
>
</q-input>
</q-td>
<q-td full-width>
<q-input
dense
filled
v-model="props.row.api_url"
@update:model-value="touchSettings()"
type="text"
>
</q-input
></q-td>
<q-td>
<q-input
dense
filled
v-model="props.row.path"
@update:model-value="touchSettings()"
type="text"
>
</q-input>
</q-td>
<q-td>
<q-select
filled
dense
v-model="props.row.exclude_to"
@update:model-value="touchSettings()"
multiple
:options="{{ currencies | safe }}"
></q-select>
</q-td>
<q-td>
<q-btn
@click="showTickerConversionDialog(props.row)"
round
icon="add"
size="sm"
color="gray"
class="q-ml-xs"
>
</q-btn>
<q-chip
v-for="ticker, index in props.row.ticker_conversion"
:key="ticker"
removable
dense
filled
@remove="removeExchangeTickerConversion(props.row, ticker)"
color="primary"
text-color="white"
:label="ticker"
class="ellipsis"
>
</q-chip>
</q-td>
</q-tr>
</template>
</q-table>
<ul>
<li>
<code>API URL</code> and <code>JSON Path</code> fields can use the
<code>{to}</code> and <code>{TO}</code> placeholders for the code of the
currency
</li>
<li>
<code>{TO}</code> is the uppercase code and <code>{to}</code> is the
lowercase code
</li>
</ul>
<q-separator class="q-ma-md"></q-separator>
<div class="row">
<div class="col-md-4 col-sm-12">
<q-input
filled
v-model="formData.lnbits_exchange_rate_cache_seconds"
type="number"
label="Exchange rate cache (seconds)"
hint="For how many seconds should the exchange rate be cached."
>
</q-input>
</div>
<div class="col-md-8 col-sm-12"></div>
</div>
</q-tab-panel>
@@ -1,4 +1,4 @@
<template id="lnbits-admin-extensions">
<q-tab-panel name="extensions">
<q-card-section class="q-pa-none">
<div>
<h6 class="q-my-none">
@@ -137,20 +137,7 @@
:hint="$t('extension_builder_manifest_url_hint')"
></q-input>
</div>
<div class="col-12 col-md-6 q-pb-md">
<p>
<span v-text="$t('reviews_url')"></span>
</p>
<q-input
filled
v-model="formData.lnbits_extensions_reviews_url"
:label="$t('reviews_url_label')"
:hint="$t('reviews_url_hint')"
type="url"
autocomplete="off"
></q-input>
</div>
</div>
</div>
</q-card-section>
</template>
</q-tab-panel>
@@ -0,0 +1,287 @@
<q-tab-panel name="fiat_providers">
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('fiat_providers')"></span>
<q-btn
round
flat
@click="hideInputsToggle()"
:icon="hideInputToggle ? 'visibility_off' : 'visibility'"
></q-btn>
</h6>
<div class="row">
<div class="col">
<q-list bordered class="rounded-borders">
<q-expansion-item header-class="text-primary text-bold">
<template v-slot:header>
<q-item-section avatar>
<q-avatar>
<img
:src="'{{ static_url_for('static', 'images/stripe_logo.ico') }}'"
/>
</q-avatar>
</q-item-section>
<q-item-section> Stripe </q-item-section>
<q-item-section side>
<div class="row items-center">
<q-toggle
size="md"
:label="$t('enabled')"
v-model="formData.stripe_enabled"
color="green"
unchecked-icon="clear"
/>
</div>
</q-item-section>
</template>
<q-card class="q-pb-xl">
<q-expansion-item :label="$t('api')" default-opened>
<q-card-section class="q-pa-md">
<q-input
filled
type="text"
v-model="formData.stripe_api_endpoint"
:label="$t('endpoint')"
></q-input>
<q-input
filled
class="q-mt-md"
:type="hideInputToggle ? 'password' : 'text'"
v-model="formData.stripe_api_secret_key"
:label="$t('secret_key')"
></q-input>
<q-input
filled
class="q-mt-md"
type="text"
v-model="formData.stripe_payment_success_url"
:label="$t('callback_success_url')"
:hint="$t('callback_success_url_hint')"
></q-input>
</q-card-section>
<q-card-section class="q-pa-md">
<div class="row">
<div class="col">
<q-btn
outline
color="grey"
class="float-right"
:label="$t('check_connection')"
@click="checkFiatProvider('stripe')"
></q-btn>
</div>
</div>
</q-card-section>
</q-expansion-item>
<q-expansion-item :label="$t('webhook')" default-opened>
<q-card-section>
<span v-text="$t('webhook_stripe_description')"></span>
</q-card-section>
<q-card-section>
<q-input
filled
class="q-mt-md"
type="text"
disable
v-model="formData.stripe_payment_webhook_url"
:label="$t('webhook_url')"
:hint="$t('webhook_url_hint')"
></q-input>
<q-input
filled
class="q-mt-md"
:type="hideInputToggle ? 'password' : 'text'"
v-model="formData.stripe_webhook_signing_secret"
:label="$t('signing_secret')"
:hint="$t('signing_secret_hint')"
></q-input>
</q-card-section>
<q-card-section>
<span v-text="$t('webhook_events_list')"></span>
<ul>
<li><code>checkout.session.async_payment_failed</code></li>
<li><code>checkout.session.async_payment_succeeded</code></li>
<li><code>checkout.session.completed</code></li>
<li><code>checkout.session.expired</code></li>
</ul>
</q-card-section>
</q-expansion-item>
<q-expansion-item :label="$t('service_fee')">
<q-card-section>
<div class="row">
<div class="col-md-4 col-sm-12">
<q-input
filled
class="q-ma-sm"
type="number"
min="0"
v-model="formData.stripe_limits.service_fee_percent"
@update:model-value="touchSettings()"
:label="$t('service_fee_label')"
:hint="$t('service_fee_hint')"
></q-input>
</div>
<div class="col-md-4 col-sm-12">
<q-input
filled
class="q-ma-sm"
type="number"
min="0"
v-model="formData.stripe_limits.service_max_fee_sats"
@update:model-value="touchSettings()"
:label="$t('service_fee_max')"
:hint="$t('service_fee_max_hint')"
></q-input>
</div>
<div class="col-md-4 col-sm-12">
<q-input
filled
class="q-ma-sm"
type="text"
v-model="formData.stripe_limits.service_fee_wallet_id"
@update:model-value="touchSettings()"
:label="$t('fee_wallet_label')"
:hint="$t('fee_wallet_hint')"
></q-input>
</div>
</div>
</q-card-section>
</q-expansion-item>
<q-expansion-item :label="$t('amount_limits')">
<q-card-section>
<div class="row">
<div class="col-md-4 col-sm-12">
<q-input
filled
class="q-ma-sm"
type="number"
min="0"
v-model="formData.stripe_limits.service_min_amount_sats"
@update:model-value="touchSettings()"
:label="$t('min_incoming_payment_amount')"
:hint="$t('min_incoming_payment_amount_desc')"
></q-input>
</div>
<div class="col-md-4 col-sm-12">
<q-input
filled
class="q-ma-sm"
type="number"
min="0"
v-model="formData.stripe_limits.service_max_amount_sats"
@update:model-value="touchSettings()"
:label="$t('max_incoming_payment_amount')"
:hint="$t('max_incoming_payment_amount_desc')"
></q-input>
</div>
<div class="col-md-4 col-sm-12">
<q-input
filled
class="q-ma-sm"
v-model="formData.stripe_limits.service_faucet_wallet_id"
@update:model-value="touchSettings()"
:label="$t('faucest_wallet_id')"
:hint="$t('faucest_wallet_id_hint')"
></q-input>
</div>
</div>
<q-item>
<q-item-section>
<q-item-label v-text="$t('faucest_wallet')"></q-item-label>
<q-item-label caption>
<ul>
<li>
<span
v-text="$t('faucest_wallet_desc_1', {provider: 'stripe'})"
></span>
</li>
<li>
<span
v-text="$t('faucest_wallet_desc_2', {provider: 'stripe'})"
></span>
</li>
<li>
<span v-text="$t('faucest_wallet_desc_3')"></span>
</li>
<li>
<span
v-text="$t('faucest_wallet_desc_4', {provider: 'stripe'})"
></span>
</li>
<li>
<span v-text="$t('faucest_wallet_desc_5')"></span>
</li>
</ul>
<br />
</q-item-label>
</q-item-section>
</q-item>
</q-card-section>
</q-expansion-item>
<q-expansion-item :label="$t('allowed_users')">
<q-card-section>
<q-input
filled
v-model="formAddStripeUser"
@keydown.enter="addAllowedUser"
type="text"
:label="$t('allowed_users_label')"
:hint="$t('allowed_users_hint_feature', {feature: 'Stripe'})"
>
<q-btn
@click="addStripeAllowedUser"
dense
flat
icon="add"
></q-btn>
</q-input>
<div>
<q-chip
v-for="user in formData.stripe_limits.allowed_users"
@update:model-value="touchSettings()"
:key="user"
removable
@remove="removeStripeAllowedUser(user)"
color="primary"
text-color="white"
:label="user"
class="ellipsis"
>
</q-chip>
</div>
</q-card-section>
</q-expansion-item>
</q-card>
</q-expansion-item>
<q-separator />
<q-expansion-item header-class="text-primary text-bold">
<template v-slot:header>
<q-item-section avatar>
<q-avatar>
<img
:src="'{{ static_url_for('static', 'images/square_logo.png') }}'"
/>
</q-avatar>
</q-item-section>
<q-item-section> Square </q-item-section>
<q-item-section side>
<div class="row items-center">Disabled</div>
</q-item-section>
</template>
<q-card>
<q-card-section> Coming Soon </q-card-section>
</q-card>
</q-expansion-item>
</q-list>
</div>
</div>
</q-tab-panel>
@@ -0,0 +1,196 @@
<q-tab-panel name="funding">
<q-card-section class="q-pa-none">
<h6 class="q-my-none">
<span v-text="$t('wallets_management')"></span>
</h6>
<br />
<div>
<div class="row">
<div class="col">
<p>
<span v-text="$t('funding_source_info')"></span>
</p>
<ul>
<li
v-text="$t('funding_source', {wallet_class: settings.lnbits_backend_wallet_class})"
></li>
<li
v-text="$t('node_balance', {balance: (auditData.node_balance_sats || 0).toLocaleString()})"
></li>
<li
v-text="$t('lnbits_balance', {balance: (auditData.lnbits_balance_sats || 0).toLocaleString()})"
></li>
<li
v-text="$t('funding_reserve_percent', {
percent: auditData.lnbits_balance_sats > 0
? (auditData.node_balance_sats / auditData.lnbits_balance_sats * 100).toFixed(2)
: 100
})"
></li>
</ul>
<br />
</div>
<div class="col">
{% if LNBITS_NODE_UI_AVAILABLE %}
<p><span v-text="$t('node_management')"></span></p>
<q-toggle
:label="$t('toggle_node_ui')"
v-model="formData.lnbits_node_ui"
></q-toggle>
<q-toggle
v-if="formData.lnbits_node_ui"
:label="$t('toggle_public_node_ui')"
v-model="formData.lnbits_public_node_ui"
></q-toggle>
<br />
<q-toggle
v-if="formData.lnbits_node_ui"
:label="$t('toggle_transactions_node_ui')"
v-model="formData.lnbits_node_ui_transactions"
></q-toggle>
{% else %}
<p><span v-text="$t('node_management_not_supported')"></span></p>
{% endif %}
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-3">
<p><span v-text="$t('fee_reserve')"></span></p>
<q-input
type="number"
filled
v-model="formData.lnbits_reserve_fee_min"
:label="$t('fee_reserve_msats')"
>
</q-input>
</div>
<div class="col-12 col-md-3">
<p><span v-text="$t('fee_reserve_percent')"></span></p>
<q-input
type="number"
filled
name="lnbits_reserve_fee_percent"
v-model="formData.lnbits_reserve_fee_percent"
:label="$t('reserve_fee_in_percent')"
step="0.1"
></q-input>
</div>
<div class="col-12 col-md-3">
<p><span v-text="$t('invoice_expiry')"></span></p>
<q-input
filled
v-model.number="formData.lightning_invoice_expiry"
type="number"
:label="$t('invoice_expiry_label')"
mask="#######"
>
</q-input>
</div>
<div class="col-12 col-md-3">
<p><span v-text="$t('payment_wait_time')"></span></p>
<q-input
type="number"
filled
name="lnbits_funding_source_pay_invoice_wait_seconds"
v-model="formData.lnbits_funding_source_pay_invoice_wait_seconds"
:label="$t('payment_wait_time')"
:hint="$t('payment_wait_time_desc')"
step="1"
min="0"
></q-input>
</div>
</div>
<div v-if="isSuperUser">
<lnbits-funding-sources
:form-data="formData"
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
/>
</div>
<q-separator></q-separator>
<h6 class="q-mt-lg q-mb-sm">
<p v-text="$t('watchdog')"></p>
</h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('enable_watchdog')"></q-item-label>
<q-item-label
caption
v-text="$t('enable_watchdog_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_watchdog_switch_to_voidwallet"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12 col-md-6">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label
v-text="$t('notification_watchdog_limit')"
></q-item-label>
<q-item-label
caption
v-text="$t('notification_watchdog_limit_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_notification_watchdog"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12 col-md-6">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('watchdog_interval')"></q-item-label>
<q-item-label
caption
v-text="$t('watchdog_interval_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="formData.lnbits_watchdog_interval_minutes"
type="number"
/>
</q-item-section>
</q-item>
</div>
<div class="col-12 col-md-6">
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('watchdog_delta')"></q-item-label>
<q-item-label
caption
v-text="$t('watchdog_delta_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="formData.lnbits_watchdog_delta"
type="number"
/>
</q-item-section>
</q-item>
</div>
</div>
</div>
</q-card-section>
</q-tab-panel>
@@ -0,0 +1,67 @@
<q-tab-panel name="library">
<q-card-section class="q-pa-none">
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('image_library')"></span>
</h6>
<q-btn
color="primary"
label="Add Image"
@click="$refs.imageInput.click()"
class="q-mb-md"
/>
<input
type="file"
ref="imageInput"
accept="image/png, image/jpeg, image/gif"
style="display: none"
@change="onImageInput"
/>
</q-card-section>
<div class="row q-col-gutter-sm q-pa-sm">
<div
v-for="image in library_images"
:key="image.filename"
class="col-6 col-sm-4 col-md-3 col-lg-2"
style="max-width: 200px"
>
<q-card class="q-mb-sm">
<q-img :src="image.url" style="height: 150px" />
<q-card-section
class="q-pt-md q-pb-md row items-center justify-between"
>
<small
><div
class="text-caption ellipsis"
style="max-width: 100px"
:title="image.filename"
v-text="image.filename"
></div
></small>
<q-btn
dense
flat
size="sm"
icon="content_copy"
@click="copyText(image.url)"
:title="$t('copy')"
><q-tooltip>Copy image link</q-tooltip></q-btn
>
<q-btn
dense
flat
size="sm"
icon="delete"
color="negative"
@click="deleteImage(image.filename)"
:title="$t('delete')"
><q-tooltip>Delete image</q-tooltip></q-btn
>
</q-card-section>
</q-card>
</div>
</div>
<div v-if="library_images.length === 0" class="q-pa-xl">
<div class="text-subtitle2 text-grey">No images uploaded yet.</div>
</div>
</q-tab-panel>
@@ -1,4 +1,4 @@
<template id="lnbits-admin-notifications">
<q-tab-panel name="notifications">
<q-card-section class="q-pa-none">
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('notifications_configure')"></span>
@@ -76,25 +76,19 @@
icon="add"
></q-btn>
</q-input>
<div>
<q-chip
v-for="identifier in formData.lnbits_nostr_notifications_identifiers"
:key="identifier"
removable
@remove="removeNostrNotificationIdentifier(identifier)"
color="primary"
text-color="white"
><span class="ellipsis" v-text="identifier"></span
></q-chip>
</div>
</q-item-section>
</q-item>
<div>
<q-chip
v-for="identifier in formData.lnbits_nostr_notifications_identifiers"
:key="identifier"
removable
@remove="removeNostrNotificationIdentifier(identifier)"
color="primary"
text-color="white"
class="ellipsis"
:label="identifier"
><q-tooltip
v-if="identifier"
anchor="top middle"
self="bottom middle"
><span v-text="identifier"></span></q-tooltip
></q-chip>
</div>
</div>
<div class="col-sm-12 col-md-6">
@@ -404,14 +398,13 @@
v-text="$t('notification_balance_delta_changed_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-input
class="flow-right"
type="number"
min="0"
filled
v-model="formData.notification_balance_delta_threshold_sats"
<q-toggle
size="md"
v-model="formData.notification_balance_delta_changed"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
@@ -479,9 +472,7 @@
type="number"
min="0"
filled
v-model="
formData.lnbits_notification_incoming_payment_amount_sats
"
v-model="formData.lnbits_notification_incoming_payment_amount_sats"
/>
</q-item-section>
</q-item>
@@ -504,13 +495,11 @@
type="number"
min="0"
filled
v-model="
formData.lnbits_notification_outgoing_payment_amount_sats
"
v-model="formData.lnbits_notification_outgoing_payment_amount_sats"
/>
</q-item-section>
</q-item>
</div>
</div>
</q-card-section>
</template>
</q-tab-panel>
@@ -1,10 +1,9 @@
<template id="lnbits-admin-security">
<q-tab-panel name="security">
<q-card-section class="q-pa-none">
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('server_management')"></span>
</h6>
<h6 class="q-my-none"><span v-text="$t('server_management')"></span></h6>
<div class="row">
<div class="col-12 col-md-6">
<div class="col-md-6">
<p><span v-text="$t('base_url')"></span></p>
<q-input
filled
v-model.number="formData.lnbits_baseurl"
@@ -16,8 +15,8 @@
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('authentication')"></span>
</h6>
<div class="row q-col-gutter-sm q-mb-md">
<div class="col-12 col-md-6">
<div class="row q-col-gutter-sm">
<div class="col-12 col-sm-6">
<q-input
filled
v-model="formData.auth_token_expire_minutes"
@@ -27,17 +26,7 @@
>
</q-input>
</div>
<div class="col-12 col-md-6">
<q-input
filled
v-model="formData.auth_authentication_cache_minutes"
type="number"
:label="$t('auth_authentication_cache_label')"
:hint="$t('auth_authentication_cache_hint')"
>
</q-input>
</div>
<div class="col-12 col-md-6">
<div class="col-12 col-sm-6">
<q-select
filled
v-model="formData.auth_allowed_methods"
@@ -45,10 +34,6 @@
:hint="$t('auth_allowed_methods_hint')"
:label="$t('auth_allowed_methods_label')"
:options="formData.auth_all_methods"
:option-label="
option =>
option.length > 25 ? option.substring(0, 22) + '...' : option
"
></q-select>
</div>
</div>
@@ -94,7 +79,7 @@
<strong class="q-my-none q-mb-sm">Google Auth</strong>
<div class="row">
<div class="col-12 col-md-6 q-pr-sm">
<div class="col-md-6 col-sm-12 q-pr-sm">
<q-input
filled
v-model="formData.google_client_id"
@@ -103,7 +88,7 @@
>
</q-input>
</div>
<div class="col-12 col-md-6">
<div class="col-md-6 col-sm-12">
<q-input
filled
v-model="formData.google_client_secret"
@@ -121,7 +106,7 @@
<strong class="q-my-none q-mb-sm">GitHub Auth</strong>
<div class="row">
<div class="col-12 col-md-6 q-pr-sm">
<div class="col-md-6 col-sm-12 q-pr-sm">
<q-input
filled
v-model="formData.github_client_id"
@@ -130,7 +115,7 @@
>
</q-input>
</div>
<div class="col-12 col-md-6">
<div class="col-md-6 col-sm-12">
<q-input
filled
v-model="formData.github_client_secret"
@@ -148,7 +133,7 @@
<strong class="q-my-none q-mb-sm">Keycloak Auth</strong>
<div class="row q-col-gutter-sm q-col-gutter-y-md">
<div class="col-12 col-md-4">
<div class="col-md-4 col-sm-12">
<q-input
filled
v-model="formData.keycloak_discovery_url"
@@ -156,7 +141,7 @@
>
</q-input>
</div>
<div class="col-12 col-md-4">
<div class="col-md-4 col-sm-12">
<q-input
filled
v-model="formData.keycloak_client_id"
@@ -165,7 +150,7 @@
>
</q-input>
</div>
<div class="col-12 col-md-4">
<div class="col-md-4 col-sm-12">
<q-input
filled
v-model="formData.keycloak_client_secret"
@@ -174,7 +159,7 @@
>
</q-input>
</div>
<div class="col-12 col-md-4">
<div class="col-md-4 col-sm-12">
<q-input
filled
v-model="formData.keycloak_client_custom_org"
@@ -182,7 +167,7 @@
>
</q-input>
</div>
<div class="col-12 col-md-8">
<div class="col-md-8 col-sm-12">
<q-input
filled
v-model="formData.keycloak_client_custom_icon"
@@ -192,57 +177,6 @@
</div>
</div>
</q-card-section>
<q-card-section
v-if="formData.auth_allowed_methods?.includes('oidc-auth')"
class="q-pl-xl"
>
<strong class="q-my-none q-mb-sm">OIDC Auth</strong>
<div class="row q-col-gutter-sm q-col-gutter-y-md">
<div class="col-12 col-md-4">
<q-input
filled
v-model="formData.oidc_discovery_url"
:label="$t('auth_oidc_label')"
>
</q-input>
</div>
<div class="col-12 col-md-4">
<q-input
filled
v-model="formData.oidc_client_id"
:label="$t('auth_oidc_ci_label')"
:hint="$t('auth_oidc_ci_hint')"
>
</q-input>
</div>
<div class="col-12 col-md-4">
<q-input
filled
v-model="formData.oidc_client_secret"
type="password"
:label="$t('auth_oidc_cs_label')"
>
</q-input>
</div>
<div class="col-12 col-md-4">
<q-input
filled
v-model="formData.oidc_client_custom_org"
:label="$t('auth_oidc_custom_org_label')"
>
</q-input>
</div>
<div class="col-12 col-md-8">
<q-input
filled
v-model="formData.oidc_client_custom_icon"
:label="$t('auth_oidc_custom_icon_label')"
>
</q-input>
</div>
</div>
</q-card-section>
<q-separator></q-separator>
<q-card-section class="q-pa-none">
<br />
@@ -271,11 +205,7 @@
dense
flat
color="primary"
:label="
serverlogEnabled
? $t('disable_server_log')
: $t('enable_server_log')
"
:label="(serverlogEnabled) ? $t('disable_server_log') : $t('enable_server_log')"
></q-btn>
</div>
<br />
@@ -283,7 +213,7 @@
<div class="col-12 col-md-12">
<p v-text="$t('ip_blocker')"></p>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<div class="col-6">
<q-input
filled
v-model="formBlockedIPs"
@@ -313,7 +243,7 @@
</div>
<br />
</div>
<div class="col-12 col-md-6">
<div class="col-6">
<q-input
filled
v-model="formAllowedIPs"
@@ -349,25 +279,18 @@
<div class="col-12 col-md-12">
<p v-text="$t('rate_limiter')"></p>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<div class="col-6">
<q-input
filled
type="number"
v-model.number="formData.lnbits_rate_limit_no"
:label="$t('number_of_requests')"
:hint="$t('number_of_requests_hint')"
></q-input>
</div>
<div class="col-12 col-md-6">
<div class="col-6">
<q-select
filled
:options="[
{label: $t('second'), value: 'second'},
{label: $t('minute'), value: 'minute'},
{label: $t('hour'), value: 'hour'}
]"
emit-value
map-options
:options="[$t('second'),$t('minute'),$t('hour')]"
v-model="formData.lnbits_rate_limit_unit"
:label="$t('time_unit')"
></q-select>
@@ -413,4 +336,4 @@
</div>
</div>
</q-card-section>
</template>
</q-tab-panel>
@@ -1,48 +1,37 @@
<template id="lnbits-admin-server">
<q-tab-panel name="server">
<q-card-section class="q-pa-none">
<div>
<h6 class="q-my-none">
<span v-text="$t('currency_settings')"></span>
</h6>
<h6 class="q-my-none"><span v-text="$t('currency_settings')"></span></h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p>
<span v-text="$t('allowed_currencies')"></span>
</p>
<p><span v-text="$t('allowed_currencies')"></span></p>
<q-select
filled
v-model="formData.lnbits_allowed_currencies"
multiple
:hint="$t('allowed_currencies_hint')"
:label="$t('allowed_currencies')"
:options="g.currencies"
:options="{{ currencies | safe }}"
></q-select>
</div>
<div class="col-12 col-md-6">
<p>
<span v-text="$t('default_account_currency')"></span>
</p>
<p><span v-text="$t('default_account_currency')"></span></p>
<q-select
filled
v-model="formData.lnbits_default_accounting_currency"
clearable
:hint="$t('default_account_currency_hint')"
:label="$t('currency')"
:options="
formData.lnbits_allowed_currencies?.length
? formData.lnbits_allowed_currencies
: g.allowedCurrencies
"
:options="formData.lnbits_allowed_currencies?.length ? formData.lnbits_allowed_currencies : {{ currencies }}"
></q-select>
</div>
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('payments')"></span>
</h6>
<h6 class="q-my-none"><span v-text="$t('payments')"></span></h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-4">
<div class="col-12 col-md-3">
<p><span v-text="$t('max_outgoing_payment_amount')"></span></p>
<q-input
filled
type="number"
@@ -54,7 +43,8 @@
></q-input>
</div>
<div class="col-12 col-md-4">
<div class="col-12 col-md-3">
<p><span v-text="$t('max_incoming_payment_amount')"></span></p>
<q-input
filled
type="number"
@@ -69,9 +59,7 @@
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('wallet_limiter')"></span>
</h6>
<h6 class="q-my-none"><span v-text="$t('wallet_limiter')"></span></h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-3">
<q-input
@@ -107,32 +95,33 @@
</div>
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('service_fees')"></span>
</h6>
<h6 class="q-my-none"><span v-text="$t('service_fee')"></span></h6>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-4">
<div class="col-12 col-md-6">
<p><span v-text="$t('service_fee')"></span></p>
<q-input
filled
type="number"
v-model.number="formData.lnbits_service_fee"
:label="$t('service_fee_label')"
step="0.1"
:suffix="'%'"
:hint="$t('service_fee_hint')"
></q-input>
<br />
</div>
<div class="col-12 col-md-4">
<div class="col-12 col-md-6">
<p><span v-text="$t('service_fee_max')"></span></p>
<q-input
filled
type="number"
v-model.number="formData.lnbits_service_fee_max"
:label="$t('service_fee_max_label')"
:suffix="$t('sats')"
:hint="$t('service_fee_max_hint')"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('fee_wallet')"></span></p>
<q-input
filled
v-model="formData.lnbits_service_fee_wallet"
@@ -142,9 +131,14 @@
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('disable_fee_internal')"></span></p>
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('disable_fee')"></q-item-label>
<q-item-label
caption
v-text="$t('disable_fee_internal_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
@@ -161,4 +155,4 @@
</div>
</div>
</q-card-section>
</template>
</q-tab-panel>
+210
View File
@@ -0,0 +1,210 @@
<q-tab-panel name="site_customisation">
<q-card-section class="q-pa-none">
<h6 class="q-my-none"><span v-text="$t('ui_management')"></span></h6>
<br />
<div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-5">
<p><span v-text="$t('ui_site_title')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_site_title"
:label="$t('ui_site_title') + $t('ui_changing_remove_lnbits_elements')"
></q-input>
<br />
</div>
<div class="col-12 col-md-5">
<p><span v-text="$t('ui_site_tagline')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_site_tagline"
:label="$t('ui_site_tagline')"
></q-input>
<br />
</div>
<div class="col-12 col-md-2 q-mt-xl">
<q-toggle
:tip="$t('ui_toggle_elements_tip')"
v-model="formData.lnbits_show_home_page_elements"
:label="formData.lnbits_show_home_page_elements ? $t('ui_elements_enable') : $t('ui_elements_disable')"
></q-toggle>
</div>
</div>
<div>
<p><span v-text="$t('ui_site_description')"></span></p>
<q-input
v-model="formData.lnbits_site_description"
filled
type="textarea"
:hint="$t('ui_site_description_hint')"
/>
</div>
<br />
<div class="row q-col-gutter-md">
<div class="col-12 col-md-4">
<p><span v-text="$t('ui_default_wallet_name')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_default_wallet_name"
:label="$t('lnbits_wallet')"
></q-input>
</div>
<div class="col-12 col-md-4">
<p><span v-text="$t('ui_qr_code_logo')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_qr_logo"
label="https://example.com/image.svg"
:hint="$t('ui_qr_code_logo_hint')"
></q-input>
</div>
</div>
<div class="row q-col-gutter-md q-mt-md">
<div class="col-12 col-md-6">
<p><span v-text="$t('ui_custom_badge')"></span></p>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8">
<q-input
filled
type="text"
tip="Custom Badge"
v-model.trim="formData.lnbits_custom_badge"
:label="$t('ui_custom_badge_label')"
></q-input>
</div>
<div class="col-12 col-md-4">
<q-select
filled
v-model="formData.lnbits_custom_badge_color"
:options="colors"
:label="$t('ui_custom_badge_color_label')"
></q-select>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('ui_custom_image')"></span></p>
<q-input
filled
type="text"
tip="Custom Image"
v-model.trim="formData.lnbits_custom_image"
:label="$t('ui_custom_image_label')"
:hint="$t('ui_custom_image_hint')"
></q-input>
</div>
</div>
<br />
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p><span v-text="$t('themes')"></span></p>
<q-select
filled
v-model="formData.lnbits_theme_options"
multiple
:hint="$t('themes_hint')"
:options="lnbits_theme_options"
:label="$t('themes')"
></q-select>
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('custom_logo')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_custom_logo"
label="https://example.com/image.png"
:hint="$t('custom_logo_hint')"
></q-input>
<br />
</div>
</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<p><span v-text="$t('ad_space_title')"></span></p>
<q-input
filled
type="text"
v-model="formData.lnbits_ad_space_title"
:label="$t('ad_space_title_label')"
></q-input>
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('ad_slots')"></span></p>
<q-input
class="q-mb-md"
filled
v-model="formData.lnbits_ad_space"
type="text"
:label="$t('ad_slots_label')"
:hint="$t('ad_slots_hint')"
><q-tooltip>
format {url};{img-light};{img-dark},{url};{img-light};{img-dark}"
</q-tooltip>
</q-input>
<q-toggle
v-model="formData.lnbits_ad_space_enabled"
:label="formData.lnbits_ad_space_enabled ? $t('ads_enabled') : $t('ads_disabled')"
/>
<br />
</div>
</div>
<div class="row q-col-gutter-md q-my-md">
<div class="col-12" v-text="$t('ui_default_theme')"></div>
<div class="col-12 col-sm-6 col-lg-3">
<q-select
v-model="formData.lnbits_default_border"
:options="globalBorderOptions"
label="Border"
@update:model-value="applyGlobalBorder"
>
</q-select>
</div>
<div class="col-12 col-sm-6 col-lg-3">
<q-select
v-model="formData.lnbits_default_theme"
:options="lnbits_theme_options"
label="Theme"
@update:model-value="applyGlobalTheme"
>
</q-select>
</div>
<div class="col-12 col-sm-6 col-lg-3">
<q-select
v-model="formData.lnbits_default_reaction"
:options="reactionOptions"
label="Payment reaction"
@update:model-value="applyGlobalReaction"
>
</q-select>
</div>
<div class="col-12 col-sm-6 col-lg-3">
<q-input
type="text"
v-model="formData.lnbits_default_bgimage"
label="Background Image"
@update:model-value="applyGlobalBgimage"
hint="This must be a trusted source. It can change the content and it can log your IP address."
>
</q-input>
</div>
<div class="col-12 col-sm-6 col-lg-3">
<q-toggle
type="bool"
v-model="formData.lnbits_default_gradient"
color="primary"
label="Gradient Background"
>
</q-toggle>
</div>
</div>
</div>
</q-card-section>
</q-tab-panel>
@@ -0,0 +1,83 @@
<q-tab-panel name="users">
<q-card-section class="q-pa-none">
<h6 class="q-my-none q-mb-sm">
<span v-text="$t('user_management')"></span>
</h6>
<div class="row">
<div class="col-12 col-md-6 q-pr-sm">
<p><span v-text="$t('admin_users')"></span></p>
<q-input
filled
v-model="formAddAdmin"
@keydown.enter="addAdminUser"
type="text"
:label="$t('admin_users_label')"
:hint="$t('admin_users_hint')"
>
<q-btn @click="addAdminUser" dense flat icon="add"></q-btn>
</q-input>
<div>
<q-chip
v-for="user in formData.lnbits_admin_users"
:key="user"
removable
@remove="removeAdminUser(user)"
color="primary"
text-color="white"
:label="user"
class="ellipsis"
>
</q-chip>
</div>
<br />
</div>
<div class="col-12 col-md-6">
<p><span v-text="$t('allowed_users')"></span></p>
<q-input
filled
v-model="formAddUser"
@keydown.enter="addAllowedUser"
type="text"
:label="$t('allowed_users_label')"
:hint="$t('allowed_users_hint')"
>
<q-btn @click="addAllowedUser" dense flat icon="add"></q-btn>
</q-input>
<div>
<q-chip
v-for="user in formData.lnbits_allowed_users"
:key="user"
removable
@remove="removeAllowedUser(user)"
color="primary"
text-color="white"
:label="user"
class="ellipsis"
>
</q-chip>
</div>
<br />
<q-item tag="label" v-ripple>
<q-item-section>
<q-item-label v-text="$t('allow_creation_user')"></q-item-label>
<q-item-label
caption
v-text="$t('allow_creation_user_desc')"
></q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle
size="md"
v-model="formData.lnbits_allow_new_accounts"
checked-icon="check"
color="green"
unchecked-icon="clear"
/>
</q-item-section>
</q-item>
<br />
</div>
</div>
</q-card-section>
</q-tab-panel>
+251
View File
@@ -0,0 +1,251 @@
{% if not ajax %} {% extends "base.html" %} {% endif %} {% from "macros.jinja"
import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
{% endblock %} {% block page %}
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-sm">
<div class="row items-center justify-between q-gutter-xs">
<div class="col">
<q-btn
:label="$t('save')"
color="primary"
@click="updateSettings"
:disabled="!checkChanges"
>
<q-tooltip v-if="checkChanges">
<span v-text="$t('save_tooltip')"></span>
</q-tooltip>
<q-badge
v-if="checkChanges"
color="red"
rounded
floating
style="padding: 6px; border-radius: 6px"
/>
</q-btn>
<q-btn
v-if="isSuperUser"
:label="$t('restart')"
color="primary"
@click="restartServer"
class="q-ml-md"
>
<q-tooltip v-if="needsRestart">
<span v-text="$t('restart_tooltip')"></span>
</q-tooltip>
<q-badge
v-if="needsRestart"
color="red"
rounded
floating
style="padding: 6px; border-radius: 6px"
/>
</q-btn>
<q-btn
:label="$t('download_backup')"
flat
@click="downloadBackup"
></q-btn>
<q-btn
flat
v-if="isSuperUser"
:label="$t('reset_defaults')"
color="primary"
@click="deleteSettings"
class="float-right"
>
<q-tooltip>
<span v-text="$t('reset_defaults_tooltip')"></span>
</q-tooltip>
</q-btn>
</div>
<div></div>
</div>
</div>
</q-card>
</div>
</div>
<div class="row q-col-gutter-md justify-center">
<div class="col q-gutter-y-md">
<q-card>
<q-splitter>
<template v-slot:before>
<q-tabs
@update:model-value="showExchangeProvidersTab"
v-model="tab"
vertical
active-color="primary"
>
<q-tab
name="funding"
icon="account_balance_wallet"
:label="$q.screen.gt.sm ? $t('funding') : null"
@update="val => tab = val.name"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('funding')"></span></q-tooltip
></q-tab>
<q-tab
name="security"
icon="security"
:label="$q.screen.gt.sm ? $t('security') : null"
@update="val => tab = val.name"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('security')"></span></q-tooltip
></q-tab>
<q-tab
name="server"
icon="price_change"
:label="$q.screen.gt.sm ? $t('payments') : null"
@update="val => tab = val.name"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('payments')"></span></q-tooltip
></q-tab>
<q-tab
name="exchange_providers"
icon="show_chart"
:label="$q.screen.gt.sm ? $t('exchanges') : null"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('exchanges')"></span></q-tooltip
></q-tab>
<q-tab
name="fiat_providers"
icon="credit_score"
:label="$q.screen.gt.sm ? $t('fiat_providers') : null"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('fiat_providers')"></span></q-tooltip
></q-tab>
<q-tab
name="users"
icon="group"
:label="$q.screen.gt.sm ? $t('users') : null"
@update="val => tab = val.name"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('users')"></span></q-tooltip
></q-tab>
<q-tab
name="extensions"
icon="extension"
:label="$q.screen.gt.sm ? $t('extensions') : null"
@update="val => tab = val.name"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('extensions')"></span></q-tooltip
></q-tab>
<q-tab
name="notifications"
icon="notifications"
:label="$q.screen.gt.sm ? $t('notifications') : null"
@update="val => tab = val.name"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('notifications')"></span></q-tooltip
></q-tab>
<q-tab
name="audit"
icon="playlist_add_check_circle"
:label="$q.screen.gt.sm ? $t('audit') : null"
@update="val => tab = val.name"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('audit')"></span></q-tooltip
></q-tab>
<q-tab
name="library"
icon="image"
:label="$q.screen.gt.sm ? $t('library') : null"
@update="val => tab = val.name"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('library')"></span></q-tooltip
></q-tab>
<q-tab
style="word-break: break-all"
name="site_customisation"
icon="language"
:label="$q.screen.gt.sm ? $t('site_customisation') : null"
@update="val => tab = val.name"
><q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('site_customisation')"></span></q-tooltip
></q-tab>
</q-tabs>
</template>
<template v-slot:after>
<q-form name="settings_form" id="settings_form">
<q-scroll-area style="height: 100vh">
<q-tab-panels
v-model="tab"
animated
swipeable
vertical
scroll
transition-prev="jump-up"
transition-next="jump-up"
>
{% include "admin/_tab_funding.html" %} {% include
"admin/_tab_users.html" %} {% include "admin/_tab_server.html"
%} {% include "admin/_tab_exchange_providers.html" %}{% include
"admin/_tab_fiat_providers.html" %} {% include
"admin/_tab_extensions.html" %} {% include
"admin/_tab_notifications.html" %} {% include
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
%}{% include "admin/_tab_audit.html"%}{% include
"admin/_tab_library.html"%}
</q-tab-panels>
</q-scroll-area>
</q-form>
</template>
</q-splitter>
</q-card>
</div>
</div>
<q-dialog v-model="exchangeData.showTickerConversion" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<div class="q-mb-md">
<strong v-text="$t('create_ticker_converter')"></strong>
</div>
<div class="row">
<div class="col-12 q-mb-md">
<q-select
filled
dense
v-model="exchangeData.convertFromTicker"
label="From Currency"
:options="{{ currencies | safe }}"
></q-select>
</div>
<div class="col-12">
<q-input
v-model="exchangeData.convertToTicker"
dense
filled
label="New Ticker"
hint="This ticker will be used for the exchange API calls."
>
</q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn
@click="addExchangeTickerConversion()"
label="Add Ticker Conversion"
color="primary"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('close')"
></q-btn>
</div>
</q-card>
</q-dialog>
{% endblock %}
+201
View File
@@ -0,0 +1,201 @@
{% if not ajax %} {% extends "base.html" %} {% endif %}
<!---->
{% from "macros.jinja" import window_vars with context %}
<!---->
{% block scripts %} {{ window_vars(user) }} {% endblock %} {% block page %}
<div class="row q-col-gutter-md q-mb-md">
<div class="col-12">
<q-card>
<div class="q-pa-sm q-pl-lg">
<div class="row items-center justify-between q-gutter-xs">
<div class="col">
<!-- Optional: Add content here if needed -->
</div>
<div>
<q-btn
v-if="g.user.admin"
flat
round
icon="settings"
to="/admin#audit"
>
<q-tooltip v-text="$t('admin_settings')"></q-tooltip>
</q-btn>
</div>
</div>
</div>
</q-card>
</div>
</div>
<div class="row q-col-gutter-md justify-center q-mb-lg">
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
<q-card class="q-pt-sm">
<strong v-text="$t('components')"></strong>
<div style="width: 250px" class="q-pa-sm">
<canvas v-if="chartsReady" ref="componentUseChart"></canvas>
</div>
</q-card>
</div>
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
<q-card class="q-pt-sm">
<strong v-text="$t('long_running_endpoints')"></strong>
<div style="width: 250px; height: 250px" class="q-pa-sm">
<canvas v-if="chartsReady" ref="longDurationChart"></canvas>
</div>
</q-card>
</div>
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
<q-card class="q-pt-sm">
<strong v-text="$t('http_request_methods')"></strong>
<div style="width: 250px; height: 250px" class="q-pa-sm">
<canvas v-if="chartsReady" ref="requestMethodChart"></canvas>
</div>
</q-card>
</div>
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
<q-card class="q-pt-sm">
<strong v-text="$t('http_response_codes')"></strong>
<div style="width: 250px; height: 250px" class="q-pa-sm">
<canvas v-if="chartsReady" ref="responseCodeChart"></canvas>
</div>
</q-card>
</div>
</div>
<div class="row q-col-gutter-md justify-center">
<div class="col">
<q-card class="q-pa-md">
<q-table
row-key="id"
:rows="auditEntries"
:columns="auditTable.columns"
v-model:pagination="auditTable.pagination"
:filter="auditTable.search"
:loading="auditTable.loading"
@request="fetchAudit"
>
<template v-slot:header="props">
<q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props">
<q-input
v-if="['ip_address', 'user_id', 'path',].includes(col.name)"
v-model="searchData[col.name]"
@keydown.enter="searchAuditBy()"
@update:model-value="searchAuditBy()"
dense
type="text"
filled
clearable
:label="col.label"
>
<template v-slot:append>
<q-icon
name="search"
@click="searchAuditBy()"
class="cursor-pointer"
/>
</template>
</q-input>
<q-select
v-else-if="['component', 'response_code','request_method'].includes(col.name)"
v-model="searchData[col.name]"
:options="searchOptions[col.name]"
@update:model-value="searchAuditBy()"
:label="col.label"
clearable
style="width: 100px"
></q-select>
<span v-else v-text="col.label"></span>
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr auto-width :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
<div v-if="col.name == 'created_at'">
<q-btn
icon="description"
:disable="!props.row.request_details"
size="sm"
flat
class="cursor-pointer q-mr-xs"
@click="showDetailsDialog(props.row)"
>
<q-tooltip
><span v-text="$t('request_details')"></span
></q-tooltip>
</q-btn>
<span v-text="formatDate(props.row.created_at)"></span>
<q-tooltip v-if="props.row.delete_at">
<span
v-text="'Will be deleted at: ' + formatDate(props.row.delete_at)"
></span>
</q-tooltip>
</div>
<div
v-else-if="['user_id', 'request_details'].includes(col.name)"
>
<q-btn
v-if="props.row[col.name]"
icon="content_copy"
size="sm"
flat
class="cursor-pointer q-mr-xs"
@click="copyText(props.row[col.name])"
>
<q-tooltip>Copy</q-tooltip>
</q-btn>
<span v-text="shortify(props.row[col.name])"> </span>
<q-tooltip>
<span v-text="props.row[col.name]"></span>
</q-tooltip>
</div>
<span
v-else
v-text="props.row[col.name]"
@click="searchAuditBy(col.name, props.row[col.name])"
class="cursor-pointer"
></span>
</q-td>
</q-tr>
</template>
</q-table>
</q-card>
</div>
</div>
<q-dialog v-model="auditDetailsDialog.show" position="top">
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
<strong v-text="$t('http_request_details')"></strong>
<q-input
filled
dense
v-model.trim="auditDetailsDialog.data"
type="textarea"
rows="25"
></q-input>
<div class="row q-mt-lg">
<q-btn
@click="copyText(auditDetailsDialog.data)"
icon="copy_content"
color="grey"
flat
v-text="$t('copy')"
></q-btn>
<q-btn
v-close-popup
flat
color="grey"
class="q-ml-auto"
v-text="$t('close')"
></q-btn>
</div>
</q-card>
</q-dialog>
{% endblock %}
+305
View File
@@ -0,0 +1,305 @@
<q-expansion-item
group="extras"
icon="vpn_key"
:label="$t('api_keys_api_docs')"
:content-inset-level="0.5"
>
<q-card-section>
<q-list>
<q-item dense class="q-pa-none">
<q-item-section>
<q-item-label>
<strong>Node URL: </strong><em v-text="origin"></em>
</q-item-label>
</q-item-section>
</q-item>
<q-item dense class="q-pa-none">
<q-item-section>
<q-item-label>
<strong>Wallet ID: </strong><em v-text="wallet.id"></em>
</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon
name="content_copy"
class="cursor-pointer"
@click="copyText(wallet.id)"
></q-icon>
</q-item-section>
</q-item>
<q-item dense class="q-pa-none">
<q-item-section>
<q-item-label>
<strong>Admin key: </strong
><em
v-text="adminkeyHidden ? '****************' : wallet.adminkey"
></em>
</q-item-label>
</q-item-section>
<q-item-section side>
<div>
<q-icon
:name="adminkeyHidden ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="adminkeyHidden = !adminkeyHidden"
></q-icon>
<q-icon
name="content_copy"
class="cursor-pointer q-ml-sm"
@click="copyText(wallet.adminkey)"
></q-icon>
<q-icon name="qr_code" class="cursor-pointer q-ml-sm">
<q-popup-proxy>
<div class="q-pa-md">
<lnbits-qrcode
:value="wallet.adminkey"
:show-buttons="false"
></lnbits-qrcode>
</div>
</q-popup-proxy>
</q-icon>
</div>
</q-item-section>
</q-item>
<q-item dense class="q-pa-none">
<q-item-section>
<q-item-label>
<strong>Invoice/read key: </strong
><em v-text="inkeyHidden ? '****************' : wallet.inkey"></em>
</q-item-label>
</q-item-section>
<q-item-section side>
<div>
<q-icon
:name="inkeyHidden ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="inkeyHidden = !inkeyHidden"
></q-icon>
<q-icon
name="content_copy"
class="cursor-pointer q-ml-sm"
@click="copyText(wallet.inkey)"
></q-icon>
<q-icon name="qr_code" class="cursor-pointer q-ml-sm">
<q-popup-proxy>
<div class="q-pa-md">
<lnbits-qrcode
:value="wallet.inkey"
:show-buttons="false"
></lnbits-qrcode>
</div>
</q-popup-proxy>
</q-icon>
</div>
</q-item-section>
</q-item>
</q-list>
</q-card-section>
<q-expansion-item
group="api"
dense
expand-separator
label="Get wallet details"
>
<q-card>
<q-card-section>
<code><span class="text-light-green">GET</span> /api/v1/wallet</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code
>{"X-Api-Key": "<i
v-text="inkeyHidden ? '****************' : wallet.inkey"
></i
>"}</code
><br />
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code
>{"id": &lt;string&gt;, "name": &lt;string&gt;, "balance":
&lt;int&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl <span v-text="baseUrl"></span>api/v1/wallet -H "X-Api-Key:
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i
>"</code
>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Create an invoice (incoming)"
>
<q-card>
<q-card-section>
<code><span class="text-light-green">POST</span> /api/v1/payments</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code
>{"X-Api-Key": "<i
v-text="inkeyHidden ? '****************' : wallet.inkey"
></i
>"}</code
><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"out": false, "amount": &lt;int&gt;, "memo": &lt;string&gt;,
"expiry": &lt;int&gt;, "unit": &lt;string&gt;, "webhook":
&lt;url:string&gt;, "internal": &lt;bool&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"payment_hash": &lt;string&gt;, "payment_request":
&lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST <span v-text="baseUrl"></span>api/v1/payments -d
'{"out": false, "amount": &lt;int&gt;, "memo": &lt;string&gt;}' -H
"X-Api-Key:
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i>" -H
"Content-type: application/json"</code
>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Pay an invoice (outgoing)"
>
<q-card>
<q-card-section>
<code
><span class="text-light-green">POST</span> /api/v1/payments (reveal
admin keys
<q-icon
:name="adminkeyHidden ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="adminkeyHidden = !adminkeyHidden"
></q-icon
>)</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code
>{"X-Api-Key": "<i
v-text="adminkeyHidden ? '****************' : wallet.adminkey"
></i
>"}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code>{"out": true, "bolt11": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>{"payment_hash": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST <span v-text="baseUrl"></span>api/v1/payments -d
'{"out": true, "bolt11": &lt;string&gt;}' -H "X-Api-Key:
<i v-text="adminkeyHidden ? '****************' : wallet.adminkey"></i
>" -H "Content-type: application/json"</code
>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Decode an invoice"
>
<q-card>
<q-card-section>
<code
><span class="text-light-green">POST</span>
/api/v1/payments/decode</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code>{"data": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 (application/json)
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST <span v-text="baseUrl"></span>api/v1/payments/decode -d
'{"data": &lt;bolt11/lnurl, string&gt;}' -H "Content-type:
application/json"</code
>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Check an invoice (incoming or outgoing)"
class="q-pb-md"
>
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/api/v1/payments/&lt;payment_hash&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code
>{"X-Api-Key": "<i
v-text="inkeyHidden ? '****************' : wallet.inkey"
></i
>"}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>{"paid": &lt;bool&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET
<span v-text="baseUrl"></span>api/v1/payments/&lt;payment_hash&gt; -H
"X-Api-Key:
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i>" -H
"Content-type: application/json"</code
>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<code
><span class="text-pink">WS</span>
/api/v1/ws/&lt;invoice_key&gt;</code
>
<h5
class="text-caption q-mt-sm q-mb-none"
v-text="$t('websocket_example')"
></h5>
<code
>wscat -c <span v-text="websocketUrl"></span>/<span
v-text="inkeyHidden ? '****************' : wallet.inkey"
></span
></code>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)/payments
</h5>
<code>{"balance": &lt;int&gt;, "payment": &lt;object&gt;}</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-card-section>
<p v-text="$t('reset_wallet_keys_desc')"></p>
<q-btn
unelevated
color="red-10"
@click="resetKeys()"
:label="$t('reset_wallet_keys')"
></q-btn>
</q-card-section>
</q-expansion-item>
File diff suppressed because it is too large Load Diff

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