Repository: rotki/rotki
Pull request #9576: Add hyperliquid balances
Author: login: yabirgb
Created: 2025-03-07T11:32:35Z | Merged: 2025-03-12T10:44:17Z

Closes #(issue_number)

## Checklist

- [ ] The PR modified the frontend, and updated the [user guide](https://github.com/rotki/docs/blob/main/usage-guides/index.md) to reflect the changes.



Diff:
diff --git a/_typos.toml b/_typos.toml
index c9f71e02623..4972b0254e6 100644
--- a/_typos.toml
+++ b/_typos.toml
@@ -4,6 +4,7 @@ extend-exclude = [
     "**/**/*.csv",  # import data
     "**/**/*.json",  # translation files, etc.
     "frontend/pnpm-lock.yaml",  # pnpm lockfile
+    "*.db",  # skip db blobs
 ]
 
 [default]
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 6fde63b5fec..e565bff61e0 100755
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -3,7 +3,7 @@ Changelog
 =========
 
 * :feature:`9267` rotki now supports decoding of Aerodrome and Velodrome lock transactions, tracking locked balances, and automatic reminders for lock expirations.
-* :feature:`9145` Deposits and withdrawals from Hyperliquid will be decoded.
+* :feature:`9145` Deposits and withdrawals from Hyperliquid will be decoded. Balances will also be queried.
 * :bug:`-` Uniswap v3 swaps in ethereum using the universal router 2 will now be decoded properly.
 * :bug:`9547` Bitcoin balance query should work again for users of the linux binary.
 
diff --git a/rotkehlchen/chain/aggregator.py b/rotkehlchen/chain/aggregator.py
index 3ad16a564cb..6521f429752 100644
--- a/rotkehlchen/chain/aggregator.py
+++ b/rotkehlchen/chain/aggregator.py
@@ -21,6 +21,9 @@
     GearboxBalances as GearboxBalancesArbitrumOne,
 )
 from rotkehlchen.chain.arbitrum_one.modules.gmx.balances import GmxBalances
+from rotkehlchen.chain.arbitrum_one.modules.hyperliquid.balances import (
+    HyperliquidBalances,
+)
 from rotkehlchen.chain.arbitrum_one.modules.thegraph.balances import (
     ThegraphBalances as ThegraphBalancesArbitrumOne,
 )
@@ -253,6 +256,7 @@ def _module_name_to_class(module_name: ModuleName) -> type[EthereumModule]:
         UmamiBalances,
         CurveLendBalances,
         UniswapV3Balances,
+        HyperliquidBalances,
     ),
     ChainID.POLYGON_POS: (
         Compoundv3Balances,
diff --git a/rotkehlchen/chain/arbitrum_one/modules/hyperliquid/balances.py b/rotkehlchen/chain/arbitrum_one/modules/hyperliquid/balances.py
new file mode 100644
index 00000000000..ec19826e974
--- /dev/null
+++ b/rotkehlchen/chain/arbitrum_one/modules/hyperliquid/balances.py
@@ -0,0 +1,49 @@
+import logging
+from collections import defaultdict
+from typing import TYPE_CHECKING
+
+from rotkehlchen.accounting.structures.balance import Balance, BalanceSheet
+from rotkehlchen.chain.arbitrum_one.modules.hyperliquid.constants import CPT_HYPER
+from rotkehlchen.chain.ethereum.interfaces.balances import BalancesSheetType, ProtocolWithBalance
+from rotkehlchen.externalapis.hyperliquid import HyperliquidAPI
+from rotkehlchen.history.events.structures.types import HistoryEventSubType, HistoryEventType
+from rotkehlchen.inquirer import Inquirer
+from rotkehlchen.logging import RotkehlchenLogsAdapter
+
+if TYPE_CHECKING:
+    from rotkehlchen.chain.arbitrum_one.decoding.decoder import ArbitrumOneTransactionDecoder
+    from rotkehlchen.chain.arbitrum_one.node_inquirer import ArbitrumOneInquirer
+
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
+
+class HyperliquidBalances(ProtocolWithBalance):
+    def __init__(
+            self,
+            evm_inquirer: 'ArbitrumOneInquirer',
+            tx_decoder: 'ArbitrumOneTransactionDecoder',
+    ):
+        super().__init__(
+            evm_inquirer=evm_inquirer,
+            tx_decoder=tx_decoder,
+            counterparty=CPT_HYPER,
+            deposit_event_types={(HistoryEventType.DEPOSIT, HistoryEventSubType.DEPOSIT_ASSET)},
+        )
+
+    def query_balances(self) -> 'BalancesSheetType':
+        balances: BalancesSheetType = defaultdict(BalanceSheet)
+        if len(addresses_with_deposits := list(self.addresses_with_deposits(products=None))) == 0:
+            return balances
+
+        hyperliquid = HyperliquidAPI()
+        for user in addresses_with_deposits:
+            user_balances = hyperliquid.query_balances(address=user)
+            for asset, amount in user_balances.items():
+                token_price = Inquirer.find_usd_price(asset)
+                balances[user].assets[asset] += Balance(
+                    amount=amount,
+                    usd_value=token_price * amount,
+                )
+
+        return balances
diff --git a/rotkehlchen/chain/ethereum/interfaces/balances.py b/rotkehlchen/chain/ethereum/interfaces/balances.py
index cf22619abaa..92bb8c36142 100644
--- a/rotkehlchen/chain/ethereum/interfaces/balances.py
+++ b/rotkehlchen/chain/ethereum/interfaces/balances.py
@@ -52,6 +52,7 @@
     'giveth',
     'uniswap-v3',
     'hedgey',
+    'hyperliquid',
 ]
 BalancesSheetType = dict[ChecksumEvmAddress, BalanceSheet]
 
diff --git a/rotkehlchen/data/global.db b/rotkehlchen/data/global.db
index 154e4f0f61b..f5ebff8dc99 100644
Binary files a/rotkehlchen/data/global.db and b/rotkehlchen/data/global.db differ
diff --git a/rotkehlchen/errors/asset.py b/rotkehlchen/errors/asset.py
index 08c1ea28a8b..f77d9a71d81 100644
--- a/rotkehlchen/errors/asset.py
+++ b/rotkehlchen/errors/asset.py
@@ -52,3 +52,8 @@ class UnprocessableTradePair(Exception):
     def __init__(self, pair: str) -> None:
         self.pair = pair
         super().__init__(f'Unprocessable pair {pair} encountered.')
+
+
+class UnknownCounterpartyMapping(Exception):
+    def __init__(self, symbol: str, counterparty: str) -> None:
+        super().__init__(f'Cannot find {symbol} for counterparty {counterparty}')
diff --git a/rotkehlchen/externalapis/hyperliquid.py b/rotkehlchen/externalapis/hyperliquid.py
new file mode 100644
index 00000000000..bbaca17d6c1
--- /dev/null
+++ b/rotkehlchen/externalapis/hyperliquid.py
@@ -0,0 +1,134 @@
+import logging
+from collections import defaultdict
+from http import HTTPStatus
+from typing import Any, Literal
+
+import requests
+
+from rotkehlchen.assets.asset import Asset
+from rotkehlchen.assets.utils import symbol_to_asset_or_token
+from rotkehlchen.chain.arbitrum_one.modules.hyperliquid.constants import CPT_HYPER
+from rotkehlchen.constants.misc import ZERO
+from rotkehlchen.errors.asset import UnknownAsset, UnknownCounterpartyMapping, WrongAssetType
+from rotkehlchen.errors.misc import RemoteError
+from rotkehlchen.errors.serialization import DeserializationError
+from rotkehlchen.fval import FVal
+from rotkehlchen.globaldb.counterparty_mappings import get_asset_id_by_counterparty
+from rotkehlchen.logging import RotkehlchenLogsAdapter
+from rotkehlchen.serialization.deserialize import deserialize_fval
+from rotkehlchen.types import ChecksumEvmAddress
+from rotkehlchen.utils.network import create_session
+
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
+
+def asset_from_hyperliquid(symbol: str) -> Asset:
+    """Fetch asset from hyperliquid location
+
+    May raise:
+    - UnsupportedAsset
+    - UnknownAsset
+    - UnknownCounterpartyMapping
+    """
+    return symbol_to_asset_or_token(get_asset_id_by_counterparty(
+        symbol=symbol,
+        counterparty=CPT_HYPER,
+    ))
+
+
+class HyperliquidAPI:
+
+    def __init__(self) -> None:
+        self.base_url = 'https://api.hyperliquid.xyz'
+        self.session = create_session()
+        self.arb_usdc = Asset('eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831')
+
+    def _query(
+            self,
+            address: ChecksumEvmAddress,
+            account_type: Literal['clearinghouseState', 'spotClearinghouseState'],
+    ) -> dict[str, Any]:
+        """Query hyperliquid for balances.
+
+        May raise:
+            - RemoteError
+        """
+        log.debug(f'Querying hyperliquid balances at {self.base_url}/info with {account_type=} and {address=}')  # noqa: E501
+        try:
+            response = self.session.post(
+                url=f'{self.base_url}/info',
+                json={'type': account_type, 'user': address},
+            )
+            data = response.json()
+        except requests.exceptions.RequestException as e:
+            log.error(
+                f'Querying hyperliquid for {address} and {account_type=} failed due to '
+                f'{e}. Skipping',
+            )
+            raise RemoteError(
+                f'Failed to query hyperliquid balances of {account_type} for {address} due to {e}',
+            ) from e
+
+        if response.status_code != HTTPStatus.OK:
+            log.error(
+                f'Failed to query {account_type} balances for {address} with status '
+                f'code {response.status_code} and text {response.text}',
+            )
+            raise RemoteError(f'Failed to query hyperliquid balances of {account_type} for {address} with status code {response.status_code}')  # noqa: E501
+
+        return data
+
+    def query_balances(self, address: ChecksumEvmAddress) -> dict[Asset, FVal]:
+        """
+        Queries both spot and perp balances since they are returned in two different endpoints.
+        Hyperliquid has two `accounts` for each address, one for spot and one for perpetuals.
+
+        https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-a-users-token-balances
+        https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-users-perpetuals-account-summary
+        """
+        balances: defaultdict[Asset, FVal] = defaultdict(FVal)
+        try:
+            data = self._query(address=address, account_type='spotClearinghouseState')
+        except RemoteError as e:
+            log.error(f'Skipping spotClearinghouseState balances in hyperliquid of {address} due to {e}')  # noqa: E501
+            data = {}
+
+        for asset_entry in data.get('balances', []):
+            try:
+                if (balance := deserialize_fval(
+                    value=asset_entry['total'],
+                    name='spotClearinghouseState total balances',
+                    location='hyperliquid',
+                )) == ZERO:
+                    continue
+
+                asset = asset_from_hyperliquid(asset_entry['coin'])
+            except (
+                DeserializationError,
+                UnknownAsset,
+                WrongAssetType,
+                KeyError,
+                UnknownCounterpartyMapping,
+            ) as e:
+                log.error(f'Failed to read balance {asset_entry} from hyperliquid due to {e}. Skipping')  # noqa: E501
+                continue
+
+            if balance != ZERO:
+                balances[asset] += balance
+
+        try:
+            perp_data = self._query(address=address, account_type='clearinghouseState')
+        except RemoteError as e:
+            log.error(f'Skipping spotClearinghouseState hyperliquid balances of {address=} due to {e}')  # noqa: E501
+        else:
+            try:
+                balances[self.arb_usdc] += deserialize_fval(
+                    value=perp_data.get('crossMarginSummary', {}).get('accountValue', 0),
+                    name='clearinghouseState total balances',
+                    location='hyperliquid',
+                )
+            except DeserializationError as e:
+                log.error(f'Failed to read hyperliquid crossMarginSummary due to {e}')
+
+        return balances
diff --git a/rotkehlchen/globaldb/counterparty_mappings.py b/rotkehlchen/globaldb/counterparty_mappings.py
new file mode 100644
index 00000000000..4be7f336160
--- /dev/null
+++ b/rotkehlchen/globaldb/counterparty_mappings.py
@@ -0,0 +1,21 @@
+from rotkehlchen.errors.asset import UnknownCounterpartyMapping
+from rotkehlchen.globaldb.handler import GlobalDBHandler
+
+
+def get_asset_id_by_counterparty(symbol: str, counterparty: str) -> str:
+    """
+    Fetch asset id mapped from the symbol and counterparty combination.
+
+    May raise:
+        - UnknownCounterpartyMapping
+    """
+    with GlobalDBHandler().conn.read_ctx() as cursor:
+        cursor.execute(
+            'SELECT local_id FROM counterparty_asset_mappings WHERE '
+            'symbol=? AND counterparty=?',
+            (symbol, counterparty),
+        )
+        if (result := cursor.fetchone()) is not None:
+            return result[0]
+
+        raise UnknownCounterpartyMapping(symbol=symbol, counterparty=counterparty)
diff --git a/rotkehlchen/globaldb/minimized_schema.py b/rotkehlchen/globaldb/minimized_schema.py
index a3d76bf0f6e..5700a7ca6c4 100644
--- a/rotkehlchen/globaldb/minimized_schema.py
+++ b/rotkehlchen/globaldb/minimized_schema.py
@@ -1,5 +1,5 @@
 # This file contains minimized db schema and it should not be touched manually but only generated by tools/scripts/generate_minimized_db_schema.py
