diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 536c76f..8cff05d 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -338,6 +338,8 @@ class Ledger: # Public methods def get_keyset(self, keyset_id: Optional[str] = None): + if keyset_id and keyset_id not in self.keysets.keysets: + raise Exception("keyset does not exist") keyset = self.keysets.keysets[keyset_id] if keyset_id else self.keyset assert keyset.public_keys, Exception("no public keys for this keyset") return {a: p.serialize().hex() for a, p in keyset.public_keys.items()} diff --git a/cashu/mint/router.py b/cashu/mint/router.py index 1d486ea..f927d6a 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -43,16 +43,19 @@ async def keys() -> KeysResponse: name="Keyset public keys", summary="Public keys of a specific keyset", ) -async def keyset_keys(idBase64Urlsafe: str) -> KeysResponse: +async def keyset_keys(idBase64Urlsafe: str) -> Union[KeysResponse, CashuError]: """ Get the public keys of the mint from a specific keyset id. The id is encoded in idBase64Urlsafe (by a wallet) and is converted back to normal base64 before it can be processed (by the mint). """ - id = idBase64Urlsafe.replace("-", "+").replace("_", "/") - keyset = ledger.get_keyset(keyset_id=id) - keys = KeysResponse.parse_obj(keyset) - return keys + try: + id = idBase64Urlsafe.replace("-", "+").replace("_", "/") + keyset = ledger.get_keyset(keyset_id=id) + keys = KeysResponse.parse_obj(keyset) + return keys + except Exception as exc: + return CashuError(code=0, error=str(exc)) @router.get( diff --git a/cashu/wallet/cli/cli_helpers.py b/cashu/wallet/cli/cli_helpers.py index 67075bc..3f53c23 100644 --- a/cashu/wallet/cli/cli_helpers.py +++ b/cashu/wallet/cli/cli_helpers.py @@ -36,10 +36,10 @@ async def verify_mints(ctx: Context, token: TokenV2): ) # make sure that this mint supports this keyset mint_keysets = await keyset_wallet._get_keyset_ids(mint.url) - assert keyset in mint_keysets["keysets"], "mint does not have this keyset." + assert keyset in mint_keysets, "mint does not have this keyset." # we validate the keyset id by fetching the keys from the mint and computing the id locally - mint_keyset = await keyset_wallet._get_keyset(mint.url, keyset) + mint_keyset = await keyset_wallet._get_keys_of_keyset(mint.url, keyset) assert keyset == mint_keyset.id, Exception("keyset not valid.") # we check the db whether we know this mint already and ask the user if not diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 9087eaf..7923e71 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -91,8 +91,9 @@ def async_set_requests(func): class LedgerAPI: - keys: Dict[int, PublicKey] - keyset: str + keys: WalletKeyset # holds current keys of mint + keyset_id: str # holds id of current keyset + public_keys: Dict[int, PublicKey] # holds public keys of tor: TorProxy db: Database s: requests.Session @@ -113,7 +114,7 @@ class LedgerAPI: proofs: List[Proof] = [] for promise, secret, r in zip(promises, secrets, rs): C_ = PublicKey(bytes.fromhex(promise.C_), raw=True) - C = b_dhke.step3_alice(C_, r, self.keys[promise.amount]) + C = b_dhke.step3_alice(C_, r, self.public_keys[promise.amount]) proof = Proof( id=self.keyset_id, amount=promise.amount, @@ -133,48 +134,60 @@ class LedgerAPI: """Returns base64 encoded random string.""" return scrts.token_urlsafe(randombits // 8) - async def _load_mint(self, keyset_id: str = ""): - """ - Loads the public keys of the mint. Either gets the keys for the specified - `keyset_id` or loads the most recent one from the mint. - Gets and the active keyset ids of the mint and stores in `self.keysets`. - """ + async def _load_mint_keys(self, keyset_id: str = ""): assert len( self.url ), "Ledger not initialized correctly: mint URL not specified yet. " if keyset_id: # get requested keyset - keyset = await self._get_keyset(self.url, keyset_id) + keyset = await self._get_keys_of_keyset(self.url, keyset_id) else: # get current keyset keyset = await self._get_keys(self.url) - # store current keyset assert keyset.public_keys assert keyset.id assert len(keyset.public_keys) > 0, "did not receive keys from mint." # check if current keyset is in db keyset_local: Optional[WalletKeyset] = await get_keyset(keyset.id, db=self.db) + # if not, store it if keyset_local is None: await store_keyset(keyset=keyset, db=self.db) + self.keys = keyset + logger.debug(f"Current mint keyset: {self.keys.id}") + return self.keys + + async def _load_mint_keysets(self): # get all active keysets of this mint mint_keysets = [] try: - keysets_resp = await self._get_keyset_ids(self.url) - mint_keysets = keysets_resp["keysets"] - # store active keysets + mint_keysets = await self._get_keyset_ids(self.url) except: + assert self.keys.id, "could not get keysets from mint, and do not have keys" pass - self.keysets = mint_keysets if len(mint_keysets) else [keyset.id] - + self.keysets = mint_keysets or [self.keys.id] logger.debug(f"Mint keysets: {self.keysets}") - logger.debug(f"Current mint keyset: {keyset.id}") + return self.keysets - self.keys = keyset.public_keys - self.keyset_id = keyset.id + async def _load_mint(self, keyset_id: str = ""): + """ + 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. + Gets the active keyset ids of the mint and stores in `self.keysets`. + """ + await self._load_mint_keys(keyset_id) + await self._load_mint_keysets() + + if keyset_id: + assert keyset_id in self.keysets, f"keyset {keyset_id} not active on mint" + + assert self.keys.public_keys + self.public_keys = self.keys.public_keys + assert self.keys.id + self.keyset_id = self.keys.id @staticmethod def _construct_outputs(amounts: List[int], secrets: List[str]): @@ -211,6 +224,14 @@ class LedgerAPI: @async_set_requests async def _get_keys(self, url: str): + """API that gets the current keys of the mint + + Args: + url (str): Mint URL + + Returns: + WalletKeyset: Current mint keyset + """ resp = self.s.get( url + "/keys", ) @@ -225,9 +246,16 @@ class LedgerAPI: return keyset @async_set_requests - async def _get_keyset(self, url: str, keyset_id: str): - """ - keyset_id is base64, needs to be urlsafe-encoded. + async def _get_keys_of_keyset(self, url: str, keyset_id: str): + """API that gets the keys of a specific keyset from the mint. + + + Args: + url (str): Mint URL + keyset_id (str): base64 keyset ID, needs to be urlsafe-encoded before sending to mint (done in this method) + + Returns: + WalletKeyset: Keyset with ID keyset_id """ keyset_id_urlsafe = keyset_id.replace("+", "-").replace("/", "_") resp = self.s.get( @@ -235,6 +263,7 @@ class LedgerAPI: ) resp.raise_for_status() keys = resp.json() + self.raise_on_error(keys) assert len(keys), Exception("did not receive any keys") keyset_keys = { int(amt): PublicKey(bytes.fromhex(val), raw=True) @@ -245,6 +274,14 @@ class LedgerAPI: @async_set_requests async def _get_keyset_ids(self, url: str): + """API that gets a list of all active keysets of the mint. + + Args: + url (str): Mint URL + + Returns: + KeysetsResponse (List[str]): List of all active keyset IDs of the mint + """ resp = self.s.get( url + "/keysets", ) @@ -252,7 +289,7 @@ class LedgerAPI: keysets_dict = resp.json() keysets = KeysetsResponse.parse_obj(keysets_dict) assert len(keysets.keysets), Exception("did not receive any keysets") - return keysets.dict() + return keysets.keysets @async_set_requests async def request_mint(self, amount): diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 6fb60d6..ea25596 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -50,7 +50,8 @@ async def wallet2(mint): @pytest.mark.asyncio async def test_get_keys(wallet1: Wallet): - assert len(wallet1.keys) == MAX_ORDER + assert wallet1.keys.public_keys + assert len(wallet1.keys.public_keys) == MAX_ORDER keyset = await wallet1._get_keys(wallet1.url) assert keyset.id is not None assert type(keyset.id) == str @@ -59,24 +60,32 @@ async def test_get_keys(wallet1: Wallet): @pytest.mark.asyncio async def test_get_keyset(wallet1: Wallet): - assert len(wallet1.keys) == MAX_ORDER - # ket's get the keys first so we can get a keyset ID that we use later + assert wallet1.keys.public_keys + assert len(wallet1.keys.public_keys) == MAX_ORDER + # let's get the keys first so we can get a keyset ID that we use later keys1 = await wallet1._get_keys(wallet1.url) # gets the keys of a specific keyset assert keys1.id is not None assert keys1.public_keys is not None - keys2 = await wallet1._get_keyset(wallet1.url, keys1.id) + keys2 = await wallet1._get_keys_of_keyset(wallet1.url, keys1.id) assert keys2.public_keys is not None assert len(keys1.public_keys) == len(keys2.public_keys) +@pytest.mark.asyncio +async def test_get_nonexistent_keyset(wallet1: Wallet): + await assert_err( + wallet1._get_keys_of_keyset(wallet1.url, "nonexistent"), + "Mint Error: keyset does not exist", + ) + + @pytest.mark.asyncio async def test_get_keyset_ids(wallet1: Wallet): keyset = await wallet1._get_keyset_ids(wallet1.url) - assert type(keyset) == dict - assert type(keyset["keysets"]) == list - assert len(keyset["keysets"]) > 0 - assert keyset["keysets"][-1] == wallet1.keyset_id + assert type(keyset) == list + assert len(keyset) > 0 + assert keyset[-1] == wallet1.keyset_id @pytest.mark.asyncio