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/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")