web3-agent-kit 1.3.0__tar.gz → 1.4.0__tar.gz

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.
Files changed (93) hide show
  1. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/PKG-INFO +1 -1
  2. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/pyproject.toml +1 -1
  3. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/dashboard.py +1 -1
  4. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/wl_grinder.py +3 -3
  5. web3_agent_kit-1.4.0/src/mev/__init__.py +495 -0
  6. web3_agent_kit-1.4.0/src/nft/__init__.py +442 -0
  7. web3_agent_kit-1.4.0/src/notifications/__init__.py +417 -0
  8. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/web3_agent_kit.egg-info/PKG-INFO +1 -1
  9. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/web3_agent_kit.egg-info/SOURCES.txt +3 -0
  10. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/LICENSE +0 -0
  11. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/README.md +0 -0
  12. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/setup.cfg +0 -0
  13. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/setup.py +0 -0
  14. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/__init__.py +0 -0
  15. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/agent.py +0 -0
  16. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/__init__.py +0 -0
  17. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/base.py +0 -0
  18. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/discovery.py +0 -0
  19. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/__init__.py +0 -0
  20. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/base_executor.py +0 -0
  21. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/browser.py +0 -0
  22. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/captcha_solver.py +0 -0
  23. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/cli.py +0 -0
  24. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/galxe_exec.py +0 -0
  25. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/gleam_exec.py +0 -0
  26. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/intract_exec.py +0 -0
  27. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/layer3_exec.py +0 -0
  28. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/plugin_registry.py +0 -0
  29. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/port3_exec.py +0 -0
  30. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/questn.py +0 -0
  31. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/social_exec.py +0 -0
  32. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/taskon.py +0 -0
  33. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/zealy_exec.py +0 -0
  34. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/faucet.py +0 -0
  35. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/form_filler.py +0 -0
  36. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/galxe.py +0 -0
  37. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/gleam.py +0 -0
  38. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/multi_wallet.py +0 -0
  39. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/onchain.py +0 -0
  40. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/referral.py +0 -0
  41. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/scheduler.py +0 -0
  42. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/social.py +0 -0
  43. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/tracker.py +0 -0
  44. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/zealy.py +0 -0
  45. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/__init__.py +0 -0
  46. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/models.py +0 -0
  47. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/__init__.py +0 -0
  48. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/approval.py +0 -0
  49. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/bridge.py +0 -0
  50. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/dca.py +0 -0
  51. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/gas.py +0 -0
  52. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/portfolio.py +0 -0
  53. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/swap.py +0 -0
  54. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/wallet.py +0 -0
  55. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/watcher.py +0 -0
  56. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/yield_opt.py +0 -0
  57. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/approval_manager.py +0 -0
  58. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/bridge.py +0 -0
  59. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/chain.py +0 -0
  60. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/dca_bot.py +0 -0
  61. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/defi/__init__.py +0 -0
  62. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/gas_optimizer.py +0 -0
  63. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/llm.py +0 -0
  64. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/multi_wallet.py +0 -0
  65. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/plugins/__init__.py +0 -0
  66. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/plugins/examples/gas_tracker.py +0 -0
  67. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/portfolio.py +0 -0
  68. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/security/__init__.py +0 -0
  69. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/sniper.py +0 -0
  70. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/utils/__init__.py +0 -0
  71. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/wallet.py +0 -0
  72. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/wallet_watcher.py +0 -0
  73. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/yield_optimizer.py +0 -0
  74. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_airdrop.py +0 -0
  75. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_airdrop_new_modules.py +0 -0
  76. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_api.py +0 -0
  77. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_bridge.py +0 -0
  78. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_core.py +0 -0
  79. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_dca_bot.py +0 -0
  80. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_defi.py +0 -0
  81. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_executor.py +0 -0
  82. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_llm.py +0 -0
  83. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_multi_wallet.py +0 -0
  84. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_new_features.py +0 -0
  85. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_platforms.py +0 -0
  86. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_plugins.py +0 -0
  87. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_portfolio.py +0 -0
  88. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_security.py +0 -0
  89. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_sniper.py +0 -0
  90. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_yield_optimizer.py +0 -0
  91. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/web3_agent_kit.egg-info/dependency_links.txt +0 -0
  92. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/web3_agent_kit.egg-info/requires.txt +0 -0
  93. {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/web3_agent_kit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: web3-agent-kit
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: Open-source framework for building autonomous AI agents that interact with blockchain networks
5
5
  Author-email: Maulana <khasbim240803@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "web3-agent-kit"
7
- version = "1.3.0"
7
+ version = "1.4.0"
8
8
  description = "Open-source framework for building autonomous AI agents that interact with blockchain networks"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -117,7 +117,7 @@ class PointsDashboard:
117
117
  Example::
118
118
 
119
119
  dashboard = PointsDashboard(config)
120
- dashboard.sync_all(wallet="0x721e885...A522")
120
+ dashboard.sync_all(wallet="0xYOUR_WALLET_ADDRESS")
121
121
  dashboard.print_summary()
122
122
  """
123
123
 
@@ -130,10 +130,10 @@ class WLGrinder:
130
130
  Example::
131
131
 
132
132
  profile = WLProfile(
133
- wallet="0x721e885...",
134
- twitter="@itseywacc",
133
+ wallet="0xYOUR_WALLET...",
134
+ twitter="@your_twitter",
135
135
  discord="user#1234",
136
- email="khasbimln@gmail.com",
136
+ email="your_email@gmail.com",
137
137
  )
138
138
  grinder = WLGrinder(profile)
139
139
  result = grinder.apply("https://typeform.com/to/abc123")
@@ -0,0 +1,495 @@
1
+ """MEV Protection — protect transactions from sandwich attacks.
2
+
3
+ Integrates with Flashbots and private transaction relays to
4
+ protect trades from MEV (Maximal Extractable Value) attacks.
5
+
6
+ Usage::
7
+
8
+ from web3_agent_kit.mev import MEVProtector, MEVConfig
9
+
10
+ protector = MEVProtector(MEVConfig(
11
+ chain="ethereum",
12
+ use_flashbots=True,
13
+ ))
14
+ protected_tx = protector.protect_tx(raw_tx)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import logging
21
+ import time
22
+ from dataclasses import dataclass, field
23
+ from enum import Enum
24
+ from typing import Any, Optional
25
+
26
+ import requests
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class MEVStrategy(Enum):
32
+ """MEV protection strategies."""
33
+ FLASHBOTS = "flashbots" # Flashbots Protect
34
+ PRIVATE_RPC = "private_rpc" # Private transaction relay
35
+ BUNDLE = "bundle" # Flashbots bundle
36
+ NONE = "none" # No protection
37
+
38
+
39
+ @dataclass
40
+ class MEVConfig:
41
+ """Configuration for MEV protection."""
42
+ chain: str = "ethereum"
43
+ strategy: MEVStrategy = MEVStrategy.FLASHBOTS
44
+ # Flashbots
45
+ flashbots_rpc: str = "https://rpc.flashbots.net"
46
+ flashbots_auth_key: str = "" # Optional: Flashbots auth key
47
+ # Private RPCs
48
+ private_rpcs: list[str] = field(default_factory=lambda: [
49
+ "https://rpc.flashbots.net",
50
+ "https://protect.flashbots.net",
51
+ "https://mev-share.flashbots.net",
52
+ ])
53
+ # Settings
54
+ max_block_range: int = 25 # Max blocks for bundle
55
+ retry_count: int = 3
56
+ timeout: int = 30
57
+ # Refund
58
+ refund_address: str = "" # Address for MEV refunds
59
+ min_profit_wei: int = 0 # Minimum profit to include in bundle
60
+
61
+
62
+ @dataclass
63
+ class ProtectedTx:
64
+ """A protected transaction."""
65
+ original_tx: dict
66
+ protected_tx: dict
67
+ strategy: MEVStrategy
68
+ target_block: int = 0
69
+ bundle_hash: str = ""
70
+ status: str = "pending"
71
+ submitted_at: float = field(default_factory=time.time)
72
+ confirmed_at: float = 0.0
73
+ error: str = ""
74
+
75
+ def to_dict(self) -> dict:
76
+ return {
77
+ "strategy": self.strategy.value,
78
+ "target_block": self.target_block,
79
+ "bundle_hash": self.bundle_hash,
80
+ "status": self.status,
81
+ "error": self.error,
82
+ }
83
+
84
+
85
+ @dataclass
86
+ class BundleResult:
87
+ """Result of submitting a Flashbots bundle."""
88
+ bundle_hash: str
89
+ block_number: int
90
+ success: bool = False
91
+ tx_count: int = 0
92
+ gas_used: int = 0
93
+ mev_profit: int = 0
94
+ error: str = ""
95
+
96
+ def to_dict(self) -> dict:
97
+ return {
98
+ "bundle_hash": self.bundle_hash,
99
+ "block_number": self.block_number,
100
+ "success": self.success,
101
+ "tx_count": self.tx_count,
102
+ "gas_used": self.gas_used,
103
+ }
104
+
105
+
106
+ class MEVProtector:
107
+ """Protect transactions from MEV attacks.
108
+
109
+ Uses Flashbots and private transaction relays to prevent
110
+ sandwich attacks and frontrunning.
111
+
112
+ Example::
113
+
114
+ protector = MEVProtector(MEVConfig(
115
+ chain="ethereum",
116
+ use_flashbots=True,
117
+ ))
118
+
119
+ # Protect a swap transaction
120
+ protected = protector.protect_tx(raw_tx)
121
+ tx_hash = protector.send_protected(protected)
122
+ """
123
+
124
+ # Flashbots endpoints
125
+ FLASHBOTS_RPC = "https://rpc.flashbots.net"
126
+ FLASHBOTS_PROTECT = "https://protect.flashbots.net"
127
+ FLASHBOTS_BUNDLER = "https://relay.flashbots.net"
128
+ FLASHBOTS_SHARE = "https://mev-share.flashbots.net"
129
+
130
+ # Block time per chain (seconds)
131
+ BLOCK_TIMES = {
132
+ "ethereum": 12,
133
+ "base": 2,
134
+ "arbitrum": 0.25,
135
+ "optimism": 2,
136
+ "polygon": 2,
137
+ "bnb": 3,
138
+ }
139
+
140
+ def __init__(self, config: Optional[MEVConfig] = None):
141
+ """Initialize MEV protector.
142
+
143
+ Args:
144
+ config: MEV configuration.
145
+ """
146
+ self.config = config or MEVConfig()
147
+ self.session = requests.Session()
148
+ self.session.headers.update({
149
+ "Content-Type": "application/json",
150
+ })
151
+ if self.config.flashbots_auth_key:
152
+ self.session.headers["X-Flashbots-Signature"] = self.config.flashbots_auth_key
153
+ self._web3 = None
154
+ self._bundles: list[BundleResult] = []
155
+ logger.info("MEVProtector initialized")
156
+
157
+ def protect_tx(self, raw_tx: dict) -> ProtectedTx:
158
+ """Protect a transaction from MEV.
159
+
160
+ Args:
161
+ raw_tx: Raw transaction dict.
162
+
163
+ Returns:
164
+ ProtectedTx with protection applied.
165
+ """
166
+ strategy = self.config.strategy
167
+
168
+ if strategy == MEVStrategy.FLASHBOTS:
169
+ return self._protect_flashbots(raw_tx)
170
+ elif strategy == MEVStrategy.PRIVATE_RPC:
171
+ return self._protect_private_rpc(raw_tx)
172
+ elif strategy == MEVStrategy.BUNDLE:
173
+ return self._protect_bundle(raw_tx)
174
+ else:
175
+ return ProtectedTx(
176
+ original_tx=raw_tx,
177
+ protected_tx=raw_tx,
178
+ strategy=MEVStrategy.NONE,
179
+ status="unprotected",
180
+ )
181
+
182
+ def send_protected(self, protected: ProtectedTx) -> Optional[str]:
183
+ """Send a protected transaction.
184
+
185
+ Args:
186
+ protected: ProtectedTx to send.
187
+
188
+ Returns:
189
+ Transaction hash, or None if failed.
190
+ """
191
+ try:
192
+ if protected.strategy == MEVStrategy.FLASHBOTS:
193
+ return self._send_flashbots(protected)
194
+ elif protected.strategy == MEVStrategy.PRIVATE_RPC:
195
+ return self._send_private_rpc(protected)
196
+ elif protected.strategy == MEVStrategy.BUNDLE:
197
+ return self._send_bundle(protected)
198
+ else:
199
+ return self._send_raw(protected)
200
+ except Exception as e:
201
+ protected.status = "failed"
202
+ protected.error = str(e)
203
+ logger.error(f"Failed to send protected tx: {e}")
204
+ return None
205
+
206
+ def create_bundle(self, txs: list[dict], target_block: int = 0) -> BundleResult:
207
+ """Create a Flashbots bundle.
208
+
209
+ Args:
210
+ txs: List of raw transactions.
211
+ target_block: Target block number (0 = next block).
212
+
213
+ Returns:
214
+ BundleResult with bundle details.
215
+ """
216
+ if not target_block:
217
+ target_block = self._get_current_block() + 1
218
+
219
+ bundle_hash = f"0x{hash(str(txs)) & 0xFFFFFFFFFFFFFFFF:016x}"
220
+ result = BundleResult(
221
+ bundle_hash=bundle_hash,
222
+ block_number=target_block,
223
+ tx_count=len(txs),
224
+ )
225
+
226
+ try:
227
+ # Submit bundle to Flashbots
228
+ payload = {
229
+ "jsonrpc": "2.0",
230
+ "id": 1,
231
+ "method": "eth_sendBundle",
232
+ "params": [{
233
+ "txs": [self._encode_tx(tx) for tx in txs],
234
+ "blockNumber": hex(target_block),
235
+ }],
236
+ }
237
+
238
+ resp = self.session.post(
239
+ self.FLASHBOTS_BUNDLER,
240
+ json=payload,
241
+ timeout=self.config.timeout,
242
+ )
243
+
244
+ if resp.status_code == 200:
245
+ data = resp.json()
246
+ if "result" in data:
247
+ result.success = True
248
+ logger.info(f"Bundle submitted: {bundle_hash}")
249
+ else:
250
+ result.error = data.get("error", {}).get("message", "Unknown error")
251
+ else:
252
+ result.error = f"HTTP {resp.status_code}"
253
+
254
+ except Exception as e:
255
+ result.error = str(e)
256
+
257
+ self._bundles.append(result)
258
+ return result
259
+
260
+ def simulate_bundle(self, txs: list[dict], block_number: int = 0) -> dict:
261
+ """Simulate a Flashbots bundle.
262
+
263
+ Args:
264
+ txs: List of raw transactions.
265
+ block_number: Block number to simulate at.
266
+
267
+ Returns:
268
+ Simulation results.
269
+ """
270
+ if not block_number:
271
+ block_number = self._get_current_block() + 1
272
+
273
+ try:
274
+ payload = {
275
+ "jsonrpc": "2.0",
276
+ "id": 1,
277
+ "method": "eth_callBundle",
278
+ "params": [{
279
+ "txs": [self._encode_tx(tx) for tx in txs],
280
+ "blockNumber": hex(block_number),
281
+ }],
282
+ }
283
+
284
+ resp = self.session.post(
285
+ self.FLASHBOTS_BUNDLER,
286
+ json=payload,
287
+ timeout=self.config.timeout,
288
+ )
289
+
290
+ if resp.status_code == 200:
291
+ return resp.json()
292
+ else:
293
+ return {"error": f"HTTP {resp.status_code}"}
294
+
295
+ except Exception as e:
296
+ return {"error": str(e)}
297
+
298
+ def get_pending_bundles(self) -> list[BundleResult]:
299
+ """Get list of pending bundles.
300
+
301
+ Returns:
302
+ List of BundleResult objects.
303
+ """
304
+ return [b for b in self._bundles if not b.success]
305
+
306
+ def get_bundle_history(self) -> list[BundleResult]:
307
+ """Get bundle submission history.
308
+
309
+ Returns:
310
+ List of BundleResult objects.
311
+ """
312
+ return self._bundles.copy()
313
+
314
+ def check_sandwich_risk(self, tx: dict) -> dict:
315
+ """Check if a transaction is at risk of sandwich attack.
316
+
317
+ Args:
318
+ tx: Raw transaction dict.
319
+
320
+ Returns:
321
+ Risk assessment dict.
322
+ """
323
+ risk_factors = []
324
+ risk_score = 0
325
+
326
+ # Check if it's a swap (high MEV risk)
327
+ data = tx.get("data", "0x")
328
+ if data.startswith("0x38ed1739"): # swapExactTokensForTokens
329
+ risk_factors.append("Swap transaction (high MEV risk)")
330
+ risk_score += 30
331
+ elif data.startswith("0x7ff36ab5"): # swapExactETHForTokens
332
+ risk_factors.append("ETH swap (high MEV risk)")
333
+ risk_score += 30
334
+
335
+ # Check value
336
+ value = int(tx.get("value", "0x0"), 16) if isinstance(tx.get("value"), str) else tx.get("value", 0)
337
+ if value > 1e18: # > 1 ETH
338
+ risk_factors.append("High value transaction")
339
+ risk_score += 20
340
+
341
+ # Check gas price
342
+ gas_price = int(tx.get("gasPrice", "0x0"), 16) if isinstance(tx.get("gasPrice"), str) else tx.get("gasPrice", 0)
343
+ if gas_price > 50e9: # > 50 Gwei
344
+ risk_factors.append("High gas price (competitive)")
345
+ risk_score += 10
346
+
347
+ return {
348
+ "risk_score": min(100, risk_score),
349
+ "risk_factors": risk_factors,
350
+ "recommendation": "Use Flashbots Protect" if risk_score > 30 else "Standard RPC is fine",
351
+ }
352
+
353
+ # ─── Private Methods ─────────────────────────────────────────
354
+
355
+ def _protect_flashbots(self, raw_tx: dict) -> ProtectedTx:
356
+ """Protect via Flashbots Protect RPC."""
357
+ protected_tx = raw_tx.copy()
358
+ # Flashbots Protect automatically handles protection
359
+ # Just need to send to Flashbots RPC
360
+ return ProtectedTx(
361
+ original_tx=raw_tx,
362
+ protected_tx=protected_tx,
363
+ strategy=MEVStrategy.FLASHBOTS,
364
+ )
365
+
366
+ def _protect_private_rpc(self, raw_tx: dict) -> ProtectedTx:
367
+ """Protect via private transaction relay."""
368
+ protected_tx = raw_tx.copy()
369
+ # Private RPCs don't expose tx to public mempool
370
+ return ProtectedTx(
371
+ original_tx=raw_tx,
372
+ protected_tx=protected_tx,
373
+ strategy=MEVStrategy.PRIVATE_RPC,
374
+ )
375
+
376
+ def _protect_bundle(self, raw_tx: dict) -> ProtectedTx:
377
+ """Protect via Flashbots bundle."""
378
+ target_block = self._get_current_block() + 1
379
+ protected_tx = raw_tx.copy()
380
+ return ProtectedTx(
381
+ original_tx=raw_tx,
382
+ protected_tx=protected_tx,
383
+ strategy=MEVStrategy.BUNDLE,
384
+ target_block=target_block,
385
+ )
386
+
387
+ def _send_flashbots(self, protected: ProtectedTx) -> Optional[str]:
388
+ """Send via Flashbots Protect."""
389
+ try:
390
+ # Flashbots Protect is a JSON-RPC endpoint
391
+ payload = {
392
+ "jsonrpc": "2.0",
393
+ "id": 1,
394
+ "method": "eth_sendRawTransaction",
395
+ "params": [self._encode_tx(protected.protected_tx)],
396
+ }
397
+
398
+ resp = self.session.post(
399
+ self.FLASHBOTS_PROTECT,
400
+ json=payload,
401
+ timeout=self.config.timeout,
402
+ )
403
+
404
+ if resp.status_code == 200:
405
+ data = resp.json()
406
+ if "result" in data:
407
+ protected.status = "submitted"
408
+ protected.bundle_hash = data["result"]
409
+ return data["result"]
410
+ else:
411
+ protected.error = data.get("error", {}).get("message")
412
+ else:
413
+ protected.error = f"HTTP {resp.status_code}"
414
+
415
+ except Exception as e:
416
+ protected.error = str(e)
417
+
418
+ return None
419
+
420
+ def _send_private_rpc(self, protected: ProtectedTx) -> Optional[str]:
421
+ """Send via private RPC."""
422
+ for rpc in self.config.private_rpcs:
423
+ try:
424
+ payload = {
425
+ "jsonrpc": "2.0",
426
+ "id": 1,
427
+ "method": "eth_sendRawTransaction",
428
+ "params": [self._encode_tx(protected.protected_tx)],
429
+ }
430
+
431
+ resp = self.session.post(
432
+ rpc,
433
+ json=payload,
434
+ timeout=self.config.timeout,
435
+ )
436
+
437
+ if resp.status_code == 200:
438
+ data = resp.json()
439
+ if "result" in data:
440
+ protected.status = "submitted"
441
+ return data["result"]
442
+
443
+ except Exception as e:
444
+ logger.debug(f"Private RPC {rpc} failed: {e}")
445
+ continue
446
+
447
+ protected.error = "All private RPCs failed"
448
+ return None
449
+
450
+ def _send_bundle(self, protected: ProtectedTx) -> Optional[str]:
451
+ """Send as part of a bundle."""
452
+ result = self.create_bundle(
453
+ [protected.protected_tx],
454
+ protected.target_block,
455
+ )
456
+ if result.success:
457
+ protected.status = "bundled"
458
+ protected.bundle_hash = result.bundle_hash
459
+ return result.bundle_hash
460
+ else:
461
+ protected.error = result.error
462
+ return None
463
+
464
+ def _send_raw(self, protected: ProtectedTx) -> Optional[str]:
465
+ """Send raw transaction (no protection)."""
466
+ # This would use web3 to send
467
+ logger.warning("Sending raw transaction without MEV protection")
468
+ return None
469
+
470
+ def _get_current_block(self) -> int:
471
+ """Get current block number."""
472
+ try:
473
+ resp = self.session.post(
474
+ "https://eth.llamarpc.com",
475
+ json={
476
+ "jsonrpc": "2.0",
477
+ "id": 1,
478
+ "method": "eth_blockNumber",
479
+ "params": [],
480
+ },
481
+ timeout=10,
482
+ )
483
+ if resp.status_code == 200:
484
+ return int(resp.json()["result"], 16)
485
+ except Exception:
486
+ pass
487
+ return 0
488
+
489
+ def _encode_tx(self, tx: dict) -> str:
490
+ """Encode transaction to hex string."""
491
+ # If already encoded
492
+ if isinstance(tx, str):
493
+ return tx
494
+ # This would use rlp encoding in production
495
+ return tx.get("raw", "0x")