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.
Files changed (53) hide show
  1. {crypttrace-0.18.0/src/crypttrace.egg-info → crypttrace-0.19.0}/PKG-INFO +1 -2
  2. {crypttrace-0.18.0 → crypttrace-0.19.0}/README.md +0 -1
  3. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/__init__.py +1 -1
  4. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/chains.py +15 -4
  5. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/config.py +16 -1
  6. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/fetchers/etherscan.py +13 -0
  7. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/labels/known.json +2 -2
  8. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/labels/tronscan.py +1 -11
  9. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/store.py +8 -0
  10. {crypttrace-0.18.0 → crypttrace-0.19.0/src/crypttrace.egg-info}/PKG-INFO +1 -2
  11. {crypttrace-0.18.0 → crypttrace-0.19.0}/LICENSE +0 -0
  12. {crypttrace-0.18.0 → crypttrace-0.19.0}/pyproject.toml +0 -0
  13. {crypttrace-0.18.0 → crypttrace-0.19.0}/setup.cfg +0 -0
  14. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/addresses.py +0 -0
  15. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/analysis.py +0 -0
  16. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/assess.py +0 -0
  17. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/assets.py +0 -0
  18. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/bridges.py +0 -0
  19. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/cli.py +0 -0
  20. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/drain.py +0 -0
  21. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/evidence.py +0 -0
  22. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/fetchers/__init__.py +0 -0
  23. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/fetchers/bitcoin.py +0 -0
  24. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/fetchers/http.py +0 -0
  25. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/fetchers/solana.py +0 -0
  26. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/fetchers/ton.py +0 -0
  27. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/fetchers/tron.py +0 -0
  28. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/freeze.py +0 -0
  29. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/funder.py +0 -0
  30. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/investigate.py +0 -0
  31. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/labels/__init__.py +0 -0
  32. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/labels/audit.py +0 -0
  33. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/labels/bulk.py +0 -0
  34. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/labels/labels.py +0 -0
  35. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/labels/names.py +0 -0
  36. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/labels/partial.json +0 -0
  37. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/labels/tonapi.py +0 -0
  38. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/offramp.py +0 -0
  39. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/poisoning.py +0 -0
  40. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/prices.py +0 -0
  41. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/render.py +0 -0
  42. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/report.py +0 -0
  43. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/spam.py +0 -0
  44. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/trace.py +0 -0
  45. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/verify.py +0 -0
  46. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/watch.py +0 -0
  47. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/web/index.html +0 -0
  48. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace/webapp.py +0 -0
  49. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace.egg-info/SOURCES.txt +0 -0
  50. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace.egg-info/dependency_links.txt +0 -0
  51. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace.egg-info/entry_points.txt +0 -0
  52. {crypttrace-0.18.0 → crypttrace-0.19.0}/src/crypttrace.egg-info/requires.txt +0 -0
  53. {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.18.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.18.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
- rows.append({"from": tx.get("from", "").lower(), "to": tx.get("to", "").lower(),
112
- "value": val, "timestamp": int(tx.get("timeStamp", "0") or 0),
113
- "hash": tx.get("hash", ""), "symbol": symbol(chain)})
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 = os.environ.get("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", "type": "mixer"},
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
- k = os.environ.get("TRONSCAN_API_KEY")
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.18.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