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
14 changed files with 903 additions and 395 deletions
+49 -16
View File
@@ -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 }}
+62
View File
@@ -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(
*,
@@ -139,7 +199,9 @@ async def create_fiat_invoice(
payment_hash=internal_payment.payment_hash,
currency=invoice_data.unit,
memo=invoice_data.memo,
extra=invoice_data.extra or {},
)
if fiat_invoice.failed:
logger.warning(fiat_invoice.error_message)
internal_payment.status = PaymentState.FAILED
+28 -1
View File
@@ -1,10 +1,11 @@
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.services.fiat_providers import test_connection
from lnbits.decorators import check_admin
from lnbits.fiat import StripeWallet, get_fiat_provider
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:
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 -1
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import AsyncGenerator, Coroutine
from typing import TYPE_CHECKING, NamedTuple
from typing import TYPE_CHECKING, Any, NamedTuple
if TYPE_CHECKING:
pass
@@ -106,6 +106,7 @@ class FiatProvider(ABC):
payment_hash: str,
currency: str,
memo: str | None = None,
extra: dict[str, Any] | None = None,
**kwargs,
) -> Coroutine[None, None, FiatInvoiceResponse]:
pass
+223 -62
View File
@@ -2,10 +2,12 @@ import asyncio
import json
from collections.abc import AsyncGenerator
from datetime import datetime, timedelta, timezone
from typing import Any, Literal
from urllib.parse import urlencode
import httpx
from loguru import logger
from pydantic import BaseModel, Field, ValidationError
from lnbits.helpers import normalize_endpoint
from lnbits.settings import settings
@@ -21,6 +23,34 @@ from .base import (
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):
"""https://docs.stripe.com/api"""
@@ -30,9 +60,9 @@ class StripeWallet(FiatProvider):
self._settings_fields = self._settings_connection_fields()
if not settings.stripe_api_endpoint:
raise ValueError("Cannot initialize StripeWallet: missing endpoint.")
if not settings.stripe_api_secret_key:
raise ValueError("Cannot initialize StripeWallet: missing API secret key.")
self.endpoint = normalize_endpoint(settings.stripe_api_endpoint)
self.headers = {
"Authorization": f"Bearer {settings.stripe_api_secret_key}",
@@ -60,8 +90,10 @@ class StripeWallet(FiatProvider):
r.raise_for_status()
data = r.json()
available_balance = data.get("available", [{}])[0].get("amount", 0)
# pending_balance = data.get("pending", {}).get("amount", 0)
available = data.get("available") or []
available_balance = 0
if available and isinstance(available, list):
available_balance = int(available[0].get("amount", 0))
return FiatStatusResponse(balance=available_balance)
except json.JSONDecodeError:
@@ -76,81 +108,47 @@ class StripeWallet(FiatProvider):
payment_hash: str,
currency: str,
memo: str | None = None,
extra: dict[str, Any] | None = None,
**kwargs,
) -> FiatInvoiceResponse:
amount_cents = int(amount * 100)
form_data = [
("mode", "payment"),
(
"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)
opts = self._parse_create_opts(extra or {})
if not opts:
return FiatInvoiceResponse(ok=False, error_message="Invalid Stripe options")
try:
headers = self.headers.copy()
headers["Content-Type"] = "application/x-www-form-urlencoded"
r = await self.client.post(
url="/v1/checkout/sessions", headers=headers, content=encoded_data
if opts.fiat_method == "checkout":
return await self._create_checkout_invoice(
amount_cents, currency, payment_hash, memo, opts
)
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")
if not session_id:
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}."
)
return FiatInvoiceResponse(
ok=False, error_message=f"Unsupported fiat_method: {opts.fiat_method}"
)
async def pay_invoice(self, payment_request: str) -> FiatPaymentResponse:
raise NotImplementedError("Stripe does not support paying invoices directly.")
async def get_invoice_status(self, checking_id: str) -> FiatPaymentStatus:
try:
r = await self.client.get(
url=f"/v1/checkout/sessions/{checking_id}",
)
r.raise_for_status()
stripe_id = self._normalize_stripe_id(checking_id)
data = r.json()
payment_status = data.get("payment_status")
if not payment_status:
return FiatPaymentPendingStatus()
if payment_status == "paid":
# todo: handle fee
return FiatPaymentSuccessStatus()
if stripe_id.startswith("cs_"):
r = await self.client.get(f"/v1/checkout/sessions/{stripe_id}")
r.raise_for_status()
return self._status_from_checkout_session(r.json())
expires_at = data.get("expires_at")
_24_hours_ago = datetime.now(timezone.utc) - timedelta(hours=24)
if expires_at and expires_at < _24_hours_ago.timestamp():
# be defensive: add a 24 hour buffer
return FiatPaymentFailedStatus()
if stripe_id.startswith("pi_"):
r = await self.client.get(f"/v1/payment_intents/{stripe_id}")
r.raise_for_status()
return self._status_from_payment_intent(r.json())
logger.debug(f"Unknown Stripe id prefix: {checking_id}")
return FiatPaymentPendingStatus()
except Exception as exc:
logger.debug(f"Error getting invoice status: {exc}")
return FiatPaymentPendingStatus()
@@ -167,6 +165,169 @@ class StripeWallet(FiatProvider):
value = await mock_queue.get()
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:
return "-".join(
[str(settings.stripe_api_endpoint), str(settings.stripe_api_secret_key)]
+1
View File
@@ -974,6 +974,7 @@ class SuperUserSettings(LNbitsSettings):
"ZBDWallet",
"NWCWallet",
"StrikeWallet",
"ArkFakeWallet",
]
)
+2 -2
View File
File diff suppressed because one or more lines are too long
+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 .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",
]
+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
+5 -5
View File
@@ -6,7 +6,7 @@
"": {
"name": "lnbits",
"dependencies": {
"axios": "^1.8.2",
"axios": "^1.12.0",
"chart.js": "^4.4.4",
"moment": "^2.30.1",
"nostr-tools": "^2.7.2",
@@ -402,13 +402,13 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz",
"integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
+1 -1
View File
@@ -20,7 +20,7 @@
"sass": "^1.78.0"
},
"dependencies": {
"axios": "^1.8.2",
"axios": "^1.12.0",
"chart.js": "^4.4.4",
"moment": "^2.30.1",
"qrcode.vue": "^3.4.1",
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "lnbits"
version = "1.3.0-rc7"
version = "1.3.0-rc8"
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.0rc7"
version = "1.3.0rc8"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },