From 1766b6e92e1f90a51cc45e2ef7be68b0f87cb539 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:50:39 +0200 Subject: [PATCH] [Mint] Add support for BTC and EUR in `StrikeWallet` backend, add EUR to `FakeWallet` (#561) * strike for btc and eur * strike works with eur * backend check --- .env.example | 5 +++-- cashu/core/base.py | 33 +++++++++++++++++++++++++++++++++ cashu/core/settings.py | 1 + cashu/lightning/__init__.py | 13 ++++++++++--- cashu/lightning/fake.py | 10 +++++----- cashu/lightning/strike.py | 29 ++++++++++++++++++----------- cashu/mint/startup.py | 5 +++++ cashu/mint/tasks.py | 11 +++++++++-- cashu/wallet/cli/cli.py | 4 ++-- 9 files changed, 86 insertions(+), 25 deletions(-) diff --git a/.env.example b/.env.example index 0e420dd..96c68bd 100644 --- a/.env.example +++ b/.env.example @@ -50,13 +50,14 @@ MINT_DERIVATION_PATH="m/0'/0'/0'" # m/0'/0'/0' is "sat" (default) # m/0'/1'/0' is "msat" # m/0'/2'/0' is "usd" +# m/0'/3'/0' is "eur" # In this example, we have 2 keysets for sat, 1 for msat and 1 for usd # MINT_DERIVATION_PATH_LIST=["m/0'/0'/0'", "m/0'/0'/1'", "m/0'/1'/0'", "m/0'/2'/0'"] MINT_DATABASE=data/mint # Funding source backends -# Supported: FakeWallet, LndRestWallet, CoreLightningRestWallet, BlinkWallet, LNbitsWallet, StrikeUSDWallet +# Supported: FakeWallet, LndRestWallet, CoreLightningRestWallet, BlinkWallet, LNbitsWallet, StrikeWallet MINT_BACKEND_BOLT11_SAT=FakeWallet # Only works if a usd derivation path is set # MINT_BACKEND_BOLT11_SAT=FakeWallet @@ -78,7 +79,7 @@ MINT_CORELIGHTNING_REST_CERT="./clightning-2-rest/certificate.pem" # Use with BlinkWallet MINT_BLINK_KEY=blink_abcdefgh -# Use with StrikeUSDWallet (usd only, does not currently support sats/msats) +# Use with StrikeWallet for BTC, USD, and EUR MINT_STRIKE_KEY=ABC123 # fee to reserve in percent of the amount diff --git a/cashu/core/base.py b/cashu/core/base.py index e5b921e..3d9312a 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -436,6 +436,8 @@ class Unit(Enum): sat = 0 msat = 1 usd = 2 + eur = 3 + btc = 4 def str(self, amount: int) -> str: if self == Unit.sat: @@ -444,6 +446,10 @@ class Unit(Enum): return f"{amount} msat" elif self == Unit.usd: return f"${amount/100:.2f} USD" + elif self == Unit.eur: + return f"{amount/100:.2f} EUR" + elif self == Unit.btc: + return f"{amount/1e8:.8f} BTC" else: raise Exception("Invalid unit") @@ -478,6 +484,33 @@ class Amount: else: return self + def to_float_string(self) -> str: + if self.unit == Unit.usd or self.unit == Unit.eur: + return self.cents_to_usd() + elif self.unit == Unit.sat: + return self.sat_to_btc() + else: + raise Exception("Amount must be in satoshis or cents") + + @classmethod + def from_float(cls, amount: float, unit: Unit) -> "Amount": + if unit == Unit.usd or unit == Unit.eur: + return cls(unit, int(amount * 100)) + elif unit == Unit.sat: + return cls(unit, int(amount * 1e8)) + else: + raise Exception("Amount must be in satoshis or cents") + + def sat_to_btc(self) -> str: + if self.unit != Unit.sat: + raise Exception("Amount must be in satoshis") + return f"{self.amount/1e8:.8f}" + + def cents_to_usd(self) -> str: + if self.unit != Unit.usd and self.unit != Unit.eur: + raise Exception("Amount must be in cents") + return f"{self.amount/100:.2f}" + def str(self) -> str: return self.unit.str(self.amount) diff --git a/cashu/core/settings.py b/cashu/core/settings.py index b4c3269..aa96bc0 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -67,6 +67,7 @@ class MintBackends(MintSettings): mint_lightning_backend: str = Field(default="") # deprecated mint_backend_bolt11_sat: str = Field(default="") mint_backend_bolt11_usd: str = Field(default="") + mint_backend_bolt11_eur: str = Field(default="") mint_lnbits_endpoint: str = Field(default=None) mint_lnbits_key: str = Field(default=None) diff --git a/cashu/lightning/__init__.py b/cashu/lightning/__init__.py index 521eb49..6bbf1dd 100644 --- a/cashu/lightning/__init__.py +++ b/cashu/lightning/__init__.py @@ -5,7 +5,14 @@ from .corelightningrest import CoreLightningRestWallet # noqa: F401 from .fake import FakeWallet # noqa: F401 from .lnbits import LNbitsWallet # noqa: F401 from .lndrest import LndRestWallet # noqa: F401 -from .strike import StrikeUSDWallet # noqa: F401 +from .strike import StrikeWallet # noqa: F401 -if settings.mint_backend_bolt11_sat is None or settings.mint_backend_bolt11_usd is None: - raise Exception("MINT_BACKEND_BOLT11_SAT or MINT_BACKEND_BOLT11_USD not set") +backend_settings = [ + settings.mint_backend_bolt11_sat, + settings.mint_backend_bolt11_usd, + settings.mint_backend_bolt11_eur, +] +if all([s is None for s in backend_settings]): + raise Exception( + "MINT_BACKEND_BOLT11_SAT or MINT_BACKEND_BOLT11_USD or MINT_BACKEND_BOLT11_EUR not set" + ) diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 7c50306..6fca6dd 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -45,7 +45,7 @@ class FakeWallet(LightningBackend): 32, ).hex() - supported_units = set([Unit.sat, Unit.msat, Unit.usd]) + supported_units = set([Unit.sat, Unit.msat, Unit.usd, Unit.eur]) unit = Unit.sat supports_incoming_payment_stream: bool = True @@ -113,7 +113,7 @@ class FakeWallet(LightningBackend): amount_msat = 0 if self.unit == Unit.sat: amount_msat = MilliSatoshi(amount.to(Unit.msat, round="up").amount) - elif self.unit == Unit.usd: + elif self.unit == Unit.usd or self.unit == Unit.eur: amount_msat = MilliSatoshi( math.ceil(amount.amount / self.fake_btc_price * 1e9) ) @@ -194,10 +194,10 @@ class FakeWallet(LightningBackend): fees_msat = fee_reserve(amount_msat) fees = Amount(unit=Unit.msat, amount=fees_msat) amount = Amount(unit=Unit.msat, amount=amount_msat) - elif self.unit == Unit.usd: + elif self.unit == Unit.usd or self.unit == Unit.eur: amount_usd = math.ceil(invoice_obj.amount_msat / 1e9 * self.fake_btc_price) - amount = Amount(unit=Unit.usd, amount=amount_usd) - fees = Amount(unit=Unit.usd, amount=2) + amount = Amount(unit=self.unit, amount=amount_usd) + fees = Amount(unit=self.unit, amount=2) else: raise NotImplementedError() diff --git a/cashu/lightning/strike.py b/cashu/lightning/strike.py index 422a701..15a47aa 100644 --- a/cashu/lightning/strike.py +++ b/cashu/lightning/strike.py @@ -17,15 +17,17 @@ from .base import ( ) -class StrikeUSDWallet(LightningBackend): - """https://github.com/lnbits/lnbits""" +class StrikeWallet(LightningBackend): + """https://docs.strike.me/api/""" - supported_units = [Unit.usd] + supported_units = [Unit.sat, Unit.usd, Unit.eur] + currency_map = {Unit.sat: "BTC", Unit.usd: "USD", Unit.eur: "EUR"} - def __init__(self, unit: Unit = Unit.usd, **kwargs): + def __init__(self, unit: Unit, **kwargs): self.assert_unit_supported(unit) self.unit = unit self.endpoint = "https://api.strike.me" + self.currency = self.currency_map[self.unit] # bearer auth with settings.mint_strike_key bearer_auth = { @@ -57,8 +59,13 @@ class StrikeUSDWallet(LightningBackend): ) for balance in data: - if balance["currency"] == "USD": - return StatusResponse(error_message=None, balance=balance["total"]) + if balance["currency"] == self.currency: + return StatusResponse( + error_message=None, + balance=Amount.from_float( + float(balance["total"]), self.unit + ).amount, + ) async def create_invoice( self, @@ -79,7 +86,7 @@ class StrikeUSDWallet(LightningBackend): payload = { "correlationId": secrets.token_hex(16), "description": "Invoice for order 123", - "amount": {"amount": str(amount.amount / 100), "currency": "USD"}, + "amount": {"amount": amount.to_float_string(), "currency": self.currency}, } try: r = await self.client.post(url=f"{self.endpoint}/v1/invoices", json=payload) @@ -126,7 +133,7 @@ class StrikeUSDWallet(LightningBackend): try: r = await self.client.post( url=f"{self.endpoint}/v1/payment-quotes/lightning", - json={"sourceCurrency": "USD", "lnInvoice": bolt11}, + json={"sourceCurrency": self.currency, "lnInvoice": bolt11}, timeout=None, ) r.raise_for_status() @@ -135,11 +142,11 @@ class StrikeUSDWallet(LightningBackend): raise Exception(error_message) data = r.json() - amount_cent = int(float(data.get("amount").get("amount")) * 100) + amount = Amount.from_float(float(data.get("amount").get("amount")), self.unit) quote = PaymentQuoteResponse( - amount=Amount(Unit.usd, amount=amount_cent), + amount=amount, checking_id=data.get("paymentQuoteId"), - fee=Amount(Unit.usd, 0), + fee=Amount(self.unit, 0), ) return quote diff --git a/cashu/mint/startup.py b/cashu/mint/startup.py index e66be50..9dcf0df 100644 --- a/cashu/mint/startup.py +++ b/cashu/mint/startup.py @@ -55,6 +55,11 @@ if settings.mint_backend_bolt11_usd: unit=Unit.usd ) backends.setdefault(Method.bolt11, {})[Unit.usd] = backend_bolt11_usd +if settings.mint_backend_bolt11_eur: + backend_bolt11_eur = getattr(wallets_module, settings.mint_backend_bolt11_eur)( + unit=Unit.eur + ) + backends.setdefault(Method.bolt11, {})[Unit.eur] = backend_bolt11_eur if not backends: raise Exception("No backends are set.") diff --git a/cashu/mint/tasks.py b/cashu/mint/tasks.py index fa231c2..edd61a0 100644 --- a/cashu/mint/tasks.py +++ b/cashu/mint/tasks.py @@ -26,8 +26,15 @@ class LedgerTasks(SupportsDb, SupportsBackends, SupportsEvents): asyncio.create_task(self.invoice_listener(backend)) async def invoice_listener(self, backend: LightningBackend) -> None: - async for checking_id in backend.paid_invoices_stream(): - await self.invoice_callback_dispatcher(checking_id) + if backend.supports_incoming_payment_stream: + while True: + try: + async for checking_id in backend.paid_invoices_stream(): + await self.invoice_callback_dispatcher(checking_id) + except Exception as e: + logger.error(f"Error in invoice listener: {e}") + logger.info("Restarting invoice listener...") + await asyncio.sleep(1) async def invoice_callback_dispatcher(self, checking_id: str) -> None: logger.debug(f"Invoice callback dispatcher: {checking_id}") diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py index d64a4b1..f3da086 100644 --- a/cashu/wallet/cli/cli.py +++ b/cashu/wallet/cli/cli.py @@ -262,7 +262,7 @@ async def invoice(ctx: Context, amount: float, id: str, split: int, no_check: bo wallet: Wallet = ctx.obj["WALLET"] await wallet.load_mint() await print_balance(ctx) - amount = int(amount * 100) if wallet.unit == Unit.usd else int(amount) + amount = int(amount * 100) if wallet.unit in [Unit.usd, Unit.eur] else int(amount) print(f"Requesting invoice for {wallet.unit.str(amount)}.") # in case the user wants a specific split, we create a list of amounts optional_split = None @@ -546,7 +546,7 @@ async def send_command( include_fees: bool, ): wallet: Wallet = ctx.obj["WALLET"] - amount = int(amount * 100) if wallet.unit == Unit.usd else int(amount) + amount = int(amount * 100) if wallet.unit in [Unit.usd, Unit.eur] else int(amount) if not nostr and not nopt: await send( wallet,