Compare commits

...
17 Commits
Author SHA1 Message Date
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
dni ⚡andGitHub 213bde61a8 CI: add poetry lock file check job and update poetry lock (#3343) 2025-09-02 19:50:59 +03:00
dni ⚡andGitHub 59553a6e1e chore: update to v1.3.0-rc6 (#3342) 2025-09-02 17:50:20 +02:00
918add5e6e fix: start background tasks when activating extensions at runtime (#3333)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
2025-09-02 17:39:50 +02:00
dni ⚡andGitHub 2375f7bcf8 feat: add copy, download and write NFC to qrcode components (#3335) 2025-09-02 14:48:29 +02:00
dni ⚡andGitHub 5021570f68 feat: add has_connection, listen and receive_queue to websocket_manager (#3330) 2025-09-02 10:02:22 +02:00
dni ⚡andGitHub 0d91d5b5be chore: update to lnurl v0.8.1 (#3341) 2025-09-02 10:01:37 +02:00
Vlad StanandGitHub 49f02a5b77 chore: ignore .venv dir (#3338) 2025-09-01 15:10:56 +03:00
dni ⚡andGitHub 95b9dabb60 fix: docker for boltz-client broke on rc release (#3329) 2025-08-22 20:17:01 +02:00
dni ⚡andGitHub bc0087ec36 ci: fix pip cache is not needed for make (#3331) 2025-08-22 16:04:01 +02:00
33 changed files with 451 additions and 293 deletions
-1
View File
@@ -21,7 +21,6 @@ runs:
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: ${{ inputs.python-version }} python-version: ${{ inputs.python-version }}
cache: "pip"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@v6 uses: astral-sh/setup-uv@v6
+3
View File
@@ -53,3 +53,6 @@ jobs:
with: with:
make: checkbundle make: checkbundle
npm: true npm: true
poetry:
uses: ./.github/workflows/poetry.yml
+24
View File
@@ -0,0 +1,24 @@
name: poetry
on:
workflow_call:
inputs:
python-version:
description: "python version"
type: string
default: "3.10"
jobs:
poetry:
name: run poetry install to check lock file
runs-on: "ubuntu-24.04"
steps:
- uses: actions/checkout@v4
- name: set up python ${{ inputs.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ inputs.python-version }}
- name: install poetry
run: |
curl -sSL https://install.python-poetry.org | python3 -
poetry install
+1
View File
@@ -61,3 +61,4 @@ dist
# jetbrains # jetbrains
.idea .idea
.venv
+5 -5
View File
@@ -2,7 +2,7 @@ exclude: '^lnbits/static/bundle.*|^docs/.*|^lnbits/static/vendor/.*|^lnbits/exte
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0 rev: v6.0.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
@@ -14,16 +14,16 @@ repos:
- id: mixed-line-ending - id: mixed-line-ending
- id: check-case-conflict - id: check-case-conflict
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 24.2.0 rev: 25.1.0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.2 rev: v0.12.10
hooks: hooks:
- id: ruff - id: ruff
args: [ --fix, --exit-non-zero-on-fix ] args: [ --fix, --exit-non-zero-on-fix ]
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/rbubley/mirrors-prettier
rev: "v4.0.0-alpha.8" rev: v3.6.2
hooks: hooks:
- id: prettier - id: prettier
types_or: [css, javascript, html, json] types_or: [css, javascript, html, json]
+3
View File
@@ -7,6 +7,9 @@ RUN ls -l /usr/local/bin/boltzd
RUN apt-get update && apt-get -y upgrade && \ RUN apt-get update && apt-get -y upgrade && \
apt-get install -y netcat-openbsd apt-get install -y netcat-openbsd
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:$PATH"
# Reinstall dependencies just in case (needed for CMD usage) # Reinstall dependencies just in case (needed for CMD usage)
RUN uv sync --all-extras RUN uv sync --all-extras
+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",
+2
View File
@@ -73,11 +73,13 @@ async def uninstall_extension(ext_id: str):
async def activate_extension(ext: Extension): async def activate_extension(ext: Extension):
core_app_extra.register_new_ext_routes(ext) core_app_extra.register_new_ext_routes(ext)
await update_installed_extension_state(ext_id=ext.code, active=True) await update_installed_extension_state(ext_id=ext.code, active=True)
await start_extension_background_work(ext.code)
async def deactivate_extension(ext_id: str): async def deactivate_extension(ext_id: str):
settings.deactivate_extension_paths(ext_id) settings.deactivate_extension_paths(ext_id)
await update_installed_extension_state(ext_id=ext_id, active=False) await update_installed_extension_state(ext_id=ext_id, active=False)
await stop_extension_background_work(ext_id)
async def stop_extension_background_work(ext_id: str) -> bool: async def stop_extension_background_work(ext_id: str) -> bool:
+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:
+5 -6
View File
@@ -277,7 +277,7 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
async def send_ws_payment_notification(wallet: Wallet, payment: Payment): async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
# TODO: websocket message should be a clean payment model # TODO: websocket message should be a clean payment model
# await websocket_manager.send_data(payment.json(), wallet.inkey) # await websocket_manager.send(wallet.inkey, payment.json())
# TODO: figure out why we send the balance with the payment here. # TODO: figure out why we send the balance with the payment here.
# cleaner would be to have a separate message for the balance # cleaner would be to have a separate message for the balance
# and send it with the id of the wallet so wallets can subscribe to it # and send it with the id of the wallet so wallets can subscribe to it
@@ -288,12 +288,11 @@ async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
"payment": json.loads(payment.json()), "payment": json.loads(payment.json()),
}, },
) )
await websocket_manager.send_data(payment_notification, wallet.inkey) await websocket_manager.send(wallet.inkey, payment_notification)
await websocket_manager.send_data(payment_notification, wallet.adminkey) await websocket_manager.send(wallet.adminkey, payment_notification)
await websocket_manager.send(
await websocket_manager.send_data(
json.dumps({"pending": payment.pending, "status": payment.status}),
payment.payment_hash, payment.payment_hash,
json.dumps({"pending": payment.pending, "status": payment.status}),
) )
+50 -12
View File
@@ -1,27 +1,65 @@
from fastapi import WebSocket from asyncio import Queue
from dataclasses import dataclass
from fastapi import WebSocket, WebSocketDisconnect
from loguru import logger from loguru import logger
from lnbits.settings import settings
@dataclass
class WebsocketConnection:
item_id: str
websocket: WebSocket
receive_queue: Queue[str]
class WebsocketConnectionManager: class WebsocketConnectionManager:
def __init__(self) -> None: def __init__(self) -> None:
self.active_connections: list[WebSocket] = [] self.active_connections: list[WebsocketConnection] = []
async def connect(self, websocket: WebSocket, item_id: str): async def connect(self, item_id: str, websocket: WebSocket) -> WebsocketConnection:
logger.debug(f"Websocket connected to {item_id}") logger.debug(f"Websocket connected to {item_id}")
await websocket.accept() await websocket.accept()
self.active_connections.append(websocket) conn = WebsocketConnection(
item_id=item_id,
websocket=websocket,
receive_queue=Queue(),
)
self.active_connections.append(conn)
return conn
def disconnect(self, websocket: WebSocket): async def listen(self, conn: WebsocketConnection) -> None:
self.active_connections.remove(websocket) while settings.lnbits_running:
try:
data = await conn.websocket.receive_text()
logger.debug(f"WS received data from {conn.item_id}: {data}")
conn.receive_queue.put_nowait(data)
except WebSocketDisconnect:
for _conn in self.active_connections:
if _conn.websocket == conn.websocket:
self.active_connections.remove(_conn)
logger.debug(f"WS disconnected from {conn.item_id}")
break # out of the listen and the fastapi route
async def send_data(self, message: str, item_id: str): def get_connections(self, item_id: str) -> list[WebsocketConnection]:
for connection in self.active_connections: conns = []
if connection.path_params["item_id"] == item_id: for conn in self.active_connections:
await connection.send_text(message) if conn.item_id == item_id:
conns.append(conn)
return conns
def has_connection(self, item_id: str) -> bool:
return len(self.get_connections(item_id)) > 0
async def send(self, item_id: str, data: str) -> None:
for conn in self.get_connections(item_id):
await conn.websocket.send_text(data)
websocket_manager = WebsocketConnectionManager() websocket_manager = WebsocketConnectionManager()
async def websocket_updater(item_id: str, data: str): # deprecated import and use `websocket_manager.send()` instead
return await websocket_manager.send_data(data, item_id) async def websocket_updater(item_id: str, data: str) -> None:
return await websocket_manager.send(item_id, data)
+2 -4
View File
@@ -53,8 +53,7 @@
<div class="q-pa-md"> <div class="q-pa-md">
<lnbits-qrcode <lnbits-qrcode
:value="wallet.adminkey" :value="wallet.adminkey"
:options="{ width: 250 }" :show-buttons="false"
class="rounded-borders"
></lnbits-qrcode> ></lnbits-qrcode>
</div> </div>
</q-popup-proxy> </q-popup-proxy>
@@ -86,8 +85,7 @@
<div class="q-pa-md"> <div class="q-pa-md">
<lnbits-qrcode <lnbits-qrcode
:value="wallet.inkey" :value="wallet.inkey"
:options="{ width: 250 }" :show-buttons="false"
class="rounded-borders"
></lnbits-qrcode> ></lnbits-qrcode>
</div> </div>
</q-popup-proxy> </q-popup-proxy>
+7 -13
View File
@@ -328,13 +328,10 @@
<q-card v-if="selectedRelease" class="q-pa-lg lnbits__dialog-card"> <q-card v-if="selectedRelease" class="q-pa-lg lnbits__dialog-card">
<q-card-section> <q-card-section>
<div v-if="selectedRelease.paymentRequest"> <div v-if="selectedRelease.paymentRequest">
<a :href="'lightning:' + selectedRelease.paymentRequest"> <lnbits-qrcode
<q-responsive :ratio="1" class="q-mx-xl"> :value="'lightning:' + selectedRelease.paymentRequest.toUpperCase()"
<lnbits-qrcode :href="'lightning:' + selectedRelease.paymentRequest"
:value="'lightning:' + selectedRelease.paymentRequest.toUpperCase()" ></lnbits-qrcode>
></lnbits-qrcode>
</q-responsive>
</a>
</div> </div>
<div v-else> <div v-else>
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars> <q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
@@ -702,13 +699,10 @@
<q-card-section v-if="selectedExtension.payToEnable.showQRCode"> <q-card-section v-if="selectedExtension.payToEnable.showQRCode">
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<div v-if="selectedExtension.payToEnable.paymentRequest" class="col"> <div v-if="selectedExtension.payToEnable.paymentRequest" class="col">
<a <lnbits-qrcode
:value="'lightning:' + selectedExtension.payToEnable.paymentRequest.toUpperCase()"
:href="'lightning:' + selectedExtension.payToEnable.paymentRequest" :href="'lightning:' + selectedExtension.payToEnable.paymentRequest"
> ></lnbits-qrcode>
<lnbits-qrcode
:value="'lightning:' + selectedExtension.payToEnable.paymentRequest.toUpperCase()"
></lnbits-qrcode>
</a>
</div> </div>
<div v-else class="col"> <div v-else class="col">
<q-spinner-bars color="primary" size="2.55em"></q-spinner-bars> <q-spinner-bars color="primary" size="2.55em"></q-spinner-bars>
+27 -46
View File
@@ -209,16 +209,6 @@
></payment-list> ></payment-list>
</q-card-section> </q-card-section>
</q-card> </q-card>
<div id="hiddenQrCodeContainer" style="display: none">
<lnbits-qrcode
v-if="receive.fiatPaymentReq"
:value="receive.fiatPaymentReq"
></lnbits-qrcode>
<lnbits-qrcode
v-else
:value="'lightning:' + (this.receive.paymentReq || '').toUpperCase()"
></lnbits-qrcode>
</div>
</div> </div>
{% if HIDE_API %} {% if HIDE_API %}
<div class="col-12 col-md-4 q-gutter-y-md"> <div class="col-12 col-md-4 q-gutter-y-md">
@@ -277,13 +267,15 @@
:label="$t('drain_funds')" :label="$t('drain_funds')"
> >
<q-card> <q-card>
<q-card-section class="text-center"> <q-card-section>
<a href="lightning:{{wallet.lnurlwithdraw_full}}"> <lnbits-qrcode
<lnbits-qrcode value="lightning:{{wallet.lnurlwithdraw_full}}"
:value="lightning:{{wallet.lnurlwithdraw_full}}" href="lightning:{{wallet.lnurlwithdraw_full}}"
></lnbits-qrcode> ></lnbits-qrcode>
</a> <p
<p v-text="$t('drain_funds_desc')"></p> class="text-center"
v-text="$t('drain_funds_desc')"
></p>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
@@ -411,21 +403,15 @@
:label="$t('export_to_phone')" :label="$t('export_to_phone')"
> >
<q-card> <q-card>
<q-card-section class="text-center"> <q-card-section>
<p v-text="$t('export_to_phone_desc')"></p> <p
class="text-center"
v-text="$t('export_to_phone_desc')"
></p>
<lnbits-qrcode <lnbits-qrcode
:value="`${baseUrl}wallet?usr=${g.user.id}&wal=${g.wallet.id}`" :value="`${baseUrl}wallet?usr=${g.user.id}&wal=${g.wallet.id}`"
></lnbits-qrcode> ></lnbits-qrcode>
</q-card-section> </q-card-section>
<span v-text="exportWalletQR"></span>
<q-card-actions class="flex-center q-pb-md">
<q-btn
outline
color="grey"
:label="$t('copy_wallet_url')"
@click="copyText(`${baseUrl}wallet?usr=${g.user.id}&wal=${g.wallet.id}`)"
></q-btn>
</q-card-actions>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>
</q-card-section> </q-card-section>
@@ -862,18 +848,19 @@
v-else-if="receive.paymentReq && receive.lnurl == null" v-else-if="receive.paymentReq && receive.lnurl == null"
class="q-pa-lg q-pt-xl lnbits__dialog-card" class="q-pa-lg q-pt-xl lnbits__dialog-card"
> >
<div class="text-center q-mb-lg"> <lnbits-qrcode
<a v-if="receive.fiatPaymentReq"
v-if="receive.fiatPaymentReq" :show-buttons="false"
:href="receive.fiatPaymentReq" :href="receive.fiatPaymentReq"
target="_blank" :value="receive.fiatPaymentReq"
> >
<div v-html="invoiceQrCode"></div> </lnbits-qrcode>
</a> <lnbits-qrcode
<a v-else :href="'lightning:' + receive.paymentReq"> v-else
<div v-html="invoiceQrCode"></div> :href="'lightning:' + receive.paymentReq"
</a> :value="'lightning:' + receive.paymentReq"
</div> >
</lnbits-qrcode>
<div class="text-center"> <div class="text-center">
<h3 class="q-my-md"> <h3 class="q-my-md">
<span v-text="formattedAmount"></span> <span v-text="formattedAmount"></span>
@@ -898,12 +885,6 @@
</div> </div>
</div> </div>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText(receive.fiatPaymentReq || receive.paymentReq)"
:label="$t('copy_invoice')"
></q-btn>
<q-btn <q-btn
v-close-popup v-close-popup
flat flat
+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.")
+7 -20
View File
@@ -1,33 +1,20 @@
from fastapi import ( from fastapi import APIRouter, WebSocket
APIRouter,
WebSocket,
WebSocketDisconnect,
)
from lnbits.settings import settings from ..services import websocket_manager
from ..services import (
websocket_manager,
websocket_updater,
)
websocket_router = APIRouter(prefix="/api/v1/ws", tags=["Websocket"]) websocket_router = APIRouter(prefix="/api/v1/ws", tags=["Websocket"])
@websocket_router.websocket("/{item_id}") @websocket_router.websocket("/{item_id}")
async def websocket_connect(websocket: WebSocket, item_id: str): async def websocket_connect(websocket: WebSocket, item_id: str) -> None:
await websocket_manager.connect(websocket, item_id) conn = await websocket_manager.connect(item_id, websocket)
try: await websocket_manager.listen(conn)
while settings.lnbits_running:
await websocket.receive_text()
except WebSocketDisconnect:
websocket_manager.disconnect(websocket)
@websocket_router.post("/{item_id}") @websocket_router.post("/{item_id}")
async def websocket_update_post(item_id: str, data: str): async def websocket_update_post(item_id: str, data: str):
try: try:
await websocket_updater(item_id, data) await websocket_manager.send(item_id, data)
return {"sent": True, "data": data} return {"sent": True, "data": data}
except Exception: except Exception:
return {"sent": False, "data": data} return {"sent": False, "data": data}
@@ -36,7 +23,7 @@ async def websocket_update_post(item_id: str, data: str):
@websocket_router.get("/{item_id}/{data}") @websocket_router.get("/{item_id}/{data}")
async def websocket_update_get(item_id: str, data: str): async def websocket_update_get(item_id: str, data: str):
try: try:
await websocket_updater(item_id, data) await websocket_manager.send(item_id, data)
return {"sent": True, "data": data} return {"sent": True, "data": data}
except Exception: except Exception:
return {"sent": False, "data": data} return {"sent": False, "data": data}
File diff suppressed because one or more lines are too long
+1 -1
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)
} }
+113 -9
View File
@@ -9,19 +9,123 @@ window.app.component('lnbits-qrcode', {
type: String, type: String,
required: true required: true
}, },
options: Object nfc: {
type: Boolean,
default: false
},
showButtons: {
type: Boolean,
default: true
},
href: {
type: String,
default: ''
},
margin: {
type: Number,
default: 3
},
maxWidth: {
type: Number,
default: 450
},
logo: {
type: String,
default: LNBITS_QR_LOGO
}
}, },
data() { data() {
return { return {
custom: { nfcTagWriting: false,
margin: 3, nfcSupported: typeof NDEFReader != 'undefined'
width: 350,
size: 350,
logo: LNBITS_QR_LOGO
}
} }
}, },
created() { methods: {
this.custom = {...this.custom, ...this.options} clickQrCode(event) {
if (this.href === '') {
this.copyText(this.value)
event.preventDefault()
event.stopPropagation()
return false
}
},
async writeNfcTag() {
try {
if (!this.nfcSupported) {
throw {
toString: function () {
return 'NFC not supported on this device or browser.'
}
}
}
const ndef = new NDEFReader()
this.nfcTagWriting = true
this.$q.notify({
message: 'Tap your NFC tag to write the LNURL-withdraw link to it.'
})
await ndef.write({
records: [{recordType: 'url', data: this.value, lang: 'en'}]
})
this.nfcTagWriting = false
this.$q.notify({
type: 'positive',
message: 'NFC tag written successfully.'
})
} catch (error) {
this.nfcTagWriting = false
this.$q.notify({
type: 'negative',
message: error
? error.toString()
: 'An unexpected error has occurred.'
})
}
},
downloadSVG() {
const filename = 'qrcode.svg'
const svg = this.$refs.qrCode.$el
if (!svg) {
console.error('SVG element not found')
return
}
// Serialize the SVG content
const serializer = new XMLSerializer()
let source = serializer.serializeToString(svg)
// Add SVG namespace if not present
if (!source.match(/^<svg[^>]+xmlns="http:\/\/www\.w3\.org\/2000\/svg"/)) {
source = source.replace(
/^<svg/,
'<svg xmlns="http://www.w3.org/2000/svg"'
)
}
// Add XML declaration
source = '<?xml version="1.0" standalone="no"?>\n' + source
// Convert to Blob and trigger download
const blob = new Blob([source], {type: 'image/svg+xml;charset=utf-8'})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
// Cleanup
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
},
mounted() {
this.$refs.qrCode.$el.style.maxWidth = this.maxWidth + 'px'
this.$refs.qrCode.$el.setAttribute('width', '100%')
this.$refs.qrCode.$el.removeAttribute('height')
} }
}) })
+30 -29
View File
@@ -154,7 +154,7 @@ function confettiStars() {
setTimeout(shoot, 200) setTimeout(shoot, 200)
} }
!(function (t, e) { !(function (t, e) {
!(function t(e, n, a, i) { ;(!(function t(e, n, a, i) {
var o = !!( var o = !!(
e.Worker && e.Worker &&
e.Blob && e.Blob &&
@@ -248,12 +248,12 @@ function confettiStars() {
function e(e, n) { function e(e, n) {
t.postMessage({options: e || {}, callback: n}) t.postMessage({options: e || {}, callback: n})
} }
;(t.init = function (e) { ;((t.init = function (e) {
var n = e.transferControlToOffscreen() var n = e.transferControlToOffscreen()
t.postMessage({canvas: n}, [n]) t.postMessage({canvas: n}, [n])
}), }),
(t.fire = function (n, a, i) { (t.fire = function (n, a, i) {
if (g) return e(n, null), g if (g) return (e(n, null), g)
var o = Math.random().toString(36).slice(2) var o = Math.random().toString(36).slice(2)
return (g = l(function (a) { return (g = l(function (a) {
function r(e) { function r(e) {
@@ -264,15 +264,15 @@ function confettiStars() {
i(), i(),
a()) a())
} }
t.addEventListener('message', r), ;(t.addEventListener('message', r),
e(n, o), e(n, o),
(m[o] = r.bind(null, {data: {callback: o}})) (m[o] = r.bind(null, {data: {callback: o}})))
})) }))
}), }),
(t.reset = function () { (t.reset = function () {
for (var e in (t.postMessage({reset: !0}), m)) for (var e in (t.postMessage({reset: !0}), m))
m[e](), delete m[e] (m[e](), delete m[e])
}) }))
})(h) })(h)
} }
return h return h
@@ -328,12 +328,12 @@ function confettiStars() {
) )
} }
function k(t) { function k(t) {
;(t.width = document.documentElement.clientWidth), ;((t.width = document.documentElement.clientWidth),
(t.height = document.documentElement.clientHeight) (t.height = document.documentElement.clientHeight))
} }
function I(t) { function I(t) {
var e = t.getBoundingClientRect() var e = t.getBoundingClientRect()
;(t.width = e.width), (t.height = e.height) ;((t.width = e.width), (t.height = e.height))
} }
function T(t, e, n, o, r) { function T(t, e, n, o, r) {
var c, var c,
@@ -342,10 +342,10 @@ function confettiStars() {
d = t.getContext('2d'), d = t.getContext('2d'),
f = l(function (e) { f = l(function (e) {
function l() { function l() {
;(c = s = null), d.clearRect(0, 0, o.width, o.height), r(), e() ;((c = s = null), d.clearRect(0, 0, o.width, o.height), r(), e())
} }
;(c = b.frame(function e() { ;((c = b.frame(function e() {
!a || ;(!a ||
(o.width === i.width && o.height === i.height) || (o.width === i.width && o.height === i.height) ||
((o.width = t.width = i.width), (o.height = t.height = i.height)), ((o.width = t.width = i.width), (o.height = t.height = i.height)),
o.width || o.width ||
@@ -354,7 +354,7 @@ function confettiStars() {
d.clearRect(0, 0, o.width, o.height), d.clearRect(0, 0, o.width, o.height),
(u = u.filter(function (t) { (u = u.filter(function (t) {
return (function (t, e) { return (function (t, e) {
;(e.x += Math.cos(e.angle2D) * e.velocity + e.drift), ;((e.x += Math.cos(e.angle2D) * e.velocity + e.drift),
(e.y += Math.sin(e.angle2D) * e.velocity + e.gravity), (e.y += Math.sin(e.angle2D) * e.velocity + e.gravity),
(e.wobble += 0.1), (e.wobble += 0.1),
(e.velocity *= e.decay), (e.velocity *= e.decay),
@@ -363,7 +363,7 @@ function confettiStars() {
(e.tiltCos = Math.cos(e.tiltAngle)), (e.tiltCos = Math.cos(e.tiltAngle)),
(e.random = Math.random() + 5), (e.random = Math.random() + 5),
(e.wobbleX = e.x + 10 * e.scalar * Math.cos(e.wobble)), (e.wobbleX = e.x + 10 * e.scalar * Math.cos(e.wobble)),
(e.wobbleY = e.y + 10 * e.scalar * Math.sin(e.wobble)) (e.wobbleY = e.y + 10 * e.scalar * Math.sin(e.wobble)))
var n = e.tick++ / e.totalTicks, var n = e.tick++ / e.totalTicks,
a = e.x + e.random * e.tiltCos, a = e.x + e.random * e.tiltCos,
i = e.y + e.random * e.tiltSin, i = e.y + e.random * e.tiltSin,
@@ -393,12 +393,12 @@ function confettiStars() {
2 * Math.PI 2 * Math.PI
) )
: (function (t, e, n, a, i, o, r, l, c) { : (function (t, e, n, a, i, o, r, l, c) {
t.save(), ;(t.save(),
t.translate(e, n), t.translate(e, n),
t.rotate(o), t.rotate(o),
t.scale(a, i), t.scale(a, i),
t.arc(0, 0, 1, r, l, c), t.arc(0, 0, 1, r, l, c),
t.restore() t.restore())
})( })(
t, t,
e.x, e.x,
@@ -420,18 +420,18 @@ function confettiStars() {
})(d, t) })(d, t)
})).length })).length
? (c = b.frame(e)) ? (c = b.frame(e))
: l() : l())
})), })),
(s = l) (s = l))
}) })
return { return {
addFettis: function (t) { addFettis: function (t) {
return (u = u.concat(t)), f return ((u = u.concat(t)), f)
}, },
canvas: t, canvas: t,
promise: f, promise: f,
reset: function () { reset: function () {
c && b.cancel(c), s && s() ;(c && b.cancel(c), s && s())
} }
} }
} }
@@ -466,7 +466,7 @@ function confettiStars() {
k = p(e, 'scalar'), k = p(e, 'scalar'),
I = (function (t) { I = (function (t) {
var e = p(t, 'origin', Object) var e = p(t, 'origin', Object)
return (e.x = p(e, 'x', Number)), (e.y = p(e, 'y', Number)), e return ((e.x = p(e, 'x', Number)), (e.y = p(e, 'y', Number)), e)
})(e), })(e),
E = d, E = d,
S = [], S = [],
@@ -531,7 +531,7 @@ function confettiStars() {
return l(function (t) { return l(function (t) {
t() t()
}) })
i && a ;(i && a
? (t = a.canvas) ? (t = a.canvas)
: i && : i &&
!t && !t &&
@@ -547,7 +547,7 @@ function confettiStars() {
) )
})(g)), })(g)),
document.body.appendChild(t)), document.body.appendChild(t)),
r && !d && u(t) r && !d && u(t))
var m = {width: t.width, height: t.height} var m = {width: t.width, height: t.height}
function b() { function b() {
if (s) { if (s) {
@@ -564,9 +564,9 @@ function confettiStars() {
m.width = m.height = null m.width = m.height = null
} }
function v() { function v() {
;(a = null), ;((a = null),
r && e.removeEventListener('resize', b), r && e.removeEventListener('resize', b),
i && t && (document.body.removeChild(t), (t = null), (d = !1)) i && t && (document.body.removeChild(t), (t = null), (d = !1)))
} }
return ( return (
s && !d && s.init(t), s && !d && s.init(t),
@@ -578,12 +578,13 @@ function confettiStars() {
} }
return ( return (
(g.reset = function () { (g.reset = function () {
s && s.reset(), a && a.reset() ;(s && s.reset(), a && a.reset())
}), }),
g g
) )
} }
;(n.exports = E(null, {useWorker: !0, resize: !0})), (n.exports.create = E) ;((n.exports = E(null, {useWorker: !0, resize: !0})),
(n.exports.create = E))
})( })(
(function () { (function () {
return void 0 !== t ? t : 'undefined' != typeof self ? self : this || {} return void 0 !== t ? t : 'undefined' != typeof self ? self : this || {}
@@ -591,5 +592,5 @@ function confettiStars() {
e, e,
!1 !1
), ),
(t.confetti = e.exports) (t.confetti = e.exports))
})(window, {}) })(window, {})
+1 -8
View File
@@ -45,7 +45,6 @@ window.WalletPageLogic = {
payment_hash: null payment_hash: null
} }
}, },
invoiceQrCode: '',
disclaimerDialog: { disclaimerDialog: {
show: false, show: false,
location: window.location location: window.location
@@ -313,13 +312,6 @@ window.WalletPageLogic = {
}) })
} }
} }
// Hack as rendering in dialog causes reactivity issues. Does speed up, as only rendering lnbits-qrcode once.
this.$nextTick(() => {
this.invoiceQrCode = document.getElementById(
'hiddenQrCodeContainer'
).innerHTML
})
}) })
.catch(err => { .catch(err => {
LNbits.utils.notifyApiError(err) LNbits.utils.notifyApiError(err)
@@ -583,6 +575,7 @@ window.WalletPageLogic = {
} }
} }
}) })
.catch(LNbits.utils.notifyApiError)
}, },
authLnurl() { authLnurl() {
const dismissAuthMsg = Quasar.Notify.create({ const dismissAuthMsg = Quasar.Notify.create({
+79 -52
View File
@@ -617,21 +617,65 @@
</template> </template>
<template id="lnbits-qrcode"> <template id="lnbits-qrcode">
<div class="qrcode__wrapper"> <div
<qrcode-vue class="qrcode__outer"
:value="value" :style="`margin: 13px auto; max-width: ${maxWidth}px`"
level="Q" >
render-as="svg" <div ref="qrWrapper" class="qrcode__wrapper">
:margin="custom.margin" <a
:size="custom.width" :href="href"
class="rounded-borders" :title="href === '' ? value : href"
></qrcode-vue> @click="clickQrCode"
<img class="no-link full-width"
v-if="custom.logo" >
class="qrcode__image" <qrcode-vue
:src="custom.logo" ref="qrCode"
alt="qrcode icon" :value="value"
/> :margin="margin"
:size="size"
level="Q"
render-as="svg"
class="rounded-borders q-mb-sm"
>
<q-tooltip :model-value="href === '' ? value : href"></q-tooltip>
</qrcode-vue>
</a>
<img
:src="logo"
class="qrcode__image"
alt="qrcode icon"
style="pointer-events: none"
/>
</div>
<div
v-if="showButtons"
class="qrcode__buttons row q-gutter-x-sm"
style="justify-content: flex-end"
>
<q-btn
v-if="nfc && nfcSupported"
:disabled="nfcTagWriting"
flat
dense
class="text-grey"
icon="nfc"
@click="writeNfcTag"
>
<q-tooltip>Write NFC Tag</q-tooltip>
</q-btn>
<q-btn flat dense class="text-grey" icon="download" @click="downloadSVG">
<q-tooltip>Download SVG</q-tooltip>
</q-btn>
<q-btn
flat
dense
class="text-grey"
@click="copyText(value)"
icon="content_copy"
>
<q-tooltip>Copy</q-tooltip>
</q-btn>
</div>
</div> </div>
</template> </template>
@@ -640,17 +684,16 @@
<q-tabs <q-tabs
v-model="tab" v-model="tab"
dense dense
class="text-grey q-mb-md" class="text-grey"
active-color="primary" active-color="primary"
indicator-color="primary" indicator-color="primary"
align="justify" align="justify"
narrow-indicator
inline-label inline-label
> >
<q-tab name="bech32" icon="qr_code" label="bech32"></q-tab> <q-tab name="bech32" icon="qr_code" label="bech32"></q-tab>
<q-tab name="lud17" icon="link" label="url (lud17)"></q-tab> <q-tab name="lud17" icon="link" label="url (lud17)"></q-tab>
</q-tabs> </q-tabs>
<lnbits-qrcode :value="lnurl" class="rounded-borders"></lnbits-qrcode> <lnbits-qrcode :value="lnurl" nfc="true"></lnbits-qrcode>
</div> </div>
</template> </template>
@@ -1044,42 +1087,22 @@
<div <div
v-if="props.row.isIn && props.row.isPending && props.row.bolt11" v-if="props.row.isIn && props.row.isPending && props.row.bolt11"
class="text-center q-my-lg"
> >
<div v-if="props.row.extra.fiat_payment_request"> <div v-if="props.row.extra.fiat_payment_request">
<a <lnbits-qrcode
:value="props.row.extra.fiat_payment_request"
:href="props.row.extra.fiat_payment_request" :href="props.row.extra.fiat_payment_request"
target="_blank" :show-buttons="false"
> ></lnbits-qrcode>
<lnbits-qrcode
:value="props.row.extra.fiat_payment_request"
></lnbits-qrcode>
</a>
</div> </div>
<div v-else> <div v-else>
<a :href="'lightning:' + props.row.bolt11"> <lnbits-qrcode
<lnbits-qrcode :value="'lightning:' + props.row.bolt11.toUpperCase()"
:value="'lightning:' + props.row.bolt11.toUpperCase()" :href="'lightning:' + props.row.bolt11"
></lnbits-qrcode> ></lnbits-qrcode>
</a>
</div> </div>
</div> </div>
</q-card-section> <div class="row q-mt-md">
<q-card-section>
<div class="row q-gutter-x-sm">
<q-btn
v-if="
props.row.isIn && props.row.isPending && props.row.bolt11
"
outline
color="grey"
@click="
copyText(
props.row.extra.fiat_payment_request || props.row.bolt11
)
"
:label="$t('copy_invoice')"
></q-btn>
<q-btn <q-btn
outline outline
color="grey" color="grey"
@@ -1196,9 +1219,13 @@
:endpoint="endpoint" :endpoint="endpoint"
> >
<template v-slot:actions> <template v-slot:actions>
<q-btn v-close-popup flat color="grey" class="q-ml-auto" <q-btn
>Close</q-btn v-close-popup
> flat
color="grey"
class="q-ml-auto"
:label="$t('close')"
></q-btn>
</template> </template>
</lnbits-extension-settings-form> </lnbits-extension-settings-form>
</q-card> </q-card>
@@ -1282,10 +1309,10 @@
<q-dialog v-model="showQRDialog"> <q-dialog v-model="showQRDialog">
<q-card class="q-pa-md"> <q-card class="q-pa-md">
<q-card-section> <q-card-section>
<lnbits-qrcode :value="qrValue" :size="200"></lnbits-qrcode> <lnbits-qrcode :value="qrValue"></lnbits-qrcode>
</q-card-section> </q-card-section>
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn flat label="Close" v-close-popup /> <q-btn flat :label="$t('close')" v-close-popup />
</q-card-actions> </q-card-actions>
</q-card> </q-card>
</q-dialog> </q-dialog>
+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
+4 -4
View File
@@ -23,7 +23,7 @@
"devDependencies": { "devDependencies": {
"concat": "^1.0.3", "concat": "^1.0.3",
"minify": "^9.2.0", "minify": "^9.2.0",
"prettier": "^3.3.3", "prettier": "^3.6.2",
"pyright": "1.1.289", "pyright": "1.1.289",
"sass": "^1.78.0" "sass": "^1.78.0"
} }
@@ -1222,9 +1222,9 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.3.3", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
+1 -1
View File
@@ -15,7 +15,7 @@
"devDependencies": { "devDependencies": {
"concat": "^1.0.3", "concat": "^1.0.3",
"minify": "^9.2.0", "minify": "^9.2.0",
"prettier": "^3.3.3", "prettier": "^3.6.2",
"pyright": "1.1.289", "pyright": "1.1.289",
"sass": "^1.78.0" "sass": "^1.78.0"
}, },
Generated
+4 -4
View File
@@ -2134,14 +2134,14 @@ valkey = ["valkey (>=6)"]
[[package]] [[package]]
name = "lnurl" name = "lnurl"
version = "0.8.0" 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.0-py3-none-any.whl", hash = "sha256:260be16dbcc45fd7dd937f781bd79c9fdaf38b4bb04d6e13a1820f24eac9389c"}, {file = "lnurl-0.8.3-py3-none-any.whl", hash = "sha256:670cdeaef2c55de986dad89126ab58275d5199ba6554a93d9965d1e162080c2a"},
{file = "lnurl-0.8.0.tar.gz", hash = "sha256:fa970f7d343caae7435848c592dd223d7ac8120e337d2d5e2d3ea23a0f8f84ef"}, {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 = "2cd12b894b669ffac32a2584e899fcc6116c625b5b59d16586fe5ec1b1edc5f0" content-hash = "6b3b2f3c3163bc7a7bc2029ff6dd67f67c37057d9efbdf866b9d744f9e4ee9a5"
+2 -2
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "lnbits" name = "lnbits"
version = "1.3.0-rc5" version = "1.3.0-rc7"
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.0", "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.0rc5" version = "1.3.0rc7"
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.0" }, { 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.0" 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/8f/b2/9e4f4ce76c50825b5ae4935affa877941f41ce8e44ef0f9c5a6757c1243f/lnurl-0.8.0.tar.gz", hash = "sha256:fa970f7d343caae7435848c592dd223d7ac8120e337d2d5e2d3ea23a0f8f84ef", size = 17008, upload-time = "2025-08-21T12:30:11.315Z" } 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/f3/16/fdfb77478b95b7863d7822683a1fed34c0ef66bc857d8ebc52687b5f3833/lnurl-0.8.0-py3-none-any.whl", hash = "sha256:260be16dbcc45fd7dd937f781bd79c9fdaf38b4bb04d6e13a1820f24eac9389c", size = 17280, upload-time = "2025-08-21T12:30:10.32Z" }, { 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]]