Example #1
0
def pairs_from_ethereum(ethereum: EthereumManager) -> Dict[str, Any]:
    """Detect the uniswap v2 pool tokens by using an ethereum node"""
    contracts_file = Path(__file__).resolve().parent / 'contracts.json'
    with contracts_file.open('r') as f:
        contracts = json.loads(f.read())

    univ2factory = EthereumContract(
        address=contracts['UNISWAPV2FACTORY']['address'],
        abi=contracts['UNISWAPV2FACTORY']['abi'],
        deployed_block=0,  # whatever
    )
    pairs_num = univ2factory.call(ethereum, 'allPairsLength')
    chunks = list(get_chunks([[x] for x in range(pairs_num)], n=500))
    pairs = []
    for idx, chunk in enumerate(chunks):
        print(f'Querying univ2 pairs chunk {idx + 1} / {len(chunks)}')
        result = multicall_specific(ethereum, univ2factory, 'allPairs', chunk)
        try:
            pairs.extend([deserialize_ethereum_address(x[0]) for x in result])
        except DeserializationError:
            print(
                'Error deserializing address while fetching uniswap v2 pool tokens'
            )
            sys.exit(1)

    return pairs
Example #2
0
    def find_yearn_price(
        self,
        token: EthereumToken,
    ) -> Optional[Price]:
        """
        Query price for a yearn vault v2 token using the pricePerShare method
        and the price of the underlying token.
        """
        assert self._ethereum is not None, 'Inquirer ethereum manager should have been initialized'  # noqa: E501

        maybe_underlying_token = GlobalDBHandler().fetch_underlying_tokens(
            token.ethereum_address)
        if maybe_underlying_token is None or len(maybe_underlying_token) != 1:
            log.error(f'Yearn vault token {token} without an underlying asset')
            return None

        underlying_token = EthereumToken(maybe_underlying_token[0].address)
        underlying_token_price = self.find_usd_price(underlying_token)
        # Get the price per share from the yearn contract
        contract = EthereumContract(
            address=token.ethereum_address,
            abi=YEARN_VAULT_V2_ABI,
            deployed_block=0,
        )
        try:
            price_per_share = contract.call(self._ethereum, 'pricePerShare')
            return Price(price_per_share * underlying_token_price /
                         10**token.decimals)
        except (RemoteError, BlockchainQueryError) as e:
            log.error(
                f'Failed to query pricePerShare method in Yearn v2 Vault. {str(e)}'
            )

        return None
Example #3
0
def uniswap_lp_token_balances(
    userdb: 'DBHandler',
    address: ChecksumEthAddress,
    ethereum: 'EthereumManager',
    lp_addresses: List[ChecksumEthAddress],
    known_assets: Set[EthereumToken],
    unknown_assets: Set[EthereumToken],
) -> List[LiquidityPool]:
    """Query uniswap token balances from ethereum chain

    The number of addresses to query in one call depends a lot on the node used.
    With an infura node we saw the following:
    500 addresses per call took on average 43 seconds for 20450 addresses
    2000 addresses per call took on average 36 seconds for 20450 addresses
    4000 addresses per call took on average 32.6 seconds for 20450 addresses
    5000 addresses timed out a few times
    """
    zerion_contract = EthereumContract(
        address=ZERION_ADAPTER_ADDRESS,
        abi=ZERION_ABI,
        deployed_block=1586199170,
    )
    if NodeName.OWN in ethereum.web3_mapping:
        chunks = list(get_chunks(lp_addresses, n=4000))
        call_order = [NodeName.OWN]
    else:
        chunks = list(get_chunks(lp_addresses, n=700))
        call_order = ethereum.default_call_order(skip_etherscan=True)

    balances = []
    for chunk in chunks:
        result = zerion_contract.call(
            ethereum=ethereum,
            method_name='getAdapterBalance',
            arguments=[
                address, '0x4EdBac5c8cb92878DD3fd165e43bBb8472f34c3f', chunk
            ],
            call_order=call_order,
        )

        for entry in result[1]:
            balances.append(
                _decode_result(userdb, entry, known_assets, unknown_assets))

    return balances
Example #4
0
    def get_pool(
        self,
        token_0: EthereumToken,
        token_1: EthereumToken,
    ) -> List[str]:
        result = multicall_specific(
            ethereum=self.eth_manager,
            contract=UNISWAP_V3_FACTORY,
            method_name='getPool',
            arguments=[[
                token_0.ethereum_address,
                token_1.ethereum_address,
                fee,
            ] for fee in (3000, 500, 10000)],
        )

        # get liquidity for each pool and choose the pool with the highest liquidity
        best_pool, max_liquidity = to_checksum_address(result[0][0]), 0
        for query in result:
            if query[0] == ZERO_ADDRESS:
                continue
            pool_address = to_checksum_address(query[0])
            pool_contract = EthereumContract(
                address=pool_address,
                abi=UNISWAP_V3_POOL_ABI,
                deployed_block=UNISWAP_FACTORY_DEPLOYED_BLOCK,
            )
            pool_liquidity = pool_contract.call(
                ethereum=self.eth_manager,
                method_name='liquidity',
                arguments=[],
                call_order=None,
            )
            if pool_liquidity > max_liquidity:
                best_pool = pool_address

        return [best_pool]
Example #5
0
class ZerionSDK():
    """Adapter for the Zerion DeFi SDK https://github.com/zeriontech/defi-sdk"""
    def __init__(
        self,
        ethereum_manager: 'EthereumManager',
        msg_aggregator: MessagesAggregator,
    ) -> None:
        self.ethereum = ethereum_manager
        self.msg_aggregator = msg_aggregator
        self.contract = EthereumContract(
            address=ZERION_ADAPTER_ADDRESS,
            abi=ZERION_ABI,
            deployed_block=1586199170,
        )

    def all_balances_for_account(
            self, account: ChecksumEthAddress) -> List[DefiProtocolBalances]:
        """Calls the contract's getBalances() to get all protocol balances for account

        https://docs.zerion.io/smart-contracts/adapterregistry-v3#getbalances
        """
        result = self.contract.call(
            ethereum=self.ethereum,
            method_name='getBalances',
            arguments=[account],
        )
        protocol_balances = []
        for entry in result:
            protocol = DefiProtocol(
                name=entry[0][0],
                description=entry[0][1],
                url=entry[0][2],
                version=entry[0][4],
            )
            for adapter_balance in entry[1]:
                balance_type = adapter_balance[0][
                    1]  # can be either 'Asset' or 'Debt'
                for balances in adapter_balance[1]:
                    underlying_balances = []
                    base_balance = self._get_single_balance(
                        protocol.name, balances[0])
                    for balance in balances[1]:
                        defi_balance = self._get_single_balance(
                            protocol.name, balance)
                        underlying_balances.append(defi_balance)

                    if base_balance.balance.usd_value == ZERO:
                        # This can happen. We can't find a price for some assets
                        # such as combined pool assets. But we can instead use
                        # the sum of the usd_value of the underlying_balances
                        usd_sum = sum(x.balance.usd_value
                                      for x in underlying_balances)
                        base_balance.balance.usd_value = usd_sum  # type: ignore

                    protocol_balances.append(
                        DefiProtocolBalances(
                            protocol=protocol,
                            balance_type=balance_type,
                            base_balance=base_balance,
                            underlying_balances=underlying_balances,
                        ))

        return protocol_balances

    def _get_single_balance(
        self,
        protocol_name: str,
        entry: Tuple[Tuple[str, str, str, int], int],
    ) -> DefiBalance:
        metadata = entry[0]
        balance_value = entry[1]
        decimals = metadata[3]
        normalized_value = token_normalized_value_decimals(
            balance_value, decimals)
        token_symbol = metadata[2]
        token_address = to_checksum_address(metadata[0])
        token_name = metadata[1]

        special_handling = self.handle_protocols(
            protocol_name=protocol_name,
            token_symbol=token_symbol,
            normalized_balance=normalized_value,
            token_address=token_address,
            token_name=token_name,
        )
        if special_handling:
            return special_handling

        try:
            asset = Asset(token_symbol)
            usd_price = Inquirer().find_usd_price(asset)
        except (UnknownAsset, UnsupportedAsset):
            if not _is_token_non_standard(token_symbol, token_address):
                self.msg_aggregator.add_warning(
                    f'Unsupported asset {token_symbol} with address '
                    f'{token_address} encountered during DeFi protocol queries',
                )
            usd_price = Price(ZERO)

        usd_value = normalized_value * usd_price
        defi_balance = DefiBalance(
            token_address=token_address,
            token_name=token_name,
            token_symbol=token_symbol,
            balance=Balance(amount=normalized_value, usd_value=usd_value),
        )
        return defi_balance

    def handle_protocols(
        self,
        protocol_name: str,
        token_symbol: str,
        normalized_balance: FVal,
        token_address: str,
        token_name: str,
    ) -> Optional[DefiBalance]:
        """Special handling for price for token/protocols which are easier to do onchain
        or need some kind of special treatment.
        """
        if protocol_name == 'PoolTogether':
            result = _handle_pooltogether(normalized_balance, token_name)
            if result is not None:
                return result

        underlying_asset_price = get_underlying_asset_price(token_symbol)
        usd_price = handle_defi_price_query(self.ethereum, token_symbol,
                                            underlying_asset_price)
        if usd_price is None:
            return None

        return DefiBalance(
            token_address=to_checksum_address(token_address),
            token_name=token_name,
            token_symbol=token_symbol,
            balance=Balance(amount=normalized_balance,
                            usd_value=normalized_balance * usd_price),
        )
