Repository: hummingbot/hummingbot
Pull request: cleanup/Remove legacy throttler
**Before submitting this PR, please make sure**:

- [X] Your code builds clean without any errors or warnings
- [X] You are using approved title ("feat/", "fix/", "docs/", "refactor/")

**A description of the changes proposed in the pull request**:
Removes the remaining legacy throttler and its associated test cases.


**Tests performed by the developer**:
None


**Tips for QA testing**:
None




Problem statement:
Remove the remaining API Throttlers from the code base
## Description

Remove all remaining unused code related to API throttles from the code base

## Details

After refactoring the existing connectors that make use of the old API Throttlers to the consolidated, clean up the code base.

The remaining files are everything left inside the `/hummingbot/core/api_throttler/` folder.

Note: this can only be done after there is no more connectors using the files inside that folder.

## Definition of done
- [ ] Removal of all old API Throttlers code

Hints from issue discussion:

Commit message: (fix) Fixes one of the new test cases.

Patch to review:
diff --git a/hummingbot/connector/exchange/binance/binance_exchange.pyx b/hummingbot/connector/exchange/binance/binance_exchange.pyx
index 32faf96d9be..e50429d5d58 100755
--- a/hummingbot/connector/exchange/binance/binance_exchange.pyx
+++ b/hummingbot/connector/exchange/binance/binance_exchange.pyx
@@ -26,7 +26,6 @@ from typing import (
 )
 
 import conf
-from hummingbot.core.utils.asyncio_throttle import Throttler
 from hummingbot.core.utils import async_ttl_cache
 from hummingbot.core.utils.async_call_scheduler import AsyncCallScheduler
 from hummingbot.core.clock cimport Clock
diff --git a/hummingbot/core/data_type/order_book_message.py b/hummingbot/core/data_type/order_book_message.py
index 7df41b33662..75f0a677aa8 100644
--- a/hummingbot/core/data_type/order_book_message.py
+++ b/hummingbot/core/data_type/order_book_message.py
@@ -79,7 +79,9 @@ def has_trade_id(self) -> bool:
         return self.type == OrderBookMessageType.TRADE
 
     def __eq__(self, other: "OrderBookMessage") -> bool:
-        if self.has_update_id and other.has_update_id:
+        if self.type != other.type:
+            return False
+        elif self.has_update_id and other.has_update_id:
             return self.update_id == other.update_id
         elif self.has_trade_id and other.has_trade_id:
             return self.trade_id == other.trade_id
