Beispiel #1
0
 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
Beispiel #3
0
 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 = {}
Beispiel #5
0
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