diff --git a/cashu/core/models.py b/cashu/core/models.py index 3c4cae6e..aeef9523 100644 --- a/cashu/core/models.py +++ b/cashu/core/models.py @@ -46,6 +46,8 @@ class GetInfoResponse(BaseModel): description_long: Optional[str] = None contact: Optional[List[MintInfoContact]] = None motd: Optional[str] = None + icon_url: Optional[str] = None + time: Optional[int] = None nuts: Optional[Dict[int, Any]] = None def supports(self, nut: int) -> Optional[bool]: @@ -123,6 +125,9 @@ class KeysetsResponse_deprecated(BaseModel): class PostMintQuoteRequest(BaseModel): unit: str = Field(..., max_length=settings.mint_max_request_length) # output unit amount: int = Field(..., gt=0) # output amount + description: Optional[str] = Field( + default=None, max_length=settings.mint_max_request_length + ) # invoice description class PostMintQuoteResponse(BaseModel): @@ -212,7 +217,7 @@ class PostMeltQuoteResponse(BaseModel): state: Optional[str] # state of the quote expiry: Optional[int] # expiry of the quote payment_preimage: Optional[str] = None # payment preimage - change: Union[List[BlindedSignature], None] = None + change: Union[List[BlindedSignature], None] = None # NUT-08 change @classmethod def from_melt_quote(self, melt_quote: MeltQuote) -> "PostMeltQuoteResponse": diff --git a/cashu/core/settings.py b/cashu/core/settings.py index 5104cf15..e7cf5d0c 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -144,6 +144,7 @@ class MintInformation(CashuSettings): mint_info_description_long: str = Field(default=None) mint_info_contact: List[List[str]] = Field(default=[]) mint_info_motd: str = Field(default=None) + mint_info_icon_url: str = Field(default=None) class WalletSettings(CashuSettings): @@ -201,11 +202,13 @@ class LndRestFundingSource(MintSettings): mint_lnd_rest_invoice_macaroon: Optional[str] = Field(default=None) mint_lnd_enable_mpp: bool = Field(default=False) + class LndRPCFundingSource(MintSettings): mint_lnd_rpc_endpoint: Optional[str] = Field(default=None) mint_lnd_rpc_cert: Optional[str] = Field(default=None) mint_lnd_rpc_macaroon: Optional[str] = Field(default=None) + class CLNRestFundingSource(MintSettings): mint_clnrest_url: Optional[str] = Field(default=None) mint_clnrest_cert: Optional[str] = Field(default=None) diff --git a/cashu/lightning/base.py b/cashu/lightning/base.py index 06fbe975..c1500baa 100644 --- a/cashu/lightning/base.py +++ b/cashu/lightning/base.py @@ -70,6 +70,7 @@ class LightningBackend(ABC): supports_mpp: bool = False supports_incoming_payment_stream: bool = False supported_units: set[Unit] + supports_description: bool = False unit: Unit def assert_unit_supported(self, unit: Unit): diff --git a/cashu/lightning/blink.py b/cashu/lightning/blink.py index 2320baad..1acb8a51 100644 --- a/cashu/lightning/blink.py +++ b/cashu/lightning/blink.py @@ -47,6 +47,7 @@ class BlinkWallet(LightningBackend): payment_statuses = {"SUCCESS": True, "PENDING": None, "FAILURE": False} supported_units = set([Unit.sat, Unit.msat]) + supports_description: bool = True unit = Unit.sat def __init__(self, unit: Unit = Unit.sat, **kwargs): diff --git a/cashu/lightning/clnrest.py b/cashu/lightning/clnrest.py index 129c4d7f..0dcb68eb 100644 --- a/cashu/lightning/clnrest.py +++ b/cashu/lightning/clnrest.py @@ -31,6 +31,7 @@ class CLNRestWallet(LightningBackend): unit = Unit.sat supports_mpp = settings.mint_clnrest_enable_mpp supports_incoming_payment_stream: bool = True + supports_description: bool = True def __init__(self, unit: Unit = Unit.sat, **kwargs): self.assert_unit_supported(unit) diff --git a/cashu/lightning/corelightningrest.py b/cashu/lightning/corelightningrest.py index 94dcf6f8..51a1ac23 100644 --- a/cashu/lightning/corelightningrest.py +++ b/cashu/lightning/corelightningrest.py @@ -30,6 +30,7 @@ class CoreLightningRestWallet(LightningBackend): supported_units = set([Unit.sat, Unit.msat]) unit = Unit.sat supports_incoming_payment_stream: bool = True + supports_description: bool = True def __init__(self, unit: Unit = Unit.sat, **kwargs): self.assert_unit_supported(unit) diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 7517bfe3..b9d3cb39 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -48,6 +48,7 @@ class FakeWallet(LightningBackend): unit = Unit.sat supports_incoming_payment_stream: bool = True + supports_description: bool = True def __init__(self, unit: Unit = Unit.sat, **kwargs): self.assert_unit_supported(unit) diff --git a/cashu/lightning/lnbits.py b/cashu/lightning/lnbits.py index d231e043..7daff1fe 100644 --- a/cashu/lightning/lnbits.py +++ b/cashu/lightning/lnbits.py @@ -28,6 +28,7 @@ class LNbitsWallet(LightningBackend): supported_units = set([Unit.sat]) unit = Unit.sat supports_incoming_payment_stream: bool = True + supports_description: bool = True def __init__(self, unit: Unit = Unit.sat, **kwargs): self.assert_unit_supported(unit) diff --git a/cashu/lightning/lnd_grpc/lnd_grpc.py b/cashu/lightning/lnd_grpc/lnd_grpc.py index 0c38e623..fa667ee0 100644 --- a/cashu/lightning/lnd_grpc/lnd_grpc.py +++ b/cashu/lightning/lnd_grpc/lnd_grpc.py @@ -45,39 +45,43 @@ lnrpc.Invoice.InvoiceState.ACCEPTED: None, } -class LndRPCWallet(LightningBackend): +class LndRPCWallet(LightningBackend): supports_mpp = settings.mint_lnd_enable_mpp supports_incoming_payment_stream = True supported_units = set([Unit.sat, Unit.msat]) + supports_description: bool = True + unit = Unit.sat def __init__(self, unit: Unit = Unit.sat, **kwargs): self.assert_unit_supported(unit) self.unit = unit - self.endpoint = settings.mint_lnd_rpc_endpoint cert_path = settings.mint_lnd_rpc_cert macaroon_path = settings.mint_lnd_rpc_macaroon - if not self.endpoint: + if not settings.mint_lnd_rpc_endpoint: raise Exception("cannot initialize LndRPCWallet: no endpoint") + self.endpoint = settings.mint_lnd_rpc_endpoint + if not macaroon_path: raise Exception("cannot initialize LndRPCWallet: no macaroon") if not cert_path: raise Exception("no certificate for LndRPCWallet provided") - self.macaroon = codecs.encode(open(macaroon_path, 'rb').read(), 'hex') + self.macaroon = codecs.encode(open(macaroon_path, "rb").read(), "hex") def metadata_callback(context, callback): - callback([('macaroon', self.macaroon)], None) + callback([("macaroon", self.macaroon)], None) + auth_creds = grpc.metadata_call_credentials(metadata_callback) # create SSL credentials - os.environ['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA' - cert = open(cert_path, 'rb').read() + os.environ["GRPC_SSL_CIPHER_SUITES"] = "HIGH+ECDSA" + cert = open(cert_path, "rb").read() ssl_creds = grpc.ssl_channel_credentials(cert) # combine macaroon and SSL credentials @@ -86,11 +90,12 @@ def metadata_callback(context, callback): if self.supports_mpp: logger.info("LndRPCWallet enabling MPP feature") - async def status(self) -> StatusResponse: r = None try: - async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel: + async with grpc.aio.secure_channel( + self.endpoint, self.combined_creds + ) as channel: lnstub = lightningstub.LightningStub(channel) r = await lnstub.ChannelBalance(lnrpc.ChannelBalanceRequest()) except AioRpcError as e: @@ -98,9 +103,8 @@ async def status(self) -> StatusResponse: error_message=f"Error calling Lnd gRPC: {e}", balance=0 ) # NOTE: `balance` field is deprecated. Change this. - return StatusResponse(error_message=None, balance=r.balance*1000) + return StatusResponse(error_message=None, balance=r.balance * 1000) - async def create_invoice( self, amount: Amount, @@ -124,7 +128,9 @@ async def create_invoice( r = None try: - async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel: + async with grpc.aio.secure_channel( + self.endpoint, self.combined_creds + ) as channel: lnstub = lightningstub.LightningStub(channel) r = await lnstub.AddInvoice(data) except AioRpcError as e: @@ -144,7 +150,7 @@ async def create_invoice( payment_request=payment_request, error_message=None, ) - + async def pay_invoice( self, quote: MeltQuote, fee_limit_msat: int ) -> PaymentResponse: @@ -159,12 +165,12 @@ async def pay_invoice( ) # set the fee limit for the payment - feelimit = lnrpc.FeeLimit( - fixed_msat=fee_limit_msat - ) + feelimit = lnrpc.FeeLimit(fixed_msat=fee_limit_msat) r = None try: - async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel: + async with grpc.aio.secure_channel( + self.endpoint, self.combined_creds + ) as channel: lnstub = lightningstub.LightningStub(channel) r = await lnstub.SendPaymentSync( lnrpc.SendRequest( @@ -195,14 +201,12 @@ async def pay_invoice( preimage=preimage, error_message=None, ) - + async def pay_partial_invoice( self, quote: MeltQuote, amount: Amount, fee_limit_msat: int ) -> PaymentResponse: # set the fee limit for the payment - feelimit = lnrpc.FeeLimit( - fixed_msat=fee_limit_msat - ) + feelimit = lnrpc.FeeLimit(fixed_msat=fee_limit_msat) invoice = bolt11.decode(quote.request) invoice_amount = invoice.amount_msat @@ -220,7 +224,9 @@ async def pay_partial_invoice( # get the route r = None try: - async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel: + async with grpc.aio.secure_channel( + self.endpoint, self.combined_creds + ) as channel: lnstub = lightningstub.LightningStub(channel) router_stub = routerstub.RouterStub(channel) r = await lnstub.QueryRoutes( @@ -230,23 +236,27 @@ async def pay_partial_invoice( fee_limit=feelimit, ) ) - ''' + """ # We need to set the mpp_record for a partial payment mpp_record = lnrpc.MPPRecord( payment_addr=bytes.fromhex(payer_addr), total_amt_msat=total_amount_msat, ) - ''' + """ # modify the mpp_record in the last hop route_nr = 0 - r.routes[route_nr].hops[-1].mpp_record.payment_addr = bytes.fromhex(payer_addr) - r.routes[route_nr].hops[-1].mpp_record.total_amt_msat = total_amount_msat + r.routes[route_nr].hops[-1].mpp_record.payment_addr = bytes.fromhex( # type: ignore + payer_addr + ) + r.routes[route_nr].hops[ # type: ignore + -1 + ].mpp_record.total_amt_msat = total_amount_msat # Send to route request r = await router_stub.SendToRouteV2( routerrpc.SendToRouteRequest( payment_hash=bytes.fromhex(invoice.payment_hash), - route=r.routes[route_nr], + route=r.routes[route_nr], # type: ignore ) ) except AioRpcError as e: @@ -275,16 +285,16 @@ async def pay_partial_invoice( preimage=preimage, error_message=None, ) - + async def get_invoice_status(self, checking_id: str) -> PaymentStatus: r = None try: - async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel: + async with grpc.aio.secure_channel( + self.endpoint, self.combined_creds + ) as channel: lnstub = lightningstub.LightningStub(channel) r = await lnstub.LookupInvoice( - lnrpc.PaymentHash( - r_hash=bytes.fromhex(checking_id) - ) + lnrpc.PaymentHash(r_hash=bytes.fromhex(checking_id)) ) except AioRpcError as e: error_message = f"LookupInvoice failed: {e}" @@ -292,7 +302,7 @@ async def get_invoice_status(self, checking_id: str) -> PaymentStatus: return PaymentStatus(paid=None) return PaymentStatus(paid=INVOICE_STATUSES[r.state]) - + async def get_payment_status(self, checking_id: str) -> PaymentStatus: """ This routine checks the payment status using routerpc.TrackPaymentV2. @@ -307,7 +317,9 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus: request = routerrpc.TrackPaymentRequest(payment_hash=checking_id_bytes) try: - async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel: + async with grpc.aio.secure_channel( + self.endpoint, self.combined_creds + ) as channel: router_stub = routerstub.RouterStub(channel) async for payment in router_stub.TrackPaymentV2(request): if payment is not None and payment.status: @@ -325,21 +337,23 @@ async def get_payment_status(self, checking_id: str) -> PaymentStatus: logger.error(error_message) return PaymentStatus(paid=None) - + async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: while True: try: - async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel: + async with grpc.aio.secure_channel( + self.endpoint, self.combined_creds + ) as channel: lnstub = lightningstub.LightningStub(channel) - async for invoice in lnstub.SubscribeInvoices(lnrpc.InvoiceSubscription()): + async for invoice in lnstub.SubscribeInvoices( + lnrpc.InvoiceSubscription() + ): if invoice.state != lnrpc.Invoice.InvoiceState.SETTLED: continue payment_hash = invoice.r_hash.hex() yield payment_hash except AioRpcError as exc: - logger.error( - f"SubscribeInvoices failed: {exc}. Retrying in 1 sec..." - ) + logger.error(f"SubscribeInvoices failed: {exc}. Retrying in 1 sec...") await asyncio.sleep(1) async def get_payment_quote( diff --git a/cashu/lightning/lndrest.py b/cashu/lightning/lndrest.py index 3274da97..18dbcd49 100644 --- a/cashu/lightning/lndrest.py +++ b/cashu/lightning/lndrest.py @@ -33,6 +33,7 @@ class LndRestWallet(LightningBackend): supports_mpp = settings.mint_lnd_enable_mpp supports_incoming_payment_stream = True supported_units = set([Unit.sat, Unit.msat]) + supports_description: bool = True unit = Unit.sat def __init__(self, unit: Unit = Unit.sat, **kwargs): diff --git a/cashu/lightning/strike.py b/cashu/lightning/strike.py index cf35cb4a..2103c2ce 100644 --- a/cashu/lightning/strike.py +++ b/cashu/lightning/strike.py @@ -1,6 +1,6 @@ # type: ignore import secrets -from typing import AsyncGenerator, Dict, Optional +from typing import AsyncGenerator, Optional import httpx @@ -23,6 +23,7 @@ class StrikeWallet(LightningBackend): """https://docs.strike.me/api/""" supported_units = [Unit.sat, Unit.usd, Unit.eur] + supports_description: bool = False currency_map = {Unit.sat: "BTC", Unit.usd: "USD", Unit.eur: "EUR"} def __init__(self, unit: Unit, **kwargs): @@ -95,13 +96,6 @@ async def create_invoice( ) -> InvoiceResponse: self.assert_unit_supported(amount.unit) - data: Dict = {"out": False, "amount": amount} - if description_hash: - data["description_hash"] = description_hash.hex() - if unhashed_description: - data["unhashed_description"] = unhashed_description.hex() - - data["memo"] = memo or "" payload = { "correlationId": secrets.token_hex(16), "description": "Invoice for order 123", diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index d3926370..92a12552 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -418,6 +418,12 @@ async def mint_quote(self, quote_request: PostMintQuoteRequest) -> MintQuote: quote_request.unit, Method.bolt11.name ) + if ( + quote_request.description + and not self.backends[method][unit].supports_description + ): + raise NotAllowedError("Backend does not support descriptions.") + if settings.mint_max_balance: balance = await self.get_balance() if balance + quote_request.amount > settings.mint_max_balance: @@ -426,7 +432,10 @@ async def mint_quote(self, quote_request: PostMintQuoteRequest) -> MintQuote: logger.trace(f"requesting invoice for {unit.str(quote_request.amount)}") invoice_response: InvoiceResponse = await self.backends[method][ unit - ].create_invoice(Amount(unit=unit, amount=quote_request.amount)) + ].create_invoice( + amount=Amount(unit=unit, amount=quote_request.amount), + memo=quote_request.description, + ) logger.trace( f"got invoice {invoice_response.payment_request} with checking id" f" {invoice_response.checking_id}" diff --git a/cashu/mint/router.py b/cashu/mint/router.py index fd6d7626..3aaa0b80 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -1,4 +1,5 @@ import asyncio +import time from fastapi import APIRouter, Request, WebSocket from loguru import logger @@ -60,7 +61,9 @@ async def info() -> GetInfoResponse: description_long=settings.mint_info_description_long, contact=contact_info, nuts=mint_features, + icon_url=settings.mint_info_icon_url, motd=settings.mint_info_motd, + time=int(time.time()), ) diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py index 4eef2b22..9f1f4f20 100644 --- a/cashu/wallet/cli/cli.py +++ b/cashu/wallet/cli/cli.py @@ -241,6 +241,7 @@ async def pay( @cli.command("invoice", help="Create Lighting invoice.") @click.argument("amount", type=float) +@click.option("memo", "-m", default="", help="Memo for the invoice.", type=str) @click.option("--id", default="", help="Id of the paid invoice.", type=str) @click.option( "--split", @@ -259,7 +260,14 @@ async def pay( ) @click.pass_context @coro -async def invoice(ctx: Context, amount: float, id: str, split: int, no_check: bool): +async def invoice( + ctx: Context, + amount: float, + memo: str, + id: str, + split: int, + no_check: bool, +): wallet: Wallet = ctx.obj["WALLET"] await wallet.load_mint() await print_balance(ctx) @@ -324,11 +332,11 @@ def mint_invoice_callback(msg: JSONRPCNotficationParams): ) if mint_supports_websockets and not no_check: invoice, subscription = await wallet.request_mint_with_callback( - amount, callback=mint_invoice_callback + amount, callback=mint_invoice_callback, memo=memo ) invoice_nonlocal, subscription_nonlocal = invoice, subscription else: - invoice = await wallet.request_mint(amount) + invoice = await wallet.request_mint(amount, memo=memo) if invoice.bolt11: print("") print(f"Pay invoice to mint {wallet.unit.str(amount)}:") @@ -1014,6 +1022,8 @@ async def info(ctx: Context, mint: bool, mnemonic: bool): print(f" - Version: {mint_info['version']}") if mint_info.get("motd"): print(f" - Message of the day: {mint_info['motd']}") + if mint_info.get("time"): + print(f" - Server time: {mint_info['time']}") if mint_info.get("nuts"): print( " - Supported NUTS:" diff --git a/cashu/wallet/lightning/lightning.py b/cashu/wallet/lightning/lightning.py index 56ab06aa..3e5489d7 100644 --- a/cashu/wallet/lightning/lightning.py +++ b/cashu/wallet/lightning/lightning.py @@ -1,3 +1,5 @@ +from typing import Optional + import bolt11 from ...core.base import Amount, ProofSpentState, Unit @@ -33,15 +35,18 @@ def __init__(self, *args, **kwargs): pass super().__init__(*args, **kwargs) - async def create_invoice(self, amount: int) -> InvoiceResponse: + async def create_invoice( + self, amount: int, memo: Optional[str] = None + ) -> InvoiceResponse: """Create lightning invoice Args: amount (int): amount in satoshis + memo (str, optional): invoice memo. Defaults to None. Returns: str: invoice """ - invoice = await self.request_mint(amount) + invoice = await self.request_mint(amount, memo) return InvoiceResponse( ok=True, payment_request=invoice.bolt11, checking_id=invoice.payment_hash ) diff --git a/cashu/wallet/mint_info.py b/cashu/wallet/mint_info.py index 760f43c9..ccd00087 100644 --- a/cashu/wallet/mint_info.py +++ b/cashu/wallet/mint_info.py @@ -15,6 +15,8 @@ class MintInfo(BaseModel): description_long: Optional[str] contact: Optional[List[MintInfoContact]] motd: Optional[str] + icon_url: Optional[str] + time: Optional[int] nuts: Optional[Dict[int, Any]] def __str__(self): diff --git a/cashu/wallet/utils.py b/cashu/wallet/utils.py new file mode 100644 index 00000000..b5b1115b --- /dev/null +++ b/cashu/wallet/utils.py @@ -0,0 +1,10 @@ +def sanitize_url(url: str) -> str: + # extract host from url and lower case it, remove trailing slash from url + protocol = url.split("://")[0] + host = url.split("://")[1].split("/")[0].lower() + path = ( + url.split("://")[1].split("/", 1)[1].rstrip("/") + if "/" in url.split("://")[1] + else "" + ) + return f"{protocol}://{host}{'/' + path if path else ''}" diff --git a/cashu/wallet/v1_api.py b/cashu/wallet/v1_api.py index d5d9ce50..f1a8bb16 100644 --- a/cashu/wallet/v1_api.py +++ b/cashu/wallet/v1_api.py @@ -283,11 +283,15 @@ async def _get_info(self) -> GetInfoResponse: @async_set_httpx_client @async_ensure_mint_loaded - async def mint_quote(self, amount: int, unit: Unit) -> PostMintQuoteResponse: + async def mint_quote( + self, amount: int, unit: Unit, memo: Optional[str] = None + ) -> PostMintQuoteResponse: """Requests a mint quote from the server and returns a payment request. Args: amount (int): Amount of tokens to mint + unit (Unit): Unit of the amount + memo (Optional[str], optional): Memo to attach to Lightning invoice. Defaults to None. Returns: PostMintQuoteResponse: Mint Quote Response @@ -295,8 +299,8 @@ async def mint_quote(self, amount: int, unit: Unit) -> PostMintQuoteResponse: Raises: Exception: If the mint request fails """ - logger.trace("Requesting mint: GET /v1/mint/bolt11") - payload = PostMintQuoteRequest(unit=unit.name, amount=amount) + logger.trace("Requesting mint: POST /v1/mint/bolt11") + payload = PostMintQuoteRequest(unit=unit.name, amount=amount, description=memo) resp = await self.httpx.post( join(self.url, "/v1/mint/quote/bolt11"), json=payload.dict() ) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index c44d6964..71bb838a 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -8,9 +8,6 @@ from bip32 import BIP32 from loguru import logger -from cashu.core.crypto.keys import derive_keyset_id -from cashu.core.json_rpc.base import JSONRPCSubscriptionKinds - from ..core.base import ( BlindedMessage, BlindedSignature, @@ -22,10 +19,16 @@ WalletKeyset, ) from ..core.crypto import b_dhke +from ..core.crypto.keys import derive_keyset_id from ..core.crypto.secp import PrivateKey, PublicKey from ..core.db import Database from ..core.errors import KeysetNotFoundError -from ..core.helpers import amount_summary, calculate_number_of_blank_outputs, sum_proofs +from ..core.helpers import ( + amount_summary, + calculate_number_of_blank_outputs, + sum_proofs, +) +from ..core.json_rpc.base import JSONRPCSubscriptionKinds from ..core.migrations import migrate_databases from ..core.models import ( PostCheckStateResponse, @@ -34,7 +37,8 @@ from ..core.p2pk import Secret from ..core.settings import settings from ..core.split import amount_split -from ..wallet.crud import ( +from . import migrations +from .crud import ( bump_secret_derivation, get_keysets, get_proofs, @@ -48,7 +52,6 @@ update_lightning_invoice, update_proof, ) -from . import migrations from .dlc import WalletSCT from .htlc import WalletHTLC from .mint_info import MintInfo @@ -57,6 +60,7 @@ from .secrets import WalletSecrets from .subscriptions import SubscriptionManager from .transactions import WalletTransactions +from .utils import sanitize_url from .v1_api import LedgerAPI @@ -108,7 +112,8 @@ def __init__(self, url: str, db: str, name: str = "no_name", unit: str = "sat"): self.proofs: List[Proof] = [] self.name = name self.unit = Unit[unit] - url = url.rstrip("/") + url = sanitize_url(url) + super().__init__(url=url, db=self.db) logger.debug("Wallet initialized") logger.debug(f"Mint URL: {url}") @@ -375,18 +380,19 @@ async def _check_used_secrets(self, secrets): logger.trace("Secret check complete.") async def request_mint_with_callback( - self, amount: int, callback: Callable + self, amount: int, callback: Callable, memo: Optional[str] = None ) -> Tuple[Invoice, SubscriptionManager]: """Request a Lightning invoice for minting tokens. Args: amount (int): Amount for Lightning invoice in satoshis callback (Callable): Callback function to be called when the invoice is paid. + memo (Optional[str], optional): Memo for the Lightning invoice. Defaults Returns: Invoice: Lightning invoice """ - mint_qoute = await super().mint_quote(amount, self.unit) + mint_qoute = await super().mint_quote(amount, self.unit, memo) subscriptions = SubscriptionManager(self.url) threading.Thread( target=subscriptions.connect, name="SubscriptionManager", daemon=True @@ -409,17 +415,18 @@ async def request_mint_with_callback( await store_lightning_invoice(db=self.db, invoice=invoice) return invoice, subscriptions - async def request_mint(self, amount: int) -> Invoice: + async def request_mint(self, amount: int, memo: Optional[str] = None) -> Invoice: """Request a Lightning invoice for minting tokens. Args: amount (int): Amount for Lightning invoice in satoshis callback (Optional[Callable], optional): Callback function to be called when the invoice is paid. Defaults to None. + memo (Optional[str], optional): Memo for the Lightning invoice. Defaults to None. Returns: PostMintQuoteResponse: Mint Quote Response """ - mint_quote_response = await super().mint_quote(amount, self.unit) + mint_quote_response = await super().mint_quote(amount, self.unit, memo) decoded_invoice = bolt11.decode(mint_quote_response.request) invoice = Invoice( amount=amount, @@ -476,11 +483,12 @@ def split_wallet_state(self, amount: int) -> List[int]: return amounts - async def mint_quote(self, amount: int) -> Invoice: + async def mint_quote(self, amount: int, memo: Optional[str] = None) -> Invoice: """Request a Lightning invoice for minting tokens. Args: amount (int): Amount for Lightning invoice in satoshis + memo (Optional[str], optional): Memo for the Lightning invoice. Defaults to None. Returns: Invoice: Lightning invoice for minting tokens diff --git a/mypy.ini b/mypy.ini index 0df56bd0..087ac3c0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -6,3 +6,6 @@ ignore_missing_imports = True [mypy-cashu.nostr.*] ignore_errors = True + +[mypy-cashu.lightning.lnd_grpc.protos.*] +ignore_errors = True diff --git a/poetry.lock b/poetry.lock index a488b045..09fc43d1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -145,18 +145,17 @@ files = [ [[package]] name = "bip32" -version = "3.4" +version = "4.0" description = "Minimalistic implementation of the BIP32 key derivation scheme" optional = false python-versions = "*" files = [ - {file = "bip32-3.4-py3-none-any.whl", hash = "sha256:c18099c25cabc087d081142d040eacb6fdf09f2c61fde3247e1f61313d1c0d26"}, - {file = "bip32-3.4.tar.gz", hash = "sha256:09213225d99ede936c39a4f5a520549d8ecc49ebe3fa15608a17ea4aa9f61a10"}, + {file = "bip32-4.0-py3-none-any.whl", hash = "sha256:9728b38336129c00e1f870bbb3e328c9632d51c1bddeef4011fd3115cb3aeff9"}, + {file = "bip32-4.0.tar.gz", hash = "sha256:8035588f252f569bb414bc60df151ae431fc1c6789a19488a32890532ef3a2fc"}, ] [package.dependencies] -base58 = ">=2.0,<3.0" -coincurve = ">=15.0,<19" +coincurve = ">=15.0,<21" [[package]] name = "bitstring" @@ -341,59 +340,70 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "coincurve" -version = "18.0.0" +version = "20.0.0" description = "Cross-platform Python CFFI bindings for libsecp256k1" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "coincurve-18.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b1a42eba91b9e4f833309e94bc6a270b1700cb4567d4809ef91f00968b57925"}, - {file = "coincurve-18.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:116bf1b60a6e72e23c6b153d7c79f0e565d82973d917a3cecf655ffb29263163"}, - {file = "coincurve-18.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d53e2a268142924c24e9b786b3e6c3603fae54bb8211560036b0e9ce6a9f2dbc"}, - {file = "coincurve-18.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b31ab366fadff16ecfdde96ffc07e70fee83850f88bd1f985a8b4977a68bbfb"}, - {file = "coincurve-18.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e3c37cfadac6896668a130ea46296a3dfdeea0160fd66a51e377ad00795269"}, - {file = "coincurve-18.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f3e5f2a2d774050b3ea8bf2167f2d598fde58d7690779931516714d98b65d884"}, - {file = "coincurve-18.0.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83379dd70291480df2052554851bfd17444c003aef7c4bb02d96d73eec69fe28"}, - {file = "coincurve-18.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:33678f6b43edbeab6605584c725305f4f814239780c53eba0f8e4bc4a52b1d1a"}, - {file = "coincurve-18.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f40646d5f29ac9026f8cc1b368bc9ab68710fad055b64fbec020f9bbfc99b242"}, - {file = "coincurve-18.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:779da694dea1b1d09e16b00e079f6a1195290ce9568f39c95cddf35f1f49ec49"}, - {file = "coincurve-18.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7844f01904e32317a00696a27fd771860e53a2fa62e5c66eace9337d2742c9e6"}, - {file = "coincurve-18.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257c6171cd0301c119ef41360f0d0c2fb5cc288717b33d3bd5482a4c9ae04551"}, - {file = "coincurve-18.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f8bcb9c40fd730cf377fa448f1304355d6497fb3d00b7b0a69a10dfcc14a6d28"}, - {file = "coincurve-18.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e3abb7f65e2b5fb66a15e374faeaafe6700fdb83fb66d1873ddff91c395a3b74"}, - {file = "coincurve-18.0.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f44b9ba588b34795d1b4074f9a9fa372adef3fde58300bf32f40a69e8cd72a23"}, - {file = "coincurve-18.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:908467330cd3047c71105a08394c4f3e7dce76e4371b030ba8b0ef863013e3ca"}, - {file = "coincurve-18.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:599b1b3cf097cae920d97f31a5b8e8aff185ca8fa5d8a785b2edf7b199fb9731"}, - {file = "coincurve-18.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d2c20d108580bce5efedb980688031462168f4de2446de95898b48a249127a2"}, - {file = "coincurve-18.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eba563f7f70c10323227d1890072172bd84df6f814c9a6b012033b214426b6cf"}, - {file = "coincurve-18.0.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:412a06b7d1b8229f25318f05e76310298da5ad55d73851eabac7ddfdcdc5bff4"}, - {file = "coincurve-18.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:286969b6f789bbd9d744d28350a3630c1cb3ee045263469a28892f70a4a6654a"}, - {file = "coincurve-18.0.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:14700463009c7d799a746929728223aa53ff1ece394ea408516d98d637434883"}, - {file = "coincurve-18.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7f1142252e870f091b2c2c21cc1fadfdd29af23d02e99f29add0f14d1ba94b4c"}, - {file = "coincurve-18.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cd11d2ca5b7e989c5ce1af217a2ad78c19c21afca786f198d1b1a408d6f408dc"}, - {file = "coincurve-18.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1bce17d7475cee9db2c2fa7af07eaab582732b378acf6dcaee417de1df2d8661"}, - {file = "coincurve-18.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ab662b67454fea7f0a5ae855ba6ad9410bcaebe68b97f4dade7b5944dec3a11"}, - {file = "coincurve-18.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23b9ced9cce32dabb4bc15fa6449252fa51efddf0268481973e4c3772a5a68c6"}, - {file = "coincurve-18.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d05641cf31d68514c47cb54105d20acbae79fc3ee3942454eaaf411babb3f880"}, - {file = "coincurve-18.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a7b31efe56b3f6434828ad5f6ecde4a95747bb69b59032746482eebb8f3456a4"}, - {file = "coincurve-18.0.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2d95103ed43df855121cd925869ae2589360a8d94fcd61b236958deacfb9a359"}, - {file = "coincurve-18.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:abeb4c1d78e1a81a3f1c99a406cd858669582ada2d976e876ef694f57dec95ca"}, - {file = "coincurve-18.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fceca9d6ecaa1e8f891675e4f4ff530d54e41c648fc6e8a816835ffa640fa899"}, - {file = "coincurve-18.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e009f06287507158f16c82cc313c0f3bfd0e9ec1e82d1a4d5fa1c5b6c0060f69"}, - {file = "coincurve-18.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a0c0c1e492ef08efe99d25a23d535e2bff667bbef43d71a6f8893ae811b3d81"}, - {file = "coincurve-18.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3caf58877bcf41eb4c1be7a2d54317f0b31541d99ba248dae28821b19c52a0db"}, - {file = "coincurve-18.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8964e680c622a2b5eea940abdf51c77c1bd3d4fde2a04cec2420bf91981b198a"}, - {file = "coincurve-18.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:73e464e0ace77c686fdc54590e5592905b6802f9fc20a0c023f0b1585669d6a3"}, - {file = "coincurve-18.0.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ba9eaddd50a2ce0d891af7cee11c2e048d1f0f44bf87db00a5c4b1eee7e3391b"}, - {file = "coincurve-18.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8290903d4629f27f9f3cdeec72ffa97536c5a6ed5ba7e3413b2707991c650fbe"}, - {file = "coincurve-18.0.0-py3-none-win32.whl", hash = "sha256:c60690bd7704d8563968d2dded33eb514875a52b5964f085409965ad041b2555"}, - {file = "coincurve-18.0.0-py3-none-win_amd64.whl", hash = "sha256:704d1abf2e78def33988368592233a8ec9b98bfc45dfa2ec9e898adfad46e5ad"}, - {file = "coincurve-18.0.0.tar.gz", hash = "sha256:c86626afe417a09d8e80e56780efcae3ae516203b23b5ade84813916e1c94fc1"}, + {file = "coincurve-20.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d559b22828638390118cae9372a1bb6f6594f5584c311deb1de6a83163a0919b"}, + {file = "coincurve-20.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:33d7f6ebd90fcc550f819f7f2cce2af525c342aac07f0ccda46ad8956ad9d99b"}, + {file = "coincurve-20.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22d70dd55d13fd427418eb41c20fde0a20a5e5f016e2b1bb94710701e759e7e0"}, + {file = "coincurve-20.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46f18d481eaae72c169f334cde1fd22011a884e0c9c6adc3fdc1fd13df8236a3"}, + {file = "coincurve-20.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9de1ec57f43c3526bc462be58fb97910dc1fdd5acab6c71eda9f9719a5bd7489"}, + {file = "coincurve-20.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a6f007c44c726b5c0b3724093c0d4fb8e294f6b6869beb02d7473b21777473a3"}, + {file = "coincurve-20.0.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0ff1f3b81330db5092c24da2102e4fcba5094f14945b3eb40746456ceabdd6d9"}, + {file = "coincurve-20.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82f7de97694d9343f26bd1c8e081b168e5f525894c12445548ce458af227f536"}, + {file = "coincurve-20.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:e905b4b084b4f3b61e5a5d58ac2632fd1d07b7b13b4c6d778335a6ca1dafd7a3"}, + {file = "coincurve-20.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:3657bb5ed0baf1cf8cf356e7d44aa90a7902cc3dd4a435c6d4d0bed0553ad4f7"}, + {file = "coincurve-20.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:44087d1126d43925bf9a2391ce5601bf30ce0dba4466c239172dc43226696018"}, + {file = "coincurve-20.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ccf0ba38b0f307a9b3ce28933f6c71dc12ef3a0985712ca09f48591afd597c8"}, + {file = "coincurve-20.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:566bc5986debdf8572b6be824fd4de03d533c49f3de778e29f69017ae3fe82d8"}, + {file = "coincurve-20.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4d70283168e146f025005c15406086513d5d35e89a60cf4326025930d45013a"}, + {file = "coincurve-20.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:763c6122dd7d5e7a81c86414ce360dbe9a2d4afa1ca6c853ee03d63820b3d0c5"}, + {file = "coincurve-20.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f00c361c356bcea386d47a191bb8ac60429f4b51c188966a201bfecaf306ff7f"}, + {file = "coincurve-20.0.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4af57bdadd2e64d117dd0b33cfefe76e90c7a6c496a7b034fc65fd01ec249b15"}, + {file = "coincurve-20.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a26437b7cbde13fb6e09261610b788ca2a0ca2195c62030afd1e1e0d1a62e035"}, + {file = "coincurve-20.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ed51f8bba35e6c7676ad65539c3dbc35acf014fc402101fa24f6b0a15a74ab9e"}, + {file = "coincurve-20.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:594b840fc25d74118407edbbbc754b815f1bba9759dbf4f67f1c2b78396df2d3"}, + {file = "coincurve-20.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4df4416a6c0370d777aa725a25b14b04e45aa228da1251c258ff91444643f688"}, + {file = "coincurve-20.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1ccc3e4db55abf3fc0e604a187fdb05f0702bc5952e503d9a75f4ae6eeb4cb3a"}, + {file = "coincurve-20.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac8335b1658a2ef5b3eb66d52647742fe8c6f413ad5b9d5310d7ea6d8060d40f"}, + {file = "coincurve-20.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ac025e485a0229fd5394e0bf6b4a75f8a4f6cee0dcf6f0b01a2ef05c5210ff"}, + {file = "coincurve-20.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e46e3f1c21b3330857bcb1a3a5b942f645c8bce912a8a2b252216f34acfe4195"}, + {file = "coincurve-20.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:df9ff9b17a1d27271bf476cf3fa92df4c151663b11a55d8cea838b8f88d83624"}, + {file = "coincurve-20.0.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4155759f071375699282e03b3d95fb473ee05c022641c077533e0d906311e57a"}, + {file = "coincurve-20.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0530b9dd02fc6f6c2916716974b79bdab874227f560c422801ade290e3fc5013"}, + {file = "coincurve-20.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:eacf9c0ce8739c84549a89c083b1f3526c8780b84517ee75d6b43d276e55f8a0"}, + {file = "coincurve-20.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:52a67bfddbd6224dfa42085c88ad176559801b57d6a8bd30d92ee040de88b7b3"}, + {file = "coincurve-20.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61e951b1d695b62376f60519a84c4facaf756eeb9c5aff975bea0942833f185d"}, + {file = "coincurve-20.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e9e548db77f4ea34c0d748dddefc698adb0ee3fab23ed19f80fb2118dac70f6"}, + {file = "coincurve-20.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cdbf0da0e0809366fdfff236b7eb6e663669c7b1f46361a4c4d05f5b7e94c57"}, + {file = "coincurve-20.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72222b4ecd3952e8ffcbf59bc7e0d1b181161ba170b60e5c8e1f359a43bbe7e"}, + {file = "coincurve-20.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9add43c4807f0c17a940ce4076334c28f51d09c145cd478400e89dcfb83fb59d"}, + {file = "coincurve-20.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bcc94cceea6ec8863815134083e6221a034b1ecef822d0277cf6ad2e70009b7f"}, + {file = "coincurve-20.0.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ffbdfef6a6d147988eabaed681287a9a7e6ba45ecc0a8b94ba62ad0a7656d97"}, + {file = "coincurve-20.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13335c19c7e5f36eaba2a53c68073d981980d7dc7abfee68d29f2da887ccd24e"}, + {file = "coincurve-20.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:7fbfb8d16cf2bea2cf48fc5246d4cb0a06607d73bb5c57c007c9aed7509f855e"}, + {file = "coincurve-20.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4870047704cddaae7f0266a549c927407c2ba0ec92d689e3d2b511736812a905"}, + {file = "coincurve-20.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81ce41263517b0a9f43cd570c87720b3c13324929584fa28d2e4095969b6015d"}, + {file = "coincurve-20.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:572083ccce6c7b514d482f25f394368f4ae888f478bd0b067519d33160ea2fcc"}, + {file = "coincurve-20.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee5bc78a31a2f1370baf28aaff3949bc48f940a12b0359d1cd2c4115742874e6"}, + {file = "coincurve-20.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2895d032e281c4e747947aae4bcfeef7c57eabfd9be22886c0ca4e1365c7c1f"}, + {file = "coincurve-20.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d3e2f21957ada0e1742edbde117bb41758fa8691b69c8d186c23e9e522ea71cd"}, + {file = "coincurve-20.0.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c2baa26b1aad1947ca07b3aa9e6a98940c5141c6bdd0f9b44d89e36da7282ffa"}, + {file = "coincurve-20.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7eacc7944ddf9e2b7448ecbe84753841ab9874b8c332a4f5cc3b2f184db9f4a2"}, + {file = "coincurve-20.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:c293c095dc690178b822cadaaeb81de3cc0d28f8bdf8216ed23551dcce153a26"}, + {file = "coincurve-20.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:11a47083a0b7092d3eb50929f74ffd947c4a5e7035796b81310ea85289088c7a"}, + {file = "coincurve-20.0.0.tar.gz", hash = "sha256:872419e404300302e938849b6b92a196fabdad651060b559dc310e52f8392829"}, ] [package.dependencies] asn1crypto = "*" cffi = ">=1.3.0" +[package.extras] +dev = ["coverage", "pytest", "pytest-benchmark"] + [[package]] name = "colorama" version = "0.4.6" @@ -1496,7 +1506,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1504,16 +1513,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1530,7 +1531,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1538,7 +1538,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -2076,4 +2075,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "54e4b2461b7a222dc287c8e9da19edaa3a18268a2f0da4230b7613fa6ef884ec" +content-hash = "38db19797cb04d1f941f6f58c56c8dfe09ac34d45ac2f654bc97dd8110912da8" diff --git a/pyproject.toml b/pyproject.toml index 1bf7810a..b650750e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ setuptools = "^68.1.2" wheel = "^0.41.1" importlib-metadata = "^6.8.0" httpx = {extras = ["socks"], version = "^0.25.1"} -bip32 = "^3.4" +bip32 = "^4.0" mnemonic = "^0.20" bolt11 = "^2.0.5" pre-commit = "^3.5.0" diff --git a/requirements.txt b/requirements.txt index 7139a70b..84819b60 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ certifi==2024.7.4 ; python_full_version >= "3.8.1" and python_full_version < "4. cffi==1.16.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" cfgv==3.4.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" click==8.1.7 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" -coincurve==18.0.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" +coincurve==20.0.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" colorama==0.4.6 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" and (platform_system == "Windows" or sys_platform == "win32") cryptography==41.0.7 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" deprecated==1.2.14 ; python_full_version >= "3.8.1" and python_version < "4.0" diff --git a/tests/test_mint.py b/tests/test_mint.py index f5976a82..9d097296 100644 --- a/tests/test_mint.py +++ b/tests/test_mint.py @@ -86,6 +86,22 @@ async def test_mint(ledger: Ledger): ) +@pytest.mark.asyncio +async def test_mint_invalid_quote(ledger: Ledger): + await assert_err( + ledger.get_mint_quote(quote_id="invalid_quote_id"), + "quote not found", + ) + + +@pytest.mark.asyncio +async def test_melt_invalid_quote(ledger: Ledger): + await assert_err( + ledger.get_melt_quote(quote_id="invalid_quote_id"), + "quote not found", + ) + + @pytest.mark.asyncio async def test_mint_invalid_blinded_message(ledger: Ledger): quote = await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat")) diff --git a/tests/test_wallet_cli.py b/tests/test_wallet_cli.py index 81a2e4b6..995518dd 100644 --- a/tests/test_wallet_cli.py +++ b/tests/test_wallet_cli.py @@ -1,6 +1,7 @@ import asyncio from typing import Tuple +import bolt11 import pytest from click.testing import CliRunner @@ -8,7 +9,7 @@ from cashu.core.settings import settings from cashu.wallet.cli.cli import cli from cashu.wallet.wallet import Wallet -from tests.helpers import is_fake, pay_if_regtest +from tests.helpers import is_deprecated_api_only, is_fake, is_regtest, pay_if_regtest @pytest.fixture(autouse=True, scope="session") @@ -107,6 +108,19 @@ def test_balance(cli_prefix): assert result.exit_code == 0 +@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet") +def test_invoice(mint, cli_prefix): + runner = CliRunner() + result = runner.invoke( + cli, + [*cli_prefix, "invoice", "1000"], + ) + + wallet = asyncio.run(init_wallet()) + assert wallet.available_balance >= 1000 + assert result.exit_code == 0 + + def test_invoice_return_immediately(mint, cli_prefix): runner = CliRunner() result = runner.invoke( @@ -130,6 +144,30 @@ def test_invoice_return_immediately(mint, cli_prefix): assert result.exit_code == 0 +@pytest.mark.skipif(is_deprecated_api_only, reason="only works with v1 API") +def test_invoice_with_memo(mint, cli_prefix): + runner = CliRunner() + result = runner.invoke( + cli, + [*cli_prefix, "invoice", "-n", "1000", "-m", "test memo"], + ) + assert result.exception is None + + # find word starting with ln in the output + lines = result.output.split("\n") + invoice_str = "" + for line in lines: + for word in line.split(" "): + if word.startswith("ln"): + invoice_str = word + break + if not invoice_str: + raise Exception("No invoice found in the output") + invoice_obj = bolt11.decode(invoice_str) + assert invoice_obj.amount_msat == 1000_000 + assert invoice_obj.description == "test memo" + + def test_invoice_with_split(mint, cli_prefix): runner = CliRunner() result = runner.invoke( diff --git a/tests/test_wallet_lightning.py b/tests/test_wallet_lightning.py index 3b3cdd48..5dd567f8 100644 --- a/tests/test_wallet_lightning.py +++ b/tests/test_wallet_lightning.py @@ -1,5 +1,6 @@ from typing import List, Union +import bolt11 import pytest import pytest_asyncio @@ -7,7 +8,13 @@ from cashu.core.errors import CashuError from cashu.wallet.lightning import LightningWallet from tests.conftest import SERVER_ENDPOINT -from tests.helpers import get_real_invoice, is_fake, is_regtest, pay_if_regtest +from tests.helpers import ( + get_real_invoice, + is_deprecated_api_only, + is_fake, + is_regtest, + pay_if_regtest, +) async def assert_err(f, msg: Union[str, CashuError]): @@ -58,6 +65,16 @@ async def test_create_invoice(wallet: LightningWallet): assert invoice.payment_request.startswith("ln") +@pytest.mark.asyncio +@pytest.mark.skipif(is_deprecated_api_only, reason="only works with v1 API") +async def test_create_invoice_with_description(wallet: LightningWallet): + invoice = await wallet.create_invoice(64, "test description") + assert invoice.payment_request + assert invoice.payment_request.startswith("ln") + invoiceObj = bolt11.decode(invoice.payment_request) + assert invoiceObj.description == "test description" + + @pytest.mark.asyncio @pytest.mark.skipif(is_regtest, reason="only works with FakeWallet") async def test_check_invoice_internal(wallet: LightningWallet): diff --git a/tests/test_wallet_utils.py b/tests/test_wallet_utils.py new file mode 100644 index 00000000..534db8df --- /dev/null +++ b/tests/test_wallet_utils.py @@ -0,0 +1,63 @@ +from typing import List, Union + +from cashu.core.errors import CashuError +from cashu.wallet.utils import sanitize_url + + +async def assert_err(f, msg: Union[str, CashuError]): + """Compute f() and expect an error message 'msg'.""" + try: + await f + except Exception as exc: + error_message: str = str(exc.args[0]) + if isinstance(msg, CashuError): + if msg.detail not in error_message: + raise Exception( + f"CashuError. Expected error: {msg.detail}, got: {error_message}" + ) + return + if msg not in error_message: + raise Exception(f"Expected error: {msg}, got: {error_message}") + return + raise Exception(f"Expected error: {msg}, got no error") + + +async def assert_err_multiple(f, msgs: List[str]): + """Compute f() and expect an error message 'msg'.""" + try: + await f + except Exception as exc: + for msg in msgs: + if msg in str(exc.args[0]): + return + raise Exception(f"Expected error: {msgs}, got: {exc.args[0]}") + raise Exception(f"Expected error: {msgs}, got no error") + + +def test_sanitize_url(): + url = "https://localhost:3338" + assert sanitize_url(url) == "https://localhost:3338" + + url = "https://mint.com:3338" + assert sanitize_url(url) == "https://mint.com:3338" + + url = "https://Mint.com:3338" + assert sanitize_url(url) == "https://mint.com:3338" + + url = "https://mint.com:3338/" + assert sanitize_url(url) == "https://mint.com:3338" + + url = "https://mint.com:3338/abc" + assert sanitize_url(url) == "https://mint.com:3338/abc" + + url = "https://mint.com:3338/Abc" + assert sanitize_url(url) == "https://mint.com:3338/Abc" + + url = "https://mint.com:3338/abc/" + assert sanitize_url(url) == "https://mint.com:3338/abc" + + url = "https://mint.com:3338/Abc/" + assert sanitize_url(url) == "https://mint.com:3338/Abc" + + url = "https://Mint.com:3338/Abc/def" + assert sanitize_url(url) == "https://mint.com:3338/Abc/def"