Skip to content

Commit

Permalink
add timeout to smart_contract_transact functions (#1402)
Browse files Browse the repository at this point in the history
  • Loading branch information
wakamex authored Apr 10, 2024
1 parent 495d0b0 commit 2d5eb14
Showing 1 changed file with 80 additions and 40 deletions.
120 changes: 80 additions & 40 deletions src/agent0/ethpy/base/transactions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Web3 powered functions for interfacing with smart contracts"""
"""Web3 powered functions for interfacing with smart contracts."""

from __future__ import annotations

Expand All @@ -23,21 +23,28 @@
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."
)


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"


Expand All @@ -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
---------
Expand Down Expand Up @@ -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
---------
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
---------
Expand Down Expand Up @@ -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
-------
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
-------
Expand All @@ -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
Expand All @@ -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
---------
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand Down

0 comments on commit 2d5eb14

Please sign in to comment.