Skip to content

Commit

Permalink
Support VPN tunnel + Send kind 9735 zap receipt (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
UTXOnly authored Mar 25, 2024
1 parent b73c118 commit 3bd3184
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 11 deletions.
3 changes: 2 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ VPN_HOST=''# <YOUR_HOST_IP> for you VPN client
LND_REST_PORT=8080 #Default LND REST port is 8080, in most cases you can leave this untouched
LND_INVOICE_MACAROON_HEX="<YOUR_LND_INVOICE_HEX_HERE>"
INTERNET_IDENTIFIER="<IDENTIFIER_HERE>" ## Add the value on the left side of your LNURL identifier for example if your LNURL identifier is "[email protected]" you would add "nabismo" here
HEX_PUBKEY=# <YOUR_NOSTR_HEX_PUBKEY>
HEX_PUBKEY='' # <YOUR_NOSTR_HEX_PUBKEY>
HEX_PRIV_KEY='' #<YOUR_NOSTR_HEX_PRIV_KEY>
DOMAIN="<YOUR_DOMAIN_HERE>" # For example nostpy.lol
CONTACT=<YOUR_EMAIL_ADDRESS> #Enter your email address for the certbot command to get emails about your TLS certificate when it is near expiration
NGINX_FILE_PATH=/etc/nginx/sites-available/default #Leave this untouched
3 changes: 1 addition & 2 deletions menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ def start_flask_server():
print_color("\nStarting Zap Server", "33")
subprocess.run(["python3", "-m", "venv", "zap_venv"], check=True)
activate_cmd = ". zap_venv/bin/activate && "
commands = ["tmux new-session -s zap_server -d python zap_server.py"]
#commands = ["python zap_server.py"]
commands = ["tmux new-session -s zap_server -d python ./src/zap_server.py"]
for cmd in commands:
subprocess.run(["bash", "-c", activate_cmd + cmd], check=True)

Expand Down
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ Flask==3.0.0
requests==2.31.0
PySocks==1.7.1
python-dotenv==0.19.2
ddtrace
ddtrace
secp256k1
websocket-client
Empty file added src/lnurl.py
Empty file.
100 changes: 100 additions & 0 deletions src/nostr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import hashlib
import json
import logging
import secp256k1
import time
from websocket import create_connection


class NostpyClient:
def __init__(self, relays, pubkey, privkey, nostr_event, response) -> None:
self.relays = relays
self.pubkey = pubkey
self.privkey = privkey
self.kind9734 = nostr_event
self.created_at = response["settle_date"]
self.zap_reciept_tags = [
["description", json.dumps(nostr_event)],
["bolt11", response["payment_request"]],
["preimage", response["r_preimage"]],
]

def sign_event_id(self, event_id: str, private_key_hex: str) -> str:
private_key = secp256k1.PrivateKey(bytes.fromhex(private_key_hex))
sig = private_key.schnorr_sign(
bytes.fromhex(event_id), bip340tag=None, raw=True
)
return sig.hex()

def calc_event_id(
self,
public_key: str,
created_at: int,
kind_number: int,
tags: list,
content: str,
) -> str:
data = [0, public_key, created_at, kind_number, tags, content]
data_str = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
return hashlib.sha256(data_str.encode("UTF-8")).hexdigest()

def parse_tags(self, logger):
try:
tag_list = [tag_pair for tag_pair in self.kind9734["tags"]]
tag_list.append(self.zap_reciept_tags)
return tag_list
except Exception as exc:
logger.error(f"Error parsing kind 9735 tags: {exc}")

def create_event(self, kind_number, logger):
kind_9735_tags = self.parse_tags(logger)
content = ""
event_id = self.calc_event_id(
self.pubkey, self.created_at, kind_number, kind_9735_tags, content
)
signature_hex = self.sign_event_id(event_id, self.privkey)
event_data = {
"id": event_id,
"pubkey": self.pubkey,
"kind": kind_number,
"created_at": self.created_at,
"tags": kind_9735_tags,
"content": content,
"sig": signature_hex,
}

return event_data

def verify_signature(self, event_id: str, pubkey: str, sig: str, logger) -> bool:
try:
pub_key = secp256k1.PublicKey(bytes.fromhex("02" + pubkey), True)
result = pub_key.schnorr_verify(
bytes.fromhex(event_id), bytes.fromhex(sig), None, raw=True
)
if result:
logger.info(f"Verification successful for event: {event_id}")
else:
logger.error(f"Verification failed for event: {event_id}")
return result
except (ValueError, TypeError) as e:
logger.error(f"Error verifying signature for event {event_id}: {e}")
return False

def send_event(self, ws_relay, logger):
try:
ws = create_connection(ws_relay)
logger.info("WebSocket connection created.")
event_data = self.create_event(9735, logger)
sig = event_data["sig"]
id = event_data["id"]
signature_valid = self.verify_signature(id, self.pubkey, sig, logger)
if signature_valid:
event_json = json.dumps(("EVENT", event_data))
ws.send(event_json)
logger.debug(f"Event sent: {event_json}")
else:
logger.error("Invalid signature, event not sent.")
ws.close()
logger.info("WebSocket connection closed.")
except Exception as exc:
logger.error(f"Error sending ws event: {exc}")
82 changes: 75 additions & 7 deletions zap_server.py → src/zap_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
import logging
import os
import socket
import time
import urllib.parse
import threading
import asyncio

import requests
import socks
from dotenv import load_dotenv
from ddtrace import tracer, patch_all
from flask import Flask, jsonify, request
from nostr import NostpyClient
from flask import Flask, jsonify, request, after_this_request


app = Flask(__name__)
Expand All @@ -23,6 +28,7 @@
LND_INVOICE_MACAROON_HEX = os.getenv("LND_INVOICE_MACAROON_HEX")
INTERNET_IDENTIFIER = os.getenv("INTERNET_IDENTIFIER")
HEX_PUBKEY = os.getenv("HEX_PUBKEY")
HEX_PRIV_KEY = os.getenv("HEX_PRIV_KEY")
DOMAIN = os.getenv("DOMAIN")
IDENTITY = INTERNET_IDENTIFIER.split("@")[0]

Expand All @@ -34,8 +40,6 @@
)
logger = logging.getLogger(__name__)

