private requests, feedback, results, zaps

This commit is contained in:
Believethehype 2023-11-27 23:37:44 +01:00
parent f5c6a5a3bf
commit 508278d10b
15 changed files with 291 additions and 166 deletions

160
bot.py
View File

@ -16,7 +16,6 @@ from utils.zap_utils import parse_zap_event_tags, pay_bolt11_ln_bits, zap
class Bot:
job_list: list
# This is a simple list just to keep track which events we created and manage, so we don't pay for other requests
def __init__(self, dvm_config, admin_config=None):
self.NAME = "Bot"
@ -61,7 +60,8 @@ class Bot:
keys = self.keys
def handle(self, relay_url, nostr_event):
if EventDefinitions.KIND_NIP90_EXTRACT_TEXT + 1000 <= nostr_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC + 1000:
if (EventDefinitions.KIND_NIP90_EXTRACT_TEXT + 1000 <= nostr_event.kind()
<= EventDefinitions.KIND_NIP90_GENERIC + 1000):
handle_nip90_response_event(nostr_event)
elif nostr_event.kind() == EventDefinitions.KIND_FEEDBACK:
handle_nip90_feedback(nostr_event)
@ -81,7 +81,6 @@ class Bot:
print(decrypted_text)
user = get_or_add_user(db=self.dvm_config.DB, npub=sender, client=self.client, config=self.dvm_config)
# We do a selection of tasks now, maybe change this later, Idk.
if decrypted_text[0].isdigit():
index = int(decrypted_text.split(' ')[0]) - 1
task = self.dvm_config.SUPPORTED_DVMS[index].TASK
@ -99,65 +98,58 @@ class Bot:
"services.", None).to_event(self.keys)
send_event(evt, client=self.client, dvm_config=dvm_config)
elif user.iswhitelisted or user.balance >= required_amount or required_amount == 0:
time.sleep(2.0)
if user.iswhitelisted:
evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(),
"As you are "
"whitelisted, your balance remains at"
+ str(user.balance) + " Sats.\n",
nostr_event.id()).to_event(self.keys)
elif user.balance >= required_amount or required_amount == 0:
command = decrypted_text.replace(decrypted_text.split(' ')[0] + " ", "")
input = command.split("-")[0].rstrip()
else:
balance = max(user.balance - required_amount, 0)
update_sql_table(db=self.dvm_config.DB, npub=user.npub, balance=balance,
iswhitelisted=user.iswhitelisted, isblacklisted=user.isblacklisted,
nip05=user.nip05, lud16=user.lud16, name=user.name,
lastactive=Timestamp.now().as_secs())
evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(),
"New balance is " +
str(balance)
+ " Sats.\n",
nostr_event.id()).to_event(self.keys)
input = decrypted_text.replace(decrypted_text.split(' ')[0] + " ", "")
dvm_keys = Keys.from_sk_str(self.dvm_config.SUPPORTED_DVMS[index].PK)
i_tag = Tag.parse(["i", input, "text"])
# we use the y tag to keep information about the original sender, in order to forward the
# results later
# TODO more advanced logic, more parsing, params etc, just very basic test functions for now
# outTag = Tag.parse(["output", "image/png;format=url"])
# paramTag1 = Tag.parse(["param", "size", "1024x1024"])
bid = str(self.dvm_config.SUPPORTED_DVMS[index].COST * 1000)
bid_tag = Tag.parse(['bid', bid, bid])
relays_tag = Tag.parse(["relays", json.dumps(self.dvm_config.RELAY_LIST)])
alt_tag = Tag.parse(["alt", self.dvm_config.SUPPORTED_DVMS[index].TASK])
p_tag = Tag.parse(['p', dvm_keys.public_key().to_hex()])
encrypted_params_string = json.dumps([i_tag.as_vec(), bid_tag.as_vec(),
relays_tag.as_vec(), alt_tag.as_vec(), p_tag.as_vec()])
tags = [i_tag.as_vec(), bid_tag.as_vec(), relays_tag.as_vec(), alt_tag.as_vec()]
remaining_text = command.replace(input, "")
params = remaining_text.rstrip().split("-")
for i in params:
if i != " ":
try:
split = i.split(" ")
param = str(split[0])
print(param)
value = str(split[1])
print(value)
tag = Tag.parse(["param", param, value])
tags.append(tag.as_vec())
print("Added params: " + tag.as_vec())
except Exception as e:
print(e)
print("Couldn't add " + i)
encrypted_params_string = json.dumps(tags)
print(encrypted_params_string)
encrypted_params = nip04_encrypt(self.keys.secret_key(), dvm_keys.public_key(),
encrypted_params = nip04_encrypt(self.keys.secret_key(),
PublicKey.from_hex(
self.dvm_config.SUPPORTED_DVMS[index].PUBLIC_KEY),
encrypted_params_string)
encrypted_tag = Tag.parse(['encrypted'])
nip90request = EventBuilder(self.dvm_config.SUPPORTED_DVMS[index].KIND, encrypted_params,
[p_tag, encrypted_tag]).to_event(self.keys)
p_tag = Tag.parse(['p', self.dvm_config.SUPPORTED_DVMS[index].PUBLIC_KEY])
encrypted_nip90request = (EventBuilder(self.dvm_config.SUPPORTED_DVMS[index].KIND,
encrypted_params, [p_tag, encrypted_tag]).
to_event(self.keys))
entry = {"npub": user.npub, "event_id": nip90request.id().to_hex(),
"dvm_key": dvm_keys.public_key().to_hex(), "is_paid": False}
entry = {"npub": user.npub, "event_id": encrypted_nip90request.id().to_hex(),
"dvm_key": self.dvm_config.SUPPORTED_DVMS[index].PUBLIC_KEY, "is_paid": False}
self.job_list.append(entry)
send_event(nip90request, client=self.client, dvm_config=dvm_config)
send_event(encrypted_nip90request, client=self.client, dvm_config=dvm_config)
print("[" + self.NAME + "] Replying " + user.name + " with \"scheduled\" confirmation")
send_event(evt, client=self.client, dvm_config=dvm_config)
else:
print("Bot payment-required")
time.sleep(2.0)
@ -190,7 +182,9 @@ class Bot:
print("Error in bot " + str(e))
def handle_nip90_feedback(nostr_event):
try:
is_encrypted = False
status = ""
etag = ""
ptag = ""
@ -202,6 +196,33 @@ class Bot:
etag = tag.as_vec()[1]
elif tag.as_vec()[0] == "p":
ptag = tag.as_vec()[1]
elif tag.as_vec()[0] == "encrypted":
is_encrypted = True
content = nostr_event.content()
if is_encrypted:
if ptag == self.dvm_config.PUBLIC_KEY:
tags_str = nip04_decrypt(Keys.from_sk_str(dvm_config.PRIVATE_KEY).secret_key(),
nostr_event.pubkey(), nostr_event.content())
params = json.loads(tags_str)
params.append(Tag.parse(["p", ptag]).as_vec())
params.append(Tag.parse(["encrypted"]).as_vec())
print(params)
event_as_json = json.loads(nostr_event.as_json())
event_as_json['tags'] = params
event_as_json['content'] = ""
nostr_event = Event.from_json(json.dumps(event_as_json))
for tag in nostr_event.tags():
if tag.as_vec()[0] == "status":
status = tag.as_vec()[1]
elif tag.as_vec()[0] == "e":
etag = tag.as_vec()[1]
elif tag.as_vec()[0] == "content":
content = tag.as_vec()[1]
else:
return
if status == "success" or status == "error" or status == "processing" or status == "partial":
entry = next((x for x in self.job_list if x['event_id'] == etag), None)
@ -211,34 +232,49 @@ class Bot:
reply_event = EventBuilder.new_encrypted_direct_msg(self.keys,
PublicKey.from_hex(user.npub),
nostr_event.content(),
content,
None).to_event(self.keys)
print(status + ": " + nostr_event.content())
print(status + ": " + content)
print(
"[" + self.NAME + "] Received reaction from " + nostr_event.pubkey().to_hex() + " message to orignal sender " + user.name)
send_event(reply_event, client=self.client, dvm_config=dvm_config)
elif status == "payment-required" or status == "partial":
amount = 0
for tag in nostr_event.tags():
if tag.as_vec()[0] == "amount":
amount_msats = int(tag.as_vec()[1])
amount = int(amount_msats / 1000)
entry = next((x for x in self.job_list if x['event_id'] == etag), None)
if entry is not None and entry['is_paid'] is False and entry['dvm_key'] == ptag:
print("PAYMENT: " + nostr_event.as_json())
#if we get a bolt11, we pay and move on
if entry is not None and entry['is_paid'] is False and entry['dvm_key'] == nostr_event.pubkey().to_hex():
# if we get a bolt11, we pay and move on
user = get_or_add_user(db=self.dvm_config.DB, npub=entry["npub"],
client=self.client, config=self.dvm_config)
balance = max(user.balance - amount, 0)
update_sql_table(db=self.dvm_config.DB, npub=user.npub, balance=balance,
iswhitelisted=user.iswhitelisted, isblacklisted=user.isblacklisted,
nip05=user.nip05, lud16=user.lud16, name=user.name,
lastactive=Timestamp.now().as_secs())
time.sleep(2.0)
evt = EventBuilder.new_encrypted_direct_msg(self.keys,
PublicKey.from_hex(entry["npub"]),
"Paid " + str(
amount) + " Sats from balance to DVM. New balance is " +
str(balance)
+ " Sats.\n",
None).to_event(self.keys)
print("[" + self.NAME + "] Replying " + user.name + " with \"scheduled\" confirmation")
send_event(evt, client=self.client, dvm_config=dvm_config)
if len(tag.as_vec()) > 2:
bolt11 = tag.as_vec()[2]
# else we create a zap
else:
user = get_or_add_user(db=self.dvm_config.DB, npub=nostr_event.pubkey().to_hex(),
client=self.client, config=self.dvm_config)
print("PAYING: " + user.name)
bolt11 = zap(user.lud16, amount, "Zap", nostr_event, self.keys, self.dvm_config, "private")
print("Paying: " + user.name)
bolt11 = zap(user.lud16, amount, "Zap", nostr_event, self.keys, self.dvm_config,
"private")
if bolt11 == None:
print("Receiver has no Lightning address")
return
@ -256,6 +292,7 @@ class Bot:
def handle_nip90_response_event(nostr_event: Event):
try:
ptag = ""
is_encrypted = False
for tag in nostr_event.tags():
if tag.as_vec()[0] == "e":
@ -274,7 +311,10 @@ class Bot:
self.job_list.remove(entry)
content = nostr_event.content()
if is_encrypted:
content = nip04_decrypt(self.keys.secret_key(), nostr_event.pubkey(), content)
if ptag == self.dvm_config.PUBLIC_KEY:
content = nip04_decrypt(self.keys.secret_key(), nostr_event.pubkey(), content)
else:
return
print("[" + self.NAME + "] Received results, message to orignal sender " + user.name)
time.sleep(1.0)
@ -291,11 +331,11 @@ class Bot:
print("[" + self.NAME + "] Zap received")
try:
invoice_amount, zapped_event, sender, message, anon = parse_zap_event_tags(zap_event,
self.keys, self.NAME,
self.client, self.dvm_config)
self.keys, self.NAME,
self.client, self.dvm_config)
user = get_or_add_user(self.dvm_config.DB, sender, client=self.client, config=self.dvm_config)
print("ZAPED EVENT: " + zapped_event.as_json())
if zapped_event is not None:
if not anon:
print("[" + self.NAME + "] Note Zap received for Bot balance: " + str(

65
dvm.py
View File

@ -90,8 +90,6 @@ class DVM:
user = get_or_add_user(self.dvm_config.DB, nip90_event.pubkey().to_hex(), client=self.client,
config=self.dvm_config)
cashu = ""
p_tag_str = ""
for tag in nip90_event.tags():
@ -120,8 +118,13 @@ class DVM:
task_is_free = True
cashu_redeemed = False
cashu_message = ""
if cashu != "":
cashu_redeemed = redeem_cashu(cashu, self.dvm_config, self.client)
cashu_redeemed, cashu_message = redeem_cashu(cashu, amount, self.dvm_config, self.client)
if cashu_message != "":
send_job_status_reaction(nip90_event, "error", False, amount, self.client, cashu_message,
self.dvm_config)
return
# if user is whitelisted or task is free, just do the job
if user.iswhitelisted or task_is_free or cashu_redeemed:
print(
@ -135,7 +138,7 @@ class DVM:
# if task is directed to us via p tag and user has balance, do the job and update balance
elif p_tag_str == Keys.from_sk_str(
self.dvm_config.PRIVATE_KEY).public_key().to_hex() and user.balance >= amount:
self.dvm_config.PUBLIC_KEY) and user.balance >= amount:
balance = max(user.balance - amount, 0)
update_sql_table(db=self.dvm_config.DB, npub=user.npub, balance=balance,
iswhitelisted=user.iswhitelisted, isblacklisted=user.isblacklisted,
@ -176,11 +179,10 @@ class DVM:
send_job_status_reaction(nip90_event, "payment-required",
False, amount, client=self.client, dvm_config=self.dvm_config)
else:
print("[" + self.dvm_config.NIP89.name + "] Task " + task + " not supported on this DVM, skipping..")
#else:
#print("[" + self.dvm_config.NIP89.name + "] Task " + task + " not supported on this DVM, skipping..")
def handle_zap(zap_event):
try:
invoice_amount, zapped_event, sender, message, anon = parse_zap_event_tags(zap_event,
self.keys,
@ -188,6 +190,7 @@ class DVM:
self.client, self.dvm_config)
user = get_or_add_user(db=self.dvm_config.DB, npub=sender, client=self.client, config=self.dvm_config)
if zapped_event is not None:
if zapped_event.kind() == EventDefinitions.KIND_FEEDBACK:
@ -201,12 +204,11 @@ class DVM:
job_event = get_event_by_id(tag.as_vec()[1], client=self.client, config=self.dvm_config)
if job_event is not None:
job_event = check_and_decrypt_tags(job_event, self.dvm_config)
if job_event is None:
return
else:
return
if p_tag_str is None:
return
# if a reaction by us got zapped
task_supported, task, duration = check_task_is_supported(job_event,
@ -330,11 +332,11 @@ class DVM:
original_event = Event.from_json(original_event_as_str)
request_tag = Tag.parse(["request", original_event_as_str.replace("\\", "")])
e_tag = Tag.parse(["e", original_event.id().to_hex()])
# p_tag = Tag.parse(["p", original_event.pubkey().to_hex()])
p_tag = Tag.parse(["p", original_event.pubkey().to_hex()])
alt_tag = Tag.parse(["alt", "This is the result of a NIP90 DVM AI task with kind " + str(
original_event.kind()) + ". The task was: " + original_event.content()])
status_tag = Tag.parse(["status", "success"])
reply_tags = [request_tag, e_tag, alt_tag, status_tag]
reply_tags = [request_tag, e_tag, p_tag, alt_tag, status_tag]
encrypted = False
for tag in original_event.tags():
if tag.as_vec()[0] == "encrypted":
@ -347,9 +349,6 @@ class DVM:
i_tag = tag
if not encrypted:
reply_tags.append(i_tag)
elif tag.as_vec()[0] == "p":
p_tag = tag
reply_tags.append(p_tag)
if encrypted:
content = nip04_encrypt(self.keys.secret_key(), PublicKey.from_hex(original_event.pubkey().to_hex()),
@ -369,16 +368,23 @@ class DVM:
alt_description, reaction = build_status_reaction(status, task, amount, content)
e_tag = Tag.parse(["e", original_event.id().to_hex()])
# p_tag = Tag.parse(["p", original_event.pubkey().to_hex()])
p_tag = Tag.parse(["p", original_event.pubkey().to_hex()])
alt_tag = Tag.parse(["alt", alt_description])
status_tag = Tag.parse(["status", status])
reply_tags = [e_tag, alt_tag, status_tag]
encryption_tags = []
tags = [e_tag, alt_tag, status_tag]
encrypted = False
for tag in original_event.tags():
if tag.as_vec()[0] == "encrypted":
encrypted = True
encrypted_tag = Tag.parse(["encrypted"])
encryption_tags.append(encrypted_tag)
if tag.as_vec()[0] == "p":
p_tag = tag
tags.append(p_tag)
if encrypted:
encryption_tags.append(p_tag)
else:
reply_tags.append(p_tag)
if status == "success" or status == "error": #
for x in self.job_list:
@ -415,10 +421,25 @@ class DVM:
amount_tag = Tag.parse(["amount", str(amount * 1000), bolt11])
else:
amount_tag = Tag.parse(["amount", str(amount * 1000)]) # to millisats
tags.append(amount_tag)
reply_tags.append(amount_tag)
if encrypted:
content_tag = Tag.parse(["content", reaction])
reply_tags.append(content_tag)
str_tags = []
for element in reply_tags:
str_tags.append(element.as_vec())
content = json.dumps(str_tags)
content = nip04_encrypt(self.keys.secret_key(), PublicKey.from_hex(original_event.pubkey().to_hex()),
content)
reply_tags = encryption_tags
else:
content = reaction
keys = Keys.from_sk_str(dvm_config.PRIVATE_KEY)
reaction_event = EventBuilder(EventDefinitions.KIND_FEEDBACK, reaction, tags).to_event(keys)
reaction_event = EventBuilder(EventDefinitions.KIND_FEEDBACK, str(content), reply_tags).to_event(keys)
send_event(reaction_event, client=self.client, dvm_config=self.dvm_config)
print("[" + self.dvm_config.NIP89.name + "]" + ": Sent Kind " + str(
EventDefinitions.KIND_FEEDBACK) + " Reaction: " + status + " " + reaction_event.as_json())

View File

@ -12,23 +12,21 @@ class DVMTaskInterface:
KIND: int
TASK: str
COST: int
PK: str
PRIVATE_KEY: str
PUBLIC_KEY: str
DVM = DVM
dvm_config: DVMConfig
admin_config: AdminConfig
def NIP89_announcement(self, nip89config: NIP89Config):
nip89 = NIP89Announcement()
nip89.name = self.NAME
nip89.kind = self.KIND
nip89.pk = self.PK
nip89.dtag = nip89config.DTAG
nip89.content = nip89config.CONTENT
return nip89
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None,
options=None):
self.init(name, dvm_config, admin_config, nip89config)
self.options = options
def init(self, name, dvm_config, admin_config, nip89config):
def init(self, name, dvm_config, admin_config=None, nip89config=None):
self.NAME = name
self.PK = dvm_config.PRIVATE_KEY
self.PRIVATE_KEY = dvm_config.PRIVATE_KEY
self.PUBLIC_KEY = dvm_config.PUBLIC_KEY
if dvm_config.COST is not None:
self.COST = dvm_config.COST
@ -43,6 +41,14 @@ class DVMTaskInterface:
nostr_dvm_thread = Thread(target=self.DVM, args=[self.dvm_config, self.admin_config])
nostr_dvm_thread.start()
def NIP89_announcement(self, nip89config: NIP89Config):
nip89 = NIP89Announcement()
nip89.name = self.NAME
nip89.kind = self.KIND
nip89.pk = self.PRIVATE_KEY
nip89.dtag = nip89config.DTAG
nip89.content = nip89config.CONTENT
return nip89
def is_input_supported(self, input_type, input_content) -> bool:
"""Check if input is supported for current Task."""

View File

@ -18,6 +18,7 @@ def run_nostr_dvm_with_local_config():
# Note this is very basic for now and still under development
bot_config = DVMConfig()
bot_config.PRIVATE_KEY = os.getenv("BOT_PRIVATE_KEY")
bot_config.PUBLIC_KEY = Keys.from_sk_str(bot_config.PRIVATE_KEY).public_key().to_hex()
bot_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY")
bot_config.LNBITS_ADMIN_KEY = os.getenv("LNBITS_ADMIN_KEY") # The bot will forward zaps for us, use responsibly
bot_config.LNBITS_URL = os.getenv("LNBITS_HOST")

View File

@ -1,6 +1,9 @@
import json
import os
from nostr_sdk import PublicKey, Keys
from interfaces.dvmtaskinterface import DVMTaskInterface
from tasks.imagegeneration_openai_dalle import ImageGenerationDALLE
from tasks.imagegeneration_sdxl import ImageGenerationSDXL
from tasks.textextractionpdf import TextExtractionPDF
@ -40,6 +43,7 @@ admin_config.REBROADCAST_NIP89 = False
def build_pdf_extractor(name):
dvm_config = DVMConfig()
dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY")
dvm_config.PUBLIC_KEY = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex()
dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY")
dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST")
# Add NIP89
@ -61,6 +65,7 @@ def build_pdf_extractor(name):
def build_translator(name):
dvm_config = DVMConfig()
dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY")
dvm_config.PUBLIC_KEY = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex()
dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY")
dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST")
@ -93,6 +98,7 @@ def build_translator(name):
def build_unstable_diffusion(name):
dvm_config = DVMConfig()
dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY")
dvm_config.PUBLIC_KEY = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex()
dvm_config.LNBITS_INVOICE_KEY = "" #This one will not use Lnbits to create invoices, but rely on zaps
dvm_config.LNBITS_URL = ""
@ -126,6 +132,7 @@ def build_unstable_diffusion(name):
def build_sketcher(name):
dvm_config = DVMConfig()
dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY2")
dvm_config.PUBLIC_KEY = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex()
dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY")
dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST")
@ -161,6 +168,7 @@ def build_sketcher(name):
def build_dalle(name):
dvm_config = DVMConfig()
dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY3")
dvm_config.PUBLIC_KEY = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex()
dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY")
dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST")
profit_in_sats = 10
@ -190,6 +198,17 @@ def build_dalle(name):
admin_config=admin_config)
def external_dvm(name, pubkey):
dvm_config = DVMConfig()
dvm_config.PUBLIC_KEY = Keys.from_public_key(pubkey).public_key().to_hex()
nip89info = {
"name": name,
}
nip89config = NIP89Config()
nip89config.CONTENT = json.dumps(nip89info)
return DVMTaskInterface(name=name, dvm_config=dvm_config, nip89config=nip89config)
# Little Gimmick:
# For Dalle where we have to pay 4cent per image, we fetch current sat price in fiat
# and update cost at each start

View File

@ -27,11 +27,9 @@ class ImageGenerationDALLE(DVMTaskInterface):
TASK: str = "text-to-image"
COST: int = 120
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None,
options=None):
self.init(name, dvm_config, admin_config, nip89config)
self.options = options
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
admin_config: AdminConfig = None, options=None):
super().__init__(name, dvm_config, nip89config, admin_config, options)
def is_input_supported(self, input_type, input_content):
if input_type != "text":

View File

@ -24,9 +24,9 @@ class ImageGenerationSDXL(DVMTaskInterface):
TASK: str = "text-to-image"
COST: int = 50
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None, options=None):
self.init(name, dvm_config, admin_config, nip89config)
self.options = options
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
admin_config: AdminConfig = None, options=None):
super().__init__(name, dvm_config, nip89config, admin_config, options)
def is_input_supported(self, input_type, input_content):
if input_type != "text":

