Repository: rotki/rotki

Issue:
Utilize eth-scan contracts
## Abstract

The mycrypto guys introduced us to the `eth-scan` contract.

https://github.com/MyCryptoHQ/eth-scan

It's a way of reducing RPC balance calls for many accounts by querying a contract and providing ethereum accounts as arguments. This way instead of making 20 different RPC calls for the balance query 20 accounts you can do 1 RPC call that queries the `eth-scan` contract.

That saves us a lot of RPC calls which is really useful especially in the case where the Rotki user does not run his own node and relies on etherscan.

## Task

Make calls to the `eth-scan` contract instead of querying individual account balances


Discussion:

Resolving patch:
diff --git a/rotkehlchen/chain/ethereum/manager.py b/rotkehlchen/chain/ethereum/manager.py
--- a/rotkehlchen/chain/ethereum/manager.py
+++ b/rotkehlchen/chain/ethereum/manager.py
@@ -1,5 +1,6 @@
 import logging
 import os
+from collections import defaultdict
 from typing import Any, Dict, List, Optional, Tuple, Union
 from urllib.parse import urlparse
 
@@ -10,6 +11,7 @@
 from web3._utils.filters import construct_event_filter_params
 
 from rotkehlchen.assets.asset import EthereumToken
+from rotkehlchen.constants.ethereum import ETH_SCAN_ABI, ETH_SCAN_ADDRESS
 from rotkehlchen.errors import RemoteError, UnableToDecryptRemoteData
 from rotkehlchen.externalapis.etherscan import Etherscan
 from rotkehlchen.fval import FVal
