mirror of
https://github.com/believethehype/nostrdvm.git
synced 2025-06-11 05:50:47 +02:00
cashu integration
This commit is contained in:
parent
ee69c35e4b
commit
2f09ddd674
7
dvm.py
7
dvm.py
@ -96,9 +96,9 @@ class DVM:
|
|||||||
p_tag_str = ""
|
p_tag_str = ""
|
||||||
for tag in nip90_event.tags():
|
for tag in nip90_event.tags():
|
||||||
if tag.as_vec()[0] == "cashu":
|
if tag.as_vec()[0] == "cashu":
|
||||||
cashu = tag.as_vec()[0]
|
cashu = tag.as_vec()[1]
|
||||||
elif tag.as_vec()[0] == "p":
|
elif tag.as_vec()[0] == "p":
|
||||||
p_tag_str = tag.as_vec()[0]
|
p_tag_str = tag.as_vec()[1]
|
||||||
|
|
||||||
task_supported, task, duration = check_task_is_supported(nip90_event, client=self.client,
|
task_supported, task, duration = check_task_is_supported(nip90_event, client=self.client,
|
||||||
get_duration=(not user.iswhitelisted),
|
get_duration=(not user.iswhitelisted),
|
||||||
@ -121,8 +121,7 @@ class DVM:
|
|||||||
|
|
||||||
cashu_redeemed = False
|
cashu_redeemed = False
|
||||||
if cashu != "":
|
if cashu != "":
|
||||||
cashu_redeemed = redeem_cashu(cashu, self.dvm_config)
|
cashu_redeemed = redeem_cashu(cashu, self.dvm_config, self.client)
|
||||||
|
|
||||||
# if user is whitelisted or task is free, just do the job
|
# if user is whitelisted or task is free, just do the job
|
||||||
if user.iswhitelisted or task_is_free or cashu_redeemed:
|
if user.iswhitelisted or task_is_free or cashu_redeemed:
|
||||||
print(
|
print(
|
||||||
|
@ -81,7 +81,7 @@ def nostr_client_test_image_private(prompt, cashutoken):
|
|||||||
i_tag = Tag.parse(["i", prompt, "text"])
|
i_tag = Tag.parse(["i", prompt, "text"])
|
||||||
outTag = Tag.parse(["output", "image/png;format=url"])
|
outTag = Tag.parse(["output", "image/png;format=url"])
|
||||||
paramTag1 = Tag.parse(["param", "size", "1024x1024"])
|
paramTag1 = Tag.parse(["param", "size", "1024x1024"])
|
||||||
tTag = Tag.parse(["t", "bitcoin"])
|
pTag = Tag.parse(["p", receiver_keys.public_key().to_hex()])
|
||||||
|
|
||||||
bid = str(50 * 1000)
|
bid = str(50 * 1000)
|
||||||
bid_tag = Tag.parse(['bid', bid, bid])
|
bid_tag = Tag.parse(['bid', bid, bid])
|
||||||
@ -89,17 +89,17 @@ def nostr_client_test_image_private(prompt, cashutoken):
|
|||||||
alt_tag = Tag.parse(["alt", "Super secret test"])
|
alt_tag = Tag.parse(["alt", "Super secret test"])
|
||||||
cashu_tag = Tag.parse(["cashu", cashutoken])
|
cashu_tag = Tag.parse(["cashu", cashutoken])
|
||||||
|
|
||||||
encrypted_params_string = json.dumps([i_tag.as_vec(), outTag.as_vec(), paramTag1.as_vec(), tTag, bid_tag.as_vec(),
|
encrypted_params_string = json.dumps([i_tag.as_vec(), outTag.as_vec(), paramTag1.as_vec(), bid_tag.as_vec(),
|
||||||
relays_tag.as_vec(), alt_tag.as_vec(), cashu_tag.as_vec()])
|
relays_tag.as_vec(), alt_tag.as_vec(), pTag.as_vec(), cashu_tag.as_vec()])
|
||||||
|
|
||||||
|
|
||||||
encrypted_params = nip04_encrypt(keys.secret_key(), receiver_keys.public_key(),
|
encrypted_params = nip04_encrypt(keys.secret_key(), receiver_keys.public_key(),
|
||||||
encrypted_params_string)
|
encrypted_params_string)
|
||||||
|
|
||||||
p_tag = Tag.parse(['p', keys.public_key().to_hex()])
|
|
||||||
encrypted_tag = Tag.parse(['encrypted'])
|
encrypted_tag = Tag.parse(['encrypted'])
|
||||||
nip90request = EventBuilder(EventDefinitions.KIND_NIP90_GENERATE_IMAGE, encrypted_params,
|
nip90request = EventBuilder(EventDefinitions.KIND_NIP90_GENERATE_IMAGE, encrypted_params,
|
||||||
[p_tag, encrypted_tag]).to_event(keys)
|
[pTag, encrypted_tag]).to_event(keys)
|
||||||
client = Client(keys)
|
client = Client(keys)
|
||||||
for relay in relay_list:
|
for relay in relay_list:
|
||||||
client.add_relay(relay)
|
client.add_relay(relay)
|
||||||
@ -130,9 +130,10 @@ def nostr_client():
|
|||||||
#nostr_client_test_translation("note1p8cx2dz5ss5gnk7c59zjydcncx6a754c0hsyakjvnw8xwlm5hymsnc23rs", "event", "es", 20,20)
|
#nostr_client_test_translation("note1p8cx2dz5ss5gnk7c59zjydcncx6a754c0hsyakjvnw8xwlm5hymsnc23rs", "event", "es", 20,20)
|
||||||
#nostr_client_test_translation("44a0a8b395ade39d46b9d20038b3f0c8a11168e67c442e3ece95e4a1703e2beb", "event", "zh", 20, 20)
|
#nostr_client_test_translation("44a0a8b395ade39d46b9d20038b3f0c8a11168e67c442e3ece95e4a1703e2beb", "event", "zh", 20, 20)
|
||||||
|
|
||||||
nostr_client_test_image("a beautiful purple ostrich watching the sunset")
|
#nostr_client_test_image("a beautiful purple ostrich watching the sunset")
|
||||||
|
|
||||||
#nostr_client_test_image_private("a beautiful ostrich watching the sunset", cashutoken )
|
cashutoken = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6MSwiQyI6IjAzMjBjMjBkNWZkNTYwODlmYjZjYTllNDFkYjVlM2MzYTAwMTdjNTUzYmY5MzNkZTgwNTg3NDg1YTk5Yjk2Y2E3OSIsInNlY3JldCI6IktrcnVtakdSeDlHTExxZHBQU3J4WUxaZnJjWmFHekdmZ3Q4T2pZN0c4NHM9In0seyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6MiwiQyI6IjAyNjYyMjQzNWUxMzBmM2E0ZWE2NGUyMmI4NGQyYWRhNzM2MjE4MTE3YzZjOWIyMmFkYjAwZTFjMzhmZDBiOTNjNCIsInNlY3JldCI6Ikw4dU1BbnBsQm1pdDA4cDZjQk0vcXhpVDFmejlpbnA3V3RzZEJTV284aEk9In0seyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6NCwiQyI6IjAzMTAxNWM0ZmZhN2U1NzhkNjA0MjFhY2Q2OWEzMTY5NGI4YmRlYTI2YjIwZjgxOWYxOWZhOTNjN2QwZTBiMTdlOCIsInNlY3JldCI6ImRVZ2E2VFo2emRhclozN015NXg2MFdHMzMraitDZnEyOWkzWExjVStDMFE9In0seyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6MTYsIkMiOiIwMzU0YmYxODdjOTgxZjdmNDk5MGExMDVlMmI2MjIxZDNmYTQ2ZWNlMmNjNWE0ZmI2Mzc3NTdjZDJjM2VhZTkzMGMiLCJzZWNyZXQiOiIyeUJJeEo4dkNGVnUvK1VWSzdwSXFjekkrbkZISngyNXF2ZGhWNDByNzZnPSJ9LHsiaWQiOiJYV0MwL0V0YXFTOEMiLCJhbW91bnQiOjMyLCJDIjoiMDJlYTNmMmFhZGI5MTA4MzljZDA5YTlmMTQ1YTZkY2Q4OGZmZDFmM2M5MjZhMzM5MGFmZjczYjM4ZjY0YjQ5NTU2Iiwic2VjcmV0IjoiQU1mU2FxUWFTN0l5WVdEbUpUaVM4NW9ReFNva0p6SzVJL1R6OUJ5UlFLdz0ifV0sIm1pbnQiOiJodHRwczovL2xuYml0cy5iaXRjb2luZml4ZXN0aGlzLm9yZy9jYXNodS9hcGkvdjEvOXVDcDIyUllWVXE4WjI0bzVCMlZ2VyJ9XX0="
|
||||||
|
nostr_client_test_image_private("a beautiful ostrich watching the sunset", cashutoken )
|
||||||
class NotificationHandler(HandleNotification):
|
class NotificationHandler(HandleNotification):
|
||||||
def handle(self, relay_url, event):
|
def handle(self, relay_url, event):
|
||||||
print(f"Received new event from {relay_url}: {event.as_json()}")
|
print(f"Received new event from {relay_url}: {event.as_json()}")
|
||||||
|
@ -3,45 +3,19 @@ import base64
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
from Crypto.Util.Padding import pad
|
from Crypto.Util.Padding import pad
|
||||||
from bech32 import bech32_decode, convertbits, bech32_encode
|
from bech32 import bech32_decode, convertbits, bech32_encode
|
||||||
from nostr_sdk import nostr_sdk, PublicKey, SecretKey, Event, EventBuilder, Tag, Keys
|
from nostr_sdk import nostr_sdk, PublicKey, SecretKey, Event, EventBuilder, Tag, Keys
|
||||||
|
|
||||||
|
from utils.database_utils import get_or_add_user
|
||||||
from utils.dvmconfig import DVMConfig
|
from utils.dvmconfig import DVMConfig
|
||||||
from utils.nostr_utils import get_event_by_id
|
from utils.nostr_utils import get_event_by_id
|
||||||
import lnurl
|
import lnurl
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
|
|
||||||
|
|
||||||
def parse_amount_from_bolt11_invoice(bolt11_invoice: str) -> int:
|
|
||||||
def get_index_of_first_letter(ip):
|
|
||||||
index = 0
|
|
||||||
for c in ip:
|
|
||||||
if c.isalpha():
|
|
||||||
return index
|
|
||||||
else:
|
|
||||||
index = index + 1
|
|
||||||
return len(ip)
|
|
||||||
|
|
||||||
remaining_invoice = bolt11_invoice[4:]
|
|
||||||
index = get_index_of_first_letter(remaining_invoice)
|
|
||||||
identifier = remaining_invoice[index]
|
|
||||||
number_string = remaining_invoice[:index]
|
|
||||||
number = float(number_string)
|
|
||||||
if identifier == 'm':
|
|
||||||
number = number * 100000000 * 0.001
|
|
||||||
elif identifier == 'u':
|
|
||||||
number = number * 100000000 * 0.000001
|
|
||||||
elif identifier == 'n':
|
|
||||||
number = number * 100000000 * 0.000000001
|
|
||||||
elif identifier == 'p':
|
|
||||||
number = number * 100000000 * 0.000000000001
|
|
||||||
|
|
||||||
return int(number)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_zap_event_tags(zap_event, keys, name, client, config):
|
def parse_zap_event_tags(zap_event, keys, name, client, config):
|
||||||
zapped_event = None
|
zapped_event = None
|
||||||
invoice_amount = 0
|
invoice_amount = 0
|
||||||
@ -79,7 +53,36 @@ def parse_zap_event_tags(zap_event, keys, name, client, config):
|
|||||||
return invoice_amount, zapped_event, sender, message, anon
|
return invoice_amount, zapped_event, sender, message, anon
|
||||||
|
|
||||||
|
|
||||||
|
def parse_amount_from_bolt11_invoice(bolt11_invoice: str) -> int:
|
||||||
|
def get_index_of_first_letter(ip):
|
||||||
|
index = 0
|
||||||
|
for c in ip:
|
||||||
|
if c.isalpha():
|
||||||
|
return index
|
||||||
|
else:
|
||||||
|
index = index + 1
|
||||||
|
return len(ip)
|
||||||
|
|
||||||
|
remaining_invoice = bolt11_invoice[4:]
|
||||||
|
index = get_index_of_first_letter(remaining_invoice)
|
||||||
|
identifier = remaining_invoice[index]
|
||||||
|
number_string = remaining_invoice[:index]
|
||||||
|
number = float(number_string)
|
||||||
|
if identifier == 'm':
|
||||||
|
number = number * 100000000 * 0.001
|
||||||
|
elif identifier == 'u':
|
||||||
|
number = number * 100000000 * 0.000001
|
||||||
|
elif identifier == 'n':
|
||||||
|
number = number * 100000000 * 0.000000001
|
||||||
|
elif identifier == 'p':
|
||||||
|
number = number * 100000000 * 0.000000000001
|
||||||
|
|
||||||
|
return int(number)
|
||||||
|
|
||||||
|
|
||||||
def create_bolt11_ln_bits(sats: int, config: DVMConfig) -> (str, str):
|
def create_bolt11_ln_bits(sats: int, config: DVMConfig) -> (str, str):
|
||||||
|
if config.LNBITS_URL == "":
|
||||||
|
return None
|
||||||
url = config.LNBITS_URL + "/api/v1/payments"
|
url = config.LNBITS_URL + "/api/v1/payments"
|
||||||
data = {'out': False, 'amount': sats, 'memo': "Nostr-DVM " + config.NIP89.name}
|
data = {'out': False, 'amount': sats, 'memo': "Nostr-DVM " + config.NIP89.name}
|
||||||
headers = {'X-API-Key': config.LNBITS_INVOICE_KEY, 'Content-Type': 'application/json', 'charset': 'UTF-8'}
|
headers = {'X-API-Key': config.LNBITS_INVOICE_KEY, 'Content-Type': 'application/json', 'charset': 'UTF-8'}
|
||||||
@ -92,6 +95,24 @@ def create_bolt11_ln_bits(sats: int, config: DVMConfig) -> (str, str):
|
|||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def create_bolt11_lud16(lud16, amount):
|
||||||
|
if lud16.startswith("LNURL") or lud16.startswith("lnurl"):
|
||||||
|
url = lnurl.decode(lud16)
|
||||||
|
elif '@' in lud16: # LNaddress
|
||||||
|
url = 'https://' + str(lud16).split('@')[1] + '/.well-known/lnurlp/' + str(lud16).split('@')[0]
|
||||||
|
else: # No lud16 set or format invalid
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
response = requests.get(url)
|
||||||
|
ob = json.loads(response.content)
|
||||||
|
callback = ob["callback"]
|
||||||
|
response = requests.get(callback + "?amount=" + str(int(amount) * 1000))
|
||||||
|
ob = json.loads(response.content)
|
||||||
|
return ob["pr"]
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def check_bolt11_ln_bits_is_paid(payment_hash: str, config: DVMConfig):
|
def check_bolt11_ln_bits_is_paid(payment_hash: str, config: DVMConfig):
|
||||||
url = config.LNBITS_URL + "/api/v1/payments/" + payment_hash
|
url = config.LNBITS_URL + "/api/v1/payments/" + payment_hash
|
||||||
headers = {'X-API-Key': config.LNBITS_INVOICE_KEY, 'Content-Type': 'application/json', 'charset': 'UTF-8'}
|
headers = {'X-API-Key': config.LNBITS_INVOICE_KEY, 'Content-Type': 'application/json', 'charset': 'UTF-8'}
|
||||||
@ -215,50 +236,61 @@ def zap(lud16: str, amount: int, content, zapped_event: Event, keys, dvm_config,
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def parse_cashu(cashuToken):
|
def parse_cashu(cashu_token):
|
||||||
try:
|
try:
|
||||||
base64token = cashuToken.replace("cashuA", "")
|
try:
|
||||||
cashu = json.loads(base64.b64decode(base64token).decode('utf-8'))
|
prefix = "cashuA"
|
||||||
|
assert cashu_token.startswith(prefix), Exception(
|
||||||
|
f"Token prefix not valid. Expected {prefix}."
|
||||||
|
)
|
||||||
|
token_base64 = cashu_token[len(prefix):]
|
||||||
|
cashu = json.loads(base64.urlsafe_b64decode(token_base64))
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
token = cashu["token"][0]
|
token = cashu["token"][0]
|
||||||
|
print(token)
|
||||||
proofs = token["proofs"]
|
proofs = token["proofs"]
|
||||||
mint = token["mint"]
|
mint = token["mint"]
|
||||||
|
total_amount = 0
|
||||||
totalAmount = 0
|
|
||||||
for proof in proofs:
|
for proof in proofs:
|
||||||
totalAmount += proof["amount"]
|
total_amount += proof["amount"]
|
||||||
|
fees = max(int(total_amount * 0.02), 2)
|
||||||
|
redeem_invoice_amount = total_amount - fees
|
||||||
|
return proofs, mint, redeem_invoice_amount
|
||||||
|
|
||||||
fees = max(int(totalAmount * 0.02), 2)
|
|
||||||
redeemInvoiceAmount = totalAmount - fees
|
|
||||||
|
|
||||||
return cashuToken, mint, totalAmount, fees, redeemInvoiceAmount, proofs
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Could not parse this cashu token")
|
print("Could not parse this cashu token")
|
||||||
return None, None, None, None, None, None
|
return None, None, None
|
||||||
|
|
||||||
|
|
||||||
def redeem_cashu(cashu, config):
|
def redeem_cashu(cashu, config, client):
|
||||||
# TODO untested
|
proofs, mint, redeem_invoice_amount = parse_cashu(cashu)
|
||||||
is_redeemed = False
|
if config.LNBITS_INVOICE_KEY != "":
|
||||||
cashuToken, mint, totalAmount, fees, redeemInvoiceAmount, proofs = parse_cashu(cashu)
|
invoice = create_bolt11_ln_bits(redeem_invoice_amount, config)
|
||||||
invoice = create_bolt11_ln_bits(totalAmount, config)
|
else:
|
||||||
|
user = get_or_add_user(db=config.DB, npub=Keys.from_sk_str(config.PRIVATE_KEY).public_key().to_hex(),
|
||||||
|
client=client, config=config)
|
||||||
|
invoice = create_bolt11_lud16(user.lud16, redeem_invoice_amount)
|
||||||
|
print(invoice)
|
||||||
|
if invoice is None:
|
||||||
|
return False
|
||||||
try:
|
try:
|
||||||
url = mint + "/melt" # Melt cashu tokens at Mint
|
url = mint + "/melt" # Melt cashu tokens at Mint
|
||||||
json_object = {"proofs": proofs, "pr": invoice}
|
json_object = {"proofs": proofs, "pr": invoice}
|
||||||
|
|
||||||
headers = {"Content-Type": "application/json; charset=utf-8"}
|
headers = {"Content-Type": "application/json; charset=utf-8"}
|
||||||
request = requests.post(url, json=json_object, headers=headers)
|
request_body = json.dumps(json_object).encode('utf-8')
|
||||||
|
request = requests.post(url, data=request_body, headers=headers)
|
||||||
if request.status_code == 200:
|
tree = json.loads(request.text)
|
||||||
tree = json.loads(request.text)
|
is_paid = (tree.get("paid") == "true") if tree.get("detail") else False
|
||||||
successful = tree.get("paid") == "true"
|
if is_paid:
|
||||||
if successful:
|
print("token redeemed")
|
||||||
is_redeemed = True
|
return True
|
||||||
else:
|
else:
|
||||||
msg = tree.get("detail", "").split('.')[0].strip() if tree.get("detail") else None
|
msg = tree.get("detail").split('.')[0].strip() if tree.get("detail") else None
|
||||||
is_redeemed = False
|
print(msg)
|
||||||
print(msg)
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
return is_redeemed
|
return False
|
||||||
|
Loading…
x
Reference in New Issue
Block a user