Repository: rotki/rotki

Issue:
Support ENS domains for BTC
ENS domains support multichain (EIP-2304), therefore we can support adding a BTC address via an ENS domain.
Supported formats are: [base58check P2PKH and P2SH, and bech32 segwit)](https://github.com/ensdomains/address-encoder)

[ENS domain with a BTC address](https://app.ens.domains/name/bruno.eth)
[ENS BTC address encoder](https://github.com/ensdomains/address-encoder/blob/master/src/index.ts#L134)
[ENS tests for BTC addresses](https://github.com/ensdomains/address-encoder/blob/master/src/__tests__/index.test.ts#L13)
[EIP-2304 P2PKH, P2SH and SegWit](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2304.md#p2pkhversion)


Discussion:

Resolving patch:
diff --git a/rotkehlchen/api/v1/encoding.py b/rotkehlchen/api/v1/encoding.py
--- a/rotkehlchen/api/v1/encoding.py
+++ b/rotkehlchen/api/v1/encoding.py
@@ -13,7 +13,11 @@
 from rotkehlchen.assets.asset import Asset
 from rotkehlchen.balances.manual import ManuallyTrackedBalance
 from rotkehlchen.chain.bitcoin.hdkey import HDKey, XpubType
-from rotkehlchen.chain.bitcoin.utils import is_valid_btc_address, is_valid_derivation_path
+from rotkehlchen.chain.bitcoin.utils import (
+    is_valid_btc_address,
+    is_valid_derivation_path,
+    scriptpubkey_to_btc_address,
+)
 from rotkehlchen.chain.ethereum.manager import EthereumManager
 from rotkehlchen.chain.substrate.typing import KusamaAddress, SubstratePublicKey
 from rotkehlchen.chain.substrate.utils import (
@@ -22,7 +26,7 @@
 )
 from rotkehlchen.constants.misc import ZERO
 from rotkehlchen.db.settings import ModifiableDBSettings
-from rotkehlchen.errors import DeserializationError, UnknownAsset, XPUBError
+from rotkehlchen.errors import DeserializationError, EncodingError, UnknownAsset, XPUBError
 from rotkehlchen.exchanges.kraken import KrakenAccountType
 from rotkehlchen.exchanges.manager import SUPPORTED_EXCHANGES
 from rotkehlchen.fval import FVal
@@ -45,6 +49,7 @@
     ApiKey,
     ApiSecret,
     AssetAmount,
+    BTCAddress,
     ChecksumEthAddress,
     ExternalService,
     ExternalServiceApiCredentials,
@@ -1146,7 +1151,8 @@ def _validate_blockchain_account_schemas(
     elif data['blockchain'] == SupportedBlockchain.BITCOIN:
         for account_data in data['accounts']:
             address = address_getter(account_data)
-            if not is_valid_btc_address(address):
+            # ENS domain will be checked in the transformation step
+            if not address.endswith('.eth') and not is_valid_btc_address(address):
                 raise ValidationError(
                     f'Given value {address} is not a valid bitcoin address',
                     field_name='address',
@@ -1176,6 +1182,41 @@ def _validate_blockchain_account_schemas(
             given_addresses.add(address)
 
 
+def _transform_btc_address(
+        ethereum: EthereumManager,
+        given_address: str,
+) -> BTCAddress:
+    """Returns a SegWit/P2PKH/P2SH address (if existing) given an ENS domain.
+
+    NB: ENS domains for BTC store the scriptpubkey. Check EIP-2304.
+    """
+    if not given_address.endswith('.eth'):
+        return BTCAddress(given_address)
+
+    resolved_address = ethereum.ens_lookup(
+        given_address,
+        blockchain=SupportedBlockchain.BITCOIN,
+    )
+    if resolved_address is None:
+        raise ValidationError(
+            f'Given ENS address {given_address} could not be resolved for Bitcoin',
+            field_name='address',
+        ) from None
+
+    try:
+        address = scriptpubkey_to_btc_address(bytes.fromhex(resolved_address))
+    except EncodingError as e:
+        raise ValidationError(
+            f'Given ENS address {given_address} does not contain a valid Bitcoin '
+            f"scriptpubkey: {resolved_address}. Bitcoin address can't be obtained.",
+            field_name='address',
+        ) from e
+
+    log.debug(f'Resolved BTC ENS {given_address} to {address}')
+
+    return address
+
+
 def _transform_eth_address(
         ethereum: EthereumManager, given_address: str) -> ChecksumEthAddress:
     try:
@@ -1263,6 +1304,12 @@ def transform_data(  # pylint: disable=no-self-use
             data: Dict[str, Any],
             **_kwargs: Any,
     ) -> Any:
+        if data['blockchain'] == SupportedBlockchain.BITCOIN:
+            for idx, account in enumerate(data['accounts']):
+                data['accounts'][idx]['address'] = _transform_btc_address(
+                    ethereum=self.ethereum_manager,
+                    given_address=account['address'],
+                )
         if data['blockchain'] == SupportedBlockchain.ETHEREUM:
             for idx, account in enumerate(data['accounts']):
                 data['accounts'][idx]['address'] = _transform_eth_address(
@@ -1306,6 +1353,10 @@ def transform_data(  # pylint: disable=no-self-use
             data: Dict[str, Any],
             **_kwargs: Any,
     ) -> Any:
+        if data['blockchain'] == SupportedBlockchain.BITCOIN:
+            data['accounts'] = [
+                _transform_btc_address(self.ethereum_manager, x) for x in data['accounts']
+            ]
         if data['blockchain'] == SupportedBlockchain.ETHEREUM:
             data['accounts'] = [
                 _transform_eth_address(self.ethereum_manager, x) for x in data['accounts']
diff --git a/rotkehlchen/chain/bitcoin/utils.py b/rotkehlchen/chain/bitcoin/utils.py
--- a/rotkehlchen/chain/bitcoin/utils.py
+++ b/rotkehlchen/chain/bitcoin/utils.py
@@ -10,6 +10,17 @@
 BIP32_HARDEN: int = 0x80000000
 
 
+class OpCodes:
+    op_0 = b'\x00'
+    op_1 = b'\x51'
+    op_16 = b'\x60'
+    op_dup = b'\x76'
+    op_equal = b'\x87'
+    op_equalverify = b'\x88'
+    op_hash160 = b'\xa9'
+    op_checksig = b'\xac'
+
+
 def is_valid_btc_address(value: str) -> bool:
     return is_valid_base58_address(value) or is_valid_bech32_address(value)
 
@@ -146,3 +157,68 @@ def is_valid_derivation_path(path: Any) -> Tuple[bool, str]:
             return False, f'Found negative integer node {value} in xpub derivation path'
 
     return True, ''
+
+
+def scriptpubkey_to_p2pkh_address(data: bytes) -> BTCAddress:
+    """Return a P2PKH address given a scriptpubkey
+
+    P2PKH: OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
+    """
+    if (
+        data[0:1] != OpCodes.op_dup or
+        data[1:2] != OpCodes.op_hash160 or
+        data[-2:-1] != OpCodes.op_equalverify or
+        data[-1:] != OpCodes.op_checksig
+    ):
+        raise EncodingError(f'Invalid P2PKH scriptpubkey: {data.hex()}')
+
+    prefixed_hash = bytes.fromhex('00') + data[3:23]  # 20 byte pubkey hash
+    checksum = hashlib.sha256(hashlib.sha256(prefixed_hash).digest()).digest()
+    address = base58check.b58encode(prefixed_hash + checksum[:4])
+    return BTCAddress(address.decode('ascii'))
+
+
+def scriptpubkey_to_p2sh_address(data: bytes) -> BTCAddress:
+    """Return a P2SH address given a scriptpubkey
+
+    P2SH: OP_HASH160 <scriptHash> OP_EQUAL
+    """
+    if data[0:1] != OpCodes.op_hash160 or data[-1:] != OpCodes.op_equal:
+        raise EncodingError(f'Invalid P2SH scriptpubkey: {data.hex()}')
+
+    prefixed_hash = bytes.fromhex('05') + data[2:22]  # 20 byte pubkey hash
+    checksum = hashlib.sha256(hashlib.sha256(prefixed_hash).digest()).digest()
+    address = base58check.b58encode(prefixed_hash + checksum[:4])
+    return BTCAddress(address.decode('ascii'))
+
+
+def scriptpubkey_to_bech32_address(data: bytes) -> BTCAddress:
+    """Return a native SegWit (bech32) address given a scriptpubkey"""
+    version = data[0]
+    if OpCodes.op_1 <= data[0:1] <= OpCodes.op_16:
+        version -= 0x50
+    elif data[0:1] != OpCodes.op_0:
+        raise EncodingError(f'Invalid bech32 scriptpubkey: {data.hex()}')
+
+    address = bech32.encode('bc', version, data[2:])
+    if not address:  # should not happen
+        raise EncodingError('Could not derive bech32 address from given scriptpubkey')
+
+    return BTCAddress(address)
+
+
+def scriptpubkey_to_btc_address(data: bytes) -> BTCAddress:
+    """Return a Bitcoin address given a scriptpubkey.
+    Supported formats are: P2PKH, P2SH and native SegWit (bech32).
+
+    May raise EncodingError if the scriptpubkey is invalid.
+    """
+    first_op_code = data[0:1]
+
+    if first_op_code == OpCodes.op_dup:
+        return scriptpubkey_to_p2pkh_address(data)
+
+    if first_op_code == OpCodes.op_hash160:
+        return scriptpubkey_to_p2sh_address(data)
+
+    return scriptpubkey_to_bech32_address(data)


Test patch:
diff --git a/rotkehlchen/tests/api/test_balances.py b/rotkehlchen/tests/api/test_balances.py
--- a/rotkehlchen/tests/api/test_balances.py
+++ b/rotkehlchen/tests/api/test_balances.py
@@ -623,6 +623,7 @@ def test_balances_caching_mixup(
         assert result_btc['totals']['liabilities'] == {}
 
 
+@pytest.mark.parametrize('number_of_eth_accounts', [0])
 @pytest.mark.parametrize('kusama_manager_connect_at_start', [KUSAMA_TEST_NODES])
 @pytest.mark.parametrize('ksm_accounts', [[SUBSTRATE_ACC1_KSM_ADDR, SUBSTRATE_ACC2_KSM_ADDR]])
 def test_query_ksm_balances(rotkehlchen_api_server):
@@ -630,20 +631,29 @@ def test_query_ksm_balances(rotkehlchen_api_server):
     expected.
     """
     async_query = random.choice([False, True])
-
-    response = requests.get(
-        api_url_for(
-            rotkehlchen_api_server,
-            "named_blockchain_balances_resource",
-            blockchain=SupportedBlockchain.KUSAMA.value,
-        ),
-        json={'async_query': async_query},
+    setup = setup_balances(
+        rotki=rotkehlchen_api_server.rest_api.rotkehlchen,
+        ethereum_accounts=None,
+        btc_accounts=None,
+        eth_balances=None,
+        token_balances=None,
+        btc_balances=None,
     )
-    if async_query:
-        task_id = assert_ok_async_response(response)
-        result = wait_for_async_task_with_result(rotkehlchen_api_server, task_id)
-    else:
-        result = assert_proper_response_with_result(response)
+    with ExitStack() as stack:
+        setup.enter_blockchain_patches(stack)
+        response = requests.get(
+            api_url_for(
+                rotkehlchen_api_server,
+                "named_blockchain_balances_resource",
+                blockchain=SupportedBlockchain.KUSAMA.value,
+            ),
+            json={'async_query': async_query},
+        )
+        if async_query:
+            task_id = assert_ok_async_response(response)
+            result = wait_for_async_task_with_result(rotkehlchen_api_server, task_id)
+        else:
+            result = assert_proper_response_with_result(response)
 
     # Check per account
     account_1_balances = result['per_account']['KSM'][SUBSTRATE_ACC1_KSM_ADDR]
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
@@ -27,6 +27,7 @@
     compare_account_data,
 )
 from rotkehlchen.tests.utils.constants import A_RDN
+from rotkehlchen.tests.utils.ens import ENS_BRUNO, ENS_BRUNO_BTC_ADDR, ENS_BRUNO_KSM_ADDR
 from rotkehlchen.tests.utils.factories import (
     UNIT_BTC_ADDRESS1,
     UNIT_BTC_ADDRESS2,
@@ -35,8 +36,6 @@
 )
 from rotkehlchen.tests.utils.rotkehlchen import setup_balances
 from rotkehlchen.tests.utils.substrate import (
-    ENS_BRUNO,
-    ENS_BRUNO_KSM_ADDR,
     KUSAMA_TEST_NODES,
     SUBSTRATE_ACC1_DOT_ADDR,
     SUBSTRATE_ACC1_KSM_ADDR,
@@ -1626,17 +1625,75 @@ def test_remove_blockchain_account_with_tags_removes_mapping(rotkehlchen_api_ser
     assert query[0][1] == 'desktop'
 
 
+@pytest.mark.parametrize('number_of_eth_accounts', [0])
+def test_add_btc_blockchain_account_ens_domain(rotkehlchen_api_server):
+    """Test adding a Bitcoin blockchain account via ENS domain when there is none
+    in the db works as expected.
+    """
+    async_query = random.choice([False, True])
+
+    setup = setup_balances(
+        rotki=rotkehlchen_api_server.rest_api.rotkehlchen,
+        ethereum_accounts=None,
+        btc_accounts=[ENS_BRUNO_BTC_ADDR],
+        eth_balances=None,
+        token_balances=None,
+        btc_balances=['77700000000'],
+    )
+    with ExitStack() as stack:
+        setup.enter_blockchain_patches(stack)
+        response = requests.put(
+            api_url_for(
+                rotkehlchen_api_server,
+                "blockchainsaccountsresource",
+                blockchain=SupportedBlockchain.BITCOIN.value,
+            ),
+            json={
+                'accounts': [{'address': ENS_BRUNO}],
+                'async_query': async_query,
+            },
+        )
+        if async_query:
+            task_id = assert_ok_async_response(response)
+            result = wait_for_async_task_with_result(rotkehlchen_api_server, task_id)
+        else:
+            result = assert_proper_response_with_result(response)
+
+    # Check per account
+    asset_btc = result['per_account']['BTC']['standalone'][ENS_BRUNO_BTC_ADDR]
+    assert FVal(asset_btc['amount']) >= FVal('777.00000000')
+    assert FVal(asset_btc['usd_value']) >= FVal('1165.500000000')
+
+    # Check totals
+    assert 'liabilities' in result['totals']
+    total_btc = result['totals']['assets']['BTC']
+    assert FVal(total_btc['amount']) >= FVal('777.00000000')
+    assert FVal(total_btc['usd_value']) >= FVal('1165.500000000')
+
+
+@pytest.mark.parametrize('number_of_eth_accounts', [0])
 def test_add_ksm_blockchain_account_invalid(rotkehlchen_api_server):
     """Test adding an invalid Kusama blockchain account works as expected.
     """
-    response = requests.put(
-        api_url_for(
-            rotkehlchen_api_server,
-            "blockchainsaccountsresource",
-            blockchain=SupportedBlockchain.KUSAMA.value,
-        ),
-        json={'accounts': [{'address': SUBSTRATE_ACC1_DOT_ADDR}]},
+    setup = setup_balances(
+        rotki=rotkehlchen_api_server.rest_api.rotkehlchen,
+        ethereum_accounts=None,
+        btc_accounts=None,
+        eth_balances=None,
+        token_balances=None,
+        btc_balances=None,
     )
+    with ExitStack() as stack:
+        setup.enter_blockchain_patches(stack)
+        response = requests.put(
+            api_url_for(
+                rotkehlchen_api_server,
+                "blockchainsaccountsresource",
+                blockchain=SupportedBlockchain.KUSAMA.value,
+            ),
+            json={'accounts': [{'address': SUBSTRATE_ACC1_DOT_ADDR}]},
+        )
+
     assert_error_response(
         response=response,
         contained_in_msg=f'{SUBSTRATE_ACC1_DOT_ADDR} is not a valid kusama address',
@@ -1644,6 +1701,7 @@ def test_add_ksm_blockchain_account_invalid(rotkehlchen_api_server):
     )
 
 
+@pytest.mark.parametrize('number_of_eth_accounts', [0])
 @pytest.mark.parametrize('kusama_manager_connect_at_start', [KUSAMA_TEST_NODES])
 def test_add_ksm_blockchain_account(rotkehlchen_api_server):
     """Test adding a Kusama blockchain account when there is none in the db
@@ -1652,22 +1710,32 @@ def test_add_ksm_blockchain_account(rotkehlchen_api_server):
     """
     async_query = random.choice([False, True])
 
-    response = requests.put(
-        api_url_for(
-            rotkehlchen_api_server,
-            "blockchainsaccountsresource",
-            blockchain=SupportedBlockchain.KUSAMA.value,
-        ),
-        json={
-            'accounts': [{'address': SUBSTRATE_ACC1_KSM_ADDR}],
-            'async_query': async_query,
-        },
+    setup = setup_balances(
+        rotki=rotkehlchen_api_server.rest_api.rotkehlchen,
+        ethereum_accounts=None,
+        btc_accounts=None,
+        eth_balances=None,
+        token_balances=None,
+        btc_balances=None,
     )
-    if async_query:
-        task_id = assert_ok_async_response(response)
-        result = wait_for_async_task_with_result(rotkehlchen_api_server, task_id)
-    else:
-        result = assert_proper_response_with_result(response)
+    with ExitStack() as stack:
+        setup.enter_blockchain_patches(stack)
+        response = requests.put(
+            api_url_for(
+                rotkehlchen_api_server,
+                "blockchainsaccountsresource",
+                blockchain=SupportedBlockchain.KUSAMA.value,
+            ),
+            json={
+                'accounts': [{'address': SUBSTRATE_ACC1_KSM_ADDR}],
+                'async_query': async_query,
+            },
+        )
+        if async_query:
+            task_id = assert_ok_async_response(response)
+            result = wait_for_async_task_with_result(rotkehlchen_api_server, task_id)
+        else:
+            result = assert_proper_response_with_result(response)
 
     # Check per account
     account_balances = result['per_account']['KSM'][SUBSTRATE_ACC1_KSM_ADDR]
@@ -1683,6 +1751,7 @@ def test_add_ksm_blockchain_account(rotkehlchen_api_server):
     assert FVal(total_ksm['usd_value']) >= ZERO
 
 
+@pytest.mark.parametrize('number_of_eth_accounts', [0])
 @pytest.mark.parametrize('ksm_accounts', [[SUBSTRATE_ACC1_KSM_ADDR, SUBSTRATE_ACC2_KSM_ADDR]])
 @pytest.mark.parametrize('kusama_manager_connect_at_start', [KUSAMA_TEST_NODES])
 def test_remove_ksm_blockchain_account(rotkehlchen_api_server):
@@ -1701,23 +1770,32 @@ def test_remove_ksm_blockchain_account(rotkehlchen_api_server):
         blockchain=SupportedBlockchain.KUSAMA,
         account_data=accounts_data,
     )
-
-    response = requests.delete(
-        api_url_for(
-            rotkehlchen_api_server,
-            "blockchainsaccountsresource",
-            blockchain=SupportedBlockchain.KUSAMA.value,
-        ),
-        json={
-            'accounts': [SUBSTRATE_ACC2_KSM_ADDR],
-            'async_query': async_query,
-        },
+    setup = setup_balances(
+        rotki=rotki,
+        ethereum_accounts=None,
+        btc_accounts=None,
+        eth_balances=None,
+        token_balances=None,
+        btc_balances=None,
     )
-    if async_query:
-        task_id = assert_ok_async_response(response)
-        result = wait_for_async_task_with_result(rotkehlchen_api_server, task_id)
-    else:
-        result = assert_proper_response_with_result(response)
+    with ExitStack() as stack:
+        setup.enter_blockchain_patches(stack)
+        response = requests.delete(
+            api_url_for(
+                rotkehlchen_api_server,
+                "blockchainsaccountsresource",
+                blockchain=SupportedBlockchain.KUSAMA.value,
+            ),
+            json={
+                'accounts': [SUBSTRATE_ACC2_KSM_ADDR],
+                'async_query': async_query,
+            },
+        )
+        if async_query:
+            task_id = assert_ok_async_response(response)
+            result = wait_for_async_task_with_result(rotkehlchen_api_server, task_id)
+        else:
+            result = assert_proper_response_with_result(response)
 
     # Check per account
     assert SUBSTRATE_ACC2_KSM_ADDR not in result['per_account']['KSM']
@@ -1738,19 +1816,30 @@ def test_remove_ksm_blockchain_account(rotkehlchen_api_server):
     assert db_accounts.ksm[0] == SUBSTRATE_ACC1_KSM_ADDR
 
 
+@pytest.mark.parametrize('number_of_eth_accounts', [0])
 def test_add_ksm_blockchain_account_invalid_ens_domain(rotkehlchen_api_server):
     """Test adding an invalid Kusama blockchain account via ENS domain works as
     expected.
     """
     invalid_ens_domain = 'craigwright.eth'
-    response = requests.put(
-        api_url_for(
-            rotkehlchen_api_server,
-            "blockchainsaccountsresource",
-            blockchain=SupportedBlockchain.KUSAMA.value,
-        ),
-        json={'accounts': [{'address': invalid_ens_domain}]},
+    setup = setup_balances(
+        rotki=rotkehlchen_api_server.rest_api.rotkehlchen,
+        ethereum_accounts=None,
+        btc_accounts=None,
+        eth_balances=None,
+        token_balances=None,
+        btc_balances=None,
     )
+    with ExitStack() as stack:
+        setup.enter_blockchain_patches(stack)
+        response = requests.put(
+            api_url_for(
+                rotkehlchen_api_server,
+                "blockchainsaccountsresource",
+                blockchain=SupportedBlockchain.KUSAMA.value,
+            ),
+            json={'accounts': [{'address': invalid_ens_domain}]},
+        )
     assert_error_response(
         response=response,
         contained_in_msg=(
@@ -1760,6 +1849,7 @@ def test_add_ksm_blockchain_account_invalid_ens_domain(rotkehlchen_api_server):
     )
 
 
+@pytest.mark.parametrize('number_of_eth_accounts', [0])
 @pytest.mark.parametrize('kusama_manager_connect_at_start', [KUSAMA_TEST_NODES])
 def test_add_ksm_blockchain_account_ens_domain(rotkehlchen_api_server):
     """Test adding a Kusama blockchain account via ENS domain when there is none
@@ -1767,23 +1857,32 @@ def test_add_ksm_blockchain_account_ens_domain(rotkehlchen_api_server):
     to the nodes.
     """
     async_query = random.choice([False, True])
-
-    response = requests.put(
-        api_url_for(
-            rotkehlchen_api_server,
-            "blockchainsaccountsresource",
-            blockchain=SupportedBlockchain.KUSAMA.value,
-        ),
-        json={
-            'accounts': [{'address': ENS_BRUNO}],
-            'async_query': async_query,
-        },
+    setup = setup_balances(
+        rotki=rotkehlchen_api_server.rest_api.rotkehlchen,
+        ethereum_accounts=None,
+        btc_accounts=None,
+        eth_balances=None,
+        token_balances=None,
+        btc_balances=None,
     )
-    if async_query:
-        task_id = assert_ok_async_response(response)
-        result = wait_for_async_task_with_result(rotkehlchen_api_server, task_id)
-    else:
-        result = assert_proper_response_with_result(response)
+    with ExitStack() as stack:
+        setup.enter_blockchain_patches(stack)
+        response = requests.put(
+            api_url_for(
+                rotkehlchen_api_server,
+                "blockchainsaccountsresource",
+                blockchain=SupportedBlockchain.KUSAMA.value,
+            ),
+            json={
+                'accounts': [{'address': ENS_BRUNO}],
+                'async_query': async_query,
+            },
+        )
+        if async_query:
+            task_id = assert_ok_async_response(response)
+            result = wait_for_async_task_with_result(rotkehlchen_api_server, task_id)
+        else:
+            result = assert_proper_response_with_result(response)
 
     # Check per account
     account_balances = result['per_account']['KSM'][ENS_BRUNO_KSM_ADDR]
@@ -1799,6 +1898,7 @@ def test_add_ksm_blockchain_account_ens_domain(rotkehlchen_api_server):
     assert FVal(total_ksm['usd_value']) >= ZERO
 
 
+@pytest.mark.parametrize('number_of_eth_accounts', [0])
 @pytest.mark.parametrize('ksm_accounts', [[SUBSTRATE_ACC1_KSM_ADDR, ENS_BRUNO_KSM_ADDR]])
 @pytest.mark.parametrize('kusama_manager_connect_at_start', [KUSAMA_TEST_NODES])
 def test_remove_ksm_blockchain_account_ens_domain(rotkehlchen_api_server):
@@ -1817,23 +1917,32 @@ def test_remove_ksm_blockchain_account_ens_domain(rotkehlchen_api_server):
         blockchain=SupportedBlockchain.KUSAMA,
         account_data=accounts_data,
     )
-
-    response = requests.delete(
-        api_url_for(
-            rotkehlchen_api_server,
-            "blockchainsaccountsresource",
-            blockchain=SupportedBlockchain.KUSAMA.value,
-        ),
-        json={
-            'accounts': [ENS_BRUNO],
-            'async_query': async_query,
-        },
+    setup = setup_balances(
+        rotki=rotki,
+        ethereum_accounts=None,
+        btc_accounts=None,
+        eth_balances=None,
+        token_balances=None,
+        btc_balances=None,
     )
-    if async_query:
-        task_id = assert_ok_async_response(response)
-        result = wait_for_async_task_with_result(rotkehlchen_api_server, task_id)
-    else:
-        result = assert_proper_response_with_result(response)
+    with ExitStack() as stack:
+        setup.enter_blockchain_patches(stack)
+        response = requests.delete(
+            api_url_for(
+                rotkehlchen_api_server,
+                "blockchainsaccountsresource",
+                blockchain=SupportedBlockchain.KUSAMA.value,
+            ),
+            json={
+                'accounts': [ENS_BRUNO],
+                'async_query': async_query,
+            },
+        )
+        if async_query:
+            task_id = assert_ok_async_response(response)
+            result = wait_for_async_task_with_result(rotkehlchen_api_server, task_id)
+        else:
+            result = assert_proper_response_with_result(response)
 
     # Check per account
     assert ENS_BRUNO_KSM_ADDR not in result['per_account']['KSM']
diff --git a/rotkehlchen/tests/unit/test_bitcoin.py b/rotkehlchen/tests/unit/test_bitcoin.py
--- a/rotkehlchen/tests/unit/test_bitcoin.py
+++ b/rotkehlchen/tests/unit/test_bitcoin.py
@@ -6,9 +6,13 @@
     is_valid_derivation_path,
     pubkey_to_base58_address,
     pubkey_to_bech32_address,
+    scriptpubkey_to_bech32_address,
+    scriptpubkey_to_p2pkh_address,
+    scriptpubkey_to_p2sh_address,
 )
 from rotkehlchen.chain.bitcoin.xpub import XpubData
 from rotkehlchen.errors import XPUBError
+from rotkehlchen.tests.utils.ens import ENS_BRUNO_BTC_ADDR, ENS_BRUNO_BTC_BYTES
 from rotkehlchen.tests.utils.factories import (
     UNIT_BTC_ADDRESS1,
     UNIT_BTC_ADDRESS2,
@@ -289,3 +293,52 @@ def test_is_valid_derivation_path():
     )
     assert not valid
     assert msg == expected_msg
+
+
+@pytest.mark.parametrize('scriptpubkey, expected_address', [
+    (
+        '76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac',
+        '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa',
+    ),
+    (
+        ENS_BRUNO_BTC_BYTES,
+        ENS_BRUNO_BTC_ADDR,
+    ),
+])
+def test_scriptpubkey_to_p2pkh_address(scriptpubkey, expected_address):
+    address = scriptpubkey_to_p2pkh_address(bytes.fromhex(scriptpubkey))
+    assert address == expected_address
+
+
+def test_scriptpubkey_to_p2sh_address():
+    scriptpubkey = 'a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1887'
+
+    address = scriptpubkey_to_p2sh_address(bytes.fromhex(scriptpubkey))
+    assert address == '3Ai1JZ8pdJb2ksieUV8FsxSNVJCpoPi8W6'
+
+
+@pytest.mark.parametrize('scriptpubkey, expected_address', [
+    (
+        '0014751e76e8199196d454941c45d1b3a323f1433bd6',
+        'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4',
+    ),
+    (
+        '5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6',
+        'bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx',
+    ),
+    (
+        '6002751e',
+        'bc1sw50qa3jx3s',
+    ),
+    (
+        '5210751e76e8199196d454941c45d1b3a323',
+        'bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj',
+    ),
+    (
+        '00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262',
+        'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3',
+    ),
+])
+def test_scriptpubkey_to_bech32_address(scriptpubkey, expected_address):
+    address = scriptpubkey_to_bech32_address(bytes.fromhex(scriptpubkey))
+    assert address == expected_address
diff --git a/rotkehlchen/tests/unit/test_ens.py b/rotkehlchen/tests/unit/test_ens.py
--- a/rotkehlchen/tests/unit/test_ens.py
+++ b/rotkehlchen/tests/unit/test_ens.py
@@ -3,16 +3,16 @@
 import pytest
 
 from rotkehlchen.chain.ethereum.defi.zerionsdk import ZERION_ADAPTER_ADDRESS
-from rotkehlchen.tests.utils.ethereum import (
-    ETHEREUM_TEST_PARAMETERS,
-    wait_until_all_nodes_connected,
-)
-from rotkehlchen.tests.utils.substrate import (
+from rotkehlchen.tests.utils.ens import (
     ENS_BRUNO,
     ENS_BRUNO_BTC_BYTES,
     ENS_BRUNO_ETH_ADDR,
     ENS_BRUNO_SUBSTRATE_PUBLIC_KEY,
 )
+from rotkehlchen.tests.utils.ethereum import (
+    ETHEREUM_TEST_PARAMETERS,
+    wait_until_all_nodes_connected,
+)
 from rotkehlchen.typing import SupportedBlockchain
 
 
diff --git a/rotkehlchen/tests/unit/test_inquirer.py b/rotkehlchen/tests/unit/test_inquirer.py
--- a/rotkehlchen/tests/unit/test_inquirer.py
+++ b/rotkehlchen/tests/unit/test_inquirer.py
@@ -122,6 +122,7 @@ def mock_query_price(from_asset, to_asset):
         'query_current_price',
         wraps=mock_query_price,
     )
+    inquirer.set_oracles_order(oracles=[CurrentPriceOracle.CRYPTOCOMPARE])
 
     with cc_patch as cc:
         price = inquirer.find_usd_price(Asset('ETH'))
diff --git a/rotkehlchen/tests/utils/ens.py b/rotkehlchen/tests/utils/ens.py
new file mode 100644
--- /dev/null
+++ b/rotkehlchen/tests/utils/ens.py
@@ -0,0 +1,10 @@
+# ENS domain bruno.eth dataset
+# https://app.ens.domains/name/bruno.eth
+ENS_BRUNO = 'bruno.eth'
+ENS_BRUNO_ETH_ADDR = '0xB9b8EF61b7851276B0239757A039d54a23804CBb'
+ENS_BRUNO_BTC_BYTES = '76a9147f1fff38da2809b9368262284e0608dfb5c4537988ac'
+ENS_BRUNO_BTC_ADDR = '1CbB9YvEFUbb2mXb2jZJQ9Vj9Hasg9XGz8'
+# Both Kusama (434) and Polkadot (354) coinType return the substrate public key
+ENS_BRUNO_SUBSTRATE_PUBLIC_KEY = '0aff6865635ae11013a83835c019d44ec3f865145943f487ae82a8e7bed3a66b'  # noqa: E501
+ENS_BRUNO_KSM_ADDR = 'CpjsLDC1JFyrhm3ftC9Gs4QoyrkHKhZKtK7YqGTRFtTafgp'
+ENS_BRUNO_DOT_ADDR = '1FRMM8PEiWXYax7rpS6X4XZX1aAAxSWx1CrKTyrVYhV24fg'
diff --git a/rotkehlchen/tests/utils/substrate.py b/rotkehlchen/tests/utils/substrate.py
--- a/rotkehlchen/tests/utils/substrate.py
+++ b/rotkehlchen/tests/utils/substrate.py
@@ -40,17 +40,6 @@
 SUBSTRATE_ACC5_DOT_ADDR = '1gXKQA8JDTjetR759QGGXX98siY4AvaCdp35bswUiuGormc'
 SUBSTRATE_ACC5_KSM_ADDR = 'DFqqPEw4oDBy1E2tDAK2L3zRr18AYBcaWvJJyAYQS6FNFzG'
 
-# ENS domain bruno.eth dataset
-# https://app.ens.domains/name/bruno.eth
-ENS_BRUNO = 'bruno.eth'
-ENS_BRUNO_ETH_ADDR = '0xB9b8EF61b7851276B0239757A039d54a23804CBb'
-ENS_BRUNO_BTC_BYTES = '76a9147f1fff38da2809b9368262284e0608dfb5c4537988ac'
-ENS_BRUNO_BTC_ADDR = '1CbB9YvEFUbb2mXb2jZJQ9Vj9Hasg9XGz8'
-# Both Kusama (434) and Polkadot (354) coinType return the substrate public key
-ENS_BRUNO_SUBSTRATE_PUBLIC_KEY = '0aff6865635ae11013a83835c019d44ec3f865145943f487ae82a8e7bed3a66b'  # noqa: E501
-ENS_BRUNO_KSM_ADDR = 'CpjsLDC1JFyrhm3ftC9Gs4QoyrkHKhZKtK7YqGTRFtTafgp'
-ENS_BRUNO_DOT_ADDR = '1FRMM8PEiWXYax7rpS6X4XZX1aAAxSWx1CrKTyrVYhV24fg'
-
 KUSAMA_TEST_NODES = (KusamaNodeName.PARITY, )
 KUSAMA_SS58_FORMAT = 2
 KUSAMA_TOKEN = A_KSM