Skip to content

Commit

Permalink
refactor: utility for estimating maxPriorityFeePerGas via eth_feeHistory
Browse files Browse the repository at this point in the history
Refactor idea from PR ethereum#2259 into sync and async fee utility methods. Change params passed into eth_feeHistory to values that allowed for better results when we tested locally. Add a min and max to the estimated fee history so that we don't allow unsuspecting users to contribute to fee bloating. Max and min values keep the priority fee within a range that healthy blocks should accept, so these transactions would be accepted when fee prices settle from high-fee periods.
  • Loading branch information
fselmo committed Feb 1, 2022
1 parent 8c3c7a4 commit 7addd65
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 20 deletions.
1 change: 1 addition & 0 deletions newsfragments/2259.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Calculate a default ``maxPriorityFeePerGas`` using ``eth_feeHistory`` when ``eth_maxPriorityFeePerGas`` is not available, since the latter is not a part of the Ethereum JSON-RPC specs and only supported by certain clients.
8 changes: 4 additions & 4 deletions tests/integration/test_ethereum_tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@
from eth_tester.exceptions import (
TransactionFailed,
)
from eth_typing import (
BlockNumber,
)
from eth_utils import (
is_checksum_address,
is_dict,
Expand Down Expand Up @@ -295,12 +292,15 @@ def test_eth_getBlockByHash_pending(
block = web3.eth.get_block('pending')
assert block['hash'] is not None

@pytest.mark.xfail(reason='eth_feeHistory is not implemented on eth-tester')
def test_eth_fee_history(self, web3: "Web3"):
super().test_eth_fee_history(web3)

@pytest.mark.xfail(reason='eth_feeHistory is not implemented on eth-tester')
def test_eth_fee_history_with_integer(self, web3: "Web3"):
super().test_eth_fee_history_with_integer(web3, BlockData(number=BlockNumber(1)))
super().test_eth_fee_history_with_integer(web3)

@pytest.mark.xfail(reason='eth_feeHistory is not implemented on eth-tester')
def test_eth_fee_history_no_reward_percentiles(self, web3: "Web3"):
super().test_eth_fee_history_no_reward_percentiles(web3)

Expand Down
53 changes: 53 additions & 0 deletions web3/_utils/fee_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from typing import (
TYPE_CHECKING,
)

from web3.types import (
FeeHistory,
Wei,
)

if TYPE_CHECKING:
from web3.eth import (
AsyncEth, # noqa: F401
Eth, # noqa: F401
)

PRIORITY_FEE_MAX = Wei(1500000000) # 1.5 gwei
PRIORITY_FEE_MIN = Wei(1000000000) # 1 gwei

# 5th percentile fee history from the last 10 blocks
PRIORITY_FEE_HISTORY_PARAMS = (10, 'pending', [5.0])


def _fee_history_priority_fee_estimate(fee_history: FeeHistory) -> Wei:
# grab only non-zero fees and average against only that list
non_empty_block_fees = [fee[0] for fee in fee_history['reward'] if fee[0] != 0]

# prevent division by zero in the extremely unlikely case that all fees within the polled fee
# history range for the specified percentile are 0
divisor = len(non_empty_block_fees) if len(non_empty_block_fees) != 0 else 1

priority_fee_average_for_percentile = Wei(
round(sum(non_empty_block_fees) / divisor)
)

return ( # keep estimated priority fee within a max / min range
PRIORITY_FEE_MAX if priority_fee_average_for_percentile > PRIORITY_FEE_MAX else
PRIORITY_FEE_MIN if priority_fee_average_for_percentile < PRIORITY_FEE_MIN else
priority_fee_average_for_percentile
)


def fee_history_priority_fee(eth: "Eth") -> Wei:
# This is a tested internal call so no need for type hinting. We can keep better consistency
# between the sync and async calls by unpacking PRIORITY_FEE_HISTORY_PARAMS as constants here.
fee_history = eth.fee_history(*PRIORITY_FEE_HISTORY_PARAMS) # type: ignore
return _fee_history_priority_fee_estimate(fee_history)


async def async_fee_history_priority_fee(async_eth: "AsyncEth") -> Wei:
# This is a tested internal call so no need for type hinting. We can keep better consistency
# between the sync and async calls by unpacking PRIORITY_FEE_HISTORY_PARAMS as constants here.
fee_history = await async_eth.fee_history(*PRIORITY_FEE_HISTORY_PARAMS) # type: ignore
return _fee_history_priority_fee_estimate(fee_history)
38 changes: 28 additions & 10 deletions web3/eth.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
from web3._utils.encoding import (
to_hex,
)
from web3._utils.fee_utils import (
async_fee_history_priority_fee,
fee_history_priority_fee,
)
from web3._utils.filters import (
select_filter_method,
)
Expand Down Expand Up @@ -356,11 +360,18 @@ async def hashrate(self) -> int:

@property
async def max_priority_fee(self) -> Wei:
fee_history = await self.fee_history(20, 'pending', [float(1)])
priority_fees_per_gas = fee_history['reward']
fees_sum = sum([fee[0] for fee in priority_fees_per_gas])
max_priority_fee_estimate = round(fees_sum / len(priority_fees_per_gas))
return Wei(max_priority_fee_estimate)
"""
Try to use eth_maxPriorityFeePerGas but, since this is not part of the spec and is only
supported by some clients, fall back to an eth_feeHistory calculation with min and max caps.
"""
try:
return await self._max_priority_fee() # type: ignore
except ValueError:
warnings.warn(
"There was an issue with the method eth_maxPriorityFeePerGas. Calculating using "
"eth_feeHistory."
)
return await async_fee_history_priority_fee(self)

@property
async def mining(self) -> bool:
Expand Down Expand Up @@ -608,11 +619,18 @@ def chainId(self) -> int:

@property
def max_priority_fee(self) -> Wei:
fee_history = self.fee_history(20, 'pending', [float(1)])
priority_fees_per_gas = fee_history['reward']
fees_sum = sum([fee[0] for fee in priority_fees_per_gas])
max_priority_fee_estimate = round(fees_sum / len(priority_fees_per_gas))
return Wei(max_priority_fee_estimate)
"""
Try to use eth_maxPriorityFeePerGas but, since this is not part of the spec and is only
supported by some clients, fall back to an eth_feeHistory calculation with min and max caps.
"""
try:
return self._max_priority_fee()
except ValueError:
warnings.warn(
"There was an issue with the method eth_maxPriorityFeePerGas. Calculating using "
"eth_feeHistory."
)
return fee_history_priority_fee(self)

def get_storage_at_munger(
self,
Expand Down
7 changes: 1 addition & 6 deletions web3/providers/eth_tester/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,7 @@ def personal_send_transaction(eth_tester: "EthereumTester", params: Any) -> HexS
'mining': static_return(False),
'hashrate': static_return(0),
'chainId': static_return('0x3d'),
'feeHistory': static_return(
{'baseFeePerGas': [134919017071, 134775902021, 117928914269],
'gasUsedRatio': [0.4957570088140204, 0.0],
'oldestBlock': 13865084,
'reward': [[2500000000], [1500000000]]}
),
'feeHistory': not_implemented,
'maxPriorityFeePerGas': static_return(10 ** 9),
'gasPrice': static_return(10 ** 9), # must be >= base fee post-London
'accounts': call_eth_tester('get_accounts'),
Expand Down

0 comments on commit 7addd65

Please sign in to comment.