diff --git a/Dockerfile b/Dockerfile index 052503bca..3a5c02606 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,3 +12,4 @@ WORKDIR /app COPY . . RUN poetry config virtualenvs.create false RUN poetry install --no-dev --no-root + diff --git a/cashu/core/settings.py b/cashu/core/settings.py index 9a7029af8..827dd073f 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -35,9 +35,6 @@ class Config(BaseSettings.Config): case_sensitive = False extra = Extra.ignore - # def __init__(self, env_file=None): - # self.env_file = env_file or self.env_file - class EnvSettings(CashuSettings): debug: bool = Field(default=False) @@ -104,6 +101,14 @@ class MintBackends(MintSettings): mint_strike_key: str = Field(default=None) mint_blink_key: str = Field(default=None) + # Spark SDK settings + mint_spark_api_key: str = Field(default=None) + mint_spark_mnemonic: str = Field(default=None) + mint_spark_network: str = Field(default="mainnet") + mint_spark_storage_dir: str = Field(default="data/spark") + mint_spark_connection_timeout: int = Field(default=30) + mint_spark_retry_attempts: int = Field(default=3) + class MintLimits(MintSettings): mint_rate_limit: bool = Field( diff --git a/cashu/lightning/__init__.py b/cashu/lightning/__init__.py index dfa66b941..af9992ace 100644 --- a/cashu/lightning/__init__.py +++ b/cashu/lightning/__init__.py @@ -7,6 +7,7 @@ from .lnbits import LNbitsWallet # noqa: F401 from .lnd_grpc.lnd_grpc import LndRPCWallet # noqa: F401 from .lndrest import LndRestWallet # noqa: F401 +from .spark import SparkWallet # noqa: F401 from .strike import StrikeWallet # noqa: F401 backend_settings = [ diff --git a/cashu/lightning/spark.py b/cashu/lightning/spark.py new file mode 100644 index 000000000..9274c03c9 --- /dev/null +++ b/cashu/lightning/spark.py @@ -0,0 +1,524 @@ +from __future__ import annotations + +import asyncio +import inspect +from typing import AsyncGenerator, Optional + +from bolt11 import decode +from loguru import logger + +from ..core.base import Amount, MeltQuote, Unit +from ..core.helpers import fee_reserve +from ..core.models import PostMeltQuoteRequest +from ..core.settings import settings +from .base import ( + InvoiceResponse, + LightningBackend, + PaymentQuoteResponse, + PaymentResponse, + PaymentResult, + PaymentStatus, + StatusResponse, + Unsupported, +) + +try: + from breez_sdk_spark import ( + BreezSdk, + ConnectRequest, + EventListener, + GetInfoRequest, + ListPaymentsRequest, + Network, + PaymentMethod, + PaymentType, + PrepareSendPaymentRequest, + ReceivePaymentMethod, + ReceivePaymentRequest, + SdkEvent, + Seed, + SendPaymentOptions, + SendPaymentRequest, + connect, + default_config, + ) + from breez_sdk_spark import ( + PaymentStatus as SparkPaymentStatus, + ) + from breez_sdk_spark import breez_sdk_spark as spark_bindings +except ImportError as exc: # pragma: no cover - optional dependency + logger.warning("Breez Spark SDK not available - Spark backend disabled: %s", exc) + BreezSdk = None # type: ignore[assignment] + EventListener = object # type: ignore[assignment] + PaymentMethod = None + PaymentType = None + SparkPaymentStatus = None + spark_bindings = None + + +def _register_sdk_event_loop(loop: asyncio.AbstractEventLoop) -> None: + """Inform the Spark SDK which asyncio loop to talk to.""" + if spark_bindings is None: # pragma: no cover - SDK missing + return + try: + spark_bindings._uniffi_get_event_loop = lambda: loop + except Exception as exc: # pragma: no cover - defensive measure + logger.debug(f"Could not patch Spark SDK event loop: {exc}") + + +def _extract_invoice_id(payment) -> Optional[str]: + """Normalize the identifier reported by the SDK.""" + details = getattr(payment, "details", None) + payment_hash = getattr(payment, "payment_hash", None) + if payment_hash: + return str(payment_hash).lower() + + if details: + details_hash = getattr(details, "payment_hash", None) + if details_hash: + return str(details_hash).lower() + + invoice = getattr(details, "invoice", None) or getattr( + getattr(details, "bolt11_invoice", None), "bolt11", None + ) + if invoice: + try: + invoice_obj = decode(invoice) + if invoice_obj.payment_hash: + return invoice_obj.payment_hash.lower() + except Exception: + return invoice.lower() + + return None + + +def _payment_fee_sats(payment) -> Optional[int]: + for attr in ("fee_sats", "fees", "fee"): + fee = getattr(payment, attr, None) + if fee is not None: + try: + return max(int(fee), 0) + except (TypeError, ValueError): + try: + return max(int(str(fee)), 0) + except (TypeError, ValueError): + continue + + details = getattr(payment, "details", None) + if details and hasattr(details, "fees"): + try: + return max(int(details.fees), 0) + except (TypeError, ValueError): + pass + return None + + +def _payment_preimage(payment) -> Optional[str]: + preimage = getattr(payment, "preimage", None) + if preimage: + return preimage + details = getattr(payment, "details", None) + if details and hasattr(details, "preimage"): + return getattr(details, "preimage") or None + return None + + +def _is_lightning_payment(payment) -> bool: + method = getattr(payment, "method", None) + lightning_method = getattr(PaymentMethod, "LIGHTNING", None) if PaymentMethod else None + if lightning_method is None or method is None: + return True + return method == lightning_method + + +SPARK_PAYMENT_RESULT_MAP = {} +if SparkPaymentStatus is not None: + SPARK_PAYMENT_RESULT_MAP = { + SparkPaymentStatus.COMPLETED: PaymentResult.SETTLED, + getattr(SparkPaymentStatus, "SETTLED", SparkPaymentStatus.COMPLETED): PaymentResult.SETTLED, + SparkPaymentStatus.FAILED: PaymentResult.FAILED, + SparkPaymentStatus.PENDING: PaymentResult.PENDING, + } + + +class SparkEventListener(EventListener): # type: ignore[misc] + """Push settled payments into an asyncio queue.""" + + def __init__(self, queue: asyncio.Queue[str], loop: asyncio.AbstractEventLoop): + super().__init__() + self.queue = queue + self.loop = loop + + async def on_event(self, event: SdkEvent) -> None: # pragma: no cover - SDK callback + payment = getattr(event, "payment", None) + if payment is None: + return + + payment_type = getattr(payment, "payment_type", None) + receive_type = getattr(PaymentType, "RECEIVE", None) if PaymentType else None + if receive_type and payment_type and payment_type != receive_type: + return + if not _is_lightning_payment(payment): + return + + status = getattr(payment, "status", None) + if status not in SPARK_PAYMENT_RESULT_MAP: + return + + checking_id = _extract_invoice_id(payment) + if not checking_id: + return + + def _enqueue() -> None: + try: + self.queue.put_nowait(checking_id) + except asyncio.QueueFull: + logger.warning("Spark event queue full, dropping payment notification") + + if self.loop.is_closed(): + return + self.loop.call_soon_threadsafe(_enqueue) + + +class SparkWallet(LightningBackend): + """Lightning backend that talks to the Breez Spark SDK.""" + + supported_units = {Unit.sat, Unit.msat} + supports_description = True + supports_incoming_payment_stream = True + supports_mpp = False + unit = Unit.sat + + def __init__(self, unit: Unit = Unit.sat, **kwargs): + if BreezSdk is None: + raise Unsupported("breez-sdk-spark is required for SparkWallet") + + self.assert_unit_supported(unit) + self.unit = unit + + assert settings.mint_spark_api_key, "MINT_SPARK_API_KEY not set" + assert settings.mint_spark_mnemonic, "MINT_SPARK_MNEMONIC not set" + + network_name = getattr(settings, "mint_spark_network", "mainnet").lower() + self.network = ( + Network.MAINNET if network_name == "mainnet" else Network.TESTNET + ) + self.storage_dir = getattr(settings, "mint_spark_storage_dir", "data/spark") + self.connection_timeout = getattr( + settings, "mint_spark_connection_timeout", 30 + ) + self.max_retry_attempts = getattr(settings, "mint_spark_retry_attempts", 3) + + self._sdk: Optional[BreezSdk] = None + self._listener_id: Optional[str] = None + self._listener: Optional[SparkEventListener] = None + self._event_queue: Optional[asyncio.Queue[str]] = None + self._loop: Optional[asyncio.AbstractEventLoop] = None + self._lock = asyncio.Lock() + + async def cleanup(self) -> None: + await self._reset_sdk() + + async def status(self) -> StatusResponse: + try: + sdk = await self._sdk_instance() + info = await sdk.get_info(request=GetInfoRequest(ensure_synced=True)) + return StatusResponse( + error_message=None, balance=Amount(Unit.sat, info.balance_sats) + ) + except Exception as exc: + logger.error(f"Spark status error: {exc}") + return StatusResponse( + error_message=f"Failed to connect to Spark SDK: {exc}", + balance=Amount(self.unit, 0), + ) + + async def create_invoice( + self, + amount: Amount, + memo: Optional[str] = None, + description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, + ) -> InvoiceResponse: + self.assert_unit_supported(amount.unit) + try: + sdk = await self._sdk_instance() + payment_method = ReceivePaymentMethod.BOLT11_INVOICE( + description=memo or "", + amount_sats=amount.to(Unit.sat).amount, + ) + request = ReceivePaymentRequest(payment_method=payment_method) + response = await sdk.receive_payment(request=request) + + invoice = decode(response.payment_request) + checking_id = invoice.payment_hash or response.payment_request.lower() + return InvoiceResponse( + ok=True, + checking_id=checking_id.lower(), + payment_request=response.payment_request, + ) + except Exception as exc: + logger.error(f"Spark invoice creation failed: {exc}") + return InvoiceResponse(ok=False, error_message=str(exc)) + + async def pay_invoice( + self, quote: MeltQuote, fee_limit_msat: int + ) -> PaymentResponse: + try: + sdk = await self._sdk_instance() + prepare_request = PrepareSendPaymentRequest( + payment_request=quote.request, + amount=None, + ) + prepare_response = await sdk.prepare_send_payment( + request=prepare_request + ) + + options = SendPaymentOptions.BOLT11_INVOICE( + prefer_spark=False, completion_timeout_secs=30 + ) + request = SendPaymentRequest( + prepare_response=prepare_response, options=options + ) + response = await sdk.send_payment(request=request) + payment = response.payment + + result = self._map_payment_status(payment) + fee_sats = _payment_fee_sats(payment) + preimage = _payment_preimage(payment) + checking_id = ( + quote.checking_id + or _extract_invoice_id(payment) + or getattr(payment, "payment_hash", None) + ) + + return PaymentResponse( + result=result, + checking_id=checking_id, + fee=Amount(Unit.sat, fee_sats) if fee_sats is not None else None, + preimage=preimage, + ) + except Exception as exc: + logger.error(f"Spark payment failed: {exc}") + return PaymentResponse( + result=PaymentResult.FAILED, + error_message=f"Payment failed: {exc}", + ) + + async def get_invoice_status(self, checking_id: str) -> PaymentStatus: + return await self._get_payment_status(checking_id, PaymentType.RECEIVE if PaymentType else None) + + async def get_payment_status(self, checking_id: str) -> PaymentStatus: + return await self._get_payment_status(checking_id, PaymentType.SEND if PaymentType else None) + + async def _get_payment_status( + self, checking_id: str, payment_type: Optional[PaymentType] + ) -> PaymentStatus: + try: + payment = await self._find_payment(checking_id, payment_type) + if not payment: + return PaymentStatus( + result=PaymentResult.PENDING, + error_message="Payment not found yet", + ) + + result = self._map_payment_status(payment) + fee_sats = _payment_fee_sats(payment) + preimage = _payment_preimage(payment) + return PaymentStatus( + result=result, + fee=Amount(Unit.sat, fee_sats) if fee_sats is not None else None, + preimage=preimage, + ) + except Exception as exc: + logger.error(f"Spark status check failed: {exc}") + return PaymentStatus(result=PaymentResult.UNKNOWN, error_message=str(exc)) + + async def get_payment_quote( + self, melt_quote: PostMeltQuoteRequest + ) -> PaymentQuoteResponse: + invoice_obj = decode(melt_quote.request) + assert invoice_obj.amount_msat, "invoice has no amount." + + amount_msat = int(invoice_obj.amount_msat) + fees_msat = fee_reserve(amount_msat) + + try: + sdk = await self._sdk_instance() + prepare_response = await sdk.prepare_send_payment( + request=PrepareSendPaymentRequest( + payment_request=melt_quote.request, amount=None + ) + ) + + estimated_fee = getattr(prepare_response, "fees_sats", None) or getattr( + prepare_response, "fees", None + ) + if estimated_fee: + buffered_fee_sats = int(int(estimated_fee) * 1.2) + fees_msat = max(buffered_fee_sats, 1) * 1000 + except Exception as exc: + logger.debug(f"Spark fee estimation failed, using default reserve: {exc}") + + fees = Amount(unit=Unit.msat, amount=fees_msat) + amount = Amount(unit=Unit.msat, amount=amount_msat) + return PaymentQuoteResponse( + checking_id=invoice_obj.payment_hash, + fee=fees.to(self.unit, round="up"), + amount=amount.to(self.unit, round="up"), + ) + + async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: + base_delay = settings.mint_retry_exponential_backoff_base_delay + max_delay = settings.mint_retry_exponential_backoff_max_delay + retry_delay = base_delay + + while True: + try: + await self._sdk_instance() + assert self._event_queue is not None + checking_id = await asyncio.wait_for( + self._event_queue.get(), timeout=30 + ) + + status = await self.get_invoice_status(checking_id) + if status.settled: + retry_delay = base_delay + yield checking_id + else: + logger.debug( + "Spark stream saw unsettled payment %s, skipping", + checking_id[:20], + ) + except asyncio.TimeoutError: + if not await self._check_connectivity(): + await self._reset_sdk() + continue + except Exception as exc: + logger.error(f"Spark payment stream error: {exc}") + await asyncio.sleep(retry_delay) + retry_delay = min(retry_delay * 2, max_delay) + + async def _sdk_instance(self) -> BreezSdk: + if self._sdk: + return self._sdk + + async with self._lock: + if self._sdk: + return self._sdk + self._sdk = await self._connect_sdk_with_retry() + return self._sdk + + async def _connect_sdk_with_retry(self) -> BreezSdk: + attempt = 0 + last_exc: Optional[Exception] = None + while attempt < self.max_retry_attempts: + try: + return await self._connect_sdk() + except Exception as exc: + last_exc = exc + delay = 2**attempt + logger.warning( + f"Spark SDK connect attempt {attempt + 1} failed: {exc}; retrying in {delay}s" + ) + await asyncio.sleep(delay) + attempt += 1 + assert last_exc is not None + raise last_exc + + async def _connect_sdk(self) -> BreezSdk: + loop = asyncio.get_running_loop() + _register_sdk_event_loop(loop) + + config = default_config(network=self.network) + config.api_key = settings.mint_spark_api_key + + seed = Seed.MNEMONIC( + mnemonic=settings.mint_spark_mnemonic, passphrase=None + ) + sdk = await asyncio.wait_for( + connect( + request=ConnectRequest( + config=config, seed=seed, storage_dir=self.storage_dir + ) + ), + timeout=self.connection_timeout, + ) + + queue: asyncio.Queue[str] = asyncio.Queue(maxsize=1024) + self._listener = SparkEventListener(queue, loop) + self._listener_id = await _await_if_needed( + sdk.add_event_listener(listener=self._listener) + ) + self._event_queue = queue + self._loop = loop + return sdk + + async def _reset_sdk(self) -> None: + sdk, listener_id = self._sdk, self._listener_id + self._sdk = None + self._listener_id = None + try: + if sdk and listener_id: + await _await_if_needed(sdk.remove_event_listener(id=listener_id)) + if sdk: + await _await_if_needed(sdk.disconnect()) + except Exception as exc: # pragma: no cover - defensive cleanup + logger.warning(f"Spark cleanup failed: {exc}") + finally: + self._listener = None + self._event_queue = None + self._loop = None + + async def _check_connectivity(self) -> bool: + if not self._sdk: + return False + try: + await asyncio.wait_for( + self._sdk.get_info(request=GetInfoRequest(ensure_synced=None)), + timeout=5.0, + ) + return True + except Exception: + return False + + async def _find_payment( + self, checking_id: str, payment_type: Optional[PaymentType] + ): + sdk = await self._sdk_instance() + request = ( + ListPaymentsRequest(type_filter=[payment_type]) + if payment_type + else ListPaymentsRequest() + ) + response = await sdk.list_payments(request=request) + normalized_id = checking_id.lower() + for payment in response.payments: + if payment_type and getattr(payment, "payment_type", None) != payment_type: + continue + if not _is_lightning_payment(payment): + continue + invoice_id = _extract_invoice_id(payment) + if invoice_id and invoice_id == normalized_id: + return payment + return None + + def _map_payment_status(self, payment) -> PaymentResult: + status = getattr(payment, "status", None) + if status in SPARK_PAYMENT_RESULT_MAP: + return SPARK_PAYMENT_RESULT_MAP[status] + + status_str = str(status).lower() + if any(token in status_str for token in ("complete", "settled", "success")): + return PaymentResult.SETTLED + if any(token in status_str for token in ("fail", "cancel", "expire")): + return PaymentResult.FAILED + if "pending" in status_str or "process" in status_str: + return PaymentResult.PENDING + return PaymentResult.UNKNOWN + + +async def _await_if_needed(value): + if inspect.isawaitable(value): + return await value + return value diff --git a/cashu/mint/startup.py b/cashu/mint/startup.py index bc14a6713..4af5a703d 100644 --- a/cashu/mint/startup.py +++ b/cashu/mint/startup.py @@ -35,6 +35,8 @@ "mint_lnbits_key", "mint_blink_key", "mint_strike_key", + "mint_spark_api_key", + "mint_spark_mnemonic", "mint_lnd_rest_macaroon", "mint_lnd_rest_admin_macaroon", "mint_lnd_rest_invoice_macaroon", diff --git a/docker-compose.yaml b/docker-compose.yaml index f2695db0a..8eeab51dc 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,3 @@ -version: "3" services: mint: build: @@ -7,11 +6,13 @@ services: container_name: mint ports: - "3338:3338" + env_file: + - .env environment: - - MINT_BACKEND_BOLT11_SAT=FakeWallet - - MINT_LISTEN_HOST=0.0.0.0 - - MINT_LISTEN_PORT=3338 - - MINT_PRIVATE_KEY=TEST_PRIVATE_KEY + - MINT_BACKEND_BOLT11_SAT=${MINT_BACKEND_BOLT11_SAT:-FakeWallet} + - MINT_LISTEN_HOST=${MINT_LISTEN_HOST:-0.0.0.0} + - MINT_LISTEN_PORT=${MINT_LISTEN_PORT:-3338} + - MINT_PRIVATE_KEY=${MINT_PRIVATE_KEY:-TEST_PRIVATE_KEY} command: ["poetry", "run", "mint"] wallet: build: diff --git a/poetry.lock b/poetry.lock index 2ea7dee34..c6b38a79c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "aiosqlite" @@ -197,6 +197,50 @@ bitstring = "*" click = "*" coincurve = "*" +[[package]] +name = "breez-sdk-spark" +version = "0.4.2" +description = "Python language bindings for the Breez Spark SDK" +optional = false +python-versions = "*" +files = [ + {file = "breez_sdk_spark-0.4.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:546675c389bc4f5d0262e53905074ea83c5ce21295321b0da29518f9e0bd4eb1"}, + {file = "breez_sdk_spark-0.4.2-cp310-cp310-manylinux_2_31_aarch64.whl", hash = "sha256:faf7ea92dfc3620f9e77d4b53b3bb2e5ad1f4fb0ecb40d4c83ad4f9b05c214cf"}, + {file = "breez_sdk_spark-0.4.2-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:5f02ac027edb973ada9e1912725d02a8dafcb3518c0c7f9707c1388be2db908a"}, + {file = "breez_sdk_spark-0.4.2-cp310-cp310-win32.whl", hash = "sha256:4175d83c7f6506a57479dc0fc58d20ed397e3ac5a8687780aae8bb2ed413f367"}, + {file = "breez_sdk_spark-0.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:845cdf07b746d4702bd477bd574cc408b76baecf8104e1f160cd299404bff910"}, + {file = "breez_sdk_spark-0.4.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:6e9ef7192bbc9f4c93444b671a5a8e3af1686cec6c464b1122a23cc18e9ca708"}, + {file = "breez_sdk_spark-0.4.2-cp311-cp311-manylinux_2_31_aarch64.whl", hash = "sha256:1e7f77b100704ff1d4a7648e393e613443da4be7adc484397a787b609d1a72f7"}, + {file = "breez_sdk_spark-0.4.2-cp311-cp311-manylinux_2_31_x86_64.whl", hash = "sha256:9bcaa5453613b9ff51af3a1cfb5f1b23260f54a8703f61c9dbcf47bdbfd82b93"}, + {file = "breez_sdk_spark-0.4.2-cp311-cp311-win32.whl", hash = "sha256:9676e73d08d7ae59a73e28e6b0d92bcf6406d8c94cff7136092fa97e26c01d82"}, + {file = "breez_sdk_spark-0.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e0bf9dfcc0587298405772ef752deac2e6f4d6985e1f26a643b86fd41a8d3dd4"}, + {file = "breez_sdk_spark-0.4.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:5a699d536b29ec9fea14bde8c2fb0e11611fb85a10848c0a83121a3ff24d97f3"}, + {file = "breez_sdk_spark-0.4.2-cp312-cp312-manylinux_2_31_aarch64.whl", hash = "sha256:0f054dfb9c528d7c688177123d8837294e385295b2be5ef7909c76ec7c3d5012"}, + {file = "breez_sdk_spark-0.4.2-cp312-cp312-manylinux_2_31_x86_64.whl", hash = "sha256:6ecbc6e02baef53f306b0783b7f98fa0b3e71ebe74b97770300820412af14fa3"}, + {file = "breez_sdk_spark-0.4.2-cp312-cp312-win32.whl", hash = "sha256:2eb6bd85be892627f83b16f235d92c47c8ba1f4742fb9c19816caecc60fc180f"}, + {file = "breez_sdk_spark-0.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc06c25662ecaddd876641417a109e307f503dbb1cb24e7c77344bf5cedc3182"}, + {file = "breez_sdk_spark-0.4.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5cd95e53c04f3172e0716f5fd2808f3bdf7cd73ff3bc898480c6e04836c73282"}, + {file = "breez_sdk_spark-0.4.2-cp313-cp313-manylinux_2_31_aarch64.whl", hash = "sha256:32232e5bb5b1601dcba2f76511b80cec7c2dae184c3427d9a01b91b995c38bc5"}, + {file = "breez_sdk_spark-0.4.2-cp313-cp313-manylinux_2_31_x86_64.whl", hash = "sha256:3f422019fbd23b935bfb38cf2a644b6c0beb9eb2407cf817a96c536cbcdb53ab"}, + {file = "breez_sdk_spark-0.4.2-cp313-cp313-win32.whl", hash = "sha256:ce2f107be9c61145a77eb843df27097af435bfa44ce2c90db0f6a3afc27eae50"}, + {file = "breez_sdk_spark-0.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:e9b81df1f0383b5540c6669fa076e0d1fc052931f5f9b862e2c8fce6b51947ff"}, + {file = "breez_sdk_spark-0.4.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:0cf5023484efdbfb66378a9ac170790db3cfff0fea6b79f4bf44db650e874d2c"}, + {file = "breez_sdk_spark-0.4.2-cp314-cp314-manylinux_2_31_aarch64.whl", hash = "sha256:36617a5698144cb8203182a9df97362f2ebe00a724ea1322df612a0c590cf6db"}, + {file = "breez_sdk_spark-0.4.2-cp314-cp314-manylinux_2_31_x86_64.whl", hash = "sha256:940683ee2f430dfb274c851cc230e76c0cfcc827cea2120a6a55077435e54206"}, + {file = "breez_sdk_spark-0.4.2-cp314-cp314-win32.whl", hash = "sha256:b87cf95b5365e182f7e7f9e20d7676099935fd73ebd5301320db7a7474c830a4"}, + {file = "breez_sdk_spark-0.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:138ac415c9a2ce6a795e5d2b7ab0155c0d550caeb0cd46d9451a0e1089a0fd5a"}, + {file = "breez_sdk_spark-0.4.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:efc1db5a2d2e249599010d777357e1f8ca85edc3ff2234df5fe73ae0d60d1a0b"}, + {file = "breez_sdk_spark-0.4.2-cp38-cp38-manylinux_2_31_aarch64.whl", hash = "sha256:97715b2b8b6b608cfcc324413e3e5e11497449edf558af2d94fe9baee74feb67"}, + {file = "breez_sdk_spark-0.4.2-cp38-cp38-manylinux_2_31_x86_64.whl", hash = "sha256:91efb146410222959c5ce8182921ba432288ce2961646bf921da5612fa8f959b"}, + {file = "breez_sdk_spark-0.4.2-cp38-cp38-win32.whl", hash = "sha256:90c9d50adb3e942ebed41bd982ffe58c54011953d558de200b7b66dddf5b36ea"}, + {file = "breez_sdk_spark-0.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:865ef7181431a311c48c87e96267ea0c99bcc839431e522c9905b689a786a8f9"}, + {file = "breez_sdk_spark-0.4.2-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:5ae040d1717f0e9c8c8d219d5e651b1d20548506ef98f4d37f27fa356cacc1d6"}, + {file = "breez_sdk_spark-0.4.2-cp39-cp39-manylinux_2_31_aarch64.whl", hash = "sha256:b9f1ccbec9e119c2db1375e0f67a6463de90818e0e09139a4c24cb70bb232933"}, + {file = "breez_sdk_spark-0.4.2-cp39-cp39-manylinux_2_31_x86_64.whl", hash = "sha256:3a339eba9b932974e3690a7e218eadf11b1d71073e7dc93062f221902fab02d3"}, + {file = "breez_sdk_spark-0.4.2-cp39-cp39-win32.whl", hash = "sha256:e73e55965028c7fe5460bf26ce63ca1f499deb28fe58766518610200fd0ccb68"}, + {file = "breez_sdk_spark-0.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:dfe3182ddd07fcb1c964922d4284efa03562a2d5dbd397689789fc00e7369d2b"}, +] + [[package]] name = "brotli" version = "1.1.0" @@ -2657,4 +2701,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "5007f3202dedffb266c3bb0ba3101141a6d865e6979185a0ab6ea7d08c13213c" +content-hash = "ba313b7a4686a95efdca1a7743d365245246bba613d1d0ec13f986c45a1edc08" diff --git a/pyproject.toml b/pyproject.toml index c1a82c13b..afb33bf98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ redis = "^5.1.1" brotli = "^1.1.0" zstandard = "^0.23.0" jinja2 = "^3.1.5" +breez-sdk-spark = "^0.4.2" [tool.poetry.group.dev.dependencies] pytest-asyncio = "^0.24.0"