diff --git a/docs/userguides/config.md b/docs/userguides/config.md index a5c7bb969a..7861d18191 100644 --- a/docs/userguides/config.md +++ b/docs/userguides/config.md @@ -102,8 +102,19 @@ You may use one of: - `"auto"` - gas limit is estimated for each transaction - `"max"` - the maximum block gas limit is used - A number or numeric string, base 10 or 16 (e.g. `1234`, `"1234"`, `0x1234`, `"0x1234"`) +- An object with key `"auto"` for specifying an estimate-multiplier for transaction insurance -For the local network configuration, the default is `"max"`. Otherwise it is `"auto"`. +To use the auto-multiplier, make your config like this: + +```yaml +ethereum: + mainnet: + gas_limit: + auto: + multiplier: 1.2 # Multiply 1.2 times the result of eth_estimateGas +``` + +For the local network configuration, the default is `"max"`. Otherwise, it is `"auto"`. ## Plugins diff --git a/src/ape/api/accounts.py b/src/ape/api/accounts.py index 6a6aec93be..2f474fc925 100644 --- a/src/ape/api/accounts.py +++ b/src/ape/api/accounts.py @@ -117,7 +117,7 @@ def call( raise TransactionError("Transaction not prepared.") # The conditions below should never reached but are here for mypy's sake. - # The `max_fee` was either set manaully or from `prepare_transaction()`. + # The `max_fee` was either set manually or from `prepare_transaction()`. # The `gas_limit` was either set manually or from `prepare_transaction()`. if max_fee is None: raise TransactionError("`max_fee` failed to get set in transaction preparation.") diff --git a/src/ape/api/networks.py b/src/ape/api/networks.py index 30f92e36b5..bb45a69b89 100644 --- a/src/ape/api/networks.py +++ b/src/ape/api/networks.py @@ -21,7 +21,7 @@ SignatureError, ) from ape.logging import logger -from ape.types import AddressType, CallTreeNode, ContractLog, GasLimit, RawAddress +from ape.types import AddressType, AutoGasLimit, CallTreeNode, ContractLog, GasLimit, RawAddress from ape.utils import ( DEFAULT_TRANSACTION_ACCEPTANCE_TIMEOUT, BaseInterfaceModel, @@ -710,6 +710,13 @@ def _network_config(self) -> Dict: def gas_limit(self) -> GasLimit: return self._network_config.get("gas_limit", "auto") + @cached_property + def auto_gas_multiplier(self) -> float: + """ + The value to multiply estimated gas by for tx-insurance. + """ + return self.gas_limit.multiplier if isinstance(self.gas_limit, AutoGasLimit) else 1.0 + @property def chain_id(self) -> int: """ diff --git a/src/ape/api/providers.py b/src/ape/api/providers.py index ce9c99d5e1..e16037f3f8 100644 --- a/src/ape/api/providers.py +++ b/src/ape/api/providers.py @@ -49,6 +49,7 @@ from ape.logging import LogLevel, logger from ape.types import ( AddressType, + AutoGasLimit, BlockID, CallTreeNode, ContractCode, @@ -813,9 +814,11 @@ def estimate_gas_cost(self, txn: TransactionAPI, **kwargs) -> int: txn_dict["type"] = HexBytes(txn_dict["type"]).hex() # NOTE: "auto" means to enter this method, so remove it from dict - if "gas" in txn_dict and txn_dict["gas"] == "auto": + if "gas" in txn_dict and ( + txn_dict["gas"] == "auto" or isinstance(txn_dict["auto"], AutoGasLimit) + ): txn_dict.pop("gas") - # Also pop these, they are overriden by "auto" + # Also pop these, they are overridden by "auto" txn_dict.pop("maxFeePerGas", None) txn_dict.pop("maxPriorityFeePerGas", None) @@ -1309,7 +1312,13 @@ def prepare_transaction(self, txn: TransactionAPI) -> TransactionAPI: # else: Assume user specified the correct amount or txn will fail and waste gas if txn.gas_limit is None: - txn.gas_limit = self.estimate_gas_cost(txn) + multiplier = self.network.auto_gas_multiplier + if multiplier != 1.0: + gas = min(int(self.estimate_gas_cost(txn) * multiplier), self.max_gas) + else: + gas = self.estimate_gas_cost(txn) + + txn.gas_limit = gas if txn.required_confirmations is None: txn.required_confirmations = self.network.required_confirmations diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index 1a024d1aea..94e764598c 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -21,6 +21,7 @@ from ape.logging import logger from ape.types import ( AddressType, + AutoGasLimit, ContractLogContainer, SourceTraceback, TraceFrame, @@ -68,7 +69,7 @@ def validate_gas_limit(cls, value): value = cls.network_manager.active_provider.network.gas_limit - if value == "auto": + if value == "auto" or isinstance(value, AutoGasLimit): return None # Delegate to `ProviderAPI.estimate_gas_cost` elif value == "max": diff --git a/src/ape/types/__init__.py b/src/ape/types/__init__.py index e184ed1230..32b95fa21c 100644 --- a/src/ape/types/__init__.py +++ b/src/ape/types/__init__.py @@ -61,7 +61,25 @@ """ -GasLimit = Union[Literal["auto", "max"], int, str] +class AutoGasLimit(BaseModel): + """ + Additional settings for ``gas_limit: auto``. + """ + + multiplier: float = 1.0 + """ + A multiplier to estimated gas. + """ + + @validator("multiplier", pre=True) + def validate_multiplier(cls, value): + if isinstance(value, str): + return float(value) + + return value + + +GasLimit = Union[Literal["auto", "max"], int, str, AutoGasLimit] """ A value you can give to Ape for handling gas-limit calculations. ``"auto"`` refers to automatically figuring out the gas, diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index c0a7926bc7..f1279fe9a5 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -32,6 +32,7 @@ from ape.logging import logger from ape.types import ( AddressType, + AutoGasLimit, CallTreeNode, ContractLog, GasLimit, @@ -101,7 +102,10 @@ class Config: @validator("gas_limit", pre=True, allow_reuse=True) def validate_gas_limit(cls, value): - if value in ("auto", "max"): + if isinstance(value, dict) and "auto" in value: + return AutoGasLimit.parse_obj(value["auto"]) + + elif value in ("auto", "max") or isinstance(value, AutoGasLimit): return value elif isinstance(value, int): @@ -110,7 +114,7 @@ def validate_gas_limit(cls, value): elif isinstance(value, str) and value.isnumeric(): return int(value) - elif is_hex(value) and is_0x_prefixed(value): + elif isinstance(value, str) and is_hex(value) and is_0x_prefixed(value): return to_int(HexBytes(value)) elif is_hex(value): diff --git a/tests/functional/test_accounts.py b/tests/functional/test_accounts.py index 0402e468af..36a9317c66 100644 --- a/tests/functional/test_accounts.py +++ b/tests/functional/test_accounts.py @@ -7,6 +7,7 @@ import ape from ape.api import ImpersonatedAccount from ape.exceptions import AccountsError, NetworkError, ProjectError, SignatureError +from ape.types import AutoGasLimit from ape.types.signatures import recover_signer from ape.utils.testing import DEFAULT_NUMBER_OF_TEST_ACCOUNTS from ape_ethereum.ecosystem import ProxyType @@ -499,3 +500,40 @@ def test_iter_test_accounts(test_accounts): def test_declare(contract_container, sender): receipt = sender.declare(contract_container) assert not receipt.failed + + +@pytest.mark.parametrize( + "tx_type,params", [(0, ["gas_price"]), (2, ["max_fee", "max_priority_fee"])] +) +def test_prepare_transaction(sender, ethereum, tx_type, params): + # Create a test tx and estimate gas. + tx0 = ethereum.create_transaction(type=tx_type, gas="auto") + tx1 = ethereum.create_transaction(type=tx_type, gas=AutoGasLimit(multiplier=1.1)) + + tx0_gas = None + for tx in (tx0, tx1): + # Show tx doesn't have these by default. + assert tx.nonce is None + for param in params: + # Custom fields depending on type. + assert getattr(tx, param) is None + + # Gas should NOT yet be estimated, as that happens closer to sending. + assert tx.gas_limit is None + + # Sets fields. + tx = sender.prepare_transaction(tx) + + # We expect these fields to have been set. + assert tx.nonce is not None + assert tx.gas_limit is not None # Gas was estimated (using eth_estimateGas). + + if tx0_gas is None: + # Set tx0 gas for tx1 check. + tx0_gas = tx.gas_limit + else: + # Check that that multiplier causes higher gas limit + assert tx.gas_limit > tx0_gas + + for param in params: + assert getattr(tx, param) is not None