def __init__(self, ethereum_client: EthereumClient, uniswap_factory_address: str, kyber_network_proxy_address: str): self.ethereum_client = ethereum_client self.uniswap_oracle = UniswapOracle(self.ethereum_client, uniswap_factory_address) self.kyber_oracle = KyberOracle(self.ethereum_client, kyber_network_proxy_address) self.cache_eth_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_info = {}
def get_price(self, ticker: str) -> float: """ :param ticker: Address of the token :return: price """ ethereum_client = EthereumClientProvider() kyber = KyberOracle(ethereum_client, self.kyber_network_proxy_address) try: return kyber.get_price(ticker) except OracleException as e: raise CannotGetTokenPriceFromApi from e
def __init__(self, ethereum_client: EthereumClient, redis: Redis): self.ethereum_client = ethereum_client self.redis = redis self.binance_client = BinanceClient() self.coingecko_client = CoingeckoClient() self.curve_oracle = CurveOracle( self.ethereum_client) # Curve returns price in usd self.kraken_client = KrakenClient() self.kucoin_client = KucoinClient() self.kyber_oracle = KyberOracle(self.ethereum_client) self.sushiswap_oracle = SushiswapOracle(self.ethereum_client) self.uniswap_oracle = UniswapOracle(self.ethereum_client) self.uniswap_v2_oracle = UniswapV2Oracle(self.ethereum_client) self.yearn_oracle = YearnOracle(self.ethereum_client) self.balancer_oracle = BalancerOracle(self.ethereum_client, self.uniswap_v2_oracle) self.mooniswap_oracle = MooniswapOracle(self.ethereum_client, self.uniswap_v2_oracle) self.cache_eth_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_token_info = {}
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.binance_client = BinanceClient() self.coingecko_client = CoingeckoClient(self.ethereum_network) self.curve_oracle = CurveOracle(self.ethereum_client) self.kraken_client = KrakenClient() self.kucoin_client = KucoinClient() self.kyber_oracle = KyberOracle(self.ethereum_client) self.sushiswap_oracle = SushiswapOracle(self.ethereum_client) self.uniswap_oracle = UniswapOracle(self.ethereum_client) self.uniswap_v2_oracle = UniswapV2Oracle(self.ethereum_client) self.pool_together_oracle = PoolTogetherOracle(self.ethereum_client) self.yearn_oracle = YearnOracle(self.ethereum_client) self.enzyme_oracle = EnzymeOracle(self.ethereum_client) self.aave_oracle = AaveOracle(self.ethereum_client, self.uniswap_v2_oracle) self.balancer_oracle = BalancerOracle(self.ethereum_client, self.uniswap_v2_oracle) self.mooniswap_oracle = MooniswapOracle(self.ethereum_client, self.uniswap_v2_oracle) self.cache_eth_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 = {}
class BalanceService: def __init__(self, ethereum_client: EthereumClient, uniswap_factory_address: str, kyber_network_proxy_address: str): self.ethereum_client = ethereum_client self.uniswap_oracle = UniswapOracle(self.ethereum_client, uniswap_factory_address) self.kyber_oracle = KyberOracle(self.ethereum_client, kyber_network_proxy_address) self.cache_eth_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_info = {} def get_balances(self, safe_address: str) -> List[Balance]: """ :param safe_address: :return: `{'token_address': str, 'balance': int}`. For ether, `token_address` is `None` """ assert Web3.isChecksumAddress( safe_address ), f'Not valid address {safe_address} for getting balances' erc20_addresses = list( EthereumEvent.objects.erc20_tokens_used_by_address(safe_address)) raw_balances = self.ethereum_client.erc20.get_balances( safe_address, erc20_addresses) balances = [] for balance in raw_balances: if not balance['token_address']: # Ether balance['token'] = None elif balance['balance'] > 0: balance['token'] = self.get_token_info( balance['token_address']) if not balance[ 'token']: # Ignore ERC20 tokens that cannot be queried continue else: continue balances.append(Balance(**balance)) return balances def get_eth_usd_price_binance(self) -> float: """ :return: current USD price for ethereum using Kraken :raises: CannotGetEthereumPrice """ url = 'https://api.binance.com/api/v3/avgPrice?symbol=ETHUSDT' response = requests.get(url) api_json = response.json() if not response.ok: logger.warning('Cannot get price from url=%s', url) raise CannotGetEthereumPrice(api_json.get('msg')) try: price = float(api_json['price']) if not price: raise CannotGetEthereumPrice( f'Price from url={url} is {price}') return price except ValueError as e: raise CannotGetEthereumPrice from e def get_eth_usd_price_kraken(self) -> float: """ :return: current USD price for ethereum using Kraken :raises: CannotGetEthereumPrice """ # Use kraken for eth_value url = 'https://api.kraken.com/0/public/Ticker?pair=ETHUSD' response = requests.get(url) 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 CannotGetEthereumPrice(str(api_json['error'])) try: result = api_json['result'] for new_ticker in result: price = float(result[new_ticker]['c'][0]) if not price: raise CannotGetEthereumPrice( f'Price from url={url} is {price}') return price except ValueError as e: raise CannotGetEthereumPrice from e @cachedmethod(cache=operator.attrgetter('cache_eth_usd_price')) def get_eth_usd_price(self) -> float: try: return self.get_eth_usd_price_kraken() except CannotGetEthereumPrice: return self.get_eth_usd_price_binance() @cachedmethod(cache=operator.attrgetter('cache_token_eth_value')) def get_token_eth_value(self, token_address: str) -> float: """ Return current ether value for a given `token_address` """ try: return self.uniswap_oracle.get_price(token_address) except OracleException: logger.warning( 'Cannot get eth value for token-address=%s on uniswap, trying Kyber', token_address) try: return self.kyber_oracle.get_price(token_address) except OracleException: logger.warning( 'Cannot get eth value for token-address=%s from Kyber', token_address) return 0. @cachedmethod(cache=operator.attrgetter('cache_token_info')) def get_token_info(self, token_address: str) -> Optional[Erc20InfoWithLogo]: try: erc20_info = self.ethereum_client.erc20.get_info(token_address) return Erc20InfoWithLogo(token_address, erc20_info.name, erc20_info.symbol, erc20_info.decimals) except InvalidERC20Info: logger.warning('Cannot get token info for token-address=%s', token_address) return None def get_usd_balances(self, safe_address: str) -> List[BalanceWithUsd]: """ All this could be more optimal (e.g. batching requests), but as everything is cached I think we should be alright """ balances: List[Balance] = self.get_balances(safe_address) eth_value = self.get_eth_usd_price() balances_with_usd = [] for balance in balances: token_address = balance.token_address if not token_address: # Ether balance_usd = eth_value * (balance.balance / 10**18) else: token_to_eth_price = self.get_token_eth_value(token_address) if token_to_eth_price: balance_with_decimals = balance.balance / 10**balance.token.decimals balance_usd = eth_value * token_to_eth_price * balance_with_decimals else: balance_usd = 0. balances_with_usd.append( BalanceWithUsd(balance.token_address, balance.token, balance.balance, round(balance_usd, 4))) return balances_with_usd
class BalanceService: def __init__(self, ethereum_client: EthereumClient, uniswap_factory_address: str, kyber_network_proxy_address: str): self.ethereum_client = ethereum_client self.uniswap_oracle = UniswapOracle(self.ethereum_client, uniswap_factory_address) self.uniswap_v2_oracle = UniswapV2Oracle(self.ethereum_client) self.kyber_oracle = KyberOracle(self.ethereum_client, kyber_network_proxy_address) self.cache_eth_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_info = {} @cached_property def ethereum_network(self): return self.ethereum_client.get_network() def _filter_addresses(self, erc20_addresses: Sequence[str], only_trusted: bool, exclude_spam: bool) -> List[str]: """ :param erc20_addresses: :param only_trusted: :param exclude_spam: :return: ERC20 tokens filtered by spam or trusted """ base_queryset = Token.objects.filter( address__in=erc20_addresses).values_list( 'address', flat=True).order_by('name') if only_trusted: addresses = list(base_queryset.filter(trusted=True)) elif exclude_spam: addresses = list(base_queryset.filter(spam=False)) else: addresses = list(base_queryset) # Add missing tokens not on database for erc20_address in erc20_addresses: if erc20_address not in addresses: addresses.append(erc20_address) return addresses def get_balances(self, safe_address: str, only_trusted: bool = False, exclude_spam: bool = False) -> List[Balance]: """ :param safe_address: :param only_trusted: If True, return balance only for trusted tokens :param exclude_spam: If True, exclude spam tokens :return: `{'token_address': str, 'balance': int}`. For ether, `token_address` is `None` """ assert Web3.isChecksumAddress( safe_address ), f'Not valid address {safe_address} for getting balances' all_erc20_addresses = list( EthereumEvent.objects.erc20_tokens_used_by_address(safe_address)) for address in all_erc20_addresses: # Store tokens in database if not present self.get_token_info(address) # This is cached erc20_addresses = self._filter_addresses(all_erc20_addresses, only_trusted, exclude_spam) raw_balances = self.ethereum_client.erc20.get_balances( safe_address, erc20_addresses) balances = [] for balance in raw_balances: if not balance['token_address']: # Ether balance['token'] = None elif balance['balance'] > 0: balance['token'] = self.get_token_info( balance['token_address']) if not balance[ 'token']: # Ignore ERC20 tokens that cannot be queried continue else: continue balances.append(Balance(**balance)) return balances def get_binance_price(self, symbol: str): url = f'https://api.binance.com/api/v3/avgPrice?symbol={symbol}' try: response = requests.get(url) api_json = response.json() if not response.ok: logger.warning('Cannot get price from url=%s', url) raise CannotGetEthereumPrice(api_json.get('msg')) price = float(api_json['price']) if not price: raise CannotGetEthereumPrice( f'Price from url={url} is {price}') return price except (ValueError, ConnectionError) as e: raise CannotGetEthereumPrice from e def get_kraken_price(self, symbol: str): url = f'https://api.kraken.com/0/public/Ticker?pair={symbol}' try: response = requests.get(url) 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 CannotGetEthereumPrice(str(api_json['error'])) result = api_json['result'] for new_ticker in result: price = float(result[new_ticker]['c'][0]) if not price: raise CannotGetEthereumPrice( f'Price from url={url} is {price}') return price except (ValueError, ConnectionError) as e: raise CannotGetEthereumPrice from e def get_dai_usd_price_kraken(self) -> float: """ :return: current USD price for ethereum using Kraken :raises: CannotGetEthereumPrice """ return self.get_kraken_price('DAIUSD') def get_eth_usd_price_binance(self) -> float: """ :return: current USD price for ethereum using Kraken :raises: CannotGetEthereumPrice """ return self.get_binance_price('ETHUSDT') def get_eth_usd_price_kraken(self) -> float: """ :return: current USD price for ethereum using Kraken :raises: CannotGetEthereumPrice """ return self.get_kraken_price('ETHUSD') def get_ewt_usd_price_kucoin(self) -> float: url = 'https://api.kucoin.com/api/v1/market/orderbook/level1?symbol=EWT-USDT' response = requests.get(url) try: result = response.json() return float(result['data']['price']) except (ValueError, ConnectionError) as e: raise CannotGetEthereumPrice from e @cachedmethod(cache=operator.attrgetter('cache_eth_price')) @cache_memoize(60 * 30, prefix='balances-get_eth_price') # 30 minutes def get_eth_price(self) -> float: """ Get USD price for Ether. On xDAI we use DAI price. :return: USD price for Ether """ if self.ethereum_network == EthereumNetwork.XDAI: try: return self.get_dai_usd_price_kraken() except CannotGetEthereumPrice: return 1 # DAI/USD should be close to 1 elif self.ethereum_network in (EthereumNetwork.ENERGY_WEB_CHAIN, EthereumNetwork.VOLTA): return self.get_ewt_usd_price_kucoin() else: try: return self.get_eth_usd_price_kraken() except CannotGetEthereumPrice: return self.get_eth_usd_price_binance() @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: str) -> float: """ Return current ether value for a given `token_address` """ try: return self.kyber_oracle.get_price(token_address) except OracleException: logger.warning( 'Cannot get eth value for token-address=%s from Kyber, trying Uniswap V2', token_address) try: return self.uniswap_v2_oracle.get_price(token_address) except OracleException: logger.warning( 'Cannot get eth value for token-address=%s on Uniswap V2, trying Uniswap', token_address) try: return self.uniswap_oracle.get_price(token_address) except OracleException: logger.warning( 'Cannot get eth value for token-address=%s on Uniswap', token_address) return 0. @cachedmethod(cache=operator.attrgetter('cache_token_info')) @cache_memoize(60 * 60 * 24, prefix='balances-get_token_info') # 1 day def get_token_info(self, token_address: str) -> Optional[Erc20InfoWithLogo]: try: token = Token.objects.get(address=token_address) return Erc20InfoWithLogo.from_token(token) except Token.DoesNotExist: try: erc20_info = self.ethereum_client.erc20.get_info(token_address) token = Token.objects.create(address=token_address, name=erc20_info.name, symbol=erc20_info.symbol, decimals=erc20_info.decimals) return Erc20InfoWithLogo.from_token(token) except InvalidERC20Info: logger.warning( 'Cannot get erc20 token info for token-address=%s', token_address) return None def get_usd_balances(self, safe_address: str, only_trusted: bool = False, 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 :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 """ balances: List[Balance] = self.get_balances(safe_address, only_trusted, exclude_spam) eth_value = self.get_eth_price() balances_with_usd = [] for balance in balances: token_address = balance.token_address if not token_address: # Ether fiat_conversion = eth_value fiat_balance = fiat_conversion * (balance.balance / 10**18) else: token_to_eth_price = self.get_token_eth_value(token_address) if token_to_eth_price: fiat_conversion = eth_value * token_to_eth_price balance_with_decimals = balance.balance / 10**balance.token.decimals fiat_balance = fiat_conversion * balance_with_decimals else: fiat_conversion = 0. fiat_balance = 0. balances_with_usd.append( BalanceWithFiat(balance.token_address, balance.token, balance.balance, round(fiat_balance, 4), round(fiat_conversion, 4), 'USD')) return balances_with_usd