diff --git a/cashu/core/base.py b/cashu/core/base.py index b5e14f8d..45f3c317 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -532,6 +532,9 @@ def get_amount(self): def get_keysets(self): return list(set([p.id for p in self.get_proofs()])) + def get_mints(self): + return list(set([t.mint for t in self.token if t.mint])) + @classmethod def deserialize(cls, tokenv3_serialized: str) -> "TokenV3": """ @@ -542,6 +545,9 @@ def deserialize(cls, tokenv3_serialized: str) -> "TokenV3": f"Token prefix not valid. Expected {prefix}." ) token_base64 = tokenv3_serialized[len(prefix) :] + # if base64 string is not a multiple of 4, pad it with "=" + token_base64 += "=" * (4 - len(token_base64) % 4) + token = json.loads(base64.urlsafe_b64decode(token_base64)) return cls.parse_obj(token) diff --git a/cashu/nostr/client/client.py b/cashu/nostr/client/client.py index 26d523fa..3af7303b 100644 --- a/cashu/nostr/client/client.py +++ b/cashu/nostr/client/client.py @@ -122,7 +122,7 @@ def get_dm(self, sender_publickey: PublicKey, callback_func=None, filter_kwargs= message = json.dumps(request) self.relay_manager.publish_message(message) - while True: + while any([r.connected for r in self.relay_manager.relays.values()]): while self.relay_manager.message_pool.has_events(): event_msg = self.relay_manager.message_pool.get_event() if "?iv=" in event_msg.event.content: @@ -143,7 +143,7 @@ def get_dm(self, sender_publickey: PublicKey, callback_func=None, filter_kwargs= time.sleep(0.1) def subscribe(self, callback_func=None): - while True: + while any([r.connected for r in self.relay_manager.relays.values()]): while self.relay_manager.message_pool.has_events(): event_msg = self.relay_manager.message_pool.get_event() if callback_func: diff --git a/cashu/wallet/helpers.py b/cashu/wallet/helpers.py index c62797a0..50425dc6 100644 --- a/cashu/wallet/helpers.py +++ b/cashu/wallet/helpers.py @@ -107,16 +107,13 @@ def deserialize_token_from_string(token: str) -> TokenV3: except Exception: pass - # ----- receive token ----- + if token.startswith("cashu"): + tokenObj = TokenV3.deserialize(token) + assert len(tokenObj.token), Exception("no proofs in token") + assert len(tokenObj.token[0].proofs), Exception("no proofs in token") + return tokenObj - # deserialize token - # dtoken = json.loads(base64.urlsafe_b64decode(token)) - tokenObj = TokenV3.deserialize(token) - - # tokenObj = TokenV2.parse_obj(dtoken) - assert len(tokenObj.token), Exception("no proofs in token") - assert len(tokenObj.token[0].proofs), Exception("no proofs in token") - return tokenObj + raise Exception("Invalid token") async def receive( diff --git a/cashu/wallet/migrations.py b/cashu/wallet/migrations.py index b22d2f6a..5cd6c19e 100644 --- a/cashu/wallet/migrations.py +++ b/cashu/wallet/migrations.py @@ -139,24 +139,24 @@ async def m007_nostr(db: Database): """ Stores timestamps of nostr operations. """ - # async with db.connect() as conn: - # await conn.execute(""" - # CREATE TABLE IF NOT EXISTS nostr ( - # type TEXT NOT NULL, - # last TIMESTAMP DEFAULT NULL - # ) - # """) - # await conn.execute( - # """ - # INSERT INTO nostr - # (type, last) - # VALUES (?, ?) - # """, - # ( - # "dm", - # None, - # ), - # ) + async with db.connect() as conn: + await conn.execute(""" + CREATE TABLE IF NOT EXISTS nostr ( + type TEXT NOT NULL, + last TIMESTAMP DEFAULT NULL + ) + """) + await conn.execute( + """ + INSERT INTO nostr + (type, last) + VALUES (?, ?) + """, + ( + "dm", + None, + ), + ) async def m008_keysets_add_public_keys(db: Database): diff --git a/cashu/wallet/nostr.py b/cashu/wallet/nostr.py index 42485820..b6099cc9 100644 --- a/cashu/wallet/nostr.py +++ b/cashu/wallet/nostr.py @@ -1,10 +1,13 @@ import asyncio +import datetime import threading import click from httpx import ConnectError from loguru import logger +from cashu.core.base import TokenV3 + from ..core.settings import settings from ..nostr.client.client import NostrClient from ..nostr.event import Event @@ -97,7 +100,7 @@ async def send_nostr( async def receive_nostr( wallet: Wallet, -): +) -> NostrClient: if settings.nostr_private_key is None: print( "Warning: No nostr private key set! You don't have NOSTR_PRIVATE_KEY set in" @@ -113,18 +116,28 @@ async def receive_nostr( await asyncio.sleep(2) def get_token_callback(event: Event, decrypted_content: str): + date_str = datetime.datetime.fromtimestamp(event.created_at).strftime( + "%Y-%m-%d %H:%M:%S" + ) logger.debug( - f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}" + f"From {event.public_key[:3]}..{event.public_key[-3:]} on {date_str}:" + f" {decrypted_content}" ) # split the content into words words = decrypted_content.split(" ") for w in words: try: logger.trace( - f"Nostr: setting last check timestamp to {event.created_at}" + "Nostr: setting last check timestamp to" + f" {event.created_at} ({date_str})" ) # call the receive method - tokenObj = deserialize_token_from_string(w) + tokenObj: TokenV3 = deserialize_token_from_string(w) + print( + f"Receiving {tokenObj.get_amount()} sat on mint" + f" {tokenObj.get_mints()[0]} from nostr user {event.public_key} at" + f" {date_str}" + ) asyncio.run( receive( wallet, @@ -143,8 +156,11 @@ def get_token_callback(event: Event, decrypted_content: str): # determine timestamp of last check so we don't scan all historical DMs last_check = await get_nostr_last_check_timestamp(db=wallet.db) - logger.debug(f"Last check: {last_check}") if last_check: + date_str = datetime.datetime.fromtimestamp(last_check).strftime( + "%Y-%m-%d %H:%M:%S" + ) + logger.debug(f"Last check: {date_str}") last_check -= 60 * 60 # 1 hour tolerance logger.debug("Starting Nostr DM thread") @@ -154,3 +170,4 @@ def get_token_callback(event: Event, decrypted_content: str): name="Nostr DM", ) t.start() + return client diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index b3c8baf4..92d87297 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -94,7 +94,7 @@ async def wrapper(self, *args, **kwargs): proxies=proxies_dict, # type: ignore headers=headers_dict, base_url=self.url, - timeout=None if settings.debug else 5, + timeout=5, ) return await func(self, *args, **kwargs) @@ -450,6 +450,9 @@ async def split( logger.debug("Calling split. POST /split") split_payload = PostSplitRequest(proofs=proofs, outputs=outputs) + # BEGIN: backwards compatibility pre 0.13.0 + split_payload.amount = outputs[0].amount + # construct payload def _splitrequest_include_fields(proofs: List[Proof]): """strips away fields from the model that aren't necessary for the /split""" @@ -732,9 +735,7 @@ async def redeem( proofs (List[Proof]): Proofs to be redeemed. """ # verify DLEQ of incoming proofs - logger.debug("Verifying DLEQ of incoming proofs.") self.verify_proofs_dleq(proofs) - logger.debug("DLEQ verified.") return await self.split(proofs, sum_proofs(proofs)) async def split( @@ -928,7 +929,8 @@ def verify_proofs_dleq(self, proofs: List[Proof]): ): raise Exception("DLEQ proof invalid.") else: - logger.debug("DLEQ proof valid.") + logger.trace("DLEQ proof valid.") + logger.debug("Verified incoming DLEQ proofs.") async def _construct_proofs( self,