Repository: rotki/rotki
Pull request #9966: Give priority to blockscout when querying blocknumber
Author: login: yabirgb
Labels: name: ready for final review
description: Backend PR ready to be reviewed by great Lefteris
Created: 2025-05-16T17:13:56Z | Merged: 2025-05-20T08:42:12Z

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/rotkehlchen/chain/binance_sc/node_inquirer.py b/rotkehlchen/chain/binance_sc/node_inquirer.py
index 610c98aa593..28e37e1b571 100644
--- a/rotkehlchen/chain/binance_sc/node_inquirer.py
+++ b/rotkehlchen/chain/binance_sc/node_inquirer.py
@@ -8,7 +8,13 @@
 from rotkehlchen.constants.assets import A_BSC_BNB
 from rotkehlchen.fval import FVal
 from rotkehlchen.greenlets.manager import GreenletManager
-from rotkehlchen.types import ChainID, ChecksumEvmAddress, EVMTxHash, SupportedBlockchain
+from rotkehlchen.types import (
+    ChainID,
+    ChecksumEvmAddress,
+    EVMTxHash,
+    SupportedBlockchain,
+    Timestamp,
+)
 
 from .constants import (
     ARCHIVE_NODE_CHECK_ADDRESS,
@@ -58,3 +64,19 @@ def _get_archive_check_data(self) -> tuple[ChecksumEvmAddress, int, FVal]:
             ARCHIVE_NODE_CHECK_BLOCK,
             ARCHIVE_NODE_CHECK_EXPECTED_BALANCE,
         )
+
+    def get_blocknumber_by_time(
+            self,
+            ts: 'Timestamp',
+            closest: Literal['before', 'after'] = 'before',
+    ) -> int:
+        """Searches for the blocknumber of a specific timestamp.
+        Reimplemented because bsc doesn't have a blockscout api.
+
+        May raise RemoteError
+        """
+        return self.etherscan.get_blocknumber_by_time(
+            chain_id=self.chain_id,
+            ts=ts,
+            closest=closest,
+        )
diff --git a/rotkehlchen/chain/ethereum/node_inquirer.py b/rotkehlchen/chain/ethereum/node_inquirer.py
index dbca7144b29..4c0f8a64d90 100644
--- a/rotkehlchen/chain/ethereum/node_inquirer.py
+++ b/rotkehlchen/chain/ethereum/node_inquirer.py
@@ -1,6 +1,5 @@
 import logging
 from collections.abc import Sequence
-from contextlib import suppress
 from typing import TYPE_CHECKING, Literal, overload
 
 from ens.abis import PUBLIC_RESOLVER_2 as ENS_RESOLVER_ABI
@@ -26,7 +25,7 @@
 )
 from rotkehlchen.chain.evm.types import string_to_evm_address
 from rotkehlchen.constants.assets import A_ETH
-from rotkehlchen.errors.misc import InputError, RemoteError
+from rotkehlchen.errors.misc import InputError
 from rotkehlchen.errors.serialization import DeserializationError
 from rotkehlchen.externalapis.blockscout import Blockscout
 from rotkehlchen.fval import FVal
@@ -38,7 +37,6 @@
     ChecksumEvmAddress,
     EVMTxHash,
     SupportedBlockchain,
-    Timestamp,
 )
 from rotkehlchen.utils.misc import get_chunks
 
@@ -258,25 +256,6 @@ def _get_archive_check_data(self) -> tuple[ChecksumEvmAddress, int, FVal]:
             ARCHIVE_NODE_CHECK_EXPECTED_BALANCE,
         )
 
