diff --git a/autotx/AutoTx.py b/autotx/AutoTx.py index b5b8382..26a558c 100644 --- a/autotx/AutoTx.py +++ b/autotx/AutoTx.py @@ -209,7 +209,7 @@ async def try_run(self, prompt: str, non_interactive: bool, summary_method: str is_goal_supported = chat.chat_history[-1]["content"] != "Goal not supported: TERMINATE" try: - result = self.wallet.on_intents_ready(self.intents) + result = await self.wallet.on_intents_ready(self.intents) if isinstance(result, str): intents_info ="\n".join( diff --git a/autotx/agents/SwapTokensAgent.py b/autotx/agents/SwapTokensAgent.py index 1cd4cdb..c6b2259 100644 --- a/autotx/agents/SwapTokensAgent.py +++ b/autotx/agents/SwapTokensAgent.py @@ -7,7 +7,7 @@ from autotx.intents import BuyIntent, Intent, SellIntent from autotx.token import Token from autotx.eth_address import ETHAddress -from autotx.utils.ethereum.lifi.swap import SUPPORTED_NETWORKS_BY_LIFI, can_build_swap_transaction +from autotx.utils.ethereum.lifi.swap import SUPPORTED_NETWORKS_BY_LIFI, a_can_build_swap_transaction from autotx.utils.ethereum.networks import NetworkInfo from gnosis.eth import EthereumNetworkNotSupported as ChainIdNotSupported @@ -81,7 +81,7 @@ def get_tokens_address(token_in: str, token_out: str, network_info: NetworkInfo) class InvalidInput(Exception): pass -def swap(autotx: AutoTx, token_to_sell: str, token_to_buy: str) -> Intent: +async def swap(autotx: AutoTx, token_to_sell: str, token_to_buy: str) -> Intent: sell_parts = token_to_sell.split(" ") buy_parts = token_to_buy.split(" ") @@ -118,7 +118,7 @@ def swap(autotx: AutoTx, token_to_sell: str, token_to_buy: str) -> Intent: token_in, token_out, autotx.network ) - can_build_swap_transaction( + await a_can_build_swap_transaction( autotx.web3, Decimal(exact_amount), ETHAddress(token_in_address), @@ -152,7 +152,7 @@ class BulkSwapTool(AutoTxTool): ) def build_tool(self, autotx: AutoTx) -> Callable[[str], str]: - def run( + async def run( tokens: Annotated[ str, """ @@ -171,7 +171,7 @@ def run( for swap_str in swaps: (token_to_sell, token_to_buy) = swap_str.strip().split(" to ") try: - all_intents.append(swap(autotx, token_to_sell, token_to_buy)) + all_intents.append(await swap(autotx, token_to_sell, token_to_buy)) except InvalidInput as e: all_errors.append(e) except Exception as e: diff --git a/autotx/intents.py b/autotx/intents.py index 69ee59a..4949e5f 100644 --- a/autotx/intents.py +++ b/autotx/intents.py @@ -13,7 +13,7 @@ from autotx.utils.ethereum.build_transfer_native import build_transfer_native from autotx.utils.ethereum.constants import NATIVE_TOKEN_ADDRESS from autotx.eth_address import ETHAddress -from autotx.utils.ethereum.lifi.swap import build_swap_transaction +from autotx.utils.ethereum.lifi.swap import a_build_swap_transaction from autotx.utils.ethereum.networks import NetworkInfo from autotx.utils.format_amount import format_amount @@ -27,7 +27,7 @@ class IntentBase(BaseModel): summary: str @abstractmethod - def build_transactions(self, web3: Web3, network: NetworkInfo, smart_wallet_address: ETHAddress) -> list[Transaction]: + async def build_transactions(self, web3: Web3, network: NetworkInfo, smart_wallet_address: ETHAddress) -> list[Transaction]: pass class SendIntent(IntentBase): @@ -45,7 +45,7 @@ def create(cls, token: Token, amount: float, receiver: ETHAddress) -> 'SendInten summary=f"Transfer {format_amount(amount)} {token.symbol} to {receiver}", ) - def build_transactions(self, web3: Web3, network: NetworkInfo, smart_wallet_address: ETHAddress) -> list[Transaction]: + async def build_transactions(self, web3: Web3, network: NetworkInfo, smart_wallet_address: ETHAddress) -> list[Transaction]: tx: TxParams if self.token.address == NATIVE_TOKEN_ADDRESS: @@ -79,8 +79,8 @@ def create(cls, from_token: Token, to_token: Token, amount: float) -> 'BuyIntent summary=f"Buy {format_amount(amount)} {to_token.symbol} with {from_token.symbol}", ) - def build_transactions(self, web3: Web3, network: NetworkInfo, smart_wallet_address: ETHAddress) -> list[Transaction]: - transactions = build_swap_transaction( + async def build_transactions(self, web3: Web3, network: NetworkInfo, smart_wallet_address: ETHAddress) -> list[Transaction]: + transactions = await a_build_swap_transaction( web3, Decimal(str(self.amount)), ETHAddress(self.from_token.address), @@ -107,9 +107,9 @@ def create(cls, from_token: Token, to_token: Token, amount: float) -> 'SellInten summary=f"Sell {format_amount(amount)} {from_token.symbol} for {to_token.symbol}", ) - def build_transactions(self, web3: Web3, network: NetworkInfo, smart_wallet_address: ETHAddress) -> list[Transaction]: + async def build_transactions(self, web3: Web3, network: NetworkInfo, smart_wallet_address: ETHAddress) -> list[Transaction]: - transactions = build_swap_transaction( + transactions = await a_build_swap_transaction( web3, Decimal(str(self.amount)), ETHAddress(self.from_token.address), diff --git a/autotx/server.py b/autotx/server.py index c0f10ab..a5f4f0c 100644 --- a/autotx/server.py +++ b/autotx/server.py @@ -39,7 +39,7 @@ def __init__(self, self.max_rounds = max_rounds self.is_dev = is_dev -autotx_params: AutoTxParams = AutoTxParams(verbose=False, logs=None, cache=False, max_rounds=None, is_dev=False) +autotx_params: AutoTxParams = AutoTxParams(verbose=False, logs=None, cache=False, max_rounds=200, is_dev=False) app_router = APIRouter() @@ -82,7 +82,7 @@ def authorize_app_and_user(authorization: str | None, user_id: str) -> tuple[mod return (app, app_user) -def build_transactions(app_id: str, user_id: str, chain_id: int, address: str, task: models.Task) -> List[Transaction]: +async def build_transactions(app_id: str, user_id: str, chain_id: int, address: str, task: models.Task) -> List[Transaction]: if task.running: raise HTTPException(status_code=400, detail="Task is still running") @@ -94,7 +94,7 @@ def build_transactions(app_id: str, user_id: str, chain_id: int, address: str, t transactions: list[Transaction] = [] for intent in task.intents: - transactions.extend(intent.build_transactions(app_config.web3, app_config.network_info, app_config.manager.address)) + transactions.extend(await intent.build_transactions(app_config.web3, app_config.network_info, app_config.manager.address)) return transactions @@ -211,7 +211,7 @@ def get_intents(task_id: str, authorization: Annotated[str | None, Header()] = N return task.intents @app_router.get("/api/v1/tasks/{task_id}/transactions", response_model=List[Transaction]) -def get_transactions( +async def get_transactions( task_id: str, address: str, chain_id: int, @@ -227,7 +227,7 @@ def get_transactions( if task.chain_id != chain_id: raise HTTPException(status_code=400, detail="Chain ID does not match task") - transactions = build_transactions(app.id, user_id, chain_id, address, task) + transactions = await build_transactions(app.id, user_id, chain_id, address, task) return transactions @@ -236,7 +236,7 @@ class PreparedTransactionsDto(BaseModel): transactions: List[Transaction] @app_router.post("/api/v1/tasks/{task_id}/transactions/prepare", response_model=PreparedTransactionsDto) -def prepare_transactions( +async def prepare_transactions( task_id: str, address: str, chain_id: int, @@ -252,7 +252,7 @@ def prepare_transactions( if task.chain_id != chain_id: raise HTTPException(status_code=400, detail="Chain ID does not match task") - transactions = build_transactions(app.id, app_user.user_id, chain_id, address, task) + transactions = await build_transactions(app.id, app_user.user_id, chain_id, address, task) if len(transactions) == 0: raise HTTPException(status_code=400, detail="No transactions to send") diff --git a/autotx/utils/ethereum/lifi/swap.py b/autotx/utils/ethereum/lifi/swap.py index 8a2f163..7abb5c8 100644 --- a/autotx/utils/ethereum/lifi/swap.py +++ b/autotx/utils/ethereum/lifi/swap.py @@ -1,3 +1,4 @@ +import asyncio from dataclasses import dataclass from decimal import Decimal from typing import Any, cast @@ -103,6 +104,49 @@ def get_quote( quote["toolDetails"]["name"], ) +async def fetch_quote_with_retries( + token_in_address: ETHAddress, + token_in_decimals: int, + token_in_symbol: str, + token_out_address: ETHAddress, + token_out_decimals: int, + token_out_symbol: str, + chain: ChainId, + amount: Decimal, + is_exact_input: bool, + _from: ETHAddress, +) -> QuoteInformation: + retries = 0 + while True: + try: + quote = get_quote( + token_in_address, + token_in_decimals, + token_in_symbol, + token_out_address, + token_out_decimals, + token_out_symbol, + chain, + amount, + not is_exact_input, + _from, + ) + return quote + except Exception as e: + if "The from amount must be greater than zero." in str(e): + if is_exact_input: + raise Exception(f"The specified amount of {token_in_symbol} is too low") + else: + raise Exception(f"The specified amount of {token_out_symbol} is too low") + + elif "No available quotes for the requested transfer" in str(e): + if retries < 5: + retries += 1 + await asyncio.sleep(1) + continue + raise e + + def build_swap_transaction( web3: Web3, amount: Decimal, @@ -111,6 +155,17 @@ def build_swap_transaction( _from: ETHAddress, is_exact_input: bool, chain: ChainId, +) -> list[Transaction]: + return asyncio.run(a_build_swap_transaction(web3, amount, token_in_address, token_out_address, _from, is_exact_input, chain)) + +async def a_build_swap_transaction( + web3: Web3, + amount: Decimal, + token_in_address: ETHAddress, + token_out_address: ETHAddress, + _from: ETHAddress, + is_exact_input: bool, + chain: ChainId, ) -> list[Transaction]: native_token_symbol = get_native_token_symbol(chain) token_in_is_native = token_in_address.hex == NATIVE_TOKEN_ADDRESS @@ -139,7 +194,8 @@ def build_swap_transaction( if token_out_is_native else token_out.functions.symbol().call() ) - quote = get_quote( + + quote = await fetch_quote_with_retries( token_in_address, token_in_decimals, token_in_symbol, @@ -148,10 +204,9 @@ def build_swap_transaction( token_out_symbol, chain, amount, - not is_exact_input, + is_exact_input, _from, ) - transactions: list[Transaction] = [] if not token_in_is_native: approval_address = quote.approval_address @@ -194,6 +249,17 @@ def can_build_swap_transaction( _from: ETHAddress, is_exact_input: bool, chain: ChainId, +) -> bool: + return asyncio.run(a_can_build_swap_transaction(web3, amount, token_in_address, token_out_address, _from, is_exact_input, chain)) + +async def a_can_build_swap_transaction( + web3: Web3, + amount: Decimal, + token_in_address: ETHAddress, + token_out_address: ETHAddress, + _from: ETHAddress, + is_exact_input: bool, + chain: ChainId, ) -> bool: native_token_symbol = get_native_token_symbol(chain) token_in_is_native = token_in_address.hex == NATIVE_TOKEN_ADDRESS @@ -222,7 +288,8 @@ def can_build_swap_transaction( if token_out_is_native else token_out.functions.symbol().call() ) - quote = get_quote( + + quote = await fetch_quote_with_retries( token_in_address, token_in_decimals, token_in_symbol, @@ -231,10 +298,9 @@ def can_build_swap_transaction( token_out_symbol, chain, amount, - not is_exact_input, + is_exact_input, _from, ) - if not token_in_is_native: approval_address = quote.approval_address allowance = token_in.functions.allowance(_from.hex, approval_address).call() diff --git a/autotx/wallets/api_smart_wallet.py b/autotx/wallets/api_smart_wallet.py index e2f0e40..ee2c4b5 100644 --- a/autotx/wallets/api_smart_wallet.py +++ b/autotx/wallets/api_smart_wallet.py @@ -26,5 +26,5 @@ def on_intents_prepared(self, intents: list[Intent]) -> None: saved_task.intents.extend(intents) self.tasks.update(saved_task) - def on_intents_ready(self, _intents: list[Intent]) -> bool | str: + async def on_intents_ready(self, _intents: list[Intent]) -> bool | str: return True \ No newline at end of file diff --git a/autotx/wallets/safe_smart_wallet.py b/autotx/wallets/safe_smart_wallet.py index c3db603..d472a35 100644 --- a/autotx/wallets/safe_smart_wallet.py +++ b/autotx/wallets/safe_smart_wallet.py @@ -17,10 +17,10 @@ def __init__(self, manager: SafeManager, auto_submit_tx: bool): def on_intents_prepared(self, intents: list[Intent]) -> None: pass - def on_intents_ready(self, intents: list[Intent]) -> bool | str: + async def on_intents_ready(self, intents: list[Intent]) -> bool | str: transactions: list[TransactionBase] = [] for intent in intents: - transactions.extend(intent.build_transactions(self.manager.web3, self.manager.network, self.manager.address)) + transactions.extend(await intent.build_transactions(self.manager.web3, self.manager.network, self.manager.address)) return self.manager.send_multisend_tx_batch(transactions, not self.auto_submit_tx) diff --git a/autotx/wallets/smart_wallet.py b/autotx/wallets/smart_wallet.py index cc54425..dadcb6d 100644 --- a/autotx/wallets/smart_wallet.py +++ b/autotx/wallets/smart_wallet.py @@ -16,7 +16,7 @@ def on_intents_prepared(self, intents: list[Intent]) -> None: pass @abstractmethod - def on_intents_ready(self, intents: list[Intent]) -> bool | str: # True if sent, False if declined, str if feedback + async def on_intents_ready(self, intents: list[Intent]) -> bool | str: # True if sent, False if declined, str if feedback pass def balance_of(self, token_address: ETHAddress | None = None) -> float: