crypttrace 0.23.0__tar.gz → 0.24.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.23.0/src/crypttrace.egg-info → crypttrace-0.24.0}/PKG-INFO +3 -3
- {crypttrace-0.23.0 → crypttrace-0.24.0}/README.md +2 -2
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/__init__.py +1 -1
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/bridges.py +14 -7
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/cli.py +8 -2
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/config.py +2 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/fetchers/etherscan.py +30 -9
- {crypttrace-0.23.0 → crypttrace-0.24.0/src/crypttrace.egg-info}/PKG-INFO +3 -3
- {crypttrace-0.23.0 → crypttrace-0.24.0}/LICENSE +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/pyproject.toml +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/setup.cfg +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/addresses.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/analysis.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/assess.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/assets.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/chains.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/drain.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/evidence.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/fetchers/__init__.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/fetchers/bitcoin.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/fetchers/http.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/fetchers/solana.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/fetchers/ton.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/fetchers/tron.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/freeze.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/funder.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/gascluster.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/investigate.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/labels/__init__.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/labels/audit.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/labels/bulk.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/labels/known.json +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/labels/labels.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/labels/names.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/labels/partial.json +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/labels/tonapi.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/labels/tronscan.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/offramp.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/poisoning.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/prices.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/render.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/report.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/spam.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/store.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/trace.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/verify.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/watch.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/web/index.html +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace/webapp.py +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace.egg-info/SOURCES.txt +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace.egg-info/dependency_links.txt +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace.egg-info/entry_points.txt +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.0}/src/crypttrace.egg-info/requires.txt +0 -0
- {crypttrace-0.23.0 → crypttrace-0.24.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.24.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
|
|
@@ -79,7 +79,7 @@ python selftest.py # offline checks, no API keys needed
|
|
|
79
79
|
|
|
80
80
|
| | Needed? | How to get it |
|
|
81
81
|
|---|---|---|
|
|
82
|
-
| **EVM chains** (Ethereum, BSC, Polygon, Arbitrum, Optimism, Base) | **Required** | Free key at [etherscan.io/myapikey](https://etherscan.io/myapikey).
|
|
82
|
+
| **EVM chains** (Ethereum, BSC, Polygon, Arbitrum, Optimism, Base) | **Required** | Free key at [etherscan.io/myapikey](https://etherscan.io/myapikey) covers Ethereum, Polygon and Arbitrum. BSC, Optimism and Base need a paid Etherscan plan; on a free key crypttrace says so instead of showing an empty wallet. |
|
|
83
83
|
| **Bitcoin, Tron, Solana, TON** | **Not required** | Work out of the box via public endpoints. |
|
|
84
84
|
|
|
85
85
|
```bash
|
|
@@ -120,7 +120,7 @@ the number either way.
|
|
|
120
120
|
|
|
121
121
|
| Chain | `--chain` | Data source | Model |
|
|
122
122
|
|---|---|---|---|
|
|
123
|
-
| Ethereum, BSC, Polygon, Arbitrum, Optimism, Base | `eth` `bsc` `polygon` `arbitrum` `optimism` `base` | Etherscan v2 | account |
|
|
123
|
+
| Ethereum, BSC, Polygon, Arbitrum, Optimism, Base | `eth` `bsc` `polygon` `arbitrum` `optimism` `base` | Etherscan v2 (BSC, Optimism, Base: paid plan) | account |
|
|
124
124
|
| Bitcoin | `btc` | mempool.space | UTXO |
|
|
125
125
|
| Tron (TRX + USDT-TRC20) | `tron` | TronGrid | account |
|
|
126
126
|
| Solana (SOL + SPL) | `sol` | public JSON-RPC | account |
|
|
@@ -45,7 +45,7 @@ python selftest.py # offline checks, no API keys needed
|
|
|
45
45
|
|
|
46
46
|
| | Needed? | How to get it |
|
|
47
47
|
|---|---|---|
|
|
48
|
-
| **EVM chains** (Ethereum, BSC, Polygon, Arbitrum, Optimism, Base) | **Required** | Free key at [etherscan.io/myapikey](https://etherscan.io/myapikey).
|
|
48
|
+
| **EVM chains** (Ethereum, BSC, Polygon, Arbitrum, Optimism, Base) | **Required** | Free key at [etherscan.io/myapikey](https://etherscan.io/myapikey) covers Ethereum, Polygon and Arbitrum. BSC, Optimism and Base need a paid Etherscan plan; on a free key crypttrace says so instead of showing an empty wallet. |
|
|
49
49
|
| **Bitcoin, Tron, Solana, TON** | **Not required** | Work out of the box via public endpoints. |
|
|
50
50
|
|
|
51
51
|
```bash
|
|
@@ -86,7 +86,7 @@ the number either way.
|
|
|
86
86
|
|
|
87
87
|
| Chain | `--chain` | Data source | Model |
|
|
88
88
|
|---|---|---|---|
|
|
89
|
-
| Ethereum, BSC, Polygon, Arbitrum, Optimism, Base | `eth` `bsc` `polygon` `arbitrum` `optimism` `base` | Etherscan v2 | account |
|
|
89
|
+
| Ethereum, BSC, Polygon, Arbitrum, Optimism, Base | `eth` `bsc` `polygon` `arbitrum` `optimism` `base` | Etherscan v2 (BSC, Optimism, Base: paid plan) | account |
|
|
90
90
|
| Bitcoin | `btc` | mempool.space | UTXO |
|
|
91
91
|
| Tron (TRX + USDT-TRC20) | `tron` | TronGrid | account |
|
|
92
92
|
| Solana (SOL + SPL) | `sol` | public JSON-RPC | account |
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""crypttrace — OSINT crypto investigation CLI."""
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.24.0"
|
|
@@ -15,7 +15,7 @@ are reported as likely continuations with the delay and amount so the analyst
|
|
|
15
15
|
can judge. (v1 matches native-coin value; token-bridging and recipient-address
|
|
16
16
|
decoding from bridge calldata are natural extensions.)
|
|
17
17
|
"""
|
|
18
|
-
from typing import List,
|
|
18
|
+
from typing import Dict, List, Optional
|
|
19
19
|
|
|
20
20
|
from crypttrace.fetchers import etherscan
|
|
21
21
|
from crypttrace.labels import labels
|
|
@@ -46,9 +46,13 @@ def bridge_outflows(address: str, chain: str = "eth") -> List[Dict]:
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def find_arrivals(address: str, source_chain: str, amount: float, after_ts: int,
|
|
49
|
-
tol: float = 0.05, window_h: int = 48
|
|
49
|
+
tol: float = 0.05, window_h: int = 48,
|
|
50
|
+
unread: Optional[Dict[str, str]] = None) -> List[Dict]:
|
|
50
51
|
"""Search every other supported chain for an inbound transfer to `address`
|
|
51
|
-
of ~`amount` (within `tol`) within `window_h` hours after `after_ts`.
|
|
52
|
+
of ~`amount` (within `tol`) within `window_h` hours after `after_ts`.
|
|
53
|
+
|
|
54
|
+
A chain that could not be read is no evidence of "no arrival": it goes into
|
|
55
|
+
`unread` (chain -> why) instead of silently counting as searched."""
|
|
52
56
|
me = address.lower()
|
|
53
57
|
hits = []
|
|
54
58
|
for ch in config.CHAINS:
|
|
@@ -56,7 +60,9 @@ def find_arrivals(address: str, source_chain: str, amount: float, after_ts: int,
|
|
|
56
60
|
continue
|
|
57
61
|
try:
|
|
58
62
|
txs = etherscan.get_txs(address, ch, limit=200, sort="asc")
|
|
59
|
-
except etherscan.EtherscanError:
|
|
63
|
+
except etherscan.EtherscanError as e:
|
|
64
|
+
if unread is not None:
|
|
65
|
+
unread[ch] = str(e)
|
|
60
66
|
continue
|
|
61
67
|
for tx in txs:
|
|
62
68
|
if tx.get("to", "").lower() != me:
|
|
@@ -80,11 +86,12 @@ def find_arrivals(address: str, source_chain: str, amount: float, after_ts: int,
|
|
|
80
86
|
|
|
81
87
|
|
|
82
88
|
def trace_cross(address: str, chain: str = "eth", tol: float = 0.05,
|
|
83
|
-
window_h: int = 48) -> List[Dict]:
|
|
84
|
-
"""For each bridge-out from `address`, list likely continuations on other chains.
|
|
89
|
+
window_h: int = 48, unread: Optional[Dict[str, str]] = None) -> List[Dict]:
|
|
90
|
+
"""For each bridge-out from `address`, list likely continuations on other chains.
|
|
91
|
+
Chains that could not be searched are collected in `unread`."""
|
|
85
92
|
results = []
|
|
86
93
|
for out in bridge_outflows(address, chain):
|
|
87
94
|
arrivals = find_arrivals(address, chain, out["amount"], out["timestamp"],
|
|
88
|
-
tol=tol, window_h=window_h)
|
|
95
|
+
tol=tol, window_h=window_h, unread=unread)
|
|
89
96
|
results.append({"bridge_out": out, "arrivals": arrivals})
|
|
90
97
|
return results
|
|
@@ -303,8 +303,9 @@ def crosschain(
|
|
|
303
303
|
tol: float = typer.Option(0.05, "--tol", help="Amount tolerance (0.05 = 5%, for bridge fees)"),
|
|
304
304
|
):
|
|
305
305
|
"""Follow funds across bridges: find likely arrivals of the same address on other chains."""
|
|
306
|
+
unread = {}
|
|
306
307
|
try:
|
|
307
|
-
results = bridges_mod.trace_cross(address, chain, tol=tol, window_h=window)
|
|
308
|
+
results = bridges_mod.trace_cross(address, chain, tol=tol, window_h=window, unread=unread)
|
|
308
309
|
except etherscan.EtherscanError as e:
|
|
309
310
|
console.print(f"[red]Error:[/red] {e}")
|
|
310
311
|
raise typer.Exit(1)
|
|
@@ -313,6 +314,10 @@ def crosschain(
|
|
|
313
314
|
"address on this chain.[/dim]")
|
|
314
315
|
return
|
|
315
316
|
console.print(render.crosschain_tree(address, chain, results))
|
|
317
|
+
if unread:
|
|
318
|
+
console.print(f"\n[yellow]Not searched:[/yellow] {', '.join(sorted(unread))} — "
|
|
319
|
+
"could not be read, so no arrival there is not evidence of none. "
|
|
320
|
+
f"({next(iter(unread.values()))})")
|
|
316
321
|
console.print("\n[dim]Cross-chain links are heuristic (same-address arrival by amount+time), "
|
|
317
322
|
"not proof. Verify each candidate before relying on it.[/dim]")
|
|
318
323
|
|
|
@@ -875,7 +880,8 @@ def chains():
|
|
|
875
880
|
"""List supported chains."""
|
|
876
881
|
console.print("[bold]EVM[/bold] (Etherscan v2, needs ETHERSCAN_API_KEY):")
|
|
877
882
|
for name, cid in config.CHAINS.items():
|
|
878
|
-
|
|
883
|
+
paid = " (paid Etherscan plan only)" if name in config.PAID_PLAN_CHAINS else ""
|
|
884
|
+
console.print(f" {name:<10} chainid {cid}{paid}")
|
|
879
885
|
console.print("\n[bold]Non-EVM[/bold] (no API key needed):")
|
|
880
886
|
console.print(" btc Bitcoin — mempool.space (UTXO)")
|
|
881
887
|
console.print(" tron Tron — TronGrid (TRX + USDT-TRC20)")
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
The blockchain is public: every transaction is queryable. This module wraps the
|
|
4
4
|
Etherscan API and caches responses so repeated traces don't re-hit the API.
|
|
5
|
+
|
|
6
|
+
Etherscan's free plan does not cover every chain: BNB Chain, Base and Optimism
|
|
7
|
+
need a paid plan. Etherscan says so in an error, which used to pass for an
|
|
8
|
+
empty history — a wallet there looked untouched. Now any error Etherscan
|
|
9
|
+
returns is raised (and never cached); only "No transactions found" is empty.
|
|
5
10
|
"""
|
|
6
11
|
import json
|
|
7
12
|
import sqlite3
|
|
@@ -50,6 +55,15 @@ class EtherscanError(RuntimeError):
|
|
|
50
55
|
pass
|
|
51
56
|
|
|
52
57
|
|
|
58
|
+
def _empty(data: dict) -> bool:
|
|
59
|
+
"""A valid answer with nothing in it ("No transactions found") — not an error."""
|
|
60
|
+
return str(data.get("message", "")).startswith("No ") or isinstance(data.get("result"), list)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _failed(data: dict) -> bool:
|
|
64
|
+
return str(data.get("status")) == "0" and not _empty(data)
|
|
65
|
+
|
|
66
|
+
|
|
53
67
|
MIN_INTERVAL = 0.22 # the free plan allows 5 calls a second
|
|
54
68
|
_last_call = 0.0
|
|
55
69
|
|
|
@@ -70,7 +84,7 @@ def _call(chain: str, params: Dict[str, str], cache_age: int = 3600, _tries: int
|
|
|
70
84
|
key = f"{chain}:" + "&".join(f"{k}={v}" for k, v in sorted(q.items()) if k != "apikey")
|
|
71
85
|
from crypttrace.fetchers import http
|
|
72
86
|
cached = None if http.FRESH else _cache_get(key, cache_age)
|
|
73
|
-
if cached is not None:
|
|
87
|
+
if cached is not None and not _failed(cached): # older versions cached errors too
|
|
74
88
|
return cached
|
|
75
89
|
|
|
76
90
|
global _last_call
|
|
@@ -78,16 +92,23 @@ def _call(chain: str, params: Dict[str, str], cache_age: int = 3600, _tries: int
|
|
|
78
92
|
if wait > 0:
|
|
79
93
|
time.sleep(wait)
|
|
80
94
|
_last_call = time.time()
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
95
|
+
try:
|
|
96
|
+
resp = requests.get(config.ETHERSCAN_BASE, params=q, timeout=30)
|
|
97
|
+
resp.raise_for_status()
|
|
98
|
+
data = resp.json()
|
|
99
|
+
except (requests.RequestException, ValueError) as e:
|
|
100
|
+
raise EtherscanError(f"Etherscan request failed: {e}")
|
|
101
|
+
if _failed(data):
|
|
102
|
+
said = str(data.get("result") or data.get("message") or "")
|
|
103
|
+
if "rate limit" in said.lower() and _tries < 4:
|
|
88
104
|
time.sleep(1 + _tries)
|
|
89
105
|
return _call(chain, params, cache_age, _tries + 1)
|
|
90
|
-
|
|
106
|
+
if "not supported for this chain" in said:
|
|
107
|
+
raise EtherscanError(
|
|
108
|
+
f"Etherscan's free plan does not cover {chain}: its history needs a paid "
|
|
109
|
+
"Etherscan API plan (https://etherscan.io/apis). Without one, crypttrace "
|
|
110
|
+
f"cannot read {chain} — it is not that the wallet is empty.")
|
|
111
|
+
raise EtherscanError(f"Etherscan answered: {said}")
|
|
91
112
|
_cache_put(key, data)
|
|
92
113
|
return data
|
|
93
114
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crypttrace
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.24.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
|
|
@@ -79,7 +79,7 @@ python selftest.py # offline checks, no API keys needed
|
|
|
79
79
|
|
|
80
80
|
| | Needed? | How to get it |
|
|
81
81
|
|---|---|---|
|
|
82
|
-
| **EVM chains** (Ethereum, BSC, Polygon, Arbitrum, Optimism, Base) | **Required** | Free key at [etherscan.io/myapikey](https://etherscan.io/myapikey).
|
|
82
|
+
| **EVM chains** (Ethereum, BSC, Polygon, Arbitrum, Optimism, Base) | **Required** | Free key at [etherscan.io/myapikey](https://etherscan.io/myapikey) covers Ethereum, Polygon and Arbitrum. BSC, Optimism and Base need a paid Etherscan plan; on a free key crypttrace says so instead of showing an empty wallet. |
|
|
83
83
|
| **Bitcoin, Tron, Solana, TON** | **Not required** | Work out of the box via public endpoints. |
|
|
84
84
|
|
|
85
85
|
```bash
|
|
@@ -120,7 +120,7 @@ the number either way.
|
|
|
120
120
|
|
|
121
121
|
| Chain | `--chain` | Data source | Model |
|
|
122
122
|
|---|---|---|---|
|
|
123
|
-
| Ethereum, BSC, Polygon, Arbitrum, Optimism, Base | `eth` `bsc` `polygon` `arbitrum` `optimism` `base` | Etherscan v2 | account |
|
|
123
|
+
| Ethereum, BSC, Polygon, Arbitrum, Optimism, Base | `eth` `bsc` `polygon` `arbitrum` `optimism` `base` | Etherscan v2 (BSC, Optimism, Base: paid plan) | account |
|
|
124
124
|
| Bitcoin | `btc` | mempool.space | UTXO |
|
|
125
125
|
| Tron (TRX + USDT-TRC20) | `tron` | TronGrid | account |
|
|
126
126
|
| Solana (SOL + SPL) | `sol` | public JSON-RPC | account |
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|