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.
- {crypttrace-0.21.0/src/crypttrace.egg-info → crypttrace-0.22.0}/PKG-INFO +6 -3
- {crypttrace-0.21.0 → crypttrace-0.22.0}/README.md +5 -2
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/__init__.py +1 -1
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/chains.py +39 -4
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/fetchers/ton.py +8 -5
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/fetchers/tron.py +11 -7
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/funder.py +5 -3
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/store.py +11 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0/src/crypttrace.egg-info}/PKG-INFO +6 -3
- {crypttrace-0.21.0 → crypttrace-0.22.0}/LICENSE +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/pyproject.toml +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/setup.cfg +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/addresses.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/analysis.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/assess.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/assets.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/bridges.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/cli.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/config.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/drain.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/evidence.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/fetchers/__init__.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/fetchers/bitcoin.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/fetchers/etherscan.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/fetchers/http.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/fetchers/solana.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/freeze.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/gascluster.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/investigate.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/labels/__init__.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/labels/audit.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/labels/bulk.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/labels/known.json +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/labels/labels.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/labels/names.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/labels/partial.json +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/labels/tonapi.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/labels/tronscan.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/offramp.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/poisoning.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/prices.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/render.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/report.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/spam.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/trace.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/verify.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/watch.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/web/index.html +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace/webapp.py +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace.egg-info/SOURCES.txt +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace.egg-info/dependency_links.txt +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace.egg-info/entry_points.txt +0 -0
- {crypttrace-0.21.0 → crypttrace-0.22.0}/src/crypttrace.egg-info/requires.txt +0 -0
- {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.
|
|
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.
|
|
426
|
-
|
|
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.
|
|
392
|
-
|
|
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.
|
|
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
|
-
|
|
109
|
-
calls
|
|
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
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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.
|
|
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.
|
|
426
|
-
|
|
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
|
|
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
|