-# Created at 2024-12-05 09:19:46 UTC with rotki version 1.23.5.dev7172+gd55bcd2a5 by prettyirrelevant
+# Created at 2025-03-11 11:45:25 UTC with rotki version 1.37.2.dev444+gc78769ff5.d20250311 by yabirgb
 MINIMIZED_GLOBAL_DB_SCHEMA = {
     "token_kinds": "token_kindchar(1)primarykeynotnull,seqintegerunique",
     "underlying_tokens_list": "identifiertextnotnull,weighttextnotnull,parent_token_entrytextnotnull,foreignkey(parent_token_entry)referencesevm_tokens(identifier)ondeletecascadeonupdatecascadeforeignkey(identifier)referencesevm_tokens(identifier)onupdatecascadeondeletecascadeprimarykey(identifier,parent_token_entry)",
@@ -23,4 +23,5 @@
     "default_rpc_nodes": "identifierintegernotnullprimarykey,nametextnotnull,endpointtextnotnull,ownedintegernotnullcheck(ownedin(0,1)),activeintegernotnullcheck(activein(0,1)),weighttextnotnull,blockchaintextnotnull",
     "location_asset_mappings": "locationtext,exchange_symboltextnotnull,local_idtextnotnullcollatenocase,unique(location,exchange_symbol)",
     "location_unsupported_assets": "locationchar(1)notnull,exchange_symboltextnotnull,unique(location,exchange_symbol)",
+    "counterparty_asset_mappings": "counterpartytextnotnull,symboltextnotnull,local_idtextnotnullcollatenocase,primarykey(counterparty,symbol)",
 }
diff --git a/rotkehlchen/globaldb/schema.py b/rotkehlchen/globaldb/schema.py
index 212f3505097..2504fb90e51 100644
--- a/rotkehlchen/globaldb/schema.py
+++ b/rotkehlchen/globaldb/schema.py
@@ -322,6 +322,16 @@
 );
 """
 
+# Asset mappings for counterparties that are not actual locations.
+DB_CREATE_COUNTERPARTY_ASSET_MAPPINGS = """
+CREATE TABLE IF NOT EXISTS counterparty_asset_mappings (
+    counterparty TEXT NOT NULL,
+    symbol TEXT NOT NULL,
+    local_id TEXT NOT NULL COLLATE NOCASE,
+    PRIMARY KEY (counterparty, symbol)
+);
+"""
+
 DB_CREATE_LOCATION_UNSUPPORTED_ASSETS = """
 CREATE TABLE IF NOT EXISTS location_unsupported_assets (
     location CHAR(1) NOT NULL,
@@ -369,6 +379,7 @@
 {DB_CREATE_DEFAULT_RPC_NODES}
 {DB_CREATE_LOCATION_ASSET_MAPPINGS}
 {DB_CREATE_LOCATION_UNSUPPORTED_ASSETS}
+{DB_CREATE_COUNTERPARTY_ASSET_MAPPINGS}
 {DB_CREATE_INDEXES}
 COMMIT;
 PRAGMA foreign_keys=on;
diff --git a/rotkehlchen/globaldb/upgrades/manager.py b/rotkehlchen/globaldb/upgrades/manager.py
index e3c1a2195f8..35ac4b2f748 100644
--- a/rotkehlchen/globaldb/upgrades/manager.py
+++ b/rotkehlchen/globaldb/upgrades/manager.py
@@ -29,7 +29,8 @@
 from .v7_v8 import migrate_to_v8
 from .v8_v9 import migrate_to_v9
 from .v9_v10 import migrate_to_v10
-from .v10_11 import migrate_to_v11
+from .v10_v11 import migrate_to_v11
+from .v11_v12 import migrate_to_v12
 
 if TYPE_CHECKING:
     from rotkehlchen.db.drivers.gevent import DBConnection
@@ -78,6 +79,10 @@
         from_version=10,
         function=migrate_to_v11,
     ),
+    UpgradeRecord(
+        from_version=11,
+        function=migrate_to_v12,
+    ),
 ]
 
 
diff --git a/rotkehlchen/globaldb/upgrades/v10_11.py b/rotkehlchen/globaldb/upgrades/v10_v11.py
similarity index 100%
rename from rotkehlchen/globaldb/upgrades/v10_11.py
rename to rotkehlchen/globaldb/upgrades/v10_v11.py
diff --git a/rotkehlchen/globaldb/upgrades/v11_v12.py b/rotkehlchen/globaldb/upgrades/v11_v12.py
new file mode 100644
index 00000000000..aadb710a36a
--- /dev/null
+++ b/rotkehlchen/globaldb/upgrades/v11_v12.py
@@ -0,0 +1,30 @@
+from typing import TYPE_CHECKING
+
+from rotkehlchen.logging import enter_exit_debug_log
+from rotkehlchen.utils.progress import perform_globaldb_upgrade_steps, progress_step
+
+if TYPE_CHECKING:
+    from rotkehlchen.db.drivers.gevent import DBConnection, DBCursor
+    from rotkehlchen.db.upgrade_manager import DBUpgradeProgressHandler
+
+
+@enter_exit_debug_log(name='globaldb v11->v12 upgrade')
+def migrate_to_v12(connection: 'DBConnection', progress_handler: 'DBUpgradeProgressHandler') -> None:  # noqa: E501
+    """This globalDB upgrade does the following:
+    - Add new table for counterparty mappings
+
+    This upgrade takes place in v1.39.0"""
+    @progress_step('Adding new tables.')
+    def _create_new_tables(write_cursor: 'DBCursor') -> None:
+        write_cursor.execute(
+            """
+            CREATE TABLE IF NOT EXISTS counterparty_asset_mappings (
+                counterparty TEXT NOT NULL,
+                symbol TEXT NOT NULL,
+                local_id TEXT NOT NULL COLLATE NOCASE,
+                PRIMARY KEY (counterparty, symbol)
+            );
+            """,
+        )
+
+    perform_globaldb_upgrade_steps(connection, progress_handler)
diff --git a/rotkehlchen/globaldb/utils.py b/rotkehlchen/globaldb/utils.py
index b45532cb821..7d8a2807c25 100644
--- a/rotkehlchen/globaldb/utils.py
+++ b/rotkehlchen/globaldb/utils.py
@@ -17,7 +17,7 @@
 # Whenever you upgrade the global DB make sure to:
 # 1. Go to assets repo and tweak the min/max schema of the updates
 # 2. Tweak ASSETS_FILE_IMPORT_ACCEPTED_GLOBALDB_VERSIONS
