From 28024e0608546ac704f4e7e426ec19b0aea78654 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Thu, 2 Jul 2026 15:46:10 +0300 Subject: [PATCH] feat: pay_invoice --- lnbits/core/extensions/api.py | 90 ++++++++++++++++++++++++++++++++ lnbits/core/extensions/models.py | 37 +++++++++++++ lnbits/static/i18n/en.js | 6 +++ 3 files changed, 133 insertions(+) diff --git a/lnbits/core/extensions/api.py b/lnbits/core/extensions/api.py index 2ca800c68..612f5145e 100644 --- a/lnbits/core/extensions/api.py +++ b/lnbits/core/extensions/api.py @@ -26,6 +26,8 @@ from .models import ( LogRequest, LogResponse, NowResponse, + PayInvoiceRequest, + PayInvoiceResponse, RandomIdRequest, RandomIdResponse, StorageDeleteRequest, @@ -37,6 +39,8 @@ from .models import ( StorageSetRequest, StorageSetResponse, UserWalletSummary, + WalletBalanceRequest, + WalletBalanceResponse, ) from .storage import ( storage_delete_row, @@ -410,6 +414,92 @@ class ExtensionAPI: ] ) + @extension_api_method( + method_id="wallet.balance", + namespace="wallet", + name="Read wallet balance", + host_name="wallet_balance", + sdk_name="balance", + description="Read the balance of a wallet available to the user.", + required_permission="wallet.balance.read", + ) + async def wallet_balance( + self, request: WalletBalanceRequest + ) -> WalletBalanceResponse: + from lnbits.core.crud.wallets import get_wallet + + if not self.user_id: + raise PermissionError( + "Reading a wallet balance requires an authenticated user context." + ) + + wallet = await get_wallet(request.wallet_id) + if wallet is None or wallet.user != self.user_id: + raise PermissionError("Reading this wallet balance is not allowed.") + + withdrawable_msat = max(wallet.withdrawable_balance, 0) + fee_reserve_msat = max(wallet.balance_msat - withdrawable_msat, 0) + return WalletBalanceResponse( + wallet_id=wallet.id, + name=wallet.name, + currency=wallet.currency, + balance_msat=wallet.balance_msat, + balance_sat=wallet.balance, + withdrawable_msat=withdrawable_msat, + withdrawable_sat=withdrawable_msat // 1000, + fee_reserve_msat=fee_reserve_msat, + fee_reserve_sat=fee_reserve_msat // 1000, + can_send_payments=wallet.can_send_payments, + ) + + @extension_api_method( + method_id="wallet.pay_invoice", + namespace="wallet", + name="Pay invoice", + host_name="pay_invoice", + sdk_name="payInvoice", + description="Pay a Lightning invoice from a wallet available to the user.", + required_permission="wallet.pay_invoice", + ) + async def wallet_pay_invoice( + self, request: PayInvoiceRequest + ) -> PayInvoiceResponse: + from lnbits.core.crud.wallets import get_wallet + from lnbits.core.services.payments import pay_invoice + from lnbits.exceptions import PaymentError + + if not self.user_id: + raise PermissionError( + "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: + payment = await pay_invoice( + wallet_id=request.wallet_id, + payment_request=request.payment_request, + max_sat=request.max_sat, + extra={"tag": self.extension_id, **request.extra}, + description=request.description, + tag=self.extension_id, + ) + except (PaymentError, ValueError) as exc: + return PayInvoiceResponse(ok=False, error=str(exc)) + + return PayInvoiceResponse( + ok=True, + checking_id=payment.checking_id, + payment_hash=payment.payment_hash, + status=payment.status, + amount_msat=abs(payment.amount), + fee_msat=abs(payment.fee), + pending=payment.pending, + success=payment.success, + ) + @extension_api_method( method_id="http.request", namespace="http", diff --git a/lnbits/core/extensions/models.py b/lnbits/core/extensions/models.py index b8270b4be..5ce1801d2 100644 --- a/lnbits/core/extensions/models.py +++ b/lnbits/core/extensions/models.py @@ -117,6 +117,43 @@ class ListUserWalletsResponse(BaseModel): wallets: list[UserWalletSummary] = Field(default_factory=list) +class WalletBalanceRequest(BaseModel): + wallet_id: str = Field(..., min_length=1, max_length=128) + + +class WalletBalanceResponse(BaseModel): + wallet_id: str + name: str + currency: str | None = None + balance_msat: int + balance_sat: int + withdrawable_msat: int + withdrawable_sat: int + fee_reserve_msat: int + fee_reserve_sat: int + can_send_payments: bool + + +class PayInvoiceRequest(BaseModel): + wallet_id: str = Field(..., min_length=1, max_length=128) + payment_request: str = Field(..., min_length=1, max_length=8192) + max_sat: int | None = Field(None, gt=0) + description: str = Field("", max_length=512) + extra: dict[str, str] = Field(default_factory=dict) + + +class PayInvoiceResponse(BaseModel): + ok: bool = True + error: str | None = None + checking_id: str | None = None + payment_hash: str | None = None + status: str | None = None + amount_msat: int = 0 + fee_msat: int = 0 + pending: bool = False + success: bool = False + + class HttpRequest(BaseModel): method: Literal["DELETE", "GET", "HEAD", "PATCH", "POST", "PUT"] = "GET" url: str = Field(..., min_length=1, max_length=2048) diff --git a/lnbits/static/i18n/en.js b/lnbits/static/i18n/en.js index 421bc6c9f..b649e58eb 100644 --- a/lnbits/static/i18n/en.js +++ b/lnbits/static/i18n/en.js @@ -542,7 +542,13 @@ window.localisation.en = { 'Create Lightning invoices from public pages', extension_permission_wallet_create_invoice_public_desc: 'Create incoming Lightning invoices from public pages.', + extension_permission_wallet_balance_read: 'View wallet balances', + extension_permission_wallet_balance_read_desc: + 'Read balances of wallets available to your account.', extension_permission_wallet_list: 'List wallets', + extension_permission_wallet_pay_invoice: 'Pay invoices', + extension_permission_wallet_pay_invoice_desc: + 'Send Lightning payments from wallets available to your account.', create_extension: 'Create Extension', release_details_error: 'Cannot get the release details.', pay_from_wallet: 'Pay from Wallet',