-    def get_blocknumber_by_time(
-            self,
-            ts: Timestamp,
-            closest: Literal['before', 'after'] = 'before',
-    ) -> int:
-        """Searches for the blocknumber of a specific timestamp
-        - Performs the etherscan api call by default first
-        - If RemoteError raised or etherscan flag set to false
-            -> queries blocks subgraph
-        """
-        with suppress(RemoteError):
-            return self.etherscan.get_blocknumber_by_time(
-                chain_id=ChainID.ETHEREUM,
-                ts=ts,
-                closest=closest,
-            )
-
-        return self.blockscout.get_blocknumber_by_time(ts, closest)
-
     # -- Implementation of EvmNodeInquirer optional methods --
 
     def logquery_block_range(
diff --git a/rotkehlchen/chain/evm/node_inquirer.py b/rotkehlchen/chain/evm/node_inquirer.py
index 9caeb46e26e..b0ede0c865c 100644
--- a/rotkehlchen/chain/evm/node_inquirer.py
+++ b/rotkehlchen/chain/evm/node_inquirer.py
@@ -1384,9 +1384,14 @@ def get_blocknumber_by_time(
             closest: Literal['before', 'after'] = 'before',
     ) -> int:
         """Searches for the blocknumber of a specific timestamp
-
+        - Performs the blockscout api call by default first
+        - If RemoteError gets raised it queries etherscan
         May raise RemoteError
         """
+        assert self.blockscout is not None, 'Blockscout should be set'
+        with suppress(RemoteError):
+            return self.blockscout.get_blocknumber_by_time(ts, closest)
+
         return self.etherscan.get_blocknumber_by_time(
             chain_id=self.chain_id,
             ts=ts,
diff --git a/rotkehlchen/chain/scroll/node_inquirer.py b/rotkehlchen/chain/scroll/node_inquirer.py
index d6c1827273a..e4202b0aa51 100644
--- a/rotkehlchen/chain/scroll/node_inquirer.py
+++ b/rotkehlchen/chain/scroll/node_inquirer.py
@@ -7,6 +7,7 @@
 from rotkehlchen.chain.evm.l2_with_l1_fees.node_inquirer import L2WithL1FeesInquirer
 from rotkehlchen.chain.evm.types import string_to_evm_address
 from rotkehlchen.constants.assets import A_ETH
+from rotkehlchen.externalapis.blockscout import Blockscout
 from rotkehlchen.fval import FVal
 from rotkehlchen.greenlets.manager import GreenletManager
 from rotkehlchen.logging import RotkehlchenLogsAdapter
@@ -51,6 +52,11 @@ def __init__(
             contract_multicall=contracts.contract(string_to_evm_address('0xcA11bde05977b3631167028862bE2a173976CA11')),
             contract_scan=contracts.contract(BALANCE_SCANNER_ADDRESS),
             native_token=A_ETH.resolve_to_crypto_asset(),
+            blockscout=Blockscout(
+                blockchain=SupportedBlockchain.SCROLL,
+                database=database,
+                msg_aggregator=database.msg_aggregator,
+            ),
         )
 
     # -- Implementation of EvmNodeInquirer base methods --
diff --git a/rotkehlchen/externalapis/blockscout.py b/rotkehlchen/externalapis/blockscout.py
index 946108542b9..a6a35b8f9df 100644
--- a/rotkehlchen/externalapis/blockscout.py
+++ b/rotkehlchen/externalapis/blockscout.py
@@ -66,6 +66,8 @@ def __init__(
                 self.url = 'https://gnosis.blockscout.com/api'
             case SupportedBlockchain.POLYGON_POS:
                 self.url = 'https://polygon.blockscout.com/api'
+            case SupportedBlockchain.SCROLL:
+                self.url = 'https://scroll.blockscout.com/api'
             case _:
                 raise NotImplementedError(f'Blockscout not implement for {blockchain}')
 
diff --git a/rotkehlchen/tests/api/test_external_services.py b/rotkehlchen/tests/api/test_external_services.py
index f9df00b28f1..5986aaaa3ea 100644
--- a/rotkehlchen/tests/api/test_external_services.py
+++ b/rotkehlchen/tests/api/test_external_services.py
@@ -15,7 +15,7 @@
 
 
 EMPTY_RESULT = {
-    'blockscout': {'ethereum': None, 'optimism': None, 'polygon_pos': None, 'arbitrum_one': None, 'base': None, 'gnosis': None},  # noqa: E501
+    'blockscout': {'ethereum': None, 'optimism': None, 'polygon_pos': None, 'arbitrum_one': None, 'base': None, 'gnosis': None, 'scroll': None},  # noqa: E501
 }
 
 
@@ -34,7 +34,7 @@ def test_add_get_external_service(rotkehlchen_api_server: 'APIServer') -> None:
     # Now add some data and see that the response shows they are added
     expected_result: dict[str, Any] = {
         'etherscan': {'api_key': 'key1'},
-        'blockscout': {'ethereum': None, 'optimism': None, 'polygon_pos': None, 'arbitrum_one': None, 'base': None, 'gnosis': None},  # noqa: E501
+        **EMPTY_RESULT,
         'cryptocompare': {'api_key': 'key2'},
         'monerium': {'username': 'Ben', 'password': 'supersafepassword'},
     }
@@ -81,8 +81,8 @@ def test_delete_external_service(rotkehlchen_api_server: 'APIServer') -> None:
     """Tests that delete external service credentials works"""
     # Add some data and see that the response shows they are added
     expected_result: dict[str, Any] = {
-        'etherscan': {'api_key': 'key1'},
-        'blockscout': {'ethereum': None, 'optimism': None, 'polygon_pos': None, 'arbitrum_one': None, 'base': None, 'gnosis': None},  # noqa: E501
+        'etherscan':  {'api_key': 'key1'},
+        **EMPTY_RESULT,
         'cryptocompare': {'api_key': 'key2'},
     }
     response = requests.put(
diff --git a/rotkehlchen/tests/unit/test_defi_oracles.py b/rotkehlchen/tests/unit/test_defi_oracles.py
index 8fd5d8394ee..21f9835cbc5 100644
--- a/rotkehlchen/tests/unit/test_defi_oracles.py
+++ b/rotkehlchen/tests/unit/test_defi_oracles.py
@@ -80,7 +80,7 @@ def test_uniswap_oracles_historic_price(inquirer_defi, socket_enabled):  # pylin
         from_asset=A_1INCH,
         to_asset=A_LINK,
         timestamp=1653454800,
-    ) == Price(FVal('0.139152791117568194371010708385710558327124437415165328415539849552333178019986'))  # noqa: E501
+    ) == Price(FVal('0.138028273714701208394300881476926660441297470010798769978393676678931896933043'))  # noqa: E501
 
     with pytest.raises(NoPriceForGivenTimestamp):
         inquirer_defi._uniswapv3.query_historical_price(
diff --git a/rotkehlchen/tests/unit/test_ethereum_inquirer.py b/rotkehlchen/tests/unit/test_ethereum_inquirer.py
index 91e33ab040b..460e06aeb7c 100644
--- a/rotkehlchen/tests/unit/test_ethereum_inquirer.py
+++ b/rotkehlchen/tests/unit/test_ethereum_inquirer.py
@@ -325,17 +325,18 @@ def _test_get_blocknumber_by_time(ethereum_inquirer):
 @pytest.mark.vcr(filter_query_parameters=['apikey'])
 def test_get_blocknumber_by_time_blockscout(ethereum_inquirer):
     """Queries blockscout api for known block times"""
-    with patch(
-        'rotkehlchen.externalapis.etherscan.Etherscan.get_blocknumber_by_time',
-        side_effect=RemoteError('Mocked failed etherscan api query'),
-    ):
-        _test_get_blocknumber_by_time(ethereum_inquirer)
+    _test_get_blocknumber_by_time(ethereum_inquirer)
 
 
 @pytest.mark.vcr(filter_query_parameters=['apikey'])
 def test_get_blocknumber_by_time_etherscan(ethereum_inquirer):
     """Queries etherscan for known block times"""
-    _test_get_blocknumber_by_time(ethereum_inquirer)
+    with patch.object(
+        ethereum_inquirer.blockscout,
+        'get_blocknumber_by_time',
+        side_effect=RemoteError('Intentional blockscout remote error to test etherscan'),
+    ):
+        _test_get_blocknumber_by_time(ethereum_inquirer)
 
 
 @pytest.mark.vcr(filter_query_parameters=['apikey'])
diff --git a/rotkehlchen/types.py b/rotkehlchen/types.py
index 1080a6b5010..9c7ad32c6ea 100644
--- a/rotkehlchen/types.py
+++ b/rotkehlchen/types.py
@@ -151,6 +151,7 @@ class ExternalService(SerializableEnumNameMixin):
     DEFILLAMA = auto()
     COINGECKO = auto()
     ALCHEMY = auto()
+    SCROLL_BLOCKSCOUT = auto()
 
     def get_chain_for_etherscan(self) -> Optional['ChainID']:
         """If the service is an etherscan service return its chain"""
@@ -354,6 +355,7 @@ def to_blockchain(self) -> 'SupportedBlockchain':
     ExternalService.ARBITRUM_ONE_BLOCKSCOUT: ChainID.ARBITRUM_ONE,
     ExternalService.BASE_BLOCKSCOUT: ChainID.BASE,
     ExternalService.GNOSIS_BLOCKSCOUT: ChainID.GNOSIS,
+    ExternalService.SCROLL_BLOCKSCOUT: ChainID.SCROLL,
 }
 
 ETHERSCAN_TO_CHAINID = {


Reviews:
user: login: LefterisJP
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-18T09:08:43Z
user: login: yabirgb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-18T09:10:55Z
user: login: yabirgb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-18T09:11:11Z
user: login: yabirgb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T20:36:36Z
user: login: LefterisJP
body: LGTM but could this also go to bugfixes?
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-19T21:18:16Z

Review comments:
diff_hunk: @@ -1379,9 +1379,14 @@ def get_blocknumber_by_time(
             closest: Literal['before', 'after'] = 'before',
     ) -> int:
         """Searches for the blocknumber of a specific timestamp
-
+        - Performs the blockscout api call by default first
+        - If RemoteError gets raised it queries etherscan
         May raise RemoteError
         """
+        if self.blockscout:
path: rotkehlchen/chain/evm/node_inquirer.py
user: login: LefterisJP
body: when can this if be off? Do we allow the user to choose if blockscout should be queried?
created_at: 2025-05-18T09:07:57Z
author_association: MEMBER
diff_hunk: @@ -1379,9 +1379,14 @@ def get_blocknumber_by_time(
             closest: Literal['before', 'after'] = 'before',
     ) -> int:
         """Searches for the blocknumber of a specific timestamp
-
+        - Performs the blockscout api call by default first
+        - If RemoteError gets raised it queries etherscan
         May raise RemoteError
         """
+        if self.blockscout:
+            with suppress(RemoteError):
+                return self.blockscout.get_blocknumber_by_time(ts, closest)
path: rotkehlchen/chain/evm/node_inquirer.py
user: login: LefterisJP
body: With the suppression here, do we log anywhere that blockscout fails and now we are falling back to etherscan.
created_at: 2025-05-18T09:08:20Z
author_association: MEMBER
diff_hunk: @@ -1379,9 +1379,14 @@ def get_blocknumber_by_time(
             closest: Literal['before', 'after'] = 'before',
     ) -> int:
         """Searches for the blocknumber of a specific timestamp
-
+        - Performs the blockscout api call by default first
+        - If RemoteError gets raised it queries etherscan
         May raise RemoteError
         """
+        if self.blockscout:
path: rotkehlchen/chain/evm/node_inquirer.py
user: login: yabirgb
body: this will be false only for scroll. No, blockscout can't be configured to be queried but this is just the get block by timestamp function. This call is one of the failing from your logs. the other place where blockscout is called is in the eth staking logic.  
created_at: 2025-05-18T09:10:55Z
author_association: MEMBER
diff_hunk: @@ -1379,9 +1379,14 @@ def get_blocknumber_by_time(
             closest: Literal['before', 'after'] = 'before',
     ) -> int:
         """Searches for the blocknumber of a specific timestamp
-
+        - Performs the blockscout api call by default first
+        - If RemoteError gets raised it queries etherscan
         May raise RemoteError
         """
+        if self.blockscout:
+            with suppress(RemoteError):
+                return self.blockscout.get_blocknumber_by_time(ts, closest)
path: rotkehlchen/chain/evm/node_inquirer.py
user: login: yabirgb
body: yes, it gets logged at the blockscout query logic
created_at: 2025-05-18T09:11:11Z
author_association: MEMBER
diff_hunk: @@ -80,7 +80,7 @@ def test_uniswap_oracles_historic_price(inquirer_defi, socket_enabled):  # pylin
         from_asset=A_1INCH,
         to_asset=A_LINK,
         timestamp=1653454800,
-    ) == Price(FVal('0.139152791117568194371010708385710558327124437415165328415539849552333178019986'))  # noqa: E501
+    ) == Price(FVal('0.138028273714701208394300881476926660441297470010798769978393676678931896933043'))  # noqa: E501
path: rotkehlchen/tests/unit/test_defi_oracles.py
user: login: yabirgb
body: changed because I re-decoded and it routes using a different path. It is slightly lower
created_at: 2025-05-19T20:36:32Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-05-16T17:25:03Z
body: ## [Codecov](https://app.codecov.io/gh/rotki/rotki/pull/9966?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 `81.81818%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 68.30%. Comparing base [(`eb1a07f`)](https://app.codecov.io/gh/rotki/rotki/commit/eb1a07f9ff4c9549b588a865ffdb0bae338e96c5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) to head [(`51a1d9c`)](https://app.codecov.io/gh/rotki/rotki/commit/51a1d9cbebf1420a7e63b601751c5d377743e84d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki).
> Report is 44 commits behind head on bugfixes.

| [Files with missing lines](https://app.codecov.io/gh/rotki/rotki/pull/9966?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/chain/binance\_sc/node\_inquirer.py](https://app.codecov.io/gh/rotki/rotki/pull/9966?src=pr&el=tree&filepath=rotkehlchen%2Fchain%2Fbinance_sc%2Fnode_inquirer.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki#diff-cm90a2VobGNoZW4vY2hhaW4vYmluYW5jZV9zYy9ub2RlX2lucXVpcmVyLnB5) | 66.66% | [1 Missing :warning: ](https://app.codecov.io/gh/rotki/rotki/pull/9966?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) |
| [rotkehlchen/externalapis/blockscout.py](https://app.codecov.io/gh/rotki/rotki/pull/9966?src=pr&el=tree&filepath=rotkehlchen%2Fexternalapis%2Fblockscout.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki#diff-cm90a2VobGNoZW4vZXh0ZXJuYWxhcGlzL2Jsb2Nrc2NvdXQucHk=) | 50.00% | [0 Missing and 1 partial :warning: ](https://app.codecov.io/gh/rotki/rotki/pull/9966?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              @@
##           bugfixes    #9966       +/-   ##
=============================================
+ Coverage     55.89%   68.30%   +12.41%     
=============================================
  Files          1889      909      -980     
  Lines        138924    62223    -76701     
  Branches      13761     7729     -6032     
=============================================
- Hits          77649    42504    -35145     
+ Misses        58465    17131    -41334     
+ Partials       2810     2588      -222     
```

| [Flag](https://app.codecov.io/gh/rotki/rotki/pull/9966/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/9966/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) | `68.30% <81.81%> (-12.70%)` | :arrow_down: |
| [frontend_integration](https://app.codecov.io/gh/rotki/rotki/pull/9966/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) | `?` | |
| [frontend_unit](https://app.codecov.io/gh/rotki/rotki/pull/9966/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rotki) | `?` | |

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/9966?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> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [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: LefterisJP
created_at: 2025-05-20T08:42:05Z
body: again failed test is `test_asset_updates_consistency_with_packaged_db`. Will merge.
author_association: MEMBER
user: login: rotkibot
created_at: 2025-05-20T08:42:24Z
body: rotki/test-caching#167 was successfully merged
author_association: NONE