diff --git a/Biconomy.md b/Biconomy.md new file mode 100644 index 0000000..0bc9f85 --- /dev/null +++ b/Biconomy.md @@ -0,0 +1,23 @@ +# Biconomy Smart Accounts + +## Description +AutoTx can be used with a Biconomy Smart Account. + +## Getting started +First, add the private key of the account you want to use as the owner to the `.env` file. Use the variable `SMART_ACCOUNT_OWNER_PK`. +Since the private key is stored as plain text, it is recommended to use a test account. + +Next, start the `smart_account_api`, run: `poetry run start-smart-account-api`. This API server will be used to interact with the Biconomy Smart Account. +You can run `poetry run stop-smart-account-api` to stop the server. + +To start AutoTx, you can now run: `poetry run ask "my-prompt-here"`. + +When running for the first time, you will be prompted to send funds to your new Biconomy Smart Account in order to pay for the gas fees. +``` +Using Biconomy smart account: {SMART-ACCOUNT-ADDRESS} +Detected empty account balance. +To use your new smart account, please top it up with some native currency. +Send the funds to: {SMART-ACCOUNT-ADDRESS} on {NETWORK-NAME} +Waiting... +``` +After sending the funds, AutoTx will automatically detect the balance and continue with the transaction. \ No newline at end of file diff --git a/README.md b/README.md index 8b63362..40169dd 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,8 @@ python benchmarks.py ./autotx/tests/file_name.py::function_name 5 # run a specific test with 5 iterations and name the output folder (instead of the default timestamp) python benchmarks.py ./autotx/tests/file_name.py::function_name 5 output_folder_name ``` +# Biconomy Smart Accounts +To view the Biconomy Smart Accounts documentation, please see the [Biconomy.md](./Biconomy.md) file. # API Server To view the API server documentation, please see the [API.md](./API.md) file. diff --git a/autotx/AutoTx.py b/autotx/AutoTx.py index 29b196f..48d9790 100644 --- a/autotx/AutoTx.py +++ b/autotx/AutoTx.py @@ -19,7 +19,7 @@ from autotx.utils.logging.Logger import Logger from autotx.utils.ethereum.networks import NetworkInfo from autotx.utils.constants import OPENAI_BASE_URL, OPENAI_MODEL_NAME -from autotx.wallets.smart_wallet import SmartWallet +from autotx.smart_accounts.smart_account import SmartAccount @dataclass(kw_only=True) class CustomModel: @@ -75,7 +75,7 @@ class RunResult: class AutoTx: web3: Web3 - wallet: SmartWallet + wallet: SmartAccount logger: Logger intents: list[Intent] network: NetworkInfo @@ -94,7 +94,7 @@ class AutoTx: def __init__( self, web3: Web3, - wallet: SmartWallet, + wallet: SmartAccount, network: NetworkInfo, agents: list[AutoTxAgent], config: Config, diff --git a/autotx/agents/SwapTokensAgent.py b/autotx/agents/SwapTokensAgent.py index 895f541..012e222 100644 --- a/autotx/agents/SwapTokensAgent.py +++ b/autotx/agents/SwapTokensAgent.py @@ -171,7 +171,8 @@ async def run( for swap_str in swaps: (token_to_sell, token_to_buy) = swap_str.strip().split(" to ") try: - all_intents.append(await swap(autotx, token_to_sell, token_to_buy)) + intent = await swap(autotx, token_to_sell, token_to_buy) + all_intents.append(intent) except InvalidInput as e: all_errors.append(e) except Exception as e: diff --git a/autotx/cli.py b/autotx/cli.py index 02881c0..c15c2c6 100644 --- a/autotx/cli.py +++ b/autotx/cli.py @@ -1,17 +1,29 @@ from dotenv import load_dotenv load_dotenv() +from eth_account import Account +import time +from web3 import Web3 import uuid import uvicorn from typing import cast import click - -from autotx.wallets.safe_smart_wallet import SafeSmartWallet +import uuid +from eth_account.signers.local import LocalAccount + +from autotx.eth_address import ETHAddress +from autotx.utils.ethereum.get_native_balance import get_native_balance +from autotx.utils.ethereum.networks import NetworkInfo +from autotx.utils.constants import SMART_ACCOUNT_OWNER_PK +from autotx.smart_accounts.safe_smart_account import SafeSmartAccount +from autotx.smart_accounts.smart_account import SmartAccount from autotx.utils.configuration import AppConfig from autotx.utils.is_dev_env import is_dev_env from autotx.setup import print_agent_address, setup_agents from autotx.AutoTx import AutoTx, Config from autotx.utils.ethereum.helpers.show_address_balances import show_address_balances +from autotx.smart_accounts.smart_account import SmartAccount +from autotx.smart_accounts.local_biconomy_smart_account import LocalBiconomySmartAccount def print_autotx_info() -> None: print(""" @@ -32,6 +44,15 @@ def print_autotx_info() -> None: def main() -> None: pass +def wait_for_native_top_up(web3: Web3, address: ETHAddress) -> None: + network = NetworkInfo(web3.eth.chain_id) + + print(f"Detected empty account balance.\nTo use your new smart account, please top it up with some native currency.\nSend the funds to: {address} on {network.chain_id.name}") + print("Waiting...") + while get_native_balance(web3, address) == 0: + time.sleep(2) + print(f"Account balance detected ({get_native_balance(web3, address)}). Ready to use.") + @main.command() @click.argument('prompt', required=False) @click.option("-n", "--non-interactive", is_flag=True, help="Non-interactive mode (will not expect further user input or approval)") @@ -45,8 +66,17 @@ def run(prompt: str | None, non_interactive: bool, verbose: bool, logs: str | No if prompt == None: prompt = click.prompt("What do you want to do?") - app_config = AppConfig.load(fill_dev_account=True) - wallet = SafeSmartWallet(app_config.manager, auto_submit_tx=non_interactive) + app_config = AppConfig() + wallet: SmartAccount + if SMART_ACCOUNT_OWNER_PK: + smart_account_owner = cast(LocalAccount, Account.from_key(SMART_ACCOUNT_OWNER_PK)) + wallet = LocalBiconomySmartAccount(app_config.web3, smart_account_owner, auto_submit_tx=non_interactive) + print(f"Using Biconomy smart account: {wallet.address}") + if get_native_balance(app_config.web3, wallet.address) == 0: + wait_for_native_top_up(app_config.web3, wallet.address) + else: + wallet = SafeSmartAccount(app_config.rpc_url, app_config.network_info, auto_submit_tx=non_interactive, fill_dev_account=True) + print(f"Using Safe smart account: {wallet.address}") (get_llm_config, agents, logs_dir) = setup_agents(logs, cache) diff --git a/autotx/load_tokens.py b/autotx/load_tokens.py index f3ecec4..f020a53 100644 --- a/autotx/load_tokens.py +++ b/autotx/load_tokens.py @@ -1,8 +1,7 @@ import json from textwrap import dedent from typing import Union - -import requests +import aiohttp KLEROS_TOKENS_LIST = "https://t2crtokens.eth.link/" COINGECKO_TOKENS_LISTS = [ @@ -19,14 +18,16 @@ TOKENS_LIST = [KLEROS_TOKENS_LIST, *COINGECKO_TOKENS_LISTS] -def fetch_tokens_list() -> None: +async def fetch_tokens_list() -> None: loaded_tokens: list[dict[str, Union[str, int]]] = [] for token_list_url in TOKENS_LIST: try: - response = requests.get(token_list_url) - tokens = json.loads(response.text)["tokens"] - loaded_tokens.extend(tokens) + async with aiohttp.ClientSession() as session: + response = await session.get(token_list_url) + result = await response.json() + tokens = result["tokens"] + loaded_tokens.extend(tokens) except: print("Error while trying to fetch list:", token_list_url) @@ -41,5 +42,5 @@ def fetch_tokens_list() -> None: f.write(content) -def run() -> None: - fetch_tokens_list() +async def run() -> None: + await fetch_tokens_list() diff --git a/autotx/server.py b/autotx/server.py index e62b29d..2dbf5d2 100644 --- a/autotx/server.py +++ b/autotx/server.py @@ -1,3 +1,4 @@ +from datetime import datetime import json from typing import Annotated, Any, Dict, List from eth_account import Account @@ -12,12 +13,13 @@ from autotx import models, setup, task_logs from autotx import db from autotx.intents import Intent +from autotx.smart_accounts.smart_account import SmartAccount from autotx.transactions import Transaction from autotx.utils.configuration import AppConfig from autotx.utils.ethereum.chain_short_names import CHAIN_ID_TO_SHORT_NAME from autotx.utils.ethereum.networks import SUPPORTED_NETWORKS_CONFIGURATION_MAP -from autotx.wallets.api_smart_wallet import ApiSmartWallet -from autotx.wallets.smart_wallet import SmartWallet +from autotx.smart_accounts.api_smart_account import ApiSmartAccount +from autotx.smart_accounts.safe_smart_account import SafeSmartAccount class AutoTxParams: verbose: bool @@ -61,7 +63,7 @@ def authorize(authorization: str | None) -> models.App: return app -def load_config_for_user(app_id: str, user_id: str, address: str, chain_id: int) -> AppConfig: +def load_wallet_for_user(app_config: AppConfig, app_id: str, user_id: str, address: str) -> SmartAccount: agent_private_key = db.get_agent_private_key(app_id, user_id) if not agent_private_key: @@ -69,9 +71,9 @@ def load_config_for_user(app_id: str, user_id: str, address: str, chain_id: int) agent = Account.from_key(agent_private_key) - app_config = AppConfig.load(smart_account_addr=address, subsidized_chain_id=chain_id, agent=agent) + wallet = SafeSmartAccount(app_config.rpc_url, app_config.network_info, auto_submit_tx=False, smart_account_addr=address, agent=agent) - return app_config + return wallet def authorize_app_and_user(authorization: str | None, user_id: str) -> tuple[models.App, models.AppUser]: app = authorize(authorization) @@ -86,7 +88,8 @@ async def build_transactions(app_id: str, user_id: str, chain_id: int, address: if task.running: raise HTTPException(status_code=400, detail="Task is still running") - app_config = load_config_for_user(app_id, user_id, address, chain_id) + app_config = AppConfig(subsidized_chain_id=chain_id) + wallet = load_wallet_for_user(app_config, app_id, user_id, address) if task.intents is None or len(task.intents) == 0: return [] @@ -94,7 +97,7 @@ async def build_transactions(app_id: str, user_id: str, chain_id: int, address: transactions: list[Transaction] = [] for intent in task.intents: - transactions.extend(await 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, wallet.address)) return transactions @@ -108,6 +111,19 @@ def stop_task_for_error(tasks: db.TasksRepository, task_id: str, error: str, use task.messages.append(user_error_message) tasks.update(task) +def log(log_type: str, obj: Any, task_id: str, tasks: db.TasksRepository) -> None: + add_task_log(models.TaskLog(type=log_type, obj=json.dumps(obj), created_at=datetime.now()), task_id, tasks) + +def add_task_log(log: models.TaskLog, task_id: str, tasks: db.TasksRepository) -> None: + task = tasks.get(task_id) + if task is None: + raise Exception("Task not found: " + task_id) + + if task.logs is None: + task.logs = [] + task.logs.append(log) + tasks.update(task) + @app_router.post("/api/v1/tasks", response_model=models.Task) async def create_task(task: models.TaskCreate, background_tasks: BackgroundTasks, authorization: Annotated[str | None, Header()] = None) -> models.Task: from autotx.AutoTx import AutoTx, Config as AutoTxConfig @@ -125,17 +141,14 @@ async def create_task(task: models.TaskCreate, background_tasks: BackgroundTasks prompt = task.prompt - app_config = AppConfig.load(smart_account_addr=task.address, subsidized_chain_id=task.chain_id) + app_config = AppConfig(subsidized_chain_id=task.chain_id) - wallet: SmartWallet - if autotx_params.is_dev: - wallet = ApiSmartWallet(app_config.web3, app_config.manager, tasks) - else: - wallet = ApiSmartWallet(app_config.web3, app_config.manager, tasks) + wallet = SafeSmartAccount(app_config.rpc_url, app_config.network_info, smart_account_addr=task.address) + api_wallet = ApiSmartAccount(app_config.web3, wallet, tasks) - created_task: models.Task = tasks.start(prompt, wallet.address.hex, app_config.network_info.chain_id.value, app_user.id) + created_task: models.Task = tasks.start(prompt, api_wallet.address.hex, app_config.network_info.chain_id.value, app_user.id) task_id = created_task.id - wallet.task_id = task_id + api_wallet.task_id = task_id try: (get_llm_config, agents, logs_dir) = setup.setup_agents(autotx_params.logs, cache=autotx_params.cache) @@ -149,20 +162,15 @@ def on_notify_user(message: str) -> None: tasks.update(task) def on_agent_message(from_agent: str, to_agent: str, message: Any) -> None: - task = tasks.get(task_id) - if task is None: - raise Exception("Task not found: " + task_id) - - if task.logs is None: - task.logs = [] - task.logs.append( - task_logs.build_agent_message_log(from_agent, to_agent, message) + add_task_log( + task_logs.build_agent_message_log(from_agent, to_agent, message), + task_id, + tasks ) - tasks.update(task) autotx = AutoTx( app_config.web3, - wallet, + api_wallet, app_config.network_info, agents, AutoTxConfig( @@ -177,13 +185,16 @@ def on_agent_message(from_agent: str, to_agent: str, message: Any) -> None: async def run_task() -> None: try: + log("execution", "run-start", task_id, tasks) await autotx.a_run(prompt, non_interactive=True) + log("execution", "run-end", task_id, tasks) except Exception as e: error = traceback.format_exc() db.add_task_error(f"AutoTx run", app.id, app_user.id, task_id, error) stop_task_for_error(tasks, task_id, error, f"An error caused AutoTx to stop ({task_id})") raise e tasks.stop(task_id) + log("execution", "task-stop", task_id, tasks) background_tasks.add_task(run_task) @@ -295,7 +306,7 @@ async def prepare_transactions( return PreparedTransactionsDto(batch_id=submitted_batch_id, transactions=transactions) @app_router.post("/api/v1/tasks/{task_id}/transactions") -def send_transactions( +async def send_transactions( task_id: str, address: str, chain_id: int, @@ -330,12 +341,10 @@ def send_transactions( return f"https://app.safe.global/transactions/queue?safe={CHAIN_ID_TO_SHORT_NAME[str(chain_id)]}:{address}" try: - app_config = load_config_for_user(app.id, user_id, address, chain_id) + app_config = AppConfig(subsidized_chain_id=chain_id) + wallet = load_wallet_for_user(app_config, app.id, user_id, address) - app_config.manager.send_multisend_tx_batch( - transactions, - require_approval=False, - ) + await wallet.send_transactions(transactions) except SafeAPIException as e: if "is not an owner or delegate" in str(e): raise HTTPException(status_code=400, detail="Agent is not an owner or delegate") @@ -346,6 +355,8 @@ def send_transactions( except Exception as e: db.add_task_error(f"Route: send_transactions", app.id, app_user.id, task_id, traceback.format_exc()) raise e + + db.submit_transactions(app.id, app_user.id, batch_id) return f"https://app.safe.global/transactions/queue?safe={CHAIN_ID_TO_SHORT_NAME[str(chain_id)]}:{address}" @@ -366,17 +377,25 @@ def get_task_logs(task_id: str) -> list[models.TaskLog]: @app_router.get("/api/v1/tasks/{task_id}/logs/{log_type}", response_class=HTMLResponse) def get_task_logs_formatted(task_id: str, log_type: str) -> str: - if log_type != "agent-message": + if log_type != "agent-message" and log_type != "execution": raise HTTPException(status_code=400, detail="Log type not supported") logs = db.get_task_logs(task_id) if logs is None: raise HTTPException(status_code=404, detail="Task not found") + + filtered_logs = [log for log in logs if log.type == log_type] + + if len(filtered_logs) == 0: + return "
No logs found" - agent_logs = [task_logs.format_agent_message_log(json.loads(log.obj)) for log in logs if log.type == "agent-message"] + if log_type == "execution": + return "
" + "\n".join([log.created_at.strftime("%Y-%m-%d %H:%M:%S") + f": {json.loads(log.obj)}" for log in filtered_logs]) + "" + else: + agent_logs = [task_logs.format_agent_message_log(json.loads(log.obj)) for log in filtered_logs] - text = "\n\n".join(agent_logs) - return f"
{text}" + text = "\n\n".join(agent_logs) + return f"
{text}" @app_router.get("/api/v1/version", response_class=JSONResponse) async def get_version() -> Dict[str, str]: @@ -398,7 +417,9 @@ async def get_version() -> Dict[str, str]: def setup_server(verbose: bool, logs: str | None, max_rounds: int | None, cache: bool, is_dev: bool, check_valid_safe: bool) -> None: if is_dev: - AppConfig.load(check_valid_safe=check_valid_safe) # Loading the configuration deploys the dev wallet in dev mode + app_config = AppConfig() + # Loading the SafeSmartAccount will deploy a new Safe if one is not already deployed + SafeSmartAccount(app_config.rpc_url, app_config.network_info, fill_dev_account=True, check_valid_safe=check_valid_safe) global autotx_params autotx_params = AutoTxParams( diff --git a/autotx/setup.py b/autotx/setup.py index 5bd71af..8def330 100644 --- a/autotx/setup.py +++ b/autotx/setup.py @@ -10,9 +10,7 @@ from autotx.autotx_agent import AutoTxAgent from autotx.utils.constants import COINGECKO_API_KEY, OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL_NAME from autotx.utils.ethereum import SafeManager -from autotx.utils.ethereum.cached_safe_address import get_cached_safe_address from autotx.eth_address import ETHAddress -from autotx.utils.ethereum.helpers.fill_dev_account_with_tokens import fill_dev_account_with_tokens from autotx.utils.ethereum.helpers.get_dev_account import get_dev_account from autotx.utils.ethereum.helpers.show_address_balances import show_address_balances from autotx.utils.ethereum.networks import NetworkInfo @@ -23,7 +21,7 @@ def print_agent_address() -> None: acc = get_or_create_agent_account() print(f"Agent address: {acc.address}") -def setup_safe(smart_account_addr: ETHAddress | None, agent: LocalAccount, client: EthereumClient, fill_dev_account: bool, check_valid_safe: bool) -> SafeManager: +def setup_safe(smart_account_addr: ETHAddress | None, agent: LocalAccount, client: EthereumClient, check_valid_safe: bool) -> SafeManager: web3 = client.w3 chain_id = web3.eth.chain_id @@ -51,13 +49,8 @@ def setup_safe(smart_account_addr: ETHAddress | None, agent: LocalAccount, clien print("No smart account connected, deploying a new one...") dev_account = get_dev_account() - is_safe_deployed = get_cached_safe_address() manager = SafeManager.deploy_safe(client, dev_account, agent, [dev_account.address, agent.address], 1) print(f"Smart account deployed: {manager.address}") - - if not is_safe_deployed and fill_dev_account: - fill_dev_account_with_tokens(client, dev_account, manager.address, network_info) - print(f"Funds sent to smart account for testing purposes") print("=" * 50) print("Starting smart account balances:") diff --git a/autotx/smart_account_api.py b/autotx/smart_account_api.py new file mode 100644 index 0000000..3767b4f --- /dev/null +++ b/autotx/smart_account_api.py @@ -0,0 +1,33 @@ +import subprocess +import sys + +container_name = "smart_account_api" + +def start() -> None: + build = subprocess.run( + ["docker", "build", "-t", "smart_account_api", ".", "-f", "smart-account-api.Dockerfile"], capture_output=True + ) + + if build.returncode != 0: + sys.exit( + "Local node start up has failed. Make sure you have docker desktop installed and running" + ) + + subprocess.run( + [ + "docker", + "run", + "-d", + "--name", + container_name, + "-p", + "7080:7080", + "--env-file", + ".env", + "smart_account_api", + ], + check=True, + ) + +def stop() -> None: + subprocess.run(["docker", "container", "rm", container_name, "-f"], check=True) diff --git a/autotx/smart_accounts/api_smart_account.py b/autotx/smart_accounts/api_smart_account.py new file mode 100644 index 0000000..0f83920 --- /dev/null +++ b/autotx/smart_accounts/api_smart_account.py @@ -0,0 +1,37 @@ +from web3 import Web3 +from autotx import db +from autotx.intents import Intent +from autotx.smart_accounts.smart_account import SmartAccount +from autotx.transactions import TransactionBase + +# ApiSmartAccount is a wrapper around other SmartAccounts that allows for hooks to be added to the transaction process +class ApiSmartAccount(SmartAccount): + wallet: SmartAccount + tasks: db.TasksRepository + task_id: str | None + + def __init__(self, web3: Web3, wallet: SmartAccount, tasks: db.TasksRepository, task_id: str | None = None): + super().__init__(web3, wallet.address) + self.wallet = wallet + self.tasks = tasks + self.task_id = task_id + + def on_intents_prepared(self, intents: list[Intent]) -> None: + if self.task_id is None: + raise ValueError("Task ID is required") + + saved_task = self.tasks.get(self.task_id) + if saved_task is None: + raise ValueError("Task not found") + + saved_task.intents.extend(intents) + self.tasks.update(saved_task) + + async def on_intents_ready(self, _intents: list[Intent]) -> bool | str: + return True + + async def send_transaction(self, transaction: TransactionBase) -> None: + await self.wallet.send_transaction(transaction) + + async def send_transactions(self, transactions: list[TransactionBase]) -> None: + await self.wallet.send_transactions(transactions) \ No newline at end of file diff --git a/autotx/smart_accounts/local_biconomy_smart_account.py b/autotx/smart_accounts/local_biconomy_smart_account.py new file mode 100644 index 0000000..c5779c4 --- /dev/null +++ b/autotx/smart_accounts/local_biconomy_smart_account.py @@ -0,0 +1,81 @@ +import asyncio +import json +from typing import cast +import aiohttp +from eth_account.signers.local import LocalAccount +from web3 import Web3 + +from autotx.eth_address import ETHAddress +from autotx.intents import Intent +from autotx.transactions import TransactionBase +from autotx.smart_accounts.smart_account import SmartAccount +from autotx.utils.ethereum.networks import NetworkInfo + +class LocalBiconomySmartAccount(SmartAccount): + web3: Web3 + owner: LocalAccount + auto_submit_tx: bool + + def __init__(self, web3: Web3, owner: LocalAccount, auto_submit_tx: bool): + self.web3 = web3 + self.owner = owner + self.auto_submit_tx = auto_submit_tx + address = asyncio.run(self.get_address()) + super().__init__(web3, ETHAddress(address)) + + def on_intents_prepared(self, intents: list[Intent]) -> None: + pass + + async def on_intents_ready(self, intents: list[Intent]) -> bool | str: + if self.auto_submit_tx: + if not intents: + return False + + transactions: list[TransactionBase] = [] + + for intent in intents: + transactions.extend(await intent.build_transactions(self.web3, NetworkInfo(self.web3.eth.chain_id), self.address)) + + dict_transactions = [json.loads(transaction.json()) for transaction in transactions] + + await self.send_transactions(dict_transactions) + + return True + else: + return False + + async def get_address(self) -> str: + async with aiohttp.ClientSession() as session: + response = await session.get( + f"http://localhost:7080/api/v1/account/address?chainId={self.web3.eth.chain_id}", + headers={"Content-Type": "application/json"}, + ) + + if response.status != 200: + raise ValueError(f"Failed to get address: Biconomy API internal error") + + return cast(str, await response.json()) + + async def send_transaction(self, transaction: TransactionBase) -> None: + async with aiohttp.ClientSession() as session: + response = await session.post( + f"http://localhost:7080/api/v1/account/transactions?chainId={self.web3.eth.chain_id}", + headers={"Content-Type": "application/json"}, + data=json.dumps([transaction]), + ) + if response.status != 200: + raise ValueError(f"Transaction failed: {await response.json()}") + + print(f"Transaction sent: {await response.json()}") + + async def send_transactions(self, transactions: list[TransactionBase]) -> None: + async with aiohttp.ClientSession() as session: + response = await session.post( + f"http://localhost:7080/api/v1/account/transactions?chainId={self.web3.eth.chain_id}", + headers={"Content-Type": "application/json"}, + data=json.dumps(transactions) + ) + if response.status != 200: + raise ValueError(f"Transaction failed: Biconomy API internal error") + + print(f"Transaction sent: {await response.json()}") \ No newline at end of file diff --git a/autotx/smart_accounts/safe_smart_account.py b/autotx/smart_accounts/safe_smart_account.py new file mode 100644 index 0000000..b5aedac --- /dev/null +++ b/autotx/smart_accounts/safe_smart_account.py @@ -0,0 +1,75 @@ +import os +from eth_account.signers.local import LocalAccount +from eth_typing import URI +from eth_account.signers.local import LocalAccount +from gnosis.eth import EthereumClient + +from autotx.intents import Intent +from autotx.transactions import TransactionBase +from autotx.utils.ethereum import SafeManager +from autotx.smart_accounts.smart_account import SmartAccount +from autotx.setup import setup_safe +from autotx.utils.ethereum import SafeManager +from autotx.utils.ethereum.agent_account import get_or_create_agent_account +from autotx.utils.ethereum.cached_safe_address import get_cached_safe_address +from autotx.eth_address import ETHAddress +from autotx.utils.ethereum.helpers.fill_dev_account_with_tokens import fill_dev_account_with_tokens +from autotx.smart_accounts.smart_account import SmartAccount +from autotx.utils.ethereum.networks import NetworkInfo + +class SafeSmartAccount(SmartAccount): + agent: LocalAccount + manager: SafeManager + auto_submit_tx: bool + + def __init__( + self, + rpc_url: str, + network_info: NetworkInfo, + auto_submit_tx: bool = False, + smart_account_addr: str | None = None, + agent: LocalAccount | None = None, + check_valid_safe: bool = False, + fill_dev_account: bool = False, + ): + client = EthereumClient(URI(rpc_url)) + + agent = agent if agent else get_or_create_agent_account() + + smart_account_addr = smart_account_addr if smart_account_addr else os.getenv("SMART_ACCOUNT_ADDRESS") + smart_account = ETHAddress(smart_account_addr) if smart_account_addr else None + + is_safe_deployed = get_cached_safe_address() + + manager = setup_safe(smart_account, agent, client, check_valid_safe) + + if not is_safe_deployed and fill_dev_account: + fill_dev_account_with_tokens(client.w3, manager.address, network_info) + print(f"Funds sent to smart account for testing purposes") + + super().__init__(client.w3, manager.address) + + self.manager = manager + self.agent = agent + self.auto_submit_tx = auto_submit_tx + + def on_intents_prepared(self, intents: list[Intent]) -> None: + pass + + async def on_intents_ready(self, intents: list[Intent]) -> bool | str: + transactions: list[TransactionBase] = [] + + for intent in intents: + transactions.extend(await intent.build_transactions(self.web3, self.manager.network, self.address)) + + return self.manager.send_multisend_tx_batch(transactions, not self.auto_submit_tx) + + async def send_transaction(self, transaction: TransactionBase) -> None: + self.manager.send_multisend_tx_batch([transaction], require_approval=False) + + async def send_transactions(self, transactions: list[TransactionBase]) -> None: + self.manager.send_multisend_tx_batch( + transactions, + require_approval=False, + ) + diff --git a/autotx/smart_accounts/smart_account.py b/autotx/smart_accounts/smart_account.py new file mode 100644 index 0000000..646d0ac --- /dev/null +++ b/autotx/smart_accounts/smart_account.py @@ -0,0 +1,35 @@ +from abc import abstractmethod +from hexbytes import HexBytes +from web3 import Web3 +from web3.types import TxReceipt + +from autotx.intents import Intent +from autotx.transactions import TransactionBase +from autotx.eth_address import ETHAddress + + +class SmartAccount: + web3: Web3 + address: ETHAddress + + def __init__(self, web3: Web3, address: ETHAddress): + self.web3 = web3 + self.address = address + + def on_intents_prepared(self, intents: list[Intent]) -> None: + pass + + @abstractmethod + async def on_intents_ready(self, intents: list[Intent]) -> bool | str: # True if sent, False if declined, str if feedback + pass + + @abstractmethod + async def send_transaction(self, transaction: TransactionBase) -> None: + pass + + @abstractmethod + async def send_transactions(self, transactions: list[TransactionBase]) -> None: + pass + + def wait(self, tx_hash: HexBytes) -> TxReceipt: + return self.web3.eth.wait_for_transaction_receipt(tx_hash) \ No newline at end of file diff --git a/autotx/tests/agents/regression/token/test_tokens_regression.py b/autotx/tests/agents/regression/token/test_tokens_regression.py index d000b8e..ad61920 100644 --- a/autotx/tests/agents/regression/token/test_tokens_regression.py +++ b/autotx/tests/agents/regression/token/test_tokens_regression.py @@ -5,8 +5,7 @@ @pytest.mark.skip() -def test_auto_tx_send_erc20(configuration, auto_tx, usdc, test_accounts): - (_, _, client, _, _) = configuration +def test_auto_tx_send_erc20(smart_account, auto_tx, usdc, test_accounts): receiver = test_accounts[0] @@ -25,11 +24,11 @@ def test_auto_tx_send_erc20(configuration, auto_tx, usdc, test_accounts): ] for prompt in prompts: - balance = get_erc20_balance(client.w3, usdc, receiver) + balance = get_erc20_balance(smart_account.web3, usdc, receiver) auto_tx.run(prompt, non_interactive=True) - new_balance = get_erc20_balance(client.w3, usdc, receiver) + new_balance = get_erc20_balance(smart_account.web3, usdc, receiver) try: assert balance + 10 == new_balance @@ -39,10 +38,8 @@ def test_auto_tx_send_erc20(configuration, auto_tx, usdc, test_accounts): raise @pytest.mark.skip() -def test_auto_tx_swap(configuration, auto_tx): - (_, _, client, manager, _) = configuration - web3 = client.w3 - network_info = NetworkInfo(web3.eth.chain_id) +def test_auto_tx_swap(smart_account, auto_tx): + network_info = NetworkInfo(smart_account.web3.eth.chain_id) usdc_address = ETHAddress(network_info.tokens["usdc"]) prompts = [ @@ -60,11 +57,11 @@ def test_auto_tx_swap(configuration, auto_tx): ] for prompt in prompts: - balance = manager.balance_of(usdc_address) + balance = get_erc20_balance(smart_account.web3, usdc_address, smart_account.address) auto_tx.run(prompt, non_interactive=True) - new_balance = manager.balance_of(usdc_address) + new_balance = get_erc20_balance(smart_account.web3, usdc_address, smart_account.address) try: assert balance + 100 == new_balance @@ -74,8 +71,7 @@ def test_auto_tx_swap(configuration, auto_tx): raise @pytest.mark.skip() -def test_auto_tx_multiple_sends(configuration, auto_tx, usdc, test_accounts): - (_, _, client, _, _) = configuration +def test_auto_tx_multiple_sends(smart_account, auto_tx, usdc, test_accounts): receiver_one = test_accounts[0] receiver_two = test_accounts[1] @@ -95,13 +91,13 @@ def test_auto_tx_multiple_sends(configuration, auto_tx, usdc, test_accounts): ] for prompt in prompts: - balance_one = get_erc20_balance(client.w3, usdc, receiver_one) - balance_two = get_erc20_balance(client.w3, usdc, receiver_two) + balance_one = get_erc20_balance(smart_account.web3, usdc, receiver_one) + balance_two = get_erc20_balance(smart_account.web3, usdc, receiver_two) auto_tx.run(prompt, non_interactive=True) - new_balance_one = get_erc20_balance(client.w3, usdc, receiver_one) - new_balance_two = get_erc20_balance(client.w3, usdc, receiver_two) + new_balance_one = get_erc20_balance(smart_account.web3, usdc, receiver_one) + new_balance_two = get_erc20_balance(smart_account.web3, usdc, receiver_two) try: assert balance_one + 10 == new_balance_one @@ -113,10 +109,8 @@ def test_auto_tx_multiple_sends(configuration, auto_tx, usdc, test_accounts): raise @pytest.mark.skip() -def test_auto_tx_swap_and_send(configuration, auto_tx, test_accounts): - (_, _, client, manager, _) = configuration - web3 = client.w3 - network_info = NetworkInfo(web3.eth.chain_id) +def test_auto_tx_swap_and_send(smart_account, auto_tx, test_accounts): + network_info = NetworkInfo(smart_account.web3.eth.chain_id) usdc_address = ETHAddress(network_info.tokens["usdc"]) wbtc_address = ETHAddress(network_info.tokens["wbtc"]) @@ -137,15 +131,15 @@ def test_auto_tx_swap_and_send(configuration, auto_tx, test_accounts): ] for prompt in prompts: - wbtc_safe_address = manager.balance_of(wbtc_address) - usdc_safe_address = manager.balance_of(usdc_address) - receiver_usdc_balance = get_erc20_balance(client.w3, usdc_address, receiver) + wbtc_safe_address = get_erc20_balance(smart_account.web3, wbtc_address, smart_account.address) + usdc_safe_address = get_erc20_balance(smart_account.web3, usdc_address, smart_account.address) + receiver_usdc_balance = get_erc20_balance(smart_account.web3, usdc_address, receiver) auto_tx.run(prompt, non_interactive=True) - new_wbtc_safe_address = manager.balance_of(wbtc_address) - new_usdc_safe_address = manager.balance_of(usdc_address) - new_receiver_usdc_balance = get_erc20_balance(client.w3, usdc_address, receiver) + new_wbtc_safe_address = get_erc20_balance(smart_account.web3, wbtc_address, smart_account.address) + new_usdc_safe_address = get_erc20_balance(smart_account.web3, usdc_address, smart_account.address) + new_receiver_usdc_balance = get_erc20_balance(smart_account.web3, usdc_address, receiver) try: assert new_wbtc_safe_address > wbtc_safe_address diff --git a/autotx/tests/agents/token/research/test_advanced.py b/autotx/tests/agents/token/research/test_advanced.py index 0b72746..8b2ee51 100644 --- a/autotx/tests/agents/token/research/test_advanced.py +++ b/autotx/tests/agents/token/research/test_advanced.py @@ -1,60 +1,69 @@ +import pytest + from autotx.tests.agents.token.research.test_research import get_top_token_addresses_by_market_cap from autotx.eth_address import ETHAddress +from autotx.tests.conftest import MAX_TEST_TIMEOUT_SEC +from autotx.utils.ethereum.get_erc20_balance import get_erc20_balance +from autotx.utils.ethereum.get_native_balance import get_native_balance -def test_research_and_swap_many_tokens_subjective_simple(configuration, auto_tx): - (_, _, _, manager, _) = configuration +@pytest.mark.timeout(MAX_TEST_TIMEOUT_SEC) +def test_research_and_swap_many_tokens_subjective_simple(smart_account, auto_tx): uni_address = ETHAddress(auto_tx.network.tokens["uni"]) - uni_balance_in_safe = manager.balance_of(uni_address) + uni_balance_in_safe = get_erc20_balance(smart_account.web3, uni_address, smart_account.address) assert uni_balance_in_safe == 0 - starting_balance = manager.balance_of() + starting_balance = get_native_balance(smart_account.web3, smart_account.address) prompt = f"I want to use 3 ETH to purchase 3 of the best projects in: GameFi, AI, and MEMEs. Please research the top projects, come up with a strategy, and purchase the tokens that look most promising. All of this should be on ETH mainnet." result = auto_tx.run(prompt, non_interactive=True) - ending_balance = manager.balance_of() + ending_balance = get_native_balance(smart_account.web3, smart_account.address) - gaming_token_address = get_top_token_addresses_by_market_cap("gaming", "MAINNET", 1, auto_tx)[0] - gaming_token_balance_in_safe = manager.balance_of(gaming_token_address) + gaming_token_address1 = get_top_token_addresses_by_market_cap("gaming", "MAINNET", 1, auto_tx)[0] + gaming_token_address2 = get_top_token_addresses_by_market_cap("on-chain-gaming", "MAINNET", 1, auto_tx)[0] + gaming_token_address3 = get_top_token_addresses_by_market_cap("play-to-earn", "MAINNET", 1, auto_tx)[0] + gaming_token_balance_in_safe1 = get_erc20_balance(smart_account.web3, gaming_token_address1, smart_account.address) + gaming_token_balance_in_safe2 = get_erc20_balance(smart_account.web3, gaming_token_address2, smart_account.address) + gaming_token_balance_in_safe3 = get_erc20_balance(smart_account.web3, gaming_token_address3, smart_account.address) ai_token_address = get_top_token_addresses_by_market_cap("artificial-intelligence", "MAINNET", 1, auto_tx)[0] - ai_token_balance_in_safe = manager.balance_of(ai_token_address) + ai_token_balance_in_safe = get_erc20_balance(smart_account.web3, ai_token_address, smart_account.address) meme_token_address = get_top_token_addresses_by_market_cap("meme-token", "MAINNET", 1, auto_tx)[0] - meme_token_balance_in_safe = manager.balance_of(meme_token_address) + meme_token_balance_in_safe = get_erc20_balance(smart_account.web3, meme_token_address, smart_account.address) # Verify the balance is lower by max 3 ETH assert starting_balance - ending_balance <= 3 - # Verify there are at least 3 transactions - assert len(result.transactions) == 3 - # Verify there are only swap transactions - assert all([tx.summary.startswith("Swap") for tx in result.transactions]) + # Verify there are at exactly 3 intents + assert len(result.intents) == 3 + # Verify there are only swap intents + assert all([intent.type == "buy" or intent.type == "sell" for intent in result.intents]) # Verify the tokens are different - assert len(set([tx.summary.split(" ")[-1] for tx in result.transactions])) == 3 + assert len(set([intent.to_token.symbol for intent in result.intents])) == 3 # Verify the tokens are in the safe - assert gaming_token_balance_in_safe > 0 + assert gaming_token_balance_in_safe1 > 0 or gaming_token_balance_in_safe2 > 0 or gaming_token_balance_in_safe3 > 0 assert ai_token_balance_in_safe > 0 assert meme_token_balance_in_safe > 0 -def test_research_and_swap_many_tokens_subjective_complex(configuration, auto_tx): - (_, _, _, manager, _) = configuration +@pytest.mark.timeout(MAX_TEST_TIMEOUT_SEC) +def test_research_and_swap_many_tokens_subjective_complex(smart_account, auto_tx): - starting_balance = manager.balance_of() + starting_balance = get_native_balance(smart_account.web3, smart_account.address) prompt = f"I want to use 3 ETH to purchase exactly 10 of the best projects in: GameFi, NFTs, ZK, AI, and MEMEs. Please research the top projects, come up with a strategy, and purchase the tokens that look most promising. All of this should be on ETH mainnet." result = auto_tx.run(prompt, non_interactive=True) - ending_balance = manager.balance_of() + ending_balance = get_native_balance(smart_account.web3, smart_account.address) # Verify the balance is lower by max 3 ETH assert starting_balance - ending_balance <= 3 - # Verify there are at least 5 transactions - assert len(result.transactions) == 10 - # Verify there are only swap transactions - assert all([tx.summary.startswith("Swap") for tx in result.transactions]) + # Verify there are at exactly 10 intents + assert len(result.intents) == 10 + # Verify there are only swap intents + assert all([intent.type == "buy" or intent.type == "sell" for intent in result.intents]) # Verify the tokens are different - assert len(set([tx.summary.split(" ")[-1] for tx in result.transactions])) == 10 + assert len(set([intent.to_token.symbol for intent in result.intents])) == 10 diff --git a/autotx/tests/agents/token/research/test_research.py b/autotx/tests/agents/token/research/test_research.py index 251cc70..e98773a 100644 --- a/autotx/tests/agents/token/research/test_research.py +++ b/autotx/tests/agents/token/research/test_research.py @@ -1,11 +1,15 @@ +import pytest + from autotx.agents.ResearchTokensAgent import ( filter_token_list_by_network, get_coingecko, ) from autotx.eth_address import ETHAddress +from autotx.tests.conftest import FAST_TEST_TIMEOUT_SEC from autotx.utils.ethereum.networks import ChainId +@pytest.mark.timeout(FAST_TEST_TIMEOUT_SEC) def get_top_token_addresses_by_market_cap(category: str, network: str, count: int, auto_tx) -> list[ETHAddress]: tokens = get_coingecko().coins.get_markets(vs_currency="usd", category=category, per_page=250) tokens_in_network = filter_token_list_by_network( @@ -14,6 +18,7 @@ def get_top_token_addresses_by_market_cap(category: str, network: str, count: in return [ETHAddress(auto_tx.network.tokens[token["symbol"].lower()]) for token in tokens_in_network[:count]] +@pytest.mark.timeout(FAST_TEST_TIMEOUT_SEC) def test_price_change_information(auto_tx): token_information = get_coingecko().coins.get_id( id="starknet", @@ -33,6 +38,7 @@ def test_price_change_information(auto_tx): str(price_change) in "\n".join(result.info_messages).lower() or str(price_change_rounded) in "\n".join(result.info_messages).lower() ) +@pytest.mark.timeout(FAST_TEST_TIMEOUT_SEC) def test_get_top_5_tokens_from_base(auto_tx): tokens = get_coingecko().coins.get_markets( vs_currency="usd", category="base-ecosystem" @@ -45,6 +51,7 @@ def test_get_top_5_tokens_from_base(auto_tx): symbol: str = token["symbol"] assert symbol.lower() in "\n".join(result.info_messages).lower() +@pytest.mark.timeout(FAST_TEST_TIMEOUT_SEC) def test_get_top_5_most_traded_tokens_from_l1(auto_tx): tokens = get_coingecko().coins.get_markets( vs_currency="usd", category="layer-1", order="volume_desc" @@ -57,6 +64,7 @@ def test_get_top_5_most_traded_tokens_from_l1(auto_tx): symbol: str = token["symbol"] assert symbol.lower() in "\n".join(result.info_messages).lower() +@pytest.mark.timeout(FAST_TEST_TIMEOUT_SEC) def test_get_top_5_memecoins(auto_tx): tokens = get_coingecko().coins.get_markets(vs_currency="usd", category="meme-token") tokens_in_network = filter_token_list_by_network( @@ -71,6 +79,7 @@ def test_get_top_5_memecoins(auto_tx): symbol: str = token["symbol"] assert symbol.lower() in "\n".join(result.info_messages).lower() +@pytest.mark.timeout(FAST_TEST_TIMEOUT_SEC) def test_get_top_5_memecoins_in_optimism(auto_tx): tokens = get_coingecko().coins.get_markets(vs_currency="usd", category="meme-token") prompt = "What are the top 5 meme coins on Optimism?" diff --git a/autotx/tests/agents/token/research/test_research_and_swap.py b/autotx/tests/agents/token/research/test_research_and_swap.py index 87a9789..15f631b 100644 --- a/autotx/tests/agents/token/research/test_research_and_swap.py +++ b/autotx/tests/agents/token/research/test_research_and_swap.py @@ -1,7 +1,12 @@ +import pytest + from autotx.tests.agents.token.research.test_research import get_top_token_addresses_by_market_cap +from autotx.tests.conftest import FAST_TEST_TIMEOUT_SEC, MAX_TEST_TIMEOUT_SEC, SLOW_TEST_TIMEOUT_SEC +from autotx.utils.ethereum.get_erc20_balance import get_erc20_balance +from autotx.utils.ethereum.get_native_balance import get_native_balance -def test_research_and_buy_one(configuration, auto_tx): - (_, _, _, manager, _) = configuration +@pytest.mark.timeout(FAST_TEST_TIMEOUT_SEC) +def test_research_and_buy_one(smart_account, auto_tx): prompt = ( f"Buy 1 ETH worth of a meme token with the largest market cap in Ethereum mainnet" @@ -11,13 +16,13 @@ def test_research_and_buy_one(configuration, auto_tx): token_address = get_top_token_addresses_by_market_cap("meme-token", "MAINNET", 1, auto_tx)[0] - token_balance_in_safe = manager.balance_of(token_address) + token_balance_in_safe = get_erc20_balance(smart_account.web3, token_address, smart_account.address) assert token_balance_in_safe > 1000 -def test_research_and_buy_multiple(configuration, auto_tx): - (_, _, _, manager, _) = configuration +@pytest.mark.timeout(MAX_TEST_TIMEOUT_SEC) +def test_research_and_buy_multiple(smart_account, auto_tx): - old_eth_balance = manager.balance_of() + old_eth_balance = get_native_balance(smart_account.web3, smart_account.address) prompt = f""" Buy 1 ETH worth of a meme token with the largest market cap @@ -27,7 +32,7 @@ def test_research_and_buy_multiple(configuration, auto_tx): auto_tx.run(prompt, non_interactive=True) - new_eth_balance = manager.balance_of() + new_eth_balance = get_native_balance(smart_account.web3, smart_account.address) assert old_eth_balance - new_eth_balance == 1.5 @@ -35,8 +40,8 @@ def test_research_and_buy_multiple(configuration, auto_tx): governance_token_address = get_top_token_addresses_by_market_cap("governance", "MAINNET", 1, auto_tx)[0] - meme_token_balance_in_safe = manager.balance_of(meme_token_address) + meme_token_balance_in_safe = get_erc20_balance(smart_account.web3, meme_token_address, smart_account.address) assert meme_token_balance_in_safe > 1000 - governance_token_balance_in_safe = manager.balance_of(governance_token_address) + governance_token_balance_in_safe = get_erc20_balance(smart_account.web3, governance_token_address, smart_account.address) assert governance_token_balance_in_safe > 90 diff --git a/autotx/tests/agents/token/research/test_research_swap_and_send.py b/autotx/tests/agents/token/research/test_research_swap_and_send.py index 14bc8c4..e6acdfb 100644 --- a/autotx/tests/agents/token/research/test_research_swap_and_send.py +++ b/autotx/tests/agents/token/research/test_research_swap_and_send.py @@ -1,11 +1,14 @@ +import pytest + from autotx.tests.agents.token.research.test_research import get_top_token_addresses_by_market_cap +from autotx.tests.conftest import MAX_TEST_TIMEOUT_SEC, SLOW_TEST_TIMEOUT_SEC from autotx.utils.ethereum import get_erc20_balance +from autotx.utils.ethereum.get_native_balance import get_native_balance DIFFERENCE_PERCENTAGE = 0.01 -def test_research_buy_one_send_one(configuration, auto_tx, test_accounts): - (_, _, client, manager, _) = configuration - web3 = client.w3 +@pytest.mark.timeout(MAX_TEST_TIMEOUT_SEC) +def test_research_buy_one_send_one(smart_account, auto_tx, test_accounts): receiver = test_accounts[0] @@ -16,16 +19,15 @@ def test_research_buy_one_send_one(configuration, auto_tx, test_accounts): auto_tx.run(prompt, non_interactive=True) token_address = get_top_token_addresses_by_market_cap("meme-token", "MAINNET", 1, auto_tx)[0] - token_balance_in_safe = manager.balance_of(token_address) + token_balance_in_safe = get_erc20_balance(smart_account.web3, token_address, smart_account.address) - receiver_balance = get_erc20_balance(web3, token_address, receiver) + receiver_balance = get_erc20_balance(smart_account.web3, token_address, receiver) assert receiver_balance > 10000 assert token_balance_in_safe / receiver_balance < DIFFERENCE_PERCENTAGE -def test_research_buy_one_send_multiple(configuration, auto_tx, test_accounts): - (_, _, client, manager, _) = configuration - web3 = client.w3 +@pytest.mark.timeout(MAX_TEST_TIMEOUT_SEC) +def test_research_buy_one_send_multiple(smart_account, auto_tx, test_accounts): receiver_1 = test_accounts[0] receiver_2 = test_accounts[1] @@ -37,24 +39,23 @@ def test_research_buy_one_send_multiple(configuration, auto_tx, test_accounts): auto_tx.run(prompt, non_interactive=True) meme_token_address = get_top_token_addresses_by_market_cap("meme-token", "MAINNET", 1, auto_tx)[0] - meme_token_balance_in_safe = manager.balance_of(meme_token_address) + meme_token_balance_in_safe = get_erc20_balance(smart_account.web3, meme_token_address, smart_account.address) - receiver_1_balance = get_erc20_balance(web3, meme_token_address, receiver_1) + receiver_1_balance = get_erc20_balance(smart_account.web3, meme_token_address, receiver_1) assert receiver_1_balance == 10000 - receiver_2_balance = get_erc20_balance(web3, meme_token_address, receiver_2) + receiver_2_balance = get_erc20_balance(smart_account.web3, meme_token_address, receiver_2) assert receiver_2_balance == 250 assert meme_token_balance_in_safe > 10000 -def test_research_buy_multiple_send_multiple(configuration, auto_tx, test_accounts): - (_, _, client, manager, _) = configuration - web3 = client.w3 +@pytest.mark.timeout(MAX_TEST_TIMEOUT_SEC) +def test_research_buy_multiple_send_multiple(smart_account, auto_tx, test_accounts): receiver_1 = test_accounts[0] receiver_2 = test_accounts[1] - old_eth_balance = manager.balance_of() + old_eth_balance = get_native_balance(smart_account.web3, smart_account.address) prompt = f""" Buy 1 ETH worth of a meme token with the largest market cap @@ -65,20 +66,20 @@ def test_research_buy_multiple_send_multiple(configuration, auto_tx, test_accoun auto_tx.run(prompt, non_interactive=True) - new_eth_balance = manager.balance_of() + new_eth_balance = get_native_balance(smart_account.web3, smart_account.address) assert old_eth_balance - new_eth_balance == 1.5 meme_token_address = get_top_token_addresses_by_market_cap("meme-token", "MAINNET", 1, auto_tx)[0] - meme_token_balance_in_safe = manager.balance_of(meme_token_address) + meme_token_balance_in_safe = get_erc20_balance(smart_account.web3, meme_token_address, smart_account.address) governance_token_address = get_top_token_addresses_by_market_cap("governance", "MAINNET", 1, auto_tx)[0] - governance_token_balance_in_safe = manager.balance_of(governance_token_address) + governance_token_balance_in_safe = get_erc20_balance(smart_account.web3, governance_token_address, smart_account.address) - meme_balance = get_erc20_balance(web3, meme_token_address, receiver_1) + meme_balance = get_erc20_balance(smart_account.web3, meme_token_address, receiver_1) assert meme_balance > 10000 - governance_balance = get_erc20_balance(web3, governance_token_address, receiver_2) + governance_balance = get_erc20_balance(smart_account.web3, governance_token_address, receiver_2) assert governance_balance > 90 assert meme_token_balance_in_safe / meme_balance < DIFFERENCE_PERCENTAGE diff --git a/autotx/tests/agents/token/send/test_send.py b/autotx/tests/agents/token/send/test_send.py index 03f06b6..263a5b8 100644 --- a/autotx/tests/agents/token/send/test_send.py +++ b/autotx/tests/agents/token/send/test_send.py @@ -1,67 +1,70 @@ +import pytest + +from autotx.tests.conftest import FAST_TEST_TIMEOUT_SEC from autotx.utils.ethereum import get_erc20_balance from autotx.utils.ethereum.get_native_balance import get_native_balance -def test_send_native(configuration, auto_tx, test_accounts): - (_, _, client, _, _) = configuration +@pytest.mark.timeout(FAST_TEST_TIMEOUT_SEC) +def test_send_native(smart_account, auto_tx, test_accounts): receiver = test_accounts[0] - balance = get_native_balance(client.w3, receiver) + balance = get_native_balance(smart_account.web3, receiver) assert balance == 0 auto_tx.run(f"Send 1 ETH to {receiver}", non_interactive=True) - balance = get_native_balance(client.w3, receiver) + balance = get_native_balance(smart_account.web3, receiver) assert balance == 1 -def test_send_erc20(configuration, auto_tx, usdc, test_accounts): - (_, _, client, _, _) = configuration +@pytest.mark.timeout(FAST_TEST_TIMEOUT_SEC) +def test_send_erc20(smart_account, auto_tx, usdc, test_accounts): receiver = test_accounts[0] prompt = f"Send 10 USDC to {receiver}" - balance = get_erc20_balance(client.w3, usdc, receiver) + balance = get_erc20_balance(smart_account.web3, usdc, receiver) auto_tx.run(prompt, non_interactive=True) - new_balance = get_erc20_balance(client.w3, usdc, receiver) + new_balance = get_erc20_balance(smart_account.web3, usdc, receiver) assert balance + 10 == new_balance -def test_send_native_sequential(configuration, auto_tx, test_accounts): - (_, _, client, _, _) = configuration +@pytest.mark.timeout(FAST_TEST_TIMEOUT_SEC) +def test_send_native_sequential(smart_account, auto_tx, test_accounts): receiver = test_accounts[0] auto_tx.run(f"Send 1 ETH to {receiver}", non_interactive=True) - balance = get_native_balance(client.w3, receiver) + balance = get_native_balance(smart_account.web3, receiver) assert balance == 1 auto_tx.run(f"Send 0.5 ETH to {receiver}", non_interactive=True) - balance = get_native_balance(client.w3, receiver) + balance = get_native_balance(smart_account.web3, receiver) assert balance == 1.5 -def test_send_erc20_parallel(configuration, auto_tx, usdc, test_accounts): - (_, _, client, _, _) = configuration +@pytest.mark.timeout(FAST_TEST_TIMEOUT_SEC) +def test_send_erc20_parallel(smart_account, auto_tx, usdc, test_accounts): receiver_one = test_accounts[0] receiver_two = test_accounts[1] prompt = f"Send 2 USDC to {receiver_one} and 3 USDC to {receiver_two}" - balance_one = get_erc20_balance(client.w3, usdc, receiver_one) - balance_two = get_erc20_balance(client.w3, usdc, receiver_two) + balance_one = get_erc20_balance(smart_account.web3, usdc, receiver_one) + balance_two = get_erc20_balance(smart_account.web3, usdc, receiver_two) auto_tx.run(prompt, non_interactive=True) - new_balance_one = get_erc20_balance(client.w3, usdc, receiver_one) - new_balance_two = get_erc20_balance(client.w3, usdc, receiver_two) + new_balance_one = get_erc20_balance(smart_account.web3, usdc, receiver_one) + new_balance_two = get_erc20_balance(smart_account.web3, usdc, receiver_two) assert balance_one + 2 == new_balance_one assert balance_two + 3 == new_balance_two -def test_send_eth_multiple(configuration, auto_tx, usdc, test_accounts): - (_, _, client, _, _) = configuration +@pytest.mark.timeout(FAST_TEST_TIMEOUT_SEC) +def test_send_eth_multiple(smart_account, auto_tx, usdc, test_accounts): receiver_1 = test_accounts[0] receiver_2 = test_accounts[1] @@ -71,19 +74,19 @@ def test_send_eth_multiple(configuration, auto_tx, usdc, test_accounts): prompt = f"Send 1.3 USDC to {receiver_1}, {receiver_2}, {receiver_3}, {receiver_4} and {receiver_5}" - balance_1 = get_erc20_balance(client.w3, usdc, receiver_1) - balance_2 = get_erc20_balance(client.w3, usdc, receiver_2) - balance_3 = get_erc20_balance(client.w3, usdc, receiver_3) - balance_4 = get_erc20_balance(client.w3, usdc, receiver_4) - balance_5 = get_erc20_balance(client.w3, usdc, receiver_5) + balance_1 = get_erc20_balance(smart_account.web3, usdc, receiver_1) + balance_2 = get_erc20_balance(smart_account.web3, usdc, receiver_2) + balance_3 = get_erc20_balance(smart_account.web3, usdc, receiver_3) + balance_4 = get_erc20_balance(smart_account.web3, usdc, receiver_4) + balance_5 = get_erc20_balance(smart_account.web3, usdc, receiver_5) auto_tx.run(prompt, non_interactive=True) - new_balance_1 = get_erc20_balance(client.w3, usdc, receiver_1) - new_balance_2 = get_erc20_balance(client.w3, usdc, receiver_2) - new_balance_3 = get_erc20_balance(client.w3, usdc, receiver_3) - new_balance_4 = get_erc20_balance(client.w3, usdc, receiver_4) - new_balance_5 = get_erc20_balance(client.w3, usdc, receiver_5) + new_balance_1 = get_erc20_balance(smart_account.web3, usdc, receiver_1) + new_balance_2 = get_erc20_balance(smart_account.web3, usdc, receiver_2) + new_balance_3 = get_erc20_balance(smart_account.web3, usdc, receiver_3) + new_balance_4 = get_erc20_balance(smart_account.web3, usdc, receiver_4) + new_balance_5 = get_erc20_balance(smart_account.web3, usdc, receiver_5) assert balance_1 + 1.3 == new_balance_1 assert balance_2+ 1.3 == new_balance_2 diff --git a/autotx/tests/agents/token/test_swap.py b/autotx/tests/agents/token/test_swap.py index 9894c27..2fecdcc 100644 --- a/autotx/tests/agents/token/test_swap.py +++ b/autotx/tests/agents/token/test_swap.py @@ -1,77 +1,76 @@ +import pytest + +from autotx.tests.conftest import FAST_TEST_TIMEOUT_SEC +from autotx.utils.ethereum.get_erc20_balance import get_erc20_balance from autotx.utils.ethereum.networks import NetworkInfo from autotx.eth_address import ETHAddress DIFFERENCE_PERCENTAGE = 1.01 -def test_swap_with_non_default_token(configuration, auto_tx): - (_, _, client, manager, _) = configuration - web3 = client.w3 - network_info = NetworkInfo(web3.eth.chain_id) +@pytest.mark.timeout(FAST_TEST_TIMEOUT_SEC) +def test_swap_with_non_default_token(smart_account, auto_tx): + network_info = NetworkInfo(smart_account.web3.eth.chain_id) shib_address = ETHAddress(network_info.tokens["shib"]) prompt = "Buy 100000 SHIB with ETH" - balance = manager.balance_of(shib_address) + balance = get_erc20_balance(smart_account.web3, shib_address, smart_account.address) assert balance == 0 auto_tx.run(prompt, non_interactive=True) - new_balance = manager.balance_of(shib_address) + new_balance = get_erc20_balance(smart_account.web3, shib_address, smart_account.address) expected_shib_amount = 100000 assert expected_shib_amount <= new_balance <= expected_shib_amount * DIFFERENCE_PERCENTAGE -def test_swap_native(configuration, auto_tx): - (_, _, client, manager, _) = configuration - web3 = client.w3 - network_info = NetworkInfo(web3.eth.chain_id) +@pytest.mark.timeout(FAST_TEST_TIMEOUT_SEC) +def test_swap_native(smart_account, auto_tx): + network_info = NetworkInfo(smart_account.web3.eth.chain_id) usdc_address = ETHAddress(network_info.tokens["usdc"]) prompt = "Buy 100 USDC with ETH" auto_tx.run(prompt, non_interactive=True) - new_balance = manager.balance_of(usdc_address) + new_balance = get_erc20_balance(smart_account.web3, usdc_address, smart_account.address) expected_usdc_amount = 100 assert expected_usdc_amount <= new_balance <= expected_usdc_amount * DIFFERENCE_PERCENTAGE -def test_swap_multiple_1(configuration, auto_tx): - (_, _, client, manager, _) = configuration - web3 = client.w3 - network_info = NetworkInfo(web3.eth.chain_id) +@pytest.mark.timeout(FAST_TEST_TIMEOUT_SEC) +def test_swap_multiple_1(smart_account, auto_tx): + network_info = NetworkInfo(smart_account.web3.eth.chain_id) usdc_address = ETHAddress(network_info.tokens["usdc"]) wbtc_address = ETHAddress(network_info.tokens["wbtc"]) prompt = "Buy 1000 USDC with ETH and then buy WBTC with 500 USDC" - wbtc_balance = manager.balance_of(wbtc_address) + wbtc_balance = get_erc20_balance(smart_account.web3, wbtc_address, smart_account.address) auto_tx.run(prompt, non_interactive=True) expected_usdc_amount = 500 - usdc_balance = manager.balance_of(usdc_address) + usdc_balance = get_erc20_balance(smart_account.web3, usdc_address, smart_account.address) # 1000 is the amount bought so we need to get the difference from that amount expected_usdc_amount_plus_slippage = 1000 * DIFFERENCE_PERCENTAGE assert expected_usdc_amount <= usdc_balance <= expected_usdc_amount_plus_slippage - expected_usdc_amount - assert wbtc_balance < manager.balance_of(wbtc_address) + assert wbtc_balance < get_erc20_balance(smart_account.web3, wbtc_address, smart_account.address) -def test_swap_multiple_2(configuration, auto_tx): - (_, _, client, manager, _) = configuration - web3 = client.w3 - network_info = NetworkInfo(web3.eth.chain_id) +@pytest.mark.timeout(FAST_TEST_TIMEOUT_SEC) +def test_swap_multiple_2(smart_account, auto_tx): + network_info = NetworkInfo(smart_account.web3.eth.chain_id) usdc_address = ETHAddress(network_info.tokens["usdc"]) wbtc_address = ETHAddress(network_info.tokens["wbtc"]) prompt = "Sell ETH for 1000 USDC and then sell 500 USDC for WBTC" - wbtc_balance = manager.balance_of(wbtc_address) + wbtc_balance = get_erc20_balance(smart_account.web3, wbtc_address, smart_account.address) auto_tx.run(prompt, non_interactive=True) expected_amount = 500 - usdc_balance = manager.balance_of(usdc_address) + usdc_balance = get_erc20_balance(smart_account.web3, usdc_address, smart_account.address) assert expected_amount <= usdc_balance - assert wbtc_balance < manager.balance_of(wbtc_address) + assert wbtc_balance < get_erc20_balance(smart_account.web3, wbtc_address, smart_account.address) -def test_swap_triple(configuration, auto_tx): - (_, _, client, manager, _) = configuration - web3 = client.w3 - network_info = NetworkInfo(web3.eth.chain_id) +@pytest.mark.timeout(FAST_TEST_TIMEOUT_SEC) +def test_swap_triple(smart_account, auto_tx): + network_info = NetworkInfo(smart_account.web3.eth.chain_id) usdc_address = ETHAddress(network_info.tokens["usdc"]) uni_address = ETHAddress(network_info.tokens["uni"]) wbtc_address = ETHAddress(network_info.tokens["wbtc"]) @@ -83,42 +82,40 @@ def test_swap_triple(configuration, auto_tx): expected_usdc_amount = 1 expected_uni_amount = 0.5 expected_wbtc_amount = 0.05 - usdc_balance = manager.balance_of(usdc_address) - uni_balance = manager.balance_of(uni_address) - wbtc_balance = manager.balance_of(wbtc_address) + usdc_balance = get_erc20_balance(smart_account.web3, usdc_address, smart_account.address) + uni_balance = get_erc20_balance(smart_account.web3, uni_address, smart_account.address) + wbtc_balance = get_erc20_balance(smart_account.web3, wbtc_address, smart_account.address) assert expected_usdc_amount <= usdc_balance <= expected_usdc_amount * DIFFERENCE_PERCENTAGE assert expected_uni_amount <= uni_balance <= expected_uni_amount * DIFFERENCE_PERCENTAGE assert expected_wbtc_amount <= wbtc_balance <= expected_wbtc_amount * DIFFERENCE_PERCENTAGE -def test_swap_complex_1(configuration, auto_tx): # This one is complex because it confuses the LLM with WBTC amount - (_, _, client, manager, _) = configuration - web3 = client.w3 - network_info = NetworkInfo(web3.eth.chain_id) +@pytest.mark.timeout(FAST_TEST_TIMEOUT_SEC) +def test_swap_complex_1(smart_account, auto_tx): # This one is complex because it confuses the LLM with WBTC amount + network_info = NetworkInfo(smart_account.web3.eth.chain_id) usdc_address = ETHAddress(network_info.tokens["usdc"]) wbtc_address = ETHAddress(network_info.tokens["wbtc"]) prompt = "Swap ETH to 0.05 WBTC, then, swap WBTC to 1000 USDC" - wbtc_balance = manager.balance_of(wbtc_address) + wbtc_balance = get_erc20_balance(smart_account.web3, wbtc_address, smart_account.address) auto_tx.run(prompt, non_interactive=True) expected_usdc_amount = 1000 - usdc_balance = manager.balance_of(usdc_address) + usdc_balance = get_erc20_balance(smart_account.web3, usdc_address, smart_account.address) assert expected_usdc_amount <= usdc_balance <= expected_usdc_amount * DIFFERENCE_PERCENTAGE - assert wbtc_balance < manager.balance_of(wbtc_address) + assert wbtc_balance < get_erc20_balance(smart_account.web3, wbtc_address, smart_account.address) -def test_swap_complex_2(configuration, auto_tx): # This one is complex because it confuses the LLM with WBTC amount - (_, _, client, manager, _) = configuration - web3 = client.w3 - network_info = NetworkInfo(web3.eth.chain_id) +@pytest.mark.timeout(FAST_TEST_TIMEOUT_SEC) +def test_swap_complex_2(smart_account, auto_tx): # This one is complex because it confuses the LLM with WBTC amount + network_info = NetworkInfo(smart_account.web3.eth.chain_id) usdc_address = ETHAddress(network_info.tokens["usdc"]) wbtc_address = ETHAddress(network_info.tokens["wbtc"]) prompt = "Buy 1000 USDC with ETH, then sell USDC to buy 0.001 WBTC" - usdc_balance = manager.balance_of(usdc_address) + usdc_balance = get_erc20_balance(smart_account.web3, usdc_address, smart_account.address) auto_tx.run(prompt, non_interactive=True) - wbtc_balance = manager.balance_of(wbtc_address) + wbtc_balance = get_erc20_balance(smart_account.web3, wbtc_address, smart_account.address) expected_wbtc_amount = 0.001 assert expected_wbtc_amount <= wbtc_balance <= expected_wbtc_amount * DIFFERENCE_PERCENTAGE - assert usdc_balance < manager.balance_of(usdc_address) + assert usdc_balance < get_erc20_balance(smart_account.web3, usdc_address, smart_account.address) diff --git a/autotx/tests/agents/token/test_swap_and_send.py b/autotx/tests/agents/token/test_swap_and_send.py index d69aaf7..04d4e6b 100644 --- a/autotx/tests/agents/token/test_swap_and_send.py +++ b/autotx/tests/agents/token/test_swap_and_send.py @@ -1,13 +1,14 @@ +import pytest +from autotx.tests.conftest import SLOW_TEST_TIMEOUT_SEC from autotx.utils.ethereum import get_erc20_balance, get_native_balance from autotx.utils.ethereum.networks import NetworkInfo from autotx.eth_address import ETHAddress DIFFERENCE_PERCENTAGE = 1.01 -def test_swap_and_send_simple(configuration, auto_tx, test_accounts): - (_, _, client, manager, _) = configuration - web3 = client.w3 - network_info = NetworkInfo(web3.eth.chain_id) +@pytest.mark.timeout(SLOW_TEST_TIMEOUT_SEC) +def test_swap_and_send_simple(smart_account, auto_tx, test_accounts): + network_info = NetworkInfo(smart_account.web3.eth.chain_id) wbtc_address = ETHAddress(network_info.tokens["wbtc"]) receiver = test_accounts[0] @@ -16,16 +17,15 @@ def test_swap_and_send_simple(configuration, auto_tx, test_accounts): auto_tx.run(prompt, non_interactive=True) - new_wbtc_safe_address = manager.balance_of(wbtc_address) - new_receiver_wbtc_balance = get_erc20_balance(client.w3, wbtc_address, receiver) + new_wbtc_safe_address = get_erc20_balance(smart_account.web3, wbtc_address, smart_account.address) + new_receiver_wbtc_balance = get_erc20_balance(smart_account.web3, wbtc_address, receiver) excepted_safe_wbtc_balance = 0.04 assert excepted_safe_wbtc_balance <= new_wbtc_safe_address <= new_wbtc_safe_address * DIFFERENCE_PERCENTAGE assert new_receiver_wbtc_balance == 0.01 -def test_swap_and_send_complex(configuration, auto_tx, test_accounts): - (_, _, client, manager, _) = configuration - web3 = client.w3 - network_info = NetworkInfo(web3.eth.chain_id) +@pytest.mark.timeout(SLOW_TEST_TIMEOUT_SEC) +def test_swap_and_send_complex(smart_account, auto_tx, test_accounts): + network_info = NetworkInfo(smart_account.web3.eth.chain_id) usdc_address = ETHAddress(network_info.tokens["usdc"]) wbtc_address = ETHAddress(network_info.tokens["wbtc"]) @@ -33,36 +33,35 @@ def test_swap_and_send_complex(configuration, auto_tx, test_accounts): prompt = f"Swap ETH to 0.05 WBTC, then, swap WBTC to 1000 USDC and send 50 USDC to {receiver}" - wbtc_safe_address = manager.balance_of(wbtc_address) + wbtc_safe_address = get_erc20_balance(smart_account.web3, wbtc_address, smart_account.address) auto_tx.run(prompt, non_interactive=True) - new_wbtc_safe_address = manager.balance_of(wbtc_address) - new_usdc_safe_address = manager.balance_of(usdc_address) - new_receiver_usdc_balance = get_erc20_balance(client.w3, usdc_address, receiver) + new_wbtc_safe_address = get_erc20_balance(smart_account.web3, wbtc_address, smart_account.address) + new_usdc_safe_address = get_erc20_balance(smart_account.web3, usdc_address, smart_account.address) + new_receiver_usdc_balance = get_erc20_balance(smart_account.web3, usdc_address, receiver) expected_usdc_safe_balance = 950 assert new_wbtc_safe_address > wbtc_safe_address assert expected_usdc_safe_balance <= new_usdc_safe_address <= expected_usdc_safe_balance * DIFFERENCE_PERCENTAGE assert new_receiver_usdc_balance == 50 -def test_send_and_swap_simple(configuration, auto_tx, test_accounts): - (_, _, client, manager, _) = configuration - web3 = client.w3 - network_info = NetworkInfo(web3.eth.chain_id) +@pytest.mark.timeout(SLOW_TEST_TIMEOUT_SEC) +def test_send_and_swap_simple(smart_account, auto_tx, test_accounts): + network_info = NetworkInfo(smart_account.web3.eth.chain_id) wbtc_address = ETHAddress(network_info.tokens["wbtc"]) receiver = test_accounts[0] prompt = f"Send 0.1 ETH to {receiver}, and then swap ETH to 0.05 WBTC" - receiver_native_balance = get_native_balance(client.w3, receiver) - receiver_wbtc_balance = get_erc20_balance(client.w3, wbtc_address, receiver) + receiver_native_balance = get_native_balance(smart_account.web3, receiver) + receiver_wbtc_balance = get_erc20_balance(smart_account.web3, wbtc_address, receiver) auto_tx.run(prompt, non_interactive=True) - safe_wbtc_balance = manager.balance_of(wbtc_address) - new_receiver_native_balance = get_native_balance(client.w3, receiver) - new_receiver_wbtc_balance = get_erc20_balance(client.w3, wbtc_address, receiver) + safe_wbtc_balance = get_erc20_balance(smart_account.web3, wbtc_address, smart_account.address) + new_receiver_native_balance = get_native_balance(smart_account.web3, receiver) + new_receiver_wbtc_balance = get_erc20_balance(smart_account.web3, wbtc_address, receiver) expected_wbtc_safe_balance = 0.05 assert expected_wbtc_safe_balance <= safe_wbtc_balance <= expected_wbtc_safe_balance * DIFFERENCE_PERCENTAGE @@ -70,10 +69,9 @@ def test_send_and_swap_simple(configuration, auto_tx, test_accounts): assert new_receiver_wbtc_balance == receiver_wbtc_balance assert new_receiver_native_balance == receiver_native_balance + 0.1 -def test_send_and_swap_complex(configuration, auto_tx, test_accounts): - (_, _, client, manager, _) = configuration - web3 = client.w3 - network_info = NetworkInfo(web3.eth.chain_id) +@pytest.mark.timeout(SLOW_TEST_TIMEOUT_SEC) +def test_send_and_swap_complex(smart_account, auto_tx, test_accounts): + network_info = NetworkInfo(smart_account.web3.eth.chain_id) usdc_address = ETHAddress(network_info.tokens["usdc"]) wbtc_address = ETHAddress(network_info.tokens["wbtc"]) @@ -82,19 +80,19 @@ def test_send_and_swap_complex(configuration, auto_tx, test_accounts): prompt = f"Send 0.1 ETH to {receiver_1}, then swap ETH to 0.05 WBTC, then, swap WBTC to 1000 USDC and send 50 USDC to {receiver_2}" - wbtc_safe_balance = manager.balance_of(wbtc_address) - receiver_1_native_balance = get_native_balance(client.w3, receiver_1) - receiver_2_usdc_balance = get_erc20_balance(client.w3, usdc_address, receiver_2) + wbtc_safe_balance = get_erc20_balance(smart_account.web3, wbtc_address, smart_account.address) + receiver_1_native_balance = get_native_balance(smart_account.web3, receiver_1) + receiver_2_usdc_balance = get_erc20_balance(smart_account.web3, usdc_address, receiver_2) auto_tx.run(prompt, non_interactive=True) - new_wbtc_safe_balance = manager.balance_of(wbtc_address) - new_usdc_safe_balance = manager.balance_of(usdc_address) - new_receiver_1_native_balance = get_native_balance(client.w3, receiver_1) - new_receiver_1_usdc_balance = get_erc20_balance(client.w3, usdc_address, receiver_1) - new_receiver_1_wbtc_balance = get_erc20_balance(client.w3, wbtc_address, receiver_1) - new_receiver_2_wbtc_balance = get_erc20_balance(client.w3, wbtc_address, receiver_2) - new_receiver_2_usdc_balance = get_erc20_balance(client.w3, usdc_address, receiver_2) + new_wbtc_safe_balance = get_erc20_balance(smart_account.web3, wbtc_address, smart_account.address) + new_usdc_safe_balance = get_erc20_balance(smart_account.web3, usdc_address, smart_account.address) + new_receiver_1_native_balance = get_native_balance(smart_account.web3, receiver_1) + new_receiver_1_usdc_balance = get_erc20_balance(smart_account.web3, usdc_address, receiver_1) + new_receiver_1_wbtc_balance = get_erc20_balance(smart_account.web3, wbtc_address, receiver_1) + new_receiver_2_wbtc_balance = get_erc20_balance(smart_account.web3, wbtc_address, receiver_2) + new_receiver_2_usdc_balance = get_erc20_balance(smart_account.web3, usdc_address, receiver_2) expected_usdc_safe_balance = 950 assert expected_usdc_safe_balance <= new_usdc_safe_balance <= expected_usdc_safe_balance * DIFFERENCE_PERCENTAGE assert new_wbtc_safe_balance > wbtc_safe_balance diff --git a/autotx/tests/conftest.py b/autotx/tests/conftest.py index 2131eb5..53cb369 100644 --- a/autotx/tests/conftest.py +++ b/autotx/tests/conftest.py @@ -1,29 +1,29 @@ from dotenv import load_dotenv +load_dotenv() +import pytest +from eth_account import Account from autotx.utils.configuration import AppConfig from autotx.utils.ethereum.helpers.swap_from_eoa import swap from autotx.utils.ethereum.send_native import send_native -from autotx.wallets.safe_smart_wallet import SafeSmartWallet -load_dotenv() - +from autotx.smart_accounts.safe_smart_account import SafeSmartAccount from autotx.agents.DelegateResearchTokensAgent import DelegateResearchTokensAgent from autotx.agents.SendTokensAgent import SendTokensAgent from autotx.agents.SwapTokensAgent import SwapTokensAgent - -from eth_account import Account - from autotx.utils.constants import OPENAI_API_KEY, OPENAI_MODEL_NAME from autotx.utils.ethereum.networks import NetworkInfo from autotx.eth_address import ETHAddress from autotx.utils.ethereum.helpers.get_dev_account import get_dev_account - -import pytest from autotx.AutoTx import AutoTx, Config from autotx.chain_fork import stop, start from autotx.utils.ethereum import ( transfer_erc20, ) +FAST_TEST_TIMEOUT_SEC = 100 +SLOW_TEST_TIMEOUT_SEC = 200 +MAX_TEST_TIMEOUT_SEC = 500 + @pytest.fixture(autouse=True) def start_and_stop_local_fork(): start() @@ -33,38 +33,35 @@ def start_and_stop_local_fork(): stop() @pytest.fixture() -def configuration(): - app_config = AppConfig.load() - wallet = SafeSmartWallet(app_config.manager, auto_submit_tx=True) +def smart_account() -> SafeSmartAccount: + app_config = AppConfig() + account = SafeSmartAccount(app_config.rpc_url, app_config.network_info, auto_submit_tx=True) dev_account = get_dev_account() - send_native(dev_account, wallet.address, 10, app_config.web3) + send_native(dev_account, account.address, 10, app_config.web3) - return (dev_account, app_config.agent, app_config.client, app_config.manager, wallet) + return account @pytest.fixture() -def auto_tx(configuration): - (_, _, client, _, wallet) = configuration - network_info = NetworkInfo(client.w3.eth.chain_id) +def auto_tx(smart_account): + network_info = NetworkInfo(smart_account.web3.eth.chain_id) get_llm_config = lambda: { "cache_seed": None, "config_list": [{"model": OPENAI_MODEL_NAME, "api_key": OPENAI_API_KEY}]} return AutoTx( - client.w3, - wallet, + smart_account.web3, + smart_account, network_info, [ SendTokensAgent(), SwapTokensAgent(), DelegateResearchTokensAgent() ], - Config(verbose=True, get_llm_config=get_llm_config, logs_dir=None, log_costs=True), + Config(verbose=True, get_llm_config=get_llm_config, logs_dir=None, log_costs=True, max_rounds=20) ) @pytest.fixture() -def usdc(configuration) -> ETHAddress: - (user, _, client, _, wallet) = configuration - - chain_id = client.w3.eth.chain_id +def usdc(smart_account) -> ETHAddress: + chain_id = smart_account.web3.eth.chain_id network_info = NetworkInfo(chain_id) eth_address = ETHAddress(network_info.tokens["eth"]) @@ -72,9 +69,10 @@ def usdc(configuration) -> ETHAddress: amount = 100 - swap(client, user, amount, eth_address, usdc_address, network_info.chain_id) + dev_account = get_dev_account() + swap(smart_account.web3, dev_account, amount, eth_address, usdc_address, network_info.chain_id) - transfer_erc20(client.w3, usdc_address, user, wallet.address, amount) + transfer_erc20(smart_account.web3, usdc_address, dev_account, smart_account.address, amount) return usdc_address diff --git a/autotx/tests/integration/test_swap.py b/autotx/tests/integration/test_swap.py index 32d5b38..9e4f37d 100644 --- a/autotx/tests/integration/test_swap.py +++ b/autotx/tests/integration/test_swap.py @@ -1,197 +1,194 @@ +import asyncio from decimal import Decimal from autotx.eth_address import ETHAddress +from autotx.utils.ethereum.get_erc20_balance import get_erc20_balance +from autotx.utils.ethereum.get_native_balance import get_native_balance from autotx.utils.ethereum.lifi.swap import build_swap_transaction from autotx.utils.ethereum.networks import NetworkInfo DIFFERENCE_PERCENTAGE = 1.01 -def test_buy_one_usdc(configuration): - (_, _, client, manager, _) = configuration - network_info = NetworkInfo(client.w3.eth.chain_id) +def test_buy_one_usdc(smart_account): + network_info = NetworkInfo(smart_account.web3.eth.chain_id) eth_address = ETHAddress(network_info.tokens["eth"]) usdc_address = ETHAddress(network_info.tokens["usdc"]) expected_usdc_amount = 1 buy_usdc_with_eth_transaction = build_swap_transaction( - client.w3, + smart_account.web3, expected_usdc_amount, eth_address, usdc_address, - manager.address, + smart_account.address, False, network_info.chain_id, ) - hash = manager.send_tx(buy_usdc_with_eth_transaction[0].params) - manager.wait(hash) - usdc_balance = manager.balance_of(usdc_address) + hash = smart_account.send_tx(buy_usdc_with_eth_transaction[0].params) + smart_account.wait(hash) + usdc_balance = get_erc20_balance(smart_account.web3, usdc_address, smart_account.address) assert expected_usdc_amount <= usdc_balance <= expected_usdc_amount * DIFFERENCE_PERCENTAGE -def test_buy_one_thousand_usdc(configuration): - (_, _, client, manager, _) = configuration - network_info = NetworkInfo(client.w3.eth.chain_id) +def test_buy_one_thousand_usdc(smart_account): + network_info = NetworkInfo(smart_account.web3.eth.chain_id) eth_address = ETHAddress(network_info.tokens["eth"]) usdc_address = ETHAddress(network_info.tokens["usdc"]) expected_usdc_amount = 1000 buy_usdc_with_eth_transaction = build_swap_transaction( - client.w3, + smart_account.web3, expected_usdc_amount, eth_address, usdc_address, - manager.address, + smart_account.address, False, network_info.chain_id, ) print(buy_usdc_with_eth_transaction[0].summary) - hash = manager.send_tx(buy_usdc_with_eth_transaction[0].params) - manager.wait(hash) - usdc_balance = manager.balance_of(usdc_address) + hash = asyncio.run(smart_account.send_transaction(buy_usdc_with_eth_transaction[0].params)) + smart_account.wait(hash) + usdc_balance = get_erc20_balance(smart_account.web3, usdc_address, smart_account.address) assert expected_usdc_amount <= usdc_balance <= expected_usdc_amount * DIFFERENCE_PERCENTAGE -def test_receive_native(configuration): - (_, _, client, manager, _) = configuration +def test_receive_native(smart_account): - network_info = NetworkInfo(client.w3.eth.chain_id) + network_info = NetworkInfo(smart_account.web3.eth.chain_id) eth_address = ETHAddress(network_info.tokens["eth"]) usdc_address = ETHAddress(network_info.tokens["usdc"]) - safe_eth_balance = manager.balance_of() + safe_eth_balance = get_native_balance(smart_account.web3, smart_account.address) assert safe_eth_balance == 10 buy_usdc_with_eth_transaction = build_swap_transaction( - client.w3, + smart_account.web3, 5, eth_address, usdc_address, - manager.address, + smart_account.address, True, network_info.chain_id, ) - hash = manager.send_tx(buy_usdc_with_eth_transaction[0].params) - manager.wait(hash) - safe_eth_balance = manager.balance_of() + hash = asyncio.run(smart_account.send_transaction(buy_usdc_with_eth_transaction[0].params)) + smart_account.wait(hash) + safe_eth_balance = get_native_balance(smart_account.web3, smart_account.address) assert safe_eth_balance == 5 buy_eth_with_usdc_transaction = build_swap_transaction( - client.w3, + smart_account.web3, 4, usdc_address, eth_address, - manager.address, + smart_account.address, False, network_info.chain_id, ) - hash = manager.send_tx(buy_eth_with_usdc_transaction[0].params) - manager.wait(hash) - hash = manager.send_tx(buy_eth_with_usdc_transaction[1].params) - manager.wait(hash) - safe_eth_balance = manager.balance_of() + hash = asyncio.run(smart_account.send_transaction(buy_eth_with_usdc_transaction[0].params)) + smart_account.wait(hash) + hash = asyncio.run(smart_account.send_transaction(buy_eth_with_usdc_transaction[1].params)) + smart_account.wait(hash) + safe_eth_balance = get_native_balance(smart_account.web3, smart_account.address) assert safe_eth_balance >= 9 -def test_buy_small_amount_wbtc_with_eth(configuration): - (_, _, client, manager, _) = configuration - network_info = NetworkInfo(client.w3.eth.chain_id) +def test_buy_small_amount_wbtc_with_eth(smart_account): + network_info = NetworkInfo(smart_account.web3.eth.chain_id) eth_address = ETHAddress(network_info.tokens["eth"]) wbtc_address = ETHAddress(network_info.tokens["wbtc"]) expected_wbtc_amount = 0.01 buy_wbtc_with_eth_transaction = build_swap_transaction( - client.w3, + smart_account.web3, Decimal(str(expected_wbtc_amount)), eth_address, wbtc_address, - manager.address, + smart_account.address, False, network_info.chain_id, ) - hash = manager.send_tx(buy_wbtc_with_eth_transaction[0].params) - manager.wait(hash) - wbtc_balance = manager.balance_of(wbtc_address) + hash = asyncio.run(smart_account.send_transaction(buy_wbtc_with_eth_transaction[0].params)) + smart_account.wait(hash) + wbtc_balance = get_erc20_balance(smart_account.web3, wbtc_address, smart_account.address) assert expected_wbtc_amount <= wbtc_balance <= expected_wbtc_amount * DIFFERENCE_PERCENTAGE -def test_buy_big_amount_wbtc_with_eth(configuration): - (_, _, client, manager, _) = configuration - network_info = NetworkInfo(client.w3.eth.chain_id) +def test_buy_big_amount_wbtc_with_eth(smart_account): + network_info = NetworkInfo(smart_account.web3.eth.chain_id) eth_address = ETHAddress(network_info.tokens["eth"]) wbtc_address = ETHAddress(network_info.tokens["wbtc"]) expected_wbtc_amount = 0.1 buy_wbtc_with_eth_transaction = build_swap_transaction( - client.w3, + smart_account.web3, Decimal(str(expected_wbtc_amount)), eth_address, wbtc_address, - manager.address, + smart_account.address, False, network_info.chain_id, ) - hash = manager.send_tx(buy_wbtc_with_eth_transaction[0].params) - manager.wait(hash) - wbtc_balance = manager.balance_of(wbtc_address) + hash = asyncio.run(smart_account.send_transaction(buy_wbtc_with_eth_transaction[0].params)) + smart_account.wait(hash) + wbtc_balance = get_erc20_balance(smart_account.web3, wbtc_balance, smart_account.address) assert expected_wbtc_amount <= wbtc_balance <= expected_wbtc_amount * DIFFERENCE_PERCENTAGE -def test_swap_multiple_tokens(configuration): - (_, _, client, manager, _) = configuration - network_info = NetworkInfo(client.w3.eth.chain_id) +def test_swap_multiple_tokens(smart_account): + network_info = NetworkInfo(smart_account.web3.eth.chain_id) eth_address = ETHAddress(network_info.tokens["eth"]) usdc_address = ETHAddress(network_info.tokens["usdc"]) wbtc_address = ETHAddress(network_info.tokens["wbtc"]) shib_address = ETHAddress(network_info.tokens["shib"]) - usdc_balance = manager.balance_of(usdc_address) + usdc_balance = get_erc20_balance(smart_account.web3, usdc_address, smart_account.address) assert usdc_balance == 0 sell_eth_for_usdc_transaction = build_swap_transaction( - client.w3, + smart_account.web3, 1, eth_address, usdc_address, - manager.address, + smart_account.address, True, network_info.chain_id, ) - hash = manager.send_tx(sell_eth_for_usdc_transaction[0].params) - manager.wait(hash) - usdc_balance = manager.balance_of(usdc_address) + hash = asyncio.run(smart_account.send_transaction(sell_eth_for_usdc_transaction[0].params)) + smart_account.wait(hash) + usdc_balance = get_erc20_balance(smart_account.web3, usdc_address, smart_account.address) assert usdc_balance > 2900 - wbtc_balance = manager.balance_of(wbtc_address) + wbtc_balance = get_erc20_balance(smart_account.web3, wbtc_address, smart_account.address) assert wbtc_balance == 0 buy_wbtc_with_usdc_transaction = build_swap_transaction( - client.w3, + smart_account.web3, 0.01, usdc_address, wbtc_address, - manager.address, + smart_account.address, False, network_info.chain_id, ) - hash = manager.send_tx(buy_wbtc_with_usdc_transaction[0].params) - manager.wait(hash) - hash = manager.send_tx(buy_wbtc_with_usdc_transaction[1].params) - manager.wait(hash) - wbtc_balance = manager.balance_of(wbtc_address) + hash = asyncio.run(smart_account.send_transaction(buy_wbtc_with_usdc_transaction[0].params)) + smart_account.wait(hash) + hash = asyncio.run(smart_account.send_transaction(buy_wbtc_with_usdc_transaction[1].params)) + smart_account.wait(hash) + wbtc_balance = get_erc20_balance(smart_account.web3, wbtc_address, smart_account.address) assert wbtc_balance >= 0.01 - shib_balance = manager.balance_of(shib_address) + shib_balance = get_erc20_balance(smart_account.web3, shib_address, smart_account.address) assert shib_balance == 0 sell_wbtc_for_shib = build_swap_transaction( - client.w3, + smart_account.web3, 0.005, wbtc_address, shib_address, - manager.address, + smart_account.address, True, network_info.chain_id, ) - hash = manager.send_tx(sell_wbtc_for_shib[0].params) - manager.wait(hash) - hash = manager.send_tx(sell_wbtc_for_shib[1].params) - manager.wait(hash) - shib_balance = manager.balance_of(shib_address) - shib_balance = manager.balance_of(shib_address) + hash = asyncio.run(smart_account.send_transaction(sell_wbtc_for_shib[0].params)) + smart_account.wait(hash) + hash = asyncio.run(smart_account.send_transaction(sell_wbtc_for_shib[1].params)) + smart_account.wait(hash) + shib_balance = get_erc20_balance(smart_account.web3, shib_address, smart_account.address) + shib_balance = get_erc20_balance(smart_account.web3, shib_address, smart_account.address) assert shib_balance > 0 diff --git a/autotx/utils/configuration.py b/autotx/utils/configuration.py index 60bd533..c5d5eae 100644 --- a/autotx/utils/configuration.py +++ b/autotx/utils/configuration.py @@ -2,52 +2,25 @@ import sys from time import sleep -from web3 import Web3 +from web3 import HTTPProvider, Web3 from autotx.get_env_vars import get_env_vars -from gnosis.eth import EthereumClient from eth_typing import URI -from eth_account.signers.local import LocalAccount -from autotx.setup import setup_safe -from autotx.utils.ethereum import SafeManager -from autotx.utils.ethereum.agent_account import get_or_create_agent_account from autotx.utils.ethereum.constants import DEVNET_RPC_URL -from autotx.eth_address import ETHAddress from autotx.utils.ethereum.networks import NetworkInfo from autotx.utils.is_dev_env import is_dev_env -from autotx.wallets.smart_wallet import SmartWallet smart_account_addr = get_env_vars() class AppConfig: + rpc_url: str web3: Web3 - client: EthereumClient - agent: LocalAccount - manager: SafeManager network_info: NetworkInfo def __init__( self, - web3: Web3, - client: EthereumClient, - agent: LocalAccount, - manager: SafeManager, - network_info: NetworkInfo, - ): - self.web3 = web3 - self.client = client - self.agent = agent - self.manager = manager - self.network_info = network_info - - @staticmethod - def load( - smart_account_addr: str | None = None, subsidized_chain_id: int | None = None, - fill_dev_account: bool = False, - agent: LocalAccount | None = None, - check_valid_safe: bool = False, - ) -> "AppConfig": + ): rpc_url: str if subsidized_chain_id: @@ -66,10 +39,9 @@ def load( rpc_url = provided_rpc_url - client = EthereumClient(URI(rpc_url)) - + web3 = Web3(HTTPProvider(rpc_url)) for i in range(16): - if client.w3.is_connected(): + if web3.is_connected(): break if i == 15: if is_dev_env(): @@ -78,11 +50,6 @@ def load( sys.exit("Can not connect with remote node. Check your CHAIN_RPC_URL") sleep(0.5) - agent = agent if agent else get_or_create_agent_account() - - smart_account_addr = smart_account_addr if smart_account_addr else os.getenv("SMART_ACCOUNT_ADDRESS") - smart_account = ETHAddress(smart_account_addr) if smart_account_addr else None - - manager = setup_safe(smart_account, agent, client, fill_dev_account, check_valid_safe) - - return AppConfig(client.w3, client, agent, manager, NetworkInfo(client.w3.eth.chain_id)) + self.rpc_url = rpc_url + self.web3 = web3 + self.network_info = NetworkInfo(web3.eth.chain_id) diff --git a/autotx/utils/constants.py b/autotx/utils/constants.py index 09bd2dd..442df41 100644 --- a/autotx/utils/constants.py +++ b/autotx/utils/constants.py @@ -8,4 +8,5 @@ COINGECKO_API_KEY = os.environ.get("COINGECKO_API_KEY", None) LIFI_API_KEY = os.environ.get("LIFI_API_KEY", None) ALCHEMY_API_KEY = os.environ.get("ALCHEMY_API_KEY") -MAINNET_DEFAULT_RPC = f"https://eth-mainnet.g.alchemy.com/v2/{ALCHEMY_API_KEY}" \ No newline at end of file +MAINNET_DEFAULT_RPC = f"https://eth-mainnet.g.alchemy.com/v2/{ALCHEMY_API_KEY}" +SMART_ACCOUNT_OWNER_PK = os.environ.get("SMART_ACCOUNT_OWNER_PK", None) \ No newline at end of file diff --git a/autotx/utils/ethereum/SafeManager.py b/autotx/utils/ethereum/SafeManager.py index 542d591..5319aec 100644 --- a/autotx/utils/ethereum/SafeManager.py +++ b/autotx/utils/ethereum/SafeManager.py @@ -182,7 +182,6 @@ def execute_tx(self, tx: TxParams | dict[str, Any], safe_nonce: Optional[int] = raise Exception("Unknown error executing transaction", e) - def execute_multisend_tx(self, txs: list[TxParams | dict[str, Any]], safe_nonce: Optional[int] = None) -> HexBytes: if not self.dev_account: raise ValueError("Dev account not set. This function should not be called in production.") @@ -191,14 +190,22 @@ def execute_multisend_tx(self, txs: list[TxParams | dict[str, Any]], safe_nonce: safe_tx.sign(self.agent.key.hex()) - safe_tx.call(tx_sender_address=self.dev_account.address) - - tx_hash, _ = safe_tx.execute( - tx_sender_private_key=self.dev_account.key.hex() - ) + try: + safe_tx.call(tx_sender_address=self.dev_account.address) + tx_hash, _ = safe_tx.execute( + tx_sender_private_key=self.dev_account.key.hex() + ) + return tx_hash + except Exception as e: + if "revert: GS013" in str(e): + print(str(e)) + print("Executing transactions one by one to get a more detailed revert message") + nonce = self.track_nonce(safe_nonce) + for i, tx in enumerate(txs): + print(f"Executing transaction {i + 1}...") + self.execute_tx(tx, nonce + i) + raise e - return tx_hash - def post_transaction(self, tx: TxParams | dict[str, Any], safe_nonce: Optional[int] = None) -> None: ts_api = TransactionServiceApi( self.network.chain_id, ethereum_client=self.client, base_url=self.transaction_service_url diff --git a/autotx/utils/ethereum/helpers/fill_dev_account_with_tokens.py b/autotx/utils/ethereum/helpers/fill_dev_account_with_tokens.py index 8e66679..f3f0211 100644 --- a/autotx/utils/ethereum/helpers/fill_dev_account_with_tokens.py +++ b/autotx/utils/ethereum/helpers/fill_dev_account_with_tokens.py @@ -1,23 +1,23 @@ +from web3 import Web3 from autotx.utils.ethereum import transfer_erc20 from autotx.utils.ethereum.constants import NATIVE_TOKEN_ADDRESS from autotx.eth_address import ETHAddress +from autotx.utils.ethereum.helpers.get_dev_account import get_dev_account from autotx.utils.ethereum.helpers.swap_from_eoa import swap from autotx.utils.ethereum.networks import ChainId, NetworkInfo -from eth_account.signers.local import LocalAccount -from gnosis.eth import EthereumClient from autotx.utils.ethereum.send_native import send_native def fill_dev_account_with_tokens( - client: EthereumClient, - dev_account: LocalAccount, + web3: Web3, safe_address: ETHAddress, network_info: NetworkInfo, ) -> None: + dev_account = get_dev_account() # XDAI or MATIC doesn't have the same value as ETH, so we need to fill more amount_to_fill = 3000 if network_info.chain_id in [ChainId.POLYGON, ChainId.GNOSIS] else 10 - send_native(dev_account, safe_address, amount_to_fill, client.w3) + send_native(dev_account, safe_address, amount_to_fill, web3) tokens_to_transfer = {"usdc": 3500, "dai": 3500, "wbtc": 0.1} if network_info.chain_id is ChainId.GNOSIS: @@ -31,11 +31,11 @@ def fill_dev_account_with_tokens( token_address = ETHAddress(network_info.tokens[token]) amount = tokens_to_transfer[token] swap( - client, + web3, dev_account, amount, native_token_address, token_address, network_info.chain_id, ) - transfer_erc20(client.w3, token_address, dev_account, safe_address, amount) + transfer_erc20(web3, token_address, dev_account, safe_address, amount) diff --git a/autotx/utils/ethereum/helpers/swap_from_eoa.py b/autotx/utils/ethereum/helpers/swap_from_eoa.py index 54a1276..c9cf065 100644 --- a/autotx/utils/ethereum/helpers/swap_from_eoa.py +++ b/autotx/utils/ethereum/helpers/swap_from_eoa.py @@ -1,7 +1,7 @@ from decimal import Decimal from typing import cast from eth_account.signers.local import LocalAccount -from gnosis.eth import EthereumClient +from web3 import Web3 from web3.types import TxParams from autotx.eth_address import ETHAddress @@ -10,7 +10,7 @@ def swap( - client: EthereumClient, + web3: Web3, user: LocalAccount, amount: float, from_token: ETHAddress, @@ -18,7 +18,7 @@ def swap( chain: ChainId, ) -> None: txs = build_swap_transaction( - client.w3, + web3, Decimal(str(amount)), from_token, to_token, @@ -29,19 +29,19 @@ def swap( for tx in txs: del tx.params["gas"] - gas = client.w3.eth.estimate_gas(cast(TxParams, tx.params)) + gas = web3.eth.estimate_gas(cast(TxParams, tx.params)) tx.params.update({"gas": gas}) transaction = user.sign_transaction( # type: ignore { **tx.params, - "nonce": client.w3.eth.get_transaction_count(user.address), + "nonce": web3.eth.get_transaction_count(user.address), } ) - hash = client.w3.eth.send_raw_transaction(transaction.rawTransaction) + hash = web3.eth.send_raw_transaction(transaction.rawTransaction) - receipt = client.w3.eth.wait_for_transaction_receipt(hash) + receipt = web3.eth.wait_for_transaction_receipt(hash) if receipt["status"] == 0: print(f"Transaction to swap {from_token.hex} to {amount} {to_token.hex} failed") diff --git a/autotx/utils/ethereum/lifi/__init__.py b/autotx/utils/ethereum/lifi/__init__.py index ccafabc..5cd288a 100644 --- a/autotx/utils/ethereum/lifi/__init__.py +++ b/autotx/utils/ethereum/lifi/__init__.py @@ -1,8 +1,7 @@ -import json -import os +import asyncio from typing import Any -import requests import re +import aiohttp from autotx.utils.constants import LIFI_API_KEY from autotx.eth_address import ETHAddress @@ -19,9 +18,9 @@ def __init__(self, token_address: str): self.token_address = token_address -def handle_lifi_response(response: requests.Response) -> dict[str, Any]: - response_json: dict[str, Any] = json.loads(response.text) - if response.status_code == 200: +async def handle_lifi_response(response: aiohttp.ClientResponse) -> dict[str, Any]: + response_json: dict[str, Any] = await response.json() + if response.status == 200: return response_json if response_json["code"] == 1011: @@ -30,7 +29,7 @@ def handle_lifi_response(response: requests.Response) -> dict[str, Any]: token_address = match.group() raise TokenNotSupported(token_address) - if response.status_code == 429 or ( + if response.status == 429 or ( response_json["message"] == "Unauthorized" and response_json["code"] == 1005 ): raise LifiApiError("Rate limit exceeded") @@ -50,7 +49,7 @@ class Lifi: BASE_URL = "https://li.quest/v1" @classmethod - def get_quote_to_amount( # type: ignore + async def get_quote_to_amount( cls, from_token: ETHAddress, to_token: ETHAddress, @@ -70,24 +69,28 @@ def get_quote_to_amount( # type: ignore "contractCalls": [], } headers = add_authorization_info_if_provided(params) - attempt_count = 0 - while attempt_count < 2: - response = requests.post(cls.BASE_URL + "/quote/contractCalls", json=params, headers=headers) - try: - return handle_lifi_response(response) - except LifiApiError as e: - if ( - str(e) - == "Fetch quote failed with error: Unable to find quote to match expected output." - and attempt_count < 1 - ): - attempt_count += 1 - continue - else: - raise e + + async with aiohttp.ClientSession() as session: + attempt_count = 0 + while True: + try: + response = await session.post(cls.BASE_URL + "/quote/contractCalls", json=params, headers=headers, timeout=10) + result = await handle_lifi_response(response) + return result + except asyncio.TimeoutError as e: + if attempt_count < 5: + attempt_count += 1 + await asyncio.sleep(0.5) + continue + except Exception as e: + if "No available quotes for the requested transfer" in str(e) or "Unable to find quote to match expected output" in str(e): + if attempt_count < 5: + attempt_count += 1 + await asyncio.sleep(0.5) + continue @classmethod - def get_quote_from_amount( + async def get_quote_from_amount( cls, from_token: ETHAddress, to_token: ETHAddress, @@ -106,5 +109,22 @@ def get_quote_from_amount( "slippage": slippage, } headers = add_authorization_info_if_provided(params) - response = requests.get(cls.BASE_URL + "/quote", params=params, headers=headers) # type: ignore - return handle_lifi_response(response) + + async with aiohttp.ClientSession() as session: + attempt_count = 0 + while True: + try: + response = await session.get(cls.BASE_URL + "/quote", params=params, headers=headers, timeout=10) + result = await handle_lifi_response(response) + return result + except asyncio.TimeoutError as e: + if attempt_count < 5: + attempt_count += 1 + await asyncio.sleep(0.5) + continue + except Exception as e: + if "No available quotes for the requested transfer" in str(e) or "Unable to find quote to match expected output" in str(e): + if attempt_count < 5: + attempt_count += 1 + await asyncio.sleep(0.5) + continue \ No newline at end of file diff --git a/autotx/utils/ethereum/lifi/swap.py b/autotx/utils/ethereum/lifi/swap.py index 7abb5c8..979ef66 100644 --- a/autotx/utils/ethereum/lifi/swap.py +++ b/autotx/utils/ethereum/lifi/swap.py @@ -37,7 +37,7 @@ class QuoteInformation: exchange_name: str -def get_quote( +async def get_quote( token_in_address: ETHAddress, token_in_decimals: int, token_in_symbol: str, @@ -52,7 +52,7 @@ def get_quote( quote: dict[str, Any] | None = None try: if amount_is_output: - quote = Lifi.get_quote_to_amount( + quote = await Lifi.get_quote_to_amount( token_in_address, token_out_address, int(expected_amount * (10**token_out_decimals)), @@ -64,7 +64,7 @@ def get_quote( else: amount_in_integer = int(expected_amount * (10**token_in_decimals)) - quote = Lifi.get_quote_from_amount( + quote = await Lifi.get_quote_from_amount( token_in_address, token_out_address, amount_in_integer, @@ -81,6 +81,13 @@ def get_quote( raise Exception( f"Token {token_out_symbol} is not supported. Please try another one" ) + except Exception as e: + if "The from amount must be greater than zero." in str(e): + if amount_is_output: + raise Exception(f"The specified amount of {token_out_symbol} is too low") + else: + raise Exception(f"The specified amount of {token_in_symbol} is too low") + raise e if not quote: raise Exception("Quote has not been fetched") @@ -104,49 +111,6 @@ 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, @@ -195,7 +159,7 @@ async def a_build_swap_transaction( else token_out.functions.symbol().call() ) - quote = await fetch_quote_with_retries( + quote = await get_quote( token_in_address, token_in_decimals, token_in_symbol, @@ -204,7 +168,7 @@ async def a_build_swap_transaction( token_out_symbol, chain, amount, - is_exact_input, + not is_exact_input, _from, ) transactions: list[Transaction] = [] @@ -289,7 +253,7 @@ async def a_can_build_swap_transaction( else token_out.functions.symbol().call() ) - quote = await fetch_quote_with_retries( + quote = await get_quote( token_in_address, token_in_decimals, token_in_symbol, @@ -298,7 +262,7 @@ async def a_can_build_swap_transaction( token_out_symbol, chain, amount, - is_exact_input, + not is_exact_input, _from, ) if not token_in_is_native: diff --git a/autotx/utils/is_dev_env.py b/autotx/utils/is_dev_env.py index 8ad9473..ed096cb 100644 --- a/autotx/utils/is_dev_env.py +++ b/autotx/utils/is_dev_env.py @@ -1,4 +1,4 @@ import os def is_dev_env() -> bool: - return not os.getenv("SMART_ACCOUNT_ADDRESS") \ No newline at end of file + return not os.getenv("SMART_ACCOUNT_ADDRESS") and not os.getenv("SMART_ACCOUNT_OWNER_PK") \ No newline at end of file diff --git a/autotx/wallets/api_smart_wallet.py b/autotx/wallets/api_smart_wallet.py deleted file mode 100644 index ee2c4b5..0000000 --- a/autotx/wallets/api_smart_wallet.py +++ /dev/null @@ -1,30 +0,0 @@ -from web3 import Web3 -from autotx import db -from autotx.intents import Intent -from autotx.transactions import Transaction -from autotx.utils.ethereum.SafeManager import SafeManager -from autotx.wallets.smart_wallet import SmartWallet - - -class ApiSmartWallet(SmartWallet): - manager: SafeManager - - def __init__(self, web3: Web3, manager: SafeManager, tasks: db.TasksRepository, task_id: str | None = None): - super().__init__(web3, manager.address) - self.task_id = task_id - self.manager = manager - self.tasks = tasks - - def on_intents_prepared(self, intents: list[Intent]) -> None: - if self.task_id is None: - raise ValueError("Task ID is required") - - saved_task = self.tasks.get(self.task_id) - if saved_task is None: - raise ValueError("Task not found") - - saved_task.intents.extend(intents) - self.tasks.update(saved_task) - - 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 deleted file mode 100644 index d472a35..0000000 --- a/autotx/wallets/safe_smart_wallet.py +++ /dev/null @@ -1,26 +0,0 @@ -from autotx.intents import Intent -from autotx.transactions import TransactionBase -from autotx.utils.ethereum import SafeManager -from autotx.wallets.smart_wallet import SmartWallet - - -class SafeSmartWallet(SmartWallet): - manager: SafeManager - auto_submit_tx: bool - - def __init__(self, manager: SafeManager, auto_submit_tx: bool): - super().__init__(manager.client.w3, manager.address) - - self.manager = manager - self.auto_submit_tx = auto_submit_tx - - def on_intents_prepared(self, intents: list[Intent]) -> None: - pass - - async def on_intents_ready(self, intents: list[Intent]) -> bool | str: - transactions: list[TransactionBase] = [] - - for intent in intents: - 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 deleted file mode 100644 index dadcb6d..0000000 --- a/autotx/wallets/smart_wallet.py +++ /dev/null @@ -1,26 +0,0 @@ -from abc import abstractmethod - -from web3 import Web3 -from autotx.intents import Intent -from autotx.utils.ethereum.get_erc20_balance import get_erc20_balance -from autotx.eth_address import ETHAddress -from autotx.utils.ethereum.get_native_balance import get_native_balance - - -class SmartWallet: - def __init__(self, web3: Web3, address: ETHAddress): - self.web3 = web3 - self.address = address - - def on_intents_prepared(self, intents: list[Intent]) -> None: - pass - - @abstractmethod - 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: - if token_address is None: - return get_native_balance(self.web3, self.address) - else: - return get_erc20_balance(self.web3, token_address, self.address) \ No newline at end of file diff --git a/benchmarks.py b/benchmarks.py index a0393ac..06fd69e 100644 --- a/benchmarks.py +++ b/benchmarks.py @@ -62,7 +62,7 @@ def run_test(test_name, iterations, avg_time_across_tests, completed_tests, rema estimated_time_left = remaining_time_current_test + (estimated_avg_time_across_tests * remaining_tests) total_completion_time = datetime.now() + timedelta(seconds=estimated_time_left) - new_costs = os.listdir("costs") + new_costs = os.listdir("costs") if os.path.exists("costs") else [] # Find all new cost files that are not in old costs current_run_costs = list(set(new_costs) - set(old_costs)) for cost_file in current_run_costs: diff --git a/poetry.lock b/poetry.lock index fff9099..7831d5a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -760,13 +760,13 @@ websockets = ["websocket-client (>=1.3.0)"] [[package]] name = "email-validator" -version = "2.1.1" +version = "2.2.0" description = "A robust email address syntax and deliverability validation library." optional = false python-versions = ">=3.8" files = [ - {file = "email_validator-2.1.1-py3-none-any.whl", hash = "sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05"}, - {file = "email_validator-2.1.1.tar.gz", hash = "sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84"}, + {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, + {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, ] [package.dependencies] @@ -928,15 +928,18 @@ test = ["eth-hash[pycryptodome]", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "eth-typing" -version = "4.2.3" +version = "4.3.1" description = "eth-typing: Common type annotations for ethereum python packages" optional = false python-versions = "<4,>=3.8" files = [ - {file = "eth_typing-4.2.3-py3-none-any.whl", hash = "sha256:b2df49fa89d2e85f2cc3fb1c903b0cd183d524f7a045e3db8cc720cf41adcd3d"}, - {file = "eth_typing-4.2.3.tar.gz", hash = "sha256:8ee3ae7d4136d14fcb955c34f9dbef8e52170984d4dc68c0ab0d61621eab29d8"}, + {file = "eth_typing-4.3.1-py3-none-any.whl", hash = "sha256:b4d7cee912c7779da75da4b42fa61475c1089d35a4df5081a786eaa29d5f6865"}, + {file = "eth_typing-4.3.1.tar.gz", hash = "sha256:4504559c87a9f71f4b99aa5a1e0549adaa7f192cbf8e37a295acfcddb1b5412d"}, ] +[package.dependencies] +typing-extensions = ">=4.5.0" + [package.extras] dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "ipython", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] docs = ["sphinx (>=6.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] @@ -1147,13 +1150,13 @@ files = [ [[package]] name = "gotrue" -version = "2.4.4" +version = "2.5.4" description = "Python Client Library for Supabase Auth" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "gotrue-2.4.4-py3-none-any.whl", hash = "sha256:2eef9c962820b114d355cd0690ec6aaeb03374efe8c6c75d2265d34483e9a67e"}, - {file = "gotrue-2.4.4.tar.gz", hash = "sha256:ba4652e3adb39c341a3a4f6a93ebe56b54e25b0959c66d1b38fd40fe4d75bff5"}, + {file = "gotrue-2.5.4-py3-none-any.whl", hash = "sha256:6f45003bc73cdee612a2d0be79cffed39c91cc8ad43a7440c02c320c7ad03a8e"}, + {file = "gotrue-2.5.4.tar.gz", hash = "sha256:acf0644a2e5d1bd70f66452361bfea4ba9621a0354a13154a333671a4c751c53"}, ] [package.dependencies] @@ -1357,12 +1360,12 @@ referencing = ">=0.31.0" [[package]] name = "llama-cpp-python" -version = "0.2.78" +version = "0.2.79" description = "Python bindings for the llama.cpp library" optional = false python-versions = ">=3.8" files = [ - {file = "llama_cpp_python-0.2.78.tar.gz", hash = "sha256:3df7cfde84287faaf29675fba8939060c3ab3f0ce8db875dabf7df5d83bd8751"}, + {file = "llama_cpp_python-0.2.79.tar.gz", hash = "sha256:19406225a37d816dc2fb911ba8e3ff2a48880dd79754820c55ed85ebc8238da4"}, ] [package.dependencies] @@ -1676,38 +1679,38 @@ files = [ [[package]] name = "mypy" -version = "1.10.0" +version = "1.10.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, - {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, - {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, - {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, - {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, - {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, - {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, - {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, - {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, - {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, - {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, - {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, - {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, - {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, - {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, - {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, - {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, - {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, - {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, - {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, - {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, - {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, - {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, - {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, - {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, - {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, - {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, + {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, + {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, + {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"}, + {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"}, + {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, + {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, + {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, + {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, + {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, + {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"}, + {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"}, + {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"}, + {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"}, + {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"}, + {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"}, + {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"}, + {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"}, + {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, + {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, ] [package.dependencies] @@ -1790,13 +1793,13 @@ files = [ [[package]] name = "openai" -version = "1.32.0" +version = "1.35.5" description = "The official Python library for the openai API" optional = false python-versions = ">=3.7.1" files = [ - {file = "openai-1.32.0-py3-none-any.whl", hash = "sha256:953d57669f309002044fd2f678aba9f07a43256d74b3b00cd04afb5b185568ea"}, - {file = "openai-1.32.0.tar.gz", hash = "sha256:a6df15a7ab9344b1bc2bc8d83639f68b7a7e2453c0f5e50c1666547eee86f0bd"}, + {file = "openai-1.35.5-py3-none-any.whl", hash = "sha256:28d92503c6e4b6a32a89277b36693023ef41f60922a4b5c8c621e8c5697ae3a6"}, + {file = "openai-1.35.5.tar.gz", hash = "sha256:67ef289ae22d350cbf9381d83ae82c4e3596d71b7ad1cc886143554ee12fe0c9"}, ] [package.dependencies] @@ -1813,68 +1816,68 @@ datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] [[package]] name = "orjson" -version = "3.10.3" +version = "3.10.5" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" files = [ - {file = "orjson-3.10.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9fb6c3f9f5490a3eb4ddd46fc1b6eadb0d6fc16fb3f07320149c3286a1409dd8"}, - {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:252124b198662eee80428f1af8c63f7ff077c88723fe206a25df8dc57a57b1fa"}, - {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9f3e87733823089a338ef9bbf363ef4de45e5c599a9bf50a7a9b82e86d0228da"}, - {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8334c0d87103bb9fbbe59b78129f1f40d1d1e8355bbed2ca71853af15fa4ed3"}, - {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1952c03439e4dce23482ac846e7961f9d4ec62086eb98ae76d97bd41d72644d7"}, - {file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0403ed9c706dcd2809f1600ed18f4aae50be263bd7112e54b50e2c2bc3ebd6d"}, - {file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:382e52aa4270a037d41f325e7d1dfa395b7de0c367800b6f337d8157367bf3a7"}, - {file = "orjson-3.10.3-cp310-none-win32.whl", hash = "sha256:be2aab54313752c04f2cbaab4515291ef5af8c2256ce22abc007f89f42f49109"}, - {file = "orjson-3.10.3-cp310-none-win_amd64.whl", hash = "sha256:416b195f78ae461601893f482287cee1e3059ec49b4f99479aedf22a20b1098b"}, - {file = "orjson-3.10.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:73100d9abbbe730331f2242c1fc0bcb46a3ea3b4ae3348847e5a141265479700"}, - {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:544a12eee96e3ab828dbfcb4d5a0023aa971b27143a1d35dc214c176fdfb29b3"}, - {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:520de5e2ef0b4ae546bea25129d6c7c74edb43fc6cf5213f511a927f2b28148b"}, - {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccaa0a401fc02e8828a5bedfd80f8cd389d24f65e5ca3954d72c6582495b4bcf"}, - {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7bc9e8bc11bac40f905640acd41cbeaa87209e7e1f57ade386da658092dc16"}, - {file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3582b34b70543a1ed6944aca75e219e1192661a63da4d039d088a09c67543b08"}, - {file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c23dfa91481de880890d17aa7b91d586a4746a4c2aa9a145bebdbaf233768d5"}, - {file = "orjson-3.10.3-cp311-none-win32.whl", hash = "sha256:1770e2a0eae728b050705206d84eda8b074b65ee835e7f85c919f5705b006c9b"}, - {file = "orjson-3.10.3-cp311-none-win_amd64.whl", hash = "sha256:93433b3c1f852660eb5abdc1f4dd0ced2be031ba30900433223b28ee0140cde5"}, - {file = "orjson-3.10.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a39aa73e53bec8d410875683bfa3a8edf61e5a1c7bb4014f65f81d36467ea098"}, - {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0943a96b3fa09bee1afdfccc2cb236c9c64715afa375b2af296c73d91c23eab2"}, - {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e852baafceff8da3c9defae29414cc8513a1586ad93e45f27b89a639c68e8176"}, - {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18566beb5acd76f3769c1d1a7ec06cdb81edc4d55d2765fb677e3eaa10fa99e0"}, - {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bd2218d5a3aa43060efe649ec564ebedec8ce6ae0a43654b81376216d5ebd42"}, - {file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cf20465e74c6e17a104ecf01bf8cd3b7b252565b4ccee4548f18b012ff2f8069"}, - {file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba7f67aa7f983c4345eeda16054a4677289011a478ca947cd69c0a86ea45e534"}, - {file = "orjson-3.10.3-cp312-none-win32.whl", hash = "sha256:17e0713fc159abc261eea0f4feda611d32eabc35708b74bef6ad44f6c78d5ea0"}, - {file = "orjson-3.10.3-cp312-none-win_amd64.whl", hash = "sha256:4c895383b1ec42b017dd2c75ae8a5b862fc489006afde06f14afbdd0309b2af0"}, - {file = "orjson-3.10.3-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:be2719e5041e9fb76c8c2c06b9600fe8e8584e6980061ff88dcbc2691a16d20d"}, - {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0175a5798bdc878956099f5c54b9837cb62cfbf5d0b86ba6d77e43861bcec2"}, - {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:978be58a68ade24f1af7758626806e13cff7748a677faf95fbb298359aa1e20d"}, - {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16bda83b5c61586f6f788333d3cf3ed19015e3b9019188c56983b5a299210eb5"}, - {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ad1f26bea425041e0a1adad34630c4825a9e3adec49079b1fb6ac8d36f8b754"}, - {file = "orjson-3.10.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9e253498bee561fe85d6325ba55ff2ff08fb5e7184cd6a4d7754133bd19c9195"}, - {file = "orjson-3.10.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0a62f9968bab8a676a164263e485f30a0b748255ee2f4ae49a0224be95f4532b"}, - {file = "orjson-3.10.3-cp38-none-win32.whl", hash = "sha256:8d0b84403d287d4bfa9bf7d1dc298d5c1c5d9f444f3737929a66f2fe4fb8f134"}, - {file = "orjson-3.10.3-cp38-none-win_amd64.whl", hash = "sha256:8bc7a4df90da5d535e18157220d7915780d07198b54f4de0110eca6b6c11e290"}, - {file = "orjson-3.10.3-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9059d15c30e675a58fdcd6f95465c1522b8426e092de9fff20edebfdc15e1cb0"}, - {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d40c7f7938c9c2b934b297412c067936d0b54e4b8ab916fd1a9eb8f54c02294"}, - {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4a654ec1de8fdaae1d80d55cee65893cb06494e124681ab335218be6a0691e7"}, - {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:831c6ef73f9aa53c5f40ae8f949ff7681b38eaddb6904aab89dca4d85099cb78"}, - {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99b880d7e34542db89f48d14ddecbd26f06838b12427d5a25d71baceb5ba119d"}, - {file = "orjson-3.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2e5e176c994ce4bd434d7aafb9ecc893c15f347d3d2bbd8e7ce0b63071c52e25"}, - {file = "orjson-3.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b69a58a37dab856491bf2d3bbf259775fdce262b727f96aafbda359cb1d114d8"}, - {file = "orjson-3.10.3-cp39-none-win32.whl", hash = "sha256:b8d4d1a6868cde356f1402c8faeb50d62cee765a1f7ffcfd6de732ab0581e063"}, - {file = "orjson-3.10.3-cp39-none-win_amd64.whl", hash = "sha256:5102f50c5fc46d94f2033fe00d392588564378260d64377aec702f21a7a22912"}, - {file = "orjson-3.10.3.tar.gz", hash = "sha256:2b166507acae7ba2f7c315dcf185a9111ad5e992ac81f2d507aac39193c2c818"}, + {file = "orjson-3.10.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:545d493c1f560d5ccfc134803ceb8955a14c3fcb47bbb4b2fee0232646d0b932"}, + {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4324929c2dd917598212bfd554757feca3e5e0fa60da08be11b4aa8b90013c1"}, + {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c13ca5e2ddded0ce6a927ea5a9f27cae77eee4c75547b4297252cb20c4d30e6"}, + {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6c8e30adfa52c025f042a87f450a6b9ea29649d828e0fec4858ed5e6caecf63"}, + {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:338fd4f071b242f26e9ca802f443edc588fa4ab60bfa81f38beaedf42eda226c"}, + {file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6970ed7a3126cfed873c5d21ece1cd5d6f83ca6c9afb71bbae21a0b034588d96"}, + {file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:235dadefb793ad12f7fa11e98a480db1f7c6469ff9e3da5e73c7809c700d746b"}, + {file = "orjson-3.10.5-cp310-none-win32.whl", hash = "sha256:be79e2393679eda6a590638abda16d167754393f5d0850dcbca2d0c3735cebe2"}, + {file = "orjson-3.10.5-cp310-none-win_amd64.whl", hash = "sha256:c4a65310ccb5c9910c47b078ba78e2787cb3878cdded1702ac3d0da71ddc5228"}, + {file = "orjson-3.10.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cdf7365063e80899ae3a697def1277c17a7df7ccfc979990a403dfe77bb54d40"}, + {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b68742c469745d0e6ca5724506858f75e2f1e5b59a4315861f9e2b1df77775a"}, + {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d10cc1b594951522e35a3463da19e899abe6ca95f3c84c69e9e901e0bd93d38"}, + {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcbe82b35d1ac43b0d84072408330fd3295c2896973112d495e7234f7e3da2e1"}, + {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c0eb7e0c75e1e486c7563fe231b40fdd658a035ae125c6ba651ca3b07936f5"}, + {file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:53ed1c879b10de56f35daf06dbc4a0d9a5db98f6ee853c2dbd3ee9d13e6f302f"}, + {file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:099e81a5975237fda3100f918839af95f42f981447ba8f47adb7b6a3cdb078fa"}, + {file = "orjson-3.10.5-cp311-none-win32.whl", hash = "sha256:1146bf85ea37ac421594107195db8bc77104f74bc83e8ee21a2e58596bfb2f04"}, + {file = "orjson-3.10.5-cp311-none-win_amd64.whl", hash = "sha256:36a10f43c5f3a55c2f680efe07aa93ef4a342d2960dd2b1b7ea2dd764fe4a37c"}, + {file = "orjson-3.10.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:68f85ecae7af14a585a563ac741b0547a3f291de81cd1e20903e79f25170458f"}, + {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28afa96f496474ce60d3340fe8d9a263aa93ea01201cd2bad844c45cd21f5268"}, + {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cd684927af3e11b6e754df80b9ffafd9fb6adcaa9d3e8fdd5891be5a5cad51e"}, + {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d21b9983da032505f7050795e98b5d9eee0df903258951566ecc358f6696969"}, + {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ad1de7fef79736dde8c3554e75361ec351158a906d747bd901a52a5c9c8d24b"}, + {file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d97531cdfe9bdd76d492e69800afd97e5930cb0da6a825646667b2c6c6c0211"}, + {file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d69858c32f09c3e1ce44b617b3ebba1aba030e777000ebdf72b0d8e365d0b2b3"}, + {file = "orjson-3.10.5-cp312-none-win32.whl", hash = "sha256:64c9cc089f127e5875901ac05e5c25aa13cfa5dbbbd9602bda51e5c611d6e3e2"}, + {file = "orjson-3.10.5-cp312-none-win_amd64.whl", hash = "sha256:b2efbd67feff8c1f7728937c0d7f6ca8c25ec81373dc8db4ef394c1d93d13dc5"}, + {file = "orjson-3.10.5-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:03b565c3b93f5d6e001db48b747d31ea3819b89abf041ee10ac6988886d18e01"}, + {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:584c902ec19ab7928fd5add1783c909094cc53f31ac7acfada817b0847975f26"}, + {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a35455cc0b0b3a1eaf67224035f5388591ec72b9b6136d66b49a553ce9eb1e6"}, + {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1670fe88b116c2745a3a30b0f099b699a02bb3482c2591514baf5433819e4f4d"}, + {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185c394ef45b18b9a7d8e8f333606e2e8194a50c6e3c664215aae8cf42c5385e"}, + {file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ca0b3a94ac8d3886c9581b9f9de3ce858263865fdaa383fbc31c310b9eac07c9"}, + {file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dfc91d4720d48e2a709e9c368d5125b4b5899dced34b5400c3837dadc7d6271b"}, + {file = "orjson-3.10.5-cp38-none-win32.whl", hash = "sha256:c05f16701ab2a4ca146d0bca950af254cb7c02f3c01fca8efbbad82d23b3d9d4"}, + {file = "orjson-3.10.5-cp38-none-win_amd64.whl", hash = "sha256:8a11d459338f96a9aa7f232ba95679fc0c7cedbd1b990d736467894210205c09"}, + {file = "orjson-3.10.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:85c89131d7b3218db1b24c4abecea92fd6c7f9fab87441cfc342d3acc725d807"}, + {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66215277a230c456f9038d5e2d84778141643207f85336ef8d2a9da26bd7ca"}, + {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51bbcdea96cdefa4a9b4461e690c75ad4e33796530d182bdd5c38980202c134a"}, + {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbead71dbe65f959b7bd8cf91e0e11d5338033eba34c114f69078d59827ee139"}, + {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df58d206e78c40da118a8c14fc189207fffdcb1f21b3b4c9c0c18e839b5a214"}, + {file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c4057c3b511bb8aef605616bd3f1f002a697c7e4da6adf095ca5b84c0fd43595"}, + {file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b39e006b00c57125ab974362e740c14a0c6a66ff695bff44615dcf4a70ce2b86"}, + {file = "orjson-3.10.5-cp39-none-win32.whl", hash = "sha256:eded5138cc565a9d618e111c6d5c2547bbdd951114eb822f7f6309e04db0fb47"}, + {file = "orjson-3.10.5-cp39-none-win_amd64.whl", hash = "sha256:cc28e90a7cae7fcba2493953cff61da5a52950e78dc2dacfe931a317ee3d8de7"}, + {file = "orjson-3.10.5.tar.gz", hash = "sha256:7a5baef8a4284405d96c90c7c62b755e9ef1ada84c2406c24a9ebec86b89f46d"}, ] [[package]] name = "packaging" -version = "24.0" +version = "24.1" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] @@ -1925,22 +1928,22 @@ strenum = ">=0.4.9,<0.5.0" [[package]] name = "protobuf" -version = "5.27.1" +version = "5.27.2" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "protobuf-5.27.1-cp310-abi3-win32.whl", hash = "sha256:3adc15ec0ff35c5b2d0992f9345b04a540c1e73bfee3ff1643db43cc1d734333"}, - {file = "protobuf-5.27.1-cp310-abi3-win_amd64.whl", hash = "sha256:25236b69ab4ce1bec413fd4b68a15ef8141794427e0b4dc173e9d5d9dffc3bcd"}, - {file = "protobuf-5.27.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4e38fc29d7df32e01a41cf118b5a968b1efd46b9c41ff515234e794011c78b17"}, - {file = "protobuf-5.27.1-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:917ed03c3eb8a2d51c3496359f5b53b4e4b7e40edfbdd3d3f34336e0eef6825a"}, - {file = "protobuf-5.27.1-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:ee52874a9e69a30271649be88ecbe69d374232e8fd0b4e4b0aaaa87f429f1631"}, - {file = "protobuf-5.27.1-cp38-cp38-win32.whl", hash = "sha256:7a97b9c5aed86b9ca289eb5148df6c208ab5bb6906930590961e08f097258107"}, - {file = "protobuf-5.27.1-cp38-cp38-win_amd64.whl", hash = "sha256:f6abd0f69968792da7460d3c2cfa7d94fd74e1c21df321eb6345b963f9ec3d8d"}, - {file = "protobuf-5.27.1-cp39-cp39-win32.whl", hash = "sha256:dfddb7537f789002cc4eb00752c92e67885badcc7005566f2c5de9d969d3282d"}, - {file = "protobuf-5.27.1-cp39-cp39-win_amd64.whl", hash = "sha256:39309898b912ca6febb0084ea912e976482834f401be35840a008da12d189340"}, - {file = "protobuf-5.27.1-py3-none-any.whl", hash = "sha256:4ac7249a1530a2ed50e24201d6630125ced04b30619262f06224616e0030b6cf"}, - {file = "protobuf-5.27.1.tar.gz", hash = "sha256:df5e5b8e39b7d1c25b186ffdf9f44f40f810bbcc9d2b71d9d3156fee5a9adf15"}, + {file = "protobuf-5.27.2-cp310-abi3-win32.whl", hash = "sha256:354d84fac2b0d76062e9b3221f4abbbacdfd2a4d8af36bab0474f3a0bb30ab38"}, + {file = "protobuf-5.27.2-cp310-abi3-win_amd64.whl", hash = "sha256:0e341109c609749d501986b835f667c6e1e24531096cff9d34ae411595e26505"}, + {file = "protobuf-5.27.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a109916aaac42bff84702fb5187f3edadbc7c97fc2c99c5ff81dd15dcce0d1e5"}, + {file = "protobuf-5.27.2-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:176c12b1f1c880bf7a76d9f7c75822b6a2bc3db2d28baa4d300e8ce4cde7409b"}, + {file = "protobuf-5.27.2-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:b848dbe1d57ed7c191dfc4ea64b8b004a3f9ece4bf4d0d80a367b76df20bf36e"}, + {file = "protobuf-5.27.2-cp38-cp38-win32.whl", hash = "sha256:4fadd8d83e1992eed0248bc50a4a6361dc31bcccc84388c54c86e530b7f58863"}, + {file = "protobuf-5.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:610e700f02469c4a997e58e328cac6f305f649826853813177e6290416e846c6"}, + {file = "protobuf-5.27.2-cp39-cp39-win32.whl", hash = "sha256:9e8f199bf7f97bd7ecebffcae45ebf9527603549b2b562df0fbc6d4d688f14ca"}, + {file = "protobuf-5.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:7fc3add9e6003e026da5fc9e59b131b8f22b428b991ccd53e2af8071687b4fce"}, + {file = "protobuf-5.27.2-py3-none-any.whl", hash = "sha256:54330f07e4949d09614707c48b06d1a22f8ffb5763c159efd5c0928326a91470"}, + {file = "protobuf-5.27.2.tar.gz", hash = "sha256:f3ecdef226b9af856075f28227ff2c90ce3a594d092c39bee5513573f25e2714"}, ] [[package]] @@ -1997,13 +2000,13 @@ test = ["factory-boy (>=3.0.0)", "hypothesis (>=6,<7)", "pytest (>=7.0.0)", "pyt [[package]] name = "pyautogen" -version = "0.2.28" +version = "0.2.31" description = "Enabling Next-Gen LLM Applications via Multi-Agent Conversation Framework" optional = false python-versions = "<3.13,>=3.8" files = [ - {file = "pyautogen-0.2.28-py3-none-any.whl", hash = "sha256:69dffa4053096f496a50c8a252bbe23105b58fd6ffbb422fa8c043ecf3fc732b"}, - {file = "pyautogen-0.2.28.tar.gz", hash = "sha256:f74686a981f2b6046a9cf6aff5a5e61615ec60d5559a49e7474467fbdf4e077b"}, + {file = "pyautogen-0.2.31-py3-none-any.whl", hash = "sha256:f1268dbbb191756a105815e1f46a6c6786c3059784b04a3831f4c24b9429c8c7"}, + {file = "pyautogen-0.2.31.tar.gz", hash = "sha256:157a6d2c68f1fe0c8d1e07c6886f97962dc85effd8da4baad7d7804ad284cc76"}, ] [package.dependencies] @@ -2019,21 +2022,24 @@ termcolor = "*" tiktoken = "*" [package.extras] -autobuild = ["chromadb", "huggingface-hub", "sentence-transformers"] +anthropic = ["anthropic (>=0.23.1)"] +autobuild = ["chromadb", "huggingface-hub", "pysqlite3", "sentence-transformers"] blendsearch = ["flaml[blendsearch]"] cosmosdb = ["azure-cosmos (>=4.2.0)"] -gemini = ["google-generativeai (>=0.5,<1)", "pillow", "pydantic"] +gemini = ["google-auth", "google-cloud-aiplatform", "google-generativeai (>=0.5,<1)", "pillow", "pydantic"] graph = ["matplotlib", "networkx"] jupyter-executor = ["ipykernel (>=6.29.0)", "jupyter-client (>=8.6.0)", "jupyter-kernel-gateway", "requests", "websocket-client"] lmm = ["pillow", "replicate"] long-context = ["llmlingua (<0.3)"] mathchat = ["pydantic (==1.10.9)", "sympy", "wolframalpha"] +mistral = ["mistralai (>=0.2.0)"] redis = ["redis"] retrievechat = ["beautifulsoup4", "chromadb", "ipython", "markdownify", "protobuf (==4.25.3)", "pypdf", "sentence-transformers"] retrievechat-pgvector = ["beautifulsoup4", "chromadb", "ipython", "markdownify", "pgvector (>=0.2.5)", "protobuf (==4.25.3)", "psycopg (>=3.1.18)", "pypdf", "sentence-transformers"] -retrievechat-qdrant = ["beautifulsoup4", "chromadb", "ipython", "markdownify", "protobuf (==4.25.3)", "pypdf", "qdrant-client[fastembed]", "sentence-transformers"] +retrievechat-qdrant = ["beautifulsoup4", "chromadb", "ipython", "markdownify", "protobuf (==4.25.3)", "pypdf", "qdrant-client[fastembed] (<1.9.2)", "sentence-transformers"] teachable = ["chromadb"] test = ["ipykernel", "nbconvert", "nbformat", "pandas", "pre-commit", "pytest (>=6.1.1,<8)", "pytest-asyncio", "pytest-cov (>=5)"] +together = ["together (>=1.2)"] types = ["ipykernel (>=6.29.0)", "jupyter-client (>=8.6.0)", "jupyter-kernel-gateway", "mypy (==1.9.0)", "pytest (>=6.1.1,<8)", "requests", "websocket-client"] websockets = ["websockets (>=12.0,<13)"] websurfer = ["beautifulsoup4", "markdownify", "pathvalidate", "pdfminer.six"] @@ -2081,13 +2087,13 @@ files = [ [[package]] name = "pydantic" -version = "2.7.3" +version = "2.7.4" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.7.3-py3-none-any.whl", hash = "sha256:ea91b002777bf643bb20dd717c028ec43216b24a6001a280f83877fd2655d0b4"}, - {file = "pydantic-2.7.3.tar.gz", hash = "sha256:c46c76a40bb1296728d7a8b99aa73dd70a48c3510111ff290034f860c99c419e"}, + {file = "pydantic-2.7.4-py3-none-any.whl", hash = "sha256:ee8538d41ccb9c0a9ad3e0e5f07bf15ed8015b481ced539a1759d8cc89ae90d0"}, + {file = "pydantic-2.7.4.tar.gz", hash = "sha256:0c84efd9548d545f63ac0060c1e4d39bb9b14db8b3c0652338aecc07b5adec52"}, ] [package.dependencies] @@ -2205,13 +2211,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyright" -version = "1.1.366" +version = "1.1.369" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.366-py3-none-any.whl", hash = "sha256:c09e73ccc894976bcd6d6a5784aa84d724dbd9ceb7b873b39d475ca61c2de071"}, - {file = "pyright-1.1.366.tar.gz", hash = "sha256:10e4d60be411f6d960cd39b0b58bf2ff76f2c83b9aeb102ffa9d9fda2e1303cb"}, + {file = "pyright-1.1.369-py3-none-any.whl", hash = "sha256:06d5167a8d7be62523ced0265c5d2f1e022e110caf57a25d92f50fb2d07bcda0"}, + {file = "pyright-1.1.369.tar.gz", hash = "sha256:ad290710072d021e213b98cc7a2f90ae3a48609ef5b978f749346d1a47eb9af8"}, ] [package.dependencies] @@ -2243,6 +2249,20 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-timeout" +version = "2.3.1" +description = "pytest plugin to abort hanging tests" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9"}, + {file = "pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + [[package]] name = "pytest-vcr" version = "1.0.2" @@ -2395,18 +2415,18 @@ files = [ [[package]] name = "realtime" -version = "1.0.5" +version = "1.0.6" description = "" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "realtime-1.0.5-py3-none-any.whl", hash = "sha256:93342fbcb8812ed8d81733f2782c1199376f0471e78014675420c7d31f2f327d"}, - {file = "realtime-1.0.5.tar.gz", hash = "sha256:4abbb3218b6ce8bd8d9d3b1112661d325e36ceab67a0e918673d0fd8fca04fb1"}, + {file = "realtime-1.0.6-py3-none-any.whl", hash = "sha256:c66918a106d8ef348d1821f2dbf6683d8833825580d95b2fdea9995406b42838"}, + {file = "realtime-1.0.6.tar.gz", hash = "sha256:2be0d8a6305513d423604ee319216108fc20105cb7438922d5c8958c48f40a47"}, ] [package.dependencies] python-dateutil = ">=2.8.1,<3.0.0" -typing-extensions = ">=4.11.0,<5.0.0" +typing-extensions = ">=4.12.2,<5.0.0" websockets = ">=11,<13" [[package]] @@ -2805,13 +2825,13 @@ test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"] [[package]] name = "supabase" -version = "2.5.0" +version = "2.5.1" description = "Supabase client for Python." optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "supabase-2.5.0-py3-none-any.whl", hash = "sha256:13e5ed9e9377a1a69e70ad18ed7b82997cf13ffcd28173952f7503e4d5067771"}, - {file = "supabase-2.5.0.tar.gz", hash = "sha256:133dc832dfdd617f2f90ac5b52664df96ac8a9302ac6656ee769dc3f545812f0"}, + {file = "supabase-2.5.1-py3-none-any.whl", hash = "sha256:74a1f24f04fede1967ef084b50dea688228f7b10eb2f9d73350fe2251a865188"}, + {file = "supabase-2.5.1.tar.gz", hash = "sha256:c50e0eba5b03de3abd5ac0f887957ca43558ba44c4d17bb44e73ec454b41734c"}, ] [package.dependencies] @@ -2986,13 +3006,13 @@ typing-extensions = ">=3.7.4.3" [[package]] name = "typing-extensions" -version = "4.12.1" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.12.1-py3-none-any.whl", hash = "sha256:6024b58b69089e5a89c347397254e35f1bf02a907728ec7fee9bf0fe837d203a"}, - {file = "typing_extensions-4.12.1.tar.gz", hash = "sha256:915f5e35ff76f56588223f15fdd5938f9a1cf9195c0de25130c627e4d597f6d1"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] @@ -3084,13 +3104,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.1" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] @@ -3274,13 +3294,13 @@ anyio = ">=3.0.0" [[package]] name = "web3" -version = "6.19.0" +version = "6.20.0" description = "web3.py" optional = false python-versions = ">=3.7.2" files = [ - {file = "web3-6.19.0-py3-none-any.whl", hash = "sha256:fb39683d6aa7586ce0ab0be4be392f8acb62c2503958079d61b59f2a0b883718"}, - {file = "web3-6.19.0.tar.gz", hash = "sha256:d27fbd4ac5aa70d0e0c516bd3e3b802fbe74bc159b407c34052d9301b400f757"}, + {file = "web3-6.20.0-py3-none-any.whl", hash = "sha256:ec09882d21378b688210cf29385e82b604bdc18fe5c2e238bf3b5fe2a6e6dbbc"}, + {file = "web3-6.20.0.tar.gz", hash = "sha256:b04725517502cad4f15e39356eaf7c4fcb0127c7728f83eec8cbafb5b6455f33"}, ] [package.dependencies] @@ -3572,4 +3592,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "5dfc1bc10f28a24e09c829fbdf2469dfcd0ab7dcd8c51312e7f15e8ffd02601f" +content-hash = "c358c59f6f591afe7264818076545c27f31d51e67663bd4b67c8227bfe8689ab" diff --git a/pyproject.toml b/pyproject.toml index 7678976..df271af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ python = ">=3.10,<3.13" pyright = "^1.1.350" click = "^8.1.7" eth-account = "^0.11.0" -requests = "^2.31.0" python-dotenv = "^1.0.0" coingecko = "^0.13" pyautogen = "^0.2.27" @@ -20,6 +19,8 @@ safe-eth-py = "^5.8.0" uvicorn = "^0.29.0" supabase = "^2.5.0" llama-cpp-python = "^0.2.78" +aiohttp = "^3.9.5" +pytest-timeout = "^2.3.1" [tool.poetry.group.dev.dependencies] mypy = "^1.8.0" @@ -31,6 +32,8 @@ pytest-vcr = "^1.0.2" [tool.poetry.scripts] start-devnet = "autotx.chain_fork:start" stop-devnet = "autotx.chain_fork:stop" +start-smart-account-api = "autotx.smart_account_api:start" +stop-smart-account-api = "autotx.smart_account_api:stop" ask = "autotx.cli:run" agent = "autotx.cli:agent" serve = "autotx.cli:serve" diff --git a/smart-account-api.Dockerfile b/smart-account-api.Dockerfile new file mode 100644 index 0000000..13e609b --- /dev/null +++ b/smart-account-api.Dockerfile @@ -0,0 +1,15 @@ +FROM node:20 + +WORKDIR /usr/src/app + +ENV APP_HOME /root +WORKDIR $APP_HOME +COPY /smart_account_api $APP_HOME +COPY .env $APP_HOME/.env + +RUN yarn install + +RUN yarn build + +EXPOSE 7080 +CMD ["yarn", "start"] \ No newline at end of file diff --git a/smart_account_api/.nvmrc b/smart_account_api/.nvmrc new file mode 100644 index 0000000..0fdd238 --- /dev/null +++ b/smart_account_api/.nvmrc @@ -0,0 +1 @@ +v20.10 \ No newline at end of file diff --git a/smart_account_api/package.json b/smart_account_api/package.json new file mode 100644 index 0000000..4a06403 --- /dev/null +++ b/smart_account_api/package.json @@ -0,0 +1,27 @@ +{ + "name": "smart-account-api", + "version": "1.0.0", + "main": "main.js", + "license": "MIT", + "scripts": { + "build": "npx tsc", + "start": "node dist/index.js", + "dev": "nodemon src/index.ts" + }, + "dependencies": { + "@biconomy/account": "^4.4.6", + "dotenv": "^16.4.5", + "entry-point-gas-estimations": "^0.0.19", + "ethers": "^6.13.1", + "express": "^4.19.2", + "viem": "^2.15.1" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.14.6", + "nodemon": "^3.1.4", + "ts-node": "^10.9.2", + "tsc": "^2.0.4", + "typescript": "^5.4.5" + } +} diff --git a/smart_account_api/src/account-abstraction.ts b/smart_account_api/src/account-abstraction.ts new file mode 100644 index 0000000..600d0cc --- /dev/null +++ b/smart_account_api/src/account-abstraction.ts @@ -0,0 +1,27 @@ +import { BiconomySmartAccountV2, createSmartAccountClient } from "@biconomy/account"; +import { WalletClient, createWalletClient, Hex, http } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { getNetwork } from "./networks"; + +export async function initClientWithAccount(ownerPk: string, chainId: number): Promise<{ + client: WalletClient; + smartAccount: BiconomySmartAccountV2; + }> { + const { chain, bundlerUrl } = getNetwork(chainId); + + const client = createWalletClient({ + account: privateKeyToAccount(ownerPk as Hex), + chain: chain, + transport: http(), + }); + + const smartAccount = await createSmartAccountClient({ + signer: client, + bundlerUrl, + }); + + return { + client, + smartAccount, + } +}; diff --git a/smart_account_api/src/constants.ts b/smart_account_api/src/constants.ts new file mode 100644 index 0000000..4fc4497 --- /dev/null +++ b/smart_account_api/src/constants.ts @@ -0,0 +1,9 @@ +export const CHAIN_RPC_URL = process.env.CHAIN_RPC_URL as string; +export const SMART_ACCOUNT_OWNER_PK = process.env.SMART_ACCOUNT_OWNER_PK as string; +if (!CHAIN_RPC_URL) { + throw new Error("CHAIN_RPC_URL is not set"); +} + +if (!SMART_ACCOUNT_OWNER_PK) { + throw new Error("SMART_ACCOUNT_OWNER_PK is not set"); +} diff --git a/smart_account_api/src/index.ts b/smart_account_api/src/index.ts new file mode 100644 index 0000000..423bb1d --- /dev/null +++ b/smart_account_api/src/index.ts @@ -0,0 +1,112 @@ +import express, { Express, NextFunction, Request, Response } from "express"; +import dotenv from "dotenv"; +import { Hex, Transaction } from "@biconomy/account"; +import { initClientWithAccount } from "./account-abstraction"; +import { SMART_ACCOUNT_OWNER_PK } from "./constants"; +import { privateKeyToAccount } from "viem/accounts"; + +dotenv.config(); + +const app: Express = express(); +const port = process.env.PORT || 7080; + +app.use(express.json()); + +app.all('*', handleError(async (req: Request, res: Response, next: NextFunction) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); + + //Trim and redirect multiple slashes in URL + if (req.url.match(/[/]{2,}/g)) { + req.url = req.url.replace(/[/]+/g, '/'); + res.redirect(req.url); + return; + } + + if (req.method === 'OPTIONS') { + res.send(200); + } else { + console.log(`Request: ${req.method} --- ${req.url}`); + next(); + } +})); + +app.use((req: Request, res: Response, next: NextFunction) => { + res.on('finish', () => { + console.log(`Response: ${req.method} ${res.statusCode} ${req.url}`); + }); + next(); +}); + +app.get("/api/v1/account/address", handleError(async (req: Request, res: Response) => { + const chainId = parseInt(req.query.chainId as string); + + const { smartAccount } = await initClientWithAccount(SMART_ACCOUNT_OWNER_PK, chainId); + + const smartAccountAddress = await smartAccount.getAccountAddress(); + + const result = smartAccountAddress; + + res.json(result); +})); + +app.post("/api/v1/account/deploy", handleError(async (req: Request, res: Response) => { + const chainId = parseInt(req.query.chainId as string); + + const { smartAccount } = await initClientWithAccount(SMART_ACCOUNT_OWNER_PK, chainId); + + const response = await smartAccount.deploy(); + + const receipt = await response.wait(); + + res.json(receipt.receipt.transactionHash) +})); + +app.post("/api/v1/account/transactions", handleError(async (req: Request, res: Response, next: NextFunction) => { + const chainId = parseInt(req.query.chainId as string); + + const transactions: TransactionDto[] = req.body; + + const { smartAccount } = await initClientWithAccount(SMART_ACCOUNT_OWNER_PK, chainId); + + const txs: Transaction[] = transactions.map((tx) => { + return { + to: tx.params.to, + value: tx.params.value, + data: tx.params.data, + }; + }); + + console.log(txs); + + const response = await smartAccount.sendTransaction(txs); + + console.log(response); + + const receipt = await response.wait(); + + res.json(receipt.receipt.transactionHash); +})); + +app.listen(port, () => { + console.log(`[server]: Server is running at http://localhost:${port}`); +}); + +type TransactionType = "send" | "approve" | "swap"; +type TransactionDto = { + type: TransactionType; + summary: string; + params: { + from: string; + to: string; + value: string; + data: string; + }; +}; + +export function handleError(callback: (req: Request<{}>, res: Response, next: NextFunction) => Promise