diff --git a/nostr_dvm/utils/nut_wallet_utils.py b/nostr_dvm/utils/nut_wallet_utils.py index 49c90b1..9c44a53 100644 --- a/nostr_dvm/utils/nut_wallet_utils.py +++ b/nostr_dvm/utils/nut_wallet_utils.py @@ -563,9 +563,15 @@ class NutZapWallet: break # If that's not the case, lets look or mints we both trust, take the first one. if not sufficent_budget: - mint_url = next(i for i in nut_wallet.mints if i in mints) - mint = self.get_mint(nut_wallet, mint_url) - if mint.available_balance() < amount: + try: + mint_url = next(i for i in nut_wallet.mints if i in mints) + if mint_url is not None: + mint = self.get_mint(nut_wallet, mint_url) + if mint.available_balance() < amount: + await self.handle_low_balance_on_mint(nut_wallet, mint_url, mint, amount, client, keys) + except StopIteration: + mint = self.get_mint(nut_wallet, mints[0]) + mint_url = mint.mint_url await self.handle_low_balance_on_mint(nut_wallet, mint_url, mint, amount, client, keys) # If that's not the case, iterate over the recipents mints and try to mint there. This might be a bit dangerous as not all mints might give cashu, so loss of ln is possible diff --git a/tutorials/03_client.py b/tutorials/03_client.py index 66487e0..04bb667 100644 --- a/tutorials/03_client.py +++ b/tutorials/03_client.py @@ -5,9 +5,6 @@ import asyncio from pathlib import Path - -from secp256k1 import PublicKey - from nostr_dvm.utils.dvmconfig import DVMConfig from nostr_dvm.utils.print_utils import bcolors diff --git a/tutorials/08_nip60-61.py b/tutorials/08_dvm_with_nutwallet.py similarity index 92% rename from tutorials/08_nip60-61.py rename to tutorials/08_dvm_with_nutwallet.py index 0287b98..30434b5 100644 --- a/tutorials/08_nip60-61.py +++ b/tutorials/08_dvm_with_nutwallet.py @@ -21,6 +21,7 @@ from nostr_sdk import Kind, Keys from nostr_dvm.utils.admin_utils import AdminConfig from nostr_dvm.utils.dvmconfig import build_default_config, DVMConfig from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag +from nostr_dvm.utils.nut_wallet_utils import NutZapWallet from nostr_dvm.utils.outbox_utils import AVOID_OUTBOX_RELAY_LIST from nostr_dvm.utils.zap_utils import change_ln_address @@ -30,6 +31,7 @@ def run_dvm(identifier, announce): dvm_config = build_default_config(identifier) kind = Kind(5050) dvm_config.KIND = kind + dvm_config.FIX_COST = 3 # Once you installed cashu, the rest is pretty staight forward: @@ -38,10 +40,14 @@ def run_dvm(identifier, announce): # Define a relay to update your balance dvm_config.NUTZAP_RELAYS = ["wss://relay.primal.net"] # Define one or more mints you would like to receive on. - dvm_config.NUZAP_MINTS = ["https://mint.minibits.cash/Bitcoin", "https://mint.gwoq.com"] + dvm_config.NUZAP_MINTS = ["https://mint.gwoq.com"] # If you want you can auto_melt cashu token to your lightning address once they pass a certain threshold. dvm_config.ENABLE_AUTO_MELT = False dvm_config.AUTO_MELT_AMOUNT = 1000 + # If you update your mints in NUTZAP_MINTS make sure to reannounce these. + dvm_config.REANNOUNCE_MINTS = True + + options = { @@ -67,7 +73,7 @@ def run_dvm(identifier, announce): return result dvm.process = process - dvm.run() + dvm.run(True) diff --git a/tutorials/09_nutzap_client.py b/tutorials/09_nutzap_client.py new file mode 100644 index 0000000..788ae5d --- /dev/null +++ b/tutorials/09_nutzap_client.py @@ -0,0 +1,131 @@ + +# This is the complementary tutorial for tutorial 08. Sending nutzaps. +# Make sure tutorial 08 is running while testing this. Also make sure you lnbits config is set, +# or manually change the code to access a wallet to mint tokens. +# Check especially the code in line 76 and make sure to replace the npub with the one from +# the dvm in tutorial 8 in line 120. + +from pathlib import Path + +import dotenv +from nostr_sdk import PublicKey, Client, NostrSigner, EventBuilder, Kind, Tag, Keys, Timestamp, HandleNotification, \ + Filter, Event + +import asyncio + +from nostr_dvm.utils.definitions import EventDefinitions +from nostr_dvm.utils.dvmconfig import DVMConfig +from nostr_dvm.utils.nostr_utils import send_event, check_and_set_private_key +from nostr_dvm.utils.nut_wallet_utils import NutZapWallet +from nostr_dvm.utils.print_utils import bcolors + + +# You already know this from our previous test client in tutorial 03: We simply send a kind 5050 request +# to return some text, just as before. Note: Our dvm in tutorial 08 requires a payment of 3 sats. +async def nostr_client_generic_test(ptag): + keys = Keys.parse(check_and_set_private_key("test_client")) + relay_list = ["wss://nostr.oxtr.dev", "wss://relay.primal.net"] + relaysTag = Tag.parse(["relays"] + relay_list) + alttag = Tag.parse(["alt", "This is a NIP90 DVM AI task"]) + paramTag = Tag.parse(["param", "some_option", "#RUNDVM - With a Nutzap."]) + pTag = Tag.parse(["p", PublicKey.parse(ptag).to_hex()]) + tags = [relaysTag, alttag, pTag, paramTag] + event = EventBuilder(Kind(5050), "This is a test", + tags).to_event(keys) + + signer = NostrSigner.keys(keys) + client = Client(signer) + for relay in relay_list: + await client.add_relay(relay) + await client.connect() + result = await send_event(event, client=client, dvm_config=DVMConfig()) + print(result) + + +async def nostr_client(target_dvm_npub): + + keys = Keys.parse(check_and_set_private_key("test_client")) + pk = keys.public_key() + print(f"Nostr Client public key: {pk.to_bech32()}, Hex: {pk.to_hex()} ") + signer = NostrSigner.keys(keys) + client = Client(signer) + + dvmconfig = DVMConfig() + for relay in dvmconfig.RELAY_LIST: + await client.add_relay(relay) + await client.connect() + + kinds = [EventDefinitions.KIND_NIP90_GENERIC] + for kind in range(6000, 7001): + if kind not in kinds: + kinds.append(Kind(kind)) + + dvm_filter = (Filter().kinds(kinds).since(Timestamp.now()).pubkey(pk)) + await client.subscribe([dvm_filter], None) + + # This will send a request to the DVM + await nostr_client_generic_test(target_dvm_npub) + + # We listen to + class NotificationHandler(HandleNotification): + last_event_time = 0 + async def handle(self, relay_url, subscription_id, event: Event): + if event.kind().as_u16() == 7000: + print(bcolors.YELLOW + "[Nostr Client]: " + event.content() + bcolors.ENDC) + amount_sats = 0 + status = "" + for tag in event.tags(): + if tag.as_vec()[0] == "amount": + amount_sats = int(int(tag.as_vec()[1]) / 1000) # millisats + if tag.as_vec()[0] == "status": + status = tag.as_vec()[1] + + if status == "payment-required": + print("do a nutzap of " + str(amount_sats) +" sats here") + + send_zap_amount = amount_sats + send_zap_message = "From my nutsack" + send_receiver = event.author().to_hex() + send_zapped_event = event.id().to_hex() # None # None, or zap an event like this: Nip19Event.from_nostr_uri("nostr:nevent1qqsxq59mhz8s6aj9jzltcmqmmv3eutsfcpkeny2x755vdu5dtq44ldqpz3mhxw309ucnydewxqhrqt338g6rsd3e9upzp75cf0tahv5z7plpdeaws7ex52nmnwgtwfr2g3m37r844evqrr6jqvzqqqqqqyqtxyr6").event_id().to_hex() + zapped_event_id_hex = send_zapped_event + nutzap_wallet = NutZapWallet() + nut_wallet = await nutzap_wallet.get_nut_wallet(client, keys) + + if nut_wallet is None: + relays = DVMConfig().NUTZAP_RELAYS + mints = DVMConfig().NUZAP_MINTS + await nutzap_wallet.create_new_nut_wallet(mints, relays, client, keys, "Test", "My Nutsack") + nut_wallet = await nutzap_wallet.get_nut_wallet(client, keys) + if nut_wallet is not None: + await nutzap_wallet.announce_nutzap_info_event(nut_wallet, client, keys) + else: + print("Couldn't fetch wallet, please restart and see if it is there") + + await nutzap_wallet.send_nut_zap(send_zap_amount, send_zap_message, nut_wallet, zapped_event_id_hex, send_receiver, client, + keys) + + elif 6000 < event.kind().as_u16() < 6999: + print(bcolors.GREEN + "[Nostr Client]: " + event.content() + bcolors.ENDC) + + + async def handle_msg(self, relay_url, msg): + return + + asyncio.create_task(client.handle_notifications(NotificationHandler())) + # await client.handle_notifications(NotificationHandler()) + while True: + await asyncio.sleep(2) + + +if __name__ == '__main__': + + env_path = Path('.env') + if env_path.is_file(): + print(f'loading environment from {env_path.resolve()}') + dotenv.load_dotenv(env_path, verbose=True, override=True) + else: + raise FileNotFoundError(f'.env file not found at {env_path} ') + + # Replace this key with the one from your DVM from part 8. + target_dvm_npub = "npub150laafgrw4dlj5pc7jk2qzhxkghtqn9pplsyg8u6jkpd9afaszzsu06p39" + asyncio.run(nostr_client(target_dvm_npub)) \ No newline at end of file