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.
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/PKG-INFO +1 -1
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/pyproject.toml +1 -1
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/dashboard.py +1 -1
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/wl_grinder.py +3 -3
- web3_agent_kit-1.4.0/src/mev/__init__.py +495 -0
- web3_agent_kit-1.4.0/src/nft/__init__.py +442 -0
- web3_agent_kit-1.4.0/src/notifications/__init__.py +417 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/web3_agent_kit.egg-info/PKG-INFO +1 -1
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/web3_agent_kit.egg-info/SOURCES.txt +3 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/LICENSE +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/README.md +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/setup.cfg +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/setup.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/__init__.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/agent.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/__init__.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/base.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/discovery.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/__init__.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/base_executor.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/browser.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/captcha_solver.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/cli.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/galxe_exec.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/gleam_exec.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/intract_exec.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/layer3_exec.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/plugin_registry.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/port3_exec.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/questn.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/social_exec.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/taskon.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/executor/zealy_exec.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/faucet.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/form_filler.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/galxe.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/gleam.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/multi_wallet.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/onchain.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/referral.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/scheduler.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/social.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/tracker.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/airdrop/zealy.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/__init__.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/models.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/__init__.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/approval.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/bridge.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/dca.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/gas.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/portfolio.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/swap.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/wallet.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/watcher.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/api/routes/yield_opt.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/approval_manager.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/bridge.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/chain.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/dca_bot.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/defi/__init__.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/gas_optimizer.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/llm.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/multi_wallet.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/plugins/__init__.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/plugins/examples/gas_tracker.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/portfolio.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/security/__init__.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/sniper.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/utils/__init__.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/wallet.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/wallet_watcher.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/src/yield_optimizer.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_airdrop.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_airdrop_new_modules.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_api.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_bridge.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_core.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_dca_bot.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_defi.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_executor.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_llm.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_multi_wallet.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_new_features.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_platforms.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_plugins.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_portfolio.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_security.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_sniper.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/tests/test_yield_optimizer.py +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/web3_agent_kit.egg-info/dependency_links.txt +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/web3_agent_kit.egg-info/requires.txt +0 -0
- {web3_agent_kit-1.3.0 → web3_agent_kit-1.4.0}/web3_agent_kit.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "web3-agent-kit"
|
|
7
|
-
version = "1.
|
|
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"}
|
|
@@ -130,10 +130,10 @@ class WLGrinder:
|
|
|
130
130
|
Example::
|
|
131
131
|
|
|
132
132
|
profile = WLProfile(
|
|
133
|
-
wallet="
|
|
134
|
-
twitter="@
|
|
133
|
+
wallet="0xYOUR_WALLET...",
|
|
134
|
+
twitter="@your_twitter",
|
|
135
135
|
discord="user#1234",
|
|
136
|
-
email="
|
|
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")
|