Repository: ethereum/web3.py

Issue:
Web3 crashes in time-based gas price strategies with empty chain
* Version: 4.7.1
* Python: 3.6
* OS: linux

### What was wrong?

Running our test suite web3 crashes when estimating time based gas price. Probably it is caused by not having any transactions in our test chain. However when probabilities are calculated they are empty (empty tuple) which causes then an Index Error in ` '104, in _compute_gas_price\r\n    first = probabilities[0]\r\nIndexError: tuple index out of range\`

It might be the case that there is a problem with our geth client during runtime.


```
File “/home/circleci/raiden/raiden/network/rpc/client.py”, line 346, in gas_price
return int(self.web3.eth.generateGasPrice())
File “/home/circleci/venv-3.7/lib/python3.7/site-packages/web3/eth.py”, line 385, in generateGasPrice
return self.gasPriceStrategy(self.web3, transaction_params)
File “/home/circleci/venv-3.7/lib/python3.7/site-packages/web3/gas_strategies/time_based.py”, line 168, in time_based_gas_price_strategy
gas_price = _compute_gas_price(probabilities, probability / 100)
File “/home/circleci/venv-3.7/lib/python3.7/site-packages/web3/gas_strategies/time_based.py”, line 104, in _compute_gas_price
first = probabilities[0]
IndexError: tuple index out of range
```


### How can it be fixed?

However I would suggest handling the edge case that there is an empty tuple and not unconditionally access `first = probabilities[0] `

I'm happy to give more information when needed

Discussion:

Patch:
diff --git a/newsfragments/1149.bugfix.rst b/newsfragments/1149.bugfix.rst
new file mode 100644
index 00000000..d9071f5d
--- /dev/null
+++ b/newsfragments/1149.bugfix.rst
@@ -0,0 +1,1 @@
+Return `w3.eth.gas_price` when calculating time based gas price strategy for an empty chain.
\ No newline at end of file
diff --git a/newsfragments/3101.feature.rst b/newsfragments/3101.feature.rst
new file mode 100644
index 00000000..141022b9
--- /dev/null
+++ b/newsfragments/3101.feature.rst
@@ -0,0 +1,1 @@
+Properly JSON encode ``AttributeDict``, ``bytes``, and ``HexBytes`` when sending a JSON-RPC request by utilizing the in-house ``Web3JsonEncoder`` class.
diff --git a/web3/_utils/encoding.py b/web3/_utils/encoding.py
index 1b2af3f0..cea2ff34 100644
--- a/web3/_utils/encoding.py
+++ b/web3/_utils/encoding.py
@@ -290,9 +290,11 @@ def encode_single_packed(_type: TypeStr, value: Any) -> bytes:
 class Web3JsonEncoder(json.JSONEncoder):
     def default(self, obj: Any) -> Union[Dict[Any, Any], HexStr]:
         if isinstance(obj, AttributeDict):
-            return {k: v for k, v in obj.items()}
-        if isinstance(obj, HexBytes):
+            return obj.__dict__
+        elif isinstance(obj, HexBytes):
             return HexStr(obj.hex())
+        elif isinstance(obj, bytes):
+            return to_hex(obj)
         return json.JSONEncoder.default(self, obj)
 
 
diff --git a/web3/gas_strategies/time_based.py b/web3/gas_strategies/time_based.py
index c48a104b..d66342aa 100644
--- a/web3/gas_strategies/time_based.py
+++ b/web3/gas_strategies/time_based.py
@@ -216,6 +216,10 @@ def construct_time_based_gas_price_strategy(
     """
 
     def time_based_gas_price_strategy(w3: Web3, transaction_params: TxParams) -> Wei:
+        # return gas price when no transactions available to sample
+        if w3.eth.get_block("latest")["number"] == 0:
+            return w3.eth.gas_price
+
         if weighted:
             avg_block_time = _get_weighted_avg_block_time(w3, sample_size=sample_size)
         else:
diff --git a/web3/providers/async_base.py b/web3/providers/async_base.py
index 939f0258..30404b6b 100644
--- a/web3/providers/async_base.py
+++ b/web3/providers/async_base.py
@@ -17,6 +17,7 @@ from eth_utils import (
 
 from web3._utils.encoding import (
     FriendlyJsonSerde,
+    Web3JsonEncoder,
 )
 from web3.exceptions import (
     ProviderConnectionError,
@@ -106,7 +107,7 @@ class AsyncJSONBaseProvider(AsyncBaseProvider):
             "params": params or [],
             "id": request_id,
         }
-        encoded = FriendlyJsonSerde().json_encode(rpc_dict)
+        encoded = FriendlyJsonSerde().json_encode(rpc_dict, cls=Web3JsonEncoder)
         return to_bytes(text=encoded)
 
     def decode_rpc_response(self, raw_response: bytes) -> RPCResponse:
diff --git a/web3/providers/base.py b/web3/providers/base.py
index 3f8c175d..d7877546 100644
--- a/web3/providers/base.py
+++ b/web3/providers/base.py
@@ -15,6 +15,7 @@ from eth_utils import (
 
 from web3._utils.encoding import (
     FriendlyJsonSerde,
+    Web3JsonEncoder,
 )
 from web3.exceptions import (
     ProviderConnectionError,
@@ -106,7 +107,7 @@ class JSONBaseProvider(BaseProvider):
             "params": params or [],
             "id": next(self.request_counter),
         }
-        encoded = FriendlyJsonSerde().json_encode(rpc_dict)
+        encoded = FriendlyJsonSerde().json_encode(rpc_dict, Web3JsonEncoder)
         return to_bytes(text=encoded)
 
     def is_connected(self, show_traceback: bool = False) -> bool:


Test patch:
diff --git a/tests/core/gas-strategies/test_time_based_gas_price_strategy.py b/tests/core/gas-strategies/test_time_based_gas_price_strategy.py
index 348bfbdd..f0f15979 100644
--- a/tests/core/gas-strategies/test_time_based_gas_price_strategy.py
+++ b/tests/core/gas-strategies/test_time_based_gas_price_strategy.py
@@ -171,6 +171,46 @@ def test_time_based_gas_price_strategy(strategy_params, expected):
     assert actual == expected
 
 
+def _get_initial_block(method, params):
+    return {
+        "hash": constants.HASH_ZERO,
+        "number": 0,
+        "parentHash": None,
+        "transactions": [],
+        "miner": "0x" + "Aa" * 20,
+        "timestamp": 0,
+    }
+
+
+def _get_gas_price(method, params):
+    return 4321
+
+
+def test_time_based_gas_price_strategy_without_transactions():
+    fixture_middleware = construct_result_generator_middleware(
+        {
+            "eth_getBlockByHash": _get_initial_block,
+            "eth_getBlockByNumber": _get_initial_block,
+            "eth_gasPrice": _get_gas_price,
+        }
+    )
+
+    w3 = Web3(
+        provider=BaseProvider(),
+        middlewares=[fixture_middleware],
+    )
+
+    time_based_gas_price_strategy = construct_time_based_gas_price_strategy(
+        max_wait_seconds=80,
+        sample_size=5,
+        probability=50,
+        weighted=True,
+    )
+    w3.eth.set_gas_price_strategy(time_based_gas_price_strategy)
+    actual = w3.eth.generate_gas_price()
+    assert actual == w3.eth.gas_price
+
+
 @pytest.mark.parametrize(
     "strategy_params_zero,expected_exception_message",
     (
diff --git a/tests/core/utilities/test_encoding.py b/tests/core/utilities/test_encoding.py
index 0581917a..81d3dd18 100644
--- a/tests/core/utilities/test_encoding.py
+++ b/tests/core/utilities/test_encoding.py
@@ -11,6 +11,9 @@ from eth_utils import (
     is_hex,
     to_hex,
 )
+from hexbytes import (
+    HexBytes,
+)
 from hypothesis import (
     example,
     given,
@@ -19,6 +22,7 @@ from hypothesis import (
 
 from web3._utils.encoding import (
     FriendlyJsonSerde as FriendlyJson,
+    Web3JsonEncoder,
     hex_encode_abi_type,
     hexstr_if_str,
     text_if_str,
@@ -26,6 +30,9 @@ from web3._utils.encoding import (
 from web3._utils.hypothesis import (
     hexstr_strategy,
 )
+from web3.datastructures import (
+    AttributeDict,
+)
 from web3.providers import (
     JSONBaseProvider,
 )
@@ -139,13 +146,85 @@ def test_text_if_str_on_text(val):
             TypeError,
             "Could not encode to JSON: .*'other_date'.*is not JSON serializable",
         ),
+        (
+            {
+                "bytes_obj": b"\x00\x01\x02\x03",
+                "hexbytes_obj": HexBytes(b"\x00\x01\x02\x03"),
+            },
+            None,
+            '{"bytes_obj": "0x00010203", "hexbytes_obj": "0x00010203"}',
+        ),
+        (
+            AttributeDict(
+                {
+                    "transactions": [
+                        AttributeDict(
+                            {
+                                "hash": HexBytes(
+                                    "0x142ab034696c09dcfb2a8b086b494f3f4c419e67b6c04d95882f87156a3b6f35"  # noqa: E501
+                                ),
+                                "nonce": 3,
+                                "blockHash": HexBytes(
+                                    "0xe14a0029f8ae6f41ab2287871d7f2f0658696ce0a842883147629cc1b300fc89"  # noqa: E501
+                                ),
+                                "blockNumber": 4322026,
+                                "transactionIndex": 2,
+                                "from": "0xb17473E95Cc2c37f88C56593Ff8767e10c972359",
+                                "to": "0x8daF62dF221D11b470Ca4531305470DaE4A65784",
+                                "value": 0,
+                                "gasPrice": 3459216019,
+                                "maxPriorityFeePerGas": 2500000000,
+                                "maxFeePerGas": 4421310622,
+                                "gas": 26856,
+                                "input": HexBytes("0x3857"),
+                                "chainId": 1337,
+                                "type": 2,
+                                "accessList": [],
+                                "v": 1,
+                                "s": HexBytes(
+                                    "0x6f5216fc207221a11efe2e4c3e3a881a0b5ca286ede538fc9dbc403b2009ea76"  # noqa: E501
+                                ),
+                                "r": HexBytes(
+                                    "0xd148ae70c8cbef3a038e70e6d1639f0951e60a2965820f33bad19d0a6c2b8116"  # noqa: E501
+                                ),
+                                "yParity": 1,
+                            },
+                        )
+                    ]
+                }
+            ),
+            None,
+            """{"transactions": [{
+                "hash": "0x142ab034696c09dcfb2a8b086b494f3f4c419e67b6c04d95882f87156a3b6f35",  # noqa: E501
+                "nonce": 3,
+                "blockHash": "0xe14a0029f8ae6f41ab2287871d7f2f0658696ce0a842883147629cc1b300fc89",  # noqa: E501
+                "blockNumber": 4322026,
+                "transactionIndex": 2,
+                "from": "0xb17473E95Cc2c37f88C56593Ff8767e10c972359",
+                "to": "0x8daF62dF221D11b470Ca4531305470DaE4A65784",
+                "value": 0,
+                "gasPrice": 3459216019,
+                "maxPriorityFeePerGas": 2500000000,
+                "maxFeePerGas": 4421310622,
+                "gas": 26856,
+                "input": "0x3857",
+                "chainId": 1337,
+                "type": 2,
+                "accessList": [],
+                "v": 1,
+                "s": "0x6f5216fc207221a11efe2e4c3e3a881a0b5ca286ede538fc9dbc403b2009ea76",  # noqa: E501
+                "r": "0xd148ae70c8cbef3a038e70e6d1639f0951e60a2965820f33bad19d0a6c2b8116",  # noqa: E501
+                "yParity": 1,
+            }]}""",
+        ),
     ),
+    ids=("datetime", "datetime_error", "bytes types", "nested attrdict with hexbytes"),
 )
-def test_friendly_json_encode(py_obj, exc_type, expected):
+def test_friendly_json_encode_with_web3_json_encoder(py_obj, exc_type, expected):
     if exc_type is None:
-        assert literal_eval(FriendlyJson().json_encode(py_obj)) == literal_eval(
-            expected
-        )
+        assert literal_eval(
+            FriendlyJson().json_encode(py_obj, Web3JsonEncoder)
+        ) == literal_eval(expected)
     else:
         with pytest.raises(exc_type, match=expected):
             FriendlyJson().json_encode(py_obj)