cashu integration

This commit is contained in:
Believethehype 2023-11-27 15:45:32 +01:00
parent ee69c35e4b
commit 2f09ddd674
3 changed files with 101 additions and 69 deletions

7
dvm.py
View File

@ -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(

View File

@ -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()}")

View File

@ -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