Example #6
0
class ZerionSDK():
    """Adapter for the Zerion DeFi SDK https://github.com/zeriontech/defi-sdk"""
    def __init__(
        self,
        ethereum_manager: 'EthereumManager',
        msg_aggregator: MessagesAggregator,
    ) -> None:
        self.ethereum = ethereum_manager
        self.msg_aggregator = msg_aggregator
        self.contract = EthereumContract(
            address=ZERION_ADAPTER_ADDRESS,
            abi=ZERION_ABI,
            deployed_block=1586199170,
        )
        self.protocol_names: Optional[List[str]] = None

    def _get_protocol_names(self) -> List[str]:
        if self.protocol_names is not None:
            return self.protocol_names

        try:
            protocol_names = self.contract.call(
                ethereum=self.ethereum,
                method_name='getProtocolNames',
                arguments=[],
            )
        except RemoteError as e:
            log.warning(
                f'Failed to query zerion defi sdk for protocol names due to {str(e)}'
                f'Falling back to known list of names.', )
            return list(KNOWN_ZERION_PROTOCOL_NAMES)

        self.protocol_names = protocol_names
        return protocol_names

    def _query_chain_for_all_balances(self,
                                      account: ChecksumEthAddress) -> List:
        if NodeName.OWN in self.ethereum.web3_mapping:
            try:
                # In this case we don't care about the gas limit
                return self.contract.call(
                    ethereum=self.ethereum,
                    method_name='getBalances',
                    arguments=[account],
                    call_order=[NodeName.OWN, NodeName.ONEINCH],
                )
            except RemoteError:
                log.warning(
                    'Failed to query zerionsdk balances with own node. Falling '
                    'back to multiple calls to getProtocolBalances', )

        # but if we are not connected to our own node the zerion sdk get balances call
        # has unfortunately crossed the default limits of almost all open nodes apart from 1inch
        # https://github.com/rotki/rotki/issues/1969
        # So now we get all supported protocols and query in batches
        protocol_names = self._get_protocol_names()
        result = []
        protocol_chunks: List[List[str]] = list(
            get_chunks(
                list(protocol_names),
                n=PROTOCOLS_QUERY_NUM,
            ))
        for protocol_names in protocol_chunks:
            contract_result = self.contract.call(
                ethereum=self.ethereum,
                method_name='getProtocolBalances',
                arguments=[account, protocol_names],
            )
            if len(contract_result) == 0:
                continue
            result.extend(contract_result)

        return result

    def all_balances_for_account(
            self, account: ChecksumEthAddress) -> List[DefiProtocolBalances]:
        """Calls the contract's getBalances() to get all protocol balances for account

        https://docs.zerion.io/smart-contracts/adapterregistry-v3#getbalances
        """
        result = self._query_chain_for_all_balances(account=account)
        protocol_balances = []
        for entry in result:
            protocol = DefiProtocol(
                name=entry[0][0],
                description=entry[0][1],
                url=entry[0][2],
                version=entry[0][4],
            )
            for adapter_balance in entry[1]:
                balance_type = adapter_balance[0][
                    1]  # can be either 'Asset' or 'Debt'
                for balances in adapter_balance[1]:
                    underlying_balances = []
                    base_balance = self._get_single_balance(
                        protocol.name, balances[0])
                    for balance in balances[1]:
                        defi_balance = self._get_single_balance(
                            protocol.name, balance)
                        underlying_balances.append(defi_balance)

                    if base_balance.balance.usd_value == ZERO:
                        # This can happen. We can't find a price for some assets
                        # such as combined pool assets. But we can instead use
                        # the sum of the usd_value of the underlying_balances
                        usd_sum = sum(x.balance.usd_value
                                      for x in underlying_balances)
                        base_balance.balance.usd_value = usd_sum  # type: ignore

                    protocol_balances.append(
                        DefiProtocolBalances(
                            protocol=protocol,
                            balance_type=balance_type,
                            base_balance=base_balance,
                            underlying_balances=underlying_balances,
                        ))

        return protocol_balances

    def _get_single_balance(
        self,
        protocol_name: str,
        entry: Tuple[Tuple[str, str, str, int], int],
    ) -> DefiBalance:
        metadata = entry[0]
        balance_value = entry[1]
        decimals = metadata[3]
        normalized_value = token_normalized_value_decimals(
            balance_value, decimals)
        token_symbol = metadata[2]
        token_address = to_checksum_address(metadata[0])
        token_name = metadata[1]

        special_handling = self.handle_protocols(
            protocol_name=protocol_name,
            token_symbol=token_symbol,
            normalized_balance=normalized_value,
            token_address=token_address,
            token_name=token_name,
        )
        if special_handling:
            return special_handling

        try:
            asset = Asset(token_symbol)
            usd_price = Inquirer().find_usd_price(asset)
        except (UnknownAsset, UnsupportedAsset):
            if not _is_token_non_standard(token_symbol, token_address):
                self.msg_aggregator.add_warning(
                    f'Unsupported asset {token_symbol} with address '
                    f'{token_address} encountered during DeFi protocol queries',
                )
            usd_price = Price(ZERO)

        usd_value = normalized_value * usd_price
        defi_balance = DefiBalance(
            token_address=token_address,
            token_name=token_name,
            token_symbol=token_symbol,
            balance=Balance(amount=normalized_value, usd_value=usd_value),
        )
        return defi_balance

    def handle_protocols(
        self,
        protocol_name: str,
        token_symbol: str,
        normalized_balance: FVal,
        token_address: str,
        token_name: str,
    ) -> Optional[DefiBalance]:
        """Special handling for price for token/protocols which are easier to do onchain
        or need some kind of special treatment.
        """
        if protocol_name == 'PoolTogether':
            result = _handle_pooltogether(normalized_balance, token_name)
            if result is not None:
                return result

        underlying_asset_price = get_underlying_asset_price(token_symbol)
        usd_price = handle_defi_price_query(self.ethereum, token_symbol,
                                            underlying_asset_price)
        if usd_price is None:
            return None

        return DefiBalance(
            token_address=to_checksum_address(token_address),
            token_name=token_name,
            token_symbol=token_symbol,
            balance=Balance(amount=normalized_balance,
                            usd_value=normalized_balance * usd_price),
        )