SatSale

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

commit 58a55b8ed9fdd1d4b2ba949226d6d854cf058539
parent 9b4a7e71df64ce5b2fb2ba1b8c5c71fdea990d43
Author: ingolevin <34458804+ingolevin@users.noreply.github.com>
Date:   Fri, 19 Nov 2021 11:53:02 +0100

Add ability to change base currency (#24)

* add extra currency provider

* WIP: currency conversion working

* Global replacement of variable names

- From: dollar_amount|value  To: fiat_amount|value
- Note. The database.db has to be recreated.
        If there existed a database.db prior to that,
        this will be a breaking change

* Use rate_float instead of rate from coindesk response json

* Add generic get_price function + variable renaming

* Dynymically replace currency symbol in donate.html based on config

* Moving base_currency upwards in file

* Fix centering

* Fix spacing in config file

* Refactor payment_provider constants + reformat

- Move payment_provider constants into a separate function/disctionary  get_payment_provider_constants()
- Added basic unittests: test_get_currency_provider_constants, test_get_price
- Apply PEP8 Autoformatting to price_feed.py

* Remove tests: Add these great tests to another PR (ive got some in progress too)

* remove currency docs arg for create_payment (again oops)

Co-authored-by: Ingo Levin <ingo.levin@absolute-bi.com>
Co-authored-by: nickfarrow <nicholas.w.farrow@gmail.com>
Diffstat:
Mconfig.py | 5+++++
Mgateways/woo_webhook.py | 2+-
Mpayments/database.py | 6+++---
Mpayments/price_feed.py | 37+++++++++++++++++++++++++++----------
Msatsale.py | 18++++++++++--------
Mtemplates/donate.html | 2+-
6 files changed, 47 insertions(+), 23 deletions(-)

diff --git a/config.py b/config.py @@ -67,6 +67,10 @@ connection_attempts = 3 # Generic redirect url after payment redirect = "https://github.com/nickfarrow/satsale" +# Currency and exchange rate provider +base_currency = "USD" +currency_provider = "COINGECKO" # Supported: COINDESK | COINGECKO + # Lightning Address e.g. name@you.satsale.domain (think this requires https url) lightning_address = None lightning_address_comment = None # Defaults to: "Thank you for your support <3" @@ -80,3 +84,4 @@ liquid_address = None # DO NOT CHANGE THIS TO TRUE UNLESS YOU WANT ALL PAYMENTS TO AUTOMATICALLY # BE CONSIDERED AS PAID. free_mode = False + diff --git a/gateways/woo_webhook.py b/gateways/woo_webhook.py @@ -11,7 +11,7 @@ def hook(satsale_secret, invoice, order_id): # 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(invoice["dollar_value"]))).encode("utf-8") + secret_seed = str(int(100 * float(invoice["fiat_value"]))).encode("utf-8") print("Secret seed: {}".format(secret_seed)) secret = hmac.new(key, secret_seed, hashlib.sha256).hexdigest() diff --git a/payments/database.py b/payments/database.py @@ -5,7 +5,7 @@ def create_database(name="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)" + "CREATE TABLE payments (uuid TEXT, fiat_value DECIMAL, btc_value DECIMAL, method TEXT, address TEXT, time DECIMAL, webhook TEXT, rhash TEXT)" ) return @@ -14,10 +14,10 @@ def write_to_database(invoice, name="database.db"): 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 (?,?,?,?,?,?,?,?)", + "INSERT INTO payments (uuid,fiat_value,btc_value,method,address,time,webhook,rhash) VALUES (?,?,?,?,?,?,?,?)", ( invoice["uuid"], - invoice["dollar_value"], + invoice["fiat_value"], invoice["btc_value"], invoice["method"], invoice["address"], diff --git a/payments/price_feed.py b/payments/price_feed.py @@ -3,14 +3,31 @@ import requests import config -def get_price(currency): - price_feed = "https://api.coindesk.com/v1/bpi/currentprice.json" - r = requests.get(price_feed) +def get_currency_provider(currency, currency_provider): + # Define some currency_provider-specific settings + if currency_provider == "COINDESK": + return { + "price_feed": "https://api.coindesk.com/v1/bpi/currentprice.json", + "result_root": "bpi", + "value_attribute": "rate_float", + "ticker": currency.upper(), + } + else: + return { + "price_feed": "https://api.coingecko.com/api/v3/exchange_rates", + "result_root": "rates", + "value_attribute": "value", + "ticker": currency.lower(), + } + +def get_price(currency, currency_provider=config.currency_provider): + provider = get_currency_provider(currency, currency_provider) for i in range(config.connection_attempts): try: + r = requests.get(provider["price_feed"]) price_data = r.json() - prices = price_data["bpi"] + prices = price_data[provider["result_root"]] break except Exception as e: @@ -23,7 +40,7 @@ def get_price(currency): raise ("Failed to reach {}.".format(price_feed)) try: - price = prices[currency]["rate"].replace(",", "") + price = prices[provider["ticker"]][provider["value_attribute"]] return price except: @@ -31,17 +48,17 @@ def get_price(currency): return None -def get_btc_value(dollar_value, currency): +def get_btc_value(base_amount, currency): price = get_price(currency) - if price is not None: + if price is not None: try: - float_value = float(dollar_value) / float(price) + float_value = float(base_amount) / float(price) if not isinstance(float_value, float): - raise Exception("Dollar value should be a float.") + raise Exception("Fiat value should be a float.") except Exception as e: print(e) return float_value - raise Exception("Failed to get dollar value.") + raise Exception("Failed to get fiat value.") diff --git a/satsale.py b/satsale.py @@ -47,8 +47,10 @@ if not os.path.exists("database.db"): # This is currently a donation page that submits to /pay @app.route("/") def index(): + params = dict(request.args) + params["currency"] = config.base_currency headers = {"Content-Type": "text/html"} - return make_response(render_template("donate.html"), 200, headers) + return make_response(render_template("donate.html", params=params), 200, headers) # /pay is the main page for initiating a payment, takes a GET request with ?amount= @@ -79,7 +81,7 @@ invoice_model = api.model( "invoice", { "uuid": fields.String(), - "dollar_value": fields.Float(), + "fiat_value": fields.Float(), "btc_value": fields.Float(), "method": fields.String(), "address": fields.String(), @@ -102,7 +104,7 @@ status_model = api.model( @api.doc( params={ - "amount": "An amount in USD.", + "amount": "An amount in `config.base_currency`.", "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.", } @@ -112,9 +114,9 @@ class create_payment(Resource): @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" + """Initiate a new payment with an `amount` in `config.base_currecy`.""" + base_amount = request.args.get("amount") + currency = config.base_currency label = "" # request.args.get('label') payment_method = request.args.get("method") if payment_method is None: @@ -134,8 +136,8 @@ class create_payment(Resource): invoice = { "uuid": str(uuid.uuid4().hex), - "dollar_value": dollar_amount, - "btc_value": round(get_btc_value(dollar_amount, currency), 8), + "fiat_value": base_amount, + "btc_value": round(get_btc_value(base_amount, currency), 8), "method": payment_method, "time": time.time(), "webhook": webhook, diff --git a/templates/donate.html b/templates/donate.html @@ -49,7 +49,7 @@ <center> <form id="pay" action='/pay' style="margin:0;padding0;"> <h2 style="margin:0;padding0;">Amount: - <input id="amountenter" style="display:inline" size="4" type="float" name="amount" id="amount" placeholder="USD" required> + <input id="amountenter" style="display:inline" size="4" type="float" name="amount" id="amount" placeholder="{{ params.currency }}" required> </h2> <br> <input class="button button1" style="width:40%" type="submit" value="Donate">