Compare commits

..
16 changed files with 89 additions and 84 deletions
+2 -1
View File
@@ -2,7 +2,7 @@ from .funding_source import (
get_balance_delta,
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 .payments import (
calculate_fiat_amounts,
@@ -57,6 +57,7 @@ __all__ = [
"get_payments_daily_stats",
"get_pr_from_lnurl",
"pay_invoice",
"perform_withdraw",
"send_payment_notification",
"service_fee",
"settle_hold_invoice",
+29
View File
@@ -7,7 +7,10 @@ from lnurl import (
LnurlPayActionResponse,
LnurlPayResponse,
LnurlResponseException,
LnurlSuccessResponse,
LnurlWithdrawResponse,
execute_pay_request,
execute_withdraw,
handle,
)
from loguru import logger
@@ -15,10 +18,36 @@ from loguru import logger
from lnbits.core.crud import update_wallet
from lnbits.core.models import CreateLnurlPayment, Wallet
from lnbits.core.models.lnurl import StoredPayLink
from lnbits.helpers import check_callback_url
from lnbits.settings import settings
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(
lnurl: str, amount_msat: int, comment: str | None = None
) -> str:
+2 -41
View File
@@ -6,12 +6,8 @@ from fastapi import (
Depends,
HTTPException,
)
from lnurl import (
LnurlResponseException,
LnurlSuccessResponse,
)
from lnurl import LnurlResponseException
from lnurl import execute_login as lnurlauth
from lnurl import execute_withdraw as lnurl_withdraw
from lnurl import handle as lnurl_handle
from lnurl.models import (
LnurlAuthResponse,
@@ -22,7 +18,7 @@ from lnurl.models import (
)
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.decorators import (
WalletTypeInfo,
@@ -138,38 +134,3 @@ async def api_payments_pay_lnurl(
)
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 (
CancelInvoice,
CreateInvoice,
CreateLnurlWithdraw,
DecodePayment,
KeyType,
Payment,
@@ -29,6 +30,7 @@ from lnbits.core.models import (
PaymentHistoryPoint,
PaymentWalletStats,
SettleInvoice,
SimpleStatus,
)
from lnbits.core.models.users import User
from lnbits.db import Filters, Page
@@ -59,6 +61,7 @@ from ..services import (
fee_reserve_total,
get_payments_daily_stats,
pay_invoice,
perform_withdraw,
settle_hold_invoice,
update_pending_payment,
update_pending_payments,
@@ -359,3 +362,23 @@ async def api_payments_cancel(
detail="Payment does not exist or does not belong to this wallet.",
)
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.")
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',
not_enough_funds: 'Not enough funds!',
search_by_tag_memo_amount: 'Search by tag, memo, amount',
search: 'Search',
invoice_waiting: 'Invoice waiting to be paid',
payment_received: 'Payment Received',
payment_sent: 'Payment Sent',
@@ -24,7 +24,11 @@ window.app.component('lnbits-qrcode-lnurl', {
const bech32 = NostrTools.nip19.encodeBytes('lnurl', bytes)
this.lnurl = `lightning:${bech32.toUpperCase()}`
} 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)
}
+1 -1
View File
@@ -126,6 +126,6 @@ window.app.component('lnbits-qrcode', {
mounted() {
this.$refs.qrCode.$el.style.maxWidth = this.maxWidth + 'px'
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() {
const dismissAuthMsg = Quasar.Notify.create({
+2 -6
View File
@@ -226,9 +226,7 @@ class PhoenixdWallet(Wallet):
)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
r = await self.client.get(
f"/payments/incoming/{checking_id}?all=true&limit=1000"
)
r = await self.client.get(f"/payments/incoming/{checking_id}")
if r.is_error:
if r.status_code == 404:
# invoice does not exist in phoenixd, so it was never paid
@@ -260,9 +258,7 @@ class PhoenixdWallet(Wallet):
return PaymentPendingStatus()
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
r = await self.client.get(
f"/payments/outgoing/{checking_id}?all=true&limit=1000"
)
r = await self.client.get(f"/payments/outgoingbyhash/{checking_id}")
if r.is_error:
if r.status_code == 404:
# payment does not exist in phoenixd, so it was never paid
Generated
+4 -4
View File
@@ -2134,14 +2134,14 @@ valkey = ["valkey (>=6)"]
[[package]]
name = "lnurl"
version = "0.8.1"
version = "0.8.3"
description = "LNURL implementation for Python."
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "lnurl-0.8.1-py3-none-any.whl", hash = "sha256:a451f1b78c62146fda7a17954e2eaa1de994092a19266b5c64c25577ecd66eae"},
{file = "lnurl-0.8.1.tar.gz", hash = "sha256:7d2412c309227a3148510cbed92c3500c9366c624354eef2fc205bdf30d4655c"},
{file = "lnurl-0.8.3-py3-none-any.whl", hash = "sha256:670cdeaef2c55de986dad89126ab58275d5199ba6554a93d9965d1e162080c2a"},
{file = "lnurl-0.8.3.tar.gz", hash = "sha256:8ca73af84fb9ee36a184d731d165f289ba7bc6260d4dadb2b6cf24f381c3afba"},
]
[package.dependencies]
@@ -4592,4 +4592,4 @@ migration = ["psycopg2-binary"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<3.13"
content-hash = "d124502a4452455773117a06f08fec01bf4ac4980b617ed26b49f070a0b89cb8"
content-hash = "6b3b2f3c3163bc7a7bc2029ff6dd67f67c37057d9efbdf866b9d744f9e4ee9a5"
+2 -2
View File
@@ -1,6 +1,6 @@
[project]
name = "lnbits"
version = "1.3.0-rc6"
version = "1.3.0-rc7"
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" }]
@@ -14,7 +14,7 @@ dependencies = [
"starlette==0.47.1",
"httpx==0.27.0",
"jinja2==3.1.6",
"lnurl==0.8.1",
"lnurl==0.8.3",
"pydantic==1.10.22",
"pyqrcode==1.2.1",
"shortuuid==1.0.13",
+9 -20
View File
@@ -1,5 +1,4 @@
import hashlib
from http import HTTPStatus
from json import JSONDecodeError
from unittest.mock import AsyncMock, Mock
@@ -591,31 +590,26 @@ async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
"minWithdrawable": 1000,
"maxWithdrawable": 1_500_000,
},
{
"status": "OK",
},
{
"status": "OK",
},
{"status": "OK"},
{"success": True, "message": "Payment sent with NFC."},
),
# Error loading LNURL request
(
"error_loading_lnurl",
None,
{
"status": "ERROR",
"reason": "Error loading callback request",
"detail": "Error loading callback request",
},
),
# LNURL response with error status
(
{
"status": "ERROR",
"reason": "Invalid LNURL-withdraw response.",
},
None,
{
"status": "ERROR",
"reason": "Invalid LNURL-withdraw response.",
"detail": "Invalid LNURL-withdraw response.",
},
),
# Invalid LNURL-withdraw pay request
@@ -629,8 +623,7 @@ async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
},
None,
{
"status": "ERROR",
"reason": "Invalid LNURL-withdraw response.",
"detail": "Invalid LNURL-withdraw response.",
},
),
# Error loading callback request
@@ -644,8 +637,7 @@ async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
},
"error_loading_callback",
{
"status": "ERROR",
"reason": "Error loading callback request",
"detail": "Error loading callback request",
},
),
# Callback response with error status
@@ -662,8 +654,7 @@ async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
"reason": "Callback failed",
},
{
"status": "ERROR",
"reason": "Callback failed",
"detail": "Callback failed",
},
),
# 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",
None,
{
"status": "ERROR",
"reason": "Invalid JSON response from https://example.com/lnurl",
"detail": "Invalid JSON response from https://example.com/lnurl",
},
),
],
@@ -768,7 +758,6 @@ async def test_api_payment_pay_with_nfc(
json={"lnurl_w": lnurl},
)
assert response.status_code == HTTPStatus.OK
assert response.json() == expected_response
@@ -2474,7 +2474,7 @@
},
"phoenixd": {
"get_payment_status_endpoint": {
"uri": "/payments/outgoing/e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"uri": "/payments/outgoingbyhash/e35526a43d04e985594c0dfab848814f524b1c786598ec9a63beddb2d726ac96",
"headers": {
"Authorization": "Basic OmYxNzFiYTAyMmE3NjRlNjc5ZWVmOTUwYjIxZmIxYzA0ZjE3MWJhMDIyYTc2NGU2NzllZWY5NTBiMjFmYjFjMDQ=",
"User-Agent": "LNbits/Tests"
Generated
+5 -5
View File
@@ -1260,7 +1260,7 @@ wheels = [
[[package]]
name = "lnbits"
version = "1.3.0rc6"
version = "1.3.0rc7"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },
@@ -1363,7 +1363,7 @@ requires-dist = [
{ name = "itsdangerous", specifier = "==2.2.0" },
{ name = "jinja2", specifier = "==3.1.6" },
{ 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 = "nostr-sdk", specifier = "==0.42.1" },
{ name = "packaging", specifier = "==25.0" },
@@ -1418,7 +1418,7 @@ dev = [
[[package]]
name = "lnurl"
version = "0.8.1"
version = "0.8.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bech32" },
@@ -1429,9 +1429,9 @@ dependencies = [
{ name = "pycryptodomex" },
{ 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 = [
{ 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]]