SatSale

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

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]&currency=[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)