def _decode_result( data: Tuple, known_assets: Set[EthereumToken], unknown_assets: Set[UnknownEthereumToken], ) -> LiquidityPool: pool_token = _decode_token(data[0]) token0 = _decode_token(data[1][0]) token1 = _decode_token(data[1][1]) assets = [] for token in (token0, token1): asset = get_ethereum_token( symbol=token.symbol, ethereum_address=token.address, name=token.name, decimals=token.decimals, ) # Classify the asset either as known or unknown if isinstance(asset, EthereumToken): known_assets.add(asset) elif isinstance(asset, UnknownEthereumToken): unknown_assets.add(asset) assets.append(LiquidityPoolAsset( asset=asset, total_amount=None, user_balance=Balance(amount=token.amount), )) pool = LiquidityPool( address=pool_token.address, assets=assets, total_supply=None, user_balance=Balance(amount=pool_token.amount), ) return pool
def deserialize_bpt_event( raw_event: Dict[str, Any], event_type: Literal[BalancerBPTEventType.MINT, BalancerBPTEventType.BURN], ) -> BalancerBPTEvent: """May raise DeserializationError""" try: tx_hash, log_index = deserialize_transaction_id(raw_event['id']) raw_user_address = raw_event['user']['id'] amount = deserialize_asset_amount(raw_event['amount']) raw_pool = raw_event['pool'] raw_pool_address = raw_pool['id'] raw_pool_tokens = raw_pool['tokens'] total_weight = deserialize_asset_amount(raw_pool['totalWeight']) except KeyError as e: raise DeserializationError(f'Missing key: {str(e)}.') from e if total_weight == ZERO: raise DeserializationError('Pool weight is zero.') user_address = deserialize_ethereum_address(raw_user_address) pool_address = deserialize_ethereum_address(raw_pool_address) pool_tokens = [] for raw_token in raw_pool_tokens: try: raw_token_address = raw_token['address'] token_symbol = raw_token['symbol'] token_name = raw_token['name'] token_decimals = raw_token['decimals'] token_weight = deserialize_asset_amount(raw_token['denormWeight']) except KeyError as e: raise DeserializationError(f'Missing key: {str(e)}.') from e token_address = deserialize_ethereum_address(raw_token_address) token = get_ethereum_token( symbol=token_symbol, ethereum_address=token_address, name=token_name, decimals=token_decimals, ) pool_token = BalancerBPTEventPoolToken( token=token, weight=token_weight * 100 / total_weight, ) pool_tokens.append(pool_token) pool_tokens.sort(key=lambda pool_token: pool_token.token.ethereum_address) bpt_event = BalancerBPTEvent( tx_hash=tx_hash, log_index=log_index, address=user_address, event_type=event_type, pool_address=pool_address, pool_tokens=pool_tokens, amount=amount, ) return bpt_event
def test_get_ethereum_token(): assert A_DAI == get_ethereum_token( symbol='DAI', ethereum_address='0x6B175474E89094C44Da98b954EedeAC495271d0F', ) unknown_token = UnknownEthereumToken( symbol='DAI', ethereum_address='0xA379B8204A49A72FF9703e18eE61402FAfCCdD60', decimals=18, ) assert unknown_token == get_ethereum_token( symbol='DAI', ethereum_address='0xA379B8204A49A72FF9703e18eE61402FAfCCdD60', ), 'correct symbol but wrong address should result in unknown token' unknown_token = UnknownEthereumToken( symbol='DOT', ethereum_address='0xA379B8204A49A72FF9703e18eE61402FAfCCdD60', decimals=18, ) assert unknown_token == get_ethereum_token( symbol='DOT', ethereum_address='0xA379B8204A49A72FF9703e18eE61402FAfCCdD60', ), 'symbol of normal chain (polkadot here) should result in unknown token'
def _get_trades_graph_for_address( self, address: ChecksumEthAddress, start_ts: Timestamp, end_ts: Timestamp, ) -> List[AMMTrade]: """Get the address' trades data querying the Uniswap subgraph Each trade (swap) instantiates an <AMMTrade>. The trade pair (i.e. BASE_QUOTE) is determined by `reserve0_reserve1`. Translated to Uniswap lingo: Trade type BUY: - `asset1In` (QUOTE, reserve1) is gt 0. - `asset0Out` (BASE, reserve0) is gt 0. Trade type SELL: - `asset0In` (BASE, reserve0) is gt 0. - `asset1Out` (QUOTE, reserve1) is gt 0. """ trades: List[AMMTrade] = [] param_types = { '$limit': 'Int!', '$offset': 'Int!', '$address': 'Bytes!', '$start_ts': 'BigInt!', '$end_ts': 'BigInt!', } param_values = { 'limit': GRAPH_QUERY_LIMIT, 'offset': 0, 'address': address.lower(), 'start_ts': str(start_ts), 'end_ts': str(end_ts), } querystr = format_query_indentation(SWAPS_QUERY.format()) while True: result = self.graph.query( # type: ignore # caller already checks querystr=querystr, param_types=param_types, param_values=param_values, ) result_data = result['swaps'] for entry in result_data: swaps = [] for swap in entry['transaction']['swaps']: timestamp = swap['timestamp'] swap_token0 = swap['pair']['token0'] swap_token1 = swap['pair']['token1'] token0 = get_ethereum_token( symbol=swap_token0['symbol'], ethereum_address=to_checksum_address(swap_token0['id']), name=swap_token0['name'], decimals=swap_token0['decimals'], ) token1 = get_ethereum_token( symbol=swap_token1['symbol'], ethereum_address=to_checksum_address(swap_token1['id']), name=swap_token1['name'], decimals=int(swap_token1['decimals']), ) amount0_in = FVal(swap['amount0In']) amount1_in = FVal(swap['amount1In']) amount0_out = FVal(swap['amount0Out']) amount1_out = FVal(swap['amount1Out']) swaps.append(AMMSwap( tx_hash=swap['id'].split('-')[0], log_index=int(swap['logIndex']), address=address, from_address=to_checksum_address(swap['sender']), to_address=to_checksum_address(swap['to']), timestamp=Timestamp(int(timestamp)), location=Location.UNISWAP, token0=token0, token1=token1, amount0_in=AssetAmount(amount0_in), amount1_in=AssetAmount(amount1_in), amount0_out=AssetAmount(amount0_out), amount1_out=AssetAmount(amount1_out), )) # Now that we got all swaps for a transaction, create the trade object trades.extend(self._tx_swaps_to_trades(swaps)) # Check whether an extra request is needed if len(result_data) < GRAPH_QUERY_LIMIT: break # Update pagination step param_values = { **param_values, 'offset': param_values['offset'] + GRAPH_QUERY_LIMIT, # type: ignore } return trades
def _get_events_graph( self, address: ChecksumEthAddress, start_ts: Timestamp, end_ts: Timestamp, event_type: EventType, ) -> List[LiquidityPoolEvent]: """Get the address' events (mints & burns) querying the Uniswap subgraph Each event data is stored in a <LiquidityPoolEvent>. """ address_events: List[LiquidityPoolEvent] = [] if event_type == EventType.MINT: query = MINTS_QUERY query_schema = 'mints' elif event_type == EventType.BURN: query = BURNS_QUERY query_schema = 'burns' else: log.error(f'Unexpected event_type: {event_type}. Skipping events query.') return address_events param_types = { '$limit': 'Int!', '$offset': 'Int!', '$address': 'Bytes!', '$start_ts': 'BigInt!', '$end_ts': 'BigInt!', } param_values = { 'limit': GRAPH_QUERY_LIMIT, 'offset': 0, 'address': address.lower(), 'start_ts': str(start_ts), 'end_ts': str(end_ts), } querystr = format_query_indentation(query.format()) while True: result = self.graph.query( # type: ignore # caller already checks querystr=querystr, param_types=param_types, param_values=param_values, ) result_data = result[query_schema] for event in result_data: token0_ = event['pair']['token0'] token1_ = event['pair']['token1'] token0 = get_ethereum_token( symbol=token0_['symbol'], ethereum_address=to_checksum_address(token0_['id']), name=token0_['name'], decimals=token0_['decimals'], ) token1 = get_ethereum_token( symbol=token1_['symbol'], ethereum_address=to_checksum_address(token1_['id']), name=token1_['name'], decimals=int(token1_['decimals']), ) lp_event = LiquidityPoolEvent( tx_hash=event['transaction']['id'], log_index=int(event['logIndex']), address=address, timestamp=Timestamp(int(event['timestamp'])), event_type=event_type, pool_address=to_checksum_address(event['pair']['id']), token0=token0, token1=token1, amount0=AssetAmount(FVal(event['amount0'])), amount1=AssetAmount(FVal(event['amount1'])), usd_price=Price(FVal(event['amountUSD'])), lp_amount=AssetAmount(FVal(event['liquidity'])), ) address_events.append(lp_event) # Check whether an extra request is needed if len(result_data) < GRAPH_QUERY_LIMIT: break # Update pagination step param_values = { **param_values, 'offset': param_values['offset'] + GRAPH_QUERY_LIMIT, # type: ignore } return address_events
def _get_balances_graph( self, addresses: List[ChecksumEthAddress], ) -> ProtocolBalance: """Get the addresses' pools data querying the Uniswap subgraph Each liquidity position is converted into a <LiquidityPool>. """ address_balances: DDAddressBalances = defaultdict(list) known_assets: Set[EthereumToken] = set() unknown_assets: Set[UnknownEthereumToken] = set() addresses_lower = [address.lower() for address in addresses] querystr = format_query_indentation(LIQUIDITY_POSITIONS_QUERY.format()) param_types = { '$limit': 'Int!', '$offset': 'Int!', '$addresses': '[String!]', '$balance': 'BigDecimal!', } param_values = { 'limit': GRAPH_QUERY_LIMIT, 'offset': 0, 'addresses': addresses_lower, 'balance': '0', } while True: result = self.graph.query( # type: ignore # caller already checks querystr=querystr, param_types=param_types, param_values=param_values, ) result_data = result['liquidityPositions'] for lp in result_data: user_address = to_checksum_address(lp['user']['id']) user_lp_balance = FVal(lp['liquidityTokenBalance']) lp_pair = lp['pair'] lp_address = to_checksum_address(lp_pair['id']) lp_total_supply = FVal(lp_pair['totalSupply']) # Insert LP tokens reserves within tokens dicts token0 = lp_pair['token0'] token0['total_amount'] = lp_pair['reserve0'] token1 = lp_pair['token1'] token1['total_amount'] = lp_pair['reserve1'] liquidity_pool_assets = [] for token in token0, token1: # Get the token <EthereumToken> or <UnknownEthereumToken> asset = get_ethereum_token( symbol=token['symbol'], ethereum_address=to_checksum_address(token['id']), name=token['name'], decimals=int(token['decimals']), ) # Classify the asset either as known or unknown if isinstance(asset, EthereumToken): known_assets.add(asset) elif isinstance(asset, UnknownEthereumToken): unknown_assets.add(asset) # Estimate the underlying asset total_amount asset_total_amount = FVal(token['total_amount']) user_asset_balance = ( user_lp_balance / lp_total_supply * asset_total_amount ) liquidity_pool_asset = LiquidityPoolAsset( asset=asset, total_amount=asset_total_amount, user_balance=Balance(amount=user_asset_balance), ) liquidity_pool_assets.append(liquidity_pool_asset) liquidity_pool = LiquidityPool( address=lp_address, assets=liquidity_pool_assets, total_supply=lp_total_supply, user_balance=Balance(amount=user_lp_balance), ) address_balances[user_address].append(liquidity_pool) # Check whether an extra request is needed if len(result_data) < GRAPH_QUERY_LIMIT: break # Update pagination step param_values = { **param_values, 'offset': param_values['offset'] + GRAPH_QUERY_LIMIT, # type: ignore } protocol_balance = ProtocolBalance( address_balances=dict(address_balances), known_assets=known_assets, unknown_assets=unknown_assets, ) return protocol_balance
def deserialize_swap(raw_swap: Dict[str, Any]) -> AMMSwap: """May raise DeserializationError""" try: tx_hash, log_index = deserialize_transaction_id(raw_swap['id']) timestamp = deserialize_timestamp(raw_swap['timestamp']) raw_tokens = raw_swap['poolAddress']['tokens'] token0_symbol = raw_swap['tokenInSym'] token1_symbol = raw_swap['tokenOutSym'] amount0_in = deserialize_asset_amount(raw_swap['tokenAmountIn']) amount1_out = deserialize_asset_amount(raw_swap['tokenAmountOut']) raw_user_address = raw_swap['userAddress']['id'] # address raw_caller_address = raw_swap['caller'] # from_address raw_pool_address = raw_swap['poolAddress']['id'] # to_address raw_token_in_address = raw_swap['tokenIn'] # token0_address raw_token_out_address = raw_swap['tokenOut'] # token1_address except KeyError as e: raise DeserializationError(f'Missing key: {str(e)}.') from e if amount0_in == ZERO: # Prevent a division by zero error when creating the trade raise DeserializationError('TokenAmountIn balance is zero.') # Checksum addresses try: user_address = to_checksum_address(raw_user_address) except ValueError as e: raise DeserializationError( f'Invalid ethereum address: {raw_user_address} in userAddress.id.', ) from e try: caller_address = to_checksum_address(raw_caller_address) except ValueError as e: raise DeserializationError( f'Invalid ethereum address: {raw_caller_address} in caller.', ) from e try: pool_address = to_checksum_address(raw_pool_address) except ValueError as e: raise DeserializationError( f'Invalid ethereum address: {raw_pool_address} in poolAddress.id.', ) from e try: token_in_address = to_checksum_address(raw_token_in_address) except ValueError as e: raise DeserializationError( f'Invalid ethereum address: {raw_token_in_address} in tokenIn.', ) from e try: token_out_address = to_checksum_address(raw_token_out_address) except ValueError as e: raise DeserializationError( f'Invalid ethereum address: {raw_token_out_address} in tokenOut.', ) from e # Get token0 and token1 # When the controller removes all the tokens from a pool, `raw_tokens` will # be an empty list. Therefore it won't be possible to get their names and # decimals. In case of having to instantiate an UnknownEthereumToken both # params will be None. if len(raw_tokens) != 0: try: raw_address_tokens = { raw_token['address']: raw_token for raw_token in raw_tokens } raw_token0 = raw_address_tokens[raw_token_in_address] raw_token1 = raw_address_tokens[raw_token_out_address] token0_name = raw_token0['name'] token0_decimals = raw_token0['decimals'] token1_name = raw_token1['name'] token1_decimals = raw_token1['decimals'] except KeyError as e: raise DeserializationError(f'Missing key: {str(e)}.') from e else: token0_name = None token0_decimals = None token1_name = None token1_decimals = None token0 = get_ethereum_token( symbol=token0_symbol, ethereum_address=token_in_address, name=token0_name, decimals=token0_decimals, ) token1 = get_ethereum_token( symbol=token1_symbol, ethereum_address=token_out_address, name=token1_name, decimals=token1_decimals, ) amm_swap = AMMSwap( tx_hash=tx_hash, log_index=log_index, address=user_address, from_address=caller_address, to_address=pool_address, timestamp=timestamp, location=Location.BALANCER, token0=token0, token1=token1, amount0_in=amount0_in, amount1_in=AssetAmount(ZERO), amount0_out=AssetAmount(ZERO), amount1_out=amount1_out, ) return amm_swap
def deserialize_pool_share( raw_pool_share: Dict[str, Any], ) -> Tuple[ChecksumEthAddress, BalancerPoolBalance]: """May raise DeserializationError""" try: raw_user_address = raw_pool_share['userAddress']['id'] user_amount = deserialize_asset_amount(raw_pool_share['balance']) raw_pool = raw_pool_share['poolId'] total_amount = deserialize_asset_amount(raw_pool['totalShares']) raw_address = raw_pool['id'] raw_tokens = raw_pool['tokens'] total_weight = deserialize_asset_amount(raw_pool['totalWeight']) except KeyError as e: raise DeserializationError(f'Missing key: {str(e)}.') from e if total_weight == ZERO: raise DeserializationError('Pool weight is zero.') try: user_address = to_checksum_address(raw_user_address) except ValueError as e: raise DeserializationError( f'Invalid ethereum address: {raw_user_address} in userAddress.id.', ) from e try: address = to_checksum_address(raw_address) except ValueError as e: raise DeserializationError( f'Invalid ethereum address: {raw_address} in poolId.id.', ) from e pool_tokens = [] for raw_token in raw_tokens: try: raw_token_address = raw_token['address'] token_symbol = raw_token['symbol'] token_name = raw_token['name'] token_decimals = raw_token['decimals'] token_total_amount = deserialize_asset_amount(raw_token['balance']) token_weight = deserialize_asset_amount(raw_token['denormWeight']) except KeyError as e: raise DeserializationError(f'Missing key: {str(e)}.') from e try: token_address = to_checksum_address(raw_token_address) except ValueError as e: raise DeserializationError( f'Invalid ethereum address: {raw_token_address} in {token_symbol} token.address.', # noqa: E501 ) from e token = get_ethereum_token( symbol=token_symbol, ethereum_address=token_address, name=token_name, decimals=token_decimals, ) if token_total_amount == ZERO: raise DeserializationError( f'Token {token.identifier} balance is zero.') token_user_amount = user_amount / total_amount * token_total_amount weight = token_weight * 100 / total_weight pool_token = BalancerPoolTokenBalance( token=token, total_amount=token_total_amount, user_balance=Balance(amount=token_user_amount), weight=weight, ) pool_tokens.append(pool_token) pool_tokens.sort(key=lambda pool_token: pool_token.token.ethereum_address) pool = BalancerPoolBalance( address=address, tokens=pool_tokens, total_amount=total_amount, user_balance=Balance(amount=user_amount), ) return user_address, pool
def _get_trades_graph_for_address( self, address: ChecksumEthAddress, start_ts: Timestamp, end_ts: Timestamp, ) -> List[AMMTrade]: """Get the address' trades data querying the Uniswap subgraph Each trade (swap) instantiates an <AMMTrade>. The trade pair (i.e. BASE_QUOTE) is determined by `reserve0_reserve1`. Translated to Uniswap lingo: Trade type BUY: - `asset1In` (QUOTE, reserve1) is gt 0. - `asset0Out` (BASE, reserve0) is gt 0. Trade type SELL: - `asset0In` (BASE, reserve0) is gt 0. - `asset1Out` (QUOTE, reserve1) is gt 0. """ trades: List[AMMTrade] = [] param_types = { '$limit': 'Int!', '$offset': 'Int!', '$address': 'Bytes!', '$start_ts': 'BigInt!', '$end_ts': 'BigInt!', } param_values = { 'limit': GRAPH_QUERY_LIMIT, 'offset': 0, 'address': address.lower(), 'start_ts': str(start_ts), 'end_ts': str(end_ts), } querystr = format_query_indentation(SWAPS_QUERY.format()) while True: try: result = self.graph.query( querystr=querystr, param_types=param_types, param_values=param_values, ) except RemoteError as e: self.msg_aggregator.add_error( SUBGRAPH_REMOTE_ERROR_MSG.format(error_msg=str(e))) raise # Combine the two list of the query result_data = result['swaps_from'] result_data.extend(result['swaps_to']) # Store ids of swaps to avoid possible duplicates fetched_ids = set() for entry in result_data: swaps = [] for swap in entry['transaction']['swaps']: if swap['id'] in fetched_ids: continue fetched_ids.add(swap['id']) timestamp = swap['timestamp'] swap_token0 = swap['pair']['token0'] swap_token1 = swap['pair']['token1'] try: token0_deserialized = deserialize_ethereum_address( swap_token0['id']) token1_deserialized = deserialize_ethereum_address( swap_token1['id']) from_address_deserialized = deserialize_ethereum_address( swap['sender']) to_address_deserialized = deserialize_ethereum_address( swap['to']) except DeserializationError as e: msg = ( f'Failed to deserialize addresses in trade from uniswap graph with ' f'token 0: {swap_token0["id"]}, token 1: {swap_token1["id"]}, ' f'swap sender: {swap["sender"]}, swap receiver {swap["to"]}' ) log.error(msg) raise RemoteError(msg) from e token0 = get_ethereum_token( symbol=swap_token0['symbol'], ethereum_address=token0_deserialized, name=swap_token0['name'], decimals=swap_token0['decimals'], ) token1 = get_ethereum_token( symbol=swap_token1['symbol'], ethereum_address=token1_deserialized, name=swap_token1['name'], decimals=int(swap_token1['decimals']), ) amount0_in = FVal(swap['amount0In']) amount1_in = FVal(swap['amount1In']) amount0_out = FVal(swap['amount0Out']) amount1_out = FVal(swap['amount1Out']) swaps.append( AMMSwap( tx_hash=swap['id'].split('-')[0], log_index=int(swap['logIndex']), address=address, from_address=from_address_deserialized, to_address=to_address_deserialized, timestamp=Timestamp(int(timestamp)), location=Location.UNISWAP, token0=token0, token1=token1, amount0_in=AssetAmount(amount0_in), amount1_in=AssetAmount(amount1_in), amount0_out=AssetAmount(amount0_out), amount1_out=AssetAmount(amount1_out), )) # with the new logic the list of swaps can be empty, in that case don't try # to make trades from the swaps if len(swaps) == 0: continue # Now that we got all swaps for a transaction, create the trade object trades.extend(self._tx_swaps_to_trades(swaps)) # Check whether an extra request is needed if len(result_data) < GRAPH_QUERY_LIMIT: break # Update pagination step param_values = { **param_values, 'offset': param_values['offset'] + GRAPH_QUERY_LIMIT, # type: ignore } return trades
def _get_events_graph( self, address: ChecksumEthAddress, start_ts: Timestamp, end_ts: Timestamp, event_type: EventType, ) -> List[LiquidityPoolEvent]: """Get the address' events (mints & burns) querying the Uniswap subgraph Each event data is stored in a <LiquidityPoolEvent>. """ address_events: List[LiquidityPoolEvent] = [] if event_type == EventType.MINT: query = MINTS_QUERY query_schema = 'mints' elif event_type == EventType.BURN: query = BURNS_QUERY query_schema = 'burns' else: log.error( f'Unexpected event_type: {event_type}. Skipping events query.') return address_events param_types = { '$limit': 'Int!', '$offset': 'Int!', '$address': 'Bytes!', '$start_ts': 'BigInt!', '$end_ts': 'BigInt!', } param_values = { 'limit': GRAPH_QUERY_LIMIT, 'offset': 0, 'address': address.lower(), 'start_ts': str(start_ts), 'end_ts': str(end_ts), } querystr = format_query_indentation(query.format()) while True: try: result = self.graph.query( querystr=querystr, param_types=param_types, param_values=param_values, ) except RemoteError as e: self.msg_aggregator.add_error( SUBGRAPH_REMOTE_ERROR_MSG.format(error_msg=str(e))) raise result_data = result[query_schema] for event in result_data: token0_ = event['pair']['token0'] token1_ = event['pair']['token1'] try: token0_deserialized = deserialize_ethereum_address( token0_['id']) token1_deserialized = deserialize_ethereum_address( token1_['id']) pool_deserialized = deserialize_ethereum_address( event['pair']['id']) except DeserializationError as e: msg = ( f'Failed to deserialize address involved in liquidity pool event. ' f'Token 0: {token0_["id"]}, token 1: {token0_["id"]},' f' pair: {event["pair"]["id"]}.') log.error(msg) raise RemoteError(msg) from e token0 = get_ethereum_token( symbol=token0_['symbol'], ethereum_address=token0_deserialized, name=token0_['name'], decimals=token0_['decimals'], ) token1 = get_ethereum_token( symbol=token1_['symbol'], ethereum_address=token1_deserialized, name=token1_['name'], decimals=int(token1_['decimals']), ) lp_event = LiquidityPoolEvent( tx_hash=event['transaction']['id'], log_index=int(event['logIndex']), address=address, timestamp=Timestamp(int(event['timestamp'])), event_type=event_type, pool_address=pool_deserialized, token0=token0, token1=token1, amount0=AssetAmount(FVal(event['amount0'])), amount1=AssetAmount(FVal(event['amount1'])), usd_price=Price(FVal(event['amountUSD'])), lp_amount=AssetAmount(FVal(event['liquidity'])), ) address_events.append(lp_event) # Check whether an extra request is needed if len(result_data) < GRAPH_QUERY_LIMIT: break # Update pagination step param_values = { **param_values, 'offset': param_values['offset'] + GRAPH_QUERY_LIMIT, # type: ignore } return address_events
def _get_balances_graph( self, addresses: List[ChecksumEthAddress], ) -> ProtocolBalance: """Get the addresses' pools data querying the Uniswap subgraph Each liquidity position is converted into a <LiquidityPool>. """ address_balances: DDAddressBalances = defaultdict(list) known_assets: Set[EthereumToken] = set() unknown_assets: Set[UnknownEthereumToken] = set() addresses_lower = [address.lower() for address in addresses] querystr = format_query_indentation(LIQUIDITY_POSITIONS_QUERY.format()) param_types = { '$limit': 'Int!', '$offset': 'Int!', '$addresses': '[String!]', '$balance': 'BigDecimal!', } param_values = { 'limit': GRAPH_QUERY_LIMIT, 'offset': 0, 'addresses': addresses_lower, 'balance': '0', } while True: try: result = self.graph.query( querystr=querystr, param_types=param_types, param_values=param_values, ) except RemoteError as e: self.msg_aggregator.add_error( SUBGRAPH_REMOTE_ERROR_MSG.format(error_msg=str(e))) raise result_data = result['liquidityPositions'] for lp in result_data: lp_pair = lp['pair'] lp_total_supply = FVal(lp_pair['totalSupply']) user_lp_balance = FVal(lp['liquidityTokenBalance']) try: user_address = deserialize_ethereum_address( lp['user']['id']) lp_address = deserialize_ethereum_address(lp_pair['id']) except DeserializationError as e: msg = ( f'Failed to Deserialize address. Skipping pool {lp_pair}' f'with user address {lp["user"]["id"]}') log.error(msg) raise RemoteError(msg) from e # Insert LP tokens reserves within tokens dicts token0 = lp_pair['token0'] token0['total_amount'] = lp_pair['reserve0'] token1 = lp_pair['token1'] token1['total_amount'] = lp_pair['reserve1'] liquidity_pool_assets = [] for token in token0, token1: # Get the token <EthereumToken> or <UnknownEthereumToken> try: deserialized_eth_address = deserialize_ethereum_address( token['id']) except DeserializationError as e: msg = ( f'Failed to deserialize token address {token["id"]}' f'Bad token address in lp pair came from the graph.' ) log.error(msg) raise RemoteError(msg) from e asset = get_ethereum_token( symbol=token['symbol'], ethereum_address=deserialized_eth_address, name=token['name'], decimals=int(token['decimals']), ) # Classify the asset either as known or unknown if isinstance(asset, EthereumToken): known_assets.add(asset) elif isinstance(asset, UnknownEthereumToken): unknown_assets.add(asset) # Estimate the underlying asset total_amount asset_total_amount = FVal(token['total_amount']) user_asset_balance = (user_lp_balance / lp_total_supply * asset_total_amount) liquidity_pool_asset = LiquidityPoolAsset( asset=asset, total_amount=asset_total_amount, user_balance=Balance(amount=user_asset_balance), ) liquidity_pool_assets.append(liquidity_pool_asset) liquidity_pool = LiquidityPool( address=lp_address, assets=liquidity_pool_assets, total_supply=lp_total_supply, user_balance=Balance(amount=user_lp_balance), ) address_balances[user_address].append(liquidity_pool) # Check whether an extra request is needed if len(result_data) < GRAPH_QUERY_LIMIT: break # Update pagination step param_values = { **param_values, 'offset': param_values['offset'] + GRAPH_QUERY_LIMIT, # type: ignore } protocol_balance = ProtocolBalance( address_balances=dict(address_balances), known_assets=known_assets, unknown_assets=unknown_assets, ) return protocol_balance