add tutorials, get did of admin_id (same as wallet_id)

This commit is contained in:
Believethehype
2024-09-17 15:40:31 +02:00
parent a66aaa8daa
commit 9f4e7c5d40
16 changed files with 510 additions and 19 deletions

View File

@ -0,0 +1,122 @@
{
"cells": [
{
"metadata": {},
"cell_type": "markdown",
"source": "",
"id": "1865ec9f54c7b22e"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"# Hello there, fellow DVM enthusiast\n",
"This is the first of a series of tutorials on how to use nostr-dvm.\n",
"\n",
"Before we start, we have to make sure you have a .env file in the working directory.\n",
"\n",
"In order to make sure you have it and to get you started quickly, here is a small function to create an inital .env file for you. Just make sure you set the parameters in the following field:"
],
"id": "e1f5dcfcad069462"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"lnbits_admin_key = \"\" #TODO set your key here\n",
"lnbits_wallet_id = \"\" #TODO set your key here\n",
"lnbits_host= \"https://demo.lnbits.com\" #TODO use demo.lnbits.com, but rather use your own instance\n",
"nostdress_domain = \"nostrdvm.com\" #TODO use your own nostdress instance, or use the default one"
],
"id": "2e71e7aa2ebdac50"
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"LNBITS_HOST is an Lnbits instance. We'll use Lnbits to generate wallets for each DVM automatically.\n",
"You can use the demo server, but it is highly recommended to move to your own instance,\n",
"or an instance of someone you trust.\n",
"\n",
"LNBITS_ADMIN_KEY and LNBITS_WALLET_ID are the API keys you get from Lnbits. You might also want to activate the User Manager module in LNBits.\n",
"\n",
"NOSTDRESS_DOMAIN: You can run your own instance of Nostdress https://github.com/believethehype/nostdress, so your DVMs get their own unique zapable lightning address, or you can use the default one. Make sure that the used identifier is unique, as otherwise the lnaddress will not work, if the nostdress acount already exists\n",
"\n",
" By the way, if that's not your kind of thing, you don't have to set an Lnbits account. If your DVM should receive zaps somehow, make sure you manually set a valid, zappable lightning address in its profile, once we created it. In this case leave the lnbits_admin_key and lnbits_wallet_id empty\n"
],
"id": "bb26e7180f1d1f0"
},
{
"metadata": {},
"cell_type": "markdown",
"source": "The following script will make an initial .env file with the parameters set in the field above, if it doesn't exist yet.\n",
"id": "f6b316e1cace5adc"
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2024-09-16T13:36:56.451990Z",
"start_time": "2024-09-16T13:36:56.448752Z"
}
},
"cell_type": "code",
"source": [
"config = \"LNBITS_ADMIN_KEY = \\\"\"+lnbits_admin_key+\"\\\"\\nLNBITS_WALLET_ID = \\\"\"+ lnbits_wallet_id +\"\\\"\\nLNBITS_HOST = \\\"\"+ lnbits_host + \"\\\"\\nNOSTDRESS_DOMAIN = \\\"\" + nostdress_domain + \"\\\"\"\n",
"\n",
"import os.path\n",
"if not os.path.isfile(\".env\"):\n",
" with open(\".env\", \"w\") as f: # Opens file and casts as f \n",
" f.write(config) # Writing\n",
" # File closed automatically\n",
"else:\n",
" print(\".env file already exists\")"
],
"id": "a3e16002be388215",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
".env file already exists\n"
]
}
],
"execution_count": 8
},
{
"metadata": {},
"cell_type": "markdown",
"source": "Cool. That's it for the prepartion. Once the .env file exists we won't overwrite it here again. You can open the .env file in this folder (maybe refresh your IDE) and check if everything worked as expected",
"id": "f4e3ed80abdad23a"
},
{
"metadata": {},
"cell_type": "markdown",
"source": "",
"id": "d32b5abd00600e64"
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.12"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -0,0 +1,161 @@
{
"cells": [
{
"metadata": {},
"cell_type": "markdown",
"source": "First we will load our .env file that we created in the last tutorial.",
"id": "de06960a2f8dae68"
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2024-09-16T14:12:25.797327Z",
"start_time": "2024-09-16T14:12:25.792936Z"
}
},
"cell_type": "code",
"source": [
"import dotenv\n",
"from pathlib import Path\n",
"\n",
"env_path = Path('.env')\n",
"if not env_path.is_file():\n",
" with open('.env', 'w') as f:\n",
" print(\"Writing new .env file\")\n",
" f.write('')\n",
"if env_path.is_file():\n",
" print(f'loading environment from {env_path.resolve()}')\n",
" dotenv.load_dotenv(env_path, verbose=True, override=True)\n",
"else:\n",
" raise FileNotFoundError(f'.env file not found at {env_path} ')"
],
"id": "7a3909a4945f62c3",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"loading environment from /Users/tobias/Documents/dvm/tutorials/.env\n"
]
}
],
"execution_count": 6
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"Ok, lets start with the very basics. We create the configuration for own DVM. The configuration contains infos like the lightning address, the private keys and some parameters that might come in handy for each DVM.\n",
"\n",
"In order to create a new Config, including everything we need to start, like a nostr private key, a lnbits wallet, and a lightning address, all we need to do is set an identifier and call the build_default_config function.\n",
"\n",
"Note: For the sake of this tutorial our identifier is tutorial01_ + some random word, to make sure ln addresses will not be overwritten. You can also pick an identifier of your choice (if its not taken on the nostdress server)."
],
"id": "75ae876bf5da9d35"
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2024-09-16T14:12:27.820688Z",
"start_time": "2024-09-16T14:12:27.817674Z"
}
},
"cell_type": "code",
"source": [
"from nostr_dvm.utils.dvmconfig import build_default_config\n",
"from helper import randomword\n",
"identifier = \"tutorial01_\" + randomword(10)\n",
"print(identifier)"
],
"id": "5f5cfdf34488074a",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"tutorial01_knicqxeqwq\n"
]
}
],
"execution_count": 7
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"Ok, now something important. I need you to do a manual step. Put what is printed above (tutorial01_somerandomword) in the next line down there:\n",
"\n",
"This way we make sure we don't create multiple configs for the same dvm. We just want to give it one fixed name and that's the one we created above. Or you can still pick your own one, if its unique.\n",
"\n",
"identifier = tutorial01_... "
],
"id": "e0f9f41907628fd7"
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2024-09-16T14:12:34.572391Z",
"start_time": "2024-09-16T14:12:34.047756Z"
}
},
"cell_type": "code",
"source": [
"identifier = \"tutorial01_xxxxxxxx\"\n",
"\n",
"dvm_config = build_default_config(identifier)"
],
"id": "d9ef15853d433c66",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"loading environment from /Users/tobias/Documents/dvm/tutorials/.env\n",
"https://demo.lnbits.com/usermanager/api/v1/users\n",
"{'id': 'b719b9d29e9d49a7a05d7eebfa4b6ebb', 'name': 'tutorial01_knicqxeqwq', 'admin': '43a50a6074f6427385b49460cf60c6a1', 'email': '', 'password': '', 'extra': None, 'wallets': [{'id': '8743fff6b78e429894062bca8ab076f2', 'admin': '43a50a6074f6427385b49460cf60c6a1', 'name': 'tutorial01_knicqxeqwq', 'user': 'b719b9d29e9d49a7a05d7eebfa4b6ebb', 'adminkey': '846aa1465c4641729a275e4988a8a458', 'inkey': '65851d26d8254836ae1a06e954a5311f'}]}\n",
"nostrdvm.com\n",
"65851d26d8254836ae1a06e954a5311f\n",
"{\"ok\":true,\"pin\":\"f0247b53c856e1a4d879e13c8daf88e16da32740fd23cf24e3ded8c0b6e7745b\"}\n",
"\n"
]
}
],
"execution_count": 8
},
{
"metadata": {},
"cell_type": "markdown",
"source": "If everything worked, your .env file should now be updated with the information. If you run this code box again, the dvm will know it already is configured. That's why we set the name manually. We don't want a random name each time we call it, but the identifier stays with the DVM.",
"id": "536ad95eaa6e628f"
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": "",
"id": "ea6243e7373d4b70"
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

78
tutorials/03_run_dvm.py Normal file
View File

@ -0,0 +1,78 @@
# Welcome back, this time we don't use a notebook, but we run an actual Python Script.
# Go to the very bottom of this code and replace the identifier with the one from last exercise.
# We use a GenericDVM kind to start with. Now what's this? We have many predefined tasks in the task folder, but
# the genericDVM gives you some control for simple manipulation without caring about the tasks. Important is that
# we set the Kind of the GenericDVM. In Line 20 you see that we give it Kind 5050 (Text generation).
# On https://www.data-vending-machines.org/ there's an overview on all current kinds.
# On https://github.com/nostr-protocol/data-vending-machines/ you can make a PR for your own kind, if you come up with one later.
# Check the run_dvm function for more explanations
from pathlib import Path
import dotenv
from nostr_dvm.tasks.generic_dvm import GenericDVM
from nostr_sdk import Kind
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.dvmconfig import build_default_config
from nostr_dvm.utils.nip89_utils import NIP89Config
def run_dvm(identifier):
# You have seen this one before, we did this in tutorial 2. This function will either create or load. the parameters of our DVMConfig.
# Make sure you replace the identifier down in the main function with the one you generated in tutorial 2, or we will create a new one here.
dvm_config = build_default_config(identifier)
# As we will use a GenericDVM we need to give it a kind. Here we use kind 5050 (Text Generation) as we want to reply with some simple text.
# There is a bunch of predefined DVMs in tasks that already have a kind set, but as we use the genericDVM we have to manually set it here.
dvm_config.KIND = Kind(5050)
# We can set options that we can later use in our process function. They are stored in a simple JSON
options = {
"some_option": "#RunDVM",
}
# We give the DVM a human readable name
name = "My very first DVM"
# Next we initalize a GenericDVM with the name and the dvm_config and the options we just created, as well as
# an empty AdminConfig() and NIP89Config(). We will check these out in later tutorials, so don't worry about them now.
dvm = GenericDVM(name=name, dvm_config=dvm_config, options=options,
nip89config=NIP89Config(), admin_config=AdminConfig())
# Normally we would define the dvm interface as we do in the tasks folder (we will do it later in the tutorials as well,
# but here is a small hack to quickly manipulate what our dvm will do.
async def process(request_form):
# First we always parse the options from our request_form, that is build internally in the create_request_from_nostr_event function.
options = dvm.set_options(request_form)
# We build our result we are giving back from some text
result = "The result of the DVM is: "
# and the option we defined above and handed over to our DVM (some_option)
result += options["some_option"]
print(result)
# Then we simply return the result
return result
dvm.process = process # now we simply overwrite our DVM's process function with the one we defined here.
# and finally we run the DVM #RunDVM
dvm.run()
# When the DVM is running you should see a blue message with the name and the public key in bech32 and hex format.
# For the next exercise, copy the Hex key, and let this DVM run, you will need it :)
if __name__ == '__main__':
#We open the .env file we created before.
env_path = Path('.env')
if not env_path.is_file():
with open('.env', 'w') as f:
print("Writing new .env file")
f.write('')
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 the identifier with the one from the last notebook, or a new dvmconfig will be stored
identifier = "tutorial01_xxxxxxxx"
run_dvm(identifier)

130
tutorials/04_client.py Normal file
View File

@ -0,0 +1,130 @@
#Welcome to part 4. This actually is is a simplistic client that will interact with our DVM.
# We will address the DVM we created in part 03, so make sure it's still running and run this Script in a new instance.
# Copy the DVM's hex key that pops up at the beginning and replace the one down in the main function with your DVM's key.
# This way we will tag it and it will know it should reply to us.
import asyncio
from pathlib import Path
from secp256k1 import PublicKey
from nostr_dvm.utils.dvmconfig import DVMConfig
from nostr_dvm.utils.print import bcolors
import dotenv
from nostr_sdk import Keys, Client, Tag, EventBuilder, Filter, HandleNotification, Timestamp, nip04_decrypt, \
NostrSigner, Event, Kind, PublicKey
from nostr_dvm.utils.nostr_utils import send_event, check_and_set_private_key
from nostr_dvm.utils.definitions import EventDefinitions
async def nostr_client_generic_test(ptag):
# Create or manage some private keys for our client.
keys = Keys.parse(check_and_set_private_key("test_client"))
# We tell the DVM to which relays it should reply
relay_list = ["wss://nostr.oxtr.dev", "wss://relay.primal.net"]
relaysTag = Tag.parse(["relays"] + relay_list)
# The alt tag is optional, and just describes what the event does.
alttag = Tag.parse(["alt", "This is a NIP90 DVM AI task"])
# The ptag tags the DVM we want to address. Make sure to set it down in the main function.
pTag = Tag.parse(["p", PublicKey.parse(ptag).to_hex()])
# These are out tags
tags = [relaysTag, alttag, pTag]
# We now send a 5050 Request (for Text Generation) with our tags. The content is optional.
event = EventBuilder(Kind(5050), "This is a test",
tags).to_event(keys)
# We create a signer with some random keys
signer = NostrSigner.keys(keys)
client = Client(signer)
# We add the relays we defined above and told our DVM we would want to receive events to.
for relay in relay_list:
await client.add_relay(relay)
# We connect the client
await client.connect()
# and send the Event.
result = await send_event(event, client=client, dvm_config=DVMConfig())
print(result)
async def nostr_client(target_dvm_npub):
# This is some logic for listening to events. For example we want to see replies from the DVM.
keys = Keys.parse(check_and_set_private_key("test_client"))
sk = keys.secret_key()
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()
dm_zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM,
EventDefinitions.KIND_ZAP]).since(Timestamp.now())
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([dm_zap_filter, 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):
print(
bcolors.BLUE + f"Received new event from {relay_url}: {event.as_json()}" + bcolors.ENDC)
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]
elif 6000 < event.kind().as_u16() < 6999:
print(bcolors.GREEN + "[Nostr Client]: " + event.content() + bcolors.ENDC)
elif event.kind().as_u16() == 4:
dec_text = nip04_decrypt(sk, event.author(), event.content() )
print("[Nostr Client]: " + f"Received new msg: {dec_text}")
elif event.kind().as_u16() == 9735:
print("[Nostr Client]: " + f"Received new zap:")
print(event.as_json())
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 3.
target_dvm_npub = "9937b858d8482610d67957778a62e2617260952c192579a7c7859bf18f86baf1"
asyncio.run(nostr_client(target_dvm_npub))

5
tutorials/helper.py Normal file
View File

@ -0,0 +1,5 @@
import random, string
def randomword(length):
letters = string.ascii_lowercase
return ''.join(random.choice(letters) for i in range(length))