We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN 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

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `web3/gas_strategies/time_based.py`
Content:
```
1 import collections
2 import math
3 import operator
4 from typing import (
5     Iterable,
6     Sequence,
7     Tuple,
8 )
9 
10 from eth_typing import (
11     ChecksumAddress,
12 )
13 from eth_utils import (
14     to_tuple,
15 )
16 from eth_utils.toolz import (
17     curry,
18     groupby,
19     sliding_window,
20 )
21 from hexbytes import (
22     HexBytes,
23 )
24 
25 from web3 import (
26     Web3,
27 )
28 from web3._utils.math import (
29     percentile,
30 )
31 from web3.exceptions import (
32     InsufficientData,
33     Web3ValidationError,
34 )
35 from web3.types import (
36     BlockNumber,
37     GasPriceStrategy,
38     TxParams,
39     Wei,
40 )
41 
42 MinerData = collections.namedtuple(
43     "MinerData", ["miner", "num_blocks", "min_gas_price", "low_percentile_gas_price"]
44 )
45 Probability = collections.namedtuple("Probability", ["gas_price", "prob"])
46 
47 
48 def _get_avg_block_time(w3: Web3, sample_size: int) -> float:
49     latest = w3.eth.get_block("latest")
50 
51     constrained_sample_size = min(sample_size, latest["number"])
52     if constrained_sample_size == 0:
53         raise Web3ValidationError("Constrained sample size is 0")
54 
55     oldest = w3.eth.get_block(BlockNumber(latest["number"] - constrained_sample_size))
56     return (latest["timestamp"] - oldest["timestamp"]) / constrained_sample_size
57 
58 
59 def _get_weighted_avg_block_time(w3: Web3, sample_size: int) -> float:
60     latest_block_number = w3.eth.get_block("latest")["number"]
61     constrained_sample_size = min(sample_size, latest_block_number)
62     if constrained_sample_size == 0:
63         raise Web3ValidationError("Constrained sample size is 0")
64     oldest_block = w3.eth.get_block(
65         BlockNumber(latest_block_number - constrained_sample_size)
66     )
67     oldest_block_number = oldest_block["number"]
68     prev_timestamp = oldest_block["timestamp"]
69     weighted_sum = 0.0
70     sum_of_weights = 0.0
71     for i in range(oldest_block_number + 1, latest_block_number + 1):
72         curr_timestamp = w3.eth.get_block(BlockNumber(i))["timestamp"]
73         time = curr_timestamp - prev_timestamp
74         weight = (i - oldest_block_number) / constrained_sample_size
75         weighted_sum += time * weight
76         sum_of_weights += weight
77         prev_timestamp = curr_timestamp
78     return weighted_sum / sum_of_weights
79 
80 
81 def _get_raw_miner_data(
82     w3: Web3, sample_size: int
83 ) -> Iterable[Tuple[ChecksumAddress, HexBytes, Wei]]:
84     latest = w3.eth.get_block("latest", full_transactions=True)
85 
86     for transaction in latest["transactions"]:
87         # type ignored b/c actual transaction is TxData not HexBytes
88         yield (latest["miner"], latest["hash"], transaction["gasPrice"])  # type: ignore
89 
90     block = latest
91 
92     for _ in range(sample_size - 1):
93         if block["number"] == 0:
94             break
95 
96         # we intentionally trace backwards using parent hashes rather than
97         # block numbers to make caching the data easier to implement.
98         block = w3.eth.get_block(block["parentHash"], full_transactions=True)
99         for transaction in block["transactions"]:
100             # type ignored b/c actual transaction is TxData not HexBytes
101             yield (block["miner"], block["hash"], transaction["gasPrice"])  # type: ignore  # noqa: E501
102 
103 
104 def _aggregate_miner_data(
105     raw_data: Iterable[Tuple[ChecksumAddress, HexBytes, Wei]]
106 ) -> Iterable[MinerData]:
107     data_by_miner = groupby(0, raw_data)
108 
109     for miner, miner_data in data_by_miner.items():
110         _, block_hashes, gas_prices = map(set, zip(*miner_data))
111         try:
112             # types ignored b/c mypy has trouble inferring gas_prices: Sequence[Wei]
113             price_percentile = percentile(gas_prices, percentile=20)  # type: ignore
114         except InsufficientData:
115             price_percentile = min(gas_prices)  # type: ignore
116         yield MinerData(
117             miner,
118             len(set(block_hashes)),
119             min(gas_prices),  # type: ignore
120             price_percentile,
121         )
122 
123 
124 @to_tuple
125 def _compute_probabilities(
126     miner_data: Iterable[MinerData], wait_blocks: int, sample_size: int
127 ) -> Iterable[Probability]:
128     """
129     Computes the probabilities that a txn will be accepted at each of the gas
130     prices accepted by the miners.
131     """
132     miner_data_by_price = tuple(
133         sorted(
134             miner_data,
135             key=operator.attrgetter("low_percentile_gas_price"),
136             reverse=True,
137         )
138     )
139     for idx in range(len(miner_data_by_price)):
140         low_percentile_gas_price = miner_data_by_price[idx].low_percentile_gas_price
141         num_blocks_accepting_price = sum(
142             m.num_blocks for m in miner_data_by_price[idx:]
143         )
144         inv_prob_per_block = (sample_size - num_blocks_accepting_price) / sample_size
145         probability_accepted = 1 - inv_prob_per_block**wait_blocks
146         yield Probability(low_percentile_gas_price, probability_accepted)
147 
148 
149 def _compute_gas_price(
150     probabilities: Sequence[Probability], desired_probability: float
151 ) -> Wei:
152     """
153     Given a sorted range of ``Probability`` named-tuples returns a gas price
154     computed based on where the ``desired_probability`` would fall within the
155     range.
156 
157     :param probabilities: An iterable of `Probability` named-tuples
158         sorted in reverse order.
159     :param desired_probability: An floating point representation of the desired
160         probability. (e.g. ``85% -> 0.85``)
161     """
162     first = probabilities[0]
163     last = probabilities[-1]
164 
165     if desired_probability >= first.prob:
166         return Wei(int(first.gas_price))
167     elif desired_probability <= last.prob:
168         return Wei(int(last.gas_price))
169 
170     for left, right in sliding_window(2, probabilities):
171         if desired_probability < right.prob:
172             continue
173         elif desired_probability > left.prob:
174             # This code block should never be reachable as it would indicate
175             # that we already passed by the probability window in which our
176             # `desired_probability` is located.
177             raise Exception("Invariant")
178 
179         adj_prob = desired_probability - right.prob
180         window_size = left.prob - right.prob
181         position = adj_prob / window_size
182         gas_window_size = left.gas_price - right.gas_price
183         gas_price = int(math.ceil(right.gas_price + gas_window_size * position))
184         return Wei(gas_price)
185     else:
186         # The initial `if/else` clause in this function handles the case where
187         # the `desired_probability` is either above or below the min/max
188         # probability found in the `probabilities`.
189         #
190         # With these two cases handled, the only way this code block should be
191         # reachable would be if the `probabilities` were not sorted correctly.
192         # Otherwise, the `desired_probability` **must** fall between two of the
193         # values in the `probabilities``.
194         raise Exception("Invariant")
195 
196 
197 @curry
198 def construct_time_based_gas_price_strategy(
199     max_wait_seconds: int,
200     sample_size: int = 120,
201     probability: int = 98,
202     weighted: bool = False,
203 ) -> GasPriceStrategy:
204     """
205     A gas pricing strategy that uses recently mined block data to derive a gas
206     price for which a transaction is likely to be mined within X seconds with
207     probability P. If the weighted kwarg is True, more recent block
208     times will be more heavily weighted.
209 
210     :param max_wait_seconds: The desired maximum number of seconds the
211         transaction should take to mine.
212     :param sample_size: The number of recent blocks to sample
213     :param probability: An integer representation of the desired probability
214         that the transaction will be mined within ``max_wait_seconds``.  0 means 0%
215         and 100 means 100%.
216     """
217 
218     def time_based_gas_price_strategy(w3: Web3, transaction_params: TxParams) -> Wei:
219         if weighted:
220             avg_block_time = _get_weighted_avg_block_time(w3, sample_size=sample_size)
221         else:
222             avg_block_time = _get_avg_block_time(w3, sample_size=sample_size)
223 
224         wait_blocks = int(math.ceil(max_wait_seconds / avg_block_time))
225         raw_miner_data = _get_raw_miner_data(w3, sample_size=sample_size)
226         miner_data = _aggregate_miner_data(raw_miner_data)
227 
228         probabilities = _compute_probabilities(
229             miner_data,
230             wait_blocks=wait_blocks,
231             sample_size=sample_size,
232         )
233 
234         gas_price = _compute_gas_price(probabilities, probability / 100)
235         return gas_price
236 
237     return time_based_gas_price_strategy
238 
239 
240 # fast: mine within 1 minute
241 fast_gas_price_strategy = construct_time_based_gas_price_strategy(
242     max_wait_seconds=60,
243     sample_size=120,
244 )
245 # medium: mine within 10 minutes
246 medium_gas_price_strategy = construct_time_based_gas_price_strategy(
247     max_wait_seconds=600,
248     sample_size=120,
249 )
250 # slow: mine within 1 hour (60 minutes)
251 slow_gas_price_strategy = construct_time_based_gas_price_strategy(
252     max_wait_seconds=60 * 60,
253     sample_size=120,
254 )
255 # glacial: mine within the next 24 hours.
256 glacial_gas_price_strategy = construct_time_based_gas_price_strategy(
257     max_wait_seconds=24 * 60 * 60,
258     sample_size=720,
259 )
260
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/web3/gas_strategies/time_based.py b/web3/gas_strategies/time_based.py
--- a/web3/gas_strategies/time_based.py
+++ b/web3/gas_strategies/time_based.py
@@ -216,6 +216,10 @@
     """
 
     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:

{"golden_diff": "diff --git a/web3/gas_strategies/time_based.py b/web3/gas_strategies/time_based.py\n--- a/web3/gas_strategies/time_based.py\n+++ b/web3/gas_strategies/time_based.py\n@@ -216,6 +216,10 @@\n     \"\"\"\n \n     def time_based_gas_price_strategy(w3: Web3, transaction_params: TxParams) -> Wei:\n+        # return gas price when no transactions available to sample\n+        if w3.eth.get_block(\"latest\")[\"number\"] == 0:\n+            return w3.eth.gas_price\n+\n         if weighted:\n             avg_block_time = _get_weighted_avg_block_time(w3, sample_size=sample_size)\n         else:\n", "issue": "Web3 crashes in time-based gas price strategies with empty chain\n* Version: 4.7.1\r\n* Python: 3.6\r\n* OS: linux\r\n\r\n### What was wrong?\r\n\r\nRunning 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\\`\r\n\r\nIt might be the case that there is a problem with our geth client during runtime.\r\n\r\n\r\n```\r\nFile \u201c/home/circleci/raiden/raiden/network/rpc/client.py\u201d, line 346, in gas_price\r\nreturn int(self.web3.eth.generateGasPrice())\r\nFile \u201c/home/circleci/venv-3.7/lib/python3.7/site-packages/web3/eth.py\u201d, line 385, in generateGasPrice\r\nreturn self.gasPriceStrategy(self.web3, transaction_params)\r\nFile \u201c/home/circleci/venv-3.7/lib/python3.7/site-packages/web3/gas_strategies/time_based.py\u201d, line 168, in time_based_gas_price_strategy\r\ngas_price = _compute_gas_price(probabilities, probability / 100)\r\nFile \u201c/home/circleci/venv-3.7/lib/python3.7/site-packages/web3/gas_strategies/time_based.py\u201d, line 104, in _compute_gas_price\r\nfirst = probabilities[0]\r\nIndexError: tuple index out of range\r\n```\r\n\r\n\r\n### How can it be fixed?\r\n\r\nHowever I would suggest handling the edge case that there is an empty tuple and not unconditionally access `first = probabilities[0] `\r\n\r\nI'm happy to give more information when needed\n", "before_files": [{"content": "import collections\nimport math\nimport operator\nfrom typing import (\n    Iterable,\n    Sequence,\n    Tuple,\n)\n\nfrom eth_typing import (\n    ChecksumAddress,\n)\nfrom eth_utils import (\n    to_tuple,\n)\nfrom eth_utils.toolz import (\n    curry,\n    groupby,\n    sliding_window,\n)\nfrom hexbytes import (\n    HexBytes,\n)\n\nfrom web3 import (\n    Web3,\n)\nfrom web3._utils.math import (\n    percentile,\n)\nfrom web3.exceptions import (\n    InsufficientData,\n    Web3ValidationError,\n)\nfrom web3.types import (\n    BlockNumber,\n    GasPriceStrategy,\n    TxParams,\n    Wei,\n)\n\nMinerData = collections.namedtuple(\n    \"MinerData\", [\"miner\", \"num_blocks\", \"min_gas_price\", \"low_percentile_gas_price\"]\n)\nProbability = collections.namedtuple(\"Probability\", [\"gas_price\", \"prob\"])\n\n\ndef _get_avg_block_time(w3: Web3, sample_size: int) -> float:\n    latest = w3.eth.get_block(\"latest\")\n\n    constrained_sample_size = min(sample_size, latest[\"number\"])\n    if constrained_sample_size == 0:\n        raise Web3ValidationError(\"Constrained sample size is 0\")\n\n    oldest = w3.eth.get_block(BlockNumber(latest[\"number\"] - constrained_sample_size))\n    return (latest[\"timestamp\"] - oldest[\"timestamp\"]) / constrained_sample_size\n\n\ndef _get_weighted_avg_block_time(w3: Web3, sample_size: int) -> float:\n    latest_block_number = w3.eth.get_block(\"latest\")[\"number\"]\n    constrained_sample_size = min(sample_size, latest_block_number)\n    if constrained_sample_size == 0:\n        raise Web3ValidationError(\"Constrained sample size is 0\")\n    oldest_block = w3.eth.get_block(\n        BlockNumber(latest_block_number - constrained_sample_size)\n    )\n    oldest_block_number = oldest_block[\"number\"]\n    prev_timestamp = oldest_block[\"timestamp\"]\n    weighted_sum = 0.0\n    sum_of_weights = 0.0\n    for i in range(oldest_block_number + 1, latest_block_number + 1):\n        curr_timestamp = w3.eth.get_block(BlockNumber(i))[\"timestamp\"]\n        time = curr_timestamp - prev_timestamp\n        weight = (i - oldest_block_number) / constrained_sample_size\n        weighted_sum += time * weight\n        sum_of_weights += weight\n        prev_timestamp = curr_timestamp\n    return weighted_sum / sum_of_weights\n\n\ndef _get_raw_miner_data(\n    w3: Web3, sample_size: int\n) -> Iterable[Tuple[ChecksumAddress, HexBytes, Wei]]:\n    latest = w3.eth.get_block(\"latest\", full_transactions=True)\n\n    for transaction in latest[\"transactions\"]:\n        # type ignored b/c actual transaction is TxData not HexBytes\n        yield (latest[\"miner\"], latest[\"hash\"], transaction[\"gasPrice\"])  # type: ignore\n\n    block = latest\n\n    for _ in range(sample_size - 1):\n        if block[\"number\"] == 0:\n            break\n\n        # we intentionally trace backwards using parent hashes rather than\n        # block numbers to make caching the data easier to implement.\n        block = w3.eth.get_block(block[\"parentHash\"], full_transactions=True)\n        for transaction in block[\"transactions\"]:\n            # type ignored b/c actual transaction is TxData not HexBytes\n            yield (block[\"miner\"], block[\"hash\"], transaction[\"gasPrice\"])  # type: ignore  # noqa: E501\n\n\ndef _aggregate_miner_data(\n    raw_data: Iterable[Tuple[ChecksumAddress, HexBytes, Wei]]\n) -> Iterable[MinerData]:\n    data_by_miner = groupby(0, raw_data)\n\n    for miner, miner_data in data_by_miner.items():\n        _, block_hashes, gas_prices = map(set, zip(*miner_data))\n        try:\n            # types ignored b/c mypy has trouble inferring gas_prices: Sequence[Wei]\n            price_percentile = percentile(gas_prices, percentile=20)  # type: ignore\n        except InsufficientData:\n            price_percentile = min(gas_prices)  # type: ignore\n        yield MinerData(\n            miner,\n            len(set(block_hashes)),\n            min(gas_prices),  # type: ignore\n            price_percentile,\n        )\n\n\n@to_tuple\ndef _compute_probabilities(\n    miner_data: Iterable[MinerData], wait_blocks: int, sample_size: int\n) -> Iterable[Probability]:\n    \"\"\"\n    Computes the probabilities that a txn will be accepted at each of the gas\n    prices accepted by the miners.\n    \"\"\"\n    miner_data_by_price = tuple(\n        sorted(\n            miner_data,\n            key=operator.attrgetter(\"low_percentile_gas_price\"),\n            reverse=True,\n        )\n    )\n    for idx in range(len(miner_data_by_price)):\n        low_percentile_gas_price = miner_data_by_price[idx].low_percentile_gas_price\n        num_blocks_accepting_price = sum(\n            m.num_blocks for m in miner_data_by_price[idx:]\n        )\n        inv_prob_per_block = (sample_size - num_blocks_accepting_price) / sample_size\n        probability_accepted = 1 - inv_prob_per_block**wait_blocks\n        yield Probability(low_percentile_gas_price, probability_accepted)\n\n\ndef _compute_gas_price(\n    probabilities: Sequence[Probability], desired_probability: float\n) -> Wei:\n    \"\"\"\n    Given a sorted range of ``Probability`` named-tuples returns a gas price\n    computed based on where the ``desired_probability`` would fall within the\n    range.\n\n    :param probabilities: An iterable of `Probability` named-tuples\n        sorted in reverse order.\n    :param desired_probability: An floating point representation of the desired\n        probability. (e.g. ``85% -> 0.85``)\n    \"\"\"\n    first = probabilities[0]\n    last = probabilities[-1]\n\n    if desired_probability >= first.prob:\n        return Wei(int(first.gas_price))\n    elif desired_probability <= last.prob:\n        return Wei(int(last.gas_price))\n\n    for left, right in sliding_window(2, probabilities):\n        if desired_probability < right.prob:\n            continue\n        elif desired_probability > left.prob:\n            # This code block should never be reachable as it would indicate\n            # that we already passed by the probability window in which our\n            # `desired_probability` is located.\n            raise Exception(\"Invariant\")\n\n        adj_prob = desired_probability - right.prob\n        window_size = left.prob - right.prob\n        position = adj_prob / window_size\n        gas_window_size = left.gas_price - right.gas_price\n        gas_price = int(math.ceil(right.gas_price + gas_window_size * position))\n        return Wei(gas_price)\n    else:\n        # The initial `if/else` clause in this function handles the case where\n        # the `desired_probability` is either above or below the min/max\n        # probability found in the `probabilities`.\n        #\n        # With these two cases handled, the only way this code block should be\n        # reachable would be if the `probabilities` were not sorted correctly.\n        # Otherwise, the `desired_probability` **must** fall between two of the\n        # values in the `probabilities``.\n        raise Exception(\"Invariant\")\n\n\n@curry\ndef construct_time_based_gas_price_strategy(\n    max_wait_seconds: int,\n    sample_size: int = 120,\n    probability: int = 98,\n    weighted: bool = False,\n) -> GasPriceStrategy:\n    \"\"\"\n    A gas pricing strategy that uses recently mined block data to derive a gas\n    price for which a transaction is likely to be mined within X seconds with\n    probability P. If the weighted kwarg is True, more recent block\n    times will be more heavily weighted.\n\n    :param max_wait_seconds: The desired maximum number of seconds the\n        transaction should take to mine.\n    :param sample_size: The number of recent blocks to sample\n    :param probability: An integer representation of the desired probability\n        that the transaction will be mined within ``max_wait_seconds``.  0 means 0%\n        and 100 means 100%.\n    \"\"\"\n\n    def time_based_gas_price_strategy(w3: Web3, transaction_params: TxParams) -> Wei:\n        if weighted:\n            avg_block_time = _get_weighted_avg_block_time(w3, sample_size=sample_size)\n        else:\n            avg_block_time = _get_avg_block_time(w3, sample_size=sample_size)\n\n        wait_blocks = int(math.ceil(max_wait_seconds / avg_block_time))\n        raw_miner_data = _get_raw_miner_data(w3, sample_size=sample_size)\n        miner_data = _aggregate_miner_data(raw_miner_data)\n\n        probabilities = _compute_probabilities(\n            miner_data,\n            wait_blocks=wait_blocks,\n            sample_size=sample_size,\n        )\n\n        gas_price = _compute_gas_price(probabilities, probability / 100)\n        return gas_price\n\n    return time_based_gas_price_strategy\n\n\n# fast: mine within 1 minute\nfast_gas_price_strategy = construct_time_based_gas_price_strategy(\n    max_wait_seconds=60,\n    sample_size=120,\n)\n# medium: mine within 10 minutes\nmedium_gas_price_strategy = construct_time_based_gas_price_strategy(\n    max_wait_seconds=600,\n    sample_size=120,\n)\n# slow: mine within 1 hour (60 minutes)\nslow_gas_price_strategy = construct_time_based_gas_price_strategy(\n    max_wait_seconds=60 * 60,\n    sample_size=120,\n)\n# glacial: mine within the next 24 hours.\nglacial_gas_price_strategy = construct_time_based_gas_price_strategy(\n    max_wait_seconds=24 * 60 * 60,\n    sample_size=720,\n)\n", "path": "web3/gas_strategies/time_based.py"}], "after_files": [{"content": "import collections\nimport math\nimport operator\nfrom typing import (\n    Iterable,\n    Sequence,\n    Tuple,\n)\n\nfrom eth_typing import (\n    ChecksumAddress,\n)\nfrom eth_utils import (\n    to_tuple,\n)\nfrom eth_utils.toolz import (\n    curry,\n    groupby,\n    sliding_window,\n)\nfrom hexbytes import (\n    HexBytes,\n)\n\nfrom web3 import (\n    Web3,\n)\nfrom web3._utils.math import (\n    percentile,\n)\nfrom web3.exceptions import (\n    InsufficientData,\n    Web3ValidationError,\n)\nfrom web3.types import (\n    BlockNumber,\n    GasPriceStrategy,\n    TxParams,\n    Wei,\n)\n\nMinerData = collections.namedtuple(\n    \"MinerData\", [\"miner\", \"num_blocks\", \"min_gas_price\", \"low_percentile_gas_price\"]\n)\nProbability = collections.namedtuple(\"Probability\", [\"gas_price\", \"prob\"])\n\n\ndef _get_avg_block_time(w3: Web3, sample_size: int) -> float:\n    latest = w3.eth.get_block(\"latest\")\n\n    constrained_sample_size = min(sample_size, latest[\"number\"])\n    if constrained_sample_size == 0:\n        raise Web3ValidationError(\"Constrained sample size is 0\")\n\n    oldest = w3.eth.get_block(BlockNumber(latest[\"number\"] - constrained_sample_size))\n    return (latest[\"timestamp\"] - oldest[\"timestamp\"]) / constrained_sample_size\n\n\ndef _get_weighted_avg_block_time(w3: Web3, sample_size: int) -> float:\n    latest_block_number = w3.eth.get_block(\"latest\")[\"number\"]\n    constrained_sample_size = min(sample_size, latest_block_number)\n    if constrained_sample_size == 0:\n        raise Web3ValidationError(\"Constrained sample size is 0\")\n    oldest_block = w3.eth.get_block(\n        BlockNumber(latest_block_number - constrained_sample_size)\n    )\n    oldest_block_number = oldest_block[\"number\"]\n    prev_timestamp = oldest_block[\"timestamp\"]\n    weighted_sum = 0.0\n    sum_of_weights = 0.0\n    for i in range(oldest_block_number + 1, latest_block_number + 1):\n        curr_timestamp = w3.eth.get_block(BlockNumber(i))[\"timestamp\"]\n        time = curr_timestamp - prev_timestamp\n        weight = (i - oldest_block_number) / constrained_sample_size\n        weighted_sum += time * weight\n        sum_of_weights += weight\n        prev_timestamp = curr_timestamp\n    return weighted_sum / sum_of_weights\n\n\ndef _get_raw_miner_data(\n    w3: Web3, sample_size: int\n) -> Iterable[Tuple[ChecksumAddress, HexBytes, Wei]]:\n    latest = w3.eth.get_block(\"latest\", full_transactions=True)\n\n    for transaction in latest[\"transactions\"]:\n        # type ignored b/c actual transaction is TxData not HexBytes\n        yield (latest[\"miner\"], latest[\"hash\"], transaction[\"gasPrice\"])  # type: ignore\n\n    block = latest\n\n    for _ in range(sample_size - 1):\n        if block[\"number\"] == 0:\n            break\n\n        # we intentionally trace backwards using parent hashes rather than\n        # block numbers to make caching the data easier to implement.\n        block = w3.eth.get_block(block[\"parentHash\"], full_transactions=True)\n        for transaction in block[\"transactions\"]:\n            # type ignored b/c actual transaction is TxData not HexBytes\n            yield (block[\"miner\"], block[\"hash\"], transaction[\"gasPrice\"])  # type: ignore  # noqa: E501\n\n\ndef _aggregate_miner_data(\n    raw_data: Iterable[Tuple[ChecksumAddress, HexBytes, Wei]]\n) -> Iterable[MinerData]:\n    data_by_miner = groupby(0, raw_data)\n\n    for miner, miner_data in data_by_miner.items():\n        _, block_hashes, gas_prices = map(set, zip(*miner_data))\n        try:\n            # types ignored b/c mypy has trouble inferring gas_prices: Sequence[Wei]\n            price_percentile = percentile(gas_prices, percentile=20)  # type: ignore\n        except InsufficientData:\n            price_percentile = min(gas_prices)  # type: ignore\n        yield MinerData(\n            miner,\n            len(set(block_hashes)),\n            min(gas_prices),  # type: ignore\n            price_percentile,\n        )\n\n\n@to_tuple\ndef _compute_probabilities(\n    miner_data: Iterable[MinerData], wait_blocks: int, sample_size: int\n) -> Iterable[Probability]:\n    \"\"\"\n    Computes the probabilities that a txn will be accepted at each of the gas\n    prices accepted by the miners.\n    \"\"\"\n    miner_data_by_price = tuple(\n        sorted(\n            miner_data,\n            key=operator.attrgetter(\"low_percentile_gas_price\"),\n            reverse=True,\n        )\n    )\n    for idx in range(len(miner_data_by_price)):\n        low_percentile_gas_price = miner_data_by_price[idx].low_percentile_gas_price\n        num_blocks_accepting_price = sum(\n            m.num_blocks for m in miner_data_by_price[idx:]\n        )\n        inv_prob_per_block = (sample_size - num_blocks_accepting_price) / sample_size\n        probability_accepted = 1 - inv_prob_per_block**wait_blocks\n        yield Probability(low_percentile_gas_price, probability_accepted)\n\n\ndef _compute_gas_price(\n    probabilities: Sequence[Probability], desired_probability: float\n) -> Wei:\n    \"\"\"\n    Given a sorted range of ``Probability`` named-tuples returns a gas price\n    computed based on where the ``desired_probability`` would fall within the\n    range.\n\n    :param probabilities: An iterable of `Probability` named-tuples\n        sorted in reverse order.\n    :param desired_probability: An floating point representation of the desired\n        probability. (e.g. ``85% -> 0.85``)\n    \"\"\"\n    first = probabilities[0]\n    last = probabilities[-1]\n\n    if desired_probability >= first.prob:\n        return Wei(int(first.gas_price))\n    elif desired_probability <= last.prob:\n        return Wei(int(last.gas_price))\n\n    for left, right in sliding_window(2, probabilities):\n        if desired_probability < right.prob:\n            continue\n        elif desired_probability > left.prob:\n            # This code block should never be reachable as it would indicate\n            # that we already passed by the probability window in which our\n            # `desired_probability` is located.\n            raise Exception(\"Invariant\")\n\n        adj_prob = desired_probability - right.prob\n        window_size = left.prob - right.prob\n        position = adj_prob / window_size\n        gas_window_size = left.gas_price - right.gas_price\n        gas_price = int(math.ceil(right.gas_price + gas_window_size * position))\n        return Wei(gas_price)\n    else:\n        # The initial `if/else` clause in this function handles the case where\n        # the `desired_probability` is either above or below the min/max\n        # probability found in the `probabilities`.\n        #\n        # With these two cases handled, the only way this code block should be\n        # reachable would be if the `probabilities` were not sorted correctly.\n        # Otherwise, the `desired_probability` **must** fall between two of the\n        # values in the `probabilities``.\n        raise Exception(\"Invariant\")\n\n\n@curry\ndef construct_time_based_gas_price_strategy(\n    max_wait_seconds: int,\n    sample_size: int = 120,\n    probability: int = 98,\n    weighted: bool = False,\n) -> GasPriceStrategy:\n    \"\"\"\n    A gas pricing strategy that uses recently mined block data to derive a gas\n    price for which a transaction is likely to be mined within X seconds with\n    probability P. If the weighted kwarg is True, more recent block\n    times will be more heavily weighted.\n\n    :param max_wait_seconds: The desired maximum number of seconds the\n        transaction should take to mine.\n    :param sample_size: The number of recent blocks to sample\n    :param probability: An integer representation of the desired probability\n        that the transaction will be mined within ``max_wait_seconds``.  0 means 0%\n        and 100 means 100%.\n    \"\"\"\n\n    def time_based_gas_price_strategy(w3: Web3, transaction_params: TxParams) -> Wei:\n        # return gas price when no transactions available to sample\n        if w3.eth.get_block(\"latest\")[\"number\"] == 0:\n            return w3.eth.gas_price\n\n        if weighted:\n            avg_block_time = _get_weighted_avg_block_time(w3, sample_size=sample_size)\n        else:\n            avg_block_time = _get_avg_block_time(w3, sample_size=sample_size)\n\n        wait_blocks = int(math.ceil(max_wait_seconds / avg_block_time))\n        raw_miner_data = _get_raw_miner_data(w3, sample_size=sample_size)\n        miner_data = _aggregate_miner_data(raw_miner_data)\n\n        probabilities = _compute_probabilities(\n            miner_data,\n            wait_blocks=wait_blocks,\n            sample_size=sample_size,\n        )\n\n        gas_price = _compute_gas_price(probabilities, probability / 100)\n        return gas_price\n\n    return time_based_gas_price_strategy\n\n\n# fast: mine within 1 minute\nfast_gas_price_strategy = construct_time_based_gas_price_strategy(\n    max_wait_seconds=60,\n    sample_size=120,\n)\n# medium: mine within 10 minutes\nmedium_gas_price_strategy = construct_time_based_gas_price_strategy(\n    max_wait_seconds=600,\n    sample_size=120,\n)\n# slow: mine within 1 hour (60 minutes)\nslow_gas_price_strategy = construct_time_based_gas_price_strategy(\n    max_wait_seconds=60 * 60,\n    sample_size=120,\n)\n# glacial: mine within the next 24 hours.\nglacial_gas_price_strategy = construct_time_based_gas_price_strategy(\n    max_wait_seconds=24 * 60 * 60,\n    sample_size=720,\n)\n", "path": "web3/gas_strategies/time_based.py"}]}