diff --git a/hummingbot/core/utils/asyncio_throttle.py b/hummingbot/core/utils/asyncio_throttle.py
deleted file mode 100644
index 757f360abd3..00000000000
--- a/hummingbot/core/utils/asyncio_throttle.py
+++ /dev/null
@@ -1,105 +0,0 @@
-import logging
-import time
-import asyncio
-from collections import deque
-from typing import (
-    Optional,
-    Tuple,
-    Deque
-)
-
-RequestWeight = int
-Seconds = float
-Timestamp_s = float
-TaskLog = Tuple[Timestamp_s, RequestWeight]
-
-
-class Throttler:
-    def __init__(self,
-                 rate_limit: Tuple[RequestWeight, Seconds],
-                 period_safety_margin: Seconds = 0.1,
-                 retry_interval: Seconds = 0.1):
-        """
-        :param rate_limit: Max weight allowed in the given period
-        :param retry_interval: Time between each limit check
-        """
-        self._rate_limit_weight: int = rate_limit[0]
-        self._period: float = rate_limit[1]
-        self._retry_interval: float = retry_interval
-        self._period_safety_margin = period_safety_margin
-        self._task_logs: Deque[TaskLog] = deque()
-
-    def weighted_task(self,
-                      request_weight):
-        return ThrottlerContextManager(
-            rate_limit=self._rate_limit_weight,
-            period=self._period,
-            request_weight=request_weight,
-            task_logs=self._task_logs)
-
-
-class ThrottlerContextManager:
-    throttler_logger: Optional[logging.Logger] = None
-
-    @classmethod
-    def logger(cls) -> logging.Logger:
-        if cls.throttler_logger is None:
-            cls.throttler_logger = logging.getLogger(__name__)
-        return cls.throttler_logger
-
-    def __init__(self,
-                 task_logs: Deque[TaskLog],
-                 rate_limit: RequestWeight,
-                 request_weight: RequestWeight = 1,
-                 period_safety_margin: Seconds = 0.1,
-                 period: Seconds = 1.0,
-                 retry_interval: Seconds = 0.1):
-        """
-        :param task_logs: Shared task logs
-        :param rate_limit: Max weight allowed in the given period
-        :param request_weight: Weight of the request of the added task
-        :param period_safety_margin: estimate for the network latency
-        :param period: Time interval of the rate limit
-        :param retry_interval: Time between each limit check
-        """
-        self._period_safety_margin = period_safety_margin
-        self._lock = asyncio.Lock()
-        self._request_weight: RequestWeight = request_weight
-        self._rate_limit: int = rate_limit
-        self._period: float = period
-        self._retry_interval: float = retry_interval
-        self._task_logs: Deque[TaskLog] = task_logs
-
-    def flush(self):
-        """
-        Remove task logs that have passed rate limit periods
-        :return:
-        """
-        now: float = time.time()
-        while self._task_logs:
-            task_ts, _ = self._task_logs[0]
-            elapsed: float = now - task_ts
-            if elapsed > self._period - self._period_safety_margin:
-                self._task_logs.popleft()
-            else:
-                break
-
-    async def acquire(self):
-        while True:
-            self.flush()
-            current_capacity: int = self._rate_limit - sum(weight for (ts, weight) in self._task_logs)
-            if current_capacity - self._request_weight > 0:
-                break
-            await self._sleep(self._retry_interval)
-        self._task_logs.append((time.time(), self._request_weight))
-
-    async def __aenter__(self):
-        async with self._lock:
-            await self.acquire()
-
-    async def __aexit__(self, exc_type, exc, tb):
-        pass
-
-    @staticmethod
-    async def _sleep(delay, result=None, *, loop=None):
-        await asyncio.sleep(delay, result, loop=loop)
diff --git a/hummingbot/core/utils/download_token_addresses.py b/hummingbot/core/utils/download_token_addresses.py
index 967954f2473..bff9d043c9c 100644
--- a/hummingbot/core/utils/download_token_addresses.py
+++ b/hummingbot/core/utils/download_token_addresses.py
@@ -39,35 +39,17 @@ async def download_dolomite_token_addresses(token_dict: Dict[str, str]):
 
 
 async def download_radar_relay_token_addresses(token_dict: Dict[str, str]):
-    page_count = 1
-    while True:
-        url = f"{RADAR_RELAY_ENDPOINT}?perPage=100&page={page_count}"
-        async with aiohttp.ClientSession() as client:
-            async with client.get(url, timeout=API_CALL_TIMEOUT) as response:
-                page_count += 1
-                try:
-                    if response.status == 200:
-                        markets = await response.json()
-                        if len(markets) == 0:
-                            break
-                        for market in markets:
-                            market_id = market.get("id")
-                            base, quote = market_id.split("-")
-                            if base not in token_dict:
-                                token_dict[base] = Web3.toChecksumAddress(market.get("baseTokenAddress"))
-                            if quote not in token_dict:
-                                token_dict[quote] = Web3.toChecksumAddress(market.get("quoteTokenAddress"))
-                    else:
-                        raise Exception(f"Call to {url} failed with status {response.status}")
-                except Exception as err:
-                    logging.getLogger().error(err)
-                    break
+    await _download_some_relay_token_addresses(token_dict, base_url=RADAR_RELAY_ENDPOINT)
 
 
 async def download_bamboo_relay_token_addresses(token_dict: Dict[str, str]):
+    await _download_some_relay_token_addresses(token_dict, base_url=BAMBOO_RELAY_ENDPOINT)
+
+
+async def _download_some_relay_token_addresses(token_dict: Dict[str, str], base_url):
     page_count = 1
     while True:
-        url = f"{BAMBOO_RELAY_ENDPOINT}?perPage=1000&page={page_count}"
+        url = f"{base_url}?perPage=1000&page={page_count}"
         async with aiohttp.ClientSession() as client:
             async with client.get(url, timeout=API_CALL_TIMEOUT) as response:
                 page_count += 1
