Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Biconomy integration #277

Merged
merged 27 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4cf7925
implemented starting smart account api
nerfZael Jun 20, 2024
3b66585
removed the reliance on manager.balance_of
nerfZael Jun 20, 2024
cf5e542
decoupling safe manager from smart wallet
nerfZael Jun 20, 2024
b0f9076
Merge remote-tracking branch 'origin/main' into nerfzael/biconomy
nerfZael Jun 21, 2024
ca52a5d
Merge remote-tracking branch 'origin/dev' into nerfzael/biconomy
nerfZael Jun 24, 2024
eca4e24
smart-account-api docker implementation
nerfZael Jun 24, 2024
56cff6d
sending transactions with local biconomy account
nerfZael Jun 24, 2024
e352897
initial biconomy api implementation
nerfZael Jun 24, 2024
27d67ef
updated readme with biconomy instructions
nerfZael Jun 24, 2024
0f45fbb
added network with bundler url
nerfZael Jun 24, 2024
d03dc70
typing fixes
nerfZael Jun 24, 2024
a0b62cc
Merge remote-tracking branch 'origin/nerfzael/logging-and-errors' int…
nerfZael Jun 27, 2024
8197f6b
swapping is now async
nerfZael Jun 27, 2024
52c0bd1
lowered max rounds to 20 when testing
nerfZael Jun 27, 2024
224fb06
added execution logs
nerfZael Jun 27, 2024
7649dce
fixed inverted bool
nerfZael Jun 28, 2024
e95d49d
catching all exceptions for swap
nerfZael Jun 28, 2024
0fa32df
added pytest timeout
nerfZael Jun 28, 2024
0214491
fixes issue where benchmarks break if first test fails
nerfZael Jun 28, 2024
625acef
added timeouts to tests
nerfZael Jun 28, 2024
906aef9
using aiohttp directly to not dispose of session
nerfZael Jun 28, 2024
305acc4
added constants for test timeouts
nerfZael Jun 28, 2024
e7ab90c
executing transactions one by one in safe if the multisend fails to e…
nerfZael Jun 28, 2024
0749613
raising exception where appropriate
nerfZael Jun 28, 2024
4c2d438
Merge remote-tracking branch 'origin/dev' into nerfzael/biconomy
nerfZael Jun 28, 2024
6532b06
increased test timeout of research tests
nerfZael Jun 28, 2024
fd61f5c
updated tests with intents and using more gaming categories as valid
nerfZael Jun 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Biconomy.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions autotx/AutoTx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -75,7 +75,7 @@ class RunResult:

class AutoTx:
web3: Web3
wallet: SmartWallet
wallet: SmartAccount
logger: Logger
intents: list[Intent]
network: NetworkInfo
Expand All @@ -94,7 +94,7 @@ class AutoTx:
def __init__(
self,
web3: Web3,
wallet: SmartWallet,
wallet: SmartAccount,
network: NetworkInfo,
agents: list[AutoTxAgent],
config: Config,
Expand Down
3 changes: 2 additions & 1 deletion autotx/agents/SwapTokensAgent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
38 changes: 34 additions & 4 deletions autotx/cli.py
Original file line number Diff line number Diff line change
@@ -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("""
Expand All @@ -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)")
Expand All @@ -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)

Expand Down
17 changes: 9 additions & 8 deletions autotx/load_tokens.py
Original file line number Diff line number Diff line change
@@ -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 = [
Expand All @@ -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)

Expand All @@ -41,5 +42,5 @@ def fetch_tokens_list() -> None:
f.write(content)


def run() -> None:
fetch_tokens_list()
async def run() -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any reason to make this async? this is expected to be run through the script poetry run load-token; I just tried it and got the error

autotx-py3.10cesar@dandy:~/dev/polywrap/auto-tx$ poetry run load-tokens
<coroutine object run at 0x7e0b30163d10>
sys:1: RuntimeWarning: coroutine 'run' was never awaited
RuntimeWarning: Enable tracemalloc to get the object allocation traceback

await fetch_tokens_list()
93 changes: 57 additions & 36 deletions autotx/server.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from datetime import datetime
import json
from typing import Annotated, Any, Dict, List
from eth_account import Account
Expand All @@ -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
Expand Down Expand Up @@ -61,17 +63,17 @@ 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:
raise HTTPException(status_code=400, detail="User not found")

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)
Expand All @@ -86,15 +88,16 @@ 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 []

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

Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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(
Expand All @@ -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)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Expand All @@ -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}"

Expand All @@ -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 "<pre>No logs found</pre>"

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 "<pre>" + "\n".join([log.created_at.strftime("%Y-%m-%d %H:%M:%S") + f": {json.loads(log.obj)}" for log in filtered_logs]) + "</pre>"
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"<pre>{text}</pre>"
text = "\n\n".join(agent_logs)
return f"<pre>{text}</pre>"

@app_router.get("/api/v1/version", response_class=JSONResponse)
async def get_version() -> Dict[str, str]:
Expand All @@ -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(
Expand Down
Loading
Loading