crypttrace 0.9.0__tar.gz → 0.10.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.9.0/src/crypttrace.egg-info → crypttrace-0.10.0}/PKG-INFO +11 -6
- {crypttrace-0.9.0 → crypttrace-0.10.0}/README.md +10 -5
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/__init__.py +1 -1
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/fetchers/tron.py +24 -4
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/poisoning.py +31 -6
- {crypttrace-0.9.0 → crypttrace-0.10.0/src/crypttrace.egg-info}/PKG-INFO +11 -6
- {crypttrace-0.9.0 → crypttrace-0.10.0}/LICENSE +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/pyproject.toml +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/setup.cfg +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/addresses.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/analysis.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/assess.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/assets.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/bridges.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/chains.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/cli.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/config.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/fetchers/__init__.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/fetchers/bitcoin.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/fetchers/etherscan.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/fetchers/http.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/fetchers/solana.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/freeze.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/funder.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/investigate.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/labels/__init__.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/labels/audit.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/labels/bulk.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/labels/known.json +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/labels/labels.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/labels/partial.json +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/offramp.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/prices.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/render.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/report.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/store.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/trace.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/verify.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/watch.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/web/index.html +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/webapp.py +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace.egg-info/SOURCES.txt +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace.egg-info/dependency_links.txt +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace.egg-info/entry_points.txt +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace.egg-info/requires.txt +0 -0
- {crypttrace-0.9.0 → crypttrace-0.10.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.10.0
|
|
4
4
|
Summary: OSINT toolkit for crypto investigations: trace stolen funds across Ethereum, Bitcoin, Tron and Solana
|
|
5
5
|
Author: bobslayerX
|
|
6
6
|
License-Expression: MIT
|
|
@@ -338,11 +338,16 @@ Tested on a documented Tron case from August 2026: from the victim's wallet it
|
|
|
338
338
|
finds the 2,527,862 USDT sent to `TDDDHi…rCr9Ps`, a look-alike of
|
|
339
339
|
`TDDD34…rCr9Ps`, an address the wallet had paid 8.4M USDT in all. From the
|
|
340
340
|
attacker's side it finds the same payment and two more victims of $2.4M and
|
|
341
|
-
$2.0M, each lured by a look-alike created three to twelve minutes earlier
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
341
|
+
$2.0M, each lured by a look-alike created three to twelve minutes earlier, and
|
|
342
|
+
for all three it names the address that was imitated.
|
|
343
|
+
|
|
344
|
+
Look-alikes come in three strengths. Matching several characters at both ends is
|
|
345
|
+
reported on sight. So is one that copies only the ending, as long as the shared
|
|
346
|
+
ending is long enough to be no accident (6 characters on Tron and Solana, 7 on
|
|
347
|
+
bech32 Bitcoin, 8 on hex) — one of the three victims above was caught that way,
|
|
348
|
+
by `…E1doqb`. Cheap look-alikes that match one or two characters at each end
|
|
349
|
+
happen by chance, so they are reported only when they arrived as bait after the
|
|
350
|
+
real address was in use.
|
|
346
351
|
|
|
347
352
|
### Off-ramp detection
|
|
348
353
|
|
|
@@ -304,11 +304,16 @@ Tested on a documented Tron case from August 2026: from the victim's wallet it
|
|
|
304
304
|
finds the 2,527,862 USDT sent to `TDDDHi…rCr9Ps`, a look-alike of
|
|
305
305
|
`TDDD34…rCr9Ps`, an address the wallet had paid 8.4M USDT in all. From the
|
|
306
306
|
attacker's side it finds the same payment and two more victims of $2.4M and
|
|
307
|
-
$2.0M, each lured by a look-alike created three to twelve minutes earlier
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
307
|
+
$2.0M, each lured by a look-alike created three to twelve minutes earlier, and
|
|
308
|
+
for all three it names the address that was imitated.
|
|
309
|
+
|
|
310
|
+
Look-alikes come in three strengths. Matching several characters at both ends is
|
|
311
|
+
reported on sight. So is one that copies only the ending, as long as the shared
|
|
312
|
+
ending is long enough to be no accident (6 characters on Tron and Solana, 7 on
|
|
313
|
+
bech32 Bitcoin, 8 on hex) — one of the three victims above was caught that way,
|
|
314
|
+
by `…E1doqb`. Cheap look-alikes that match one or two characters at each end
|
|
315
|
+
happen by chance, so they are reported only when they arrived as bait after the
|
|
316
|
+
real address was in use.
|
|
312
317
|
|
|
313
318
|
### Off-ramp detection
|
|
314
319
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""crypttrace — OSINT crypto investigation CLI."""
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.10.0"
|
|
@@ -82,10 +82,31 @@ def balance(address: str) -> float:
|
|
|
82
82
|
return (data[0].get("balance") or 0) / SUN
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
PAGE = 200 # TronGrid's page size
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _pages(path: str, limit: int) -> List[Dict]:
|
|
89
|
+
"""Up to `limit` items, following TronGrid's fingerprint cursor page by page.
|
|
90
|
+
|
|
91
|
+
One page (200 items) used to be all we read, so a busy wallet's history
|
|
92
|
+
ended a few days back and older transfers were simply invisible."""
|
|
93
|
+
items: List[Dict] = []
|
|
94
|
+
params = {"limit": min(limit, PAGE)}
|
|
95
|
+
while len(items) < limit:
|
|
96
|
+
d = _get(path, dict(params))
|
|
97
|
+
page = d.get("data", []) or []
|
|
98
|
+
items += page
|
|
99
|
+
cursor = (d.get("meta") or {}).get("fingerprint")
|
|
100
|
+
if len(page) < params["limit"] or not cursor:
|
|
101
|
+
break
|
|
102
|
+
params["fingerprint"] = cursor
|
|
103
|
+
return items[:limit]
|
|
104
|
+
|
|
105
|
+
|
|
85
106
|
def _native(address: str, limit: int) -> List[Dict]:
|
|
86
|
-
|
|
107
|
+
# the endpoint lists every transaction type; TRX transfers are a subset of it
|
|
87
108
|
rows = []
|
|
88
|
-
for tx in
|
|
109
|
+
for tx in _pages(f"/v1/accounts/{address}/transactions", limit):
|
|
89
110
|
try:
|
|
90
111
|
c = (tx.get("raw_data", {}).get("contract") or [])[0]
|
|
91
112
|
if c.get("type") != "TransferContract":
|
|
@@ -106,9 +127,8 @@ def _native(address: str, limit: int) -> List[Dict]:
|
|
|
106
127
|
|
|
107
128
|
def token_transfers(address: str, limit: int = 200) -> List[Dict]:
|
|
108
129
|
"""TRC20 transfers (USDT and friends) — already base58 in the API."""
|
|
109
|
-
d = _get(f"/v1/accounts/{address}/transactions/trc20", {"limit": min(limit, 200)})
|
|
110
130
|
rows = []
|
|
111
|
-
for t in
|
|
131
|
+
for t in _pages(f"/v1/accounts/{address}/transactions/trc20", limit):
|
|
112
132
|
# the endpoint also lists Approval events, whose "value" is an allowance
|
|
113
133
|
# (often 2**256-1), not money that moved
|
|
114
134
|
if t.get("type", "Transfer") != "Transfer":
|
|
@@ -19,6 +19,7 @@ Three views of the same attack:
|
|
|
19
19
|
* campaign(address) — an address sending zero/dust transfers to many
|
|
20
20
|
unrelated wallets (the dust-sending style, common on Tron).
|
|
21
21
|
"""
|
|
22
|
+
import math
|
|
22
23
|
from collections import defaultdict
|
|
23
24
|
from typing import Dict, List, Optional, Tuple
|
|
24
25
|
|
|
@@ -30,6 +31,20 @@ from crypttrace import analysis, assets, chains
|
|
|
30
31
|
# the look-alike arrived with bait after the genuine address was in use.
|
|
31
32
|
STRONG = (3, 3, 7) # min prefix, min suffix, min total
|
|
32
33
|
WEAK = (1, 2, 3)
|
|
34
|
+
# Many people check only the ending, and poisoners know it: a look-alike can match
|
|
35
|
+
# the last characters alone. That is strong evidence once the shared ending carries
|
|
36
|
+
# 32 bits — 6 base58 characters, 7 bech32, 8 hex — about 1 in 4 billion by chance.
|
|
37
|
+
SUFFIX_BITS = 32
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _suffix_chars(address: str, chain: str) -> int:
|
|
41
|
+
if chains.is_evm(chain):
|
|
42
|
+
bits = 4.0 # hex
|
|
43
|
+
elif address.lower().startswith(("bc1", "tb1")):
|
|
44
|
+
bits = 5.0 # bech32
|
|
45
|
+
else:
|
|
46
|
+
bits = 5.86 # base58: Tron, Solana, legacy Bitcoin
|
|
47
|
+
return math.ceil(SUFFIX_BITS / bits)
|
|
33
48
|
# A campaign: this many distinct recipients of zero/dust transfers.
|
|
34
49
|
CAMPAIGN_MIN_TARGETS = 10
|
|
35
50
|
|
|
@@ -62,9 +77,12 @@ def resemblance(a: str, b: str, chain: str) -> Optional[str]:
|
|
|
62
77
|
if chains.norm_addr(a, chain) == chains.norm_addr(b, chain):
|
|
63
78
|
return None
|
|
64
79
|
pre, suf = match_lengths(a, b, chain)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
80
|
+
p, s, t = STRONG
|
|
81
|
+
if (pre >= p and suf >= s and pre + suf >= t) or suf >= _suffix_chars(a, chain):
|
|
82
|
+
return "strong"
|
|
83
|
+
p, s, t = WEAK
|
|
84
|
+
if pre >= p and suf >= s and pre + suf >= t:
|
|
85
|
+
return "weak"
|
|
68
86
|
return None
|
|
69
87
|
|
|
70
88
|
|
|
@@ -128,13 +146,17 @@ def lookalikes(address: str, chain: str, limit: int = 1000) -> List[dict]:
|
|
|
128
146
|
c["sent_first_ts"] = ts if c["sent_first_ts"] is None else min(c["sent_first_ts"], ts)
|
|
129
147
|
|
|
130
148
|
# only compare addresses that could match: bucket by visible ends
|
|
149
|
+
# (and by the ending alone, for look-alikes that only imitate the last characters)
|
|
131
150
|
buckets = defaultdict(list)
|
|
132
151
|
for a in seen:
|
|
133
152
|
b = _body(a, chain)
|
|
134
153
|
if len(b) >= WEAK[0] + WEAK[1]:
|
|
135
|
-
buckets[(b[:WEAK[0]], b[-WEAK[1]:])].append(a)
|
|
154
|
+
buckets[("ends", b[:WEAK[0]], b[-WEAK[1]:])].append(a)
|
|
155
|
+
n = _suffix_chars(a, chain)
|
|
156
|
+
if len(b) > n:
|
|
157
|
+
buckets[("tail", b[-n:])].append(a)
|
|
136
158
|
|
|
137
|
-
findings = []
|
|
159
|
+
findings, compared = [], set()
|
|
138
160
|
for group in buckets.values():
|
|
139
161
|
if len(group) < 2:
|
|
140
162
|
continue
|
|
@@ -144,7 +166,10 @@ def lookalikes(address: str, chain: str, limit: int = 1000) -> List[dict]:
|
|
|
144
166
|
return (c["sent_first_ts"] is None, c["sent_first_ts"] or c["first_ts"], c["first_ts"])
|
|
145
167
|
genuine = min(group, key=rank)
|
|
146
168
|
for fake in group:
|
|
147
|
-
|
|
169
|
+
if fake == genuine or (genuine, fake) in compared:
|
|
170
|
+
continue # a pair can sit in both kinds of bucket
|
|
171
|
+
compared.add((genuine, fake))
|
|
172
|
+
level = resemblance(genuine, fake, chain)
|
|
148
173
|
if not level:
|
|
149
174
|
continue
|
|
150
175
|
g, f = seen[genuine], seen[fake]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crypttrace
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: OSINT toolkit for crypto investigations: trace stolen funds across Ethereum, Bitcoin, Tron and Solana
|
|
5
5
|
Author: bobslayerX
|
|
6
6
|
License-Expression: MIT
|
|
@@ -338,11 +338,16 @@ Tested on a documented Tron case from August 2026: from the victim's wallet it
|
|
|
338
338
|
finds the 2,527,862 USDT sent to `TDDDHi…rCr9Ps`, a look-alike of
|
|
339
339
|
`TDDD34…rCr9Ps`, an address the wallet had paid 8.4M USDT in all. From the
|
|
340
340
|
attacker's side it finds the same payment and two more victims of $2.4M and
|
|
341
|
-
$2.0M, each lured by a look-alike created three to twelve minutes earlier
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
341
|
+
$2.0M, each lured by a look-alike created three to twelve minutes earlier, and
|
|
342
|
+
for all three it names the address that was imitated.
|
|
343
|
+
|
|
344
|
+
Look-alikes come in three strengths. Matching several characters at both ends is
|
|
345
|
+
reported on sight. So is one that copies only the ending, as long as the shared
|
|
346
|
+
ending is long enough to be no accident (6 characters on Tron and Solana, 7 on
|
|
347
|
+
bech32 Bitcoin, 8 on hex) — one of the three victims above was caught that way,
|
|
348
|
+
by `…E1doqb`. Cheap look-alikes that match one or two characters at each end
|
|
349
|
+
happen by chance, so they are reported only when they arrived as bait after the
|
|
350
|
+
real address was in use.
|
|
346
351
|
|
|
347
352
|
### Off-ramp detection
|
|
348
353
|
|
|
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
|