feat: update lnurl lib refactor lnurl endpoints (#3271)
This commit is contained in:
parent
98f732fb6f
commit
a36ab2d408
16 changed files with 417 additions and 723 deletions
|
|
@ -11,6 +11,7 @@ from .views.fiat_api import fiat_router
|
||||||
|
|
||||||
# this compat is needed for usermanager extension
|
# this compat is needed for usermanager extension
|
||||||
from .views.generic import generic_router
|
from .views.generic import generic_router
|
||||||
|
from .views.lnurl_api import lnurl_router
|
||||||
from .views.node_api import node_router, public_node_router, super_node_router
|
from .views.node_api import node_router, public_node_router, super_node_router
|
||||||
from .views.payment_api import payment_router
|
from .views.payment_api import payment_router
|
||||||
from .views.tinyurl_api import tinyurl_router
|
from .views.tinyurl_api import tinyurl_router
|
||||||
|
|
@ -42,6 +43,7 @@ def init_core_routers(app: FastAPI):
|
||||||
app.include_router(users_router)
|
app.include_router(users_router)
|
||||||
app.include_router(audit_router)
|
app.include_router(audit_router)
|
||||||
app.include_router(fiat_router)
|
app.include_router(fiat_router)
|
||||||
|
app.include_router(lnurl_router)
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["core_app", "core_app_extra", "db"]
|
__all__ = ["core_app", "core_app_extra", "db"]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from .audit import AuditEntry, AuditFilters
|
from .audit import AuditEntry, AuditFilters
|
||||||
from .lnurl import CreateLnurl, CreateLnurlAuth, PayLnurlWData
|
from .lnurl import CreateLnurlPayment, CreateLnurlWithdraw
|
||||||
from .misc import (
|
from .misc import (
|
||||||
BalanceDelta,
|
BalanceDelta,
|
||||||
Callback,
|
Callback,
|
||||||
|
|
@ -63,8 +63,8 @@ __all__ = [
|
||||||
"ConversionData",
|
"ConversionData",
|
||||||
"CoreAppExtra",
|
"CoreAppExtra",
|
||||||
"CreateInvoice",
|
"CreateInvoice",
|
||||||
"CreateLnurl",
|
"CreateLnurlPayment",
|
||||||
"CreateLnurlAuth",
|
"CreateLnurlWithdraw",
|
||||||
"CreatePayment",
|
"CreatePayment",
|
||||||
"CreateUser",
|
"CreateUser",
|
||||||
"CreateWallet",
|
"CreateWallet",
|
||||||
|
|
@ -75,7 +75,6 @@ __all__ = [
|
||||||
"LoginUsernamePassword",
|
"LoginUsernamePassword",
|
||||||
"LoginUsr",
|
"LoginUsr",
|
||||||
"PayInvoice",
|
"PayInvoice",
|
||||||
"PayLnurlWData",
|
|
||||||
"Payment",
|
"Payment",
|
||||||
"PaymentCountField",
|
"PaymentCountField",
|
||||||
"PaymentCountStat",
|
"PaymentCountStat",
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,16 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from lnurl import Lnurl, LnurlPayResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class CreateLnurl(BaseModel):
|
class CreateLnurlPayment(BaseModel):
|
||||||
description_hash: str
|
res: LnurlPayResponse
|
||||||
callback: str
|
|
||||||
amount: int
|
amount: int
|
||||||
comment: Optional[str] = None
|
comment: Optional[str] = None
|
||||||
description: Optional[str] = None
|
|
||||||
unit: Optional[str] = None
|
unit: Optional[str] = None
|
||||||
internal_memo: Optional[str] = None
|
internal_memo: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class CreateLnurlAuth(BaseModel):
|
class CreateLnurlWithdraw(BaseModel):
|
||||||
callback: str
|
lnurl_w: Lnurl
|
||||||
|
|
||||||
|
|
||||||
class PayLnurlWData(BaseModel):
|
|
||||||
lnurl_w: str
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from enum import Enum
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
|
from lnurl import LnurlWithdrawResponse
|
||||||
from pydantic import BaseModel, Field, validator
|
from pydantic import BaseModel, Field, validator
|
||||||
|
|
||||||
from lnbits.db import FilterModel
|
from lnbits.db import FilterModel
|
||||||
|
|
@ -262,7 +263,7 @@ class CreateInvoice(BaseModel):
|
||||||
extra: dict | None = None
|
extra: dict | None = None
|
||||||
webhook: str | None = None
|
webhook: str | None = None
|
||||||
bolt11: str | None = None
|
bolt11: str | None = None
|
||||||
lnurl_callback: str | None = None
|
lnurl_withdraw: LnurlWithdrawResponse | None = None
|
||||||
fiat_provider: str | None = None
|
fiat_provider: str | None = None
|
||||||
|
|
||||||
@validator("payment_hash")
|
@validator("payment_hash")
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from .funding_source import (
|
||||||
get_balance_delta,
|
get_balance_delta,
|
||||||
switch_to_voidwallet,
|
switch_to_voidwallet,
|
||||||
)
|
)
|
||||||
from .lnurl import perform_lnurlauth, redeem_lnurl_withdraw
|
from .lnurl import fetch_lnurl_pay_request
|
||||||
from .notifications import enqueue_notification, send_payment_notification
|
from .notifications import enqueue_notification, send_payment_notification
|
||||||
from .payments import (
|
from .payments import (
|
||||||
calculate_fiat_amounts,
|
calculate_fiat_amounts,
|
||||||
|
|
@ -52,11 +52,10 @@ __all__ = [
|
||||||
"enqueue_notification",
|
"enqueue_notification",
|
||||||
"fee_reserve",
|
"fee_reserve",
|
||||||
"fee_reserve_total",
|
"fee_reserve_total",
|
||||||
|
"fetch_lnurl_pay_request",
|
||||||
"get_balance_delta",
|
"get_balance_delta",
|
||||||
"get_payments_daily_stats",
|
"get_payments_daily_stats",
|
||||||
"pay_invoice",
|
"pay_invoice",
|
||||||
"perform_lnurlauth",
|
|
||||||
"redeem_lnurl_withdraw",
|
|
||||||
"send_payment_notification",
|
"send_payment_notification",
|
||||||
"service_fee",
|
"service_fee",
|
||||||
"settle_hold_invoice",
|
"settle_hold_invoice",
|
||||||
|
|
|
||||||
|
|
@ -1,159 +1,29 @@
|
||||||
import asyncio
|
from lnurl import LnurlPayActionResponse
|
||||||
import json
|
from lnurl import execute_pay_request as lnurlp
|
||||||
from io import BytesIO
|
|
||||||
from typing import Optional
|
|
||||||
from urllib.parse import parse_qs, urlparse
|
|
||||||
|
|
||||||
import httpx
|
from lnbits.core.models import CreateLnurlPayment
|
||||||
from fastapi import Depends
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.db import Connection
|
|
||||||
from lnbits.decorators import (
|
|
||||||
WalletTypeInfo,
|
|
||||||
require_admin_key,
|
|
||||||
)
|
|
||||||
from lnbits.helpers import check_callback_url, url_for
|
|
||||||
from lnbits.lnurl import LnurlErrorResponse
|
|
||||||
from lnbits.lnurl import decode as decode_lnurl
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
||||||
from .payments import create_invoice
|
|
||||||
|
|
||||||
|
|
||||||
async def redeem_lnurl_withdraw(
|
async def fetch_lnurl_pay_request(data: CreateLnurlPayment) -> LnurlPayActionResponse:
|
||||||
wallet_id: str,
|
"""
|
||||||
lnurl_request: str,
|
Pay an LNURL payment request.
|
||||||
memo: Optional[str] = None,
|
|
||||||
extra: Optional[dict] = None,
|
|
||||||
wait_seconds: int = 0,
|
|
||||||
conn: Optional[Connection] = None,
|
|
||||||
) -> None:
|
|
||||||
if not lnurl_request:
|
|
||||||
return None
|
|
||||||
|
|
||||||
res = {}
|
raises `LnurlResponseException` if pay request fails
|
||||||
|
"""
|
||||||
|
|
||||||
headers = {"User-Agent": settings.user_agent}
|
if data.unit and data.unit != "sat":
|
||||||
async with httpx.AsyncClient(headers=headers) as client:
|
# shift to float with 2 decimal places
|
||||||
lnurl = decode_lnurl(lnurl_request)
|
amount = round(data.amount / 1000, 2)
|
||||||
check_callback_url(str(lnurl))
|
amount_msat = await fiat_amount_as_satoshis(amount, data.unit)
|
||||||
r = await client.get(str(lnurl))
|
amount_msat *= 1000
|
||||||
res = r.json()
|
else:
|
||||||
|
amount_msat = data.amount
|
||||||
|
|
||||||
try:
|
return await lnurlp(
|
||||||
_, payment_request = await create_invoice(
|
data.res,
|
||||||
wallet_id=wallet_id,
|
msat=str(amount_msat),
|
||||||
amount=int(res["maxWithdrawable"] / 1000),
|
user_agent=settings.user_agent,
|
||||||
memo=memo or res["defaultDescription"] or "",
|
timeout=5,
|
||||||
extra=extra,
|
|
||||||
conn=conn,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
logger.warning(
|
|
||||||
f"failed to create invoice on redeem_lnurl_withdraw "
|
|
||||||
f"from {lnurl}. params: {res}"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
if wait_seconds:
|
|
||||||
await asyncio.sleep(wait_seconds)
|
|
||||||
|
|
||||||
params = {"k1": res["k1"], "pr": payment_request}
|
|
||||||
|
|
||||||
try:
|
|
||||||
params["balanceNotify"] = url_for(
|
|
||||||
f"/withdraw/notify/{urlparse(lnurl_request).netloc}",
|
|
||||||
external=True,
|
|
||||||
wal=wallet_id,
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
logger.debug(exc)
|
|
||||||
|
|
||||||
headers = {"User-Agent": settings.user_agent}
|
|
||||||
async with httpx.AsyncClient(headers=headers) as client:
|
|
||||||
try:
|
|
||||||
check_callback_url(res["callback"])
|
|
||||||
await client.get(res["callback"], params=params)
|
|
||||||
except Exception as exc:
|
|
||||||
logger.debug(exc)
|
|
||||||
|
|
||||||
|
|
||||||
async def perform_lnurlauth(
|
|
||||||
callback: str,
|
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
||||||
) -> Optional[LnurlErrorResponse]:
|
|
||||||
cb = urlparse(callback)
|
|
||||||
|
|
||||||
k1 = bytes.fromhex(parse_qs(cb.query)["k1"][0])
|
|
||||||
|
|
||||||
key = wallet.wallet.lnurlauth_key(cb.netloc)
|
|
||||||
|
|
||||||
def int_to_bytes_suitable_der(x: int) -> bytes:
|
|
||||||
"""for strict DER we need to encode the integer with some quirks"""
|
|
||||||
b = x.to_bytes((x.bit_length() + 7) // 8, "big")
|
|
||||||
|
|
||||||
if len(b) == 0:
|
|
||||||
# ensure there's at least one byte when the int is zero
|
|
||||||
return bytes([0])
|
|
||||||
|
|
||||||
if b[0] & 0x80 != 0:
|
|
||||||
# ensure it doesn't start with a 0x80 and so it isn't
|
|
||||||
# interpreted as a negative number
|
|
||||||
return bytes([0]) + b
|
|
||||||
|
|
||||||
return b
|
|
||||||
|
|
||||||
def encode_strict_der(r: int, s: int, order: int):
|
|
||||||
# if s > order/2 verification will fail sometimes
|
|
||||||
# so we must fix it here see:
|
|
||||||
# https://github.com/indutny/elliptic/blob/e71b2d9359c5fe9437fbf46f1f05096de447de57/lib/elliptic/ec/index.js#L146-L147
|
|
||||||
if s > order // 2:
|
|
||||||
s = order - s
|
|
||||||
|
|
||||||
# now we do the strict DER encoding copied from
|
|
||||||
# https://github.com/KiriKiri/bip66 (without any checks)
|
|
||||||
r_temp = int_to_bytes_suitable_der(r)
|
|
||||||
s_temp = int_to_bytes_suitable_der(s)
|
|
||||||
|
|
||||||
r_len = len(r_temp)
|
|
||||||
s_len = len(s_temp)
|
|
||||||
sign_len = 6 + r_len + s_len
|
|
||||||
|
|
||||||
signature = BytesIO()
|
|
||||||
signature.write(0x30.to_bytes(1, "big", signed=False))
|
|
||||||
signature.write((sign_len - 2).to_bytes(1, "big", signed=False))
|
|
||||||
signature.write(0x02.to_bytes(1, "big", signed=False))
|
|
||||||
signature.write(r_len.to_bytes(1, "big", signed=False))
|
|
||||||
signature.write(r_temp)
|
|
||||||
signature.write(0x02.to_bytes(1, "big", signed=False))
|
|
||||||
signature.write(s_len.to_bytes(1, "big", signed=False))
|
|
||||||
signature.write(s_temp)
|
|
||||||
|
|
||||||
return signature.getvalue()
|
|
||||||
|
|
||||||
sig = key.sign_digest_deterministic(k1, sigencode=encode_strict_der)
|
|
||||||
|
|
||||||
headers = {"User-Agent": settings.user_agent}
|
|
||||||
async with httpx.AsyncClient(headers=headers) as client:
|
|
||||||
if not key.verifying_key:
|
|
||||||
raise ValueError("LNURLauth verifying_key does not exist")
|
|
||||||
check_callback_url(callback)
|
|
||||||
r = await client.get(
|
|
||||||
callback,
|
|
||||||
params={
|
|
||||||
"k1": k1.hex(),
|
|
||||||
"key": key.verifying_key.to_string("compressed").hex(),
|
|
||||||
"sig": sig.hex(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
resp = json.loads(r.text)
|
|
||||||
if resp["status"] == "OK":
|
|
||||||
return None
|
|
||||||
|
|
||||||
return LnurlErrorResponse(reason=resp["reason"])
|
|
||||||
except (KeyError, json.decoder.JSONDecodeError):
|
|
||||||
return LnurlErrorResponse(
|
|
||||||
reason=r.text[:200] + "..." if len(r.text) > 200 else r.text
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
|
||||||
from bolt11 import Bolt11, MilliSatoshi, Tags
|
from bolt11 import Bolt11, MilliSatoshi, Tags
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from bolt11 import encode as bolt11_encode
|
from bolt11 import encode as bolt11_encode
|
||||||
|
from lnurl import LnurlErrorResponse, LnurlSuccessResponse
|
||||||
|
from lnurl import execute_withdraw as lnurl_withdraw
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.crud.payments import get_daily_stats
|
from lnbits.core.crud.payments import get_daily_stats
|
||||||
|
|
@ -172,8 +172,8 @@ async def create_fiat_invoice(
|
||||||
|
|
||||||
|
|
||||||
async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
|
async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
|
||||||
description_hash = b""
|
description_hash = None
|
||||||
unhashed_description = b""
|
unhashed_description = None
|
||||||
memo = data.memo or settings.lnbits_site_title
|
memo = data.memo or settings.lnbits_site_title
|
||||||
if data.description_hash or data.unhashed_description:
|
if data.description_hash or data.unhashed_description:
|
||||||
if data.description_hash:
|
if data.description_hash:
|
||||||
|
|
@ -207,28 +207,26 @@ async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
|
||||||
payment_hash=data.payment_hash,
|
payment_hash=data.payment_hash,
|
||||||
)
|
)
|
||||||
|
|
||||||
# lnurl_response is not saved in the database
|
if data.lnurl_withdraw:
|
||||||
if data.lnurl_callback:
|
|
||||||
headers = {"User-Agent": settings.user_agent}
|
|
||||||
async with httpx.AsyncClient(headers=headers) as client:
|
|
||||||
try:
|
try:
|
||||||
check_callback_url(data.lnurl_callback)
|
check_callback_url(data.lnurl_withdraw.callback)
|
||||||
r = await client.get(
|
res = await lnurl_withdraw(
|
||||||
data.lnurl_callback,
|
data.lnurl_withdraw,
|
||||||
params={"pr": payment.bolt11},
|
payment.bolt11,
|
||||||
|
user_agent=settings.user_agent,
|
||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
if r.is_error:
|
if isinstance(res, LnurlErrorResponse):
|
||||||
payment.extra["lnurl_response"] = r.text
|
payment.extra["lnurl_response"] = res.reason
|
||||||
else:
|
payment.status = "failed"
|
||||||
resp = json.loads(r.text)
|
elif isinstance(res, LnurlSuccessResponse):
|
||||||
if resp["status"] != "OK":
|
|
||||||
payment.extra["lnurl_response"] = resp["reason"]
|
|
||||||
else:
|
|
||||||
payment.extra["lnurl_response"] = True
|
payment.extra["lnurl_response"] = True
|
||||||
except (httpx.ConnectError, httpx.RequestError) as ex:
|
payment.status = "success"
|
||||||
logger.error(ex)
|
except Exception as exc:
|
||||||
payment.extra["lnurl_response"] = False
|
payment.extra["lnurl_response"] = str(exc)
|
||||||
|
payment.status = "failed"
|
||||||
|
# updating to payment here would run into a race condition
|
||||||
|
# with the payment listeners and they will overwrite each other
|
||||||
|
|
||||||
return payment
|
return payment
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,20 @@
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
import pyqrcode
|
import pyqrcode
|
||||||
from fastapi import (
|
from fastapi import APIRouter, Depends
|
||||||
APIRouter,
|
|
||||||
Depends,
|
|
||||||
)
|
|
||||||
from fastapi.exceptions import HTTPException
|
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
BaseWallet,
|
BaseWallet,
|
||||||
ConversionData,
|
ConversionData,
|
||||||
CreateLnurlAuth,
|
|
||||||
CreateWallet,
|
CreateWallet,
|
||||||
User,
|
User,
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import check_user_exists
|
||||||
WalletTypeInfo,
|
|
||||||
check_user_exists,
|
|
||||||
require_admin_key,
|
|
||||||
require_invoice_key,
|
|
||||||
)
|
|
||||||
from lnbits.helpers import check_callback_url
|
|
||||||
from lnbits.lnurl import decode as lnurl_decode
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.exchange_rates import (
|
from lnbits.utils.exchange_rates import (
|
||||||
allowed_currencies,
|
allowed_currencies,
|
||||||
|
|
@ -41,7 +25,7 @@ from lnbits.utils.exchange_rates import (
|
||||||
from lnbits.wallets import get_funding_source
|
from lnbits.wallets import get_funding_source
|
||||||
from lnbits.wallets.base import StatusResponse
|
from lnbits.wallets.base import StatusResponse
|
||||||
|
|
||||||
from ..services import create_user_account, perform_lnurlauth
|
from ..services import create_user_account
|
||||||
|
|
||||||
api_router = APIRouter(tags=["Core"])
|
api_router = APIRouter(tags=["Core"])
|
||||||
|
|
||||||
|
|
@ -92,144 +76,6 @@ async def api_create_account(data: CreateWallet) -> Wallet:
|
||||||
return user.wallets[0]
|
return user.wallets[0]
|
||||||
|
|
||||||
|
|
||||||
@api_router.get("/api/v1/lnurlscan/{code}")
|
|
||||||
async def api_lnurlscan( # noqa: C901
|
|
||||||
code: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
url = str(lnurl_decode(code))
|
|
||||||
domain = urlparse(url).netloc
|
|
||||||
except Exception as exc:
|
|
||||||
# parse internet identifier (user@domain.com)
|
|
||||||
name_domain = code.split("@")
|
|
||||||
if len(name_domain) == 2 and len(name_domain[1].split(".")) >= 2:
|
|
||||||
name, domain = name_domain
|
|
||||||
url = (
|
|
||||||
("http://" if domain.endswith(".onion") else "https://")
|
|
||||||
+ domain
|
|
||||||
+ "/.well-known/lnurlp/"
|
|
||||||
+ name
|
|
||||||
)
|
|
||||||
# will proceed with these values
|
|
||||||
else:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail="invalid lnurl"
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
# params is what will be returned to the client
|
|
||||||
params: dict = {"domain": domain}
|
|
||||||
|
|
||||||
if "tag=login" in url:
|
|
||||||
params.update(kind="auth")
|
|
||||||
params.update(callback=url) # with k1 already in it
|
|
||||||
|
|
||||||
lnurlauth_key = wallet.wallet.lnurlauth_key(domain)
|
|
||||||
if not lnurlauth_key.verifying_key:
|
|
||||||
raise ValueError("LNURL auth key not found for this domain.")
|
|
||||||
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
|
|
||||||
else:
|
|
||||||
headers = {"User-Agent": settings.user_agent}
|
|
||||||
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
|
|
||||||
check_callback_url(url)
|
|
||||||
try:
|
|
||||||
r = await client.get(url, timeout=5)
|
|
||||||
r.raise_for_status()
|
|
||||||
except httpx.HTTPStatusError as exc:
|
|
||||||
if exc.response.status_code == 404:
|
|
||||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Not found") from exc
|
|
||||||
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
|
||||||
detail={
|
|
||||||
"domain": domain,
|
|
||||||
"message": "failed to get parameters",
|
|
||||||
},
|
|
||||||
) from exc
|
|
||||||
try:
|
|
||||||
data = json.loads(r.text)
|
|
||||||
except json.decoder.JSONDecodeError as exc:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
|
||||||
detail={
|
|
||||||
"domain": domain,
|
|
||||||
"message": f"got invalid response '{r.text[:200]}'",
|
|
||||||
},
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
try:
|
|
||||||
tag: str = data.get("tag")
|
|
||||||
params.update(**data)
|
|
||||||
if tag == "channelRequest":
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail={
|
|
||||||
"domain": domain,
|
|
||||||
"kind": "channel",
|
|
||||||
"message": "unsupported",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
elif tag == "withdrawRequest":
|
|
||||||
params.update(kind="withdraw")
|
|
||||||
params.update(fixed=data["minWithdrawable"] == data["maxWithdrawable"])
|
|
||||||
|
|
||||||
# callback with k1 already in it
|
|
||||||
parsed_callback: ParseResult = urlparse(data["callback"])
|
|
||||||
qs: dict = parse_qs(parsed_callback.query)
|
|
||||||
qs["k1"] = data["k1"]
|
|
||||||
|
|
||||||
# balanceCheck/balanceNotify
|
|
||||||
if "balanceCheck" in data:
|
|
||||||
params.update(balanceCheck=data["balanceCheck"])
|
|
||||||
|
|
||||||
# format callback url and send to client
|
|
||||||
parsed_callback = parsed_callback._replace(
|
|
||||||
query=urlencode(qs, doseq=True)
|
|
||||||
)
|
|
||||||
params.update(callback=urlunparse(parsed_callback))
|
|
||||||
elif tag == "payRequest":
|
|
||||||
params.update(kind="pay")
|
|
||||||
params.update(fixed=data["minSendable"] == data["maxSendable"])
|
|
||||||
|
|
||||||
params.update(
|
|
||||||
description_hash=hashlib.sha256(
|
|
||||||
data["metadata"].encode()
|
|
||||||
).hexdigest()
|
|
||||||
)
|
|
||||||
metadata = json.loads(data["metadata"])
|
|
||||||
for [k, v] in metadata:
|
|
||||||
if k == "text/plain":
|
|
||||||
params.update(description=v)
|
|
||||||
if k in ("image/jpeg;base64", "image/png;base64"):
|
|
||||||
data_uri = f"data:{k},{v}"
|
|
||||||
params.update(image=data_uri)
|
|
||||||
if k in ("text/email", "text/identifier"):
|
|
||||||
params.update(targetUser=v)
|
|
||||||
params.update(commentAllowed=data.get("commentAllowed", 0))
|
|
||||||
|
|
||||||
except KeyError as exc:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
|
||||||
detail={
|
|
||||||
"domain": domain,
|
|
||||||
"message": f"lnurl JSON response invalid: {exc}",
|
|
||||||
},
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
@api_router.post("/api/v1/lnurlauth")
|
|
||||||
async def api_perform_lnurlauth(
|
|
||||||
data: CreateLnurlAuth, wallet: WalletTypeInfo = Depends(require_admin_key)
|
|
||||||
):
|
|
||||||
err = await perform_lnurlauth(data.callback, wallet=wallet)
|
|
||||||
if err:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail=err.reason
|
|
||||||
)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
@api_router.get(
|
@api_router.get(
|
||||||
"/api/v1/rate/history",
|
"/api/v1/rate/history",
|
||||||
dependencies=[Depends(check_user_exists)],
|
dependencies=[Depends(check_user_exists)],
|
||||||
|
|
|
||||||
143
lnbits/core/views/lnurl_api.py
Normal file
143
lnbits/core/views/lnurl_api.py
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi import (
|
||||||
|
APIRouter,
|
||||||
|
Depends,
|
||||||
|
HTTPException,
|
||||||
|
)
|
||||||
|
from lnurl import (
|
||||||
|
LnurlResponseException,
|
||||||
|
LnurlSuccessResponse,
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
LnurlErrorResponse,
|
||||||
|
LnurlPayResponse,
|
||||||
|
LnurlResponseModel,
|
||||||
|
LnurlWithdrawResponse,
|
||||||
|
)
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.core.models import CreateLnurlWithdraw, Payment
|
||||||
|
from lnbits.core.models.lnurl import CreateLnurlPayment
|
||||||
|
from lnbits.decorators import (
|
||||||
|
WalletTypeInfo,
|
||||||
|
require_admin_key,
|
||||||
|
require_invoice_key,
|
||||||
|
)
|
||||||
|
from lnbits.helpers import check_callback_url
|
||||||
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
from ..services import fetch_lnurl_pay_request, pay_invoice
|
||||||
|
|
||||||
|
lnurl_router = APIRouter(tags=["LNURL"])
|
||||||
|
|
||||||
|
|
||||||
|
@lnurl_router.get(
|
||||||
|
"/api/v1/lnurlscan/{code}",
|
||||||
|
dependencies=[Depends(require_invoice_key)],
|
||||||
|
response_model=LnurlPayResponse
|
||||||
|
| LnurlWithdrawResponse
|
||||||
|
| LnurlAuthResponse
|
||||||
|
| LnurlErrorResponse,
|
||||||
|
)
|
||||||
|
async def api_lnurlscan(code: str) -> LnurlResponseModel:
|
||||||
|
try:
|
||||||
|
res = await lnurl_handle(code, user_agent=settings.user_agent, timeout=5)
|
||||||
|
except LnurlResponseException as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
if isinstance(res, (LnurlPayResponse, LnurlWithdrawResponse, LnurlAuthResponse)):
|
||||||
|
check_callback_url(res.callback)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@lnurl_router.post("/api/v1/lnurlauth")
|
||||||
|
async def api_perform_lnurlauth(
|
||||||
|
data: LnurlAuthResponse, key_type: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
) -> LnurlResponseModel:
|
||||||
|
check_callback_url(data.callback)
|
||||||
|
try:
|
||||||
|
res = await lnurlauth(
|
||||||
|
res=data,
|
||||||
|
seed=key_type.wallet.adminkey,
|
||||||
|
user_agent=settings.user_agent,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
return res
|
||||||
|
except LnurlResponseException as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@lnurl_router.post("/api/v1/payments/lnurl")
|
||||||
|
async def api_payments_pay_lnurl(
|
||||||
|
data: CreateLnurlPayment, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
) -> Payment:
|
||||||
|
try:
|
||||||
|
res = await fetch_lnurl_pay_request(data=data)
|
||||||
|
except LnurlResponseException as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
msg = f"Failed to connect to {data.res.callback}."
|
||||||
|
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=msg) from exc
|
||||||
|
|
||||||
|
extra: dict[str, Any] = {}
|
||||||
|
if res.success_action:
|
||||||
|
extra["success_action"] = res.success_action.json()
|
||||||
|
if data.comment:
|
||||||
|
extra["comment"] = data.comment
|
||||||
|
if data.unit and data.unit != "sat":
|
||||||
|
extra["fiat_currency"] = data.unit
|
||||||
|
extra["fiat_amount"] = data.amount / 1000
|
||||||
|
|
||||||
|
payment = await pay_invoice(
|
||||||
|
wallet_id=wallet.wallet.id,
|
||||||
|
payment_request=str(res.pr),
|
||||||
|
description=data.res.metadata.text,
|
||||||
|
extra=extra,
|
||||||
|
)
|
||||||
|
|
||||||
|
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:
|
||||||
|
try:
|
||||||
|
res = await lnurl_handle(
|
||||||
|
lnurl_data.lnurl_w.callback_url, user_agent=settings.user_agent, timeout=10
|
||||||
|
)
|
||||||
|
except (LnurlResponseException, Exception) as exc:
|
||||||
|
logger.warning(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
|
||||||
|
|
@ -1,12 +1,7 @@
|
||||||
import json
|
|
||||||
import ssl
|
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from math import ceil
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
APIRouter,
|
APIRouter,
|
||||||
Depends,
|
Depends,
|
||||||
|
|
@ -15,7 +10,7 @@ from fastapi import (
|
||||||
Query,
|
Query,
|
||||||
)
|
)
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from loguru import logger
|
from lnurl import decode as lnurl_decode
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
from lnbits.core.crud.payments import (
|
from lnbits.core.crud.payments import (
|
||||||
|
|
@ -25,10 +20,8 @@ from lnbits.core.crud.payments import (
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
CancelInvoice,
|
CancelInvoice,
|
||||||
CreateInvoice,
|
CreateInvoice,
|
||||||
CreateLnurl,
|
|
||||||
DecodePayment,
|
DecodePayment,
|
||||||
KeyType,
|
KeyType,
|
||||||
PayLnurlWData,
|
|
||||||
Payment,
|
Payment,
|
||||||
PaymentCountField,
|
PaymentCountField,
|
||||||
PaymentCountStat,
|
PaymentCountStat,
|
||||||
|
|
@ -48,13 +41,9 @@ from lnbits.decorators import (
|
||||||
require_invoice_key,
|
require_invoice_key,
|
||||||
)
|
)
|
||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
check_callback_url,
|
|
||||||
filter_dict_keys,
|
filter_dict_keys,
|
||||||
generate_filter_params_openapi,
|
generate_filter_params_openapi,
|
||||||
)
|
)
|
||||||
from lnbits.lnurl import decode as lnurl_decode
|
|
||||||
from lnbits.settings import settings
|
|
||||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
|
||||||
from lnbits.wallets.base import InvoiceResponse
|
from lnbits.wallets.base import InvoiceResponse
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
|
|
@ -284,92 +273,6 @@ async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONRespo
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _validate_lnurl_response(
|
|
||||||
params: dict, domain: str, amount_msat: int
|
|
||||||
) -> bolt11.Invoice:
|
|
||||||
if params.get("status") == "ERROR":
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail=f"{domain} said: '{params.get('reason', '')}'",
|
|
||||||
)
|
|
||||||
if not params.get("pr"):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail=f"{domain} did not return a payment request.",
|
|
||||||
)
|
|
||||||
invoice = bolt11.decode(params["pr"])
|
|
||||||
if invoice.amount_msat != amount_msat:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail=(
|
|
||||||
f"{domain} returned an invalid invoice. Expected"
|
|
||||||
f" {amount_msat} msat, got {invoice.amount_msat}."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return invoice
|
|
||||||
|
|
||||||
|
|
||||||
async def _fetch_lnurl_params(data: CreateLnurl, amount_msat: int, domain: str) -> dict:
|
|
||||||
headers = {"User-Agent": settings.user_agent}
|
|
||||||
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
|
|
||||||
try:
|
|
||||||
r: httpx.Response = await client.get(
|
|
||||||
data.callback,
|
|
||||||
params={"amount": amount_msat, "comment": data.comment},
|
|
||||||
timeout=40,
|
|
||||||
)
|
|
||||||
if r.is_error:
|
|
||||||
raise httpx.ConnectError("LNURL callback connection error")
|
|
||||||
r.raise_for_status()
|
|
||||||
except (httpx.HTTPError, ssl.SSLError) as exc:
|
|
||||||
logger.warning(exc)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail=f"Failed to connect to {domain}.",
|
|
||||||
) from exc
|
|
||||||
return json.loads(r.text)
|
|
||||||
|
|
||||||
|
|
||||||
@payment_router.post("/lnurl")
|
|
||||||
async def api_payments_pay_lnurl(
|
|
||||||
data: CreateLnurl, wallet: WalletTypeInfo = Depends(require_admin_key)
|
|
||||||
) -> Payment:
|
|
||||||
domain = urlparse(data.callback).netloc
|
|
||||||
check_callback_url(data.callback)
|
|
||||||
|
|
||||||
if data.unit and data.unit != "sat":
|
|
||||||
amount_msat = await fiat_amount_as_satoshis(data.amount, data.unit)
|
|
||||||
amount_msat = ceil(amount_msat // 1000) * 1000
|
|
||||||
else:
|
|
||||||
amount_msat = data.amount
|
|
||||||
|
|
||||||
params = await _fetch_lnurl_params(data, amount_msat, domain)
|
|
||||||
_validate_lnurl_response(params, domain, amount_msat)
|
|
||||||
|
|
||||||
extra = {}
|
|
||||||
if params.get("successAction"):
|
|
||||||
extra["success_action"] = params["successAction"]
|
|
||||||
if data.comment:
|
|
||||||
extra["comment"] = data.comment
|
|
||||||
if data.unit and data.unit != "sat":
|
|
||||||
extra["fiat_currency"] = data.unit
|
|
||||||
extra["fiat_amount"] = data.amount / 1000
|
|
||||||
if data.internal_memo is not None:
|
|
||||||
if len(data.internal_memo) > 512:
|
|
||||||
raise ValueError("Internal memo must be 512 characters or less.")
|
|
||||||
extra["internal_memo"] = data.internal_memo
|
|
||||||
if data.description is None:
|
|
||||||
raise ValueError("Description is required")
|
|
||||||
|
|
||||||
payment = await pay_invoice(
|
|
||||||
wallet_id=wallet.wallet.id,
|
|
||||||
payment_request=params["pr"],
|
|
||||||
description=data.description,
|
|
||||||
extra=extra,
|
|
||||||
)
|
|
||||||
return payment
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: refactor this route into a public and admin one
|
# TODO: refactor this route into a public and admin one
|
||||||
@payment_router.get("/{payment_hash}")
|
@payment_router.get("/{payment_hash}")
|
||||||
async def api_payment(payment_hash, x_api_key: Optional[str] = Header(None)):
|
async def api_payment(payment_hash, x_api_key: Optional[str] = Header(None)):
|
||||||
|
|
@ -428,62 +331,6 @@ async def api_payments_decode(data: DecodePayment) -> JSONResponse:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@payment_router.post("/{payment_request}/pay-with-nfc", status_code=HTTPStatus.OK)
|
|
||||||
async def api_payment_pay_with_nfc(
|
|
||||||
payment_request: str,
|
|
||||||
lnurl_data: PayLnurlWData,
|
|
||||||
) -> JSONResponse:
|
|
||||||
lnurl = lnurl_data.lnurl_w.lower()
|
|
||||||
|
|
||||||
# Follow LUD-17 -> https://github.com/lnurl/luds/blob/luds/17.md
|
|
||||||
url = lnurl.replace("lnurlw://", "https://")
|
|
||||||
|
|
||||||
headers = {"User-Agent": settings.user_agent}
|
|
||||||
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
|
|
||||||
try:
|
|
||||||
check_callback_url(url)
|
|
||||||
lnurl_req = await client.get(url, timeout=10)
|
|
||||||
if lnurl_req.is_error:
|
|
||||||
return JSONResponse(
|
|
||||||
{"success": False, "detail": "Error loading LNURL request"}
|
|
||||||
)
|
|
||||||
|
|
||||||
lnurl_res = lnurl_req.json()
|
|
||||||
|
|
||||||
if lnurl_res.get("status") == "ERROR":
|
|
||||||
return JSONResponse({"success": False, "detail": lnurl_res["reason"]})
|
|
||||||
|
|
||||||
if lnurl_res.get("tag") != "withdrawRequest":
|
|
||||||
return JSONResponse(
|
|
||||||
{"success": False, "detail": "Invalid LNURL-withdraw"}
|
|
||||||
)
|
|
||||||
|
|
||||||
callback_url = lnurl_res["callback"]
|
|
||||||
k1 = lnurl_res["k1"]
|
|
||||||
|
|
||||||
callback_req = await client.get(
|
|
||||||
callback_url,
|
|
||||||
params={"k1": k1, "pr": payment_request},
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
if callback_req.is_error:
|
|
||||||
return JSONResponse(
|
|
||||||
{"success": False, "detail": "Error loading callback request"}
|
|
||||||
)
|
|
||||||
|
|
||||||
callback_res = callback_req.json()
|
|
||||||
|
|
||||||
if callback_res.get("status") == "ERROR":
|
|
||||||
return JSONResponse(
|
|
||||||
{"success": False, "detail": callback_res["reason"]}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return JSONResponse({"success": True, "detail": callback_res})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return JSONResponse({"success": False, "detail": f"Unexpected error: {e}"})
|
|
||||||
|
|
||||||
|
|
||||||
@payment_router.post("/settle")
|
@payment_router.post("/settle")
|
||||||
async def api_payments_settle(
|
async def api_payments_settle(
|
||||||
data: SettleInvoice, key_type: WalletTypeInfo = Depends(require_admin_key)
|
data: SettleInvoice, key_type: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
|
|
||||||
2
lnbits/static/bundle.min.js
vendored
2
lnbits/static/bundle.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -19,7 +19,7 @@ window.LNbits = {
|
||||||
amount,
|
amount,
|
||||||
memo,
|
memo,
|
||||||
unit = 'sat',
|
unit = 'sat',
|
||||||
lnurlCallback = null,
|
lnurlWithdraw = null,
|
||||||
fiatProvider = null,
|
fiatProvider = null,
|
||||||
internalMemo = null,
|
internalMemo = null,
|
||||||
payment_hash = null
|
payment_hash = null
|
||||||
|
|
@ -29,7 +29,7 @@ window.LNbits = {
|
||||||
amount: amount,
|
amount: amount,
|
||||||
memo: memo,
|
memo: memo,
|
||||||
unit: unit,
|
unit: unit,
|
||||||
lnurl_callback: lnurlCallback,
|
lnurl_withdraw: lnurlWithdraw,
|
||||||
fiat_provider: fiatProvider,
|
fiat_provider: fiatProvider,
|
||||||
payment_hash: payment_hash
|
payment_hash: payment_hash
|
||||||
}
|
}
|
||||||
|
|
@ -52,36 +52,6 @@ window.LNbits = {
|
||||||
}
|
}
|
||||||
return this.request('post', '/api/v1/payments', wallet.adminkey, data)
|
return this.request('post', '/api/v1/payments', wallet.adminkey, data)
|
||||||
},
|
},
|
||||||
payLnurl(
|
|
||||||
wallet,
|
|
||||||
callback,
|
|
||||||
description_hash,
|
|
||||||
amount,
|
|
||||||
description = '',
|
|
||||||
comment = '',
|
|
||||||
unit = '',
|
|
||||||
internalMemo = null
|
|
||||||
) {
|
|
||||||
const data = {
|
|
||||||
callback,
|
|
||||||
description_hash,
|
|
||||||
amount,
|
|
||||||
comment,
|
|
||||||
description,
|
|
||||||
unit
|
|
||||||
}
|
|
||||||
|
|
||||||
if (internalMemo) {
|
|
||||||
data.internal_memo = String(internalMemo)
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.request(
|
|
||||||
'post',
|
|
||||||
'/api/v1/payments/lnurl',
|
|
||||||
wallet.adminkey,
|
|
||||||
data
|
|
||||||
)
|
|
||||||
},
|
|
||||||
cancelInvoice(wallet, paymentHash) {
|
cancelInvoice(wallet, paymentHash) {
|
||||||
return this.request('post', '/api/v1/payments/cancel', wallet.adminkey, {
|
return this.request('post', '/api/v1/payments/cancel', wallet.adminkey, {
|
||||||
payment_hash: paymentHash
|
payment_hash: paymentHash
|
||||||
|
|
@ -92,11 +62,6 @@ window.LNbits = {
|
||||||
preimage: preimage
|
preimage: preimage
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
authLnurl(wallet, callback) {
|
|
||||||
return this.request('post', '/api/v1/lnurlauth', wallet.adminkey, {
|
|
||||||
callback
|
|
||||||
})
|
|
||||||
},
|
|
||||||
createAccount(name) {
|
createAccount(name) {
|
||||||
return this.request('post', '/api/v1/account', null, {
|
return this.request('post', '/api/v1/account', null, {
|
||||||
name: name
|
name: name
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,7 @@ window.WalletPageLogic = {
|
||||||
this.receive.data.amount,
|
this.receive.data.amount,
|
||||||
this.receive.data.memo,
|
this.receive.data.memo,
|
||||||
this.receive.unit,
|
this.receive.unit,
|
||||||
this.receive.lnurl && this.receive.lnurl.callback,
|
this.receive.lnurlWithdraw,
|
||||||
this.receive.fiatProvider,
|
this.receive.fiatProvider,
|
||||||
this.receive.data.internalMemo,
|
this.receive.data.internalMemo,
|
||||||
this.receive.data.payment_hash
|
this.receive.data.payment_hash
|
||||||
|
|
@ -276,24 +276,24 @@ window.WalletPageLogic = {
|
||||||
}
|
}
|
||||||
// TODO: lnurl_callback and lnurl_response
|
// TODO: lnurl_callback and lnurl_response
|
||||||
// WITHDRAW
|
// WITHDRAW
|
||||||
if (response.data.lnurl_response !== null) {
|
if (response.data.extra.lnurl_response !== null) {
|
||||||
if (response.data.lnurl_response === false) {
|
if (response.data.extra.lnurl_response === false) {
|
||||||
response.data.lnurl_response = `Unable to connect`
|
response.data.extra.lnurl_response = `Unable to connect`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof response.data.lnurl_response === 'string') {
|
if (typeof response.data.extra.lnurl_response === 'string') {
|
||||||
// failure
|
// failure
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: `${this.receive.lnurl.domain} lnurl-withdraw call failed.`,
|
message: `${this.receive.lnurl.domain} lnurl-withdraw call failed.`,
|
||||||
caption: response.data.lnurl_response
|
caption: response.data.extra.lnurl_response
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
} else if (response.data.lnurl_response === true) {
|
} else if (response.data.extra.lnurl_response === true) {
|
||||||
// success
|
// success
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
timeout: 5000,
|
timeout: 3000,
|
||||||
message: `Invoice sent to ${this.receive.lnurl.domain}!`,
|
message: `Invoice sent to ${this.receive.lnurl.domain}!`,
|
||||||
spinner: true
|
spinner: true
|
||||||
})
|
})
|
||||||
|
|
@ -361,14 +361,15 @@ window.WalletPageLogic = {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.kind === 'pay') {
|
if (data.tag === 'payRequest') {
|
||||||
this.parse.lnurlpay = Object.freeze(data)
|
this.parse.lnurlpay = Object.freeze(data)
|
||||||
this.parse.data.amount = data.minSendable / 1000
|
this.parse.data.amount = data.minSendable / 1000
|
||||||
} else if (data.kind === 'auth') {
|
} else if (data.tag === 'login') {
|
||||||
this.parse.lnurlauth = Object.freeze(data)
|
this.parse.lnurlauth = Object.freeze(data)
|
||||||
} else if (data.kind === 'withdraw') {
|
} else if (data.tag === 'withdrawRequest') {
|
||||||
this.parse.show = false
|
this.parse.show = false
|
||||||
this.receive.show = true
|
this.receive.show = true
|
||||||
|
this.receive.lnurlWithdraw = Object.freeze(data)
|
||||||
this.receive.status = 'pending'
|
this.receive.status = 'pending'
|
||||||
this.receive.paymentReq = null
|
this.receive.paymentReq = null
|
||||||
this.receive.paymentHash = null
|
this.receive.paymentHash = null
|
||||||
|
|
@ -378,8 +379,9 @@ window.WalletPageLogic = {
|
||||||
data.minWithdrawable / 1000,
|
data.minWithdrawable / 1000,
|
||||||
data.maxWithdrawable / 1000
|
data.maxWithdrawable / 1000
|
||||||
]
|
]
|
||||||
|
const domain = data.callback.split('/')[2]
|
||||||
this.receive.lnurl = {
|
this.receive.lnurl = {
|
||||||
domain: data.domain,
|
domain: domain,
|
||||||
callback: data.callback,
|
callback: data.callback,
|
||||||
fixed: data.fixed
|
fixed: data.fixed
|
||||||
}
|
}
|
||||||
|
|
@ -515,43 +517,23 @@ window.WalletPageLogic = {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
payLnurl() {
|
payLnurl() {
|
||||||
const dismissPaymentMsg = Quasar.Notify.create({
|
|
||||||
timeout: 0,
|
|
||||||
message: 'Processing payment...'
|
|
||||||
})
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.payLnurl(
|
.request('post', '/api/v1/payments/lnurl', this.g.wallet.adminkey, {
|
||||||
this.g.wallet,
|
res: this.parse.lnurlpay,
|
||||||
this.parse.lnurlpay.callback,
|
unit: this.parse.data.unit,
|
||||||
this.parse.lnurlpay.description_hash,
|
amount: this.parse.data.amount * 1000,
|
||||||
this.parse.data.amount * 1000,
|
comment: this.parse.data.comment,
|
||||||
this.parse.lnurlpay.description.slice(0, 120),
|
internalMemo: this.parse.data.internalMemo
|
||||||
this.parse.data.comment,
|
})
|
||||||
this.parse.data.unit,
|
|
||||||
this.parse.data.internalMemo
|
|
||||||
)
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.parse.show = false
|
this.parse.show = false
|
||||||
|
if (response.data.extra.success_action) {
|
||||||
clearInterval(this.parse.paymentChecker)
|
const action = JSON.parse(response.data.extra.success_action)
|
||||||
setTimeout(() => {
|
switch (action.tag) {
|
||||||
clearInterval(this.parse.paymentChecker)
|
|
||||||
}, 40000)
|
|
||||||
this.parse.paymentChecker = setInterval(() => {
|
|
||||||
LNbits.api
|
|
||||||
.getPayment(this.g.wallet, response.data.payment_hash)
|
|
||||||
.then(res => {
|
|
||||||
if (res.data.paid) {
|
|
||||||
dismissPaymentMsg()
|
|
||||||
clearInterval(this.parse.paymentChecker)
|
|
||||||
// show lnurlpay success action
|
|
||||||
const extra = response.data.extra
|
|
||||||
if (extra.success_action) {
|
|
||||||
switch (extra.success_action.tag) {
|
|
||||||
case 'url':
|
case 'url':
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
message: `<a target="_blank" style="color: inherit" href="${extra.success_action.url}">${extra.success_action.url}</a>`,
|
message: `<a target="_blank" style="color: inherit" href="${action.url}">${action.url}</a>`,
|
||||||
caption: extra.success_action.description,
|
caption: action.description,
|
||||||
html: true,
|
html: true,
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
|
|
@ -560,22 +542,14 @@ window.WalletPageLogic = {
|
||||||
break
|
break
|
||||||
case 'message':
|
case 'message':
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
message: extra.success_action.message,
|
message: action.message,
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
closeBtn: true
|
closeBtn: true
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'aes':
|
case 'aes':
|
||||||
LNbits.api
|
decryptLnurlPayAES(action, response.data.preimage)
|
||||||
.getPayment(this.g.wallet, response.data.payment_hash)
|
|
||||||
.then(({data: payment}) =>
|
|
||||||
decryptLnurlPayAES(
|
|
||||||
extra.success_action,
|
|
||||||
payment.preimage
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.then(value => {
|
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
message: value,
|
message: value,
|
||||||
caption: extra.success_action.description,
|
caption: extra.success_action.description,
|
||||||
|
|
@ -584,17 +558,8 @@ window.WalletPageLogic = {
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
closeBtn: true
|
closeBtn: true
|
||||||
})
|
})
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
}, 2000)
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
dismissPaymentMsg()
|
|
||||||
LNbits.utils.notifyApiError(err)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
authLnurl() {
|
authLnurl() {
|
||||||
|
|
@ -602,9 +567,13 @@ window.WalletPageLogic = {
|
||||||
timeout: 10,
|
timeout: 10,
|
||||||
message: 'Performing authentication...'
|
message: 'Performing authentication...'
|
||||||
})
|
})
|
||||||
|
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.authLnurl(this.g.wallet, this.parse.lnurlauth.callback)
|
.request(
|
||||||
|
'post',
|
||||||
|
'/api/v1/lnurlauth',
|
||||||
|
wallet.adminkey,
|
||||||
|
this.parse.lnurlauth
|
||||||
|
)
|
||||||
.then(_ => {
|
.then(_ => {
|
||||||
dismissAuthMsg()
|
dismissAuthMsg()
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
|
|
@ -615,10 +584,9 @@ window.WalletPageLogic = {
|
||||||
this.parse.show = false
|
this.parse.show = false
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
dismissAuthMsg()
|
|
||||||
if (err.response.data.reason) {
|
if (err.response.data.reason) {
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
message: `Authentication failed. ${this.parse.lnurlauth.domain} says:`,
|
message: `Authentication failed. ${this.parse.lnurlauth.callback} says:`,
|
||||||
caption: err.response.data.reason,
|
caption: err.response.data.reason,
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
timeout: 5000
|
timeout: 5000
|
||||||
|
|
|
||||||
130
poetry.lock
generated
130
poetry.lock
generated
|
|
@ -455,6 +455,21 @@ files = [
|
||||||
{file = "bech32-1.2.0.tar.gz", hash = "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899"},
|
{file = "bech32-1.2.0.tar.gz", hash = "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bip32"
|
||||||
|
version = "4.0"
|
||||||
|
description = "Minimalistic implementation of the BIP32 key derivation scheme"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "bip32-4.0-py3-none-any.whl", hash = "sha256:9728b38336129c00e1f870bbb3e328c9632d51c1bddeef4011fd3115cb3aeff9"},
|
||||||
|
{file = "bip32-4.0.tar.gz", hash = "sha256:8035588f252f569bb414bc60df151ae431fc1c6789a19488a32890532ef3a2fc"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
coincurve = ">=15.0,<21"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitarray"
|
name = "bitarray"
|
||||||
version = "3.4.3"
|
version = "3.4.3"
|
||||||
|
|
@ -2118,21 +2133,23 @@ valkey = ["valkey (>=6)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lnurl"
|
name = "lnurl"
|
||||||
version = "0.5.3"
|
version = "0.6.8"
|
||||||
description = "LNURL implementation for Python."
|
description = "LNURL implementation for Python."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "<4.0,>=3.9"
|
python-versions = ">=3.10"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "lnurl-0.5.3-py3-none-any.whl", hash = "sha256:feaf6c60b0b7f104894ef3accbd30d23d52e038c2797c58432baea7f4a8aa952"},
|
{file = "lnurl-0.6.8-py3-none-any.whl", hash = "sha256:4fff53efcdd401cf4169676bd1ab85e9e241a762a9a5407ee11e6a6e120e8279"},
|
||||||
{file = "lnurl-0.5.3.tar.gz", hash = "sha256:60154bcfdbb98fb143eeca970a16d73a582f28e057a826b5f222259411c497fe"},
|
{file = "lnurl-0.6.8.tar.gz", hash = "sha256:de64a47179980a4b52cd6b89ad377cda14502f1998f53724490683f6f5c4ed90"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
bech32 = ">=1.2.0,<2.0.0"
|
bech32 = "*"
|
||||||
bolt11 = ">=2.0.5,<3.0.0"
|
bip32 = ">=4.0,<5.0"
|
||||||
ecdsa = ">=0.19.0,<0.20.0"
|
bolt11 = "*"
|
||||||
httpx = ">=0.27.0,<0.28.0"
|
ecdsa = "*"
|
||||||
|
httpx = "*"
|
||||||
|
pycryptodomex = ">=3.21.0,<4.0.0"
|
||||||
pydantic = ">=1,<2"
|
pydantic = ">=1,<2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2981,55 +2998,62 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "1.10.18"
|
version = "1.10.22"
|
||||||
description = "Data validation and settings management using python type hints"
|
description = "Data validation and settings management using python type hints"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
groups = ["main", "dev"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic-1.10.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02"},
|
{file = "pydantic-1.10.22-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:57889565ccc1e5b7b73343329bbe6198ebc472e3ee874af2fa1865cfe7048228"},
|
||||||
{file = "pydantic-1.10.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80"},
|
{file = "pydantic-1.10.22-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90729e22426de79bc6a3526b4c45ec4400caf0d4f10d7181ba7f12c01bb3897d"},
|
||||||
{file = "pydantic-1.10.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62"},
|
{file = "pydantic-1.10.22-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8684d347f351554ec94fdcb507983d3116dc4577fb8799fed63c65869a2d10"},
|
||||||
{file = "pydantic-1.10.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab"},
|
{file = "pydantic-1.10.22-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8dad498ceff2d9ef1d2e2bc6608f5b59b8e1ba2031759b22dfb8c16608e1802"},
|
||||||
{file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0"},
|
{file = "pydantic-1.10.22-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fac529cc654d4575cf8de191cce354b12ba705f528a0a5c654de6d01f76cd818"},
|
||||||
{file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861"},
|
{file = "pydantic-1.10.22-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4148232aded8dd1dd13cf910a01b32a763c34bd79a0ab4d1ee66164fcb0b7b9d"},
|
||||||
{file = "pydantic-1.10.18-cp310-cp310-win_amd64.whl", hash = "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70"},
|
{file = "pydantic-1.10.22-cp310-cp310-win_amd64.whl", hash = "sha256:ece68105d9e436db45d8650dc375c760cc85a6793ae019c08769052902dca7db"},
|
||||||
{file = "pydantic-1.10.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec"},
|
{file = "pydantic-1.10.22-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e530a8da353f791ad89e701c35787418605d35085f4bdda51b416946070e938"},
|
||||||
{file = "pydantic-1.10.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5"},
|
{file = "pydantic-1.10.22-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:654322b85642e9439d7de4c83cb4084ddd513df7ff8706005dada43b34544946"},
|
||||||
{file = "pydantic-1.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481"},
|
{file = "pydantic-1.10.22-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8bece75bd1b9fc1c32b57a32831517943b1159ba18b4ba32c0d431d76a120ae"},
|
||||||
{file = "pydantic-1.10.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3"},
|
{file = "pydantic-1.10.22-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eccb58767f13c6963dcf96d02cb8723ebb98b16692030803ac075d2439c07b0f"},
|
||||||
{file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588"},
|
{file = "pydantic-1.10.22-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7778e6200ff8ed5f7052c1516617423d22517ad36cc7a3aedd51428168e3e5e8"},
|
||||||
{file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f"},
|
{file = "pydantic-1.10.22-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bffe02767d27c39af9ca7dc7cd479c00dda6346bb62ffc89e306f665108317a2"},
|
||||||
{file = "pydantic-1.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33"},
|
{file = "pydantic-1.10.22-cp311-cp311-win_amd64.whl", hash = "sha256:23bc19c55427091b8e589bc08f635ab90005f2dc99518f1233386f46462c550a"},
|
||||||
{file = "pydantic-1.10.18-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2"},
|
{file = "pydantic-1.10.22-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:92d0f97828a075a71d9efc65cf75db5f149b4d79a38c89648a63d2932894d8c9"},
|
||||||
{file = "pydantic-1.10.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048"},
|
{file = "pydantic-1.10.22-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af5a2811b6b95b58b829aeac5996d465a5f0c7ed84bd871d603cf8646edf6ff"},
|
||||||
{file = "pydantic-1.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7"},
|
{file = "pydantic-1.10.22-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cf06d8d40993e79af0ab2102ef5da77b9ddba51248e4cb27f9f3f591fbb096e"},
|
||||||
{file = "pydantic-1.10.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890"},
|
{file = "pydantic-1.10.22-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:184b7865b171a6057ad97f4a17fbac81cec29bd103e996e7add3d16b0d95f609"},
|
||||||
{file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f"},
|
{file = "pydantic-1.10.22-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:923ad861677ab09d89be35d36111156063a7ebb44322cdb7b49266e1adaba4bb"},
|
||||||
{file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a"},
|
{file = "pydantic-1.10.22-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:82d9a3da1686443fb854c8d2ab9a473251f8f4cdd11b125522efb4d7c646e7bc"},
|
||||||
{file = "pydantic-1.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357"},
|
{file = "pydantic-1.10.22-cp312-cp312-win_amd64.whl", hash = "sha256:1612604929af4c602694a7f3338b18039d402eb5ddfbf0db44f1ebfaf07f93e7"},
|
||||||
{file = "pydantic-1.10.18-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3d5492dbf953d7d849751917e3b2433fb26010d977aa7a0765c37425a4026ff1"},
|
{file = "pydantic-1.10.22-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b259dc89c9abcd24bf42f31951fb46c62e904ccf4316393f317abeeecda39978"},
|
||||||
{file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe734914977eed33033b70bfc097e1baaffb589517863955430bf2e0846ac30f"},
|
{file = "pydantic-1.10.22-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9238aa0964d80c0908d2f385e981add58faead4412ca80ef0fa352094c24e46d"},
|
||||||
{file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15fdbe568beaca9aacfccd5ceadfb5f1a235087a127e8af5e48df9d8a45ae85c"},
|
{file = "pydantic-1.10.22-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f8029f05b04080e3f1a550575a1bca747c0ea4be48e2d551473d47fd768fc1b"},
|
||||||
{file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c3e742f62198c9eb9201781fbebe64533a3bbf6a76a91b8d438d62b813079dbc"},
|
{file = "pydantic-1.10.22-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5c06918894f119e0431a36c9393bc7cceeb34d1feeb66670ef9b9ca48c073937"},
|
||||||
{file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:19a3bd00b9dafc2cd7250d94d5b578edf7a0bd7daf102617153ff9a8fa37871c"},
|
{file = "pydantic-1.10.22-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e205311649622ee8fc1ec9089bd2076823797f5cd2c1e3182dc0e12aab835b35"},
|
||||||
{file = "pydantic-1.10.18-cp37-cp37m-win_amd64.whl", hash = "sha256:2ce3fcf75b2bae99aa31bd4968de0474ebe8c8258a0110903478bd83dfee4e3b"},
|
{file = "pydantic-1.10.22-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:815f0a73d5688d6dd0796a7edb9eca7071bfef961a7b33f91e618822ae7345b7"},
|
||||||
{file = "pydantic-1.10.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03"},
|
{file = "pydantic-1.10.22-cp313-cp313-win_amd64.whl", hash = "sha256:9dfce71d42a5cde10e78a469e3d986f656afc245ab1b97c7106036f088dd91f8"},
|
||||||
{file = "pydantic-1.10.18-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a"},
|
{file = "pydantic-1.10.22-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3ecaf8177b06aac5d1f442db1288e3b46d9f05f34fd17fdca3ad34105328b61a"},
|
||||||
{file = "pydantic-1.10.18-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b"},
|
{file = "pydantic-1.10.22-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb36c2de9ea74bd7f66b5481dea8032d399affd1cbfbb9bb7ce539437f1fce62"},
|
||||||
{file = "pydantic-1.10.18-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682"},
|
{file = "pydantic-1.10.22-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6b8d14a256be3b8fff9286d76c532f1a7573fbba5f189305b22471c6679854d"},
|
||||||
{file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe"},
|
{file = "pydantic-1.10.22-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:1c33269e815db4324e71577174c29c7aa30d1bba51340ce6be976f6f3053a4c6"},
|
||||||
{file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3"},
|
{file = "pydantic-1.10.22-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:8661b3ab2735b2a9ccca2634738534a795f4a10bae3ab28ec0a10c96baa20182"},
|
||||||
{file = "pydantic-1.10.18-cp38-cp38-win_amd64.whl", hash = "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620"},
|
{file = "pydantic-1.10.22-cp37-cp37m-win_amd64.whl", hash = "sha256:22bdd5fe70d4549995981c55b970f59de5c502d5656b2abdfcd0a25be6f3763e"},
|
||||||
{file = "pydantic-1.10.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf"},
|
{file = "pydantic-1.10.22-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e3f33d1358aa4bc2795208cc29ff3118aeaad0ea36f0946788cf7cadeccc166b"},
|
||||||
{file = "pydantic-1.10.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9"},
|
{file = "pydantic-1.10.22-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:813f079f9cd136cac621f3f9128a4406eb8abd2ad9fdf916a0731d91c6590017"},
|
||||||
{file = "pydantic-1.10.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d"},
|
{file = "pydantic-1.10.22-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab618ab8dca6eac7f0755db25f6aba3c22c40e3463f85a1c08dc93092d917704"},
|
||||||
{file = "pydantic-1.10.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485"},
|
{file = "pydantic-1.10.22-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d128e1aaa38db88caca920d5822c98fc06516a09a58b6d3d60fa5ea9099b32cc"},
|
||||||
{file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f"},
|
{file = "pydantic-1.10.22-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:cc97bbc25def7025e55fc9016080773167cda2aad7294e06a37dda04c7d69ece"},
|
||||||
{file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86"},
|
{file = "pydantic-1.10.22-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dda5d7157d543b1fa565038cae6e952549d0f90071c839b3740fb77c820fab8"},
|
||||||
{file = "pydantic-1.10.18-cp39-cp39-win_amd64.whl", hash = "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518"},
|
{file = "pydantic-1.10.22-cp38-cp38-win_amd64.whl", hash = "sha256:a093fe44fe518cb445d23119511a71f756f8503139d02fcdd1173f7b76c95ffe"},
|
||||||
{file = "pydantic-1.10.18-py3-none-any.whl", hash = "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82"},
|
{file = "pydantic-1.10.22-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec54c89b2568b258bb30d7348ac4d82bec1b58b377fb56a00441e2ac66b24587"},
|
||||||
{file = "pydantic-1.10.18.tar.gz", hash = "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a"},
|
{file = "pydantic-1.10.22-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d8f1d1a1532e4f3bcab4e34e8d2197a7def4b67072acd26cfa60e92d75803a48"},
|
||||||
|
{file = "pydantic-1.10.22-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ad83ca35508c27eae1005b6b61f369f78aae6d27ead2135ec156a2599910121"},
|
||||||
|
{file = "pydantic-1.10.22-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53cdb44b78c420f570ff16b071ea8cd5a477635c6b0efc343c8a91e3029bbf1a"},
|
||||||
|
{file = "pydantic-1.10.22-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:16d0a5ae9d98264186ce31acdd7686ec05fd331fab9d68ed777d5cb2d1514e5e"},
|
||||||
|
{file = "pydantic-1.10.22-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8aee040e25843f036192b1a1af62117504a209a043aa8db12e190bb86ad7e611"},
|
||||||
|
{file = "pydantic-1.10.22-cp39-cp39-win_amd64.whl", hash = "sha256:7f691eec68dbbfca497d3c11b92a3e5987393174cbedf03ec7a4184c35c2def6"},
|
||||||
|
{file = "pydantic-1.10.22-py3-none-any.whl", hash = "sha256:343037d608bcbd34df937ac259708bfc83664dadf88afe8516c4f282d7d471a9"},
|
||||||
|
{file = "pydantic-1.10.22.tar.gz", hash = "sha256:ee1006cebd43a8e7158fb7190bb8f4e2da9649719bff65d0c287282ec38dec6d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -4549,4 +4573,4 @@ migration = ["psycopg2-binary"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = "~3.12 | ~3.11 | ~3.10"
|
python-versions = "~3.12 | ~3.11 | ~3.10"
|
||||||
content-hash = "9f42f1abe4036957a574f61f84566522334414e1d43e5b993cc989470945aa32"
|
content-hash = "c1714a4df0e8f7d8702fffe9c57b21fc39effb194631741b4718466c18b73d8f"
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ fastapi = "0.116.1"
|
||||||
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.5.3"
|
lnurl = "0.6.8"
|
||||||
pydantic = "1.10.18"
|
pydantic = "1.10.22"
|
||||||
pyqrcode = "1.2.1"
|
pyqrcode = "1.2.1"
|
||||||
shortuuid = "1.0.13"
|
shortuuid = "1.0.13"
|
||||||
sse-starlette = "2.3.6"
|
sse-starlette = "2.3.6"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from json import JSONDecodeError
|
||||||
from unittest.mock import AsyncMock, Mock
|
from unittest.mock import AsyncMock, Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -587,13 +588,14 @@ async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
|
||||||
"tag": "withdrawRequest",
|
"tag": "withdrawRequest",
|
||||||
"callback": "https://example.com/callback",
|
"callback": "https://example.com/callback",
|
||||||
"k1": "randomk1value",
|
"k1": "randomk1value",
|
||||||
|
"minWithdrawable": 1000,
|
||||||
|
"maxWithdrawable": 1_500_000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"status": "OK",
|
"status": "OK",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"success": True,
|
"status": "OK",
|
||||||
"detail": {"status": "OK"},
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
# Error loading LNURL request
|
# Error loading LNURL request
|
||||||
|
|
@ -601,33 +603,35 @@ async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
|
||||||
"error_loading_lnurl",
|
"error_loading_lnurl",
|
||||||
None,
|
None,
|
||||||
{
|
{
|
||||||
"success": False,
|
"status": "ERROR",
|
||||||
"detail": "Error loading LNURL request",
|
"reason": "Error loading callback request",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
# LNURL response with error status
|
# LNURL response with error status
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
"status": "ERROR",
|
"status": "ERROR",
|
||||||
"reason": "LNURL request failed",
|
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
{
|
{
|
||||||
"success": False,
|
"status": "ERROR",
|
||||||
"detail": "LNURL request failed",
|
"reason": "Invalid LNURL-withdraw response.",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
# Invalid LNURL-withdraw
|
# Invalid LNURL-withdraw pay request
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
"tag": "payRequest",
|
"tag": "payRequest",
|
||||||
"callback": "https://example.com/callback",
|
"callback": "https://example.com/callback",
|
||||||
"k1": "randomk1value",
|
"k1": "randomk1value",
|
||||||
|
"minSendable": 1000,
|
||||||
|
"maxSendable": 1_500_000,
|
||||||
|
"metadata": '[["text/plain", "Payment to yo"]]',
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
{
|
{
|
||||||
"success": False,
|
"status": "ERROR",
|
||||||
"detail": "Invalid LNURL-withdraw",
|
"reason": "Invalid LNURL-withdraw response.",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
# Error loading callback request
|
# Error loading callback request
|
||||||
|
|
@ -636,11 +640,13 @@ async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
|
||||||
"tag": "withdrawRequest",
|
"tag": "withdrawRequest",
|
||||||
"callback": "https://example.com/callback",
|
"callback": "https://example.com/callback",
|
||||||
"k1": "randomk1value",
|
"k1": "randomk1value",
|
||||||
|
"minWithdrawable": 1000,
|
||||||
|
"maxWithdrawable": 1_500_000,
|
||||||
},
|
},
|
||||||
"error_loading_callback",
|
"error_loading_callback",
|
||||||
{
|
{
|
||||||
"success": False,
|
"status": "ERROR",
|
||||||
"detail": "Error loading callback request",
|
"reason": "Error loading callback request",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
# Callback response with error status
|
# Callback response with error status
|
||||||
|
|
@ -649,14 +655,16 @@ async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
|
||||||
"tag": "withdrawRequest",
|
"tag": "withdrawRequest",
|
||||||
"callback": "https://example.com/callback",
|
"callback": "https://example.com/callback",
|
||||||
"k1": "randomk1value",
|
"k1": "randomk1value",
|
||||||
|
"minWithdrawable": 1000,
|
||||||
|
"maxWithdrawable": 1_500_000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"status": "ERROR",
|
"status": "ERROR",
|
||||||
"reason": "Callback failed",
|
"reason": "Callback failed",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"success": False,
|
"status": "ERROR",
|
||||||
"detail": "Callback failed",
|
"reason": "Callback failed",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
# Unexpected exception during LNURL response JSON parsing
|
# Unexpected exception during LNURL response JSON parsing
|
||||||
|
|
@ -664,8 +672,8 @@ async def test_fiat_tracking(client, adminkey_headers_from, settings: Settings):
|
||||||
"exception_in_lnurl_response_json",
|
"exception_in_lnurl_response_json",
|
||||||
None,
|
None,
|
||||||
{
|
{
|
||||||
"success": False,
|
"status": "ERROR",
|
||||||
"detail": "Unexpected error: Simulated exception",
|
"reason": "Invalid JSON response from https://example.com/lnurl",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -677,25 +685,35 @@ async def test_api_payment_pay_with_nfc(
|
||||||
callback_response_data,
|
callback_response_data,
|
||||||
expected_response,
|
expected_response,
|
||||||
):
|
):
|
||||||
payment_request = "lnbc1..."
|
payment_request = (
|
||||||
|
"lnbc15u1p3xnhl2pp5jptserfk3zk4qy42tlucycrfwxhydvlemu9pqr93tuzlv9cc7g3sdq"
|
||||||
|
"svfhkcap3xyhx7un8cqzpgxqzjcsp5f8c52y2stc300gl6s4xswtjpc37hrnnr3c9wvtgjfu"
|
||||||
|
"vqmpm35evq9qyyssqy4lgd8tj637qcjp05rdpxxykjenthxftej7a2zzmwrmrl70fyj9hvj0"
|
||||||
|
"rewhzj7jfyuwkwcg9g2jpwtk3wkjtwnkdks84hsnu8xps5vsq4gj5hs"
|
||||||
|
)
|
||||||
lnurl = "lnurlw://example.com/lnurl"
|
lnurl = "lnurlw://example.com/lnurl"
|
||||||
lnurl_data = {"lnurl_w": lnurl}
|
|
||||||
|
|
||||||
# Create a mock for httpx.AsyncClient
|
# Create a mock for httpx.AsyncClient
|
||||||
mock_async_client = AsyncMock()
|
mock_async_client = AsyncMock()
|
||||||
mock_async_client.__aenter__.return_value = mock_async_client
|
mock_async_client.__aenter__.return_value = mock_async_client
|
||||||
|
|
||||||
# Mock the get method
|
# Mock the get method
|
||||||
async def mock_get(url, *args, **kwargs):
|
async def mock_get(url, *_, **__):
|
||||||
if url == "https://example.com/lnurl":
|
if url == "https://example.com/lnurl":
|
||||||
if lnurl_response_data == "error_loading_lnurl":
|
if lnurl_response_data == "error_loading_lnurl":
|
||||||
response = Mock()
|
response = Mock()
|
||||||
response.is_error = True
|
response.is_error = True
|
||||||
|
response.status_code = 500
|
||||||
|
response.raise_for_status.side_effect = Exception(
|
||||||
|
"Error loading callback request"
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
elif lnurl_response_data == "exception_in_lnurl_response_json":
|
elif lnurl_response_data == "exception_in_lnurl_response_json":
|
||||||
response = Mock()
|
response = Mock()
|
||||||
response.is_error = False
|
response.is_error = False
|
||||||
response.json.side_effect = Exception("Simulated exception")
|
response.json.side_effect = JSONDecodeError(
|
||||||
|
doc="Simulated exception", pos=0, msg="JSONDecodeError"
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
elif isinstance(lnurl_response_data, dict):
|
elif isinstance(lnurl_response_data, dict):
|
||||||
response = Mock()
|
response = Mock()
|
||||||
|
|
@ -706,11 +724,19 @@ async def test_api_payment_pay_with_nfc(
|
||||||
# Handle unexpected data
|
# Handle unexpected data
|
||||||
response = Mock()
|
response = Mock()
|
||||||
response.is_error = True
|
response.is_error = True
|
||||||
|
response.status_code = 500
|
||||||
|
response.raise_for_status.side_effect = Exception(
|
||||||
|
"Error loading callback request"
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
elif url == "https://example.com/callback":
|
elif url == "https://example.com/callback":
|
||||||
if callback_response_data == "error_loading_callback":
|
if callback_response_data == "error_loading_callback":
|
||||||
response = Mock()
|
response = Mock()
|
||||||
response.is_error = True
|
response.is_error = True
|
||||||
|
response.status_code = 500
|
||||||
|
response.raise_for_status.side_effect = Exception(
|
||||||
|
"Error loading callback request"
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
elif isinstance(callback_response_data, dict):
|
elif isinstance(callback_response_data, dict):
|
||||||
response = Mock()
|
response = Mock()
|
||||||
|
|
@ -721,10 +747,16 @@ async def test_api_payment_pay_with_nfc(
|
||||||
# Handle cases where callback is not called
|
# Handle cases where callback is not called
|
||||||
response = Mock()
|
response = Mock()
|
||||||
response.is_error = True
|
response.is_error = True
|
||||||
|
response.raise_for_status.side_effect = Exception(
|
||||||
|
"Error loading callback request"
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
response = Mock()
|
response = Mock()
|
||||||
response.is_error = True
|
response.is_error = True
|
||||||
|
response.raise_for_status.side_effect = Exception(
|
||||||
|
"Error loading callback request"
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
mock_async_client.get.side_effect = mock_get
|
mock_async_client.get.side_effect = mock_get
|
||||||
|
|
@ -734,7 +766,7 @@ async def test_api_payment_pay_with_nfc(
|
||||||
|
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
f"/api/v1/payments/{payment_request}/pay-with-nfc",
|
f"/api/v1/payments/{payment_request}/pay-with-nfc",
|
||||||
json=lnurl_data,
|
json={"lnurl_w": lnurl},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == HTTPStatus.OK
|
assert response.status_code == HTTPStatus.OK
|
||||||
|
|
@ -743,27 +775,32 @@ async def test_api_payment_pay_with_nfc(
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_api_payments_pay_lnurl(client, adminkey_headers_from):
|
async def test_api_payments_pay_lnurl(client, adminkey_headers_from):
|
||||||
valid_lnurl_data = {
|
lnurl_data = {
|
||||||
"description_hash": "randomhash",
|
"res": {
|
||||||
"callback": "https://xxxxxxx.lnbits.com",
|
"callback": "https://xxxxxxx.lnbits.com",
|
||||||
|
"minSendable": 1000,
|
||||||
|
"maxSendable": 1_500_000,
|
||||||
|
"metadata": '[["text/plain", "Payment to yo"]]',
|
||||||
|
},
|
||||||
"amount": 1000,
|
"amount": 1000,
|
||||||
"unit": "sat",
|
"unit": "sat",
|
||||||
"comment": "test comment",
|
"comment": "test comment",
|
||||||
"description": "test description",
|
"description": "test description",
|
||||||
}
|
}
|
||||||
|
|
||||||
invalid_lnurl_data = {**valid_lnurl_data, "callback": "invalid_url"}
|
|
||||||
|
|
||||||
# Test with valid callback URL
|
# Test with valid callback URL
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
"/api/v1/payments/lnurl", json=valid_lnurl_data, headers=adminkey_headers_from
|
"/api/v1/payments/lnurl", json=lnurl_data, headers=adminkey_headers_from
|
||||||
)
|
)
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert response.json()["detail"] == "Failed to connect to xxxxxxx.lnbits.com."
|
assert (
|
||||||
|
response.json()["detail"] == "Failed to connect to https://xxxxxxx.lnbits.com."
|
||||||
|
)
|
||||||
|
|
||||||
# Test with invalid callback URL
|
# Test with invalid callback URL
|
||||||
|
lnurl_data["res"]["callback"] = "invalid-url.lnbits.com"
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
"/api/v1/payments/lnurl", json=invalid_lnurl_data, headers=adminkey_headers_from
|
"/api/v1/payments/lnurl", json=lnurl_data, headers=adminkey_headers_from
|
||||||
)
|
)
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert "Callback not allowed." in response.json()["detail"]
|
assert "value_error.url.scheme" in response.json()["detail"]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue