Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into dlc
Browse files Browse the repository at this point in the history
  • Loading branch information
lollerfirst committed Jul 10, 2024
2 parents cc4aeb5 + 26b9495 commit 97edb79
Show file tree
Hide file tree
Showing 12 changed files with 330 additions and 185 deletions.
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
164 changes: 143 additions & 21 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 @@ -156,7 +157,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 @@ -770,6 +774,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 @@ -781,33 +827,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"

class Config:
allow_population_by_field_name = True

def get_proofs(self):
@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]))

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

def get_mints(self):
@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 @@ -834,10 +907,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 @@ -886,7 +979,8 @@ class TokenV4Token(BaseModel):
p: List[TokenV4Proof]


class TokenV4(BaseModel):
@dataclass
class TokenV4(Token):
# mint URL
m: str
# unit
Expand All @@ -902,14 +996,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 @@ -940,17 +1045,25 @@ def proofs(self) -> List[Proof]:
for token in self.t
for p in token.p
]

@property
def dlc_root(self) -> str:
return self.r

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

@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 @@ -984,7 +1097,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 @@ -1040,7 +1153,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 Down Expand Up @@ -1068,6 +1181,15 @@ 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),
)

# -------- DLC STUFF --------

class DiscreteLogContract(BaseModel):
Expand Down
12 changes: 6 additions & 6 deletions cashu/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,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 @@ -185,8 +183,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

0 comments on commit 97edb79

Please sign in to comment.