feat: adds lnaddress support to payments in host
This commit is contained in:
@@ -50,6 +50,11 @@ from .registry import extension_api_method
|
|||||||
logger = logging.getLogger("lnbits.extensions")
|
logger = logging.getLogger("lnbits.extensions")
|
||||||
|
|
||||||
|
|
||||||
|
def _looks_like_lnurl_pay_target(payment_request: str) -> bool:
|
||||||
|
normalized = payment_request.strip().lower()
|
||||||
|
return normalized.startswith(("lnurl", "lightning:lnurl")) or "@" in normalized
|
||||||
|
|
||||||
|
|
||||||
class ExtensionHostAPI:
|
class ExtensionHostAPI:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -60,6 +65,7 @@ class ExtensionHostAPI:
|
|||||||
access_token: str | None = None,
|
access_token: str | None = None,
|
||||||
context: str = "user",
|
context: str = "user",
|
||||||
owner_id: str | None = None,
|
owner_id: str | None = None,
|
||||||
|
wallet_id: str | None = None,
|
||||||
invocation_id: str | None = None,
|
invocation_id: str | None = None,
|
||||||
runtime_limits: dict[str, int] | None = None,
|
runtime_limits: dict[str, int] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -69,6 +75,7 @@ class ExtensionHostAPI:
|
|||||||
self.access_token = access_token
|
self.access_token = access_token
|
||||||
self.context = context
|
self.context = context
|
||||||
self.owner_id = sha256s(user_id) if user_id else owner_id
|
self.owner_id = sha256s(user_id) if user_id else owner_id
|
||||||
|
self.wallet_id = wallet_id
|
||||||
self.invocation_id = invocation_id
|
self.invocation_id = invocation_id
|
||||||
self.runtime_limits = runtime_limits or {}
|
self.runtime_limits = runtime_limits or {}
|
||||||
from .utils import ExtensionAPIUtils
|
from .utils import ExtensionAPIUtils
|
||||||
@@ -367,29 +374,48 @@ class ExtensionHostAPI:
|
|||||||
async def wallet_pay_invoice(
|
async def wallet_pay_invoice(
|
||||||
self, request: PayInvoiceRequest
|
self, request: PayInvoiceRequest
|
||||||
) -> PayInvoiceResponse:
|
) -> PayInvoiceResponse:
|
||||||
|
from lnurl import LnurlResponseException
|
||||||
|
|
||||||
from lnbits.core.crud.wallets import get_wallet
|
from lnbits.core.crud.wallets import get_wallet
|
||||||
|
from lnbits.core.services.lnurl import get_pr_from_lnurl
|
||||||
from lnbits.core.services.payments import pay_invoice
|
from lnbits.core.services.payments import pay_invoice
|
||||||
from lnbits.exceptions import PaymentError
|
from lnbits.exceptions import PaymentError
|
||||||
|
|
||||||
if not self.user_id:
|
wallet = await get_wallet(request.wallet_id)
|
||||||
|
if wallet is None:
|
||||||
|
raise PermissionError("Paying invoices from this wallet is not allowed.")
|
||||||
|
if self.user_id:
|
||||||
|
if wallet.user != self.user_id:
|
||||||
|
raise PermissionError(
|
||||||
|
"Paying invoices from this wallet is not allowed."
|
||||||
|
)
|
||||||
|
elif not (self.context == "event" and request.wallet_id == self.wallet_id):
|
||||||
raise PermissionError(
|
raise PermissionError(
|
||||||
"Paying an invoice requires an authenticated user context."
|
"Paying an invoice requires an authenticated user context."
|
||||||
)
|
)
|
||||||
|
|
||||||
wallet = await get_wallet(request.wallet_id)
|
|
||||||
if wallet is None or wallet.user != self.user_id:
|
|
||||||
raise PermissionError("Paying invoices from this wallet is not allowed.")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
payment_request = request.payment_request
|
||||||
|
if _looks_like_lnurl_pay_target(payment_request):
|
||||||
|
if request.max_sat is None:
|
||||||
|
return PayInvoiceResponse(
|
||||||
|
ok=False,
|
||||||
|
error="max_sat is required for LNURL payments.",
|
||||||
|
)
|
||||||
|
payment_request = await get_pr_from_lnurl(
|
||||||
|
payment_request,
|
||||||
|
request.max_sat * 1000,
|
||||||
|
request.description or None,
|
||||||
|
)
|
||||||
payment = await pay_invoice(
|
payment = await pay_invoice(
|
||||||
wallet_id=request.wallet_id,
|
wallet_id=request.wallet_id,
|
||||||
payment_request=request.payment_request,
|
payment_request=payment_request,
|
||||||
max_sat=request.max_sat,
|
max_sat=request.max_sat,
|
||||||
extra={"tag": self.extension_id, **request.extra},
|
extra={"tag": self.extension_id, **request.extra},
|
||||||
description=request.description,
|
description=request.description,
|
||||||
tag=self.extension_id,
|
tag=self.extension_id,
|
||||||
)
|
)
|
||||||
except (PaymentError, ValueError) as exc:
|
except (PaymentError, ValueError, LnurlResponseException) as exc:
|
||||||
return PayInvoiceResponse(ok=False, error=str(exc))
|
return PayInvoiceResponse(ok=False, error=str(exc))
|
||||||
|
|
||||||
return PayInvoiceResponse(
|
return PayInvoiceResponse(
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ async def invoke_wasm_extension_export(
|
|||||||
access_token=access_token,
|
access_token=access_token,
|
||||||
context=context,
|
context=context,
|
||||||
owner_id=owner_id,
|
owner_id=owner_id,
|
||||||
|
wallet_id=wallet_id,
|
||||||
invocation_id=invocation.id,
|
invocation_id=invocation.id,
|
||||||
runtime_limits=limits,
|
runtime_limits=limits,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from lnbits.core.wasm_ext.api.host import ExtensionHostAPI
|
||||||
|
from lnbits.core.wasm_ext.api.models import PayInvoiceRequest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_wasm_wallet_pay_invoice_resolves_ln_address(mocker):
|
||||||
|
calls = {}
|
||||||
|
|
||||||
|
async def get_wallet(wallet_id: str):
|
||||||
|
calls["get_wallet"] = wallet_id
|
||||||
|
return SimpleNamespace(user="user1")
|
||||||
|
|
||||||
|
async def get_pr_from_lnurl(lnurl: str, amount_msat: int, comment: str | None):
|
||||||
|
calls["lnurl"] = (lnurl, amount_msat, comment)
|
||||||
|
return "lnbc1resolved"
|
||||||
|
|
||||||
|
async def pay_invoice(**kwargs):
|
||||||
|
calls["pay_invoice"] = kwargs
|
||||||
|
return SimpleNamespace(
|
||||||
|
checking_id="checking",
|
||||||
|
payment_hash="hash",
|
||||||
|
status="success",
|
||||||
|
amount=-21_000,
|
||||||
|
fee=-10,
|
||||||
|
pending=False,
|
||||||
|
success=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mocker.patch("lnbits.core.crud.wallets.get_wallet", get_wallet)
|
||||||
|
mocker.patch("lnbits.core.services.lnurl.get_pr_from_lnurl", get_pr_from_lnurl)
|
||||||
|
mocker.patch("lnbits.core.services.payments.pay_invoice", pay_invoice)
|
||||||
|
|
||||||
|
api = ExtensionHostAPI("demoext", ["wallet.pay_invoice"], user_id="user1")
|
||||||
|
response = await api.wallet_pay_invoice(
|
||||||
|
PayInvoiceRequest(
|
||||||
|
wallet_id="wallet1",
|
||||||
|
payment_request="alice@example.com",
|
||||||
|
max_sat=21,
|
||||||
|
description="winner",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.ok is True
|
||||||
|
assert response.checking_id == "checking"
|
||||||
|
assert calls["get_wallet"] == "wallet1"
|
||||||
|
assert calls["lnurl"] == ("alice@example.com", 21_000, "winner")
|
||||||
|
assert calls["pay_invoice"]["payment_request"] == "lnbc1resolved"
|
||||||
|
assert calls["pay_invoice"]["max_sat"] == 21
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_wasm_wallet_pay_invoice_allows_event_wallet_only(mocker):
|
||||||
|
async def get_wallet(wallet_id: str):
|
||||||
|
return SimpleNamespace(user="other-user")
|
||||||
|
|
||||||
|
async def pay_invoice(**kwargs):
|
||||||
|
return SimpleNamespace(
|
||||||
|
checking_id="checking",
|
||||||
|
payment_hash="hash",
|
||||||
|
status="success",
|
||||||
|
amount=-1_000,
|
||||||
|
fee=0,
|
||||||
|
pending=False,
|
||||||
|
success=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mocker.patch("lnbits.core.crud.wallets.get_wallet", get_wallet)
|
||||||
|
mocker.patch("lnbits.core.services.payments.pay_invoice", pay_invoice)
|
||||||
|
|
||||||
|
api = ExtensionHostAPI(
|
||||||
|
"demoext",
|
||||||
|
["wallet.pay_invoice"],
|
||||||
|
context="event",
|
||||||
|
owner_id="owner",
|
||||||
|
wallet_id="wallet1",
|
||||||
|
)
|
||||||
|
allowed = await api.wallet_pay_invoice(
|
||||||
|
PayInvoiceRequest(wallet_id="wallet1", payment_request="lnbc1invoice")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert allowed.ok is True
|
||||||
|
|
||||||
|
with pytest.raises(PermissionError, match="authenticated user context"):
|
||||||
|
await api.wallet_pay_invoice(
|
||||||
|
PayInvoiceRequest(wallet_id="wallet2", payment_request="lnbc1invoice")
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user