diff --git a/src/dexorder/base/account.py b/src/dexorder/base/account.py index 4bc4b32..02a4b5a 100644 --- a/src/dexorder/base/account.py +++ b/src/dexorder/base/account.py @@ -42,10 +42,12 @@ class Account (LocalAccount): # log.debug(f'available accounts: {Account._pool.qsize()}') try: async with asyncio.timeout(1): - result = await Account._pool.get() + result: "Account" = await Account._pool.get() except asyncio.TimeoutError: log.error('waiting for an available account') result = await Account._pool.get() + # mark as out of pool + result._in_pool = False metric.account_available.set(Account._pool.qsize()) return result @@ -59,17 +61,20 @@ class Account (LocalAccount): if Account._main_account is None: Account._main_account = account Account._pool.put_nowait(account) + account._in_pool = True # this account is now in the pool Account._all.append(account) metric.account_available.set(Account._pool.qsize()) metric.account_total.set(len(Account._all)) log.info(f'Account pool {[a.address for a in Account._all]}') - def __init__(self, local_account: LocalAccount): # todo chain_id? - super().__init__(local_account._key_obj, local_account._publicapi) # from digging into the source code + def __init__(self, local_account: LocalAccount): # todo chain_id? + super().__init__(local_account._key_obj, local_account._publicapi) # from digging into the source code self.chain_id = current_chain.get().id self.signing_middleware = construct_sign_and_send_raw_middleware(self) self._nonce: Optional[int] = None self.tx_id: Optional[str] = None # current transaction id + # release() idempotency tracking + self._in_pool: bool = False async def next_nonce(self): if self._nonce is None: @@ -86,8 +91,21 @@ class Account (LocalAccount): return current_w3.get().eth.get_balance(self.address) def release(self): - metric.account_available.set(Account._pool.qsize() + 1) + """ + Return this Account to the pool. + + Idempotent: calling release() multiple times without a new acquire() + will only enqueue the account once. + """ + # If we're already in the pool, do nothing. + if self._in_pool: + # Optional debug log; comment out if too noisy. + # log.debug(f'Account {self.address} already in pool; ignoring extra release()') + return + Account._pool.put_nowait(self) + self._in_pool = True + metric.account_available.set(Account._pool.qsize()) def __str__(self): return self.address diff --git a/src/dexorder/contract/contract_proxy.py b/src/dexorder/contract/contract_proxy.py index ceed163..1082c63 100644 --- a/src/dexorder/contract/contract_proxy.py +++ b/src/dexorder/contract/contract_proxy.py @@ -33,7 +33,8 @@ class ContractTransaction: async def wait(self) -> TxReceipt: if self.receipt is None: self.receipt = await current_w3.get().eth.wait_for_transaction_receipt(self.id) - self.account.release() + if self.account is not None: + self.account.release() return self.receipt async def sign(self, account: Account): @@ -153,10 +154,14 @@ class ContractProxy: def __getattr__(self, item): if item == 'constructor': found = self.contract.constructor - elif item in self.contract.functions: - found = self.contract.functions[item] else: - raise AttributeError(item) + funcs = self.contract.functions + # In web3.py v6+, contract functions are exposed as attributes, not via __getitem__. + # Using getattr ensures we obtain the callable factory for the function; indexing may return None. + # Additionally, guard against unexpected None to fail fast with a clear error. + found = getattr(funcs, item, None) + if not callable(found): + raise AttributeError(f"Function '{item}' not found on contract {self._interface_name} at {self.address}") return self._wrapper(self.address, item, found) def __repr__(self): diff --git a/src/dexorder/order/executionhandler.py b/src/dexorder/order/executionhandler.py index 1ad8e4c..f5d7586 100644 --- a/src/dexorder/order/executionhandler.py +++ b/src/dexorder/order/executionhandler.py @@ -3,7 +3,6 @@ from dataclasses import dataclass from typing import Optional, Union, Any from uuid import UUID -from triton.profiler import deactivate from web3.exceptions import ContractPanicError, ContractLogicError from web3.types import EventData