diff --git a/src/collectors.py b/src/collectors.py index 5afaa26..dd838eb 100644 --- a/src/collectors.py +++ b/src/collectors.py @@ -393,3 +393,56 @@ def client_version(self): def latency(self): """Returns connection latency.""" return self.interface.latest_query_latency + +class TronCollector(): + """A collector to fetch information from Tron endpoints.""" + + def __init__(self, url, labels, chain_id, **client_parameters): + + self.labels = labels + self.chain_id = chain_id + self.interface = HttpsInterface(url, client_parameters.get('open_timeout'), + client_parameters.get('ping_timeout')) + + self._logger_metadata = { + 'component': 'TronCollector', + 'url': strip_url(url) + } + self.client_version_payload = { + 'jsonrpc': '2.0', + 'method': "web3_clientVersion", + 'id': 1 + } + self.block_height_payload = { + 'jsonrpc': '2.0', + 'method': "eth_blockNumber", + 'id': 1 + } + + def alive(self): + """Returns true if endpoint is alive, false if not.""" + # Run cached query because we can also fetch client version from this + # later on. This will save us an RPC call per run. + return self.interface.cached_json_rpc_post( + self.client_version_payload) is not None + + def block_height(self): + """Cached query and returns blockheight after converting hex string value to an int""" + result = self.interface.cached_json_rpc_post(self.block_height_payload) + + if result and isinstance(result, str) and result.startswith('0x'): + return int(result, 16) + raise ValueError(f"Invalid block height result: {result}") + + def client_version(self): + """Runs a cached query to return client version.""" + version = self.interface.cached_json_rpc_post( + self.client_version_payload) + if version is None: + return None + client_version = {"client_version": version} + return client_version + + def latency(self): + """Returns connection latency.""" + return self.interface.latest_query_latency diff --git a/src/configuration.py b/src/configuration.py index 93788df..2b8acb2 100644 --- a/src/configuration.py +++ b/src/configuration.py @@ -47,7 +47,8 @@ def endpoints(self): def _load_configuration(self): allowed_providers = self._load_validation_file() supported_collectors = ('evm', 'cardano', 'conflux', 'solana', - 'bitcoin', 'doge', 'filecoin', 'starknet', 'aptos') + 'bitcoin', 'doge', 'filecoin', 'starknet', 'aptos', + 'tron') configuration_schema = Schema({ 'blockchain': diff --git a/src/registries.py b/src/registries.py index 7ad7e80..c1f0482 100644 --- a/src/registries.py +++ b/src/registries.py @@ -83,6 +83,8 @@ def get_collector_registry(self) -> list: collector = collectors.StarknetCollector case "aptos", "aptos": collector = collectors.AptosCollector + case "tron", "tron": + collector = collectors.TronCollector case "evm", other: # pylint: disable=unused-variable collector = collectors.EvmCollector if collector is None: diff --git a/src/test_collectors.py b/src/test_collectors.py index e296289..5ade458 100644 --- a/src/test_collectors.py +++ b/src/test_collectors.py @@ -734,3 +734,80 @@ def test_latency(self): """Tests that the latency is obtained from the interface based on latest_query_latency""" self.mocked_connection.return_value.latest_query_latency = 0.123 self.assertEqual(0.123, self.aptos_collector.latency()) + +class TestTronCollector(TestCase): + """Tests the Tron collector class""" + + def setUp(self): + self.url = "https://test.com" + self.labels = ["dummy", "labels"] + self.chain_id = 123 + self.open_timeout = 8 + self.ping_timeout = 9 + self.client_params = { + "open_timeout": self.open_timeout, "ping_timeout": self.ping_timeout} + with mock.patch('collectors.HttpsInterface') as mocked_connection: + self.tron_collector = collectors.TronCollector( + self.url, self.labels, self.chain_id, **self.client_params) + self.mocked_connection = mocked_connection + + def test_logger_metadata(self): + """Validate logger metadata. Makes sure url is stripped by helpers.strip_url function.""" + expected_metadata = { + 'component': 'TronCollector', 'url': 'test.com'} + self.assertEqual(expected_metadata, + self.tron_collector._logger_metadata) + + def test_https_interface_created(self): + """Tests that the Tron collector calls the https interface with the correct args""" + self.mocked_connection.assert_called_once_with( + self.url, self.open_timeout, self.ping_timeout) + + def test_interface_attribute_exists(self): + """Tests that the interface attribute exists.""" + self.assertTrue(hasattr(self.tron_collector, 'interface')) + + def test_alive_call(self): + """Tests the alive function uses the correct call""" + self.tron_collector.alive() + self.mocked_connection.return_value.cached_json_rpc_post.assert_called_once_with( + self.tron_collector.client_version_payload) + + def test_alive_false(self): + """Tests the alive function returns false when post returns None""" + self.mocked_connection.return_value.cached_json_rpc_post.return_value = None + result = self.tron_collector.alive() + self.assertFalse(result) + + def test_block_height(self): + """Tests the block_height function uses the correct call to get block height""" + self.mocked_connection.return_value.cached_json_rpc_post.return_value = "0x1a2b3c" + result = self.tron_collector.block_height() + self.mocked_connection.return_value.cached_json_rpc_post.assert_called_once_with( + self.tron_collector.block_height_payload) + self.assertEqual(result, 1715004) + + def test_block_height_raises_value_error(self): + """Tests that the block height raises ValueError if result is invalid""" + self.mocked_connection.return_value.cached_json_rpc_post.return_value = "invalid" + with self.assertRaises(ValueError): + self.tron_collector.block_height() + + def test_client_version(self): + """Tests the client_version function uses the correct call to get client version""" + self.mocked_connection.return_value.cached_json_rpc_post.return_value = "Tron/v1.0.0" + result = self.tron_collector.client_version() + self.mocked_connection.return_value.cached_json_rpc_post.assert_called_once_with( + self.tron_collector.client_version_payload) + self.assertEqual(result, {"client_version": "Tron/v1.0.0"}) + + def test_client_version_returns_none(self): + """Tests that the client_version returns None if cached_json_rpc_post returns None""" + self.mocked_connection.return_value.cached_json_rpc_post.return_value = None + result = self.tron_collector.client_version() + self.assertIsNone(result) + + def test_latency(self): + """Tests that the latency is obtained from the interface based on latest_query_latency""" + self.mocked_connection.return_value.latest_query_latency = 0.123 + self.assertEqual(0.123, self.tron_collector.latency()) diff --git a/src/test_registries.py b/src/test_registries.py index b202efc..a728962 100644 --- a/src/test_registries.py +++ b/src/test_registries.py @@ -139,6 +139,17 @@ def test_get_collector_registry_for_aptos(self): with mock.patch('collectors.AptosCollector', new=mock.Mock()) as collector: helper_test_collector_registry(self, collector) + @mock.patch.dict(os.environ, { + "CONFIG_FILE_PATH": "tests/fixtures/configuration_tron.yaml", + "VALIDATION_FILE_PATH": "tests/fixtures/validation.yaml" + }) + def test_get_collector_registry_for_tron(self): + """Tests that the Tron collector is called with the correct args""" + self.collector_registry = CollectorRegistry() + with mock.patch('collectors.TronCollector', new=mock.Mock()) as collector: + helper_test_collector_registry(self, collector) + + @mock.patch.dict(os.environ, { "CONFIG_FILE_PATH": "tests/fixtures/configuration_evm.yaml", "VALIDATION_FILE_PATH": "tests/fixtures/validation.yaml" diff --git a/src/tests/fixtures/configuration_tron.yaml b/src/tests/fixtures/configuration_tron.yaml new file mode 100644 index 0000000..aa4f380 --- /dev/null +++ b/src/tests/fixtures/configuration_tron.yaml @@ -0,0 +1,12 @@ +blockchain: "Tron" +chain_id: 1234 +network_name: "Testnet" +network_type: "Testnet" +collector: "tron" +endpoints: + - url: https://test1.com + provider: TestProvider1 + - url: https://test2.com + provider: TestProvider2 + - url: https://test3.com + provider: TestProvider3