Repository: ethereum/web3.py

Issue:
Cannot unmarshal non-string into Go struct field OverrideAccount.balance of type *hexutil.Big
 * Version: 6.0.0-beta.10
* Python: 3.10
* OS: Linux(WSL)

### What was wrong?
Occurred an exception when calling `eth_call` with `state_override`. The exception message is as follows:
```
ValueError: {'code': -32602, 'message': 'invalid argument 2: json: cannot unmarshal non-string into Go struct field OverrideAccount.balance of type *hexutil.Big'}
```
I found this problem when using `AsyncHTTPProvider` to connect my Geth node. I overrode the balance of an account in the `state_override` parameter, and this exception occurred at runtime.

A minimal example to reproduce the problem is as follows:
```python
import asyncio
import secrets
import typing

from eth_typing import ChecksumAddress
from web3 import AsyncHTTPProvider, Web3
from web3.eth.async_eth import AsyncEth
from web3.types import TxParams, CallOverrideParams, Wei, Nonce

rand: bytes = secrets.token_bytes(20)
addr = Web3.to_checksum_address(f"0x{rand.hex()}")

txn: TxParams = {"to": addr}
over: dict[ChecksumAddress, CallOverrideParams] = {
    addr: {
        "balance": Wei(0)
    }
}


async def main():
    url = "http://localhost:8545/"
    w3 = Web3(AsyncHTTPProvider(url))
    await typing.cast(AsyncEth, w3.eth).call(txn, "latest", over)


asyncio.run(main())

```

Running this example, I get the following error message:
```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/.../lib/python3.10/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/.../lib/python3.10/asyncio/base_events.py", line 649, in run_until_complete
    return future.result()
  File "<stdin>", line 3, in main
  File "/.../lib/python3.10/site-packages/web3/eth/async_eth.py", line 240, in call
    return await self._durin_call(transaction, block_identifier, state_override)
  File "/.../lib/python3.10/site-packages/web3/eth/async_eth.py", line 259, in _durin_call
    return await self._call(transaction, block_identifier, state_override)
  File "/.../lib/python3.10/site-packages/web3/module.py", line 90, in caller
    result = await w3.manager.coro_request(
  File "/.../lib/python3.10/site-packages/web3/manager.py", line 236, in coro_request
    return self.formatted_response(
  File "/.../lib/python3.10/site-packages/web3/manager.py", line 194, in formatted_response
    raise ValueError(response["error"])
ValueError: {'code': -32602, 'message': 'invalid argument 2: json: cannot unmarshal non-string into Go struct field OverrideAccount.balance of type *hexutil.Big'}
```


### How can it be fixed?

According to the error report, a hex string is needed to convert to `*hexutil.Big`, but the corresponding type in web3.py is `Wei`. Web3.py should convert the numbers here into hex string and pass  them to Geth.

It seems that all parameters covered here needed to be converted to hex string. `Nonce` needs to be converted to `*hexutil.Uint64` after my testing.




Discussion:

Resolving patch:
diff --git a/web3/_utils/method_formatters.py b/web3/_utils/method_formatters.py
--- a/web3/_utils/method_formatters.py
+++ b/web3/_utils/method_formatters.py
@@ -85,6 +85,9 @@
     RPC_ABIS,
     abi_request_formatters,
 )
