satsale.py (14104B)
1 from flask import ( 2 Flask, 3 render_template, 4 request, 5 make_response 6 ) 7 from flask_restx import Resource, Api, fields 8 import time 9 import os 10 import uuid 11 from pprint import pprint 12 import qrcode 13 import logging 14 15 import config 16 17 # Initialise logging before importing other modules 18 logging.basicConfig( 19 format="[%(asctime)s] [%(levelname)s] %(message)s", 20 datefmt="%Y-%m-%d %H:%M:%S %z", 21 level=getattr(logging, config.loglevel), 22 ) 23 24 from gateways import paynym 25 from payments import database, weakhands 26 from payments.price_feed import get_btc_value 27 from node import bitcoind 28 from node import xpub 29 from node import lnd 30 from node import clightning 31 from utils import btc_amount_format 32 33 from gateways import woo_webhook 34 35 app = Flask(__name__) 36 37 # Load a SatSale API key or create a new one 38 if os.path.exists("SatSale_API_key"): 39 with open("SatSale_API_key", "r") as f: 40 app.config["SECRET_KEY"] = f.read().strip() 41 else: 42 with open("SatSale_API_key", "w") as f: 43 app.config["SECRET_KEY"] = os.urandom(64).hex() 44 f.write(app.config["SECRET_KEY"]) 45 46 logging.info("Initialised Flask with secret key: {}".format(app.config["SECRET_KEY"])) 47 48 # Create payment database if it does not exist 49 if not os.path.exists("database.db"): 50 database.create_database() 51 # Check and migrate database to current version if needed 52 database.migrate_database() 53 54 55 # Render index page 56 # This is currently a donation page that submits to /pay 57 @app.route("/") 58 def index(): 59 params = dict(request.args) 60 params["supported_currencies"] = config.supported_currencies 61 params["base_currency"] = config.base_currency 62 params["node_info"] = config.node_info 63 headers = {"Content-Type": "text/html"} 64 return make_response(render_template("donate.html", params=params), 200, headers) 65 66 67 # /pay is the main page for initiating a payment, takes a GET request with ?amount=[x]¤cy=[x] 68 @app.route("/pay") 69 def pay(): 70 params = dict(request.args) 71 params["payment_methods"] = enabled_payment_methods 72 params["redirect"] = config.redirect 73 params["node_info"] = config.node_info 74 # Render payment page with the request arguments (?amount= etc.) 75 headers = {"Content-Type": "text/html"} 76 return make_response(render_template("index.html", params=params), 200, headers) 77 78 79 # Now we build the API docs 80 # (if you do this before the above @app.routes then / gets overwritten.) 81 api = Api( 82 app, 83 version="0.1", 84 title="SatSale API", 85 default="SatSale /api/", 86 description="API for creating Bitcoin invoices and processing payments.", 87 doc="/docs/", 88 order=True, 89 ) 90 91 # Model templates for API responses 92 invoice_model = api.model( 93 "invoice", 94 { 95 "uuid": fields.String(), 96 "base_currency": fields.String(), 97 "base_value": fields.Float(), 98 "btc_value": fields.Float(), 99 "method": fields.String(), 100 "address": fields.String(), 101 "time": fields.Float(), 102 "webhook": fields.String(), 103 "rhash": fields.String(), 104 "time_left": fields.Float(), 105 }, 106 ) 107 status_model = api.model( 108 "status", 109 { 110 "payment_complete": fields.Integer(), 111 "confirmed_paid": fields.Float(), 112 "unconfirmed_paid": fields.Float(), 113 "expired": fields.Integer(), 114 }, 115 ) 116 117 118 @api.doc( 119 params={ 120 "amount": "An amount.", 121 "currency": "(Opional) Currency units of the amount (defaults to `config.base_currency`).", 122 "method": "(Optional) Specify a payment method: `bitcoind` for onchain, `lnd` for lightning).", 123 "w_url": "(Optional) Specify a webhook url to call after successful payment. Currently only supports WooCommerce plugin.", 124 } 125 ) 126 class create_payment(Resource): 127 @api.response(200, "Success", invoice_model) 128 @api.response(400, "Invalid payment method") 129 @api.response(406, "Amount below dust limit") 130 @api.response(522, "Error fetching address from node") 131 def get(self): 132 "Create Payment" 133 """Initiate a new payment with an `amount` in specified currency.""" 134 base_amount = request.args.get("amount") 135 currency = request.args.get("currency") 136 if currency is None: 137 currency = config.base_currency 138 payment_method = request.args.get("method") 139 if payment_method is None: 140 payment_method = enabled_payment_methods[0] 141 webhook = request.args.get("w_url") 142 if webhook is None: 143 webhook = None 144 else: 145 logging.info("Webhook payment: {}".format(webhook)) 146 147 # Create the payment using one of the connected nodes as a base 148 # ready to recieve the invoice. 149 node = get_node(payment_method) 150 if node is None: 151 logging.warning("Invalid payment method {}".format(payment_method)) 152 return {"message": "Invalid payment method."}, 400 153 154 btc_value = get_btc_value(base_amount, currency) 155 if node.is_onchain and btc_value < config.onchain_dust_limit: 156 logging.warning( 157 "Requested onchain payment for {} {} below dust limit ({} < {})".format( 158 base_amount, 159 currency, 160 btc_amount_format(btc_value), 161 btc_amount_format(config.onchain_dust_limit), 162 ) 163 ) 164 return {"message": "Amount below dust limit."}, 406 165 166 if config.store_name: 167 invoice_uuid = "{}-{}".format(config.store_name, str(uuid.uuid4().hex)) 168 else: 169 invoice_uuid = str(uuid.uuid4().hex) 170 invoice = { 171 "uuid": invoice_uuid, 172 "base_currency": currency, 173 "base_value": base_amount, 174 "btc_value": btc_amount_format(btc_value), 175 "method": payment_method, 176 "time": time.time(), 177 "webhook": webhook, 178 "onchain_dust_limit": config.onchain_dust_limit, 179 } 180 181 # Get an address / invoice, and create a QR code 182 try: 183 invoice["address"], invoice["rhash"] = node.get_address( 184 invoice["btc_value"], invoice["uuid"], config.payment_timeout 185 ) 186 except Exception as e: 187 logging.error("Failed to fetch address: {}".format(e)) 188 return {"message": "Error fetching address. Check config.."}, 522 189 190 node.create_qr(invoice["uuid"], invoice["address"], invoice["btc_value"]) 191 192 # Save invoice to database 193 database.write_to_database(invoice) 194 195 invoice["time_left"] = config.payment_timeout - (time.time() - invoice["time"]) 196 logging.info("Created invoice:") 197 pprint(invoice) 198 print() 199 200 return {"invoice": invoice}, 200 201 202 203 @api.doc(params={"uuid": "A payment uuid. Received from /createpayment."}) 204 class check_payment(Resource): 205 @api.response(200, "Success", status_model) 206 @api.response(201, "Unconfirmed", status_model) 207 @api.response(202, "Payment Expired", status_model) 208 def get(self): 209 "Check Payment" 210 """Check the status of a payment.""" 211 uuid = request.args.get("uuid") 212 status = check_payment_status(uuid) 213 214 response = { 215 "payment_complete": 0, 216 "confirmed_paid": 0, 217 "unconfirmed_paid": 0, 218 "expired": 0, 219 } 220 221 # If payment is expired 222 if status["time_left"] <= 0: 223 response.update({"expired": 1}) 224 code = 202 225 else: 226 # Update response with confirmed and unconfirmed amounts 227 response.update(status) 228 229 # Return whether paid or unpaid 230 if response["payment_complete"] == 1: 231 code = 200 232 else: 233 code = 201 234 235 return {"status": response}, code 236 237 238 @api.doc(params={"uuid": "A payment uuid. Received from /createpayment."}) 239 class complete_payment(Resource): 240 @api.response(200, "Payment confirmed.") 241 @api.response(400, "Payment expired.") 242 @api.response(500, "Webhook failure.") 243 def get(self): 244 "Complete Payment" 245 """Run post-payment processing such as any webhooks.""" 246 uuid = request.args.get("uuid") 247 order_id = request.args.get("id") 248 249 invoice = database.load_invoice_from_db(uuid) 250 status = check_payment_status(uuid) 251 252 if status["time_left"] < 0: 253 return {"message": "Expired."}, 400 254 255 if status["payment_complete"] != 1: 256 return {"message": "You havent paid you stingy bastard"} 257 258 if (config.liquid_address is not None) and ( 259 invoice["method"] in ["lnd", "clightning"] 260 ): 261 weakhands.swap_lnbtc_for_lusdt( 262 lightning_node, invoice["btc_value"], config.liquid_address 263 ) 264 265 # Call webhook to confirm payment with merchant 266 if (invoice["webhook"] is not None) and (invoice["webhook"] != ""): 267 logging.info("Calling webhook {}".format(invoice["webhook"])) 268 response = woo_webhook.hook(app.config["SECRET_KEY"], invoice, order_id) 269 270 if response.status_code != 200: 271 err = "Failed to confirm order payment via webhook {}, please contact the store to ensure the order has been confirmed, error response is: {}".format( 272 response.status_code, response.text 273 ) 274 logging.error(err) 275 return {"message": err}, 500 276 277 logging.info("Successfully confirmed payment via webhook.") 278 return {"message": "Payment confirmed with store."}, 200 279 280 return {"message": "Payment confirmed."}, 200 281 282 283 def check_payment_status(uuid): 284 status = { 285 "payment_complete": 0, 286 "confirmed_paid": 0, 287 "unconfirmed_paid": 0, 288 } 289 invoice = database.load_invoice_from_db(uuid) 290 if invoice is None: 291 status.update({"time_left": 0, "not_found": 1}) 292 else: 293 status["time_left"] = config.payment_timeout - (time.time() - invoice["time"]) 294 295 # If payment has not expired, then we're going to check for any transactions 296 if status["time_left"] > 0: 297 node = get_node(invoice["method"]) 298 if node.config['name'] == "lnd": 299 conf_paid, unconf_paid = node.check_payment(invoice["rhash"]) 300 elif (node.config['name'] == "bitcoind") or (node.config['name'] == "clightning"): 301 # Lookup bitcoind / clightning invoice based on label (uuid) 302 conf_paid, unconf_paid = node.check_payment(invoice["uuid"]) 303 elif node.config['name'] == "xpub": 304 conf_paid, unconf_paid = node.check_payment(invoice["address"]) 305 306 # Remove any Decimal types 307 conf_paid, unconf_paid = float(conf_paid), float(unconf_paid) 308 309 # Debugging and demo mode which auto confirms payments after 5 seconds 310 dbg_free_mode_cond = config.free_mode and (time.time() - invoice["time"] > 5) 311 312 # If payment is paid 313 if (conf_paid >= float(invoice["btc_value"]) - config.allowed_underpay_amount) or dbg_free_mode_cond: 314 status.update( 315 { 316 "payment_complete": 1, 317 "confirmed_paid": btc_amount_format(conf_paid), 318 "unconfirmed_paid": btc_amount_format(unconf_paid), 319 } 320 ) 321 else: 322 status.update( 323 { 324 "payment_complete": 0, 325 "confirmed_paid": btc_amount_format(conf_paid), 326 "unconfirmed_paid": btc_amount_format(unconf_paid), 327 } 328 ) 329 330 logging.debug("Invoice {} status: {}".format(uuid, status)) 331 return status 332 333 334 def get_node(payment_method): 335 if payment_method == "onchain": 336 node = bitcoin_node 337 elif payment_method == "lightning": 338 node = lightning_node 339 else: 340 node = None 341 return node 342 343 344 # Add API endpoints 345 api.add_resource(create_payment, "/api/createpayment") 346 api.add_resource(check_payment, "/api/checkpayment") 347 api.add_resource(complete_payment, "/api/completepayment") 348 349 # Test connections on startup: 350 enabled_payment_methods = [] 351 for method in config.payment_methods: 352 #print(method) 353 if method['name'] == "bitcoind": 354 bitcoin_node = bitcoind.btcd(method) 355 logging.info("Connection to bitcoin node successful.") 356 enabled_payment_methods.append("onchain") 357 358 elif method['name'] == "lnd": 359 lightning_node = lnd.lnd(method) 360 logging.info("Connection to lightning node (lnd) successful.") 361 if lightning_node.config['lightning_address'] is not None: 362 from gateways import lightning_address 363 lightning_address.add_ln_address_decorators(app, api, lightning_node) 364 enabled_payment_methods.append("lightning") 365 366 elif method['name'] == "clightning": 367 lightning_node = clightning.clightning(method) 368 logging.info("Connection to lightning node (clightning) successful.") 369 enabled_payment_methods.append("lightning") 370 371 elif method['name'] == "xpub": 372 bitcoin_node = xpub.xpub(method) 373 logging.info("Not connecting to a bitcoin node, using xpubs and blockexplorer APIs.") 374 enabled_payment_methods.append("onchain") 375 376 # Add node connection page 377 if config.node_info is not None: 378 @app.route("/node/") 379 def node(): 380 if config.node_info is True: 381 uri = lightning_node.get_uri() 382 else: 383 uri = config.node_info 384 img = qrcode.make(uri) 385 img.save("static/qr_codes/node.png") 386 headers = {"Content-Type": "text/html"} 387 return make_response( 388 render_template("node.html", params={"uri": uri}), 200, headers 389 ) 390 391 # Add lightning address 392 try: 393 if lightning_node.config['lightning_address'] is not None: 394 from gateways import lightning_address 395 lightning_address.add_ln_address_decorators(app, api, lightning_node) 396 except NameError: 397 # lightning_node is not defined if no LN support configured 398 pass 399 400 # Add Paynym 401 if config.paynym is not None: 402 paynym.insert_paynym_html(config.paynym) 403 404 if __name__ == "__main__": 405 app.run(debug=False)