commit cfabe729bd4e9f11c1a1191d8ab95bc709f16f3a
parent 45b14146a368e03601f5656171e72a55cfb66c30
Author: Kristaps Kaupe <kristaps@blogiem.lv>
Date: Thu, 13 Oct 2022 16:55:35 +0300
Refactor price_feed and add tests
Co-authored-by: eddie <eddiepease1@gmail.com>
Diffstat:
6 files changed, 183 insertions(+), 60 deletions(-)
diff --git a/config.py b/config.py
@@ -8,7 +8,10 @@ for i, arg in enumerate(sys.argv):
conf_path = sys.argv[i + 1]
break
else:
- conf_path = "config.toml"
+ if "pytest" in sys.modules:
+ conf_path = "test/config.toml"
+ else:
+ conf_path = "config.toml"
with open(conf_path, "r") as config_file:
config = toml.load(config_file)
diff --git a/payments/price_feed.py b/payments/price_feed.py
@@ -1,76 +1,107 @@
import requests
import logging
+from abc import ABC, abstractmethod
import config
-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(),
- }
+class PriceFeed(ABC):
+
+ def __init__(self, price_feed_url: str = None) -> None:
+ self._price_data = None
+ self._price_feed_url = price_feed_url
+
+ @abstractmethod
+ def _get_rate(self, base_currency: str) -> float:
+ pass
+
+ def _fetch_price_data(self) -> None:
+ if self._price_feed_url is not None:
+ for i in range(config.connection_attempts):
+ try:
+ r = requests.get(self._price_feed_url)
+ self._price_data = r.json()
+ return
+ except Exception as e:
+ logging.error(e)
+ logging.info(
+ "Attempting again... {}/{}...".format(
+ i + 1, config.connection_attempts)
+ )
+
+ else:
+ raise RuntimeError("Failed to reach {}.".format(
+ self._price_feed_url))
+ # used by tests
+ def set_price_data(self, price_data: dict) -> None:
+ self._price_data = price_data
-def get_price(currency, currency_provider=config.currency_provider, bitcoin_rate_multiplier=config.bitcoin_rate_multiplier):
- provider = get_currency_provider(currency, currency_provider)
- for i in range(config.connection_attempts):
+ def _get_btc_exchange_rate(self, base_currency: str, bitcoin_rate_multiplier: float) -> float:
+ self._fetch_price_data()
try:
- r = requests.get(provider["price_feed"])
- price_data = r.json()
- prices = price_data[provider["result_root"]]
- break
-
- except Exception as e:
- logging.error(e)
- logging.info(
- "Attempting again... {}/{}...".format(i + 1, config.connection_attempts)
+ rate = self._get_rate(base_currency)
+ if bitcoin_rate_multiplier != 1.00:
+ logging.debug(
+ "Adjusting BTC/{} exchange rate from {} to {} " +
+ "because of rate multiplier {}.".format(
+ base_currency, rate, rate * bitcoin_rate_multiplier,
+ bitcoin_rate_multiplier))
+ rate = rate * bitcoin_rate_multiplier
+ return rate
+ except Exception:
+ logging.error(
+ "Failed to find currency {} from {}.".format(
+ base_currency, self._price_feed_url)
)
+ return None
- else:
- raise ("Failed to reach {}.".format(provider["price_feed"]))
-
- try:
- price = prices[provider["ticker"]][provider["value_attribute"]]
- if bitcoin_rate_multiplier != 1.00:
- logging.debug("Adjusting BTC price from {} to {} because of rate multiplier {}.".format(
- price, price * bitcoin_rate_multiplier, bitcoin_rate_multiplier))
- price = price * bitcoin_rate_multiplier
- return price
-
- except Exception:
- logging.error(
- "Failed to find currency {} from {}.".format(currency, provider["price_feed"])
- )
- return None
+ def get_btc_value(self, base_amount: float, base_currency: str) -> float:
+ if base_currency == "BTC":
+ return float(base_amount)
+ elif base_currency == "sats":
+ return float(base_amount) / 10**8
+ exchange_rate = self._get_btc_exchange_rate(
+ base_currency, config.bitcoin_rate_multiplier)
-def get_btc_value(base_amount, currency):
- if currency == "BTC":
- return float(base_amount)
- elif currency == "sats":
- return float(base_amount) / 10**8
+ if exchange_rate is not None:
+ try:
+ float_value = float(base_amount) / exchange_rate
+ except Exception as e:
+ logging.error(e)
- price = get_price(currency)
+ return round(float_value, 8)
+
+ raise RuntimeError("Failed to get base currency value.")
- if price is not None:
- try:
- float_value = float(base_amount) / float(price)
- if not isinstance(float_value, float):
- raise Exception("Fiat value should be a float.")
- except Exception as e:
- logging.error(e)
- return float_value
+class CoinDeskPriceFeed(PriceFeed):
- raise Exception("Failed to get base currency value.")
+ def __init__(self, price_feed_url: str = "https://api.coindesk.com/v1/bpi/currentprice.json") -> None:
+ super().__init__(price_feed_url)
+
+ def _get_rate(self, base_currency: str) -> float:
+ return float(self._price_data["bpi"][base_currency.upper()]["rate_float"])
+
+
+class CoinGeckoPriceFeed(PriceFeed):
+
+ def __init__(self, price_feed_url: str = "https://api.coingecko.com/api/v3/exchange_rates") -> None:
+ super().__init__(price_feed_url)
+
+ def _get_rate(self, base_currency: str) -> float:
+ return float(self._price_data["rates"][base_currency.lower()]["value"])
+
+
+def get_btc_value(base_amount: float, base_currency: str) -> float:
+ if config.currency_provider == "COINDESK":
+ provider = CoinDeskPriceFeed()
+ elif config.currency_provider == "COINGECKO":
+ provider = CoinGeckoPriceFeed()
+ else:
+ raise Exception(
+ "Unsupported exchange rate provider (currency_provider): " +
+ config.currency_provider
+ )
+ return provider.get_btc_value(base_amount, base_currency)
diff --git a/test/coindesk_price_data.json b/test/coindesk_price_data.json
@@ -0,0 +1 @@
+{"time":{"updated":"Oct 13, 2022 12:36:00 UTC","updatedISO":"2022-10-13T12:36:00+00:00","updateduk":"Oct 13, 2022 at 13:36 BST"},"disclaimer":"This data was produced from the CoinDesk Bitcoin Price Index (USD). Non-USD currency data converted using hourly conversion rate from openexchangerates.org","chartName":"Bitcoin","bpi":{"USD":{"code":"USD","symbol":"$","rate":"18,379.9154","description":"United States Dollar","rate_float":18379.9154},"GBP":{"code":"GBP","symbol":"£","rate":"15,358.1103","description":"British Pound Sterling","rate_float":15358.1103},"EUR":{"code":"EUR","symbol":"€","rate":"17,904.7211","description":"Euro","rate_float":17904.7211}}}
+\ No newline at end of file
diff --git a/test/coingecko_price_data.json b/test/coingecko_price_data.json
@@ -0,0 +1 @@
+{"rates":{"btc":{"name":"Bitcoin","unit":"BTC","value":1.0,"type":"crypto"},"eth":{"name":"Ether","unit":"ETH","value":14.874,"type":"crypto"},"ltc":{"name":"Litecoin","unit":"LTC","value":374.065,"type":"crypto"},"bch":{"name":"Bitcoin Cash","unit":"BCH","value":175.631,"type":"crypto"},"bnb":{"name":"Binance Coin","unit":"BNB","value":71.593,"type":"crypto"},"eos":{"name":"EOS","unit":"EOS","value":19401.117,"type":"crypto"},"xrp":{"name":"XRP","unit":"XRP","value":41747.379,"type":"crypto"},"xlm":{"name":"Lumens","unit":"XLM","value":171888.576,"type":"crypto"},"link":{"name":"Chainlink","unit":"LINK","value":2827.288,"type":"crypto"},"dot":{"name":"Polkadot","unit":"DOT","value":3204.529,"type":"crypto"},"yfi":{"name":"Yearn.finance","unit":"YFI","value":2.52,"type":"crypto"},"usd":{"name":"US Dollar","unit":"$","value":18980.011,"type":"fiat"},"aed":{"name":"United Arab Emirates Dirham","unit":"DH","value":69713.771,"type":"fiat"},"ars":{"name":"Argentine Peso","unit":"$","value":2866519.321,"type":"fiat"},"aud":{"name":"Australian Dollar","unit":"A$","value":30242.939,"type":"fiat"},"bdt":{"name":"Bangladeshi Taka","unit":"৳","value":1930084.243,"type":"fiat"},"bhd":{"name":"Bahraini Dinar","unit":"BD","value":7155.767,"type":"fiat"},"bmd":{"name":"Bermudian Dollar","unit":"$","value":18980.011,"type":"fiat"},"brl":{"name":"Brazil Real","unit":"R$","value":100464.996,"type":"fiat"},"cad":{"name":"Canadian Dollar","unit":"CA$","value":26228.098,"type":"fiat"},"chf":{"name":"Swiss Franc","unit":"Fr.","value":18976.291,"type":"fiat"},"clp":{"name":"Chilean Peso","unit":"CLP$","value":17927000.314,"type":"fiat"},"cny":{"name":"Chinese Yuan","unit":"¥","value":136553.589,"type":"fiat"},"czk":{"name":"Czech Koruna","unit":"Kč","value":481088.245,"type":"fiat"},"dkk":{"name":"Danish Krone","unit":"kr.","value":145681.077,"type":"fiat"},"eur":{"name":"Euro","unit":"€","value":19582.0,"type":"fiat"},"gbp":{"name":"British Pound Sterling","unit":"£","value":17130.807,"type":"fiat"},"hkd":{"name":"Hong Kong Dollar","unit":"HK$","value":148975.058,"type":"fiat"},"huf":{"name":"Hungarian Forint","unit":"Ft","value":8488225.678,"type":"fiat"},"idr":{"name":"Indonesian Rupiah","unit":"Rp","value":291513045.227,"type":"fiat"},"ils":{"name":"Israeli New Shekel","unit":"₪","value":67962.296,"type":"fiat"},"inr":{"name":"Indian Rupee","unit":"₹","value":1563678.673,"type":"fiat"},"jpy":{"name":"Japanese Yen","unit":"¥","value":2786398.525,"type":"fiat"},"krw":{"name":"South Korean Won","unit":"₩","value":27187333.879,"type":"fiat"},"kwd":{"name":"Kuwaiti Dinar","unit":"KD","value":5892.97,"type":"fiat"},"lkr":{"name":"Sri Lankan Rupee","unit":"Rs","value":6957533.448,"type":"fiat"},"mmk":{"name":"Burmese Kyat","unit":"K","value":39866555.944,"type":"fiat"},"mxn":{"name":"Mexican Peso","unit":"MX$","value":379462.982,"type":"fiat"},"myr":{"name":"Malaysian Ringgit","unit":"RM","value":89044.723,"type":"fiat"},"ngn":{"name":"Nigerian Naira","unit":"₦","value":8381327.047,"type":"fiat"},"nok":{"name":"Norwegian Krone","unit":"kr","value":204340.054,"type":"fiat"},"nzd":{"name":"New Zealand Dollar","unit":"NZ$","value":33852.121,"type":"fiat"},"php":{"name":"Philippine Peso","unit":"₱","value":1119146.821,"type":"fiat"},"pkr":{"name":"Pakistani Rupee","unit":"₨","value":4133789.739,"type":"fiat"},"pln":{"name":"Polish Zloty","unit":"zł","value":94990.059,"type":"fiat"},"rub":{"name":"Russian Ruble","unit":"₽","value":1206654.145,"type":"fiat"},"sar":{"name":"Saudi Riyal","unit":"SR","value":71336.353,"type":"fiat"},"sek":{"name":"Swedish Krona","unit":"kr","value":215842.473,"type":"fiat"},"sgd":{"name":"Singapore Dollar","unit":"S$","value":27249.735,"type":"fiat"},"thb":{"name":"Thai Baht","unit":"฿","value":720243.98,"type":"fiat"},"try":{"name":"Turkish Lira","unit":"₺","value":352815.634,"type":"fiat"},"twd":{"name":"New Taiwan Dollar","unit":"NT$","value":605462.342,"type":"fiat"},"uah":{"name":"Ukrainian hryvnia","unit":"₴","value":701117.823,"type":"fiat"},"vef":{"name":"Venezuelan bolívar fuerte","unit":"Bs.F","value":1900.468,"type":"fiat"},"vnd":{"name":"Vietnamese đồng","unit":"₫","value":457791341.297,"type":"fiat"},"zar":{"name":"South African Rand","unit":"R","value":348477.734,"type":"fiat"},"xdr":{"name":"IMF Special Drawing Rights","unit":"XDR","value":14190.044,"type":"fiat"},"xag":{"name":"Silver - Troy Ounce","unit":"XAG","value":1001.222,"type":"commodity"},"xau":{"name":"Gold - Troy Ounce","unit":"XAU","value":11.376,"type":"commodity"},"bits":{"name":"Bits","unit":"μBTC","value":1000000.0,"type":"crypto"},"sats":{"name":"Satoshi","unit":"sats","value":100000000.0,"type":"crypto"}}}
+\ No newline at end of file
diff --git a/test/config.toml b/test/config.toml
@@ -0,0 +1,5 @@
+# Bare minimum config for tests
+
+payment_methods = []
+
+[satsale]
diff --git a/test/test_price_feed.py b/test/test_price_feed.py
@@ -0,0 +1,80 @@
+import json
+import os
+import pytest
+import sys
+
+sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
+from payments.price_feed import \
+ CoinDeskPriceFeed, \
+ CoinGeckoPriceFeed
+
+testdir = os.path.dirname(os.path.realpath(__file__))
+
+
+def read_price_feed_data(filename: str) -> dict:
+ with open(os.path.join(testdir, filename), "r") as f:
+ json_data = f.read()
+ return json.loads(json_data)
+
+
+@pytest.mark.parametrize(
+ "price_feed",
+ [
+ {
+ "class": CoinDeskPriceFeed,
+ "data_file": "coindesk_price_data.json"
+ },
+ {
+ "class": CoinGeckoPriceFeed,
+ "data_file": "coingecko_price_data.json"
+ }
+ ])
+def test_invalid_base_currency(price_feed: dict) -> None:
+ provider = price_feed["class"](price_feed_url=None)
+ provider.set_price_data(read_price_feed_data(price_feed["data_file"]))
+ with pytest.raises(RuntimeError):
+ provider.get_btc_value(1, "xxx")
+
+
+@pytest.mark.parametrize(
+ "price_feed",
+ [
+ {"class": CoinDeskPriceFeed},
+ {"class": CoinGeckoPriceFeed}
+ ])
+def test_btc_btc_price(price_feed: dict) -> None:
+ provider = price_feed["class"](price_feed_url=None)
+ assert(provider.get_btc_value(1, "BTC") == 1)
+ assert(provider.get_btc_value(1000000, "sats") == 0.01)
+
+
+@pytest.mark.parametrize(
+ "price_feed",
+ [
+ {
+ "class": CoinDeskPriceFeed,
+ "data_file": "coindesk_price_data.json",
+ "conversions": [
+ {
+ "base_value": 100, "base_currency": "USD", "btc_value": 0.00544072
+ }
+ ]
+ },
+ {
+ "class": CoinGeckoPriceFeed,
+ "data_file": "coingecko_price_data.json",
+ "conversions": [
+ {
+ "base_value": 100, "base_currency": "USD", "btc_value": 0.0052687
+ }
+ ]
+ }
+ ])
+def test_fiat_btc_price(price_feed: dict) -> None:
+ provider = price_feed["class"](price_feed_url=None)
+ provider.set_price_data(read_price_feed_data(price_feed["data_file"]))
+ for conv in price_feed["conversions"]:
+ assert(
+ provider.get_btc_value(conv["base_value"], conv["base_currency"]) ==
+ conv["btc_value"]
+ )