refactor: better namespace
This commit is contained in:
+48
-372
@@ -7,7 +7,6 @@ import secrets
|
|||||||
import time
|
import time
|
||||||
from collections.abc import Awaitable, Callable, Iterable, Mapping
|
from collections.abc import Awaitable, Callable, Iterable, Mapping
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Any, TypeVar, cast, get_type_hints
|
from typing import Any, TypeVar, cast, get_type_hints
|
||||||
|
|
||||||
@@ -16,37 +15,19 @@ from pydantic import BaseModel
|
|||||||
from lnbits.helpers import sha256s
|
from lnbits.helpers import sha256s
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Bolt11Request,
|
|
||||||
CreateInvoicePublicRequest,
|
CreateInvoicePublicRequest,
|
||||||
CreateInvoiceRequest,
|
CreateInvoiceRequest,
|
||||||
CreateInvoiceResponse,
|
CreateInvoiceResponse,
|
||||||
CurrencyConvertRequest,
|
|
||||||
CurrencyConvertResponse,
|
|
||||||
CurrencyListResponse,
|
|
||||||
CurrencyRateRequest,
|
|
||||||
CurrencyRateResponse,
|
|
||||||
DecodeInvoiceResponse,
|
|
||||||
EmptyRequest,
|
EmptyRequest,
|
||||||
ExtensionApiRequest,
|
ExtensionApiRequest,
|
||||||
FiatToSatsRequest,
|
|
||||||
FiatToSatsResponse,
|
|
||||||
HttpRequest,
|
HttpRequest,
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
InvoiceAmountMsatResponse,
|
|
||||||
InvoiceExpiryResponse,
|
|
||||||
InvoiceMemoResponse,
|
|
||||||
InvoicePaymentHashResponse,
|
|
||||||
ListUserWalletsResponse,
|
ListUserWalletsResponse,
|
||||||
LogRequest,
|
LogRequest,
|
||||||
LogResponse,
|
LogResponse,
|
||||||
NowResponse,
|
NowResponse,
|
||||||
RandomIdRequest,
|
RandomIdRequest,
|
||||||
RandomIdResponse,
|
RandomIdResponse,
|
||||||
RandomSecretAndHashRequest,
|
|
||||||
RandomSecretAndHashResponse,
|
|
||||||
SatsToFiatRequest,
|
|
||||||
SatsToFiatResponse,
|
|
||||||
ServerHealthResponse,
|
|
||||||
StorageDeleteRequest,
|
StorageDeleteRequest,
|
||||||
StorageDeleteResponse,
|
StorageDeleteResponse,
|
||||||
StorageGetRequest,
|
StorageGetRequest,
|
||||||
@@ -56,9 +37,6 @@ from .models import (
|
|||||||
StorageSetRequest,
|
StorageSetRequest,
|
||||||
StorageSetResponse,
|
StorageSetResponse,
|
||||||
UserWalletSummary,
|
UserWalletSummary,
|
||||||
ValidateInvoiceResponse,
|
|
||||||
VerifyPreimageRequest,
|
|
||||||
VerifyPreimageResponse,
|
|
||||||
)
|
)
|
||||||
from .storage import (
|
from .storage import (
|
||||||
storage_delete_row,
|
storage_delete_row,
|
||||||
@@ -80,6 +58,7 @@ class ExtensionAPIMethodExport:
|
|||||||
method_id: str
|
method_id: str
|
||||||
namespace: str
|
namespace: str
|
||||||
name: str
|
name: str
|
||||||
|
host_interface: str
|
||||||
host_name: str
|
host_name: str
|
||||||
sdk_name: str
|
sdk_name: str
|
||||||
description: str
|
description: str
|
||||||
@@ -93,6 +72,7 @@ class ExtensionAPIMethod:
|
|||||||
namespace: str
|
namespace: str
|
||||||
name: str
|
name: str
|
||||||
python_name: str
|
python_name: str
|
||||||
|
host_interface: str
|
||||||
host_name: str
|
host_name: str
|
||||||
sdk_name: str
|
sdk_name: str
|
||||||
description: str
|
description: str
|
||||||
@@ -114,16 +94,18 @@ def extension_api_method(
|
|||||||
host_name: str,
|
host_name: str,
|
||||||
sdk_name: str,
|
sdk_name: str,
|
||||||
description: str,
|
description: str,
|
||||||
|
host_interface: str = "host",
|
||||||
required_permission: str | None = None,
|
required_permission: str | None = None,
|
||||||
require_auth: bool = True,
|
require_auth: bool = True,
|
||||||
) -> Callable[
|
) -> Callable[
|
||||||
[Callable[[ExtensionAPI, _RequestModel], Awaitable[_ResponseModel]]],
|
[Callable[[Any, _RequestModel], Awaitable[_ResponseModel]]],
|
||||||
Callable[[ExtensionAPI, _RequestModel], Awaitable[_ResponseModel]],
|
Callable[[Any, _RequestModel], Awaitable[_ResponseModel]],
|
||||||
]:
|
]:
|
||||||
export = ExtensionAPIMethodExport(
|
export = ExtensionAPIMethodExport(
|
||||||
method_id=method_id,
|
method_id=method_id,
|
||||||
namespace=namespace,
|
namespace=namespace,
|
||||||
name=name,
|
name=name,
|
||||||
|
host_interface=host_interface,
|
||||||
host_name=host_name,
|
host_name=host_name,
|
||||||
sdk_name=sdk_name,
|
sdk_name=sdk_name,
|
||||||
description=description,
|
description=description,
|
||||||
@@ -132,15 +114,16 @@ def extension_api_method(
|
|||||||
)
|
)
|
||||||
|
|
||||||
def decorator(
|
def decorator(
|
||||||
function: Callable[[ExtensionAPI, _RequestModel], Awaitable[_ResponseModel]],
|
function: Callable[[Any, _RequestModel], Awaitable[_ResponseModel]],
|
||||||
) -> Callable[[ExtensionAPI, _RequestModel], Awaitable[_ResponseModel]]:
|
) -> Callable[[Any, _RequestModel], Awaitable[_ResponseModel]]:
|
||||||
@wraps(function)
|
@wraps(function)
|
||||||
async def wrapper(self: ExtensionAPI, request: _RequestModel) -> _ResponseModel:
|
async def wrapper(self: Any, request: _RequestModel) -> _ResponseModel:
|
||||||
if require_auth and not self.has_authenticated_context():
|
api = getattr(self, "api", self)
|
||||||
|
if require_auth and not api.has_authenticated_context():
|
||||||
raise PermissionError(
|
raise PermissionError(
|
||||||
f"Extension API method '{method_id}' requires authentication."
|
f"Extension API method '{method_id}' requires authentication."
|
||||||
)
|
)
|
||||||
self.require_permission(required_permission)
|
api.require_permission(required_permission)
|
||||||
return await function(self, request)
|
return await function(self, request)
|
||||||
|
|
||||||
setattr(wrapper, _EXTENSION_API_METHOD_ATTR, export)
|
setattr(wrapper, _EXTENSION_API_METHOD_ATTR, export)
|
||||||
@@ -167,6 +150,9 @@ class ExtensionAPI:
|
|||||||
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._uuid = secrets.token_urlsafe(12).replace("-", "_")
|
self._uuid = secrets.token_urlsafe(12).replace("-", "_")
|
||||||
|
from .api_utils import ExtensionAPIUtils
|
||||||
|
|
||||||
|
self.utils = ExtensionAPIUtils(self)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return (
|
return (
|
||||||
@@ -502,281 +488,6 @@ class ExtensionAPI:
|
|||||||
log("extension:%s %s", self.extension_id, request.message)
|
log("extension:%s %s", self.extension_id, request.message)
|
||||||
return LogResponse()
|
return LogResponse()
|
||||||
|
|
||||||
@extension_api_method(
|
|
||||||
method_id="utils.currencies.list",
|
|
||||||
namespace="utils",
|
|
||||||
name="List currencies",
|
|
||||||
host_name="utils_currencies_list",
|
|
||||||
sdk_name="currenciesList",
|
|
||||||
description="List currencies supported by LNbits exchange-rate conversion.",
|
|
||||||
required_permission="utils.basic",
|
|
||||||
require_auth=False,
|
|
||||||
)
|
|
||||||
async def utils_currencies_list(
|
|
||||||
self, request: EmptyRequest
|
|
||||||
) -> CurrencyListResponse:
|
|
||||||
from lnbits.utils.exchange_rates import allowed_currencies
|
|
||||||
|
|
||||||
return CurrencyListResponse(currencies=allowed_currencies())
|
|
||||||
|
|
||||||
@extension_api_method(
|
|
||||||
method_id="utils.currencies.rate",
|
|
||||||
namespace="utils",
|
|
||||||
name="Get currency rate",
|
|
||||||
host_name="utils_currencies_rate",
|
|
||||||
sdk_name="currenciesRate",
|
|
||||||
description="Get sats-per-fiat and BTC price for a currency.",
|
|
||||||
required_permission="utils.basic",
|
|
||||||
require_auth=False,
|
|
||||||
)
|
|
||||||
async def utils_currencies_rate(
|
|
||||||
self, request: CurrencyRateRequest
|
|
||||||
) -> CurrencyRateResponse:
|
|
||||||
from lnbits.utils.exchange_rates import get_fiat_rate_and_price_satoshis
|
|
||||||
|
|
||||||
rate, price = await get_fiat_rate_and_price_satoshis(request.currency)
|
|
||||||
return CurrencyRateResponse(rate=rate, price=price)
|
|
||||||
|
|
||||||
@extension_api_method(
|
|
||||||
method_id="utils.currencies.convert",
|
|
||||||
namespace="utils",
|
|
||||||
name="Convert currency amount",
|
|
||||||
host_name="utils_currencies_convert",
|
|
||||||
sdk_name="currenciesConvert",
|
|
||||||
description="Convert between sats, BTC, and supported fiat currencies.",
|
|
||||||
required_permission="utils.basic",
|
|
||||||
require_auth=False,
|
|
||||||
)
|
|
||||||
async def utils_currencies_convert(
|
|
||||||
self, request: CurrencyConvertRequest
|
|
||||||
) -> CurrencyConvertResponse:
|
|
||||||
from lnbits.utils.exchange_rates import (
|
|
||||||
fiat_amount_as_satoshis,
|
|
||||||
satoshis_amount_as_fiat,
|
|
||||||
)
|
|
||||||
|
|
||||||
from_currency = request.from_currency
|
|
||||||
if from_currency == "sats":
|
|
||||||
from_currency = "sat"
|
|
||||||
|
|
||||||
amounts: list[tuple[str, float]] = []
|
|
||||||
if from_currency == "sat":
|
|
||||||
sats = int(request.amount)
|
|
||||||
amounts.append(("BTC", sats / 100_000_000))
|
|
||||||
amounts.append(("sats", sats))
|
|
||||||
for currency in request.to.split(","):
|
|
||||||
currency = currency.strip()
|
|
||||||
if currency:
|
|
||||||
amounts.append(
|
|
||||||
(
|
|
||||||
currency.upper(),
|
|
||||||
await satoshis_amount_as_fiat(sats, currency),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
sats = await fiat_amount_as_satoshis(request.amount, from_currency)
|
|
||||||
amounts.append((from_currency.upper(), request.amount))
|
|
||||||
amounts.append(("sats", sats))
|
|
||||||
amounts.append(("BTC", sats / 100_000_000))
|
|
||||||
return CurrencyConvertResponse(amounts=amounts)
|
|
||||||
|
|
||||||
@extension_api_method(
|
|
||||||
method_id="utils.currencies.fiat_to_sats",
|
|
||||||
namespace="utils",
|
|
||||||
name="Convert fiat to sats",
|
|
||||||
host_name="utils_currencies_fiat_to_sats",
|
|
||||||
sdk_name="currenciesFiatToSats",
|
|
||||||
description="Convert a fiat amount to sats.",
|
|
||||||
required_permission="utils.basic",
|
|
||||||
require_auth=False,
|
|
||||||
)
|
|
||||||
async def utils_currencies_fiat_to_sats(
|
|
||||||
self, request: FiatToSatsRequest
|
|
||||||
) -> FiatToSatsResponse:
|
|
||||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
|
||||||
|
|
||||||
return FiatToSatsResponse(
|
|
||||||
amount_sat=await fiat_amount_as_satoshis(
|
|
||||||
request.amount,
|
|
||||||
request.currency,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@extension_api_method(
|
|
||||||
method_id="utils.currencies.sats_to_fiat",
|
|
||||||
namespace="utils",
|
|
||||||
name="Convert sats to fiat",
|
|
||||||
host_name="utils_currencies_sats_to_fiat",
|
|
||||||
sdk_name="currenciesSatsToFiat",
|
|
||||||
description="Convert a sats amount to fiat.",
|
|
||||||
required_permission="utils.basic",
|
|
||||||
require_auth=False,
|
|
||||||
)
|
|
||||||
async def utils_currencies_sats_to_fiat(
|
|
||||||
self, request: SatsToFiatRequest
|
|
||||||
) -> SatsToFiatResponse:
|
|
||||||
from lnbits.utils.exchange_rates import satoshis_amount_as_fiat
|
|
||||||
|
|
||||||
return SatsToFiatResponse(
|
|
||||||
amount=await satoshis_amount_as_fiat(request.amount, request.currency)
|
|
||||||
)
|
|
||||||
|
|
||||||
@extension_api_method(
|
|
||||||
method_id="utils.server.health",
|
|
||||||
namespace="utils",
|
|
||||||
name="Server health",
|
|
||||||
host_name="utils_server_health",
|
|
||||||
sdk_name="serverHealth",
|
|
||||||
description="Return basic public LNbits server health data.",
|
|
||||||
required_permission="utils.basic",
|
|
||||||
require_auth=False,
|
|
||||||
)
|
|
||||||
async def utils_server_health(self, request: EmptyRequest) -> ServerHealthResponse:
|
|
||||||
from lnbits.settings import settings
|
|
||||||
|
|
||||||
return ServerHealthResponse(
|
|
||||||
server_time=int(time.time()),
|
|
||||||
up_time=settings.lnbits_server_up_time,
|
|
||||||
)
|
|
||||||
|
|
||||||
@extension_api_method(
|
|
||||||
method_id="utils.lightning.decode_invoice",
|
|
||||||
namespace="utils",
|
|
||||||
name="Decode Lightning invoice",
|
|
||||||
host_name="utils_lightning_decode_invoice",
|
|
||||||
sdk_name="lightningDecodeInvoice",
|
|
||||||
description="Decode a BOLT11 Lightning invoice.",
|
|
||||||
required_permission="utils.basic",
|
|
||||||
require_auth=False,
|
|
||||||
)
|
|
||||||
async def utils_lightning_decode_invoice(
|
|
||||||
self, request: Bolt11Request
|
|
||||||
) -> DecodeInvoiceResponse:
|
|
||||||
invoice = _decode_bolt11(request.bolt11)
|
|
||||||
return _decoded_invoice_response(invoice)
|
|
||||||
|
|
||||||
@extension_api_method(
|
|
||||||
method_id="utils.lightning.validate_invoice",
|
|
||||||
namespace="utils",
|
|
||||||
name="Validate Lightning invoice",
|
|
||||||
host_name="utils_lightning_validate_invoice",
|
|
||||||
sdk_name="lightningValidateInvoice",
|
|
||||||
description="Validate whether a string is a BOLT11 Lightning invoice.",
|
|
||||||
required_permission="utils.basic",
|
|
||||||
require_auth=False,
|
|
||||||
)
|
|
||||||
async def utils_lightning_validate_invoice(
|
|
||||||
self, request: Bolt11Request
|
|
||||||
) -> ValidateInvoiceResponse:
|
|
||||||
try:
|
|
||||||
_decode_bolt11(request.bolt11)
|
|
||||||
return ValidateInvoiceResponse(valid=True)
|
|
||||||
except Exception as exc:
|
|
||||||
return ValidateInvoiceResponse(valid=False, error=str(exc))
|
|
||||||
|
|
||||||
@extension_api_method(
|
|
||||||
method_id="utils.lightning.invoice_payment_hash",
|
|
||||||
namespace="utils",
|
|
||||||
name="Get Lightning invoice payment hash",
|
|
||||||
host_name="utils_lightning_invoice_payment_hash",
|
|
||||||
sdk_name="lightningInvoicePaymentHash",
|
|
||||||
description="Get the payment hash from a BOLT11 Lightning invoice.",
|
|
||||||
required_permission="utils.basic",
|
|
||||||
require_auth=False,
|
|
||||||
)
|
|
||||||
async def utils_lightning_invoice_payment_hash(
|
|
||||||
self, request: Bolt11Request
|
|
||||||
) -> InvoicePaymentHashResponse:
|
|
||||||
return InvoicePaymentHashResponse(
|
|
||||||
payment_hash=str(_decode_bolt11(request.bolt11).payment_hash)
|
|
||||||
)
|
|
||||||
|
|
||||||
@extension_api_method(
|
|
||||||
method_id="utils.lightning.invoice_amount_msat",
|
|
||||||
namespace="utils",
|
|
||||||
name="Get Lightning invoice amount",
|
|
||||||
host_name="utils_lightning_invoice_amount_msat",
|
|
||||||
sdk_name="lightningInvoiceAmountMsat",
|
|
||||||
description="Get the amount in msat from a BOLT11 Lightning invoice.",
|
|
||||||
required_permission="utils.basic",
|
|
||||||
require_auth=False,
|
|
||||||
)
|
|
||||||
async def utils_lightning_invoice_amount_msat(
|
|
||||||
self, request: Bolt11Request
|
|
||||||
) -> InvoiceAmountMsatResponse:
|
|
||||||
return InvoiceAmountMsatResponse(
|
|
||||||
amount_msat=_invoice_amount_msat(_decode_bolt11(request.bolt11))
|
|
||||||
)
|
|
||||||
|
|
||||||
@extension_api_method(
|
|
||||||
method_id="utils.lightning.invoice_expiry",
|
|
||||||
namespace="utils",
|
|
||||||
name="Get Lightning invoice expiry",
|
|
||||||
host_name="utils_lightning_invoice_expiry",
|
|
||||||
sdk_name="lightningInvoiceExpiry",
|
|
||||||
description="Get the expiry timestamp from a BOLT11 Lightning invoice.",
|
|
||||||
required_permission="utils.basic",
|
|
||||||
require_auth=False,
|
|
||||||
)
|
|
||||||
async def utils_lightning_invoice_expiry(
|
|
||||||
self, request: Bolt11Request
|
|
||||||
) -> InvoiceExpiryResponse:
|
|
||||||
return InvoiceExpiryResponse(
|
|
||||||
expires_at=_invoice_expires_at(_decode_bolt11(request.bolt11))
|
|
||||||
)
|
|
||||||
|
|
||||||
@extension_api_method(
|
|
||||||
method_id="utils.lightning.invoice_memo",
|
|
||||||
namespace="utils",
|
|
||||||
name="Get Lightning invoice memo",
|
|
||||||
host_name="utils_lightning_invoice_memo",
|
|
||||||
sdk_name="lightningInvoiceMemo",
|
|
||||||
description="Get the memo from a BOLT11 Lightning invoice.",
|
|
||||||
required_permission="utils.basic",
|
|
||||||
require_auth=False,
|
|
||||||
)
|
|
||||||
async def utils_lightning_invoice_memo(
|
|
||||||
self, request: Bolt11Request
|
|
||||||
) -> InvoiceMemoResponse:
|
|
||||||
return InvoiceMemoResponse(memo=_invoice_memo(_decode_bolt11(request.bolt11)))
|
|
||||||
|
|
||||||
@extension_api_method(
|
|
||||||
method_id="utils.lightning.verify_preimage",
|
|
||||||
namespace="utils",
|
|
||||||
name="Verify Lightning preimage",
|
|
||||||
host_name="utils_lightning_verify_preimage",
|
|
||||||
sdk_name="lightningVerifyPreimage",
|
|
||||||
description="Verify that a preimage matches a payment hash.",
|
|
||||||
required_permission="utils.basic",
|
|
||||||
require_auth=False,
|
|
||||||
)
|
|
||||||
async def utils_lightning_verify_preimage(
|
|
||||||
self, request: VerifyPreimageRequest
|
|
||||||
) -> VerifyPreimageResponse:
|
|
||||||
from lnbits.utils.crypto import verify_preimage
|
|
||||||
|
|
||||||
return VerifyPreimageResponse(
|
|
||||||
valid=verify_preimage(request.preimage, request.payment_hash)
|
|
||||||
)
|
|
||||||
|
|
||||||
@extension_api_method(
|
|
||||||
method_id="utils.lightning.random_secret_and_hash",
|
|
||||||
namespace="utils",
|
|
||||||
name="Random Lightning secret and hash",
|
|
||||||
host_name="utils_lightning_random_secret_and_hash",
|
|
||||||
sdk_name="lightningRandomSecretAndHash",
|
|
||||||
description="Create a random secret and matching SHA256 hash.",
|
|
||||||
required_permission="utils.basic",
|
|
||||||
require_auth=False,
|
|
||||||
)
|
|
||||||
async def utils_lightning_random_secret_and_hash(
|
|
||||||
self, request: RandomSecretAndHashRequest
|
|
||||||
) -> RandomSecretAndHashResponse:
|
|
||||||
from lnbits.utils.crypto import random_secret_and_hash
|
|
||||||
|
|
||||||
secret, payment_hash = random_secret_and_hash(request.length)
|
|
||||||
return RandomSecretAndHashResponse(secret=secret, hash=payment_hash)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _permission_data(
|
def _permission_data(
|
||||||
permissions: Iterable[Any],
|
permissions: Iterable[Any],
|
||||||
@@ -846,85 +557,49 @@ class ExtensionAPI:
|
|||||||
return table, wallet_field
|
return table, wallet_field
|
||||||
|
|
||||||
|
|
||||||
def _decode_bolt11(payment_request: str) -> Any:
|
|
||||||
from lnbits import bolt11
|
|
||||||
|
|
||||||
return bolt11.decode(payment_request)
|
|
||||||
|
|
||||||
|
|
||||||
def _decoded_invoice_response(invoice: Any) -> DecodeInvoiceResponse:
|
|
||||||
return DecodeInvoiceResponse(
|
|
||||||
payment_hash=str(getattr(invoice, "payment_hash", "")) or None,
|
|
||||||
amount_msat=_invoice_amount_msat(invoice),
|
|
||||||
expiry=_invoice_expiry(invoice),
|
|
||||||
expires_at=_invoice_expires_at(invoice),
|
|
||||||
memo=_invoice_memo(invoice),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _invoice_amount_msat(invoice: Any) -> int | None:
|
|
||||||
amount_msat = getattr(invoice, "amount_msat", None)
|
|
||||||
if amount_msat is None:
|
|
||||||
return None
|
|
||||||
return int(amount_msat)
|
|
||||||
|
|
||||||
|
|
||||||
def _invoice_expiry(invoice: Any) -> int | None:
|
|
||||||
expiry = getattr(invoice, "expiry", None)
|
|
||||||
if expiry is None:
|
|
||||||
return None
|
|
||||||
return int(expiry)
|
|
||||||
|
|
||||||
|
|
||||||
def _invoice_expires_at(invoice: Any) -> int | None:
|
|
||||||
expiry_date = getattr(invoice, "expiry_date", None)
|
|
||||||
if isinstance(expiry_date, datetime):
|
|
||||||
return int(expiry_date.timestamp())
|
|
||||||
|
|
||||||
date = getattr(invoice, "date", None)
|
|
||||||
expiry = getattr(invoice, "expiry", None)
|
|
||||||
if isinstance(date, datetime) and expiry is not None:
|
|
||||||
return int(date.timestamp() + int(expiry))
|
|
||||||
if isinstance(date, (int, float)) and expiry is not None:
|
|
||||||
return int(date + int(expiry))
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _invoice_memo(invoice: Any) -> str | None:
|
|
||||||
memo = getattr(invoice, "description", None)
|
|
||||||
return str(memo) if memo is not None else None
|
|
||||||
|
|
||||||
|
|
||||||
def list_extension_api_methods(
|
def list_extension_api_methods(
|
||||||
api_cls: type[ExtensionAPI] = ExtensionAPI,
|
api_cls: type[ExtensionAPI] = ExtensionAPI,
|
||||||
) -> list[ExtensionAPIMethod]:
|
) -> list[ExtensionAPIMethod]:
|
||||||
methods: list[ExtensionAPIMethod] = []
|
methods: list[ExtensionAPIMethod] = []
|
||||||
|
|
||||||
for python_name, function in inspect.getmembers(api_cls, inspect.isfunction):
|
for prefix, method_cls in _extension_api_method_sources(api_cls):
|
||||||
export = getattr(function, _EXTENSION_API_METHOD_ATTR, None)
|
for python_name, function in inspect.getmembers(method_cls, inspect.isfunction):
|
||||||
if not export:
|
export = getattr(function, _EXTENSION_API_METHOD_ATTR, None)
|
||||||
continue
|
if not export:
|
||||||
|
continue
|
||||||
|
|
||||||
request_model, response_model = _get_method_models(function)
|
request_model, response_model = _get_method_models(function)
|
||||||
methods.append(
|
methods.append(
|
||||||
ExtensionAPIMethod(
|
ExtensionAPIMethod(
|
||||||
method_id=export.method_id,
|
method_id=export.method_id,
|
||||||
namespace=export.namespace,
|
namespace=export.namespace,
|
||||||
name=export.name,
|
name=export.name,
|
||||||
python_name=python_name,
|
python_name=f"{prefix}.{python_name}" if prefix else python_name,
|
||||||
host_name=export.host_name,
|
host_interface=export.host_interface,
|
||||||
sdk_name=export.sdk_name,
|
host_name=export.host_name,
|
||||||
description=export.description,
|
sdk_name=export.sdk_name,
|
||||||
request_model=request_model,
|
description=export.description,
|
||||||
response_model=response_model,
|
request_model=request_model,
|
||||||
required_permission=export.required_permission,
|
response_model=response_model,
|
||||||
require_auth=export.require_auth,
|
required_permission=export.required_permission,
|
||||||
|
require_auth=export.require_auth,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
return sorted(methods, key=lambda method: method.method_id)
|
return sorted(methods, key=lambda method: method.method_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _extension_api_method_sources(
|
||||||
|
api_cls: type[ExtensionAPI],
|
||||||
|
) -> list[tuple[str, type[Any]]]:
|
||||||
|
sources: list[tuple[str, type[Any]]] = [("", api_cls)]
|
||||||
|
if issubclass(api_cls, ExtensionAPI):
|
||||||
|
from .api_utils import extension_api_utils_method_classes
|
||||||
|
|
||||||
|
sources.extend(extension_api_utils_method_classes().items())
|
||||||
|
return sources
|
||||||
|
|
||||||
|
|
||||||
def extension_api_permission_ids(
|
def extension_api_permission_ids(
|
||||||
api_cls: type[ExtensionAPI] = ExtensionAPI,
|
api_cls: type[ExtensionAPI] = ExtensionAPI,
|
||||||
) -> set[str]:
|
) -> set[str]:
|
||||||
@@ -956,6 +631,7 @@ def extension_api_contract(
|
|||||||
"namespace": method.namespace,
|
"namespace": method.namespace,
|
||||||
"name": method.name,
|
"name": method.name,
|
||||||
"python_name": method.python_name,
|
"python_name": method.python_name,
|
||||||
|
"host_interface": method.host_interface,
|
||||||
"host_name": method.host_name,
|
"host_name": method.host_name,
|
||||||
"sdk_name": method.sdk_name,
|
"sdk_name": method.sdk_name,
|
||||||
"sdk_qualified_name": method.sdk_qualified_name,
|
"sdk_qualified_name": method.sdk_qualified_name,
|
||||||
|
|||||||
@@ -0,0 +1,381 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
from .api import extension_api_method
|
||||||
|
from .models import (
|
||||||
|
Bolt11Request,
|
||||||
|
CurrencyConvertRequest,
|
||||||
|
CurrencyConvertResponse,
|
||||||
|
CurrencyListResponse,
|
||||||
|
CurrencyRateRequest,
|
||||||
|
CurrencyRateResponse,
|
||||||
|
DecodeInvoiceResponse,
|
||||||
|
EmptyRequest,
|
||||||
|
FiatToSatsRequest,
|
||||||
|
FiatToSatsResponse,
|
||||||
|
InvoiceAmountMsatResponse,
|
||||||
|
InvoiceExpiryResponse,
|
||||||
|
InvoiceMemoResponse,
|
||||||
|
InvoicePaymentHashResponse,
|
||||||
|
RandomSecretAndHashRequest,
|
||||||
|
RandomSecretAndHashResponse,
|
||||||
|
SatsToFiatRequest,
|
||||||
|
SatsToFiatResponse,
|
||||||
|
ServerHealthResponse,
|
||||||
|
ValidateInvoiceResponse,
|
||||||
|
VerifyPreimageRequest,
|
||||||
|
VerifyPreimageResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .api import ExtensionAPI
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionAPIUtils:
|
||||||
|
def __init__(self, api: ExtensionAPI) -> None:
|
||||||
|
self.api = api
|
||||||
|
self.currencies = ExtensionCurrencyUtils(api)
|
||||||
|
self.server = ExtensionServerUtils(api)
|
||||||
|
self.lightning = ExtensionLightningUtils(api)
|
||||||
|
|
||||||
|
|
||||||
|
class _ExtensionAPIUtilsGroup:
|
||||||
|
def __init__(self, api: ExtensionAPI) -> None:
|
||||||
|
self.api = api
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionCurrencyUtils(_ExtensionAPIUtilsGroup):
|
||||||
|
@extension_api_method(
|
||||||
|
method_id="utils.currencies.list",
|
||||||
|
namespace="utils.currencies",
|
||||||
|
name="List currencies",
|
||||||
|
host_interface="utils-currencies",
|
||||||
|
host_name="list_currencies",
|
||||||
|
sdk_name="list",
|
||||||
|
description="List currencies supported by LNbits exchange-rate conversion.",
|
||||||
|
required_permission="utils.basic",
|
||||||
|
require_auth=False,
|
||||||
|
)
|
||||||
|
async def list(self, request: EmptyRequest) -> CurrencyListResponse:
|
||||||
|
from lnbits.utils.exchange_rates import allowed_currencies
|
||||||
|
|
||||||
|
return CurrencyListResponse(currencies=allowed_currencies())
|
||||||
|
|
||||||
|
@extension_api_method(
|
||||||
|
method_id="utils.currencies.rate",
|
||||||
|
namespace="utils.currencies",
|
||||||
|
name="Get currency rate",
|
||||||
|
host_interface="utils-currencies",
|
||||||
|
host_name="rate",
|
||||||
|
sdk_name="rate",
|
||||||
|
description="Get sats-per-fiat and BTC price for a currency.",
|
||||||
|
required_permission="utils.basic",
|
||||||
|
require_auth=False,
|
||||||
|
)
|
||||||
|
async def rate(self, request: CurrencyRateRequest) -> CurrencyRateResponse:
|
||||||
|
from lnbits.utils.exchange_rates import get_fiat_rate_and_price_satoshis
|
||||||
|
|
||||||
|
rate, price = await get_fiat_rate_and_price_satoshis(request.currency)
|
||||||
|
return CurrencyRateResponse(rate=rate, price=price)
|
||||||
|
|
||||||
|
@extension_api_method(
|
||||||
|
method_id="utils.currencies.convert",
|
||||||
|
namespace="utils.currencies",
|
||||||
|
name="Convert currency amount",
|
||||||
|
host_interface="utils-currencies",
|
||||||
|
host_name="convert",
|
||||||
|
sdk_name="convert",
|
||||||
|
description="Convert between sats, BTC, and supported fiat currencies.",
|
||||||
|
required_permission="utils.basic",
|
||||||
|
require_auth=False,
|
||||||
|
)
|
||||||
|
async def convert(self, request: CurrencyConvertRequest) -> CurrencyConvertResponse:
|
||||||
|
from lnbits.utils.exchange_rates import (
|
||||||
|
fiat_amount_as_satoshis,
|
||||||
|
satoshis_amount_as_fiat,
|
||||||
|
)
|
||||||
|
|
||||||
|
from_currency = request.from_currency
|
||||||
|
if from_currency == "sats":
|
||||||
|
from_currency = "sat"
|
||||||
|
|
||||||
|
amounts: list[tuple[str, float]] = []
|
||||||
|
if from_currency == "sat":
|
||||||
|
sats = int(request.amount)
|
||||||
|
amounts.append(("BTC", sats / 100_000_000))
|
||||||
|
amounts.append(("sats", sats))
|
||||||
|
for currency in request.to.split(","):
|
||||||
|
currency = currency.strip()
|
||||||
|
if currency:
|
||||||
|
amounts.append(
|
||||||
|
(
|
||||||
|
currency.upper(),
|
||||||
|
await satoshis_amount_as_fiat(sats, currency),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
sats = await fiat_amount_as_satoshis(request.amount, from_currency)
|
||||||
|
amounts.append((from_currency.upper(), request.amount))
|
||||||
|
amounts.append(("sats", sats))
|
||||||
|
amounts.append(("BTC", sats / 100_000_000))
|
||||||
|
return CurrencyConvertResponse(amounts=amounts)
|
||||||
|
|
||||||
|
@extension_api_method(
|
||||||
|
method_id="utils.currencies.fiat_to_sats",
|
||||||
|
namespace="utils.currencies",
|
||||||
|
name="Convert fiat to sats",
|
||||||
|
host_interface="utils-currencies",
|
||||||
|
host_name="fiat_to_sats",
|
||||||
|
sdk_name="fiatToSats",
|
||||||
|
description="Convert a fiat amount to sats.",
|
||||||
|
required_permission="utils.basic",
|
||||||
|
require_auth=False,
|
||||||
|
)
|
||||||
|
async def fiat_to_sats(self, request: FiatToSatsRequest) -> FiatToSatsResponse:
|
||||||
|
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
||||||
|
|
||||||
|
return FiatToSatsResponse(
|
||||||
|
amount_sat=await fiat_amount_as_satoshis(
|
||||||
|
request.amount,
|
||||||
|
request.currency,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@extension_api_method(
|
||||||
|
method_id="utils.currencies.sats_to_fiat",
|
||||||
|
namespace="utils.currencies",
|
||||||
|
name="Convert sats to fiat",
|
||||||
|
host_interface="utils-currencies",
|
||||||
|
host_name="sats_to_fiat",
|
||||||
|
sdk_name="satsToFiat",
|
||||||
|
description="Convert a sats amount to fiat.",
|
||||||
|
required_permission="utils.basic",
|
||||||
|
require_auth=False,
|
||||||
|
)
|
||||||
|
async def sats_to_fiat(self, request: SatsToFiatRequest) -> SatsToFiatResponse:
|
||||||
|
from lnbits.utils.exchange_rates import satoshis_amount_as_fiat
|
||||||
|
|
||||||
|
return SatsToFiatResponse(
|
||||||
|
amount=await satoshis_amount_as_fiat(request.amount, request.currency)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionServerUtils(_ExtensionAPIUtilsGroup):
|
||||||
|
@extension_api_method(
|
||||||
|
method_id="utils.server.health",
|
||||||
|
namespace="utils.server",
|
||||||
|
name="Server health",
|
||||||
|
host_interface="utils-server",
|
||||||
|
host_name="health",
|
||||||
|
sdk_name="health",
|
||||||
|
description="Return basic public LNbits server health data.",
|
||||||
|
required_permission="utils.basic",
|
||||||
|
require_auth=False,
|
||||||
|
)
|
||||||
|
async def health(self, request: EmptyRequest) -> ServerHealthResponse:
|
||||||
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
return ServerHealthResponse(
|
||||||
|
server_time=int(time.time()),
|
||||||
|
up_time=settings.lnbits_server_up_time,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionLightningUtils(_ExtensionAPIUtilsGroup):
|
||||||
|
@extension_api_method(
|
||||||
|
method_id="utils.lightning.decode_invoice",
|
||||||
|
namespace="utils.lightning",
|
||||||
|
name="Decode Lightning invoice",
|
||||||
|
host_interface="utils-lightning",
|
||||||
|
host_name="decode_invoice",
|
||||||
|
sdk_name="decodeInvoice",
|
||||||
|
description="Decode a BOLT11 Lightning invoice.",
|
||||||
|
required_permission="utils.basic",
|
||||||
|
require_auth=False,
|
||||||
|
)
|
||||||
|
async def decode_invoice(self, request: Bolt11Request) -> DecodeInvoiceResponse:
|
||||||
|
invoice = _decode_bolt11(request.bolt11)
|
||||||
|
return _decoded_invoice_response(invoice)
|
||||||
|
|
||||||
|
@extension_api_method(
|
||||||
|
method_id="utils.lightning.validate_invoice",
|
||||||
|
namespace="utils.lightning",
|
||||||
|
name="Validate Lightning invoice",
|
||||||
|
host_interface="utils-lightning",
|
||||||
|
host_name="validate_invoice",
|
||||||
|
sdk_name="validateInvoice",
|
||||||
|
description="Validate whether a string is a BOLT11 Lightning invoice.",
|
||||||
|
required_permission="utils.basic",
|
||||||
|
require_auth=False,
|
||||||
|
)
|
||||||
|
async def validate_invoice(self, request: Bolt11Request) -> ValidateInvoiceResponse:
|
||||||
|
try:
|
||||||
|
_decode_bolt11(request.bolt11)
|
||||||
|
return ValidateInvoiceResponse(valid=True)
|
||||||
|
except Exception as exc:
|
||||||
|
return ValidateInvoiceResponse(valid=False, error=str(exc))
|
||||||
|
|
||||||
|
@extension_api_method(
|
||||||
|
method_id="utils.lightning.invoice_payment_hash",
|
||||||
|
namespace="utils.lightning",
|
||||||
|
name="Get Lightning invoice payment hash",
|
||||||
|
host_interface="utils-lightning",
|
||||||
|
host_name="invoice_payment_hash",
|
||||||
|
sdk_name="invoicePaymentHash",
|
||||||
|
description="Get the payment hash from a BOLT11 Lightning invoice.",
|
||||||
|
required_permission="utils.basic",
|
||||||
|
require_auth=False,
|
||||||
|
)
|
||||||
|
async def invoice_payment_hash(
|
||||||
|
self, request: Bolt11Request
|
||||||
|
) -> InvoicePaymentHashResponse:
|
||||||
|
return InvoicePaymentHashResponse(
|
||||||
|
payment_hash=str(_decode_bolt11(request.bolt11).payment_hash)
|
||||||
|
)
|
||||||
|
|
||||||
|
@extension_api_method(
|
||||||
|
method_id="utils.lightning.invoice_amount_msat",
|
||||||
|
namespace="utils.lightning",
|
||||||
|
name="Get Lightning invoice amount",
|
||||||
|
host_interface="utils-lightning",
|
||||||
|
host_name="invoice_amount_msat",
|
||||||
|
sdk_name="invoiceAmountMsat",
|
||||||
|
description="Get the amount in msat from a BOLT11 Lightning invoice.",
|
||||||
|
required_permission="utils.basic",
|
||||||
|
require_auth=False,
|
||||||
|
)
|
||||||
|
async def invoice_amount_msat(
|
||||||
|
self, request: Bolt11Request
|
||||||
|
) -> InvoiceAmountMsatResponse:
|
||||||
|
return InvoiceAmountMsatResponse(
|
||||||
|
amount_msat=_invoice_amount_msat(_decode_bolt11(request.bolt11))
|
||||||
|
)
|
||||||
|
|
||||||
|
@extension_api_method(
|
||||||
|
method_id="utils.lightning.invoice_expiry",
|
||||||
|
namespace="utils.lightning",
|
||||||
|
name="Get Lightning invoice expiry",
|
||||||
|
host_interface="utils-lightning",
|
||||||
|
host_name="invoice_expiry",
|
||||||
|
sdk_name="invoiceExpiry",
|
||||||
|
description="Get the expiry timestamp from a BOLT11 Lightning invoice.",
|
||||||
|
required_permission="utils.basic",
|
||||||
|
require_auth=False,
|
||||||
|
)
|
||||||
|
async def invoice_expiry(self, request: Bolt11Request) -> InvoiceExpiryResponse:
|
||||||
|
return InvoiceExpiryResponse(
|
||||||
|
expires_at=_invoice_expires_at(_decode_bolt11(request.bolt11))
|
||||||
|
)
|
||||||
|
|
||||||
|
@extension_api_method(
|
||||||
|
method_id="utils.lightning.invoice_memo",
|
||||||
|
namespace="utils.lightning",
|
||||||
|
name="Get Lightning invoice memo",
|
||||||
|
host_interface="utils-lightning",
|
||||||
|
host_name="invoice_memo",
|
||||||
|
sdk_name="invoiceMemo",
|
||||||
|
description="Get the memo from a BOLT11 Lightning invoice.",
|
||||||
|
required_permission="utils.basic",
|
||||||
|
require_auth=False,
|
||||||
|
)
|
||||||
|
async def invoice_memo(self, request: Bolt11Request) -> InvoiceMemoResponse:
|
||||||
|
return InvoiceMemoResponse(memo=_invoice_memo(_decode_bolt11(request.bolt11)))
|
||||||
|
|
||||||
|
@extension_api_method(
|
||||||
|
method_id="utils.lightning.verify_preimage",
|
||||||
|
namespace="utils.lightning",
|
||||||
|
name="Verify Lightning preimage",
|
||||||
|
host_interface="utils-lightning",
|
||||||
|
host_name="verify_preimage",
|
||||||
|
sdk_name="verifyPreimage",
|
||||||
|
description="Verify that a preimage matches a payment hash.",
|
||||||
|
required_permission="utils.basic",
|
||||||
|
require_auth=False,
|
||||||
|
)
|
||||||
|
async def verify_preimage(
|
||||||
|
self, request: VerifyPreimageRequest
|
||||||
|
) -> VerifyPreimageResponse:
|
||||||
|
from lnbits.utils.crypto import verify_preimage
|
||||||
|
|
||||||
|
return VerifyPreimageResponse(
|
||||||
|
valid=verify_preimage(request.preimage, request.payment_hash)
|
||||||
|
)
|
||||||
|
|
||||||
|
@extension_api_method(
|
||||||
|
method_id="utils.lightning.random_secret_and_hash",
|
||||||
|
namespace="utils.lightning",
|
||||||
|
name="Random Lightning secret and hash",
|
||||||
|
host_interface="utils-lightning",
|
||||||
|
host_name="random_secret_and_hash",
|
||||||
|
sdk_name="randomSecretAndHash",
|
||||||
|
description="Create a random secret and matching SHA256 hash.",
|
||||||
|
required_permission="utils.basic",
|
||||||
|
require_auth=False,
|
||||||
|
)
|
||||||
|
async def random_secret_and_hash(
|
||||||
|
self, request: RandomSecretAndHashRequest
|
||||||
|
) -> RandomSecretAndHashResponse:
|
||||||
|
from lnbits.utils.crypto import random_secret_and_hash
|
||||||
|
|
||||||
|
secret, payment_hash = random_secret_and_hash(request.length)
|
||||||
|
return RandomSecretAndHashResponse(secret=secret, hash=payment_hash)
|
||||||
|
|
||||||
|
|
||||||
|
def extension_api_utils_method_classes() -> dict[str, type[_ExtensionAPIUtilsGroup]]:
|
||||||
|
return {
|
||||||
|
"utils.currencies": ExtensionCurrencyUtils,
|
||||||
|
"utils.server": ExtensionServerUtils,
|
||||||
|
"utils.lightning": ExtensionLightningUtils,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_bolt11(payment_request: str) -> Any:
|
||||||
|
from lnbits import bolt11
|
||||||
|
|
||||||
|
return bolt11.decode(payment_request)
|
||||||
|
|
||||||
|
|
||||||
|
def _decoded_invoice_response(invoice: Any) -> DecodeInvoiceResponse:
|
||||||
|
return DecodeInvoiceResponse(
|
||||||
|
payment_hash=str(getattr(invoice, "payment_hash", "")) or None,
|
||||||
|
amount_msat=_invoice_amount_msat(invoice),
|
||||||
|
expiry=_invoice_expiry(invoice),
|
||||||
|
expires_at=_invoice_expires_at(invoice),
|
||||||
|
memo=_invoice_memo(invoice),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _invoice_amount_msat(invoice: Any) -> int | None:
|
||||||
|
amount_msat = getattr(invoice, "amount_msat", None)
|
||||||
|
if amount_msat is None:
|
||||||
|
return None
|
||||||
|
return int(amount_msat)
|
||||||
|
|
||||||
|
|
||||||
|
def _invoice_expiry(invoice: Any) -> int | None:
|
||||||
|
expiry = getattr(invoice, "expiry", None)
|
||||||
|
if expiry is None:
|
||||||
|
return None
|
||||||
|
return int(expiry)
|
||||||
|
|
||||||
|
|
||||||
|
def _invoice_expires_at(invoice: Any) -> int | None:
|
||||||
|
expiry_date = getattr(invoice, "expiry_date", None)
|
||||||
|
if isinstance(expiry_date, datetime):
|
||||||
|
return int(expiry_date.timestamp())
|
||||||
|
|
||||||
|
date = getattr(invoice, "date", None)
|
||||||
|
expiry = getattr(invoice, "expiry", None)
|
||||||
|
if isinstance(date, datetime) and expiry is not None:
|
||||||
|
return int(date.timestamp() + int(expiry))
|
||||||
|
if isinstance(date, (int, float)) and expiry is not None:
|
||||||
|
return int(date + int(expiry))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _invoice_memo(invoice: Any) -> str | None:
|
||||||
|
memo = getattr(invoice, "description", None)
|
||||||
|
return str(memo) if memo is not None else None
|
||||||
@@ -30,26 +30,34 @@ class ExtensionAPIHost:
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
method = self._require_method(host_name)
|
method = self._require_method(host_name)
|
||||||
request = self._request_model(method, payload)
|
request = self._request_model(method, payload)
|
||||||
handler = getattr(self.api, method.python_name)
|
handler = _resolve_attr_path(self.api, method.python_name)
|
||||||
response = handler(request)
|
response = handler(request)
|
||||||
if inspect.isawaitable(response):
|
if inspect.isawaitable(response):
|
||||||
response = await response
|
response = await response
|
||||||
return self._response_payload(method, response)
|
return self._response_payload(method, response)
|
||||||
|
|
||||||
def imports(self) -> dict[str, HostImport]:
|
def imports(self) -> dict[str, HostImport]:
|
||||||
|
return self.imports_for_interface("host")
|
||||||
|
|
||||||
|
def import_object(self) -> dict[str, dict[str, HostImport]]:
|
||||||
|
interfaces = sorted({method.host_interface for method in self.methods})
|
||||||
|
return {
|
||||||
|
f"lnbits:extension/{interface}": self.imports_for_interface(interface)
|
||||||
|
for interface in interfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
def imports_for_interface(self, host_interface: str) -> dict[str, HostImport]:
|
||||||
return {
|
return {
|
||||||
_snake_to_camel(method.host_name): self._make_import(method)
|
_snake_to_camel(method.host_name): self._make_import(method)
|
||||||
for method in self.methods
|
for method in self.methods
|
||||||
|
if method.host_interface == host_interface
|
||||||
}
|
}
|
||||||
|
|
||||||
def import_object(self) -> dict[str, dict[str, HostImport]]:
|
|
||||||
return {"lnbits:extension/host": self.imports()}
|
|
||||||
|
|
||||||
def _make_import(self, method: ExtensionAPIMethod) -> HostImport:
|
def _make_import(self, method: ExtensionAPIMethod) -> HostImport:
|
||||||
async def host_import(
|
async def host_import(
|
||||||
payload: Mapping[str, Any] | BaseModel | None = None,
|
payload: Mapping[str, Any] | BaseModel | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await self.invoke(method.host_name, payload)
|
return await self.invoke(method.method_id, payload)
|
||||||
|
|
||||||
return host_import
|
return host_import
|
||||||
|
|
||||||
@@ -66,6 +74,8 @@ class ExtensionAPIHost:
|
|||||||
index: dict[str, ExtensionAPIMethod] = {}
|
index: dict[str, ExtensionAPIMethod] = {}
|
||||||
for method in methods:
|
for method in methods:
|
||||||
for host_name in {
|
for host_name in {
|
||||||
|
method.method_id,
|
||||||
|
f"{method.host_interface}:{method.host_name}",
|
||||||
method.host_name,
|
method.host_name,
|
||||||
_snake_to_camel(method.host_name),
|
_snake_to_camel(method.host_name),
|
||||||
method.host_name.replace("_", "-"),
|
method.host_name.replace("_", "-"),
|
||||||
@@ -118,3 +128,9 @@ def _snake_to_camel(value: str) -> str:
|
|||||||
def _to_snake(value: str) -> str:
|
def _to_snake(value: str) -> str:
|
||||||
value = value.replace("-", "_")
|
value = value.replace("-", "_")
|
||||||
return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", value).lower()
|
return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", value).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_attr_path(value: Any, path: str) -> Any:
|
||||||
|
for part in path.split("."):
|
||||||
|
value = getattr(value, part)
|
||||||
|
return value
|
||||||
|
|||||||
@@ -128,12 +128,17 @@ def _add_extension_host_imports(
|
|||||||
event_loop: asyncio.AbstractEventLoop,
|
event_loop: asyncio.AbstractEventLoop,
|
||||||
) -> None:
|
) -> None:
|
||||||
with linker.root() as root:
|
with linker.root() as root:
|
||||||
with root.add_instance("lnbits:extension/host") as host:
|
methods_by_interface: dict[str, list[Any]] = {}
|
||||||
for method in list_extension_api_methods():
|
for method in list_extension_api_methods():
|
||||||
host.add_func(
|
methods_by_interface.setdefault(method.host_interface, []).append(method)
|
||||||
method.host_name.replace("_", "-"),
|
|
||||||
_make_host_import(api_host, method.host_name, event_loop),
|
for host_interface, methods in methods_by_interface.items():
|
||||||
)
|
with root.add_instance(f"lnbits:extension/{host_interface}") as host:
|
||||||
|
for method in methods:
|
||||||
|
host.add_func(
|
||||||
|
method.host_name.replace("_", "-"),
|
||||||
|
_make_host_import(api_host, method.method_id, event_loop),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _make_host_import(
|
def _make_host_import(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import re
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -185,6 +186,7 @@ def _render_method_metadata(
|
|||||||
f' namespace: "{method.namespace}",',
|
f' namespace: "{method.namespace}",',
|
||||||
f' sdkName: "{method.sdk_name}",',
|
f' sdkName: "{method.sdk_name}",',
|
||||||
f' pythonName: "{method.python_name}",',
|
f' pythonName: "{method.python_name}",',
|
||||||
|
f' hostInterface: "{method.host_interface}",',
|
||||||
f' hostName: "{method.host_name}",',
|
f' hostName: "{method.host_name}",',
|
||||||
f' hostJsName: "{_camel(method.host_name)}",',
|
f' hostJsName: "{_camel(method.host_name)}",',
|
||||||
f" requiredPermission: {permission},",
|
f" requiredPermission: {permission},",
|
||||||
@@ -197,76 +199,120 @@ def _render_method_metadata(
|
|||||||
|
|
||||||
def _render_host_type(methods: Sequence[ExtensionAPIMethod]) -> list[str]:
|
def _render_host_type(methods: Sequence[ExtensionAPIMethod]) -> list[str]:
|
||||||
lines = ["export type ExtensionHost = {"]
|
lines = ["export type ExtensionHost = {"]
|
||||||
for method in sorted(methods, key=lambda item: item.host_name):
|
for host_interface, interface_methods in _methods_by_host_interface(
|
||||||
request = _model_name(method.request_model)
|
methods
|
||||||
response = _model_name(method.response_model)
|
).items():
|
||||||
if _is_empty_model(method.request_model):
|
lines.append(f" {_ts_property(host_interface)}: {{")
|
||||||
lines.append(f" {_camel(method.host_name)}(): MaybePromise<{response}>")
|
for method in sorted(interface_methods, key=lambda item: item.host_name):
|
||||||
else:
|
|
||||||
lines.append(
|
|
||||||
f" {_camel(method.host_name)}"
|
|
||||||
f"(input: {request}): MaybePromise<{response}>"
|
|
||||||
)
|
|
||||||
lines.append("}")
|
|
||||||
return lines
|
|
||||||
|
|
||||||
|
|
||||||
def _render_sdk_type(methods: Sequence[ExtensionAPIMethod]) -> list[str]:
|
|
||||||
namespaces = _methods_by_namespace(methods)
|
|
||||||
lines = ["export type ExtensionSdk = {"]
|
|
||||||
for namespace, namespace_methods in namespaces.items():
|
|
||||||
lines.append(f" {namespace}: {{")
|
|
||||||
for method in namespace_methods:
|
|
||||||
request = _model_name(method.request_model)
|
request = _model_name(method.request_model)
|
||||||
response = _model_name(method.response_model)
|
response = _model_name(method.response_model)
|
||||||
if _is_empty_model(method.request_model):
|
if _is_empty_model(method.request_model):
|
||||||
lines.append(f" {method.sdk_name}(): Promise<{response}>")
|
lines.append(
|
||||||
|
f" {_camel(method.host_name)}(): MaybePromise<{response}>"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
lines.append(
|
lines.append(
|
||||||
f" {method.sdk_name}(input: {request}): Promise<{response}>"
|
f" {_camel(method.host_name)}"
|
||||||
|
f"(input: {request}): MaybePromise<{response}>"
|
||||||
)
|
)
|
||||||
lines.append(" }")
|
lines.append(" }")
|
||||||
lines.append("}")
|
lines.append("}")
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def _render_sdk_type(methods: Sequence[ExtensionAPIMethod]) -> list[str]:
|
||||||
|
lines = ["export type ExtensionSdk = {"]
|
||||||
|
_render_sdk_type_node(lines, _namespace_tree(methods), 1)
|
||||||
|
lines.append("}")
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def _render_create_sdk(methods: Sequence[ExtensionAPIMethod]) -> list[str]:
|
def _render_create_sdk(methods: Sequence[ExtensionAPIMethod]) -> list[str]:
|
||||||
namespaces = _methods_by_namespace(methods)
|
|
||||||
lines = [
|
lines = [
|
||||||
"export function createExtensionSdk(",
|
"export function createExtensionSdk(",
|
||||||
" host: ExtensionHost",
|
" host: ExtensionHost",
|
||||||
"): ExtensionSdk {",
|
"): ExtensionSdk {",
|
||||||
" return {",
|
" return {",
|
||||||
]
|
]
|
||||||
for namespace, namespace_methods in namespaces.items():
|
_render_create_sdk_node(lines, _namespace_tree(methods), 2)
|
||||||
lines.append(f" {namespace}: {{")
|
|
||||||
for method in namespace_methods:
|
|
||||||
host_name = _camel(method.host_name)
|
|
||||||
if _is_empty_model(method.request_model):
|
|
||||||
signature = f"{method.sdk_name}()"
|
|
||||||
host_call = f"host.{host_name}()"
|
|
||||||
else:
|
|
||||||
signature = f"{method.sdk_name}(input)"
|
|
||||||
host_call = f"host.{host_name}(input)"
|
|
||||||
lines.extend(
|
|
||||||
[
|
|
||||||
f" async {signature} {{",
|
|
||||||
f" return {host_call}",
|
|
||||||
" },",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
lines.append(" },")
|
|
||||||
lines.extend([" }", "}"])
|
lines.extend([" }", "}"])
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
def _methods_by_namespace(
|
def _render_sdk_type_node(lines: list[str], node: dict[str, Any], level: int) -> None:
|
||||||
|
indent = " " * level
|
||||||
|
for namespace, child in _iter_child_namespaces(node):
|
||||||
|
lines.append(f"{indent}{namespace}: {{")
|
||||||
|
_render_sdk_type_node(lines, child, level + 1)
|
||||||
|
lines.append(f"{indent}}}")
|
||||||
|
|
||||||
|
for method in node.get("__methods__", []):
|
||||||
|
request = _model_name(method.request_model)
|
||||||
|
response = _model_name(method.response_model)
|
||||||
|
if _is_empty_model(method.request_model):
|
||||||
|
lines.append(f"{indent}{method.sdk_name}(): Promise<{response}>")
|
||||||
|
else:
|
||||||
|
lines.append(
|
||||||
|
f"{indent}{method.sdk_name}(input: {request}): Promise<{response}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_create_sdk_node(lines: list[str], node: dict[str, Any], level: int) -> None:
|
||||||
|
indent = " " * level
|
||||||
|
for namespace, child in _iter_child_namespaces(node):
|
||||||
|
lines.append(f"{indent}{namespace}: {{")
|
||||||
|
_render_create_sdk_node(lines, child, level + 1)
|
||||||
|
lines.append(f"{indent}}},")
|
||||||
|
|
||||||
|
for method in node.get("__methods__", []):
|
||||||
|
host_call_target = (
|
||||||
|
f"host{_ts_access(method.host_interface)}"
|
||||||
|
f"{_ts_access(_camel(method.host_name))}"
|
||||||
|
)
|
||||||
|
if _is_empty_model(method.request_model):
|
||||||
|
signature = f"{method.sdk_name}()"
|
||||||
|
host_call = f"{host_call_target}()"
|
||||||
|
else:
|
||||||
|
signature = f"{method.sdk_name}(input)"
|
||||||
|
host_call = f"{host_call_target}(input)"
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
f"{indent}async {signature} {{",
|
||||||
|
f"{indent} return {host_call}",
|
||||||
|
f"{indent}}},",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _methods_by_host_interface(
|
||||||
methods: Sequence[ExtensionAPIMethod],
|
methods: Sequence[ExtensionAPIMethod],
|
||||||
) -> dict[str, list[ExtensionAPIMethod]]:
|
) -> dict[str, list[ExtensionAPIMethod]]:
|
||||||
namespaces: dict[str, list[ExtensionAPIMethod]] = defaultdict(list)
|
interfaces: dict[str, list[ExtensionAPIMethod]] = defaultdict(list)
|
||||||
|
for method in sorted(
|
||||||
|
methods, key=lambda item: (item.host_interface, item.host_name)
|
||||||
|
):
|
||||||
|
interfaces[method.host_interface].append(method)
|
||||||
|
return dict(sorted(interfaces.items()))
|
||||||
|
|
||||||
|
|
||||||
|
def _namespace_tree(methods: Sequence[ExtensionAPIMethod]) -> dict[str, Any]:
|
||||||
|
tree: dict[str, Any] = {}
|
||||||
for method in sorted(methods, key=lambda item: (item.namespace, item.sdk_name)):
|
for method in sorted(methods, key=lambda item: (item.namespace, item.sdk_name)):
|
||||||
namespaces[method.namespace].append(method)
|
node = tree
|
||||||
return dict(sorted(namespaces.items()))
|
for part in method.namespace.split("."):
|
||||||
|
node = node.setdefault(part, {})
|
||||||
|
node.setdefault("__methods__", []).append(method)
|
||||||
|
return tree
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_child_namespaces(
|
||||||
|
node: dict[str, Any],
|
||||||
|
) -> list[tuple[str, dict[str, Any]]]:
|
||||||
|
return [
|
||||||
|
(key, value)
|
||||||
|
for key, value in sorted(node.items())
|
||||||
|
if key != "__methods__" and isinstance(value, dict)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _model_name(model: type[BaseModel]) -> str:
|
def _model_name(model: type[BaseModel]) -> str:
|
||||||
@@ -278,6 +324,18 @@ def _camel(value: str) -> str:
|
|||||||
return head + "".join(part.capitalize() for part in tail)
|
return head + "".join(part.capitalize() for part in tail)
|
||||||
|
|
||||||
|
|
||||||
|
def _ts_property(value: str) -> str:
|
||||||
|
if re.match(r"^[A-Za-z_$][A-Za-z0-9_$]*$", value):
|
||||||
|
return value
|
||||||
|
return f'"{value}"'
|
||||||
|
|
||||||
|
|
||||||
|
def _ts_access(value: str) -> str:
|
||||||
|
if re.match(r"^[A-Za-z_$][A-Za-z0-9_$]*$", value):
|
||||||
|
return f".{value}"
|
||||||
|
return f'["{value}"]'
|
||||||
|
|
||||||
|
|
||||||
def _is_empty_model(model: type[BaseModel]) -> bool:
|
def _is_empty_model(model: type[BaseModel]) -> bool:
|
||||||
return not model.__fields__
|
return not model.__fields__
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user