diff --git a/.env_example b/.env_example index aaa3c46..6c7bf5f 100644 --- a/.env_example +++ b/.env_example @@ -1,8 +1,13 @@ # Optional LNBITS options to create invoices (if empty, it will use the lud16 from profile) # Admin Key is (only) required for bot or if any payments should be made -LNBITS_INVOICE_KEY = "" + +#Create an account with a lnbits instance of your choice, add the admin key and id here. This account will be used to create a new lnbits wallet for each dvm/bot LNBITS_ADMIN_KEY = "" # In order to pay invoices, e.g. from the bot to DVMs, or reimburse users. Keep this secret and use responsibly. +LNBITS_ADMIN_ID = "" LNBITS_HOST = "https://lnbits.com" +# In order to create a zappable lightning address, host nostdress on your domain or use this preinstalled domain. +# We will use the api to create and manage zapable lightning addresses +NOSTDRESS_DOMAIN = "nostrdvm.com" #Backend Specific Options for tasks that require them. A DVM needing these should only be started if these are set. @@ -13,4 +18,7 @@ REPLICATE_API_TOKEN = "" #API Key to run models on replicate.com # We will automatically create dtags and private keys based on the identifier variable in main. # If your DVM already has a dtag and private key you can replace it here before publishing the DTAG to not create a new one. -# The name and NIP90 info of the DVM can be changed but the identifier must stay the same in order to not create a different dtag. \ No newline at end of file +# The name and NIP90 info of the DVM can be changed but the identifier must stay the same in order to not create a different dtag. + +# We will also create new wallets on the given lnbits instance for each dvm. If you want to use an existing wallet, you can replace the parameters here as well. +# Make sure you backup this file to keep access to your wallets \ No newline at end of file diff --git a/bot/bot.py b/bot/bot.py index ad4cf0d..f5820e3 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -298,6 +298,7 @@ class Bot: print("Receiver has no Lightning address") return try: + print(bolt11) payment_hash = pay_bolt11_ln_bits(bolt11, self.dvm_config) self.job_list[self.job_list.index(entry)]['is_paid'] = True print("[" + self.NAME + "] payment_hash: " + payment_hash + @@ -366,16 +367,39 @@ class Bot: 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) - if zapped_event is not None: - if not anon: - print("[" + self.NAME + "] Note Zap received for Bot balance: " + str( - invoice_amount) + " Sats from " + str( - user.name)) - update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, - config=self.dvm_config) + etag = "" + for tag in zap_event.tags(): + if tag.as_vec()[0] == "e": + etag = tag.as_vec()[1] - # a regular note + + user = get_or_add_user(self.dvm_config.DB, sender, client=self.client, config=self.dvm_config) + + + entry = next((x for x in self.job_list if x['event_id'] == etag), None) + print(entry) + #print(entry['dvm_key']) + # print(str(zapped_event.pubkey().to_hex())) + # print(str(zap_event.pubkey().to_hex())) + print(sender) + if entry is not None and entry['is_paid'] is True and entry['dvm_key'] == sender: + # 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) + + print("HELLO: " + user.name) + sender = user.npub + #print(zap_event.as_json()) + + if zapped_event is not None: + if not anon: + print("[" + self.NAME + "] Note Zap received for Bot balance: " + str( + invoice_amount) + " Sats from " + str( + user.name)) + update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, + config=self.dvm_config) + + # a regular note elif not anon: print("[" + self.NAME + "] Profile Zap received for Bot balance: " + str( invoice_amount) + " Sats from " + str( diff --git a/core/dvm.py b/core/dvm.py index 7926ded..f931f85 100644 --- a/core/dvm.py +++ b/core/dvm.py @@ -15,7 +15,8 @@ from utils.database_utils import create_sql_table, get_or_add_user, update_user_ from utils.mediasource_utils import input_data_file_duration from utils.nostr_utils import get_event_by_id, get_referenced_event_by_id, send_event, check_and_decrypt_tags from utils.output_utils import build_status_reaction -from utils.zap_utils import check_bolt11_ln_bits_is_paid, create_bolt11_ln_bits, parse_zap_event_tags +from utils.zap_utils import check_bolt11_ln_bits_is_paid, create_bolt11_ln_bits, parse_zap_event_tags, \ + parse_amount_from_bolt11_invoice, zap, pay_bolt11_ln_bits from utils.cashu_utils import redeem_cashu use_logger = False @@ -115,7 +116,8 @@ class DVM: cashu_redeemed = False if cashu != "": print(cashu) - cashu_redeemed, cashu_message, redeem_amount, fees = redeem_cashu(cashu, self.dvm_config, self.client, int(amount)) + cashu_redeemed, cashu_message, redeem_amount, fees = redeem_cashu(cashu, self.dvm_config, + self.client, int(amount)) print(cashu_message) if cashu_message != "success": send_job_status_reaction(nip90_event, "error", False, amount, self.client, cashu_message, @@ -131,7 +133,10 @@ class DVM: send_job_status_reaction(nip90_event, "processing", True, 0, client=self.client, dvm_config=self.dvm_config) - do_work(nip90_event) + # when we reimburse users on error make sure to not send anything if it was free + if user.iswhitelisted or task_is_free: + amount = 0 + do_work(nip90_event, amount) # if task is directed to us via p tag and user has balance, do the job and update balance elif p_tag_str == self.dvm_config.PUBLIC_KEY and user.balance >= int(amount): balance = max(user.balance - int(amount), 0) @@ -147,7 +152,7 @@ class DVM: send_job_status_reaction(nip90_event, "processing", True, 0, client=self.client, dvm_config=self.dvm_config) - do_work(nip90_event) + do_work(nip90_event, amount) # else send a payment required event to user elif p_tag_str == "" or p_tag_str == self.dvm_config.PUBLIC_KEY: @@ -228,10 +233,10 @@ class DVM: # If payment-required appears before processing self.job_list.pop(index) print("Starting work...") - do_work(job_event) + do_work(job_event, invoice_amount) else: print("Job not in List, but starting work...") - do_work(job_event) + do_work(job_event, invoice_amount) else: send_job_status_reaction(job_event, "payment-rejected", @@ -314,17 +319,14 @@ class DVM: task = get_task(original_event, self.client, self.dvm_config) for dvm in self.dvm_config.SUPPORTED_DVMS: - if task == dvm.TASK: - try: - post_processed = dvm.post_process(data, original_event) - send_nostr_reply_event(post_processed, original_event.as_json()) - except Exception as e: - send_job_status_reaction(original_event, "error", content=str(e), + if task == dvm.TASK: + try: + post_processed = dvm.post_process(data, original_event) + send_nostr_reply_event(post_processed, original_event.as_json()) + except Exception as e: + send_job_status_reaction(original_event, "error", content=str(e), dvm_config=self.dvm_config, -) - - - + ) def send_nostr_reply_event(content, original_event_as_str): original_event = Event.from_json(original_event_as_str) @@ -443,7 +445,7 @@ class DVM: EventDefinitions.KIND_FEEDBACK) + " Reaction: " + status + " " + reaction_event.as_json()) return reaction_event.as_json() - def do_work(job_event): + def do_work(job_event, amount): if ((EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= job_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC) or job_event.kind() == EventDefinitions.KIND_DM): @@ -460,14 +462,27 @@ class DVM: send_nostr_reply_event(post_processed, job_event.as_json()) except Exception as e: send_job_status_reaction(job_event, "error", content=str(e), - dvm_config=self.dvm_config, - ) + dvm_config=self.dvm_config) except Exception as e: print(e) # we could send the exception here to the user, but maybe that's not a good idea after all. send_job_status_reaction(job_event, "error", content="An error occurred", dvm_config=self.dvm_config) - # TODO send sats back on error + # Zapping back the user on error + if amount > 0: + user = get_or_add_user(self.dvm_config.DB, job_event.pubkey().to_hex(), + client=self.client, config=self.dvm_config) + print(user.lud16 + " " + str(amount)) + bolt11 = zap(user.lud16, amount, "Couldn't finish job, returning sats", job_event, + self.keys, self.dvm_config, zaptype="private") + if bolt11 is None: + print("Receiver has no Lightning address, can't zap back.") + return + try: + payment_hash = pay_bolt11_ln_bits(bolt11, self.dvm_config) + except Exception as e: + print(e) + return @@ -484,7 +499,8 @@ class DVM: client=self.client, dvm_config=self.dvm_config) print("[" + self.dvm_config.NIP89.NAME + "] doing work from joblist") - do_work(job.event) + amount = parse_amount_from_bolt11_invoice(job.bolt11) + do_work(job.event, amount) elif ispaid is None: # invoice expired self.job_list.remove(job) diff --git a/main.py b/main.py index bda58d4..f81f61e 100644 --- a/main.py +++ b/main.py @@ -19,6 +19,8 @@ from utils.dvmconfig import DVMConfig from utils.external_dvm_utils import build_external_dvm from utils.nostr_utils import check_and_set_private_key from utils.output_utils import PostProcessFunctionType +from utils.zap_utils import check_and_set_ln_bits_keys +from nostr_sdk import Keys def playground(): @@ -26,10 +28,13 @@ def playground(): # Note this is very basic for now and still under development bot_config = DVMConfig() bot_config.PRIVATE_KEY = check_and_set_private_key("bot") - 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 + npub = Keys.from_sk_str(bot_config.PRIVATE_KEY).public_key().to_bech32() + invoice_key, admin_key, wallet_id, user_id, lnaddress = check_and_set_ln_bits_keys("bot", npub) + bot_config.LNBITS_INVOICE_KEY = invoice_key + bot_config.LNBITS_ADMIN_KEY = admin_key # The dvm might pay failed jobs back bot_config.LNBITS_URL = os.getenv("LNBITS_HOST") + # Generate an optional Admin Config, in this case, whenever we give our DVMs this config, they will (re)broadcast # their NIP89 announcement # You can create individual admins configs and hand them over when initializing the dvm, @@ -37,10 +42,13 @@ def playground(): # If you use this global config, options will be set for all dvms that use it. admin_config = AdminConfig() admin_config.REBROADCAST_NIP89 = False + admin_config.LUD16 = lnaddress # Set rebroadcast to true once you have set your NIP89 descriptions and d tags. You only need to rebroadcast once you # want to update your NIP89 descriptions + + # Update the DVMs (not the bot) profile. For example after you updated the NIP89 or the lnaddress, you can automatically update profiles here. admin_config.UPDATE_PROFILE = False - admin_config.LUD16 = "" + # Spawn some DVMs in the playground and run them # You can add arbitrary DVMs there and instantiate them here diff --git a/tasks/advanced_search.py b/tasks/advanced_search.py index ec1f8ab..42b78e1 100644 --- a/tasks/advanced_search.py +++ b/tasks/advanced_search.py @@ -17,6 +17,7 @@ from utils.dvmconfig import DVMConfig from utils.nip89_utils import NIP89Config, check_and_set_d_tag from utils.nostr_utils import get_event_by_id, check_and_set_private_key from utils.output_utils import post_process_list_to_users, post_process_list_to_events +from utils.zap_utils import check_and_set_ln_bits_keys """ This File contains a Module to search for notes @@ -143,8 +144,12 @@ class AdvancedSearch(DVMTaskInterface): def build_example(name, identifier, admin_config): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) - dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + npub = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_bech32() + invoice_key, admin_key, wallet_id, user_id, lnaddress = check_and_set_ln_bits_keys(identifier, npub) + dvm_config.LNBITS_INVOICE_KEY = invoice_key + dvm_config.LNBITS_ADMIN_KEY = admin_key # The dvm might pay failed jobs back dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + admin_config.LUD16 = lnaddress # Add NIP89 nip90params = { "user": { diff --git a/tasks/convert_media.py b/tasks/convert_media.py index 8933bd9..a5e38c2 100644 --- a/tasks/convert_media.py +++ b/tasks/convert_media.py @@ -15,6 +15,8 @@ from utils.nip89_utils import NIP89Config, check_and_set_d_tag from utils.mediasource_utils import organize_input_media_data from utils.nostr_utils import check_and_set_private_key from utils.output_utils import upload_media_to_hoster +from utils.zap_utils import check_and_set_ln_bits_keys +from nostr_sdk import Keys """ This File contains a Module to call Google Translate Services locally on the DVM Machine @@ -87,8 +89,12 @@ class MediaConverter(DVMTaskInterface): def build_example(name, identifier, admin_config): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) - dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + npub = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_bech32() + invoice_key, admin_key, wallet_id, user_id, lnaddress = check_and_set_ln_bits_keys(identifier, npub) + dvm_config.LNBITS_INVOICE_KEY = invoice_key + dvm_config.LNBITS_ADMIN_KEY = admin_key # The dvm might pay failed jobs back dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + admin_config.LUD16 = lnaddress # Add NIP89 nip90params = { "media_format": { diff --git a/tasks/discovery_inactive_follows.py b/tasks/discovery_inactive_follows.py index bc7c9af..1f6165b 100644 --- a/tasks/discovery_inactive_follows.py +++ b/tasks/discovery_inactive_follows.py @@ -17,6 +17,7 @@ from utils.dvmconfig import DVMConfig from utils.nip89_utils import NIP89Config, check_and_set_d_tag from utils.nostr_utils import get_event_by_id, check_and_set_private_key from utils.output_utils import post_process_list_to_users +from utils.zap_utils import check_and_set_ln_bits_keys """ This File contains a Module to find inactive follows for a user on nostr @@ -174,8 +175,12 @@ class DiscoverInactiveFollows(DVMTaskInterface): def build_example(name, identifier, admin_config): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) - dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + npub = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_bech32() + invoice_key, admin_key, wallet_id, user_id, lnaddress = check_and_set_ln_bits_keys(identifier, npub) + dvm_config.LNBITS_INVOICE_KEY = invoice_key + dvm_config.LNBITS_ADMIN_KEY = admin_key # The dvm might pay failed jobs back dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + admin_config.LUD16 = lnaddress # Add NIP89 nip90params = { "user": { diff --git a/tasks/imagegeneration_openai_dalle.py b/tasks/imagegeneration_openai_dalle.py index 5b02fbc..02112d6 100644 --- a/tasks/imagegeneration_openai_dalle.py +++ b/tasks/imagegeneration_openai_dalle.py @@ -15,7 +15,8 @@ from utils.dvmconfig import DVMConfig from utils.nip89_utils import NIP89Config, check_and_set_d_tag from utils.nostr_utils import check_and_set_private_key from utils.output_utils import upload_media_to_hoster -from utils.zap_utils import get_price_per_sat +from utils.zap_utils import get_price_per_sat, check_and_set_ln_bits_keys +from nostr_sdk import Keys """ This File contains a Module to transform Text input on OpenAI's servers with DALLE-3 and receive results back. @@ -127,8 +128,12 @@ class ImageGenerationDALLE(DVMTaskInterface): def build_example(name, identifier, admin_config): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) - dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + npub = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_bech32() + invoice_key, admin_key, wallet_id, user_id, lnaddress = check_and_set_ln_bits_keys(identifier, npub) + dvm_config.LNBITS_INVOICE_KEY = invoice_key + dvm_config.LNBITS_ADMIN_KEY = admin_key # The dvm might pay failed jobs back dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + admin_config.LUD16 = lnaddress profit_in_sats = 10 dvm_config.FIX_COST = int(((4.0 / (get_price_per_sat("USD") * 100)) + profit_in_sats)) diff --git a/tasks/imagegeneration_replicate_sdxl.py b/tasks/imagegeneration_replicate_sdxl.py index 7417827..145faf7 100644 --- a/tasks/imagegeneration_replicate_sdxl.py +++ b/tasks/imagegeneration_replicate_sdxl.py @@ -15,7 +15,8 @@ from utils.dvmconfig import DVMConfig from utils.nip89_utils import NIP89Config, check_and_set_d_tag from utils.nostr_utils import check_and_set_private_key from utils.output_utils import upload_media_to_hoster -from utils.zap_utils import get_price_per_sat +from utils.zap_utils import get_price_per_sat, check_and_set_ln_bits_keys +from nostr_sdk import Keys """ This File contains a Module to transform Text input on NOVA-Server and receive results back. @@ -122,8 +123,12 @@ class ImageGenerationReplicateSDXL(DVMTaskInterface): def build_example(name, identifier, admin_config): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) - dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + npub = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_bech32() + invoice_key, admin_key, wallet_id, user_id, lnaddress = check_and_set_ln_bits_keys(identifier, npub) + dvm_config.LNBITS_INVOICE_KEY = invoice_key + dvm_config.LNBITS_ADMIN_KEY = admin_key # The dvm might pay failed jobs back dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + admin_config.LUD16 = lnaddress profit_in_sats = 10 dvm_config.FIX_COST = int(((4.0 / (get_price_per_sat("USD") * 100)) + profit_in_sats)) diff --git a/tasks/textextraction_google.py b/tasks/textextraction_google.py index fe47b86..3ab271a 100644 --- a/tasks/textextraction_google.py +++ b/tasks/textextraction_google.py @@ -14,6 +14,8 @@ from utils.mediasource_utils import organize_input_media_data from utils.nip89_utils import NIP89Config, check_and_set_d_tag from utils.definitions import EventDefinitions from utils.nostr_utils import check_and_set_private_key +from utils.zap_utils import check_and_set_ln_bits_keys +from nostr_sdk import Keys """ This File contains a Module to transform a media file input on Google Cloud @@ -133,8 +135,12 @@ class SpeechToTextGoogle(DVMTaskInterface): def build_example(name, identifier, admin_config): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) - dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + npub = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_bech32() + invoice_key, admin_key, wallet_id, user_id, lnaddress = check_and_set_ln_bits_keys(identifier, npub) + dvm_config.LNBITS_INVOICE_KEY = invoice_key + dvm_config.LNBITS_ADMIN_KEY = admin_key # The dvm might pay failed jobs back dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + admin_config.LUD16 = lnaddress options = {'api_key': None} # A module might have options it can be initialized with, here we set a default model, and the nova-server # address it should use. These parameters can be freely defined in the task component diff --git a/tasks/textextraction_pdf.py b/tasks/textextraction_pdf.py index 4a5ad0b..8134477 100644 --- a/tasks/textextraction_pdf.py +++ b/tasks/textextraction_pdf.py @@ -12,6 +12,8 @@ from utils.definitions import EventDefinitions from utils.dvmconfig import DVMConfig from utils.nip89_utils import NIP89Config, check_and_set_d_tag from utils.nostr_utils import get_event_by_id, check_and_set_private_key +from utils.zap_utils import check_and_set_ln_bits_keys +from nostr_sdk import Keys """ This File contains a Module to extract Text from a PDF file locally on the DVM Machine @@ -96,8 +98,12 @@ class TextExtractionPDF(DVMTaskInterface): def build_example(name, identifier, admin_config): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) - dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + npub = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_bech32() + invoice_key, admin_key, wallet_id, user_id, lnaddress = check_and_set_ln_bits_keys(identifier, npub) + dvm_config.LNBITS_INVOICE_KEY = invoice_key + dvm_config.LNBITS_ADMIN_KEY = admin_key # The dvm might pay failed jobs back dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + admin_config.LUD16 = lnaddress # Add NIP89 nip90params = {} nip89info = { diff --git a/tasks/translation_google.py b/tasks/translation_google.py index b0fa412..d8df530 100644 --- a/tasks/translation_google.py +++ b/tasks/translation_google.py @@ -11,6 +11,8 @@ from utils.definitions import EventDefinitions from utils.dvmconfig import DVMConfig from utils.nip89_utils import NIP89Config, check_and_set_d_tag from utils.nostr_utils import get_referenced_event_by_id, get_event_by_id, check_and_set_private_key +from utils.zap_utils import check_and_set_ln_bits_keys +from nostr_sdk import Keys """ This File contains a Module to call Google Translate Services locally on the DVM Machine @@ -113,8 +115,12 @@ class TranslationGoogle(DVMTaskInterface): def build_example(name, identifier, admin_config): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) - dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + npub = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_bech32() + invoice_key, admin_key, wallet_id, user_id, lnaddress = check_and_set_ln_bits_keys(identifier, npub) + dvm_config.LNBITS_INVOICE_KEY = invoice_key + dvm_config.LNBITS_ADMIN_KEY = admin_key # The dvm might pay failed jobs back dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + admin_config.LUD16 = lnaddress nip90params = { "language": { diff --git a/tasks/translation_libretranslate.py b/tasks/translation_libretranslate.py index ee070fa..80b0f4c 100644 --- a/tasks/translation_libretranslate.py +++ b/tasks/translation_libretranslate.py @@ -12,6 +12,8 @@ from utils.definitions import EventDefinitions from utils.dvmconfig import DVMConfig from utils.nip89_utils import NIP89Config, check_and_set_d_tag from utils.nostr_utils import get_referenced_event_by_id, get_event_by_id, check_and_set_private_key +from utils.zap_utils import check_and_set_ln_bits_keys +from nostr_sdk import Keys """ This File contains a Module to call Google Translate Services locally on the DVM Machine @@ -110,8 +112,12 @@ class TranslationLibre(DVMTaskInterface): def build_example(name, identifier, admin_config): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) - dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + npub = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_bech32() + invoice_key, admin_key, wallet_id, user_id, lnaddress = check_and_set_ln_bits_keys(identifier, npub) + dvm_config.LNBITS_INVOICE_KEY = invoice_key + dvm_config.LNBITS_ADMIN_KEY = admin_key # The dvm might pay failed jobs back dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + admin_config.LUD16 = lnaddress options = {'libre_end_point': os.getenv("LIBRE_TRANSLATE_ENDPOINT"), 'libre_api_key': os.getenv("LIBRE_TRANSLATE_API_KEY")} diff --git a/tasks/videogeneration_replicate_svd.py b/tasks/videogeneration_replicate_svd.py index 92452e4..a62f0a2 100644 --- a/tasks/videogeneration_replicate_svd.py +++ b/tasks/videogeneration_replicate_svd.py @@ -16,7 +16,8 @@ from utils.dvmconfig import DVMConfig from utils.nip89_utils import NIP89Config, check_and_set_d_tag from utils.nostr_utils import check_and_set_private_key from utils.output_utils import upload_media_to_hoster -from utils.zap_utils import get_price_per_sat +from utils.zap_utils import get_price_per_sat, check_and_set_ln_bits_keys +from nostr_sdk import Keys """ This File contains a Module to transform an image to a short video clip using Stable Video Diffusion with replicate @@ -113,8 +114,12 @@ class VideoGenerationReplicateSVD(DVMTaskInterface): def build_example(name, identifier, admin_config): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) - dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + npub = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_bech32() + invoice_key, admin_key, wallet_id, user_id, lnaddress = check_and_set_ln_bits_keys(identifier, npub) + dvm_config.LNBITS_INVOICE_KEY = invoice_key + dvm_config.LNBITS_ADMIN_KEY = admin_key # The dvm might pay failed jobs back dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + admin_config.LUD16 = lnaddress profit_in_sats = 10 cost_in_cent = 4.0 dvm_config.FIX_COST = int(((cost_in_cent / (get_price_per_sat("USD") * 100)) + profit_in_sats)) @@ -136,6 +141,7 @@ def build_example(name, identifier, admin_config): nip89info["image"]) nip89config.CONTENT = json.dumps(nip89info) # We add an optional AdminConfig for this one, and tell the dvm to rebroadcast its NIP89 + return VideoGenerationReplicateSVD(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config) diff --git a/utils/nostr_utils.py b/utils/nostr_utils.py index 683ec16..3f7e0d6 100644 --- a/utils/nostr_utils.py +++ b/utils/nostr_utils.py @@ -150,9 +150,9 @@ def update_profile(dvm_config, client, lud16=""): .set_display_name(name) \ .set_about(about) \ .set_picture(image) \ - .set_lud16(lud16) + .set_lud16(lud16) \ + .set_nip05(lud16) # .set_banner("https://example.com/banner.png") \ - # .set_nip05("username@example.com") \ print(f"Setting profile metadata for {keys.public_key().to_bech32()}...") print(metadata.as_json()) diff --git a/utils/zap_utils.py b/utils/zap_utils.py index 0f2d23e..8c7cf98 100644 --- a/utils/zap_utils.py +++ b/utils/zap_utils.py @@ -1,18 +1,29 @@ -# LIGHTNING/CASHU/ZAP FUNCTIONS +# LIGHTNING/ZAP FUNCTIONS import json import os import urllib.parse +from pathlib import Path + import requests from Crypto.Cipher import AES from Crypto.Util.Padding import pad from bech32 import bech32_decode, convertbits, bech32_encode from nostr_sdk import nostr_sdk, PublicKey, SecretKey, Event, EventBuilder, Tag, Keys from utils.dvmconfig import DVMConfig -from utils.nostr_utils import get_event_by_id, check_and_decrypt_own_tags +from utils.nostr_utils import get_event_by_id, check_and_decrypt_own_tags, update_profile import lnurl from hashlib import sha256 +import dotenv +# TODO tor connection to lnbits +# proxies = { +# 'http': 'socks5h://127.0.0.1:9050', +# 'https': 'socks5h://127.0.0.1:9050' +# } + +proxies = {} + def parse_zap_event_tags(zap_event, keys, name, client, config): zapped_event = None invoice_amount = 0 @@ -116,12 +127,34 @@ def create_bolt11_lud16(lud16, amount): except: return None +def create_lnbits_account(name): + if os.getenv("LNBITS_ADMIN_ID") is None or os.getenv("LNBITS_ADMIN_ID") == "": + print("No admin id set, no wallet created.") + return + data = { + 'admin_id': os.getenv("LNBITS_ADMIN_ID"), + 'wallet_name': name, + 'user_name': name, + } + try: + json_object = json.dumps(data) + url = os.getenv("LNBITS_HOST") + '/usermanager/api/v1/users' + print(url) + headers = {'X-API-Key': os.getenv("LNBITS_ADMIN_KEY"), 'Content-Type': 'application/json', 'charset': 'UTF-8'} + r = requests.post(url, data=json_object, headers=headers, proxies=proxies) + walletjson = json.loads(r.text) + print(walletjson) + if walletjson.get("wallets"): + return walletjson['wallets'][0]['inkey'], walletjson['wallets'][0]['adminkey'], walletjson['wallets'][0]['id'], walletjson['id'], "success" + except: + print("error creating wallet") + def check_bolt11_ln_bits_is_paid(payment_hash: str, config: DVMConfig): url = config.LNBITS_URL + "/api/v1/payments/" + payment_hash headers = {'X-API-Key': config.LNBITS_INVOICE_KEY, 'Content-Type': 'application/json', 'charset': 'UTF-8'} try: - res = requests.get(url, headers=headers) + res = requests.get(url, headers=headers, proxies=proxies) obj = json.loads(res.text) if obj.get("paid"): return obj["paid"] @@ -264,3 +297,74 @@ def get_price_per_sat(currency): price_currency_per_sat = 0.0004 return price_currency_per_sat + + + +def make_ln_address_nostdress(identifier, npub, pin, nostdressdomain): + #env_path = Path('.env') + #if env_path.is_file(): + # dotenv.load_dotenv(env_path, verbose=True, override=True) + + + print(os.getenv("LNBITS_INVOICE_KEY_" + identifier.upper())) + data = { + 'name': identifier, + 'domain': nostdressdomain, + 'kind': "lnbits", + 'host': os.getenv("LNBITS_HOST"), + 'key': os.getenv("LNBITS_INVOICE_KEY_" + identifier.upper()), + 'pin': pin, + 'npub': npub, + 'currentname': " " + } + + + try: + url = "https://" + nostdressdomain + "/api/easy/" + res = requests.post(url, data=data) + print(res.text) + obj = json.loads(res.text) + + if obj.get("ok"): + return identifier + "@" + nostdressdomain, obj["pin"] + except Exception as e: + print(e) + return "", "" + +def check_and_set_ln_bits_keys(identifier, npub): + + if not os.getenv("LNBITS_INVOICE_KEY_" + identifier.upper()): + invoicekey, adminkey, walletid, userid, success = create_lnbits_account(identifier) + add_key_to_env_file("LNBITS_INVOICE_KEY_" + identifier.upper(), invoicekey) + add_key_to_env_file("LNBITS_ADMIN_KEY_" + identifier.upper(), adminkey) + add_key_to_env_file("LNBITS_USER_ID_" + identifier.upper(), userid) + add_key_to_env_file("LNBITS_WALLET_ID_" + identifier.upper(), userid) + + lnaddress = "" + pin = "" + if os.getenv("NOSTDRESS_DOMAIN"): + print(os.getenv("NOSTDRESS_DOMAIN")) + lnaddress, pin = make_ln_address_nostdress(identifier, npub, " ", os.getenv("NOSTDRESS_DOMAIN")) + add_key_to_env_file("LNADDRESS_" + identifier.upper(), lnaddress) + add_key_to_env_file("LNADDRESS_PIN_" + identifier.upper(), pin) + + return invoicekey, adminkey, userid, walletid, lnaddress + else: + return (os.getenv("LNBITS_INVOICE_KEY_" + identifier.upper()), + os.getenv("LNBITS_ADMIN_KEY_" + identifier.upper()), + os.getenv("LNBITS_USER_ID_" + identifier.upper()), + os.getenv("LNBITS_WALLET_ID_" + identifier.upper()), + os.getenv("LNADDRESS_" + identifier.upper())) + + + + + + +def add_key_to_env_file(value, oskey): + env_path = Path('.env') + if env_path.is_file(): + dotenv.load_dotenv(env_path, verbose=True, override=True) + dotenv.set_key(env_path, value, oskey) + +