cryptoshield 0.2.1__tar.gz → 0.2.2__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.
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/PKG-INFO +1 -1
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/cryptoshield/__init__.py +1 -1
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/cryptoshield/rugpull.py +53 -46
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/cryptoshield/solana.py +111 -101
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/cryptoshield/utils.py +56 -37
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/cryptoshield.egg-info/PKG-INFO +1 -1
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/pyproject.toml +1 -1
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/LICENSE +0 -0
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/README.md +0 -0
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/cryptoshield/approvals.py +0 -0
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/cryptoshield/cli.py +0 -0
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/cryptoshield/db.py +0 -0
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/cryptoshield/honeypot.py +0 -0
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/cryptoshield/phishing.py +0 -0
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/cryptoshield.egg-info/SOURCES.txt +0 -0
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/cryptoshield.egg-info/dependency_links.txt +0 -0
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/cryptoshield.egg-info/entry_points.txt +0 -0
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/cryptoshield.egg-info/requires.txt +0 -0
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/cryptoshield.egg-info/top_level.txt +0 -0
- {cryptoshield-0.2.1 → cryptoshield-0.2.2}/setup.cfg +0 -0
|
@@ -24,18 +24,20 @@ def analyze_rugpull(address: str, chain: str = "eth") -> dict:
|
|
|
24
24
|
"risk_level": "UNKNOWN",
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
# 1. Check if contract has code
|
|
28
|
+
try:
|
|
29
|
+
code = w3.eth.get_code(addr)
|
|
30
|
+
if code == b"" or code == b"0x":
|
|
31
|
+
report["checks"].append({"name": "Contract Code", "status": "FAIL", "detail": "No contract code — not a contract"})
|
|
32
|
+
report["score"] += 50
|
|
33
|
+
else:
|
|
34
|
+
report["checks"].append({"name": "Contract Code", "status": "OK", "detail": f"{len(code)} bytes deployed"})
|
|
35
|
+
except Exception as e:
|
|
36
|
+
report["checks"].append({"name": "Contract Code", "status": "INFO", "detail": f"Could not check: {str(e)[:50]}"})
|
|
36
37
|
|
|
37
38
|
# 2. Check if owner is renounced
|
|
38
39
|
try:
|
|
40
|
+
contract = w3.eth.contract(address=addr, abi=ERC20_ABI)
|
|
39
41
|
owner = contract.functions.owner().call()
|
|
40
42
|
zero = "0x0000000000000000000000000000000000000000"
|
|
41
43
|
dead = "0x000000000000000000000000000000000000dEaD"
|
|
@@ -47,8 +49,9 @@ def analyze_rugpull(address: str, chain: str = "eth") -> dict:
|
|
|
47
49
|
except Exception:
|
|
48
50
|
report["checks"].append({"name": "Ownership", "status": "INFO", "detail": "No owner function (could be good or bad)"})
|
|
49
51
|
|
|
50
|
-
# 3. Check total supply
|
|
52
|
+
# 3. Check total supply
|
|
51
53
|
try:
|
|
54
|
+
contract = w3.eth.contract(address=addr, abi=ERC20_ABI)
|
|
52
55
|
total_supply = contract.functions.totalSupply().call()
|
|
53
56
|
if total_supply > 0:
|
|
54
57
|
report["checks"].append({"name": "Total Supply", "status": "INFO", "detail": f"{total_supply:,}"})
|
|
@@ -56,46 +59,50 @@ def analyze_rugpull(address: str, chain: str = "eth") -> dict:
|
|
|
56
59
|
report["checks"].append({"name": "Total Supply", "status": "FAIL", "detail": "Zero supply"})
|
|
57
60
|
report["score"] += 20
|
|
58
61
|
except Exception:
|
|
59
|
-
report["checks"].append({"name": "Total Supply", "status": "
|
|
62
|
+
report["checks"].append({"name": "Total Supply", "status": "INFO", "detail": "Cannot read supply"})
|
|
60
63
|
|
|
61
|
-
# 4.
|
|
62
|
-
# This is a simplified check — real check would scan DEX pairs
|
|
64
|
+
# 4. Liquidity hint
|
|
63
65
|
report["checks"].append({"name": "Liquidity", "status": "INFO", "detail": "Check manually on DEX Screener"})
|
|
64
66
|
|
|
65
|
-
# 5.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
# 5. GoPlus data (honeypot, source code, proxy, etc.)
|
|
68
|
+
try:
|
|
69
|
+
goplus = check_honeypot(addr, chain)
|
|
70
|
+
if "error" not in goplus:
|
|
71
|
+
if goplus.get("is_honeypot"):
|
|
72
|
+
report["score"] += 40
|
|
73
|
+
report["checks"].append({"name": "Honeypot", "status": "FAIL", "detail": "Token is a honeypot"})
|
|
74
|
+
else:
|
|
75
|
+
report["checks"].append({"name": "Honeypot", "status": "OK", "detail": "Not a honeypot"})
|
|
76
|
+
|
|
77
|
+
if not goplus.get("is_open_source"):
|
|
78
|
+
report["score"] += 15
|
|
79
|
+
report["checks"].append({"name": "Source Code", "status": "FAIL", "detail": "Not verified"})
|
|
80
|
+
else:
|
|
81
|
+
report["checks"].append({"name": "Source Code", "status": "OK", "detail": "Verified on explorer"})
|
|
82
|
+
|
|
83
|
+
if goplus.get("is_proxy"):
|
|
84
|
+
report["score"] += 5
|
|
85
|
+
report["checks"].append({"name": "Proxy", "status": "WARN", "detail": "Proxy contract — logic can change"})
|
|
86
|
+
|
|
87
|
+
if goplus.get("selfdestruct"):
|
|
88
|
+
report["score"] += 15
|
|
89
|
+
report["checks"].append({"name": "Self-Destruct", "status": "FAIL", "detail": "Has selfdestruct function"})
|
|
90
|
+
|
|
91
|
+
if goplus.get("hidden_owner"):
|
|
92
|
+
report["score"] += 10
|
|
93
|
+
report["checks"].append({"name": "Hidden Owner", "status": "FAIL", "detail": "Hidden owner detected"})
|
|
94
|
+
|
|
95
|
+
if goplus.get("owner_can_mint"):
|
|
96
|
+
report["score"] += 10
|
|
97
|
+
report["checks"].append({"name": "Mintable", "status": "WARN", "detail": "Owner can mint new tokens"})
|
|
98
|
+
|
|
99
|
+
holder_count = goplus.get("holder_count", 0)
|
|
100
|
+
if holder_count:
|
|
101
|
+
report["checks"].append({"name": "Holders", "status": "INFO", "detail": f"{holder_count:,} holders"})
|
|
77
102
|
else:
|
|
78
|
-
report["checks"].append({"name": "
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
report["score"] += 5
|
|
82
|
-
report["checks"].append({"name": "Proxy", "status": "WARN", "detail": "Proxy contract — logic can change"})
|
|
83
|
-
|
|
84
|
-
if goplus.get("selfdestruct"):
|
|
85
|
-
report["score"] += 15
|
|
86
|
-
report["checks"].append({"name": "Self-Destruct", "status": "FAIL", "detail": "Has selfdestruct function"})
|
|
87
|
-
|
|
88
|
-
if goplus.get("hidden_owner"):
|
|
89
|
-
report["score"] += 10
|
|
90
|
-
report["checks"].append({"name": "Hidden Owner", "status": "FAIL", "detail": "Hidden owner detected"})
|
|
91
|
-
|
|
92
|
-
if goplus.get("owner_can_mint"):
|
|
93
|
-
report["score"] += 10
|
|
94
|
-
report["checks"].append({"name": "Mintable", "status": "WARN", "detail": "Owner can mint new tokens"})
|
|
95
|
-
|
|
96
|
-
holder_count = goplus.get("holder_count", 0)
|
|
97
|
-
if holder_count:
|
|
98
|
-
report["checks"].append({"name": "Holders", "status": "INFO", "detail": f"{holder_count:,} holders"})
|
|
103
|
+
report["checks"].append({"name": "GoPlus", "status": "INFO", "detail": f"API error: {goplus['error'][:50]}"})
|
|
104
|
+
except Exception as e:
|
|
105
|
+
report["checks"].append({"name": "GoPlus", "status": "INFO", "detail": f"Could not check: {str(e)[:50]}"})
|
|
99
106
|
|
|
100
107
|
# Cap score at 100
|
|
101
108
|
report["score"] = min(report["score"], 100)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Solana token security checker.
|
|
2
2
|
|
|
3
|
-
Uses Solana RPC +
|
|
3
|
+
Uses Solana RPC + Solana Token Registry for token data.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import requests
|
|
@@ -8,9 +8,36 @@ from .db import cache_get, cache_set
|
|
|
8
8
|
from .utils import print_header, print_ok, print_warn, print_fail, print_info
|
|
9
9
|
|
|
10
10
|
SOLANA_RPC = "https://api.mainnet-beta.solana.com"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
TOKEN_REGISTRY_URL = "https://raw.githubusercontent.com/solana-labs/token-list/main/src/tokens/solana.tokenlist.json"
|
|
12
|
+
|
|
13
|
+
# Cache the token registry
|
|
14
|
+
_registry_cache = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get_token_registry() -> dict:
|
|
18
|
+
"""Get the Solana token registry (cached)."""
|
|
19
|
+
global _registry_cache
|
|
20
|
+
if _registry_cache is not None:
|
|
21
|
+
return _registry_cache
|
|
22
|
+
|
|
23
|
+
cached = cache_get("solana_token_registry")
|
|
24
|
+
if cached:
|
|
25
|
+
_registry_cache = cached
|
|
26
|
+
return cached
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
r = requests.get(TOKEN_REGISTRY_URL, timeout=30)
|
|
30
|
+
r.raise_for_status()
|
|
31
|
+
data = r.json()
|
|
32
|
+
tokens = {}
|
|
33
|
+
for t in data.get("tokens", []):
|
|
34
|
+
tokens[t["address"]] = t
|
|
35
|
+
_registry_cache = tokens
|
|
36
|
+
cache_set("solana_token_registry", tokens, ttl=86400) # Cache 24h
|
|
37
|
+
return tokens
|
|
38
|
+
except Exception:
|
|
39
|
+
_registry_cache = {}
|
|
40
|
+
return {}
|
|
14
41
|
|
|
15
42
|
|
|
16
43
|
def check_solana_token(mint: str) -> dict:
|
|
@@ -30,35 +57,23 @@ def check_solana_token(mint: str) -> dict:
|
|
|
30
57
|
"holder_count": 0,
|
|
31
58
|
"total_supply": 0,
|
|
32
59
|
"decimals": 0,
|
|
33
|
-
"
|
|
60
|
+
"is_known": False,
|
|
34
61
|
"freeze_authority": None,
|
|
35
62
|
"mint_authority": None,
|
|
36
|
-
"
|
|
63
|
+
"tags": [],
|
|
37
64
|
}
|
|
38
65
|
|
|
39
|
-
# 1. Get token
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
pass
|
|
51
|
-
|
|
52
|
-
# 2. Check if on Jupiter strict list (vetted tokens)
|
|
53
|
-
try:
|
|
54
|
-
r = requests.get(JUPITER_LIST, timeout=10)
|
|
55
|
-
if r.status_code == 200:
|
|
56
|
-
strict_tokens = {t["address"] for t in r.json()}
|
|
57
|
-
report["is_on_jupiter_strict"] = mint in strict_tokens
|
|
58
|
-
except Exception:
|
|
59
|
-
report["is_on_jupiter_strict"] = False
|
|
60
|
-
|
|
61
|
-
# 3. Get on-chain token info via RPC
|
|
66
|
+
# 1. Get token info from registry
|
|
67
|
+
registry = _get_token_registry()
|
|
68
|
+
if mint in registry:
|
|
69
|
+
token_info = registry[mint]
|
|
70
|
+
report["token_name"] = token_info.get("name", "Unknown")
|
|
71
|
+
report["token_symbol"] = token_info.get("symbol", "?")
|
|
72
|
+
report["decimals"] = token_info.get("decimals", 0)
|
|
73
|
+
report["is_known"] = True
|
|
74
|
+
report["tags"] = token_info.get("tags", [])
|
|
75
|
+
|
|
76
|
+
# 2. Get on-chain token info via RPC
|
|
62
77
|
try:
|
|
63
78
|
payload = {
|
|
64
79
|
"jsonrpc": "2.0",
|
|
@@ -66,7 +81,7 @@ def check_solana_token(mint: str) -> dict:
|
|
|
66
81
|
"method": "getAccountInfo",
|
|
67
82
|
"params": [mint, {"encoding": "jsonParsed"}],
|
|
68
83
|
}
|
|
69
|
-
r = requests.post(SOLANA_RPC, json=payload, timeout=
|
|
84
|
+
r = requests.post(SOLANA_RPC, json=payload, timeout=15)
|
|
70
85
|
data = r.json()
|
|
71
86
|
account = data.get("result", {}).get("value", {})
|
|
72
87
|
|
|
@@ -76,26 +91,30 @@ def check_solana_token(mint: str) -> dict:
|
|
|
76
91
|
|
|
77
92
|
report["mint_authority"] = info.get("mintAuthority")
|
|
78
93
|
report["freeze_authority"] = info.get("freezeAuthority")
|
|
79
|
-
|
|
80
|
-
report["total_supply"] =
|
|
94
|
+
supply_raw = int(info.get("supply", "0"))
|
|
95
|
+
report["total_supply"] = supply_raw
|
|
81
96
|
report["decimals"] = info.get("decimals", report["decimals"])
|
|
82
|
-
except Exception:
|
|
83
|
-
pass
|
|
84
97
|
|
|
85
|
-
|
|
98
|
+
# Calculate human-readable supply
|
|
99
|
+
if report["decimals"] > 0:
|
|
100
|
+
report["total_supply_ui"] = supply_raw / (10 ** report["decimals"])
|
|
101
|
+
else:
|
|
102
|
+
report["total_supply_ui"] = supply_raw
|
|
103
|
+
except Exception as e:
|
|
104
|
+
report["rpc_error"] = str(e)[:50]
|
|
105
|
+
|
|
106
|
+
# 3. Get holder count from Solana RPC (approximate)
|
|
86
107
|
try:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
report["market_cap"] = birddata.get("mc", 0)
|
|
98
|
-
report["price"] = birddata.get("price", 0)
|
|
108
|
+
payload = {
|
|
109
|
+
"jsonrpc": "2.0",
|
|
110
|
+
"id": 1,
|
|
111
|
+
"method": "getTokenLargestAccounts",
|
|
112
|
+
"params": [mint],
|
|
113
|
+
}
|
|
114
|
+
r = requests.post(SOLANA_RPC, json=payload, timeout=15)
|
|
115
|
+
data = r.json()
|
|
116
|
+
largest = data.get("result", {}).get("value", [])
|
|
117
|
+
report["top_holders"] = len(largest)
|
|
99
118
|
except Exception:
|
|
100
119
|
pass
|
|
101
120
|
|
|
@@ -113,34 +132,23 @@ def check_solana_token(mint: str) -> dict:
|
|
|
113
132
|
risks.append("MINT AUTHORITY — unlimited supply, issuer can mint more")
|
|
114
133
|
score += 15
|
|
115
134
|
|
|
116
|
-
# Not
|
|
117
|
-
if not report["
|
|
118
|
-
risks.append("NOT
|
|
135
|
+
# Not in registry
|
|
136
|
+
if not report["is_known"]:
|
|
137
|
+
risks.append("NOT IN TOKEN REGISTRY — unvetted token, high risk")
|
|
119
138
|
score += 15
|
|
120
139
|
|
|
121
|
-
#
|
|
122
|
-
if
|
|
123
|
-
|
|
124
|
-
score += 5
|
|
140
|
+
# Stablecoin tag (positive)
|
|
141
|
+
if "stablecoin" in report.get("tags", []):
|
|
142
|
+
score -= 10 # Lower risk for known stablecoins
|
|
125
143
|
|
|
126
|
-
#
|
|
127
|
-
if
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
score += 5
|
|
133
|
-
|
|
134
|
-
# Low volume
|
|
135
|
-
vol = report.get("daily_volume", 0)
|
|
136
|
-
if vol > 0 and vol < 1000:
|
|
137
|
-
risks.append(f"LOW VOLUME — ${vol:,.0f} in 24h")
|
|
138
|
-
score += 10
|
|
139
|
-
|
|
140
|
-
report["risk_score"] = min(score, 100)
|
|
141
|
-
if score <= 20:
|
|
144
|
+
# Native SOL wrapped (positive)
|
|
145
|
+
if "native" in report.get("tags", []):
|
|
146
|
+
score -= 5
|
|
147
|
+
|
|
148
|
+
report["risk_score"] = max(min(score, 100), 0)
|
|
149
|
+
if report["risk_score"] <= 20:
|
|
142
150
|
report["risk_level"] = "LOW"
|
|
143
|
-
elif
|
|
151
|
+
elif report["risk_score"] <= 50:
|
|
144
152
|
report["risk_level"] = "MEDIUM"
|
|
145
153
|
else:
|
|
146
154
|
report["risk_level"] = "HIGH"
|
|
@@ -165,7 +173,7 @@ def check_solana_wallet(wallet: str) -> dict:
|
|
|
165
173
|
"method": "getBalance",
|
|
166
174
|
"params": [wallet],
|
|
167
175
|
}
|
|
168
|
-
r = requests.post(SOLANA_RPC, json=payload, timeout=
|
|
176
|
+
r = requests.post(SOLANA_RPC, json=payload, timeout=15)
|
|
169
177
|
data = r.json()
|
|
170
178
|
report["sol_balance"] = data.get("result", {}).get("value", 0) / 1e9
|
|
171
179
|
except Exception:
|
|
@@ -183,16 +191,30 @@ def check_solana_wallet(wallet: str) -> dict:
|
|
|
183
191
|
{"encoding": "jsonParsed"},
|
|
184
192
|
],
|
|
185
193
|
}
|
|
186
|
-
r = requests.post(SOLANA_RPC, json=payload, timeout=
|
|
194
|
+
r = requests.post(SOLANA_RPC, json=payload, timeout=15)
|
|
187
195
|
data = r.json()
|
|
188
196
|
accounts = data.get("result", {}).get("value", [])
|
|
189
197
|
|
|
198
|
+
registry = _get_token_registry()
|
|
199
|
+
|
|
190
200
|
for acc in accounts:
|
|
191
201
|
info = acc["account"]["data"]["parsed"]["info"]
|
|
192
202
|
token_amount = info.get("tokenAmount", {})
|
|
203
|
+
mint = info.get("mint", "")
|
|
204
|
+
balance = token_amount.get("uiAmount", 0)
|
|
205
|
+
|
|
206
|
+
# Get token name from registry
|
|
207
|
+
name = "Unknown"
|
|
208
|
+
symbol = mint[:6]
|
|
209
|
+
if mint in registry:
|
|
210
|
+
name = registry[mint].get("name", "Unknown")
|
|
211
|
+
symbol = registry[mint].get("symbol", symbol)
|
|
212
|
+
|
|
193
213
|
report["tokens"].append({
|
|
194
|
-
"mint":
|
|
195
|
-
"
|
|
214
|
+
"mint": mint,
|
|
215
|
+
"name": name,
|
|
216
|
+
"symbol": symbol,
|
|
217
|
+
"balance": balance,
|
|
196
218
|
"decimals": token_amount.get("decimals", 0),
|
|
197
219
|
})
|
|
198
220
|
except Exception:
|
|
@@ -207,15 +229,14 @@ def print_solana_token_report(report: dict):
|
|
|
207
229
|
symbol = report["token_symbol"]
|
|
208
230
|
print_header(f"SOLANA TOKEN CHECK — {name} ({symbol})")
|
|
209
231
|
|
|
210
|
-
#
|
|
211
|
-
if report
|
|
212
|
-
print_ok("Listed
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
print_warn("NOT on Jupiter Strict List")
|
|
232
|
+
# Known status
|
|
233
|
+
if report.get("is_known"):
|
|
234
|
+
print_ok("Listed in Solana Token Registry")
|
|
235
|
+
tags = report.get("tags", [])
|
|
236
|
+
if tags:
|
|
237
|
+
print_info(f"Tags: {', '.join(tags)}")
|
|
217
238
|
else:
|
|
218
|
-
|
|
239
|
+
print_warn("NOT in Token Registry — unvetted token")
|
|
219
240
|
|
|
220
241
|
# Freeze authority
|
|
221
242
|
if report["freeze_authority"]:
|
|
@@ -229,24 +250,13 @@ def print_solana_token_report(report: dict):
|
|
|
229
250
|
else:
|
|
230
251
|
print_ok("Mint Authority: None (fixed supply)")
|
|
231
252
|
|
|
232
|
-
#
|
|
233
|
-
if report
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
print_ok(f"Holders: {report['holder_count']:,}")
|
|
240
|
-
|
|
241
|
-
# Volume
|
|
242
|
-
vol = report.get("daily_volume", 0)
|
|
243
|
-
if vol:
|
|
244
|
-
print_info(f"24h Volume: ${vol:,.0f}")
|
|
245
|
-
|
|
246
|
-
# Market cap
|
|
247
|
-
mc = report.get("market_cap", 0)
|
|
248
|
-
if mc:
|
|
249
|
-
print_info(f"Market Cap: ${mc:,.0f}")
|
|
253
|
+
# Supply
|
|
254
|
+
if report.get("total_supply_ui"):
|
|
255
|
+
print_info(f"Total Supply: {report['total_supply_ui']:,.2f}")
|
|
256
|
+
|
|
257
|
+
# Top holders
|
|
258
|
+
if report.get("top_holders"):
|
|
259
|
+
print_info(f"Top holders: {report['top_holders']} largest accounts")
|
|
250
260
|
|
|
251
261
|
# Risk score
|
|
252
262
|
score = report["risk_score"]
|
|
@@ -6,16 +6,47 @@ from typing import Optional
|
|
|
6
6
|
import requests
|
|
7
7
|
from web3 import Web3
|
|
8
8
|
|
|
9
|
-
# Default RPC endpoints (public, no key needed)
|
|
9
|
+
# Default RPC endpoints (public, no key needed) — with fallbacks
|
|
10
10
|
RPC_ENDPOINTS = {
|
|
11
|
-
"eth":
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
11
|
+
"eth": [
|
|
12
|
+
"https://rpc.ankr.com/eth",
|
|
13
|
+
"https://eth.llamarpc.com",
|
|
14
|
+
"https://eth.drpc.org",
|
|
15
|
+
"https://cloudflare-eth.com",
|
|
16
|
+
],
|
|
17
|
+
"bsc": [
|
|
18
|
+
"https://bsc-dataseed1.binance.org",
|
|
19
|
+
"https://bsc-dataseed2.binance.org",
|
|
20
|
+
"https://rpc.ankr.com/bsc",
|
|
21
|
+
],
|
|
22
|
+
"polygon": [
|
|
23
|
+
"https://polygon-rpc.com",
|
|
24
|
+
"https://rpc.ankr.com/polygon",
|
|
25
|
+
"https://polygon.drpc.org",
|
|
26
|
+
],
|
|
27
|
+
"arbitrum": [
|
|
28
|
+
"https://arb1.arbitrum.io/rpc",
|
|
29
|
+
"https://rpc.ankr.com/arbitrum",
|
|
30
|
+
"https://arbitrum.drpc.org",
|
|
31
|
+
],
|
|
32
|
+
"optimism": [
|
|
33
|
+
"https://mainnet.optimism.io",
|
|
34
|
+
"https://rpc.ankr.com/optimism",
|
|
35
|
+
"https://optimism.drpc.org",
|
|
36
|
+
],
|
|
37
|
+
"base": [
|
|
38
|
+
"https://mainnet.base.org",
|
|
39
|
+
"https://rpc.ankr.com/base",
|
|
40
|
+
"https://base.drpc.org",
|
|
41
|
+
],
|
|
42
|
+
"avalanche": [
|
|
43
|
+
"https://api.avax.network/ext/bc/C/rpc",
|
|
44
|
+
"https://rpc.ankr.com/avalanche",
|
|
45
|
+
],
|
|
46
|
+
"fantom": [
|
|
47
|
+
"https://rpc.ftm.tools",
|
|
48
|
+
"https://rpc.ankr.com/fantom",
|
|
49
|
+
],
|
|
19
50
|
}
|
|
20
51
|
|
|
21
52
|
CHAIN_IDS = {
|
|
@@ -95,31 +126,7 @@ ERC20_ABI = [
|
|
|
95
126
|
},
|
|
96
127
|
]
|
|
97
128
|
|
|
98
|
-
#
|
|
99
|
-
TRANSFER_EVENT = {
|
|
100
|
-
"anonymous": False,
|
|
101
|
-
"inputs": [
|
|
102
|
-
{"indexed": True, "name": "from", "type": "address"},
|
|
103
|
-
{"indexed": True, "name": "to", "type": "address"},
|
|
104
|
-
{"indexed": False, "name": "value", "type": "uint256"},
|
|
105
|
-
],
|
|
106
|
-
"name": "Transfer",
|
|
107
|
-
"type": "event",
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
# Approval event ABI
|
|
111
|
-
APPROVAL_EVENT = {
|
|
112
|
-
"anonymous": False,
|
|
113
|
-
"inputs": [
|
|
114
|
-
{"indexed": True, "name": "owner", "type": "address"},
|
|
115
|
-
{"indexed": True, "name": "spender", "type": "address"},
|
|
116
|
-
{"indexed": False, "name": "value", "type": "uint256"},
|
|
117
|
-
],
|
|
118
|
-
"name": "Approval",
|
|
119
|
-
"type": "event",
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
# Well-known addresses to label
|
|
129
|
+
# Known addresses to label
|
|
123
130
|
KNOWN_ADDRESSES = {
|
|
124
131
|
"0x7a250d5630b4cf539739df2c5dacb4c659f2488d": "Uniswap V2 Router",
|
|
125
132
|
"0xe592427a0aece92de3edee1f18e0157c05861564": "Uniswap V3 Router",
|
|
@@ -140,11 +147,23 @@ KNOWN_ADDRESSES = {
|
|
|
140
147
|
|
|
141
148
|
|
|
142
149
|
def get_web3(chain: str = "eth") -> Web3:
|
|
143
|
-
|
|
144
|
-
|
|
150
|
+
"""Get Web3 instance with automatic RPC fallback."""
|
|
151
|
+
endpoints = RPC_ENDPOINTS.get(chain)
|
|
152
|
+
if not endpoints:
|
|
145
153
|
print(f"Unsupported chain: {chain}")
|
|
146
154
|
sys.exit(1)
|
|
147
|
-
|
|
155
|
+
|
|
156
|
+
for rpc in endpoints:
|
|
157
|
+
try:
|
|
158
|
+
w3 = Web3(Web3.HTTPProvider(rpc, request_kwargs={"timeout": 10}))
|
|
159
|
+
# Test connection
|
|
160
|
+
w3.eth.block_number
|
|
161
|
+
return w3
|
|
162
|
+
except Exception:
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
print(f"All RPC endpoints failed for {chain}")
|
|
166
|
+
sys.exit(1)
|
|
148
167
|
|
|
149
168
|
|
|
150
169
|
def is_valid_address(addr: str) -> bool:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|