import asyncio import threading import time import click from click import Context from requests.exceptions import ConnectionError from cashu.core.settings import NOSTR_PRIVATE_KEY, NOSTR_RELAYS from cashu.nostr.nostr.client.client import NostrClient from cashu.nostr.nostr.event import Event from cashu.nostr.nostr.key import PublicKey from cashu.wallet.cli.cli_helpers import get_mint_wallet from cashu.wallet.crud import ( get_nostr_last_check_timestamp, set_nostr_last_check_timestamp, ) from cashu.wallet.wallet import Wallet async def nip5_to_pubkey(wallet: Wallet, address: str): """ Retrieves the nostr public key of a NIP-05 identifier. """ # we will be using the requests session from the wallet await wallet._init_s() # if no username is given, use default _ (NIP-05 stuff) if "@" not in address: address = "_@" + address # now we can use it user, host = address.split("@") resp_dict = {} try: resp = wallet.s.get( f"https://{host}/.well-known/nostr.json?name={user}", ) resp.raise_for_status() except ConnectionError: raise Exception(f"Could not connect to {host}") except Exception as e: raise e resp_dict = resp.json() assert "names" in resp_dict, Exception(f"did not receive any names from {host}") assert user in resp_dict["names"], Exception(f"{user}@{host} not found") pubkey = resp_dict["names"][user] return pubkey async def send_nostr(ctx: Context, amount: int, pubkey: str, verbose: bool, yes: bool): """ Sends tokens via nostr. """ # load a wallet for the chosen mint wallet = await get_mint_wallet(ctx) if "@" in pubkey or "." in pubkey: # matches user@domain.com and domain.com (which is _@domain.com) pubkey = await nip5_to_pubkey(wallet, pubkey) await wallet.load_proofs() _, send_proofs = await wallet.split_to_send( wallet.proofs, amount, set_reserved=True ) token = await wallet.serialize_proofs(send_proofs) if pubkey.startswith("npub"): pubkey_to = PublicKey().from_npub(pubkey) else: pubkey_to = PublicKey(bytes.fromhex(pubkey)) print("") print(token) if not yes: print("") click.confirm( f"Send {amount} sat to {pubkey_to.bech32()}?", abort=True, default=True, ) client = NostrClient(private_key=NOSTR_PRIVATE_KEY or "", relays=NOSTR_RELAYS) if verbose and not NOSTR_PRIVATE_KEY: # we generated a random key if none was present print(f"Your nostr private key: {client.private_key.bech32()}") client.dm(token, pubkey_to) print(f"Token sent to {pubkey_to.bech32()}") await asyncio.sleep(5) client.close() async def receive_nostr(ctx: Context, verbose: bool): if NOSTR_PRIVATE_KEY is None: print( "Warning: No nostr private key set! You don't have NOSTR_PRIVATE_KEY set in your .env file. I will create a random private key for this session but I will not remember it." ) print("") client = NostrClient(private_key=NOSTR_PRIVATE_KEY, relays=NOSTR_RELAYS) print(f"Your nostr public key: {client.public_key.bech32()}") if verbose: print(f"Your nostr private key (do not share!): {client.private_key.bech32()}") await asyncio.sleep(2) def get_token_callback(event: Event, decrypted_content): if verbose: print( f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}" ) try: # call the receive method from cashu.wallet.cli.cli import receive asyncio.run(receive(ctx, decrypted_content, "")) except Exception as e: pass # determine timestamp of last check so we don't scan all historical DMs wallet: Wallet = ctx.obj["WALLET"] last_check = await get_nostr_last_check_timestamp(db=wallet.db) if last_check: last_check -= 60 * 60 # 1 hour tolerance await set_nostr_last_check_timestamp(timestamp=int(time.time()), db=wallet.db) t = threading.Thread( target=client.get_dm, args=(client.public_key, get_token_callback, {"since": last_check}), name="Nostr DM", ) t.start()