@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.
Files changed (28) hide show
  1. package/.claude-plugin/plugin.json +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +97 -0
  4. package/agents/mempool-agent.md +158 -0
  5. package/package.json +43 -0
  6. package/skills/analyzing-mempool/ARD.md +146 -0
  7. package/skills/analyzing-mempool/PRD.md +71 -0
  8. package/skills/analyzing-mempool/SKILL.md +110 -0
  9. package/skills/analyzing-mempool/config/settings.yaml +43 -0
  10. package/skills/analyzing-mempool/references/errors.md +122 -0
  11. package/skills/analyzing-mempool/references/examples.md +189 -0
  12. package/skills/analyzing-mempool/references/implementation.md +67 -0
  13. package/skills/analyzing-mempool/scripts/formatters.py +244 -0
  14. package/skills/analyzing-mempool/scripts/gas_analyzer.py +299 -0
  15. package/skills/analyzing-mempool/scripts/mempool_analyzer.py +320 -0
  16. package/skills/analyzing-mempool/scripts/mev_detector.py +387 -0
  17. package/skills/analyzing-mempool/scripts/rpc_client.py +311 -0
  18. package/skills/analyzing-mempool/scripts/tx_decoder.py +273 -0
  19. package/skills/skill-adapter/assets/README.md +6 -0
  20. package/skills/skill-adapter/assets/config-template.json +32 -0
  21. package/skills/skill-adapter/assets/skill-schema.json +28 -0
  22. package/skills/skill-adapter/assets/test-data.json +27 -0
  23. package/skills/skill-adapter/references/README.md +4 -0
  24. package/skills/skill-adapter/references/best-practices.md +69 -0
  25. package/skills/skill-adapter/references/examples.md +73 -0
  26. package/skills/skill-adapter/scripts/README.md +8 -0
  27. package/skills/skill-adapter/scripts/helper-template.sh +42 -0
  28. package/skills/skill-adapter/scripts/validation.sh +32 -0
