crypttrace 0.18.0__tar.gz → 0.19.0__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.
- {crypttrace-0.18.0/src/crypttrace.egg-info → crypttrace-0.19.0}/PKG-INFO +1 -2
- {crypttrace-0.18.0 → crypttrace-0.19.0}/README.md +0 -1
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/__init__.py +1 -1
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/chains.py +15 -4
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/config.py +16 -1
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/fetchers/etherscan.py +13 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/labels/known.json +2 -2
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/labels/tronscan.py +1 -11
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/store.py +8 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0/src/crypttrace.egg-info}/PKG-INFO +1 -2
- {crypttrace-0.18.0 → crypttrace-0.19.0}/LICENSE +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/pyproject.toml +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/setup.cfg +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/addresses.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/analysis.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/assess.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/assets.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/bridges.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/cli.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/drain.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/evidence.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/fetchers/__init__.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/fetchers/bitcoin.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/fetchers/http.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/fetchers/solana.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/fetchers/ton.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/fetchers/tron.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/freeze.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/funder.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/investigate.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/labels/__init__.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/labels/audit.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/labels/bulk.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/labels/labels.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/labels/names.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/labels/partial.json +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/labels/tonapi.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/offramp.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/poisoning.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/prices.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/render.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/report.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/spam.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/trace.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/verify.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/watch.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/web/index.html +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/webapp.py +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace.egg-info/SOURCES.txt +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace.egg-info/dependency_links.txt +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace.egg-info/entry_points.txt +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace.egg-info/requires.txt +0 -0
- {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crypttrace
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.0
|
|
4
4
|
Summary: OSINT toolkit for crypto investigations: trace stolen funds across Ethereum, Bitcoin, Tron, Solana and TON
|
|
5
5
|
Author: bobslayerX
|
|
6
6
|
License-Expression: MIT
|
|
@@ -689,7 +689,6 @@ cases/ # worked investigations with their data
|
|
|
689
689
|
- More exchanges on Bitcoin, Tron, Solana (now: Binance, OKX, HTX, Bybit) and
|
|
690
690
|
TON (now: Binance),
|
|
691
691
|
and refreshing these lists as the exchanges republish them
|
|
692
|
-
- Internal transactions (completes `funder` and contract-mediated transfers)
|
|
693
692
|
- More label sources: Chainabuse, CryptoScamDB, exchange deposit-address sets
|
|
694
693
|
- Entity clustering on EVM via the gas-funding heuristic
|
|
695
694
|
|
|
@@ -655,7 +655,6 @@ cases/ # worked investigations with their data
|
|
|
655
655
|
- More exchanges on Bitcoin, Tron, Solana (now: Binance, OKX, HTX, Bybit) and
|
|
656
656
|
TON (now: Binance),
|
|
657
657
|
and refreshing these lists as the exchanges republish them
|
|
658
|
-
- Internal transactions (completes `funder` and contract-mediated transfers)
|
|
659
658
|
- More label sources: Chainabuse, CryptoScamDB, exchange deposit-address sets
|
|
660
659
|
- Entity clustering on EVM via the gas-funding heuristic
|
|
661
660
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""crypttrace — OSINT crypto investigation CLI."""
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.19.0"
|
|
@@ -102,15 +102,26 @@ def balance(address: str, chain: str = "eth") -> float:
|
|
|
102
102
|
# ---------- normalized row builders ----------
|
|
103
103
|
|
|
104
104
|
def _evm_native(address: str, chain: str, limit: int) -> List[Dict]:
|
|
105
|
+
"""The native coin moved: normal transactions plus internal ones (sent by contracts —
|
|
106
|
+
withdrawals from mixers and exchanges, ETH back from swaps). Failed calls moved nothing."""
|
|
105
107
|
rows = []
|
|
106
|
-
for tx in etherscan.get_txs(address, chain, limit=limit)
|
|
108
|
+
calls = [(tx, False) for tx in etherscan.get_txs(address, chain, limit=limit)]
|
|
109
|
+
calls += [(tx, True) for tx in etherscan.get_internal_txs(address, chain, limit=limit)]
|
|
110
|
+
for tx, internal in calls:
|
|
111
|
+
if str(tx.get("isError", "0")) == "1":
|
|
112
|
+
continue
|
|
107
113
|
try:
|
|
108
114
|
val = int(tx.get("value", 0)) / config.WEI
|
|
109
115
|
except (TypeError, ValueError):
|
|
110
116
|
val = 0.0
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
if internal and val <= 0:
|
|
118
|
+
continue
|
|
119
|
+
row = {"from": tx.get("from", "").lower(), "to": (tx.get("to") or "").lower(),
|
|
120
|
+
"value": val, "timestamp": int(tx.get("timeStamp", "0") or 0),
|
|
121
|
+
"hash": tx.get("hash", ""), "symbol": symbol(chain)}
|
|
122
|
+
if internal:
|
|
123
|
+
row["internal"] = True
|
|
124
|
+
rows.append(row)
|
|
114
125
|
return rows
|
|
115
126
|
|
|
116
127
|
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
"""Configuration and shared constants."""
|
|
2
2
|
import os
|
|
3
|
+
import sys
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
6
|
+
|
|
7
|
+
def env(name: str, default: str = "") -> str:
|
|
8
|
+
"""An environment variable — falling back, on Windows, to the user environment, so a key
|
|
9
|
+
saved with [Environment]::SetEnvironmentVariable(..., "User") works in an already-open shell."""
|
|
10
|
+
value = os.environ.get(name)
|
|
11
|
+
if not value and sys.platform == "win32":
|
|
12
|
+
try:
|
|
13
|
+
import winreg
|
|
14
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment") as reg:
|
|
15
|
+
value = winreg.QueryValueEx(reg, name)[0]
|
|
16
|
+
except OSError:
|
|
17
|
+
value = None
|
|
18
|
+
return (value or default).strip()
|
|
19
|
+
|
|
5
20
|
# Data dir for cache + labels (created on first run)
|
|
6
21
|
DATA_DIR = Path(os.environ.get("CRYPTTRACE_HOME", Path.home() / ".crypttrace"))
|
|
7
22
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
@@ -11,7 +26,7 @@ CACHE_DB = DATA_DIR / "cache.sqlite"
|
|
|
11
26
|
# Etherscan v2 API. One key works across all EVM chains via chainid param.
|
|
12
27
|
# Get a free key at https://etherscan.io/myapikey and export it:
|
|
13
28
|
# export ETHERSCAN_API_KEY=xxxx
|
|
14
|
-
ETHERSCAN_API_KEY =
|
|
29
|
+
ETHERSCAN_API_KEY = env("ETHERSCAN_API_KEY")
|
|
15
30
|
ETHERSCAN_BASE = "https://api.etherscan.io/v2/api"
|
|
16
31
|
|
|
17
32
|
# Supported EVM chains: name -> chainid
|
|
@@ -104,6 +104,19 @@ def get_txs(address: str, chain: str = "eth", limit: int = 1000,
|
|
|
104
104
|
return result if isinstance(result, list) else []
|
|
105
105
|
|
|
106
106
|
|
|
107
|
+
def get_internal_txs(address: str, chain: str = "eth", limit: int = 1000,
|
|
108
|
+
sort: str = "desc") -> List[dict]:
|
|
109
|
+
"""Internal transactions: the native coin a contract sent or received as part of a call
|
|
110
|
+
(a mixer or exchange withdrawal, ETH back from a DEX swap, a multisig payout)."""
|
|
111
|
+
data = _call(chain, {
|
|
112
|
+
"module": "account", "action": "txlistinternal", "address": address,
|
|
113
|
+
"startblock": "0", "endblock": "99999999",
|
|
114
|
+
"page": "1", "offset": str(limit), "sort": sort,
|
|
115
|
+
})
|
|
116
|
+
result = data.get("result")
|
|
117
|
+
return result if isinstance(result, list) else []
|
|
118
|
+
|
|
119
|
+
|
|
107
120
|
def get_token_txs(address: str, chain: str = "eth", limit: int = 1000) -> List[dict]:
|
|
108
121
|
"""ERC-20 token transfers, newest first."""
|
|
109
122
|
data = _call(chain, {
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
"0x1522900b6dafac587d499a862861c0869be6e428": {"name": "OKX", "type": "exchange"},
|
|
11
11
|
"0x2faf487a4414fe77e2327f0bf4ae2a264a776ad2": {"name": "FTX (defunct)", "type": "exchange"},
|
|
12
12
|
"0x722122df12d4e14e13ac3b6895a86e84145b6967": {"name": "Tornado Cash: Router", "type": "mixer"},
|
|
13
|
-
"0xd90e2f925da726b50c4ed8d0fb90ad053324f31b": {"name": "Tornado Cash: 0.1 ETH", "
|
|
13
|
+
"0xd90e2f925da726b50c4ed8d0fb90ad053324f31b": {"name": "Tornado Cash: router", "type": "mixer", "chain": "eth", "source": "own check of its internal transactions (2026-10-17): pays out withdrawals of the 0.1, 1 and 10 ETH pools, so a router, not the 0.1 ETH pool it was labelled as", "source_kind": "research", "added": "2026-10-17"},
|
|
14
14
|
"0x910cbd523d972eb0a6f4cae4618ad62622b39dbf": {"name": "Tornado Cash: 10 ETH", "type": "mixer"},
|
|
15
15
|
"0xa160cdab225685da1d56aa342ad8841c3b53f291": {"name": "Tornado Cash: 100 ETH", "type": "mixer"},
|
|
16
|
-
"0x12d66f87a04a9e220743712ce6d9bb1b5616b8fc": {"name": "Tornado Cash: 1 ETH", "type": "mixer"},
|
|
16
|
+
"0x12d66f87a04a9e220743712ce6d9bb1b5616b8fc": {"name": "Tornado Cash: 0.1 ETH", "type": "mixer", "chain": "eth", "source": "own check of its internal transactions (2026-10-17): every withdrawal pays out 0.1 ETH in total (recipient plus relayer fee); was labelled 1 ETH", "source_kind": "research", "added": "2026-10-17"},
|
|
17
17
|
"0x3ee18b2214aff97000d974cf647e7c347e8fa585": {"name": "Wormhole: Token Bridge", "type": "bridge"},
|
|
18
18
|
"0x99c9fc46f92e8a1c0dec1b1747d010903e884be1": {"name": "Optimism: Gateway (L1 Bridge)", "type": "bridge"},
|
|
19
19
|
"0x4dbd4fc535ac27206064b68ffcf827b0a60bab3f": {"name": "Arbitrum: Delayed Inbox", "type": "bridge"},
|
|
@@ -12,8 +12,6 @@ Lookups are cached on disk for a week; only `enrich()` uses the network.
|
|
|
12
12
|
"""
|
|
13
13
|
import datetime as dt
|
|
14
14
|
import json
|
|
15
|
-
import os
|
|
16
|
-
import sys
|
|
17
15
|
import time
|
|
18
16
|
from typing import Dict, Iterable, Optional
|
|
19
17
|
|
|
@@ -31,15 +29,7 @@ _cache: Optional[Dict[str, dict]] = None
|
|
|
31
29
|
|
|
32
30
|
|
|
33
31
|
def _key() -> Optional[str]:
|
|
34
|
-
|
|
35
|
-
if not k and sys.platform == "win32": # set with [Environment]::SetEnvironmentVariable(..., "User")
|
|
36
|
-
try:
|
|
37
|
-
import winreg
|
|
38
|
-
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment") as reg:
|
|
39
|
-
k = winreg.QueryValueEx(reg, "TRONSCAN_API_KEY")[0]
|
|
40
|
-
except OSError:
|
|
41
|
-
k = None
|
|
42
|
-
return (k or "").strip() or None
|
|
32
|
+
return config.env("TRONSCAN_API_KEY") or None
|
|
43
33
|
|
|
44
34
|
|
|
45
35
|
def _load() -> Dict[str, dict]:
|
|
@@ -86,6 +86,14 @@ def _conn() -> sqlite3.Connection:
|
|
|
86
86
|
c.execute("DELETE FROM fetched WHERE chain='ton'")
|
|
87
87
|
c.execute("PRAGMA user_version=3")
|
|
88
88
|
c.commit()
|
|
89
|
+
if c.execute("PRAGMA user_version").fetchone()[0] < 4:
|
|
90
|
+
# 0.19: EVM native history now includes internal transactions — read it again
|
|
91
|
+
evm = tuple(config.CHAINS)
|
|
92
|
+
marks = ",".join("?" * len(evm))
|
|
93
|
+
c.execute(f"DELETE FROM transfers WHERE chain IN ({marks}) AND (contract IS NULL OR contract='')", evm)
|
|
94
|
+
c.execute(f"DELETE FROM fetched WHERE chain IN ({marks}) AND asset=''", evm)
|
|
95
|
+
c.execute("PRAGMA user_version=4")
|
|
96
|
+
c.commit()
|
|
89
97
|
return c
|
|
90
98
|
|
|
91
99
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crypttrace
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.0
|
|
4
4
|
Summary: OSINT toolkit for crypto investigations: trace stolen funds across Ethereum, Bitcoin, Tron, Solana and TON
|
|
5
5
|
Author: bobslayerX
|
|
6
6
|
License-Expression: MIT
|
|
@@ -689,7 +689,6 @@ cases/ # worked investigations with their data
|
|
|
689
689
|
- More exchanges on Bitcoin, Tron, Solana (now: Binance, OKX, HTX, Bybit) and
|
|
690
690
|
TON (now: Binance),
|
|
691
691
|
and refreshing these lists as the exchanges republish them
|
|
692
|
-
- Internal transactions (completes `funder` and contract-mediated transfers)
|
|
693
692
|
- More label sources: Chainabuse, CryptoScamDB, exchange deposit-address sets
|
|
694
693
|
- Entity clustering on EVM via the gas-funding heuristic
|
|
695
694
|
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|