Skip to content

Commit

Permalink
backwards compatible api upgrade
Browse files Browse the repository at this point in the history
  • Loading branch information
callebtc committed Oct 5, 2023
1 parent ce6a673 commit c91e799
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 55 deletions.
29 changes: 16 additions & 13 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,14 @@ class KeysetsResponse(BaseModel):
keysets: list[KeysetsResponseKeyset]


class KeysResponse_deprecated(BaseModel):
__root__: Dict[str, str]


class KeysetsResponse_deprecated(BaseModel):
keysets: list[str]


# ------- API: MINT -------


Expand Down Expand Up @@ -299,6 +307,7 @@ class WalletKeyset:
"""

id: str
id_deprecated: str # deprecated since 0.14.0
public_keys: Dict[int, PublicKey]
mint_url: Union[str, None] = None
valid_from: Union[str, None] = None
Expand All @@ -325,7 +334,9 @@ def __init__(
self.public_keys = public_keys
# overwrite id by deriving it from the public keys
self.id = derive_keyset_id(self.public_keys)
# BEGIN BACKWARDS COMPATIBILITY < 0.14.0
self.id_deprecated = derive_keyset_id_deprecated(self.public_keys)
# END BACKWARDS COMPATIBILITY < 0.14.0

def serialize(self):
return json.dumps(
Expand Down Expand Up @@ -361,6 +372,7 @@ class MintKeyset:
"""

id: str
id_deprecated: str # deprecated since 0.14.0
derivation_path: str
private_keys: Dict[int, PrivateKey]
public_keys: Union[Dict[int, PublicKey], None] = None
Expand Down Expand Up @@ -424,19 +436,10 @@ def generate_keys(self, seed):
self.private_keys = derive_keys(seed, self.derivation_path)
self.public_keys = derive_pubkeys(self.private_keys) # type: ignore

if (
self.version
and len(self.version.split(".")) > 1
and int(self.version.split(".")[0]) == 0
and int(self.version.split(".")[1]) <= 13
):
self.id = derive_keyset_id_deprecated(self.public_keys) # type: ignore
logger.warning(
"WARNING: Using deriving keyset id with old base64 format"
f" {self.id} (backwards compatibility < 0.14)"
)
else:
self.id = derive_keyset_id(self.public_keys) # type: ignore
self.id = derive_keyset_id(self.public_keys) # type: ignore
# BEGIN BACKWARDS COMPATIBILITY < 0.14.0
self.id_deprecated = derive_keyset_id_deprecated(self.public_keys) # type: ignore
# END BACKWARDS COMPATIBILITY < 0.14.0


class MintKeysets:
Expand Down
9 changes: 9 additions & 0 deletions cashu/mint/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ async def load_keyset(self, derivation_path, autosave=True) -> MintKeyset:

# store the new keyset in the current keysets
self.keysets.keysets[keyset.id] = keyset
# BEGIN BACKWARDS COMPATIBILITY < 0.14.0
self.keysets.keysets[keyset.id_deprecated] = keyset
# END BACKWARDS COMPATIBILITY < 0.14.0
logger.debug(f"Loaded keyset {keyset.id}.")
return keyset

Expand Down Expand Up @@ -132,6 +135,12 @@ async def init_keysets(self, autosave=True) -> None:
logger.debug(f"Generating keys for keyset {v.id}")
v.generate_keys(self.master_key)

# BEGIN BACKWARDS COMPATIBILITY < 0.14.0
# we store all keysets also by their deprecated id
logger.debug(f"Loading deprecated keyset {v.id_deprecated} (new: {v.id})")
self.keysets.keysets[v.id_deprecated] = v
# END BACKWARDS COMPATIBILITY < 0.14.0

logger.debug(
f"Initialized {len(self.keysets.keysets)} keysets from the database."
)
Expand Down
99 changes: 84 additions & 15 deletions cashu/mint/router.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import List, Optional, Union

from fastapi import APIRouter
from fastapi import APIRouter, Request
from loguru import logger