+from web3._utils.type_conversion import (
+    to_hex_if_bytes,
+)
 from web3.datastructures import (
     AttributeDict,
     ReadableAttributeDict,
@@ -120,9 +123,7 @@ def bytes_to_ascii(value: bytes) -> str:
 to_integer_if_hex = apply_formatter_if(is_string, hex_to_integer)
 to_hex_if_integer = apply_formatter_if(is_integer, integer_to_hex)
 
-
 is_false = partial(operator.is_, False)
-
 is_not_false = complement(is_false)
 is_not_null = complement(is_null)
 
@@ -167,6 +168,24 @@ def type_aware_apply_formatters_to_dict(
     )
 
 
+def type_aware_apply_formatters_to_dict_keys_and_values(
+    key_formatters: Callable[[Any], Any],
+    value_formatters: Callable[[Any], Any],
+    dict_like_object: Union[AttributeDict[str, Any], Dict[str, Any]],
+) -> Union[ReadableAttributeDict[str, Any], Dict[str, Any]]:
+    """
+    Preserve ``AttributeDict`` types if original ``value`` was an ``AttributeDict``.
+    """
+    formatted_dict = dict(
+        (key_formatters(k), value_formatters(v)) for k, v in dict_like_object.items()
+    )
+    return (
+        AttributeDict.recursive(formatted_dict)
+        if is_attrdict(dict_like_object)
+        else formatted_dict
+    )
+
+
 TRANSACTION_RESULT_FORMATTERS = {
     "blockHash": apply_formatter_if(is_not_null, to_hexbytes(32)),
     "blockNumber": apply_formatter_if(is_not_null, to_integer_if_hex),
@@ -377,22 +396,30 @@ def apply_list_to_array_formatter(formatter: Any) -> Callable[..., Any]:
 
 call_without_override: Callable[
     [Tuple[TxParams, BlockIdentifier]], Tuple[Dict[str, Any], int]
-]
-call_without_override = apply_formatters_to_sequence(
+] = apply_formatters_to_sequence(
     [
         transaction_param_formatter,
         to_hex_if_integer,
     ]
 )
+
+CALL_OVERRIDE_FORMATTERS = {
+    "balance": to_hex_if_integer,
+    "nonce": to_hex_if_integer,
+    "code": to_hex_if_bytes,
+}
 call_with_override: Callable[
     [Tuple[TxParams, BlockIdentifier, CallOverrideParams]],
     Tuple[Dict[str, Any], int, Dict[str, Any]],
-]
-call_with_override = apply_formatters_to_sequence(
+] = apply_formatters_to_sequence(
     [
         transaction_param_formatter,
         to_hex_if_integer,
-        lambda x: x,
+        lambda val: type_aware_apply_formatters_to_dict_keys_and_values(
+            to_checksum_address,
+            type_aware_apply_formatters_to_dict(CALL_OVERRIDE_FORMATTERS),
+            val,
+        ),
     ]
 )
 
diff --git a/web3/_utils/type_conversion.py b/web3/_utils/type_conversion.py
--- a/web3/_utils/type_conversion.py
+++ b/web3/_utils/type_conversion.py
@@ -16,6 +16,9 @@ def to_hex_if_bytes(val: Union[HexStr, str, bytes, bytearray]) -> HexStr:
     Note: This method does not validate against all cases and is only
     meant to work with bytes and hex strings.
     """
+    if isinstance(val, str) and not val.startswith("0x"):
+        raise ValueError(f"Expected a hex string. Got: {val!r}")
+
     return to_hex(val) if isinstance(val, (bytes, bytearray)) else to_hex(hexstr=val)
 
 
diff --git a/web3/types.py b/web3/types.py
--- a/web3/types.py
+++ b/web3/types.py
@@ -245,8 +245,8 @@ class LogReceipt(TypedDict):
         "balance": Optional[Wei],
         "nonce": Optional[int],
         "code": Optional[Union[bytes, HexStr]],
-        "state": Optional[Dict[str, Any]],
-        "stateDiff": Optional[Dict[Address, Dict[str, Any]]],
+        "state": Optional[Dict[HexStr, HexStr]],
+        "stateDiff": Optional[Dict[HexStr, HexStr]],
     },
     total=False,
 )


Test patch:
diff --git a/tests/integration/test_ethereum_tester.py b/tests/integration/test_ethereum_tester.py
--- a/tests/integration/test_ethereum_tester.py
+++ b/tests/integration/test_ethereum_tester.py
@@ -457,8 +457,14 @@ def test_eth_wait_for_transaction_receipt_unmined(
     @pytest.mark.xfail(
         raises=TypeError, reason="call override param not implemented on eth-tester"
     )
-    def test_eth_call_with_override(self, w3, revert_contract):
-        super().test_eth_call_with_override(w3, revert_contract)
+    def test_eth_call_with_override_code(self, w3, revert_contract):
+        super().test_eth_call_with_override_code(w3, revert_contract)
+
+    @pytest.mark.xfail(
+        raises=TypeError, reason="call override param not implemented on eth-tester"
+    )
+    def test_eth_call_with_override_param_type_check(self, w3, math_contract):
+        super().test_eth_call_with_override_param_type_check(w3, math_contract)
 
     def test_eth_call_revert_with_msg(self, w3, revert_contract, unlocked_account):
         with pytest.raises(
diff --git a/web3/_utils/module_testing/eth_module.py b/web3/_utils/module_testing/eth_module.py
--- a/web3/_utils/module_testing/eth_module.py
+++ b/web3/_utils/module_testing/eth_module.py
@@ -26,6 +26,7 @@
     is_list_like,
     is_same_address,
     is_string,
+    to_bytes,
 )
 from eth_utils.toolz import (
     assoc,
@@ -74,11 +75,11 @@
     async_construct_result_generator_middleware,
     construct_error_generator_middleware,
 )
-from web3.types import (  # noqa: F401
+from web3.types import (
     ENS,
     BlockData,
+    CallOverrideParams,
     FilterParams,
-    LogReceipt,
     Nonce,
     RPCEndpoint,
     SyncStatus,
@@ -758,8 +759,10 @@ async def test_eth_call(
         assert result == 18
 
     @pytest.mark.asyncio
-    async def test_eth_call_with_override(
-        self, async_w3: "AsyncWeb3", revert_contract: "Contract"
+    async def test_eth_call_with_override_code(
+        self,
+        async_w3: "AsyncWeb3",
+        revert_contract: "Contract",
     ) -> None:
         coinbase = await async_w3.eth.coinbase
         txn_params = revert_contract._prepare_transaction(
@@ -780,6 +783,48 @@ async def test_eth_call_with_override(
         (result,) = async_w3.codec.decode(["bool"], call_result)
         assert result is False
 
+        # test bytes
+
+        bytes_call_result = await async_w3.eth.call(
+            txn_params,
+            "latest",
+            {revert_contract.address: {"code": to_bytes(hexstr=override_code)}},
+        )
+        (bytes_result,) = async_w3.codec.decode(["bool"], bytes_call_result)
+        assert bytes_result is False
+
+    @pytest.mark.asyncio
+    @pytest.mark.parametrize(
+        "params",
+        (
+            {
+                "nonce": 1,  # int
+                "balance": 1,  # int
+                "code": HexStr("0x"),  # HexStr
+                # with state
+                "state": {HexStr(f"0x{'00' * 32}"): HexStr(f"0x{'00' * 32}")},
+            },
+            {
+                "nonce": HexStr("0x1"),  # HexStr
+                "balance": HexStr("0x1"),  # HexStr
+                "code": b"\x00",  # bytes
+                # with stateDiff
+                "stateDiff": {HexStr(f"0x{'00' * 32}"): HexStr(f"0x{'00' * 32}")},
+            },
+        ),
+    )
+    async def test_eth_call_with_override_param_type_check(
+        self,
+        async_w3: "AsyncWeb3",
+        math_contract: "Contract",
+        params: CallOverrideParams,
+    ) -> None:
+        coinbase = await async_w3.eth.coinbase
+        txn_params: TxParams = {"from": coinbase}
+
+        # assert does not raise
+        await async_w3.eth.call(txn_params, "latest", {math_contract.address: params})
+
     @pytest.mark.asyncio
     async def test_eth_call_with_0_result(
         self, async_w3: "AsyncWeb3", math_contract: "Contract"
@@ -2657,7 +2702,7 @@ def test_eth_call(self, w3: "Web3", math_contract: "Contract") -> None:
         (result,) = w3.codec.decode(["uint256"], call_result)
         assert result == 18
 
-    def test_eth_call_with_override(
+    def test_eth_call_with_override_code(
         self, w3: "Web3", revert_contract: "Contract"
     ) -> None:
         coinbase = w3.eth.coinbase
@@ -2679,6 +2724,46 @@ def test_eth_call_with_override(
         (result,) = w3.codec.decode(["bool"], call_result)
         assert result is False
 
+        # test bytes
+
+        bytes_call_result = w3.eth.call(
+            txn_params,
+            "latest",
+            {revert_contract.address: {"code": to_bytes(hexstr=override_code)}},
+        )
+        (bytes_result,) = w3.codec.decode(["bool"], bytes_call_result)
+        assert bytes_result is False
+
+    @pytest.mark.parametrize(
+        "params",
+        (
+            {
+                "nonce": 1,  # int
+                "balance": 1,  # int
+                "code": HexStr("0x"),  # HexStr
+                # with state
+                "state": {HexStr(f"0x{'00' * 32}"): HexStr(f"0x{'00' * 32}")},
+            },
+            {
+                "nonce": HexStr("0x1"),  # HexStr
+                "balance": HexStr("0x1"),  # HexStr
+                "code": b"\x00",  # bytes
+                # with stateDiff
+                "stateDiff": {HexStr(f"0x{'00' * 32}"): HexStr(f"0x{'00' * 32}")},
+            },
+        ),
+    )
+    def test_eth_call_with_override_param_type_check(
+        self,
+        w3: "Web3",
+        math_contract: "Contract",
+        params: CallOverrideParams,
+    ) -> None:
+        txn_params: TxParams = {"from": w3.eth.coinbase}
+
+        # assert does not raise
+        w3.eth.call(txn_params, "latest", {math_contract.address: params})
+
     def test_eth_call_with_0_result(
         self, w3: "Web3", math_contract: "Contract"
     ) -> None: