Repository: ethereum/web3.py

Issue:
Call contract function using block hash makes two requests inside
Hi!

All call to contract functions with block identifier as block hash makes two requests:
1. Fetch block by block hash.
2. Send call request to contract with block number from previous response.

https://github.com/ethereum/web3.py/blob/master/web3/contract/contract.py#L406

### How can it be fixed?

I propose to replace this part
https://github.com/ethereum/web3.py/blob/a35351d8bb3305a3b31bcbcac9f76a00c83c6d23/web3/_utils/contracts.py#L419-L421
with
```python
elif is_hex_encoded_block_hash(block_identifier):
    return block_identifier
elif isinstance(block_identifier, bytes):
    retrurn '0x' + block_identifier.hex()
```



Discussion:
reedsa: @fselmo Good catch, added those in and cleaned up those tests a bit more.

Patch:
diff --git a/newsfragments/3257.performance.rst b/newsfragments/3257.performance.rst
new file mode 100644
index 00000000..0a0d109d
--- /dev/null
+++ b/newsfragments/3257.performance.rst
@@ -0,0 +1,1 @@
+Remove call to ``parse_block_identifier`` when initializing ``ContractCaller`` functions.
diff --git a/web3/_utils/contracts.py b/web3/_utils/contracts.py
index 93a49af1..3871bb3b 100644
--- a/web3/_utils/contracts.py
+++ b/web3/_utils/contracts.py
@@ -438,23 +438,6 @@ def parse_block_identifier_int(w3: "Web3", block_identifier_int: int) -> BlockNu
     return BlockNumber(block_num)
 
 
-def parse_block_identifier_no_extra_call(
-    w3: Union["Web3", "AsyncWeb3"], block_identifier: BlockIdentifier
-) -> BlockIdentifier:
-    if block_identifier is None:
-        return w3.eth.default_block
-    elif isinstance(block_identifier, int) and block_identifier >= 0:
-        return block_identifier
-    elif block_identifier in ["latest", "earliest", "pending", "safe", "finalized"]:
-        return block_identifier
-    elif isinstance(block_identifier, bytes):
-        return HexBytes(block_identifier)
-    elif is_hex_encoded_block_hash(block_identifier):
-        return HexStr(str(block_identifier))
-    else:
-        raise BlockNumberOutofRange
-
-
 async def async_parse_block_identifier(
     async_w3: "AsyncWeb3", block_identifier: BlockIdentifier
 ) -> BlockIdentifier:
diff --git a/web3/contract/async_contract.py b/web3/contract/async_contract.py
index e6de2baa..9d828c6a 100644
--- a/web3/contract/async_contract.py
+++ b/web3/contract/async_contract.py
@@ -39,7 +39,6 @@ from web3._utils.compat import (
 )
 from web3._utils.contracts import (
     async_parse_block_identifier,
-    parse_block_identifier_no_extra_call,
 )
 from web3._utils.datatypes import (
     PropertyCheckingFactory,
@@ -584,15 +583,11 @@ class AsyncContractCaller(BaseContractCaller):
                     decode_tuples=decode_tuples,
                 )
 
-                # TODO: The no_extra_call method gets around the fact that we can't call
-                #  the full async method from within a class's __init__ method. We need
-                #  to see if there's a way to account for all desired elif cases.
-                block_id = parse_block_identifier_no_extra_call(w3, block_identifier)
                 caller_method = partial(
                     self.call_function,
                     fn,
                     transaction=transaction,
-                    block_identifier=block_id,
+                    block_identifier=block_identifier,
                     ccip_read_enabled=ccip_read_enabled,
                 )
 
diff --git a/web3/contract/contract.py b/web3/contract/contract.py
index e6c1ab17..900b25de 100644
--- a/web3/contract/contract.py
+++ b/web3/contract/contract.py
@@ -588,12 +588,11 @@ class ContractCaller(BaseContractCaller):
                     decode_tuples=decode_tuples,
                 )
 
-                block_id = parse_block_identifier(w3, block_identifier)
                 caller_method = partial(
                     self.call_function,
                     fn,
                     transaction=transaction,
-                    block_identifier=block_id,
+                    block_identifier=block_identifier,
                     ccip_read_enabled=ccip_read_enabled,
                 )
 


