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.
@@ -0,0 +1,4 @@
1
+ """CryptoShield — All-in-one crypto security toolkit."""
2
+
3
+ __version__ = "0.2.0"
4
+ __author__ = "yossweh"
@@ -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()
@@ -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