diff --git a/config/settings/base.py b/config/settings/base.py index 39e8a9766..1ab64641a 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -496,10 +496,6 @@ "TOKENS_ERC20_GET_BALANCES_BATCH", default=2_000 ) # Number of tokens to get balances from in the same request. From 2_500 some nodes raise HTTP 413 -TOKEN_ETH_PRICE_TTL = env.int( - "TOKEN_ETH_PRICE_TTL", default=60 * 30 # 30 minutes -) # Expiration time for token eth price - # Notifications # ------------------------------------------------------------------------------ SLACK_API_WEBHOOK = env("SLACK_API_WEBHOOK", default=None) diff --git a/safe_transaction_service/history/services/balance_service.py b/safe_transaction_service/history/services/balance_service.py index 915c4046a..2831bccdb 100644 --- a/safe_transaction_service/history/services/balance_service.py +++ b/safe_transaction_service/history/services/balance_service.py @@ -2,6 +2,7 @@ import operator from dataclasses import dataclass from datetime import datetime +from enum import Enum from typing import List, Optional, Sequence from django.conf import settings @@ -16,13 +17,7 @@ from gnosis.eth import EthereumClient, EthereumClientProvider from gnosis.eth.utils import fast_is_checksum_address -from safe_transaction_service.tokens.clients import CannotGetPrice from safe_transaction_service.tokens.models import Token -from safe_transaction_service.tokens.services.price_service import ( - FiatCode, - PriceService, - PriceServiceProvider, -) from safe_transaction_service.utils.redis import get_redis from safe_transaction_service.utils.utils import chunks @@ -72,6 +67,11 @@ def get_price_address(self) -> ChecksumAddress: return self.token_address +class FiatCode(Enum): + USD = 1 + EUR = 2 + + @dataclass class BalanceWithFiat(Balance): eth_value: float # Value in ether @@ -84,9 +84,7 @@ class BalanceWithFiat(Balance): class BalanceServiceProvider: def __new__(cls): if not hasattr(cls, "instance"): - cls.instance = BalanceService( - EthereumClientProvider(), PriceServiceProvider(), get_redis() - ) + cls.instance = BalanceService(EthereumClientProvider(), get_redis()) return cls.instance @classmethod @@ -96,12 +94,9 @@ def del_singleton(cls): class BalanceService: - def __init__( - self, ethereum_client: EthereumClient, price_service: PriceService, redis: Redis - ): + def __init__(self, ethereum_client: EthereumClient, redis: Redis): self.ethereum_client = ethereum_client self.ethereum_network = self.ethereum_client.get_network() - self.price_service = price_service self.redis = redis self.cache_token_info = TTLCache( maxsize=4096, ttl=60 * 30 @@ -268,50 +263,28 @@ def get_usd_balances( exclude_spam: bool = False, ) -> List[BalanceWithFiat]: """ - All this could be more optimal (e.g. batching requests), but as everything is cached - I think we should be alright + NOTE: PriceService was removed, this function return balances with price 0. :param safe_address: :param only_trusted: If True, return balance only for trusted tokens :param exclude_spam: If True, exclude spam tokens :return: List of BalanceWithFiat """ - # TODO Use price service get_token_cached_usd_values balances: List[Balance] = self.get_balances( safe_address, only_trusted, exclude_spam ) - try: - eth_price = self.price_service.get_native_coin_usd_price() - except CannotGetPrice: - logger.warning("Cannot get network ether price", exc_info=True) - eth_price = 0 balances_with_usd = [] - price_token_addresses = [balance.get_price_address() for balance in balances] - token_eth_values_with_timestamp = ( - self.price_service.get_token_cached_eth_values(price_token_addresses) - ) - for balance, token_eth_value_with_timestamp in zip( - balances, token_eth_values_with_timestamp - ): - token_eth_value = token_eth_value_with_timestamp.eth_value - token_address = balance.token_address - if not token_address: # Ether - fiat_conversion = eth_price - fiat_balance = fiat_conversion * (balance.balance / 10**18) - else: - fiat_conversion = eth_price * token_eth_value - balance_with_decimals = balance.balance / 10**balance.token.decimals - fiat_balance = fiat_conversion * balance_with_decimals + for balance in balances: balances_with_usd.append( BalanceWithFiat( balance.token_address, balance.token, balance.balance, - token_eth_value, - token_eth_value_with_timestamp.timestamp, - round(fiat_balance, 4), - round(fiat_conversion, 4), + 0.0, + datetime.utcfromtimestamp(0), + 0.0, + 0.0, FiatCode.USD.name, ) ) diff --git a/safe_transaction_service/history/tests/test_balance_service.py b/safe_transaction_service/history/tests/test_balance_service.py index 49a106f37..c8c7bbc11 100644 --- a/safe_transaction_service/history/tests/test_balance_service.py +++ b/safe_transaction_service/history/tests/test_balance_service.py @@ -1,18 +1,13 @@ -from typing import Optional -from unittest import mock -from unittest.mock import MagicMock +import datetime from django.test import TestCase -from django.utils import timezone from eth_account import Account -from eth_typing import ChecksumAddress from gnosis.eth.tests.ethereum_test_case import EthereumTestCaseMixin from gnosis.eth.tests.utils import deploy_erc20 from safe_transaction_service.tokens.models import Token -from safe_transaction_service.tokens.services.price_service import PriceService from safe_transaction_service.tokens.tests.factories import TokenFactory from ..services import BalanceServiceProvider @@ -41,18 +36,8 @@ def test_get_token_info(self): self.assertEqual(token_info.symbol, token_db.symbol) self.assertEqual(token_info.decimals, token_db.decimals) - @mock.patch.object( - PriceService, "get_token_eth_value", return_value=0.4, autospec=True - ) - @mock.patch.object( - PriceService, "get_native_coin_usd_price", return_value=123.4, autospec=True - ) - @mock.patch.object(timezone, "now", return_value=timezone.now()) def test_get_usd_balances( self, - timezone_now_mock: MagicMock, - get_native_coin_usd_price_mock: MagicMock, - get_token_eth_value_mock: MagicMock, ): balance_service = self.balance_service @@ -85,16 +70,22 @@ def test_get_usd_balances( balances, [ BalanceWithFiat( - None, None, value, 1.0, timezone_now_mock.return_value, 0.0, 123.4 + None, + None, + value, + 0.0, + datetime.datetime.utcfromtimestamp(0), + 0.0, + 0.0, ), BalanceWithFiat( erc20.address, token_info, tokens_value, - 0.4, - timezone_now_mock.return_value, - round(123.4 * 0.4 * (tokens_value / 1e18), 4), - round(123.4 * 0.4, 4), + 0.0, + datetime.datetime.utcfromtimestamp(0), + 0.0, + 0.0, ), ], ) @@ -104,7 +95,7 @@ def test_get_usd_balances( balances, [ BalanceWithFiat( - None, None, value, 1.0, timezone_now_mock.return_value, 0.0, 123.4 + None, None, value, 0.0, datetime.datetime.utcfromtimestamp(0), 0, 0 ), ], ) @@ -115,16 +106,16 @@ def test_get_usd_balances( balances, [ BalanceWithFiat( - None, None, value, 1.0, timezone_now_mock.return_value, 0.0, 123.4 + None, None, value, 0.0, datetime.datetime.utcfromtimestamp(0), 0, 0 ), BalanceWithFiat( erc20.address, token_info, tokens_value, - 0.4, - timezone_now_mock.return_value, - round(123.4 * 0.4 * (tokens_value / 1e18), 4), - round(123.4 * 0.4, 4), + 0, + datetime.datetime.utcfromtimestamp(0), + 0, + 0, ), ], ) @@ -167,57 +158,46 @@ def test_get_usd_balances( None, None, value, - 1.0, - timezone_now_mock.return_value, 0.0, - 123.4, + datetime.datetime.utcfromtimestamp(0), + 0.0, + 0.0, ), BalanceWithFiat( erc20_3.address, token_info_3, tokens_value, - 0.4, - timezone_now_mock.return_value, - round(123.4 * 0.4 * (tokens_value / 1e18), 4), - round(123.4 * 0.4, 4), + 0.0, + datetime.datetime.utcfromtimestamp(0), + 0.0, + 0.0, ), BalanceWithFiat( erc20.address, token_info, tokens_value, - 0.4, - timezone_now_mock.return_value, - round(123.4 * 0.4 * (tokens_value / 1e18), 4), - round(123.4 * 0.4, 4), + 0.0, + datetime.datetime.utcfromtimestamp(0), + 0.0, + 0.0, ), BalanceWithFiat( erc20_2.address, token_info_2, tokens_value, - 0.4, - timezone_now_mock.return_value, - round(123.4 * 0.4 * (tokens_value / 1e18), 4), - round(123.4 * 0.4, 4), + 0.0, + datetime.datetime.utcfromtimestamp(0), + 0.0, + 0.0, ), ], ) - @mock.patch.object( - PriceService, "get_token_eth_value", return_value=0.4, autospec=True - ) - @mock.patch.object( - PriceService, "get_native_coin_usd_price", return_value=123.4, autospec=True - ) - @mock.patch.object(timezone, "now", return_value=timezone.now()) - def test_get_usd_balances_copy_price( - self, - timezone_now_mock: MagicMock, - get_native_coin_usd_price_mock: MagicMock, - get_token_eth_value_mock: MagicMock, - ): + def test_get_usd_balances_copy_price(self): balance_service = self.balance_service safe_address = SafeContractFactory().address random_address = Account.create().address + timestamp_str = "1970-01-01T00:00:00Z" balances = balance_service.get_usd_balances(safe_address) self.assertEqual(len(balances), 1) @@ -235,16 +215,7 @@ def test_get_usd_balances_copy_price( ) ERC20TransferFactory(address=erc20.address, to=safe_address) - def get_token_eth_value( - self, token_address: ChecksumAddress - ) -> Optional[float]: - if token_address == erc20.address: - return 0.4 - elif token_address == random_address: - return 0.1 - - get_token_eth_value_mock.side_effect = get_token_eth_value - for expected_token_eth_value in (0.4, 0.1): + for expected_token_eth_value in (0, 0): with self.subTest(expected_token_eth_value=expected_token_eth_value): balances = balance_service.get_usd_balances(safe_address) self.assertEqual(len(balances), 2) @@ -255,24 +226,19 @@ def get_token_eth_value( None, None, 0, - 1.0, - timezone_now_mock.return_value, + expected_token_eth_value, + datetime.datetime.utcfromtimestamp(0), + 0.0, 0.0, - 123.4, ), BalanceWithFiat( erc20.address, balance_service.get_token_info(erc20.address), tokens_value, expected_token_eth_value, - timezone_now_mock.return_value, - round( - 123.4 - * expected_token_eth_value - * (tokens_value / 1e18), - 4, - ), - round(123.4 * expected_token_eth_value, 4), + datetime.datetime.utcfromtimestamp(0), + 0.0, + 0.0, ), ], ) diff --git a/safe_transaction_service/history/tests/test_views.py b/safe_transaction_service/history/tests/test_views.py index fec17894d..7e8d9a15a 100644 --- a/safe_transaction_service/history/tests/test_views.py +++ b/safe_transaction_service/history/tests/test_views.py @@ -31,7 +31,6 @@ from safe_transaction_service.contracts.tests.factories import ContractFactory from safe_transaction_service.contracts.tx_decoder import DbTxDecoder from safe_transaction_service.tokens.models import Token -from safe_transaction_service.tokens.services.price_service import PriceService from safe_transaction_service.tokens.tests.factories import TokenFactory from ...utils.redis import get_redis @@ -1796,23 +1795,13 @@ def test_safe_balances_view(self): ) @mock.patch.object(BalanceService, "get_token_info", autospec=True) - @mock.patch.object( - PriceService, "get_token_eth_value", return_value=0.4, autospec=True - ) - @mock.patch.object( - PriceService, "get_native_coin_usd_price", return_value=123.4, autospec=True - ) @mock.patch.object(timezone, "now", return_value=timezone.now()) def test_safe_balances_usd_view( self, timezone_now_mock: MagicMock, - get_native_coin_usd_price_mock: MagicMock, - get_token_eth_value_mock: MagicMock, get_token_info_mock: MagicMock, ): - timestamp_str = timezone_now_mock.return_value.isoformat().replace( - "+00:00", "Z" - ) + timestamp_str = "1970-01-01T00:00:00Z" safe_address = Account.create().address response = self.client.get( reverse("v1:history:safe-balances-usd", args=(safe_address,)), format="json" @@ -1829,7 +1818,7 @@ def test_safe_balances_usd_view( self.assertEqual(len(response.data), 1) self.assertIsNone(response.data[0]["token_address"]) self.assertEqual(response.data[0]["balance"], str(value)) - self.assertEqual(response.data[0]["eth_value"], "1.0") + self.assertEqual(response.data[0]["eth_value"], "0.0") tokens_value = int(12 * 1e18) erc20 = self.deploy_example_erc20(tokens_value, safe_address) @@ -1859,20 +1848,20 @@ def test_safe_balances_usd_view( "token_address": None, "token": None, "balance": str(value), - "eth_value": "1.0", + "eth_value": "0.0", "timestamp": timestamp_str, "fiat_balance": "0.0", - "fiat_conversion": "123.4", + "fiat_conversion": "0.0", "fiat_code": "USD", }, # 7 wei is rounded to 0.0 { "token_address": erc20.address, "token": token_dict, "balance": str(tokens_value), - "eth_value": "0.4", + "eth_value": "0.0", "timestamp": timestamp_str, - "fiat_balance": str(round(123.4 * 0.4 * (tokens_value / 1e18), 4)), - "fiat_conversion": str(round(123.4 * 0.4, 4)), + "fiat_balance": "0.0", + "fiat_conversion": "0.0", "fiat_code": "USD", }, ], diff --git a/safe_transaction_service/tokens/clients/__init__.py b/safe_transaction_service/tokens/clients/__init__.py index 7fddb40b4..494386c22 100644 --- a/safe_transaction_service/tokens/clients/__init__.py +++ b/safe_transaction_service/tokens/clients/__init__.py @@ -1,8 +1,4 @@ # flake8: noqa F401 -from .binance_client import BinanceClient from .coingecko_client import CoingeckoClient from .coinmarketcap_client import CoinMarketCapClient, CoinMarketCapToken -from .exceptions import CannotGetPrice from .kleros_client import KlerosClient, KlerosToken -from .kraken_client import KrakenClient -from .kucoin_client import KucoinClient diff --git a/safe_transaction_service/tokens/clients/binance_client.py b/safe_transaction_service/tokens/clients/binance_client.py deleted file mode 100644 index 7581036e6..000000000 --- a/safe_transaction_service/tokens/clients/binance_client.py +++ /dev/null @@ -1,47 +0,0 @@ -import logging - -from .base_client import BaseHTTPClient -from .exceptions import CannotGetPrice - -logger = logging.getLogger(__name__) - - -class BinanceClient(BaseHTTPClient): # pragma: no cover - def _get_price(self, symbol: str) -> float: - url = f"https://api.binance.com/api/v3/avgPrice?symbol={symbol}" - try: - response = self.http_session.get(url, timeout=self.request_timeout) - api_json = response.json() - if not response.ok: - logger.warning("Cannot get price from url=%s", url) - raise CannotGetPrice(api_json.get("msg")) - - price = float(api_json["price"]) - if not price: - raise CannotGetPrice(f"Price from url={url} is {price}") - return price - except (ValueError, IOError) as e: - raise CannotGetPrice from e - - def get_ada_usd_price(self) -> float: - return self._get_price("ADAUSDT") - - def get_aurora_usd_price(self): - return self._get_price("NEARUSDT") - - def get_bnb_usd_price(self) -> float: - return self._get_price("BNBUSDT") - - def get_ether_usd_price(self) -> float: - """ - :return: current USD price for Ethereum - :raises: CannotGetPrice - """ - return self._get_price("ETHUSDT") - - def get_matic_usd_price(self) -> float: - """ - :return: current USD price for MATIC - :raises: CannotGetPrice - """ - return self._get_price("MATICUSDT") diff --git a/safe_transaction_service/tokens/clients/coingecko_client.py b/safe_transaction_service/tokens/clients/coingecko_client.py index a327930ce..9451bb9f5 100644 --- a/safe_transaction_service/tokens/clients/coingecko_client.py +++ b/safe_transaction_service/tokens/clients/coingecko_client.py @@ -9,7 +9,6 @@ from safe_transaction_service.tokens.clients.base_client import BaseHTTPClient from safe_transaction_service.tokens.clients.exceptions import ( - CannotGetPrice, Coingecko404, CoingeckoRateLimitError, CoingeckoRequestError, @@ -61,44 +60,6 @@ def _do_request(self, url: str) -> Dict[str, Any]: logger.warning("Problem fetching %s", url) raise CoingeckoRequestError from e - def _get_price(self, url: str, name: str): - try: - result = self._do_request(url) - - # Result is returned with lowercased `name` (if querying by contract address, then `token_address`) - price = result.get(name) - if price and price.get("usd"): - return price["usd"] - else: - raise CannotGetPrice(f"Price from url={url} is {price}") - except CoingeckoRequestError as e: - raise CannotGetPrice( - f"Cannot get price from Coingecko for token={name}" - ) from e - - def get_price(self, name: str) -> float: - """ - :param name: coin name - :return: usd price for token name, 0. if not found - """ - name = name.lower() - url = urljoin( - self.base_url, f"/api/v3/simple/price?ids={name}&vs_currencies=usd" - ) - return self._get_price(url, name) - - def get_token_price(self, token_address: ChecksumAddress) -> float: - """ - :param token_address: - :return: usd price for token address, 0. if not found - """ - token_address = token_address.lower() - url = urljoin( - self.base_url, - f"api/v3/simple/token_price/{self.asset_platform}?contract_addresses={token_address}&vs_currencies=usd", - ) - return self._get_price(url, token_address) - @lru_cache(maxsize=128) def get_token_info( self, token_address: ChecksumAddress @@ -117,36 +78,3 @@ def get_token_logo_url(self, token_address: ChecksumAddress) -> Optional[str]: token_info = self.get_token_info(token_address) if token_info: return token_info["image"]["large"] - - def get_ada_usd_price(self) -> float: - return self.get_price("cardano") - - def get_avax_usd_price(self) -> float: - return self.get_price("avalanche-2") - - def get_aoa_usd_price(self) -> float: - return self.get_price("aurora") - - def get_bnb_usd_price(self) -> float: - return self.get_price("binancecoin") - - def get_ewt_usd_price(self) -> float: - return self.get_price("energy-web-token") - - def get_matic_usd_price(self) -> float: - return self.get_price("matic-network") - - def get_gather_usd_price(self) -> float: - return self.get_price("gather") - - def get_fuse_usd_price(self) -> float: - return self.get_price("fuse-network-token") - - def get_kcs_usd_price(self) -> float: - return self.get_price("kucoin-shares") - - def get_metis_usd_price(self) -> float: - return self.get_price("metis-token") - - def get_mtr_usd_price(self) -> float: - return self.get_price("meter-stable") diff --git a/safe_transaction_service/tokens/clients/exceptions.py b/safe_transaction_service/tokens/clients/exceptions.py index 15046cf3d..ca0d1e29e 100644 --- a/safe_transaction_service/tokens/clients/exceptions.py +++ b/safe_transaction_service/tokens/clients/exceptions.py @@ -15,7 +15,3 @@ class CoingeckoRateLimitError(CoingeckoRequestError): } } """ - - -class CannotGetPrice(CoingeckoRequestError): - pass diff --git a/safe_transaction_service/tokens/clients/kraken_client.py b/safe_transaction_service/tokens/clients/kraken_client.py deleted file mode 100644 index 8e593c954..000000000 --- a/safe_transaction_service/tokens/clients/kraken_client.py +++ /dev/null @@ -1,72 +0,0 @@ -import logging - -from .base_client import BaseHTTPClient -from .exceptions import CannotGetPrice - -logger = logging.getLogger(__name__) - - -class KrakenClient(BaseHTTPClient): - def _get_price(self, symbol: str) -> float: - url = f"https://api.kraken.com/0/public/Ticker?pair={symbol}" - try: - response = self.http_session.get(url, timeout=self.request_timeout) - api_json = response.json() - error = api_json.get("error") - if not response.ok or error: - logger.warning("Cannot get price from url=%s", url) - raise CannotGetPrice(str(api_json["error"])) - - result = api_json["result"] - for new_ticker in result: - price = float(result[new_ticker]["c"][0]) - if not price: - raise CannotGetPrice(f"Price from url={url} is {price}") - return price - except (ValueError, IOError) as e: - raise CannotGetPrice from e - - def get_ada_usd_price(self) -> float: - return self._get_price("ADAUSD") - - def get_avax_usd_price(self) -> float: - """ - :return: current USD price for AVAX - :raises: CannotGetPrice - """ - return self._get_price("AVAXUSD") - - def get_dai_usd_price(self) -> float: - """ - :return: current USD price for DAI - :raises: CannotGetPrice - """ - return self._get_price("DAIUSD") - - def get_ether_usd_price(self) -> float: - """ - :return: current USD price for Ethereum - :raises: CannotGetPrice - """ - return self._get_price("ETHUSD") - - def get_matic_usd_price(self): - """ - :return: current USD price for MATIC - :raises: CannotGetPrice - """ - return self._get_price("MATICUSD") - - def get_ewt_usd_price(self) -> float: - """ - :return: current USD price for Energy Web Token - :raises: CannotGetPrice - """ - return self._get_price("EWTUSD") - - def get_algo_usd_price(self): - """ - :return: current USD price for Algorand - :raises: CannotGetPrice - """ - return self._get_price("ALGOUSD") diff --git a/safe_transaction_service/tokens/clients/kucoin_client.py b/safe_transaction_service/tokens/clients/kucoin_client.py deleted file mode 100644 index b4aa4a465..000000000 --- a/safe_transaction_service/tokens/clients/kucoin_client.py +++ /dev/null @@ -1,89 +0,0 @@ -import logging - -from .base_client import BaseHTTPClient -from .exceptions import CannotGetPrice - -logger = logging.getLogger(__name__) - - -class KucoinClient(BaseHTTPClient): - def _get_price(self, symbol: str): - url = f"https://api.kucoin.com/api/v1/market/orderbook/level1?symbol={symbol}" - - try: - response = self.http_session.get(url, timeout=self.request_timeout) - result = response.json() - return float(result["data"]["price"]) - except (ValueError, IOError) as e: - logger.warning("Cannot get price from url=%s", url) - raise CannotGetPrice from e - - def get_ether_usd_price(self) -> float: - """ - :return: current USD price for ETH Coin - :raises: CannotGetPrice - """ - return self._get_price("ETH-USDT") - - def get_aurora_usd_price(self) -> float: - """ - :return: current USD price for Aurora Coin - :raises: CannotGetPrice - """ - return self._get_price("AURORA-USDT") - - def get_bnb_usd_price(self) -> float: - """ - :return: current USD price for Binance Coin - :raises: CannotGetPrice - """ - return self._get_price("BNB-USDT") - - def get_celo_usd_price(self) -> float: - """ - :return: current USD price for Celo - :raises: CannotGetPrice - """ - return self._get_price("CELO-USDT") - - def get_cro_usd_price(self) -> float: - """ - :return: current USD price for Cronos - :raises: CannotGetPrice - """ - return self._get_price("CRO-USDT") - - def get_ewt_usd_price(self) -> float: - """ - :return: current USD price for Energy Web Token - :raises: CannotGetPrice - """ - return self._get_price("EWT-USDT") - - def get_kcs_usd_price(self) -> float: - """ - :return: current USD price for KuCoin Token - :raises: CannotGetPrice - """ - return self._get_price("KCS-USDT") - - def get_matic_usd_price(self) -> float: - """ - :return: current USD price for MATIC Token - :raises: CannotGetPrice - """ - return self._get_price("MATIC-USDT") - - def get_xdc_usd_price(self) -> float: - """ - :return: current USD price for XDC Token - :raises: CannotGetPrice - """ - return self._get_price("XDC-USDT") - - def get_ftm_usd_price(self) -> float: - """ - :return: current USD price for FTM Token - :raises: CannotGetPrice - """ - return self._get_price("FTM-USDT") diff --git a/safe_transaction_service/tokens/services/__init__.py b/safe_transaction_service/tokens/services/__init__.py deleted file mode 100644 index c50f75339..000000000 --- a/safe_transaction_service/tokens/services/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# flake8: noqa F401 -from .price_service import PriceService, PriceServiceProvider diff --git a/safe_transaction_service/tokens/services/price_service.py b/safe_transaction_service/tokens/services/price_service.py deleted file mode 100644 index 56f2077cb..000000000 --- a/safe_transaction_service/tokens/services/price_service.py +++ /dev/null @@ -1,489 +0,0 @@ -import operator -from dataclasses import dataclass -from datetime import datetime -from enum import Enum -from functools import cached_property -from logging import getLogger -from typing import Iterator, List, Optional, Sequence, Tuple - -from django.utils import timezone - -from cache_memoize import cache_memoize -from cachetools import TTLCache, cachedmethod -from eth_typing import ChecksumAddress -from redis import Redis - -from gnosis.eth import EthereumClient, EthereumClientProvider -from gnosis.eth.constants import NULL_ADDRESS -from gnosis.eth.ethereum_client import EthereumNetwork -from gnosis.eth.oracles import ( - AaveOracle, - BalancerOracle, - ComposedPriceOracle, - CowswapOracle, - CurveOracle, - EnzymeOracle, - KyberOracle, - MooniswapOracle, - OracleException, - PoolTogetherOracle, - PriceOracle, - PricePoolOracle, - SuperfluidOracle, - SushiswapOracle, - UnderlyingToken, - UniswapV2Oracle, - UniswapV3Oracle, - YearnOracle, -) - -from safe_transaction_service.utils.redis import get_redis - -from ..clients import CannotGetPrice, CoingeckoClient, KrakenClient, KucoinClient -from ..tasks import EthValueWithTimestamp, calculate_token_eth_price_task - -logger = getLogger(__name__) - - -class FiatCode(Enum): - USD = 1 - EUR = 2 - - -@dataclass -class FiatPriceWithTimestamp: - fiat_price: float - fiat_code: FiatCode - timestamp: datetime - - -class PriceServiceProvider: - def __new__(cls): - if not hasattr(cls, "instance"): - cls.instance = PriceService(EthereumClientProvider(), get_redis()) - return cls.instance - - @classmethod - def del_singleton(cls): - if hasattr(cls, "instance"): - del cls.instance - - -class PriceService: - def __init__(self, ethereum_client: EthereumClient, redis: Redis): - self.ethereum_client = ethereum_client - self.ethereum_network = self.ethereum_client.get_network() - self.redis = redis - self.coingecko_client = CoingeckoClient(self.ethereum_network) - self.kraken_client = KrakenClient() - self.kucoin_client = KucoinClient() - self.cache_ether_usd_price = TTLCache( - maxsize=2048, ttl=60 * 30 - ) # 30 minutes of caching - self.cache_native_coin_usd_price = TTLCache( - maxsize=2048, ttl=60 * 30 - ) # 30 minutes of caching - self.cache_token_eth_value = TTLCache( - maxsize=2048, ttl=60 * 30 - ) # 30 minutes of caching - self.cache_token_usd_value = TTLCache( - maxsize=2048, ttl=60 * 30 - ) # 30 minutes of caching - self.cache_underlying_token = TTLCache( - maxsize=2048, ttl=60 * 30 - ) # 30 minutes of caching - self.cache_token_info = {} - - @cached_property - def enabled_price_oracles(self) -> Tuple[PriceOracle]: - oracles = tuple( - Oracle(self.ethereum_client) - for Oracle in ( - UniswapV3Oracle, - CowswapOracle, - UniswapV2Oracle, - SushiswapOracle, - KyberOracle, - ) - if Oracle.is_available(self.ethereum_client) - ) - if oracles: - if AaveOracle.is_available(self.ethereum_client): - oracles += (AaveOracle(self.ethereum_client, oracles[0]),) - if SuperfluidOracle.is_available(self.ethereum_client): - oracles += (SuperfluidOracle(self.ethereum_client, oracles[0]),) - - return oracles - - @cached_property - def enabled_price_pool_oracles(self) -> Tuple[PricePoolOracle]: - if not self.enabled_price_oracles: - return tuple() - oracles = tuple( - Oracle(self.ethereum_client, self.enabled_price_oracles[0]) - for Oracle in ( - BalancerOracle, - MooniswapOracle, - ) - ) - - if UniswapV2Oracle.is_available(self.ethereum_client): - # Uses a different constructor that others pool oracles - oracles = (UniswapV2Oracle(self.ethereum_client),) + oracles - return oracles - - @cached_property - def enabled_composed_price_oracles(self) -> Tuple[ComposedPriceOracle]: - return tuple( - Oracle(self.ethereum_client) - for Oracle in (CurveOracle, YearnOracle, PoolTogetherOracle, EnzymeOracle) - if Oracle.is_available(self.ethereum_client) - ) - - def get_avalanche_usd_price(self) -> float: - try: - return self.kraken_client.get_avax_usd_price() - except CannotGetPrice: - return self.coingecko_client.get_avax_usd_price() - - def get_aurora_usd_price(self) -> float: - try: - return self.kucoin_client.get_aurora_usd_price() - except CannotGetPrice: - return self.coingecko_client.get_aoa_usd_price() - - def get_cardano_usd_price(self) -> float: - try: - return self.kraken_client.get_ada_usd_price() - except CannotGetPrice: - return self.coingecko_client.get_ada_usd_price() - - def get_algorand_usd_price(self) -> float: - return self.kraken_client.get_algo_usd_price() - - def get_binance_usd_price(self) -> float: - try: - return self.kucoin_client.get_bnb_usd_price() - except CannotGetPrice: - return self.coingecko_client.get_bnb_usd_price() - - def get_ewt_usd_price(self) -> float: - try: - return self.kraken_client.get_ewt_usd_price() - except CannotGetPrice: - try: - return self.kucoin_client.get_ewt_usd_price() - except CannotGetPrice: - return self.coingecko_client.get_ewt_usd_price() - - def get_matic_usd_price(self) -> float: - try: - return self.kraken_client.get_matic_usd_price() - except CannotGetPrice: - try: - return self.kucoin_client.get_matic_usd_price() - except CannotGetPrice: - return self.coingecko_client.get_matic_usd_price() - - def get_cronos_usd_price(self) -> float: - return self.kucoin_client.get_cro_usd_price() - - def get_xdc_usd_price(self) -> float: - return self.kucoin_client.get_xdc_usd_price() - - def get_ftm_usd_price(self) -> float: - return self.kucoin_client.get_ftm_usd_price() - - def get_kcs_usd_price(self) -> float: - try: - return self.kucoin_client.get_kcs_usd_price() - except CannotGetPrice: - return self.coingecko_client.get_kcs_usd_price() - - def get_mtr_usd_price(self) -> float: - return self.coingecko_client.get_mtr_usd_price() - - @cachedmethod(cache=operator.attrgetter("cache_ether_usd_price")) - @cache_memoize(60 * 30, prefix="balances-get_ether_usd_price") # 30 minutes - def get_ether_usd_price(self) -> float: - """ - :return: USD Price for Ether - """ - try: - return self.kraken_client.get_ether_usd_price() - except CannotGetPrice: - return self.kucoin_client.get_ether_usd_price() - - @cachedmethod(cache=operator.attrgetter("cache_native_coin_usd_price")) - @cache_memoize(60 * 30, prefix="balances-get_native_coin_usd_price") # 30 minutes - def get_native_coin_usd_price(self) -> float: - """ - Get USD price for native coin. It depends on the ethereum network: - - On mainnet, use ETH/USD - - On xDAI, use DAI/USD. - - On EWT/VOLTA, use EWT/USD - - ... - - :return: USD price for Ether - """ - if self.ethereum_network == EthereumNetwork.GNOSIS: - try: - return self.kraken_client.get_dai_usd_price() - except CannotGetPrice: - return 1 # DAI/USD should be close to 1 - elif self.ethereum_network in ( - EthereumNetwork.ENERGY_WEB_CHAIN, - EthereumNetwork.ENERGY_WEB_VOLTA_TESTNET, - ): - return self.get_ewt_usd_price() - elif self.ethereum_network in (EthereumNetwork.POLYGON, EthereumNetwork.MUMBAI): - return self.get_matic_usd_price() - elif self.ethereum_network == EthereumNetwork.BINANCE_SMART_CHAIN_MAINNET: - return self.get_binance_usd_price() - elif self.ethereum_network in ( - EthereumNetwork.GATHER_DEVNET_NETWORK, - EthereumNetwork.GATHER_TESTNET_NETWORK, - EthereumNetwork.GATHER_MAINNET_NETWORK, - ): - return self.coingecko_client.get_gather_usd_price() - elif self.ethereum_network == EthereumNetwork.AVALANCHE_C_CHAIN: - return self.get_avalanche_usd_price() - elif self.ethereum_network in ( - EthereumNetwork.MILKOMEDA_C1_TESTNET, - EthereumNetwork.MILKOMEDA_C1_MAINNET, - ): - return self.get_cardano_usd_price() - elif self.ethereum_network in ( - EthereumNetwork.AURORA_MAINNET, - EthereumNetwork.ARBITRUM_RINKEBY, - ): - return self.get_aurora_usd_price() - elif self.ethereum_network in ( - EthereumNetwork.CRONOS_TESTNET, - EthereumNetwork.CRONOS_MAINNET_BETA, - ): - return self.get_cronos_usd_price() - elif self.ethereum_network in ( - EthereumNetwork.FUSE_MAINNET, - EthereumNetwork.FUSE_SPARKNET, - ): - return self.coingecko_client.get_fuse_usd_price() - elif self.ethereum_network in ( - EthereumNetwork.KCC_MAINNET, - EthereumNetwork.KCC_TESTNET, - ): - return self.get_kcs_usd_price() - elif self.ethereum_network in ( - EthereumNetwork.METIS_ANDROMEDA_MAINNET, - EthereumNetwork.METIS_GOERLI_TESTNET, - EthereumNetwork.METIS_STARDUST_TESTNET, - ): - return self.coingecko_client.get_metis_usd_price() - elif self.ethereum_network in ( - EthereumNetwork.MILKOMEDA_A1_TESTNET, - EthereumNetwork.MILKOMEDA_A1_MAINNET, - ): - return self.get_algorand_usd_price() - elif self.ethereum_network in ( - EthereumNetwork.CELO_MAINNET, - EthereumNetwork.CELO_ALFAJORES_TESTNET, - EthereumNetwork.CELO_BAKLAVA_TESTNET, - ): - return self.kucoin_client.get_celo_usd_price() - elif self.ethereum_network in ( - EthereumNetwork.XINFIN_XDC_NETWORK, - EthereumNetwork.XDC_APOTHEM_NETWORK, - ): - return self.get_xdc_usd_price() - elif self.ethereum_network in ( - EthereumNetwork.METER_MAINNET, - EthereumNetwork.METER_TESTNET, - ): - return self.coingecko_client.get_mtr_usd_price() - elif self.ethereum_network in ( - EthereumNetwork.FANTOM_OPERA, - EthereumNetwork.FANTOM_TESTNET, - ): - return self.get_ftm_usd_price() - else: - return self.get_ether_usd_price() - - @cachedmethod(cache=operator.attrgetter("cache_token_eth_value")) - @cache_memoize(60 * 30, prefix="balances-get_token_eth_value") # 30 minutes - def get_token_eth_value(self, token_address: ChecksumAddress) -> float: - """ - Uses multiple decentralized and centralized oracles to get token prices - - :param token_address: - :return: Current ether value for a given `token_address` - """ - if token_address in ( - "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", # Used by some oracles - NULL_ADDRESS, - ): # Ether - return 1.0 - - for oracle in self.enabled_price_oracles: - try: - eth_value = oracle.get_price(token_address) - logger.info( - "Retrieved eth-value=%.4f for token-address=%s from %s", - eth_value, - token_address, - oracle.__class__.__name__, - ) - return eth_value - except OracleException: - logger.debug( - "Cannot get eth value for token-address=%s from %s", - token_address, - oracle.__class__.__name__, - ) - - # Try pool tokens - for oracle in self.enabled_price_pool_oracles: - try: - eth_value = oracle.get_pool_token_price(token_address) - logger.info( - "Retrieved eth-value=%.4f for token-address=%s from %s", - eth_value, - token_address, - oracle.__class__.__name__, - ) - return eth_value - except OracleException: - logger.debug( - "Cannot get eth value for token-address=%s from %s", - token_address, - oracle.__class__.__name__, - ) - - logger.warning("Cannot find eth value for token-address=%s", token_address) - return 0.0 - - @cachedmethod(cache=operator.attrgetter("cache_token_usd_value")) - @cache_memoize(60 * 30, prefix="balances-get_token_usd_price") # 30 minutes - def get_token_usd_price(self, token_address: ChecksumAddress) -> float: - """ - :param token_address: - :return: usd value for a given `token_address` using Coingecko - """ - if self.coingecko_client.supports_network(self.ethereum_network): - try: - return self.coingecko_client.get_token_price(token_address) - except CannotGetPrice: - pass - return 0.0 - - @cachedmethod(cache=operator.attrgetter("cache_underlying_token")) - @cache_memoize(60 * 30, prefix="balances-get_underlying_tokens") # 30 minutes - def get_underlying_tokens( - self, token_address: ChecksumAddress - ) -> Optional[List[UnderlyingToken]]: - """ - :param token_address: - :return: usd value for a given `token_address` using Curve, if not use Coingecko as last resource - """ - for oracle in self.enabled_composed_price_oracles: - try: - underlying_tokens = oracle.get_underlying_tokens(token_address) - logger.info( - "Retrieved underlying tokens %s for token-address=%s from %s", - underlying_tokens, - token_address, - oracle.__class__.__name__, - ) - return underlying_tokens - except OracleException: - logger.debug( - "Cannot get an underlying token for token-address=%s from %s", - token_address, - oracle.__class__.__name__, - ) - - def get_token_cached_eth_values( - self, token_addresses: Sequence[ChecksumAddress] - ) -> Iterator[EthValueWithTimestamp]: - """ - Get token eth prices with timestamp of calculation if ready on cache. If not, schedule tasks to do - the calculation so next time is available on cache and return `0.` and current datetime - - :param token_addresses: - :return: eth prices with timestamp if ready on cache, `0.` and None otherwise - """ - cache_keys = [ - f"price-service:{token_address}:eth-price" - for token_address in token_addresses - ] - results = self.redis.mget(cache_keys) # eth_value:epoch_timestamp - for token_address, cache_key, result in zip( - token_addresses, cache_keys, results - ): - if not token_address: # Ether, this will not be used - yield EthValueWithTimestamp( - 1.0, timezone.now() - ) # Even if not used, Ether value in ether is 1 :) - elif result: - yield EthValueWithTimestamp.from_string(result.decode()) - else: - task_result = calculate_token_eth_price_task.delay( - token_address, cache_key - ) - if task_result.ready(): - yield task_result.get() - else: - yield EthValueWithTimestamp(0.0, timezone.now()) - - def get_token_cached_usd_values( - self, token_addresses: Sequence[ChecksumAddress] - ) -> Iterator[FiatPriceWithTimestamp]: - """ - Get token usd prices with timestamp of calculation if ready on cache. - - :param token_addresses: - :return: eth prices with timestamp if ready on cache, `0.` and None otherwise - """ - try: - native_coin_usd_price = self.get_native_coin_usd_price() - except CannotGetPrice: - logger.warning("Cannot get Ether USD price", exc_info=True) - native_coin_usd_price = 0 - - for token_eth_values_with_timestamp in self.get_token_cached_eth_values( - token_addresses - ): - yield FiatPriceWithTimestamp( - native_coin_usd_price * token_eth_values_with_timestamp.eth_value, - FiatCode.USD, - token_eth_values_with_timestamp.timestamp, - ) - - def get_token_eth_price_from_oracles(self, token_address: ChecksumAddress) -> float: - """ - :param token_address - :return: Token/Ether price from oracles - """ - return ( - self.get_token_eth_value(token_address) - or self.get_token_usd_price(token_address) - / self.get_native_coin_usd_price() - ) - - def get_token_eth_price_from_composed_oracles( - self, token_address: ChecksumAddress - ) -> float: - """ - :param token_address - :return: Token/Ether price from composed oracles - """ - eth_price = 0 - if underlying_tokens := self.get_underlying_tokens(token_address): - for underlying_token in underlying_tokens: - # Find underlying token price and multiply by quantity - address = underlying_token.address - eth_price += ( - self.get_token_eth_price_from_oracles(address) - * underlying_token.quantity - ) - - return eth_price diff --git a/safe_transaction_service/tokens/signals.py b/safe_transaction_service/tokens/signals.py index d7b01b660..3a21b786c 100644 --- a/safe_transaction_service/tokens/signals.py +++ b/safe_transaction_service/tokens/signals.py @@ -11,7 +11,6 @@ ) from .models import Token -from .services import PriceServiceProvider logger = logging.getLogger(__name__) @@ -34,9 +33,3 @@ def clear_cache(sender: Type[Model], instance: Token, created: bool, **kwargs) - collectibles_service = CollectiblesServiceProvider() collectibles_service.cache_token_info.clear() - - price_service = PriceServiceProvider() - price_service.cache_token_eth_value.clear() - price_service.cache_token_info.clear() - price_service.cache_token_usd_value.clear() - price_service.cache_underlying_token.clear() diff --git a/safe_transaction_service/tokens/tasks.py b/safe_transaction_service/tokens/tasks.py index 051cf25ca..d9766deb7 100644 --- a/safe_transaction_service/tokens/tasks.py +++ b/safe_transaction_service/tokens/tasks.py @@ -1,21 +1,17 @@ -import random from dataclasses import dataclass from datetime import datetime from typing import Optional -from django.conf import settings from django.db import transaction from django.utils import timezone from celery import app from celery.utils.log import get_task_logger -from eth_typing import ChecksumAddress from gnosis.eth.ethereum_client import EthereumNetwork from gnosis.eth.utils import fast_to_checksum_address from safe_transaction_service.utils.ethereum import get_ethereum_network -from safe_transaction_service.utils.redis import get_redis from safe_transaction_service.utils.utils import close_gevent_db_connection_decorator from .exceptions import TokenListRetrievalException @@ -46,70 +42,6 @@ def __str__(self): return f"{self.eth_value}:{self.timestamp.timestamp()}" -@app.shared_task(soft_time_limit=TASK_SOFT_TIME_LIMIT, time_limit=TASK_TIME_LIMIT) -@close_gevent_db_connection_decorator -def calculate_token_eth_price_task( - token_address: ChecksumAddress, redis_key: str, force_recalculation: bool = False -) -> Optional[EthValueWithTimestamp]: - """ - Do price calculation for token in an async way and store it with its timestamp on redis - - :param token_address: Token address - :param redis_key: Redis key for token price - :param force_recalculation: Force a new calculation even if an old one is on cache - :return: token price (in ether) when calculated - """ - from .services.price_service import PriceServiceProvider - - redis = get_redis() - now = timezone.now() - current_timestamp = int(now.timestamp()) - key_was_set = redis.set( - redis_key, f"0:{current_timestamp}", ex=60 * 15, nx=True - ) # Expire in 15 minutes - # Only calculate the price if key was not set previously or if `force_recalculation` is `True` - if key_was_set or force_recalculation: - price_service = PriceServiceProvider() - eth_price = price_service.get_token_eth_price_from_oracles(token_address) - if not eth_price: - eth_price = price_service.get_token_eth_price_from_composed_oracles( - token_address - ) - - logger.debug("Calculated eth-price=%f for token=%s", eth_price, token_address) - if not eth_price: - logger.warning( - "Cannot calculate eth price for token=%s - Trying to use previous price", - token_address, - ) - last_redis_value = redis.get(redis_key) - if last_redis_value: - logger.warning("Using previous eth price for token=%s", token_address) - eth_price = EthValueWithTimestamp.from_string( - last_redis_value.decode() - ).eth_value - else: - logger.warning("Cannot calculate eth price for token=%s", token_address) - return EthValueWithTimestamp(eth_price, now) - - eth_value_with_timestamp = EthValueWithTimestamp(eth_price, now) - redis.setex( - redis_key, settings.TOKEN_ETH_PRICE_TTL, str(eth_value_with_timestamp) - ) - if not getattr(settings, "CELERY_ALWAYS_EAGER", False): - # Recalculate price before cache expires and prevents recursion checking Celery Eager property - # Use randint to prevent triggering all the tasks at the same time - calculate_token_eth_price_task.apply_async( - (token_address, redis_key), - {"force_recalculation": True}, - countdown=settings.TOKEN_ETH_PRICE_TTL - random.randint(60, 300), - ) - - return EthValueWithTimestamp(eth_price, now) - else: - return EthValueWithTimestamp.from_string(redis.get(redis_key).decode()) - - @app.shared_task(soft_time_limit=TASK_SOFT_TIME_LIMIT, time_limit=TASK_TIME_LIMIT) @close_gevent_db_connection_decorator def fix_pool_tokens_task() -> Optional[int]: diff --git a/safe_transaction_service/tokens/tests/clients/test_clients.py b/safe_transaction_service/tokens/tests/clients/test_clients.py deleted file mode 100644 index d83b34829..000000000 --- a/safe_transaction_service/tokens/tests/clients/test_clients.py +++ /dev/null @@ -1,88 +0,0 @@ -from unittest import mock - -from django.test import TestCase - -from requests import Session - -from gnosis.eth.tests.utils import just_test_if_mainnet_node - -from ...clients import CannotGetPrice, CoingeckoClient, KrakenClient, KucoinClient - - -class TestClients(TestCase): - def test_get_bnb_usd_price(self) -> float: - just_test_if_mainnet_node() - kucoin_client = KucoinClient() - coingecko_client = CoingeckoClient() - - price = kucoin_client.get_bnb_usd_price() - self.assertIsInstance(price, float) - self.assertGreater(price, 0) - - price = coingecko_client.get_bnb_usd_price() - self.assertIsInstance(price, float) - self.assertGreater(price, 0) - - def test_get_dai_usd_price_kraken(self) -> float: - just_test_if_mainnet_node() - kraken_client = KrakenClient() - - # Kraken is used - price = kraken_client.get_dai_usd_price() - self.assertIsInstance(price, float) - self.assertGreater(price, 0) - - def test_get_ether_usd_price_kraken(self): - just_test_if_mainnet_node() - kraken_client = KrakenClient() - - # Kraken is used - eth_usd_price = kraken_client.get_ether_usd_price() - self.assertIsInstance(eth_usd_price, float) - self.assertGreater(eth_usd_price, 0) - - def test_get_ewt_usd_price_kraken(self) -> float: - just_test_if_mainnet_node() - kraken_client = KrakenClient() - - # Kraken is used - price = kraken_client.get_ewt_usd_price() - self.assertIsInstance(price, float) - self.assertGreater(price, 0) - - def test_get_ether_usd_price_kucoin(self): - just_test_if_mainnet_node() - kucoin_client = KucoinClient() - - eth_usd_price = kucoin_client.get_ether_usd_price() - self.assertIsInstance(eth_usd_price, float) - self.assertGreater(eth_usd_price, 0) - - def test_get_matic_usd_price(self) -> float: - just_test_if_mainnet_node() - - for provider in [KucoinClient(), KrakenClient(), CoingeckoClient()]: - with self.subTest(provider=provider): - price = provider.get_matic_usd_price() - self.assertIsInstance(price, float) - self.assertGreater(price, 0) - - def test_get_ewt_usd_price_coingecko(self) -> float: - just_test_if_mainnet_node() - coingecko_client = CoingeckoClient() - - price = coingecko_client.get_ewt_usd_price() - self.assertIsInstance(price, float) - self.assertGreater(price, 0) - - def test_get_ewt_usd_price_kucoin(self) -> float: - just_test_if_mainnet_node() - kucoin_client = KucoinClient() - - price = kucoin_client.get_ewt_usd_price() - self.assertIsInstance(price, float) - self.assertGreater(price, 0) - - with mock.patch.object(Session, "get", side_effect=IOError("Connection Error")): - with self.assertRaises(CannotGetPrice): - kucoin_client.get_ewt_usd_price() diff --git a/safe_transaction_service/tokens/tests/clients/test_coingecko_client.py b/safe_transaction_service/tokens/tests/clients/test_coingecko_client.py index a08ae820e..00a799d0f 100644 --- a/safe_transaction_service/tokens/tests/clients/test_coingecko_client.py +++ b/safe_transaction_service/tokens/tests/clients/test_coingecko_client.py @@ -4,7 +4,6 @@ from safe_transaction_service.history.tests.utils import skip_on -from ...clients import CannotGetPrice from ...clients.coingecko_client import CoingeckoClient from ...clients.exceptions import CoingeckoRateLimitError @@ -13,38 +12,6 @@ class TestCoingeckoClient(TestCase): GNO_TOKEN_ADDRESS = "0x6810e776880C02933D47DB1b9fc05908e5386b96" GNO_GNOSIS_CHAIN_ADDRESS = "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb" - @skip_on(CannotGetPrice, reason="Cannot get price from Coingecko") - def test_coingecko_client(self): - self.assertTrue(CoingeckoClient.supports_network(EthereumNetwork.MAINNET)) - self.assertTrue( - CoingeckoClient.supports_network( - EthereumNetwork.BINANCE_SMART_CHAIN_MAINNET - ) - ) - self.assertTrue(CoingeckoClient.supports_network(EthereumNetwork.POLYGON)) - self.assertTrue(CoingeckoClient.supports_network(EthereumNetwork.GNOSIS)) - - # Test Mainnet - coingecko_client = CoingeckoClient() - non_existing_token_address = "0xda2f8b8386302C354a90DB670E40beA3563AF454" - self.assertGreater(coingecko_client.get_token_price(self.GNO_TOKEN_ADDRESS), 0) - with self.assertRaises(CannotGetPrice): - coingecko_client.get_token_price(non_existing_token_address) - - # Test Binance - bsc_coingecko_client = CoingeckoClient( - EthereumNetwork.BINANCE_SMART_CHAIN_MAINNET - ) - binance_peg_ethereum_address = "0x2170Ed0880ac9A755fd29B2688956BD959F933F8" - self.assertGreater( - bsc_coingecko_client.get_token_price(binance_peg_ethereum_address), 0 - ) - - # Test Polygon - polygon_coingecko_client = CoingeckoClient(EthereumNetwork.POLYGON) - bnb_pos_address = "0xb33EaAd8d922B1083446DC23f610c2567fB5180f" - self.assertGreater(polygon_coingecko_client.get_token_price(bnb_pos_address), 0) - @skip_on(CoingeckoRateLimitError, reason="Coingecko rate limit reached") def test_get_logo_url(self): # Test Mainnet diff --git a/safe_transaction_service/tokens/tests/test_price_service.py b/safe_transaction_service/tokens/tests/test_price_service.py deleted file mode 100644 index 8e4c519eb..000000000 --- a/safe_transaction_service/tokens/tests/test_price_service.py +++ /dev/null @@ -1,305 +0,0 @@ -from unittest import mock -from unittest.mock import MagicMock - -from django.test import TestCase - -from eth_account import Account - -from gnosis.eth import EthereumClient, EthereumClientProvider, EthereumNetwork -from gnosis.eth.oracles import KyberOracle, OracleException, UnderlyingToken - -from safe_transaction_service.history.tests.utils import just_test_if_mainnet_node -from safe_transaction_service.utils.redis import get_redis - -from ..clients import CannotGetPrice, CoingeckoClient, KrakenClient, KucoinClient -from ..services.price_service import PriceService, PriceServiceProvider - - -class TestPriceService(TestCase): - @classmethod - def setUpClass(cls) -> None: - cls.redis = get_redis() - cls.ethereum_client = EthereumClientProvider() - - @classmethod - def tearDownClass(cls) -> None: - PriceServiceProvider.del_singleton() - - def setUp(self) -> None: - self.price_service = PriceServiceProvider() - - def tearDown(self) -> None: - PriceServiceProvider.del_singleton() - - def test_available_price_oracles(self): - # Ganache should have no oracle enabled - self.assertEqual(len(self.price_service.enabled_price_oracles), 0) - self.assertEqual(len(self.price_service.enabled_price_pool_oracles), 0) - self.assertEqual(len(self.price_service.enabled_composed_price_oracles), 0) - - def test_available_price_oracles_mainnet(self): - # Mainnet should have every oracle enabled - mainnet_node = just_test_if_mainnet_node() - price_service = PriceService(EthereumClient(mainnet_node), self.redis) - self.assertEqual(len(price_service.enabled_price_oracles), 6) - self.assertEqual(len(price_service.enabled_price_pool_oracles), 3) - self.assertEqual(len(price_service.enabled_composed_price_oracles), 4) - - @mock.patch.object(KrakenClient, "get_ether_usd_price", return_value=0.4) - @mock.patch.object(KucoinClient, "get_ether_usd_price", return_value=0.5) - def test_get_ether_usd_price(self, kucoin_mock: MagicMock, kraken_mock: MagicMock): - price_service = self.price_service - eth_usd_price = price_service.get_ether_usd_price() - self.assertEqual(eth_usd_price, kraken_mock.return_value) - kucoin_mock.assert_not_called() - - kraken_mock.side_effect = CannotGetPrice - - # cache_ether_usd_price is working - eth_usd_price = price_service.get_native_coin_usd_price() - self.assertEqual(eth_usd_price, kraken_mock.return_value) - - # Clear cache_ether_usd_price - price_service.cache_ether_usd_price.clear() - self.assertEqual(eth_usd_price, kraken_mock.return_value) - kucoin_mock.assert_not_called() - - def test_get_native_coin_usd_price(self): - price_service = self.price_service - - # Unsupported network (Ganache) - with mock.patch.object( - KrakenClient, "get_ether_usd_price", return_value=1_600 - ) as kraken_mock: - price_service.cache_native_coin_usd_price.clear() - self.assertEqual(price_service.get_native_coin_usd_price(), 1_600) - - # Test cache is working - kraken_mock.side_effect = CannotGetPrice - self.assertEqual(price_service.get_native_coin_usd_price(), 1_600) - - # Gnosis Chain - price_service.ethereum_network = EthereumNetwork.GNOSIS - with mock.patch.object(KrakenClient, "get_dai_usd_price", return_value=1.5): - price_service.cache_native_coin_usd_price.clear() - self.assertEqual(price_service.get_native_coin_usd_price(), 1.5) - - with mock.patch.object( - KrakenClient, "get_dai_usd_price", side_effect=CannotGetPrice - ): - price_service.cache_native_coin_usd_price.clear() - self.assertEqual(price_service.get_native_coin_usd_price(), 1) - - # POLYGON - price_service.ethereum_network = EthereumNetwork.POLYGON - with mock.patch.object(KrakenClient, "get_matic_usd_price", return_value=0.7): - price_service.cache_native_coin_usd_price.clear() - self.assertEqual(price_service.get_native_coin_usd_price(), 0.7) - - # EWT - price_service.ethereum_network = EthereumNetwork.ENERGY_WEB_CHAIN - with mock.patch.object(KrakenClient, "get_ewt_usd_price", return_value=0.9): - price_service.cache_native_coin_usd_price.clear() - self.assertEqual(price_service.get_native_coin_usd_price(), 0.9) - - # BINANCE - price_service.ethereum_network = EthereumNetwork.BINANCE_SMART_CHAIN_MAINNET - with mock.patch.object(KucoinClient, "get_bnb_usd_price", return_value=1.2): - price_service.cache_native_coin_usd_price.clear() - self.assertEqual(price_service.get_native_coin_usd_price(), 1.2) - - # Gather - price_service.ethereum_network = EthereumNetwork.GATHER_MAINNET_NETWORK - with mock.patch.object( - CoingeckoClient, "get_gather_usd_price", return_value=1.7 - ): - price_service.cache_native_coin_usd_price.clear() - self.assertEqual(price_service.get_native_coin_usd_price(), 1.7) - - # Avalanche - price_service.ethereum_network = EthereumNetwork.AVALANCHE_C_CHAIN - with mock.patch.object(KrakenClient, "get_avax_usd_price", return_value=6.5): - price_service.cache_native_coin_usd_price.clear() - self.assertEqual(price_service.get_native_coin_usd_price(), 6.5) - - # Aurora - price_service.ethereum_network = EthereumNetwork.AURORA_MAINNET - with mock.patch.object(KucoinClient, "get_aurora_usd_price", return_value=1.3): - price_service.cache_native_coin_usd_price.clear() - self.assertEqual(price_service.get_native_coin_usd_price(), 1.3) - - # Cronos - with mock.patch.object(KucoinClient, "get_cro_usd_price", return_value=4.4): - price_service.ethereum_network = EthereumNetwork.CRONOS_MAINNET_BETA - price_service.cache_native_coin_usd_price.clear() - self.assertEqual(price_service.get_native_coin_usd_price(), 4.4) - - # KuCoin - with mock.patch.object(KucoinClient, "get_kcs_usd_price", return_value=4.4): - price_service.ethereum_network = EthereumNetwork.KCC_MAINNET - price_service.cache_native_coin_usd_price.clear() - self.assertEqual(price_service.get_native_coin_usd_price(), 4.4) - - # Milkomeda Cardano - with mock.patch.object(KrakenClient, "get_ada_usd_price", return_value=5.5): - price_service.ethereum_network = EthereumNetwork.MILKOMEDA_C1_MAINNET - price_service.cache_native_coin_usd_price.clear() - self.assertEqual(price_service.get_native_coin_usd_price(), 5.5) - - # Milkomeda Algorand - with mock.patch.object(KrakenClient, "get_algo_usd_price", return_value=6.6): - price_service.ethereum_network = EthereumNetwork.MILKOMEDA_A1_MAINNET - price_service.cache_native_coin_usd_price.clear() - self.assertEqual(price_service.get_algorand_usd_price(), 6.6) - - # XDC - with mock.patch.object(KucoinClient, "get_xdc_usd_price", return_value=7.7): - price_service.ethereum_network = EthereumNetwork.XINFIN_XDC_NETWORK - price_service.cache_native_coin_usd_price.clear() - self.assertEqual(price_service.get_xdc_usd_price(), 7.7) - - price_service.ethereum_network = EthereumNetwork.XDC_APOTHEM_NETWORK - price_service.cache_native_coin_usd_price.clear() - self.assertEqual(price_service.get_xdc_usd_price(), 7.7) - - # Meter - with mock.patch.object(CoingeckoClient, "get_mtr_usd_price", return_value=8.0): - price_service.ethereum_network = EthereumNetwork.METER_MAINNET - price_service.cache_native_coin_usd_price.clear() - self.assertEqual(price_service.get_mtr_usd_price(), 8.0) - - price_service.ethereum_network = EthereumNetwork.METER_TESTNET - price_service.cache_native_coin_usd_price.clear() - self.assertEqual(price_service.get_mtr_usd_price(), 8.0) - - @mock.patch.object(CoingeckoClient, "get_bnb_usd_price", return_value=3.0) - @mock.patch.object(KucoinClient, "get_bnb_usd_price", return_value=5.0) - def test_get_binance_usd_price( - self, - get_bnb_usd_price_binance_mock: MagicMock, - get_bnb_usd_price_coingecko: MagicMock, - ): - price_service = self.price_service - - price = price_service.get_binance_usd_price() - self.assertEqual(price, 5.0) - - get_bnb_usd_price_binance_mock.side_effect = CannotGetPrice - price = price_service.get_binance_usd_price() - self.assertEqual(price, 3.0) - - @mock.patch.object(CoingeckoClient, "get_ewt_usd_price", return_value=3.0) - @mock.patch.object(KucoinClient, "get_ewt_usd_price", return_value=7.0) - @mock.patch.object(KrakenClient, "get_ewt_usd_price", return_value=5.0) - def test_get_ewt_usd_price( - self, - get_ewt_usd_price_kraken_mock: MagicMock, - get_ewt_usd_price_kucoin_mock: MagicMock, - get_ewt_usd_price_coingecko_mock: MagicMock, - ): - price_service = self.price_service - - price = price_service.get_ewt_usd_price() - self.assertEqual(price, 5.0) - - get_ewt_usd_price_kraken_mock.side_effect = CannotGetPrice - price = price_service.get_ewt_usd_price() - self.assertEqual(price, 7.0) - - get_ewt_usd_price_kucoin_mock.side_effect = CannotGetPrice - price = price_service.get_ewt_usd_price() - self.assertEqual(price, 3.0) - - @mock.patch.object(CoingeckoClient, "get_matic_usd_price", return_value=3.0) - @mock.patch.object(KucoinClient, "get_matic_usd_price", return_value=7.0) - @mock.patch.object(KrakenClient, "get_matic_usd_price", return_value=5.0) - def test_get_matic_usd_price( - self, - get_matic_usd_price_kraken_mock: MagicMock, - get_matic_usd_price_binance_mock: MagicMock, - get_matic_usd_price_coingecko_mock: MagicMock, - ): - price_service = self.price_service - - price = price_service.get_matic_usd_price() - self.assertEqual(price, 5.0) - - get_matic_usd_price_kraken_mock.side_effect = CannotGetPrice - price = price_service.get_matic_usd_price() - self.assertEqual(price, 7.0) - - get_matic_usd_price_binance_mock.side_effect = CannotGetPrice - price = price_service.get_matic_usd_price() - self.assertEqual(price, 3.0) - - def test_get_token_eth_value(self): - mainnet_node = just_test_if_mainnet_node() - price_service = PriceService(EthereumClient(mainnet_node), self.redis) - gno_token_address = "0x6810e776880C02933D47DB1b9fc05908e5386b96" - token_eth_value = price_service.get_token_eth_value(gno_token_address) - self.assertIsInstance(token_eth_value, float) - self.assertGreater(token_eth_value, 0) - - @mock.patch.object(KyberOracle, "get_price", return_value=1.23, autospec=True) - def test_get_token_eth_value_mocked(self, kyber_get_price_mock: MagicMock): - price_service = self.price_service - oracle_1 = mock.MagicMock() - oracle_1.get_price.return_value = 1.23 - oracle_2 = mock.MagicMock() - oracle_3 = mock.MagicMock() - price_service.enabled_price_oracles = (oracle_1, oracle_2, oracle_3) - self.assertEqual(len(price_service.enabled_price_oracles), 3) - random_address = Account.create().address - self.assertEqual(len(price_service.cache_token_eth_value), 0) - - self.assertEqual(price_service.get_token_eth_value(random_address), 1.23) - self.assertEqual(price_service.cache_token_eth_value[(random_address,)], 1.23) - - # Make every oracle fail - oracle_1.get_price.side_effect = OracleException - oracle_2.get_price.side_effect = OracleException - oracle_3.get_price.side_effect = OracleException - - # Check cache - self.assertEqual(price_service.get_token_eth_value(random_address), 1.23) - random_address_2 = Account.create().address - self.assertEqual(price_service.get_token_eth_value(random_address_2), 0.0) - self.assertEqual(price_service.cache_token_eth_value[(random_address,)], 1.23) - self.assertEqual(price_service.cache_token_eth_value[(random_address_2,)], 0.0) - - @mock.patch.object( - PriceService, "get_underlying_tokens", return_value=[], autospec=True - ) - @mock.patch.object( - PriceService, "get_token_eth_value", autospec=True, return_value=1.0 - ) - def test_get_token_eth_price_from_composed_oracles( - self, get_token_eth_value_mock: MagicMock, price_service_mock: MagicMock - ): - price_service = self.price_service - token_one = UnderlyingToken("0x48f07301E9E29c3C38a80ae8d9ae771F224f1054", 0.482) - token_two = UnderlyingToken("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 0.376) - token_three = UnderlyingToken("0xA0b86991c6218b36c1d19D4a2e9Eb0cE360", 0.142) - price_service_mock.return_value = [token_one, token_two, token_three] - curve_price = "0xe7ce624c00381b4b7abb03e633fb4acac4537dd6" - eth_price = price_service.get_token_eth_price_from_composed_oracles(curve_price) - self.assertEqual(eth_price, 1.0) - - def test_get_token_eth_price_from_oracles(self): - mainnet_node = just_test_if_mainnet_node() - price_service = PriceService(EthereumClient(mainnet_node), self.redis) - gno_token_address = "0x6810e776880C02933D47DB1b9fc05908e5386b96" - token_eth_value = price_service.get_token_eth_price_from_oracles( - gno_token_address - ) - self.assertIsInstance(token_eth_value, float) - self.assertGreater(token_eth_value, 0) - with mock.patch.object( - PriceService, "get_token_eth_value", autospec=True, return_value=0 - ): - token_eth_value_from_coingecko = ( - price_service.get_token_eth_price_from_oracles(gno_token_address) - ) - self.assertAlmostEqual( - token_eth_value, token_eth_value_from_coingecko, delta=0.1 - ) diff --git a/safe_transaction_service/tokens/tests/test_tasks.py b/safe_transaction_service/tokens/tests/test_tasks.py index 96d099a2d..0070a709b 100644 --- a/safe_transaction_service/tokens/tests/test_tasks.py +++ b/safe_transaction_service/tokens/tests/test_tasks.py @@ -3,26 +3,12 @@ from unittest.mock import MagicMock from django.test import TestCase -from django.utils import timezone -from eth_account import Account +from gnosis.eth.ethereum_client import EthereumNetwork -from gnosis.eth.ethereum_client import ( - EthereumClient, - EthereumClientProvider, - EthereumNetwork, -) - -from ...history.tests.utils import just_test_if_mainnet_node from ...utils.redis import get_redis from ..models import TokenList -from ..services import PriceService, PriceServiceProvider -from ..tasks import ( - EthValueWithTimestamp, - calculate_token_eth_price_task, - fix_pool_tokens_task, - update_token_info_from_token_list_task, -) +from ..tasks import fix_pool_tokens_task, update_token_info_from_token_list_task from .factories import TokenFactory, TokenListFactory from .mocks import token_list_mock @@ -31,7 +17,6 @@ class TestTasks(TestCase): def setUp(self) -> None: - PriceServiceProvider.del_singleton() get_redis().flushall() def tearDown(self) -> None: @@ -47,99 +32,6 @@ def test_fix_pool_tokens_task(self, get_network_mock: MagicMock): get_network_mock.return_value = EthereumNetwork.RINKEBY self.assertIsNone(fix_pool_tokens_task.delay().result) - @mock.patch.object( - PriceService, "get_token_eth_value", autospec=True, return_value=4815 - ) - @mock.patch.object(timezone, "now", return_value=timezone.now()) - def test_calculate_token_eth_price_task( - self, timezone_now_mock: MagicMock, get_token_eth_value_mock: MagicMock - ): - random_token_address = Account.create().address - random_redis_key = Account.create().address - expected = EthValueWithTimestamp( - get_token_eth_value_mock.return_value, timezone_now_mock.return_value - ) - self.assertEqual( - calculate_token_eth_price_task.delay( - random_token_address, random_redis_key - ).result, - expected, - ) - - # Check caching works even if we change the token_address - another_token_address = Account.create().address - self.assertEqual( - calculate_token_eth_price_task.delay( - another_token_address, random_redis_key - ).result, - expected, - ) - - with self.settings(CELERY_ALWAYS_EAGER=False): - random_token_address = Account.create().address - random_redis_key = Account.create().address - calculate_token_eth_price_task.delay(random_token_address, random_redis_key) - - def test_calculate_token_eth_price_task_without_mock(self): - mainnet_node_url = just_test_if_mainnet_node() - EthereumClientProvider.instance = EthereumClient(mainnet_node_url) - - dai_address = "0x6B175474E89094C44Da98b954EedeAC495271d0F" - random_redis_key = Account.create().address - eth_value_with_timestamp = calculate_token_eth_price_task( - dai_address, random_redis_key - ) - self.assertGreater(eth_value_with_timestamp.eth_value, 0.0) - - pool_together_address = "0x334cBb5858417Aee161B53Ee0D5349cCF54514CF" - random_redis_key = Account.create().address - eth_value_with_timestamp = calculate_token_eth_price_task( - pool_together_address, random_redis_key - ) - self.assertGreater(eth_value_with_timestamp.eth_value, 0.0) - - random_token_address = Account.create().address - random_redis_key = Account.create().address - eth_value_with_timestamp = calculate_token_eth_price_task( - random_token_address, random_redis_key - ) - self.assertEqual(eth_value_with_timestamp.eth_value, 0.0) - del EthereumClientProvider.instance - - @mock.patch.object( - PriceService, "get_token_eth_value", autospec=True, return_value=4815 - ) - @mock.patch.object( - PriceService, "get_token_usd_price", autospec=True, return_value=0.0 - ) - @mock.patch.object(timezone, "now", return_value=timezone.now()) - def test_return_last_valid_token_price( - self, - timezone_now_mock: MagicMock, - get_token_usd_price: MagicMock, - get_token_eth_value_mock: MagicMock, - ): - random_token_address = Account.create().address - random_redis_key = Account.create().address - expected = EthValueWithTimestamp( - get_token_eth_value_mock.return_value, timezone_now_mock.return_value - ) - self.assertEqual( - calculate_token_eth_price_task.delay( - random_token_address, random_redis_key - ).result, - expected, - ) - - get_token_eth_value_mock.return_value = 0.0 - - self.assertEqual( - calculate_token_eth_price_task.delay( - random_token_address, random_redis_key, True - ).result, - expected, - ) - @mock.patch( "safe_transaction_service.tokens.tasks.get_ethereum_network", return_value=EthereumNetwork.MAINNET, diff --git a/safe_transaction_service/tokens/tests/test_views.py b/safe_transaction_service/tokens/tests/test_views.py index 7976ec5a3..d0e852ec8 100644 --- a/safe_transaction_service/tokens/tests/test_views.py +++ b/safe_transaction_service/tokens/tests/test_views.py @@ -3,7 +3,6 @@ from unittest.mock import MagicMock from django.urls import reverse -from django.utils import timezone from eth_account import Account from rest_framework import status @@ -13,10 +12,7 @@ from gnosis.eth.ethereum_client import Erc20Manager, InvalidERC20Info from gnosis.safe.tests.safe_test_case import SafeTestCaseMixin -from ..clients import CannotGetPrice from ..models import Token -from ..services import PriceService -from ..services.price_service import FiatCode, FiatPriceWithTimestamp from .factories import TokenFactory logger = logging.getLogger(__name__) @@ -102,104 +98,3 @@ def test_tokens_view(self): } ], ) - - def test_token_price_view(self): - invalid_address = "0x1234" - response = self.client.get( - reverse("v1:tokens:price-usd", args=(invalid_address,)) - ) - self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) - - random_address = Account.create().address - response = self.client.get( - reverse("v1:tokens:price-usd", args=(random_address,)) - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertEqual( - response.data, - {"detail": ErrorDetail(string="Not found.", code="not_found")}, - ) - - token = TokenFactory(address=random_address, decimals=18) # ERC20 - response = self.client.get( - reverse("v1:tokens:price-usd", args=(token.address,)) - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["fiat_code"], "USD") - self.assertEqual(response.data["fiat_price"], "0.0") - self.assertTrue(response.data["timestamp"]) - - fiat_price_with_timestamp = FiatPriceWithTimestamp( - 48.1516, FiatCode.USD, timezone.now() - ) - with mock.patch.object( - PriceService, - "get_token_cached_usd_values", - autospec=True, - return_value=iter([fiat_price_with_timestamp]), - ) as get_token_cached_usd_values_mock: - response = self.client.get( - reverse("v1:tokens:price-usd", args=(token.address,)) - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["fiat_code"], "USD") - self.assertEqual( - response.data["fiat_price"], str(fiat_price_with_timestamp.fiat_price) - ) - self.assertTrue(response.data["timestamp"]) - self.assertEqual( - get_token_cached_usd_values_mock.call_args.args[1], [token.address] - ) - - # Test copy price address - get_token_cached_usd_values_mock.return_value = iter( - [fiat_price_with_timestamp] - ) - token.copy_price = Account.create().address - token.save(update_fields=["copy_price"]) - response = self.client.get( - reverse("v1:tokens:price-usd", args=(token.address,)) - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["fiat_code"], "USD") - self.assertEqual( - response.data["fiat_price"], str(fiat_price_with_timestamp.fiat_price) - ) - self.assertTrue(response.data["timestamp"]) - self.assertEqual( - get_token_cached_usd_values_mock.call_args.args[1], [token.copy_price] - ) - - @mock.patch.object( - PriceService, "get_native_coin_usd_price", return_value=321.2, autospec=True - ) - def test_token_price_view_address_0( - self, get_native_coin_usd_price_mock: MagicMock - ): - token_address = "0x0000000000000000000000000000000000000000" - - response = self.client.get( - reverse("v1:tokens:price-usd", args=(token_address,)) - ) - - # Native token should be retrieved even if it is not part of the Token table - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["fiat_code"], "USD") - self.assertEqual(response.data["fiat_price"], "321.2") - self.assertTrue(response.data["timestamp"]) - - @mock.patch.object( - PriceService, - "get_native_coin_usd_price", - side_effect=CannotGetPrice(), - ) - def test_token_price_view_error(self, get_native_coin_usd_price_mock: MagicMock): - token_address = "0x0000000000000000000000000000000000000000" - - response = self.client.get( - reverse("v1:tokens:price-usd", args=(token_address,)) - ) - - self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE) - self.assertEqual(response.data["message"], "Price retrieval failed") - self.assertEqual(response.data["arguments"], [token_address]) diff --git a/safe_transaction_service/tokens/urls.py b/safe_transaction_service/tokens/urls.py index 5b636d17e..60e46f12e 100644 --- a/safe_transaction_service/tokens/urls.py +++ b/safe_transaction_service/tokens/urls.py @@ -7,5 +7,4 @@ urlpatterns = [ path("", views.TokensView.as_view(), name="list"), path("/", views.TokenView.as_view(), name="detail"), - path("/prices/usd/", views.TokenPriceView.as_view(), name="price-usd"), ] diff --git a/safe_transaction_service/tokens/views.py b/safe_transaction_service/tokens/views.py index 08a8d3af4..006fface1 100644 --- a/safe_transaction_service/tokens/views.py +++ b/safe_transaction_service/tokens/views.py @@ -1,21 +1,15 @@ -from django.utils import timezone from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page import django_filters.rest_framework -from drf_yasg.utils import swagger_auto_schema from rest_framework import response, status from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.generics import ListAPIView, RetrieveAPIView -from rest_framework.response import Response -from gnosis.eth.constants import NULL_ADDRESS from gnosis.eth.utils import fast_is_checksum_address from . import filters, serializers -from .clients import CannotGetPrice from .models import Token -from .services import PriceServiceProvider class TokenView(RetrieveAPIView): @@ -55,59 +49,3 @@ class TokensView(ListAPIView): @method_decorator(cache_page(60 * 15)) # Cache 15 minutes def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) - - -class TokenPriceView(RetrieveAPIView): - serializer_class = serializers.TokenPriceResponseSerializer - lookup_field = "address" - queryset = Token.objects.all() - - @swagger_auto_schema( - deprecated=True, - operation_description="Eth/token pricing features will be removed at 30/11/2023", - ) - @method_decorator(cache_page(60 * 10)) # Cache 10 minutes - def get(self, request, *args, **kwargs): - address = self.kwargs["address"] - if not fast_is_checksum_address(address): - return response.Response( - status=status.HTTP_422_UNPROCESSABLE_ENTITY, - data={ - "code": 1, - "message": "Invalid ethereum address", - "arguments": [address], - }, - ) - try: - price_service = PriceServiceProvider() - if address == NULL_ADDRESS: - data = { - "fiat_code": "USD", - "fiat_price": str(price_service.get_native_coin_usd_price()), - "timestamp": timezone.now(), - } - else: - token = self.get_object() # Raises 404 if not found - fiat_price_with_timestamp = next( - price_service.get_token_cached_usd_values( - [token.get_price_address()] - ) - ) - data = { - "fiat_code": fiat_price_with_timestamp.fiat_code.name, - "fiat_price": str(fiat_price_with_timestamp.fiat_price), - "timestamp": fiat_price_with_timestamp.timestamp, - } - serializer = self.get_serializer(data=data) - assert serializer.is_valid() - return Response(status=status.HTTP_200_OK, data=serializer.data) - - except CannotGetPrice: - return Response( - status=status.HTTP_503_SERVICE_UNAVAILABLE, - data={ - "code": 10, - "message": "Price retrieval failed", - "arguments": [address], - }, - )