diff --git a/.env.example b/.env.example index 4fa15ba..aae43e0 100644 --- a/.env.example +++ b/.env.example @@ -117,6 +117,15 @@ LIGHTNING_FEE_PERCENT=1.0 # minimum fee to reserve LIGHTNING_RESERVE_FEE_MIN=2000 +# Mint Management gRPC service configurations +MINT_RPC_SERVER_ENABLE=FALSE +MINT_RPC_SERVER_ADDR=localhost +MINT_RPC_SERVER_PORT=8086 +MINT_RPC_SERVER_MUTUAL_TLS=TRUE +MINT_RPC_SERVER_KEY="./server_private.pem" +MINT_RPC_SERVER_CERT="./server_cert.pem" +MINT_RPC_SERVER_CA="./ca_cert.pem" + # Limits # Max mint balance in satoshis # MINT_MAX_BALANCE=1000000 diff --git a/.gitignore b/.gitignore index 21e0d77..aaf7c6b 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,7 @@ tor.pid # MacOS .DS_Store +.aider* + +# PEM certs +*.pem \ No newline at end of file diff --git a/cashu/core/mint_info.py b/cashu/core/mint_info.py index 2dc040a..19efaf3 100644 --- a/cashu/core/mint_info.py +++ b/cashu/core/mint_info.py @@ -18,6 +18,7 @@ class MintInfo(BaseModel): contact: Optional[List[MintInfoContact]] motd: Optional[str] icon_url: Optional[str] + urls: Optional[List[str]] tos_url: Optional[str] time: Optional[int] nuts: Dict[int, Any] diff --git a/cashu/core/settings.py b/cashu/core/settings.py index 3dab28e..9add28a 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -203,6 +203,14 @@ class MintInformation(CashuSettings): mint_info_urls: List[str] = Field(default=None) mint_info_tos_url: str = Field(default=None) +class MintManagementRPCSettings(MintSettings): + mint_rpc_server_enable: bool = Field(default=False) + mint_rpc_server_ca: str = Field(default=None) + mint_rpc_server_cert: str = Field(default=None) + mint_rpc_server_key: str = Field(default=None) + mint_rpc_server_addr: str = Field(default="localhost") + mint_rpc_server_port: int = Field(default=8086) + mint_rpc_server_mutual_tls: bool = Field(default=True) class WalletSettings(CashuSettings): tor: bool = Field(default=False) @@ -320,6 +328,7 @@ class Settings( AuthSettings, MintRedisCache, MintDeprecationFlags, + MintManagementRPCSettings, MintWatchdogSettings, MintSettings, MintInformation, diff --git a/cashu/mint/app.py b/cashu/mint/app.py index b71b136..f4b6a0b 100644 --- a/cashu/mint/app.py +++ b/cashu/mint/app.py @@ -16,7 +16,13 @@ from ..core.settings import settings from .auth.router import auth_router from .router import redis, router from .router_deprecated import router_deprecated -from .startup import shutdown_mint, start_auth, start_mint +from .startup import ( + shutdown_management_rpc, + shutdown_mint, + start_auth, + start_management_rpc, + start_mint, +) if settings.debug_profiling: pass @@ -32,6 +38,8 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]: await start_mint() if settings.mint_require_auth: await start_auth() + if settings.mint_rpc_server_enable: + await start_management_rpc() try: yield except asyncio.CancelledError: @@ -39,10 +47,11 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]: logger.info("Shutdown process interrupted by CancelledError") finally: try: + await shutdown_management_rpc() await redis.disconnect() await shutdown_mint() except asyncio.CancelledError: - logger.info("CancelledError during shutdown, shutting down forcefully") + logger.error("CancelledError during shutdown, shutting down forcefully") def create_app(config_object="core.settings") -> FastAPI: diff --git a/cashu/mint/cache.py b/cashu/mint/cache.py index 023ce3e..ea72aaa 100644 --- a/cashu/mint/cache.py +++ b/cashu/mint/cache.py @@ -14,7 +14,6 @@ from ..core.settings import settings class RedisCache: initialized = False - expiry = settings.mint_redis_cache_ttl def __init__(self): if settings.mint_redis_cache_enabled: @@ -58,7 +57,7 @@ class RedisCache: else: raise Exception(f"Found no cached response for key {key}") result = await func(request, payload) - await self.redis.set(name=key, value=result.json(), ex=self.expiry) + await self.redis.set(name=key, value=result.json(), ex=settings.mint_redis_cache_ttl) return result return wrapper diff --git a/cashu/mint/db/write.py b/cashu/mint/db/write.py index fbbebae..eeb38fe 100644 --- a/cashu/mint/db/write.py +++ b/cashu/mint/db/write.py @@ -256,3 +256,23 @@ class DbWriteHelper: await self.events.submit(quote_copy) return quote_copy + + async def _update_mint_quote_state( + self, quote_id: str, state: MintQuoteState + ): + async with self.db.get_connection(lock_table="mint_quotes") as conn: + mint_quote = await self.crud.get_mint_quote(quote_id=quote_id, db=self.db, conn=conn) + if not mint_quote: + raise TransactionError("Mint quote not found.") + mint_quote.state = state + await self.crud.update_mint_quote(quote=mint_quote, db=self.db, conn=conn) + + async def _update_melt_quote_state( + self, quote_id: str, state: MeltQuoteState, + ): + async with self.db.get_connection(lock_table="melt_quotes") as conn: + melt_quote = await self.crud.get_melt_quote(quote_id=quote_id, db=self.db, conn=conn) + if not melt_quote: + raise TransactionError("Melt quote not found.") + melt_quote.state = state + await self.crud.update_melt_quote(quote=melt_quote, db=self.db, conn=conn) diff --git a/cashu/mint/features.py b/cashu/mint/features.py index 3a8ed65..aa85372 100644 --- a/cashu/mint/features.py +++ b/cashu/mint/features.py @@ -61,6 +61,7 @@ class LedgerFeatures(SupportsBackends, SupportsPubkey): contact=contact_info, nuts=self.mint_features, icon_url=settings.mint_info_icon_url, + urls=settings.mint_info_urls, tos_url=settings.mint_info_tos_url, motd=settings.mint_info_motd, time=None, diff --git a/cashu/mint/keysets.py b/cashu/mint/keysets.py index 4d1c52b..ee5f318 100644 --- a/cashu/mint/keysets.py +++ b/cashu/mint/keysets.py @@ -35,7 +35,10 @@ class LedgerKeysets(SupportsKeysets, SupportsSeed, SupportsDb): return derivation_path async def rotate_next_keyset( - self, unit: Unit, max_order: Optional[int], input_fee_ppk: Optional[int] + self, + unit: Unit, + max_order: Optional[int] = None, + input_fee_ppk: Optional[int] = None, ) -> MintKeyset: """ This function: @@ -93,6 +96,7 @@ class LedgerKeysets(SupportsKeysets, SupportsSeed, SupportsDb): seed=self.seed, amounts=amounts, input_fee_ppk=input_fee_ppk, + active=True, ) logger.debug(f"New keyset was generated with Id {new_keyset.id}. Saving...") diff --git a/cashu/mint/management_rpc/__init__.py b/cashu/mint/management_rpc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cashu/mint/management_rpc/cli/__init__.py b/cashu/mint/management_rpc/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cashu/mint/management_rpc/cli/cli.py b/cashu/mint/management_rpc/cli/cli.py new file mode 100644 index 0000000..fac3898 --- /dev/null +++ b/cashu/mint/management_rpc/cli/cli.py @@ -0,0 +1,328 @@ +import os +from typing import Optional + +import click +import grpc +from click import Context + +from cashu.mint.management_rpc.protos import management_pb2, management_pb2_grpc + + +class NaturalOrderGroup(click.Group): + """For listing commands in help in order of definition""" + + def list_commands(self, ctx): + return self.commands.keys() + +''' +# https://github.com/pallets/click/issues/85#issuecomment-503464628 +def coro(f): + @wraps(f) + def wrapper(*args, **kwargs): + return asyncio.run(f(*args, **kwargs)) + + return wrapper +''' + +@click.group(cls=NaturalOrderGroup) +@click.option( + "--host", + "-h", + default="localhost", + help="Mint address." +) +@click.option( + "--port", + "-p", + default=8086, + help="Mint gRPC port." +) +@click.option( + "--insecure", + "-i", + is_flag=True, + default=False, + help="Connect without mutual TLS." +) +@click.option( + "--ca-cert-path", + "-ca", + default="./ca_cert.pem", + help="path to the Certificate Authority (CA) certificate file." +) +@click.option( + "--client-key-path", + "-k", + default="./client_private.pem", + help="path to the client's TLS key file." +) +@click.option( + "--client-cert-path", + "-c", + default="./client_cert.pem", + help="path to the client's TLS certificate file." +) +@click.pass_context +def cli( + ctx: Context, + host: str, + port: int, + insecure: bool, + ca_cert_path: str, + client_key_path: str, + client_cert_path: str, +): + ctx.ensure_object(dict) + if not insecure: + # Verify the existence of the paths + for (what, path) in [("CA certificate", ca_cert_path), ("client key", client_key_path), ("client certificate", client_cert_path)]: + if not path or not os.path.exists(path): + click.echo(f"Error: Couldn't get {what}. The path '{path}' does not exist.", err=True) + ctx.exit(1) + + with open(client_key_path, "rb") as key_file, open(client_cert_path, "rb") as cert_file, open(ca_cert_path, "rb") as ca_file: + credentials = grpc.ssl_channel_credentials( + root_certificates=ca_file.read(), + private_key=key_file.read(), + certificate_chain=cert_file.read() + ) + + channel = grpc.secure_channel(f"{host}:{port}", credentials) + ctx.obj['STUB'] = management_pb2_grpc.MintStub(channel) + else: + channel = grpc.insecure_channel(f"{host}:{port}") + ctx.obj['STUB'] = management_pb2_grpc.MintStub(channel) + +@cli.command("get-info", help="Get Mint info") +@click.pass_context +def get_info(ctx: Context): + """Fetch server information""" + stub = ctx.obj['STUB'] + try: + response = stub.GetInfo(management_pb2.GetInfoRequest()) + click.echo(f"Mint Info:\n{response}") + except grpc.RpcError as e: + click.echo(f"Error: {e.details()}", err=True) + +@cli.group() +@click.pass_context +def update(ctx: Context): + """Update server information""" + pass + +@update.command("motd", help="Set the message of the day.") +@click.argument("motd") +@click.pass_context +def update_motd(ctx: Context, motd: str): + stub = ctx.obj['STUB'] + try: + stub.UpdateMotd(management_pb2.UpdateMotdRequest(motd=motd)) + click.echo("Motd successfully updated!") + except grpc.RpcError as e: + click.echo(f"Error: {e.details()}", err=True) + +@update.command("description", help="Update the short description.") +@click.argument("description") +@click.pass_context +def update_short_description(ctx: Context, description: str): + stub = ctx.obj['STUB'] + try: + stub.UpdateShortDescription(management_pb2.UpdateDescriptionRequest(description=description)) + click.echo("Short description successfully updated!") + except grpc.RpcError as e: + click.echo(f"Error: {e.details()}", err=True) + +@update.command("long-description", help="Update the long description.") +@click.argument("description") +@click.pass_context +def update_long_description(ctx: Context, description: str): + stub = ctx.obj['STUB'] + try: + stub.UpdateLongDescription(management_pb2.UpdateDescriptionRequest(description=description)) + click.echo("Long description successfully updated!") + except grpc.RpcError as e: + click.echo(f"Error: {e.details()}", err=True) + +@update.command("icon-url", help="Update the icon url.") +@click.argument("url") +@click.pass_context +def update_icon_url(ctx: Context, url: str): + stub = ctx.obj['STUB'] + try: + stub.UpdateLongDescription(management_pb2.UpdateIconUrlRequest(icon_url=url)) + click.echo("Icon url successfully updated!") + except grpc.RpcError as e: + click.echo(f"Error: {e.details()}", err=True) + +@update.command("name", help="Set the Mint's name.") +@click.argument("name") +@click.pass_context +def update_name(ctx: Context, name: str): + stub = ctx.obj['STUB'] + try: + stub.UpdateName(management_pb2.UpdateNameRequest(name=name)) + click.echo("Name successfully updated!") + except grpc.RpcError as e: + click.echo(f"Error: {e.details()}", err=True) + +@update.group() +@click.pass_context +def url(ctx: Context): + pass + +@url.command("add", help="Add a new URL for this Mint.") +@click.argument("url") +@click.pass_context +def add_mint_url(ctx: Context, url: str): + stub = ctx.obj['STUB'] + try: + stub.AddUrl(management_pb2.UpdateUrlRequest(url=url)) + click.echo("Url successfully added!") + except grpc.RpcError as e: + click.echo(f"Error: {e.details()}", err=True) + +@url.command("remove", help="Remove a URL of this Mint.") +@click.argument("url") +@click.pass_context +def remove_mint_url(ctx: Context, url: str): + stub = ctx.obj['STUB'] + try: + stub.RemoveUrl(management_pb2.UpdateUrlRequest(url=url)) + click.echo("Url successfully removed!") + except grpc.RpcError as e: + click.echo(f"Error: {e.details()}", err=True) + +@update.group() +@click.pass_context +def contact(context: Context): + pass + +@contact.command("add", help="Add contact information.") +@click.argument("method") +@click.argument("info") +@click.pass_context +def add_contact(ctx: Context, method: str, info: str): + stub = ctx.obj['STUB'] + try: + stub.AddContact(management_pb2.UpdateContactRequest(method=method, info=info)) + click.echo("Contact successfully added!") + except grpc.RpcError as e: + click.echo(f"Error: {e.details()}", err=True) + +@contact.command("remove", help="Remove contact information.") +@click.argument("method") +@click.pass_context +def remove_contact(ctx: Context, method: str): + stub = ctx.obj['STUB'] + try: + stub.RemoveContact(management_pb2.UpdateContactRequest(method=method, info="")) + click.echo("Contact successfully removed!") + except grpc.RpcError as e: + click.echo(f"Error: {e.details()}", err=True) + +@update.command("mint-quote", help="Set the state for a specific mint quote") +@click.argument("quote_id") +@click.argument("state") +@click.pass_context +def update_mint_quote(ctx: Context, quote_id: str, state: str): + allowed_states = ["PENDING", "UNPAID", "PAID", "ISSUED"] + if state not in allowed_states: + click.echo(f"state must be one of: {allowed_states}", err=True) + ctx.exit(1) + stub = ctx.obj['STUB'] + try: + stub.UpdateNut04Quote(management_pb2.UpdateQuoteRequest(quote_id=quote_id, state=state)) + click.echo("Successfully updated!") + except grpc.RpcError as e: + click.echo(f"Error: {e.details()}", err=True) + +@update.command("melt-quote", help="Set the state for a specific melt quote.") +@click.argument("quote_id") +@click.argument("state") +@click.pass_context +def update_melt_quote(ctx: Context, quote_id: str, state: str): + allowed_states = ["PENDING", "UNPAID", "PAID"] + if state not in allowed_states: + click.echo(f"State must be one of: {allowed_states}", err=True) + ctx.exit(1) + stub = ctx.obj['STUB'] + try: + stub.UpdateNut05Quote(management_pb2.UpdateQuoteRequest(quote_id=quote_id, state=state)) + click.echo("Successfully updated!") + except grpc.RpcError as e: + click.echo(f"Error: {e.details()}", err=True) + +@update.command("lightning-fee", help="Set new lightning fees.") +@click.argument("fee_percent", required=False, type=float) +@click.argument("min_fee_reserve", required=False, type=int) +@click.pass_context +def update_lightning_fee(ctx: Context, fee_percent: Optional[float], min_fee_reserve: Optional[int]): + stub = ctx.obj['STUB'] + try: + stub.UpdateLightningFee(management_pb2.UpdateLightningFeeRequest( + fee_percent=fee_percent, + fee_min_reserve=min_fee_reserve, + ) + ) + click.echo("Lightning fee successfully updated!") + except grpc.RpcError as e: + click.echo(f"Error: {e.details()}", err=True) + +@update.command("auth", help="Set the limits for auth requests") +@click.argument("rate_limit_per_minute", required=False, type=int) +@click.argument("max_tokens_per_request", required=False, type=int) +@click.pass_context +def update_auth_limits(ctx: Context, rate_limit_per_minute: Optional[int], max_tokens_per_request: Optional[int]): + stub = ctx.obj['STUB'] + try: + stub.UpdateAuthLimits( + management_pb2.UpdateAuthLimitsRequest( + auth_rate_limit_per_minute=rate_limit_per_minute, + auth_max_blind_tokens=max_tokens_per_request, + ) + ) + click.echo("Rate limit per minute successfully updated!") + except grpc.RpcError as e: + click.echo(f"Error: {e.details()}", err=True) + +@cli.group() +@click.pass_context +def get(ctx: Context): + """Get mint information""" + pass + +@get.command("mint-quote", help="Get a mint quote by id.") +@click.argument("quote_id") +@click.pass_context +def get_mint_quote(ctx: Context, quote_id: str): + stub = ctx.obj['STUB'] + try: + mint_quote = stub.GetNut04Quote(management_pb2.GetNut04QuoteRequest(quote_id=quote_id)) + click.echo(f"mint quote:\n{mint_quote}") + except grpc.RpcError as e: + click.echo(f"Error: {e.details()}", err=True) + +@get.command("melt-quote", help="Get a melt quote by id.") +@click.argument("quote_id") +@click.pass_context +def get_melt_quote(ctx: Context, quote_id: str): + stub = ctx.obj['STUB'] + try: + melt_quote = stub.GetNut05Quote(management_pb2.GetNut05QuoteRequest(quote_id=quote_id)) + click.echo(f"melt quote:\n{melt_quote}") + except grpc.RpcError as e: + click.echo(f"Error: {e.details()}", err=True) + + +@cli.command("next-keyset", help="Rotate to the next keyset for the specified unit.") +@click.argument("unit") +@click.argument("input_fee_ppk", required=False, type=int) +@click.argument("max_order", required=False, type=int) +@click.pass_context +def rotate_next_keyset(ctx: Context, unit: str, input_fee_ppk: Optional[int], max_order: Optional[int]): + stub = ctx.obj['STUB'] + try: + keyset = stub.RotateNextKeyset(management_pb2.RotateNextKeysetRequest(unit=unit, max_order=max_order, input_fee_ppk=input_fee_ppk)) + click.echo(f"New keyset successfully created:\n{keyset.id = }\n{keyset.unit = }\n{keyset.max_order = }\n{keyset.input_fee_ppk = }") + except grpc.RpcError as e: + click.echo(f"Error: {e.details()}", err=True) \ No newline at end of file diff --git a/cashu/mint/management_rpc/generate_certificates.sh b/cashu/mint/management_rpc/generate_certificates.sh new file mode 100755 index 0000000..7cff0f6 --- /dev/null +++ b/cashu/mint/management_rpc/generate_certificates.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +echo "*** WARNING: this script is only to be used for development/testing purposes! ***" +sleep 2 +echo -n "Continue? [Y/n]: " +read -r response +if [[ "$response" =~ ^[Yy]$ ]]; then + echo "Continuing..." +else + exit 1 +fi + +echo "Generating CA certificate..." +openssl genpkey -algorithm RSA -out ca_private.pem +openssl req -x509 -new -key ca_private.pem -sha256 -days 365 -out ca_cert.pem -subj "/CN=cashuCA" + +echo "Generating server certificate" +openssl genpkey -algorithm RSA -out server_private.pem +openssl req -new -key server_private.pem -out server.csr -subj "/CN=localhost" +openssl x509 -req -in server.csr -CA ca_cert.pem -CAkey ca_private.pem -CAcreateserial -out server_cert.pem -days 365 -sha256 + +echo "Generating client certificate" +openssl genpkey -algorithm RSA -out client_private.pem +openssl req -new -key client_private.pem -out client.csr -subj "/CN=client" +openssl x509 -req -in client.csr -CA ca_cert.pem -CAkey ca_private.pem -CAcreateserial -out client_cert.pem -days 365 -sha256 + +echo "Removing intermediate fiels..." +rm server.csr client.csr ca_cert.srl + +echo "All done!" diff --git a/cashu/mint/management_rpc/management_rpc.py b/cashu/mint/management_rpc/management_rpc.py new file mode 100644 index 0000000..95499e1 --- /dev/null +++ b/cashu/mint/management_rpc/management_rpc.py @@ -0,0 +1,216 @@ + +import os + +import grpc +from loguru import logger + +import cashu.mint.management_rpc.protos.management_pb2 as management_pb2 +import cashu.mint.management_rpc.protos.management_pb2_grpc as management_pb2_grpc +from cashu.core.base import ( + MeltQuoteState, + MintQuoteState, + Unit, +) +from cashu.core.settings import settings + +from ..ledger import Ledger + + +class MintManagementRPC(management_pb2_grpc.MintServicer): + + def __init__(self, ledger: Ledger): + self.ledger = ledger + super().__init__() + + def GetInfo(self, request, _): + logger.debug("gRPC GetInfo has been called") + mint_info_dict = self.ledger.mint_info.dict() + del mint_info_dict["nuts"] + response = management_pb2.GetInfoResponse(**mint_info_dict) + return response + + async def UpdateMotd(self, request, _): + logger.debug("gRPC UpdateMotd has been called") + settings.mint_info_motd = request.motd + return management_pb2.UpdateResponse() + + async def UpdateShortDescription(self, request, context): + logger.debug("gRPC UpdateShortDescription has been called") + settings.mint_info_description = request.description + return management_pb2.UpdateResponse() + + async def UpdateLongDescription(self, request, context): + logger.debug("gRPC UpdateLongDescription has been called") + settings.mint_info_description_long = request.description + return management_pb2.UpdateResponse() + + async def UpdateIconUrl(self, request, context): + logger.debug("gRPC UpdateIconUrl has been called") + settings.mint_info_icon_url = request.icon_url + return management_pb2.UpdateResponse() + + async def UpdateName(self, request, context): + logger.debug("gRPC UpdateName has been called") + settings.mint_info_name = request.name + return management_pb2.UpdateResponse() + + async def AddUrl(self, request, context): + logger.debug("gRPC AddUrl has been called") + if settings.mint_info_urls and request.url not in settings.mint_info_urls: + settings.mint_info_urls.append(request.url) + elif settings.mint_info_urls is None: + settings.mint_info_urls = [request.url] + else: + raise Exception("URL already in mint_info_urls") + return management_pb2.UpdateResponse() + + async def RemoveUrl(self, request, context): + logger.debug("gRPC RemoveUrl has been called") + if settings.mint_info_urls and request.url in settings.mint_info_urls: + settings.mint_info_urls.remove(request.url) + return management_pb2.UpdateResponse() + else: + raise Exception("No such URL in mint_info_urls") + + async def AddContact(self, request, context): + logger.debug("gRPC AddContact has been called") + for contact in settings.mint_info_contact: + if contact[0] == request.method: + raise Exception("Contact method already set") + settings.mint_info_contact.append([request.method, request.info]) + return management_pb2.UpdateResponse() + + async def RemoveContact(self, request, context): + logger.debug("gRPC RemoveContact has been called") + for i, contact in enumerate(settings.mint_info_contact): + if contact[0] == request.method: + del settings.mint_info_contact[i] + return management_pb2.UpdateResponse() + raise Exception("Contact method not found") + + async def UpdateNut04(self, request, context): + """Cannot implement this yet""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + async def UpdateNut05(self, request, context): + """Cannot implement this yet""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + async def UpdateQuoteTtl(self, request, context): + logger.debug("gRPC UpdateQuoteTtl has been called") + settings.mint_redis_cache_ttl = request.ttl + return management_pb2.UpdateResponse() + + async def GetNut04Quote(self, request, _): + logger.debug("gRPC GetNut04Quote has been called") + mint_quote = await self.ledger.get_mint_quote(request.quote_id) + mint_quote_dict = mint_quote.dict() + mint_quote_dict['state'] = str(mint_quote_dict['state']) + del mint_quote_dict['mint'] # unused + del mint_quote_dict['privkey'] # unused + return management_pb2.GetNut04QuoteResponse( + quote=management_pb2.Nut04Quote(**mint_quote_dict) + ) + + async def UpdateNut04Quote(self, request, _): + logger.debug("gRPC UpdateNut04Quote has been called") + state = MintQuoteState(request.state) + await self.ledger.db_write._update_mint_quote_state(request.quote_id, state) + return management_pb2.UpdateResponse() + + async def GetNut05Quote(self, request, _): + logger.debug("gRPC GetNut05Quote has been called") + melt_quote = await self.ledger.get_melt_quote(request.quote_id) + melt_quote_dict = melt_quote.dict() + melt_quote_dict['state'] = str(melt_quote_dict['state']) + del melt_quote_dict['mint'] + return management_pb2.GetNut05QuoteResponse( + quote=management_pb2.Nut05Quote(**melt_quote_dict) + ) + + async def UpdateNut05Quote(self, request, _): + logger.debug("gRPC UpdateNut05Quote has been called") + state = MeltQuoteState(request.state) + await self.ledger.db_write._update_melt_quote_state(request.quote_id, state) + return management_pb2.UpdateResponse() + + async def RotateNextKeyset(self, request, context): + logger.debug("gRPC RotateNextKeyset has been called") + # TODO: Fix this. Currently, we do not allow setting a max_order because + # it influences the keyset ID and -in turn- the Mint behaviour when activating keysets + # upon a restar (it will activate a new keyset with the standard max order) + if request.max_order: + logger.warning(f"Ignoring custom max_order of 2**{request.max_order}. This functionality is restricted.") + new_keyset = await self.ledger.rotate_next_keyset(Unit[request.unit], input_fee_ppk=request.input_fee_ppk) + return management_pb2.RotateNextKeysetResponse( + id=new_keyset.id, + unit=str(new_keyset.unit), + max_order=new_keyset.amounts[-1].bit_length(), # Neat trick to get log_2(last_amount) + 1 + input_fee_ppk=new_keyset.input_fee_ppk + ) + + async def UpdateLightningFee(self, request, _): + logger.debug("gRPC UpdateLightningFee has been called") + if request.fee_percent: + settings.lightning_fee_percent = request.fee_percent + elif request.fee_min_reserve: + settings.lightning_reserve_fee_min = request.fee_min_reserve + else: + raise Exception("No fee specified") + return management_pb2.UpdateResponse() + + async def UpdateAuthLimits(self, request, _): + logger.debug("gRPC UpdateAuthLimits has been called") + if request.auth_rate_limit_per_minute: + settings.mint_auth_rate_limit_per_minute = request.auth_rate_limit_per_minute + elif request.auth_max_blind_tokens: + settings.mint_auth_max_blind_tokens = request.auth_max_blind_tokens + else: + raise Exception("No auth limit was specified") + return management_pb2.UpdateResponse() + +async def serve(ledger: Ledger): + host = settings.mint_rpc_server_addr + port = settings.mint_rpc_server_port + server = grpc.aio.server() + management_pb2_grpc.add_MintServicer_to_server(MintManagementRPC(ledger=ledger), server) + + if settings.mint_rpc_server_mutual_tls: + # Verify the existence of the required paths + mint_rpc_key_path = settings.mint_rpc_server_key + mint_rpc_ca_path = settings.mint_rpc_server_ca + mint_rpc_cert_path = settings.mint_rpc_server_cert + + if not all(os.path.exists(path) if path else False for path in [mint_rpc_key_path, mint_rpc_ca_path, mint_rpc_cert_path]): + logger.error("One or more required files for mTLS are missing:") + if not mint_rpc_key_path or not os.path.exists(mint_rpc_key_path): + logger.error(f"Missing key file: {mint_rpc_key_path}") + if not mint_rpc_ca_path or not os.path.exists(mint_rpc_ca_path): + logger.error(f"Missing CA file: {mint_rpc_ca_path}") + if not mint_rpc_cert_path or not os.path.exists(mint_rpc_cert_path): + logger.error(f"Missing cert file: {mint_rpc_cert_path}") + raise FileNotFoundError("Required mTLS files are missing. Please check the paths.") + + logger.info(f"Starting mTLS Management RPC service on {host}:{port}") + # Load server credentials + server_credentials = grpc.ssl_server_credentials( + ((open(mint_rpc_key_path, 'rb').read(), open(mint_rpc_cert_path, 'rb').read()),), + root_certificates=open(mint_rpc_ca_path, 'rb').read(), + require_client_auth=True, + ) + server.add_secure_port(f"{host}:{port}", server_credentials) + else: + logger.info(f"Starting INSECURE Management RPC service on {host}:{port}") + server.add_insecure_port(f"{host}:{port}") + + await server.start() + return server + +async def shutdown(server: grpc.aio.Server): + logger.info("Shutting down management RPC gracefully...") + await server.stop(grace=2) # Give clients 2 seconds to finish requests + logger.debug("Management RPC shut down.") \ No newline at end of file diff --git a/cashu/mint/management_rpc/protos/__init__.py b/cashu/mint/management_rpc/protos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cashu/mint/management_rpc/protos/build.sh b/cashu/mint/management_rpc/protos/build.sh new file mode 100755 index 0000000..9c383ff --- /dev/null +++ b/cashu/mint/management_rpc/protos/build.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# *** RUN THIS FROM THE ROOT OF THE PROJECT *** + +BASE_DIR=./cashu/mint/management_rpc/protos + +# Check if the googleapis directory exists +if [ -d "$BASE_DIR/googleapis" ]; then + echo "$BASE_DIR/googleapis directory already exists. Skipping clone." +else + echo "Cloning googleapis..." + echo "If this doesn't work, clone it manually." + git clone https://github.com/googleapis/googleapis.git $BASE_DIR/googleapis +fi + +echo "Ensuring grpcio is installed..." +poetry add grpcio grpcio-tools + +echo "Compiling proto files..." +poetry run python3 -m grpc_tools.protoc --proto_path=$BASE_DIR/googleapis:$BASE_DIR --mypy_out=$BASE_DIR --python_out=$BASE_DIR --grpc_python_out=$BASE_DIR $BASE_DIR/management.proto + +echo "fixing imports on autogenerated files..." +for file in $BASE_DIR/*.{py,pyi}; do + if [ -f "$file" ]; then + sed -i -e 's/import management_pb2/import cashu.mint.management_rpc.protos.management_pb2/g' $file + fi +done + +echo "Finished!" \ No newline at end of file diff --git a/cashu/mint/management_rpc/protos/management.proto b/cashu/mint/management_rpc/protos/management.proto new file mode 100644 index 0000000..a571c9c --- /dev/null +++ b/cashu/mint/management_rpc/protos/management.proto @@ -0,0 +1,197 @@ +syntax = "proto3"; + +package management; + +service Mint { + rpc GetInfo(GetInfoRequest) returns (GetInfoResponse) {} + rpc UpdateMotd(UpdateMotdRequest) returns (UpdateResponse) {} + rpc UpdateShortDescription(UpdateDescriptionRequest) returns (UpdateResponse) {} + rpc UpdateLongDescription(UpdateDescriptionRequest) returns (UpdateResponse) {} + rpc UpdateIconUrl(UpdateIconUrlRequest) returns (UpdateResponse) {} + rpc UpdateName(UpdateNameRequest) returns (UpdateResponse) {} + rpc AddUrl(UpdateUrlRequest) returns (UpdateResponse) {} + rpc RemoveUrl(UpdateUrlRequest) returns (UpdateResponse) {} + rpc AddContact(UpdateContactRequest) returns (UpdateResponse) {} + rpc RemoveContact(UpdateContactRequest) returns (UpdateResponse) {} + rpc GetNut04Quote(GetNut04QuoteRequest) returns (GetNut04QuoteResponse) {} + rpc GetNut05Quote(GetNut05QuoteRequest) returns (GetNut05QuoteResponse) {} + rpc UpdateNut04(UpdateNut04Request) returns (UpdateResponse) {} + rpc UpdateNut05(UpdateNut05Request) returns (UpdateResponse) {} + rpc UpdateQuoteTtl(UpdateQuoteTtlRequest) returns (UpdateResponse) {} + rpc UpdateNut04Quote(UpdateQuoteRequest) returns (UpdateResponse) {} + rpc UpdateNut05Quote(UpdateQuoteRequest) returns (UpdateResponse) {} + rpc RotateNextKeyset(RotateNextKeysetRequest) returns (RotateNextKeysetResponse) {} + rpc UpdateLightningFee(UpdateLightningFeeRequest) returns (UpdateResponse) {} + rpc UpdateAuthLimits(UpdateAuthLimitsRequest) returns (UpdateResponse) {} +} + +message GetInfoRequest { +} + +message MintInfoContact { + string method = 1; + string info = 2; +} + +message GetInfoResponse { + optional string name = 1; + optional string pubkey = 2; + optional string version = 3; + optional string description = 4; + optional string description_long = 5; + repeated MintInfoContact contact = 6; + optional string motd = 7; + optional string icon_url = 8; + repeated string urls = 9; + optional int64 time = 10; + optional string tos_url = 11; +} + +message UpdateResponse{ +} + +message UpdateMotdRequest { + string motd = 1; +} + +message UpdateDescriptionRequest { + string description = 1; +} + + +message UpdateIconUrlRequest { + string icon_url = 1; +} + +message UpdateNameRequest { + string name = 1; +} + + +message UpdateUrlRequest { + string url = 1; +} + +message UpdateContactRequest { + string method = 1; + string info = 2; +} + +message UpdateNut04Request { + string unit = 1; + string method = 2; + optional bool disabled = 3; + optional uint64 min = 4; + optional uint64 max = 5; + optional bool description = 6; +} + + +message UpdateNut05Request { + string unit = 1; + string method = 2; + optional bool disabled = 3; + optional uint64 min = 4; + optional uint64 max = 5; +} + +message UpdateQuoteTtlRequest { + optional uint64 ttl = 1; +} + +message Nut04Quote { + string quote = 1; + string method = 2; + string request = 3; + string checking_id = 4; + string unit = 5; + uint64 amount = 6; + optional string state = 7; + optional int64 created_time = 8; + optional int64 paid_time = 9; + optional int64 expiry = 10; + optional string pubkey = 13; +} + +message BlindedMessage { + int32 amount = 1; + string id = 2; + string B_ = 3; + optional string witness = 4; +} + +message DLEQ { + string e = 1; + string s = 2; +} + +message BlindedSignature { + string id = 1; + int32 amount = 2; + string C_ = 3; + optional DLEQ dleq = 4; +} + +message Nut05Quote { + string quote = 1; + string method = 2; + string request = 3; + string checking_id = 4; + string unit = 5; + int32 amount = 6; + int32 fee_reserve = 7; + string state = 8; + optional int64 created_time = 9; + optional int64 paid_time = 10; + int32 fee_paid = 11; + optional string payment_preimage = 12; + optional int64 expiry = 13; + repeated BlindedMessage outputs = 14; + repeated BlindedSignature change = 15; +} + +message GetNut04QuoteRequest { + string quote_id = 1; +} + +message GetNut04QuoteResponse { + Nut04Quote quote = 1; +} + +message GetNut05QuoteRequest { + string quote_id = 1; +} + +message GetNut05QuoteResponse { + Nut05Quote quote = 1; +} + +message UpdateQuoteRequest { + string quote_id = 1; + string state = 2; +} + +message RotateNextKeysetRequest { + string unit = 1; + optional uint32 max_order = 2; + optional uint64 input_fee_ppk = 3; +} + + +message RotateNextKeysetResponse { + string id = 1; + string unit = 2; + uint32 max_order = 3; + uint64 input_fee_ppk = 4; +} + + +message UpdateLightningFeeRequest { + optional double fee_percent = 1; + optional uint64 fee_min_reserve = 2; +} + +message UpdateAuthLimitsRequest { + optional uint64 auth_rate_limit_per_minute = 1; + optional uint64 auth_max_blind_tokens = 2; +} \ No newline at end of file diff --git a/cashu/mint/management_rpc/protos/management_pb2.py b/cashu/mint/management_rpc/protos/management_pb2.py new file mode 100644 index 0000000..5790de5 --- /dev/null +++ b/cashu/mint/management_rpc/protos/management_pb2.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: management.proto +# Protobuf Python Version: 5.29.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 0, + '', + 'management.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10management.proto\x12\nmanagement\"\x10\n\x0eGetInfoRequest\"/\n\x0fMintInfoContact\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x0c\n\x04info\x18\x02 \x01(\t\"\x87\x03\n\x0fGetInfoResponse\x12\x11\n\x04name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x13\n\x06pubkey\x18\x02 \x01(\tH\x01\x88\x01\x01\x12\x14\n\x07version\x18\x03 \x01(\tH\x02\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x03\x88\x01\x01\x12\x1d\n\x10\x64\x65scription_long\x18\x05 \x01(\tH\x04\x88\x01\x01\x12,\n\x07\x63ontact\x18\x06 \x03(\x0b\x32\x1b.management.MintInfoContact\x12\x11\n\x04motd\x18\x07 \x01(\tH\x05\x88\x01\x01\x12\x15\n\x08icon_url\x18\x08 \x01(\tH\x06\x88\x01\x01\x12\x0c\n\x04urls\x18\t \x03(\t\x12\x11\n\x04time\x18\n \x01(\x03H\x07\x88\x01\x01\x12\x14\n\x07tos_url\x18\x0b \x01(\tH\x08\x88\x01\x01\x42\x07\n\x05_nameB\t\n\x07_pubkeyB\n\n\x08_versionB\x0e\n\x0c_descriptionB\x13\n\x11_description_longB\x07\n\x05_motdB\x0b\n\t_icon_urlB\x07\n\x05_timeB\n\n\x08_tos_url\"\x10\n\x0eUpdateResponse\"!\n\x11UpdateMotdRequest\x12\x0c\n\x04motd\x18\x01 \x01(\t\"/\n\x18UpdateDescriptionRequest\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\"(\n\x14UpdateIconUrlRequest\x12\x10\n\x08icon_url\x18\x01 \x01(\t\"!\n\x11UpdateNameRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x1f\n\x10UpdateUrlRequest\x12\x0b\n\x03url\x18\x01 \x01(\t\"4\n\x14UpdateContactRequest\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x0c\n\x04info\x18\x02 \x01(\t\"\xb4\x01\n\x12UpdateNut04Request\x12\x0c\n\x04unit\x18\x01 \x01(\t\x12\x0e\n\x06method\x18\x02 \x01(\t\x12\x15\n\x08\x64isabled\x18\x03 \x01(\x08H\x00\x88\x01\x01\x12\x10\n\x03min\x18\x04 \x01(\x04H\x01\x88\x01\x01\x12\x10\n\x03max\x18\x05 \x01(\x04H\x02\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x06 \x01(\x08H\x03\x88\x01\x01\x42\x0b\n\t_disabledB\x06\n\x04_minB\x06\n\x04_maxB\x0e\n\x0c_description\"\x8a\x01\n\x12UpdateNut05Request\x12\x0c\n\x04unit\x18\x01 \x01(\t\x12\x0e\n\x06method\x18\x02 \x01(\t\x12\x15\n\x08\x64isabled\x18\x03 \x01(\x08H\x00\x88\x01\x01\x12\x10\n\x03min\x18\x04 \x01(\x04H\x01\x88\x01\x01\x12\x10\n\x03max\x18\x05 \x01(\x04H\x02\x88\x01\x01\x42\x0b\n\t_disabledB\x06\n\x04_minB\x06\n\x04_max\"1\n\x15UpdateQuoteTtlRequest\x12\x10\n\x03ttl\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\x06\n\x04_ttl\"\x9f\x02\n\nNut04Quote\x12\r\n\x05quote\x18\x01 \x01(\t\x12\x0e\n\x06method\x18\x02 \x01(\t\x12\x0f\n\x07request\x18\x03 \x01(\t\x12\x13\n\x0b\x63hecking_id\x18\x04 \x01(\t\x12\x0c\n\x04unit\x18\x05 \x01(\t\x12\x0e\n\x06\x61mount\x18\x06 \x01(\x04\x12\x12\n\x05state\x18\x07 \x01(\tH\x00\x88\x01\x01\x12\x19\n\x0c\x63reated_time\x18\x08 \x01(\x03H\x01\x88\x01\x01\x12\x16\n\tpaid_time\x18\t \x01(\x03H\x02\x88\x01\x01\x12\x13\n\x06\x65xpiry\x18\n \x01(\x03H\x03\x88\x01\x01\x12\x13\n\x06pubkey\x18\r \x01(\tH\x04\x88\x01\x01\x42\x08\n\x06_stateB\x0f\n\r_created_timeB\x0c\n\n_paid_timeB\t\n\x07_expiryB\t\n\x07_pubkey\"Z\n\x0e\x42lindedMessage\x12\x0e\n\x06\x61mount\x18\x01 \x01(\x05\x12\n\n\x02id\x18\x02 \x01(\t\x12\n\n\x02\x42_\x18\x03 \x01(\t\x12\x14\n\x07witness\x18\x04 \x01(\tH\x00\x88\x01\x01\x42\n\n\x08_witness\"\x1c\n\x04\x44LEQ\x12\t\n\x01\x65\x18\x01 \x01(\t\x12\t\n\x01s\x18\x02 \x01(\t\"h\n\x10\x42lindedSignature\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\n\n\x02\x43_\x18\x03 \x01(\t\x12#\n\x04\x64leq\x18\x04 \x01(\x0b\x32\x10.management.DLEQH\x00\x88\x01\x01\x42\x07\n\x05_dleq\"\xa6\x03\n\nNut05Quote\x12\r\n\x05quote\x18\x01 \x01(\t\x12\x0e\n\x06method\x18\x02 \x01(\t\x12\x0f\n\x07request\x18\x03 \x01(\t\x12\x13\n\x0b\x63hecking_id\x18\x04 \x01(\t\x12\x0c\n\x04unit\x18\x05 \x01(\t\x12\x0e\n\x06\x61mount\x18\x06 \x01(\x05\x12\x13\n\x0b\x66\x65\x65_reserve\x18\x07 \x01(\x05\x12\r\n\x05state\x18\x08 \x01(\t\x12\x19\n\x0c\x63reated_time\x18\t \x01(\x03H\x00\x88\x01\x01\x12\x16\n\tpaid_time\x18\n \x01(\x03H\x01\x88\x01\x01\x12\x10\n\x08\x66\x65\x65_paid\x18\x0b \x01(\x05\x12\x1d\n\x10payment_preimage\x18\x0c \x01(\tH\x02\x88\x01\x01\x12\x13\n\x06\x65xpiry\x18\r \x01(\x03H\x03\x88\x01\x01\x12+\n\x07outputs\x18\x0e \x03(\x0b\x32\x1a.management.BlindedMessage\x12,\n\x06\x63hange\x18\x0f \x03(\x0b\x32\x1c.management.BlindedSignatureB\x0f\n\r_created_timeB\x0c\n\n_paid_timeB\x13\n\x11_payment_preimageB\t\n\x07_expiry\"(\n\x14GetNut04QuoteRequest\x12\x10\n\x08quote_id\x18\x01 \x01(\t\">\n\x15GetNut04QuoteResponse\x12%\n\x05quote\x18\x01 \x01(\x0b\x32\x16.management.Nut04Quote\"(\n\x14GetNut05QuoteRequest\x12\x10\n\x08quote_id\x18\x01 \x01(\t\">\n\x15GetNut05QuoteResponse\x12%\n\x05quote\x18\x01 \x01(\x0b\x32\x16.management.Nut05Quote\"5\n\x12UpdateQuoteRequest\x12\x10\n\x08quote_id\x18\x01 \x01(\t\x12\r\n\x05state\x18\x02 \x01(\t\"{\n\x17RotateNextKeysetRequest\x12\x0c\n\x04unit\x18\x01 \x01(\t\x12\x16\n\tmax_order\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x1a\n\rinput_fee_ppk\x18\x03 \x01(\x04H\x01\x88\x01\x01\x42\x0c\n\n_max_orderB\x10\n\x0e_input_fee_ppk\"^\n\x18RotateNextKeysetResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04unit\x18\x02 \x01(\t\x12\x11\n\tmax_order\x18\x03 \x01(\r\x12\x15\n\rinput_fee_ppk\x18\x04 \x01(\x04\"w\n\x19UpdateLightningFeeRequest\x12\x18\n\x0b\x66\x65\x65_percent\x18\x01 \x01(\x01H\x00\x88\x01\x01\x12\x1c\n\x0f\x66\x65\x65_min_reserve\x18\x02 \x01(\x04H\x01\x88\x01\x01\x42\x0e\n\x0c_fee_percentB\x12\n\x10_fee_min_reserve\"\x9f\x01\n\x17UpdateAuthLimitsRequest\x12\'\n\x1a\x61uth_rate_limit_per_minute\x18\x01 \x01(\x04H\x00\x88\x01\x01\x12\"\n\x15\x61uth_max_blind_tokens\x18\x02 \x01(\x04H\x01\x88\x01\x01\x42\x1d\n\x1b_auth_rate_limit_per_minuteB\x18\n\x16_auth_max_blind_tokens2\xf0\x0c\n\x04Mint\x12\x44\n\x07GetInfo\x12\x1a.management.GetInfoRequest\x1a\x1b.management.GetInfoResponse\"\x00\x12I\n\nUpdateMotd\x12\x1d.management.UpdateMotdRequest\x1a\x1a.management.UpdateResponse\"\x00\x12\\\n\x16UpdateShortDescription\x12$.management.UpdateDescriptionRequest\x1a\x1a.management.UpdateResponse\"\x00\x12[\n\x15UpdateLongDescription\x12$.management.UpdateDescriptionRequest\x1a\x1a.management.UpdateResponse\"\x00\x12O\n\rUpdateIconUrl\x12 .management.UpdateIconUrlRequest\x1a\x1a.management.UpdateResponse\"\x00\x12I\n\nUpdateName\x12\x1d.management.UpdateNameRequest\x1a\x1a.management.UpdateResponse\"\x00\x12\x44\n\x06\x41\x64\x64Url\x12\x1c.management.UpdateUrlRequest\x1a\x1a.management.UpdateResponse\"\x00\x12G\n\tRemoveUrl\x12\x1c.management.UpdateUrlRequest\x1a\x1a.management.UpdateResponse\"\x00\x12L\n\nAddContact\x12 .management.UpdateContactRequest\x1a\x1a.management.UpdateResponse\"\x00\x12O\n\rRemoveContact\x12 .management.UpdateContactRequest\x1a\x1a.management.UpdateResponse\"\x00\x12V\n\rGetNut04Quote\x12 .management.GetNut04QuoteRequest\x1a!.management.GetNut04QuoteResponse\"\x00\x12V\n\rGetNut05Quote\x12 .management.GetNut05QuoteRequest\x1a!.management.GetNut05QuoteResponse\"\x00\x12K\n\x0bUpdateNut04\x12\x1e.management.UpdateNut04Request\x1a\x1a.management.UpdateResponse\"\x00\x12K\n\x0bUpdateNut05\x12\x1e.management.UpdateNut05Request\x1a\x1a.management.UpdateResponse\"\x00\x12Q\n\x0eUpdateQuoteTtl\x12!.management.UpdateQuoteTtlRequest\x1a\x1a.management.UpdateResponse\"\x00\x12P\n\x10UpdateNut04Quote\x12\x1e.management.UpdateQuoteRequest\x1a\x1a.management.UpdateResponse\"\x00\x12P\n\x10UpdateNut05Quote\x12\x1e.management.UpdateQuoteRequest\x1a\x1a.management.UpdateResponse\"\x00\x12_\n\x10RotateNextKeyset\x12#.management.RotateNextKeysetRequest\x1a$.management.RotateNextKeysetResponse\"\x00\x12Y\n\x12UpdateLightningFee\x12%.management.UpdateLightningFeeRequest\x1a\x1a.management.UpdateResponse\"\x00\x12U\n\x10UpdateAuthLimits\x12#.management.UpdateAuthLimitsRequest\x1a\x1a.management.UpdateResponse\"\x00\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'management_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_GETINFOREQUEST']._serialized_start=32 + _globals['_GETINFOREQUEST']._serialized_end=48 + _globals['_MINTINFOCONTACT']._serialized_start=50 + _globals['_MINTINFOCONTACT']._serialized_end=97 + _globals['_GETINFORESPONSE']._serialized_start=100 + _globals['_GETINFORESPONSE']._serialized_end=491 + _globals['_UPDATERESPONSE']._serialized_start=493 + _globals['_UPDATERESPONSE']._serialized_end=509 + _globals['_UPDATEMOTDREQUEST']._serialized_start=511 + _globals['_UPDATEMOTDREQUEST']._serialized_end=544 + _globals['_UPDATEDESCRIPTIONREQUEST']._serialized_start=546 + _globals['_UPDATEDESCRIPTIONREQUEST']._serialized_end=593 + _globals['_UPDATEICONURLREQUEST']._serialized_start=595 + _globals['_UPDATEICONURLREQUEST']._serialized_end=635 + _globals['_UPDATENAMEREQUEST']._serialized_start=637 + _globals['_UPDATENAMEREQUEST']._serialized_end=670 + _globals['_UPDATEURLREQUEST']._serialized_start=672 + _globals['_UPDATEURLREQUEST']._serialized_end=703 + _globals['_UPDATECONTACTREQUEST']._serialized_start=705 + _globals['_UPDATECONTACTREQUEST']._serialized_end=757 + _globals['_UPDATENUT04REQUEST']._serialized_start=760 + _globals['_UPDATENUT04REQUEST']._serialized_end=940 + _globals['_UPDATENUT05REQUEST']._serialized_start=943 + _globals['_UPDATENUT05REQUEST']._serialized_end=1081 + _globals['_UPDATEQUOTETTLREQUEST']._serialized_start=1083 + _globals['_UPDATEQUOTETTLREQUEST']._serialized_end=1132 + _globals['_NUT04QUOTE']._serialized_start=1135 + _globals['_NUT04QUOTE']._serialized_end=1422 + _globals['_BLINDEDMESSAGE']._serialized_start=1424 + _globals['_BLINDEDMESSAGE']._serialized_end=1514 + _globals['_DLEQ']._serialized_start=1516 + _globals['_DLEQ']._serialized_end=1544 + _globals['_BLINDEDSIGNATURE']._serialized_start=1546 + _globals['_BLINDEDSIGNATURE']._serialized_end=1650 + _globals['_NUT05QUOTE']._serialized_start=1653 + _globals['_NUT05QUOTE']._serialized_end=2075 + _globals['_GETNUT04QUOTEREQUEST']._serialized_start=2077 + _globals['_GETNUT04QUOTEREQUEST']._serialized_end=2117 + _globals['_GETNUT04QUOTERESPONSE']._serialized_start=2119 + _globals['_GETNUT04QUOTERESPONSE']._serialized_end=2181 + _globals['_GETNUT05QUOTEREQUEST']._serialized_start=2183 + _globals['_GETNUT05QUOTEREQUEST']._serialized_end=2223 + _globals['_GETNUT05QUOTERESPONSE']._serialized_start=2225 + _globals['_GETNUT05QUOTERESPONSE']._serialized_end=2287 + _globals['_UPDATEQUOTEREQUEST']._serialized_start=2289 + _globals['_UPDATEQUOTEREQUEST']._serialized_end=2342 + _globals['_ROTATENEXTKEYSETREQUEST']._serialized_start=2344 + _globals['_ROTATENEXTKEYSETREQUEST']._serialized_end=2467 + _globals['_ROTATENEXTKEYSETRESPONSE']._serialized_start=2469 + _globals['_ROTATENEXTKEYSETRESPONSE']._serialized_end=2563 + _globals['_UPDATELIGHTNINGFEEREQUEST']._serialized_start=2565 + _globals['_UPDATELIGHTNINGFEEREQUEST']._serialized_end=2684 + _globals['_UPDATEAUTHLIMITSREQUEST']._serialized_start=2687 + _globals['_UPDATEAUTHLIMITSREQUEST']._serialized_end=2846 + _globals['_MINT']._serialized_start=2849 + _globals['_MINT']._serialized_end=4497 +# @@protoc_insertion_point(module_scope) diff --git a/cashu/mint/management_rpc/protos/management_pb2.pyi b/cashu/mint/management_rpc/protos/management_pb2.pyi new file mode 100644 index 0000000..2b40553 --- /dev/null +++ b/cashu/mint/management_rpc/protos/management_pb2.pyi @@ -0,0 +1,674 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins +import collections.abc +import typing + +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +@typing.final +class GetInfoRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___GetInfoRequest = GetInfoRequest + +@typing.final +class MintInfoContact(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + METHOD_FIELD_NUMBER: builtins.int + INFO_FIELD_NUMBER: builtins.int + method: builtins.str + info: builtins.str + def __init__( + self, + *, + method: builtins.str = ..., + info: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["info", b"info", "method", b"method"]) -> None: ... + +global___MintInfoContact = MintInfoContact + +@typing.final +class GetInfoResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + PUBKEY_FIELD_NUMBER: builtins.int + VERSION_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + DESCRIPTION_LONG_FIELD_NUMBER: builtins.int + CONTACT_FIELD_NUMBER: builtins.int + MOTD_FIELD_NUMBER: builtins.int + ICON_URL_FIELD_NUMBER: builtins.int + URLS_FIELD_NUMBER: builtins.int + TIME_FIELD_NUMBER: builtins.int + TOS_URL_FIELD_NUMBER: builtins.int + name: builtins.str + pubkey: builtins.str + version: builtins.str + description: builtins.str + description_long: builtins.str + motd: builtins.str + icon_url: builtins.str + time: builtins.int + tos_url: builtins.str + @property + def contact(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MintInfoContact]: ... + @property + def urls(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ... + def __init__( + self, + *, + name: builtins.str | None = ..., + pubkey: builtins.str | None = ..., + version: builtins.str | None = ..., + description: builtins.str | None = ..., + description_long: builtins.str | None = ..., + contact: collections.abc.Iterable[global___MintInfoContact] | None = ..., + motd: builtins.str | None = ..., + icon_url: builtins.str | None = ..., + urls: collections.abc.Iterable[builtins.str] | None = ..., + time: builtins.int | None = ..., + tos_url: builtins.str | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "_description_long", b"_description_long", "_icon_url", b"_icon_url", "_motd", b"_motd", "_name", b"_name", "_pubkey", b"_pubkey", "_time", b"_time", "_tos_url", b"_tos_url", "_version", b"_version", "description", b"description", "description_long", b"description_long", "icon_url", b"icon_url", "motd", b"motd", "name", b"name", "pubkey", b"pubkey", "time", b"time", "tos_url", b"tos_url", "version", b"version"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "_description_long", b"_description_long", "_icon_url", b"_icon_url", "_motd", b"_motd", "_name", b"_name", "_pubkey", b"_pubkey", "_time", b"_time", "_tos_url", b"_tos_url", "_version", b"_version", "contact", b"contact", "description", b"description", "description_long", b"description_long", "icon_url", b"icon_url", "motd", b"motd", "name", b"name", "pubkey", b"pubkey", "time", b"time", "tos_url", b"tos_url", "urls", b"urls", "version", b"version"]) -> None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_description_long", b"_description_long"]) -> typing.Literal["description_long"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_icon_url", b"_icon_url"]) -> typing.Literal["icon_url"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_motd", b"_motd"]) -> typing.Literal["motd"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_name", b"_name"]) -> typing.Literal["name"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_pubkey", b"_pubkey"]) -> typing.Literal["pubkey"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_time", b"_time"]) -> typing.Literal["time"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_tos_url", b"_tos_url"]) -> typing.Literal["tos_url"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_version", b"_version"]) -> typing.Literal["version"] | None: ... + +global___GetInfoResponse = GetInfoResponse + +@typing.final +class UpdateResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___UpdateResponse = UpdateResponse + +@typing.final +class UpdateMotdRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + MOTD_FIELD_NUMBER: builtins.int + motd: builtins.str + def __init__( + self, + *, + motd: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["motd", b"motd"]) -> None: ... + +global___UpdateMotdRequest = UpdateMotdRequest + +@typing.final +class UpdateDescriptionRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + DESCRIPTION_FIELD_NUMBER: builtins.int + description: builtins.str + def __init__( + self, + *, + description: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["description", b"description"]) -> None: ... + +global___UpdateDescriptionRequest = UpdateDescriptionRequest + +@typing.final +class UpdateIconUrlRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ICON_URL_FIELD_NUMBER: builtins.int + icon_url: builtins.str + def __init__( + self, + *, + icon_url: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["icon_url", b"icon_url"]) -> None: ... + +global___UpdateIconUrlRequest = UpdateIconUrlRequest + +@typing.final +class UpdateNameRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + name: builtins.str + def __init__( + self, + *, + name: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["name", b"name"]) -> None: ... + +global___UpdateNameRequest = UpdateNameRequest + +@typing.final +class UpdateUrlRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + URL_FIELD_NUMBER: builtins.int + url: builtins.str + def __init__( + self, + *, + url: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["url", b"url"]) -> None: ... + +global___UpdateUrlRequest = UpdateUrlRequest + +@typing.final +class UpdateContactRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + METHOD_FIELD_NUMBER: builtins.int + INFO_FIELD_NUMBER: builtins.int + method: builtins.str + info: builtins.str + def __init__( + self, + *, + method: builtins.str = ..., + info: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["info", b"info", "method", b"method"]) -> None: ... + +global___UpdateContactRequest = UpdateContactRequest + +@typing.final +class UpdateNut04Request(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + UNIT_FIELD_NUMBER: builtins.int + METHOD_FIELD_NUMBER: builtins.int + DISABLED_FIELD_NUMBER: builtins.int + MIN_FIELD_NUMBER: builtins.int + MAX_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + unit: builtins.str + method: builtins.str + disabled: builtins.bool + min: builtins.int + max: builtins.int + description: builtins.bool + def __init__( + self, + *, + unit: builtins.str = ..., + method: builtins.str = ..., + disabled: builtins.bool | None = ..., + min: builtins.int | None = ..., + max: builtins.int | None = ..., + description: builtins.bool | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_description", b"_description", "_disabled", b"_disabled", "_max", b"_max", "_min", b"_min", "description", b"description", "disabled", b"disabled", "max", b"max", "min", b"min"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_description", b"_description", "_disabled", b"_disabled", "_max", b"_max", "_min", b"_min", "description", b"description", "disabled", b"disabled", "max", b"max", "method", b"method", "min", b"min", "unit", b"unit"]) -> None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_description", b"_description"]) -> typing.Literal["description"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_disabled", b"_disabled"]) -> typing.Literal["disabled"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_max", b"_max"]) -> typing.Literal["max"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_min", b"_min"]) -> typing.Literal["min"] | None: ... + +global___UpdateNut04Request = UpdateNut04Request + +@typing.final +class UpdateNut05Request(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + UNIT_FIELD_NUMBER: builtins.int + METHOD_FIELD_NUMBER: builtins.int + DISABLED_FIELD_NUMBER: builtins.int + MIN_FIELD_NUMBER: builtins.int + MAX_FIELD_NUMBER: builtins.int + unit: builtins.str + method: builtins.str + disabled: builtins.bool + min: builtins.int + max: builtins.int + def __init__( + self, + *, + unit: builtins.str = ..., + method: builtins.str = ..., + disabled: builtins.bool | None = ..., + min: builtins.int | None = ..., + max: builtins.int | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_disabled", b"_disabled", "_max", b"_max", "_min", b"_min", "disabled", b"disabled", "max", b"max", "min", b"min"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_disabled", b"_disabled", "_max", b"_max", "_min", b"_min", "disabled", b"disabled", "max", b"max", "method", b"method", "min", b"min", "unit", b"unit"]) -> None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_disabled", b"_disabled"]) -> typing.Literal["disabled"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_max", b"_max"]) -> typing.Literal["max"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_min", b"_min"]) -> typing.Literal["min"] | None: ... + +global___UpdateNut05Request = UpdateNut05Request + +@typing.final +class UpdateQuoteTtlRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TTL_FIELD_NUMBER: builtins.int + ttl: builtins.int + def __init__( + self, + *, + ttl: builtins.int | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_ttl", b"_ttl", "ttl", b"ttl"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_ttl", b"_ttl", "ttl", b"ttl"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_ttl", b"_ttl"]) -> typing.Literal["ttl"] | None: ... + +global___UpdateQuoteTtlRequest = UpdateQuoteTtlRequest + +@typing.final +class Nut04Quote(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + QUOTE_FIELD_NUMBER: builtins.int + METHOD_FIELD_NUMBER: builtins.int + REQUEST_FIELD_NUMBER: builtins.int + CHECKING_ID_FIELD_NUMBER: builtins.int + UNIT_FIELD_NUMBER: builtins.int + AMOUNT_FIELD_NUMBER: builtins.int + STATE_FIELD_NUMBER: builtins.int + CREATED_TIME_FIELD_NUMBER: builtins.int + PAID_TIME_FIELD_NUMBER: builtins.int + EXPIRY_FIELD_NUMBER: builtins.int + PUBKEY_FIELD_NUMBER: builtins.int + quote: builtins.str + method: builtins.str + request: builtins.str + checking_id: builtins.str + unit: builtins.str + amount: builtins.int + state: builtins.str + created_time: builtins.int + paid_time: builtins.int + expiry: builtins.int + pubkey: builtins.str + def __init__( + self, + *, + quote: builtins.str = ..., + method: builtins.str = ..., + request: builtins.str = ..., + checking_id: builtins.str = ..., + unit: builtins.str = ..., + amount: builtins.int = ..., + state: builtins.str | None = ..., + created_time: builtins.int | None = ..., + paid_time: builtins.int | None = ..., + expiry: builtins.int | None = ..., + pubkey: builtins.str | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_created_time", b"_created_time", "_expiry", b"_expiry", "_paid_time", b"_paid_time", "_pubkey", b"_pubkey", "_state", b"_state", "created_time", b"created_time", "expiry", b"expiry", "paid_time", b"paid_time", "pubkey", b"pubkey", "state", b"state"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_created_time", b"_created_time", "_expiry", b"_expiry", "_paid_time", b"_paid_time", "_pubkey", b"_pubkey", "_state", b"_state", "amount", b"amount", "checking_id", b"checking_id", "created_time", b"created_time", "expiry", b"expiry", "method", b"method", "paid_time", b"paid_time", "pubkey", b"pubkey", "quote", b"quote", "request", b"request", "state", b"state", "unit", b"unit"]) -> None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_created_time", b"_created_time"]) -> typing.Literal["created_time"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_expiry", b"_expiry"]) -> typing.Literal["expiry"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_paid_time", b"_paid_time"]) -> typing.Literal["paid_time"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_pubkey", b"_pubkey"]) -> typing.Literal["pubkey"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_state", b"_state"]) -> typing.Literal["state"] | None: ... + +global___Nut04Quote = Nut04Quote + +@typing.final +class BlindedMessage(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + AMOUNT_FIELD_NUMBER: builtins.int + ID_FIELD_NUMBER: builtins.int + B__FIELD_NUMBER: builtins.int + WITNESS_FIELD_NUMBER: builtins.int + amount: builtins.int + id: builtins.str + B_: builtins.str + witness: builtins.str + def __init__( + self, + *, + amount: builtins.int = ..., + id: builtins.str = ..., + B_: builtins.str = ..., + witness: builtins.str | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_witness", b"_witness", "witness", b"witness"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["B_", b"B_", "_witness", b"_witness", "amount", b"amount", "id", b"id", "witness", b"witness"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_witness", b"_witness"]) -> typing.Literal["witness"] | None: ... + +global___BlindedMessage = BlindedMessage + +@typing.final +class DLEQ(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + E_FIELD_NUMBER: builtins.int + S_FIELD_NUMBER: builtins.int + e: builtins.str + s: builtins.str + def __init__( + self, + *, + e: builtins.str = ..., + s: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["e", b"e", "s", b"s"]) -> None: ... + +global___DLEQ = DLEQ + +@typing.final +class BlindedSignature(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ID_FIELD_NUMBER: builtins.int + AMOUNT_FIELD_NUMBER: builtins.int + C__FIELD_NUMBER: builtins.int + DLEQ_FIELD_NUMBER: builtins.int + id: builtins.str + amount: builtins.int + C_: builtins.str + @property + def dleq(self) -> global___DLEQ: ... + def __init__( + self, + *, + id: builtins.str = ..., + amount: builtins.int = ..., + C_: builtins.str = ..., + dleq: global___DLEQ | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_dleq", b"_dleq", "dleq", b"dleq"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["C_", b"C_", "_dleq", b"_dleq", "amount", b"amount", "dleq", b"dleq", "id", b"id"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_dleq", b"_dleq"]) -> typing.Literal["dleq"] | None: ... + +global___BlindedSignature = BlindedSignature + +@typing.final +class Nut05Quote(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + QUOTE_FIELD_NUMBER: builtins.int + METHOD_FIELD_NUMBER: builtins.int + REQUEST_FIELD_NUMBER: builtins.int + CHECKING_ID_FIELD_NUMBER: builtins.int + UNIT_FIELD_NUMBER: builtins.int + AMOUNT_FIELD_NUMBER: builtins.int + FEE_RESERVE_FIELD_NUMBER: builtins.int + STATE_FIELD_NUMBER: builtins.int + CREATED_TIME_FIELD_NUMBER: builtins.int + PAID_TIME_FIELD_NUMBER: builtins.int + FEE_PAID_FIELD_NUMBER: builtins.int + PAYMENT_PREIMAGE_FIELD_NUMBER: builtins.int + EXPIRY_FIELD_NUMBER: builtins.int + OUTPUTS_FIELD_NUMBER: builtins.int + CHANGE_FIELD_NUMBER: builtins.int + quote: builtins.str + method: builtins.str + request: builtins.str + checking_id: builtins.str + unit: builtins.str + amount: builtins.int + fee_reserve: builtins.int + state: builtins.str + created_time: builtins.int + paid_time: builtins.int + fee_paid: builtins.int + payment_preimage: builtins.str + expiry: builtins.int + @property + def outputs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___BlindedMessage]: ... + @property + def change(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___BlindedSignature]: ... + def __init__( + self, + *, + quote: builtins.str = ..., + method: builtins.str = ..., + request: builtins.str = ..., + checking_id: builtins.str = ..., + unit: builtins.str = ..., + amount: builtins.int = ..., + fee_reserve: builtins.int = ..., + state: builtins.str = ..., + created_time: builtins.int | None = ..., + paid_time: builtins.int | None = ..., + fee_paid: builtins.int = ..., + payment_preimage: builtins.str | None = ..., + expiry: builtins.int | None = ..., + outputs: collections.abc.Iterable[global___BlindedMessage] | None = ..., + change: collections.abc.Iterable[global___BlindedSignature] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_created_time", b"_created_time", "_expiry", b"_expiry", "_paid_time", b"_paid_time", "_payment_preimage", b"_payment_preimage", "created_time", b"created_time", "expiry", b"expiry", "paid_time", b"paid_time", "payment_preimage", b"payment_preimage"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_created_time", b"_created_time", "_expiry", b"_expiry", "_paid_time", b"_paid_time", "_payment_preimage", b"_payment_preimage", "amount", b"amount", "change", b"change", "checking_id", b"checking_id", "created_time", b"created_time", "expiry", b"expiry", "fee_paid", b"fee_paid", "fee_reserve", b"fee_reserve", "method", b"method", "outputs", b"outputs", "paid_time", b"paid_time", "payment_preimage", b"payment_preimage", "quote", b"quote", "request", b"request", "state", b"state", "unit", b"unit"]) -> None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_created_time", b"_created_time"]) -> typing.Literal["created_time"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_expiry", b"_expiry"]) -> typing.Literal["expiry"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_paid_time", b"_paid_time"]) -> typing.Literal["paid_time"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_payment_preimage", b"_payment_preimage"]) -> typing.Literal["payment_preimage"] | None: ... + +global___Nut05Quote = Nut05Quote + +@typing.final +class GetNut04QuoteRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + QUOTE_ID_FIELD_NUMBER: builtins.int + quote_id: builtins.str + def __init__( + self, + *, + quote_id: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["quote_id", b"quote_id"]) -> None: ... + +global___GetNut04QuoteRequest = GetNut04QuoteRequest + +@typing.final +class GetNut04QuoteResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + QUOTE_FIELD_NUMBER: builtins.int + @property + def quote(self) -> global___Nut04Quote: ... + def __init__( + self, + *, + quote: global___Nut04Quote | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["quote", b"quote"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["quote", b"quote"]) -> None: ... + +global___GetNut04QuoteResponse = GetNut04QuoteResponse + +@typing.final +class GetNut05QuoteRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + QUOTE_ID_FIELD_NUMBER: builtins.int + quote_id: builtins.str + def __init__( + self, + *, + quote_id: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["quote_id", b"quote_id"]) -> None: ... + +global___GetNut05QuoteRequest = GetNut05QuoteRequest + +@typing.final +class GetNut05QuoteResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + QUOTE_FIELD_NUMBER: builtins.int + @property + def quote(self) -> global___Nut05Quote: ... + def __init__( + self, + *, + quote: global___Nut05Quote | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["quote", b"quote"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["quote", b"quote"]) -> None: ... + +global___GetNut05QuoteResponse = GetNut05QuoteResponse + +@typing.final +class UpdateQuoteRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + QUOTE_ID_FIELD_NUMBER: builtins.int + STATE_FIELD_NUMBER: builtins.int + quote_id: builtins.str + state: builtins.str + def __init__( + self, + *, + quote_id: builtins.str = ..., + state: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["quote_id", b"quote_id", "state", b"state"]) -> None: ... + +global___UpdateQuoteRequest = UpdateQuoteRequest + +@typing.final +class RotateNextKeysetRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + UNIT_FIELD_NUMBER: builtins.int + MAX_ORDER_FIELD_NUMBER: builtins.int + INPUT_FEE_PPK_FIELD_NUMBER: builtins.int + unit: builtins.str + max_order: builtins.int + input_fee_ppk: builtins.int + def __init__( + self, + *, + unit: builtins.str = ..., + max_order: builtins.int | None = ..., + input_fee_ppk: builtins.int | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_input_fee_ppk", b"_input_fee_ppk", "_max_order", b"_max_order", "input_fee_ppk", b"input_fee_ppk", "max_order", b"max_order"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_input_fee_ppk", b"_input_fee_ppk", "_max_order", b"_max_order", "input_fee_ppk", b"input_fee_ppk", "max_order", b"max_order", "unit", b"unit"]) -> None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_input_fee_ppk", b"_input_fee_ppk"]) -> typing.Literal["input_fee_ppk"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_max_order", b"_max_order"]) -> typing.Literal["max_order"] | None: ... + +global___RotateNextKeysetRequest = RotateNextKeysetRequest + +@typing.final +class RotateNextKeysetResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ID_FIELD_NUMBER: builtins.int + UNIT_FIELD_NUMBER: builtins.int + MAX_ORDER_FIELD_NUMBER: builtins.int + INPUT_FEE_PPK_FIELD_NUMBER: builtins.int + id: builtins.str + unit: builtins.str + max_order: builtins.int + input_fee_ppk: builtins.int + def __init__( + self, + *, + id: builtins.str = ..., + unit: builtins.str = ..., + max_order: builtins.int = ..., + input_fee_ppk: builtins.int = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["id", b"id", "input_fee_ppk", b"input_fee_ppk", "max_order", b"max_order", "unit", b"unit"]) -> None: ... + +global___RotateNextKeysetResponse = RotateNextKeysetResponse + +@typing.final +class UpdateLightningFeeRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + FEE_PERCENT_FIELD_NUMBER: builtins.int + FEE_MIN_RESERVE_FIELD_NUMBER: builtins.int + fee_percent: builtins.float + fee_min_reserve: builtins.int + def __init__( + self, + *, + fee_percent: builtins.float | None = ..., + fee_min_reserve: builtins.int | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_fee_min_reserve", b"_fee_min_reserve", "_fee_percent", b"_fee_percent", "fee_min_reserve", b"fee_min_reserve", "fee_percent", b"fee_percent"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_fee_min_reserve", b"_fee_min_reserve", "_fee_percent", b"_fee_percent", "fee_min_reserve", b"fee_min_reserve", "fee_percent", b"fee_percent"]) -> None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_fee_min_reserve", b"_fee_min_reserve"]) -> typing.Literal["fee_min_reserve"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_fee_percent", b"_fee_percent"]) -> typing.Literal["fee_percent"] | None: ... + +global___UpdateLightningFeeRequest = UpdateLightningFeeRequest + +@typing.final +class UpdateAuthLimitsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + AUTH_RATE_LIMIT_PER_MINUTE_FIELD_NUMBER: builtins.int + AUTH_MAX_BLIND_TOKENS_FIELD_NUMBER: builtins.int + auth_rate_limit_per_minute: builtins.int + auth_max_blind_tokens: builtins.int + def __init__( + self, + *, + auth_rate_limit_per_minute: builtins.int | None = ..., + auth_max_blind_tokens: builtins.int | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_auth_max_blind_tokens", b"_auth_max_blind_tokens", "_auth_rate_limit_per_minute", b"_auth_rate_limit_per_minute", "auth_max_blind_tokens", b"auth_max_blind_tokens", "auth_rate_limit_per_minute", b"auth_rate_limit_per_minute"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_auth_max_blind_tokens", b"_auth_max_blind_tokens", "_auth_rate_limit_per_minute", b"_auth_rate_limit_per_minute", "auth_max_blind_tokens", b"auth_max_blind_tokens", "auth_rate_limit_per_minute", b"auth_rate_limit_per_minute"]) -> None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_auth_max_blind_tokens", b"_auth_max_blind_tokens"]) -> typing.Literal["auth_max_blind_tokens"] | None: ... + @typing.overload + def WhichOneof(self, oneof_group: typing.Literal["_auth_rate_limit_per_minute", b"_auth_rate_limit_per_minute"]) -> typing.Literal["auth_rate_limit_per_minute"] | None: ... + +global___UpdateAuthLimitsRequest = UpdateAuthLimitsRequest diff --git a/cashu/mint/management_rpc/protos/management_pb2_grpc.py b/cashu/mint/management_rpc/protos/management_pb2_grpc.py new file mode 100644 index 0000000..6f7bcc0 --- /dev/null +++ b/cashu/mint/management_rpc/protos/management_pb2_grpc.py @@ -0,0 +1,914 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" + +import grpc + +import cashu.mint.management_rpc.protos.management_pb2 as management__pb2 + +GRPC_GENERATED_VERSION = '1.69.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + ' but the generated code in management_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class MintStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.GetInfo = channel.unary_unary( + '/management.Mint/GetInfo', + request_serializer=management__pb2.GetInfoRequest.SerializeToString, + response_deserializer=management__pb2.GetInfoResponse.FromString, + _registered_method=True) + self.UpdateMotd = channel.unary_unary( + '/management.Mint/UpdateMotd', + request_serializer=management__pb2.UpdateMotdRequest.SerializeToString, + response_deserializer=management__pb2.UpdateResponse.FromString, + _registered_method=True) + self.UpdateShortDescription = channel.unary_unary( + '/management.Mint/UpdateShortDescription', + request_serializer=management__pb2.UpdateDescriptionRequest.SerializeToString, + response_deserializer=management__pb2.UpdateResponse.FromString, + _registered_method=True) + self.UpdateLongDescription = channel.unary_unary( + '/management.Mint/UpdateLongDescription', + request_serializer=management__pb2.UpdateDescriptionRequest.SerializeToString, + response_deserializer=management__pb2.UpdateResponse.FromString, + _registered_method=True) + self.UpdateIconUrl = channel.unary_unary( + '/management.Mint/UpdateIconUrl', + request_serializer=management__pb2.UpdateIconUrlRequest.SerializeToString, + response_deserializer=management__pb2.UpdateResponse.FromString, + _registered_method=True) + self.UpdateName = channel.unary_unary( + '/management.Mint/UpdateName', + request_serializer=management__pb2.UpdateNameRequest.SerializeToString, + response_deserializer=management__pb2.UpdateResponse.FromString, + _registered_method=True) + self.AddUrl = channel.unary_unary( + '/management.Mint/AddUrl', + request_serializer=management__pb2.UpdateUrlRequest.SerializeToString, + response_deserializer=management__pb2.UpdateResponse.FromString, + _registered_method=True) + self.RemoveUrl = channel.unary_unary( + '/management.Mint/RemoveUrl', + request_serializer=management__pb2.UpdateUrlRequest.SerializeToString, + response_deserializer=management__pb2.UpdateResponse.FromString, + _registered_method=True) + self.AddContact = channel.unary_unary( + '/management.Mint/AddContact', + request_serializer=management__pb2.UpdateContactRequest.SerializeToString, + response_deserializer=management__pb2.UpdateResponse.FromString, + _registered_method=True) + self.RemoveContact = channel.unary_unary( + '/management.Mint/RemoveContact', + request_serializer=management__pb2.UpdateContactRequest.SerializeToString, + response_deserializer=management__pb2.UpdateResponse.FromString, + _registered_method=True) + self.GetNut04Quote = channel.unary_unary( + '/management.Mint/GetNut04Quote', + request_serializer=management__pb2.GetNut04QuoteRequest.SerializeToString, + response_deserializer=management__pb2.GetNut04QuoteResponse.FromString, + _registered_method=True) + self.GetNut05Quote = channel.unary_unary( + '/management.Mint/GetNut05Quote', + request_serializer=management__pb2.GetNut05QuoteRequest.SerializeToString, + response_deserializer=management__pb2.GetNut05QuoteResponse.FromString, + _registered_method=True) + self.UpdateNut04 = channel.unary_unary( + '/management.Mint/UpdateNut04', + request_serializer=management__pb2.UpdateNut04Request.SerializeToString, + response_deserializer=management__pb2.UpdateResponse.FromString, + _registered_method=True) + self.UpdateNut05 = channel.unary_unary( + '/management.Mint/UpdateNut05', + request_serializer=management__pb2.UpdateNut05Request.SerializeToString, + response_deserializer=management__pb2.UpdateResponse.FromString, + _registered_method=True) + self.UpdateQuoteTtl = channel.unary_unary( + '/management.Mint/UpdateQuoteTtl', + request_serializer=management__pb2.UpdateQuoteTtlRequest.SerializeToString, + response_deserializer=management__pb2.UpdateResponse.FromString, + _registered_method=True) + self.UpdateNut04Quote = channel.unary_unary( + '/management.Mint/UpdateNut04Quote', + request_serializer=management__pb2.UpdateQuoteRequest.SerializeToString, + response_deserializer=management__pb2.UpdateResponse.FromString, + _registered_method=True) + self.UpdateNut05Quote = channel.unary_unary( + '/management.Mint/UpdateNut05Quote', + request_serializer=management__pb2.UpdateQuoteRequest.SerializeToString, + response_deserializer=management__pb2.UpdateResponse.FromString, + _registered_method=True) + self.RotateNextKeyset = channel.unary_unary( + '/management.Mint/RotateNextKeyset', + request_serializer=management__pb2.RotateNextKeysetRequest.SerializeToString, + response_deserializer=management__pb2.RotateNextKeysetResponse.FromString, + _registered_method=True) + self.UpdateLightningFee = channel.unary_unary( + '/management.Mint/UpdateLightningFee', + request_serializer=management__pb2.UpdateLightningFeeRequest.SerializeToString, + response_deserializer=management__pb2.UpdateResponse.FromString, + _registered_method=True) + self.UpdateAuthLimits = channel.unary_unary( + '/management.Mint/UpdateAuthLimits', + request_serializer=management__pb2.UpdateAuthLimitsRequest.SerializeToString, + response_deserializer=management__pb2.UpdateResponse.FromString, + _registered_method=True) + + +class MintServicer(object): + """Missing associated documentation comment in .proto file.""" + + def GetInfo(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateMotd(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateShortDescription(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateLongDescription(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateIconUrl(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateName(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def AddUrl(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def RemoveUrl(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def AddContact(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def RemoveContact(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetNut04Quote(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetNut05Quote(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateNut04(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateNut05(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateQuoteTtl(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateNut04Quote(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateNut05Quote(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def RotateNextKeyset(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateLightningFee(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateAuthLimits(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_MintServicer_to_server(servicer, server): + rpc_method_handlers = { + 'GetInfo': grpc.unary_unary_rpc_method_handler( + servicer.GetInfo, + request_deserializer=management__pb2.GetInfoRequest.FromString, + response_serializer=management__pb2.GetInfoResponse.SerializeToString, + ), + 'UpdateMotd': grpc.unary_unary_rpc_method_handler( + servicer.UpdateMotd, + request_deserializer=management__pb2.UpdateMotdRequest.FromString, + response_serializer=management__pb2.UpdateResponse.SerializeToString, + ), + 'UpdateShortDescription': grpc.unary_unary_rpc_method_handler( + servicer.UpdateShortDescription, + request_deserializer=management__pb2.UpdateDescriptionRequest.FromString, + response_serializer=management__pb2.UpdateResponse.SerializeToString, + ), + 'UpdateLongDescription': grpc.unary_unary_rpc_method_handler( + servicer.UpdateLongDescription, + request_deserializer=management__pb2.UpdateDescriptionRequest.FromString, + response_serializer=management__pb2.UpdateResponse.SerializeToString, + ), + 'UpdateIconUrl': grpc.unary_unary_rpc_method_handler( + servicer.UpdateIconUrl, + request_deserializer=management__pb2.UpdateIconUrlRequest.FromString, + response_serializer=management__pb2.UpdateResponse.SerializeToString, + ), + 'UpdateName': grpc.unary_unary_rpc_method_handler( + servicer.UpdateName, + request_deserializer=management__pb2.UpdateNameRequest.FromString, + response_serializer=management__pb2.UpdateResponse.SerializeToString, + ), + 'AddUrl': grpc.unary_unary_rpc_method_handler( + servicer.AddUrl, + request_deserializer=management__pb2.UpdateUrlRequest.FromString, + response_serializer=management__pb2.UpdateResponse.SerializeToString, + ), + 'RemoveUrl': grpc.unary_unary_rpc_method_handler( + servicer.RemoveUrl, + request_deserializer=management__pb2.UpdateUrlRequest.FromString, + response_serializer=management__pb2.UpdateResponse.SerializeToString, + ), + 'AddContact': grpc.unary_unary_rpc_method_handler( + servicer.AddContact, + request_deserializer=management__pb2.UpdateContactRequest.FromString, + response_serializer=management__pb2.UpdateResponse.SerializeToString, + ), + 'RemoveContact': grpc.unary_unary_rpc_method_handler( + servicer.RemoveContact, + request_deserializer=management__pb2.UpdateContactRequest.FromString, + response_serializer=management__pb2.UpdateResponse.SerializeToString, + ), + 'GetNut04Quote': grpc.unary_unary_rpc_method_handler( + servicer.GetNut04Quote, + request_deserializer=management__pb2.GetNut04QuoteRequest.FromString, + response_serializer=management__pb2.GetNut04QuoteResponse.SerializeToString, + ), + 'GetNut05Quote': grpc.unary_unary_rpc_method_handler( + servicer.GetNut05Quote, + request_deserializer=management__pb2.GetNut05QuoteRequest.FromString, + response_serializer=management__pb2.GetNut05QuoteResponse.SerializeToString, + ), + 'UpdateNut04': grpc.unary_unary_rpc_method_handler( + servicer.UpdateNut04, + request_deserializer=management__pb2.UpdateNut04Request.FromString, + response_serializer=management__pb2.UpdateResponse.SerializeToString, + ), + 'UpdateNut05': grpc.unary_unary_rpc_method_handler( + servicer.UpdateNut05, + request_deserializer=management__pb2.UpdateNut05Request.FromString, + response_serializer=management__pb2.UpdateResponse.SerializeToString, + ), + 'UpdateQuoteTtl': grpc.unary_unary_rpc_method_handler( + servicer.UpdateQuoteTtl, + request_deserializer=management__pb2.UpdateQuoteTtlRequest.FromString, + response_serializer=management__pb2.UpdateResponse.SerializeToString, + ), + 'UpdateNut04Quote': grpc.unary_unary_rpc_method_handler( + servicer.UpdateNut04Quote, + request_deserializer=management__pb2.UpdateQuoteRequest.FromString, + response_serializer=management__pb2.UpdateResponse.SerializeToString, + ), + 'UpdateNut05Quote': grpc.unary_unary_rpc_method_handler( + servicer.UpdateNut05Quote, + request_deserializer=management__pb2.UpdateQuoteRequest.FromString, + response_serializer=management__pb2.UpdateResponse.SerializeToString, + ), + 'RotateNextKeyset': grpc.unary_unary_rpc_method_handler( + servicer.RotateNextKeyset, + request_deserializer=management__pb2.RotateNextKeysetRequest.FromString, + response_serializer=management__pb2.RotateNextKeysetResponse.SerializeToString, + ), + 'UpdateLightningFee': grpc.unary_unary_rpc_method_handler( + servicer.UpdateLightningFee, + request_deserializer=management__pb2.UpdateLightningFeeRequest.FromString, + response_serializer=management__pb2.UpdateResponse.SerializeToString, + ), + 'UpdateAuthLimits': grpc.unary_unary_rpc_method_handler( + servicer.UpdateAuthLimits, + request_deserializer=management__pb2.UpdateAuthLimitsRequest.FromString, + response_serializer=management__pb2.UpdateResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'management.Mint', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('management.Mint', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class Mint(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def GetInfo(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/GetInfo', + management__pb2.GetInfoRequest.SerializeToString, + management__pb2.GetInfoResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def UpdateMotd(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/UpdateMotd', + management__pb2.UpdateMotdRequest.SerializeToString, + management__pb2.UpdateResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def UpdateShortDescription(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/UpdateShortDescription', + management__pb2.UpdateDescriptionRequest.SerializeToString, + management__pb2.UpdateResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def UpdateLongDescription(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/UpdateLongDescription', + management__pb2.UpdateDescriptionRequest.SerializeToString, + management__pb2.UpdateResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def UpdateIconUrl(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/UpdateIconUrl', + management__pb2.UpdateIconUrlRequest.SerializeToString, + management__pb2.UpdateResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def UpdateName(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/UpdateName', + management__pb2.UpdateNameRequest.SerializeToString, + management__pb2.UpdateResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def AddUrl(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/AddUrl', + management__pb2.UpdateUrlRequest.SerializeToString, + management__pb2.UpdateResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def RemoveUrl(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/RemoveUrl', + management__pb2.UpdateUrlRequest.SerializeToString, + management__pb2.UpdateResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def AddContact(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/AddContact', + management__pb2.UpdateContactRequest.SerializeToString, + management__pb2.UpdateResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def RemoveContact(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/RemoveContact', + management__pb2.UpdateContactRequest.SerializeToString, + management__pb2.UpdateResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetNut04Quote(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/GetNut04Quote', + management__pb2.GetNut04QuoteRequest.SerializeToString, + management__pb2.GetNut04QuoteResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetNut05Quote(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/GetNut05Quote', + management__pb2.GetNut05QuoteRequest.SerializeToString, + management__pb2.GetNut05QuoteResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def UpdateNut04(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/UpdateNut04', + management__pb2.UpdateNut04Request.SerializeToString, + management__pb2.UpdateResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def UpdateNut05(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/UpdateNut05', + management__pb2.UpdateNut05Request.SerializeToString, + management__pb2.UpdateResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def UpdateQuoteTtl(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/UpdateQuoteTtl', + management__pb2.UpdateQuoteTtlRequest.SerializeToString, + management__pb2.UpdateResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def UpdateNut04Quote(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/UpdateNut04Quote', + management__pb2.UpdateQuoteRequest.SerializeToString, + management__pb2.UpdateResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def UpdateNut05Quote(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/UpdateNut05Quote', + management__pb2.UpdateQuoteRequest.SerializeToString, + management__pb2.UpdateResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def RotateNextKeyset(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/RotateNextKeyset', + management__pb2.RotateNextKeysetRequest.SerializeToString, + management__pb2.RotateNextKeysetResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def UpdateLightningFee(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/UpdateLightningFee', + management__pb2.UpdateLightningFeeRequest.SerializeToString, + management__pb2.UpdateResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def UpdateAuthLimits(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/management.Mint/UpdateAuthLimits', + management__pb2.UpdateAuthLimitsRequest.SerializeToString, + management__pb2.UpdateResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/cashu/mint/startup.py b/cashu/mint/startup.py index cf6ac31..bc14a67 100644 --- a/cashu/mint/startup.py +++ b/cashu/mint/startup.py @@ -3,10 +3,13 @@ import asyncio import importlib +from copy import copy from typing import Dict from loguru import logger +import cashu.mint.management_rpc.management_rpc as management_rpc + from ..core.base import Method, Unit from ..core.db import Database from ..core.migrations import migrate_databases @@ -129,3 +132,12 @@ async def shutdown_mint(): await ledger.shutdown_ledger() logger.info("Mint shutdown.") logger.remove() + +rpc_server = None +async def start_management_rpc(): + global rpc_server + rpc_server = await management_rpc.serve(copy(ledger)) + +async def shutdown_management_rpc(): + if rpc_server: + await management_rpc.shutdown(rpc_server) diff --git a/pyproject.toml b/pyproject.toml index a38f4a4..be8bcc1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ asyncio_default_fixture_loop_scope = "function" [tool.poetry.scripts] mint = "cashu.mint.main:main" cashu = "cashu.wallet.cli.cli:cli" +mint-cli = "cashu.mint.management_rpc.cli.cli:cli" wallet-test = "tests.test_wallet:test" [tool.ruff] diff --git a/tests/conftest.py b/tests/conftest.py index fdfd10e..ed2ecb5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -54,6 +54,9 @@ settings.db_connection_pool = True settings.mint_require_auth = False settings.mint_watchdog_enabled = False +settings.mint_rpc_server_enable = True +settings.mint_rpc_server_mutual_tls = False + assert "test" in settings.cashu_dir shutil.rmtree(settings.cashu_dir, ignore_errors=True) Path(settings.cashu_dir).mkdir(parents=True, exist_ok=True) diff --git a/tests/mint/test_mint_db.py b/tests/mint/test_mint_db.py index 0b63d32..5db7a6a 100644 --- a/tests/mint/test_mint_db.py +++ b/tests/mint/test_mint_db.py @@ -18,10 +18,15 @@ from cashu.wallet.wallet import Wallet from tests.conftest import SERVER_ENDPOINT from tests.helpers import ( assert_err, + is_deprecated_api_only, is_github_actions, pay_if_regtest, ) +payment_request = ( + "lnbc1u1p5qeft3sp5jn5cqclnxvucfqtjm8qnlar2vhevcuudpccv7tsuglruj3qm579spp5ygdhy0t7xu53myke8z3z024xhz4kzgk9fcqk64sp0fyeqzhmaswqdqqcqpjrzjq0euzzxv65mts5ngg8c2t3vzz2aeuevy5845jvyqulqucd8c9kkhzrtp55qq63qqqqqqqqqqqqqzwyqqyg9qxpqysgqscprcpnk8whs3askqhgu6z5a4hupyn8du2aahdcf00s5pxrs4g94sv9f95xdn4tu0wec7kfyzj439wu9z27k6m6e3q4ysjquf5agx7gp0eeye4" +) + @pytest_asyncio.fixture(scope="function") async def wallet(ledger: Ledger): @@ -289,3 +294,31 @@ async def test_db_events_add_client(wallet: Wallet, ledger: Ledger): # remove subscription client.remove_subscription("subId") + +@pytest.mark.asyncio +async def test_db_update_mint_quote_state(wallet: Wallet, ledger: Ledger): + mint_quote = await wallet.request_mint(128) + await ledger.db_write._update_mint_quote_state(mint_quote.quote, MintQuoteState.paid) + + mint_quote_db = await ledger.crud.get_mint_quote(quote_id=mint_quote.quote, db=ledger.db) + assert mint_quote_db.state == MintQuoteState.paid + + # Update it to issued + await ledger.db_write._update_mint_quote_state(mint_quote_db.quote, MintQuoteState.issued) + + # Try and revert it back to unpaid + await assert_err(ledger.db_write._update_mint_quote_state(mint_quote_db.quote, MintQuoteState.unpaid), "Cannot change state of an issued mint quote.") + +@pytest.mark.asyncio +@pytest.mark.skipif( + is_deprecated_api_only, + reason=("Deprecated API") +) +async def test_db_update_melt_quote_state(wallet: Wallet, ledger: Ledger): + melt_quote = await wallet.melt_quote(payment_request) + await ledger.db_write._update_melt_quote_state(melt_quote.quote, MeltQuoteState.paid) + + melt_quote_db = await ledger.crud.get_melt_quote(quote_id=melt_quote.quote, db=ledger.db) + assert melt_quote_db.state == MeltQuoteState.paid + + await assert_err(ledger.db_write._update_melt_quote_state(melt_quote.quote, MeltQuoteState.unpaid), "Cannot change state of a paid melt quote.") \ No newline at end of file diff --git a/tests/test_mint_rpc_cli.py b/tests/test_mint_rpc_cli.py new file mode 100644 index 0000000..ca789c7 --- /dev/null +++ b/tests/test_mint_rpc_cli.py @@ -0,0 +1,165 @@ +import asyncio + +import pytest +from click.testing import CliRunner + +from cashu.core.settings import settings +from cashu.mint.management_rpc.cli.cli import cli +from cashu.wallet.wallet import Wallet + +from .helpers import is_deprecated_api_only, is_fake + +payment_request = ( + "lnbc10u1pjap7phpp50s9lzr3477j0tvacpfy2ucrs4q0q6cvn232ex7nt2zqxxxj8gxrsdpv2phhwetjv4jzqcneypqyc6t8dp6xu6twva2xjuzzda6qcqzzsxqrrsss" + "p575z0n39w2j7zgnpqtdlrgz9rycner4eptjm3lz363dzylnrm3h4s9qyyssqfz8jglcshnlcf0zkw4qu8fyr564lg59x5al724kms3h6gpuhx9xrfv27tgx3l3u3cyf6" + "3r52u0xmac6max8mdupghfzh84t4hfsvrfsqwnuszf" +) + +@pytest.fixture(autouse=True) +def cli_prefix(): + yield ["--insecure", "--host", settings.mint_rpc_server_addr, "--port", settings.mint_rpc_server_port] + +async def init_wallet(): + settings.debug = False + wallet = await Wallet.with_db( + url=settings.mint_url, + db="test_data/test_cli_wallet", + name="test_cli_wallet", + ) + await wallet.load_proofs() + return wallet + +def test_get_info(cli_prefix): + runner = CliRunner() + result = runner.invoke(cli, [*cli_prefix, "get-info"]) + assert result.exception is None + assert "Mint Info:" in result.output + +def test_update_motd(cli_prefix): + runner = CliRunner() + result = runner.invoke(cli, [*cli_prefix, "update", "motd", "Updated MOTD"]) + assert result.exception is None + assert "Motd successfully updated!" in result.output + +def test_update_short_description(cli_prefix): + runner = CliRunner() + result = runner.invoke(cli, [*cli_prefix, "update", "description", "New short description"]) + assert result.exception is None + assert "Short description successfully updated!" in result.output + +def test_update_long_description(cli_prefix): + runner = CliRunner() + result = runner.invoke(cli, [*cli_prefix, "update", "long-description", "New long description"]) + assert result.exception is None + assert "Long description successfully updated!" in result.output + +def test_update_icon_url(cli_prefix): + runner = CliRunner() + result = runner.invoke(cli, [*cli_prefix, "update", "icon-url", "http://example.com/icon.png"]) + assert result.exception is None + assert "Icon url successfully updated!" in result.output + +def test_update_name(cli_prefix): + runner = CliRunner() + result = runner.invoke(cli, [*cli_prefix, "update", "name", "New Mint Name"]) + assert result.exception is None + assert "Name successfully updated!" in result.output + +def test_add_mint_url(cli_prefix): + runner = CliRunner() + result = runner.invoke(cli, [*cli_prefix, "update", "url", "add", "http://example.com"]) + assert "Url successfully added!" in result.output + +def test_remove_mint_url(cli_prefix): + runner = CliRunner() + result = runner.invoke(cli, [*cli_prefix, "update", "url", "remove", "http://example.com"]) + assert result.exception is None + assert "Url successfully removed!" in result.output or "Contact method not found" in result.output + +def test_add_remove_contact(cli_prefix): + runner = CliRunner() + result = runner.invoke(cli, [*cli_prefix, "update", "contact", "add", "signal", "@example.420"]) + assert result.exception is None + assert "Contact successfully added!" in result.output + + result = runner.invoke(cli, [*cli_prefix, "update", "contact", "remove", "signal"]) + assert result.exception is None + assert "Contact successfully removed!" in result.output + +def test_update_lightning_fee(cli_prefix): + runner = CliRunner() + result = runner.invoke(cli, [*cli_prefix, "update", "lightning-fee", "2.5", "100"]) + assert result.exception is None + assert "Lightning fee successfully updated!" in result.output + +def test_update_auth_limits(cli_prefix): + runner = CliRunner() + result = runner.invoke(cli, [*cli_prefix, "update", "auth", "60", "10"]) + assert result.exception is None + assert "Rate limit per minute successfully updated!" in result.output + +@pytest.mark.asyncio +@pytest.mark.skipif(not is_fake, + reason=( + "Only FakeWallet will mark the quote as paid" + ), +) +async def test_update_mint_quote(cli_prefix): + wallet = await init_wallet() + mint_quote = await wallet.request_mint(100) + await asyncio.sleep(1) + runner = CliRunner() + result = runner.invoke(cli, [*cli_prefix, "update", "mint-quote", mint_quote.quote, "ISSUED"]) + assert result.exception is None + assert "Successfully updated!" in result.output + +@pytest.mark.asyncio +@pytest.mark.skipif( + is_deprecated_api_only, + reason=("Deprecated API"), +) +async def test_update_melt_quote(cli_prefix): + wallet = await init_wallet() + melt_quote = await wallet.melt_quote("lnbc1u1p5qefdgsp5xj5cl559ks226f3vf3d7x2ev2qadplmkswp4649h755cfekdufsspp5sxenacdev78ssuwn5vehycs7ch2ds23hhzytut4ncm27gywtv6rqdqqcqpjrzjqdgp5ar48c8k4cns58jw9lamcdlh57trvrn9psgjrsvwz94j9tqsvrqsvcqqvqsqqqqqqqlgqqqzwyqq2q9qxpqysgqzg8e75zkcxazmd0wqmre6xgkumt7sl4ftsw0q4c6zvz8hn6zjxwz9fmdmwpupw7tw79f7gmukyeeh8vusvt03pgwfud9shj849rvrnqpgcpusw") + assert melt_quote.quote + await asyncio.sleep(1) + runner = CliRunner() + result = runner.invoke(cli, [*cli_prefix, "update", "melt-quote", melt_quote.quote, "PAID"]) + assert result.exception is None + assert "Successfully updated!" in result.output + +@pytest.mark.asyncio +async def test_get_mint_quote(cli_prefix): + wallet = await init_wallet() + mint_quote = await wallet.request_mint(100) + await asyncio.sleep(1) + runner = CliRunner() + result = runner.invoke(cli, [*cli_prefix, "get", "mint-quote", mint_quote.quote]) + assert result.exception is None + assert "mint quote:" in result.output + +@pytest.mark.asyncio +@pytest.mark.skipif( + is_deprecated_api_only, + reason=("Deprecated API"), +) +async def test_get_melt_quote(cli_prefix): + wallet = await init_wallet() + melt_quote = await wallet.melt_quote("lnbc1u1p5qefd7sp55l6kmcrnqz5rejy4lghmgf9de0ucmmn2s3lvkvtkrr0qkwk5r0espp5da4x63rspz5rcfretdh6573c6qlpnzpxc8yq26cyqjc4sk0srfwsdqqcqpjrzjqv3dpepm8kfdxrk3sl6wzqdf49s9c0h9ljtjrek6c08r6aejlwcnur2z3sqqrrgqqyqqqqqqqqqqfcsqjq9qxpqysgq4l5rfjd4h84w7prmtgzjvq79ddy266svuz0d7dg44jmnwjpxg0zxef6hn4j8nzfp4c67qjpe0c9aw63ghu7rtcdg6n4zka9hym69euqq8w5wmj") + await asyncio.sleep(1) + runner = CliRunner() + result = runner.invoke(cli, [*cli_prefix, "get", "melt-quote", melt_quote.quote]) + assert result.exception is None + assert "melt quote:" in result.output + +''' +@pytest.mark.asyncio +async def test_rotate_next_keyset(cli_prefix): + runner = CliRunner() + result = runner.invoke(cli, [*cli_prefix, "next-keyset", "sat", "2"]) # Rotate keyset and add a 2 sat ppk fee + assert result.exception is None + print(result.output) + assert "New keyset successfully created:" in result.output + assert "keyset.unit = 'sat'" in result.output + assert "keyset.input_fee_ppk = 2" in result.output +''' \ No newline at end of file