Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Tokenv4 handling of base64 keysets #575

Merged
merged 6 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ This command runs the mint on your local computer. Skip this step if you want to
## Docker

```
docker run -d -p 3338:3338 --name nutshell -e MINT_BACKEND_BOLT11_SAT=FakeWallet -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY cashubtc/nutshell:0.15.3 poetry run mint
docker run -d -p 3338:3338 --name nutshell -e MINT_BACKEND_BOLT11_SAT=FakeWallet -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY cashubtc/nutshell:0.16.0 poetry run mint
```

## From this repository
Expand Down
158 changes: 138 additions & 20 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import base64
import json
import math
from dataclasses import dataclass
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum
from sqlite3 import Row
from typing import Any, Dict, List, Optional, Union
Expand Down Expand Up @@ -148,7 +149,10 @@ def __init__(self, **data):

@classmethod
def from_dict(cls, proof_dict: dict):
if proof_dict.get("dleq") and isinstance(proof_dict["dleq"], str):
if proof_dict.get("dleq") and isinstance(proof_dict["dleq"], dict):
proof_dict["dleq"] = DLEQWallet(**proof_dict["dleq"])
elif proof_dict.get("dleq") and isinstance(proof_dict["dleq"], str):
# Proofs read from the database have the DLEQ proof as a string
proof_dict["dleq"] = DLEQWallet(**json.loads(proof_dict["dleq"]))
else:
# overwrite the empty string with None
Expand Down Expand Up @@ -752,6 +756,48 @@ def generate_keys(self):
# ------- TOKEN -------


class Token(ABC):
@property
@abstractmethod
def proofs(self) -> List[Proof]:
...

@property
@abstractmethod
def amount(self) -> int:
...

@property
@abstractmethod
def mint(self) -> str:
...

@property
@abstractmethod
def keysets(self) -> List[str]:
...

@property
@abstractmethod
def memo(self) -> Optional[str]:
...

@memo.setter
@abstractmethod
def memo(self, memo: Optional[str]):
...

@property
@abstractmethod
def unit(self) -> str:
...

@unit.setter
@abstractmethod
def unit(self, unit: str):
...


class TokenV3Token(BaseModel):
mint: Optional[str] = None
proofs: List[Proof]
Expand All @@ -763,33 +809,60 @@ def to_dict(self, include_dleq=False):
return return_dict


class TokenV3(BaseModel):
@dataclass
class TokenV3(Token):
"""
A Cashu token that includes proofs and their respective mints. Can include proofs from multiple different mints and keysets.
"""

token: List[TokenV3Token] = []
memo: Optional[str] = None
unit: Optional[str] = None
token: List[TokenV3Token] = field(default_factory=list)
_memo: Optional[str] = None
_unit: str = "sat"

def get_proofs(self):
class Config:
allow_population_by_field_name = True

@property
def proofs(self) -> List[Proof]:
return [proof for token in self.token for proof in token.proofs]

def get_amount(self):
return sum([p.amount for p in self.get_proofs()])
@property
def amount(self) -> int:
return sum([p.amount for p in self.proofs])

def get_keysets(self):
return list(set([p.id for p in self.get_proofs()]))
@property
def keysets(self) -> List[str]:
return list(set([p.id for p in self.proofs]))

def get_mints(self):
@property
def mint(self) -> str:
return self.mints[0]

@property
def mints(self) -> List[str]:
return list(set([t.mint for t in self.token if t.mint]))

@property
def memo(self) -> Optional[str]:
return str(self._memo) if self._memo else None

@memo.setter
def memo(self, memo: Optional[str]):
self._memo = memo

@property
def unit(self) -> str:
return self._unit

@unit.setter
def unit(self, unit: str):
self._unit = unit

def serialize_to_dict(self, include_dleq=False):
return_dict = dict(token=[t.to_dict(include_dleq) for t in self.token])
if self.memo:
return_dict.update(dict(memo=self.memo)) # type: ignore
if self.unit:
return_dict.update(dict(unit=self.unit)) # type: ignore
return_dict.update(dict(unit=self.unit)) # type: ignore
return return_dict

@classmethod
Expand All @@ -816,10 +889,30 @@ def serialize(self, include_dleq=False) -> str:
tokenv3_serialized = prefix
# encode the token as a base64 string
tokenv3_serialized += base64.urlsafe_b64encode(
json.dumps(self.serialize_to_dict(include_dleq)).encode()
json.dumps(
self.serialize_to_dict(include_dleq), separators=(",", ":")
).encode()
).decode()
return tokenv3_serialized

@classmethod
def parse_obj(cls, token_dict: Dict[str, Any]):
if not token_dict.get("token"):
raise Exception("Token must contain proofs.")
token: List[Dict[str, Any]] = token_dict.get("token") or []
assert token, "Token must contain proofs."
return cls(
token=[
TokenV3Token(
mint=t.get("mint"),
proofs=[Proof.from_dict(p) for p in t.get("proofs") or []],
)
for t in token
],
_memo=token_dict.get("memo"),
_unit=token_dict.get("unit") or "sat",
)