from ..core.base import (
Expand All @@ -13,8 +13,10 @@
GetMeltResponse,
GetMintResponse,
KeysetsResponse,
KeysetsResponse_deprecated,
KeysetsResponseKeyset,
KeysResponse,
KeysResponse_deprecated,
KeysResponseKeyset,
PostMeltRequest,
PostMintRequest,
Expand Down Expand Up @@ -58,7 +60,7 @@ async def info() -> GetInfoResponse:


@router.get(
"/keys",
"/v1/keys",
name="Mint public keys",
summary="Get the public keys of the newest mint keyset",
response_description=(
Expand All @@ -80,7 +82,7 @@ async def keys():


@router.get(
"/keys/{idBase64Urlsafe}",
"/v1/keys/{keyset_id}",
name="Keyset public keys",
summary="Public keys of a specific keyset",
response_description=(
Expand All @@ -89,18 +91,23 @@ async def keys():
),
response_model=KeysResponse,
)
async def keyset_keys(idBase64Urlsafe: str):
async def keyset_keys(keyset_id: str, request: Request):
"""
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).
"""
logger.trace(f"> GET /keys/{idBase64Urlsafe}")
id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
keyset = ledger.get_keyset(keyset_id=id)
keyset = ledger.keysets.keysets.get(id)
logger.trace(f"> GET /keys/{keyset_id}")
# BEGIN BACKWARDS COMPATIBILITY < 0.14.0
# if keyset_id is not hex, we assume it is base64 and sanitize it
try:
int(keyset_id, 16)
except ValueError:
keyset_id = keyset_id.replace("-", "+").replace("_", "/")
# END BACKWARDS COMPATIBILITY < 0.14.0
keyset = ledger.get_keyset(keyset_id=keyset_id)
keyset = ledger.keysets.keysets.get(keyset_id)
if not keyset:
raise CashuError(code=0, detail="Keyset not found.")

keyset_for_response = KeysResponseKeyset(
id=keyset.id,
symbol=keyset.symbol,
Expand All @@ -110,7 +117,7 @@ async def keyset_keys(idBase64Urlsafe: str):


@router.get(
"/keysets",
"/v1/keysets",
name="Active keysets",
summary="Get all active keyset id of the mind",
response_model=KeysetsResponse,
Expand All @@ -120,13 +127,75 @@ async def keysets() -> KeysetsResponse:
"""This endpoint returns a list of keysets that the mint currently supports and will accept tokens from."""
logger.trace("> GET /keysets")
keysets = []
for keyset in ledger.keysets.keysets.values():
keysets.append(
KeysetsResponseKeyset(id=keyset.id, symbol=keyset.symbol, active=True)
)
for id, keyset in ledger.keysets.keysets.items():
keysets.append(KeysetsResponseKeyset(id=id, symbol=keyset.symbol, active=True))
return KeysetsResponse(keysets=keysets)


# DEPRECATED


@router.get(
"/keys",
name="Mint public keys",
summary="Get the public keys of the newest mint keyset",
response_description=(
"A dictionary of all supported token values of the mint and their associated"
" public key of the current keyset."
),
response_model=KeysResponse_deprecated,
deprecated=True,
)
async def keys_deprecated():
"""This endpoint returns a dictionary of all supported token values of the mint and their associated public key."""
logger.trace("> GET /keys")
keyset = ledger.get_keyset()
keys = KeysResponse_deprecated.parse_obj(keyset)
return keys.__root__


@router.get(
"/keys/{idBase64Urlsafe}",
name="Keyset public keys",
summary="Public keys of a specific keyset",
response_description=(
"A dictionary of all supported token values of the mint and their associated"
" public key for a specific keyset."
),
response_model=KeysResponse_deprecated,
deprecated=True,
)
async def keyset_deprecated(idBase64Urlsafe: str):
"""
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).
"""
logger.trace(f"> GET /keys/{idBase64Urlsafe}")
id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
keyset = ledger.get_keyset(keyset_id=id)
keys = KeysResponse_deprecated.parse_obj(keyset)
return keys.__root__


@router.get(
"/keysets",
name="Active keysets",
summary="Get all active keyset id of the mind",
response_model=KeysetsResponse_deprecated,
response_description="A list of all active keyset ids of the mint.",
deprecated=True,
)
async def keysets_deprecated() -> KeysetsResponse_deprecated:
"""This endpoint returns a list of keysets that the mint currently supports and will accept tokens from."""
logger.trace("> GET /keysets")
keysets = KeysetsResponse_deprecated(keysets=ledger.keysets.get_ids())
return keysets


# END DEPRECATED


@router.get(
"/mint",
name="Request mint",
Expand Down
2 changes: 1 addition & 1 deletion cashu/wallet/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ async def receive(
assert keyset_in_token
# we get the keyset from the db
mint_keysets = await get_keyset(id=keyset_in_token, db=wallet.db)
assert mint_keysets, Exception("we don't know this keyset")
assert mint_keysets, Exception(f"we don't know this keyset: {keyset_in_token}")
assert mint_keysets.mint_url, Exception("we don't know this mint's URL")
# now we have the URL
mint_wallet = await Wallet.with_db(
Expand Down
6 changes: 6 additions & 0 deletions cashu/wallet/protocols.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from typing import Protocol

import requests

from ..core.crypto.secp import PrivateKey
from ..core.db import Database

Expand All @@ -14,3 +16,7 @@ class SupportsDb(Protocol):

class SupportsKeysets(Protocol):
keyset_id: str


class SupportsRequests(Protocol):
s: requests.Session
42 changes: 33 additions & 9 deletions cashu/wallet/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import time
import uuid
from itertools import groupby
from typing import Dict, List, Optional, Tuple, Union
from typing import Callable, Dict, List, Optional, Tuple, Union

import requests
from bip32 import BIP32
Expand Down Expand Up @@ -62,6 +62,7 @@
update_proof_reserved,
)
from . import migrations
from .deprecated import LedgerAPIDeprecated
from .htlc import WalletHTLC
from .p2pk import WalletP2PK
from .secrets import WalletSecrets
Expand Down Expand Up @@ -99,7 +100,7 @@ async def wrapper(self, *args, **kwargs):
return wrapper


class LedgerAPI(object):
class LedgerAPI(LedgerAPIDeprecated, object):
keys: WalletKeyset # holds current keys of mint
keyset_id: str # holds id of current keyset
public_keys: Dict[int, PublicKey] # holds public keys of
Expand All @@ -119,11 +120,17 @@ async def _init_s(self):
return

@staticmethod
def raise_on_error(resp: Response) -> None:
def raise_on_error(
resp: Response,
call_404: Optional[Callable] = None,
call_args: List = [],
call_kwargs: Dict = {},
) -> None:
"""Raises an exception if the response from the mint contains an error.
Args:
resp_dict (Response): Response dict (previously JSON) from mint
call_instead (Callable): Function to call instead of raising an exception
Raises:
Exception: if the response contains an error
Expand All @@ -134,6 +141,11 @@ def raise_on_error(resp: Response) -> None:
error_message = f"Mint Error: {resp_dict['detail']}"
if "code" in resp_dict:
error_message += f" (Code: {resp_dict['code']})"
# BEGIN BACKWARDS COMPATIBILITY < 0.14.0
# if the error is a 404, we assume that the mint is not upgraded yet
if call_404 and resp.status_code == 404:
return call_404(*call_args, **call_kwargs)
# END BACKWARDS COMPATIBILITY < 0.14.0
raise Exception(error_message)
# raise for status if no error
resp.raise_for_status()
Expand Down Expand Up @@ -247,9 +259,13 @@ async def _get_keys(self, url: str) -> WalletKeyset:
Exception: If no keys are received from the mint
"""
resp = self.s.get(
url + "/keys",
url + "/v1/keys",
)
self.raise_on_error(
resp,
call_404=self._get_keys_deprecated, # backwards compatibility < 0.14.0
call_args=[url],
)
self.raise_on_error(resp)
keys_dict: dict = resp.json()
assert len(keys_dict), Exception("did not receive any keys")
keys = KeysResponse.parse_obj(keys_dict)
Expand Down Expand Up @@ -277,9 +293,13 @@ async def _get_keys_of_keyset(self, url: str, keyset_id: str) -> WalletKeyset:
"""
keyset_id_urlsafe = keyset_id.replace("+", "-").replace("/", "_")
resp = self.s.get(
url + f"/keys/{keyset_id_urlsafe}",
url + f"/v1/keys/{keyset_id_urlsafe}",
)
self.raise_on_error(
resp,
call_404=self._get_keys_of_keyset_deprecated, # backwards compatibility < 0.14.0
call_args=[url, keyset_id],
)
self.raise_on_error(resp)
keys_dict = resp.json()
assert len(keys_dict), Exception("did not receive any keys")
keys = KeysResponse.parse_obj(keys_dict)
Expand All @@ -304,9 +324,13 @@ async def _get_keyset_ids(self, url: str) -> List[str]:
Exception: If no keysets are received from the mint
"""
resp = self.s.get(
url + "/keysets",
url + "/v1/keysets",
)
self.raise_on_error(
resp,
call_404=self._get_keyset_ids_deprecated, # backwards compatibility < 0.14.0
call_args=[url],
)
self.raise_on_error(resp)
keysets_dict = resp.json()
keysets = KeysetsResponse.parse_obj(keysets_dict)
assert len(keysets.keysets), Exception("did not receive any keysets")
Expand Down
Loading

0 comments on commit c91e799

Please sign in to comment.