Test patch:
diff --git a/tests/core/contracts/test_contract_caller_interface.py b/tests/core/contracts/test_contract_caller_interface.py
index 046aa384..a25ef4bf 100644
--- a/tests/core/contracts/test_contract_caller_interface.py
+++ b/tests/core/contracts/test_contract_caller_interface.py
@@ -1,6 +1,7 @@
 import pytest
 
 from web3.exceptions import (
+    BlockNumberOutofRange,
     MismatchedABI,
     NoABIFound,
     NoABIFunctionsFound,
@@ -130,19 +131,56 @@ def test_caller_with_a_nonexistent_function(math_contract):
         contract.caller.thisFunctionDoesNotExist()
 
 
+def test_caller_with_invalid_block_identifier(w3, math_contract):
+    w3.provider.make_request(method="evm_mine", params=[5])
+    math_contract.functions.incrementCounter().transact()
+    math_contract.functions.incrementCounter().transact()
+
+    # Invalid `block_identifier` should not raise when passed to `caller` directly.
+    # The function call itself parses the value.
+    caller = math_contract.caller(block_identifier="abc")
+    with pytest.raises(BlockNumberOutofRange):
+        caller.counter()
+
+    # Calling the function with an invalid `block_identifier` will raise.
+    default_caller = math_contract.caller()
+    with pytest.raises(BlockNumberOutofRange):
+        default_caller.counter(block_identifier="abc")
+
+
 def test_caller_with_block_identifier(w3, math_contract):
-    start_num = w3.eth.get_block("latest").number
+    start = w3.eth.get_block("latest")
+    start_num = start.number
+    block_hash = start.hash
+    block_hex = w3.to_hex(block_hash)
+
     assert math_contract.caller.counter() == 0
 
     w3.provider.make_request(method="evm_mine", params=[5])
     math_contract.functions.incrementCounter().transact()
     math_contract.functions.incrementCounter().transact()
 
-    output1 = math_contract.caller(block_identifier=start_num + 6).counter()
-    output2 = math_contract.caller(block_identifier=start_num + 7).counter()
-
-    assert output1 == 1
-    assert output2 == 2
+    assert_ids = [
+        (block_hash, 0),
+        (block_hex, 0),
+        (start_num, 0),
+        (start_num + 6, 1),
+        (start_num + 7, 2),
+        ("latest", 2),
+        (None, 2),
+    ]
+
+    for block_identifier, expected_value in assert_ids:
+        assert (
+            math_contract.caller(block_identifier=block_identifier).counter()
+            == expected_value
+        )
+
+        # Function arg `block_identifier` produces same result
+        assert (
+            math_contract.caller().counter(block_identifier=block_identifier)
+            == expected_value
+        )
 
 
 def test_caller_with_block_identifier_and_transaction_dict(
@@ -298,21 +336,64 @@ async def test_async_caller_with_a_nonexistent_function(async_math_contract):
         await contract.caller.thisFunctionDoesNotExist()
 
 
+@pytest.mark.asyncio
+async def test_async_caller_with_invalid_block_identifier(
+    async_w3, async_math_contract
+):
+    await async_w3.provider.make_request(method="evm_mine", params=[5])
+    await async_math_contract.functions.incrementCounter().transact()
+    await async_math_contract.functions.incrementCounter().transact()
+
+    # Invalid `block_identifier` should not raise when passed to `caller` directly.
+    # The function call itself parses the value.
+    caller = async_math_contract.caller(block_identifier="abc")
+    with pytest.raises(BlockNumberOutofRange):
+        await caller.counter()
+
+    # Calling the function with an invalid `block_identifier` will raise.
+    default_caller = async_math_contract.caller()
+    with pytest.raises(BlockNumberOutofRange):
+        await default_caller.counter(block_identifier="abc")
+
+
 @pytest.mark.asyncio
 async def test_async_caller_with_block_identifier(async_w3, async_math_contract):
     start = await async_w3.eth.get_block("latest")
     start_num = start.number
+    block_hash = start.hash
+    block_hex = async_w3.to_hex(block_hash)
+
     assert await async_math_contract.caller.counter() == 0
 
     await async_w3.provider.make_request(method="evm_mine", params=[5])
     await async_math_contract.functions.incrementCounter().transact()
     await async_math_contract.functions.incrementCounter().transact()
 
-    output1 = await async_math_contract.caller(block_identifier=start_num + 6).counter()
-    output2 = await async_math_contract.caller(block_identifier=start_num + 7).counter()
-
-    assert output1 == 1
-    assert output2 == 2
+    assert_ids = [
+        (block_hash, 0),
+        (block_hex, 0),
+        (start_num, 0),
+        (start_num + 6, 1),
+        (start_num + 7, 2),
+        ("latest", 2),
+        (None, 2),
+    ]
+
+    for block_identifier, expected_value in assert_ids:
+        assert (
+            await async_math_contract.caller(
+                block_identifier=block_identifier
+            ).counter()
+            == expected_value
+        )
+
+        # Function arg `block_identifier` produces same result
+        assert (
+            await async_math_contract.caller().counter(
+                block_identifier=block_identifier
+            )
+            == expected_value
+        )
 
 
 @pytest.mark.asyncio