View File

@ -25,9 +25,9 @@ class TextExtractionPDF(DVMTaskInterface):
TASK: str = "pdf-to-text"
COST: int = 0
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None, options=None):
self.init(name, dvm_config, admin_config, nip89config)
self.options = options
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
admin_config: AdminConfig = None, options=None):
super().__init__(name, dvm_config, nip89config, admin_config, options)
def is_input_supported(self, input_type, input_content):

View File

@ -23,10 +23,9 @@ class Translation(DVMTaskInterface):
TASK: str = "translation"
COST: int = 0
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None,
options=None):
self.init(name, dvm_config, admin_config, nip89config)
self.options = options
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
admin_config: AdminConfig = None, options=None):
super().__init__(name, dvm_config, nip89config, admin_config, options)
def is_input_supported(self, input_type, input_content):
if input_type != "event" and input_type != "job" and input_type != "text":

View File

@ -132,7 +132,7 @@ def nostr_client():
#nostr_client_test_image("a beautiful purple ostrich watching the sunset")
cashutoken = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6MSwiQyI6IjAzMjBjMjBkNWZkNTYwODlmYjZjYTllNDFkYjVlM2MzYTAwMTdjNTUzYmY5MzNkZTgwNTg3NDg1YTk5Yjk2Y2E3OSIsInNlY3JldCI6IktrcnVtakdSeDlHTExxZHBQU3J4WUxaZnJjWmFHekdmZ3Q4T2pZN0c4NHM9In0seyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6MiwiQyI6IjAyNjYyMjQzNWUxMzBmM2E0ZWE2NGUyMmI4NGQyYWRhNzM2MjE4MTE3YzZjOWIyMmFkYjAwZTFjMzhmZDBiOTNjNCIsInNlY3JldCI6Ikw4dU1BbnBsQm1pdDA4cDZjQk0vcXhpVDFmejlpbnA3V3RzZEJTV284aEk9In0seyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6NCwiQyI6IjAzMTAxNWM0ZmZhN2U1NzhkNjA0MjFhY2Q2OWEzMTY5NGI4YmRlYTI2YjIwZjgxOWYxOWZhOTNjN2QwZTBiMTdlOCIsInNlY3JldCI6ImRVZ2E2VFo2emRhclozN015NXg2MFdHMzMraitDZnEyOWkzWExjVStDMFE9In0seyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6MTYsIkMiOiIwMzU0YmYxODdjOTgxZjdmNDk5MGExMDVlMmI2MjIxZDNmYTQ2ZWNlMmNjNWE0ZmI2Mzc3NTdjZDJjM2VhZTkzMGMiLCJzZWNyZXQiOiIyeUJJeEo4dkNGVnUvK1VWSzdwSXFjekkrbkZISngyNXF2ZGhWNDByNzZnPSJ9LHsiaWQiOiJYV0MwL0V0YXFTOEMiLCJhbW91bnQiOjMyLCJDIjoiMDJlYTNmMmFhZGI5MTA4MzljZDA5YTlmMTQ1YTZkY2Q4OGZmZDFmM2M5MjZhMzM5MGFmZjczYjM4ZjY0YjQ5NTU2Iiwic2VjcmV0IjoiQU1mU2FxUWFTN0l5WVdEbUpUaVM4NW9ReFNva0p6SzVJL1R6OUJ5UlFLdz0ifV0sIm1pbnQiOiJodHRwczovL2xuYml0cy5iaXRjb2luZml4ZXN0aGlzLm9yZy9jYXNodS9hcGkvdjEvOXVDcDIyUllWVXE4WjI0bzVCMlZ2VyJ9XX0="
cashutoken = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6InZxc1VRSVorb0sxOSIsImFtb3VudCI6MSwiQyI6IjAyNWU3ODZhOGFkMmExYTg0N2YxMzNiNGRhM2VhMGIyYWRhZGFkOTRiYzA4M2E2NWJjYjFlOTgwYTE1NGIyMDA2NCIsInNlY3JldCI6InQ1WnphMTZKMGY4UElQZ2FKTEg4V3pPck5rUjhESWhGa291LzVzZFd4S0U9In0seyJpZCI6InZxc1VRSVorb0sxOSIsImFtb3VudCI6NCwiQyI6IjAyOTQxNmZmMTY2MzU5ZWY5ZDc3MDc2MGNjZmY0YzliNTMzMzVmZTA2ZGI5YjBiZDg2Njg5Y2ZiZTIzMjVhYWUwYiIsInNlY3JldCI6IlRPNHB5WE43WlZqaFRQbnBkQ1BldWhncm44UHdUdE5WRUNYWk9MTzZtQXM9In0seyJpZCI6InZxc1VRSVorb0sxOSIsImFtb3VudCI6MTYsIkMiOiIwMmRiZTA3ZjgwYmMzNzE0N2YyMDJkNTZiMGI3ZTIzZTdiNWNkYTBhNmI3Yjg3NDExZWYyOGRiZDg2NjAzNzBlMWIiLCJzZWNyZXQiOiJHYUNIdHhzeG9HM3J2WWNCc0N3V0YxbU1NVXczK0dDN1RKRnVwOHg1cURzPSJ9XSwibWludCI6Imh0dHBzOi8vbG5iaXRzLmJpdGNvaW5maXhlc3RoaXMub3JnL2Nhc2h1L2FwaS92MS9ScDlXZGdKZjlxck51a3M1eVQ2SG5rIn1dfQ=="
nostr_client_test_image_private("a beautiful ostrich watching the sunset", cashutoken )
class NotificationHandler(HandleNotification):
def handle(self, relay_url, event):

