Compare commits

...
22 Commits
Author SHA1 Message Date
dni ⚡andGitHub bd07a319ab fix: appimage ci workflow (#3423) 2025-10-17 13:53:07 +02:00
dni ⚡andGitHub 6b732a2a6a chore: update to v1.3.1 (#3422) 2025-10-17 13:42:51 +02:00
25c8dd18e0 docs: update admin ui readme.md (#3402)
Co-authored-by: Dein Name <deine.email@example.com>
Co-authored-by: Arc <33088785+arcbtc@users.noreply.github.com>
2025-10-17 14:14:54 +03:00
dni ⚡andGitHub 2b1b5cadaa chore: dead cose in core/tasks.py (#3421) 2025-10-17 14:13:23 +03:00
Vlad Stan 6b41820422 refactor: re-order methods based on name 2025-10-17 14:04:28 +03:00
Vlad StanandGitHub 7116353431 feat: edit wallet name by admin (#3417) 2025-10-17 10:59:16 +03:00
Vlad StanandGitHub de6827af58 feat: add and initialize the payment_request field for Payment (#3389) 2025-10-17 09:44:01 +03:00
40c065708a [feat] add case insensitive search for users (#3413)
Co-authored-by: Arc <33088785+arcbtc@users.noreply.github.com>
2025-10-16 23:23:01 +01:00
SatandGitHub 4cf9fae3e3 feat: add batch invoice polling and persistence for StrikeWallet (#3300) 2025-10-16 23:19:41 +01:00
Vlad StanandGitHub bf06def9b7 [feat] Stripe subscription (#3369) 2025-10-16 23:14:06 +01:00
Tiago VasconcelosandGitHub 182894fd93 bug: frontend, add some margin to bottom (#3405) 2025-10-16 23:13:21 +01:00
Tiago VasconcelosandGitHub d0cf374cda feat: add UI funding source retries (#3407) 2025-10-16 23:12:22 +01:00
dni ⚡andGitHub 1e0dab32c3 ci: add appimage to release flow (#3412) 2025-10-16 23:08:47 +01:00
dni ⚡andGitHub 34c9e218bb chore: update boltz docker image to latest (#3418) 2025-10-16 16:02:45 +02:00
dni ⚡andGitHub 9cc6a96433 chore: update version to v1.3.0 (#3411) 2025-10-15 13:09:36 +02:00
e5ff928c3c feat: allow query param for qrcode (#3390)
Co-authored-by: dni  <office@dnilabs.com>
2025-10-15 12:48:32 +02:00
dni ⚡andGitHub f31ffba06c ci: lock boltz client to latest working version (#3367) 2025-10-15 12:25:11 +02:00
Tiago VasconcelosandGitHub 1163e44265 fix: overlapping input (#3404) 2025-10-15 09:18:57 +02:00
ArcandGitHub 266b16834b fix nix install doc (#3401) 2025-10-10 16:39:08 +01:00
ArcandGitHub ad606b2517 Fix: Glibc error by pegging ubuntu version, appimage include extras like breez_sdk (#3397) 2025-10-10 10:35:25 +01:00
ArcandGitHub 1ec28079d9 docs: jq needed for AppImage install (#3399) 2025-10-10 08:50:41 +02:00
Vlad StanandGitHub aeaddc1dc6 fix: donate link 2025-10-09 09:55:51 +03:00
34 changed files with 2018 additions and 724 deletions
+1 -1
View File
@@ -1 +1 @@
custom: https://demo.lnbits.com/lnurlp/link/fH59GD
custom: https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg
@@ -1,27 +1,23 @@
name: Build LNbits AppImage
on:
release:
types: [published]
workflow_call:
workflow_dispatch:
jobs:
build-linux-package:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
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
@@ -30,7 +26,6 @@ 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:
@@ -39,7 +34,6 @@ 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
@@ -47,18 +41,11 @@ 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
# Install project deps into .venv using uv
uv sync
# Install PyInstaller into the same environment
uv sync --all-extras --no-dev
uv pip install pyinstaller
# Build the LNbits binary
uv run pyinstaller \
--onefile \
--name lnbits \
@@ -66,6 +53,10 @@ jobs:
--collect-all embit \
--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)"
@@ -75,7 +66,7 @@ jobs:
chmod +x packaging/linux/AppDir/lnbits.desktop
chmod +x packaging/linux/AppDir/usr/lnbits/dist/lnbits
# Clean out non-dist content from the app dir to keep AppImage slim
# keep AppDir slim
find packaging/linux/AppDir/usr/lnbits -mindepth 1 -maxdepth 1 \
! -name 'dist' \
! -name 'lnbits' \
@@ -91,9 +82,14 @@ jobs:
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:
+4
View File
@@ -52,3 +52,7 @@ jobs:
uses: JRubics/poetry-publish@v1.15
with:
pypi_token: ${{ secrets.PYPI_API_KEY }}
appimage:
needs: [ release ]
uses: ./.github/workflows/appimage.yml
+38 -23
View File
@@ -1,39 +1,46 @@
<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>
<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: 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)
![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 src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
<img width="2000" height="203" alt="lnbits_head" src="https://github.com/user-attachments/assets/77669718-ac10-43c7-ae95-6ce236c77401" />
# The world's most powerful suite of bitcoin tools.
# LNbits — The most powerful Bitcoin & Lightning toolkit
## Run for yourself, for others, or as part of a stack.
> Run it for yourself, for your community, or as part of a larger stack.
LNbits is beta, for responsible disclosure of any concerns please contact an admin in the community chat.
## What is LNbits?
LNbits is a Python server that sits on top of any funding source. It can be used as:
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.
- 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
## What you can do with LNbits
LNbits can run on top of almost all Lightning funding sources.
- **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.
See [LNbits manual](https://docs.lnbits.org/guide/wallets.html) for more detailed documentation about each funding source.
## Funding sources
Checkout the LNbits [YouTube](https://www.youtube.com/playlist?list=PLPj3KCksGbSYG0ciIQUWJru1dWstPHshe) video series.
LNbits runs on top of most Lightning backends. Choose the one you already operate - or swap later without changing your app architecture.
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.
- 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)
## Running LNbits
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.
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.
## 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.
@@ -66,9 +73,17 @@ As well as working great in a browser, LNbits has native IoS and Android apps as
<img src="https://i.imgur.com/J96EbRf.png" style="width:800px">
## Tip us
## Powered by LNbits
If you like this project [send some tip love](https://demo.lnbits.com/lnurlp/link/fH59GD)!
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/)
[docs]: https://github.com/lnbits/lnbits/wiki
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
+93 -49
View File
@@ -1,78 +1,122 @@
---
layout: default
title: Admin UI
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)
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
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
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.
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.
<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.
Also only the super user can brrrr satoshis to different wallets.
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 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.
<img width="900" height="640" alt="grafik" src="https://github.com/user-attachments/assets/d8852b4b-21be-446f-a1e7-d3eb794d3505" />
The super user is never sent over the api and the frontend only receives a bool if you are super user or not.
> [!WARNING]
> Some settings remain `.env` only. Use **[.env.example](../../.env.example#L3-L87)** as the authoritative reference for those variables.
We also added a decorator for the API routes to check for super user.
## What you can do with the Admin UI
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`
- 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
# Admin Users
> [!NOTE]
> See **[Super User](./super_user.md)** for the role and permission differences compared to Admin Users.
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`.
## Enabling or disabling the Admin UI
# Allowed Users
The Admin UI is enabled by default on new installs. To change the state:
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.
1. Stop LNbits
Setting this environment variable also disables account creation.
Account creation can be also disabled by setting `LNBITS_ALLOW_NEW_ACCOUNTS=false`
```bash
sudo systemctl stop lnbits.service
```
# How to activate
2. Edit your `.env`
```
$ sudo systemctl stop lnbits.service
$ cd ~/lnbits
$ sudo nano .env
```
```
cd ~/lnbits
sudo nano .env
```
-> set: `LNBITS_ADMIN_UI=true`
3. Set one of
Now start LNbits once in the terminal window
```
# Enable Admin UI
LNBITS_ADMIN_UI=true
```
$ uv run lnbits
```
# Disable Admin UI
LNBITS_ADMIN_UI=false
```
You can now `cat` the Super User ID:
4. Start LNbits
```
$ cat data/.super_user
```
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.
## First run and Super User ID
On first start with the Admin UI enabled you will be prompted to generate a Super User. If you need to read it from disk later:
```bash
cat /lnbits/data/.super_user
# example
123de4bfdddddbbeb48c8bc8382fe123
```
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.
> [!WARNING]
> For security reasons, Super Users and Admin users must authenticate with credentials (username and password).
After that you will find the **`Admin` / `Manage Server`** between `Wallets` and `Extensions`
After login you will see **Settings** and **Users** in the sidebar between **Wallets** and **Extensions**, plus a role badge in the top left.
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.
<img width="1353" height="914" alt="grafik" src="https://github.com/user-attachments/assets/06bb4f36-a23a-4058-87ec-60440d322c25" />
Do not forget
## Allowed Users
```
sudo systemctl start lnbits.service
```
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.
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.
<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/)
+73 -24
View File
@@ -1,30 +1,79 @@
# 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)
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
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
The table below offers a comparison of the different Lightning Network funding sources that can be used with LNbits.
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.
## 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. |
| 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. |
| **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. |
| **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). |
---
### 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/)
+457 -197
View File
@@ -1,39 +1,66 @@
---
layout: default
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) <img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">
# Basic installation
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.
> [!NOTE]
> **Default DB:** LNbits uses SQLite by default (simple & effective). You can switch to PostgreSQL — see the section below.
## Option 1: AppImage (LInux)
## Table of contents
### 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)
Go to [releases](https://github.com/lnbits/lnbits/releases) and pull latest AppImage, or:
## Option 1: AppImage (Linux)
**Quickstart**
1. Download latest AppImage from [releases](https://github.com/lnbits/lnbits/releases) **or** run:
```sh
sudo apt-get install libfuse2
sudo apt-get install jq 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 folder the AppImage runs from.
- 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)**
## Option 2: UV (recommended for developers)
It is recommended to use the latest version of UV. Make sure you have Python version `3.12` installed.
> [!IMPORTANT]
> **It is recommended to use the latest version of UV & Make sure you have Python version 3.12 installed.**
### 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
### Verify Python
```sh
python3 --version
@@ -46,15 +73,7 @@ curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
```
### (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"
```
### install LNbits
### Install LNbits
```sh
git clone https://github.com/lnbits/lnbits.git
@@ -62,10 +81,78 @@ cd lnbits
git checkout main
uv sync --all-extras
# or poetry
# poetry env use 3.12
# poetry install --only main
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
```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"
```
### 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
cp .env.example .env
# Optional: to set funding source amongst other options via the env `nano .env`
```
@@ -73,50 +160,53 @@ cp .env.example .env
#### Running the server
```sh
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.
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)
```
#### LNbits-cli
#### LNbits CLI
```sh
# A very useful terminal client for getting the supersuer ID, updating extensions, etc
uv run lnbits-cli --help
# A very useful terminal client for getting the superuser ID, updating extensions, etc.
poetry run lnbits-cli --help
```
#### Updating the server
```sh
cd lnbits
# Stop LNbits with `ctrl + x` or with service manager
# Stop LNbits with Ctrl + X or with your service manager
# sudo systemctl stop lnbits
# Update LNbits
git pull --rebase
# 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
# 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
# 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
# Reinstall and start
poetry install --only main
poetry run lnbits
```
## Option 2: Install script (on Debian/Ubuntu)
#### 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>
```sh
wget https://raw.githubusercontent.com/lnbits/lnbits/main/lnbits.sh &&
@@ -124,15 +214,23 @@ chmod +x lnbits.sh &&
./lnbits.sh
```
Now visit `0.0.0.0:5000` to make a super-user account.
- You can use `./lnbits.sh` to run, but for more control: `cd lnbits` and use `uv run lnbits` (see Option 2).
`./lnbits.sh` can be used to run, but for more control `cd lnbits` and use `uv run lnbits` (see previous option).
> ![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 3: Nix
</details>
## Option 4: Nix
<details>
<summary><strong>Show Nix instructions</strong> (flakes, cachix, run)</summary>
```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
sh <(curl --proto '=https' --tlsv1.2 -L https://nixos.org/nix/install) --daemon --yes
# Enable nix-command and flakes experimental features for nix:
grep -qxF 'experimental-features = nix-command flakes' /etc/nix/nix.conf || \
@@ -145,16 +243,29 @@ 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
# Clone and build LNbits
git clone https://github.com/lnbits/lnbits.git
cd lnbits
# Build LNbits
nix build
mkdir data
```
#### Running the server
@@ -174,26 +285,36 @@ 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
```
## Option 4: Docker
> ![NOTE](https://img.shields.io/badge/NOTE-3b82f6?labelColor=494949)
> **Next steps**
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
Use latest version from Docker Hub.
</details>
## Option 5: Docker
<details>
<summary><strong>Show Docker instructions</strong> (official image, volumes, extensions)</summary>
**Use latest image**
```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 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:
- 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:
```sh
docker run ... -e "LNBITS_EXTENSIONS_PATH='/app/data/extensions'" --volume ${PWD}/data/:/app/data ...
```
Build the image yourself.
**Build image yourself**
```sh
git clone https://github.com/lnbits/lnbits.git
@@ -201,24 +322,39 @@ 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 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:
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**
```sh
docker build --build-arg POETRY_INSTALL_ARGS="-E breez" -t lnbits/lnbits .
```
## Option 5: Fly.io
> ![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)**
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.
</details>
First, sign up for an account at [Fly.io](https://fly.io) (no credit card required).
## Option 6: Fly.io
Then, install the Fly.io CLI onto your device [here](https://fly.io/docs/getting-started/installing-flyctl/).
<details>
<summary><strong>Deploy LNbits on Fly.io (free tier friendly)</summary>
After install is complete, the command will output a command you should copy/paste/run to get `fly` into your `$PATH`. Something like:
**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/)).
```
flyctl was installed successfully to /home/ubuntu/.fly/bin/flyctl
@@ -227,9 +363,9 @@ Manually add the directory to your $HOME/.bash_profile (or similar)
export PATH="$FLYCTL_INSTALL/bin:$PATH"
```
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`.
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`.
Once installed, run the following commands.
- Once installed, run the following commands.
```
git clone https://github.com/lnbits/lnbits.git
@@ -243,9 +379,16 @@ 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.
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"`.
> ![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: 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>`.
> ![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>`
```
...
@@ -300,26 +443,49 @@ sudo apt install python3.10-dev gcc build-essential
poetry add setuptools wheel
```
### Optional: PostgreSQL database
> ![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)**
If you want to use LNbits at scale, we recommend using PostgreSQL as the backend database. Install Postgres and setup a database for LNbits:
## Troubleshooting
```sh
# on debian/ubuntu 'sudo apt-get -y install postgresql'
# or follow instructions at https://www.postgresql.org/download/linux/
sudo apt install pkg-config libffi-dev libpq-dev
# Postgres doesn't have a default password, so we'll create one.
# 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
sudo -i -u postgres
psql
# on psql
ALTER USER postgres PASSWORD 'myPassword'; # choose whatever password you want
# in psql
ALTER USER postgres PASSWORD 'myPassword';
\q
# on postgres user
# back as postgres user
createdb lnbits
exit
```
You need to edit the `.env` file.
**Configure LNbits**
```sh
# add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL=
@@ -330,44 +496,189 @@ LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost:5432/lnbits"
# Using LNbits
Now you can visit your LNbits at http://localhost:5000/.
Visit **[http://localhost:5000/](http://localhost:5000/)** (or `0.0.0.0:5000`).
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.
### Option A — First-run setup in the Browser (UI)
Then you can restart it and it will be using the new settings.
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.
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.
> [!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)
Take a look at [Polar](https://lightningpolar.com/) for an excellent way of spinning up a Lightning Network dev environment.
### 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/
# Additional guides
## SQLite to PostgreSQL migration
## Update LNbits (all methods)
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.
> After updating, open **Admin UI → Extensions → “Update All”** to make sure extensions match the core version.
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:
<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.
```sh
# STOP LNbits
# 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
# Edit .env with Postgres URL
LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits"
# save and exit
# START LNbits
# STOP LNbits
# START then STOP LNbits once to apply schema
uv run python tools/conv.py
# or
make migration
```
Hopefully, everything works and get migrated... Launch LNbits again and check if everything is working properly.
- Launch LNbits again and verify.
## LNbits as a systemd 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:
Create `/etc/systemd/system/lnbits.service`:
```
# Systemd unit for lnbits
@@ -375,17 +686,14 @@ Systemd is great for taking care of your LNbits instance. It will start it on bo
[Unit]
Description=LNbits
# 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)
# Optional: start after your backend
#Wants=lnd.service
#After=lnd.service
[Service]
# replace with the absolute path of your lnbits installation
WorkingDirectory=/home/lnbits/lnbits
# same here. run `which uv` if you can't find the poetry binary
# Find uv path via `which uv`
ExecStart=/home/lnbits/.local/bin/uv run lnbits
# replace with the user that you're running lnbits on
User=lnbits
Restart=always
TimeoutSec=120
@@ -396,33 +704,23 @@ Environment=PYTHONUNBUFFERED=1
WantedBy=multi-user.target
```
Save the file and run the following commands:
Enable & start:
```sh
sudo systemctl enable lnbits.service
sudo systemctl start lnbits.service
```
## Reverse proxy with automatic HTTPS using Caddy
## Reverse proxy with automatic HTTPS (Caddy)
Use Caddy to make your LNbits install accessible over clearnet with a domain and https cert.
Point your domain A-record to your server IP. Install Caddy: [Caddy install guide](https://caddyserver.com/docs/install#debian-ubuntu-raspbian)
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
```
```sh
sudo caddy stop
```
Create a Caddyfile
```
sudo nano Caddyfile
```
Assuming your LNbits is running on port `5000` add:
Add:
```
yourdomain.com {
@@ -432,28 +730,21 @@ yourdomain.com {
}
```
Save and exit `CTRL + x`
Save (Ctrl+X) and start:
```
```sh
sudo caddy start
```
## Running behind an Apache2 reverse proxy over HTTPS
Install Apache2 and enable Apache2 mods:
## Apache2 reverse proxy over HTTPS
```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 an Apache2 vhost at: `/etc/apache2/sites-enabled/lnbits.conf`:
Create `/etc/apache2/sites-enabled/lnbits.conf`:
```sh
cat <<EOF > /etc/apache2/sites-enabled/lnbits.conf
@@ -480,27 +771,20 @@ cat <<EOF > /etc/apache2/sites-enabled/lnbits.conf
EOF
```
Restart Apache2:
Restart:
```sh
service apache2 restart
```
## Running behind an Nginx reverse proxy over HTTPS
Install nginx:
## Nginx reverse proxy over HTTPS
```sh
apt-get install nginx certbot
```
Create a SSL certificate with LetsEncrypt:
```sh
certbot certonly --nginx --agree-tos -d lnbits.org
```
Create an nginx vhost at `/etc/nginx/sites-enabled/lnbits.org`:
Create `/etc/nginx/sites-enabled/lnbits.org`:
```sh
cat <<EOF > /etc/nginx/sites-enabled/lnbits.org
@@ -534,23 +818,22 @@ server {
EOF
```
Restart nginx:
Restart:
```sh
service nginx restart
```
## Using https without reverse proxy
---
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.
## HTTPS without a reverse proxy (self-signed)
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.
Create a self-signed cert (useful for local/dev). Browsers wont trust it by default.
#### Install mkcert
### Install mkcert
You can find the install instructions for `mkcert` [here](https://github.com/FiloSottile/mkcert).
Install mkcert on Ubuntu:
- Install instructions: [mkcert README](https://github.com/FiloSottile/mkcert)
- Ubuntu example:
```sh
sudo apt install libnss3-tools
@@ -559,70 +842,47 @@ chmod +x mkcert-v*-linux-amd64
sudo cp mkcert-v*-linux-amd64 /usr/local/bin/mkcert
```
#### Create certificate
### Create certificate
To create a certificate, first `cd` into your LNbits folder and execute the following command on Linux:
**OpenSSL**
```sh
openssl req -new -newkey rsa:4096 -x509 -sha256 -days 3650 -nodes -out cert.pem -keyout key.pem
```
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/)):
**mkcert** (alternative)
```sh
# add your local IP (192.x.x.x) as well if you want to use it in your local network
# include your local IP (e.g., 192.x.x.x) if needed
mkcert localhost 127.0.0.1 ::1
```
You can then pass the certificate files to uvicorn when you start LNbits:
**Run with certs**
```sh
poetry run uvicorn lnbits.__main__:app --host 0.0.0.0 --port 5000 --ssl-keyfile ./key.pem --ssl-certfile ./cert.pem
```
## LNbits running on Umbrel behind Tor
## LNbits on Umbrel behind Tor
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.
See this community [guide](https://community.getumbrel.com/t/guide-lnbits-without-tor/604).
## Docker installation
## FreeBSD notes
To install using docker you first need to build the docker image as:
Issue with secp256k1 0.14.0 on FreeBSD (thanks @GitKalle):
```
git clone https://github.com/lnbits/lnbits.git
cd lnbits
docker build -t lnbits/lnbits .
```
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.
You can launch the docker in a different directory, but make sure to copy `.env.example` from lnbits there
---
```
cp <lnbits_repo>/.env.example .env
```
## Powered by LNbits
and change the configuration in `.env` as required.
LNbits empowers everyone with modular, open-source tools for building Bitcoin-based systems — fast, free, and extendable.
Then create the data directory
If you like this project [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visiting our [Shop](https://shop.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
[![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/)
+127
View File
@@ -0,0 +1,127 @@
<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)
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
# 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/)
+96
View File
@@ -0,0 +1,96 @@
<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)
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
# 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/)
+300 -110
View File
@@ -4,177 +4,367 @@ 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)
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
# Backend wallets
LNbits can run on top of many Lightning Network funding sources with more being added regularly.
**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.
A backend wallet can be configured using the following LNbits environment variables:
**What stays the same when you switch backends**
You can [compare the LNbits compatible Lightning Network funding sources here](wallets.md).
- 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) |
| [Cliche Wallet](#cliche-wallet) | [ZBD](#zbd) | [Phoenixd](#phoenixd) |
| [Breez SDK](#breez-sdk) | [Breez Liquid SDK](#breez-liquid-sdk) | [Nostr Wallet Connect](#nostr-wallet-connect-nwc) |
| [Strike](#strike) | [Eclair (ACINQ)](#eclair-acinq) | [LN.tips](#lntips) |
| [Fake Wallet](#fake-wallet) | | |
---
<a id="clnrest-runes"></a>
### 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)
- `LNBITS_BACKEND_WALLET_CLASS`: **CLNRestWallet**
**Environment variables**
- `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 `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_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_NODEID`: `lightning-cli getinfo | jq -r .id` (only required for v23.08)
### CoreLightning
**Create runes (copy/paste)**
- `LNBITS_BACKEND_WALLET_CLASS`: **CoreLightningWallet**
- `CORELIGHTNING_RPC`: /file/path/lightning-rpc
```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
```
### CoreLightning REST
```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
```
This is the old REST interface that uses [Ride The Lightning/c-lightning-REST](https://github.com/Ride-The-Lightning/c-lightning-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
```
- `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
```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
```
### Spark (Core Lightning)
Set the resulting values into:
- `LNBITS_BACKEND_WALLET_CLASS`: **SparkWallet**
- `SPARK_URL`: http://10.147.17.230:9737/rpc
- `SPARK_TOKEN`: secret_access_key
- `CLNREST_READONLY_RUNE`
- `CLNREST_INVOICE_RUNE`
- `CLNREST_PAY_RUNE`
- `CLNREST_RENEPAY_RUNE`
### LND (REST)
## CoreLightning
- `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
**Required env vars**
or
- `LNBITS_BACKEND_WALLET_CLASS`: `CoreLightningWallet`
- `CORELIGHTNING_RPC`: `/file/path/lightning-rpc`
- `LND_REST_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
## CoreLightning REST
### LND (gRPC)
Old REST interface using [RTL c-lightning-REST](https://github.com/Ride-The-Lightning/c-lightning-REST)
- `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
**Required env vars**
You can also use an AES-encrypted macaroon (more info) instead by using
- `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`
- `LND_GRPC_MACAROON_ENCRYPTED`: eNcRyPtEdMaCaRoOn
## Spark (Core Lightning)
To encrypt your macaroon, run `uv run lnbits-cli encrypt macaroon`.
**Required env vars**
### LNbits
- `LNBITS_BACKEND_WALLET_CLASS`: `SparkWallet`
- `SPARK_URL`: `http://10.147.17.230:9737/rpc`
- `SPARK_TOKEN`: `secret_access_key`
- `LNBITS_BACKEND_WALLET_CLASS`: **LNbitsWallet**
- `LNBITS_ENDPOINT`: e.g. https://lnbits.com
- `LNBITS_KEY`: lnbitsAdminKey
## LND (REST)
### LNPay
**Required env vars**
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.
- `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`: **LNPayWallet**
- `LNPAY_API_ENDPOINT`: https://api.lnpay.co/v1/
- `LNPAY_API_KEY`: sak_apiKey
- `LNPAY_WALLET_KEY`: waka_apiKey
or:
### OpenNode
- `LND_REST_MACAROON_ENCRYPTED`: `eNcRyPtEdMaCaRoOn`
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook setting is necessary.
## LND (gRPC)
- `LNBITS_BACKEND_WALLET_CLASS`: **OpenNodeWallet**
- `OPENNODE_API_ENDPOINT`: https://api.opennode.com/
- `OPENNODE_KEY`: opennodeAdminApiKey
**Required env vars**
### Blink
- `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
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```
You can also use an AES-encrypted macaroon instead:
- `LNBITS_BACKEND_WALLET_CLASS`: **BlinkWallet**
- `BLINK_API_ENDPOINT`: https://api.blink.sv/graphql
- `BLINK_WS_ENDPOINT`: wss://ws.blink.sv/graphql
- `BLINK_TOKEN`: BlinkToken
- `LND_GRPC_MACAROON_ENCRYPTED`: `eNcRyPtEdMaCaRoOn`
### Alby
To encrypt your macaroon:
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
```bash
uv run lnbits-cli encrypt macaroon
```
- `LNBITS_BACKEND_WALLET_CLASS`: **AlbyWallet**
- `ALBY_API_ENDPOINT`: https://api.getalby.com/
- `ALBY_ACCESS_TOKEN`: AlbyAccessToken
## LNbits
### 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`: `LNbitsWallet`
- `LNBITS_ENDPOINT`: for example `https://lnbits.com`
- `LNBITS_KEY`: `lnbitsAdminKey`
- `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
## LNPay
### ZBD
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).
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
**Required env vars**
- `LNBITS_BACKEND_WALLET_CLASS`: **ZBDWallet**
- `ZBD_API_ENDPOINT`: https://api.zebedee.io/v0/
- `ZBD_API_KEY`: ZBDApiKey
- `LNBITS_BACKEND_WALLET_CLASS`: `LNPayWallet`
- `LNPAY_API_ENDPOINT`: `https://api.lnpay.co/v1/`
- `LNPAY_API_KEY`: `sak_apiKey`
- `LNPAY_WALLET_KEY`: `waka_apiKey`
### Phoenixd
## OpenNode
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.
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook configuration required.
- `LNBITS_BACKEND_WALLET_CLASS`: **PhoenixdWallet**
- `PHOENIXD_API_ENDPOINT`: http://localhost:9740/
- `PHOENIXD_API_PASSWORD`: PhoenixdApiPassword
**Required env vars**
### Breez SDK
- `LNBITS_BACKEND_WALLET_CLASS`: `OpenNodeWallet`
- `OPENNODE_API_ENDPOINT`: `https://api.opennode.com/`
- `OPENNODE_KEY`: `opennodeAdminApiKey`
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.
## Blink
- `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
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook configuration required.
### Breez Liquid SDK
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)
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.
**Required env vars**
- `LNBITS_BACKEND_WALLET_CLASS`: **BreezLiquidSdkWallet**
- `BREEZ_LIQUID_SEED`: ...
- `LNBITS_BACKEND_WALLET_CLASS`: `BlinkWallet`
- `BLINK_API_ENDPOINT`: `https://api.blink.sv/graphql`
- `BLINK_WS_ENDPOINT`: `wss://ws.blink.sv/graphql`
- `BLINK_TOKEN`: `BlinkToken`
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:
## Alby
- `LNBITS_RESERVE_FEE_MIN`: ...
- `LNBITS_RESERVE_FEE_PERCENT`: ...
For the invoice to work you must have a publicly accessible URL in your LNbits. No manual webhook configuration required.
### Cliche Wallet
Generate an Alby access token here: [https://getalby.com/developer/access_tokens/new](https://getalby.com/developer/access_tokens/new)
- `CLICHE_ENDPOINT`: ws://127.0.0.1:12000
**Required env vars**
### Nostr Wallet Connect (NWC)
- `LNBITS_BACKEND_WALLET_CLASS`: `AlbyWallet`
- `ALBY_API_ENDPOINT`: `https://api.getalby.com/`
- `ALBY_ACCESS_TOKEN`: `AlbyAccessToken`
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).
## Boltz
You can configure Nostr Wallet Connect in the admin ui or using the following environment variables:
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.
- `LNBITS_BACKEND_WALLET_CLASS`: **NWCWallet**
- `NWC_PAIRING_URL`: **nostr+walletconnect://...your...pairing...secret...**
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/)
+67 -45
View File
@@ -1,53 +1,75 @@
#!/bin/bash
#!/usr/bin/env bash
set -euo pipefail
# Check install has not already run
if [ ! -d lnbits/data ]; then
# --- 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}"
# -------------------------------
# Update package list and install prerequisites non-interactively
sudo apt update -y
sudo apt install -y software-properties-common
export DEBIAN_FRONTEND=noninteractive
# Add the deadsnakes PPA repository non-interactively
sudo add-apt-repository -y ppa:deadsnakes/ppa
# Install Python 3.10 and distutils non-interactively
sudo apt install -y python3.10 python3.10-distutils
# Install UV
curl -LsSf https://astral.sh/uv/install.sh | sh
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; }
# 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
# Install the dependencies using UV
uv sync --all-extras
# 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 uv (if missing)
if ! command -v uv >/dev/null 2>&1; then
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"
fi
# Set environment variables for LNbits
export LNBITS_ADMIN_UI=true
export HOST=0.0.0.0
# Clone or reuse repo
if [[ ! -d "$APP_DIR/.git" ]]; then
git clone "$REPO_URL" "$APP_DIR"
fi
# Run LNbits
uv run lnbits
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
# 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
+8 -1
View File
@@ -68,7 +68,7 @@ class Payment(BaseModel):
amount: int
fee: int
bolt11: str
# payment_request: str | None
payment_request: str | None = Field(default=None, no_database=True)
fiat_provider: str | None = None
status: str = PaymentState.PENDING
memo: str | None = None
@@ -83,6 +83,13 @@ class Payment(BaseModel):
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
extra: dict = {}
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:
return self.status == PaymentState.PENDING.value
+68 -88
View File
@@ -5,7 +5,7 @@ import time
from loguru import logger
from lnbits.core.crud import get_wallet
from lnbits.core.crud.payments import create_payment, get_standalone_payment
from lnbits.core.crud.payments import create_payment
from lnbits.core.models import CreatePayment, Payment, PaymentState
from lnbits.core.models.misc import SimpleStatus
from lnbits.db import Connection
@@ -27,6 +27,73 @@ async def handle_fiat_payment_confirmation(
logger.warning(e)
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}.",
)
async def _credit_fiat_service_fee_wallet(
payment: Payment, conn: Connection | None = None
):
@@ -104,90 +171,3 @@ 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 -1
View File
@@ -110,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.")
-14
View File
@@ -1,6 +1,4 @@
import asyncio
import traceback
from collections.abc import Callable, Coroutine
from loguru import logger
@@ -27,7 +25,6 @@ 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()
@@ -162,14 +159,3 @@ 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())
@@ -103,10 +103,10 @@
<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>
- the user completed the checkout process
<li><code>invoice.paid</code></li>
- the invoice was successfully paid (for subscriptions)
</ul>
</q-card-section>
</q-expansion-item>
@@ -106,6 +106,28 @@
:form-data="formData"
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
/>
<div class="row q-col-gutter-md q-my-md">
<div class="col-12 col-sm-8">
<q-item tag="div">
<q-item-section>
<q-item-label
v-text="$t('funding_source_retries')"
></q-item-label>
<q-item-label
caption
v-text="$t('funding_source_retries_desc')"
></q-item-label>
</q-item-section>
<q-item-section>
<q-input
filled
v-model="formData.funding_source_max_retries"
type="number"
/>
</q-item-section>
</q-item>
</div>
</div>
</div>
<q-separator></q-separator>
<h6 class="q-mt-lg q-mb-sm">
@@ -76,19 +76,25 @@
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">
+1 -1
View File
@@ -4,7 +4,7 @@
<!---->
{% block scripts %} {{ window_vars(user, wallet) }}{% endblock %} {% block page
%}
<div class="row q-col-gutter-md">
<div class="row q-col-gutter-md" style="margin-bottom: 6rem">
{% if HIDE_API and AD_SPACE_ENABLED and AD_SPACE %}
<div class="col-12 col-md-8 q-gutter-y-md">
{% elif HIDE_API %}
+36 -13
View File
@@ -116,19 +116,6 @@
>
<q-tooltip>Delete Wallet</q-tooltip>
</q-btn>
</q-td>
<q-td auto-width>
<q-btn
icon="link"
size="sm"
flat
class="cursor-pointer q-mr-xs"
@click="copyWalletLink(props.row.id)"
>
<q-tooltip>Copy Wallet Link</q-tooltip>
</q-btn>
<span v-text="props.row.name"></span>
<q-btn
round
v-if="props.row.deleted"
@@ -140,6 +127,42 @@
>
<q-tooltip>Undelete Wallet</q-tooltip>
</q-btn>
<q-btn
icon="link"
size="sm"
flat
class="cursor-pointer q-mr-xs"
@click="copyWalletLink(props.row.id)"
>
<q-tooltip>Copy Wallet Link</q-tooltip>
</q-btn>
</q-td>
<q-td auto-width>
<span
v-text="props.row.name"
v-if="!props.row.editable"
:class="props.row.deleted ? 'text-strike' : 'cursor-pointer'"
@click="props.row.editable = true && !props.row.deleted"
></span>
<q-input
v-else
@keydown.enter="updateWallet(props.row)"
v-model="props.row.name"
size="xs"
flat
dense
>
<template v-slot:append>
<q-btn
@click="updateWallet(props.row)"
round
dense
size="xs"
flat
icon="send"
/>
</template>
</q-input>
</q-td>
<q-td auto-width>
<q-btn
+2 -1
View File
@@ -113,8 +113,9 @@ async def api_fiat_as_sats(data: ConversionData):
return output
@api_router.get("/api/v1/qrcode", response_class=StreamingResponse)
@api_router.get("/api/v1/qrcode/{data}", response_class=StreamingResponse)
async def img(data):
async def img(data: str):
qr = pyqrcode.create(data)
stream = BytesIO()
qr.svg(stream, scale=3)
+112 -2
View File
@@ -1,10 +1,18 @@
from fastapi import APIRouter, Request
import json
from fastapi import APIRouter, Request
from loguru import logger
from lnbits.core.crud.payments import (
get_standalone_payment,
)
from lnbits.core.models.misc import SimpleStatus
from lnbits.core.models.payments import CreateInvoice
from lnbits.core.services.fiat_providers import (
check_stripe_signature,
handle_stripe_event,
)
from lnbits.core.services.payments import create_fiat_invoice
from lnbits.fiat.base import FiatSubscriptionPaymentOptions
from lnbits.settings import settings
callback_router = APIRouter(prefix="/api/v1/callback", tags=["callback"])
@@ -33,3 +41,105 @@ async def api_generic_webhook_handler(
success=False,
message=f"Unknown fiat provider '{provider_name}'.",
)
async def handle_stripe_event(event: dict):
event_id = event.get("id")
event_type = event.get("type")
if event_type == "checkout.session.completed":
await _handle_stripe_checkout_session_completed(event)
elif event_type == "invoice.paid":
await _handle_stripe_subscription_invoice_paid(event)
else:
logger.info(
f"Unhandled Stripe event type: '{event_type}'." f" Event ID: '{event_id}'."
)
async def _handle_stripe_checkout_session_completed(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")
lnbits_action = event_object.get("metadata", {}).get("lnbits_action")
logger.debug(
f"Handling Stripe event: '{event_id}'. Type: '{object_type}'."
f" Payment hash: '{payment_hash}'."
)
if lnbits_action != "invoice":
logger.warning(f"Stripe event is not an invoice: '{lnbits_action}'.")
return
if not payment_hash:
raise ValueError("Stripe event does not contain a payment hash.")
payment = await get_standalone_payment(payment_hash)
if not payment:
raise ValueError(f"No payment found for hash: '{payment_hash}'.")
await payment.check_fiat_status()
async def _handle_stripe_subscription_invoice_paid(event: dict):
invoice = event.get("data", {}).get("object", {})
parent = invoice.get("parent", {})
currency = invoice.get("currency", "").upper()
if not currency:
raise ValueError("Stripe invoice.paid event missing 'currency'.")
amount_paid = invoice.get("amount_paid")
if not amount_paid:
raise ValueError("Stripe invoice.paid event missing 'amount_paid'.")
payment_options = await _get_stripe_subscription_payment_options(parent)
if not payment_options.wallet_id:
raise ValueError("Stripe invoice.paid event missing 'wallet_id' in metadata.")
memo = " | ".join(
[i.get("description", "") for i in invoice.get("lines", {}).get("data", [])]
+ [payment_options.memo or "", invoice.get("customer_email", "")]
)
extra = {
**(payment_options.extra or {}),
"fiat_method": "subscription",
"tag": payment_options.tag,
"subscription": {
"checking_id": invoice.get("id"),
"payment_request": invoice.get("hosted_invoice_url"),
},
}
payment = await create_fiat_invoice(
wallet_id=payment_options.wallet_id,
invoice_data=CreateInvoice(
unit=currency,
amount=amount_paid / 100, # convert cents to dollars
memo=memo,
extra=extra,
fiat_provider="stripe",
),
)
await payment.check_fiat_status()
async def _get_stripe_subscription_payment_options(
parent: dict,
) -> FiatSubscriptionPaymentOptions:
if not parent or not parent.get("type") == "subscription_details":
raise ValueError("Stripe invoice.paid event does not contain a subscription.")
metadata = parent.get("subscription_details", {}).get("metadata", {})
if metadata.get("lnbits_action") != "subscription":
raise ValueError("Stripe invoice.paid metadata action is not 'subscription'.")
if "extra" in metadata:
try:
metadata["extra"] = json.loads(metadata["extra"])
except json.JSONDecodeError as exc:
logger.warning(exc)
metadata["extra"] = {}
return FiatSubscriptionPaymentOptions(**metadata)
+53 -4
View File
@@ -3,9 +3,11 @@ from http import HTTPStatus
from fastapi import APIRouter, Depends, HTTPException
from lnbits.core.models.misc import SimpleStatus
from lnbits.core.models.wallets import WalletTypeInfo
from lnbits.core.services.fiat_providers import test_connection
from lnbits.decorators import check_admin
from lnbits.decorators import check_admin, require_admin_key
from lnbits.fiat import StripeWallet, get_fiat_provider
from lnbits.fiat.base import CreateFiatSubscription, FiatSubscriptionResponse
fiat_router = APIRouter(tags=["Fiat API"], prefix="/api/v1/fiat")
@@ -19,20 +21,67 @@ async def api_test_fiat_provider(provider: str) -> SimpleStatus:
return await test_connection(provider)
@fiat_router.post(
"/{provider}/subscription",
status_code=HTTPStatus.OK,
)
async def create_subscription(
provider: str,
data: CreateFiatSubscription,
key_type: WalletTypeInfo = Depends(require_admin_key),
) -> FiatSubscriptionResponse:
fiat_provider = await get_fiat_provider(provider)
if not fiat_provider:
raise HTTPException(404, "Fiat provider not found")
wallet_id = data.payment_options.wallet_id
if wallet_id and wallet_id != key_type.wallet.id:
raise HTTPException(
403,
"Wallet id does not match your API key."
"Leave it empty to use your key's wallet.",
)
data.payment_options.wallet_id = key_type.wallet.id
subscription_response = await fiat_provider.create_subscription(
data.subscription_id, data.quantity, data.payment_options
)
return subscription_response
@fiat_router.delete(
"/{provider}/subscription/{subscription_id}",
status_code=HTTPStatus.OK,
)
async def cancel_subscription(
provider: str,
subscription_id: str,
key_type: WalletTypeInfo = Depends(require_admin_key),
) -> FiatSubscriptionResponse:
fiat_provider = await get_fiat_provider(provider)
if not fiat_provider:
raise HTTPException(404, "Fiat provider not found")
resp = await fiat_provider.cancel_subscription(subscription_id, key_type.wallet.id)
return resp
@fiat_router.post(
"/{provider}/connection_token",
status_code=HTTPStatus.OK,
dependencies=[Depends(check_admin)],
)
async def connection_token(provider: str):
provider_wallet = await get_fiat_provider(provider)
fiat_provider = await get_fiat_provider(provider)
if provider == "stripe":
if not isinstance(provider_wallet, StripeWallet):
if not isinstance(fiat_provider, StripeWallet):
raise HTTPException(
status_code=500, detail="Stripe wallet/provider not configured"
)
try:
tok = await provider_wallet.create_terminal_connection_token()
tok = await fiat_provider.create_terminal_connection_token()
secret = tok.get("secret")
if not secret:
raise HTTPException(
+90 -85
View File
@@ -48,6 +48,96 @@ async def home(request: Request, lightning: str = ""):
)
@generic_router.get(
"/account",
response_class=HTMLResponse,
description="show account page",
)
async def account(
request: Request,
user: User = Depends(check_user_exists),
):
nostr_configured = settings.is_nostr_notifications_configured()
telegram_configured = settings.is_telegram_notifications_configured()
return template_renderer().TemplateResponse(
request,
"core/account.html",
{
"user": user.json(),
"nostr_configured": nostr_configured,
"telegram_configured": telegram_configured,
"ajax": _is_ajax_request(request),
},
)
@generic_router.get(
"/wallet",
response_class=HTMLResponse,
description="show wallet page",
)
async def get_user_wallet(
request: Request,
lnbits_last_active_wallet: Annotated[str | None, Cookie()] = None,
user: User = Depends(check_user_exists),
wal: UUID4 | None = Query(None),
):
if wal:
wallet = await get_wallet(wal.hex)
elif len(user.wallets) == 0:
wallet = await create_wallet(user_id=user.id)
user.wallets.append(wallet)
elif lnbits_last_active_wallet and user.get_wallet(lnbits_last_active_wallet):
wallet = await get_wallet(lnbits_last_active_wallet)
else:
wallet = user.wallets[0]
if not wallet or wallet.deleted:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Wallet not found",
)
if wallet.user != user.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Not your wallet.",
)
context = {
"user": user.json(),
"wallet": wallet.json(),
"wallet_name": wallet.name,
"currencies": allowed_currencies(),
"service_fee": settings.lnbits_service_fee,
"service_fee_max": settings.lnbits_service_fee_max,
"web_manifest": f"/manifest/{user.id}.webmanifest",
}
return template_renderer().TemplateResponse(
request,
"core/wallet.html",
{**context, "ajax": _is_ajax_request(request)},
)
@generic_router.get(
"/wallets",
response_class=HTMLResponse,
description="show wallets page",
)
async def wallets(
request: Request,
user: User = Depends(check_user_exists),
):
return template_renderer().TemplateResponse(
request,
"core/wallets.html",
{
"user": user.json(),
"ajax": _is_ajax_request(request),
},
)
@generic_router.get("/first_install", response_class=HTMLResponse)
async def first_install(request: Request):
if not settings.first_install:
@@ -243,91 +333,6 @@ async def extensions_builder_preview(
return response
@generic_router.get(
"/wallet",
response_class=HTMLResponse,
description="show wallet page",
)
async def wallet(
request: Request,
lnbits_last_active_wallet: Annotated[str | None, Cookie()] = None,
user: User = Depends(check_user_exists),
wal: UUID4 | None = Query(None),
):
if wal:
wallet = await get_wallet(wal.hex)
elif len(user.wallets) == 0:
wallet = await create_wallet(user_id=user.id)
user.wallets.append(wallet)
elif lnbits_last_active_wallet and user.get_wallet(lnbits_last_active_wallet):
wallet = await get_wallet(lnbits_last_active_wallet)
else:
wallet = user.wallets[0]
if not wallet or wallet.deleted:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Wallet not found",
)
context = {
"user": user.json(),
"wallet": wallet.json(),
"wallet_name": wallet.name,
"currencies": allowed_currencies(),
"service_fee": settings.lnbits_service_fee,
"service_fee_max": settings.lnbits_service_fee_max,
"web_manifest": f"/manifest/{user.id}.webmanifest",
}
return template_renderer().TemplateResponse(
request,
"core/wallet.html",
{**context, "ajax": _is_ajax_request(request)},
)
@generic_router.get(
"/account",
response_class=HTMLResponse,
description="show account page",
)
async def account(
request: Request,
user: User = Depends(check_user_exists),
):
nostr_configured = settings.is_nostr_notifications_configured()
telegram_configured = settings.is_telegram_notifications_configured()
return template_renderer().TemplateResponse(
request,
"core/account.html",
{
"user": user.json(),
"nostr_configured": nostr_configured,
"telegram_configured": telegram_configured,
"ajax": _is_ajax_request(request),
},
)
@generic_router.get(
"/wallets",
response_class=HTMLResponse,
description="show wallets page",
)
async def wallets(
request: Request,
user: User = Depends(check_user_exists),
):
return template_renderer().TemplateResponse(
request,
"core/wallets.html",
{
"user": user.json(),
"ajax": _is_ajax_request(request),
},
)
@generic_router.get("/service-worker.js")
async def service_worker(request: Request):
return template_renderer().TemplateResponse(
+8 -2
View File
@@ -424,6 +424,7 @@ class Operator(Enum):
LE = "le"
INCLUDE = "in"
EXCLUDE = "ex"
LIKE = "like"
@property
def as_sql(self):
@@ -443,6 +444,8 @@ class Operator(Enum):
return ">="
elif self == Operator.LE:
return "<="
elif self == Operator.LIKE:
return "LIKE"
else:
raise ValueError("Unknown SQL Operator")
@@ -499,7 +502,7 @@ class Filter(BaseModel, Generic[TFilterModel]):
return cls(field=field, op=op, values=values, model=model)
@property
def statement(self):
def statement(self) -> str:
stmt = []
for key in self.values.keys() if self.values else []:
clean_key = key.split("__")[0]
@@ -582,7 +585,10 @@ class Filters(BaseModel, Generic[TFilterModel]):
for page_filter in self.filters:
if page_filter.values:
for key, value in page_filter.values.items():
values[key] = value
if page_filter.op == Operator.LIKE:
values[key] = f"%{value}%"
else:
values[key] = value
if self.search and self.model:
values["search"] = f"%{self.search.lower()}%"
return values
+76
View File
@@ -4,6 +4,8 @@ from abc import ABC, abstractmethod
from collections.abc import AsyncGenerator, Coroutine
from typing import TYPE_CHECKING, Any, NamedTuple
from pydantic import BaseModel, Field
if TYPE_CHECKING:
pass
@@ -76,6 +78,54 @@ class FiatPaymentStatus(NamedTuple):
return "pending"
class FiatSubscriptionPaymentOptions(BaseModel):
memo: str | None = Field(
default=None,
description="Payments created by the recurring subscription"
" will have this memo.",
)
wallet_id: str | None = Field(
default=None,
description="Payments created by the recurring subscription"
" will be made to this wallet.",
)
subscription_request_id: str | None = Field(
default=None,
description="Unique ID that can be used to identify the subscription request."
"If not provided, one will be generated.",
)
tag: str | None = Field(
default=None,
description="Payments created by the recurring subscription"
" will have this tag. Admin only.",
)
extra: dict[str, Any] | None = Field(
default=None,
description="Payments created by the recurring subscription"
" will merge this extra data to the payment extra. Admin only.",
)
success_url: str | None = Field(
default="https://my.lnbits.com",
description="The URL to redirect the user to after the"
" subscription is successfully created.",
)
class CreateFiatSubscription(BaseModel):
subscription_id: str
quantity: int
payment_options: FiatSubscriptionPaymentOptions
class FiatSubscriptionResponse(BaseModel):
ok: bool = True
subscription_request_id: str | None = None
checkout_session_url: str | None = None
error_message: str | None = None
class FiatPaymentSuccessStatus(FiatPaymentStatus):
paid = True
@@ -111,6 +161,32 @@ class FiatProvider(ABC):
) -> Coroutine[None, None, FiatInvoiceResponse]:
pass
@abstractmethod
def create_subscription(
self,
subscription_id: str,
quantity: int,
payment_options: FiatSubscriptionPaymentOptions,
**kwargs,
) -> Coroutine[None, None, FiatSubscriptionResponse]:
pass
@abstractmethod
def cancel_subscription(
self,
subscription_id: str,
correlation_id: str,
**kwargs,
) -> Coroutine[None, None, FiatSubscriptionResponse]:
"""
Cancel a subscription.
Args:
subscription_id: The ID of the subscription to cancel.
correlation_id: An identifier used to verify that the subscription belongs
to the user that made the request. Usually the wallet ID.
"""
pass
@abstractmethod
def pay_invoice(
self,
+159 -9
View File
@@ -1,5 +1,6 @@
import asyncio
import json
import uuid
from collections.abc import AsyncGenerator
from datetime import datetime, timedelta, timezone
from typing import Any, Literal
@@ -9,7 +10,7 @@ import httpx
from loguru import logger
from pydantic import BaseModel, Field, ValidationError
from lnbits.helpers import normalize_endpoint
from lnbits.helpers import normalize_endpoint, urlsafe_short_hash
from lnbits.settings import settings
from .base import (
@@ -21,9 +22,11 @@ from .base import (
FiatPaymentSuccessStatus,
FiatProvider,
FiatStatusResponse,
FiatSubscriptionPaymentOptions,
FiatSubscriptionResponse,
)
FiatMethod = Literal["checkout", "terminal"]
FiatMethod = Literal["checkout", "terminal", "subscription"]
class StripeTerminalOptions(BaseModel):
@@ -43,6 +46,14 @@ class StripeCheckoutOptions(BaseModel):
line_item_name: str | None = None
class StripeSubscriptionOptions(BaseModel):
class Config:
extra = "ignore"
checking_id: str | None = None
payment_request: str | None = None
class StripeCreateInvoiceOptions(BaseModel):
class Config:
extra = "ignore"
@@ -50,6 +61,7 @@ class StripeCreateInvoiceOptions(BaseModel):
fiat_method: FiatMethod = "checkout"
terminal: StripeTerminalOptions | None = None
checkout: StripeCheckoutOptions | None = None
subscription: StripeSubscriptionOptions | None = None
class StripeWallet(FiatProvider):
@@ -118,17 +130,125 @@ class StripeWallet(FiatProvider):
if opts.fiat_method == "checkout":
return await self._create_checkout_invoice(
amount_cents, currency, payment_hash, memo, opts
amount_cents, currency, payment_hash, memo, opts.checkout
)
if opts.fiat_method == "terminal":
return await self._create_terminal_invoice(
amount_cents, currency, payment_hash, opts
amount_cents, currency, payment_hash, opts.terminal
)
if opts.fiat_method == "subscription":
return self._create_subscription_invoice(opts.subscription)
return FiatInvoiceResponse(
ok=False, error_message=f"Unsupported fiat_method: {opts.fiat_method}"
)
async def create_subscription(
self,
subscription_id: str,
quantity: int,
payment_options: FiatSubscriptionPaymentOptions,
**kwargs,
) -> FiatSubscriptionResponse:
success_url = (
payment_options.success_url
or settings.stripe_payment_success_url
or "https://lnbits.com"
)
if not payment_options.subscription_request_id:
payment_options.subscription_request_id = str(uuid.uuid4())
payment_options.extra = payment_options.extra or {}
payment_options.extra["subscription_request_id"] = (
payment_options.subscription_request_id
)
form_data: list[tuple[str, str]] = [
("mode", "subscription"),
("success_url", success_url),
("line_items[0][price]", subscription_id),
("line_items[0][quantity]", f"{quantity}"),
]
subscription_data = {**payment_options.dict(), "lnbits_action": "subscription"}
subscription_data["extra"] = json.dumps(subscription_data.get("extra") or {})
form_data += self._encode_metadata(
"subscription_data[metadata]",
subscription_data,
)
try:
r = await self.client.post(
"/v1/checkout/sessions",
headers=self._build_headers_form(),
content=urlencode(form_data),
)
r.raise_for_status()
data = r.json()
url = data.get("url")
if not url:
return FiatSubscriptionResponse(
ok=False, error_message="Server error: missing url"
)
return FiatSubscriptionResponse(
ok=True,
checkout_session_url=url,
subscription_request_id=payment_options.subscription_request_id,
)
except json.JSONDecodeError as exc:
logger.warning(exc)
return FiatSubscriptionResponse(
ok=False, error_message="Server error: invalid json response"
)
except Exception as exc:
logger.warning(exc)
return FiatSubscriptionResponse(
ok=False, error_message=f"Unable to connect to {self.endpoint}."
)
async def cancel_subscription(
self,
subscription_id: str,
correlation_id: str,
**kwargs,
) -> FiatSubscriptionResponse:
try:
params = {
"query": f"metadata['wallet_id']:'{correlation_id}'"
" AND "
f"metadata['subscription_request_id']:'{subscription_id}'"
}
r = await self.client.get(
"/v1/subscriptions/search",
params=params,
)
r.raise_for_status()
search_result = r.json()
data = search_result.get("data") or []
if not data or len(data) == 0:
return FiatSubscriptionResponse(
ok=False, error_message="Subscription not found."
)
subscription = data[0]
subscription_id = subscription.get("id")
if not subscription_id:
return FiatSubscriptionResponse(
ok=False, error_message="Subscription ID not found."
)
r = await self.client.delete(f"/v1/subscriptions/{subscription_id}")
r.raise_for_status()
return FiatSubscriptionResponse(ok=True)
except Exception as exc:
logger.warning(exc)
return FiatSubscriptionResponse(
ok=False, error_message="Unable to un subscribe."
)
async def pay_invoice(self, payment_request: str) -> FiatPaymentResponse:
raise NotImplementedError("Stripe does not support paying invoices directly.")
@@ -146,6 +266,11 @@ class StripeWallet(FiatProvider):
r.raise_for_status()
return self._status_from_payment_intent(r.json())
if stripe_id.startswith("in_"):
r = await self.client.get(f"/v1/invoices/{stripe_id}")
r.raise_for_status()
return self._status_from_invoice(r.json())
logger.debug(f"Unknown Stripe id prefix: {checking_id}")
return FiatPaymentPendingStatus()
@@ -176,9 +301,9 @@ class StripeWallet(FiatProvider):
currency: str,
payment_hash: str,
memo: str | None,
opts: StripeCreateInvoiceOptions,
opts: StripeCheckoutOptions | None = None,
) -> FiatInvoiceResponse:
co = opts.checkout or StripeCheckoutOptions()
co = opts or StripeCheckoutOptions()
success_url = (
co.success_url
or settings.stripe_payment_success_url
@@ -190,6 +315,7 @@ class StripeWallet(FiatProvider):
("mode", "payment"),
("success_url", success_url),
("metadata[payment_hash]", payment_hash),
("metadata[lnbits_action]", "invoice"),
("line_items[0][price_data][currency]", currency.lower()),
("line_items[0][price_data][product_data][name]", line_item_name),
("line_items[0][price_data][unit_amount]", str(amount_cents)),
@@ -228,9 +354,9 @@ class StripeWallet(FiatProvider):
amount_cents: int,
currency: str,
payment_hash: str,
opts: StripeCreateInvoiceOptions,
opts: StripeTerminalOptions | None = None,
) -> FiatInvoiceResponse:
term = opts.terminal or StripeTerminalOptions()
term = opts or StripeTerminalOptions()
data: dict[str, str] = {
"amount": str(amount_cents),
"currency": currency.lower(),
@@ -265,6 +391,18 @@ class StripeWallet(FiatProvider):
ok=False, error_message=f"Unable to connect to {self.endpoint}."
)
def _create_subscription_invoice(
self,
opts: StripeSubscriptionOptions | None = None,
) -> FiatInvoiceResponse:
term = opts or StripeSubscriptionOptions()
return FiatInvoiceResponse(
ok=True,
checking_id=term.checking_id or urlsafe_short_hash(),
payment_request=term.payment_request or "",
)
def _normalize_stripe_id(self, checking_id: str) -> str:
"""Remove our internal prefix so Stripe sees a real id."""
return (
@@ -308,6 +446,18 @@ class StripeWallet(FiatProvider):
return FiatPaymentPendingStatus()
def _status_from_invoice(self, invoice: dict) -> FiatPaymentStatus:
"""Map an Invoice to LNbits fiat status."""
status = invoice.get("status")
if status == "paid":
return FiatPaymentSuccessStatus()
if status in ["uncollectible", "void"]:
return FiatPaymentFailedStatus()
return FiatPaymentPendingStatus()
def _build_headers_form(self) -> dict[str, str]:
return {**self.headers, "Content-Type": "application/x-www-form-urlencoded"}
@@ -316,7 +466,7 @@ class StripeWallet(FiatProvider):
) -> list[tuple[str, str]]:
out: list[tuple[str, str]] = []
for k, v in (md or {}).items():
out.append((f"{prefix}[{k}]", str(v)))
out.append((f"{prefix}[{k}]", str(v or "")))
return out
def _parse_create_opts(
+1 -1
View File
@@ -664,10 +664,10 @@ class FundingSourcesSettings(
# How long to wait for the payment to be confirmed before returning a pending status
# It will not fail the payment, it will make it return pending after the timeout
lnbits_funding_source_pay_invoice_wait_seconds: int = Field(default=5, ge=0)
funding_source_max_retries: int = Field(default=4, ge=0)
class FiatProvidersSettings(StripeFiatProvider):
def is_fiat_provider_enabled(self, provider: str | None) -> bool:
"""
Checks if a specific fiat provider is enabled.
+1 -1
View File
File diff suppressed because one or more lines are too long
+4 -1
View File
@@ -667,5 +667,8 @@ window.localisation.en = {
connected: 'Connected',
not_connected: 'Not Connected',
free: 'Free',
paid: 'Paid'
paid: 'Paid',
funding_source_retries: 'Max Retries',
funding_source_retries_desc:
'Maximum number of retries for funding sources, before it falls back to VoidWallet.'
}
+29 -1
View File
@@ -333,8 +333,8 @@ window.UsersPageLogic = {
const url = `${window.location.origin}/wallet?usr=${this.activeWallet.userId}&wal=${walletId}`
this.copyText(url)
},
fetchUsers(props) {
this.relaxFilterForFields(['username', 'email'])
const params = LNbits.utils.prepareFilterQuery(this.usersTable, props)
LNbits.api
.request('GET', `/users/api/v1/user?${params}`)
@@ -355,6 +355,34 @@ window.UsersPageLogic = {
})
.catch(LNbits.utils.notifyApiError)
},
relaxFilterForFields(fieldNames = []) {
fieldNames.forEach(fieldName => {
const fieldValue = this.usersTable?.filter?.[fieldName]
if (fieldValue) {
if (this.usersTable.filter[fieldName]) {
this.usersTable.filter[`${fieldName}[like]`] = fieldValue
delete this.usersTable.filter[fieldName]
}
}
})
},
updateWallet(userWallet) {
LNbits.api
.request('PATCH', '/api/v1/wallet', userWallet.adminkey, {
name: userWallet.name
})
.then(() => {
userWallet.editable = false
Quasar.Notify.create({
message: 'Wallet name updated.',
type: 'positive',
timeout: 3500
})
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
},
toggleAdmin(userId) {
LNbits.api
+54 -15
View File
@@ -1,4 +1,6 @@
import asyncio
import json
import os
import time
from collections.abc import AsyncGenerator
from decimal import Decimal
@@ -110,8 +112,17 @@ class StrikeWallet(Wallet):
),
)
# runtime state
self.pending_invoices: list[str] = [] # Keep it as a list
# path for persisting pending invoices
self.state_path = os.path.join(
settings.lnbits_data_folder, "strike_pending_invoices.json"
)
# load persisted pending invoices
try:
with open(self.state_path) as f:
self.pending_invoices = json.load(f)
except Exception:
self.pending_invoices = []
self.pending_payments: dict[str, str] = {}
self.failed_payments: dict[str, str] = {}
@@ -120,6 +131,13 @@ class StrikeWallet(Wallet):
self._cached_balance_ts: float = 0.0
self._cache_ttl = 30 # seconds
def _persist_pending(self):
try:
with open(self.state_path, "w") as f:
json.dump(self.pending_invoices, f)
except Exception as e:
logger.warning(f"Could not persist pending invoices: {e}")
async def cleanup(self) -> None:
try:
await self.client.aclose()
@@ -235,6 +253,7 @@ class StrikeWallet(Wallet):
)
self.pending_invoices.append(invoice_id)
self._persist_pending()
return InvoiceResponse(
ok=True, checking_id=invoice_id, payment_request=bolt11
)
@@ -371,15 +390,36 @@ class StrikeWallet(Wallet):
logger.debug(f"Error while fetching payment {checking_id}.")
return PaymentPendingStatus()
async def _get_invoices_status_batch(
self, invoice_ids: list[str]
) -> dict[str, PaymentStatus]:
out: dict[str, PaymentStatus] = {}
if not invoice_ids:
return out
ids_list = ",".join(f"'{i}'" for i in invoice_ids)
filter_expr = f"receiveRequestId in ({ids_list})"
params = {"$filter": filter_expr, "$top": len(invoice_ids)}
r = await self._get("/receive-requests/receives", params=params)
r.raise_for_status()
items = r.json().get("items") or r.json().get("value") or []
completed = {item.get("receiveRequestId") for item in items}
for inv in invoice_ids:
out[inv] = (
PaymentSuccessStatus(fee_msat=0)
if inv in completed
else PaymentPendingStatus()
)
return out
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
"""
Poll Strike for invoice settlement while respecting the documented API limits.
Uses dynamic adjustment of polling frequency based on activity.
"""
min_poll, max_poll = 1, 15
min_poll, max_poll = 0.2, 3 # Increase polling frequency (was 1, 15)
# 1,000 requests / 10 minutes = ~100 requests/minute.
rate_limit = 100
rate_limit = 250
sleep_s = min_poll
# Main loop for polling invoices.
self._running = True
@@ -391,19 +431,18 @@ class StrikeWallet(Wallet):
req_budget = max(
1, rate_limit * sleep_s // 60
) # Calculate request budget based on sleep time.
batch = list(self.pending_invoices)[: int(req_budget)]
processed = 0
for inv in list(self.pending_invoices):
if processed >= req_budget: # If request budget is exhausted.
break
status = await self.get_invoice_status(inv)
processed += 1
if status.success or status.failed:
self.pending_invoices.remove(inv)
if status.success:
had_activity = True
yield inv
if batch:
statuses = await self._get_invoices_status_batch(batch)
processed = 1
for inv, status in statuses.items():
if status.success or status.failed:
self.pending_invoices.remove(inv)
self._persist_pending()
if status.success:
had_activity = True
yield inv
# Dynamic adjustment of polling frequency based on activity.
sleep_s = (
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "lnbits"
version = "1.3.0-rc8"
version = "1.3.1"
requires-python = ">=3.10,<3.13"
description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
Generated
+1 -1
View File
@@ -1260,7 +1260,7 @@ wheels = [
[[package]]
name = "lnbits"
version = "1.3.0rc8"
version = "1.3.1"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },