@intentsolutionsio/token-launch-tracker 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 (27) hide show
  1. package/.claude-plugin/plugin.json +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +162 -0
  4. package/agents/launch-tracker-agent.md +338 -0
  5. package/package.json +43 -0
  6. package/skills/skill-adapter/assets/README.md +5 -0
  7. package/skills/skill-adapter/assets/config-template.json +32 -0
  8. package/skills/skill-adapter/assets/skill-schema.json +28 -0
  9. package/skills/skill-adapter/assets/test-data.json +27 -0
  10. package/skills/skill-adapter/references/README.md +4 -0
  11. package/skills/skill-adapter/references/best-practices.md +69 -0
  12. package/skills/skill-adapter/references/examples.md +73 -0
  13. package/skills/skill-adapter/scripts/README.md +8 -0
  14. package/skills/skill-adapter/scripts/helper-template.sh +42 -0
  15. package/skills/skill-adapter/scripts/validation.sh +32 -0
  16. package/skills/tracking-token-launches/ARD.md +183 -0
  17. package/skills/tracking-token-launches/PRD.md +66 -0
  18. package/skills/tracking-token-launches/SKILL.md +161 -0
  19. package/skills/tracking-token-launches/config/settings.yaml +166 -0
  20. package/skills/tracking-token-launches/references/errors.md +167 -0
  21. package/skills/tracking-token-launches/references/examples.md +292 -0
  22. package/skills/tracking-token-launches/references/implementation.md +36 -0
  23. package/skills/tracking-token-launches/scripts/dex_sources.py +270 -0
  24. package/skills/tracking-token-launches/scripts/event_monitor.py +345 -0
  25. package/skills/tracking-token-launches/scripts/formatters.py +279 -0
  26. package/skills/tracking-token-launches/scripts/launch_tracker.py +572 -0
  27. package/skills/tracking-token-launches/scripts/token_analyzer.py +417 -0
@@ -0,0 +1,417 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Token Contract Analyzer
4
+
5
+ Analyze token contracts for risk indicators.
6
+
7
+ Author: Jeremy Longshore <jeremy@intentsolutions.io>
8
+ Version: 1.0.0
9
+ License: MIT
10
+ """
11
+
12
+ import os
13
+ from dataclasses import dataclass, field
14
+ from typing import Any, List, Optional
15
+
16
+ try:
17
+ import requests
18
+ except ImportError:
19
+ requests = None
20
+
21
+ from dex_sources import get_chain_config
22
+
23
+
24
+ @dataclass
25
+ class TokenInfo:
26
+ """Complete token information."""
27
+ address: str
28
+ name: str
29
+ symbol: str
30
+ decimals: int
31
+ total_supply: int
32
+ owner: Optional[str] = None
33
+ is_verified: bool = False
34
+ creation_block: Optional[int] = None
35
+
36
+
37
+ @dataclass
38
+ class RiskIndicator:
39
+ """Single risk indicator."""
40
+ name: str
41
+ detected: bool
42
+ severity: str # high, medium, low, info
43
+ description: str
44
+
45
+
46
+ @dataclass
47
+ class ContractAnalysis:
48
+ """Complete contract risk analysis."""
49
+ address: str
50
+ risk_score: int # 0-100 (higher = riskier)
51
+ indicators: List[RiskIndicator] = field(default_factory=list)
52
+ bytecode_size: int = 0
53
+ is_proxy: bool = False
54
+ ownership_renounced: bool = False
55
+
56
+
57
+ # Known risky function signatures
58
+ # Note: 0xa9059cbb is the standard ERC20 transfer function, not blacklist
59
+ RISKY_FUNCTIONS = {
60
+ "0x40c10f19": ("mint", "high", "Contract has mint function"),
61
+ "0x44337ea1": ("blacklist", "medium", "Contract has blacklist functionality"), # addToBlacklist(address)
62
+ "0x42966c68": ("burn", "info", "Contract has burn function"),
63
+ "0x23b872dd": ("transferFrom", "info", "Standard ERC20 transferFrom"),
64
+ "0x8da5cb5b": ("owner", "info", "Contract has owner"),
65
+ "0x715018a6": ("renounceOwnership", "info", "Can renounce ownership"),
66
+ "0xf2fde38b": ("transferOwnership", "low", "Can transfer ownership"),
67
+ }
68
+
69
+ # Bytecode patterns for suspicious contracts
70
+ SUSPICIOUS_PATTERNS = [
71
+ ("73757370656e64", "medium", "Contains 'suspend' string"),
72
+ ("626c61636b6c697374", "medium", "Contains 'blacklist' string"),
73
+ ("77686974656c697374", "low", "Contains 'whitelist' string"),
74
+ ("6f6e6c794f776e6572", "info", "Contains 'onlyOwner' modifier"),
75
+ ]
76
+
77
+
78
+ class TokenAnalyzer:
79
+ """Analyze token contracts for risks."""
80
+
81
+ def __init__(
82
+ self,
83
+ chain: str = "ethereum",
84
+ rpc_url: str = None,
85
+ etherscan_api_key: str = None,
86
+ verbose: bool = False
87
+ ):
88
+ """Initialize token analyzer.
89
+
90
+ Args:
91
+ chain: Chain to analyze on
92
+ rpc_url: Custom RPC URL
93
+ etherscan_api_key: Etherscan API key for verification check
94
+ verbose: Enable verbose output
95
+ """
96
+ self.chain = chain.lower()
97
+ self.config = get_chain_config(chain)
98
+ self.rpc_url = rpc_url or os.environ.get(
99
+ f"{chain.upper()}_RPC_URL",
100
+ self.config.rpc_url
101
+ )
102
+ self.etherscan_key = etherscan_api_key or os.environ.get("ETHERSCAN_API_KEY", "")
103
+ self.verbose = verbose
104
+
105
+ def _rpc_call(self, method: str, params: List = None) -> Any:
106
+ """Make JSON-RPC call."""
107
+ if not requests:
108
+ raise ImportError("requests library required")
109
+
110
+ response = requests.post(
111
+ self.rpc_url,
112
+ json={
113
+ "jsonrpc": "2.0",
114
+ "method": method,
115
+ "params": params or [],
116
+ "id": 1,
117
+ },
118
+ headers={"Content-Type": "application/json"},
119
+ timeout=30,
120
+ )
121
+ response.raise_for_status()
122
+
123
+ result = response.json()
124
+ if "error" in result:
125
+ raise Exception(f"RPC error: {result['error']}")
126
+
127
+ return result.get("result")
128
+
129
+ def _call_contract(self, address: str, data: str) -> Optional[str]:
130
+ """Make eth_call."""
131
+ try:
132
+ result = self._rpc_call("eth_call", [
133
+ {"to": address, "data": data},
134
+ "latest"
135
+ ])
136
+ return result if result and result != "0x" else None
137
+ except Exception:
138
+ return None
139
+
140
+ def get_token_info(self, address: str) -> TokenInfo:
141
+ """Get complete token information.
142
+
143
+ Args:
144
+ address: Token contract address
145
+
146
+ Returns:
147
+ TokenInfo object
148
+ """
149
+ # ERC20 function signatures
150
+ name_result = self._call_contract(address, "0x06fdde03")
151
+ symbol_result = self._call_contract(address, "0x95d89b41")
152
+ decimals_result = self._call_contract(address, "0x313ce567")
153
+ supply_result = self._call_contract(address, "0x18160ddd")
154
+ owner_result = self._call_contract(address, "0x8da5cb5b")
155
+
156
+ return TokenInfo(
157
+ address=address,
158
+ name=self._decode_string(name_result) if name_result else "Unknown",
159
+ symbol=self._decode_string(symbol_result) if symbol_result else "???",
160
+ decimals=int(decimals_result, 16) if decimals_result else 18,
161
+ total_supply=int(supply_result, 16) if supply_result else 0,
162
+ owner="0x" + owner_result[-40:] if owner_result and len(owner_result) >= 42 else None,
163
+ is_verified=self._check_verified(address),
164
+ )
165
+
166
+ def _decode_string(self, data: str) -> str:
167
+ """Decode ABI-encoded string."""
168
+ if not data or data == "0x":
169
+ return ""
170
+
171
+ try:
172
+ data = data[2:]
173
+
174
+ if len(data) <= 64:
175
+ return bytes.fromhex(data).decode("utf-8", errors="ignore").strip("\x00")
176
+
177
+ if len(data) >= 128:
178
+ length = int(data[64:128], 16)
179
+ string_data = data[128:128 + length * 2]
180
+ return bytes.fromhex(string_data).decode("utf-8", errors="ignore")
181
+
182
+ return bytes.fromhex(data).decode("utf-8", errors="ignore").strip("\x00")
183
+
184
+ except Exception:
185
+ return "Unknown"
186
+
187
+ def _check_verified(self, address: str) -> bool:
188
+ """Check if contract is verified on block explorer."""
189
+ if not requests or not self.etherscan_key:
190
+ return False
191
+
192
+ try:
193
+ # Map chain to explorer API
194
+ explorer_apis = {
195
+ "ethereum": "https://api.etherscan.io/api",
196
+ "bsc": "https://api.bscscan.com/api",
197
+ "arbitrum": "https://api.arbiscan.io/api",
198
+ "base": "https://api.basescan.org/api",
199
+ "polygon": "https://api.polygonscan.com/api",
200
+ }
201
+
202
+ api_url = explorer_apis.get(self.chain)
203
+ if not api_url:
204
+ return False
205
+
206
+ response = requests.get(api_url, params={
207
+ "module": "contract",
208
+ "action": "getsourcecode",
209
+ "address": address,
210
+ "apikey": self.etherscan_key,
211
+ }, timeout=10)
212
+
213
+ data = response.json()
214
+ if data.get("status") == "1" and data.get("result"):
215
+ source = data["result"][0].get("SourceCode", "")
216
+ return bool(source and source != "")
217
+
218
+ except Exception as e:
219
+ if self.verbose:
220
+ print(f"Verification check error: {e}")
221
+
222
+ return False
223
+
224
+ def analyze_contract(self, address: str) -> ContractAnalysis:
225
+ """Analyze contract for risk indicators.
226
+
227
+ Args:
228
+ address: Contract address
229
+
230
+ Returns:
231
+ ContractAnalysis with risk score and indicators
232
+ """
233
+ indicators = []
234
+ risk_score = 0
235
+
236
+ # Get bytecode
237
+ bytecode = self._rpc_call("eth_getCode", [address, "latest"])
238
+ bytecode_size = (len(bytecode) - 2) // 2 if bytecode else 0
239
+
240
+ # Check if proxy
241
+ is_proxy = self._detect_proxy(address, bytecode)
242
+ if is_proxy:
243
+ indicators.append(RiskIndicator(
244
+ name="Proxy Contract",
245
+ detected=True,
246
+ severity="medium",
247
+ description="Contract is a proxy - implementation can be changed"
248
+ ))
249
+ risk_score += 20
250
+
251
+ # Check ownership
252
+ owner_result = self._call_contract(address, "0x8da5cb5b")
253
+ owner = "0x" + owner_result[-40:] if owner_result and len(owner_result) >= 42 else None
254
+ ownership_renounced = owner and owner.lower() == "0x0000000000000000000000000000000000000000"
255
+
256
+ if owner:
257
+ if ownership_renounced:
258
+ indicators.append(RiskIndicator(
259
+ name="Ownership Renounced",
260
+ detected=True,
261
+ severity="info",
262
+ description="Ownership has been renounced"
263
+ ))
264
+ else:
265
+ indicators.append(RiskIndicator(
266
+ name="Has Owner",
267
+ detected=True,
268
+ severity="low",
269
+ description=f"Contract has active owner: {owner[:20]}..."
270
+ ))
271
+ risk_score += 10
272
+
273
+ # Check for risky function signatures in bytecode
274
+ for sig, (name, severity, desc) in RISKY_FUNCTIONS.items():
275
+ if sig[2:] in bytecode.lower():
276
+ indicators.append(RiskIndicator(
277
+ name=f"Has {name}",
278
+ detected=True,
279
+ severity=severity,
280
+ description=desc
281
+ ))
282
+ if severity == "high":
283
+ risk_score += 30
284
+ elif severity == "medium":
285
+ risk_score += 15
286
+ elif severity == "low":
287
+ risk_score += 5
288
+
289
+ # Check for suspicious patterns
290
+ for pattern, severity, desc in SUSPICIOUS_PATTERNS:
291
+ if pattern in bytecode.lower():
292
+ indicators.append(RiskIndicator(
293
+ name="Suspicious Pattern",
294
+ detected=True,
295
+ severity=severity,
296
+ description=desc
297
+ ))
298
+ if severity == "high":
299
+ risk_score += 25
300
+ elif severity == "medium":
301
+ risk_score += 10
302
+
303
+ # Check verification
304
+ is_verified = self._check_verified(address)
305
+ if not is_verified:
306
+ indicators.append(RiskIndicator(
307
+ name="Not Verified",
308
+ detected=True,
309
+ severity="medium",
310
+ description="Contract source code not verified"
311
+ ))
312
+ risk_score += 15
313
+
314
+ # Very small bytecode might be suspicious
315
+ if bytecode_size < 500:
316
+ indicators.append(RiskIndicator(
317
+ name="Small Contract",
318
+ detected=True,
319
+ severity="info",
320
+ description=f"Bytecode is only {bytecode_size} bytes"
321
+ ))
322
+
323
+ # Cap at 100
324
+ risk_score = min(risk_score, 100)
325
+
326
+ return ContractAnalysis(
327
+ address=address,
328
+ risk_score=risk_score,
329
+ indicators=indicators,
330
+ bytecode_size=bytecode_size,
331
+ is_proxy=is_proxy,
332
+ ownership_renounced=ownership_renounced,
333
+ )
334
+
335
+ def _detect_proxy(self, address: str, bytecode: str) -> bool:
336
+ """Detect if contract is a proxy.
337
+
338
+ Args:
339
+ address: Contract address
340
+ bytecode: Contract bytecode
341
+
342
+ Returns:
343
+ True if proxy detected
344
+ """
345
+ # Common proxy patterns
346
+ proxy_patterns = [
347
+ "363d3d373d3d3d363d73", # Minimal proxy
348
+ "5155f3", # DELEGATECALL
349
+ "363d3d373d3d3d363d", # EIP-1167
350
+ ]
351
+
352
+ bytecode_lower = bytecode.lower()
353
+ for pattern in proxy_patterns:
354
+ if pattern in bytecode_lower:
355
+ return True
356
+
357
+ # Check EIP-1967 implementation slot
358
+ impl_slot = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"
359
+ try:
360
+ storage = self._rpc_call("eth_getStorageAt", [address, impl_slot, "latest"])
361
+ if storage and storage != "0x" + "0" * 64:
362
+ return True
363
+ except Exception:
364
+ pass # Storage read failed - likely not a proxy or access issue
365
+
366
+ return False
367
+
368
+ def get_risk_summary(self, analysis: ContractAnalysis) -> str:
369
+ """Get human-readable risk summary.
370
+
371
+ Args:
372
+ analysis: ContractAnalysis object
373
+
374
+ Returns:
375
+ Risk level string
376
+ """
377
+ if analysis.risk_score >= 70:
378
+ return "HIGH RISK"
379
+ elif analysis.risk_score >= 40:
380
+ return "MEDIUM RISK"
381
+ elif analysis.risk_score >= 20:
382
+ return "LOW RISK"
383
+ else:
384
+ return "MINIMAL RISK"
385
+
386
+
387
+ def main():
388
+ """CLI entry point for testing."""
389
+ analyzer = TokenAnalyzer(chain="ethereum", verbose=True)
390
+
391
+ # Test with a known token (USDC)
392
+ test_address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
393
+
394
+ print("=== Token Info ===")
395
+ info = analyzer.get_token_info(test_address)
396
+ print(f"Name: {info.name}")
397
+ print(f"Symbol: {info.symbol}")
398
+ print(f"Decimals: {info.decimals}")
399
+ print(f"Supply: {info.total_supply / 10**info.decimals:,.0f}")
400
+ print(f"Owner: {info.owner}")
401
+ print(f"Verified: {info.is_verified}")
402
+
403
+ print("\n=== Contract Analysis ===")
404
+ analysis = analyzer.analyze_contract(test_address)
405
+ print(f"Risk Score: {analysis.risk_score}/100")
406
+ print(f"Summary: {analyzer.get_risk_summary(analysis)}")
407
+ print(f"Bytecode Size: {analysis.bytecode_size} bytes")
408
+ print(f"Is Proxy: {analysis.is_proxy}")
409
+ print(f"Ownership Renounced: {analysis.ownership_renounced}")
410
+
411
+ print("\nIndicators:")
412
+ for ind in analysis.indicators:
413
+ print(f" [{ind.severity.upper()}] {ind.name}: {ind.description}")
414
+
415
+
416
+ if __name__ == "__main__":
417
+ main()