Compare commits

...
14 Commits
Author SHA1 Message Date
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
Vlad StanandGitHub a9374d509c fix: hide payment details for failed payments (#3393) 2025-10-08 14:43:40 +02:00
Vlad StanandGitHub 3e72788fc8 chore: clean prin statement (#3391) 2025-10-07 14:39:24 +03:00
f48d24aca8 feat: show badge if an extension has paid releases (#3385)
Co-authored-by: dni  <office@dnilabs.com>
2025-10-06 17:06:18 +01:00
015262c9b3 chore: qr button tweak (quasar classes should always be used) (#3351)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2025-10-06 10:56:12 +02:00
e5e481f836 fix: correct timezone handling for transaction timestamps (#3373)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2025-10-06 10:53:03 +02:00
6c32ebf7e6 fix: remove uneven padding from Repo button (#3374)
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-02 13:12:08 +03:00
22 changed files with 226 additions and 113 deletions
+1 -1
View File
@@ -1 +1 @@
custom: https://demo.lnbits.com/lnurlp/link/fH59GD
custom: https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg
+13 -17
View File
@@ -5,23 +5,19 @@ on:
types: [published]
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:
+1 -1
View File
@@ -1,4 +1,4 @@
FROM boltz/boltz-client:latest AS boltz
FROM boltz/boltz-client:2.8.3 AS boltz
FROM lnbits/lnbits:latest
+19 -6
View File
@@ -15,7 +15,7 @@ Note that by default LNbits uses SQLite as its database, which is simple and eff
Go to [releases](https://github.com/lnbits/lnbits/releases) and pull latest AppImage, or:
```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
@@ -132,7 +132,7 @@ Now visit `0.0.0.0:5000` to make a super-user account.
```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 +145,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
+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
+37 -11
View File
@@ -39,6 +39,7 @@ class ExplicitRelease(BaseModel):
info_notification: str | None
critical_notification: str | None
details_link: str | None
paid_features: str | None
pay_link: str | None
def is_version_compatible(self):
@@ -187,6 +188,7 @@ class ExtensionRelease(BaseModel):
icon: str | None = None
details_link: str | None = None
paid_features: str | None = None
pay_link: str | None = None
cost_sats: int | None = None
paid_sats: int | None = 0
@@ -256,6 +258,7 @@ class ExtensionRelease(BaseModel):
html_url=e.html_url,
details_link=e.details_link,
pay_link=e.pay_link,
paid_features=e.paid_features,
repo=e.repo,
icon=e.icon,
)
@@ -308,6 +311,9 @@ class ExtensionMeta(BaseModel):
dependencies: list[str] = []
archive: str | None = None
featured: bool = False
paid_features: str | None = None
has_paid_release: bool = False
has_free_release: bool = False
class InstallableExtension(BaseModel):
@@ -451,9 +457,23 @@ class InstallableExtension(BaseModel):
shutil.rmtree(self.ext_upgrade_dir, True)
def check_latest_version(self, release: ExtensionRelease | None):
def check_release_updates(self, release: ExtensionRelease | None):
self._check_latest_version(release)
self._check_payment_link(release)
def find_existing_payment(self, pay_link: str | None) -> ReleasePaymentInfo | None:
if not pay_link or not self.meta or not self.meta.payments:
return None
return next(
(p for p in self.meta.payments if p.pay_link == pay_link),
None,
)
def _check_latest_version(self, release: ExtensionRelease | None):
if not release:
return
if not release.is_version_compatible:
return
if not self.meta or not self.meta.latest_release:
meta = self.meta or ExtensionMeta()
meta.latest_release = release
@@ -464,13 +484,19 @@ class InstallableExtension(BaseModel):
):
self.meta.latest_release = release
def find_existing_payment(self, pay_link: str | None) -> ReleasePaymentInfo | None:
if not pay_link or not self.meta or not self.meta.payments:
return None
return next(
(p for p in self.meta.payments if p.pay_link == pay_link),
None,
)
def _check_payment_link(self, release: ExtensionRelease | None):
if not release:
return
if not release.is_version_compatible:
return
if not self.meta:
self.meta = ExtensionMeta()
if release.pay_link:
self.meta.has_paid_release = True
else:
self.meta.has_free_release = True
if release.paid_features:
self.meta.paid_features = release.paid_features
def _restore_payment_info(self):
if (
@@ -596,7 +622,7 @@ class InstallableExtension(BaseModel):
(ee for ee in extension_list if ee.id == r.id), None
)
if existing_ext and ext.meta:
existing_ext.check_latest_version(ext.meta.latest_release)
existing_ext.check_release_updates(ext.meta.latest_release)
continue
meta = ext.meta or ExtensionMeta()
@@ -610,10 +636,10 @@ class InstallableExtension(BaseModel):
(ee for ee in extension_list if ee.id == e.id), None
)
if existing_ext:
existing_ext.check_latest_version(release)
existing_ext.check_release_updates(release)
continue
ext = InstallableExtension.from_explicit_release(e)
ext.check_latest_version(release)
ext.check_release_updates(release)
meta = ext.meta or ExtensionMeta()
meta.featured = ext.id in manifest.featured
ext.meta = meta
@@ -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">
+33 -2
View File
@@ -137,8 +137,40 @@
@click="showExtensionDetails(extension.id, extension.details_link)"
v-text="extension.name"
></div>
<div>
<div style="justify-content: space-between; display: flex">
<lnbits-extension-rating :rating="0" />
<q-btn-group size="xs" style="margin: 5px 0">
<q-btn
v-if="extension.hasFreeRelease"
color="green"
size="xs"
:label="$t('free')"
>
<q-tooltip>
<span v-text="$t('extension_has_free_release')"></span>
</q-tooltip>
</q-btn>
<q-btn
v-if="extension.hasPaidRelease || extension.paidFeatures"
color="primary"
size="xs"
:label="$t('paid')"
>
<q-tooltip>
<span
v-if="extension.hasPaidRelease"
v-text="$t('extension_has_paid_release')"
></span>
<br
v-if="extension.hasPaidRelease && extension.paidFeatures"
/>
<span
v-if="extension.paidFeatures"
v-text="extension.paidFeatures"
></span>
</q-tooltip>
</q-btn>
</q-btn-group>
</div>
<div style="justify-content: space-between; display: flex">
<q-toggle
@@ -916,7 +948,6 @@
:href="selectedExtensionDetails.repo"
target="_blank"
rel="noopener noreferrer"
class="q-pr-xs"
><q-tooltip>repository</q-tooltip></q-btn
>
</div>
+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)
+4 -3
View File
@@ -61,9 +61,10 @@ async def api_install_extension(data: CreateExtension):
data.ext_id, data.source_repo, data.archive, data.version
)
if not release:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Release not found"
)
raise HTTPException(HTTPStatus.NOT_FOUND, "Release not found")
if not release.is_version_compatible:
raise HTTPException(HTTPStatus.BAD_REQUEST, "Incompatible extension version.")
release.payment_hash = data.payment_hash
ext_meta = ExtensionMeta(installed_release=release)
+3
View File
@@ -126,6 +126,9 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
if ext.meta and ext.meta.latest_release
else None
),
"hasPaidRelease": ext.meta.has_paid_release if ext.meta else False,
"hasFreeRelease": ext.meta.has_free_release if ext.meta else False,
"paidFeatures": ext.meta.paid_features if ext.meta else False,
"installedRelease": (
dict(ext.meta.installed_release)
if ext.meta and ext.meta.installed_release
+3 -1
View File
@@ -296,7 +296,9 @@ async def api_payment(payment_hash, x_api_key: str | None = Header(None)):
return {"paid": True, "preimage": payment.preimage}
if payment.failed:
return {"paid": False, "status": "failed", "details": payment}
if wallet and wallet.id == payment.wallet_id:
return {"paid": False, "status": "failed", "details": payment}
return {"paid": False, "status": "failed"}
try:
status = await payment.check_status()
+1 -1
View File
File diff suppressed because one or more lines are too long
+5 -1
View File
@@ -168,6 +168,8 @@ window.localisation.en = {
'Only admin accounts can create extensions',
admin_only: 'Admin Only',
new_version: 'New Version',
extension_has_free_release: 'Has free releases',
extension_has_paid_release: 'Has paid releases',
extension_depends_on: 'Depends on:',
extension_rating_soon: 'Ratings coming soon',
extension_installed_version: 'Installed version',
@@ -663,5 +665,7 @@ window.localisation.en = {
callback_success_url_hint:
'The user will be redirected to this URL after the payment is successful',
connected: 'Connected',
not_connected: 'Not Connected'
not_connected: 'Not Connected',
free: 'Free',
paid: 'Paid'
}
+4 -1
View File
@@ -468,7 +468,10 @@ window.AdminPageLogic = {
.catch(LNbits.utils.notifyApiError)
},
formatDate(date) {
return moment.utc(date * 1000).fromNow()
return moment
.utc(date * 1000)
.local()
.fromNow()
},
sendTestEmail() {
LNbits.api
+2 -2
View File
@@ -267,10 +267,10 @@ window.LNbits = {
}
obj.date = moment.utc(data.created_at).local().format(window.dateFormat)
obj.dateFrom = moment.utc(data.created_at).fromNow()
obj.dateFrom = moment.utc(data.created_at).local().fromNow()
obj.expirydate = moment.utc(obj.expiry).local().format(window.dateFormat)
obj.expirydateFrom = moment.utc(obj.expiry).fromNow()
obj.expirydateFrom = moment.utc(obj.expiry).local().fromNow()
obj.msat = obj.amount
obj.sat = obj.msat / 1000
obj.tag = obj.extra?.tag
+1 -1
View File
@@ -600,7 +600,7 @@ window.app.component('lnbits-date', {
return LNbits.utils.formatDate(this.ts)
},
dateFrom() {
return moment.utc(this.date).fromNow()
return moment.utc(this.date).local().fromNow()
}
},
template: `
+1 -1
View File
@@ -164,7 +164,7 @@ window.PaymentsPageLogic = {
if (p.extra && p.extra.tag) {
p.tag = p.extra.tag
}
p.timeFrom = moment.utc(p.created_at).fromNow()
p.timeFrom = moment.utc(p.created_at).local().fromNow()
p.outgoing = p.amount < 0
p.amount =
new Intl.NumberFormat(window.LOCALE).format(p.amount / 1000) +
+9 -3
View File
@@ -179,7 +179,7 @@ window.WalletPageLogic = {
methods: {
dateFromNow(unix) {
const date = new Date(unix * 1000)
return moment.utc(date).fromNow()
return moment.utc(date).local().fromNow()
},
formatFiatAmount(amount, currency) {
this.update.currency = currency
@@ -476,8 +476,14 @@ window.WalletPageLogic = {
createdDate,
'YYYY-MM-DDTHH:mm:ss.SSSZ'
)
cleanInvoice.expireDateFrom = moment.utc(expireDate).fromNow()
cleanInvoice.createdDateFrom = moment.utc(createdDate).fromNow()
cleanInvoice.expireDateFrom = moment
.utc(expireDate)
.local()
.fromNow()
cleanInvoice.createdDateFrom = moment
.utc(createdDate)
.local()
.fromNow()
cleanInvoice.expired = false // TODO
}
+1 -2
View File
@@ -649,8 +649,7 @@
</div>
<div
v-if="showButtons"
class="qrcode__buttons row q-gutter-x-sm"
style="justify-content: flex-end"
class="qrcode__buttons row q-gutter-x-sm items-center justify-end no-wrap full-width"
>
<q-btn
v-if="nfc && nfcSupported"
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "lnbits"
version = "1.3.0-rc8"
version = "1.3.0"
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.0"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },