diff --git a/lnbits/wallets/breez.py b/lnbits/wallets/breez.py index 2dc54e50d..cc13f9726 100644 --- a/lnbits/wallets/breez.py +++ b/lnbits/wallets/breez.py @@ -1,15 +1,9 @@ import base64 +from importlib.util import find_spec from lnbits.exceptions import UnsupportedError -try: - import breez_sdk # type: ignore - - BREEZ_SDK_INSTALLED = True -except ImportError: - BREEZ_SDK_INSTALLED = False - -if not BREEZ_SDK_INSTALLED: +if not find_spec("breez_sdk"): class BreezSdkWallet: # pyright: ignore def __init__(self): @@ -24,9 +18,31 @@ else: from pathlib import Path from typing import Optional + from bolt11 import Bolt11Exception + from bolt11 import decode as bolt11_decode + from breez_sdk import ( + BreezEvent, + ConnectRequest, + EnvironmentType, + EventListener, + GreenlightCredentials, + GreenlightNodeConfig, + NodeConfig, + PaymentDetails, + PaymentType, + ReceivePaymentRequest, + ReceivePaymentResponse, + ReportIssueRequest, + ReportPaymentFailureDetails, + SendPaymentRequest, + SendPaymentResponse, + connect, + default_config, + mnemonic_to_seed, + ) + from breez_sdk import PaymentStatus as BreezPaymentStatus from loguru import logger - from lnbits import bolt11 as lnbits_bolt11 from lnbits.settings import settings from .base import ( @@ -40,7 +56,15 @@ else: Wallet, ) - breez_event_queue: asyncio.Queue = asyncio.Queue() + breez_incoming_queue: asyncio.Queue[PaymentDetails.LN] = asyncio.Queue() + + class PaymentsListener(EventListener): + def on_event(self, e: BreezEvent) -> None: + logger.debug(f"received breez sdk event: {e}") + if isinstance(e, BreezEvent.PAYMENT_SUCCEED) and isinstance( + e.details, PaymentDetails.LN + ): + breez_incoming_queue.put_nowait(e.details) def load_bytes(source: str, extension: str) -> Optional[bytes]: # first check if it can be read from a file @@ -61,11 +85,7 @@ else: logger.debug(exc) return None - def load_greenlight_credentials() -> ( - Optional[ - breez_sdk.GreenlightCredentials # pyright: ignore[reportUnboundVariable] - ] - ): + def load_greenlight_credentials() -> Optional[GreenlightCredentials]: if ( settings.breez_greenlight_device_key and settings.breez_greenlight_device_cert @@ -78,19 +98,12 @@ else: "cannot decode breez_greenlight_device_key " "or breez_greenlight_device_cert" ) - return breez_sdk.GreenlightCredentials( # pyright: ignore[reportUnboundVariable] + return GreenlightCredentials( developer_key=list(device_key_bytes), developer_cert=list(device_cert_bytes), ) return None - class SDKListener( - breez_sdk.EventListener # pyright: ignore[reportUnboundVariable] - ): - def on_event(self, event): - logger.debug(event) - breez_event_queue.put_nowait(event) - class BreezSdkWallet(Wallet): # type: ignore[no-redef] def __init__(self): if not settings.breez_greenlight_seed: @@ -118,15 +131,15 @@ else: "missing breez_greenlight_device_key" ) - self.config = breez_sdk.default_config( - breez_sdk.EnvironmentType.PRODUCTION, + gl_config = GreenlightNodeConfig( + partner_credentials=load_greenlight_credentials(), + invite_code=settings.breez_greenlight_invite_code, + ) + node_config = NodeConfig.GREENLIGHT(config=gl_config) + self.config = default_config( + EnvironmentType.PRODUCTION, settings.breez_api_key, - breez_sdk.NodeConfig.GREENLIGHT( - config=breez_sdk.GreenlightNodeConfig( - partner_credentials=load_greenlight_credentials(), - invite_code=settings.breez_greenlight_invite_code, - ) - ), + node_config=node_config, # type: ignore[arg-type] ) breez_sdk_working_dir = Path(settings.lnbits_data_folder, "breez-sdk") @@ -134,9 +147,9 @@ else: self.config.working_dir = breez_sdk_working_dir.absolute().as_posix() try: - seed = breez_sdk.mnemonic_to_seed(settings.breez_greenlight_seed) - connect_request = breez_sdk.ConnectRequest(self.config, seed) - self.sdk_services = breez_sdk.connect(connect_request, SDKListener()) + seed = mnemonic_to_seed(settings.breez_greenlight_seed) + connect_request = ConnectRequest(config=self.config, seed=seed) + self.sdk_services = connect(connect_request, PaymentsListener()) except Exception as exc: logger.warning(exc) raise ValueError(f"cannot initialize BreezSdkWallet: {exc!s}") from exc @@ -146,7 +159,7 @@ else: async def status(self) -> StatusResponse: try: - node_info: breez_sdk.NodeState = self.sdk_services.node_info() + node_info = self.sdk_services.node_info() except Exception as exc: return StatusResponse(f"Failed to connect to breez, got: '{exc}...'", 0) @@ -168,14 +181,14 @@ else: "'description_hash' unsupported by Greenlight, provide" " 'unhashed_description'" ) - breez_invoice: breez_sdk.ReceivePaymentResponse = ( + breez_invoice: ReceivePaymentResponse = ( self.sdk_services.receive_payment( - breez_sdk.ReceivePaymentRequest( - amount * 1000, # breez uses msat - ( + ReceivePaymentRequest( + amount_msat=amount * 1000, # breez uses msat + description=( unhashed_description.decode() if unhashed_description - else memo + else memo or "" ), preimage=kwargs.get("preimage"), opening_fee_params=None, @@ -198,36 +211,45 @@ else: async def pay_invoice( self, bolt11: str, fee_limit_msat: int ) -> PaymentResponse: - invoice = lnbits_bolt11.decode(bolt11) - + logger.debug(f"fee_limit_msat {fee_limit_msat} is ignored by Breez SDK") try: - send_payment_request = breez_sdk.SendPaymentRequest( + invoice = bolt11_decode(bolt11) + except Bolt11Exception as exc: + logger.warning(exc) + return PaymentResponse( + ok=False, error_message=f"invalid bolt11 invoice: {exc}" + ) + try: + send_payment_request = SendPaymentRequest( bolt11=bolt11, use_trampoline=settings.breez_use_trampoline ) - send_payment_response: breez_sdk.SendPaymentResponse = ( + send_payment_response: SendPaymentResponse = ( self.sdk_services.send_payment(send_payment_request) ) - payment: breez_sdk.Payment = send_payment_response.payment + payment = send_payment_response.payment except Exception as exc: logger.warning(exc) try: - # try to report issue to Breez to improve LSP routing - self.sdk_services.report_issue( - breez_sdk.ReportIssueRequest.PAYMENT_FAILURE( - breez_sdk.ReportPaymentFailureDetails(invoice.payment_hash) - ) + # report issue to Breez to improve LSP routing + payment_error = ReportIssueRequest.PAYMENT_FAILURE( + ReportPaymentFailureDetails(payment_hash=invoice.payment_hash) ) + self.sdk_services.report_issue(payment_error) # type: ignore[arg-type] except Exception as ex: logger.info(ex) - # assume that payment failed? - return PaymentResponse(ok=False, error_message=f"payment failed: {exc}") + return PaymentResponse(error_message=f"exception while payment {exc!s}") - if payment.status != breez_sdk.PaymentStatus.COMPLETE: - return PaymentResponse(ok=False, error_message="payment is pending") + if payment.status != BreezPaymentStatus.COMPLETE: + return PaymentResponse(ok=None, error_message="payment is pending") # let's use the payment_hash as the checking_id checking_id = invoice.payment_hash + if not isinstance(payment.details, PaymentDetails.LN): + return PaymentResponse( + error_message="Breez SDK returned a non-LN payment details object", + ) + return PaymentResponse( ok=True, checking_id=checking_id, @@ -237,18 +259,22 @@ else: async def get_invoice_status(self, checking_id: str) -> PaymentStatus: try: - payment: breez_sdk.Payment = self.sdk_services.payment_by_hash( - checking_id - ) + payment = self.sdk_services.payment_by_hash(checking_id) if payment is None: return PaymentPendingStatus() - if payment.payment_type != breez_sdk.PaymentType.RECEIVED: + if payment.payment_type != PaymentType.RECEIVED: logger.warning(f"unexpected payment type: {payment.status}") return PaymentPendingStatus() - if payment.status == breez_sdk.PaymentStatus.FAILED: + if not isinstance(payment.details, PaymentDetails.LN): + logger.warning(f"unexpected paymentdetails type: {payment.details}") + return PaymentPendingStatus() + if payment.status == BreezPaymentStatus.FAILED: return PaymentFailedStatus() - if payment.status == breez_sdk.PaymentStatus.COMPLETE: - return PaymentSuccessStatus() + if payment.status == BreezPaymentStatus.COMPLETE: + return PaymentSuccessStatus( + fee_msat=payment.fee_msat, + preimage=payment.details.data.payment_preimage, + ) return PaymentPendingStatus() except Exception as exc: logger.warning(exc) @@ -256,20 +282,21 @@ else: async def get_payment_status(self, checking_id: str) -> PaymentStatus: try: - payment: breez_sdk.Payment = self.sdk_services.payment_by_hash( - checking_id - ) + payment = self.sdk_services.payment_by_hash(checking_id) if payment is None: return PaymentPendingStatus() - if payment.payment_type != breez_sdk.PaymentType.SENT: - logger.warning(f"unexpected payment type: {payment.status}") + if payment.payment_type != PaymentType.SENT: + logger.warning(f"unexpected payment type: {payment.payment_type}") return PaymentPendingStatus() - if payment.status == breez_sdk.PaymentStatus.COMPLETE: + if not isinstance(payment.details, PaymentDetails.LN): + logger.warning(f"unexpected paymentdetails type: {payment.details}") + return PaymentPendingStatus() + if payment.status == BreezPaymentStatus.COMPLETE: return PaymentSuccessStatus( fee_msat=payment.fee_msat, preimage=payment.details.data.payment_preimage, ) - if payment.status == breez_sdk.PaymentStatus.FAILED: + if payment.status == BreezPaymentStatus.FAILED: return PaymentFailedStatus() return PaymentPendingStatus() except Exception as exc: @@ -278,6 +305,5 @@ else: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: while settings.lnbits_running: - event = await breez_event_queue.get() - if event.is_invoice_paid(): - yield event.details.payment_hash + details = await breez_incoming_queue.get() + yield details.data.payment_hash diff --git a/lnbits/wallets/breez_liquid.py b/lnbits/wallets/breez_liquid.py index b17b4d136..067b4db26 100644 --- a/lnbits/wallets/breez_liquid.py +++ b/lnbits/wallets/breez_liquid.py @@ -1,13 +1,8 @@ # Based on breez.py -try: - import breez_sdk_liquid as breez_sdk # type: ignore +from importlib.util import find_spec - BREEZ_SDK_INSTALLED = True -except ImportError: - BREEZ_SDK_INSTALLED = False - -if not BREEZ_SDK_INSTALLED: +if not find_spec("breez_sdk_liquid"): class BreezLiquidSdkWallet: # pyright: ignore def __init__(self): @@ -23,8 +18,27 @@ else: from pathlib import Path from typing import Optional - import breez_sdk_liquid as breez_sdk # type: ignore from bolt11 import decode as bolt11_decode + from breez_sdk_liquid import ( + ConnectRequest, + EventListener, + GetInfoResponse, + GetPaymentRequest, + LiquidNetwork, + Payment, + PaymentDetails, + PaymentMethod, + PaymentState, + PaymentType, + PrepareReceiveRequest, + PrepareSendRequest, + ReceiveAmount, + ReceivePaymentRequest, + SdkEvent, + SendPaymentRequest, + connect, + default_config, + ) from loguru import logger from lnbits.settings import settings @@ -40,27 +54,27 @@ else: Wallet, ) - breez_incoming_queue: Queue[breez_sdk.PaymentDetails.LIGHTNING] = Queue() - breez_outgoing_queue: dict[str, Queue[breez_sdk.PaymentDetails.LIGHTNING]] = {} + breez_incoming_queue: Queue[PaymentDetails.LIGHTNING] = Queue() + breez_outgoing_queue: dict[str, Queue[PaymentDetails.LIGHTNING]] = {} - class PaymentsListener(breez_sdk.EventListener): - def on_event(self, e: breez_sdk.SdkEvent) -> None: + class PaymentsListener(EventListener): + def on_event(self, e: SdkEvent) -> None: logger.debug(f"received breez sdk event: {e}") # TODO: when this issue is fixed: # https://github.com/breez/breez-sdk-liquid/issues/961 - # use breez_sdk.SdkEvent.PAYMENT_WAITING_CONFIRMATION - if not isinstance( - e, breez_sdk.SdkEvent.PAYMENT_SUCCEEDED - ) or not isinstance(e.details.details, breez_sdk.PaymentDetails.LIGHTNING): + # use SdkEvent.PAYMENT_WAITING_CONFIRMATION + if not isinstance(e, SdkEvent.PAYMENT_SUCCEEDED) or not isinstance( + e.details.details, PaymentDetails.LIGHTNING + ): return payment = e.details payment_details = e.details.details - if payment.payment_type is breez_sdk.PaymentType.RECEIVE: + if payment.payment_type is PaymentType.RECEIVE: breez_incoming_queue.put_nowait(payment_details) elif ( - payment.payment_type is breez_sdk.PaymentType.SEND + payment.payment_type is PaymentType.SEND and payment_details.payment_hash in breez_outgoing_queue ): breez_outgoing_queue[payment_details.payment_hash].put_nowait( @@ -78,8 +92,8 @@ else: with open(Path("lnbits/wallets", ".breez")) as f: settings.breez_liquid_api_key = f.read().strip() - self.config = breez_sdk.default_config( - breez_sdk.LiquidNetwork.MAINNET, + self.config = default_config( + LiquidNetwork.MAINNET, breez_api_key=settings.breez_liquid_api_key, ) @@ -91,10 +105,8 @@ else: try: mnemonic = settings.breez_liquid_seed - connect_request = breez_sdk.ConnectRequest( - config=self.config, mnemonic=mnemonic - ) - self.sdk_services = breez_sdk.connect(connect_request) + connect_request = ConnectRequest(config=self.config, mnemonic=mnemonic) + self.sdk_services = connect(connect_request) self.sdk_services.add_event_listener(PaymentsListener()) except Exception as exc: logger.warning(exc) @@ -107,7 +119,7 @@ else: async def status(self) -> StatusResponse: try: - info: breez_sdk.GetInfoResponse = self.sdk_services.get_info() + info: GetInfoResponse = self.sdk_services.get_info() except Exception as exc: logger.warning(exc) return StatusResponse(f"Failed to connect to breez, got: '{exc}...'", 0) @@ -124,10 +136,10 @@ else: try: # issue with breez sdk, receive_amount is of type BITCOIN # not ReceiveAmount after initialisation - receive_amount = breez_sdk.ReceiveAmount.BITCOIN(amount) + receive_amount = ReceiveAmount.BITCOIN(amount) req = self.sdk_services.prepare_receive_payment( - breez_sdk.PrepareReceiveRequest( - payment_method=breez_sdk.PaymentMethod.BOLT11_INVOICE, + PrepareReceiveRequest( + payment_method=PaymentMethod.BOLT11_INVOICE, amount=receive_amount, # type: ignore ) ) @@ -138,7 +150,7 @@ else: ) res = self.sdk_services.receive_payment( - breez_sdk.ReceivePaymentRequest( + ReceivePaymentRequest( prepare_response=req, description=description, use_description_hash=description_hash is not None, @@ -165,7 +177,7 @@ else: invoice_data = bolt11_decode(bolt11) try: - prepare_req = breez_sdk.PrepareSendRequest(destination=bolt11) + prepare_req = PrepareSendRequest(destination=bolt11) req = self.sdk_services.prepare_send_payment(prepare_req) fee_limit_sat = settings.breez_liquid_fee_offset_sat + int( @@ -182,23 +194,23 @@ else: ) send_response = self.sdk_services.send_payment( - breez_sdk.SendPaymentRequest(prepare_response=req) + SendPaymentRequest(prepare_response=req) ) except Exception as exc: logger.warning(exc) return PaymentResponse(error_message=f"Exception while payment: {exc}") - payment: breez_sdk.Payment = send_response.payment + payment: Payment = send_response.payment logger.debug(f"pay invoice res: {payment}") checking_id = invoice_data.payment_hash fees = req.fees_sat * 1000 if req.fees_sat and req.fees_sat > 0 else 0 - if payment.status != breez_sdk.PaymentState.COMPLETE: + if payment.status != PaymentState.COMPLETE: return await self._wait_for_outgoing_payment(checking_id, fees, 10) - if not isinstance(payment.details, breez_sdk.PaymentDetails.LIGHTNING): + if not isinstance(payment.details, PaymentDetails.LIGHTNING): return PaymentResponse( error_message="lightning payment details are not available" ) @@ -212,17 +224,17 @@ else: async def get_invoice_status(self, checking_id: str) -> PaymentStatus: try: - req = breez_sdk.GetPaymentRequest.PAYMENT_HASH(checking_id) + req = GetPaymentRequest.PAYMENT_HASH(checking_id) payment = self.sdk_services.get_payment(req=req) # type: ignore if payment is None: return PaymentPendingStatus() - if payment.payment_type != breez_sdk.PaymentType.RECEIVE: + if payment.payment_type != PaymentType.RECEIVE: logger.warning(f"unexpected payment type: {payment.status}") return PaymentPendingStatus() - if payment.status == breez_sdk.PaymentState.FAILED: + if payment.status == PaymentState.FAILED: return PaymentFailedStatus() - if payment.status == breez_sdk.PaymentState.COMPLETE and isinstance( - payment.details, breez_sdk.PaymentDetails.LIGHTNING + if payment.status == PaymentState.COMPLETE and isinstance( + payment.details, PaymentDetails.LIGHTNING ): return PaymentSuccessStatus( paid=True, @@ -236,24 +248,22 @@ else: async def get_payment_status(self, checking_id: str) -> PaymentStatus: try: - req = breez_sdk.GetPaymentRequest.PAYMENT_HASH(checking_id) + req = GetPaymentRequest.PAYMENT_HASH(checking_id) payment = self.sdk_services.get_payment(req=req) # type: ignore if payment is None: return PaymentPendingStatus() - if payment.payment_type != breez_sdk.PaymentType.SEND: + if payment.payment_type != PaymentType.SEND: logger.warning(f"unexpected payment type: {payment.status}") return PaymentPendingStatus() - if payment.status == breez_sdk.PaymentState.COMPLETE: - if not isinstance( - payment.details, breez_sdk.PaymentDetails.LIGHTNING - ): + if payment.status == PaymentState.COMPLETE: + if not isinstance(payment.details, PaymentDetails.LIGHTNING): logger.warning("payment details are not of type LIGHTNING") return PaymentPendingStatus() return PaymentSuccessStatus( fee_msat=int(payment.fees_sat * 1000), preimage=payment.details.preimage, ) - if payment.status == breez_sdk.PaymentState.FAILED: + if payment.status == PaymentState.FAILED: return PaymentFailedStatus() return PaymentPendingStatus() except Exception as exc: diff --git a/pyproject.toml b/pyproject.toml index 72e9edd69..0eb446eba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -151,6 +151,8 @@ module = [ "json5.*", "jsonpath_ng.*", "filetype.*", + "breez_sdk.*", + "breez_sdk_liquid.*", ] ignore_missing_imports = "True"