logger.debug(f"ONion os {LND_ONION_ADDRESS}, VPN is {VPN_HOST}")


def make_http_request(url, headers, data):
try:
Expand All @@ -48,7 +52,7 @@ def make_http_request(url, headers, data):
response.raise_for_status()
invoice_data = response.json()
logger.debug(f"Received invoice data: {invoice_data}")
return invoice_data["payment_request"]
return invoice_data["r_hash"], invoice_data["payment_request"]
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Error making HTTP request: {e}")

Expand All @@ -75,11 +79,59 @@ def get_invoice(amount, description):
raise RuntimeError(f"Error creating invoice: {e}")


def check_invoice_payment(payment_request, max_attempts=20, sleep_time=1):
"""
Check if the specified invoice has been paid.
Parameters:
- payment_request (str): The payment request of the invoice.
- max_attempts (int): The maximum number of attempts to check the payment.
- sleep_time (int): The time to sleep between each attempt.
Returns:
- bool: True if the invoice has been paid, False otherwise.
"""
try:
attempts = 0
while attempts < max_attempts:
# Encode payment request according to specified rules
encoded_payment_request = (
f"payment_hash={payment_request.replace('+', '-').replace('/', '_')}"
)
logger.debug(f"Attempt number {attempts}")
url = f"https://{VPN_HOST}:{LND_REST_PORT}/v2/invoices/lookup" # {encoded_payment_request}'
logger.debug(f"Sending request to {url}")

response = requests.get(
url,
headers={"Grpc-Metadata-macaroon": LND_INVOICE_MACAROON_HEX},
params=encoded_payment_request,
verify=False,
)
response.raise_for_status()
invoice_status = response.json()["settled"]
if invoice_status:
logger.info("Invoice has been paid successfully.")
return response.json()
else:
logger.info("Invoice not yet paid. Retrying...")
attempts += 1
time.sleep(sleep_time)

logger.warning("Maximum attempts reached. Invoice may not have been paid.")
return False

except requests.exceptions.RequestException as e:
logger.error(f"Error checking invoice payment: {e}")
raise


@app.route("/lnurl-pay", methods=["GET"])
def lnurl_pay():
try:
# Get parameters from the request or set default values
logger.debug(f"Payload is {request}")

amount_satoshis = int(
request.args.get("amount", 1000)
) # Default to 1000 millisatoshis (1 satoshi)
Expand All @@ -88,11 +140,27 @@ def lnurl_pay():
description = request.args.get("comment")
nostr_resp = request.args.get("nostr")
if nostr_resp:
description = json.loads(nostr_resp).get("content")
nostr_event = json.loads(nostr_resp)
description = nostr_event["content"]
tags = nostr_event["tags"]
relays_value = next((item[1:] for item in tags if item[0] == "relays"), [])

# Generate an invoice
payment_request = get_invoice(amount_millisatoshis, description)
logger.debug(f"Payment request is: {payment_request}")
r_hash, payment_request = get_invoice(amount_millisatoshis, description)
logger.debug(f"Payment request is: {payment_request} and r hash is {r_hash}")

def call_functions() -> None:
check = check_invoice_payment(payment_request=r_hash)
if check:
lnurl_obj = NostpyClient(
relays_value, HEX_PUBKEY, HEX_PRIV_KEY, nostr_event, check
)
for relay in lnurl_obj.relays:
lnurl_obj.send_event(relay, logger)
logger.debug(f"Event sent is {relay}")

thread = threading.Thread(target=call_functions)
thread.start()

return jsonify(
{
Expand Down

0 comments on commit 3bd3184

Please sign in to comment.