From ab624e11b854b3f32c547b6bc73358004c7049ed Mon Sep 17 00:00:00 2001 From: Believethehype Date: Sat, 2 Dec 2023 21:27:29 +0100 Subject: [PATCH] check fees at mint, accept cashu with the bot --- bot/bot.py | 23 +++++++++++--- core/dvm.py | 2 +- utils/zap_utils.py | 77 +++++++++++++++++++++++++++++----------------- 3 files changed, 67 insertions(+), 35 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index 9f911db..02b5d7b 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -10,10 +10,10 @@ from nostr_sdk import (Keys, Client, Timestamp, Filter, nip04_decrypt, HandleNot from utils.admin_utils import admin_make_database_updates from utils.database_utils import get_or_add_user, update_user_balance, create_sql_table, update_sql_table, User from utils.definitions import EventDefinitions -from utils.nip89_utils import nip89_fetch_events_pubkey +from utils.nip89_utils import nip89_fetch_events_pubkey, NIP89Config from utils.nostr_utils import send_event from utils.output_utils import PostProcessFunctionType, post_process_list_to_users, post_process_list_to_events -from utils.zap_utils import parse_zap_event_tags, pay_bolt11_ln_bits, zap +from utils.zap_utils import parse_zap_event_tags, pay_bolt11_ln_bits, zap, redeem_cashu class Bot: @@ -24,6 +24,9 @@ class Bot: self.NAME = "Bot" dvm_config.DB = "db/" + self.NAME + ".db" self.dvm_config = dvm_config + nip89config = NIP89Config() + nip89config.NAME = self.NAME + self.dvm_config.NIP89 = nip89config self.admin_config = admin_config self.keys = Keys.from_sk_str(dvm_config.PRIVATE_KEY) wait_for_send = True @@ -153,9 +156,19 @@ class Bot: , None).to_event(self.keys) send_event(evt, client=self.client, dvm_config=dvm_config) - - - + elif decrypted_text.startswith("cashuA"): + print("Received Cashu token:" + decrypted_text) + cashu_redeemed, cashu_message, total_amount, fees = redeem_cashu(decrypted_text, self.dvm_config, self.client) + print(cashu_message) + if cashu_message == "success": + update_user_balance(self.dvm_config.DB, sender, total_amount, client=self.client, + config=self.dvm_config) + else: + time.sleep(2.0) + message = "Error: " + cashu_message + ". Token has not been redeemed." + evt = EventBuilder.new_encrypted_direct_msg(self.keys, PublicKey.from_hex(sender), message, + None).to_event(self.keys) + send_event(evt, client=self.client, dvm_config=self.dvm_config) else: # Build an overview of known DVMs and send it to the user answer_overview(nostr_event) diff --git a/core/dvm.py b/core/dvm.py index 68a3276..fde9c41 100644 --- a/core/dvm.py +++ b/core/dvm.py @@ -114,7 +114,7 @@ class DVM: cashu_redeemed = False if cashu != "": print(cashu) - cashu_redeemed, cashu_message = redeem_cashu(cashu, int(amount), self.dvm_config, self.client) + 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, diff --git a/utils/zap_utils.py b/utils/zap_utils.py index 079e972..7c24999 100644 --- a/utils/zap_utils.py +++ b/utils/zap_utils.py @@ -92,7 +92,7 @@ def create_bolt11_ln_bits(sats: int, config: DVMConfig) -> (str, str): res = requests.post(url, json=data, headers=headers) obj = json.loads(res.text) if obj.get("payment_request") and obj.get("payment_hash"): - return obj["payment_request"], obj["payment_hash"]# + return obj["payment_request"], obj["payment_hash"] # else: print(res.text) return None, None @@ -102,7 +102,6 @@ def create_bolt11_ln_bits(sats: int, config: DVMConfig) -> (str, str): def create_bolt11_lud16(lud16, amount): - if lud16.startswith("LNURL") or lud16.startswith("lnurl"): url = lnurl.decode(lud16) elif '@' in lud16: # LNaddress @@ -252,43 +251,63 @@ def zap(lud16: str, amount: int, content, zapped_event: Event, keys, dvm_config, def parse_cashu(cashu_token: str): try: - try: - prefix = "cashuA" - assert cashu_token.startswith(prefix), Exception( - f"Token prefix not valid. Expected {prefix}." - ) - if not cashu_token.endswith("="): - cashu_token = cashu_token + "=" - - token_base64 = cashu_token[len(prefix):] - cashu = json.loads(base64.urlsafe_b64decode(token_base64)) - except Exception as e: - print(e) - + prefix = "cashuA" + assert cashu_token.startswith(prefix), Exception( + f"Token prefix not valid. Expected {prefix}." + ) + if not cashu_token.endswith("="): + cashu_token = str(cashu_token) + "==" + print(cashu_token) + token_base64 = cashu_token[len(prefix):].encode("utf-8") + cashu = json.loads(base64.urlsafe_b64decode(token_base64)) token = cashu["token"][0] proofs = token["proofs"] mint = token["mint"] total_amount = 0 for proof in proofs: total_amount += proof["amount"] - fees = max(int(total_amount * 0.02), 3) - redeem_invoice_amount = total_amount - fees - return proofs, mint, redeem_invoice_amount, total_amount + + + return proofs, mint, total_amount, None except Exception as e: - print("Could not parse this cashu token") - return None, None, None, None + print(e) + return None, None, None, "Cashu Parser: " + str(e) -def redeem_cashu(cashu, required_amount, config, client, update_self=False) -> (bool, str): - proofs, mint, redeem_invoice_amount, total_amount = parse_cashu(cashu) - fees = total_amount - redeem_invoice_amount +def redeem_cashu(cashu, config, client, required_amount=0, update_self=False) -> (bool, str, int, int): + proofs, mint, total_amount, message = parse_cashu(cashu) + if message is not None: + return False, message, 0, 0 + + # Not sure if this the best way to go, we first create an invoice that we send to the mint, we catch the fees + # for that invoice, and create another invoice with the amount without fees to melt. + if config.LNBITS_INVOICE_KEY != "": + invoice, paymenthash = create_bolt11_ln_bits(total_amount, config) + else: + + user = get_or_add_user(db=config.DB, npub=config.PUBLIC_KEY, + client=client, config=config, update=update_self) + invoice = create_bolt11_lud16(user.lud16, total_amount) + print(invoice) + if invoice is None: + return False, "couldn't create invoice", 0, 0 + + url = mint + "/checkfees" # Melt cashu tokens at Mint + json_object = {"pr": invoice} + headers = {"Content-Type": "application/json; charset=utf-8"} + request_body = json.dumps(json_object).encode('utf-8') + request = requests.post(url, data=request_body, headers=headers) + tree = json.loads(request.text) + fees = tree["fee"] + print("Fees on this mint are " + str(fees) + " Sats") + redeem_invoice_amount = total_amount -fees if redeem_invoice_amount < required_amount: err = ("Token value (Payment: " + str(total_amount) + " Sats. Fees: " + str(fees) + " Sats) below required amount of " + str(required_amount) + " Sats. Cashu token has not been claimed.") print("[" + config.NIP89.NAME + "] " + err) - return False, err + return False, err, 0, 0 if config.LNBITS_INVOICE_KEY != "": invoice, paymenthash = create_bolt11_ln_bits(redeem_invoice_amount, config) @@ -298,8 +317,7 @@ def redeem_cashu(cashu, required_amount, config, client, update_self=False) -> ( client=client, config=config, update=update_self) invoice = create_bolt11_lud16(user.lud16, redeem_invoice_amount) print(invoice) - if invoice is None: - return False, "couldn't create invoice" + try: url = mint + "/melt" # Melt cashu tokens at Mint json_object = {"proofs": proofs, "pr": invoice} @@ -312,7 +330,7 @@ def redeem_cashu(cashu, required_amount, config, client, update_self=False) -> ( print(is_paid) if is_paid: print("cashu token redeemed") - return True, "success" + return True, "success", redeem_invoice_amount, fees else: msg = tree.get("detail").split('.')[0].strip() if tree.get("detail") else None print(msg) @@ -320,7 +338,9 @@ def redeem_cashu(cashu, required_amount, config, client, update_self=False) -> ( except Exception as e: print(e) - return False, "" + return False, "", redeem_invoice_amount, fees + + def get_price_per_sat(currency): import requests @@ -337,4 +357,3 @@ def get_price_per_sat(currency): price_currency_per_sat = 0.0004 return price_currency_per_sat -