crypttrace 0.21.0__tar.gz → 0.22.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 (54) hide show
  1. {crypttrace-0.21.0/src/crypttrace.egg-info → crypttrace-0.22.0}/PKG-INFO +6 -3
  2. {crypttrace-0.21.0 → crypttrace-0.22.0}/README.md +5 -2
  3. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/__init__.py +1 -1
  4. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/chains.py +39 -4
  5. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/fetchers/ton.py +8 -5
  6. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/fetchers/tron.py +11 -7
  7. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/funder.py +5 -3
  8. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/store.py +11 -0
  9. {crypttrace-0.21.0 → crypttrace-0.22.0/src/crypttrace.egg-info}/PKG-INFO +6 -3
  10. {crypttrace-0.21.0 → crypttrace-0.22.0}/LICENSE +0 -0
  11. {crypttrace-0.21.0 → crypttrace-0.22.0}/pyproject.toml +0 -0
  12. {crypttrace-0.21.0 → crypttrace-0.22.0}/setup.cfg +0 -0
  13. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/addresses.py +0 -0
  14. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/analysis.py +0 -0
  15. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/assess.py +0 -0
  16. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/assets.py +0 -0
  17. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/bridges.py +0 -0
  18. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/cli.py +0 -0
  19. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/config.py +0 -0
  20. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/drain.py +0 -0
  21. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/evidence.py +0 -0
  22. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/fetchers/__init__.py +0 -0
  23. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/fetchers/bitcoin.py +0 -0
  24. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/fetchers/etherscan.py +0 -0
  25. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/fetchers/http.py +0 -0
  26. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/fetchers/solana.py +0 -0
  27. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/freeze.py +0 -0
  28. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/gascluster.py +0 -0
  29. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/investigate.py +0 -0
  30. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/labels/__init__.py +0 -0
  31. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/labels/audit.py +0 -0
  32. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/labels/bulk.py +0 -0
  33. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/labels/known.json +0 -0
  34. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/labels/labels.py +0 -0
  35. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/labels/names.py +0 -0
  36. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/labels/partial.json +0 -0
  37. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/labels/tonapi.py +0 -0
  38. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/labels/tronscan.py +0 -0
  39. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/offramp.py +0 -0
  40. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/poisoning.py +0 -0
  41. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/prices.py +0 -0
  42. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/render.py +0 -0
  43. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/report.py +0 -0
  44. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/spam.py +0 -0
  45. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/trace.py +0 -0
  46. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/verify.py +0 -0
  47. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/watch.py +0 -0
  48. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/web/index.html +0 -0
  49. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/webapp.py +0 -0
  50. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace.egg-info/SOURCES.txt +0 -0
  51. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace.egg-info/dependency_links.txt +0 -0
  52. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace.egg-info/entry_points.txt +0 -0
  53. {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace.egg-info/requires.txt +0 -0
  54. {crypttrace-0.21.0 → crypttrace-0.22.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.21.0
3
+ Version: 0.22.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
@@ -422,8 +422,11 @@ coin.
422
422
  then whoever funded that funder. A fresh laundering wallet must be bootstrapped
423
423
  from somewhere, and the chain frequently terminates at an exchange withdrawal —
424
424
  an identification point. A core primitive for tying "unrelated" wallets to one
425
- controller. (Uses external transactions; wallets first funded by an internal
426
- contract call need internal-tx data — a planned extension.)
425
+ controller. On EVM chains, Tron and TON the history is read from its start, so
426
+ the first funding of a years-old, busy wallet is found too; Bitcoin and Solana
427
+ explorers list newest first, so there it is the earliest of the latest 200
428
+ transfers. On EVM chains a first funding sent by a contract (a mixer or bridge
429
+ withdrawal, an internal transaction) counts and is flagged.
427
430
 
428
431
  ### Victim lists and timing analysis
429
432
 
@@ -388,8 +388,11 @@ coin.
388
388
  then whoever funded that funder. A fresh laundering wallet must be bootstrapped
389
389
  from somewhere, and the chain frequently terminates at an exchange withdrawal —
390
390
  an identification point. A core primitive for tying "unrelated" wallets to one
391
- controller. (Uses external transactions; wallets first funded by an internal
392
- contract call need internal-tx data — a planned extension.)
391
+ controller. On EVM chains, Tron and TON the history is read from its start, so
392
+ the first funding of a years-old, busy wallet is found too; Bitcoin and Solana
393
+ explorers list newest first, so there it is the earliest of the latest 200
394
+ transfers. On EVM chains a first funding sent by a contract (a mixer or bridge
395
+ withdrawal, an internal transaction) counts and is flagged.
393
396
 
394
397
  ### Victim lists and timing analysis
395
398
 
@@ -1,2 +1,2 @@
1
1
  """crypttrace — OSINT crypto investigation CLI."""
2
- __version__ = "0.21.0"
2
+ __version__ = "0.22.0"
@@ -101,12 +101,14 @@ def balance(address: str, chain: str = "eth") -> float:
101
101
 
102
102
  # ---------- normalized row builders ----------
103
103
 
104
- def _evm_native(address: str, chain: str, limit: int) -> List[Dict]:
104
+ def _evm_native(address: str, chain: str, limit: int, oldest: bool = False) -> List[Dict]:
105
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."""
106
+ withdrawals from mixers and exchanges, ETH back from swaps). Failed calls moved nothing.
107
+ `oldest` reads the start of the history instead of its latest part."""
107
108
  rows = []
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)]
109
+ sort = "asc" if oldest else "desc"
110
+ calls = [(tx, False) for tx in etherscan.get_txs(address, chain, limit=limit, sort=sort)]
111
+ calls += [(tx, True) for tx in etherscan.get_internal_txs(address, chain, limit=limit, sort=sort)]
110
112
  for tx, internal in calls:
