diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py index 8476e6d..ff3bf94 100644 --- a/cashu/wallet/cli/cli.py +++ b/cashu/wallet/cli/cli.py @@ -620,9 +620,12 @@ async def wallets(ctx): @cli.command("info", help="Information about Cashu wallet.") +@click.option( + "--mint", "-m", default=False, is_flag=True, help="Fetch mint information." +) @click.pass_context @coro -async def info(ctx: Context): +async def info(ctx: Context, mint: bool): print(f"Version: {settings.version}") print(f"Wallet: {ctx.obj['WALLET_NAME']}") if settings.debug: @@ -642,4 +645,25 @@ async def info(ctx: Context): if settings.socks_host: print(f"Socks proxy: {settings.socks_host}:{settings.socks_port}") print(f"Mint URL: {ctx.obj['HOST']}") + if mint: + wallet: Wallet = ctx.obj["WALLET"] + mint_info: dict = (await wallet._load_mint_info()).dict() + print("") + print("Mint information:") + print("") + if mint_info: + print(f"Mint name: {mint_info['name']}") + if mint_info["description"]: + print(f"Description: {mint_info['description']}") + if mint_info["description_long"]: + print(f"Long description: {mint_info['description_long']}") + if mint_info["contact"]: + print(f"Contact: {mint_info['contact']}") + if mint_info["version"]: + print(f"Version: {mint_info['version']}") + if mint_info["motd"]: + print(f"Message of the day: {mint_info['motd']}") + if mint_info["parameter"]: + print(f"Parameter: {mint_info['parameter']}") + return diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index c50e57a..6376650 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -18,6 +18,7 @@ from ..core.base import ( CheckFeesRequest, CheckSpendableRequest, CheckSpendableResponse, + GetInfoResponse, GetMeltResponse, GetMintResponse, Invoice, @@ -98,6 +99,7 @@ class LedgerAPI: keys: WalletKeyset # holds current keys of mint keyset_id: str # holds id of current keyset public_keys: Dict[int, PublicKey] # holds public keys of + mint_info: GetInfoResponse # holds info about mint tor: TorProxy db: Database s: requests.Session @@ -113,7 +115,7 @@ class LedgerAPI: def _construct_proofs( self, promises: List[BlindedSignature], secrets: List[str], rs: List[PrivateKey] - ): + ) -> List[Proof]: """Returns proofs of promise from promises. Wants secrets and blinding factors rs.""" logger.trace(f"Constructing proofs.") proofs: List[Proof] = [] @@ -137,7 +139,7 @@ class LedgerAPI: return proofs @staticmethod - def raise_on_error(resp_dict): + def raise_on_error(resp_dict) -> None: if "error" in resp_dict: raise Exception("Mint Error: {}".format(resp_dict["error"])) @@ -146,7 +148,7 @@ class LedgerAPI: """Returns base64 encoded random string.""" return scrts.token_urlsafe(randombits // 8) - async def _load_mint_keys(self, keyset_id: str = ""): + async def _load_mint_keys(self, keyset_id: str = "") -> WalletKeyset: assert len( self.url ), "Ledger not initialized correctly: mint URL not specified yet. " @@ -177,7 +179,7 @@ class LedgerAPI: logger.debug(f"Current mint keyset: {self.keys.id}") return self.keys - async def _load_mint_keysets(self): + async def _load_mint_keysets(self) -> List[str]: # get all active keysets of this mint mint_keysets = [] try: @@ -189,7 +191,13 @@ class LedgerAPI: logger.debug(f"Mint keysets: {self.keysets}") return self.keysets - async def _load_mint(self, keyset_id: str = ""): + async def _load_mint_info(self) -> GetInfoResponse: + """Loads the mint info from the mint.""" + self.mint_info = await self._get_info(self.url) + logger.debug(f"Mint info: {self.mint_info}") + return self.mint_info + + async def _load_mint(self, keyset_id: str = "") -> None: """ Loads the public keys of the mint. Either gets the keys for the specified `keyset_id` or gets the keys of the active keyset from the mint. @@ -197,14 +205,28 @@ class LedgerAPI: """ await self._load_mint_keys(keyset_id) await self._load_mint_keysets() + await self._load_mint_info() if keyset_id: assert keyset_id in self.keysets, f"keyset {keyset_id} not active on mint" @staticmethod - def _construct_outputs(amounts: List[int], secrets: List[str]): + def _construct_outputs( + amounts: List[int], secrets: List[str] + ) -> Tuple[List[BlindedMessage], List[PrivateKey]]: """Takes a list of amounts and secrets and returns outputs. - Outputs are blinded messages `outputs` and blinding factors `rs`""" + Outputs are blinded messages `outputs` and blinding factors `rs` + + Args: + amounts (List[int]): List of amounts + secrets (List[str]): List of secrets + + Returns: + Tuple[List[BlindedMessage], List[PrivateKey]]: Tuple of blinded messages and blinding factors + + Raises: + Exception: If len(amounts) != len(secrets) + """ logger.trace(f"Constructing outputs.") assert len(amounts) == len( secrets @@ -221,16 +243,31 @@ class LedgerAPI: logger.trace(f"Constructed {len(outputs)} outputs.") return outputs, rs - async def _check_used_secrets(self, secrets): - """Checks if any of the secrets have already been used""" + async def _check_used_secrets(self, secrets) -> None: + """Checks if any of the secrets have already been used + + Args: + secrets (List[str]): List of secrets to check + + Raises: + Exception: If any of the secrets have already been used + """ logger.trace("Checking secrets.") for s in secrets: if await secret_used(s, db=self.db): raise Exception(f"secret already used: {s}") logger.trace("Secret check complete.") - def generate_secrets(self, secret, n): - """`secret` is the base string that will be tweaked n times""" + def generate_secrets(self, secret, n) -> List[str]: + """`secret` is the base string that will be tweaked n times + + Args: + secret (str): Base secret + n (int): Number of secrets to generate + + Returns: + List[str]: List of secrets + """ if len(secret.split("P2SH:")) == 2: return [f"{secret}:{self._generate_secret()}" for i in range(n)] return [f"{i}:{secret}" for i in range(n)] @@ -240,7 +277,7 @@ class LedgerAPI: """ @async_set_requests - async def _get_keys(self, url: str): + async def _get_keys(self, url: str) -> WalletKeyset: """API that gets the current keys of the mint Args: @@ -248,6 +285,9 @@ class LedgerAPI: Returns: WalletKeyset: Current mint keyset + + Raises: + Exception: If no keys are received from the mint """ resp = self.s.get( url + "/keys", @@ -263,7 +303,7 @@ class LedgerAPI: return keyset @async_set_requests - async def _get_keys_of_keyset(self, url: str, keyset_id: str): + async def _get_keys_of_keyset(self, url: str, keyset_id: str) -> WalletKeyset: """API that gets the keys of a specific keyset from the mint. @@ -273,6 +313,9 @@ class LedgerAPI: Returns: WalletKeyset: Keyset with ID keyset_id + + Raises: + Exception: If no keys are received from the mint """ keyset_id_urlsafe = keyset_id.replace("+", "-").replace("/", "_") resp = self.s.get( @@ -290,7 +333,7 @@ class LedgerAPI: return keyset @async_set_requests - async def _get_keyset_ids(self, url: str): + async def _get_keyset_ids(self, url: str) -> List[str]: """API that gets a list of all active keysets of the mint. Args: @@ -298,6 +341,9 @@ class LedgerAPI: Returns: KeysetsResponse (List[str]): List of all active keyset IDs of the mint + + Raises: + Exception: If no keysets are received from the mint """ resp = self.s.get( url + "/keysets", @@ -309,8 +355,39 @@ class LedgerAPI: return keysets.keysets @async_set_requests - async def request_mint(self, amount): - """Requests a mint from the server and returns Lightning invoice.""" + async def _get_info(self, url: str) -> GetInfoResponse: + """API that gets the mint info. + + Args: + url (str): Mint URL + + Returns: + GetInfoResponse: Current mint info + + Raises: + Exception: If the mint info request fails + """ + resp = self.s.get( + url + "/info", + ) + resp.raise_for_status() + data: dict = resp.json() + mint_info: GetInfoResponse = GetInfoResponse.parse_obj(data) + return mint_info + + @async_set_requests + async def request_mint(self, amount) -> Invoice: + """Requests a mint from the server and returns Lightning invoice. + + Args: + amount (int): Amount of tokens to mint + + Returns: + Invoice: Lightning invoice + + Raises: + Exception: If the mint request fails + """ logger.trace("Requesting mint: GET /mint") resp = self.s.get(self.url + "/mint", params={"amount": amount}) resp.raise_for_status() @@ -320,8 +397,19 @@ class LedgerAPI: return Invoice(amount=amount, pr=mint_response.pr, hash=mint_response.hash) @async_set_requests - async def mint(self, amounts, hash=None): - """Mints new coins and returns a proof of promise.""" + async def mint(self, amounts, hash=None) -> List[Proof]: + """Mints new coins and returns a proof of promise. + + Args: + amounts (List[int]): Amounts of tokens to mint + hash (str, optional): Hash of the paid invoice. Defaults to None. + + Returns: + list[Proof]: List of proofs. + + Raises: + Exception: If the minting fails + """ secrets = [self._generate_secret() for s in range(len(amounts))] await self._check_used_secrets(secrets) outputs, rs = self._construct_outputs(amounts, secrets) @@ -348,7 +436,9 @@ class LedgerAPI: return self._construct_proofs(promises, secrets, rs) @async_set_requests - async def split(self, proofs, amount, scnd_secret: Optional[str] = None): + async def split( + self, proofs, amount, scnd_secret: Optional[str] = None + ) -> Tuple[List[Proof], List[Proof]]: """Consume proofs and create new promises based on amount split. If scnd_secret is None, random secrets will be generated for the tokens to keep (frst_outputs) diff --git a/tests/test_cli.py b/tests/test_cli.py index fe47398..e3d0828 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -45,6 +45,20 @@ def test_info(cli_prefix): assert result.exit_code == 0 +@pytest.mark.asyncio +def test_info_with_mint(cli_prefix): + runner = CliRunner() + result = runner.invoke( + cli, + [*cli_prefix, "info", "-m"], + ) + assert result.exception is None + print("INFO -M") + print(result.output) + assert "Mint name" in result.output + assert result.exit_code == 0 + + @pytest.mark.asyncio def test_balance(cli_prefix): runner = CliRunner() diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 64f7605..68c051e 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -72,6 +72,12 @@ async def test_get_keyset(wallet1: Wallet): assert len(keys1.public_keys) == len(keys2.public_keys) +@pytest.mark.asyncio +async def test_get_info(wallet1: Wallet): + info = await wallet1._get_info(wallet1.url) + assert info.name + + @pytest.mark.asyncio async def test_get_nonexistent_keyset(wallet1: Wallet): await assert_err(