Mint Management gRPC Server (#723)

* settings

* fix name settings

* management rpc

* hook up the RPC server

* working

* format

* update build script fix import error

* remove accidental commit of vscode extension data

* working ✔

* \n

* add get mint quote get melt quote

* gRPC cli update quotes commands

* update mint melt quotes from cli

* comment under get cli command group

* keyset rotation not yet implemented

* try fix

* change back contact info default to be empty list

* fix import

* add server mTLS

* ll

* script for generating certificates

* rename settings

* move generation script

* do not save TTL expiry into Cache object, rather always load from settings.

* update lightning fees

* update auth limits

* auth rate limit cli

* optional arguemnts

* better error messages

* tests for db update mint/melt quotes

* start mint rpc tests

* add tos_url field to get-info grpc response

* format checks

* add types to click groups where it's needed

* tests on updating quotes

* fix tests

* skip updating mint quote state if on regtest

* test edge case

* unified test_add_remove_contact

* mark pytest-asyncio

* fix missing db argument

* hopefully no more silly errors

* fix test_db_update_mint_quote_state

* pass in the quote id string.

* add keyset rotation

* test for keyset rotation through gRPC command

* fix logger warning

* remove rotation test because it breaks other tests

* use different bolt11 invoices

* assert returned melt quote has quote

* is_postgres

* try different things

* skip if deprecated api

* format checks

* update .gitignore

* default location for certificates
This commit is contained in:
lollerfirst
2025-06-25 12:35:53 +02:00
committed by GitHub
parent bbfb63b34e
commit 29571287b3
25 changed files with 2754 additions and 5 deletions

View File

@@ -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

4
.gitignore vendored
View File

@@ -137,3 +137,7 @@ tor.pid
# MacOS
.DS_Store
.aider*
# PEM certs
*.pem

View File

@@ -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]

View File

@@ -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,

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -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,

View File

@@ -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...")

View File

View File

@@ -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)

View File

@@ -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!"

View File

@@ -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.")

View File

@@ -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!"

View File

@@ -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;
}

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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]

View File

@@ -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)

View File

@@ -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.")

165
tests/test_mint_rpc_cli.py Normal file
View File

@@ -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
'''