-GLOBAL_DB_VERSION = 11
+GLOBAL_DB_VERSION = 12
 ASSETS_FILE_IMPORT_ACCEPTED_GLOBALDB_VERSIONS = (3, GLOBAL_DB_VERSION)
 MIN_SUPPORTED_GLOBAL_DB_VERSION = 2
 GLOBAL_DB_SCHEMA_BREAKING_CHANGES = {
diff --git a/rotkehlchen/tests/api/test_database.py b/rotkehlchen/tests/api/test_database.py
index 29cf7b01fcc..19379f24ecc 100644
--- a/rotkehlchen/tests/api/test_database.py
+++ b/rotkehlchen/tests/api/test_database.py
@@ -43,7 +43,7 @@ def test_query_db_info(
     response = requests.get(api_url_for(rotkehlchen_api_server, 'databaseinforesource'))
     result = assert_proper_sync_response_with_result(response)
     assert len(result) == 2
-    assert result['globaldb'] == {'globaldb_assets_version': 36, 'globaldb_schema_version': 11}
+    assert result['globaldb'] == {'globaldb_assets_version': 36, 'globaldb_schema_version': 12}
 
     if start_with_logged_in_user:
         userdb = result['userdb']
diff --git a/rotkehlchen/tests/data/v11_global.db b/rotkehlchen/tests/data/v11_global.db
new file mode 100644
index 00000000000..37862531ef3
Binary files /dev/null and b/rotkehlchen/tests/data/v11_global.db differ
diff --git a/rotkehlchen/tests/unit/globaldb/test_upgrades.py b/rotkehlchen/tests/unit/globaldb/test_upgrades.py
index fc936ed6a2c..204bb3df789 100644
--- a/rotkehlchen/tests/unit/globaldb/test_upgrades.py
+++ b/rotkehlchen/tests/unit/globaldb/test_upgrades.py
@@ -966,6 +966,27 @@ def test_upgrade_v10_v11(globaldb: GlobalDBHandler, messages_aggregator):
         assert cursor.execute("SELECT swapped_for FROM common_asset_details WHERE identifier='PHX'").fetchone()[0] == 'eip155:56/erc20:0x0409633A72D846fc5BBe2f98D88564D35987904D'  # noqa: E501
 
 
+@pytest.mark.parametrize('custom_globaldb', ['v11_global.db'])
+@pytest.mark.parametrize('target_globaldb_version', [11])
+@pytest.mark.parametrize('reload_user_assets', [False])
+def test_upgrade_v11_v12(globaldb: GlobalDBHandler, messages_aggregator):
+    with globaldb.conn.read_ctx() as cursor:
+        assert table_exists(cursor=cursor, name='counterparty_asset_mappings') is False
+
+    with ExitStack() as stack:
+        patch_for_globaldb_upgrade_to(stack, 12)
+        maybe_upgrade_globaldb(
+            connection=globaldb.conn,
+            global_dir=globaldb._data_directory / GLOBALDIR_NAME,  # type: ignore
+            db_filename=GLOBALDB_NAME,
+            msg_aggregator=messages_aggregator,
+        )
+
+    assert globaldb.get_setting_value('version', 0) == 12
+    with globaldb.conn.read_ctx() as cursor:
+        assert table_exists(cursor=cursor, name='counterparty_asset_mappings') is True
+
+
 @pytest.mark.parametrize('custom_globaldb', ['v2_global.db'])
 @pytest.mark.parametrize('target_globaldb_version', [2])
 @pytest.mark.parametrize('reload_user_assets', [False])
diff --git a/rotkehlchen/tests/unit/test_protocol_balances.py b/rotkehlchen/tests/unit/test_protocol_balances.py
index b5c03ecd2de..a61cea303db 100644
--- a/rotkehlchen/tests/unit/test_protocol_balances.py
+++ b/rotkehlchen/tests/unit/test_protocol_balances.py
@@ -17,6 +17,8 @@
 )
 from rotkehlchen.chain.arbitrum_one.modules.gearbox.constants import GEAR_IDENTIFIER_ARB
 from rotkehlchen.chain.arbitrum_one.modules.gmx.balances import GmxBalances
+from rotkehlchen.chain.arbitrum_one.modules.hyperliquid.balances import HyperliquidBalances
+from rotkehlchen.chain.arbitrum_one.modules.hyperliquid.constants import CPT_HYPER
 from rotkehlchen.chain.arbitrum_one.modules.thegraph.balances import (
     ThegraphBalances as ThegraphBalancesArbitrumOne,
 )
@@ -1371,3 +1373,54 @@ def test_all_balance_classes_used():
             unused_classes.add(class_entry)
 
     assert unused_classes == set(), f'Found unused classes {unused_classes}'
+
+
+@pytest.mark.vcr(filter_query_parameters=['apikey'])
+@pytest.mark.parametrize('should_mock_current_price_queries', [False])
+@pytest.mark.parametrize('arbitrum_one_accounts', [[
+    '0x4E60C2E7181EDFc5471C0cBC8D485706d1b35686',
+    '0x3Ba6eB0e4327B96aDe6D4f3b578724208a590CEF',
+]])
+def test_hyperliquid(
+        arbitrum_one_inquirer: 'ArbitrumOneInquirer',
+        arbitrum_one_accounts: list[ChecksumEvmAddress],
+        inquirer_defi: 'Inquirer',  # pylint: disable=unused-argument
+) -> None:
+    """Check balances in hyperliquid both for spot and perp
+    0x3Ba6eB0e4327B96aDe6D4f3b578724208a590CEF has mainly spot and
+    0x4E60C2E7181EDFc5471C0cBC8D485706d1b35686 has mainly perp balances.
+    """
+    _, tx_decoder = get_decoded_events_of_transaction(
+        evm_inquirer=arbitrum_one_inquirer,
+        tx_hash=deserialize_evm_tx_hash('0x2aa0a70af2347ccc4ba3d5f4eddd362c7cd8118c0f2a3617d4b4fcf78c929ea7'),
+    )
+    get_decoded_events_of_transaction(
+        evm_inquirer=arbitrum_one_inquirer,
+        tx_hash=deserialize_evm_tx_hash('0x2e9ef8a4369c33f1b3adb0d870fc60a1f721cbf4bda1181d6804acb0f41a2d66'),
+    )
+    hyper_balances = HyperliquidBalances(
+        evm_inquirer=arbitrum_one_inquirer,
+        tx_decoder=tx_decoder,
+    )
+    arb_usdc = Asset('eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831')
+    # ensure mappings are set
+    with GlobalDBHandler().conn.write_ctx() as write_cursor:
+        write_cursor.executemany(
+            'INSERT OR IGNORE INTO counterparty_asset_mappings (counterparty, symbol, local_id) VALUES (?, ?, ?)',  # noqa: E501
+            [
+                (CPT_HYPER, 'HYPE', 'HYPE'),
+                (CPT_HYPER, 'USDC', arb_usdc.identifier),
+            ],
+
+        )
+
+    protocol_balances = hyper_balances.query_balances()
+    user_4e = arbitrum_one_accounts[0]
+    user_3b = arbitrum_one_accounts[1]
+    assert protocol_balances[user_4e].assets == {
+        arb_usdc: Balance(amount=FVal(519.955570), usd_value=FVal(519.887455820330)),
+    }
+    assert protocol_balances[user_3b].assets == {
+        Asset('HYPE'): Balance(amount=FVal(14.79852012), usd_value=FVal(232.9287066888)),
+        arb_usdc: Balance(amount=FVal(27.20794226), usd_value=FVal(27.20437801956394)),
+    }
diff --git a/rotkehlchen/tests/utils/globaldb.py b/rotkehlchen/tests/utils/globaldb.py
index 21e253ba989..7ef46166a4e 100644
--- a/rotkehlchen/tests/utils/globaldb.py
+++ b/rotkehlchen/tests/utils/globaldb.py
@@ -79,7 +79,7 @@ def create_initial_expected_globaldb_test_tokens() -> list[EvmToken]:
 )
 
 
