cryptoshield 0.2.0__py3-none-any.whl
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/__init__.py +4 -0
- cryptoshield/approvals.py +185 -0
- cryptoshield/cli.py +166 -0
- cryptoshield/db.py +50 -0
- cryptoshield/honeypot.py +219 -0
- cryptoshield/phishing.py +454 -0
- cryptoshield/rugpull.py +141 -0
- cryptoshield/solana.py +264 -0
- cryptoshield/utils.py +194 -0
- cryptoshield-0.2.0.dist-info/METADATA +157 -0
- cryptoshield-0.2.0.dist-info/RECORD +15 -0
- cryptoshield-0.2.0.dist-info/WHEEL +5 -0
- cryptoshield-0.2.0.dist-info/entry_points.txt +2 -0
- cryptoshield-0.2.0.dist-info/licenses/LICENSE +21 -0
- cryptoshield-0.2.0.dist-info/top_level.txt +1 -0
cryptoshield/__init__.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Token approval scanner — find dangerous approvals on your wallet.
|
|
2
|
+
|
|
3
|
+
Scans on-chain Approval events to find all active token approvals.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from web3 import Web3
|
|
7
|
+
from .utils import (
|
|
8
|
+
get_web3, checksum, label_address, ERC20_ABI, APPROVAL_EVENT,
|
|
9
|
+
KNOWN_ADDRESSES, print_header, print_ok, print_warn, print_fail, print_info,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# Common approval spender labels
|
|
13
|
+
SPENDER_LABELS = {
|
|
14
|
+
**KNOWN_ADDRESSES,
|
|
15
|
+
"0x0000000000000000000000000000000000000000": "Zero Address (burn)",
|
|
16
|
+
"0x000000000000000000000000000000000000dead": "Dead Address (burn)",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Approvals above this are considered "unlimited"
|
|
20
|
+
UNLIMITED_THRESHOLD = 2**250
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def scan_approvals(wallet: str, chain: str = "eth", blocks_back: int = 100_000) -> list:
|
|
24
|
+
"""Scan all token approvals for a wallet."""
|
|
25
|
+
w3 = get_web3(chain)
|
|
26
|
+
wallet = checksum(wallet)
|
|
27
|
+
|
|
28
|
+
current_block = w3.eth.block_number
|
|
29
|
+
from_block = max(0, current_block - blocks_back)
|
|
30
|
+
|
|
31
|
+
# Get Approval events where owner = wallet
|
|
32
|
+
approval_topic = w3.keccak(
|
|
33
|
+
text="Approval(address,address,uint256)"
|
|
34
|
+
).hex()
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
logs = w3.eth.get_logs({
|
|
38
|
+
"fromBlock": from_block,
|
|
39
|
+
"toBlock": current_block,
|
|
40
|
+
"topics": [
|
|
41
|
+
approval_topic,
|
|
42
|
+
"0x" + wallet[2:].lower().zfill(64), # owner
|
|
43
|
+
],
|
|
44
|
+
})
|
|
45
|
+
except Exception as e:
|
|
46
|
+
return [{"error": str(e)}]
|
|
47
|
+
|
|
48
|
+
# Deduplicate: keep latest approval per (token, spender)
|
|
49
|
+
approvals = {}
|
|
50
|
+
for log in logs:
|
|
51
|
+
try:
|
|
52
|
+
token_addr = log["address"]
|
|
53
|
+
spender = "0x" + log["topics"][2].hex()[-40:]
|
|
54
|
+
spender = Web3.to_checksum_address(spender)
|
|
55
|
+
value = int(log["data"].hex(), 16)
|
|
56
|
+
|
|
57
|
+
key = (token_addr.lower(), spender.lower())
|
|
58
|
+
approvals[key] = {
|
|
59
|
+
"token": token_addr,
|
|
60
|
+
"spender": spender,
|
|
61
|
+
"value": value,
|
|
62
|
+
"block": log["blockNumber"],
|
|
63
|
+
}
|
|
64
|
+
except Exception:
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
# Filter: only keep current approvals (check on-chain allowance)
|
|
68
|
+
active = []
|
|
69
|
+
for (token_lower, spender_lower), info in approvals.items():
|
|
70
|
+
if info["value"] == 0:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
contract = w3.eth.contract(
|
|
75
|
+
address=checksum(info["token"]), abi=ERC20_ABI
|
|
76
|
+
)
|
|
77
|
+
current_allowance = contract.functions.allowance(
|
|
78
|
+
wallet, checksum(info["spender"])
|
|
79
|
+
).call()
|
|
80
|
+
|
|
81
|
+
if current_allowance == 0:
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
# Get token info
|
|
85
|
+
try:
|
|
86
|
+
name = contract.functions.name().call()
|
|
87
|
+
except Exception:
|
|
88
|
+
name = "Unknown"
|
|
89
|
+
try:
|
|
90
|
+
symbol = contract.functions.symbol().call()
|
|
91
|
+
except Exception:
|
|
92
|
+
symbol = "?"
|
|
93
|
+
|
|
94
|
+
is_unlimited = current_allowance >= UNLIMITED_THRESHOLD
|
|
95
|
+
spender_label = SPENDER_LABELS.get(
|
|
96
|
+
info["spender"].lower(), "Unknown Contract"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
active.append({
|
|
100
|
+
"token": info["token"],
|
|
101
|
+
"token_name": name,
|
|
102
|
+
"token_symbol": symbol,
|
|
103
|
+
"spender": info["spender"],
|
|
104
|
+
"spender_label": spender_label,
|
|
105
|
+
"allowance": current_allowance,
|
|
106
|
+
"is_unlimited": is_unlimited,
|
|
107
|
+
"is_known": info["spender"].lower() in KNOWN_ADDRESSES,
|
|
108
|
+
})
|
|
109
|
+
except Exception:
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
return active
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def print_approval_report(approvals: list, wallet: str):
|
|
116
|
+
"""Pretty-print approval scan results."""
|
|
117
|
+
if not approvals:
|
|
118
|
+
print_ok("No active approvals found — your wallet is clean!")
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
if "error" in approvals[0]:
|
|
122
|
+
print_fail(f"Error: {approvals[0]['error']}")
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
print_header(f"APPROVAL AUDIT — {wallet[:6]}...{wallet[-4:]}")
|
|
126
|
+
|
|
127
|
+
dangerous = []
|
|
128
|
+
caution = []
|
|
129
|
+
safe = []
|
|
130
|
+
|
|
131
|
+
for a in approvals:
|
|
132
|
+
if a["is_unlimited"] and not a["is_known"]:
|
|
133
|
+
dangerous.append(a)
|
|
134
|
+
elif a["is_unlimited"] and a["is_known"]:
|
|
135
|
+
caution.append(a)
|
|
136
|
+
else:
|
|
137
|
+
safe.append(a)
|
|
138
|
+
|
|
139
|
+
# Dangerous first
|
|
140
|
+
for a in dangerous:
|
|
141
|
+
print_fail(
|
|
142
|
+
f"{a['token_symbol']} → UNLIMITED to {label_address(a['spender'])}"
|
|
143
|
+
)
|
|
144
|
+
print(f" ⚡ RECOMMEND: revoke immediately")
|
|
145
|
+
|
|
146
|
+
# Caution
|
|
147
|
+
for a in caution:
|
|
148
|
+
print_warn(
|
|
149
|
+
f"{a['token_symbol']} → unlimited to {label_address(a['spender'])}"
|
|
150
|
+
)
|
|
151
|
+
print(f" Known protocol — consider reducing allowance")
|
|
152
|
+
|
|
153
|
+
# Safe
|
|
154
|
+
for a in safe:
|
|
155
|
+
if a["allowance"] > 10**18:
|
|
156
|
+
amount = f"{a['allowance'] / 10**18:.2f}"
|
|
157
|
+
else:
|
|
158
|
+
amount = str(a["allowance"])
|
|
159
|
+
print_ok(
|
|
160
|
+
f"{a['token_symbol']} → {amount} to {label_address(a['spender'])}"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
print()
|
|
164
|
+
print_info(f"Total approvals: {len(approvals)}")
|
|
165
|
+
if dangerous:
|
|
166
|
+
print_fail(f"{len(dangerous)} HIGH RISK — revoke now!")
|
|
167
|
+
if caution:
|
|
168
|
+
print_warn(f"{len(caution)} caution — review recommended")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
REVOKE4_ABI = [
|
|
172
|
+
{
|
|
173
|
+
"inputs": [
|
|
174
|
+
{"name": "token", "type": "address"},
|
|
175
|
+
{"name": "spender", "type": "address"},
|
|
176
|
+
],
|
|
177
|
+
"name": "revoke",
|
|
178
|
+
"outputs": [],
|
|
179
|
+
"stateMutability": "nonpayable",
|
|
180
|
+
"type": "function",
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
# revoke.xyz contract
|
|
185
|
+
REVOKE_CONTRACT = "0x000000000000Dd366e1DA4F6c8a2b2F9e01f6F1E"
|
cryptoshield/cli.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""CryptoShield CLI — All-in-one crypto security toolkit."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from . import __version__
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(
|
|
9
|
+
name="cryptoshield",
|
|
10
|
+
help="🛡 CryptoShield — All-in-one crypto security toolkit",
|
|
11
|
+
add_completion=False,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command()
|
|
16
|
+
def check(
|
|
17
|
+
address: str = typer.Argument(..., help="Token contract address"),
|
|
18
|
+
chain: str = typer.Option("eth", "-c", "--chain", help="Chain: eth, bsc, polygon, arbitrum, optimism, base, avalanche, fantom, solana"),
|
|
19
|
+
quick: bool = typer.Option(False, "-q", "--quick", help="Quick check (honeypot only)"),
|
|
20
|
+
):
|
|
21
|
+
"""Full security report for a token contract."""
|
|
22
|
+
if chain == "solana":
|
|
23
|
+
from .solana import check_solana_token, print_solana_token_report
|
|
24
|
+
print(f"\n🛡 CRYPTO SHIELD — Scanning Solana token {address[:10]}...")
|
|
25
|
+
print("━" * 50)
|
|
26
|
+
report = check_solana_token(address)
|
|
27
|
+
print_solana_token_report(report)
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
from .honeypot import check_honeypot, print_honeypot_report
|
|
31
|
+
from .rugpull import analyze_rugpull, print_rugpull_report
|
|
32
|
+
|
|
33
|
+
print(f"\n🛡 CRYPTO SHIELD — Scanning {address[:10]}... on {chain}")
|
|
34
|
+
print("━" * 50)
|
|
35
|
+
|
|
36
|
+
if quick:
|
|
37
|
+
report = check_honeypot(address, chain)
|
|
38
|
+
print_honeypot_report(report)
|
|
39
|
+
else:
|
|
40
|
+
report = analyze_rugpull(address, chain)
|
|
41
|
+
print_rugpull_report(report)
|
|
42
|
+
print()
|
|
43
|
+
hp = check_honeypot(address, chain)
|
|
44
|
+
print_honeypot_report(hp)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.command()
|
|
48
|
+
def approvals(
|
|
49
|
+
wallet: str = typer.Argument(..., help="Wallet address to scan"),
|
|
50
|
+
chain: str = typer.Option("eth", "-c", "--chain", help="Chain to scan"),
|
|
51
|
+
blocks: int = typer.Option(100000, "-b", "--blocks", help="How many blocks back to scan"),
|
|
52
|
+
):
|
|
53
|
+
"""Scan token approvals for a wallet."""
|
|
54
|
+
from .approvals import scan_approvals, print_approval_report
|
|
55
|
+
|
|
56
|
+
print(f"\n🛡 Scanning approvals for {wallet[:10]}... on {chain}")
|
|
57
|
+
results = scan_approvals(wallet, chain, blocks)
|
|
58
|
+
print_approval_report(results, wallet)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.command()
|
|
62
|
+
def rugpull(
|
|
63
|
+
address: str = typer.Argument(..., help="Token contract address"),
|
|
64
|
+
chain: str = typer.Option("eth", "-c", "--chain", help="Chain"),
|
|
65
|
+
):
|
|
66
|
+
"""Analyze rugpull risk for a token."""
|
|
67
|
+
if chain == "solana":
|
|
68
|
+
from .solana import check_solana_token, print_solana_token_report
|
|
69
|
+
report = check_solana_token(address)
|
|
70
|
+
print_solana_token_report(report)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
from .rugpull import analyze_rugpull, print_rugpull_report
|
|
74
|
+
|
|
75
|
+
report = analyze_rugpull(address, chain)
|
|
76
|
+
print_rugpull_report(report)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@app.command("check-url")
|
|
80
|
+
def check_url_cmd(
|
|
81
|
+
url: str = typer.Argument(..., help="URL to check"),
|
|
82
|
+
):
|
|
83
|
+
"""Check if a URL is a known or suspected phishing site."""
|
|
84
|
+
from .phishing import check_url, print_phishing_report
|
|
85
|
+
|
|
86
|
+
report = check_url(url)
|
|
87
|
+
print_phishing_report(report)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@app.command()
|
|
91
|
+
def solana(
|
|
92
|
+
wallet: str = typer.Argument(..., help="Solana wallet address"),
|
|
93
|
+
):
|
|
94
|
+
"""Check Solana wallet — SOL balance + token holdings."""
|
|
95
|
+
from .solana import check_solana_wallet
|
|
96
|
+
|
|
97
|
+
print(f"\n🛡 Solana wallet scan — {wallet[:10]}...")
|
|
98
|
+
print("━" * 50)
|
|
99
|
+
report = check_solana_wallet(wallet)
|
|
100
|
+
|
|
101
|
+
print(f"\n 💰 SOL Balance: {report['sol_balance']:.4f} SOL")
|
|
102
|
+
print(f" 📦 Token accounts: {len(report['tokens'])}")
|
|
103
|
+
|
|
104
|
+
if report["tokens"]:
|
|
105
|
+
print("\n Token Holdings:")
|
|
106
|
+
for t in report["tokens"]:
|
|
107
|
+
if t["balance"] > 0:
|
|
108
|
+
print(f" • {t['mint'][:8]}...{t['mint'][-4:]}: {t['balance']:,.2f}")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.command()
|
|
112
|
+
def batch(
|
|
113
|
+
addresses_file: str = typer.Argument(..., help="File with addresses (one per line)"),
|
|
114
|
+
chain: str = typer.Option("eth", "-c", "--chain", help="Chain"),
|
|
115
|
+
mode: str = typer.Option("honeypot", "-m", "--mode", help="Mode: honeypot, approvals, rugpull"),
|
|
116
|
+
):
|
|
117
|
+
"""Batch check multiple addresses from a file."""
|
|
118
|
+
from pathlib import Path
|
|
119
|
+
|
|
120
|
+
path = Path(addresses_file)
|
|
121
|
+
if not path.exists():
|
|
122
|
+
print(f"File not found: {addresses_file}")
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
|
|
125
|
+
addresses = [line.strip() for line in path.read_text().splitlines() if line.strip()]
|
|
126
|
+
print(f"\n🛡 Batch {mode} check — {len(addresses)} addresses on {chain}")
|
|
127
|
+
print("━" * 50)
|
|
128
|
+
|
|
129
|
+
for i, addr in enumerate(addresses, 1):
|
|
130
|
+
print(f"\n[{i}/{len(addresses)}] {addr[:10]}...")
|
|
131
|
+
if mode == "honeypot":
|
|
132
|
+
if chain == "solana":
|
|
133
|
+
from .solana import check_solana_token, print_solana_token_report
|
|
134
|
+
report = check_solana_token(addr)
|
|
135
|
+
print_solana_token_report(report)
|
|
136
|
+
else:
|
|
137
|
+
from .honeypot import check_honeypot, print_honeypot_report
|
|
138
|
+
report = check_honeypot(addr, chain)
|
|
139
|
+
print_honeypot_report(report)
|
|
140
|
+
elif mode == "rugpull":
|
|
141
|
+
if chain == "solana":
|
|
142
|
+
from .solana import check_solana_token, print_solana_token_report
|
|
143
|
+
report = check_solana_token(addr)
|
|
144
|
+
print_solana_token_report(report)
|
|
145
|
+
else:
|
|
146
|
+
from .rugpull import analyze_rugpull, print_rugpull_report
|
|
147
|
+
report = analyze_rugpull(addr, chain)
|
|
148
|
+
print_rugpull_report(report)
|
|
149
|
+
elif mode == "approvals":
|
|
150
|
+
from .approvals import scan_approvals, print_approval_report
|
|
151
|
+
results = scan_approvals(addr, chain)
|
|
152
|
+
print_approval_report(results, addr)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@app.command()
|
|
156
|
+
def version():
|
|
157
|
+
"""Show version."""
|
|
158
|
+
print(f"CryptoShield v{__version__}")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def main():
|
|
162
|
+
app()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
if __name__ == "__main__":
|
|
166
|
+
main()
|
cryptoshield/db.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""SQLite cache for API results."""
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
DB_PATH = Path.home() / ".cryptoshield" / "cache.db"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_db() -> sqlite3.Connection:
|
|
12
|
+
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
13
|
+
conn = sqlite3.connect(str(DB_PATH))
|
|
14
|
+
conn.execute("""
|
|
15
|
+
CREATE TABLE IF NOT EXISTS cache (
|
|
16
|
+
key TEXT PRIMARY KEY,
|
|
17
|
+
value TEXT NOT NULL,
|
|
18
|
+
expires_at REAL NOT NULL
|
|
19
|
+
)
|
|
20
|
+
""")
|
|
21
|
+
conn.commit()
|
|
22
|
+
return conn
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def cache_get(key: str) -> dict | None:
|
|
26
|
+
conn = get_db()
|
|
27
|
+
row = conn.execute(
|
|
28
|
+
"SELECT value, expires_at FROM cache WHERE key = ?", (key,)
|
|
29
|
+
).fetchone()
|
|
30
|
+
conn.close()
|
|
31
|
+
if row and row[1] > time.time():
|
|
32
|
+
return json.loads(row[0])
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def cache_set(key: str, value: dict, ttl: int = 3600):
|
|
37
|
+
conn = get_db()
|
|
38
|
+
conn.execute(
|
|
39
|
+
"INSERT OR REPLACE INTO cache (key, value, expires_at) VALUES (?, ?, ?)",
|
|
40
|
+
(key, json.dumps(value), time.time() + ttl),
|
|
41
|
+
)
|
|
42
|
+
conn.commit()
|
|
43
|
+
conn.close()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def cache_clear():
|
|
47
|
+
conn = get_db()
|
|
48
|
+
conn.execute("DELETE FROM cache")
|
|
49
|
+
conn.commit()
|
|
50
|
+
conn.close()
|
cryptoshield/honeypot.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Honeypot checker — detect scam tokens before you buy.
|
|
2
|
+
|
|
3
|
+
Uses GoPlus Security API (free, no key required).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .db import cache_get, cache_set
|
|
7
|
+
from .utils import fetch_json, print_header, print_ok, print_warn, print_fail, print_info
|
|
8
|
+
|
|
9
|
+
GOPLUS_TOKEN_SECURITY = "https://api.gopluslabs.io/api/v1/token_security/{chain_id}"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_chain_id_goplus(chain: str) -> int:
|
|
13
|
+
"""Map chain name to GoPlus chain ID."""
|
|
14
|
+
mapping = {
|
|
15
|
+
"eth": 1,
|
|
16
|
+
"bsc": 56,
|
|
17
|
+
"polygon": 137,
|
|
18
|
+
"arbitrum": 42161,
|
|
19
|
+
"optimism": 10,
|
|
20
|
+
"base": 8453,
|
|
21
|
+
"avalanche": 43114,
|
|
22
|
+
"fantom": 250,
|
|
23
|
+
}
|
|
24
|
+
return mapping.get(chain, 1)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def check_honeypot(address: str, chain: str = "eth") -> dict:
|
|
28
|
+
"""Check if a token is a honeypot using GoPlus API."""
|
|
29
|
+
cache_key = f"honeypot:{chain}:{address.lower()}"
|
|
30
|
+
cached = cache_get(cache_key)
|
|
31
|
+
if cached:
|
|
32
|
+
return cached
|
|
33
|
+
|
|
34
|
+
chain_id = get_chain_id_goplus(chain)
|
|
35
|
+
url = GOPLUS_TOKEN_SECURITY.format(chain_id=chain_id)
|
|
36
|
+
data = fetch_json(url, params={"contract_addresses": address})
|
|
37
|
+
|
|
38
|
+
if "error" in data:
|
|
39
|
+
return {"error": data["error"]}
|
|
40
|
+
|
|
41
|
+
result = data.get("result", {})
|
|
42
|
+
token_data = result.get(address.lower(), result.get(address, {}))
|
|
43
|
+
|
|
44
|
+
if not token_data:
|
|
45
|
+
return {"error": "Token not found on GoPlus"}
|
|
46
|
+
|
|
47
|
+
# Parse into structured report
|
|
48
|
+
report = {
|
|
49
|
+
"address": address,
|
|
50
|
+
"chain": chain,
|
|
51
|
+
"token_name": token_data.get("token_name", "Unknown"),
|
|
52
|
+
"token_symbol": token_data.get("token_symbol", "?"),
|
|
53
|
+
"is_honeypot": _to_bool(token_data.get("is_honeypot")),
|
|
54
|
+
"buy_tax": _to_float(token_data.get("buy_tax", "0")),
|
|
55
|
+
"sell_tax": _to_float(token_data.get("sell_tax", "0")),
|
|
56
|
+
"can_buy": not _to_bool(token_data.get("cannot_buy")),
|
|
57
|
+
"can_sell": not _to_bool(token_data.get("cannot_sell_all")),
|
|
58
|
+
"owner_can_mint": _to_bool(token_data.get("is_mintable")),
|
|
59
|
+
"owner_can_change_balance": _to_bool(token_data.get("owner_change_balance")),
|
|
60
|
+
"hidden_owner": _to_bool(token_data.get("hidden_owner")),
|
|
61
|
+
"selfdestruct": _to_bool(token_data.get("selfdestruct")),
|
|
62
|
+
"external_call": _to_bool(token_data.get("external_call")),
|
|
63
|
+
"is_proxy": _to_bool(token_data.get("is_proxy")),
|
|
64
|
+
"is_blacklisted": _to_bool(token_data.get("is_blacklisted")),
|
|
65
|
+
"is_whitelisted": _to_bool(token_data.get("is_whitelisted")),
|
|
66
|
+
"trading_cooldown": _to_bool(token_data.get("trading_cooldown")),
|
|
67
|
+
"is_open_source": _to_bool(token_data.get("is_open_source")),
|
|
68
|
+
"holder_count": _to_int(token_data.get("holder_count")),
|
|
69
|
+
"total_supply": token_data.get("total_supply", "0"),
|
|
70
|
+
"lp_total_supply": token_data.get("lp_total_supply", "0"),
|
|
71
|
+
"lp_holder_count": _to_int(token_data.get("lp_holder_count")),
|
|
72
|
+
"owner_address": token_data.get("owner_address", ""),
|
|
73
|
+
"creator_address": token_data.get("creator_address", ""),
|
|
74
|
+
"dex": token_data.get("dex", []),
|
|
75
|
+
"risks": [],
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Calculate risk flags
|
|
79
|
+
if report["is_honeypot"]:
|
|
80
|
+
report["risks"].append("HONEYPOT — cannot sell")
|
|
81
|
+
if report["buy_tax"] > 0.10:
|
|
82
|
+
report["risks"].append(f"HIGH BUY TAX — {report['buy_tax']*100:.0f}%")
|
|
83
|
+
elif report["buy_tax"] > 0.05:
|
|
84
|
+
report["risks"].append(f"BUY TAX — {report['buy_tax']*100:.0f}%")
|
|
85
|
+
if report["sell_tax"] > 0.10:
|
|
86
|
+
report["risks"].append(f"HIGH SELL TAX — {report['sell_tax']*100:.0f}%")
|
|
87
|
+
elif report["sell_tax"] > 0.05:
|
|
88
|
+
report["risks"].append(f"SELL TAX — {report['sell_tax']*100:.0f}%")
|
|
89
|
+
if report["owner_can_mint"]:
|
|
90
|
+
report["risks"].append("OWNER CAN MINT — infinite supply risk")
|
|
91
|
+
if report["owner_can_change_balance"]:
|
|
92
|
+
report["risks"].append("OWNER CAN CHANGE BALANCES")
|
|
93
|
+
if report["hidden_owner"]:
|
|
94
|
+
report["risks"].append("HIDDEN OWNER")
|
|
95
|
+
if report["selfdestruct"]:
|
|
96
|
+
report["risks"].append("SELF-DESTRUCT FUNCTION")
|
|
97
|
+
if report["external_call"]:
|
|
98
|
+
report["risks"].append("EXTERNAL CALLS — possible exploit vector")
|
|
99
|
+
if report["is_proxy"]:
|
|
100
|
+
report["risks"].append("PROXY CONTRACT — logic can change")
|
|
101
|
+
if not report["is_open_source"]:
|
|
102
|
+
report["risks"].append("CLOSED SOURCE — cannot verify code")
|
|
103
|
+
|
|
104
|
+
# Score: 0 = safe, 10 = scam
|
|
105
|
+
score = 0
|
|
106
|
+
if report["is_honeypot"]:
|
|
107
|
+
score += 50
|
|
108
|
+
score += min(int(report["buy_tax"] * 100), 20)
|
|
109
|
+
score += min(int(report["sell_tax"] * 100), 20)
|
|
110
|
+
if report["owner_can_mint"]:
|
|
111
|
+
score += 15
|
|
112
|
+
if report["owner_can_change_balance"]:
|
|
113
|
+
score += 15
|
|
114
|
+
if report["hidden_owner"]:
|
|
115
|
+
score += 10
|
|
116
|
+
if report["selfdestruct"]:
|
|
117
|
+
score += 10
|
|
118
|
+
if not report["is_open_source"]:
|
|
119
|
+
score += 10
|
|
120
|
+
if report["is_proxy"]:
|
|
121
|
+
score += 5
|
|
122
|
+
report["risk_score"] = min(score, 100)
|
|
123
|
+
|
|
124
|
+
cache_set(cache_key, report, ttl=1800) # Cache 30 min
|
|
125
|
+
return report
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def print_honeypot_report(report: dict):
|
|
129
|
+
"""Pretty-print honeypot check results."""
|
|
130
|
+
if "error" in report:
|
|
131
|
+
print_fail(f"Error: {report['error']}")
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
print_header(f"HONEYPOT CHECK — {report['token_name']} ({report['token_symbol']})")
|
|
135
|
+
|
|
136
|
+
# Sell ability
|
|
137
|
+
if report["can_sell"]:
|
|
138
|
+
print_ok("Can sell: YES")
|
|
139
|
+
else:
|
|
140
|
+
print_fail("Can sell: NO — HONEYPOT")
|
|
141
|
+
|
|
142
|
+
# Taxes
|
|
143
|
+
buy_pct = report["buy_tax"] * 100
|
|
144
|
+
sell_pct = report["sell_tax"] * 100
|
|
145
|
+
if buy_pct == 0 and sell_pct == 0:
|
|
146
|
+
print_ok("Tax: 0% buy / 0% sell")
|
|
147
|
+
elif buy_pct <= 5 and sell_pct <= 5:
|
|
148
|
+
print_ok(f"Tax: {buy_pct:.0f}% buy / {sell_pct:.0f}% sell")
|
|
149
|
+
elif buy_pct <= 10 and sell_pct <= 10:
|
|
150
|
+
print_warn(f"Tax: {buy_pct:.0f}% buy / {sell_pct:.0f}% sell")
|
|
151
|
+
else:
|
|
152
|
+
print_fail(f"Tax: {buy_pct:.0f}% buy / {sell_pct:.0f}% sell")
|
|
153
|
+
|
|
154
|
+
# Owner risks
|
|
155
|
+
if report["owner_can_mint"]:
|
|
156
|
+
print_fail("Owner can mint: YES — infinite supply risk")
|
|
157
|
+
else:
|
|
158
|
+
print_ok("Owner can mint: NO")
|
|
159
|
+
|
|
160
|
+
if report["owner_can_change_balance"]:
|
|
161
|
+
print_fail("Owner can change balances: YES")
|
|
162
|
+
|
|
163
|
+
if report["hidden_owner"]:
|
|
164
|
+
print_fail("Hidden owner: YES")
|
|
165
|
+
|
|
166
|
+
if report["selfdestruct"]:
|
|
167
|
+
print_fail("Self-destruct: YES")
|
|
168
|
+
|
|
169
|
+
if not report["is_open_source"]:
|
|
170
|
+
print_warn("Contract: NOT verified / closed source")
|
|
171
|
+
else:
|
|
172
|
+
print_ok("Contract: Verified")
|
|
173
|
+
|
|
174
|
+
if report["is_proxy"]:
|
|
175
|
+
print_warn("Proxy contract: YES — logic can be changed")
|
|
176
|
+
|
|
177
|
+
# Holder info
|
|
178
|
+
if report["holder_count"]:
|
|
179
|
+
print_info(f"Holders: {report['holder_count']}")
|
|
180
|
+
|
|
181
|
+
# Risk score
|
|
182
|
+
score = report["risk_score"]
|
|
183
|
+
if score <= 20:
|
|
184
|
+
print_ok(f"Risk Score: {score}/100 — LOW RISK")
|
|
185
|
+
elif score <= 50:
|
|
186
|
+
print_warn(f"Risk Score: {score}/100 — MEDIUM RISK")
|
|
187
|
+
else:
|
|
188
|
+
print_fail(f"Risk Score: {score}/100 — HIGH RISK")
|
|
189
|
+
|
|
190
|
+
# Risk flags
|
|
191
|
+
if report["risks"]:
|
|
192
|
+
print()
|
|
193
|
+
print(" Risks:")
|
|
194
|
+
for risk in report["risks"]:
|
|
195
|
+
print(f" ⚡ {risk}")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _to_bool(val) -> bool:
|
|
199
|
+
if isinstance(val, bool):
|
|
200
|
+
return val
|
|
201
|
+
if isinstance(val, str):
|
|
202
|
+
return val in ("1", "true", "True")
|
|
203
|
+
if isinstance(val, (int, float)):
|
|
204
|
+
return val == 1
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _to_float(val) -> float:
|
|
209
|
+
try:
|
|
210
|
+
return float(val)
|
|
211
|
+
except (ValueError, TypeError):
|
|
212
|
+
return 0.0
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _to_int(val) -> int:
|
|
216
|
+
try:
|
|
217
|
+
return int(val)
|
|
218
|
+
except (ValueError, TypeError):
|
|
219
|
+
return 0
|