View File

@ -62,39 +62,36 @@ def check_task_is_supported(event: Event, client, get_duration=False, config=Non
duration = 1
task = get_task(event, client=client, dvmconfig=dvm_config)
try:
for tag in event.tags():
if tag.as_vec()[0] == 'i':
if len(tag.as_vec()) < 3:
print("Job Event missing/malformed i tag, skipping..")
return False, "", 0
else:
input_value = tag.as_vec()[1]
input_type = tag.as_vec()[2]
if input_type == "event":
evt = get_event_by_id(input_value, client=client, config=dvm_config)
if evt is None:
print("Event not found")
return False, "", 0
elif input_type == 'url' and check_url_is_readable(input_value) is None:
print("Url not readable / supported")
return False, task, duration #
for tag in event.tags():
if tag.as_vec()[0] == 'i':
if len(tag.as_vec()) < 3:
print("Job Event missing/malformed i tag, skipping..")
return False, "", 0
else:
input_value = tag.as_vec()[1]
input_type = tag.as_vec()[2]
if input_type == "event":
evt = get_event_by_id(input_value, client=client, config=dvm_config)
if evt is None:
print("Event not found")
return False, "", 0
elif input_type == 'url' and check_url_is_readable(input_value) is None:
print("Url not readable / supported")
return False, task, duration #
elif tag.as_vec()[0] == 'output':
# TODO move this to individual modules
output = tag.as_vec()[1]
if not (output == "text/plain"
or output == "text/json" or output == "json"
or output == "image/png" or "image/jpg"
or output == "image/png;format=url" or output == "image/jpg;format=url"
or output == ""):
print("Output format not supported, skipping..")
return False, "", 0
elif tag.as_vec()[0] == 'output':
# TODO move this to individual modules
output = tag.as_vec()[1]
if not (output == "text/plain"
or output == "text/json" or output == "json"
or output == "image/png" or "image/jpg"
or output == "image/png;format=url" or output == "image/jpg;format=url"
or output == ""):
print("Output format not supported, skipping..")
return False, "", 0
except Exception as e:
print("Check task 2: " + str(e))
for dvm in dvm_config.SUPPORTED_DVMS:
print(dvm.TASK)
if dvm.TASK == task:
if not dvm.is_input_supported(input_type, event.content()):
return False, task, duration

View File

@ -1,11 +1,14 @@
import os
from nostr_sdk import Keys
from utils.nip89_utils import NIP89Announcement
class DVMConfig:
SUPPORTED_DVMS= []
PRIVATE_KEY: str = os.getenv("NOSTR_PRIVATE_KEY")
PRIVATE_KEY: str = ""
PUBLIC_KEY: str = ""
COST: int = None
RELAY_LIST = ["wss://relay.damus.io", "wss://nostr-pub.wellorder.net", "wss://nos.lol", "wss://nostr.wine",

View File

@ -74,23 +74,54 @@ def check_and_decrypt_tags(event, dvm_config):
p = tag.as_vec()[1]
if is_encrypted:
if p != Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex():
if p != dvm_config.PUBLIC_KEY:
print("[" + dvm_config.NIP89.name + "] Task encrypted and not addressed to this DVM, "
"skipping..")
return None
elif p == Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex():
print("encrypted")
elif p == dvm_config.PUBLIC_KEY:
tags_str = nip04_decrypt(Keys.from_sk_str(dvm_config.PRIVATE_KEY).secret_key(),
event.pubkey(), event.content())
params = json.loads(tags_str)
params.append(Tag.parse(["p", p]).as_vec())
print(params)
eventasjson = json.loads(event.as_json())
eventasjson['tags'] = params
eventasjson['content'] = ""
event = Event.from_json(json.dumps(eventasjson))
print(event.as_json())
params.append(Tag.parse(["encrypted"]).as_vec())
event_as_json = json.loads(event.as_json())
event_as_json['tags'] = params
event_as_json['content'] = ""
event = Event.from_json(json.dumps(event_as_json))
except Exception as e:
print(e)
return event
def check_and_decrypt_own_tags(event, dvm_config):
try:
tags = []
is_encrypted = False
p = ""
sender = event.pubkey()
for tag in event.tags():
if tag.as_vec()[0] == 'encrypted':
is_encrypted = True
elif tag.as_vec()[0] == 'p':
p = tag.as_vec()[1]
if is_encrypted:
if dvm_config.PUBLIC_KEY != event.pubkey().to_hex():
print("[" + dvm_config.NIP89.name + "] Task encrypted and not addressed to this DVM, "
"skipping..")
return None
elif event.pubkey().to_hex() == dvm_config.PUBLIC_KEY:
tags_str = nip04_decrypt(Keys.from_sk_str(dvm_config.PRIVATE_KEY).secret_key(),
PublicKey.from_hex(p), event.content())
params = json.loads(tags_str)
params.append(Tag.parse(["p", p]).as_vec())
params.append(Tag.parse(["encrypted"]).as_vec())
event_as_json = json.loads(event.as_json())
event_as_json['tags'] = params
event_as_json['content'] = ""
event = Event.from_json(json.dumps(event_as_json))
except Exception as e:
print(e)

View File

@ -18,7 +18,6 @@ def post_process_result(anno, original_event):
print("Post-processing...")
if isinstance(anno, pandas.DataFrame): # if input is an anno we parse it to required output format
for tag in original_event.tags:
print(tag.as_vec()[0])
if tag.as_vec()[0] == "output":
output_format = tag.as_vec()[1]
print("requested output is " + str(tag.as_vec()[1]) + "...")
@ -166,7 +165,7 @@ def build_status_reaction(status, task, amount, content):
if content is None:
reaction = alt_description + emoji.emojize(":thumbs_down:")
else:
reaction = alt_description + emoji.emojize(":thumbs_down:") + content
reaction = alt_description + emoji.emojize(":thumbs_down:") + " " + content
elif status == "payment-required":
alt_description = "NIP90 DVM AI task " + task + " requires payment of min " + str(

View File

@ -11,7 +11,7 @@ from nostr_sdk import nostr_sdk, PublicKey, SecretKey, Event, EventBuilder, Tag,
from utils.database_utils import get_or_add_user
from utils.dvmconfig import DVMConfig
from utils.nostr_utils import get_event_by_id
from utils.nostr_utils import get_event_by_id, check_and_decrypt_tags, check_and_decrypt_own_tags
import lnurl
from hashlib import sha256
@ -22,12 +22,14 @@ def parse_zap_event_tags(zap_event, keys, name, client, config):
anon = False
message = ""
sender = zap_event.pubkey()
for tag in zap_event.tags():
if tag.as_vec()[0] == 'bolt11':
invoice_amount = parse_amount_from_bolt11_invoice(tag.as_vec()[1])
elif tag.as_vec()[0] == 'e':
zapped_event = get_event_by_id(tag.as_vec()[1], client=client, config=config)
zapped_event = check_and_decrypt_own_tags(zapped_event, config)
elif tag.as_vec()[0] == 'p':
p_tag = tag.as_vec()[1]
elif tag.as_vec()[0] == 'description':
zap_request_event = Event.from_json(tag.as_vec()[1])
sender = check_for_zapplepay(zap_request_event.pubkey().to_hex(),
@ -249,32 +251,39 @@ def parse_cashu(cashu_token):
print(e)
token = cashu["token"][0]
print(token)
proofs = token["proofs"]
mint = token["mint"]
total_amount = 0
for proof in proofs:
total_amount += proof["amount"]
fees = max(int(total_amount * 0.02), 2)
fees = max(int(total_amount * 0.02), 3)
redeem_invoice_amount = total_amount - fees
return proofs, mint, redeem_invoice_amount
return proofs, mint, redeem_invoice_amount, total_amount
except Exception as e:
print("Could not parse this cashu token")
return None, None, None
return None, None, None, None
def redeem_cashu(cashu, config, client):
proofs, mint, redeem_invoice_amount = parse_cashu(cashu)
def redeem_cashu(cashu, required_amount, config, client) -> (bool, str):
proofs, mint, redeem_invoice_amount, total_amount = parse_cashu(cashu)
fees = total_amount - redeem_invoice_amount
if redeem_invoice_amount <= required_amount:
err = ("Token value (Payment: " + str(total_amount) + " Sats. Fees: " +
str(fees) + " Sats) below required amount of " + str(required_amount)
+ " Sats. Cashu token has not been claimed.")
print("[" + config.NIP89.name + "] " + err)
return False, err
if config.LNBITS_INVOICE_KEY != "":
invoice = create_bolt11_ln_bits(redeem_invoice_amount, config)
else:
user = get_or_add_user(db=config.DB, npub=Keys.from_sk_str(config.PRIVATE_KEY).public_key().to_hex(),
user = get_or_add_user(db=config.DB, npub=config.PUBLIC_KEY,
client=client, config=config)
invoice = create_bolt11_lud16(user.lud16, redeem_invoice_amount)
print(invoice)
if invoice is None:
return False
return False, "couldn't create invoice"
try:
url = mint + "/melt" # Melt cashu tokens at Mint
json_object = {"proofs": proofs, "pr": invoice}
@ -282,15 +291,17 @@ def redeem_cashu(cashu, config, client):
request_body = json.dumps(json_object).encode('utf-8')
request = requests.post(url, data=request_body, headers=headers)
tree = json.loads(request.text)
is_paid = (tree.get("paid") == "true") if tree.get("detail") else False
if is_paid:
print(request.text)
is_paid = tree["paid"] if tree.get("paid") else "false"
print(is_paid)
if is_paid == "true":
print("token redeemed")
return True
return True, "success"
else:
msg = tree.get("detail").split('.')[0].strip() if tree.get("detail") else None
print(msg)
return False
return False, msg
except Exception as e:
print(e)
return False
return False, ""