-def patch_for_globaldb_upgrade_to(stack: ExitStack, version: Literal[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) -> ExitStack:  # noqa: E501
+def patch_for_globaldb_upgrade_to(stack: ExitStack, version: Literal[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) -> ExitStack:  # noqa: E501
     stack.enter_context(
         patch(
             'rotkehlchen.globaldb.upgrades.manager.GLOBAL_DB_VERSION',


Reviews:
user: login: prettyirrelevant
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T11:54:06Z
user: login: prettyirrelevant
body: smol comment.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T14:08:20Z
user: login: LefterisJP
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T17:15:14Z
user: login: LefterisJP
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T14:31:51Z
user: login: LefterisJP
body: `rotkehlchen/tests/unit/globaldb/test_globaldb_consistency.py::test_asset_updates_consistency_with_packaged_db ` failed but with A LOT of mismatches. A lot more than I woudl have expected.

So

1. Is this test expected to fail in develop again now?
2. Can you check if the amount of mismatches makes sense. I kept scrolling and scrolling and it was huge.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T16:12:27Z
user: login: LefterisJP
body: LGTM
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-12T10:44:10Z

Review comments:
diff_hunk: @@ -1322,3 +1324,51 @@ def test_all_balance_classes_used():
             unused_classes.add(class_entry)
 
     assert unused_classes == set(), f'Found unused classes {unused_classes}'
+
+
+@pytest.mark.vcr(filter_query_parameters=['apikey'])
+@pytest.mark.parametrize('should_mock_current_price_queries', [False])
+@pytest.mark.parametrize('arbitrum_one_accounts', [[
+    '0x4E60C2E7181EDFc5471C0cBC8D485706d1b35686',
+    '0x3Ba6eB0e4327B96aDe6D4f3b578724208a590CEF',
+]])
+def test_hyperliquid(
+        arbitrum_one_inquirer: 'ArbitrumOneInquirer',
+        arbitrum_one_accounts: list[ChecksumEvmAddress],
+        inquirer_defi: 'Inquirer',  # pylint: disable=unused-argument
+) -> None:
+    """Check that Uniswap V3 LP positions are properly detected on arbitrum"""
path: rotkehlchen/tests/unit/test_protocol_balances.py
user: login: prettyirrelevant
body: i think you need to change this docstring.
created_at: 2025-03-07T11:43:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1322,3 +1324,51 @@ def test_all_balance_classes_used():
             unused_classes.add(class_entry)
 
     assert unused_classes == set(), f'Found unused classes {unused_classes}'
+
+
+@pytest.mark.vcr(filter_query_parameters=['apikey'])
+@pytest.mark.parametrize('should_mock_current_price_queries', [False])
+@pytest.mark.parametrize('arbitrum_one_accounts', [[
+    '0x4E60C2E7181EDFc5471C0cBC8D485706d1b35686',
+    '0x3Ba6eB0e4327B96aDe6D4f3b578724208a590CEF',
+]])
+def test_hyperliquid(
+        arbitrum_one_inquirer: 'ArbitrumOneInquirer',
+        arbitrum_one_accounts: list[ChecksumEvmAddress],
+        inquirer_defi: 'Inquirer',  # pylint: disable=unused-argument
+) -> None:
+    """Check that Uniswap V3 LP positions are properly detected on arbitrum"""
+    _, tx_decoder = get_decoded_events_of_transaction(
+        evm_inquirer=arbitrum_one_inquirer,
+        tx_hash=deserialize_evm_tx_hash('0x2aa0a70af2347ccc4ba3d5f4eddd362c7cd8118c0f2a3617d4b4fcf78c929ea7'),
+    )
+    get_decoded_events_of_transaction(
+        evm_inquirer=arbitrum_one_inquirer,
+        tx_hash=deserialize_evm_tx_hash('0x2e9ef8a4369c33f1b3adb0d870fc60a1f721cbf4bda1181d6804acb0f41a2d66'),
+    )
+    hyper_inquirer = HyperliquidBalances(
path: rotkehlchen/tests/unit/test_protocol_balances.py
user: login: prettyirrelevant
body: variable name seems wrong here.
created_at: 2025-03-07T11:44:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,49 @@
+import logging
+from collections import defaultdict
+from typing import TYPE_CHECKING
+
+from rotkehlchen.accounting.structures.balance import Balance, BalanceSheet
+from rotkehlchen.chain.arbitrum_one.modules.hyperliquid.constants import CPT_HYPER
+from rotkehlchen.chain.ethereum.interfaces.balances import BalancesSheetType, ProtocolWithBalance
+from rotkehlchen.externalapis.hyperliquid import Hyperliquid
+from rotkehlchen.history.events.structures.types import HistoryEventSubType, HistoryEventType
+from rotkehlchen.inquirer import Inquirer
+from rotkehlchen.logging import RotkehlchenLogsAdapter
+
+if TYPE_CHECKING:
+    from rotkehlchen.chain.arbitrum_one.decoding.decoder import ArbitrumOneTransactionDecoder
+    from rotkehlchen.chain.arbitrum_one.node_inquirer import ArbitrumOneInquirer
+
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
+
+class HyperliquidBalances(ProtocolWithBalance):
+    def __init__(
+            self,
+            evm_inquirer: 'ArbitrumOneInquirer',
+            tx_decoder: 'ArbitrumOneTransactionDecoder',
+    ):
+        super().__init__(
+            evm_inquirer=evm_inquirer,
+            tx_decoder=tx_decoder,
+            counterparty=CPT_HYPER,
+            deposit_event_types={(HistoryEventType.DEPOSIT, HistoryEventSubType.DEPOSIT_ASSET)},
+        )
+
+    def query_balances(self) -> 'BalancesSheetType':
+        balances: BalancesSheetType = defaultdict(BalanceSheet)
+        if len(addresses_with_deposits := list(self.addresses_with_deposits(products=None))) == 0:
+            return balances
+
+        hyperliquid = Hyperliquid()
+        for user in addresses_with_deposits:
+            user_balances = hyperliquid.query_balances(address=user)
+            for asset, amount in user_balances.items():
+                token_price = Inquirer.find_usd_price(asset)
path: rotkehlchen/chain/arbitrum_one/modules/hyperliquid/balances.py
user: login: prettyirrelevant
body: shouldn't you log a debug message or something if the price is zero?
created_at: 2025-03-07T11:49:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,29 @@
+import logging
+from typing import TYPE_CHECKING
+
+from rotkehlchen.logging import RotkehlchenLogsAdapter, enter_exit_debug_log
+from rotkehlchen.utils.progress import perform_userdb_upgrade_steps, progress_step
+
+if TYPE_CHECKING:
+    from rotkehlchen.db.dbhandler import DBHandler
+    from rotkehlchen.db.drivers.gevent import DBCursor
+    from rotkehlchen.db.upgrade_manager import DBUpgradeProgressHandler
+
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
+
+@enter_exit_debug_log(name='UserDB v47->v48 upgrade')
+def upgrade_v47_to_v48(db: 'DBHandler', progress_handler: 'DBUpgradeProgressHandler') -> None:
path: rotkehlchen/db/upgrades/v47_v48.py
user: login: prettyirrelevant
body: you're missing a test for this db upgrade.
created_at: 2025-03-07T11:49:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,29 @@
+import logging
+from typing import TYPE_CHECKING
+
+from rotkehlchen.logging import RotkehlchenLogsAdapter, enter_exit_debug_log
+from rotkehlchen.utils.progress import perform_userdb_upgrade_steps, progress_step
+
+if TYPE_CHECKING:
+    from rotkehlchen.db.dbhandler import DBHandler
+    from rotkehlchen.db.drivers.gevent import DBCursor
+    from rotkehlchen.db.upgrade_manager import DBUpgradeProgressHandler
+
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
+
+@enter_exit_debug_log(name='UserDB v47->v48 upgrade')
+def upgrade_v47_to_v48(db: 'DBHandler', progress_handler: 'DBUpgradeProgressHandler') -> None:
+    """Upgrades the DB from v47 to v48. This was in v1.39 release.
+
+    - Add Hyperliquid as location
+    """
+    @progress_step(description='Adding new locations to the DB.')
+    def _add_new_locations(write_cursor: 'DBCursor') -> None:
+        write_cursor.executescript("""
+            /* Hyperliquid */
+            INSERT OR IGNORE INTO location(location, seq) VALUES ('w', 55);
+            """)
path: rotkehlchen/db/upgrades/v47_v48.py
user: login: prettyirrelevant
body: nitpick: the indentation looks off.
created_at: 2025-03-07T11:50:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,122 @@
+import logging
+from collections import defaultdict
+from http import HTTPStatus
+from typing import Any, Literal
+
+import requests
+
+from rotkehlchen.assets.asset import Asset
+from rotkehlchen.assets.utils import symbol_to_asset_or_token
+from rotkehlchen.constants.misc import ZERO
+from rotkehlchen.errors.asset import UnknownAsset, WrongAssetType
+from rotkehlchen.errors.misc import RemoteError
+from rotkehlchen.errors.serialization import DeserializationError
+from rotkehlchen.fval import FVal
+from rotkehlchen.globaldb.handler import GlobalDBHandler
+from rotkehlchen.logging import RotkehlchenLogsAdapter
+from rotkehlchen.serialization.deserialize import deserialize_fval
+from rotkehlchen.types import ChecksumEvmAddress, Location
+from rotkehlchen.utils.network import create_session
+
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
+
+def asset_from_hyperliquid(symbol: str) -> Asset:
+    """Fetch asset from hperliquid location
+
+    May raise:
+    - UnsupportedAsset
+    - UnknownAsset
+    """
+    return symbol_to_asset_or_token(GlobalDBHandler.get_assetid_from_exchange_name(
+        exchange=Location.HYPERLIQUID,
+        symbol=symbol,
+        default=symbol,
+    ))
+
+
+class Hyperliquid:
+
+    def __init__(self) -> None:
+        self.base_url = 'https://api.hyperliquid.xyz'
+        self.session = create_session()
+        self.arb_usdc = Asset('eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831')
+
+    def _query(
+            self,
+            address: ChecksumEvmAddress,
+            account_type: Literal['clearinghouseState', 'spotClearinghouseState'],
+    ) -> dict[str, Any]:
+        """Query hyperliquid for balances.
+
+        May raise:
+            - RemoteError
+        """
+        log.debug(f'Querying hyperliquid balances at {self.base_url}/info with {account_type=} and {address=}')  # noqa: E501
+        try:
+            response = self.session.post(
+                url=f'{self.base_url}/info',
+                json={'type': account_type, 'user': address},
+            )
+            data = response.json()
+        except requests.exceptions.RequestException as e:
+            log.error(
+                f'Query to hyperliquid for {address} and {account_type=} failed due to '
+                f'{e}. Skipping',
+            )
+            raise RemoteError(
+                f'Failed to query balances of {account_type} for {address} due to {e}',
+            ) from e
+
+        if response.status_code != HTTPStatus.OK:
+            log.error(
+                f'Failed to query {account_type} balances for {address} with status '
+                f'code {response.status_code} and text {response.text}',
+            )
+            raise RemoteError(f'Failed to query balances of {account_type} for {address} with status code {response.status_code}')  # noqa: E501
+
+        return data
+
+    def query_balances(self, address: ChecksumEvmAddress) -> dict[Asset, FVal]:
+        """
+        Queries both spot and perp balances since they are returned in two different endpoints.
+        Hyperliquid has like two `accounts` for each address, one for spot and one for perpetuals.
path: rotkehlchen/externalapis/hyperliquid.py
user: login: prettyirrelevant
body: maybe add a reference to the docs.
https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-a-users-token-balances

https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-users-perpetuals-account-summary
created_at: 2025-03-07T11:52:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,125 @@
+import logging
+from collections import defaultdict
+from http import HTTPStatus
+from typing import Any, Literal
+
+import requests
+
+from rotkehlchen.assets.asset import Asset
+from rotkehlchen.assets.utils import symbol_to_asset_or_token
+from rotkehlchen.constants.misc import ZERO
+from rotkehlchen.errors.asset import UnknownAsset, WrongAssetType
+from rotkehlchen.errors.misc import RemoteError
+from rotkehlchen.errors.serialization import DeserializationError
+from rotkehlchen.fval import FVal
+from rotkehlchen.globaldb.handler import GlobalDBHandler
+from rotkehlchen.logging import RotkehlchenLogsAdapter
+from rotkehlchen.serialization.deserialize import deserialize_fval
+from rotkehlchen.types import ChecksumEvmAddress, Location
+from rotkehlchen.utils.network import create_session
+
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
+
+def asset_from_hyperliquid(symbol: str) -> Asset:
+    """Fetch asset from hperliquid location
+
+    May raise:
+    - UnsupportedAsset
+    - UnknownAsset
+    """
+    return symbol_to_asset_or_token(GlobalDBHandler.get_assetid_from_exchange_name(
+        exchange=Location.HYPERLIQUID,
+        symbol=symbol,
+        default=symbol,
+    ))
+
+
+class Hyperliquid:
+
+    def __init__(self) -> None:
+        self.base_url = 'https://api.hyperliquid.xyz'
+        self.session = create_session()
+        self.arb_usdc = Asset('eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831')
+
+    def _query(
+            self,
+            address: ChecksumEvmAddress,
+            account_type: Literal['clearinghouseState', 'spotClearinghouseState'],
+    ) -> dict[str, Any]:
+        """Query hyperliquid for balances.
+
+        May raise:
+            - RemoteError
+        """
+        log.debug(f'Querying hyperliquid balances at {self.base_url}/info with {account_type=} and {address=}')  # noqa: E501
+        try:
+            response = self.session.post(
+                url=f'{self.base_url}/info',
+                json={'type': account_type, 'user': address},
+            )
+            data = response.json()
+        except requests.exceptions.RequestException as e:
+            log.error(
+                f'Query to hyperliquid for {address} and {account_type=} failed due to '
+                f'{e}. Skipping',
+            )
+            raise RemoteError(
+                f'Failed to query balances of {account_type} for {address} due to {e}',
+            ) from e
+
+        if response.status_code != HTTPStatus.OK:
+            log.error(
+                f'Failed to query {account_type} balances for {address} with status '
+                f'code {response.status_code} and text {response.text}',
+            )
+            raise RemoteError(f'Failed to query balances of {account_type} for {address} with status code {response.status_code}')  # noqa: E501
+
+        return data
+
+    def query_balances(self, address: ChecksumEvmAddress) -> dict[Asset, FVal]:
+        """
+        Queries both spot and perp balances since they are returned in two different endpoints.
+        Hyperliquid has like two `accounts` for each address, one for spot and one for perpetuals.
+
+        https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-a-users-token-balances
+        https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-users-perpetuals-account-summary
+        """
+        balances: defaultdict[Asset, FVal] = defaultdict(FVal)
+        try:
+            data = self._query(address=address, account_type='spotClearinghouseState')
+        except RemoteError as e:
+            log.error(f'Skipping spotClearinghouseState balances in hyperliquid of {address} due to {e}')  # noqa: E501
+            data = {}
+
+        for asset_entry in data.get('balances', []):
+            try:
+                if (balance := deserialize_fval(
+                    value=asset_entry['total'],
+                    name='spotClearinghouseState total balances',
+                    location='hyperliquid',
+                )) == ZERO:
+                    continue
+
+                asset = asset_from_hyperliquid(asset_entry['coin'])
+
+            except (DeserializationError, UnknownAsset, WrongAssetType, KeyError) as e:
+                log.error(f'Failed to read balance {asset_entry} from hyperliquid due to {e}. Skipping')  # noqa: E501
+                continue
+
+            if balance != ZERO:
+                balances[asset] += balance
+
+        try:
+            perp_data = self._query(address=address, account_type='clearinghouseState')
+        except RemoteError as e:
+            log.error(f'Skipping spotClearinghouseState balances in hyperliquid of {address} due to {e}')  # noqa: E501
+        else:
+            balances[self.arb_usdc] += deserialize_fval(
path: rotkehlchen/externalapis/hyperliquid.py
user: login: prettyirrelevant
body: deserialize_fval can raise DeserializationError so it needs to be handled.
created_at: 2025-03-07T14:07:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,49 @@
+import logging
+from collections import defaultdict
+from typing import TYPE_CHECKING
+
+from rotkehlchen.accounting.structures.balance import Balance, BalanceSheet
+from rotkehlchen.chain.arbitrum_one.modules.hyperliquid.constants import CPT_HYPER
+from rotkehlchen.chain.ethereum.interfaces.balances import BalancesSheetType, ProtocolWithBalance
+from rotkehlchen.externalapis.hyperliquid import Hyperliquid
+from rotkehlchen.history.events.structures.types import HistoryEventSubType, HistoryEventType
+from rotkehlchen.inquirer import Inquirer
+from rotkehlchen.logging import RotkehlchenLogsAdapter
+
+if TYPE_CHECKING:
+    from rotkehlchen.chain.arbitrum_one.decoding.decoder import ArbitrumOneTransactionDecoder
+    from rotkehlchen.chain.arbitrum_one.node_inquirer import ArbitrumOneInquirer
+
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
+
+class HyperliquidBalances(ProtocolWithBalance):
+    def __init__(
+            self,
+            evm_inquirer: 'ArbitrumOneInquirer',
+            tx_decoder: 'ArbitrumOneTransactionDecoder',
+    ):
+        super().__init__(
+            evm_inquirer=evm_inquirer,
+            tx_decoder=tx_decoder,
+            counterparty=CPT_HYPER,
+            deposit_event_types={(HistoryEventType.DEPOSIT, HistoryEventSubType.DEPOSIT_ASSET)},
+        )
+
+    def query_balances(self) -> 'BalancesSheetType':
+        balances: BalancesSheetType = defaultdict(BalanceSheet)
+        if len(addresses_with_deposits := list(self.addresses_with_deposits(products=None))) == 0:
+            return balances
+
+        hyperliquid = Hyperliquid()
path: rotkehlchen/chain/arbitrum_one/modules/hyperliquid/balances.py
user: login: LefterisJP
body: this does not make sense. if this is the only place you use it then just make it part of this class. Or initialize it at class constructor. Or better yet initialize it first time you need it.
```suggestion
        if self.hyperliquid_api is None:
            self.hyperliquid_api = HyperliquidAPI()
```

In that class you create a connection session, initialize a token and set a string. It makes no sense to redo that every time you query balances.
created_at: 2025-03-07T17:06:12Z
author_association: MEMBER
diff_hunk: @@ -128,6 +128,8 @@
 INSERT OR IGNORE INTO location(location, seq) VALUES ('u', 53);
 /* Binance Smart Chain */
 INSERT OR IGNORE INTO location(location, seq) VALUES ('v', 54);
+/* Hyperliquid */
+INSERT OR IGNORE INTO location(location, seq) VALUES ('w', 55);
path: rotkehlchen/db/schema.py
user: login: LefterisJP
body: hmmmm No I don't think this is a good idea. We do not use protocols as locations.
created_at: 2025-03-07T17:07:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,128 @@
+import logging
+from collections import defaultdict
+from http import HTTPStatus
+from typing import Any, Literal
+
+import requests
+
+from rotkehlchen.assets.asset import Asset
+from rotkehlchen.assets.utils import symbol_to_asset_or_token
+from rotkehlchen.constants.misc import ZERO
+from rotkehlchen.errors.asset import UnknownAsset, WrongAssetType
+from rotkehlchen.errors.misc import RemoteError
+from rotkehlchen.errors.serialization import DeserializationError
+from rotkehlchen.fval import FVal
+from rotkehlchen.globaldb.handler import GlobalDBHandler
+from rotkehlchen.logging import RotkehlchenLogsAdapter
+from rotkehlchen.serialization.deserialize import deserialize_fval
+from rotkehlchen.types import ChecksumEvmAddress, Location
+from rotkehlchen.utils.network import create_session
+
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
+
+def asset_from_hyperliquid(symbol: str) -> Asset:
+    """Fetch asset from hperliquid location
+
+    May raise:
+    - UnsupportedAsset
+    - UnknownAsset
+    """
+    return symbol_to_asset_or_token(GlobalDBHandler.get_assetid_from_exchange_name(
+        exchange=Location.HYPERLIQUID,
+        symbol=symbol,
+        default=symbol,
+    ))
+
+
+class Hyperliquid:
path: rotkehlchen/externalapis/hyperliquid.py
user: login: LefterisJP
body: ```suggestion
class HyperliquidAPI:
```
created_at: 2025-03-07T17:08:21Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,128 @@
+import logging
+from collections import defaultdict
+from http import HTTPStatus
+from typing import Any, Literal
+
+import requests
+
+from rotkehlchen.assets.asset import Asset
+from rotkehlchen.assets.utils import symbol_to_asset_or_token
+from rotkehlchen.constants.misc import ZERO
+from rotkehlchen.errors.asset import UnknownAsset, WrongAssetType
+from rotkehlchen.errors.misc import RemoteError
+from rotkehlchen.errors.serialization import DeserializationError
+from rotkehlchen.fval import FVal
+from rotkehlchen.globaldb.handler import GlobalDBHandler
+from rotkehlchen.logging import RotkehlchenLogsAdapter
+from rotkehlchen.serialization.deserialize import deserialize_fval
+from rotkehlchen.types import ChecksumEvmAddress, Location
+from rotkehlchen.utils.network import create_session
+
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
+
+def asset_from_hyperliquid(symbol: str) -> Asset:
+    """Fetch asset from hperliquid location
path: rotkehlchen/externalapis/hyperliquid.py
user: login: LefterisJP
body: ```suggestion
    """Fetch asset from hyperliquid location
```
created_at: 2025-03-07T17:08:34Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,128 @@
+import logging
+from collections import defaultdict
+from http import HTTPStatus
+from typing import Any, Literal
+
+import requests
+
+from rotkehlchen.assets.asset import Asset
+from rotkehlchen.assets.utils import symbol_to_asset_or_token
+from rotkehlchen.constants.misc import ZERO
+from rotkehlchen.errors.asset import UnknownAsset, WrongAssetType
+from rotkehlchen.errors.misc import RemoteError
+from rotkehlchen.errors.serialization import DeserializationError
+from rotkehlchen.fval import FVal
+from rotkehlchen.globaldb.handler import GlobalDBHandler
+from rotkehlchen.logging import RotkehlchenLogsAdapter
+from rotkehlchen.serialization.deserialize import deserialize_fval
+from rotkehlchen.types import ChecksumEvmAddress, Location
+from rotkehlchen.utils.network import create_session
+
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
+
+def asset_from_hyperliquid(symbol: str) -> Asset:
+    """Fetch asset from hperliquid location
+
+    May raise:
+    - UnsupportedAsset
+    - UnknownAsset
+    """
+    return symbol_to_asset_or_token(GlobalDBHandler.get_assetid_from_exchange_name(
+        exchange=Location.HYPERLIQUID,
path: rotkehlchen/externalapis/hyperliquid.py
user: login: LefterisJP
body: So you want to treat it like an exchange with mappings and that's why you added a Location for it? I don't like this. This is breaking our pattern. It looks like a protocol to me and I would not add a location
created_at: 2025-03-07T17:10:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,128 @@
+import logging
+from collections import defaultdict
+from http import HTTPStatus
+from typing import Any, Literal
+
+import requests
+
+from rotkehlchen.assets.asset import Asset
+from rotkehlchen.assets.utils import symbol_to_asset_or_token
+from rotkehlchen.constants.misc import ZERO
+from rotkehlchen.errors.asset import UnknownAsset, WrongAssetType
+from rotkehlchen.errors.misc import RemoteError
+from rotkehlchen.errors.serialization import DeserializationError
+from rotkehlchen.fval import FVal
+from rotkehlchen.globaldb.handler import GlobalDBHandler
+from rotkehlchen.logging import RotkehlchenLogsAdapter
+from rotkehlchen.serialization.deserialize import deserialize_fval
+from rotkehlchen.types import ChecksumEvmAddress, Location
+from rotkehlchen.utils.network import create_session
+
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
+
+def asset_from_hyperliquid(symbol: str) -> Asset:
+    """Fetch asset from hperliquid location
+
+    May raise:
+    - UnsupportedAsset
+    - UnknownAsset
+    """
+    return symbol_to_asset_or_token(GlobalDBHandler.get_assetid_from_exchange_name(
+        exchange=Location.HYPERLIQUID,
+        symbol=symbol,
+        default=symbol,
+    ))
+
+
+class Hyperliquid:
+
+    def __init__(self) -> None:
+        self.base_url = 'https://api.hyperliquid.xyz'
+        self.session = create_session()
+        self.arb_usdc = Asset('eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831')
+
+    def _query(
+            self,
+            address: ChecksumEvmAddress,
+            account_type: Literal['clearinghouseState', 'spotClearinghouseState'],
+    ) -> dict[str, Any]:
+        """Query hyperliquid for balances.
+
+        May raise:
+            - RemoteError
+        """
+        log.debug(f'Querying hyperliquid balances at {self.base_url}/info with {account_type=} and {address=}')  # noqa: E501
+        try:
+            response = self.session.post(
+                url=f'{self.base_url}/info',
+                json={'type': account_type, 'user': address},
+            )
+            data = response.json()
+        except requests.exceptions.RequestException as e:
+            log.error(
+                f'Query to hyperliquid for {address} and {account_type=} failed due to '
path: rotkehlchen/externalapis/hyperliquid.py
user: login: LefterisJP
body: ```suggestion
                f'Querying hyperliquid for {address} and {account_type=} failed due to '
```
created_at: 2025-03-07T17:12:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,128 @@
+import logging
+from collections import defaultdict
+from http import HTTPStatus
+from typing import Any, Literal
+
+import requests
+
+from rotkehlchen.assets.asset import Asset
+from rotkehlchen.assets.utils import symbol_to_asset_or_token
+from rotkehlchen.constants.misc import ZERO
+from rotkehlchen.errors.asset import UnknownAsset, WrongAssetType
+from rotkehlchen.errors.misc import RemoteError
+from rotkehlchen.errors.serialization import DeserializationError
+from rotkehlchen.fval import FVal
+from rotkehlchen.globaldb.handler import GlobalDBHandler
+from rotkehlchen.logging import RotkehlchenLogsAdapter
+from rotkehlchen.serialization.deserialize import deserialize_fval
+from rotkehlchen.types import ChecksumEvmAddress, Location
+from rotkehlchen.utils.network import create_session
+
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
+
+def asset_from_hyperliquid(symbol: str) -> Asset:
+    """Fetch asset from hperliquid location
+
+    May raise:
+    - UnsupportedAsset
+    - UnknownAsset
+    """
+    return symbol_to_asset_or_token(GlobalDBHandler.get_assetid_from_exchange_name(
+        exchange=Location.HYPERLIQUID,
+        symbol=symbol,
+        default=symbol,
+    ))
+
+
+class Hyperliquid:
+
+    def __init__(self) -> None:
+        self.base_url = 'https://api.hyperliquid.xyz'
+        self.session = create_session()
+        self.arb_usdc = Asset('eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831')
+
+    def _query(
+            self,
+            address: ChecksumEvmAddress,
+            account_type: Literal['clearinghouseState', 'spotClearinghouseState'],
+    ) -> dict[str, Any]:
+        """Query hyperliquid for balances.
+
+        May raise:
+            - RemoteError
+        """
+        log.debug(f'Querying hyperliquid balances at {self.base_url}/info with {account_type=} and {address=}')  # noqa: E501
+        try:
+            response = self.session.post(
+                url=f'{self.base_url}/info',
+                json={'type': account_type, 'user': address},
+            )
+            data = response.json()
+        except requests.exceptions.RequestException as e:
+            log.error(
+                f'Query to hyperliquid for {address} and {account_type=} failed due to '
+                f'{e}. Skipping',
+            )
+            raise RemoteError(
+                f'Failed to query balances of {account_type} for {address} due to {e}',
path: rotkehlchen/externalapis/hyperliquid.py
user: login: LefterisJP
body: ```suggestion
                f'Failed to query hyperliquid balances of {account_type} for {address} due to {e}',
```
created_at: 2025-03-07T17:12:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,128 @@
+import logging
+from collections import defaultdict
+from http import HTTPStatus
+from typing import Any, Literal
+
+import requests
+
+from rotkehlchen.assets.asset import Asset
+from rotkehlchen.assets.utils import symbol_to_asset_or_token
+from rotkehlchen.constants.misc import ZERO
+from rotkehlchen.errors.asset import UnknownAsset, WrongAssetType
+from rotkehlchen.errors.misc import RemoteError
+from rotkehlchen.errors.serialization import DeserializationError
+from rotkehlchen.fval import FVal
+from rotkehlchen.globaldb.handler import GlobalDBHandler
+from rotkehlchen.logging import RotkehlchenLogsAdapter
+from rotkehlchen.serialization.deserialize import deserialize_fval
+from rotkehlchen.types import ChecksumEvmAddress, Location
+from rotkehlchen.utils.network import create_session
+
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
+
+def asset_from_hyperliquid(symbol: str) -> Asset:
+    """Fetch asset from hperliquid location
+
+    May raise:
+    - UnsupportedAsset
+    - UnknownAsset
+    """
+    return symbol_to_asset_or_token(GlobalDBHandler.get_assetid_from_exchange_name(
+        exchange=Location.HYPERLIQUID,
+        symbol=symbol,
+        default=symbol,
+    ))
+
+
+class Hyperliquid:
+
+    def __init__(self) -> None:
+        self.base_url = 'https://api.hyperliquid.xyz'
+        self.session = create_session()
+        self.arb_usdc = Asset('eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831')
+
+    def _query(
+            self,
+            address: ChecksumEvmAddress,
+            account_type: Literal['clearinghouseState', 'spotClearinghouseState'],
+    ) -> dict[str, Any]:
+        """Query hyperliquid for balances.
+
+        May raise:
+            - RemoteError
+        """
+        log.debug(f'Querying hyperliquid balances at {self.base_url}/info with {account_type=} and {address=}')  # noqa: E501
+        try:
+            response = self.session.post(
+                url=f'{self.base_url}/info',
+                json={'type': account_type, 'user': address},
+            )
+            data = response.json()
+        except requests.exceptions.RequestException as e:
+            log.error(
+                f'Query to hyperliquid for {address} and {account_type=} failed due to '
+                f'{e}. Skipping',
+            )
+            raise RemoteError(
+                f'Failed to query balances of {account_type} for {address} due to {e}',
+            ) from e
+
+        if response.status_code != HTTPStatus.OK:
+            log.error(
+                f'Failed to query {account_type} balances for {address} with status '
path: rotkehlchen/externalapis/hyperliquid.py
user: login: LefterisJP
body: ```suggestion
                f'Failed to query hyperliquid {account_type} balances for {address} with status '
```
created_at: 2025-03-07T17:12:43Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,128 @@
+import logging
+from collections import defaultdict
+from http import HTTPStatus
+from typing import Any, Literal
+
+import requests
+
+from rotkehlchen.assets.asset import Asset
+from rotkehlchen.assets.utils import symbol_to_asset_or_token
+from rotkehlchen.constants.misc import ZERO
+from rotkehlchen.errors.asset import UnknownAsset, WrongAssetType
+from rotkehlchen.errors.misc import RemoteError
+from rotkehlchen.errors.serialization import DeserializationError
+from rotkehlchen.fval import FVal
+from rotkehlchen.globaldb.handler import GlobalDBHandler
+from rotkehlchen.logging import RotkehlchenLogsAdapter
+from rotkehlchen.serialization.deserialize import deserialize_fval
+from rotkehlchen.types import ChecksumEvmAddress, Location
+from rotkehlchen.utils.network import create_session
+
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
+
+def asset_from_hyperliquid(symbol: str) -> Asset:
+    """Fetch asset from hperliquid location
+
+    May raise:
+    - UnsupportedAsset
+    - UnknownAsset
+    """
+    return symbol_to_asset_or_token(GlobalDBHandler.get_assetid_from_exchange_name(
+        exchange=Location.HYPERLIQUID,
+        symbol=symbol,
+        default=symbol,
+    ))
+
+
+class Hyperliquid:
+
+    def __init__(self) -> None:
+        self.base_url = 'https://api.hyperliquid.xyz'
+        self.session = create_session()
+        self.arb_usdc = Asset('eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831')
+
+    def _query(
+            self,
+            address: ChecksumEvmAddress,
+            account_type: Literal['clearinghouseState', 'spotClearinghouseState'],
+    ) -> dict[str, Any]:
+        """Query hyperliquid for balances.
+
+        May raise:
+            - RemoteError
+        """
+        log.debug(f'Querying hyperliquid balances at {self.base_url}/info with {account_type=} and {address=}')  # noqa: E501
+        try:
+            response = self.session.post(
+                url=f'{self.base_url}/info',
+                json={'type': account_type, 'user': address},
+            )
+            data = response.json()
+        except requests.exceptions.RequestException as e:
+            log.error(
+                f'Query to hyperliquid for {address} and {account_type=} failed due to '
+                f'{e}. Skipping',
+            )
+            raise RemoteError(
+                f'Failed to query balances of {account_type} for {address} due to {e}',
+            ) from e
+
+        if response.status_code != HTTPStatus.OK:
+            log.error(
+                f'Failed to query {account_type} balances for {address} with status '
+                f'code {response.status_code} and text {response.text}',
+            )
+            raise RemoteError(f'Failed to query balances of {account_type} for {address} with status code {response.status_code}')  # noqa: E501
path: rotkehlchen/externalapis/hyperliquid.py
user: login: LefterisJP
body: ```suggestion
            raise RemoteError(f'Failed to query hyperliquid balances of {account_type} for {address} with status code {response.status_code}')  # noqa: E501
```
created_at: 2025-03-07T17:12:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,128 @@
+import logging
+from collections import defaultdict
+from http import HTTPStatus
+from typing import Any, Literal
+
+import requests
+
+from rotkehlchen.assets.asset import Asset
+from rotkehlchen.assets.utils import symbol_to_asset_or_token
+from rotkehlchen.constants.misc import ZERO
+from rotkehlchen.errors.asset import UnknownAsset, WrongAssetType
+from rotkehlchen.errors.misc import RemoteError
+from rotkehlchen.errors.serialization import DeserializationError
+from rotkehlchen.fval import FVal
+from rotkehlchen.globaldb.handler import GlobalDBHandler
+from rotkehlchen.logging import RotkehlchenLogsAdapter
+from rotkehlchen.serialization.deserialize import deserialize_fval
+from rotkehlchen.types import ChecksumEvmAddress, Location
+from rotkehlchen.utils.network import create_session
+
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
+
+def asset_from_hyperliquid(symbol: str) -> Asset:
+    """Fetch asset from hperliquid location
+
+    May raise:
+    - UnsupportedAsset
+    - UnknownAsset
+    """
+    return symbol_to_asset_or_token(GlobalDBHandler.get_assetid_from_exchange_name(
+        exchange=Location.HYPERLIQUID,
+        symbol=symbol,
+        default=symbol,
+    ))
+
+
+class Hyperliquid:
+
+    def __init__(self) -> None:
+        self.base_url = 'https://api.hyperliquid.xyz'
+        self.session = create_session()
+        self.arb_usdc = Asset('eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831')
+
+    def _query(
+            self,
+            address: ChecksumEvmAddress,
+            account_type: Literal['clearinghouseState', 'spotClearinghouseState'],
+    ) -> dict[str, Any]:
+        """Query hyperliquid for balances.
+
+        May raise:
+            - RemoteError
+        """
+        log.debug(f'Querying hyperliquid balances at {self.base_url}/info with {account_type=} and {address=}')  # noqa: E501
+        try:
+            response = self.session.post(
+                url=f'{self.base_url}/info',
+                json={'type': account_type, 'user': address},
+            )
+            data = response.json()
+        except requests.exceptions.RequestException as e:
+            log.error(
+                f'Query to hyperliquid for {address} and {account_type=} failed due to '
+                f'{e}. Skipping',
+            )
+            raise RemoteError(
+                f'Failed to query balances of {account_type} for {address} due to {e}',
+            ) from e
+
+        if response.status_code != HTTPStatus.OK:
+            log.error(
+                f'Failed to query {account_type} balances for {address} with status '
+                f'code {response.status_code} and text {response.text}',
+            )
+            raise RemoteError(f'Failed to query balances of {account_type} for {address} with status code {response.status_code}')  # noqa: E501
+
+        return data
+
+    def query_balances(self, address: ChecksumEvmAddress) -> dict[Asset, FVal]:
+        """
+        Queries both spot and perp balances since they are returned in two different endpoints.
+        Hyperliquid has like two `accounts` for each address, one for spot and one for perpetuals.
path: rotkehlchen/externalapis/hyperliquid.py
user: login: LefterisJP
body: ```suggestion
        Hyperliquid has two `accounts` for each address, one for spot and one for perpetuals.
```
created_at: 2025-03-07T17:13:16Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,128 @@
+import logging
+from collections import defaultdict
+from http import HTTPStatus
+from typing import Any, Literal
+
+import requests
+
+from rotkehlchen.assets.asset import Asset
+from rotkehlchen.assets.utils import symbol_to_asset_or_token
+from rotkehlchen.constants.misc import ZERO
+from rotkehlchen.errors.asset import UnknownAsset, WrongAssetType
+from rotkehlchen.errors.misc import RemoteError
+from rotkehlchen.errors.serialization import DeserializationError
+from rotkehlchen.fval import FVal
+from rotkehlchen.globaldb.handler import GlobalDBHandler
+from rotkehlchen.logging import RotkehlchenLogsAdapter
+from rotkehlchen.serialization.deserialize import deserialize_fval
+from rotkehlchen.types import ChecksumEvmAddress, Location
+from rotkehlchen.utils.network import create_session
+
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
+
+def asset_from_hyperliquid(symbol: str) -> Asset:
+    """Fetch asset from hperliquid location
+
+    May raise:
+    - UnsupportedAsset
+    - UnknownAsset
+    """
+    return symbol_to_asset_or_token(GlobalDBHandler.get_assetid_from_exchange_name(
+        exchange=Location.HYPERLIQUID,
+        symbol=symbol,
+        default=symbol,
+    ))
+
+
+class Hyperliquid:
+
+    def __init__(self) -> None:
+        self.base_url = 'https://api.hyperliquid.xyz'
+        self.session = create_session()
+        self.arb_usdc = Asset('eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831')
+
+    def _query(
+            self,
+            address: ChecksumEvmAddress,
+            account_type: Literal['clearinghouseState', 'spotClearinghouseState'],
+    ) -> dict[str, Any]:
+        """Query hyperliquid for balances.
+
+        May raise:
+            - RemoteError
+        """
+        log.debug(f'Querying hyperliquid balances at {self.base_url}/info with {account_type=} and {address=}')  # noqa: E501
+        try:
+            response = self.session.post(
+                url=f'{self.base_url}/info',
+                json={'type': account_type, 'user': address},
+            )
+            data = response.json()
+        except requests.exceptions.RequestException as e:
+            log.error(
+                f'Query to hyperliquid for {address} and {account_type=} failed due to '
+                f'{e}. Skipping',
+            )
+            raise RemoteError(
+                f'Failed to query balances of {account_type} for {address} due to {e}',
+            ) from e
+
+        if response.status_code != HTTPStatus.OK:
+            log.error(
+                f'Failed to query {account_type} balances for {address} with status '
+                f'code {response.status_code} and text {response.text}',
+            )
+            raise RemoteError(f'Failed to query balances of {account_type} for {address} with status code {response.status_code}')  # noqa: E501
+
+        return data
+
+    def query_balances(self, address: ChecksumEvmAddress) -> dict[Asset, FVal]:
+        """
+        Queries both spot and perp balances since they are returned in two different endpoints.
+        Hyperliquid has like two `accounts` for each address, one for spot and one for perpetuals.
+
+        https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-a-users-token-balances
+        https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-users-perpetuals-account-summary
+        """
+        balances: defaultdict[Asset, FVal] = defaultdict(FVal)
+        try:
+            data = self._query(address=address, account_type='spotClearinghouseState')
+        except RemoteError as e:
+            log.error(f'Skipping spotClearinghouseState balances in hyperliquid of {address} due to {e}')  # noqa: E501
path: rotkehlchen/externalapis/hyperliquid.py
user: login: LefterisJP
body: ```suggestion
            log.error(f'Skipping spotClearinghouseState hyperliquid balances of {address=} due to {e}')  # noqa: E501
```
created_at: 2025-03-07T17:13:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,128 @@
+import logging
+from collections import defaultdict
+from http import HTTPStatus
+from typing import Any, Literal
+
+import requests
+
+from rotkehlchen.assets.asset import Asset
+from rotkehlchen.assets.utils import symbol_to_asset_or_token
+from rotkehlchen.constants.misc import ZERO
+from rotkehlchen.errors.asset import UnknownAsset, WrongAssetType
+from rotkehlchen.errors.misc import RemoteError
+from rotkehlchen.errors.serialization import DeserializationError
+from rotkehlchen.fval import FVal
+from rotkehlchen.globaldb.handler import GlobalDBHandler
+from rotkehlchen.logging import RotkehlchenLogsAdapter
+from rotkehlchen.serialization.deserialize import deserialize_fval
+from rotkehlchen.types import ChecksumEvmAddress, Location
+from rotkehlchen.utils.network import create_session
+
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
+
+def asset_from_hyperliquid(symbol: str) -> Asset:
+    """Fetch asset from hperliquid location
+
+    May raise:
+    - UnsupportedAsset
+    - UnknownAsset
+    """
+    return symbol_to_asset_or_token(GlobalDBHandler.get_assetid_from_exchange_name(
+        exchange=Location.HYPERLIQUID,
+        symbol=symbol,
+        default=symbol,
+    ))
+
+
+class Hyperliquid:
+
+    def __init__(self) -> None:
+        self.base_url = 'https://api.hyperliquid.xyz'
+        self.session = create_session()
+        self.arb_usdc = Asset('eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831')
+
+    def _query(
+            self,
+            address: ChecksumEvmAddress,
+            account_type: Literal['clearinghouseState', 'spotClearinghouseState'],
+    ) -> dict[str, Any]:
+        """Query hyperliquid for balances.
+
+        May raise:
+            - RemoteError
+        """
+        log.debug(f'Querying hyperliquid balances at {self.base_url}/info with {account_type=} and {address=}')  # noqa: E501
+        try:
+            response = self.session.post(
+                url=f'{self.base_url}/info',
+                json={'type': account_type, 'user': address},
+            )
+            data = response.json()
+        except requests.exceptions.RequestException as e:
+            log.error(
+                f'Query to hyperliquid for {address} and {account_type=} failed due to '
+                f'{e}. Skipping',
+            )
+            raise RemoteError(
+                f'Failed to query balances of {account_type} for {address} due to {e}',
+            ) from e
+
+        if response.status_code != HTTPStatus.OK:
+            log.error(
+                f'Failed to query {account_type} balances for {address} with status '
+                f'code {response.status_code} and text {response.text}',
+            )
+            raise RemoteError(f'Failed to query balances of {account_type} for {address} with status code {response.status_code}')  # noqa: E501
+
+        return data
+
+    def query_balances(self, address: ChecksumEvmAddress) -> dict[Asset, FVal]:
+        """
+        Queries both spot and perp balances since they are returned in two different endpoints.
+        Hyperliquid has like two `accounts` for each address, one for spot and one for perpetuals.
+
+        https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-a-users-token-balances
+        https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-users-perpetuals-account-summary
+        """
+        balances: defaultdict[Asset, FVal] = defaultdict(FVal)
+        try:
+            data = self._query(address=address, account_type='spotClearinghouseState')
+        except RemoteError as e:
+            log.error(f'Skipping spotClearinghouseState balances in hyperliquid of {address} due to {e}')  # noqa: E501
+            data = {}
+
+        for asset_entry in data.get('balances', []):
+            try:
+                if (balance := deserialize_fval(
+                    value=asset_entry['total'],
+                    name='spotClearinghouseState total balances',
+                    location='hyperliquid',
+                )) == ZERO:
+                    continue
+
+                asset = asset_from_hyperliquid(asset_entry['coin'])
+
+            except (DeserializationError, UnknownAsset, WrongAssetType, KeyError) as e:
+                log.error(f'Failed to read balance {asset_entry} from hyperliquid due to {e}. Skipping')  # noqa: E501
+                continue
+
+            if balance != ZERO:
+                balances[asset] += balance
+
+        try:
+            perp_data = self._query(address=address, account_type='clearinghouseState')
+        except RemoteError as e:
+            log.error(f'Skipping spotClearinghouseState balances in hyperliquid of {address} due to {e}')  # noqa: E501
path: rotkehlchen/externalapis/hyperliquid.py
user: login: LefterisJP
body: ```suggestion
            log.error(f'Skipping spotClearinghouseState hyperliquid balances of {address=} due to {e}')  # noqa: E501
```
created_at: 2025-03-07T17:14:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,128 @@
+import logging
+from collections import defaultdict
+from http import HTTPStatus
+from typing import Any, Literal
+
+import requests
+
+from rotkehlchen.assets.asset import Asset
+from rotkehlchen.assets.utils import symbol_to_asset_or_token
+from rotkehlchen.constants.misc import ZERO
+from rotkehlchen.errors.asset import UnknownAsset, WrongAssetType
+from rotkehlchen.errors.misc import RemoteError
+from rotkehlchen.errors.serialization import DeserializationError
+from rotkehlchen.fval import FVal
+from rotkehlchen.globaldb.handler import GlobalDBHandler
+from rotkehlchen.logging import RotkehlchenLogsAdapter
+from rotkehlchen.serialization.deserialize import deserialize_fval
+from rotkehlchen.types import ChecksumEvmAddress, Location
+from rotkehlchen.utils.network import create_session
+
+logger = logging.getLogger(__name__)
+log = RotkehlchenLogsAdapter(logger)
+
+
+def asset_from_hyperliquid(symbol: str) -> Asset:
+    """Fetch asset from hperliquid location
+
+    May raise:
+    - UnsupportedAsset
+    - UnknownAsset
+    """
+    return symbol_to_asset_or_token(GlobalDBHandler.get_assetid_from_exchange_name(
+        exchange=Location.HYPERLIQUID,
+        symbol=symbol,
+        default=symbol,
+    ))
+
+
+class Hyperliquid:
+
+    def __init__(self) -> None:
+        self.base_url = 'https://api.hyperliquid.xyz'
+        self.session = create_session()
+        self.arb_usdc = Asset('eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831')
+
+    def _query(
+            self,
+            address: ChecksumEvmAddress,
+            account_type: Literal['clearinghouseState', 'spotClearinghouseState'],
+    ) -> dict[str, Any]:
+        """Query hyperliquid for balances.
+
+        May raise:
+            - RemoteError
+        """
+        log.debug(f'Querying hyperliquid balances at {self.base_url}/info with {account_type=} and {address=}')  # noqa: E501
+        try:
+            response = self.session.post(
+                url=f'{self.base_url}/info',
+                json={'type': account_type, 'user': address},
+            )
+            data = response.json()
+        except requests.exceptions.RequestException as e:
+            log.error(
+                f'Query to hyperliquid for {address} and {account_type=} failed due to '
+                f'{e}. Skipping',
+            )
+            raise RemoteError(
+                f'Failed to query balances of {account_type} for {address} due to {e}',
+            ) from e
+
+        if response.status_code != HTTPStatus.OK:
+            log.error(
+                f'Failed to query {account_type} balances for {address} with status '
+                f'code {response.status_code} and text {response.text}',
+            )
+            raise RemoteError(f'Failed to query balances of {account_type} for {address} with status code {response.status_code}')  # noqa: E501
+
+        return data
+
+    def query_balances(self, address: ChecksumEvmAddress) -> dict[Asset, FVal]:
+        """
+        Queries both spot and perp balances since they are returned in two different endpoints.
+        Hyperliquid has like two `accounts` for each address, one for spot and one for perpetuals.
+
+        https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-a-users-token-balances
+        https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-users-perpetuals-account-summary
+        """
+        balances: defaultdict[Asset, FVal] = defaultdict(FVal)
+        try:
+            data = self._query(address=address, account_type='spotClearinghouseState')
+        except RemoteError as e:
+            log.error(f'Skipping spotClearinghouseState balances in hyperliquid of {address} due to {e}')  # noqa: E501
+            data = {}
+
+        for asset_entry in data.get('balances', []):
+            try:
+                if (balance := deserialize_fval(
+                    value=asset_entry['total'],
+                    name='spotClearinghouseState total balances',
+                    location='hyperliquid',
+                )) == ZERO:
+                    continue
+
+                asset = asset_from_hyperliquid(asset_entry['coin'])
+
+            except (DeserializationError, UnknownAsset, WrongAssetType, KeyError) as e:
+                log.error(f'Failed to read balance {asset_entry} from hyperliquid due to {e}. Skipping')  # noqa: E501
+                continue
+
+            if balance != ZERO:
+                balances[asset] += balance
+
+        try:
+            perp_data = self._query(address=address, account_type='clearinghouseState')
+        except RemoteError as e:
+            log.error(f'Skipping spotClearinghouseState balances in hyperliquid of {address} due to {e}')  # noqa: E501
+        else:
+            try:
+                balances[self.arb_usdc] += deserialize_fval(
+                    value=perp_data.get('crossMarginSummary', {}).get('accountValue', 0),
+                    name='clearinghouseState total balances',
+                    location='hyperliquid',
+                )
+            except DeserializationError as e:
+                log.error(f'Failed to read crossMarginSummary in hyperliquid due to {e}')
path: rotkehlchen/externalapis/hyperliquid.py
user: login: LefterisJP
body: ```suggestion
                log.error(f'Failed to read hyperliquid crossMarginSummary due to {e}')
```
created_at: 2025-03-07T17:14:52Z
author_association: MEMBER
diff_hunk: @@ -1157,7 +1161,7 @@ def _query_protocols_with_balance(self, chain_id: CHAIN_IDS_WITH_BALANCE_PROTOCO
             try:
                 protocol_balances = protocol_with_balance.query_balances()
             except RemoteError as e:
-                log.error(f'Failed to query balances for {protocol} due to {e}. Skipping')
+                log.error(f'Failed to query hyperliquid balances for {protocol} due to {e}. Skipping')  # noqa: E501
path: rotkehlchen/chain/aggregator.py
user: login: LefterisJP
body: what are you doing here?
created_at: 2025-03-11T14:24:43Z
author_association: MEMBER
diff_hunk: @@ -128,6 +128,8 @@
 INSERT OR IGNORE INTO location(location, seq) VALUES ('u', 53);
 /* Binance Smart Chain */
 INSERT OR IGNORE INTO location(location, seq) VALUES ('v', 54);
+/* Hyperliquid */
path: rotkehlchen/db/schema.py
user: login: LefterisJP
body: Didn't we say to remove this?
created_at: 2025-03-11T14:26:37Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,15 @@
+from rotkehlchen.errors.asset import UnknownAsset
+from rotkehlchen.globaldb.handler import GlobalDBHandler
+
+
+def get_asset_id_by_counterparty(symbol: str, counterparty: str) -> str:
path: rotkehlchen/globaldb/counterparty_mappings.py
user: login: LefterisJP
body: Add a docstring. Mention what can be raised
created_at: 2025-03-11T14:27:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,15 @@
+from rotkehlchen.errors.asset import UnknownAsset
+from rotkehlchen.globaldb.handler import GlobalDBHandler
+
+
+def get_asset_id_by_counterparty(symbol: str, counterparty: str) -> str:
+    with GlobalDBHandler().conn.read_ctx() as cursor:
+        cursor.execute(
+            'SELECT local_id FROM counterparty_asset_mappings WHERE '
+            'symbol=? AND counterparty=?',
+            (symbol, counterparty),
+        )
+        if (result := cursor.fetchone()) is not None:
+            return result[0]
+
+        raise UnknownAsset(f'Cannot find {symbol} for counterparty {counterparty}')
path: rotkehlchen/globaldb/counterparty_mappings.py
user: login: LefterisJP
body: This is not just unknown asset. 

Can it not also be unknown counterparty?
Can it not also be just no mapping existing for that combination?
created_at: 2025-03-11T14:28:47Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-03-07T11:43:37Z
body: ## [Codecov](https://app.codecov.io/gh/rotki/rotki/pull/9576?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) Report
Attention: Patch coverage is `77.04918%` with `28 lines` in your changes missing coverage. Please review.
> Project coverage is 55.97%. Comparing base [(`506e2a1`)](https://app.codecov.io/gh/rotki/rotki/commit/506e2a10f7e5f8644aa0885dbb564fff0cf11cd0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) to head [(`6604c7e`)](https://app.codecov.io/gh/rotki/rotki/commit/6604c7eb085eff96cecefb0c3ddf7a33fa3f05a9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki).
> Report is 9 commits behind head on develop.

| [Files with missing lines](https://app.codecov.io/gh/rotki/rotki/pull/9576?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) | Patch % | Lines |
|---|---|---|
| [rotkehlchen/externalapis/hyperliquid.py](https://app.codecov.io/gh/rotki/rotki/pull/9576?src=pr&el=tree&filepath=rotkehlchen%2Fexternalapis%2Fhyperliquid.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki#diff-cm90a2VobGNoZW4vZXh0ZXJuYWxhcGlzL2h5cGVybGlxdWlkLnB5) | 73.84% | [15 Missing and 2 partials :warning: ](https://app.codecov.io/gh/rotki/rotki/pull/9576?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) |
| [...chain/arbitrum\_one/modules/hyperliquid/balances.py](https://app.codecov.io/gh/rotki/rotki/pull/9576?src=pr&el=tree&filepath=rotkehlchen%2Fchain%2Farbitrum_one%2Fmodules%2Fhyperliquid%2Fbalances.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki#diff-cm90a2VobGNoZW4vY2hhaW4vYXJiaXRydW1fb25lL21vZHVsZXMvaHlwZXJsaXF1aWQvYmFsYW5jZXMucHk=) | 82.75% | [3 Missing and 2 partials :warning: ](https://app.codecov.io/gh/rotki/rotki/pull/9576?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) |
| [rotkehlchen/globaldb/upgrades/v11\_v12.py](https://app.codecov.io/gh/rotki/rotki/pull/9576?src=pr&el=tree&filepath=rotkehlchen%2Fglobaldb%2Fupgrades%2Fv11_v12.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki#diff-cm90a2VobGNoZW4vZ2xvYmFsZGIvdXBncmFkZXMvdjExX3YxMi5weQ==) | 75.00% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/rotki/rotki/pull/9576?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) |
| [rotkehlchen/globaldb/counterparty\_mappings.py](https://app.codecov.io/gh/rotki/rotki/pull/9576?src=pr&el=tree&filepath=rotkehlchen%2Fglobaldb%2Fcounterparty_mappings.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki#diff-cm90a2VobGNoZW4vZ2xvYmFsZGIvY291bnRlcnBhcnR5X21hcHBpbmdzLnB5) | 75.00% | [1 Missing and 1 partial :warning: ](https://app.codecov.io/gh/rotki/rotki/pull/9576?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) |
| [rotkehlchen/errors/asset.py](https://app.codecov.io/gh/rotki/rotki/pull/9576?src=pr&el=tree&filepath=rotkehlchen%2Ferrors%2Fasset.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki#diff-cm90a2VobGNoZW4vZXJyb3JzL2Fzc2V0LnB5) | 66.66% | [1 Missing :warning: ](https://app.codecov.io/gh/rotki/rotki/pull/9576?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           develop    #9576      +/-   ##
===========================================
+ Coverage    50.27%   55.97%   +5.69%     
===========================================
  Files         1909     1913       +4     
  Lines       139480   139597     +117     
  Branches     14171    14181      +10     
===========================================
+ Hits         70127    78133    +8006     
+ Misses       66760    58642    -8118     
- Partials      2593     2822     +229     
```

| [Flag](https://app.codecov.io/gh/rotki/rotki/pull/9576/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) | Coverage Δ | |
|---|---|---|
| [backend](https://app.codecov.io/gh/rotki/rotki/pull/9576/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) | `81.01% <77.04%> (+12.58%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/rotki/rotki/pull/9576?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-11T12:29:13Z
body: <!-- action/sqldiff -->

SQL Diff for `rotkehlchen/data/global.db`
```sql
CREATE TABLE counterparty_asset_mappings (
    counterparty TEXT NOT NULL,
    symbol TEXT NOT NULL,
    local_id TEXT NOT NULL COLLATE NOCASE,
    PRIMARY KEY (counterparty, symbol)
);
UPDATE settings SET value='12' WHERE name='version';

```

author_association: NONE
user: login: yabirgb
created_at: 2025-03-11T14:17:47Z
body: The failing test works after merging the changes of https://github.com/rotki/assets/pull/258

It remains the logic to update remotly the mappings but that can be another PR to not mix things
author_association: MEMBER
user: login: rotkibot
created_at: 2025-03-12T10:44:34Z
body: rotki/test-caching#114 was successfully merged
author_association: NONE