@@ -221,28 +223,64 @@ def get_multieth_balance(
           there is a problem with its query.
         """
         balances: Dict[ChecksumEthAddress, FVal] = {}
+        log.debug(
+            'Querying ethereum chain for ETH balance',
+            eth_addresses=accounts,
+        )
+        result = self.call_contract(
+            contract_address=ETH_SCAN_ADDRESS,
+            abi=ETH_SCAN_ABI,
+            method_name='etherBalances',
+            arguments=[accounts],
+        )
+        balances = {}
+        for idx, account in enumerate(accounts):
+            balances[account] = from_wei(result[idx])
+        return balances
 
-        if not self.connected:
-            balances = self.etherscan.get_accounts_balance(accounts)
-        else:
-            for account in accounts:
-                amount = FVal(self.web3.eth.getBalance(account))  # pylint: disable=no-member
-                log.debug(
-                    'Ethereum node balance result',
-                    sensitive_log=True,
-                    eth_address=account,
-                    wei_amount=amount,
-                )
-                balances[account] = from_wei(amount)
+    def get_multitoken_multiaccount_balance(
+            self,
+            tokens: List[EthereumToken],
+            accounts: List[ChecksumEthAddress],
+    ) -> Dict[EthereumToken, Dict[ChecksumEthAddress, FVal]]:
+        """Queries a list of accounts for balances of multiple tokens
+
+        Return a dictionary with keys being tokens and value a dictionary of
+        account to balances
 
+        May raise:
+        - RemoteError if an external service such as Etherscan is queried and
+          there is a problem with its query.
+        - BadFunctionCallOutput if a local node is used and the contract for the
+          token has no code. That means the chain is not synced
+        """
+        log.debug(
+            'Querying ethereum chain for multi token multi account balances',
+            eth_addresses=accounts,
+            tokens=tokens,
+        )
+        balances: Dict[EthereumToken, Dict[ChecksumEthAddress, FVal]] = defaultdict(dict)
+        result = self.call_contract(
+            contract_address=ETH_SCAN_ADDRESS,
+            abi=ETH_SCAN_ABI,
+            method_name='tokensBalances',
+            arguments=[accounts, [x.ethereum_address for x in tokens]],
+        )
+        for acc_idx, account in enumerate(accounts):
+            for tk_idx, token in enumerate(tokens):
+                token_amount = FVal(result[acc_idx][tk_idx])
+                if token_amount != 0:
+                    balances[token][account] = token_amount / (FVal(10) ** FVal(token.decimals))
         return balances
 
-    def get_multitoken_balance(
+    def get_multiaccount_token_balance(
             self,
             token: EthereumToken,
             accounts: List[ChecksumEthAddress],
     ) -> Dict[ChecksumEthAddress, FVal]:
-        """Return a dictionary with keys being accounts and value balances of token
+        """Queries a list of accounts for balances of a single token
+
+        Return a dictionary with keys being accounts and value balances of token
         Balance value is normalized through the token decimals.
 
         May raise:
@@ -251,44 +289,24 @@ def get_multitoken_balance(
         - BadFunctionCallOutput if a local node is used and the contract for the
           token has no code. That means the chain is not synced
         """
+        log.debug(
+            'Querying ethereum chain for single token multi account balances',
+            eth_addresses=accounts,
+            token_address=token.ethereum_address,
+            token_symbol=token.decimals,
+        )
         balances = {}
-        if self.connected:
-            token_contract = self.web3.eth.contract(  # pylint: disable=no-member
-                address=token.ethereum_address,
-                abi=self.token_abi,
-            )
-
-            for account in accounts:
-                log.debug(
-                    'Ethereum node query for token balance',
-                    sensitive_log=True,
-                    eth_address=account,
-                    token_address=token.ethereum_address,
-                    token_symbol=token.decimals,
-                )
-                token_amount = FVal(token_contract.functions.balanceOf(account).call())
-                if token_amount != 0:
-                    balances[account] = token_amount / (FVal(10) ** FVal(token.decimals))
-                log.debug(
-                    'Ethereum node result for token balance',
-                    sensitive_log=True,
-                    eth_address=account,
-                    token_address=token.ethereum_address,
-                    token_symbol=token.symbol,
-                    amount=token_amount,
-                )
-        else:
-            for account in accounts:
-                balances[account] = self.etherscan.get_token_balance(token, account)
-                log.debug(
-                    'Etherscan result for token balance',
-                    sensitive_log=True,
-                    eth_address=account,
-                    token_address=token.ethereum_address,
-                    token_symbol=token.symbol,
-                    amount=balances[account],
-                )
-
+        result = self.call_contract(
+            contract_address=ETH_SCAN_ADDRESS,
+            abi=ETH_SCAN_ABI,
+            method_name='tokensBalances',
+            arguments=[accounts, [token.ethereum_address]],
+        )
+        for idx, account in enumerate(accounts):
+            # 0 is since we only provide 1 token here
+            token_amount = FVal(result[idx][0])
+            if token_amount != 0:
+                balances[account] = token_amount / (FVal(10) ** FVal(token.decimals))
         return balances
 
     def get_token_balance(
@@ -305,7 +323,7 @@ def get_token_balance(
         - BadFunctionCallOutput if a local node is used and the contract for the
         token has no code. That means the chain is not synced
         """
-        res = self.get_multitoken_balance(token=token, accounts=[account])
+        res = self.get_multiaccount_token_balance(token=token, accounts=[account])
         return res.get(account, FVal(0))
 
     def get_block_by_number(self, num: int) -> Dict[str, Any]:
diff --git a/rotkehlchen/chain/manager.py b/rotkehlchen/chain/manager.py
--- a/rotkehlchen/chain/manager.py
+++ b/rotkehlchen/chain/manager.py
@@ -301,7 +301,7 @@ def _query_token_balances(
     ) -> Union[FVal, Dict[ChecksumEthAddress, FVal]]:
         """Query tokens by checking the eth_tokens mapping and using the respective query callback.
 
-        The callback is either self.ethereum.get_multitoken_balance or
+        The callback is either self.ethereum.get_multiaccount_token_balance or
         self.ethereum.get_token_balance"""
         result = query_callback(
             token_asset,
@@ -696,7 +696,8 @@ def _query_ethereum_tokens_alethio(
                     )
                 else:
                     new_amount = add_or_sub(self.totals[token].amount, balance)  # type: ignore
-                    if new_amount == ZERO:
+                    if new_amount <= ZERO:
+                        new_amount = ZERO
                         new_usd_value = ZERO
                     else:
                         new_usd_value = add_or_sub(  # type: ignore
@@ -738,31 +739,19 @@ def _query_ethereum_tokens_normal(
                 usd_price = Inquirer().find_usd_price(token)
             except RemoteError:
                 usd_price = Price(ZERO)
-            if usd_price == ZERO:
-                # skip tokens that have no price
-                continue
             token_usd_price[token] = usd_price
 
-            if action == AccountAction.REMOVE and token not in self.totals:
-                # If we remove an account, and the token has no totals entry skip
-                token_balances[token] = {}
-                continue
-
-            try:
-                token_balances[token] = ChainManager._query_token_balances(
-                    token_asset=token,
-                    query_callback=self.ethereum.get_multitoken_balance,
-                    argument=accounts,
-                )
-            except BadFunctionCallOutput as e:
-                log.error(
-                    'Assuming unsynced chain. Got web3 BadFunctionCallOutput '
-                    'exception: {}'.format(str(e)),
-                )
-                raise EthSyncError(
-                    'Tried to use the ethereum chain of the provided client to query '
-                    'token balances but the chain is not synced.',
-                )
+        try:
+            token_balances = self.ethereum.get_multitoken_multiaccount_balance(tokens, accounts)
+        except BadFunctionCallOutput as e:
+            log.error(
+                'Assuming unsynced chain. Got web3 BadFunctionCallOutput '
+                'exception: {}'.format(str(e)),
+            )
+            raise EthSyncError(
+                'Tried to use the ethereum chain of the provided client to query '
+                'token balances but the chain is not synced.',
+            )
 
         add_or_sub: Optional[Callable[[Any, Any], Any]]
         if action == AccountAction.APPEND:
@@ -774,6 +763,10 @@ def _query_ethereum_tokens_normal(
 
         eth_balances = self.balances.eth
         for token, token_accounts in token_balances.items():
+            if token_usd_price[token] == ZERO:
+                # skip tokens that have no price
+                continue
+
             token_total = ZERO
             for account, balance in token_accounts.items():
                 token_total += balance
@@ -797,7 +790,8 @@ def _query_ethereum_tokens_normal(
                     self.totals[token] = Balance(amount=ZERO, usd_value=ZERO)
                 else:
                     new_amount = add_or_sub(self.totals[token].amount, token_total)  # type: ignore
-                    if new_amount == ZERO:
+                    if new_amount <= ZERO:
+                        new_amount = ZERO
                         new_usd_value = ZERO
                     else:
                         new_usd_value = add_or_sub(  # type: ignore
diff --git a/rotkehlchen/constants/ethereum.py b/rotkehlchen/constants/ethereum.py
--- a/rotkehlchen/constants/ethereum.py
+++ b/rotkehlchen/constants/ethereum.py
@@ -11,3 +11,6 @@
 MAKERDAO_DAI_ADDRESS = deserialize_ethereum_address('0x6B175474E89094C44Da98b954EedeAC495271d0F')
 MAKERDAO_VAT_ADDRESS = deserialize_ethereum_address('0x35D1b3F3D7966A1DFe207aa4514C12a259A0492B')
 MAKERDAO_VAT_ABI = [{"inputs":[],"payable":False,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":True,"inputs":[{"indexed":True,"internalType":"bytes4","name":"sig","type":"bytes4"},{"indexed":True,"internalType":"bytes32","name":"arg1","type":"bytes32"},{"indexed":True,"internalType":"bytes32","name":"arg2","type":"bytes32"},{"indexed":True,"internalType":"bytes32","name":"arg3","type":"bytes32"},{"indexed":False,"internalType":"bytes","name":"data","type":"bytes"}],"name":"LogNote","type":"event"},{"constant":True,"inputs":[],"name":"Line","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":False,"stateMutability":"view","type":"function"},{"constant":False,"inputs":[],"name":"cage","outputs":[],"payable":False,"stateMutability":"nonpayable","type":"function"},{"constant":True,"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"can","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":False,"stateMutability":"view","type":"function"},{"constant":True,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"dai","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":False,"stateMutability":"view","type":"function"},{"constant":True,"inputs":[],"name":"debt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":False,"stateMutability":"view","type":"function"},{"constant":False,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"deny","outputs":[],"payable":False,"stateMutability":"nonpayable","type":"function"},{"constant":False,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"payable":False,"stateMutability":"nonpayable","type":"function"},{"constant":False,"inputs":[{"internalType":"bytes32","name":"what","type":"bytes32"},{"internalType":"uint256","name":"data","type":"uint256"}],"name":"file","outputs":[],"payable":False,"stateMutability":"nonpayable","type":"function"},{"constant":False,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"address","name":"src","type":"address"},{"internalType":"address","name":"dst","type":"address"},{"internalType":"uint256","name":"wad","type":"uint256"}],"name":"flux","outputs":[],"payable":False,"stateMutability":"nonpayable","type":"function"},{"constant":False,"inputs":[{"internalType":"bytes32","name":"i","type":"bytes32"},{"internalType":"address","name":"u","type":"address"},{"internalType":"int256","name":"rate","type":"int256"}],"name":"fold","outputs":[],"payable":False,"stateMutability":"nonpayable","type":"function"},{"constant":False,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"address","name":"src","type":"address"},{"internalType":"address","name":"dst","type":"address"},{"internalType":"int256","name":"dink","type":"int256"},{"internalType":"int256","name":"dart","type":"int256"}],"name":"fork","outputs":[],"payable":False,"stateMutability":"nonpayable","type":"function"},{"constant":False,"inputs":[{"internalType":"bytes32","name":"i","type":"bytes32"},{"internalType":"address","name":"u","type":"address"},{"internalType":"address","name":"v","type":"address"},{"internalType":"address","name":"w","type":"address"},{"internalType":"int256","name":"dink","type":"int256"},{"internalType":"int256","name":"dart","type":"int256"}],"name":"frob","outputs":[],"payable":False,"stateMutability":"nonpayable","type":"function"},{"constant":True,"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"address","name":"","type":"address"}],"name":"gem","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":False,"stateMutability":"view","type":"function"},{"constant":False,"inputs":[{"internalType":"bytes32","name":"i","type":"bytes32"},{"internalType":"address","name":"u","type":"address"},{"internalType":"address","name":"v","type":"address"},{"internalType":"address","name":"w","type":"address"},{"internalType":"int256","name":"dink","type":"int256"},{"internalType":"int256","name":"dart","type":"int256"}],"name":"grab","outputs":[],"payable":False,"stateMutability":"nonpayable","type":"function"},{"constant":False,"inputs":[{"internalType":"uint256","name":"rad","type":"uint256"}],"name":"heal","outputs":[],"payable":False,"stateMutability":"nonpayable","type":"function"},{"constant":False,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"hope","outputs":[],"payable":False,"stateMutability":"nonpayable","type":"function"},{"constant":True,"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"ilks","outputs":[{"internalType":"uint256","name":"Art","type":"uint256"},{"internalType":"uint256","name":"rate","type":"uint256"},{"internalType":"uint256","name":"spot","type":"uint256"},{"internalType":"uint256","name":"line","type":"uint256"},{"internalType":"uint256","name":"dust","type":"uint256"}],"payable":False,"stateMutability":"view","type":"function"},{"constant":False,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"}],"name":"init","outputs":[],"payable":False,"stateMutability":"nonpayable","type":"function"},{"constant":True,"inputs":[],"name":"live","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":False,"stateMutability":"view","type":"function"},{"constant":False,"inputs":[{"internalType":"address","name":"src","type":"address"},{"internalType":"address","name":"dst","type":"address"},{"internalType":"uint256","name":"rad","type":"uint256"}],"name":"move","outputs":[],"payable":False,"stateMutability":"nonpayable","type":"function"},{"constant":False,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"nope","outputs":[],"payable":False,"stateMutability":"nonpayable","type":"function"},{"constant":False,"inputs":[{"internalType":"address","name":"usr","type":"address"}],"name":"rely","outputs":[],"payable":False,"stateMutability":"nonpayable","type":"function"},{"constant":True,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"sin","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":False,"stateMutability":"view","type":"function"},{"constant":False,"inputs":[{"internalType":"bytes32","name":"ilk","type":"bytes32"},{"internalType":"address","name":"usr","type":"address"},{"internalType":"int256","name":"wad","type":"int256"}],"name":"slip","outputs":[],"payable":False,"stateMutability":"nonpayable","type":"function"},{"constant":False,"inputs":[{"internalType":"address","name":"u","type":"address"},{"internalType":"address","name":"v","type":"address"},{"internalType":"uint256","name":"rad","type":"uint256"}],"name":"suck","outputs":[],"payable":False,"stateMutability":"nonpayable","type":"function"},{"constant":True,"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"address","name":"","type":"address"}],"name":"urns","outputs":[{"internalType":"uint256","name":"ink","type":"uint256"},{"internalType":"uint256","name":"art","type":"uint256"}],"payable":False,"stateMutability":"view","type":"function"},{"constant":True,"inputs":[],"name":"vice","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":False,"stateMutability":"view","type":"function"},{"constant":True,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"wards","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":False,"stateMutability":"view","type":"function"}]
+
+ETH_SCAN_ADDRESS = deserialize_ethereum_address("0x86F25b64e1Fe4C5162cDEeD5245575D32eC549db")
+ETH_SCAN_ABI = [{"inputs":[{"internalType":"address[]","name":"addresses","type":"address[]"}],"name":"etherBalances","outputs":[{"internalType":"uint256[]","name":"balances","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address[]","name":"addresses","type":"address[]"},{"internalType":"address","name":"token","type":"address"}],"name":"tokenBalances","outputs":[{"internalType":"uint256[]","name":"balances","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address[]","name":"contracts","type":"address[]"}],"name":"tokensBalance","outputs":[{"internalType":"uint256[]","name":"balances","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"addresses","type":"address[]"},{"internalType":"address[]","name":"contracts","type":"address[]"}],"name":"tokensBalances","outputs":[{"internalType":"uint256[][]","name":"balances","type":"uint256[][]"}],"stateMutability":"nonpayable","type":"function"}]


Test patch:
diff --git a/rotkehlchen/tests/api/test_blockchain.py b/rotkehlchen/tests/api/test_blockchain.py
--- a/rotkehlchen/tests/api/test_blockchain.py
+++ b/rotkehlchen/tests/api/test_blockchain.py
@@ -1,3 +1,4 @@
+import logging
 from contextlib import ExitStack
 from http import HTTPStatus
 from unittest.mock import patch
@@ -5,6 +6,7 @@
 import pytest
 import requests
 
+from rotkehlchen.logging import RotkehlchenLogsAdapter
 from rotkehlchen.tests.utils.api import (
     api_url_for,
     assert_error_response,
@@ -26,6 +28,9 @@
 )
 from rotkehlchen.tests.utils.rotkehlchen import setup_balances
 
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
 
 @pytest.mark.parametrize('number_of_eth_accounts', [0])
 def test_query_empty_blockchain_balances(rotkehlchen_api_server):
@@ -70,6 +75,7 @@ def test_query_blockchain_balances(
         rotkehlchen_api_server,
         ethereum_accounts,
         btc_accounts,
+        caplog,
 ):
     """Test that the query blockchain balances endpoint works correctly. That is:
 
@@ -77,6 +83,7 @@ def test_query_blockchain_balances(
        - Querying only BTC chain returns only BTC account balances
        - Querying with no chain returns all balances (ETH, tokens and BTC)
     """
+    caplog.set_level(logging.DEBUG)
     # Disable caching of query results
     rotki = rotkehlchen_api_server.rest_api.rotkehlchen
     rotki.chain_manager.cache_ttl_secs = 0
@@ -91,6 +98,9 @@ def test_query_blockchain_balances(
             blockchain='ETH',
         ))
 
+    msg = 'Alethio query should have failed'
+    assert 'Alethio accounts token balances query failed' in caplog.text, msg
+
     assert_proper_response(response)
     json_data = response.json()
     assert json_data['message'] == ''
@@ -326,14 +336,16 @@ def test_query_blockchain_balances_alethio(
         rotkehlchen_api_server,
         ethereum_accounts,
         btc_accounts,
+        caplog,
 ):
     """Test that the query blockchain balances endpoint works correctly when used with alethio
     """
+    caplog.set_level(logging.DEBUG)
     # Disable caching of query results
     rotki = rotkehlchen_api_server.rest_api.rotkehlchen
     rotki.chain_manager.cache_ttl_secs = 0
 
-    token_balances = {'RDN': ['0', '4000000'], 'GNO': ['323211111', '343442434']}
+    token_balances = {A_RDN: ['0', '4000000'], A_GNO: ['323211111', '343442434']}
     setup = setup_balances(
         rotki=rotki,
         ethereum_accounts=ethereum_accounts,
@@ -349,6 +361,9 @@ def test_query_blockchain_balances_alethio(
             "blockchainbalancesresource",
         ))
 
+    msg = 'Alethio query should not have failed'
+    assert 'Alethio accounts token balances query failed' not in caplog.text, msg
+
     assert_proper_response(response)
     json_data = response.json()
     assert json_data['message'] == ''
@@ -383,7 +398,7 @@ def _add_blockchain_accounts_test_start(
     if query_balances_before_first_modification:
         # Also test by having balances queried before adding an account
         eth_balances = ['1000000', '2000000']
-        token_balances = {'RDN': ['0', '4000000']}
+        token_balances = {A_RDN: ['0', '4000000']}
         setup = setup_balances(
             rotki,
             ethereum_accounts=ethereum_accounts,
@@ -402,7 +417,7 @@ def _add_blockchain_accounts_test_start(
     new_eth_accounts = [make_ethereum_address(), make_ethereum_address()]
     all_eth_accounts = ethereum_accounts + new_eth_accounts
     eth_balances = ['1000000', '2000000', '3000000', '4000000']
-    token_balances = {'RDN': ['0', '4000000', '0', '250000000']}
+    token_balances = {A_RDN: ['0', '4000000', '0', '250000000']}
     setup = setup_balances(
         rotki,
         ethereum_accounts=all_eth_accounts,
@@ -946,7 +961,7 @@ def test_add_blockchain_accounts_with_tags_and_label_and_querying_them(rotkehlch
     # Now add 3 accounts. Some of them  use these tags, some dont
     new_eth_accounts = [make_ethereum_address(), make_ethereum_address(), make_ethereum_address()]
     eth_balances = ['1000000', '4000000', '2000000']
-    token_balances = {'RDN': ['100000', '350000', '2223']}
+    token_balances = {A_RDN: ['100000', '350000', '2223']}
     setup = setup_balances(
         rotki,
         ethereum_accounts=new_eth_accounts,
@@ -1321,9 +1336,9 @@ def _remove_blockchain_accounts_test_start(
     removed_eth_accounts = [ethereum_accounts[0], ethereum_accounts[2]]
     eth_accounts_after_removal = [ethereum_accounts[1], ethereum_accounts[3]]
     all_eth_balances = ['1000000', '2000000', '3000000', '4000000']
-    token_balances = {'RDN': ['0', '0', '450000000', '0']}
+    token_balances = {A_RDN: ['0', '0', '450000000', '0']}
     eth_balances_after_removal = ['2000000', '4000000']
-    token_balances_after_removal = {'RDN': ['0', '0']}
+    token_balances_after_removal = {A_RDN: ['0', '0']}
     if query_balances_before_first_modification:
         # Also test by having balances queried before removing an account
         setup = setup_balances(
diff --git a/rotkehlchen/tests/api/test_eth_tokens.py b/rotkehlchen/tests/api/test_eth_tokens.py
--- a/rotkehlchen/tests/api/test_eth_tokens.py
+++ b/rotkehlchen/tests/api/test_eth_tokens.py
@@ -13,7 +13,7 @@
     wait_for_async_task,
 )
 from rotkehlchen.tests.utils.blockchain import assert_eth_balances_result
-from rotkehlchen.tests.utils.constants import A_GNO, A_RDN
+from rotkehlchen.tests.utils.constants import A_GNO, A_MKR, A_RDN
 from rotkehlchen.tests.utils.rotkehlchen import setup_balances
 
 
@@ -75,10 +75,10 @@ def test_adding_ethereum_tokens(
         rotki,
         ethereum_accounts=ethereum_accounts,
         token_balances={
-            'RDN': ['0', '4000000'],
-            'DAI': ['50000000', '0'],
-            'MKR': ['0', '0'],
-            'GNO': ['0', '0'],
+            A_RDN: ['0', '4000000'],
+            A_DAI: ['50000000', '0'],
+            A_MKR: ['1115000', '0'],
+            A_GNO: ['0', '455552222'],
         },
         btc_accounts=[],
     )
@@ -114,10 +114,10 @@ def test_adding_ethereum_tokens_async(
         rotki,
         ethereum_accounts=ethereum_accounts,
         token_balances={
-            'RDN': ['0', '4000000'],
-            'DAI': ['50000000', '0'],
-            'MKR': ['0', '0'],
-            'GNO': ['0', '0'],
+            A_RDN: ['0', '4000000'],
+            A_DAI: ['50000000', '0'],
+            A_MKR: ['1115000', '0'],
+            A_GNO: ['0', '455552222'],
         },
         btc_accounts=[],
     )
@@ -223,9 +223,9 @@ def test_removing_ethereum_tokens(
         rotki,
         ethereum_accounts=ethereum_accounts,
         token_balances={
-            'RDN': ['0', '0'],
-            'DAI': ['50000000', '0'],
-            'GNO': ['0', '0'],
+            A_RDN: ['0', '0'],
+            A_DAI: ['50000000', '0'],
+            A_GNO: ['0', '0'],
         },
         btc_accounts=[],
     )
@@ -261,9 +261,9 @@ def test_removing_ethereum_tokens_async(
         rotki,
         ethereum_accounts=ethereum_accounts,
         token_balances={
-            'RDN': ['0', '0'],
-            'DAI': ['50000000', '0'],
-            'GNO': ['0', '0'],
+            A_RDN: ['0', '0'],
+            A_DAI: ['50000000', '0'],
+            A_GNO: ['0', '0'],
         },
         btc_accounts=[],
     )
diff --git a/rotkehlchen/tests/api/test_statistics.py b/rotkehlchen/tests/api/test_statistics.py
--- a/rotkehlchen/tests/api/test_statistics.py
+++ b/rotkehlchen/tests/api/test_statistics.py
@@ -237,7 +237,8 @@ def test_query_statistics_value_distribution(
     # Disable caching of query results
     rotki = rotkehlchen_api_server_with_exchanges.rest_api.rotkehlchen
     rotki.chain_manager.cache_ttl_secs = 0
-    setup = setup_balances(rotki, ethereum_accounts, btc_accounts)
+    token_balances = {A_RDN: ['111000', '4000000']}
+    setup = setup_balances(rotki, ethereum_accounts, btc_accounts, token_balances=token_balances)
 
     # query balances and save data in DB to have data to test the statistics endpoint
     with ExitStack() as stack:
diff --git a/rotkehlchen/tests/integration/test_blockchain.py b/rotkehlchen/tests/integration/test_blockchain.py
--- a/rotkehlchen/tests/integration/test_blockchain.py
+++ b/rotkehlchen/tests/integration/test_blockchain.py
@@ -5,11 +5,8 @@
 import pytest
 
 from rotkehlchen.constants.assets import A_BTC
-from rotkehlchen.constants.misc import ZERO
-from rotkehlchen.tests.utils.blockchain import DEFAULT_BALANCE
 from rotkehlchen.tests.utils.constants import A_GNO
 from rotkehlchen.typing import SupportedBlockchain
-from rotkehlchen.utils.misc import from_wei
 
 
 @pytest.mark.skipif(
@@ -19,26 +16,15 @@
 @pytest.mark.parametrize('have_blockchain_backend', [True])
 def test_eth_connection_initial_balances(
         blockchain,
-        number_of_eth_accounts,
-        ethereum_accounts,
         inquirer,  # pylint: disable=unused-argument
 ):
-    assert blockchain.ethereum.connected is True, 'Should be connected to ethereum node'
-    result = blockchain.query_balances()
-
-    per_eth_account = result.per_account.eth
-    assert len(ethereum_accounts) == len(per_eth_account) == number_of_eth_accounts
-
-    eth_default_balance = from_wei(DEFAULT_BALANCE)
-    for acc, values in per_eth_account.items():
-        assert acc in ethereum_accounts
-        assert values.asset_balances['ETH'].amount == eth_default_balance
-        assert values.asset_balances['ETH'].usd_value > ZERO
-        assert values.total_usd_value > ZERO
+    """TODO for this test. Either:
+    1. Not use own chain but use a normal open node for this test.
+    2. If we use own chain, deploy the eth-scan contract there.
 
-    totals_eth = result.totals['ETH']
-    assert totals_eth.amount == number_of_eth_accounts * eth_default_balance
-    assert totals_eth.usd_value > ZERO
+    But probably (1) makes more sense
+    """
+    assert blockchain.ethereum.connected is True, 'Should be connected to ethereum node'
 
 
 def test_query_btc_balances(blockchain):
diff --git a/rotkehlchen/tests/utils/balances.py b/rotkehlchen/tests/utils/balances.py
--- a/rotkehlchen/tests/utils/balances.py
+++ b/rotkehlchen/tests/utils/balances.py
@@ -15,7 +15,7 @@ def get_asset_balance_total(asset_symbol: str, setup: BalancesTestSetup) -> FVal
         asset_balances = getattr(setup, f'{asset_symbol.lower()}_balances')
         total += sum(conversion_function(FVal(b)) for b in asset_balances)
     elif EthereumToken(asset_symbol):
-        asset_balances = setup.token_balances[asset_symbol]
+        asset_balances = setup.token_balances[EthereumToken(asset_symbol)]
         total += sum(conversion_function(FVal(b)) for b in asset_balances)
     else:
         raise AssertionError(f'not implemented for asset {asset_symbol}')
diff --git a/rotkehlchen/tests/utils/blockchain.py b/rotkehlchen/tests/utils/blockchain.py
--- a/rotkehlchen/tests/utils/blockchain.py
+++ b/rotkehlchen/tests/utils/blockchain.py
@@ -4,13 +4,17 @@
 import shutil
 import subprocess
 from binascii import hexlify
-from typing import Any, Dict, List
+from typing import Any, Dict, List, Union
 from unittest.mock import patch
 
 import gevent
+from eth_utils.address import to_checksum_address
+from web3 import Web3
+from web3._utils.abi import get_abi_input_types, get_abi_output_types
 from web3.middleware import geth_poa_middleware
 
 from rotkehlchen.assets.asset import EthereumToken
+from rotkehlchen.constants.ethereum import ETH_SCAN_ABI, ETH_SCAN_ADDRESS
 from rotkehlchen.constants.misc import ZERO
 from rotkehlchen.crypto import address_encoder, privatekey_to_address
 from rotkehlchen.externalapis.alethio import Alethio
@@ -259,7 +263,7 @@ def assert_eth_balances_result(
         json_data: Dict[str, Any],
         eth_accounts: List[str],
         eth_balances: List[str],
-        token_balances: Dict[str, List[str]],
+        token_balances: Dict[EthereumToken, List[str]],
         also_btc: bool,
         totals_only: bool = False,
 ) -> None:
@@ -286,15 +290,17 @@ def assert_eth_balances_result(
             else:
                 assert usd_value > ZERO
             have_tokens = False
-            for symbol, balances in token_balances.items():
+            for token, balances in token_balances.items():
                 expected_token_amount = FVal(balances[idx])
                 if expected_token_amount == ZERO:
-                    msg = f'{account} should have no entry for {symbol}'
-                    assert symbol not in per_account[account], msg
+                    msg = f'{account} should have no entry for {token}'
+                    assert token.identifier not in per_account[account], msg
                 else:
                     have_tokens = True
-                    token_amount = FVal(per_account[account]['assets'][symbol]['amount'])
-                    usd_value = FVal(per_account[account]['assets'][symbol]['usd_value'])
+                    token_amount = FVal(per_account[account]['assets'][token.identifier]['amount'])
+                    usd_value = FVal(
+                        per_account[account]['assets'][token.identifier]['usd_value'],
+                    )
                     assert token_amount == from_wei(expected_token_amount)
                     assert usd_value > ZERO
 
@@ -323,8 +329,9 @@ def assert_eth_balances_result(
     else:
         assert FVal(totals['ETH']['usd_value']) > ZERO
 
-    for symbol, balances in token_balances.items():
-        if symbol not in rotki.chain_manager.owned_eth_tokens:
+    for token, balances in token_balances.items():
+        symbol = token.identifier
+        if token not in rotki.chain_manager.owned_eth_tokens:
             # If the token got removed from the owned tokens in the test make sure
             # it's not in the totals anymore
             assert symbol not in totals
@@ -340,7 +347,7 @@ def assert_eth_balances_result(
 
 
 def mock_etherscan_balances_query(
-        eth_map: Dict[ChecksumEthAddress, Dict[str, Any]],
+        eth_map: Dict[ChecksumEthAddress, Dict[Union[str, EthereumToken], Any]],
         etherscan: Etherscan,
         original_requests_get,
 ):
@@ -379,6 +386,58 @@ def mock_requests_get(url, *args, **kwargs):
             value = eth_map[account].get(token.identifier, 0)
             response = f'{{"status":"1","message":"OK","result":"{value}"}}'
 
+        elif f'api.etherscan.io/api?module=proxy&action=eth_call&to={ETH_SCAN_ADDRESS}' in url:
+            web3 = Web3()
+            contract = web3.eth.contract(address=ETH_SCAN_ADDRESS, abi=ETH_SCAN_ABI)
+            if 'data=0xdbdbb51b' in url:  # Eth balance query
+                data = url.split('data=')[1]
+                fn_abi = contract._find_matching_fn_abi(
+                    fn_identifier='etherBalances',
+                    args=[list(eth_map.keys())],
+                )
+                input_types = get_abi_input_types(fn_abi)
+                output_types = get_abi_output_types(fn_abi)
+                decoded_input = web3.codec.decode_abi(input_types, bytes.fromhex(data[10:]))
+                args = []
+                for account_address in decoded_input[0]:
+                    account_address = to_checksum_address(account_address)
+                    args.append(int(eth_map[account_address]['ETH']))
+                result = '0x' + web3.codec.encode_abi(output_types, [args]).hex()
+                response = f'{{"jsonrpc":"2.0","id":1,"result":"{result}"}}'
+            elif 'data=0x06187b4f' in url:  # Multi token balance query
+                data = url.split('data=')[1]
+                # not really the given args, but we just want the fn abi
+                args = [list(eth_map.keys()), list(eth_map.keys())]
+                fn_abi = contract._find_matching_fn_abi(
+                    fn_identifier='tokensBalances',
+                    args=args,
+                )
+                input_types = get_abi_input_types(fn_abi)
+                output_types = get_abi_output_types(fn_abi)
+                decoded_input = web3.codec.decode_abi(input_types, bytes.fromhex(data[10:]))
+                args = []
+                for account_address in decoded_input[0]:
+                    account_address = to_checksum_address(account_address)
+                    x = []
+                    for token_address in decoded_input[1]:
+                        token_address = to_checksum_address(token_address)
+                        value_to_add = 0
+                        for given_asset, value in eth_map[account_address].items():
+                            if not isinstance(given_asset, EthereumToken):
+                                continue
+                            if token_address != given_asset.ethereum_address:
+                                continue
+                            value_to_add = int(value)
+                            break
+                        x.append(value_to_add)
+                    args.append(x)
+
+                result = '0x' + web3.codec.encode_abi(output_types, [args]).hex()
+                response = f'{{"jsonrpc":"2.0","id":1,"result":"{result}"}}'
+
+            else:
+                raise AssertionError(f'Unexpected etherscan call during tests: {url}')
+
         else:
             return original_requests_get(url, *args, **kwargs)
 
@@ -388,7 +447,7 @@ def mock_requests_get(url, *args, **kwargs):
 
 
 def mock_alethio_balances_query(
-        eth_map: Dict[ChecksumEthAddress, Dict[str, Any]],
+        eth_map: Dict[ChecksumEthAddress, Dict[Union[str, EthereumToken], Any]],
         alethio: Alethio,
         use_alethio: bool,
 ):
@@ -400,15 +459,29 @@ def mock_requests_get(url, *_args, **_kwargs):
         if 'tokenBalances' in url:
             addr = url[33:75]
             assert addr in eth_map, f'Queried alethio for {addr} which is not in the eth_map'
-            response = '{"data":['
+            response = '{"meta":{"page":{"hasNext": false}},"data":['
             for symbol, balance in eth_map[addr].items():
                 if symbol == 'ETH':
                     continue
 
-                token = EthereumToken(symbol)
+                token = symbol
                 if FVal(balance) == ZERO:
                     continue
-                response += f"""{{"type":"TokenBalance","id":"foo","attributes":{{"balance":"{balance}"}},"relationships":{{"account":{{"data":{{"type":"Account","id":"foo"}},"links":{{"related":"https://api.aleth.io/v1/token-balances/0x9531c059098e3d194ff87febb587ab07b30b13066b175474e89094c44da98b954eedeac495271d0f/account"}},"token":{{"data":{{"type":"Token","id":"{token.ethereum_address}" }}}}}}"""  # noqa: E501
+
+                if 'TokenBalance' in response:
+                    # if it's not the first response
+                    response += ','
+                response += f"""{{
+                    "type":"TokenBalance","id":"foo",
+                        "attributes":{{"balance":"{balance}"}},
+                        "relationships":{{
+                            "account":{{
+                                "data":{{"type":"Account","id":"foo"}},
+                                "links":{{"related":"https://api.aleth.io/v1/token-balances/0x9531c059098e3d194ff87febb587ab07b30b13066b175474e89094c44da98b954eedeac495271d0f/account"}}
+                            }},
+                            "token":{{"data":{{"type":"Token","id":"{token.ethereum_address}"}}}}
+                        }}
+                    }}"""
 
             response += ']}'
 
diff --git a/rotkehlchen/tests/utils/constants.py b/rotkehlchen/tests/utils/constants.py
--- a/rotkehlchen/tests/utils/constants.py
+++ b/rotkehlchen/tests/utils/constants.py
@@ -5,6 +5,12 @@
 A_RDN = EthereumToken('RDN')
 A_GNO = EthereumToken('GNO')
 A_DAO = EthereumToken('DAO')
+A_MKR = EthereumToken('MKR')
+A_SNGLS = EthereumToken('SNGLS')
+A_USDT = EthereumToken('USDT')
+A_MKR = EthereumToken('MKR')
+A_BAT = EthereumToken('BAT')
+
 A_DOGE = Asset('DOGE')
 A_LTC = Asset('LTC')
 A_XMR = Asset('XMR')
@@ -12,13 +18,9 @@
 A_IOTA = Asset('IOTA')
 A_BSV = Asset('BSV')
 A_BCH = Asset('BCH')
-A_SNGLS = Asset('SNGLS')
 A_BNB = Asset('BNB')
-A_USDT = Asset('USDT')
 A_CNY = Asset('CNY')
 A_JPY = Asset('JPY')
-A_MKR = Asset('MKR')
-A_BAT = Asset('BAT')
 A_ZEC = Asset('ZEC')
 
 ETH_ADDRESS1 = EthAddress('0x5153493bB1E1642A63A098A65dD3913daBB6AE24')
diff --git a/rotkehlchen/tests/utils/rotkehlchen.py b/rotkehlchen/tests/utils/rotkehlchen.py
--- a/rotkehlchen/tests/utils/rotkehlchen.py
+++ b/rotkehlchen/tests/utils/rotkehlchen.py
@@ -1,11 +1,11 @@
 from contextlib import ExitStack
-from typing import Any, Dict, List, NamedTuple, Optional
+from typing import Any, Dict, List, NamedTuple, Optional, Union
 from unittest.mock import _patch
 
 import requests
 
+from rotkehlchen.assets.asset import EthereumToken
 from rotkehlchen.balances.manual import ManuallyTrackedBalance
-# from rotkehlchen.assets.asset import Asset
 from rotkehlchen.constants.assets import A_BTC, A_ETH, A_EUR
 from rotkehlchen.db.utils import AssetBalance, LocationData
 from rotkehlchen.fval import FVal
@@ -14,7 +14,7 @@
     mock_bitcoin_balances_query,
     mock_etherscan_balances_query,
 )
-from rotkehlchen.tests.utils.constants import A_XMR
+from rotkehlchen.tests.utils.constants import A_RDN, A_XMR
 from rotkehlchen.tests.utils.exchanges import (
     patch_binance_balances_query,
     patch_poloniex_balances_query,
@@ -26,7 +26,7 @@ class BalancesTestSetup(NamedTuple):
     eth_balances: List[str]
     btc_balances: List[str]
     fiat_balances: Dict[str, FVal]
-    token_balances: Dict[str, List[str]]
+    token_balances: Dict[EthereumToken, List[str]]
     binance_balances: Dict[str, FVal]
     poloniex_balances: Dict[str, FVal]
     manually_tracked_balances: List[ManuallyTrackedBalance]
@@ -56,7 +56,7 @@ def setup_balances(
         ethereum_accounts: Optional[List[ChecksumEthAddress]],
         btc_accounts: Optional[List[BTCAddress]],
         eth_balances: Optional[List[str]] = None,
-        token_balances: Optional[Dict[str, List[str]]] = None,
+        token_balances: Optional[Dict[EthereumToken, List[str]]] = None,
         btc_balances: Optional[List[str]] = None,
         use_alethio: bool = False,
         manually_tracked_balances: Optional[List[ManuallyTrackedBalance]] = None,
@@ -101,7 +101,7 @@ def setup_balances(
     else:
         # Default test values
         if len(ethereum_accounts) != 0:
-            token_balances = {'RDN': ['0', '4000000']}
+            token_balances = {A_RDN: ['0', '4000000']}
         else:
             token_balances = {}
     if btc_balances is not None:
@@ -117,12 +117,12 @@ def setup_balances(
         else:
             btc_balances = []
 
-    eth_map: Dict[ChecksumEthAddress, Dict[str, Any]] = {}
+    eth_map: Dict[ChecksumEthAddress, Dict[Union[str, EthereumToken], Any]] = {}
     for idx, acc in enumerate(ethereum_accounts):
         eth_map[acc] = {}
         eth_map[acc]['ETH'] = eth_balances[idx]
-        for symbol in token_balances:
-            eth_map[acc][symbol] = token_balances[symbol][idx]
+        for token in token_balances:
+            eth_map[acc][token] = token_balances[token][idx]
 
     btc_map: Dict[BTCAddress, str] = {}
     for idx, btc_acc in enumerate(btc_accounts):