diff --git a/test/hummingbot/core/data_type/test_order_book_message.py b/test/hummingbot/core/data_type/test_order_book_message.py
new file mode 100644
index 00000000000..9638fcd705e
--- /dev/null
+++ b/test/hummingbot/core/data_type/test_order_book_message.py
@@ -0,0 +1,250 @@
+import time
+import unittest
+
+from hummingbot.core.data_type.order_book_message import OrderBookMessage, \
+    OrderBookMessageType
+from hummingbot.core.data_type.order_book_row import OrderBookRow
+
+
+class OrderBookMessageTest(unittest.TestCase):
+    def test_update_id(self):
+        update_id = "someId"
+
+        msg = OrderBookMessage(
+            message_type=OrderBookMessageType.SNAPSHOT,
+            content={"update_id": update_id},
+            timestamp=time.time(),
+        )
+        self.assertEqual(update_id, msg.update_id)
+
+        msg = OrderBookMessage(
+            message_type=OrderBookMessageType.DIFF,
+            content={"update_id": update_id},
+            timestamp=time.time(),
+        )
+        self.assertEqual(update_id, msg.update_id)
+
+        msg = OrderBookMessage(
+            message_type=OrderBookMessageType.TRADE,
+            content={"someKey": "someValue"},
+            timestamp=time.time(),
+        )
+        self.assertEqual(-1, msg.update_id)
+
+    def test_first_update_id(self):
+        first_update_id = "firstUpdateId"
+        update_id = "someId"
+
+        msg = OrderBookMessage(
+            message_type=OrderBookMessageType.SNAPSHOT,
+            content={"someKey": "someValue"},
+            timestamp=time.time(),
+        )
+        self.assertEqual(-1, msg.first_update_id)
+
+        msg = OrderBookMessage(
+            message_type=OrderBookMessageType.DIFF,
+            content={
+                "update_id": update_id,
+                "first_update_id": first_update_id,
+            },
+            timestamp=time.time(),
+        )
+        self.assertEqual(first_update_id, msg.first_update_id)
+
+        msg = OrderBookMessage(
+            message_type=OrderBookMessageType.TRADE,
+            content={"someKey": "someValue"},
+            timestamp=time.time(),
+        )
+        self.assertEqual(-1, msg.first_update_id)
+
+    def test_trade_id(self):
+        trade_id = "someTradeId"
+
+        msg = OrderBookMessage(
+            message_type=OrderBookMessageType.SNAPSHOT,
+            content={"someKey": "someValue"},
+            timestamp=time.time(),
+        )
+        self.assertEqual(-1, msg.trade_id)
+
+        msg = OrderBookMessage(
+            message_type=OrderBookMessageType.DIFF,
+            content={"someKey": "someValue"},
+            timestamp=time.time(),
+        )
+        self.assertEqual(-1, msg.trade_id)
+
+        msg = OrderBookMessage(
+            message_type=OrderBookMessageType.TRADE,
+            content={"trade_id": trade_id},
+            timestamp=time.time(),
+        )
+        self.assertEqual(trade_id, msg.trade_id)
+
+    def test_trading_pair(self):
+        trading_pair = "BTC-USDT"
+
+        msg = OrderBookMessage(
+            message_type=OrderBookMessageType.SNAPSHOT,
+            content={"trading_pair": trading_pair},
+            timestamp=time.time(),
+        )
+        self.assertEqual(trading_pair, msg.trading_pair)
+
+    def test_bids_and_asks(self):
+        update_id = "someId"
+        msg = OrderBookMessage(
+            message_type=OrderBookMessageType.SNAPSHOT,
+            content={
+                "update_id": update_id,
+                "asks": [(1, 2), (3, 4)],
+                "bids": [(5, 6), (7, 8)],
+            },
+            timestamp=time.time(),
+        )
+
+        asks = msg.asks
+        self.assertEqual(2, len(asks))
+        self.assertTrue(isinstance(asks[0], OrderBookRow))
+        self.assertEqual(1, asks[0].price)
+        self.assertEqual(2, asks[0].amount)
+        self.assertEqual(update_id, asks[0].update_id)
+
+        bids = msg.bids
+        self.assertEqual(2, len(bids))
+        self.assertTrue(isinstance(bids[0], OrderBookRow))
+        self.assertEqual(5, bids[0].price)
+        self.assertEqual(6, bids[0].amount)
+        self.assertEqual(update_id, bids[0].update_id)
+
+    def test_has_update_id(self):
+        update_id = "someId"
+
+        msg = OrderBookMessage(
+            message_type=OrderBookMessageType.SNAPSHOT,
+            content={"update_id": update_id},
+            timestamp=time.time(),
+        )
+        self.assertTrue(msg.has_update_id)
+
+        msg = OrderBookMessage(
+            message_type=OrderBookMessageType.DIFF,
+            content={"update_id": update_id},
+            timestamp=time.time(),
+        )
+        self.assertTrue(msg.has_update_id)
+
+        msg = OrderBookMessage(
+            message_type=OrderBookMessageType.TRADE,
+            content={"someKey": "someValue"},
+            timestamp=time.time(),
+        )
+        self.assertFalse(msg.has_update_id)
+
+    def test_has_trade_id(self):
+        trade_id = "someTradeId"
+
+        msg = OrderBookMessage(
+            message_type=OrderBookMessageType.SNAPSHOT,
+            content={"someKey": "someValue"},
+            timestamp=time.time(),
+        )
+        self.assertFalse(msg.has_trade_id)
+
+        msg = OrderBookMessage(
+            message_type=OrderBookMessageType.DIFF,
+            content={"someKey": "someValue"},
+            timestamp=time.time(),
+        )
+        self.assertFalse(msg.has_trade_id)
+
+        msg = OrderBookMessage(
+            message_type=OrderBookMessageType.TRADE,
+            content={"trade_id": trade_id},
+            timestamp=time.time(),
+        )
+        self.assertTrue(msg.has_trade_id)
+
+    def test_equality(self):
+        trade_id = "someTradeId"
+        update_id = "someId"
+
+        snapshot1 = OrderBookMessage(
+            message_type=OrderBookMessageType.SNAPSHOT,
+            content={"update_id": update_id},
+            timestamp=time.time(),
+        )
+        snapshot2 = OrderBookMessage(
+            message_type=OrderBookMessageType.SNAPSHOT,
+            content={"update_id": update_id},
+            timestamp=time.time(),
+        )
+        diff1 = OrderBookMessage(
+            message_type=OrderBookMessageType.DIFF,
+            content={"update_id": update_id},
+            timestamp=time.time(),
+        )
+        diff2 = OrderBookMessage(
+            message_type=OrderBookMessageType.DIFF,
+            content={"update_id": update_id},
+            timestamp=time.time(),
+        )
+        trade1 = OrderBookMessage(
+            message_type=OrderBookMessageType.TRADE,
+            content={"trade_id": trade_id},
+            timestamp=time.time(),
+        )
+        trade2 = OrderBookMessage(
+            message_type=OrderBookMessageType.TRADE,
+            content={"trade_id": trade_id},
+            timestamp=time.time(),
+        )
+
+        self.assertNotEqual(snapshot1, diff1)
+        self.assertNotEqual(snapshot1, trade1)
+        self.assertNotEqual(diff1, trade1)
+        self.assertEqual(snapshot1, snapshot2)
+        self.assertEqual(diff1, diff2)
+        self.assertEqual(trade1, trade2)
+
+    def test_larger_than(self):
+        t = time.time()
+        trade1 = OrderBookMessage(
+            message_type=OrderBookMessageType.TRADE,
+            content={"trade_id": 5},
+            timestamp=t,
+        )
+        trade2 = OrderBookMessage(
+            message_type=OrderBookMessageType.TRADE,
+            content={"trade_id": 6},
+            timestamp=time.time() + 1,
+        )
+        snapshot1 = OrderBookMessage(
+            message_type=OrderBookMessageType.SNAPSHOT,
+            content={"update_id": 1},
+            timestamp=time.time() + 2,
+        )
+        snapshot2 = OrderBookMessage(
+            message_type=OrderBookMessageType.SNAPSHOT,
+            content={"update_id": 3},
+            timestamp=time.time() + 3,
+        )
+        diff1 = OrderBookMessage(
+            message_type=OrderBookMessageType.DIFF,
+            content={"update_id": 2},
+            timestamp=time.time() + 4,
+        )
+        diff2 = OrderBookMessage(
+            message_type=OrderBookMessageType.DIFF,
+            content={"update_id": 2},
+            timestamp=t,
+        )
+
+        self.assertTrue(trade1 < trade2)  # based on id
+        self.assertTrue(snapshot1 < snapshot2)  # based on id
+        self.assertTrue(snapshot1 < diff1)  # based on id
+        self.assertTrue(diff1 < snapshot2)  # based on id
+        self.assertTrue(trade1 < snapshot1)  # based on timestamp
+        self.assertTrue(diff2 < trade1)  # if same ts, ob messages < trade messages
diff --git a/test/hummingbot/core/utils/test_asyncio_throttle.py b/test/hummingbot/core/utils/test_asyncio_throttle.py
deleted file mode 100644
index 7547379c903..00000000000
--- a/test/hummingbot/core/utils/test_asyncio_throttle.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import unittest
-import asyncio
-import time
-from mock import patch
-from hummingbot.core.utils.asyncio_throttle import Throttler
-
-
-class AsyncioThrottleTest(unittest.TestCase):
-    def setUp(self):
-        self.retry_interval = -1
-        self.throttler = Throttler(rate_limit=(20, 1), period_safety_margin=0, retry_interval=self.retry_interval)
-        self.loop = asyncio.get_event_loop()
-
-    async def task(self, task_id, weight):
-        async with self.throttler.weighted_task(weight):
-            print(int(time.time()), f"Cat {task_id}: Meow {weight}")
-
-    @patch("hummingbot.core.utils.asyncio_throttle.ThrottlerContextManager._sleep")
-    def test_tasks_complete_without_delay_when_throttle_below_rate_limit(self, sleep_mock):
-        tasks = [self.task(1, 5), self.task(3, 5), self.task(3, 5), self.task(4, 4)]
-        self.loop.run_until_complete(asyncio.gather(*tasks))
-        for call_args in sleep_mock.call_args_list:
-            self.assertNotIn(self.retry_interval, call_args.args)
-
-    @patch("hummingbot.core.utils.asyncio_throttle.ThrottlerContextManager._sleep")
-    def test_tasks_complete_with_delay_when_throttle_above_rate_limit(self, sleep_mock):
-        tasks = [self.task(1, 5), self.task(3, 5), self.task(3, 5), self.task(4, 6)]
-        self.loop.run_until_complete(asyncio.gather(*tasks))
-        for call_args in sleep_mock.call_args_list:
-            self.assertNotIn(self.retry_interval, call_args.args)
diff --git a/test/hummingbot/core/utils/test_download_token_addresses.py b/test/hummingbot/core/utils/test_download_token_addresses.py
new file mode 100644
index 00000000000..7f71cfb81a8
--- /dev/null
+++ b/test/hummingbot/core/utils/test_download_token_addresses.py
@@ -0,0 +1,261 @@
+import asyncio
+import json
+import re
+import unittest
+from typing import Dict, Awaitable
+from unittest.mock import patch, MagicMock
+
+from aioresponses import aioresponses
+
+from hummingbot.core.utils.download_token_addresses import (
+    DOLOMITE_ENDPOINT,
+    download_dolomite_token_addresses,
+    RADAR_RELAY_ENDPOINT,
+    download_radar_relay_token_addresses,
+    BAMBOO_RELAY_ENDPOINT,
+    download_bamboo_relay_token_addresses,
+    download_erc20_token_addresses,
+)
+
+
+class DownloadTokenAddressesTest(unittest.TestCase):
+    def setUp(self) -> None:
+        super().setUp()
+        self.ev_loop = asyncio.get_event_loop()
+
+    def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1):
+        ret = self.ev_loop.run_until_complete(
+            asyncio.wait_for(coroutine, timeout))
+        return ret
+
+    @staticmethod
+    def get_dolomite_tokens_mock(
+        first_ticker: str, first_identifier: str, second_ticker: str,
+        second_identifier: str
+    ) -> Dict:
+        tokens = {
+            "global_objects": {},
+            "data": [
+                {
+                    "dolomite_token_id": 1934,
+                    "token_type": "ERC20",
+                    "version_number": 1,
+                    "identifier": first_identifier,
+                    "ticker": first_ticker,
+                    "name": {
+                        "singular": "Bitcoin",
+                        "plural": "Bitcoins"
+                    },
+                    "display_precision": 8,
+                    "precision": 18,
+                    "thumbnail_url": "string",
+                    "image_url": "string",
+                    "date_added": "string (date-time)"
+                },
+                {
+                    "dolomite_token_id": 1935,
+                    "token_type": "ERC20",
+                    "version_number": 1,
+                    "identifier": second_identifier,
+                    "ticker": second_ticker,
+                    "name": {
+                        "singular": "Bitcoin",
+                        "plural": "Bitcoins"
+                    },
+                    "display_precision": 8,
+                    "precision": 18,
+                    "thumbnail_url": "string",
+                    "image_url": "string",
+                    "date_added": "string (date-time)"
+                }
+            ],
+            "paging_metadata": None,
+            "cursor": None,
+            "error": None
+        }
+        return tokens
+
+    @aioresponses()
+    def test_download_dolomite_token_addresses(self, mocked_api):
+        url = DOLOMITE_ENDPOINT
+        lrc_identifier = "0x0000000000000000000000000000000000000000"
+        btc_identifier = "0x0000000000000000000000000000000000000001"
+        resp = self.get_dolomite_tokens_mock(
+            first_ticker="LRC",
+            first_identifier=lrc_identifier,
+            second_ticker="BTC",
+            second_identifier=btc_identifier,
+        )
+        mocked_api.get(url, body=json.dumps(resp))
+        token_dict = {}
+
+        self.async_run_with_timeout(
+            download_dolomite_token_addresses(token_dict))
+
+        self.assertIn("LRC", token_dict)
+        self.assertIn("BTC", token_dict)
+        self.assertEqual(token_dict["LRC"], lrc_identifier)
+        self.assertEqual(token_dict["BTC"], btc_identifier)
+
+    @aioresponses()
+    def test_download_dolomite_token_addresses_replaces_lrc_only(self, mocked_api):
+        url = DOLOMITE_ENDPOINT
+        lrc_identifier = "0x0000000000000000000000000000000000000000"
+        btc_identifier = "0x0000000000000000000000000000000000000001"
+        resp = self.get_dolomite_tokens_mock(
+            first_ticker="LRC",
+            first_identifier=lrc_identifier,
+            second_ticker="BTC",
+            second_identifier=btc_identifier,
+        )
+        mocked_api.get(url, body=json.dumps(resp))
+        another_btc_identifier = "anotherIdentifier"
+        token_dict = {"LRC": "someIdentifier", "BTC": another_btc_identifier}
+
+        self.async_run_with_timeout(
+            download_dolomite_token_addresses(token_dict))
+
+        self.assertIn("LRC", token_dict)
+        self.assertIn("BTC", token_dict)
+        self.assertEqual(token_dict["LRC"], lrc_identifier)
+        self.assertEqual(token_dict["BTC"], another_btc_identifier)
+
+    @patch("logging.Logger.error")
+    @aioresponses()
+    def test_download_dolomite_token_addresses_logs_errors(
+        self, error_mock: MagicMock, mocked_api
+    ):
+        url = DOLOMITE_ENDPOINT
+        resp = {}
+        mocked_api.get(url, body=json.dumps(resp))
+        token_dict = {}
+
+        self.async_run_with_timeout(
+            download_dolomite_token_addresses(token_dict))
+
+        error_mock.assert_called()
+
+    @aioresponses()
+    def test_download_radar_relay_token_addresses(self, mocked_api):
+        url = RADAR_RELAY_ENDPOINT
+        regex_url = re.compile(f"^{url}")
+        btc_address = "0x0000000000000000000000000000000000000000"
+        usdt_address = "0x0000000000000000000000000000000000000001"
+        resp = [
+            {
+                "id": "BTC-USDT",
+                "baseTokenAddress": btc_address,
+                "quoteTokenAddress": usdt_address,
+            }
+        ]
+        mocked_api.get(regex_url, body=json.dumps(resp))
+        mocked_api.get(regex_url, body=json.dumps([]))  # to break the loop
+        token_dict = {}
+
+        self.async_run_with_timeout(
+            download_radar_relay_token_addresses(token_dict))
+
+        self.assertIn("BTC", token_dict)
+        self.assertIn("USDT", token_dict)
+
+    @patch("logging.Logger.error")
+    @aioresponses()
+    def test_download_radar_relay_token_addresses_logs_errors(
+        self, error_mock: MagicMock, mocked_api
+    ):
+        url = RADAR_RELAY_ENDPOINT
+        regex_url = re.compile(f"^{url}")
+        mocked_api.get(regex_url, status=501)
+        token_dict = {}
+
+        self.async_run_with_timeout(
+            download_radar_relay_token_addresses(token_dict))
+
+        error_mock.assert_called()
+
+    @aioresponses()
+    def test_download_bamboo_relay_token_addresses(self, mocked_api):
+        url = BAMBOO_RELAY_ENDPOINT
+        regex_url = re.compile(f"^{url}")
+        btc_address = "0x0000000000000000000000000000000000000000"
+        usdt_address = "0x0000000000000000000000000000000000000001"
+        resp = [
+            {
+                "id": "BTC-USDT",
+                "baseTokenAddress": btc_address,
+                "quoteTokenAddress": usdt_address,
+            }
+        ]
+        mocked_api.get(regex_url, body=json.dumps(resp))
+        mocked_api.get(regex_url, body=json.dumps([]))  # to break the loop
+        token_dict = {}
+
+        self.async_run_with_timeout(download_bamboo_relay_token_addresses(token_dict))
+
+        self.assertIn("BTC", token_dict)
+        self.assertIn("USDT", token_dict)
+
+    @patch("logging.Logger.error")
+    @aioresponses()
+    def test_download_bamboo_relay_token_addresses_logs_errors(
+        self, error_mock: MagicMock, mocked_api
+    ):
+        url = BAMBOO_RELAY_ENDPOINT
+        regex_url = re.compile(f"^{url}")
+        mocked_api.get(regex_url, status=501)
+        token_dict = {}
+
+        self.async_run_with_timeout(download_bamboo_relay_token_addresses(token_dict))
+
+        error_mock.assert_called()
+
+    @aioresponses()
+    @patch("hummingbot.core.utils.download_token_addresses.open")
+    @patch("hummingbot.core.utils.download_token_addresses.json.load")
+    def test_download_erc20_token_addresses(
+        self, mocked_api, json_load_mock: MagicMock, open_mock: MagicMock
+    ):
+        for url in [BAMBOO_RELAY_ENDPOINT, RADAR_RELAY_ENDPOINT]:
+            regex_url = re.compile(f"^{url}")
+            initial_btc_identifier = "0x0000000000000000000000000000000000000000"
+            usdt_identifier = "0x0000000000000000000000000000000000000001"
+            resp = [
+                {
+                    "id": "BTC-USDT",
+                    "baseTokenAddress": initial_btc_identifier,
+                    "quoteTokenAddress": usdt_identifier,
+                }
+            ]
+            mocked_api.get(regex_url, body=json.dumps(resp))
+            mocked_api.get(regex_url, body=json.dumps([]))  # to break the loop
+        url = DOLOMITE_ENDPOINT
+        lrc_identifier = "0x0000000000000000000000000000000000000000"
+        subsequent_btc_identifier = "0x0000000000000000000000000000000000000001"
+        resp = self.get_dolomite_tokens_mock(
+            first_ticker="LRC",
+            first_identifier=lrc_identifier,
+            second_ticker="BTC",
+            second_identifier=subsequent_btc_identifier,
+        )
+        mocked_api.get(url, body=json.dumps(resp))
+
+        json_load_mock.return_value = {}
+        download_erc20_token_addresses()
+        open_mock().__enter__().write.assert_called_with(
+            json.dumps(
+                {"BTC": initial_btc_identifier,
+                 "USDT": usdt_identifier,
+                 "LRC": lrc_identifier}
+            )
+        )
+
+    @patch("hummingbot.core.utils.download_token_addresses.open")
+    @patch("logging.Logger.error")
+    @patch("hummingbot.core.utils.download_token_addresses.json")
+    def test_download_erc20_token_addresses_logs_errors(
+        self, json_mock: MagicMock, error_mock: MagicMock, __
+    ):
+        json_mock.load.return_value = None
+        download_erc20_token_addresses()
+
+        error_mock.assert_called()


Code review comments:
diff_hunk: @@ -79,7 +79,9 @@ def has_trade_id(self) -> bool:
         return self.type == OrderBookMessageType.TRADE
 
     def __eq__(self, other: "OrderBookMessage") -> bool:
-        if self.has_update_id and other.has_update_id:
+        if self.type != other.type:
+            return False
+        elif self.has_update_id and other.has_update_id:
             return self.update_id == other.update_id
         elif self.has_trade_id and other.has_trade_id:
             return self.trade_id == other.trade_id
path: hummingbot/core/data_type/order_book_message.py
text: @user1:
I think this should be implemented using logic operators only instead of if/elif statements