Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ethpy transaction hotfixes #1079

Merged
merged 9 commits into from
Nov 9, 2023
8 changes: 5 additions & 3 deletions lib/chainsync/chainsync/test_fixtures/db_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@

import docker
import pytest
from chainsync import PostgresConfig
from chainsync.db.base import Base, initialize_engine
from docker.errors import DockerException
from pytest_postgresql.janitor import DatabaseJanitor
from sqlalchemy import Engine
from sqlalchemy.orm import Session, sessionmaker

from chainsync import PostgresConfig
from chainsync.db.base import Base, initialize_engine


@pytest.fixture(scope="session")
def psql_docker() -> Iterator[PostgresConfig]:
Expand Down Expand Up @@ -77,7 +76,10 @@ def psql_docker() -> Iterator[PostgresConfig]:
yield postgres_config

# Docker doesn't play nice with types
# Remove the container along with volume
container.kill() # type:ignore
# Prune volumes
client.volumes.prune()


@pytest.fixture(scope="session")
Expand Down
83 changes: 64 additions & 19 deletions lib/ethpy/ethpy/base/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,6 @@ def smart_contract_preview_transaction(
else:
function = contract.get_function_by_name(function_name_or_signature)(*fn_args, **fn_kwargs)

# We build the raw transaction here in case of error. Note that we don't call `build_transaction`
# since it adds the nonce to the transaction, and we ignore nonce in preview
raw_txn = function.build_transaction({"from": signer_address})

# We define the function to check the exception to retry on
# This is the error we get when preview fails due to anvil
def retry_preview_check(exc: Exception) -> bool:
Expand All @@ -162,7 +158,13 @@ def retry_preview_check(exc: Exception) -> bool:
# This is the additional transaction argument passed into function.call
# that may contain additional call arguments such as max_gas, nonce, etc.
transaction_kwargs = {"from": signer_address}
raw_txn = {}
try:
# We build the raw transaction here in case of error. Note that we don't call `build_transaction`
# since it adds the nonce to the transaction, and we ignore nonce in preview
# Build transactions can fail, so we put this here in the try/catch
raw_txn = function.build_transaction({"from": signer_address})

return_values = retry_call(
READ_RETRY_COUNT,
retry_preview_check,
Expand All @@ -182,7 +184,7 @@ def retry_preview_check(exc: Exception) -> bool:
function_name_or_signature=function_name_or_signature,
fn_args=fn_args,
fn_kwargs=fn_kwargs,
raw_txn=raw_txn,
raw_txn=dict(raw_txn),
block_number=block_number,
) from err
except Exception as err:
Expand All @@ -193,7 +195,7 @@ def retry_preview_check(exc: Exception) -> bool:
function_name_or_signature=function_name_or_signature,
fn_args=fn_args,
fn_kwargs=fn_kwargs,
raw_txn=raw_txn,
raw_txn=dict(raw_txn),
block_number=block_number,
) from err

Expand All @@ -219,6 +221,53 @@ def retry_preview_check(exc: Exception) -> bool:
return {f"value{idx}": value for idx, value in enumerate(return_values)}


def wait_for_transaction_receipt(
web3: Web3, transaction_hash: HexBytes, timeout: float = 30, start_latency: float = 1, backoff: float = 2
) -> TxReceipt:
"""wait_for_transaction_receipt with exponential backoff
slundqui marked this conversation as resolved.
Show resolved Hide resolved
This function is copied from `web3.eth.wait_for_transaction_receipt`, but using exp backoff.

Arguments
---------
web3: Web3
web3 provider object
transaction_hash: HexBytes
The hash of the transaction
timeout: float
The amount of time in seconds to time out the connection
start_latency: float
The starting amount of time in seconds to wait between polls
backoff: float
The backoff factor for the exponential backoff

Returns
-------
TxReceipt
The transaction receipt
"""
try:
with Timeout(timeout) as _timeout:
poll_latency = start_latency + random.uniform(0, 1)
while True:
try:
tx_receipt = web3.eth.get_transaction_receipt(transaction_hash)
except TransactionNotFound:
tx_receipt = None
if tx_receipt is not None:
break
_timeout.sleep(poll_latency)
# Exp backoff
poll_latency *= backoff
# Add random latency to avoid collisions
poll_latency += random.uniform(0, 1)
return tx_receipt

