diff --git a/cashu/core/base.py b/cashu/core/base.py index 78dfc86..a120572 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -67,10 +67,22 @@ class BlindedSignature(BaseModel): C_: str -class MintRequest(BaseModel): +class KeysResponse(BaseModel): + __root__: Dict[str, str] + + +class KeysetsResponse(BaseModel): + keysets: list[str] + + +class BlindedMessages(BaseModel): blinded_messages: List[BlindedMessage] = [] +class PostMintResponse(BaseModel): + promises: List[BlindedSignature] = [] + + class GetMintResponse(BaseModel): pr: str hash: str @@ -85,9 +97,9 @@ class SplitRequest(BaseModel): proofs: List[Proof] amount: int output_data: Union[ - MintRequest, None + BlindedMessages, None ] = None # backwards compatibility with clients < v0.2.2 - outputs: Union[MintRequest, None] = None + outputs: Union[BlindedMessages, None] = None def __init__(self, **data): super().__init__(**data) @@ -129,17 +141,17 @@ class KeyBase(BaseModel): class WalletKeyset: - id: str - public_keys: Dict[int, PublicKey] + id: Union[str, None] + public_keys: Union[Dict[int, PublicKey], None] mint_url: Union[str, None] = None valid_from: Union[str, None] = None valid_to: Union[str, None] = None first_seen: Union[str, None] = None - active: bool = True + active: Union[bool, None] = True def __init__( self, - pubkeys: Dict[int, PublicKey] = None, + public_keys=None, mint_url=None, id=None, valid_from=None, @@ -153,20 +165,20 @@ class WalletKeyset: self.first_seen = first_seen self.active = active self.mint_url = mint_url - if pubkeys: - self.public_keys = pubkeys + if public_keys: + self.public_keys = public_keys self.id = derive_keyset_id(self.public_keys) class MintKeyset: - id: str + id: Union[str, None] derivation_path: str private_keys: Dict[int, PrivateKey] - public_keys: Dict[int, PublicKey] = {} + public_keys: Union[Dict[int, PublicKey], None] = None valid_from: Union[str, None] = None valid_to: Union[str, None] = None first_seen: Union[str, None] = None - active: bool = True + active: Union[bool, None] = True version: Union[str, None] = None def __init__( @@ -194,13 +206,14 @@ class MintKeyset: def generate_keys(self, seed): """Generates keys of a keyset from a seed.""" self.private_keys = derive_keys(seed, self.derivation_path) - self.public_keys = derive_pubkeys(self.private_keys) - self.id = derive_keyset_id(self.public_keys) + self.public_keys = derive_pubkeys(self.private_keys) # type: ignore + self.id = derive_keyset_id(self.public_keys) # type: ignore def get_keybase(self): + assert self.id is not None return { k: KeyBase(id=self.id, amount=k, pubkey=v.serialize().hex()) - for k, v in self.public_keys.items() + for k, v in self.public_keys.items() # type: ignore } @@ -208,7 +221,7 @@ class MintKeysets: keysets: Dict[str, MintKeyset] def __init__(self, keysets: List[MintKeyset]): - self.keysets: Dict[str, MintKeyset] = {k.id: k for k in keysets} + self.keysets = {k.id: k for k in keysets} # type: ignore def get_ids(self): return [k for k, _ in self.keysets.items()] diff --git a/cashu/core/errors.py b/cashu/core/errors.py index f770896..19e757d 100644 --- a/cashu/core/errors.py +++ b/cashu/core/errors.py @@ -2,25 +2,20 @@ from pydantic import BaseModel class CashuError(BaseModel): - code = "000" - error = "CashuError" + code: int + error: str -# class CashuError(Exception, BaseModel): -# code = "000" -# error = "CashuError" +class MintException(CashuError): + code = 100 + error = "Mint" -# class MintException(CashuError): -# code = 100 -# error = "Mint" +class LightningException(MintException): + code = 200 + error = "Lightning" -# class LightningException(MintException): -# code = 200 -# error = "Lightning" - - -# class InvoiceNotPaidException(LightningException): -# code = 201 -# error = "invoice not paid." +class InvoiceNotPaidException(LightningException): + code = 201 + error = "invoice not paid." diff --git a/cashu/mint/router.py b/cashu/mint/router.py index c7fe9f9..ab879ff 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -5,13 +5,16 @@ from secp256k1 import PublicKey from cashu.core.base import ( BlindedSignature, + KeysResponse, + KeysetsResponse, CheckFeesRequest, CheckFeesResponse, CheckRequest, GetMeltResponse, + BlindedMessages, GetMintResponse, + PostMintResponse, MeltRequest, - MintRequest, PostSplitResponse, SplitRequest, ) @@ -22,28 +25,32 @@ router: APIRouter = APIRouter() @router.get("/keys") -async def keys() -> dict[int, str]: +async def keys() -> KeysResponse: """Get the public keys of the mint of the newest keyset""" keyset = ledger.get_keyset() - return keyset + keys = KeysResponse.parse_obj(keyset) + return keys @router.get("/keys/{idBase64Urlsafe}") -async def keyset_keys(idBase64Urlsafe: str) -> dict[int, str]: +async def keyset_keys(idBase64Urlsafe: str) -> KeysResponse: """ - Get the public keys of the mint of a specificy keyset id. - The id is encoded in base64_urlsafe and needs to be converted back to + Get the public keys of the mint of a specific keyset id. + The id is encoded in idBase64Urlsafe and needs to be converted back to normal base64 before it can be processed. """ id = idBase64Urlsafe.replace("-", "+").replace("_", "/") keyset = ledger.get_keyset(keyset_id=id) - return keyset + keys = KeysResponse.parse_obj(keyset) + return keys @router.get("/keysets") -async def keysets() -> dict[str, list[str]]: - """Get all active keysets of the mint""" - return {"keysets": ledger.keysets.get_ids()} +async def keysets() -> KeysetsResponse: + """Get all active keyset ids of the mint""" + # keysets = {"keysets": ledger.keysets.get_ids()} + keysets = KeysetsResponse(keysets=ledger.keysets.get_ids()) + return keysets @router.get("/mint") @@ -62,9 +69,9 @@ async def request_mint(amount: int = 0) -> GetMintResponse: @router.post("/mint") async def mint( - mint_request: MintRequest, + mint_request: BlindedMessages, payment_hash: Union[str, None] = None, -) -> Union[List[BlindedSignature], CashuError]: +) -> Union[PostMintResponse, CashuError]: """ Requests the minting of tokens belonging to a paid payment request. @@ -74,9 +81,10 @@ async def mint( promises = await ledger.mint( mint_request.blinded_messages, payment_hash=payment_hash ) - return promises + blinded_signatures = PostMintResponse(promises=promises) + return blinded_signatures except Exception as exc: - return CashuError(error=str(exc)) + return CashuError(code=0, error=str(exc)) @router.post("/melt") @@ -103,7 +111,7 @@ async def check_fees(payload: CheckFeesRequest) -> CheckFeesResponse: This is can be useful for checking whether an invoice is internal (Cashu-to-Cashu). """ fees_msat = await ledger.check_fees(payload.pr) - return CheckFeesResponse(fee=fees_msat / 1000) + return CheckFeesResponse(fee=fees_msat // 1000) @router.post("/split") @@ -124,9 +132,9 @@ async def split( try: split_return = await ledger.split(proofs, amount, outputs) except Exception as exc: - return CashuError(error=str(exc)) + return CashuError(code=0, error=str(exc)) if not split_return: - return CashuError(error="there was an error with the split") + return CashuError(code=0, error="there was an error with the split") frst_promises, scnd_promises = split_return resp = PostSplitResponse(fst=frst_promises, snd=scnd_promises) return resp diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index a500921..10f7ef5 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -508,7 +508,7 @@ async def pending(ctx): reserved_proofs = await get_reserved_proofs(wallet.db) if len(reserved_proofs): print(f"--------------------------\n") - sorted_proofs = sorted(reserved_proofs, key=itemgetter("send_id")) + sorted_proofs = sorted(reserved_proofs, key=itemgetter("send_id")) # type: ignore for i, (key, value) in enumerate( groupby(sorted_proofs, key=itemgetter("send_id")) ): diff --git a/cashu/wallet/cli_helpers.py b/cashu/wallet/cli_helpers.py index 132b6fe..0d6b6e8 100644 --- a/cashu/wallet/cli_helpers.py +++ b/cashu/wallet/cli_helpers.py @@ -19,7 +19,7 @@ async def verify_mints(ctx, dtoken): mint_url, os.path.join(CASHU_DIR, ctx.obj["WALLET_NAME"]) ) # make sure that this mint supports this keyset - mint_keysets = await keyset_wallet._get_keysets(mint_url) + mint_keysets = await keyset_wallet._get_keyset_ids(mint_url) assert keyset in mint_keysets["keysets"], "mint does not have this keyset." # we validate the keyset id by fetching the keys from the mint @@ -119,6 +119,7 @@ async def get_mint_wallet(ctx): mint_keysets: WalletKeyset = await get_keyset(mint_url=mint_url, db=mint_wallet.db) # type: ignore # load the keys + assert mint_keysets.id await mint_wallet.load_mint(keyset_id=mint_keysets.id) return mint_wallet @@ -161,7 +162,7 @@ async def proofs_to_token(wallet, proofs, url: str): # check whether we know the mint urls for these proofs for k in keysets: ks = await get_keyset(id=k, db=wallet.db) - url = ks.mint_url if ks is not None else None + url = ks.mint_url if ks and ks.mint_url else "" url = url or ( input(f"Enter mint URL (press enter for default {MINT_URL}): ") or MINT_URL diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 378308d..f5959fc 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -16,10 +16,13 @@ from cashu.core.base import ( BlindedMessage, BlindedSignature, CheckFeesRequest, + KeysetsResponse, + GetMintResponse, + PostMintResponse, CheckRequest, Invoice, MeltRequest, - MintRequest, + BlindedMessages, P2SHScript, Proof, SplitRequest, @@ -130,6 +133,8 @@ class LedgerAPI: 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 @@ -140,7 +145,7 @@ class LedgerAPI: # get all active keysets of this mint mint_keysets = [] try: - keysets_resp = await self._get_keysets(self.url) + keysets_resp = await self._get_keyset_ids(self.url) mint_keysets = keysets_resp["keysets"] # store active keysets except: @@ -160,7 +165,7 @@ class LedgerAPI: assert len(amounts) == len( secrets ), f"len(amounts)={len(amounts)} not equal to len(secrets)={len(secrets)}" - payloads: MintRequest = MintRequest() + payloads: BlindedMessages = BlindedMessages() rs = [] for secret, amount in zip(secrets, amounts): B_, r = b_dhke.step1_alice(secret) @@ -198,7 +203,7 @@ class LedgerAPI: int(amt): PublicKey(bytes.fromhex(val), raw=True) for amt, val in keys.items() } - keyset = WalletKeyset(pubkeys=keyset_keys, mint_url=url) + keyset = WalletKeyset(public_keys=keyset_keys, mint_url=url) return keyset async def _get_keyset(self, url: str, keyset_id: str): @@ -217,18 +222,19 @@ class LedgerAPI: int(amt): PublicKey(bytes.fromhex(val), raw=True) for amt, val in keys.items() } - keyset = WalletKeyset(pubkeys=keyset_keys, mint_url=url) + keyset = WalletKeyset(public_keys=keyset_keys, mint_url=url) return keyset - async def _get_keysets(self, url: str): + async def _get_keyset_ids(self, url: str): self.s = self._set_requests() resp = self.s.get( url + "/keysets", ) resp.raise_for_status() - keysets = resp.json() - assert len(keysets), Exception("did not receive any keysets") - return keysets + keysets_dict = resp.json() + keysets = KeysetsResponse.parse_obj(keysets_dict) + assert len(keysets.keysets), Exception("did not receive any keysets") + return keysets.dict() def request_mint(self, amount): """Requests a mint from the server and returns Lightning invoice.""" @@ -237,7 +243,8 @@ class LedgerAPI: resp.raise_for_status() return_dict = resp.json() self.raise_on_error(return_dict) - return Invoice(amount=amount, pr=return_dict["pr"], hash=return_dict["hash"]) + mint_response = GetMintResponse.parse_obj(return_dict) + return Invoice(amount=amount, pr=mint_response.pr, hash=mint_response.hash) async def mint(self, amounts, payment_hash=None): """Mints new coins and returns a proof of promise.""" @@ -251,11 +258,10 @@ class LedgerAPI: params={"payment_hash": payment_hash}, ) resp.raise_for_status() - promises_list = resp.json() - self.raise_on_error(promises_list) - - promises = [BlindedSignature(**p) for p in promises_list] - return self._construct_proofs(promises, secrets, rs) + reponse_dict = resp.json() + self.raise_on_error(reponse_dict) + promises = PostMintResponse.parse_obj(reponse_dict) + return self._construct_proofs(promises.promises, secrets, rs) async def split(self, proofs, amount, scnd_secret: Optional[str] = None): """Consume proofs and create new promises based on amount split. @@ -304,7 +310,7 @@ class LedgerAPI: self.s = self._set_requests() resp = self.s.post( self.url + "/split", - json=split_payload.dict(include=_splitrequest_include_fields(proofs)), + json=split_payload.dict(include=_splitrequest_include_fields(proofs)), # type: ignore ) resp.raise_for_status() promises_dict = resp.json() @@ -337,7 +343,7 @@ class LedgerAPI: self.s = self._set_requests() resp = self.s.post( self.url + "/check", - json=payload.dict(include=_check_spendable_include_fields(proofs)), + json=payload.dict(include=_check_spendable_include_fields(proofs)), # type: ignore ) resp.raise_for_status() return_dict = resp.json() @@ -375,7 +381,7 @@ class LedgerAPI: self.s = self._set_requests() resp = self.s.post( self.url + "/melt", - json=payload.dict(include=_meltequest_include_fields(proofs)), + json=payload.dict(include=_meltequest_include_fields(proofs)), # type: ignore ) resp.raise_for_status() return_dict = resp.json() @@ -411,7 +417,9 @@ class Wallet(LedgerAPI): for id in set([p.id for p in proofs]): if id is None: continue - keyset: WalletKeyset = await get_keyset(id=id, db=self.db) + keyset_crud = await get_keyset(id=id, db=self.db) + assert keyset_crud is not None, "keyset not found" + keyset: WalletKeyset = keyset_crud if keyset.mint_url not in ret: ret[keyset.mint_url] = [p for p in proofs if p.id == id] else: @@ -504,7 +512,7 @@ class Wallet(LedgerAPI): if proof.id: # load the keyset from the db keyset = await get_keyset(id=proof.id, db=self.db) - if keyset and keyset.mint_url: + if keyset and keyset.mint_url and keyset.id: # TODO: replace this with a mint pubkey placeholder_mint_id = keyset.mint_url if placeholder_mint_id not in mints: @@ -577,7 +585,8 @@ class Wallet(LedgerAPI): Splits proofs such that a Lightning invoice can be paid. """ amount, _ = await self.get_pay_amount_with_fees(invoice) - _, send_proofs = await self.split_to_send(self.proofs, amount) + # TODO: fix mypy asyncio return multiple values + _, send_proofs = await self.split_to_send(self.proofs, amount) # type: ignore return send_proofs async def split_to_send( diff --git a/docs/specs/cashu_client_spec.md b/docs/specs/cashu_client_spec.md index 73bb92d..c41effc 100644 --- a/docs/specs/cashu_client_spec.md +++ b/docs/specs/cashu_client_spec.md @@ -52,8 +52,8 @@ Here we see how `Alice` generates `N` blinded messages `T_i`. The following step - `Alice` remembers `r` for the construction of the proof in Step 5. ### Step 4: Request tokens -- `Alice` constructs JSON `MintRequest = {"blinded_messages" : ["amount" : , "B_" : ] }` [NOTE: rename "blinded_messages", rename "B_", rename "MintRequest"] -- `Alice` requests tokens via `POST /mint?payment_hash=` with body `MintRequest` [NOTE: rename MintRequest] +- `Alice` constructs JSON `BlindedMessages = {"blinded_messages" : ["amount" : , "B_" : ] }` [NOTE: rename "blinded_messages", rename "B_", rename "BlindedMessages"] +- `Alice` requests tokens via `POST /mint?payment_hash=` with body `BlindedMessages` [NOTE: rename BlindedMessages] - `Alice` receives from `Bob` a list of blinded signatures `List[BlindedSignature]`, one for each token, e.g. `[{"amount" : , "C_" : }, ...]` [NOTE: rename C_] - If an error occured, `Alice` receives JSON `{"error" : }}`[*TODO: Specify case of error*] @@ -121,5 +121,5 @@ Here we describe how `Alice` can request from `Bob` to make a Lightning payment # Todo: - Call subsections 1. and 1.2 etc so they can be referenced -- Define objets like `MintRequest` and `SplitRequests` once when they appear and reuse them. +- Define objets like `BlindedMessages` and `SplitRequests` once when they appear and reuse them. - Clarify whether a `TOKEN` is a single Proof or a list of Proofs diff --git a/docs/specs/wip/4 - Minting tokens.md b/docs/specs/wip/4 - Minting tokens.md index 0a7dc15..5239458 100644 --- a/docs/specs/wip/4 - Minting tokens.md +++ b/docs/specs/wip/4 - Minting tokens.md @@ -10,7 +10,7 @@ Request of `Alice`: POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b64a ``` -With the data being of the form `MintRequest`: +With the data being of the form `BlindedMessages`: ```json { diff --git a/docs/specs/wip/6 - Split.md b/docs/specs/wip/6 - Split.md index e6b735f..c25e831 100644 --- a/docs/specs/wip/6 - Split.md +++ b/docs/specs/wip/6 - Split.md @@ -25,7 +25,7 @@ With the data being of the form `SplitRequest`: ```json { "proofs": Proofs, - "outputs": MintRequest, + "outputs": BlindedMessages, "amount": int } ``` diff --git a/tests/test_tor.py b/tests/test_tor.py index 4d792bb..8cbc92e 100644 --- a/tests/test_tor.py +++ b/tests/test_tor.py @@ -4,7 +4,7 @@ import requests from cashu.tor.tor import TorProxy -# @pytest.mark.skip +@pytest.mark.skip def test_tor_setup(): s = requests.Session() diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 493503f..c8b186a 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -68,8 +68,8 @@ async def test_get_keyset(wallet1: Wallet): @pytest.mark.asyncio -async def test_get_keysets(wallet1: Wallet): - keyset = await wallet1._get_keysets(wallet1.url) +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