From 2d5eb14963c477fcdf75de1b01a5d107cc297b6c Mon Sep 17 00:00:00 2001 From: Mihai Date: Wed, 10 Apr 2024 13:20:04 -0400 Subject: [PATCH] add timeout to smart_contract_transact functions (#1402) --- src/agent0/ethpy/base/transactions.py | 120 +++++++++++++++++--------- 1 file changed, 80 insertions(+), 40 deletions(-) diff --git a/src/agent0/ethpy/base/transactions.py b/src/agent0/ethpy/base/transactions.py index 82cf7b471e..32a75dfcce 100644 --- a/src/agent0/ethpy/base/transactions.py +++ b/src/agent0/ethpy/base/transactions.py @@ -1,4 +1,4 @@ -"""Web3 powered functions for interfacing with smart contracts""" +"""Web3 powered functions for interfacing with smart contracts.""" from __future__ import annotations @@ -23,13 +23,20 @@ DEFAULT_WRITE_RETRY_COUNT = 1 # pylint: disable=too-many-lines +# we have lots of parameters in smart_contract_transact and async_smart_contract_transact +# too many branches in smart_contract_preview_transaction +# ruff: noqa: PLR0912 +# too many arguments in function async_smart_contract_transact +# ruff: noqa: PLR0913 +# too many return statements in _contract_function_abi_outputs +# ruff: noqa: PLR0911 # We define the function to check the exception to retry on # for preview calls. # This is the error we get when preview fails due to anvil def _retry_preview_check(exc: Exception) -> bool: - """The exception to retry on for preview calls""" + """Check the exception to retry on for preview calls.""" return ( isinstance(exc, ContractPanicError) and exc.args[0] == "Panic error 0x11: Arithmetic operation results in underflow or overflow." @@ -37,7 +44,7 @@ def _retry_preview_check(exc: Exception) -> bool: def _retry_txn_check(exc: Exception) -> bool: - """The exception to retry on for transaction calls""" + """Check the exception to retry on for transaction calls.""" return isinstance(exc, UnknownBlockError) and exc.args[0] == "Receipt has status of 0" @@ -49,7 +56,7 @@ def smart_contract_read( read_retry_count: int | None = None, **fn_kwargs, ) -> dict[str, Any]: - """Return from a smart contract read call + """Return from a smart contract read call. Arguments --------- @@ -142,7 +149,7 @@ def smart_contract_preview_transaction( txn_options_value: int | None = None, **fn_kwargs, ) -> dict[str, Any]: - """Returns the values from a transaction without actually submitting the transaction. + """Return the values from a transaction without actually submitting the transaction. Arguments --------- @@ -263,32 +270,34 @@ def smart_contract_preview_transaction( def wait_for_transaction_receipt( web3: Web3, transaction_hash: HexBytes, - timeout: float = 30, + timeout: float | None = None, start_latency: float = 0.01, backoff_multiplier: float = 2, ) -> TxReceipt: - """Retrieves the transaction receipt, retrying with exponential backoff. + """Retrieve the transaction receipt, retrying with exponential backoff. This function is copied from `web3.eth.wait_for_transaction_receipt`, but using exponential backoff. Arguments --------- web3: Web3 - web3 provider object + web3 provider object. transaction_hash: HexBytes - The hash of the transaction - timeout: float - The amount of time in seconds to time out the connection + The hash of the transaction. + timeout: float | None, optional + The amount of time in seconds to time out the connection. Default is 30. start_latency: float - The starting amount of time in seconds to wait between polls + The starting amount of time in seconds to wait between polls. backoff_multiplier: float - The backoff factor for the exponential backoff + The backoff factor for the exponential backoff. Returns ------- TxReceipt The transaction receipt """ + if timeout is None: + timeout = 30.0 try: with Timeout(timeout) as _timeout: poll_latency = start_latency @@ -315,32 +324,34 @@ def wait_for_transaction_receipt( async def async_wait_for_transaction_receipt( web3: Web3, transaction_hash: HexBytes, - timeout: float = 30, + timeout: float | None = None, start_latency: float = 0.01, backoff_multiplier: float = 2, ) -> TxReceipt: - """Retrieves the transaction receipt asynchronously, retrying with exponential backoff. + """Retrieve the transaction receipt asynchronously, retrying with exponential backoff. This function is copied from `web3.eth.wait_for_transaction_receipt`, but using exponential backoff and async await. Arguments --------- web3: Web3 - web3 provider object + web3 provider object. transaction_hash: HexBytes - The hash of the transaction - timeout: float - The amount of time in seconds to time out the connection + The hash of the transaction. + timeout: float | None, optional + The amount of time in seconds to time out the connection. Default is 30. start_latency: float - The starting amount of time in seconds to wait between polls + The starting amount of time in seconds to wait between polls. backoff_multiplier: float - The backoff factor for the exponential backoff + The backoff factor for the exponential backoff. Returns ------- TxReceipt The transaction receipt """ + if timeout is None: + timeout = 30.0 try: with Timeout(timeout) as _timeout: poll_latency = start_latency @@ -372,7 +383,7 @@ def build_transaction( read_retry_count: int | None = None, txn_options_value: int | None = None, ) -> TxParams: - """Builds a transaction for the given function. + """Build a transaction for the given function. Arguments --------- @@ -432,18 +443,21 @@ def build_transaction( async def _async_send_transaction_and_wait_for_receipt( - unsent_txn: TxParams, signer: LocalAccount, web3: Web3 + unsent_txn: TxParams, signer: LocalAccount, web3: Web3, timeout: float | None = None ) -> TxReceipt: - """Sends a transaction and waits for the receipt asynchronously. + """Send a transaction and waits for the receipt asynchronously. Arguments --------- unsent_txn: TxParams - The built transaction ready to be sent + The built transaction ready to be sent. signer: LocalAccount - The LocalAccount that will be used to pay for the gas & sign the transaction + The LocalAccount that will be used to pay for the gas & sign the transaction. + timeout: float | None, optional + The number of seconds to wait for the transaction to be mined. + Default is defined in `async_wait_for_transaction_receipt`. web3: Web3 - web3 provider object + web3 provider object. Returns ------- @@ -452,7 +466,7 @@ async def _async_send_transaction_and_wait_for_receipt( """ signed_txn = signer.sign_transaction(unsent_txn) tx_hash = web3.eth.send_raw_transaction(signed_txn.rawTransaction) - tx_receipt = await async_wait_for_transaction_receipt(web3, tx_hash) + tx_receipt = await async_wait_for_transaction_receipt(web3, tx_hash, timeout=timeout) # Error checking when transaction doesn't throw an error, but instead # has errors in the tx_receipt @@ -480,9 +494,11 @@ async def async_smart_contract_transact( read_retry_count: int | None = None, write_retry_count: int | None = None, txn_options_value: int | None = None, + timeout: float | None = None, **fn_kwargs, ) -> TxReceipt: - """Execute a named function on a contract that requires a signature & gas + """Execute a named function on a contract that requires a signature & gas. + Copy of `smart_contract_transact`, but using async wait for `wait_for_transaction_receipt` Arguments @@ -505,6 +521,9 @@ async def async_smart_contract_transact( The number of times to retry the transact call if it fails. Defaults to no retries. txn_options_value: int | None The value field to set for transaction options. + timeout: float | None, optional + The number of seconds to wait for the transaction to be mined. + Default is defined in `_async_send_transaction_and_wait_for_receipt`. **fn_kwargs: Unknown The keyword arguments passed to the contract method. @@ -536,7 +555,13 @@ async def async_smart_contract_transact( txn_options_value=txn_options_value, ) return await retry_call( - write_retry_count, _retry_txn_check, _async_send_transaction_and_wait_for_receipt, unsent_txn, signer, web3 + write_retry_count, + _retry_txn_check, + _async_send_transaction_and_wait_for_receipt, + unsent_txn, + signer, + web3, + timeout=timeout, ) # Wraps the exception with a contract call exception, adding additional information @@ -612,17 +637,22 @@ async def async_smart_contract_transact( ) from err -def send_transaction_and_wait_for_receipt(unsent_txn: TxParams, signer: LocalAccount, web3: Web3) -> TxReceipt: - """Sends a transaction and waits for the receipt. +def send_transaction_and_wait_for_receipt( + unsent_txn: TxParams, signer: LocalAccount, web3: Web3, timeout: float | None = None +) -> TxReceipt: + """Send a transaction and waits for the receipt. Arguments --------- unsent_txn: TxParams - The built transaction ready to be sent + The built transaction ready to be sent. signer: LocalAccount - The LocalAccount that will be used to pay for the gas & sign the transaction + The LocalAccount that will be used to pay for the gas & sign the transaction. + timeout: float | None, optional + The number of seconds to wait for the transaction to be mined. + Default is defined in `wait_for_transaction_receipt`. web3: Web3 - web3 provider object + web3 provider object. Returns ------- @@ -631,7 +661,7 @@ def send_transaction_and_wait_for_receipt(unsent_txn: TxParams, signer: LocalAcc """ signed_txn = signer.sign_transaction(unsent_txn) tx_hash = web3.eth.send_raw_transaction(signed_txn.rawTransaction) - tx_receipt = wait_for_transaction_receipt(web3, tx_hash) + tx_receipt = wait_for_transaction_receipt(web3, tx_hash, timeout=timeout) # Error checking when transaction doesn't throw an error, but instead # has errors in the tx_receipt @@ -657,9 +687,10 @@ def smart_contract_transact( read_retry_count: int | None = None, write_retry_count: int | None = None, txn_options_value: int | None = None, + timeout: float | None = None, **fn_kwargs, ) -> TxReceipt: - """Execute a named function on a contract that requires a signature & gas + """Execute a named function on a contract that requires a signature & gas. Arguments --------- @@ -681,6 +712,9 @@ def smart_contract_transact( The number of times to retry the transact call if it fails. Defaults to no retries. txn_options_value: int | None The value field to set for transaction options. + timeout: float | None, optional + The number of seconds to wait for the transaction to be mined. + Default is defined in `send_transaction_and_wait_for_receipt`. **fn_kwargs: Unknown The keyword arguments passed to the contract method. @@ -712,7 +746,13 @@ def smart_contract_transact( txn_options_value=txn_options_value, ) return retry_call( - write_retry_count, _retry_txn_check, send_transaction_and_wait_for_receipt, unsent_txn, signer, web3 + write_retry_count, + _retry_txn_check, + send_transaction_and_wait_for_receipt, + unsent_txn, + signer, + web3, + timeout=timeout, ) # Wraps the exception with a contract call exception, adding additional information @@ -956,7 +996,7 @@ def fetch_contract_transactions_for_block( def _get_name_and_type_from_abi(abi_outputs: ABIFunctionComponents | ABIFunctionParams) -> tuple[str, str]: - """Retrieve and narrow the types for abi outputs""" + """Retrieve and narrow the types for abi outputs.""" return_value_name: str | None = abi_outputs.get("name") if return_value_name is None: return_value_name = "none" @@ -970,7 +1010,7 @@ def _get_name_and_type_from_abi(abi_outputs: ABIFunctionComponents | ABIFunction def _contract_function_abi_outputs(contract_abi: ABI, function_name: str) -> list[tuple[str, str]] | None: # TODO clean this function up # pylint: disable=too-many-return-statements - """Parse the function abi to get the name and type for each output""" + """Parse the function abi to get the name and type for each output.""" function_abi = None # find the first function matching the function_name for abi in contract_abi: # loop over each entry in the abi list