commit 4312c7682113d138483722e50eccdf4a314805a6
parent 79ccef94ea2c0ff6eb0d520122e42b170c0261e0
Author: Nick <nicholas.w.farrow@gmail.com>
Date: Thu, 8 Jul 2021 03:59:10 +1000
satsale v2 (#12) Refactored code into REST API, client side processing. Created API docs and removed old code. Speeed
* SatSale v2 - Refactored code into REST API, client side processing. Created API docs and removed old code. Speeed
* Fix werkzeug dep
* Fix js checkpayment json bug
* fix api responses for /checkpayment
Diffstat:
15 files changed, 424 insertions(+), 379 deletions(-)
diff --git a/README.md b/README.md
@@ -53,7 +53,7 @@ If running on a Raspberry Pi, you will want to [forward port 8000 in your router
You will want to run gunicorn with nohup so it continues serving in the background:
```
-nohup gunicorn --worker-class eventlet -w 1 -b 0.0.0.0:8000 satsale:app > log.txt 2>&1 &
+nohup gunicorn -w 1 -b 0.0.0.0:8000 satsale:app > log.txt 2>&1 &
tail -f log.txt
```
diff --git a/docs/config_lightning.py b/docs/config_lightning.py
@@ -4,7 +4,7 @@
# We will be hosting SatSale on the same machine as our node.
# SatSale & Bitcoin Lightning Node 1.1.1.1 8332, 10009 (bitcoind, lnd)
-# As we are running SatSale on our node,
+# As we are running SatSale on our node,
# it can directly talk to our node at localhost 127.0.0.1
host = "127.0.0.1"
rpcport = "8332"
@@ -27,7 +27,7 @@ tunnel_host = None
pollrate = 15
# Payment expires after xx seconds
-payment_timeout = 60*60
+payment_timeout = 60 * 60
# Required confirmations for a payment
required_confirmations = 2
@@ -39,7 +39,7 @@ connection_attempts = 3
redirect = "https://github.com/nickfarrow/satsale"
# Payment method has been switched to lnd
-#pay_method = "bitcoind"
+# pay_method = "bitcoind"
pay_method = "lnd"
# Specify lightning directory and port
diff --git a/docs/config_remote_node.py b/docs/config_remote_node.py
@@ -3,13 +3,13 @@
# and the second remote machine hosts SatSale (perhaps same server you host your website on)
# Bitcoin Lightning Node 1.1.1.1 8332, 10009, 22 (bitcoind, lnd, SSH)
-# SatSale 2.2.2.2
+# SatSale 2.2.2.2
# In this config we will tell SatSale to connect to our node,
# tunneling the required ports over SSH.
# SatSale can then talk to our node via localhost on 127.0.0.1
host = "127.0.0.1"
-rpcport = "8332" # port for bitcoind
+rpcport = "8332" # port for bitcoind
# If connections get kicked back, you may also need to set `rpcallowip=YOUR_SERVER_IP` in your `~/.bitcoin/bitcoin.conf`.
# From ~/.bitcoin/bitcoin.conf
@@ -30,7 +30,7 @@ tunnel_host = "pi@1.1.1.1"
pollrate = 15
# Payment expires after xx seconds
-payment_timeout = 60*60
+payment_timeout = 60 * 60
# Required confirmations for a payment
required_confirmations = 2
@@ -42,7 +42,7 @@ connection_attempts = 3
redirect = "https://github.com/nickfarrow/satsale"
# Payment method has been switched to lnd
-#pay_method = "bitcoind"
+# pay_method = "bitcoind"
pay_method = "lnd"
# Specify lightning directory and port
diff --git a/gateways/woo_satsale.php b/gateways/woo_satsale.php
@@ -135,7 +135,6 @@ function satsale_init_gateway_class() {
*/
$args = array(
'amount' => $order->get_total(),
- 'id' => $order->get_id(),
'w_url' => $this->callback_URL );
write_log($args);
@@ -172,7 +171,7 @@ function satsale_init_gateway_class() {
// once the payment has been confirmed by the python backend.
// By confirming it matches the order details (amount * id) we know that
// the order has not been tampered with after leaving the php payment gateway.
- $order_secret_seed = (int)($order->get_total() * 100.0 * $order->get_id());
+ $order_secret_seed = (int)($order->get_total() * 100.0);
$order_secret_seed_str = (string)$order_secret_seed;
$secret = hash_hmac('sha256', $order_secret_seed, $key);
diff --git a/gateways/woo_webhook.py b/gateways/woo_webhook.py
@@ -6,12 +6,12 @@ import time
import requests
-def hook(satsale_secret, payload, payment):
+def hook(satsale_secret, invoice):
key = codecs.decode(satsale_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(
+ secret_seed = str(int(100 * float(invoice["amount"]))).encode(
"utf-8"
)
print("Secret seed: {}".format(secret_seed))
@@ -22,7 +22,6 @@ def hook(satsale_secret, payload, payment):
paid_time = int(time.time())
params = {
"wc-api": "wc_satsale_gateway",
- "id": payload["id"],
"time": str(paid_time),
}
message = (str(paid_time) + "." + json.dumps(params, separators=(",", ":"))).encode(
@@ -38,6 +37,6 @@ def hook(satsale_secret, payload, payment):
}
# Send the webhook response, confirming the payment with woocommerce.
- response = requests.get(payload["w_url"], params=params, headers=headers)
+ response = requests.get(invoice["w_url"], params=params, headers=headers)
return response
diff --git a/invoice/payment_invoice.py b/invoice/payment_invoice.py
@@ -1,34 +0,0 @@
-import uuid
-import qrcode
-
-import config
-from .price_feed import get_btc_value
-
-
-class invoice:
- def __init__(self, dollar_value, currency, label, test=False):
- self.dollar_value = dollar_value
- self.currency = currency
- 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.time_left = config.payment_timeout
- self.confirmed_paid = 0
- self.unconfirmed_paid = 0
- self.paid = False
- self.txid = ""
- self.test = test
-
- def create_qr(self):
- if config.pay_method == "lnd":
- qr_str = "{}".format(self.address.upper())
- else:
- 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))
- return
diff --git a/invoice/price_feed.py b/invoice/price_feed.py
@@ -36,7 +36,7 @@ def get_btc_value(dollar_value, currency):
if price is not None:
try:
- float_value = dollar_value / float(price)
+ float_value = float(dollar_value) / float(price)
if not isinstance(float_value, float):
raise Exception("Dollar value should be a float.")
except Exception as e:
diff --git a/pay/bitcoind.py b/pay/bitcoind.py
@@ -6,9 +6,7 @@ import config
from invoice.price_feed import get_btc_value
-
-
-class btcd():
+class btcd:
def __init__(self):
from bitcoinrpc.authproxy import AuthServiceProxy
@@ -41,40 +39,20 @@ class btcd():
Check your RPC / port tunneling settings and try again."
)
- def invoice(self, dollar_value, currency, label):
- self.dollar_value = dollar_value
- self.currency = currency
- 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.time_left = config.payment_timeout
- self.confirmed_paid = 0
- self.unconfirmed_paid = 0
- self.paid = False
- self.txid = ""
- self.get_address()
- self.create_qr()
- return
-
- def create_qr(self):
- qr_str = "{}?amount={}&label={}".format(
- self.address.upper(), self.value, self.label
- )
+ def create_qr(self, uuid, address, value):
+ qr_str = "{}?amount={}&label={}".format(address.upper(), value, uuid)
img = qrcode.make(qr_str)
- img.save("static/qr_codes/{}.png".format(self.uuid))
+ img.save("static/qr_codes/{}.png".format(uuid))
return
- def check_payment(self):
+ def check_payment(self, address):
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"] == 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"]
else:
@@ -82,11 +60,11 @@ class btcd():
return conf_paid, unconf_paid
- def get_address(self):
+ def get_address(self, amount, label):
for i in range(config.connection_attempts):
try:
- self.address = self.rpc.getnewaddress(self.label)
- return
+ address = self.rpc.getnewaddress(label)
+ return address, None
except Exception as e:
print(e)
print(
@@ -97,4 +75,4 @@ class btcd():
if config.connection_attempts - i == 1:
print("Reconnecting...")
self.__init__()
- return
+ return None
diff --git a/pay/lnd.py b/pay/lnd.py
@@ -12,7 +12,8 @@ import qrcode
from invoice.price_feed import get_btc_value
import config
-class lnd():
+
+class lnd:
def __init__(self):
from lndgrpc import LNDClient
@@ -33,11 +34,10 @@ class lnd():
time.sleep(3)
self.lnd = LNDClient(
"{}:{}".format(config.host, config.lnd_rpcport),
- macaroon_filepath=self.certs['macaroon'],
- cert_filepath=self.certs['tls'],
+ macaroon_filepath=self.certs["macaroon"],
+ cert_filepath=self.certs["tls"],
)
-
print("Getting lnd info...")
info = self.lnd.get_info()
print(info)
@@ -61,32 +61,15 @@ class lnd():
print("Ready for payments requests.")
return
- def invoice(self, dollar_value, currency, label):
- self.dollar_value = dollar_value
- self.currency = currency
- 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.time_left = config.payment_timeout
- self.confirmed_paid = 0
- self.unconfirmed_paid = 0
- self.paid = False
- self.txid = ""
- self.get_address()
- self.create_qr()
- return
-
- def create_qr(self):
- qr_str = "{}".format(self.address.upper())
+ def create_qr(self, uuid, address, value):
+ qr_str = "{}".format(address.upper())
img = qrcode.make(qr_str)
- img.save("static/qr_codes/{}.png".format(self.uuid))
+ img.save("static/qr_codes/{}.png".format(uuid))
return
# Copy tls and macaroon certs from remote machine.
def copy_certs(self):
- self.certs = {'tls' : 'tls.cert', 'macaroon' : 'admin.macaroon'}
+ self.certs = {"tls": "tls.cert", "macaroon": "admin.macaroon"}
if (not os.path.isfile("tls.cert")) or (not os.path.isfile("admin.macaroon")):
try:
@@ -115,8 +98,10 @@ class lnd():
)
else:
- self.certs = {'tls' : os.path.expanduser(tls_file),
- 'macaroon' : os.path.expanduser(macaroon_file)}
+ self.certs = {
+ "tls": os.path.expanduser(tls_file),
+ "macaroon": os.path.expanduser(macaroon_file),
+ }
except Exception as e:
print(e)
@@ -130,26 +115,21 @@ class lnd():
# Multiplying by 10^8 to convert to satoshi units
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"]
+ lnd_invoice = json.loads(MessageToJson(res))
print("Created lightning invoice:")
- print(self.lnd_invoice)
+ print(lnd_invoice)
- return self.lnd_invoice["payment_request"]
+ return lnd_invoice["payment_request"], lnd_invoice["r_hash"]
- def get_address(self):
- self.address = self.create_lnd_invoice(self.value)
- return
+ def get_address(self, amount, label):
+ address, r_hash = self.create_lnd_invoice(amount)
+ return address, r_hash
# Check whether the payment has been paid
- def check_payment(self):
- print("Looking up invoice")
-
+ def check_payment(self, rhash):
invoice_status = json.loads(
- MessageToJson(
- self.lnd.lookup_invoice(r_hash_str=b64decode(self.hash).hex())
- )
+ MessageToJson(self.lnd.lookup_invoice(r_hash_str=b64decode(rhash).hex()))
)
if "amt_paid_sat" not in invoice_status.keys():
diff --git a/requirements.txt b/requirements.txt
@@ -1,13 +1,14 @@
python_bitcoinrpc==1.0
qrcode==6.1
-Flask_SocketIO==5.0.0
Flask==1.1.2
MarkupSafe==1.1.1
requests==2.25.0
gunicorn==20.0.4
-eventlet==0.30.2
Pillow==8.01.1
protobuf==3.15.6
+flask_restplus==0.13.0
+Werkzeug==0.16.1
+
# For lightning (optional)
setuptools==50.3.2
diff --git a/satsale.py b/satsale.py
@@ -1,7 +1,19 @@
-from flask import Flask, render_template, request, redirect
-from flask_socketio import SocketIO, emit
+from flask import (
+ Flask,
+ render_template,
+ request,
+ redirect,
+ Blueprint,
+ make_response,
+ url_for,
+)
+from flask_restplus import Resource, Api, Namespace, fields
import time
import os
+import uuid
+import sqlite3
+from pprint import pprint
+import json
import ssh_tunnel
import config
@@ -9,12 +21,11 @@ import invoice
from pay import bitcoind
from pay import lnd
from gateways import woo_webhook
+from invoice.price_feed import get_btc_value
-# Begin websocket
-async_mode = None
app = Flask(__name__)
-# Load an API key or create a new one
+# Load a SatSale API key or create a new one
if os.path.exists("SatSale_API_key"):
with open("SatSale_API_key", "r") as f:
app.config["SECRET_KEY"] = f.read().strip()
@@ -25,187 +36,272 @@ else:
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")
-def test_message(message):
- emit(
- "payresponse",
- {
- "status": "Initialising payment...",
- "time_left": 0,
- "response": "Initialising payment...",
- },
- )
+# Create payment database if it does not exist
+if not os.path.exists("database.db"):
+ with sqlite3.connect("database.db") as conn:
+ print("Creating new database.db...")
+ conn.execute(
+ "CREATE TABLE payments (uuid TEXT, dollar_value DECIMAL, btc_value DECIMAL, method TEXT, address TEXT, time DECIMAL, webhook TEXT, rhash TEXT)"
+ )
# Render index page
# This is currently a donation page that submits to /pay
@app.route("/")
def index():
- # Render donation page
- return render_template("donate.html", async_mode=socket_.async_mode)
+ headers = {"Content-Type": "text/html"}
+ return make_response(render_template("donate.html"), 200, headers)
-# /pay is the main payment method for initiating a payment websocket.
+# /pay is the main page for initiating a payment, takes a GET request with ?amount=
@app.route("/pay")
-def payment_page():
+def pay():
params = dict(request.args)
- params['lnd_enabled'] = (config.pay_method == "lnd")
+ params["lnd_enabled"] = config.pay_method == "lnd"
+ params["redirect"] = config.redirect
# Render payment page with the request arguments (?amount= etc.)
- return render_template("index.html", params=params, async_mode=socket_.async_mode)
-
-
-# Websocket payment processing method called by client
-# initiate_payment recieves amount and initiates invoice and payment processing.
-@socket_.on("initiate_payment")
-def initiate_payment(payload):
- # Check the amount is a float
- amount = payload["amount"]
- try:
- amount = float(amount)
- except Exception as e:
- 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"]
- else:
- label = "donation"
+ headers = {"Content-Type": "text/html"}
+ return make_response(render_template("index.html", params=params), 200, headers)
+
+
+# Now we build the API docs
+# (if you do this before the above @app.routes then / gets overwritten.)
+api = Api(
+ app,
+ version="0.1",
+ title="SatSale API",
+ default="SatSale /api/",
+ description="API for creating Bitcoin invoices and processing payments.",
+ doc="/docs/",
+ order=True,
+)
+
+# Model templates for API responses
+invoice_model = api.model(
+ "invoice",
+ {
+ "uuid": fields.String(),
+ "dollar_value": fields.Float(),
+ "btc_value": fields.Float(),
+ "method": fields.String(),
+ "address": fields.String(),
+ "time": fields.Float(),
+ "webhook": fields.String(),
+ "rhash": fields.String(),
+ "time_left": fields.Float(),
+ },
+)
+status_model = api.model(
+ "status",
+ {
+ "payment_complete": fields.Integer(),
+ "confirmed_paid": fields.Float(),
+ "unconfirmed_paid": fields.Float(),
+ "expired": fields.Integer(),
+ },
+)
+
+
+@api.doc(
+ params={
+ "amount": "An amount in USD.",
+ "method": "(Optional) Specify a payment method: `bitcoind` for onchain, `lnd` for lightning).",
+ "w_url": "(Optional) Specify a webhook url to call after successful payment. Currently only supports WooCommerce plugin.",
+ }
+)
+class create_payment(Resource):
+ @api.response(200, "Success", invoice_model)
+ @api.response(400, "Invalid payment method")
+ def get(self):
+ "Create Payment"
+ """Initiate a new payment with an `amount` in USD."""
+ dollar_amount = request.args.get("amount")
+ currency = "USD"
+ label = "" # request.args.get('label')
+ payment_method = request.args.get("method")
+ if payment_method is None:
+ payment_method = config.pay_method
+ webhook = request.args.get("w_url")
+ if webhook is None:
+ webhook = None
+
+ # Create the payment using one of the connected nodes as a base
+ # ready to recieve the invoice.
+ node = get_node(payment_method)
+ if node is None:
+ print("Invalid payment method {}".format(payment_method))
+ return {"message": "Invalid payment method."}, 400
+
+ invoice = {
+ "uuid": str(uuid.uuid4().hex),
+ "dollar_value": dollar_amount,
+ "btc_value": round(get_btc_value(dollar_amount, currency), 8),
+ "method": payment_method,
+ "time": time.time(),
+ "webhook": webhook,
+ }
+
+ invoice["address"], invoice["rhash"] = node.get_address(
+ invoice["btc_value"], invoice["uuid"]
+ )
+ node.create_qr(invoice["uuid"], invoice["address"], invoice["btc_value"])
+
+ with sqlite3.connect("database.db") as conn:
+ cur = conn.cursor()
+ cur.execute(
+ "INSERT INTO payments (uuid,dollar_value,btc_value,method,address,time,webhook,rhash) VALUES (?,?,?,?,?,?,?,?)",
+ (
+ invoice["uuid"],
+ invoice["dollar_value"],
+ invoice["btc_value"],
+ invoice["method"],
+ invoice["address"],
+ invoice["time"],
+ invoice["webhook"],
+ invoice["rhash"],
+ ),
+ )
- # Get payment method, use one specified in query string if provided
- if "method" in payload.keys():
- payment_method = payload['method']
- else:
- payment_method = config.pay_method
+ invoice["time_left"] = config.payment_timeout - (time.time() - invoice["time"])
+ print("Created invoice:")
+ pprint(invoice)
+ print()
- # Initialise the payment invoice
- payment = create_invoice(amount, "USD", label, payment_method)
+ return {"invoice": invoice}, 200
+
+
+@api.doc(params={"uuid": "A payment uuid. Received from /createpayment."})
+class check_payment(Resource):
+ @api.response(200, "Success", status_model)
+ @api.response(201, "Unconfirmed", status_model)
+ @api.response(202, "Payment Expired", status_model)
+ def get(self):
+ "Check Payment"
+ """Check the status of a payment."""
+ uuid = request.args.get("uuid")
+ status = check_payment_status(uuid)
+
+ response = {
+ "payment_complete": 0,
+ "confirmed_paid": 0,
+ "unconfirmed_paid": 0,
+ "expired": 0,
+ }
+
+ if status["time_left"] <= 0:
+ response.update({"expired": 1})
+ code = 202
+ else:
+ # Don't send paid amounts if payment is expired.
+ response.update(status)
- # Wait for the amount to be sent to the address
- process_payment(payment)
+ if status['payment_complete'] == 1:
+ code = 200
+ else:
+ code = 201
+
+ return {'status': response}, code
+
+
+@api.doc(params={"uuid": "A payment uuid. Received from /createpayment."})
+class complete_payment(Resource):
+ @api.response(200, "Payment confirmed.")
+ @api.response(400, "Payment expired.")
+ @api.response(500, "Webhook failure.")
+ def get(self):
+ "Complete Payment"
+ """Run post-payment processing such as any webhooks."""
+ uuid = request.args.get("uuid")
+
+ invoice = load_invoice_from_db(uuid)
+ status = check_payment_status(uuid)
- # On successful payment
- if payment.paid:
- update_status(payment, "Payment finalised. Thankyou!")
+ if status["time_left"] < 0:
+ return {"expired"}, 400
- # 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 (invoice["webhook"] != None) and (invoice["webhook"] != ""):
# Call webhook
- response = woo_webhook.hook(app.config["SECRET_KEY"], payload, payment)
+ response = woo_webhook.hook(app.config["SECRET_KEY"], invoice)
if response.status_code != 200:
- print(
- "Failed to confirm order payment via webhook {}, please contact the store to ensure the order has been confirmed, error response is: {}".format(
- response.status_code, response.text
- )
+ err = "Failed to confirm order payment via webhook {}, please contact the store to ensure the order has been confirmed, error 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.")
-
- # Redirect after payment
- # TODO: add a delay here. Test.
- if config.redirect is not None:
- print("Redirecting to {}".format(config.redirect))
- return redirect(config.redirect)
- else:
- print("No redirect, closing.")
-
- return
-
-
-# Return feedback via the websocket, updating the payment status and time remaining.
-def update_status(payment, status, console_status=True):
- payment.status = status
- payment.response = status
- # Log to python stdout also
- if console_status:
- 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,
- },
- )
- return
-
-
-# Initialise the payment via the payment method (bitcoind / lightningc / etc),
-def create_invoice(dollar_amount, currency, label, payment_method=config.pay_method):
- if payment_method == "bitcoind":
- payment = bitcoin_node
- elif payment_method == "lnd":
- payment = lightning_node
- else:
- print("Invalid payment method")
- return
+ print(err)
+ return {"message": err}, 500
- # Load invoice information
- payment.invoice(dollar_amount, currency, label)
+ print("Successfully confirmed payment via webhook.")
+ return {"message": "Payment confirmed with store."}, 200
- return payment
+ return {"message": "Payment confirmed."}, 200
-# Payment processing function.
-# Handle payment logic.
-def process_payment(payment):
- update_status(payment, "Payment intialised, awaiting payment.")
+def check_payment_status(uuid):
+ status = {}
+ invoice = load_invoice_from_db(uuid)
+ if invoice is None:
+ status.update({"time_left": 0, "not_found": 1})
+ else:
+ status["time_left"] = config.payment_timeout - (time.time() - invoice["time"])
- # Track start_time so we can detect payment timeouts
- payment.start_time = time.time()
- while (config.payment_timeout - (time.time() - payment.start_time)) > 0:
- # Not using := for compatability reasons..
- payment.time_left = config.payment_timeout - (time.time() - payment.start_time)
- # Check progress of the payment
- payment.confirmed_paid, payment.unconfirmed_paid = payment.check_payment()
- print()
- print(payment.__dict__)
+ if status["time_left"] > 0:
+ node = get_node(invoice["method"])
+ if invoice["method"] == "lnd":
+ conf_paid, unconf_paid = node.check_payment(invoice["rhash"])
+ else:
+ conf_paid, unconf_paid = node.check_payment(invoice["address"])
# Debugging and demo mode which auto confirms payments after 5 seconds
- dbg_free_mode_cond = config.free_mode and (time.time() - payment.start_time > 5)
+ dbg_free_mode_cond = config.free_mode and (time.time() - invoice["time"] > 5)
# If payment is paid
- 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)
+ if (conf_paid > invoice["btc_value"]) or dbg_free_mode_cond:
+ status.update(
+ {
+ "payment_complete": 1,
+ "confirmed_paid": conf_paid,
+ "unconfirmed_paid": unconf_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,
+ else:
+ status.update(
+ {
+ "payment_complete": 0,
+ "confirmed_paid": conf_paid,
+ "unconfirmed_paid": unconf_paid,
+ }
)
- socket_.sleep(config.pollrate)
- # Continue waiting for transaction...
- else:
- update_status(payment, "Waiting for payment...")
- socket_.sleep(config.pollrate)
+ print("Invoice {} status: {}".format(uuid, status))
+ return status
+
+
+def get_node(payment_method):
+ if payment_method == "bitcoind":
+ node = bitcoin_node
+ elif payment_method == "lnd":
+ node = lightning_node
else:
- update_status(payment, "Payment expired.")
- return
+ node = None
+ return node
+
+
+def load_invoice_from_db(uuid):
+ with sqlite3.connect("database.db") as conn:
+ conn.row_factory = sqlite3.Row
+ cur = conn.cursor()
+ rows = cur.execute(
+ "select * from payments where uuid='{}'".format(uuid)
+ ).fetchall()
+ if len(rows) > 0:
+ return [dict(ix) for ix in rows][0]
+ else:
+ return None
+
+
+# Add API endpoints
+api.add_resource(create_payment, "/api/createpayment")
+api.add_resource(check_payment, "/api/checkpayment")
+api.add_resource(complete_payment, "/api/completepayment")
# Test connections on startup:
@@ -218,4 +314,4 @@ if config.pay_method == "lnd":
if __name__ == "__main__":
- socket_.run(app, debug=False)
+ app.run(debug=False)
diff --git a/ssh_tunnel.py b/ssh_tunnel.py
@@ -1,9 +1,11 @@
import subprocess
+import time
import config
import invoice
from pay import bitcoind
+
def open_tunnel(host, port):
# If tunnel is required (might make things easier)
try:
@@ -28,19 +30,22 @@ def open_tunnel(host, port):
pass
return
+
def close_tunnel():
if tunnel_proc is not None:
tunnel_proc.kill()
print("Tunnel closed.")
return
+
# Open tunnel
if config.tunnel_host is not None:
tunnel_proc = open_tunnel(config.tunnel_host, config.rpcport)
# Also for lnd if enabled
- if 'lnd_rpcport' in config.__dict__.keys():
+ if "lnd_rpcport" in config.__dict__.keys():
open_tunnel(config.tunnel_host, config.lnd_rpcport)
+ time.sleep(1)
else:
tunnel_proc = None
diff --git a/static/satsale.js b/static/satsale.js
@@ -0,0 +1,102 @@
+// Payment logic, talks to satsale.py
+function payment(payment_data) {
+ $('document').ready(function(){
+ var payment_uuid;
+ $.get("/api/createpayment", {amount: payment_data.amount, method: payment_data.method}).then(function(data) {
+ invoice = data.invoice;
+ payment_uuid = invoice.uuid;
+
+ $('#address').text(invoice.address).html();
+ $('#amount').text(invoice.btc_value).html();
+ $('#amount_sats').text(Math.round(invoice.btc_value * 10**8)).html();
+ $('#timer').text(Math.round(invoice.time_left)).html();
+
+ return payment_uuid
+ }).then(function(payment_uuid) {
+ load_qr(payment_uuid);
+ document.getElementById('timerContainer').style.visibility = "visible";
+
+ // Pass payment uuid and the interval process to check_payment
+ var checkinterval = setInterval(function() {check_payment(payment_uuid, checkinterval, payment_data);}, 1000);
+ })
+ });
+}
+
+function check_payment(payment_uuid, checkinterval, payment_data) {
+ $.get("/api/checkpayment", {uuid: payment_uuid}).then(function(payment_data) {
+ payment_status = payment_data.status;
+ console.log(payment_status);
+ if (payment_status.expired == 1) {
+ $('#status').text("Payment expired.").html();
+ document.getElementById('timerContainer').style.visibility = "hidden";
+ clearInterval(checkinterval);
+ return 1;
+ }
+
+ if (payment_status.payment_complete == 1) {
+ $('#status').text("Payment confirmed.").html();
+ document.getElementById('timerContainer').style.visibility = "hidden";
+ clearInterval(checkinterval);
+ complete_payment(payment_uuid, payment_data);
+ return 1;
+ }
+ else {
+ if (payment_status.unconfirmed_paid > 0) {
+ $('#status').text("Discovered payment. Waiting for more confirmations...").html();
+ return 0;
+ }
+ else {
+ $('#status').text("Waiting for payment...").html();
+ return 0;
+ }
+ }
+ });
+}
+
+function complete_payment(payment_uuid, payment_data) {
+ setTimeout(() => { window.location.replace(payment_data.redirect); }, 5000);
+ $.get("/api/completepayment", {uuid: payment_uuid}).then(function(payment_completion) {
+ console.log(payment_completion);
+ $('#status').text(payment_completion.message).html();
+ });
+}
+
+function load_qr(payment_uuid) {
+ // Display QR code
+ if (payment_uuid != null) {
+ // Change image id to qr id
+ document.getElementById('qrImage').className = "qr";
+ // Insert image and link
+ document.getElementById('qrClick').href = "/static/qr_codes/" + payment_uuid + ".png";
+ document.getElementById('qrImage').src = "/static/qr_codes/" + payment_uuid + ".png";
+ }
+}
+
+function replaceUrlParam(url, paramName, paramValue)
+{
+ console.log(url);
+ var href = new URL(url);
+ href.searchParams.set(paramName, paramValue);
+ window.location = href;
+ return
+}
+
+// Payment timer, can't go below zero, update every second
+intervalTimer = setInterval(function () {
+ var currentTime = document.getElementById('timer').innerHTML;
+ if (currentTime <= 0) {
+ currentTime = 1;
+ }
+ document.getElementById('timer').innerHTML = Math.round(currentTime - 1);
+}, 1000)
+
+// Copy text functions
+function copyText(text) {
+ navigator.clipboard.writeText(text);
+}
+function copyTextFromElement(elementID) {
+ let element = document.getElementById(elementID); //select the element
+ let elementText = element.textContent; //get the text content from the element
+ copyText(elementText); //use the copyText function below
+ alert("Copied address:" + elementText)
+}
diff --git a/static/websocket.js b/static/websocket.js
@@ -1,96 +0,0 @@
-// Websocket logic, talks to satsale.py /pay
-
-// Initiate is called in the <head> of index.html with the payload provided
-// by the flask request. The data can not be passed straight from flask to this js
-// Hence we {{ load }} it in the index head and call this function.
-function open_websocket(payment_data) {
- namespace = '/';
- var socket = io(namespace);
-
- // Echo initated message for debugging.
- socket.on('connect', function() {
- socket.emit('initialise', {'data': 'Initialising payment.'});
- });
-
- // Recieving payment status from flask
- socket.on('payresponse', function(msg) {
- console.log(msg.response);
- // Display payment status
- $('#status').text(msg.status).html();
- // Display payment address
- $('#address').text(msg.address).html();
- // Display payment amount
- $('#amount').text(msg.amount).html();
- $('#amount_sats').text(Math.round(msg.amount * 10**8)).html();
- // Display payment time left
- $('#timer').text(Math.round(msg.time_left)).html();
-
- // Run additional logic that manipulates element visibility depending
- // on the contents and status of the payment.
- conditionalPageLogic(msg)
-
- console.log(msg);
-
- // Actions if paid
- if (msg.paid == true) {
-
- // Redirect if paid
- if (msg.redirect != null) {
- setTimeout(() => { window.location.replace(msg.redirect); }, 5000);
- }
- }
- });
-
- // Initiate the payment websocket with the server
- socket.emit('initiate_payment', payment_data);
- return false
-}
-
-// Run additional logic that manipulates element visibility depending
-// on the contents and status of the payment when giving a response to the webpage.
-function conditionalPageLogic(msg) {
- // Display QR code
- if (msg.address != null) {
- // Change image id to qr id
- document.getElementById('qrImage').className = "qr";
- // Insert image and link
- document.getElementById('qrClick').href = "/static/qr_codes/" + msg.uuid + ".png";
- document.getElementById('qrImage').src = "/static/qr_codes/" + msg.uuid + ".png";
- }
- // Hide timer until ready.
- if (msg.time_left == 0) {
- document.getElementById('timerContainer').style.visibility = "hidden";
- }
- else {
- document.getElementById('timerContainer').style.visibility = "visible";
- }
-}
-
-function replaceUrlParam(url, paramName, paramValue)
-{
- console.log(url);
- var href = new URL(url);
- href.searchParams.set(paramName, paramValue);
- window.location = href;
- return
-}
-
-// Payment timer, can't go below zero, update every second
-intervalTimer = setInterval(function () {
- var currentTime = document.getElementById('timer').innerHTML;
- if (currentTime <= 0) {
- currentTime = 1;
- }
- document.getElementById('timer').innerHTML = Math.round(currentTime - 1);
-}, 1000)
-
-// Copy text functions
-function copyText(text) {
- navigator.clipboard.writeText(text);
-}
-function copyTextFromElement(elementID) {
- let element = document.getElementById(elementID); //select the element
- let elementText = element.textContent; //get the text content from the element
- copyText(elementText); //use the copyText function below
- alert("Copied address:" + elementText)
-}
diff --git a/templates/index.html b/templates/index.html
@@ -4,10 +4,10 @@
<title>SatSale</title>
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
-
+
<script src="//code.jquery.com/jquery-1.12.4.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/socket.io/3.0.4/socket.io.js"></script>
- <script src="{{ url_for('static', filename='websocket.js') }}"></script>
+ <script src="{{ url_for('static', filename='satsale.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
@@ -15,7 +15,22 @@
<script type="text/javascript">
payment_data = {{ params|tojson }};
console.log(payment_data);
- open_websocket(payment_data);
+ payment(payment_data);
+
+ // Create invoice
+ // returns qr code etc
+ // While true
+ // check payment -> returns status and time left
+ // show if paid
+ // * webhook after if in params
+
+ // SatSale.py now requires a few functions,
+ // * create the invoice (bitcoind or lnd)
+ // * lookup txn
+ // * process webhook
+
+
+
</script>
</head>
@@ -40,7 +55,7 @@
<p style="padding:0;">     (<b><span id="amount"></span></b> BTC)</p>
<p style="padding:0;">To: </p><b><p id="address" onclick="copyTextFromElement('address')"></p></b>
<p style="padding:0;"><span id="status"></span></p>
- <p id="timerContainer" style="padding:0;"><span id="timer"></span> seconds remaining.</p>
+ <p id="timerContainer" style="padding:0;visibility:hidden;"><span id="timer"></span> seconds remaining.</p>
</div>
</br>