diff --git a/README.md b/README.md index 190c25b..f50d34a 100644 --- a/README.md +++ b/README.md @@ -119,15 +119,7 @@ MINT_URL=https://8333.space:3338 ```bash cashu info ``` - -Returns: -```bash -Version: 0.14.0 -Debug: False -Cashu dir: /home/user/.cashu -Wallet: wallet -Mint URL: https://8333.space:3338 -``` +This command shows information about your wallet. #### Check balance ```bash @@ -159,21 +151,11 @@ You should see the encoded token. Copy the token and send it to another user suc cashuAeyJwcm9vZnMiOiBbey... ``` -You can now see that your available balance has dropped by the amount that you reserved for sending if you enter `cashu balance`: -```bash -Balance: 420 sat -``` - #### Receive tokens To receive tokens, another user enters: ```bash cashu receive cashuAeyJwcm9vZnMiOiBbey... ``` -You should see the balance increase: -```bash -Balance: 0 sat -Balance: 69 sat -``` # Starting the wallet API daemon Nutshell wallet can be used in daemon mode that can be controlled through a REST API: diff --git a/cashu/core/base.py b/cashu/core/base.py index b5e14f8..45f3c31 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -532,6 +532,9 @@ class TokenV3(BaseModel): 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 @@ class TokenV3(BaseModel): 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/core/settings.py b/cashu/core/settings.py index d4ff75b..1c35e21 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -8,7 +8,7 @@ from pydantic import BaseSettings, Extra, Field env = Env() -VERSION = "0.14.0" +VERSION = "0.14.1" def find_env_file(): diff --git a/cashu/nostr/client/client.py b/cashu/nostr/client/client.py index 26d523f..3af7303 100644 --- a/cashu/nostr/client/client.py +++ b/cashu/nostr/client/client.py @@ -122,7 +122,7 @@ class NostrClient: 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 @@ class NostrClient: 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 c62797a..50425dc 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 b22d2f6..5cd6c19 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 4248582..b6099cc 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 @@ async def receive_nostr( # 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 @@ async def receive_nostr( name="Nostr DM", ) t.start() + return client diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index b3c8baf..bc146c4 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -94,7 +94,7 @@ def async_set_httpx_client(func): 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) @@ -198,9 +198,9 @@ class LedgerAPI(object): if keyset_id and keyset_id != keyset.id: # NOTE: Because of the upcoming change of how to calculate keyset ids - # with version 0.14.0, we overwrite the calculated keyset id with the + # with version 0.15.0, we overwrite the calculated keyset id with the # requested one. This is a temporary fix and should be removed once all - # ecash is transitioned to 0.14.0. + # ecash is transitioned to 0.15.0. logger.debug( f"Keyset ID mismatch: {keyset_id} != {keyset.id}. This can happen due" " to a version upgrade." @@ -732,9 +732,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets): 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 +926,8 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets): ): 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, diff --git a/setup.py b/setup.py index dd56a08..799de95 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli.cli:cli"]} setuptools.setup( name="cashu", - version="0.14.0", + version="0.14.1", description="Ecash wallet and mint for Bitcoin Lightning", long_description=long_description, long_description_content_type="text/markdown",