Skip to content

Commit

Permalink
wip: handle tokenv4 if the keyset is base64
Browse files Browse the repository at this point in the history
  • Loading branch information
callebtc committed Jul 9, 2024
1 parent 8eea541 commit 4ceb17c
Show file tree
Hide file tree
Showing 10 changed files with 154 additions and 65 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
90 changes: 78 additions & 12 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import base64
import json
import math
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from sqlite3 import Row
Expand Down Expand Up @@ -752,6 +753,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,25 +806,33 @@ def to_dict(self, include_dleq=False):
return return_dict


class TokenV3(BaseModel):
class TokenV3(BaseModel, 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
unit: str = "sat"

def get_proofs(self):
@property
def proofs(self):
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):
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):
return list(set([p.id for p in self.proofs]))

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

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

def serialize_to_dict(self, include_dleq=False):
Expand Down Expand Up @@ -868,7 +919,7 @@ class TokenV4Token(BaseModel):
p: List[TokenV4Proof]


class TokenV4(BaseModel):
class TokenV4(BaseModel, Token):
# mint URL
m: str
# unit
Expand All @@ -882,14 +933,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 +983,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 +1026,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
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
16 changes: 12 additions & 4 deletions cashu/wallet/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from click import Context
from loguru import logger

from ...core.base import Invoice, Method, MintQuoteState, TokenV3, TokenV4, Unit
from ...core.base import Invoice, Method, MintQuoteState, TokenV4, Unit
from ...core.helpers import sum_proofs
from ...core.json_rpc.base import JSONRPCNotficationParams
from ...core.logging import configure_logger
Expand Down Expand Up @@ -672,8 +672,8 @@ async def burn(ctx: Context, token: str, all: bool, force: bool, delete: str):
proofs = [proof for proof in reserved_proofs if proof["send_id"] == delete]
else:
# check only the specified ones
token_obj = TokenV3.deserialize(token)
proofs = token_obj.get_proofs()
tokenObj = deserialize_token_from_string(token)
proofs = tokenObj.proofs

if delete:
await wallet.invalidate(proofs)
Expand Down Expand Up @@ -709,6 +709,14 @@ async def burn(ctx: Context, token: str, all: bool, force: bool, delete: str):
@coro
async def pending(ctx: Context, legacy, number: int, offset: int):
wallet: Wallet = ctx.obj["WALLET"]
wallet = await Wallet.with_db(
url=wallet.url,
db=wallet.db.db_location,
name=wallet.name,
skip_db_read=False,
unit=wallet.unit.name,
load_all_keysets=True,
)
reserved_proofs = await get_reserved_proofs(wallet.db)
if len(reserved_proofs):
print("--------------------------\n")
Expand Down Expand Up @@ -737,7 +745,7 @@ async def pending(ctx: Context, legacy, number: int, offset: int):
).strftime("%Y-%m-%d %H:%M:%S")
print(
f"#{i} Amount:"
f" {wallet.unit.str(sum_proofs(grouped_proofs))} Time:"
f" {Unit[token_obj.unit].str(sum_proofs(grouped_proofs))} Time:"
f" {reserved_date} ID: {key} Mint: {mint}\n"
)
print(f"{token}\n")
Expand Down
34 changes: 19 additions & 15 deletions cashu/wallet/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from loguru import logger

from ..core.base import TokenV3, TokenV4
from ..core.base import Token, TokenV3, TokenV4
from ..core.db import Database
from ..core.helpers import sum_proofs
from ..core.migrations import migrate_databases
Expand Down Expand Up @@ -34,7 +34,7 @@ async def list_mints(wallet: Wallet):
return mints


async def redeem_TokenV3_multimint(wallet: Wallet, token: TokenV3) -> Wallet:
async def redeem_TokenV3(wallet: Wallet, token: TokenV3) -> Wallet:
"""
Helper function to iterate thruogh a token with multiple mints and redeem them from
these mints one keyset at a time.
Expand All @@ -46,9 +46,7 @@ async def redeem_TokenV3_multimint(wallet: Wallet, token: TokenV3) -> Wallet:
token.unit = keysets[0].unit.name

for t in token.token:
assert t.mint, Exception(
"redeem_TokenV3_multimint: multimint redeem without URL"
)
assert t.mint, Exception("redeem_TokenV3: multimint redeem without URL")
mint_wallet = await Wallet.with_db(
t.mint,
os.path.join(settings.cashu_dir, wallet.name),
Expand All @@ -74,12 +72,23 @@ async def redeem_TokenV4(wallet: Wallet, token: TokenV4) -> Wallet:
return wallet


def deserialize_token_from_string(token: str) -> TokenV4:
# deserialize token
async def redeem_universal(wallet: Wallet, token: Token) -> Wallet:
if isinstance(token, TokenV3):
return await redeem_TokenV3(wallet, token)
if isinstance(token, TokenV4):
return await redeem_TokenV4(wallet, token)
raise Exception("Invalid token type")


def deserialize_token_from_string(token: str) -> Token:
# deserialize token
if token.startswith("cashuA"):
tokenV3Obj = TokenV3.deserialize(token)
return TokenV4.from_tokenv3(tokenV3Obj)
try:
return TokenV4.from_tokenv3(tokenV3Obj)
except ValueError as e:
logger.debug(f"Error converting TokenV3 to TokenV4: {e}")
return tokenV3Obj
if token.startswith("cashuB"):
tokenObj = TokenV4.deserialize(token)
return tokenObj
Expand All @@ -89,14 +98,9 @@ def deserialize_token_from_string(token: str) -> TokenV4:

async def receive(
wallet: Wallet,
tokenObj: TokenV4,
token: Token,
) -> Wallet:
# redeem tokens with new wallet instances
mint_wallet = await redeem_TokenV4(
wallet,
tokenObj,
)

mint_wallet = await redeem_universal(wallet, token)
# reload main wallet so the balance updates
await wallet.load_proofs(reload=True)
return mint_wallet
Expand Down
11 changes: 3 additions & 8 deletions cashu/wallet/nostr.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from httpx import ConnectError
from loguru import logger

from ..core.base import TokenV4
from ..core.base import Token
from ..core.settings import settings
from ..nostr.client.client import NostrClient
from ..nostr.event import Event
Expand Down Expand Up @@ -127,18 +127,13 @@ def get_token_callback(event: Event, decrypted_content: str):
for w in words:
try:
# call the receive method
tokenObj: TokenV4 = deserialize_token_from_string(w)
tokenObj: Token = deserialize_token_from_string(w)
print(
f"Receiving {tokenObj.amount} sat on mint"
f" {tokenObj.mint} from nostr user {event.public_key} at"
f" {date_str}"
)
asyncio.run(
receive(
wallet,
tokenObj,
)
)
asyncio.run(receive(wallet, tokenObj))
logger.trace(
"Nostr: setting last check timestamp to"
f" {event.created_at} ({date_str})"
Expand Down
Loading

0 comments on commit 4ceb17c

Please sign in to comment.