mirror of
https://github.com/believethehype/nostrdvm.git
synced 2025-06-05 00:19:12 +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 = ""
|
||||
for tag in nip90_event.tags():
|
||||
if tag.as_vec()[0] == "cashu":
|
||||
cashu = tag.as_vec()[0]
|
||||
cashu = tag.as_vec()[1]
|
||||
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,
|
||||
get_duration=(not user.iswhitelisted),
|
||||
@ -121,8 +121,7 @@ class DVM:
|
||||
|
||||
cashu_redeemed = False
|
||||
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.iswhitelisted or task_is_free or cashu_redeemed:
|
||||
print(
|
||||
|
@ -81,7 +81,7 @@ def nostr_client_test_image_private(prompt, cashutoken):
|
||||
i_tag = Tag.parse(["i", prompt, "text"])
|
||||
outTag = Tag.parse(["output", "image/png;format=url"])
|
||||
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_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"])
|
||||
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(),
|
||||
relays_tag.as_vec(), alt_tag.as_vec(), cashu_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(), pTag.as_vec(), cashu_tag.as_vec()])
|
||||
|
||||
|
||||
encrypted_params = nip04_encrypt(keys.secret_key(), receiver_keys.public_key(),
|
||||
encrypted_params_string)
|
||||
|
||||
p_tag = Tag.parse(['p', keys.public_key().to_hex()])
|
||||
|
||||
encrypted_tag = Tag.parse(['encrypted'])
|
||||
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)
|
||||
for relay in relay_list:
|
||||
client.add_relay(relay)
|
||||
@ -130,9 +130,10 @@ def nostr_client():
|
||||
#nostr_client_test_translation("note1p8cx2dz5ss5gnk7c59zjydcncx6a754c0hsyakjvnw8xwlm5hymsnc23rs", "event", "es", 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):
|
||||
def handle(self, relay_url, event):
|
||||
print(f"Received new event from {relay_url}: {event.as_json()}")
|
||||
|
@ -3,45 +3,19 @@ import base64
|
||||
import json
|
||||
import os
|
||||
import urllib.parse
|
||||
|
||||
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.database_utils import get_or_add_user
|
||||
from utils.dvmconfig import DVMConfig
|
||||
from utils.nostr_utils import get_event_by_id
|
||||
import lnurl
|
||||
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):
|
||||
zapped_event = None
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
if config.LNBITS_URL == "":
|
||||
return None
|
||||
url = config.LNBITS_URL + "/api/v1/payments"
|
||||
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'}
|
||||
@ -92,6 +95,24 @@ def create_bolt11_ln_bits(sats: int, config: DVMConfig) -> (str, str):
|
||||
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):
|
||||
url = config.LNBITS_URL + "/api/v1/payments/" + payment_hash
|
||||
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
|
||||
|
||||
|
||||
def parse_cashu(cashuToken):
|
||||
def parse_cashu(cashu_token):
|
||||
try:
|
||||
base64token = cashuToken.replace("cashuA", "")
|
||||
cashu = json.loads(base64.b64decode(base64token).decode('utf-8'))
|
||||
try:
|
||||
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]
|
||||
print(token)
|
||||
proofs = token["proofs"]
|
||||
mint = token["mint"]
|
||||
|
||||
totalAmount = 0
|
||||
total_amount = 0
|
||||
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:
|
||||
print("Could not parse this cashu token")
|
||||
return None, None, None, None, None, None
|
||||
return None, None, None
|
||||
|
||||
|
||||
def redeem_cashu(cashu, config):
|
||||
# TODO untested
|
||||
is_redeemed = False
|
||||
cashuToken, mint, totalAmount, fees, redeemInvoiceAmount, proofs = parse_cashu(cashu)
|
||||
invoice = create_bolt11_ln_bits(totalAmount, config)
|
||||
def redeem_cashu(cashu, config, client):
|
||||
proofs, mint, redeem_invoice_amount = parse_cashu(cashu)
|
||||
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(),
|
||||
client=client, config=config)
|
||||
invoice = create_bolt11_lud16(user.lud16, redeem_invoice_amount)
|
||||
print(invoice)
|
||||
if invoice is None:
|
||||
return False
|
||||
try:
|
||||
url = mint + "/melt" # Melt cashu tokens at Mint
|
||||
json_object = {"proofs": proofs, "pr": invoice}
|
||||
|
||||
headers = {"Content-Type": "application/json; charset=utf-8"}
|
||||
request = requests.post(url, json=json_object, headers=headers)
|
||||
|
||||
if request.status_code == 200:
|
||||
tree = json.loads(request.text)
|
||||
successful = tree.get("paid") == "true"
|
||||
if successful:
|
||||
is_redeemed = True
|
||||
else:
|
||||
msg = tree.get("detail", "").split('.')[0].strip() if tree.get("detail") else None
|
||||
is_redeemed = False
|
||||
print(msg)
|
||||
|
||||
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("token redeemed")
|
||||
return True
|
||||
else:
|
||||
msg = tree.get("detail").split('.')[0].strip() if tree.get("detail") else None
|
||||
print(msg)
|
||||
return False
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
return is_redeemed
|
||||
return False
|
||||
|
Loading…
x
Reference in New Issue
Block a user