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 = "" 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(

View File

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

View File

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