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
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
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
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]
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), )
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), )