-
Notifications
You must be signed in to change notification settings - Fork 22
/
Copy pathSwapTokensAgent.py
219 lines (180 loc) · 9.26 KB
/
SwapTokensAgent.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
from decimal import Decimal
from textwrap import dedent
from typing import Annotated, Any, Callable, Coroutine
from autotx.AutoTx import AutoTx
from autotx.autotx_agent import AutoTxAgent
from autotx.autotx_tool import AutoTxTool
from autotx.intents import BuyIntent, Intent, SellIntent
from autotx.token import Token
from autotx.eth_address import ETHAddress
from autotx.utils.ethereum.lifi.swap import SUPPORTED_NETWORKS_BY_LIFI, a_can_build_swap_transaction
from autotx.utils.ethereum.networks import NetworkInfo
from gnosis.eth import EthereumNetworkNotSupported as ChainIdNotSupported
name = "swap-tokens"
system_message = lambda autotx: dedent(f"""
You are an expert at buying and selling tokens. Assist the user in their task of swapping tokens.
ONLY focus on the buy and sell (swap) aspect of the user's goal and let other agents handle other tasks.
You use the tools available to assist the user in their tasks.
Note a balance of a token is not required to perform a swap, if there is an earlier prepared transaction that will provide the token.
Below are examples, NOTE these are only examples and in practice you need to call the prepare_bulk_swap_transactions tool with the correct arguments.
NEVER ask the user questions.
Example 1:
User: Send 0.1 ETH to vitalik.eth and then swap ETH to 5 USDC
...
Other agent messages
...
Call prepare_bulk_swap_transactions: "ETH to 5 USDC"
Example 1:
User: Buy 10 USDC with ETH and then buy UNI with 5 USDC
Call prepare_bulk_swap_transactions: "ETH to 10 USDC\n5 USDC to UNI"
Example 2:
User: Buy UNI, WBTC, USDC and SHIB with 0.92 ETH
Call prepare_bulk_swap_transactions: "0.23 ETH to UNI\n0.23 ETH to WBTC\n0.23 ETH to USDC\n0.23 ETH to SHIB"
Example 3:
User: Swap ETH to 5 USDC, then swap that USDC for 6 UNI
Call prepare_bulk_swap_transactions: "ETH to 5 USDC\nUSDC to 6 UNI"
Example 4:
User: Buy 2 ETH worth of WBTC and then send 1 WBTC to 0x123..456
Call prepare_bulk_swap_transactions: "2 ETH to WBTC"
Example of a bad input:
User: Swap ETH to 1 UNI, then swap UNI to 4 USDC
Call prepare_bulk_swap_transactions: "ETH to 1 UNI\n1 UNI to 4 USDC"
Prepared transaction: Swap 1.0407386618866115 ETH for at least 1 WBTC
Invalid input: "1 UNI to 4 USDC". Only one token amount should be provided. IMPORTANT: Take another look at the user's goal, and try again.
In the above example, you recover with:
Call prepare_bulk_swap_transactions: "UNI to 4 USDC"
Above are examples, NOTE these are only examples and in practice you need to call the prepare_bulk_swap_transactions tool with the correct arguments.
Take extra care in ensuring you have to right amount next to the token symbol. NEVER use more than one amount per swap, the other amount will be calculated for you.
The swaps are NOT NECESSARILY correlated, focus on the exact amounts the user wants to buy or sell (leave the other amounts to be calculated for you).
You rely on the other agents to provide the token to buy or sell. Never make up a token. Unless explicitly given the name of the token, ask the 'research-tokens' agent to first search for the token.
Only call tools, do not respond with JSON.
"""
)
description = dedent(
f"""
{name} is an AI assistant that's an expert at buying and selling tokens.
The agent can prepare transactions to swap tokens.
"""
)
def get_tokens_address(token_in: str, token_out: str, network_info: NetworkInfo) -> tuple[str, str]:
token_in = token_in.lower()
token_out = token_out.lower()
if token_in not in network_info.tokens:
raise Exception(f"Token {token_in} is not supported in network {network_info.chain_id.name.lower()}")
if token_out not in network_info.tokens:
raise Exception(f"Token {token_out} is not supported in network {network_info.chain_id.name.lower()}")
return (network_info.tokens[token_in], network_info.tokens[token_out])
class InvalidInput(Exception):
pass
async def swap(autotx: AutoTx, token_to_sell: str, token_to_buy: str) -> Intent:
sell_parts = token_to_sell.split(" ")
buy_parts = token_to_buy.split(" ")
if not autotx.network.chain_id in SUPPORTED_NETWORKS_BY_LIFI:
raise ChainIdNotSupported(
f"Network {autotx.network.chain_id.name.lower()} not supported for swap"
)
if len(sell_parts) == 2 and len(buy_parts) == 2:
sell_amount = Decimal(sell_parts[0])
buy_amount = Decimal(buy_parts[0])
sell_token = sell_parts[1]
buy_token = buy_parts[1]
raise InvalidInput(f"Invalid input: \"{token_to_sell} to {token_to_buy}\". Only one token amount should be provided. Choose between '{sell_amount} {sell_token} to {buy_token}' or '{sell_token} to {buy_amount} {buy_token}'.")
if len(sell_parts) < 2 and len(buy_parts) < 2:
raise InvalidInput(f"Invalid input: \"{token_to_sell} to {token_to_buy}\". Token amount is missing. Only one token amount should be provided.")
if len(sell_parts) > 2 or len(buy_parts) > 2:
raise InvalidInput(f"Invalid input: \"{token_to_sell} to {token_to_buy}\". Too many token amounts or token symbols provided. Only one token amount and two token symbols should be provided per line.")
token_symbol_to_sell = sell_parts[1] if len(sell_parts) == 2 else sell_parts[0]
token_symbol_to_buy = buy_parts[1] if len(buy_parts) == 2 else buy_parts[0]
exact_amount = sell_parts[0] if len(sell_parts) == 2 else buy_parts[0]
amount_symbol = token_symbol_to_sell if len(sell_parts) == 2 else token_symbol_to_buy
token_in = token_symbol_to_sell.lower()
token_out = token_symbol_to_buy.lower()
is_exact_input = True if amount_symbol == token_symbol_to_sell else False
(token_in_address, token_out_address) = get_tokens_address(
token_in, token_out, autotx.network
)
await a_can_build_swap_transaction(
autotx.web3,
Decimal(exact_amount),
ETHAddress(token_in_address),
ETHAddress(token_out_address),
autotx.wallet.address,
is_exact_input,
autotx.network.chain_id
)
# Create buy intent if amount of token to buy is provided else create sell intent
swap_intent: Intent = BuyIntent.create(
from_token=Token(symbol=token_symbol_to_sell, address=token_in_address),
to_token=Token(symbol=token_symbol_to_buy, address=token_out_address),
amount=float(exact_amount),
) if len(buy_parts) == 2 else SellIntent.create(
from_token=Token(symbol=token_symbol_to_sell, address=token_in_address),
to_token=Token(symbol=token_symbol_to_buy, address=token_out_address),
amount=float(exact_amount),
)
autotx.add_intents([swap_intent])
return swap_intent
class BulkSwapTool(AutoTxTool):
name: str = "prepare_bulk_swap_transactions"
description: str = dedent(
"""
Prepares a batch of buy transactions for given amounts in decimals for given tokens.
"""
)
def build_tool(self, autotx: AutoTx) -> Callable[[str], Coroutine[Any, Any, str]]:
async def run(
tokens: Annotated[
str,
"""
Tokens and amounts to swap. ONLY one amount should be provided next to the token symbol PER line.
E.g:
10 USDC to ETH
UNI to 3.3 WBTC
1.5 WBTC to USDC
"""
],
) -> str:
swaps = tokens.split("\n")
all_intents = []
all_errors: list[Exception] = []
for swap_str in swaps:
(token_to_sell, token_to_buy) = swap_str.strip().split(" to ")
try:
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:
all_errors.append(Exception(f"Error: {e} for swap \"{token_to_sell} to {token_to_buy}\""))
summary = "".join(
f"Prepared transaction: {intent.summary}\n"
for intent in all_intents
)
if all_errors:
summary += "\n".join(str(e) for e in all_errors)
if len(all_intents) > 0:
summary += f"\n{len(all_errors)} errors occurred. {len(all_intents)} transactions were prepared. There is no need to re-run the transactions that were prepared."
else:
summary += f"\n{len(all_errors)} errors occurred."
total_summary = ("\n" + " " * 16).join(
[
f"{i + 1}. {tx.summary}"
for i, tx in enumerate(autotx.intents)
]
)
autotx.notify_user(summary)
return dedent(
f"""
{summary}
Total prepared transactions so far:
{total_summary}
"""
)
return run
class SwapTokensAgent(AutoTxAgent):
name = name
system_message = system_message
description = description
tools = [
BulkSwapTool()
]