SatSale

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 2c32f39d0b75535bbc8016e2c2238fc2b7e64dbe
parent 12dbe6f54d63b48d83b39631c1d4aeed6241adc4
Author: NicholasFarrow <nicholas.w.farrow@gmail.com>
Date:   Wed, 27 Jan 2021 23:03:32 +1100

Ran black for code formatting

Diffstat:
Mgateways/woo_webhook.py | 30++++++++++++++++++++++--------
Minvoice/payment_invoice.py | 14++++++++------
Minvoice/price_feed.py | 13+++++++------
Mpay/bitcoind.py | 30+++++++++++++++++++++---------
Mpay/lnd.py | 63++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mserver.py | 110+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mssh_tunnel.py | 11+++++++++--
7 files changed, 181 insertions(+), 90 deletions(-)

diff --git a/gateways/woo_webhook.py b/gateways/woo_webhook.py @@ -5,25 +5,39 @@ import codecs import time import requests + def hook(btcpyment_secret, payload, payment): - key = codecs.decode(btcpyment_secret, 'hex') + key = codecs.decode(btcpyment_secret, "hex") # Calculate a secret that is required to send back to the # woocommerce gateway, proving we did not modify id nor amount. - secret_seed = str(int(100*float(payload['amount'])) * int(payload['id'])).encode('utf-8') + secret_seed = str(int(100 * float(payload["amount"])) * int(payload["id"])).encode( + "utf-8" + ) print("Secret seed: {}".format(secret_seed)) - + secret = hmac.new(key, secret_seed, hashlib.sha256).hexdigest() # The main signature which proves we have paid, and very recently! paid_time = int(time.time()) - params = {"wc-api":"wc_btcpyment_gateway", 'id' : payload['id'], 'time' : str(paid_time)} - message = (str(paid_time) + '.' + json.dumps(params, separators=(',', ':'))).encode('utf-8') + params = { + "wc-api": "wc_btcpyment_gateway", + "id": payload["id"], + "time": str(paid_time), + } + message = (str(paid_time) + "." + json.dumps(params, separators=(",", ":"))).encode( + "utf-8" + ) + # Calculate the hash hash = hmac.new(key, message, hashlib.sha256).hexdigest() - headers={'Content-Type': 'application/json', 'X-Signature' : hash, 'X-Secret': secret} + headers = { + "Content-Type": "application/json", + "X-Signature": hash, + "X-Secret": secret, + } - response = requests.get( - payload['w_url'], params=params, headers=headers) + # Send the webhook response, confirming the payment with woocommerce. + response = requests.get(payload["w_url"], params=params, headers=headers) return response diff --git a/invoice/payment_invoice.py b/invoice/payment_invoice.py @@ -4,6 +4,7 @@ import qrcode import config from .price_feed import get_btc_value + class invoice: def __init__(self, dollar_value, currency, label): self.dollar_value = dollar_value @@ -11,21 +12,22 @@ class invoice: self.value = round(get_btc_value(dollar_value, currency), 8) self.uuid = str(uuid.uuid4()) self.label = self.uuid - self.status = 'Payment initialised.' - self.response = '' + self.status = "Payment initialised." + self.response = "" self.time_left = config.payment_timeout self.confirmed_paid = 0 self.unconfirmed_paid = 0 self.paid = False self.txid = "" - def create_qr(self): - if config.pay_method == 'lnd': + if config.pay_method == "lnd": qr_str = "{}".format(self.address.upper()) else: - qr_str = "{}?amount={}&label={}".format(self.address.upper(), self.value, self.label) + qr_str = "{}?amount={}&label={}".format( + self.address.upper(), self.value, self.label + ) img = qrcode.make(qr_str) - img.save('static/qr_codes/{}.png'.format(self.uuid)) + img.save("static/qr_codes/{}.png".format(self.uuid)) return diff --git a/invoice/price_feed.py b/invoice/price_feed.py @@ -2,6 +2,7 @@ import requests import config + def get_price(currency): price_feed = "https://api.coindesk.com/v1/bpi/currentprice.json" r = requests.get(price_feed) @@ -9,19 +10,20 @@ def get_price(currency): for i in range(config.connection_attempts): try: price_data = r.json() - prices = price_data['bpi'] + prices = price_data["bpi"] break except Exception as e: print(e) - print("Attempting again... {}/{}...".format(i+1, config.connection_attempts)) + print( + "Attempting again... {}/{}...".format(i + 1, config.connection_attempts) + ) else: - raise("Failed to reach {}.".format(price_feed)) - + raise ("Failed to reach {}.".format(price_feed)) try: - price = prices[currency]['rate'].replace(',', '') + price = prices[currency]["rate"].replace(",", "") return price except: @@ -40,7 +42,6 @@ def get_btc_value(dollar_value, currency): except Exception as e: print(e) - return float_value raise Exception("Failed to get dollar value.") diff --git a/pay/bitcoind.py b/pay/bitcoind.py @@ -13,7 +13,9 @@ class btcd(invoice): from bitcoinrpc.authproxy import AuthServiceProxy - connection_str = "http://{}:{}@{}:{}".format(config.username, config.password, config.host, config.rpcport) + connection_str = "http://{}:{}@{}:{}".format( + config.username, config.password, config.host, config.rpcport + ) print("Attempting to connect to {}.".format(connection_str)) for i in range(config.connection_attempts): @@ -27,22 +29,28 @@ class btcd(invoice): except Exception as e: print(e) time.sleep(config.pollrate) - print("Attempting again... {}/{}...".format(i+1, config.connection_attempts)) + print( + "Attempting again... {}/{}...".format( + i + 1, config.connection_attempts + ) + ) else: - raise Exception("Could not connect to bitcoind. Check your RPC / port tunneling settings and try again.") + raise Exception( + "Could not connect to bitcoind. Check your RPC / port tunneling settings and try again." + ) def check_payment(self): transactions = self.rpc.listtransactions() - relevant_txs = [tx for tx in transactions if tx['address'] == self.address] + relevant_txs = [tx for tx in transactions if tx["address"] == self.address] conf_paid = 0 unconf_paid = 0 for tx in relevant_txs: - self.txid = tx['txid'] - if tx['confirmations'] >= config.required_confirmations: - conf_paid += tx['amount'] + self.txid = tx["txid"] + if tx["confirmations"] >= config.required_confirmations: + conf_paid += tx["amount"] else: - unconf_paid += tx['amount'] + unconf_paid += tx["amount"] return conf_paid, unconf_paid @@ -52,5 +60,9 @@ class btcd(invoice): self.address = self.rpc.getnewaddress(self.label) except Exception as e: print(e) - print("Attempting again... {}/{}...".format(i+1, config.connection_attempts)) + print( + "Attempting again... {}/{}...".format( + i + 1, config.connection_attempts + ) + ) return diff --git a/pay/lnd.py b/pay/lnd.py @@ -8,6 +8,7 @@ from google.protobuf.json_format import MessageToJson from invoice.payment_invoice import invoice + class lnd(invoice): def __init__(self, dollar_value, currency, label): super().__init__(dollar_value, currency, label) @@ -17,12 +18,25 @@ class lnd(invoice): # Copy admin macaroon and tls cert to local machine if (not os.path.isfile("tls.cert")) or (not os.path.isfile("admin.macaroon")): - print("Could not find tls.cert or admin.macaroon in BTCPyment folder. Attempting to download from lnd directory.") + print( + "Could not find tls.cert or admin.macaroon in BTCPyment folder. Attempting to download from lnd directory." + ) try: tls_file = os.path.join(config.lnd_dir, "tls.cert") - macaroon_file = os.path.join(config.lnd_dir, "data/chain/bitcoin/mainnet/admin.macaroon") - subprocess.run(["scp", "{}:{}".format(config.tunnel_host, tls_file), "."]) - subprocess.run(["scp", "-r", "{}:{}".format(config.tunnel_host, macaroon_file), "."]) + macaroon_file = os.path.join( + config.lnd_dir, "data/chain/bitcoin/mainnet/admin.macaroon" + ) + subprocess.run( + ["scp", "{}:{}".format(config.tunnel_host, tls_file), "."] + ) + subprocess.run( + [ + "scp", + "-r", + "{}:{}".format(config.tunnel_host, macaroon_file), + ".", + ] + ) except Exception as e: print(e) print("Failed to copy tls and macaroon files to local machine.") @@ -31,14 +45,20 @@ class lnd(invoice): # Conect to lightning node connection_str = "{}:{}".format(config.host, config.rpcport) - print("Attempting to connect to lightning node {}. This may take a few minutes...".format(connection_str)) + print( + "Attempting to connect to lightning node {}. This may take a few minutes...".format( + connection_str + ) + ) for i in range(config.connection_attempts): try: print("Attempting to initialise lnd rpc client...") - self.lnd = LNDClient("{}:{}".format(config.host, config.rpcport), - macaroon_filepath="admin.macaroon", - cert_filepath="tls.cert") + self.lnd = LNDClient( + "{}:{}".format(config.host, config.rpcport), + macaroon_filepath="admin.macaroon", + cert_filepath="tls.cert", + ) print("Getting lnd info...") info = self.lnd.get_info() @@ -50,25 +70,30 @@ class lnd(invoice): except Exception as e: print(e) time.sleep(config.pollrate) - print("Attempting again... {}/{}...".format(i+1, config.connection_attempts)) + print( + "Attempting again... {}/{}...".format( + i + 1, config.connection_attempts + ) + ) else: - raise Exception("Could not connect to lnd. Check your gRPC / port tunneling settings and try again.") + raise Exception( + "Could not connect to lnd. Check your gRPC / port tunneling settings and try again." + ) print("Ready for payments requests.") # Create lightning invoice def create_lnd_invoice(self, btc_amount): # Multiplying by 10^8 to convert to satoshi units - sats_amount = int(btc_amount*10**8) + sats_amount = int(btc_amount * 10 ** 8) res = self.lnd.add_invoice(value=sats_amount) self.lnd_invoice = json.loads(MessageToJson(res)) - self.hash = self.lnd_invoice['r_hash'] + self.hash = self.lnd_invoice["r_hash"] print("Created lightning invoice:") print(self.lnd_invoice) - return self.lnd_invoice['payment_request'] - + return self.lnd_invoice["payment_request"] def get_address(self): self.address = self.create_lnd_invoice(self.value) @@ -78,14 +103,18 @@ class lnd(invoice): def check_payment(self): print("Looking up invoice") - invoice_status = json.loads(MessageToJson(self.lnd.lookup_invoice(r_hash_str=b64decode(self.hash).hex()))) + invoice_status = json.loads( + MessageToJson( + self.lnd.lookup_invoice(r_hash_str=b64decode(self.hash).hex()) + ) + ) - if 'amt_paid_sat' not in invoice_status.keys(): + if "amt_paid_sat" not in invoice_status.keys(): conf_paid = 0 unconf_paid = 0 else: # Store amount paid and convert to BTC units - conf_paid = int(invoice_status['amt_paid_sat']) * 10**8 + conf_paid = int(invoice_status["amt_paid_sat"]) * 10 ** 8 unconf_paid = 0 return conf_paid, unconf_paid diff --git a/server.py b/server.py @@ -1,6 +1,5 @@ -from flask import Flask, render_template, session, request, redirect -from flask_socketio import SocketIO, emit, disconnect -from markupsafe import escape +from flask import Flask, render_template, request, redirect +from flask_socketio import SocketIO, emit import time import os @@ -17,56 +16,63 @@ app = Flask(__name__) # Load an API key or create a new one if os.path.exists("BTCPyment_API_key"): - with open("BTCPyment_API_key", 'r') as f: - app.config['SECRET_KEY'] = f.read().strip() + with open("BTCPyment_API_key", "r") as f: + app.config["SECRET_KEY"] = f.read().strip() else: - with open("BTCPyment_API_key", 'w') as f: - app.config['SECRET_KEY'] = os.urandom(64).hex() - f.write(app.config['SECRET_KEY']) + with open("BTCPyment_API_key", "w") as f: + app.config["SECRET_KEY"] = os.urandom(64).hex() + f.write(app.config["SECRET_KEY"]) -print("Initialised Flask with secret key: {}".format(app.config['SECRET_KEY'])) +print("Initialised Flask with secret key: {}".format(app.config["SECRET_KEY"])) # cors_allowed_origins * allows for webhooks to be initiated from iframes. socket_ = SocketIO(app, async_mode=async_mode, cors_allowed_origins="*") # Basic return on initialisation -@socket_.on('initialise') +@socket_.on("initialise") def test_message(message): - emit('payresponse', { - 'status' : 'Initialising payment...', - 'time_left' : 0, - 'response': 'Initialising payment...'}) + emit( + "payresponse", + { + "status": "Initialising payment...", + "time_left": 0, + "response": "Initialising payment...", + }, + ) + # Render index page # This is currently a donation page that submits to /pay -@app.route('/') +@app.route("/") def index(): # Render donation page - return render_template('donate.html', async_mode=socket_.async_mode) + return render_template("donate.html", async_mode=socket_.async_mode) + # /pay is the main payment method for initiating a payment websocket. -@app.route('/pay') +@app.route("/pay") def payment_page(): params = dict(request.args) # Render payment page with the request arguments (?amount= etc.) - return render_template('index.html', params=params, async_mode=socket_.async_mode) + return render_template("index.html", params=params, async_mode=socket_.async_mode) + # Websocket payment processing method called by client # make_payment recieves amount and initiates invoice and payment processing. -@socket_.on('make_payment') +@socket_.on("make_payment") def make_payment(payload): # Check the amount is a float - amount = payload['amount'] + amount = payload["amount"] try: amount = float(amount) except: - update_status(payment, 'Invalid amount.') + update_status(payment, "Invalid amount.") amount = None return # Label as a donation if the id is missing - if 'id' in payload.keys(): - label = payload['id'] + if "id" in payload.keys(): + label = payload["id"] else: label = "donation" @@ -78,20 +84,24 @@ def make_payment(payload): # On successful payment if payment.paid: - update_status(payment, 'Payment finalised. Thankyou!') + update_status(payment, "Payment finalised. Thankyou!") # If a w_url for woocommerce webhook has been provided, then we need # to take some additional steps to confirm the order. - if 'w_url' in payload.keys(): + if "w_url" in payload.keys(): # Call webhook - response = woo_webhook.hook(app.config['SECRET_KEY'], payload, payment) + response = woo_webhook.hook(app.config["SECRET_KEY"], payload, payment) if response.status_code != 200: - print('Failed to confirm order payment via webhook {}, the response is: {}'.format(response.status_code, response.text)) + print( + "Failed to confirm order payment via webhook {}, the response is: {}".format( + response.status_code, response.text + ) + ) update_status(payment, response.text) else: print("Successfully confirmed payment via webhook.") - update_status(payment, 'Order confirmed.') + update_status(payment, "Order confirmed.") # Redirect after payment # TODO: add a delay here. Test. @@ -103,6 +113,7 @@ def make_payment(payload): return + # Return feedback via the websocket, updating the payment status and time remaining. def update_status(payment, status, console_status=True): payment.status = status @@ -112,15 +123,20 @@ def update_status(payment, status, console_status=True): print(payment.status) # Send status & response to client - emit('payresponse', { - 'status' : payment.status, - 'address' : payment.address, - 'amount' : payment.value, - 'time_left' : payment.time_left, - 'uuid' : payment.uuid, - 'response': payment.response}) + emit( + "payresponse", + { + "status": payment.status, + "address": payment.address, + "amount": payment.value, + "time_left": payment.time_left, + "uuid": payment.uuid, + "response": payment.response, + }, + ) return + # Initialise the payment via the payment method (bitcoind / lightningc / etc), def create_invoice(dollar_amount, currency, label): if config.pay_method == "bitcoind": @@ -136,10 +152,11 @@ def create_invoice(dollar_amount, currency, label): payment.create_qr() return payment + # Payment processing function. # Handle payment logic. def process_payment(payment): - update_status(payment, 'Payment intialised, awaiting payment.') + update_status(payment, "Payment intialised, awaiting payment.") # Track start_time so we can detect payment timeouts payment.start_time = time.time() @@ -157,13 +174,21 @@ def process_payment(payment): if (payment.confirmed_paid > payment.value) or dbg_free_mode_cond: payment.paid = True payment.time_left = 0 - update_status(payment, "Payment successful! {} BTC".format(payment.confirmed_paid)) + update_status( + payment, "Payment successful! {} BTC".format(payment.confirmed_paid) + ) break # Display unconfirmed transaction info elif payment.unconfirmed_paid > 0: - update_status(payment, "Discovered payment. \ - Waiting for {} confirmations...".format(config.required_confirmations), console_status=False) + update_status( + payment, + "Discovered payment. \ + Waiting for {} confirmations...".format( + config.required_confirmations + ), + console_status=False, + ) socket_.sleep(config.pollrate) # Continue waiting for transaction... @@ -174,14 +199,15 @@ def process_payment(payment): update_status(payment, "Payment expired.") return + # Test Bitcoind connection on startup: print("Checking node connectivity...") if config.pay_method == "bitcoind": - bitcoind.btcd(1, 'USD', 'Init test.') + bitcoind.btcd(1, "USD", "Init test.") elif config.pay_method == "lnd": - lnd.lnd(1, 'USD', 'Init test') + lnd.lnd(1, "USD", "Init test") print("Connection successful.") -if __name__ == '__main__': +if __name__ == "__main__": socket_.run(app, debug=False) diff --git a/ssh_tunnel.py b/ssh_tunnel.py @@ -9,8 +9,15 @@ import subprocess # If tunnel is required (might make things easier) try: if config.tunnel_host is not None: - command = ['ssh', config.tunnel_host, '-q', '-N', '-L', '{}:localhost:{}'.format(config.rpcport, config.rpcport)] - print("Opening tunnel to {}.".format(' '.join(command))) + command = [ + "ssh", + config.tunnel_host, + "-q", + "-N", + "-L", + "{}:localhost:{}".format(config.rpcport, config.rpcport), + ] + print("Opening tunnel to {}.".format(" ".join(command))) tunnel_proc = subprocess.Popen(command) else: tunnel_proc = None