except Timeout as exc:
raise TimeExhausted(
f"Transaction {HexBytes(transaction_hash) !r} is not in the chain " f"after {timeout} seconds"
) from exc


async def async_wait_for_transaction_receipt(
web3: Web3, transaction_hash: HexBytes, timeout: float = 30, start_latency: float = 1, backoff: float = 2
) -> TxReceipt:
Expand All @@ -234,8 +283,10 @@ async def async_wait_for_transaction_receipt(
The hash of the transaction
timeout: float
The amount of time in seconds to time out the connection
poll_latency: float
The amount of time in seconds to wait between polls
start_latency: float
The starting amount of time in seconds to wait between polls
backoff: float
The backoff factor for the exponential backoff

Returns
-------
Expand Down Expand Up @@ -336,11 +387,6 @@ async def _async_send_transaction_and_wait_for_receipt(
raise UnknownBlockError("Receipt did not return status", f"{block_number=}")
if status == 0:
raise UnknownBlockError("Receipt has status of 0", f"{block_number=}", f"{tx_receipt=}")
logs = tx_receipt.get("logs", None)
if logs is None:
raise UnknownBlockError("Receipt did not return logs", f"{block_number=}")
if len(logs) == 0:
raise UnknownBlockError("Logs have a length of 0", f"{block_number=}", f"{tx_receipt=}")
return tx_receipt


Expand Down Expand Up @@ -455,7 +501,7 @@ def _send_transaction_and_wait_for_receipt(unsent_txn: TxParams, signer: LocalAc
"""
signed_txn = signer.sign_transaction(unsent_txn)
tx_hash = web3.eth.send_raw_transaction(signed_txn.rawTransaction)
tx_receipt = web3.eth.wait_for_transaction_receipt(tx_hash)
tx_receipt = wait_for_transaction_receipt(web3, tx_hash)

# Error checking when transaction doesn't throw an error, but instead
# has errors in the tx_receipt
Expand All @@ -468,11 +514,6 @@ def _send_transaction_and_wait_for_receipt(unsent_txn: TxParams, signer: LocalAc
raise UnknownBlockError("Receipt did not return status", f"{block_number=}")
if status == 0:
raise UnknownBlockError("Receipt has status of 0", f"{block_number=}", f"{tx_receipt=}")
logs = tx_receipt.get("logs", None)
if logs is None:
raise UnknownBlockError("Receipt did not return logs", f"{block_number=}")
if len(logs) == 0:
raise UnknownBlockError("Logs have a length of 0", f"{block_number=}", f"{tx_receipt=}")
return tx_receipt


Expand Down Expand Up @@ -732,6 +773,8 @@ def _get_name_and_type_from_abi(abi_outputs: ABIFunctionComponents | ABIFunction

# TODO: add ability to parse function_signature as well
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"""
function_abi = None
# find the first function matching the function_name
Expand All @@ -754,6 +797,8 @@ def _contract_function_abi_outputs(contract_abi: ABI, function_name: str) -> lis
for output in function_outputs:
return_names_and_types.append(_get_name_and_type_from_abi(output))
return return_names_and_types
if len(function_outputs) == 0: # No function arguments returned from preview
return None
if (
function_outputs[0].get("type") == "tuple" and function_outputs[0].get("components") is not None
): # multiple named outputs were returned in a struct
Expand Down
9 changes: 8 additions & 1 deletion lib/ethpy/ethpy/hyperdrive/api/_contract_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,13 +443,20 @@ async def _async_redeem_withdraw_shares(
# before calling smart contract transact
# Since current_pool_state.block_number is a property, we want to get the static block here
current_block = cls.current_pool_state.block_number
_ = smart_contract_preview_transaction(
preview_result = smart_contract_preview_transaction(
cls.hyperdrive_contract,
agent_checksum_address,
"redeemWithdrawalShares",
*fn_args,
block_number=current_block,
)

# Here, a preview call of redeem withdrawal shares will still be successful without logs if
# the amount of shares to redeem is larger than what's in the wallet. We want to catch this error
# here with a useful error message, so we check that explicitly here
if preview_result["sharesRedeemed"] == 0 and trade_amount > 0:
raise ValueError("Preview call for redeem withdrawal shares returned 0 for non-zero input trade amount")

try:
tx_receipt = await async_smart_contract_transact(
cls.web3,
Expand Down
Loading
Loading