@intentsolutionsio/mempool-analyzer 1.0.0
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.
- package/.claude-plugin/plugin.json +22 -0
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/agents/mempool-agent.md +158 -0
- package/package.json +43 -0
- package/skills/analyzing-mempool/ARD.md +146 -0
- package/skills/analyzing-mempool/PRD.md +71 -0
- package/skills/analyzing-mempool/SKILL.md +110 -0
- package/skills/analyzing-mempool/config/settings.yaml +43 -0
- package/skills/analyzing-mempool/references/errors.md +122 -0
- package/skills/analyzing-mempool/references/examples.md +189 -0
- package/skills/analyzing-mempool/references/implementation.md +67 -0
- package/skills/analyzing-mempool/scripts/formatters.py +244 -0
- package/skills/analyzing-mempool/scripts/gas_analyzer.py +299 -0
- package/skills/analyzing-mempool/scripts/mempool_analyzer.py +320 -0
- package/skills/analyzing-mempool/scripts/mev_detector.py +387 -0
- package/skills/analyzing-mempool/scripts/rpc_client.py +311 -0
- package/skills/analyzing-mempool/scripts/tx_decoder.py +273 -0
- package/skills/skill-adapter/assets/README.md +6 -0
- package/skills/skill-adapter/assets/config-template.json +32 -0
- package/skills/skill-adapter/assets/skill-schema.json +28 -0
- package/skills/skill-adapter/assets/test-data.json +27 -0
- package/skills/skill-adapter/references/README.md +4 -0
- package/skills/skill-adapter/references/best-practices.md +69 -0
- package/skills/skill-adapter/references/examples.md +73 -0
- package/skills/skill-adapter/scripts/README.md +8 -0
- package/skills/skill-adapter/scripts/helper-template.sh +42 -0
- package/skills/skill-adapter/scripts/validation.sh +32 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Gas Price Analyzer
|
|
4
|
+
|
|
5
|
+
Analyze gas prices and recommend optimal fees.
|
|
6
|
+
|
|
7
|
+
Author: Jeremy Longshore <jeremy@intentsolutions.io>
|
|
8
|
+
Version: 1.0.0
|
|
9
|
+
License: MIT
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Dict, Any, List, Optional
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
import statistics
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class GasRecommendation:
|
|
19
|
+
"""Gas price recommendation."""
|
|
20
|
+
priority: str # slow, standard, fast, instant
|
|
21
|
+
max_fee: int # in wei
|
|
22
|
+
priority_fee: int # in wei
|
|
23
|
+
estimated_wait: str # e.g., "2-5 blocks"
|
|
24
|
+
confidence: float # 0.0 to 1.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class GasDistribution:
|
|
29
|
+
"""Gas price distribution analysis."""
|
|
30
|
+
min_gwei: float
|
|
31
|
+
max_gwei: float
|
|
32
|
+
mean_gwei: float
|
|
33
|
+
median_gwei: float
|
|
34
|
+
p10_gwei: float # 10th percentile
|
|
35
|
+
p25_gwei: float # 25th percentile
|
|
36
|
+
p50_gwei: float # 50th percentile
|
|
37
|
+
p75_gwei: float # 75th percentile
|
|
38
|
+
p90_gwei: float # 90th percentile
|
|
39
|
+
sample_count: int
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class GasAnalyzer:
|
|
43
|
+
"""Analyze mempool gas prices and provide recommendations."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, verbose: bool = False):
|
|
46
|
+
"""Initialize gas analyzer."""
|
|
47
|
+
self.verbose = verbose
|
|
48
|
+
|
|
49
|
+
def analyze_pending_gas(
|
|
50
|
+
self,
|
|
51
|
+
pending_txs: List[Any],
|
|
52
|
+
base_fee: int = None
|
|
53
|
+
) -> GasDistribution:
|
|
54
|
+
"""Analyze gas price distribution in pending transactions.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
pending_txs: List of pending transactions
|
|
58
|
+
base_fee: Current base fee in wei
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
GasDistribution with statistics
|
|
62
|
+
"""
|
|
63
|
+
# Extract gas prices
|
|
64
|
+
gas_prices = []
|
|
65
|
+
for tx in pending_txs:
|
|
66
|
+
if hasattr(tx, "gas_price"):
|
|
67
|
+
gas_prices.append(tx.gas_price)
|
|
68
|
+
elif isinstance(tx, dict) and "gasPrice" in tx:
|
|
69
|
+
price = tx["gasPrice"]
|
|
70
|
+
if isinstance(price, str):
|
|
71
|
+
price = int(price, 16)
|
|
72
|
+
gas_prices.append(price)
|
|
73
|
+
|
|
74
|
+
if not gas_prices:
|
|
75
|
+
# Return defaults if no data
|
|
76
|
+
default_gwei = 30.0
|
|
77
|
+
return GasDistribution(
|
|
78
|
+
min_gwei=default_gwei,
|
|
79
|
+
max_gwei=default_gwei,
|
|
80
|
+
mean_gwei=default_gwei,
|
|
81
|
+
median_gwei=default_gwei,
|
|
82
|
+
p10_gwei=default_gwei - 5,
|
|
83
|
+
p25_gwei=default_gwei - 2,
|
|
84
|
+
p50_gwei=default_gwei,
|
|
85
|
+
p75_gwei=default_gwei + 5,
|
|
86
|
+
p90_gwei=default_gwei + 10,
|
|
87
|
+
sample_count=0,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Convert to gwei
|
|
91
|
+
gwei_prices = [p / 10**9 for p in gas_prices]
|
|
92
|
+
sorted_prices = sorted(gwei_prices)
|
|
93
|
+
n = len(sorted_prices)
|
|
94
|
+
|
|
95
|
+
def percentile(p: float) -> float:
|
|
96
|
+
idx = int(n * p / 100)
|
|
97
|
+
return sorted_prices[min(idx, n - 1)]
|
|
98
|
+
|
|
99
|
+
return GasDistribution(
|
|
100
|
+
min_gwei=min(gwei_prices),
|
|
101
|
+
max_gwei=max(gwei_prices),
|
|
102
|
+
mean_gwei=statistics.mean(gwei_prices),
|
|
103
|
+
median_gwei=statistics.median(gwei_prices),
|
|
104
|
+
p10_gwei=percentile(10),
|
|
105
|
+
p25_gwei=percentile(25),
|
|
106
|
+
p50_gwei=percentile(50),
|
|
107
|
+
p75_gwei=percentile(75),
|
|
108
|
+
p90_gwei=percentile(90),
|
|
109
|
+
sample_count=n,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def recommend_gas(
|
|
113
|
+
self,
|
|
114
|
+
distribution: GasDistribution = None,
|
|
115
|
+
base_fee: int = None,
|
|
116
|
+
priority: str = "standard"
|
|
117
|
+
) -> GasRecommendation:
|
|
118
|
+
"""Get gas price recommendation.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
distribution: Gas distribution from analyze_pending_gas
|
|
122
|
+
base_fee: Current base fee in wei
|
|
123
|
+
priority: slow, standard, fast, or instant
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
GasRecommendation
|
|
127
|
+
"""
|
|
128
|
+
# Use distribution or defaults
|
|
129
|
+
if distribution:
|
|
130
|
+
prices = {
|
|
131
|
+
"slow": distribution.p10_gwei,
|
|
132
|
+
"standard": distribution.p50_gwei,
|
|
133
|
+
"fast": distribution.p75_gwei,
|
|
134
|
+
"instant": distribution.p90_gwei,
|
|
135
|
+
}
|
|
136
|
+
else:
|
|
137
|
+
# Default recommendations
|
|
138
|
+
prices = {
|
|
139
|
+
"slow": 25,
|
|
140
|
+
"standard": 35,
|
|
141
|
+
"fast": 50,
|
|
142
|
+
"instant": 75,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
wait_times = {
|
|
146
|
+
"slow": "10+ blocks (~3 min)",
|
|
147
|
+
"standard": "2-5 blocks (~1 min)",
|
|
148
|
+
"fast": "1-2 blocks (~30 sec)",
|
|
149
|
+
"instant": "Next block (~12 sec)",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
confidence = {
|
|
153
|
+
"slow": 0.7,
|
|
154
|
+
"standard": 0.9,
|
|
155
|
+
"fast": 0.95,
|
|
156
|
+
"instant": 0.99,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
selected_gwei = prices.get(priority, prices["standard"])
|
|
160
|
+
|
|
161
|
+
# Calculate max fee and priority fee
|
|
162
|
+
if base_fee:
|
|
163
|
+
base_gwei = base_fee / 10**9
|
|
164
|
+
priority_fee_gwei = max(selected_gwei - base_gwei, 1.0)
|
|
165
|
+
# Max fee should be 2x base fee + priority to handle base fee spikes
|
|
166
|
+
max_fee_gwei = base_gwei * 2 + priority_fee_gwei
|
|
167
|
+
else:
|
|
168
|
+
priority_fee_gwei = 2.0
|
|
169
|
+
max_fee_gwei = selected_gwei
|
|
170
|
+
|
|
171
|
+
return GasRecommendation(
|
|
172
|
+
priority=priority,
|
|
173
|
+
max_fee=int(max_fee_gwei * 10**9),
|
|
174
|
+
priority_fee=int(priority_fee_gwei * 10**9),
|
|
175
|
+
estimated_wait=wait_times.get(priority, "Unknown"),
|
|
176
|
+
confidence=confidence.get(priority, 0.5),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def estimate_inclusion_time(
|
|
180
|
+
self,
|
|
181
|
+
gas_price: int,
|
|
182
|
+
distribution: GasDistribution
|
|
183
|
+
) -> str:
|
|
184
|
+
"""Estimate time to transaction inclusion.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
gas_price: Your gas price in wei
|
|
188
|
+
distribution: Current gas distribution
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Estimated wait time string
|
|
192
|
+
"""
|
|
193
|
+
gwei = gas_price / 10**9
|
|
194
|
+
|
|
195
|
+
if gwei >= distribution.p90_gwei:
|
|
196
|
+
return "Next block (~12 sec)"
|
|
197
|
+
elif gwei >= distribution.p75_gwei:
|
|
198
|
+
return "1-2 blocks (~30 sec)"
|
|
199
|
+
elif gwei >= distribution.p50_gwei:
|
|
200
|
+
return "2-5 blocks (~1 min)"
|
|
201
|
+
elif gwei >= distribution.p25_gwei:
|
|
202
|
+
return "5-10 blocks (~2 min)"
|
|
203
|
+
elif gwei >= distribution.p10_gwei:
|
|
204
|
+
return "10+ blocks (~3+ min)"
|
|
205
|
+
else:
|
|
206
|
+
return "May not be included"
|
|
207
|
+
|
|
208
|
+
def format_distribution(self, dist: GasDistribution) -> str:
|
|
209
|
+
"""Format gas distribution for display.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
dist: GasDistribution to format
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Formatted string
|
|
216
|
+
"""
|
|
217
|
+
lines = [
|
|
218
|
+
"",
|
|
219
|
+
"GAS PRICE DISTRIBUTION",
|
|
220
|
+
"=" * 50,
|
|
221
|
+
f"Sample Size: {dist.sample_count} pending transactions",
|
|
222
|
+
"",
|
|
223
|
+
f" Min: {dist.min_gwei:6.1f} gwei",
|
|
224
|
+
f" 10th%: {dist.p10_gwei:6.1f} gwei (Slow)",
|
|
225
|
+
f" 25th%: {dist.p25_gwei:6.1f} gwei",
|
|
226
|
+
f" 50th%: {dist.p50_gwei:6.1f} gwei (Standard)",
|
|
227
|
+
f" 75th%: {dist.p75_gwei:6.1f} gwei (Fast)",
|
|
228
|
+
f" 90th%: {dist.p90_gwei:6.1f} gwei (Instant)",
|
|
229
|
+
f" Max: {dist.max_gwei:6.1f} gwei",
|
|
230
|
+
"",
|
|
231
|
+
f" Mean: {dist.mean_gwei:6.1f} gwei",
|
|
232
|
+
f" Median: {dist.median_gwei:6.1f} gwei",
|
|
233
|
+
"=" * 50,
|
|
234
|
+
]
|
|
235
|
+
return "\n".join(lines)
|
|
236
|
+
|
|
237
|
+
def format_recommendations(self, base_fee: int = None) -> str:
|
|
238
|
+
"""Format all gas recommendations for display.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
base_fee: Current base fee in wei
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Formatted string
|
|
245
|
+
"""
|
|
246
|
+
lines = [
|
|
247
|
+
"",
|
|
248
|
+
"GAS RECOMMENDATIONS",
|
|
249
|
+
"=" * 60,
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
if base_fee:
|
|
253
|
+
lines.append(f"Current Base Fee: {base_fee / 10**9:.1f} gwei")
|
|
254
|
+
lines.append("")
|
|
255
|
+
|
|
256
|
+
lines.append(f"{'Priority':<12} {'Max Fee':<12} {'Priority Fee':<14} {'Est. Wait':<20}")
|
|
257
|
+
lines.append("-" * 60)
|
|
258
|
+
|
|
259
|
+
for priority in ["slow", "standard", "fast", "instant"]:
|
|
260
|
+
rec = self.recommend_gas(priority=priority, base_fee=base_fee)
|
|
261
|
+
max_fee_gwei = rec.max_fee / 10**9
|
|
262
|
+
priority_gwei = rec.priority_fee / 10**9
|
|
263
|
+
lines.append(
|
|
264
|
+
f"{priority.capitalize():<12} {max_fee_gwei:<12.1f} {priority_gwei:<14.1f} {rec.estimated_wait:<20}"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
lines.append("=" * 60)
|
|
268
|
+
return "\n".join(lines)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def main():
|
|
272
|
+
"""CLI entry point for testing."""
|
|
273
|
+
analyzer = GasAnalyzer(verbose=True)
|
|
274
|
+
|
|
275
|
+
# Create mock pending transactions
|
|
276
|
+
class MockTx:
|
|
277
|
+
def __init__(self, gas_price):
|
|
278
|
+
self.gas_price = gas_price
|
|
279
|
+
|
|
280
|
+
import random
|
|
281
|
+
base = 30 * 10**9
|
|
282
|
+
mock_txs = [MockTx(base + random.randint(-10, 30) * 10**9) for _ in range(100)]
|
|
283
|
+
|
|
284
|
+
# Analyze
|
|
285
|
+
dist = analyzer.analyze_pending_gas(mock_txs, base_fee=base)
|
|
286
|
+
print(analyzer.format_distribution(dist))
|
|
287
|
+
|
|
288
|
+
# Recommendations
|
|
289
|
+
print(analyzer.format_recommendations(base_fee=base))
|
|
290
|
+
|
|
291
|
+
# Test inclusion estimate
|
|
292
|
+
print("\n=== Inclusion Time Estimates ===")
|
|
293
|
+
for gwei in [25, 30, 40, 50, 70]:
|
|
294
|
+
est = analyzer.estimate_inclusion_time(gwei * 10**9, dist)
|
|
295
|
+
print(f" {gwei} gwei: {est}")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
if __name__ == "__main__":
|
|
299
|
+
main()
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Mempool Analyzer CLI
|
|
4
|
+
|
|
5
|
+
Monitor blockchain mempools for pending transactions and MEV opportunities.
|
|
6
|
+
|
|
7
|
+
Author: Jeremy Longshore <jeremy@intentsolutions.io>
|
|
8
|
+
Version: 1.0.0
|
|
9
|
+
License: MIT
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
python mempool_analyzer.py pending # View pending transactions
|
|
13
|
+
python mempool_analyzer.py gas # Gas price analysis
|
|
14
|
+
python mempool_analyzer.py swaps # Pending DEX swaps
|
|
15
|
+
python mempool_analyzer.py mev # MEV opportunity scan
|
|
16
|
+
python mempool_analyzer.py summary # Overall summary
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import sys
|
|
21
|
+
|
|
22
|
+
from rpc_client import RPCClient
|
|
23
|
+
from tx_decoder import TransactionDecoder
|
|
24
|
+
from gas_analyzer import GasAnalyzer
|
|
25
|
+
from mev_detector import MEVDetector
|
|
26
|
+
from formatters import (
|
|
27
|
+
format_pending_tx_table,
|
|
28
|
+
format_pending_swaps_table,
|
|
29
|
+
format_mempool_summary,
|
|
30
|
+
format_json,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
DEFAULT_ETH_PRICE = 3000.0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def cmd_pending(args):
|
|
38
|
+
"""Show pending transactions."""
|
|
39
|
+
client = RPCClient(
|
|
40
|
+
rpc_url=args.rpc_url,
|
|
41
|
+
chain=args.chain,
|
|
42
|
+
verbose=args.verbose,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
pending = client.get_pending_transactions(limit=args.limit, allow_mock=args.demo)
|
|
46
|
+
|
|
47
|
+
if args.format == "json":
|
|
48
|
+
print(format_json(pending))
|
|
49
|
+
else:
|
|
50
|
+
print(format_pending_tx_table(pending, eth_price=args.eth_price))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def cmd_gas(args):
|
|
54
|
+
"""Analyze gas prices."""
|
|
55
|
+
client = RPCClient(
|
|
56
|
+
rpc_url=args.rpc_url,
|
|
57
|
+
chain=args.chain,
|
|
58
|
+
verbose=args.verbose,
|
|
59
|
+
)
|
|
60
|
+
analyzer = GasAnalyzer(verbose=args.verbose)
|
|
61
|
+
|
|
62
|
+
# Get gas info
|
|
63
|
+
gas_info = client.get_gas_price()
|
|
64
|
+
|
|
65
|
+
# Get pending transactions for distribution analysis
|
|
66
|
+
pending = client.get_pending_transactions(limit=200, allow_mock=args.demo)
|
|
67
|
+
distribution = analyzer.analyze_pending_gas(pending, base_fee=gas_info.base_fee)
|
|
68
|
+
|
|
69
|
+
if args.format == "json":
|
|
70
|
+
result = {
|
|
71
|
+
"gas_info": vars(gas_info),
|
|
72
|
+
"distribution": vars(distribution),
|
|
73
|
+
}
|
|
74
|
+
print(format_json(result))
|
|
75
|
+
else:
|
|
76
|
+
print(analyzer.format_distribution(distribution))
|
|
77
|
+
print(analyzer.format_recommendations(base_fee=gas_info.base_fee))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def cmd_swaps(args):
|
|
81
|
+
"""Show pending DEX swaps."""
|
|
82
|
+
client = RPCClient(
|
|
83
|
+
rpc_url=args.rpc_url,
|
|
84
|
+
chain=args.chain,
|
|
85
|
+
verbose=args.verbose,
|
|
86
|
+
)
|
|
87
|
+
detector = MEVDetector(verbose=args.verbose)
|
|
88
|
+
|
|
89
|
+
pending = client.get_pending_transactions(limit=args.limit, allow_mock=args.demo)
|
|
90
|
+
swaps = detector.detect_pending_swaps(pending, eth_price=args.eth_price)
|
|
91
|
+
|
|
92
|
+
if args.format == "json":
|
|
93
|
+
print(format_json(swaps))
|
|
94
|
+
else:
|
|
95
|
+
print(format_pending_swaps_table(swaps))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def cmd_mev(args):
|
|
99
|
+
"""Scan for MEV opportunities."""
|
|
100
|
+
client = RPCClient(
|
|
101
|
+
rpc_url=args.rpc_url,
|
|
102
|
+
chain=args.chain,
|
|
103
|
+
verbose=args.verbose,
|
|
104
|
+
)
|
|
105
|
+
detector = MEVDetector(verbose=args.verbose)
|
|
106
|
+
|
|
107
|
+
pending = client.get_pending_transactions(limit=args.limit, allow_mock=args.demo)
|
|
108
|
+
results = detector.detect_all_opportunities(pending, eth_price=args.eth_price)
|
|
109
|
+
|
|
110
|
+
if args.format == "json":
|
|
111
|
+
# Convert opportunities to serializable format
|
|
112
|
+
serialized = {
|
|
113
|
+
"pending_swaps": results["pending_swaps"],
|
|
114
|
+
"sandwich": [vars(o) for o in results["sandwich"]],
|
|
115
|
+
"arbitrage": [vars(o) for o in results["arbitrage"]],
|
|
116
|
+
"liquidation": [vars(o) for o in results["liquidation"]],
|
|
117
|
+
}
|
|
118
|
+
print(format_json(serialized))
|
|
119
|
+
else:
|
|
120
|
+
print(f"\nPending Swaps Analyzed: {results['pending_swaps']}")
|
|
121
|
+
|
|
122
|
+
all_opps = results["sandwich"] + results["arbitrage"] + results["liquidation"]
|
|
123
|
+
print(detector.format_opportunities(all_opps))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def cmd_summary(args):
|
|
127
|
+
"""Show mempool summary."""
|
|
128
|
+
client = RPCClient(
|
|
129
|
+
rpc_url=args.rpc_url,
|
|
130
|
+
chain=args.chain,
|
|
131
|
+
verbose=args.verbose,
|
|
132
|
+
)
|
|
133
|
+
detector = MEVDetector(verbose=args.verbose)
|
|
134
|
+
|
|
135
|
+
# Gather data
|
|
136
|
+
gas_info = client.get_gas_price()
|
|
137
|
+
pending = client.get_pending_transactions(limit=200, allow_mock=args.demo)
|
|
138
|
+
swaps = detector.detect_pending_swaps(pending, eth_price=args.eth_price)
|
|
139
|
+
results = detector.detect_all_opportunities(pending, eth_price=args.eth_price)
|
|
140
|
+
|
|
141
|
+
total_opportunities = (
|
|
142
|
+
len(results["sandwich"]) +
|
|
143
|
+
len(results["arbitrage"]) +
|
|
144
|
+
len(results["liquidation"])
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if args.format == "json":
|
|
148
|
+
summary = {
|
|
149
|
+
"pending_count": len(pending),
|
|
150
|
+
"swap_count": len(swaps),
|
|
151
|
+
"opportunity_count": total_opportunities,
|
|
152
|
+
"gas_info": vars(gas_info),
|
|
153
|
+
}
|
|
154
|
+
print(format_json(summary))
|
|
155
|
+
else:
|
|
156
|
+
print(format_mempool_summary(
|
|
157
|
+
pending_count=len(pending),
|
|
158
|
+
gas_info=gas_info,
|
|
159
|
+
swap_count=len(swaps),
|
|
160
|
+
opportunities=total_opportunities,
|
|
161
|
+
))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def cmd_watch(args):
|
|
165
|
+
"""Watch contract for pending interactions."""
|
|
166
|
+
client = RPCClient(
|
|
167
|
+
rpc_url=args.rpc_url,
|
|
168
|
+
chain=args.chain,
|
|
169
|
+
verbose=args.verbose,
|
|
170
|
+
)
|
|
171
|
+
decoder = TransactionDecoder(verbose=args.verbose)
|
|
172
|
+
|
|
173
|
+
contract = args.contract.lower()
|
|
174
|
+
|
|
175
|
+
print(f"\nWatching for pending transactions to: {contract}")
|
|
176
|
+
print("=" * 60)
|
|
177
|
+
|
|
178
|
+
pending = client.get_pending_transactions(limit=args.limit, allow_mock=args.demo)
|
|
179
|
+
|
|
180
|
+
matching = [
|
|
181
|
+
tx for tx in pending
|
|
182
|
+
if tx.to_address and tx.to_address.lower() == contract
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
if not matching:
|
|
186
|
+
print("No pending transactions found for this contract.")
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
if args.format == "json":
|
|
190
|
+
print(format_json(matching))
|
|
191
|
+
else:
|
|
192
|
+
print(f"Found {len(matching)} pending transactions:\n")
|
|
193
|
+
for tx in matching:
|
|
194
|
+
decoded = decoder.decode_input(tx.input_data, tx.to_address)
|
|
195
|
+
print(f" {tx.hash[:16]}...")
|
|
196
|
+
print(f" Method: {decoded.method_name}")
|
|
197
|
+
print(f" From: {tx.from_address[:16]}...")
|
|
198
|
+
print(f" Gas: {tx.gas_price / 10**9:.1f} gwei")
|
|
199
|
+
print()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def cmd_status(args):
|
|
203
|
+
"""Show connection status."""
|
|
204
|
+
client = RPCClient(
|
|
205
|
+
rpc_url=args.rpc_url,
|
|
206
|
+
chain=args.chain,
|
|
207
|
+
verbose=args.verbose,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
print("\nMEMPOOL ANALYZER STATUS")
|
|
211
|
+
print("=" * 50)
|
|
212
|
+
print(f"Chain: {args.chain}")
|
|
213
|
+
print(f"RPC URL: {client.rpc_url[:50]}...")
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
block = client.get_block_number()
|
|
217
|
+
print(f"Current Block: {block:,}")
|
|
218
|
+
print("Connection: OK")
|
|
219
|
+
except Exception as e:
|
|
220
|
+
print(f"Connection: Error - {e}")
|
|
221
|
+
|
|
222
|
+
gas_info = client.get_gas_price()
|
|
223
|
+
print(f"\nGas Price: {gas_info.gas_price / 10**9:.1f} gwei")
|
|
224
|
+
print(f"Base Fee: {gas_info.base_fee / 10**9:.1f} gwei")
|
|
225
|
+
print("=" * 50)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def main():
|
|
229
|
+
"""Main entry point."""
|
|
230
|
+
parser = argparse.ArgumentParser(
|
|
231
|
+
description="Mempool Analyzer - Monitor pending transactions and MEV",
|
|
232
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
233
|
+
epilog="""
|
|
234
|
+
Examples:
|
|
235
|
+
%(prog)s pending View pending transactions
|
|
236
|
+
%(prog)s gas Analyze gas prices
|
|
237
|
+
%(prog)s swaps Show pending DEX swaps
|
|
238
|
+
%(prog)s mev Scan for MEV opportunities
|
|
239
|
+
%(prog)s summary Overall mempool summary
|
|
240
|
+
%(prog)s watch 0x7a250d... Watch contract for pending txs
|
|
241
|
+
%(prog)s status Check connection status
|
|
242
|
+
%(prog)s --demo pending Use mock data for testing
|
|
243
|
+
"""
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
|
247
|
+
parser.add_argument("--format", choices=["table", "json"], default="table",
|
|
248
|
+
help="Output format")
|
|
249
|
+
parser.add_argument("--rpc-url", help="Custom RPC URL")
|
|
250
|
+
parser.add_argument("--chain", default="ethereum",
|
|
251
|
+
choices=["ethereum", "polygon", "arbitrum", "optimism", "base"],
|
|
252
|
+
help="Blockchain network")
|
|
253
|
+
parser.add_argument("--eth-price", type=float, default=DEFAULT_ETH_PRICE,
|
|
254
|
+
help=f"ETH price for USD conversion (default: {DEFAULT_ETH_PRICE})")
|
|
255
|
+
parser.add_argument("--demo", action="store_true",
|
|
256
|
+
help="Use mock data when RPC fails (for testing/demo)")
|
|
257
|
+
|
|
258
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
|
259
|
+
|
|
260
|
+
# pending command
|
|
261
|
+
pending_parser = subparsers.add_parser("pending", help="View pending transactions")
|
|
262
|
+
pending_parser.add_argument("--limit", type=int, default=50,
|
|
263
|
+
help="Max transactions to show")
|
|
264
|
+
|
|
265
|
+
# gas command
|
|
266
|
+
subparsers.add_parser("gas", help="Analyze gas prices")
|
|
267
|
+
|
|
268
|
+
# swaps command
|
|
269
|
+
swaps_parser = subparsers.add_parser("swaps", help="Show pending DEX swaps")
|
|
270
|
+
swaps_parser.add_argument("--limit", type=int, default=100,
|
|
271
|
+
help="Max transactions to analyze")
|
|
272
|
+
|
|
273
|
+
# mev command
|
|
274
|
+
mev_parser = subparsers.add_parser("mev", help="Scan for MEV opportunities")
|
|
275
|
+
mev_parser.add_argument("--limit", type=int, default=200,
|
|
276
|
+
help="Max transactions to analyze")
|
|
277
|
+
|
|
278
|
+
# summary command
|
|
279
|
+
subparsers.add_parser("summary", help="Mempool summary")
|
|
280
|
+
|
|
281
|
+
# watch command
|
|
282
|
+
watch_parser = subparsers.add_parser("watch", help="Watch contract for pending txs")
|
|
283
|
+
watch_parser.add_argument("contract", help="Contract address to watch")
|
|
284
|
+
watch_parser.add_argument("--limit", type=int, default=100,
|
|
285
|
+
help="Max transactions to check")
|
|
286
|
+
|
|
287
|
+
# status command
|
|
288
|
+
subparsers.add_parser("status", help="Check connection status")
|
|
289
|
+
|
|
290
|
+
args = parser.parse_args()
|
|
291
|
+
|
|
292
|
+
if not args.command:
|
|
293
|
+
# Default to summary
|
|
294
|
+
args.command = "summary"
|
|
295
|
+
|
|
296
|
+
commands = {
|
|
297
|
+
"pending": cmd_pending,
|
|
298
|
+
"gas": cmd_gas,
|
|
299
|
+
"swaps": cmd_swaps,
|
|
300
|
+
"mev": cmd_mev,
|
|
301
|
+
"summary": cmd_summary,
|
|
302
|
+
"watch": cmd_watch,
|
|
303
|
+
"status": cmd_status,
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
commands[args.command](args)
|
|
308
|
+
except KeyboardInterrupt:
|
|
309
|
+
print("\nInterrupted")
|
|
310
|
+
sys.exit(1)
|
|
311
|
+
except Exception as e:
|
|
312
|
+
print(f"Error: {e}")
|
|
313
|
+
if args.verbose:
|
|
314
|
+
import traceback
|
|
315
|
+
traceback.print_exc()
|
|
316
|
+
sys.exit(1)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if __name__ == "__main__":
|
|
320
|
+
main()
|