Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
769d3a07e8 | ||
|
|
b3dfb0384e | ||
|
|
c054e47913 |
@@ -1,4 +1,4 @@
|
||||
name: Build LNbits AppImage DMG
|
||||
name: Build LNbits AppImage
|
||||
|
||||
on:
|
||||
release:
|
||||
@@ -11,56 +11,89 @@ jobs:
|
||||
steps:
|
||||
# Step 1: Checkout the repository
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Step 2: Install Dependencies
|
||||
- name: Install Dependencies
|
||||
# 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: |
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libfuse2
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
shell: bash
|
||||
|
||||
# Step 3: Clone LNbits Repository
|
||||
- name: Clone LNbits
|
||||
# Optional: Cache uv + venv to speed up CI
|
||||
- name: Cache uv and venv
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/uv
|
||||
.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
|
||||
mkdir -p packaging/linux/AppDir/usr
|
||||
git clone https://github.com/lnbits/lnbits.git packaging/linux/AppDir/usr/lnbits
|
||||
shell: bash
|
||||
|
||||
# Step 4: Make the AppImage Asset
|
||||
- name: Make Asset
|
||||
# Step 5: Build the LNbits binary with uv + PyInstaller
|
||||
- name: Build LNbits binary (uv + PyInstaller)
|
||||
run: |
|
||||
cd packaging/linux/AppDir/usr/lnbits
|
||||
poetry install
|
||||
poetry run pip install pyinstaller
|
||||
|
||||
# Install project deps into .venv using uv
|
||||
uv sync
|
||||
|
||||
# Install PyInstaller into the same environment
|
||||
uv pip install pyinstaller
|
||||
|
||||
# Build the LNbits binary
|
||||
poetry run pyinstaller --onefile --name lnbits --hidden-import=embit --collect-all embit --collect-all lnbits --collect-all sqlalchemy --collect-all aiosqlite --hidden-import=passlib.handlers.bcrypt $(poetry run which lnbits)
|
||||
uv run pyinstaller \
|
||||
--onefile \
|
||||
--name lnbits \
|
||||
--hidden-import=embit \
|
||||
--collect-all embit \
|
||||
--collect-all lnbits \
|
||||
--collect-all sqlalchemy \
|
||||
--collect-all aiosqlite \
|
||||
--hidden-import=passlib.handlers.bcrypt \
|
||||
"$(uv run which lnbits)"
|
||||
|
||||
cd ../../../../..
|
||||
chmod +x packaging/linux/AppDir/AppRun
|
||||
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
|
||||
find packaging/linux/AppDir/usr/lnbits -mindepth 1 -maxdepth 1 \
|
||||
! -name 'dist' \
|
||||
! -name 'lnbits' \
|
||||
-exec rm -rf {} +
|
||||
|
||||
# Build AppImage
|
||||
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
chmod +x appimagetool-x86_64.AppImage
|
||||
TAG_NAME=${{ github.event.release.tag_name }}
|
||||
APPIMAGE_NAME="LNbits-${TAG_NAME}.AppImage"
|
||||
./appimagetool-x86_64.AppImage --updateinformation "gh-releases-zsync|lnbits|lnbits|latest|*.AppImage.zsync" packaging/linux/AppDir "$APPIMAGE_NAME"
|
||||
./appimagetool-x86_64.AppImage \
|
||||
--updateinformation "gh-releases-zsync|lnbits|lnbits|latest|*.AppImage.zsync" \
|
||||
packaging/linux/AppDir "$APPIMAGE_NAME"
|
||||
chmod +x "$APPIMAGE_NAME"
|
||||
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
# Step 5: Upload Linux Release Asset
|
||||
# Step 6: Upload Linux Release Asset
|
||||
- name: Upload Linux Release Asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
with:
|
||||
@@ -69,4 +102,4 @@ jobs:
|
||||
asset_name: ${{ env.APPIMAGE_NAME }}
|
||||
asset_content_type: application/octet-stream
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -9,6 +9,10 @@ from lnurl import LnurlErrorResponse, LnurlSuccessResponse
|
||||
from lnurl import execute_withdraw as lnurl_withdraw
|
||||
from loguru import logger
|
||||
|
||||
from bolt11.exceptions import Bolt11Bech32InvalidException
|
||||
import base64, json, time
|
||||
from types import SimpleNamespace
|
||||
|
||||
from lnbits.core.crud.payments import get_daily_stats
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
||||
@@ -52,6 +56,62 @@ from .notifications import send_payment_notification
|
||||
payment_lock = asyncio.Lock()
|
||||
wallets_payments_lock: dict[str, asyncio.Lock] = {}
|
||||
|
||||
def _parse_ark_ticket(ticket: str) -> SimpleNamespace:
|
||||
"""
|
||||
Accepts 'ark1' + base64url(JSON) and returns an object that mimics the
|
||||
attributes LNbits reads from a decoded BOLT11 invoice.
|
||||
Required JSON fields we honor:
|
||||
- amt_sat: int
|
||||
- hash: str (payment hash / checking_id)
|
||||
- ts: int (unix timestamp when created)
|
||||
- exp: int (seconds until expiry)
|
||||
- memo: str (optional)
|
||||
"""
|
||||
if not isinstance(ticket, str) or not ticket.startswith("ark1"):
|
||||
raise ValueError("Not an ARK ticket")
|
||||
|
||||
b64u = ticket[4:]
|
||||
b64u += "=" * (-len(b64u) % 4) # restore padding
|
||||
payload = json.loads(base64.urlsafe_b64decode(b64u).decode())
|
||||
|
||||
amt_sat = int(payload.get("amt_sat", 0))
|
||||
ts = int(payload.get("ts", int(time.time())))
|
||||
exp = int(payload.get("exp", 3600))
|
||||
memo = payload.get("memo") or ""
|
||||
p_hash = payload.get("hash") or payload.get("payment_hash")
|
||||
if not isinstance(p_hash, str):
|
||||
raise ValueError("ARK ticket missing 'hash'")
|
||||
|
||||
# Match attributes accessed later in this module:
|
||||
# - amount_msat (int)
|
||||
# - payment_hash (str)
|
||||
# - description (str)
|
||||
# - expiry_date (int unix ts expected by CreatePayment)
|
||||
# - tags.get(...) is sometimes called; return a harmless stub
|
||||
return SimpleNamespace(
|
||||
amount_msat=amt_sat * 1000,
|
||||
payment_hash=p_hash,
|
||||
description=memo,
|
||||
expiry_date=ts + exp, # unix timestamp
|
||||
timestamp=ts, # not strictly required here but nice to have
|
||||
tags=SimpleNamespace(get=lambda *a, **k: None),
|
||||
)
|
||||
|
||||
# Keep a reference to the real decoder, then override the name used below
|
||||
_real_bolt11_decode = bolt11_decode
|
||||
|
||||
def _safe_decode_payment_request(pr: str):
|
||||
try:
|
||||
return _real_bolt11_decode(pr)
|
||||
except Bolt11Bech32InvalidException:
|
||||
# Fallback: accept ARK tickets
|
||||
if isinstance(pr, str) and pr.startswith("ark1"):
|
||||
return _parse_ark_ticket(pr)
|
||||
# Not ARK? re-raise to keep existing behavior
|
||||
raise
|
||||
|
||||
# Override the imported name used throughout this file:
|
||||
bolt11_decode = _safe_decode_payment_request
|
||||
|
||||
async def pay_invoice(
|
||||
*,
|
||||
|
||||
@@ -974,6 +974,7 @@ class SuperUserSettings(LNbitsSettings):
|
||||
"ZBDWallet",
|
||||
"NWCWallet",
|
||||
"StrikeWallet",
|
||||
"ArkFakeWallet",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ from .spark import SparkWallet
|
||||
from .strike import StrikeWallet
|
||||
from .void import VoidWallet
|
||||
from .zbd import ZBDWallet
|
||||
from .fakeark import ArkFakeWallet
|
||||
|
||||
|
||||
def set_funding_source(class_name: str | None = None) -> None:
|
||||
@@ -79,4 +80,5 @@ __all__ = [
|
||||
"StrikeWallet",
|
||||
"VoidWallet",
|
||||
"ZBDWallet",
|
||||
"ArkFakeWallet",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# lnbits/wallets/ark_fake.py
|
||||
import asyncio, json, base64
|
||||
from collections.abc import AsyncGenerator
|
||||
from datetime import datetime
|
||||
from hashlib import sha256
|
||||
from os import urandom
|
||||
from typing import Any
|
||||
|
||||
from loguru import logger
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.crypto import fake_privkey # unused but kept for parity/logs
|
||||
|
||||
from .base import (
|
||||
InvoiceResponse,
|
||||
PaymentFailedStatus,
|
||||
PaymentPendingStatus,
|
||||
PaymentResponse,
|
||||
PaymentStatus,
|
||||
PaymentSuccessStatus,
|
||||
StatusResponse,
|
||||
Wallet,
|
||||
)
|
||||
|
||||
def ark_encode(payload: dict[str, Any]) -> str:
|
||||
raw = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode()
|
||||
return "ark1" + base64.urlsafe_b64encode(raw).decode().rstrip("=")
|
||||
|
||||
class ArkFakeWallet(Wallet):
|
||||
def __init__(self) -> None:
|
||||
self.queue: asyncio.Queue[str] = asyncio.Queue(0)
|
||||
self.payment_secrets: dict[str, str] = {}
|
||||
self.paid_invoices: set[str] = set()
|
||||
self.secret = settings.fake_wallet_secret
|
||||
self.privkey = fake_privkey(self.secret)
|
||||
|
||||
async def cleanup(self): pass
|
||||
|
||||
async def status(self) -> StatusResponse:
|
||||
logger.info("ArkFakeWallet: ARK-only test funding source (no bolt11).")
|
||||
return StatusResponse(None, 1_000_000_000)
|
||||
|
||||
async def create_invoice(
|
||||
self, amount: int, memo: str | None = None,
|
||||
description_hash: bytes | None = None,
|
||||
unhashed_description: bytes | None = None,
|
||||
expiry: int | None = None,
|
||||
payment_secret: bytes | None = None,
|
||||
**_,
|
||||
) -> InvoiceResponse:
|
||||
preimage = urandom(32)
|
||||
p_hash = sha256(preimage).hexdigest()
|
||||
now = int(datetime.now().timestamp())
|
||||
ticket = ark_encode({
|
||||
"v": 1,
|
||||
"ts": now,
|
||||
"amt_sat": int(amount),
|
||||
"memo": memo or "",
|
||||
"hash": p_hash,
|
||||
"exp": int(expiry or 3600),
|
||||
"secret": (payment_secret.hex() if payment_secret else urandom(32).hex()),
|
||||
})
|
||||
self.payment_secrets[p_hash] = preimage.hex()
|
||||
return InvoiceResponse(
|
||||
ok=True,
|
||||
checking_id=p_hash,
|
||||
payment_request=ticket, # <-- non-bolt11, starts with ark1
|
||||
preimage=preimage.hex(),
|
||||
)
|
||||
|
||||
async def pay_invoice(self, bolt11: str, _: int) -> PaymentResponse:
|
||||
# Only allow paying our own tickets (like FakeWallet behavior)
|
||||
# We don’t parse here because we only care about internal payments.
|
||||
# If you want to parse/validate, you can mirror the core’s ark parser.
|
||||
for p_hash in list(self.payment_secrets.keys()):
|
||||
if p_hash in bolt11: # cheap check; or properly decode ark1 and compare its "hash"
|
||||
await self.queue.put(p_hash)
|
||||
self.paid_invoices.add(p_hash)
|
||||
return PaymentResponse(
|
||||
ok=True,
|
||||
checking_id=p_hash,
|
||||
fee_msat=0,
|
||||
preimage=self.payment_secrets[p_hash],
|
||||
)
|
||||
return PaymentResponse(ok=False, error_message="Only internal ARK tickets can be used!")
|
||||
|
||||
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
||||
if checking_id in self.paid_invoices:
|
||||
return PaymentSuccessStatus()
|
||||
if checking_id in self.payment_secrets:
|
||||
return PaymentPendingStatus()
|
||||
return PaymentFailedStatus()
|
||||
|
||||
async def get_payment_status(self, _: str) -> PaymentStatus:
|
||||
return PaymentPendingStatus()
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
while settings.lnbits_running:
|
||||
checking_id = await self.queue.get()
|
||||
yield checking_id
|
||||
Reference in New Issue
Block a user