@@ -0,0 +1,387 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MEV Opportunity Detector
4
+
5
+ Detect potential MEV opportunities in pending transactions.
6
+
7
+ Author: Jeremy Longshore <jeremy@intentsolutions.io>
8
+ Version: 1.0.0
9
+ License: MIT
10
+ """
11
+
12
+ import os
13
+ from typing import Dict, Any, List, Optional
14
+ from dataclasses import dataclass
15
+
16
+ try:
17
+ import yaml
18
+ HAS_YAML = True
19
+ except ImportError:
20
+ HAS_YAML = False
21
+
22
+ from tx_decoder import TransactionDecoder
23
+
24
+
25
+ @dataclass
26
+ class MEVOpportunity:
27
+ """Detected MEV opportunity."""
28
+ opportunity_type: str # sandwich, arbitrage, liquidation, backrun
29
+ target_tx: str # Target transaction hash
30
+ estimated_profit_usd: float
31
+ required_capital_usd: float
32
+ risk_level: str # low, medium, high
33
+ confidence: float # 0.0 to 1.0
34
+ details: Dict[str, Any]
35
+
36
+
37
+ @dataclass
38
+ class PendingSwap:
39
+ """Detected pending swap transaction."""
40
+ tx_hash: str
41
+ dex: str
42
+ amount_in: Optional[int]
43
+ amount_out_min: Optional[int]
44
+ gas_price: int
45
+ from_address: str
46
+
47
+
48
+ class MEVDetector:
49
+ """Detect MEV opportunities in pending transactions."""
50
+
51
+ # Default thresholds (can be overridden via config)
52
+ DEFAULT_MIN_SWAP_VALUE_USD = 10000 # $10k minimum swap
53
+ DEFAULT_MIN_PROFIT_USD = 100 # $100 minimum profit
54
+
55
+ def __init__(
56
+ self,
57
+ verbose: bool = False,
58
+ min_swap_value_usd: Optional[float] = None,
59
+ min_profit_usd: Optional[float] = None,
60
+ config_path: Optional[str] = None,
61
+ ):
62
+ """Initialize MEV detector.
63
+
64
+ Args:
65
+ verbose: Enable verbose output
66
+ min_swap_value_usd: Minimum swap value for detection (overrides config)
67
+ min_profit_usd: Minimum profit for detection (overrides config)
68
+ config_path: Path to settings.yaml config file
69
+ """
70
+ self.verbose = verbose
71
+ self.decoder = TransactionDecoder(verbose=verbose)
72
+
73
+ # Load config if available
74
+ config = self._load_config(config_path)
75
+
76
+ # Set thresholds with priority: explicit arg > config > default
77
+ config_mev = config.get("mev", {}) if config else {}
78
+ self.min_swap_value_usd = (
79
+ min_swap_value_usd
80
+ if min_swap_value_usd is not None
81
+ else config_mev.get("min_swap_value_usd", self.DEFAULT_MIN_SWAP_VALUE_USD)
82
+ )
83
+ self.min_profit_usd = (
84
+ min_profit_usd
85
+ if min_profit_usd is not None
86
+ else config_mev.get("min_profit_usd", self.DEFAULT_MIN_PROFIT_USD)
87
+ )
88
+
89
+ def _load_config(self, config_path: Optional[str] = None) -> Optional[Dict]:
90
+ """Load configuration from YAML file.
91
+
92
+ Args:
93
+ config_path: Explicit path to config, or None to use defaults
94
+
95
+ Returns:
96
+ Config dict or None if not found/loaded
97
+ """
98
+ if not HAS_YAML:
99
+ return None
100
+
101
+ # Search paths for config
102
+ search_paths = []
103
+ if config_path:
104
+ search_paths.append(config_path)
105
+
106
+ # Default locations
107
+ script_dir = os.path.dirname(os.path.abspath(__file__))
108
+ search_paths.extend([
109
+ os.path.join(script_dir, "..", "config", "settings.yaml"),
110
+ os.path.expanduser("~/.mempool_analyzer.yaml"),
111
+ ])
112
+
113
+ for path in search_paths:
114
+ if os.path.exists(path):
115
+ try:
116
+ with open(path) as f:
117
+ return yaml.safe_load(f)
118
+ except Exception as e:
119
+ if self.verbose:
120
+ print(f"Warning: Could not load config from {path}: {e}")
121
+
122
+ return None
123
+
124
+ def detect_pending_swaps(
125
+ self,
126
+ pending_txs: List[Any],
127
+ eth_price: float = 3000.0
128
+ ) -> List[PendingSwap]:
129
+ """Identify pending swap transactions.
130
+
131
+ Args:
132
+ pending_txs: List of pending transactions
133
+ eth_price: Current ETH price for USD estimation
134
+
135
+ Returns:
136
+ List of detected pending swaps
137
+ """
138
+ swaps = []
139
+
140
+ for tx in pending_txs:
141
+ # Get transaction fields
142
+ if hasattr(tx, "input_data"):
143
+ input_data = tx.input_data
144
+ to_address = tx.to_address
145
+ tx_hash = tx.hash
146
+ gas_price = tx.gas_price
147
+ from_address = tx.from_address
148
+ else:
149
+ input_data = tx.get("input", "")
150
+ to_address = tx.get("to", "")
151
+ tx_hash = tx.get("hash", "")
152
+ gas_price = tx.get("gasPrice", 0)
153
+ from_address = tx.get("from", "")
154
+ if isinstance(gas_price, str):
155
+ gas_price = int(gas_price, 16)
156
+
157
+ # Try to identify as swap
158
+ swap_info = self.decoder.identify_dex_swap(input_data, to_address)
159
+ if swap_info:
160
+ swaps.append(PendingSwap(
161
+ tx_hash=tx_hash,
162
+ dex=swap_info.dex,
163
+ amount_in=swap_info.amount_in,
164
+ amount_out_min=swap_info.amount_out_min,
165
+ gas_price=gas_price,
166
+ from_address=from_address,
167
+ ))
168
+
169
+ return swaps
170
+
171
+ def detect_sandwich_opportunities(
172
+ self,
173
+ pending_swaps: List[PendingSwap],
174
+ eth_price: float = 3000.0
175
+ ) -> List[MEVOpportunity]:
176
+ """Detect potential sandwich attack opportunities.
177
+
178
+ Sandwich: Front-run a large swap, then back-run after price moves.
179
+
180
+ Args:
181
+ pending_swaps: List of detected pending swaps
182
+ eth_price: Current ETH price
183
+
184
+ Returns:
185
+ List of potential sandwich opportunities
186
+ """
187
+ opportunities = []
188
+
189
+ for swap in pending_swaps:
190
+ # Skip small swaps
191
+ if not swap.amount_in:
192
+ continue
193
+
194
+ # Estimate swap value (simplified - assumes ETH value)
195
+ value_eth = swap.amount_in / 10**18
196
+ value_usd = value_eth * eth_price
197
+
198
+ if value_usd < self.min_swap_value_usd:
199
+ continue
200
+
201
+ # Estimate potential profit (very simplified)
202
+ # Real sandwich profit depends on pool liquidity, slippage, etc.
203
+ estimated_slippage = min(value_usd / 1000000, 0.05) # Up to 5% for large swaps
204
+ estimated_profit = value_usd * estimated_slippage * 0.3 # Capture 30% of slippage
205
+
206
+ if estimated_profit < self.min_profit_usd:
207
+ continue
208
+
209
+ opportunities.append(MEVOpportunity(
210
+ opportunity_type="sandwich",
211
+ target_tx=swap.tx_hash,
212
+ estimated_profit_usd=estimated_profit,
213
+ required_capital_usd=value_usd * 0.5, # Need capital to front-run
214
+ risk_level="high", # Sandwiches are risky
215
+ confidence=0.3, # Low confidence without pool analysis
216
+ details={
217
+ "dex": swap.dex,
218
+ "swap_value_usd": value_usd,
219
+ "target_slippage": estimated_slippage,
220
+ "gas_price_gwei": swap.gas_price / 10**9,
221
+ },
222
+ ))
223
+
224
+ return opportunities
225
+
226
+ def detect_arbitrage_opportunities(
227
+ self,
228
+ pending_swaps: List[PendingSwap],
229
+ pool_prices: Dict[str, float] = None
230
+ ) -> List[MEVOpportunity]:
231
+ """Detect arbitrage opportunities from pending swaps.
232
+
233
+ Args:
234
+ pending_swaps: List of detected pending swaps
235
+ pool_prices: Optional pool price data for comparison
236
+
237
+ Returns:
238
+ List of potential arbitrage opportunities
239
+ """
240
+ # Simplified detection - would need real pool data
241
+ opportunities = []
242
+
243
+ # Group swaps by apparent token pair
244
+ # In real implementation, would decode full path and check pool prices
245
+
246
+ for swap in pending_swaps:
247
+ if not swap.amount_in or swap.amount_in < 10**18:
248
+ continue
249
+
250
+ # Mock arbitrage detection (placeholder)
251
+ # Real implementation would:
252
+ # 1. Decode swap path
253
+ # 2. Check prices on other DEXes
254
+ # 3. Calculate if price difference creates opportunity
255
+
256
+ if self.verbose:
257
+ print(f"Checking arb opportunity for {swap.tx_hash[:16]}...")
258
+
259
+ return opportunities
260
+
261
+ def detect_liquidation_opportunities(
262
+ self,
263
+ pending_txs: List[Any]
264
+ ) -> List[MEVOpportunity]:
265
+ """Detect pending liquidation opportunities.
266
+
267
+ Args:
268
+ pending_txs: List of pending transactions
269
+
270
+ Returns:
271
+ List of potential liquidation opportunities
272
+ """
273
+ opportunities = []
274
+
275
+ # Look for lending protocol interactions that might indicate
276
+ # underwater positions or pending liquidations
277
+
278
+ # This would require:
279
+ # 1. Monitoring Aave/Compound/etc health factors
280
+ # 2. Detecting position updates that lower health
281
+ # 3. Calculating profitability of liquidation
282
+
283
+ # Placeholder - real implementation is complex
284
+
285
+ return opportunities
286
+
287
+ def detect_all_opportunities(
288
+ self,
289
+ pending_txs: List[Any],
290
+ eth_price: float = 3000.0
291
+ ) -> Dict[str, List[MEVOpportunity]]:
292
+ """Run all MEV detection algorithms.
293
+
294
+ Args:
295
+ pending_txs: List of pending transactions
296
+ eth_price: Current ETH price
297
+
298
+ Returns:
299
+ Dict mapping opportunity type to list of opportunities
300
+ """
301
+ # First, identify swaps
302
+ swaps = self.detect_pending_swaps(pending_txs, eth_price)
303
+
304
+ results = {
305
+ "pending_swaps": len(swaps),
306
+ "sandwich": self.detect_sandwich_opportunities(swaps, eth_price),
307
+ "arbitrage": self.detect_arbitrage_opportunities(swaps),
308
+ "liquidation": self.detect_liquidation_opportunities(pending_txs),
309
+ }
310
+
311
+ return results
312
+
313
+ def format_opportunities(
314
+ self,
315
+ opportunities: List[MEVOpportunity]
316
+ ) -> str:
317
+ """Format opportunities for display.
318
+
319
+ Args:
320
+ opportunities: List of MEV opportunities
321
+
322
+ Returns:
323
+ Formatted string
324
+ """
325
+ if not opportunities:
326
+ return "No MEV opportunities detected."
327
+
328
+ lines = [
329
+ "",
330
+ "MEV OPPORTUNITIES DETECTED",
331
+ "=" * 80,
332
+ f"{'Type':<12} {'Est. Profit':<14} {'Capital Req':<14} {'Risk':<10} {'Confidence':<12}",
333
+ "-" * 80,
334
+ ]
335
+
336
+ for opp in sorted(opportunities, key=lambda x: x.estimated_profit_usd, reverse=True):
337
+ lines.append(
338
+ f"{opp.opportunity_type:<12} "
339
+ f"${opp.estimated_profit_usd:>11,.0f} "
340
+ f"${opp.required_capital_usd:>11,.0f} "
341
+ f"{opp.risk_level:<10} "
342
+ f"{opp.confidence * 100:>10.0f}%"
343
+ )
344
+
345
+ lines.append("-" * 80)
346
+ lines.append(f"Total: {len(opportunities)} potential opportunities")
347
+ lines.append("")
348
+ lines.append("⚠️ WARNING: MEV detection is for educational purposes only.")
349
+ lines.append(" Actual profitability requires real-time pool data and")
350
+ lines.append(" sophisticated execution infrastructure.")
351
+
352
+ return "\n".join(lines)
353
+
354
+
355
+ def main():
356
+ """CLI entry point for testing."""
357
+ detector = MEVDetector(verbose=True)
358
+
359
+ # Create mock pending transactions
360
+ class MockTx:
361
+ def __init__(self, value, gas_price):
362
+ import random
363
+ self.hash = f"0x{''.join(random.choices('0123456789abcdef', k=64))}"
364
+ self.from_address = f"0x{''.join(random.choices('0123456789abcdef', k=40))}"
365
+ self.to_address = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"
366
+ self.value = value
367
+ self.gas_price = gas_price
368
+ self.input_data = "0x38ed1739" + "0" * 256
369
+
370
+ import random
371
+ mock_txs = [
372
+ MockTx(random.randint(1, 100) * 10**18, (30 + random.randint(0, 20)) * 10**9)
373
+ for _ in range(20)
374
+ ]
375
+
376
+ # Detect
377
+ print("=== Scanning for Pending Swaps ===")
378
+ swaps = detector.detect_pending_swaps(mock_txs)
379
+ print(f"Found {len(swaps)} pending swaps")
380
+
381
+ print("\n=== Checking for MEV Opportunities ===")
382
+ opportunities = detector.detect_sandwich_opportunities(swaps)
383
+ print(detector.format_opportunities(opportunities))
384
+
385
+
386
+ if __name__ == "__main__":
387
+ main()
@@ -0,0 +1,311 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Ethereum RPC Client
4
+
5
+ Connect to Ethereum nodes and fetch mempool data.
6
+
7
+ Author: Jeremy Longshore <jeremy@intentsolutions.io>
8
+ Version: 1.0.0
9
+ License: MIT
10
+ """
11
+
12
+ import os
13
+ import sys
14
+ from typing import Any, Dict, List, Optional
15
+ from dataclasses import dataclass
16
+
17
+ try:
18
+ import requests
19
+ except ImportError:
20
+ requests = None
21
+
22
+
23
+ # Default RPC endpoints (public, may be rate limited)
24
+ DEFAULT_RPC_URLS = {
25
+ "ethereum": "https://eth.llamarpc.com",
26
+ "polygon": "https://polygon-rpc.com",
27
+ "arbitrum": "https://arb1.arbitrum.io/rpc",
28
+ "optimism": "https://mainnet.optimism.io",
29
+ "base": "https://mainnet.base.org",
30
+ }
31
+
32
+
33
+ @dataclass
34
+ class PendingTransaction:
35
+ """Represents a pending transaction in the mempool."""
36
+ hash: str
37
+ from_address: str
38
+ to_address: Optional[str]
39
+ value: int # in wei
40
+ gas: int
41
+ gas_price: int # in wei
42
+ max_fee_per_gas: Optional[int]
43
+ max_priority_fee_per_gas: Optional[int]
44
+ nonce: int
45
+ input_data: str
46
+ block_number: Optional[int]
47
+
48
+
49
+ @dataclass
50
+ class GasInfo:
51
+ """Current gas price information."""
52
+ base_fee: int
53
+ priority_fee: int
54
+ gas_price: int # legacy
55
+ pending_count: int
56
+
57
+
58
+ class RPCClient:
59
+ """Ethereum JSON-RPC client for mempool access."""
60
+
61
+ def __init__(
62
+ self,
63
+ rpc_url: str = None,
64
+ chain: str = "ethereum",
65
+ verbose: bool = False
66
+ ):
67
+ """Initialize RPC client.
68
+
69
+ Args:
70
+ rpc_url: Custom RPC URL or None for default
71
+ chain: Chain name for default URL lookup
72
+ verbose: Enable verbose output
73
+ """
74
+ self.rpc_url = rpc_url or os.environ.get("ETH_RPC_URL") or DEFAULT_RPC_URLS.get(chain)
75
+ self.chain = chain
76
+ self.verbose = verbose
77
+ self._request_id = 0
78
+
79
+ def _rpc_call(self, method: str, params: List = None) -> Any:
80
+ """Make JSON-RPC call."""
81
+ if not requests:
82
+ raise ImportError("requests library required: pip install requests")
83
+
84
+ self._request_id += 1
85
+ payload = {
86
+ "jsonrpc": "2.0",
87
+ "method": method,
88
+ "params": params or [],
89
+ "id": self._request_id,
90
+ }
91
+
92
+ if self.verbose:
93
+ print(f"RPC: {method}")
94
+
95
+ response = requests.post(
96
+ self.rpc_url,
97
+ json=payload,
98
+ headers={"Content-Type": "application/json"},
99
+ timeout=30,
100
+ )
101
+ response.raise_for_status()
102
+
103
+ result = response.json()
104
+ if "error" in result:
105
+ raise Exception(f"RPC error: {result['error']}")
106
+
107
+ return result.get("result")
108
+
109
+ def get_pending_transactions(self, limit: int = 100, allow_mock: bool = False) -> List[PendingTransaction]:
110
+ """Get pending transactions from mempool.
111
+
112
+ Note: Not all nodes support txpool_content.
113
+
114
+ Args:
115
+ limit: Maximum transactions to return
116
+ allow_mock: If True, return mock data when RPC fails (for demo/testing)
117
+
118
+ Returns:
119
+ List of pending transactions
120
+
121
+ Raises:
122
+ RuntimeError: If no RPC method succeeds and allow_mock is False
123
+ """
124
+ errors = []
125
+
126
+ try:
127
+ # Try txpool_content (Geth nodes)
128
+ result = self._rpc_call("txpool_content")
129
+ if result:
130
+ return self._parse_txpool_content(result, limit)
131
+ except Exception as e:
132
+ errors.append(f"txpool_content: {e}")
133
+ if self.verbose:
134
+ print(f"txpool_content not available: {e}", file=sys.stderr)
135
+
136
+ try:
137
+ # Try eth_pendingTransactions (some nodes)
138
+ result = self._rpc_call("eth_pendingTransactions")
139
+ if result:
140
+ return self._parse_pending_transactions(result[:limit])
141
+ except Exception as e:
142
+ errors.append(f"eth_pendingTransactions: {e}")
143
+ if self.verbose:
144
+ print(f"eth_pendingTransactions not available: {e}", file=sys.stderr)
145
+
146
+ # If allow_mock is True (demo mode), return mock data with warning
147
+ if allow_mock:
148
+ print("WARNING: Using mock data - could not fetch real mempool data", file=sys.stderr)
149
+ return self._get_mock_pending_transactions(limit)
150
+
151
+ # Otherwise raise an exception
152
+ raise RuntimeError(
153
+ f"Could not fetch pending transactions from RPC. "
154
+ f"Both 'txpool_content' and 'eth_pendingTransactions' failed.\n"
155
+ f"Errors: {'; '.join(errors)}\n"
156
+ f"Hint: Use --demo flag to use mock data for testing."
157
+ )
158
+
159
+ def _parse_txpool_content(self, content: Dict, limit: int) -> List[PendingTransaction]:
160
+ """Parse txpool_content response."""
161
+ transactions = []
162
+
163
+ for pool in ["pending", "queued"]:
164
+ pool_data = content.get(pool, {})
165
+ for address, nonces in pool_data.items():
166
+ for nonce, tx in nonces.items():
167
+ if len(transactions) >= limit:
168
+ break
169
+ transactions.append(self._tx_to_pending(tx))
170
+
171
+ return transactions[:limit]
172
+
173
+ def _parse_pending_transactions(self, txs: List[Dict]) -> List[PendingTransaction]:
174
+ """Parse eth_pendingTransactions response."""
175
+ return [self._tx_to_pending(tx) for tx in txs]
176
+
177
+ def _tx_to_pending(self, tx: Dict) -> PendingTransaction:
178
+ """Convert raw tx dict to PendingTransaction."""
179
+ return PendingTransaction(
180
+ hash=tx.get("hash", ""),
181
+ from_address=tx.get("from", ""),
182
+ to_address=tx.get("to"),
183
+ value=int(tx.get("value", "0x0"), 16),
184
+ gas=int(tx.get("gas", "0x0"), 16),
185
+ gas_price=int(tx.get("gasPrice", "0x0"), 16),
186
+ max_fee_per_gas=int(tx.get("maxFeePerGas", "0x0"), 16) if tx.get("maxFeePerGas") else None,
187
+ max_priority_fee_per_gas=int(tx.get("maxPriorityFeePerGas", "0x0"), 16) if tx.get("maxPriorityFeePerGas") else None,
188
+ nonce=int(tx.get("nonce", "0x0"), 16),
189
+ input_data=tx.get("input", "0x"),
190
+ block_number=int(tx.get("blockNumber", "0x0"), 16) if tx.get("blockNumber") else None,
191
+ )
192
+
193
+ def _get_mock_pending_transactions(self, limit: int) -> List[PendingTransaction]:
194
+ """Generate mock pending transactions for demo."""
195
+ import random
196
+
197
+ mock_txs = []
198
+ base_gas_price = 30 * 10**9 # 30 gwei
199
+
200
+ # Common router addresses
201
+ routers = [
202
+ "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", # Uniswap V2
203
+ "0xE592427A0AEce92De3Edee1F18E0157C05861564", # Uniswap V3
204
+ "0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F", # SushiSwap
205
+ ]
206
+
207
+ # Sample swap input data prefix
208
+ swap_input = "0x38ed1739" # swapExactTokensForTokens
209
+
210
+ for i in range(min(limit, 20)):
211
+ gas_price = base_gas_price + random.randint(-5, 20) * 10**9
212
+ mock_txs.append(PendingTransaction(
213
+ hash=f"0x{''.join(random.choices('0123456789abcdef', k=64))}",
214
+ from_address=f"0x{''.join(random.choices('0123456789abcdef', k=40))}",
215
+ to_address=random.choice(routers),
216
+ value=random.randint(0, 10) * 10**18,
217
+ gas=random.randint(100000, 500000),
218
+ gas_price=gas_price,
219
+ max_fee_per_gas=gas_price + 5 * 10**9,
220
+ max_priority_fee_per_gas=2 * 10**9,
221
+ nonce=random.randint(1, 1000),
222
+ input_data=swap_input + "0" * 128,
223
+ block_number=None,
224
+ ))
225
+
226
+ return mock_txs
227
+
228
+ def get_gas_price(self) -> GasInfo:
229
+ """Get current gas price information.
230
+
231
+ Returns:
232
+ GasInfo with current gas prices
233
+ """
234
+ try:
235
+ # Get base fee from latest block
236
+ block = self._rpc_call("eth_getBlockByNumber", ["latest", False])
237
+ base_fee = int(block.get("baseFeePerGas", "0x0"), 16)
238
+
239
+ # Get legacy gas price
240
+ gas_price = int(self._rpc_call("eth_gasPrice"), 16)
241
+
242
+ # Estimate priority fee
243
+ priority_fee = max(gas_price - base_fee, 1 * 10**9)
244
+
245
+ # Get pending tx count (optional - not all nodes support txpool_status)
246
+ pending_count = 0
247
+ try:
248
+ txpool_status = self._rpc_call("txpool_status")
249
+ pending_count = int(txpool_status.get("pending", "0x0"), 16)
250
+ except Exception:
251
+ # txpool_status not supported by this node - use 0 as fallback
252
+ pass
253
+
254
+ return GasInfo(
255
+ base_fee=base_fee,
256
+ priority_fee=priority_fee,
257
+ gas_price=gas_price,
258
+ pending_count=pending_count,
259
+ )
260
+
261
+ except Exception as e:
262
+ if self.verbose:
263
+ print(f"Error getting gas price: {e}")
264
+ # Return reasonable defaults
265
+ return GasInfo(
266
+ base_fee=30 * 10**9,
267
+ priority_fee=2 * 10**9,
268
+ gas_price=32 * 10**9,
269
+ pending_count=0,
270
+ )
271
+
272
+ def get_transaction(self, tx_hash: str) -> Optional[Dict]:
273
+ """Get transaction by hash.
274
+
275
+ Args:
276
+ tx_hash: Transaction hash
277
+
278
+ Returns:
279
+ Transaction dict or None
280
+ """
281
+ try:
282
+ return self._rpc_call("eth_getTransactionByHash", [tx_hash])
283
+ except Exception:
284
+ # Transaction not found or RPC error - return None
285
+ return None
286
+
287
+ def get_block_number(self) -> int:
288
+ """Get current block number."""
289
+ result = self._rpc_call("eth_blockNumber")
290
+ return int(result, 16)
291
+
292
+
293
+ def main():
294
+ """CLI entry point for testing."""
295
+ client = RPCClient(verbose=True)
296
+
297
+ print("=== Current Gas Prices ===")
298
+ gas = client.get_gas_price()
299
+ print(f"Base Fee: {gas.base_fee / 10**9:.2f} gwei")
300
+ print(f"Priority Fee: {gas.priority_fee / 10**9:.2f} gwei")
301
+ print(f"Gas Price: {gas.gas_price / 10**9:.2f} gwei")
302
+ print(f"Pending Txs: {gas.pending_count}")
303
+
304
+ print("\n=== Pending Transactions ===")
305
+ pending = client.get_pending_transactions(limit=5)
306
+ for tx in pending:
307
+ print(f" {tx.hash[:16]}... | {tx.gas_price / 10**9:.1f} gwei | {tx.gas} gas")
308
+
309
+
310
+ if __name__ == "__main__":
311
+ main()