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.
Files changed (46) hide show
  1. {crypttrace-0.9.0/src/crypttrace.egg-info → crypttrace-0.10.0}/PKG-INFO +11 -6
  2. {crypttrace-0.9.0 → crypttrace-0.10.0}/README.md +10 -5
  3. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/__init__.py +1 -1
  4. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/fetchers/tron.py +24 -4
  5. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/poisoning.py +31 -6
  6. {crypttrace-0.9.0 → crypttrace-0.10.0/src/crypttrace.egg-info}/PKG-INFO +11 -6
  7. {crypttrace-0.9.0 → crypttrace-0.10.0}/LICENSE +0 -0
  8. {crypttrace-0.9.0 → crypttrace-0.10.0}/pyproject.toml +0 -0
  9. {crypttrace-0.9.0 → crypttrace-0.10.0}/setup.cfg +0 -0
  10. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/addresses.py +0 -0
  11. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/analysis.py +0 -0
  12. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/assess.py +0 -0
  13. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/assets.py +0 -0
  14. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/bridges.py +0 -0
  15. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/chains.py +0 -0
  16. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/cli.py +0 -0
  17. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/config.py +0 -0
  18. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/fetchers/__init__.py +0 -0
  19. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/fetchers/bitcoin.py +0 -0
  20. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/fetchers/etherscan.py +0 -0
  21. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/fetchers/http.py +0 -0
  22. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/fetchers/solana.py +0 -0
  23. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/freeze.py +0 -0
  24. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/funder.py +0 -0
  25. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/investigate.py +0 -0
  26. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/labels/__init__.py +0 -0
  27. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/labels/audit.py +0 -0
  28. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/labels/bulk.py +0 -0
  29. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/labels/known.json +0 -0
  30. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/labels/labels.py +0 -0
  31. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/labels/partial.json +0 -0
  32. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/offramp.py +0 -0
  33. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/prices.py +0 -0
  34. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/render.py +0 -0
  35. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/report.py +0 -0
  36. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/store.py +0 -0
  37. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/trace.py +0 -0
  38. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/verify.py +0 -0
  39. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/watch.py +0 -0
  40. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/web/index.html +0 -0
  41. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace/webapp.py +0 -0
  42. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace.egg-info/SOURCES.txt +0 -0
  43. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace.egg-info/dependency_links.txt +0 -0
  44. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace.egg-info/entry_points.txt +0 -0
  45. {crypttrace-0.9.0 → crypttrace-0.10.0}/src/crypttrace.egg-info/requires.txt +0 -0
  46. {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.9.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
- Cheap look-alikes match only one or two characters at each end, which happens
344
- by chance; those are reported only when they arrived as bait after the real
345
- address was in use.
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
- Cheap look-alikes match only one or two characters at each end, which happens
310
- by chance; those are reported only when they arrived as bait after the real
311
- address was in use.
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.9.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
- d = _get(f"/v1/accounts/{address}/transactions", {"limit": min(limit, 200)})
107
+ # the endpoint lists every transaction type; TRX transfers are a subset of it
87
108
  rows = []
88
- for tx in d.get("data", []) or []:
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 d.get("data", []) or []:
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
- for name, (p, s, t) in (("strong", STRONG), ("weak", WEAK)):
66
- if pre >= p and suf >= s and pre + suf >= t:
67
- return name
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
- level = resemblance(genuine, fake, chain) if fake != genuine else None
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.9.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
- Cheap look-alikes match only one or two characters at each end, which happens
344
- by chance; those are reported only when they arrived as bait after the real
345
- address was in use.
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