web3-agent-kit 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- src/__init__.py +33 -0
- src/agent.py +239 -0
- src/bridge.py +504 -0
- src/chain.py +115 -0
- src/defi/__init__.py +476 -0
- src/llm.py +272 -0
- src/portfolio.py +326 -0
- src/sniper.py +511 -0
- src/utils/__init__.py +140 -0
- src/wallet.py +128 -0
- web3_agent_kit-0.3.0.dist-info/METADATA +333 -0
- web3_agent_kit-0.3.0.dist-info/RECORD +15 -0
- web3_agent_kit-0.3.0.dist-info/WHEEL +5 -0
- web3_agent_kit-0.3.0.dist-info/licenses/LICENSE +21 -0
- web3_agent_kit-0.3.0.dist-info/top_level.txt +1 -0
src/defi/__init__.py
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
"""DeFi protocol integrations — Uniswap, Aave, Curve, and more."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from ..wallet import Wallet
|
|
13
|
+
from ..chain import Chain, ChainManager, CHAIN_IDS
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Uniswap V2 Router ABI (minimal)
|
|
19
|
+
UNISWAP_V2_ROUTER_ABI = json.loads("""[
|
|
20
|
+
{
|
|
21
|
+
"inputs": [
|
|
22
|
+
{"internalType": "uint256", "name": "amountOutMin", "type": "uint256"},
|
|
23
|
+
{"internalType": "address[]", "name": "path", "type": "address[]"},
|
|
24
|
+
{"internalType": "address", "name": "to", "type": "address"},
|
|
25
|
+
{"internalType": "uint256", "name": "deadline", "type": "uint256"}
|
|
26
|
+
],
|
|
27
|
+
"name": "swapExactETHForTokens",
|
|
28
|
+
"outputs": [{"internalType": "uint256[]", "name": "amounts", "type": "uint256[]"}],
|
|
29
|
+
"stateMutability": "payable",
|
|
30
|
+
"type": "function"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"inputs": [
|
|
34
|
+
{"internalType": "uint256", "name": "amountIn", "type": "uint256"},
|
|
35
|
+
{"internalType": "uint256", "name": "amountOutMin", "type": "uint256"},
|
|
36
|
+
{"internalType": "address[]", "name": "path", "type": "address[]"},
|
|
37
|
+
{"internalType": "address", "name": "to", "type": "address"},
|
|
38
|
+
{"internalType": "uint256", "name": "deadline", "type": "uint256"}
|
|
39
|
+
],
|
|
40
|
+
"name": "swapExactTokensForETH",
|
|
41
|
+
"outputs": [{"internalType": "uint256[]", "name": "amounts", "type": "uint256[]"}],
|
|
42
|
+
"stateMutability": "nonpayable",
|
|
43
|
+
"type": "function"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"inputs": [
|
|
47
|
+
{"internalType": "uint256", "name": "amountIn", "type": "uint256"},
|
|
48
|
+
{"internalType": "uint256", "name": "amountOutMin", "type": "uint256"},
|
|
49
|
+
{"internalType": "address[]", "name": "path", "type": "address[]"},
|
|
50
|
+
{"internalType": "address", "name": "to", "type": "address"},
|
|
51
|
+
{"internalType": "uint256", "name": "deadline", "type": "uint256"}
|
|
52
|
+
],
|
|
53
|
+
"name": "swapExactTokensForTokens",
|
|
54
|
+
"outputs": [{"internalType": "uint256[]", "name": "amounts", "type": "uint256[]"}],
|
|
55
|
+
"stateMutability": "nonpayable",
|
|
56
|
+
"type": "function"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"inputs": [
|
|
60
|
+
{"internalType": "uint256", "name": "amountIn", "type": "uint256"},
|
|
61
|
+
{"internalType": "address[]", "name": "path", "type": "address[]"}
|
|
62
|
+
],
|
|
63
|
+
"name": "getAmountsOut",
|
|
64
|
+
"outputs": [{"internalType": "uint256[]", "name": "amounts", "type": "uint256[]"}],
|
|
65
|
+
"stateMutability": "view",
|
|
66
|
+
"type": "function"
|
|
67
|
+
}
|
|
68
|
+
]""")
|
|
69
|
+
|
|
70
|
+
# ERC20 ABI (minimal for approve + balanceOf)
|
|
71
|
+
ERC20_ABI = json.loads("""[
|
|
72
|
+
{
|
|
73
|
+
"inputs": [
|
|
74
|
+
{"internalType": "address", "name": "spender", "type": "address"},
|
|
75
|
+
{"internalType": "uint256", "name": "amount", "type": "uint256"}
|
|
76
|
+
],
|
|
77
|
+
"name": "approve",
|
|
78
|
+
"outputs": [{"internalType": "bool", "name": "", "type": "bool"}],
|
|
79
|
+
"stateMutability": "nonpayable",
|
|
80
|
+
"type": "function"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"inputs": [{"internalType": "address", "name": "account", "type": "address"}],
|
|
84
|
+
"name": "balanceOf",
|
|
85
|
+
"outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
|
|
86
|
+
"stateMutability": "view",
|
|
87
|
+
"type": "function"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"inputs": [],
|
|
91
|
+
"name": "decimals",
|
|
92
|
+
"outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}],
|
|
93
|
+
"stateMutability": "view",
|
|
94
|
+
"type": "function"
|
|
95
|
+
}
|
|
96
|
+
]""")
|
|
97
|
+
|
|
98
|
+
# WETH addresses
|
|
99
|
+
WETH = {
|
|
100
|
+
Chain.ETHEREUM: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
|
101
|
+
Chain.BASE: "0x4200000000000000000000000000000000000006",
|
|
102
|
+
Chain.ARBITRUM: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
|
|
103
|
+
Chain.OPTIMISM: "0x4200000000000000000000000000000000000006",
|
|
104
|
+
Chain.POLYGON: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# Native token address (ETH/MATIC/etc)
|
|
108
|
+
NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
|
|
109
|
+
|
|
110
|
+
# Common stablecoin addresses per chain
|
|
111
|
+
STABLECOINS = {
|
|
112
|
+
Chain.ETHEREUM: {
|
|
113
|
+
"USDC": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
114
|
+
"USDT": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
|
115
|
+
"DAI": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
|
116
|
+
},
|
|
117
|
+
Chain.BASE: {
|
|
118
|
+
"USDC": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
119
|
+
"USDbC": "0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA",
|
|
120
|
+
},
|
|
121
|
+
Chain.ARBITRUM: {
|
|
122
|
+
"USDC": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
|
123
|
+
"USDT": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class SwapResult:
|
|
130
|
+
"""Result of a token swap."""
|
|
131
|
+
|
|
132
|
+
tx_hash: str
|
|
133
|
+
token_in: str
|
|
134
|
+
token_out: str
|
|
135
|
+
amount_in: float
|
|
136
|
+
amount_out: float
|
|
137
|
+
gas_used: int
|
|
138
|
+
chain: Chain
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class YieldOpportunity:
|
|
143
|
+
"""A yield farming opportunity."""
|
|
144
|
+
|
|
145
|
+
protocol: str
|
|
146
|
+
pool: str
|
|
147
|
+
apy: float
|
|
148
|
+
tvl: float
|
|
149
|
+
chain: Chain
|
|
150
|
+
risk_score: float
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class DeFiTool(ABC):
|
|
154
|
+
"""Base class for DeFi protocol integrations."""
|
|
155
|
+
|
|
156
|
+
name: str = "base"
|
|
157
|
+
supported_chains: list[Chain] = []
|
|
158
|
+
|
|
159
|
+
@abstractmethod
|
|
160
|
+
def execute(self, wallet: Wallet, **kwargs) -> Any:
|
|
161
|
+
"""Execute a DeFi operation."""
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class Uniswap(DeFiTool):
|
|
166
|
+
"""Uniswap V2 DEX integration — actual swap execution."""
|
|
167
|
+
|
|
168
|
+
name = "uniswap"
|
|
169
|
+
supported_chains = [Chain.ETHEREUM, Chain.BASE, Chain.ARBITRUM, Chain.OPTIMISM, Chain.POLYGON]
|
|
170
|
+
|
|
171
|
+
# V2 Router addresses
|
|
172
|
+
ROUTERS = {
|
|
173
|
+
Chain.ETHEREUM: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D",
|
|
174
|
+
Chain.BASE: "0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24",
|
|
175
|
+
Chain.ARBITRUM: "0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24",
|
|
176
|
+
Chain.OPTIMISM: "0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24",
|
|
177
|
+
Chain.POLYGON: "0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff",
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
def __init__(self, chain_manager: Optional[ChainManager] = None, slippage: float = 0.5):
|
|
181
|
+
self.chain_manager = chain_manager
|
|
182
|
+
self.slippage = slippage # percent
|
|
183
|
+
|
|
184
|
+
def execute(self, wallet: Wallet, token_in: str, token_out: str, amount: float,
|
|
185
|
+
chain: Chain = Chain.ETHEREUM, **kwargs) -> SwapResult:
|
|
186
|
+
"""
|
|
187
|
+
Execute a token swap on Uniswap V2.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
wallet: Wallet to swap from
|
|
191
|
+
token_in: Input token address (or "ETH"/"NATIVE" for native token)
|
|
192
|
+
token_out: Output token address (or "ETH"/"NATIVE" for native token)
|
|
193
|
+
amount: Amount of input token (in human-readable units, e.g. 0.1 for 0.1 ETH)
|
|
194
|
+
chain: Chain to swap on
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
SwapResult with tx hash and details
|
|
198
|
+
"""
|
|
199
|
+
if chain not in self.ROUTERS:
|
|
200
|
+
raise ValueError(f"Uniswap not supported on {chain.value}")
|
|
201
|
+
|
|
202
|
+
if not self.chain_manager:
|
|
203
|
+
raise ValueError("ChainManager required for swap execution")
|
|
204
|
+
|
|
205
|
+
w3 = self.chain_manager.get_web3(chain)
|
|
206
|
+
router_addr = self.ROUTERS[chain]
|
|
207
|
+
router = w3.eth.contract(address=w3.to_checksum_address(router_addr), abi=UNISWAP_V2_ROUTER_ABI)
|
|
208
|
+
|
|
209
|
+
# Resolve token addresses
|
|
210
|
+
weth_addr = WETH.get(chain)
|
|
211
|
+
is_eth_in = token_in.upper() in ("ETH", "NATIVE", weth_addr)
|
|
212
|
+
is_eth_out = token_out.upper() in ("ETH", "NATIVE", weth_addr)
|
|
213
|
+
|
|
214
|
+
if is_eth_in:
|
|
215
|
+
token_in_addr = weth_addr
|
|
216
|
+
else:
|
|
217
|
+
token_in_addr = w3.to_checksum_address(token_in)
|
|
218
|
+
|
|
219
|
+
if is_eth_out:
|
|
220
|
+
token_out_addr = weth_addr
|
|
221
|
+
else:
|
|
222
|
+
token_out_addr = w3.to_checksum_address(token_out)
|
|
223
|
+
|
|
224
|
+
# Build swap path
|
|
225
|
+
path = [w3.to_checksum_address(token_in_addr), w3.to_checksum_address(token_out_addr)]
|
|
226
|
+
|
|
227
|
+
# Get decimals for amount conversion
|
|
228
|
+
if is_eth_in:
|
|
229
|
+
decimals = 18
|
|
230
|
+
else:
|
|
231
|
+
token_contract = w3.eth.contract(address=token_in_addr, abi=ERC20_ABI)
|
|
232
|
+
decimals = token_contract.functions.decimals().call()
|
|
233
|
+
|
|
234
|
+
amount_wei = int(amount * (10 ** decimals))
|
|
235
|
+
|
|
236
|
+
# Get quote
|
|
237
|
+
amounts_out = router.functions.getAmountsOut(amount_wei, path).call()
|
|
238
|
+
amount_out_raw = amounts_out[-1]
|
|
239
|
+
|
|
240
|
+
# Get output decimals
|
|
241
|
+
if is_eth_out:
|
|
242
|
+
out_decimals = 18
|
|
243
|
+
else:
|
|
244
|
+
out_contract = w3.eth.contract(address=token_out_addr, abi=ERC20_ABI)
|
|
245
|
+
out_decimals = out_contract.functions.decimals().call()
|
|
246
|
+
|
|
247
|
+
amount_out = amount_out_raw / (10 ** out_decimals)
|
|
248
|
+
amount_out_min = int(amount_out_raw * (1 - self.slippage / 100))
|
|
249
|
+
|
|
250
|
+
# Deadline: 20 minutes from now
|
|
251
|
+
deadline = int(time.time()) + 1200
|
|
252
|
+
|
|
253
|
+
# Get nonce and gas price
|
|
254
|
+
nonce = w3.eth.get_transaction_count(wallet.address)
|
|
255
|
+
gas_price = w3.eth.gas_price
|
|
256
|
+
|
|
257
|
+
# Build transaction based on swap direction
|
|
258
|
+
if is_eth_in:
|
|
259
|
+
# swapExactETHForTokens
|
|
260
|
+
tx = router.functions.swapExactETHForTokens(
|
|
261
|
+
amount_out_min,
|
|
262
|
+
path,
|
|
263
|
+
w3.to_checksum_address(wallet.address),
|
|
264
|
+
deadline,
|
|
265
|
+
).build_transaction({
|
|
266
|
+
"from": w3.to_checksum_address(wallet.address),
|
|
267
|
+
"value": amount_wei,
|
|
268
|
+
"gas": 250000,
|
|
269
|
+
"gasPrice": gas_price,
|
|
270
|
+
"nonce": nonce,
|
|
271
|
+
"chainId": CHAIN_IDS.get(chain, 1),
|
|
272
|
+
})
|
|
273
|
+
elif is_eth_out:
|
|
274
|
+
# swapExactTokensForETH — need approval first
|
|
275
|
+
self._approve_token(wallet, token_in_addr, router_addr, amount_wei, w3, chain, nonce)
|
|
276
|
+
nonce += 1
|
|
277
|
+
|
|
278
|
+
tx = router.functions.swapExactTokensForETH(
|
|
279
|
+
amount_wei,
|
|
280
|
+
amount_out_min,
|
|
281
|
+
path,
|
|
282
|
+
w3.to_checksum_address(wallet.address),
|
|
283
|
+
deadline,
|
|
284
|
+
).build_transaction({
|
|
285
|
+
"from": w3.to_checksum_address(wallet.address),
|
|
286
|
+
"gas": 250000,
|
|
287
|
+
"gasPrice": gas_price,
|
|
288
|
+
"nonce": nonce,
|
|
289
|
+
"chainId": CHAIN_IDS.get(chain, 1),
|
|
290
|
+
})
|
|
291
|
+
else:
|
|
292
|
+
# swapExactTokensForTokens — need approval first
|
|
293
|
+
self._approve_token(wallet, token_in_addr, router_addr, amount_wei, w3, chain, nonce)
|
|
294
|
+
nonce += 1
|
|
295
|
+
|
|
296
|
+
tx = router.functions.swapExactTokensForTokens(
|
|
297
|
+
amount_wei,
|
|
298
|
+
amount_out_min,
|
|
299
|
+
path,
|
|
300
|
+
w3.to_checksum_address(wallet.address),
|
|
301
|
+
deadline,
|
|
302
|
+
).build_transaction({
|
|
303
|
+
"from": w3.to_checksum_address(wallet.address),
|
|
304
|
+
"gas": 250000,
|
|
305
|
+
"gasPrice": gas_price,
|
|
306
|
+
"nonce": nonce,
|
|
307
|
+
"chainId": CHAIN_IDS.get(chain, 1),
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
# Sign and send
|
|
311
|
+
signed = wallet.sign_transaction(tx, chain)
|
|
312
|
+
tx_hash = w3.eth.send_raw_transaction(signed)
|
|
313
|
+
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
|
|
314
|
+
|
|
315
|
+
return SwapResult(
|
|
316
|
+
tx_hash=tx_hash.hex(),
|
|
317
|
+
token_in=token_in,
|
|
318
|
+
token_out=token_out,
|
|
319
|
+
amount_in=amount,
|
|
320
|
+
amount_out=amount_out,
|
|
321
|
+
gas_used=receipt.gasUsed,
|
|
322
|
+
chain=chain,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
def get_quote(self, token_in: str, token_out: str, amount: float,
|
|
326
|
+
chain: Chain = Chain.ETHEREUM) -> dict:
|
|
327
|
+
"""
|
|
328
|
+
Get a swap quote without executing.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
token_in: Input token address (or "ETH" for native)
|
|
332
|
+
token_out: Output token address (or "ETH" for native)
|
|
333
|
+
amount: Amount in human-readable units
|
|
334
|
+
chain: Chain to quote on
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Dict with amount_out, price_impact, etc.
|
|
338
|
+
"""
|
|
339
|
+
if chain not in self.ROUTERS:
|
|
340
|
+
raise ValueError(f"Uniswap not supported on {chain.value}")
|
|
341
|
+
|
|
342
|
+
if not self.chain_manager:
|
|
343
|
+
raise ValueError("ChainManager required for quote")
|
|
344
|
+
|
|
345
|
+
w3 = self.chain_manager.get_web3(chain)
|
|
346
|
+
router_addr = self.ROUTERS[chain]
|
|
347
|
+
router = w3.eth.contract(address=w3.to_checksum_address(router_addr), abi=UNISWAP_V2_ROUTER_ABI)
|
|
348
|
+
|
|
349
|
+
weth_addr = WETH.get(chain)
|
|
350
|
+
is_eth_in = token_in.upper() in ("ETH", "NATIVE", weth_addr)
|
|
351
|
+
is_eth_out = token_out.upper() in ("ETH", "NATIVE", weth_addr)
|
|
352
|
+
|
|
353
|
+
token_in_addr = weth_addr if is_eth_in else w3.to_checksum_address(token_in)
|
|
354
|
+
token_out_addr = weth_addr if is_eth_out else w3.to_checksum_address(token_out)
|
|
355
|
+
|
|
356
|
+
path = [w3.to_checksum_address(token_in_addr), w3.to_checksum_address(token_out_addr)]
|
|
357
|
+
|
|
358
|
+
if is_eth_in:
|
|
359
|
+
decimals = 18
|
|
360
|
+
else:
|
|
361
|
+
token_contract = w3.eth.contract(address=token_in_addr, abi=ERC20_ABI)
|
|
362
|
+
decimals = token_contract.functions.decimals().call()
|
|
363
|
+
|
|
364
|
+
amount_wei = int(amount * (10 ** decimals))
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
amounts_out = router.functions.getAmountsOut(amount_wei, path).call()
|
|
368
|
+
amount_out_raw = amounts_out[-1]
|
|
369
|
+
|
|
370
|
+
if is_eth_out:
|
|
371
|
+
out_decimals = 18
|
|
372
|
+
else:
|
|
373
|
+
out_contract = w3.eth.contract(address=token_out_addr, abi=ERC20_ABI)
|
|
374
|
+
out_decimals = out_contract.functions.decimals().call()
|
|
375
|
+
|
|
376
|
+
amount_out = amount_out_raw / (10 ** out_decimals)
|
|
377
|
+
price = amount_out / amount if amount > 0 else 0
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
"amount_in": amount,
|
|
381
|
+
"amount_out": amount_out,
|
|
382
|
+
"price": price,
|
|
383
|
+
"path": path,
|
|
384
|
+
"chain": chain.value,
|
|
385
|
+
}
|
|
386
|
+
except Exception as e:
|
|
387
|
+
return {"error": str(e), "chain": chain.value}
|
|
388
|
+
|
|
389
|
+
def _approve_token(self, wallet: Wallet, token_addr: str, spender: str,
|
|
390
|
+
amount: int, w3, chain: Chain, nonce: int):
|
|
391
|
+
"""Approve token spending for router."""
|
|
392
|
+
token = w3.eth.contract(address=w3.to_checksum_address(token_addr), abi=ERC20_ABI)
|
|
393
|
+
|
|
394
|
+
# Check current allowance
|
|
395
|
+
allowance = token.functions.allowance(
|
|
396
|
+
w3.to_checksum_address(wallet.address),
|
|
397
|
+
w3.to_checksum_address(spender)
|
|
398
|
+
).call()
|
|
399
|
+
|
|
400
|
+
if allowance >= amount:
|
|
401
|
+
logger.info(f"Token already approved ({allowance} >= {amount})")
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
# Build approve tx
|
|
405
|
+
approve_tx = token.functions.approve(
|
|
406
|
+
w3.to_checksum_address(spender),
|
|
407
|
+
2**256 - 1 # Max approval
|
|
408
|
+
).build_transaction({
|
|
409
|
+
"from": w3.to_checksum_address(wallet.address),
|
|
410
|
+
"gas": 100000,
|
|
411
|
+
"gasPrice": w3.eth.gas_price,
|
|
412
|
+
"nonce": nonce,
|
|
413
|
+
"chainId": CHAIN_IDS.get(chain, 1),
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
signed = wallet.sign_transaction(approve_tx, chain)
|
|
417
|
+
tx_hash = w3.eth.send_raw_transaction(signed)
|
|
418
|
+
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=60)
|
|
419
|
+
logger.info(f"Token approved: {tx_hash.hex()} (gas: {receipt.gasUsed})")
|
|
420
|
+
|
|
421
|
+
def resolve_token(self, symbol: str, chain: Chain) -> str:
|
|
422
|
+
"""Resolve token symbol to address."""
|
|
423
|
+
symbol = symbol.upper()
|
|
424
|
+
if symbol in ("ETH", "NATIVE", "MATIC"):
|
|
425
|
+
return NATIVE
|
|
426
|
+
if chain in STABLECOINS and symbol in STABLECOINS[chain]:
|
|
427
|
+
return STABLECOINS[chain][symbol]
|
|
428
|
+
if chain in WETH and symbol == "WETH":
|
|
429
|
+
return WETH[chain]
|
|
430
|
+
raise ValueError(f"Unknown token symbol: {symbol} on {chain.value}")
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class Aerodrome(DeFiTool):
|
|
434
|
+
"""Aerodrome DEX on Base."""
|
|
435
|
+
|
|
436
|
+
name = "aerodrome"
|
|
437
|
+
supported_chains = [Chain.BASE]
|
|
438
|
+
|
|
439
|
+
ROUTER = "0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43"
|
|
440
|
+
|
|
441
|
+
def __init__(self, chain_manager: Optional[ChainManager] = None, slippage: float = 0.5):
|
|
442
|
+
self.chain_manager = chain_manager
|
|
443
|
+
self.slippage = slippage
|
|
444
|
+
|
|
445
|
+
def execute(self, wallet: Wallet, token_in: str, token_out: str, amount: float, **kwargs) -> SwapResult:
|
|
446
|
+
"""Execute a swap on Aerodrome (uses Uniswap V2-compatible router)."""
|
|
447
|
+
# Aerodrome uses a V2-compatible router, so we reuse Uniswap logic
|
|
448
|
+
uniswap = Uniswap(chain_manager=self.chain_manager, slippage=self.slippage)
|
|
449
|
+
uniswap.ROUTERS[Chain.BASE] = self.ROUTER
|
|
450
|
+
return uniswap.execute(wallet, token_in, token_out, amount, chain=Chain.BASE, **kwargs)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class Aave(DeFiTool):
|
|
454
|
+
"""Aave lending/borrowing protocol integration."""
|
|
455
|
+
|
|
456
|
+
name = "aave"
|
|
457
|
+
supported_chains = [Chain.ETHEREUM, Chain.BASE, Chain.ARBITRUM, Chain.OPTIMISM, Chain.POLYGON]
|
|
458
|
+
|
|
459
|
+
def execute(self, wallet: Wallet, action: str, **kwargs) -> Any:
|
|
460
|
+
"""Execute an Aave operation (supply, borrow, withdraw, repay)."""
|
|
461
|
+
raise NotImplementedError("Aave operations not yet implemented")
|
|
462
|
+
|
|
463
|
+
def get_yield_opportunities(self, chain: Chain) -> list[YieldOpportunity]:
|
|
464
|
+
"""Get available yield opportunities."""
|
|
465
|
+
raise NotImplementedError("Aave yield query not yet implemented")
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
class Curve(DeFiTool):
|
|
469
|
+
"""Curve Finance stableswap integration."""
|
|
470
|
+
|
|
471
|
+
name = "curve"
|
|
472
|
+
supported_chains = [Chain.ETHEREUM, Chain.ARBITRUM, Chain.POLYGON]
|
|
473
|
+
|
|
474
|
+
def execute(self, wallet: Wallet, pool: str, token_in: str, token_out: str, amount: float, **kwargs) -> SwapResult:
|
|
475
|
+
"""Execute a swap on Curve."""
|
|
476
|
+
raise NotImplementedError("Curve swap not yet implemented")
|