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

Better nonce handling #1609

Merged
merged 9 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions scripts/checkpoint_bots.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,16 +230,13 @@ def run_checkpoint_bot(
# 0 is the max iterations for distribute excess idle, where it will default to
# the default max iterations
fn_args = (checkpoint_time, 0)
# Call the thread safe get nonce function for this transaction's nonce
nonce = async_get_nonce(web3, sender)

# Try preview call
_ = smart_contract_preview_transaction(
hyperdrive_contract,
sender.address,
"checkpoint",
*fn_args,
nonce=nonce,
)

receipt = smart_contract_transact(
Expand All @@ -248,7 +245,7 @@ def run_checkpoint_bot(
sender,
"checkpoint",
*fn_args,
nonce=nonce,
nonce_func=partial(async_get_nonce, web3, sender),
)
# Reset fail count on successful transaction
fail_count = 0
Expand Down
60 changes: 24 additions & 36 deletions src/agent0/core/hyperdrive/interactive/exec/execute_agent_trades.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,8 @@
)
from agent0.core.hyperdrive.policies import HyperdriveBasePolicy
from agent0.core.test_utils import assert_never
from agent0.ethpy.base.transactions import DEFAULT_READ_RETRY_COUNT
from agent0.ethpy.hyperdrive import HyperdriveReadInterface, HyperdriveReadWriteInterface
from agent0.ethpy.hyperdrive.event_types import BaseHyperdriveEvent
from agent0.utils import retry_call


def get_liquidation_trades(
Expand Down Expand Up @@ -129,7 +127,7 @@ async def async_execute_agent_trades(
wallet_func: Callable[[], HyperdriveWallet],
policy: HyperdriveBasePolicy | None,
preview_before_trade: bool,
base_nonce: Nonce | None = None,
nonce_func: Callable[[], Nonce],
) -> list[TradeResult]:
"""Executes a single agent's trade based on its policy.
This function is async as `_match_contract_call_to_trade` waits for a transaction receipt.
Expand Down Expand Up @@ -157,8 +155,10 @@ async def async_execute_agent_trades(
The policy being executed.
preview_before_trade: bool
Whether or not to preview the trade before it is executed.
base_nonce: Nonce, optional
The base nonce to use for this set of trades.
nonce_func: Callable[[], Nonce] | None
A callable function to use to get a nonce. This function is useful for e.g.,
passing in a safe nonce getter tied to an agent.
Defaults to setting it to the result of `get_transaction_count`.
slundqui marked this conversation as resolved.
Show resolved Hide resolved

Returns
-------
Expand All @@ -167,15 +167,6 @@ async def async_execute_agent_trades(
TradeResult handles any information about the trade, as well as any trade errors.
"""

# Make trades async for this agent. This way, an agent can submit multiple trades for a single block
# To do this, we need to manually set the nonce, so we get the base transaction count here
# and pass in an incrementing nonce per call
# TODO figure out which exception here to retry on
if base_nonce is None:
base_nonce = retry_call(
DEFAULT_READ_RETRY_COUNT, None, interface.web3.eth.get_transaction_count, account.address, "pending"
)

# Here, gather returns results based on original order of trades due to nonce getting explicitly set based
# on iterating through the list

Expand All @@ -191,7 +182,7 @@ async def async_execute_agent_trades(
account,
interface,
trade_object,
nonce=Nonce(base_nonce + i),
nonce_func=nonce_func,
preview_before_trade=preview_before_trade,
)
for i, trade_object in enumerate(trades)
Expand Down Expand Up @@ -225,8 +216,8 @@ async def async_execute_single_trade(
trade_object: Trade[HyperdriveMarketAction],
execute_policy_post_action: bool,
preview_before_trade: bool,
nonce_func: Callable[[], Nonce],
policy: HyperdriveBasePolicy | None = None,
nonce: Nonce | None = None,
) -> TradeResult:
"""Executes a single trade made by the agent.

Expand Down Expand Up @@ -255,28 +246,23 @@ async def async_execute_single_trade(
policy: HyperdriveBasePolicy | None, optional
The policy attached to the agent. This is only used to potentially call `post_action`
of the policy.
nonce: Nonce | None, optional
The nonce for the trade.
nonce_func: Callable[[], Nonce] | None
A callable function to use to get a nonce. This function is useful for e.g.,
passing in a safe nonce getter tied to an agent.
Defaults to setting it to the result of `get_transaction_count`.
slundqui marked this conversation as resolved.
Show resolved Hide resolved

Returns
-------
TradeResult
The result of the trade.
"""

# TODO we likely need to bookkeep nonces here to avoid a race condition when this function
# is being called asynchronously. We use a lock for the time being as a stopgap.
if nonce is None:
nonce = retry_call(
DEFAULT_READ_RETRY_COUNT, None, interface.web3.eth.get_transaction_count, account.address, "pending"
)

try:
receipt_or_exception = await _async_match_contract_call_to_trade(
account,
interface,
trade_object,
nonce,
nonce_func,
preview_before_trade,
)
except Exception as e: # pylint: disable=broad-except
Expand Down Expand Up @@ -372,7 +358,7 @@ async def _async_match_contract_call_to_trade(
account: LocalAccount,
interface: HyperdriveReadWriteInterface,
trade_envelope: Trade[HyperdriveMarketAction],
nonce: Nonce,
nonce_func: Callable[[], Nonce],
preview_before_trade: bool,
) -> BaseHyperdriveEvent:
"""Match statement that executes the smart contract trade based on the provided type.
Expand All @@ -385,8 +371,10 @@ async def _async_match_contract_call_to_trade(
The Hyperdrive API interface object.
trade_envelope: Trade[HyperdriveMarketAction]
A specific Hyperdrive trade requested by the given agent.
nonce: Nonce, optional
Override the transaction number assigned to the transaction call from the agent wallet.
nonce_func: Callable[[], Nonce] | None
A callable function to use to get a nonce. This function is useful for e.g.,
passing in a safe nonce getter tied to an agent.
Defaults to setting it to the result of `get_transaction_count`.
slundqui marked this conversation as resolved.
Show resolved Hide resolved
preview_before_trade: bool
Whether or not to preview the trade before it is executed.

Expand All @@ -406,7 +394,7 @@ async def _async_match_contract_call_to_trade(
trade.trade_amount,
slippage_tolerance=trade.slippage_tolerance,
gas_limit=trade.gas_limit,
nonce=nonce,
nonce_func=nonce_func,
preview_before_trade=preview_before_trade,
)

Expand All @@ -419,7 +407,7 @@ async def _async_match_contract_call_to_trade(
trade.maturity_time,
slippage_tolerance=trade.slippage_tolerance,
gas_limit=trade.gas_limit,
nonce=nonce,
nonce_func=nonce_func,
preview_before_trade=preview_before_trade,
)

Expand All @@ -429,7 +417,7 @@ async def _async_match_contract_call_to_trade(
trade.trade_amount,
slippage_tolerance=trade.slippage_tolerance,
gas_limit=trade.gas_limit,
nonce=nonce,
nonce_func=nonce_func,
preview_before_trade=preview_before_trade,
)

Expand All @@ -442,7 +430,7 @@ async def _async_match_contract_call_to_trade(
trade.maturity_time,
slippage_tolerance=trade.slippage_tolerance,
gas_limit=trade.gas_limit,
nonce=nonce,
nonce_func=nonce_func,
preview_before_trade=preview_before_trade,
)

Expand All @@ -458,7 +446,7 @@ async def _async_match_contract_call_to_trade(
trade.max_apr,
slippage_tolerance=None,
gas_limit=trade.gas_limit,
nonce=nonce,
nonce_func=nonce_func,
preview_before_trade=preview_before_trade,
)

Expand All @@ -467,7 +455,7 @@ async def _async_match_contract_call_to_trade(
account,
trade.trade_amount,
gas_limit=trade.gas_limit,
nonce=nonce,
nonce_func=nonce_func,
preview_before_trade=preview_before_trade,
)

Expand All @@ -476,7 +464,7 @@ async def _async_match_contract_call_to_trade(
account,
trade.trade_amount,
gas_limit=trade.gas_limit,
nonce=nonce,
nonce_func=nonce_func,
preview_before_trade=preview_before_trade,
)

Expand Down
39 changes: 20 additions & 19 deletions src/agent0/core/hyperdrive/interactive/hyperdrive_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ def __init__(
self.current_nonce = 0

def _get_nonce_safe(self) -> Nonce:
slundqui marked this conversation as resolved.
Show resolved Hide resolved
# This function handles getting nonces in a thread-safe manner.
# We pass in the callable function to underlying ethpy calls so that
# we get nonce when we sign the transaction.
with self.nonce_lock:
# Since we're handling nonces here, we assume this wallet isn't making other trades
# so we always use the latest block
Expand All @@ -163,7 +166,8 @@ def _get_nonce_safe(self) -> Nonce:
else:
out_nonce = self.current_nonce
self.current_nonce += 1
return Nonce(out_nonce)

return Nonce(out_nonce)

def _reset_nonce(self) -> None:
with self.nonce_lock:
Expand Down Expand Up @@ -324,8 +328,8 @@ def open_long(self, base: FixedPoint, pool: Hyperdrive | None = None) -> OpenLon
trade_object,
self.chain.config.always_execute_policy_post_action,
self.chain.config.preview_before_trade,
self._active_policy,
nonce=self._get_nonce_safe(),
nonce_func=self._get_nonce_safe,
policy=self._active_policy,
)
)
try:
Expand Down Expand Up @@ -372,8 +376,8 @@ def close_long(self, maturity_time: int, bonds: FixedPoint, pool: Hyperdrive | N
trade_object,
self.chain.config.always_execute_policy_post_action,
self.chain.config.preview_before_trade,
self._active_policy,
nonce=self._get_nonce_safe(),
nonce_func=self._get_nonce_safe,
policy=self._active_policy,
)
)
try:
Expand Down Expand Up @@ -417,8 +421,8 @@ def open_short(self, bonds: FixedPoint, pool: Hyperdrive | None = None) -> OpenS
trade_object,
self.chain.config.always_execute_policy_post_action,
self.chain.config.preview_before_trade,
self._active_policy,
nonce=self._get_nonce_safe(),
nonce_func=self._get_nonce_safe,
policy=self._active_policy,
)
)
try:
Expand Down Expand Up @@ -463,8 +467,8 @@ def close_short(self, maturity_time: int, bonds: FixedPoint, pool: Hyperdrive |
trade_object,
self.chain.config.always_execute_policy_post_action,
self.chain.config.preview_before_trade,
self._active_policy,
nonce=self._get_nonce_safe(),
nonce_func=self._get_nonce_safe,
policy=self._active_policy,
)
)
try:
Expand Down Expand Up @@ -507,8 +511,8 @@ def add_liquidity(self, base: FixedPoint, pool: Hyperdrive | None = None) -> Add
trade_object,
self.chain.config.always_execute_policy_post_action,
self.chain.config.preview_before_trade,
self._active_policy,
nonce=self._get_nonce_safe(),
nonce_func=self._get_nonce_safe,
policy=self._active_policy,
)
)
try:
Expand Down Expand Up @@ -551,8 +555,8 @@ def remove_liquidity(self, shares: FixedPoint, pool: Hyperdrive | None = None) -
trade_object,
self.chain.config.always_execute_policy_post_action,
self.chain.config.preview_before_trade,
self._active_policy,
nonce=self._get_nonce_safe(),
nonce_func=self._get_nonce_safe,
policy=self._active_policy,
)
)
try:
Expand Down Expand Up @@ -595,8 +599,8 @@ def redeem_withdrawal_share(self, shares: FixedPoint, pool: Hyperdrive | None =
trade_object,
self.chain.config.always_execute_policy_post_action,
self.chain.config.preview_before_trade,
self._active_policy,
nonce=self._get_nonce_safe(),
nonce_func=self._get_nonce_safe,
policy=self._active_policy,
)
)
try:
Expand Down Expand Up @@ -708,7 +712,7 @@ def execute_action(
# We pass in policy here for `post_action`. Post action is ignored if policy not set.
policy=self._active_policy,
preview_before_trade=self.chain.config.preview_before_trade,
base_nonce=self._get_nonce_safe(),
nonce_func=self._get_nonce_safe,
)
)
out_events = []
Expand All @@ -717,9 +721,6 @@ def execute_action(
hyperdrive_event = self._handle_trade_result(trade_result, pool, always_throw_exception=False)
if hyperdrive_event is not None:
out_events.append(hyperdrive_event)
else:
# We always reset nonce on failure to avoid skipped nonces
self._reset_nonce()
return out_events

def execute_policy_action(self, pool: Hyperdrive | None = None) -> list[BaseHyperdriveEvent]:
Expand Down
2 changes: 0 additions & 2 deletions src/agent0/ethpy/base/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@
from .receipts import get_event_object, get_transaction_logs
from .rpc_interface import get_account_balance, set_anvil_account_balance
from .transactions import (
async_eth_transfer,
async_smart_contract_transact,
async_wait_for_transaction_receipt,
eth_transfer,
fetch_contract_transactions_for_block,
smart_contract_preview_transaction,
smart_contract_read,
Expand Down
Loading
Loading