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