class TokenV4DLEQ(BaseModel):
"""
Expand Down Expand Up @@ -868,7 +961,8 @@ class TokenV4Token(BaseModel):
p: List[TokenV4Proof]


class TokenV4(BaseModel):
@dataclass
class TokenV4(Token):
# mint URL
m: str
# unit
Expand All @@ -882,14 +976,25 @@ class TokenV4(BaseModel):
def mint(self) -> str:
return self.m

def set_mint(self, mint: str):
self.m = mint

@property
def memo(self) -> Optional[str]:
return self.d

@memo.setter
def memo(self, memo: Optional[str]):
self.d = memo

@property
def unit(self) -> str:
return self.u

@unit.setter
def unit(self, unit: str):
self.u = unit

@property
def amounts(self) -> List[int]:
return [p.a for token in self.t for p in token.p]
Expand Down Expand Up @@ -921,12 +1026,16 @@ def proofs(self) -> List[Proof]:
for p in token.p
]

@property
def keysets(self) -> List[str]:
return list(set([p.i.hex() for p in self.t]))

@classmethod
def from_tokenv3(cls, tokenv3: TokenV3):
if not len(tokenv3.get_mints()) == 1:
if not len(tokenv3.mints) == 1:
raise Exception("TokenV3 must contain proofs from only one mint.")

proofs = tokenv3.get_proofs()
proofs = tokenv3.proofs
proofs_by_id: Dict[str, List[Proof]] = {}
for proof in proofs:
proofs_by_id.setdefault(proof.id, []).append(proof)
Expand Down Expand Up @@ -960,7 +1069,7 @@ def from_tokenv3(cls, tokenv3: TokenV3):
# set memo
cls.d = tokenv3.memo
# set mint
cls.m = tokenv3.get_mints()[0]
cls.m = tokenv3.mint
# set unit
cls.u = tokenv3.unit or "sat"
return cls(t=cls.t, d=cls.d, m=cls.m, u=cls.u)
Expand Down Expand Up @@ -1016,7 +1125,7 @@ def deserialize(cls, tokenv4_serialized: str) -> "TokenV4":
return cls.parse_obj(token)

def to_tokenv3(self) -> TokenV3:
tokenv3 = TokenV3()
tokenv3 = TokenV3(_memo=self.d, _unit=self.u)
for token in self.t:
tokenv3.token.append(
TokenV3Token(
Expand All @@ -1043,3 +1152,12 @@ def to_tokenv3(self) -> TokenV3:
)
)
return tokenv3

@classmethod
def parse_obj(cls, token_dict: dict):
return cls(
m=token_dict["m"],
u=token_dict["u"],
t=[TokenV4Token(**t) for t in token_dict["t"]],
d=token_dict.get("d", None),
)
12 changes: 6 additions & 6 deletions cashu/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,8 @@ class PostMintQuoteRequest(BaseModel):
class PostMintQuoteResponse(BaseModel):
quote: str # quote id
request: str # input payment request
paid: Optional[
bool
] # whether the request has been paid # DEPRECATED as per NUT PR #141
state: str # state of the quote
paid: Optional[bool] # DEPRECATED as per NUT-04 PR #141
state: Optional[str] # state of the quote
expiry: Optional[int] # expiry of the quote

@classmethod
Expand Down Expand Up @@ -180,8 +178,10 @@ class PostMeltQuoteResponse(BaseModel):
quote: str # quote id
amount: int # input amount
fee_reserve: int # input fee reserve
paid: bool # whether the request has been paid # DEPRECATED as per NUT PR #136
state: str # state of the quote
paid: Optional[
bool
] # whether the request has been paid # DEPRECATED as per NUT PR #136
state: Optional[str] # state of the quote
expiry: Optional[int] # expiry of the quote
payment_preimage: Optional[str] = None # payment preimage
change: Union[List[BlindedSignature], None] = None
Expand Down
4 changes: 2 additions & 2 deletions cashu/wallet/api/api_helpers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from ...core.base import TokenV4
from ...core.base import Token
from ...wallet.crud import get_keysets


async def verify_mints(wallet, tokenObj: TokenV4):
async def verify_mints(wallet, tokenObj: Token):
# verify mints
mint = tokenObj.mint
mint_keysets = await get_keysets(mint_url=mint, db=wallet.db)
Expand Down
6 changes: 3 additions & 3 deletions cashu/wallet/api/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from fastapi import APIRouter, Query

from ...core.base import TokenV3, TokenV4
from ...core.base import Token, TokenV3
from ...core.helpers import sum_proofs
from ...core.settings import settings
from ...lightning.base import (
Expand Down Expand Up @@ -261,7 +261,7 @@ async def receive_command(
wallet = await mint_wallet()
initial_balance = wallet.available_balance
if token:
tokenObj: TokenV4 = deserialize_token_from_string(token)
tokenObj: Token = deserialize_token_from_string(token)
await verify_mints(wallet, tokenObj)
await receive(wallet, tokenObj)
elif nostr:
Expand Down Expand Up @@ -317,7 +317,7 @@ async def burn(
else:
# check only the specified ones
tokenObj = TokenV3.deserialize(token)
proofs = tokenObj.get_proofs()
proofs = tokenObj.proofs

if delete:
await wallet.invalidate(proofs)
Expand Down
Loading
Loading