111
113
  if str(tx.get("isError", "0")) == "1":
112
114
  continue
@@ -175,6 +177,12 @@ def transfers(address: str, chain: str = "eth", limit: int = 1000,
175
177
 
176
178
  Served from the local store when it holds recent data for this address, so
177
179
  repeat analysis costs nothing and works offline.
180
+
181
+ `oldest_first` returns the *first* `limit` transfers of the address, oldest
182
+ first — what "who funded this wallet" needs. On EVM chains, Tron and TON the
183
+ explorers can list a history from its start, so a busy wallet's first
184
+ funding is found however long ago it was; on Bitcoin and Solana (newest
185
+ first only) it is the oldest of the latest `limit`.
178
186
  """
179
187
  check(chain)
180
188
  if chain == "ton":
@@ -189,6 +197,11 @@ def transfers(address: str, chain: str = "eth", limit: int = 1000,
189
197
  else:
190
198
  asset_key = (contract or "*").lower()
191
199
  skip_read = fresh or FORCE_FRESH
200
+ if oldest_first and not asset and not OFFLINE and (is_evm(chain) or chain in ("tron", "ton")):
201
+ # the store keeps the latest part of a history; only a complete one holds its start
202
+ if not (USE_STORE and not skip_read and store.is_fresh(chain, address, asset_key)
203
+ and store.is_complete(chain, address, asset_key)):
204
+ return _oldest(address, chain, limit, fresh or FORCE_FRESH)
192
205
  if USE_STORE and not skip_read:
193
206
  if OFFLINE or store.is_fresh(chain, address, asset_key):
194
207
  rows = store.load(address=address, chain=chain, contract=asset_key,
@@ -239,6 +252,28 @@ def transfers(address: str, chain: str = "eth", limit: int = 1000,
239
252
  return rows
240
253
 
241
254
 
255
+ def _oldest(address: str, chain: str, limit: int, fresh: bool) -> List[Dict]:
256
+ """The first `limit` native transfers of an address, oldest first.
257
+
258
+ Not written to the store: it keeps the latest part of each history, and a
259
+ start-of-history slice filed there would pass for it."""
260
+ was_fresh = _http.FRESH
261
+ _http.FRESH = was_fresh or fresh
262
+ try:
263
+ if is_evm(chain):
264
+ rows = _evm_native(address, chain, limit, oldest=True)
265
+ elif chain == "tron":
266
+ rows = tron.transfers(address, limit, oldest=True)
267
+ else:
268
+ rows = ton.transfers(address, limit, oldest=True)
269
+ except _UPSTREAM_ERRORS as e:
270
+ raise ChainError(str(e))
271
+ finally:
272
+ _http.FRESH = was_fresh
273
+ rows.sort(key=lambda r: r.get("timestamp", 0))
274
+ return rows
275
+
276
+
242
277
  def flows(address: str, chain: str, top: int, direction: str = "out",
243
278
  limit: int = 1000, asset: Optional[dict] = None):
244
279
  """Aggregated value per counterparty: [(other, total, tx_count)].
@@ -53,12 +53,14 @@ def _get(path: str, params: Optional[dict] = None, timeout: int = 30) -> dict:
53
53
  return data if isinstance(data, dict) else {}
54
54
 
55
55
 
56
- def _pages(path: str, key: str, params: dict, limit: int, page: int) -> List[dict]:
56
+ def _pages(path: str, key: str, params: dict, limit: int, page: int,
57
+ oldest: bool = False) -> List[dict]:
57
58
  items: List[dict] = []
58
59
  offset = 0
60
+ sort = "asc" if oldest else "desc"
59
61
  while len(items) < limit:
60
62
  n = min(page, limit - len(items))
61
- batch = _get(path, dict(params, limit=n, offset=offset, sort="desc")).get(key) or []
63
+ batch = _get(path, dict(params, limit=n, offset=offset, sort=sort)).get(key) or []
62
64
  items += batch
63
65
  if len(batch) < n:
64
66
  break
@@ -103,11 +105,12 @@ def balance(address: str) -> float:
103
105
  return int(d.get("balance") or 0) / NANO
104
106
 
105
107
 
106
- def transfers(address: str, limit: int = 1000) -> List[Dict]:
107
- """TON moved in and out of `address`: its incoming message and outgoing ones."""
108
+ def transfers(address: str, limit: int = 1000, oldest: bool = False) -> List[Dict]:
109
+ """TON moved in and out of `address`: its incoming message and outgoing ones;
110
+ from the latest transactions, or with `oldest` from the first."""
108
111
  me = _addr(address)
109
112
  rows = []
110
- for tx in _pages("/transactions", "transactions", {"account": address}, limit, TX_PAGE):
113
+ for tx in _pages("/transactions", "transactions", {"account": address}, limit, TX_PAGE, oldest):
111
114
  ts, h = int(tx.get("now") or 0), _hex(tx.get("hash", ""))
112
115
  msgs = [tx.get("in_msg") or {}] + list(tx.get("out_msgs") or [])
113
116
  for m in msgs:
@@ -85,13 +85,16 @@ def balance(address: str) -> float:
85
85
  PAGE = 200 # TronGrid's page size
86
86
 
87
87
 
88
- def _pages(path: str, limit: int) -> List[Dict]:
89
- """Up to `limit` items, following TronGrid's fingerprint cursor page by page.
88
+ def _pages(path: str, limit: int, oldest: bool = False) -> List[Dict]:
89
+ """Up to `limit` items, following TronGrid's fingerprint cursor page by page;
90
+ the latest ones, or with `oldest` the first ones.
90
91
 
91
92
  One page (200 items) used to be all we read, so a busy wallet's history
92
93
  ended a few days back and older transfers were simply invisible."""
93
94
  items: List[Dict] = []
94
95
  params = {"limit": min(limit, PAGE)}
96
+ if oldest:
97
+ params["order_by"] = "block_timestamp,asc"
95
98
  while len(items) < limit:
96
99
  d = _get(path, dict(params))
97
100
  page = d.get("data", []) or []
@@ -103,10 +106,10 @@ def _pages(path: str, limit: int) -> List[Dict]:
103
106
  return items[:limit]
104
107
 
105
108
 
106
- def _native(address: str, limit: int) -> List[Dict]:
109
+ def _native(address: str, limit: int, oldest: bool = False) -> List[Dict]:
107
110
  # the endpoint lists every transaction type; TRX transfers are a subset of it
108
111
  rows = []
109
- for tx in _pages(f"/v1/accounts/{address}/transactions", limit):
112
+ for tx in _pages(f"/v1/accounts/{address}/transactions", limit, oldest):
110
113
  try:
111
114
  c = (tx.get("raw_data", {}).get("contract") or [])[0]
112
115
  if c.get("type") != "TransferContract":
@@ -147,6 +150,7 @@ def token_transfers(address: str, limit: int = 200) -> List[Dict]:
147
150
  return rows
148
151
 
149
152
 
150
- def transfers(address: str, limit: int = 200) -> List[Dict]:
151
- """Native TRX transfers (use token_transfers for USDT-TRC20)."""
152
- return _native(address, limit)
153
+ def transfers(address: str, limit: int = 200, oldest: bool = False) -> List[Dict]:
154
+ """Native TRX transfers (use token_transfers for USDT-TRC20); the latest
155
+ `limit`, or with `oldest` the first."""
156
+ return _native(address, limit, oldest)
@@ -7,9 +7,11 @@ funding hub or — the prize — a centralised-exchange withdrawal, which is a K
7
7
  identification point. Investigators like ZachXBT lean on this constantly to tie
8
8
  "unrelated" laundering wallets back to a single controller.
9
9
 
10
- Limitation: this uses normal (external) transactions. Wallets first funded by an
11
- internal contract call (some exchanges, disperse tools) need internal-tx data,
12
- a natural next extension.
10
+ The history is read from its start on EVM chains, Tron and TON, so a busy
11
+ wallet's first funding is found however long ago it came. Bitcoin and Solana
12
+ explorers list newest first, so there it is the earliest of the latest 200
13
+ transfers. On EVM chains a first funding sent by a contract (an internal
14
+ transaction — a mixer or bridge withdrawal) counts, and is flagged.
13
15
  """
14
16
  from typing import Optional, List, Dict
15
17
 
@@ -168,6 +168,17 @@ def is_fresh(chain: str, address: str, asset_key: str = "",
168
168
  return a is not None and a <= max_age
169
169
 
170
170
 
171
+ def is_complete(chain: str, address: str, asset_key: str = "") -> bool:
172
+ """Whether the last fetch of this address read its whole history."""
173
+ c = _conn()
174
+ try:
175
+ row = c.execute("SELECT complete FROM fetched WHERE chain=? AND address=? AND asset=?",
176
+ (chain, address, asset_key)).fetchone()
177
+ finally:
178
+ c.close()
179
+ return bool(row and row[0])
180
+
181
+
171
182
  def load(chain: str, address: str, contract: str = "", native_symbol: str = "") -> List[dict]:
172
183
  """Every stored transfer touching this address, newest first."""
173
184
  c = _conn()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crypttrace
3
- Version: 0.21.0
3
+ Version: 0.22.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
@@ -422,8 +422,11 @@ coin.
422
422
  then whoever funded that funder. A fresh laundering wallet must be bootstrapped
423
423
  from somewhere, and the chain frequently terminates at an exchange withdrawal —
424
424
  an identification point. A core primitive for tying "unrelated" wallets to one
425
- controller. (Uses external transactions; wallets first funded by an internal
426
- contract call need internal-tx data — a planned extension.)
425
+ controller. On EVM chains, Tron and TON the history is read from its start, so
426
+ the first funding of a years-old, busy wallet is found too; Bitcoin and Solana
427
+ explorers list newest first, so there it is the earliest of the latest 200
428
+ transfers. On EVM chains a first funding sent by a contract (a mixer or bridge
429
+ withdrawal, an internal transaction) counts and is flagged.
427
430
 
428
431
  ### Victim lists and timing analysis
429
432
 
File without changes
File without changes
File without changes