Compare commits

...
Author SHA1 Message Date
arcbtc 769d3a07e8 hacky hijack 2025-09-15 16:36:27 +01:00
arcbtc b3dfb0384e Fake Wallet Ark, for playing with ark internally 2025-09-15 15:00:52 +01:00
ArcandGitHub c054e47913 Updated to uv (#3360) 2025-09-13 15:23:05 +01:00
dni ⚡andGitHub 92d3269a85 chore: update to v1.3.0-rc8 (#3359) 2025-09-12 14:29:42 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>dni ⚡
183b84c167 build(deps): bump axios from 1.8.2 to 1.12.0 (#3357)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: dni  <office@dnilabs.com>
2025-09-12 12:53:27 +01:00
60f50a71a2 feat: Adds stripe tap to pay flow for TPoS running on Android PoS/phone devices (#3334)
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2025-09-12 12:50:24 +01:00
dni ⚡andGitHub c3252ce4dc chore: update to version v1.3.0-rc7 (#3356) 2025-09-12 14:19:50 +03:00
dni ⚡andGitHub c312d70e63 fix: properly check outgoing payments in phoenixd (#3355) 2025-09-12 11:26:32 +02:00
dni ⚡andGitHub 36fc911b88 fix: js error on qrcode component (#3354) 2025-09-10 16:30:18 +03:00
dni ⚡andGitHub 672a5b3a4d fix: pay with nfc endpoint + add perform_withdraw (#3352) 2025-09-10 14:52:47 +02:00
Vlad StanandGitHub c0b33560bb feat: add search label (#3353) 2025-09-10 15:13:15 +03:00
dni ⚡andGitHub 15b6b1d512 fix: also replace http:// urls in <lnbits-qrcode-lnurl> (#3346) 2025-09-04 07:00:15 +02:00
dni ⚡andGitHub 67c92a79cf chore: update lnurl to v0.8.2 (#3347) 2025-09-03 17:17:48 +03:00
dni ⚡andGitHub 0d4751d6e0 fix: catch error response for lnurl pay (#3344) 2025-09-03 12:36:20 +02:00
27 changed files with 990 additions and 477 deletions
+49 -16
View File
@@ -1,4 +1,4 @@
name: Build LNbits AppImage DMG name: Build LNbits AppImage
on: on:
release: release:
@@ -11,56 +11,89 @@ jobs:
steps: steps:
# Step 1: Checkout the repository # Step 1: Checkout the repository
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
# Step 2: Install Dependencies # Step 2: Set up Python (uv will still use this toolchain)
- name: Install Dependencies - 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: | run: |
curl -sSL https://install.python-poetry.org | python3 -
echo "$HOME/.local/bin" >> $GITHUB_PATH
sudo apt-get update sudo apt-get update
sudo apt-get install -y libfuse2 sudo apt-get install -y libfuse2
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH
shell: bash shell: bash
# Step 3: Clone LNbits Repository # Optional: Cache uv + venv to speed up CI
- name: Clone LNbits - 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: | run: |
mv .github/packaging packaging mv .github/packaging packaging
mkdir -p packaging/linux/AppDir/usr mkdir -p packaging/linux/AppDir/usr
git clone https://github.com/lnbits/lnbits.git packaging/linux/AppDir/usr/lnbits git clone https://github.com/lnbits/lnbits.git packaging/linux/AppDir/usr/lnbits
shell: bash shell: bash
# Step 4: Make the AppImage Asset # Step 5: Build the LNbits binary with uv + PyInstaller
- name: Make Asset - name: Build LNbits binary (uv + PyInstaller)
run: | run: |
cd packaging/linux/AppDir/usr/lnbits 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 # 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 ../../../../.. cd ../../../../..
chmod +x packaging/linux/AppDir/AppRun chmod +x packaging/linux/AppDir/AppRun
chmod +x packaging/linux/AppDir/lnbits.desktop chmod +x packaging/linux/AppDir/lnbits.desktop
chmod +x packaging/linux/AppDir/usr/lnbits/dist/lnbits 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 \ find packaging/linux/AppDir/usr/lnbits -mindepth 1 -maxdepth 1 \
! -name 'dist' \ ! -name 'dist' \
! -name 'lnbits' \ ! -name 'lnbits' \
-exec rm -rf {} + -exec rm -rf {} +
# Build AppImage
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage chmod +x appimagetool-x86_64.AppImage
TAG_NAME=${{ github.event.release.tag_name }} TAG_NAME=${{ github.event.release.tag_name }}
APPIMAGE_NAME="LNbits-${TAG_NAME}.AppImage" 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" chmod +x "$APPIMAGE_NAME"
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
shell: bash shell: bash
# Step 5: Upload Linux Release Asset # Step 6: Upload Linux Release Asset
- name: Upload Linux Release Asset - name: Upload Linux Release Asset
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
with: with:
@@ -69,4 +102,4 @@ jobs:
asset_name: ${{ env.APPIMAGE_NAME }} asset_name: ${{ env.APPIMAGE_NAME }}
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+2 -1
View File
@@ -2,7 +2,7 @@ from .funding_source import (
get_balance_delta, get_balance_delta,
switch_to_voidwallet, switch_to_voidwallet,
) )
from .lnurl import fetch_lnurl_pay_request, get_pr_from_lnurl from .lnurl import fetch_lnurl_pay_request, get_pr_from_lnurl, perform_withdraw
from .notifications import enqueue_admin_notification, send_payment_notification from .notifications import enqueue_admin_notification, send_payment_notification
from .payments import ( from .payments import (
calculate_fiat_amounts, calculate_fiat_amounts,
@@ -57,6 +57,7 @@ __all__ = [
"get_payments_daily_stats", "get_payments_daily_stats",
"get_pr_from_lnurl", "get_pr_from_lnurl",
"pay_invoice", "pay_invoice",
"perform_withdraw",
"send_payment_notification", "send_payment_notification",
"service_fee", "service_fee",
"settle_hold_invoice", "settle_hold_invoice",
+29
View File
@@ -7,7 +7,10 @@ from lnurl import (
LnurlPayActionResponse, LnurlPayActionResponse,
LnurlPayResponse, LnurlPayResponse,
LnurlResponseException, LnurlResponseException,
LnurlSuccessResponse,
LnurlWithdrawResponse,
execute_pay_request, execute_pay_request,
execute_withdraw,
handle, handle,
) )
from loguru import logger from loguru import logger
@@ -15,10 +18,36 @@ from loguru import logger
from lnbits.core.crud import update_wallet from lnbits.core.crud import update_wallet
from lnbits.core.models import CreateLnurlPayment, Wallet from lnbits.core.models import CreateLnurlPayment, Wallet
from lnbits.core.models.lnurl import StoredPayLink from lnbits.core.models.lnurl import StoredPayLink
from lnbits.helpers import check_callback_url
from lnbits.settings import settings from lnbits.settings import settings
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
async def perform_withdraw(lnurl: str, payment_request: str) -> None:
"""
Perform an LNURL withdraw to the given LNURL-withdraw link.
:param lnurl: The LNURL-withdraw link. bech32 or lud17 format.
:param payment_request: The BOLT11 payment request to pay.
:raises LnurlResponseException: If the LNURL-withdraw process fails.
"""
res = await handle(lnurl, user_agent=settings.user_agent, timeout=10)
if isinstance(res, LnurlErrorResponse):
raise LnurlResponseException(res.reason)
if not isinstance(res, LnurlWithdrawResponse):
raise LnurlResponseException("Invalid LNURL-withdraw response.")
try:
check_callback_url(res.callback)
except ValueError as exc:
raise LnurlResponseException(f"Invalid callback URL: {exc!s}") from exc
res2 = await execute_withdraw(
res, payment_request, user_agent=settings.user_agent, timeout=10
)
if isinstance(res2, LnurlErrorResponse):
raise LnurlResponseException(res2.reason)
if not isinstance(res2, LnurlSuccessResponse):
raise LnurlResponseException("Invalid LNURL-withdraw success response.")
async def get_pr_from_lnurl( async def get_pr_from_lnurl(
lnurl: str, amount_msat: int, comment: str | None = None lnurl: str, amount_msat: int, comment: str | None = None
) -> str: ) -> str:
+62
View File
@@ -9,6 +9,10 @@ from lnurl import LnurlErrorResponse, LnurlSuccessResponse
from lnurl import execute_withdraw as lnurl_withdraw from lnurl import execute_withdraw as lnurl_withdraw
from loguru import logger 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.crud.payments import get_daily_stats
from lnbits.core.db import db from lnbits.core.db import db
from lnbits.core.models import PaymentDailyStats, PaymentFilters from lnbits.core.models import PaymentDailyStats, PaymentFilters
@@ -52,6 +56,62 @@ from .notifications import send_payment_notification
payment_lock = asyncio.Lock() payment_lock = asyncio.Lock()
wallets_payments_lock: dict[str, 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( async def pay_invoice(
*, *,
@@ -139,7 +199,9 @@ async def create_fiat_invoice(
payment_hash=internal_payment.payment_hash, payment_hash=internal_payment.payment_hash,
currency=invoice_data.unit, currency=invoice_data.unit,
memo=invoice_data.memo, memo=invoice_data.memo,
extra=invoice_data.extra or {},
) )
if fiat_invoice.failed: if fiat_invoice.failed:
logger.warning(fiat_invoice.error_message) logger.warning(fiat_invoice.error_message)
internal_payment.status = PaymentState.FAILED internal_payment.status = PaymentState.FAILED
+28 -1
View File
@@ -1,10 +1,11 @@
from http import HTTPStatus from http import HTTPStatus
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, HTTPException
from lnbits.core.models.misc import SimpleStatus from lnbits.core.models.misc import SimpleStatus
from lnbits.core.services.fiat_providers import test_connection from lnbits.core.services.fiat_providers import test_connection
from lnbits.decorators import check_admin from lnbits.decorators import check_admin
from lnbits.fiat import StripeWallet, get_fiat_provider
fiat_router = APIRouter(tags=["Fiat API"], prefix="/api/v1/fiat") fiat_router = APIRouter(tags=["Fiat API"], prefix="/api/v1/fiat")
@@ -16,3 +17,29 @@ fiat_router = APIRouter(tags=["Fiat API"], prefix="/api/v1/fiat")
) )
async def api_test_fiat_provider(provider: str) -> SimpleStatus: async def api_test_fiat_provider(provider: str) -> SimpleStatus:
return await test_connection(provider) return await test_connection(provider)
@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)
if provider == "stripe":
if not isinstance(provider_wallet, StripeWallet):
raise HTTPException(
status_code=500, detail="Stripe wallet/provider not configured"
)
try:
tok = await provider_wallet.create_terminal_connection_token()
secret = tok.get("secret")
if not secret:
raise HTTPException(
status_code=502, detail="Stripe returned no connection token"
)
return {"secret": secret}
except Exception as e:
raise HTTPException(
status_code=500, detail="Failed to create connection token"
) from e
+2 -41
View File
@@ -6,12 +6,8 @@ from fastapi import (
Depends, Depends,
HTTPException, HTTPException,
) )
from lnurl import ( from lnurl import LnurlResponseException
LnurlResponseException,
LnurlSuccessResponse,
)
from lnurl import execute_login as lnurlauth from lnurl import execute_login as lnurlauth
from lnurl import execute_withdraw as lnurl_withdraw
from lnurl import handle as lnurl_handle from lnurl import handle as lnurl_handle
from lnurl.models import ( from lnurl.models import (
LnurlAuthResponse, LnurlAuthResponse,
@@ -22,7 +18,7 @@ from lnurl.models import (
) )
from loguru import logger from loguru import logger
from lnbits.core.models import CreateLnurlWithdraw, Payment from lnbits.core.models import Payment
from lnbits.core.models.lnurl import CreateLnurlPayment, LnurlScan from lnbits.core.models.lnurl import CreateLnurlPayment, LnurlScan
from lnbits.decorators import ( from lnbits.decorators import (
WalletTypeInfo, WalletTypeInfo,
@@ -138,38 +134,3 @@ async def api_payments_pay_lnurl(
) )
return payment return payment
@lnurl_router.post(
"/api/v1/payments/{payment_request}/pay-with-nfc", status_code=HTTPStatus.OK
)
async def api_payment_pay_with_nfc(
payment_request: str,
lnurl_data: CreateLnurlWithdraw,
) -> LnurlErrorResponse | LnurlSuccessResponse:
if not lnurl_data.lnurl_w.lud17:
return LnurlErrorResponse(reason="LNURL-withdraw lud17 not provided.")
try:
url = lnurl_data.lnurl_w.lud17
res = await lnurl_handle(url, user_agent=settings.user_agent, timeout=10)
except (LnurlResponseException, Exception) as exc:
return LnurlErrorResponse(reason=str(exc))
if not isinstance(res, LnurlWithdrawResponse):
return LnurlErrorResponse(reason="Invalid LNURL-withdraw response.")
try:
check_callback_url(res.callback)
except ValueError as exc:
return LnurlErrorResponse(reason=f"Invalid callback URL: {exc!s}")
try:
res2 = await lnurl_withdraw(
res, payment_request, user_agent=settings.user_agent, timeout=10
)
except (LnurlResponseException, Exception) as exc:
logger.warning(exc)
return LnurlErrorResponse(reason=str(exc))
if not isinstance(res2, LnurlSuccessResponse | LnurlErrorResponse):
return LnurlErrorResponse(reason="Invalid LNURL-withdraw response.")
return res2
+23
View File
@@ -19,6 +19,7 @@ from lnbits.core.crud.payments import (
from lnbits.core.models import ( from lnbits.core.models import (
CancelInvoice, CancelInvoice,
CreateInvoice, CreateInvoice,
CreateLnurlWithdraw,
DecodePayment, DecodePayment,
KeyType, KeyType,
Payment, Payment,
@@ -29,6 +30,7 @@ from lnbits.core.models import (
PaymentHistoryPoint, PaymentHistoryPoint,
PaymentWalletStats, PaymentWalletStats,
SettleInvoice, SettleInvoice,
SimpleStatus,
) )
from lnbits.core.models.users import User from lnbits.core.models.users import User
from lnbits.db import Filters, Page from lnbits.db import Filters, Page
@@ -59,6 +61,7 @@ from ..services import (
fee_reserve_total, fee_reserve_total,
get_payments_daily_stats, get_payments_daily_stats,
pay_invoice, pay_invoice,
perform_withdraw,
settle_hold_invoice, settle_hold_invoice,
update_pending_payment, update_pending_payment,
update_pending_payments, update_pending_payments,
@@ -359,3 +362,23 @@ async def api_payments_cancel(
detail="Payment does not exist or does not belong to this wallet.", detail="Payment does not exist or does not belong to this wallet.",
) )
return await cancel_hold_invoice(payment) return await cancel_hold_invoice(payment)
@payment_router.post("/{payment_request}/pay-with-nfc")
async def api_payment_pay_with_nfc(
payment_request: str,
lnurl_data: CreateLnurlWithdraw,
) -> SimpleStatus:
if not lnurl_data.lnurl_w.lud17:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="LNURL-withdraw lud17 not provided.",
)
try:
await perform_withdraw(lnurl_data.lnurl_w.lud17, payment_request)
except Exception as exc:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
) from exc
return SimpleStatus(success=True, message="Payment sent with NFC.")
+2 -1
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import AsyncGenerator, Coroutine from collections.abc import AsyncGenerator, Coroutine
from typing import TYPE_CHECKING, NamedTuple from typing import TYPE_CHECKING, Any, NamedTuple
if TYPE_CHECKING: if TYPE_CHECKING:
pass pass
@@ -106,6 +106,7 @@ class FiatProvider(ABC):
payment_hash: str, payment_hash: str,
currency: str, currency: str,
memo: str | None = None, memo: str | None = None,
extra: dict[str, Any] | None = None,
**kwargs, **kwargs,
) -> Coroutine[None, None, FiatInvoiceResponse]: ) -> Coroutine[None, None, FiatInvoiceResponse]:
pass pass
+223 -62
View File
@@ -2,10 +2,12 @@ import asyncio
import json import json
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Any, Literal
from urllib.parse import urlencode from urllib.parse import urlencode
import httpx import httpx
from loguru import logger from loguru import logger
from pydantic import BaseModel, Field, ValidationError
from lnbits.helpers import normalize_endpoint from lnbits.helpers import normalize_endpoint
from lnbits.settings import settings from lnbits.settings import settings
@@ -21,6 +23,34 @@ from .base import (
FiatStatusResponse, FiatStatusResponse,
) )
FiatMethod = Literal["checkout", "terminal"]
class StripeTerminalOptions(BaseModel):
class Config:
extra = "ignore"
capture_method: Literal["automatic", "manual"] = "automatic"
metadata: dict[str, str] = Field(default_factory=dict)
class StripeCheckoutOptions(BaseModel):
class Config:
extra = "ignore"
success_url: str | None = None
metadata: dict[str, str] = Field(default_factory=dict)
line_item_name: str | None = None
class StripeCreateInvoiceOptions(BaseModel):
class Config:
extra = "ignore"
fiat_method: FiatMethod = "checkout"
terminal: StripeTerminalOptions | None = None
checkout: StripeCheckoutOptions | None = None
class StripeWallet(FiatProvider): class StripeWallet(FiatProvider):
"""https://docs.stripe.com/api""" """https://docs.stripe.com/api"""
@@ -30,9 +60,9 @@ class StripeWallet(FiatProvider):
self._settings_fields = self._settings_connection_fields() self._settings_fields = self._settings_connection_fields()
if not settings.stripe_api_endpoint: if not settings.stripe_api_endpoint:
raise ValueError("Cannot initialize StripeWallet: missing endpoint.") raise ValueError("Cannot initialize StripeWallet: missing endpoint.")
if not settings.stripe_api_secret_key: if not settings.stripe_api_secret_key:
raise ValueError("Cannot initialize StripeWallet: missing API secret key.") raise ValueError("Cannot initialize StripeWallet: missing API secret key.")
self.endpoint = normalize_endpoint(settings.stripe_api_endpoint) self.endpoint = normalize_endpoint(settings.stripe_api_endpoint)
self.headers = { self.headers = {
"Authorization": f"Bearer {settings.stripe_api_secret_key}", "Authorization": f"Bearer {settings.stripe_api_secret_key}",
@@ -60,8 +90,10 @@ class StripeWallet(FiatProvider):
r.raise_for_status() r.raise_for_status()
data = r.json() data = r.json()
available_balance = data.get("available", [{}])[0].get("amount", 0) available = data.get("available") or []
# pending_balance = data.get("pending", {}).get("amount", 0) available_balance = 0
if available and isinstance(available, list):
available_balance = int(available[0].get("amount", 0))
return FiatStatusResponse(balance=available_balance) return FiatStatusResponse(balance=available_balance)
except json.JSONDecodeError: except json.JSONDecodeError:
@@ -76,81 +108,47 @@ class StripeWallet(FiatProvider):
payment_hash: str, payment_hash: str,
currency: str, currency: str,
memo: str | None = None, memo: str | None = None,
extra: dict[str, Any] | None = None,
**kwargs, **kwargs,
) -> FiatInvoiceResponse: ) -> FiatInvoiceResponse:
amount_cents = int(amount * 100) amount_cents = int(amount * 100)
form_data = [ opts = self._parse_create_opts(extra or {})
("mode", "payment"), if not opts:
( return FiatInvoiceResponse(ok=False, error_message="Invalid Stripe options")
"success_url",
settings.stripe_payment_success_url or "https://lnbits.com",
),
("metadata[payment_hash]", payment_hash),
("line_items[0][price_data][currency]", currency.lower()),
("line_items[0][price_data][product_data][name]", memo or "LNbits Invoice"),
("line_items[0][price_data][unit_amount]", amount_cents),
("line_items[0][quantity]", "1"),
]
encoded_data = urlencode(form_data)
try: if opts.fiat_method == "checkout":
headers = self.headers.copy() return await self._create_checkout_invoice(
headers["Content-Type"] = "application/x-www-form-urlencoded" amount_cents, currency, payment_hash, memo, opts
r = await self.client.post( )
url="/v1/checkout/sessions", headers=headers, content=encoded_data if opts.fiat_method == "terminal":
return await self._create_terminal_invoice(
amount_cents, currency, payment_hash, opts
) )
r.raise_for_status()
data = r.json()
session_id = data.get("id") return FiatInvoiceResponse(
if not session_id: ok=False, error_message=f"Unsupported fiat_method: {opts.fiat_method}"
return FiatInvoiceResponse( )
ok=False, error_message="Server error: 'missing session id'"
)
payment_request = data.get("url")
if not payment_request:
return FiatInvoiceResponse(
ok=False, error_message="Server error: 'missing payment URL'"
)
return FiatInvoiceResponse(
ok=True, checking_id=session_id, payment_request=payment_request
)
except json.JSONDecodeError:
return FiatInvoiceResponse(
ok=False, error_message="Server error: 'invalid json response'"
)
except Exception as exc:
logger.warning(exc)
return FiatInvoiceResponse(
ok=False, error_message=f"Unable to connect to {self.endpoint}."
)
async def pay_invoice(self, payment_request: str) -> FiatPaymentResponse: async def pay_invoice(self, payment_request: str) -> FiatPaymentResponse:
raise NotImplementedError("Stripe does not support paying invoices directly.") raise NotImplementedError("Stripe does not support paying invoices directly.")
async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus: async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
try: try:
r = await self.client.get( stripe_id = self._normalize_stripe_id(checking_id)
url=f"/v1/checkout/sessions/{checking_id}",
)
r.raise_for_status()
data = r.json() if stripe_id.startswith("cs_"):
payment_status = data.get("payment_status") r = await self.client.get(f"/v1/checkout/sessions/{stripe_id}")
if not payment_status: r.raise_for_status()
return FiatPaymentPendingStatus() return self._status_from_checkout_session(r.json())
if payment_status == "paid":
# todo: handle fee
return FiatPaymentSuccessStatus()
expires_at = data.get("expires_at") if stripe_id.startswith("pi_"):
_24_hours_ago = datetime.now(timezone.utc) - timedelta(hours=24) r = await self.client.get(f"/v1/payment_intents/{stripe_id}")
if expires_at and expires_at < _24_hours_ago.timestamp(): r.raise_for_status()
# be defensive: add a 24 hour buffer return self._status_from_payment_intent(r.json())
return FiatPaymentFailedStatus()
logger.debug(f"Unknown Stripe id prefix: {checking_id}")
return FiatPaymentPendingStatus() return FiatPaymentPendingStatus()
except Exception as exc: except Exception as exc:
logger.debug(f"Error getting invoice status: {exc}") logger.debug(f"Error getting invoice status: {exc}")
return FiatPaymentPendingStatus() return FiatPaymentPendingStatus()
@@ -167,6 +165,169 @@ class StripeWallet(FiatProvider):
value = await mock_queue.get() value = await mock_queue.get()
yield value yield value
async def create_terminal_connection_token(self) -> dict:
r = await self.client.post("/v1/terminal/connection_tokens")
r.raise_for_status()
return r.json()
async def _create_checkout_invoice(
self,
amount_cents: int,
currency: str,
payment_hash: str,
memo: str | None,
opts: StripeCreateInvoiceOptions,
) -> FiatInvoiceResponse:
co = opts.checkout or StripeCheckoutOptions()
success_url = (
co.success_url
or settings.stripe_payment_success_url
or "https://lnbits.com"
)
line_item_name = co.line_item_name or memo or "LNbits Invoice"
form_data: list[tuple[str, str]] = [
("mode", "payment"),
("success_url", success_url),
("metadata[payment_hash]", payment_hash),
("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)),
("line_items[0][quantity]", "1"),
]
form_data += self._encode_metadata("metadata", co.metadata)
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()
session_id, url = data.get("id"), data.get("url")
if not session_id or not url:
return FiatInvoiceResponse(
ok=False, error_message="Server error: missing id or url"
)
return FiatInvoiceResponse(
ok=True, checking_id=session_id, payment_request=url
)
except json.JSONDecodeError:
return FiatInvoiceResponse(
ok=False, error_message="Server error: invalid json response"
)
except Exception as exc:
logger.warning(exc)
return FiatInvoiceResponse(
ok=False, error_message=f"Unable to connect to {self.endpoint}."
)
async def _create_terminal_invoice(
self,
amount_cents: int,
currency: str,
payment_hash: str,
opts: StripeCreateInvoiceOptions,
) -> FiatInvoiceResponse:
term = opts.terminal or StripeTerminalOptions()
data: dict[str, str] = {
"amount": str(amount_cents),
"currency": currency.lower(),
"payment_method_types[]": "card_present",
"capture_method": term.capture_method,
"metadata[payment_hash]": payment_hash,
"metadata[source]": "lnbits",
}
for k, v in (term.metadata or {}).items():
data[f"metadata[{k}]"] = str(v)
try:
r = await self.client.post("/v1/payment_intents", data=data)
r.raise_for_status()
pi = r.json()
pi_id, client_secret = pi.get("id"), pi.get("client_secret")
if not pi_id or not client_secret:
return FiatInvoiceResponse(
ok=False,
error_message="Error: missing PaymentIntent or client_secret",
)
return FiatInvoiceResponse(
ok=True, checking_id=pi_id, payment_request=client_secret
)
except json.JSONDecodeError:
return FiatInvoiceResponse(
ok=False, error_message="Error: invalid json response"
)
except Exception as exc:
logger.warning(exc)
return FiatInvoiceResponse(
ok=False, error_message=f"Unable to connect to {self.endpoint}."
)
def _normalize_stripe_id(self, checking_id: str) -> str:
"""Remove our internal prefix so Stripe sees a real id."""
return (
checking_id.replace("fiat_stripe_", "", 1)
if checking_id.startswith("fiat_stripe_")
else checking_id
)
def _status_from_checkout_session(self, data: dict) -> FiatPaymentStatus:
"""Map a Checkout Session to LNbits fiat status."""
if data.get("payment_status") == "paid":
return FiatPaymentSuccessStatus()
# Consider an expired session a fail (existing 24h rule).
expires_at = data.get("expires_at")
_24h_ago = datetime.now(timezone.utc) - timedelta(hours=24)
if expires_at and float(expires_at) < _24h_ago.timestamp():
return FiatPaymentFailedStatus()
return FiatPaymentPendingStatus()
def _status_from_payment_intent(self, pi: dict) -> FiatPaymentStatus:
"""Map a PaymentIntent to LNbits fiat status (card_present friendly)."""
status = pi.get("status")
if status == "succeeded":
return FiatPaymentSuccessStatus()
if status in ("canceled", "payment_failed"):
return FiatPaymentFailedStatus()
if status == "requires_payment_method":
if pi.get("last_payment_error"):
return FiatPaymentFailedStatus()
now_ts = datetime.now(timezone.utc).timestamp()
created_ts = float(pi.get("created") or now_ts)
is_stale = (now_ts - created_ts) > 300
if is_stale:
return FiatPaymentFailedStatus()
return FiatPaymentPendingStatus()
def _build_headers_form(self) -> dict[str, str]:
return {**self.headers, "Content-Type": "application/x-www-form-urlencoded"}
def _encode_metadata(
self, prefix: str, md: dict[str, Any]
) -> list[tuple[str, str]]:
out: list[tuple[str, str]] = []
for k, v in (md or {}).items():
out.append((f"{prefix}[{k}]", str(v)))
return out
def _parse_create_opts(
self, raw_opts: dict[str, Any]
) -> StripeCreateInvoiceOptions | None:
try:
return StripeCreateInvoiceOptions.parse_obj(raw_opts)
except ValidationError as e:
logger.warning(f"Invalid Stripe options: {e}")
return None
def _settings_connection_fields(self) -> str: def _settings_connection_fields(self) -> str:
return "-".join( return "-".join(
[str(settings.stripe_api_endpoint), str(settings.stripe_api_secret_key)] [str(settings.stripe_api_endpoint), str(settings.stripe_api_secret_key)]
+1
View File
@@ -974,6 +974,7 @@ class SuperUserSettings(LNbitsSettings):
"ZBDWallet", "ZBDWallet",
"NWCWallet", "NWCWallet",
"StrikeWallet", "StrikeWallet",
"ArkFakeWallet",
] ]
) )
File diff suppressed because one or more lines are too long
+3 -3
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -115,6 +115,7 @@ window.localisation.en = {
payment_check: 'Check payment', payment_check: 'Check payment',
not_enough_funds: 'Not enough funds!', not_enough_funds: 'Not enough funds!',
search_by_tag_memo_amount: 'Search by tag, memo, amount', search_by_tag_memo_amount: 'Search by tag, memo, amount',
search: 'Search',
invoice_waiting: 'Invoice waiting to be paid', invoice_waiting: 'Invoice waiting to be paid',
payment_received: 'Payment Received', payment_received: 'Payment Received',
payment_sent: 'Payment Sent', payment_sent: 'Payment Sent',
@@ -24,7 +24,11 @@ window.app.component('lnbits-qrcode-lnurl', {
const bech32 = NostrTools.nip19.encodeBytes('lnurl', bytes) const bech32 = NostrTools.nip19.encodeBytes('lnurl', bytes)
this.lnurl = `lightning:${bech32.toUpperCase()}` this.lnurl = `lightning:${bech32.toUpperCase()}`
} else if (this.tab == 'lud17') { } else if (this.tab == 'lud17') {
this.lnurl = this.url.replace('https://', this.prefix + '://') if (this.url.startsWith('http://')) {
this.lnurl = this.url.replace('http://', this.prefix + '://')
} else {
this.lnurl = this.url.replace('https://', this.prefix + '://')
}
} }
this.$emit('update:lnurl', this.lnurl) this.$emit('update:lnurl', this.lnurl)
} }
+1 -1
View File
@@ -126,6 +126,6 @@ window.app.component('lnbits-qrcode', {
mounted() { mounted() {
this.$refs.qrCode.$el.style.maxWidth = this.maxWidth + 'px' this.$refs.qrCode.$el.style.maxWidth = this.maxWidth + 'px'
this.$refs.qrCode.$el.setAttribute('width', '100%') this.$refs.qrCode.$el.setAttribute('width', '100%')
this.$refs.qrCode.$el.setAttribute('height', null) this.$refs.qrCode.$el.removeAttribute('height')
} }
}) })
+1
View File
@@ -575,6 +575,7 @@ window.WalletPageLogic = {
} }
} }
}) })
.catch(LNbits.utils.notifyApiError)
}, },
authLnurl() { authLnurl() {
const dismissAuthMsg = Quasar.Notify.create({ const dismissAuthMsg = Quasar.Notify.create({
+427 -305
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -32,6 +32,7 @@ from .spark import SparkWallet
from .strike import StrikeWallet from .strike import StrikeWallet
from .void import VoidWallet from .void import VoidWallet
from .zbd import ZBDWallet from .zbd import ZBDWallet
from .fakeark import ArkFakeWallet
def set_funding_source(class_name: str | None = None) -> None: def set_funding_source(class_name: str | None = None) -> None:
@@ -79,4 +80,5 @@ __all__ = [
"StrikeWallet", "StrikeWallet",
"VoidWallet", "VoidWallet",
"ZBDWallet", "ZBDWallet",
"ArkFakeWallet",
] ]
+99
View File
@@ -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 dont parse here because we only care about internal payments.
# If you want to parse/validate, you can mirror the cores 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
+2 -6
View File
@@ -226,9 +226,7 @@ class PhoenixdWallet(Wallet):
) )
async def get_invoice_status(self, checking_id: str) -> PaymentStatus: async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
r = await self.client.get( r = await self.client.get(f"/payments/incoming/{checking_id}")
f"/payments/incoming/{checking_id}?all=true&limit=1000"
)
if r.is_error: if r.is_error:
if r.status_code == 404: if r.status_code == 404:
# invoice does not exist in phoenixd, so it was never paid # invoice does not exist in phoenixd, so it was never paid
@@ -260,9 +258,7 @@ class PhoenixdWallet(Wallet):
return PaymentPendingStatus() return PaymentPendingStatus()
async def get_payment_status(self, checking_id: str) -> PaymentStatus: async def get_payment_status(self, checking_id: str) -> PaymentStatus:
r = await self.client.get( r = await self.client.get(f"/payments/outgoingbyhash/{checking_id}")
f"/payments/outgoing/{checking_id}?all=true&limit=1000"
)
if r.is_error: if r.is_error:
if r.status_code == 404: if r.status_code == 404:
# payment does not exist in phoenixd, so it was never paid # payment does not exist in phoenixd, so it was never paid
+5 -5
View File
@@ -6,7 +6,7 @@
"": { "": {
"name": "lnbits", "name": "lnbits",
"dependencies": { "dependencies": {
"axios": "^1.8.2", "axios": "^1.12.0",
"chart.js": "^4.4.4", "chart.js": "^4.4.4",
"moment": "^2.30.1", "moment": "^2.30.1",
"nostr-tools": "^2.7.2", "nostr-tools": "^2.7.2",
@@ -402,13 +402,13 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.8.2", "version": "1.12.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz",
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.0", "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
+1 -1
View File
@@ -20,7 +20,7 @@
"sass": "^1.78.0" "sass": "^1.78.0"
}, },
"dependencies": { "dependencies": {
"axios": "^1.8.2", "axios": "^1.12.0",
"chart.js": "^4.4.4", "chart.js": "^4.4.4",
"moment": "^2.30.1", "moment": "^2.30.1",
"qrcode.vue": "^3.4.1", "qrcode.vue": "^3.4.1",
Generated
+4 -4
View File
@@ -2134,14 +2134,14 @@ valkey = ["valkey (>=6)"]
[[package]] [[package]]
name = "lnurl" name = "lnurl"
version = "0.8.1" version = "0.8.3"
description = "LNURL implementation for Python." description = "LNURL implementation for Python."
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "lnurl-0.8.1-py3-none-any.whl", hash = "sha256:a451f1b78c62146fda7a17954e2eaa1de994092a19266b5c64c25577ecd66eae"}, {file = "lnurl-0.8.3-py3-none-any.whl", hash = "sha256:670cdeaef2c55de986dad89126ab58275d5199ba6554a93d9965d1e162080c2a"},
{file = "lnurl-0.8.1.tar.gz", hash = "sha256:7d2412c309227a3148510cbed92c3500c9366c624354eef2fc205bdf30d4655c"}, {file = "lnurl-0.8.3.tar.gz", hash = "sha256:8ca73af84fb9ee36a184d731d165f289ba7bc6260d4dadb2b6cf24f381c3afba"},
] ]
[package.dependencies] [package.dependencies]
@@ -4592,4 +4592,4 @@ migration = ["psycopg2-binary"]
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.10,<3.13" python-versions = ">=3.10,<3.13"
content-hash = "d124502a4452455773117a06f08fec01bf4ac4980b617ed26b49f070a0b89cb8" content-hash = "6b3b2f3c3163bc7a7bc2029ff6dd67f67c37057d9efbdf866b9d744f9e4ee9a5"
+2 -2
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "lnbits" name = "lnbits"
version = "1.3.0-rc6" version = "1.3.0-rc8"
requires-python = ">=3.10,<3.13" requires-python = ">=3.10,<3.13"
description = "LNbits, free and open-source Lightning wallet and accounts system." description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }] authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
@@ -14,7 +14,7 @@ dependencies = [
"starlette==0.47.1", "starlette==0.47.1",
"httpx==0.27.0", "httpx==0.27.0",
"jinja2==3.1.6", "jinja2==3.1.6",
"lnurl==0.8.1", "lnurl==0.8.3",
"pydantic==1.10.22", "pydantic==1.10.22",
"pyqrcode==1.2.1", "pyqrcode==1.2.1",
"shortuuid==1.0.13", "shortuuid==1.0.13",
+9 -20
View File
@@ -1,5 +1,4 @@
import hashlib import hashlib
from http import HTTPStatus
from json import JSONDecodeError from json import JSONDecodeError
from unittest.mock import AsyncMock, Mock from unittest.mock import AsyncMock, Mock
@@ -591,31 +590,26 @@ async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
"minWithdrawable": 1000, "minWithdrawable": 1000,
"maxWithdrawable": 1_500_000, "maxWithdrawable": 1_500_000,
}, },
{ {"status": "OK"},
"status": "OK", {"success": True, "message": "Payment sent with NFC."},
},
{
"status": "OK",
},
), ),
# Error loading LNURL request # Error loading LNURL request
( (
"error_loading_lnurl", "error_loading_lnurl",
None, None,
{ {
"status": "ERROR", "detail": "Error loading callback request",
"reason": "Error loading callback request",
}, },
), ),
# LNURL response with error status # LNURL response with error status
( (
{ {
"status": "ERROR", "status": "ERROR",
"reason": "Invalid LNURL-withdraw response.",
}, },
None, None,
{ {
"status": "ERROR", "detail": "Invalid LNURL-withdraw response.",
"reason": "Invalid LNURL-withdraw response.",
}, },
), ),
# Invalid LNURL-withdraw pay request # Invalid LNURL-withdraw pay request
@@ -629,8 +623,7 @@ async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
}, },
None, None,
{ {
"status": "ERROR", "detail": "Invalid LNURL-withdraw response.",
"reason": "Invalid LNURL-withdraw response.",
}, },
), ),
# Error loading callback request # Error loading callback request
@@ -644,8 +637,7 @@ async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
}, },
"error_loading_callback", "error_loading_callback",
{ {
"status": "ERROR", "detail": "Error loading callback request",
"reason": "Error loading callback request",
}, },
), ),
# Callback response with error status # Callback response with error status
@@ -662,8 +654,7 @@ async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
"reason": "Callback failed", "reason": "Callback failed",
}, },
{ {
"status": "ERROR", "detail": "Callback failed",
"reason": "Callback failed",
}, },
), ),
# Unexpected exception during LNURL response JSON parsing # Unexpected exception during LNURL response JSON parsing
@@ -671,8 +662,7 @@ async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
"exception_in_lnurl_response_json", "exception_in_lnurl_response_json",
None, None,
{ {
"status": "ERROR", "detail": "Invalid JSON response from https://example.com/lnurl",
"reason": "Invalid JSON response from https://example.com/lnurl",
}, },
), ),
], ],
@@ -768,7 +758,6 @@ async def test_api_payment_pay_with_nfc(
json={"lnurl_w": lnurl}, json={"lnurl_w": lnurl},
) )
assert response.status_code == HTTPStatus.OK
assert response.json() == expected_response assert response.json() == expected_response
@@ -2474,7 +2474,7 @@
}, },
"phoenixd": { "phoenixd": {
"get_payment_status_endpoint": { "get_payment_status_endpoint": {
"uri": "/payments/outgoing/e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96", "uri": "/payments/outgoingbyhash/e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"headers": { "headers": {
"Authorization": "Basic OmYxNzFiYTAyMmE3NjRlNjc5ZWVmOTUwYjIxZmIxYzA0ZjE3MWJhMDIyYTc2NGU2NzllZWY5NTBiMjFmYjFjMDQ=", "Authorization": "Basic OmYxNzFiYTAyMmE3NjRlNjc5ZWVmOTUwYjIxZmIxYzA0ZjE3MWJhMDIyYTc2NGU2NzllZWY5NTBiMjFmYjFjMDQ=",
"User-Agent": "LNbits/Tests" "User-Agent": "LNbits/Tests"
Generated
+5 -5
View File
@@ -1260,7 +1260,7 @@ wheels = [
[[package]] [[package]]
name = "lnbits" name = "lnbits"
version = "1.3.0rc6" version = "1.3.0rc8"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiosqlite" }, { name = "aiosqlite" },
@@ -1363,7 +1363,7 @@ requires-dist = [
{ name = "itsdangerous", specifier = "==2.2.0" }, { name = "itsdangerous", specifier = "==2.2.0" },
{ name = "jinja2", specifier = "==3.1.6" }, { name = "jinja2", specifier = "==3.1.6" },
{ name = "jsonpath-ng", specifier = "==1.7.0" }, { name = "jsonpath-ng", specifier = "==1.7.0" },
{ name = "lnurl", specifier = "==0.8.1" }, { name = "lnurl", specifier = "==0.8.3" },
{ name = "loguru", specifier = "==0.7.3" }, { name = "loguru", specifier = "==0.7.3" },
{ name = "nostr-sdk", specifier = "==0.42.1" }, { name = "nostr-sdk", specifier = "==0.42.1" },
{ name = "packaging", specifier = "==25.0" }, { name = "packaging", specifier = "==25.0" },
@@ -1418,7 +1418,7 @@ dev = [
[[package]] [[package]]
name = "lnurl" name = "lnurl"
version = "0.8.1" version = "0.8.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "bech32" }, { name = "bech32" },
@@ -1429,9 +1429,9 @@ dependencies = [
{ name = "pycryptodomex" }, { name = "pycryptodomex" },
{ name = "pydantic" }, { name = "pydantic" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/ce/32/b611804e96fd1e76e9bdf306395e4a9e52f9343994b65fd9646e48318107/lnurl-0.8.1.tar.gz", hash = "sha256:7d2412c309227a3148510cbed92c3500c9366c624354eef2fc205bdf30d4655c", size = 17096, upload-time = "2025-08-27T05:50:19.948Z" } sdist = { url = "https://files.pythonhosted.org/packages/9b/dc/29cce6bb8688622c7f5f6db4355c54b5c87bb05be58e49374fd2c6f4b0f2/lnurl-0.8.3.tar.gz", hash = "sha256:8ca73af84fb9ee36a184d731d165f289ba7bc6260d4dadb2b6cf24f381c3afba", size = 17171, upload-time = "2025-09-08T13:30:56.636Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/bf/6061b01bfc62ad3d1f82d8546dc543fc2ccc391f0911d5718abe578b0661/lnurl-0.8.1-py3-none-any.whl", hash = "sha256:a451f1b78c62146fda7a17954e2eaa1de994092a19266b5c64c25577ecd66eae", size = 17382, upload-time = "2025-08-27T05:50:19.013Z" }, { url = "https://files.pythonhosted.org/packages/fd/99/e400734afb7469a0cfa661259de2bb66417d6c3f14361444a010e9d186ee/lnurl-0.8.3-py3-none-any.whl", hash = "sha256:670cdeaef2c55de986dad89126ab58275d5199ba6554a93d9965d1e162080c2a", size = 17459, upload-time = "2025-09-08T13:30:55.691Z" },
] ]
[[package]] [[package]]