crypttrace 0.30.0__tar.gz → 0.31.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.30.0/src/crypttrace.egg-info → crypttrace-0.31.0}/PKG-INFO +6 -4
- {crypttrace-0.30.0 → crypttrace-0.31.0}/README.md +5 -3
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/__init__.py +1 -1
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/addresses.py +24 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/analysis.py +1 -1
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/assets.py +2 -2
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/chains.py +18 -10
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/cli.py +6 -4
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/drain.py +1 -1
- crypttrace-0.31.0/src/crypttrace/fetchers/bitcoin.py +287 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/fetchers/http.py +2 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/labels/known.json +72 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/poisoning.py +1 -1
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/prices.py +2 -1
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/store.py +7 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/verify.py +20 -7
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/watch.py +1 -1
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/web/index.html +1 -1
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/webapp.py +6 -1
- {crypttrace-0.30.0 → crypttrace-0.31.0/src/crypttrace.egg-info}/PKG-INFO +6 -4
- crypttrace-0.30.0/src/crypttrace/fetchers/bitcoin.py +0 -186
- {crypttrace-0.30.0 → crypttrace-0.31.0}/LICENSE +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/pyproject.toml +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/setup.cfg +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/assess.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/bridges.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/config.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/evidence.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/fetchers/__init__.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/fetchers/etherscan.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/fetchers/solana.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/fetchers/ton.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/fetchers/tron.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/freeze.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/funder.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/gascluster.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/investigate.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/labels/__init__.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/labels/audit.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/labels/bulk.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/labels/labels.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/labels/names.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/labels/partial.json +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/labels/tonapi.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/labels/tronscan.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/offramp.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/render.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/report.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/spam.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace/trace.py +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace.egg-info/SOURCES.txt +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace.egg-info/dependency_links.txt +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace.egg-info/entry_points.txt +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.0}/src/crypttrace.egg-info/requires.txt +0 -0
- {crypttrace-0.30.0 → crypttrace-0.31.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.31.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
|
|
@@ -42,7 +42,7 @@ Dynamic: license-file
|
|
|
42
42
|
Open-source OSINT toolkit for crypto investigations. Give it a suspicious
|
|
43
43
|
address; it pulls the public on-chain history, labels known entities (exchanges,
|
|
44
44
|
mixers, sanctioned wallets), scores risk, and maps where the funds moved —
|
|
45
|
-
across Ethereum, Bitcoin, Tron, Solana, TON and more.
|
|
45
|
+
across Ethereum, Bitcoin, Tron, Solana, TON, Litecoin, Dogecoin and more.
|
|
46
46
|
|
|
47
47
|
Built for the investigation that actually happens: *someone's crypto was stolen,
|
|
48
48
|
here's the wallet — where did the money go, and where can it still be stopped?*
|
|
@@ -80,7 +80,7 @@ python selftest.py # offline checks, no API keys needed
|
|
|
80
80
|
| | Needed? | How to get it |
|
|
81
81
|
|---|---|---|
|
|
82
82
|
| **EVM chains** (Ethereum, BSC, Polygon, Arbitrum, Optimism, Base) | **Required** | Free key at [etherscan.io/myapikey](https://etherscan.io/myapikey) covers Ethereum, Polygon and Arbitrum. BSC, Optimism and Base need a paid Etherscan plan; on a free key crypttrace says so instead of showing an empty wallet. |
|
|
83
|
-
| **Bitcoin, Tron, Solana, TON** | **Not required** | Work out of the box via public endpoints. |
|
|
83
|
+
| **Bitcoin, Litecoin, Dogecoin, Tron, Solana, TON** | **Not required** | Work out of the box via public endpoints. Dogecoin reads BlockCypher's free tier, about 100 requests an hour, so it reads the latest 200 transactions of an address. |
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
86
|
export ETHERSCAN_API_KEY=xxxx # required for EVM chains only
|
|
@@ -122,6 +122,8 @@ the number either way.
|
|
|
122
122
|
|---|---|---|---|
|
|
123
123
|
| Ethereum, BSC, Polygon, Arbitrum, Optimism, Base | `eth` `bsc` `polygon` `arbitrum` `optimism` `base` | Etherscan v2 (BSC, Optimism, Base: paid plan) | account |
|
|
124
124
|
| Bitcoin | `btc` | mempool.space | UTXO |
|
|
125
|
+
| Litecoin | `ltc` | litecoinspace.org | UTXO |
|
|
126
|
+
| Dogecoin | `doge` | BlockCypher | UTXO |
|
|
125
127
|
| Tron (TRX + USDT-TRC20) | `tron` | TronGrid | account |
|
|
126
128
|
| Solana (SOL + SPL) | `sol` | public JSON-RPC | account |
|
|
127
129
|
| TON (TON + jettons, e.g. USDT) | `ton` | toncenter v3 | account |
|
|
@@ -472,7 +474,7 @@ with an `nfts` column in the CSV. Failed (aborted) NFT transfers are not counted
|
|
|
472
474
|
|
|
473
475
|
### Bitcoin clustering (common-input-ownership)
|
|
474
476
|
|
|
475
|
-
Bitcoin's UTXO model enables the strongest clustering heuristic in blockchain
|
|
477
|
+
Bitcoin's UTXO model (Litecoin's and Dogecoin's too) enables the strongest clustering heuristic in blockchain
|
|
476
478
|
forensics: if several addresses sign the inputs of one transaction, one party
|
|
477
479
|
almost certainly controls all of them. `cluster` surfaces those co-signers,
|
|
478
480
|
turning a single address into a set of wallets belonging to the same owner. A
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
Open-source OSINT toolkit for crypto investigations. Give it a suspicious
|
|
9
9
|
address; it pulls the public on-chain history, labels known entities (exchanges,
|
|
10
10
|
mixers, sanctioned wallets), scores risk, and maps where the funds moved —
|
|
11
|
-
across Ethereum, Bitcoin, Tron, Solana, TON and more.
|
|
11
|
+
across Ethereum, Bitcoin, Tron, Solana, TON, Litecoin, Dogecoin and more.
|
|
12
12
|
|
|
13
13
|
Built for the investigation that actually happens: *someone's crypto was stolen,
|
|
14
14
|
here's the wallet — where did the money go, and where can it still be stopped?*
|
|
@@ -46,7 +46,7 @@ python selftest.py # offline checks, no API keys needed
|
|
|
46
46
|
| | Needed? | How to get it |
|
|
47
47
|
|---|---|---|
|
|
48
48
|
| **EVM chains** (Ethereum, BSC, Polygon, Arbitrum, Optimism, Base) | **Required** | Free key at [etherscan.io/myapikey](https://etherscan.io/myapikey) covers Ethereum, Polygon and Arbitrum. BSC, Optimism and Base need a paid Etherscan plan; on a free key crypttrace says so instead of showing an empty wallet. |
|
|
49
|
-
| **Bitcoin, Tron, Solana, TON** | **Not required** | Work out of the box via public endpoints. |
|
|
49
|
+
| **Bitcoin, Litecoin, Dogecoin, Tron, Solana, TON** | **Not required** | Work out of the box via public endpoints. Dogecoin reads BlockCypher's free tier, about 100 requests an hour, so it reads the latest 200 transactions of an address. |
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
export ETHERSCAN_API_KEY=xxxx # required for EVM chains only
|
|
@@ -88,6 +88,8 @@ the number either way.
|
|
|
88
88
|
|---|---|---|---|
|
|
89
89
|
| Ethereum, BSC, Polygon, Arbitrum, Optimism, Base | `eth` `bsc` `polygon` `arbitrum` `optimism` `base` | Etherscan v2 (BSC, Optimism, Base: paid plan) | account |
|
|
90
90
|
| Bitcoin | `btc` | mempool.space | UTXO |
|
|
91
|
+
| Litecoin | `ltc` | litecoinspace.org | UTXO |
|
|
92
|
+
| Dogecoin | `doge` | BlockCypher | UTXO |
|
|
91
93
|
| Tron (TRX + USDT-TRC20) | `tron` | TronGrid | account |
|
|
92
94
|
| Solana (SOL + SPL) | `sol` | public JSON-RPC | account |
|
|
93
95
|
| TON (TON + jettons, e.g. USDT) | `ton` | toncenter v3 | account |
|
|
@@ -438,7 +440,7 @@ with an `nfts` column in the CSV. Failed (aborted) NFT transfers are not counted
|
|
|
438
440
|
|
|
439
441
|
### Bitcoin clustering (common-input-ownership)
|
|
440
442
|
|
|
441
|
-
Bitcoin's UTXO model enables the strongest clustering heuristic in blockchain
|
|
443
|
+
Bitcoin's UTXO model (Litecoin's and Dogecoin's too) enables the strongest clustering heuristic in blockchain
|
|
442
444
|
forensics: if several addresses sign the inputs of one transaction, one party
|
|
443
445
|
almost certainly controls all of them. `cluster` surfaces those co-signers,
|
|
444
446
|
turning a single address into a set of wallets belonging to the same owner. A
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""crypttrace — OSINT crypto investigation CLI."""
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.31.0"
|
|
@@ -82,6 +82,18 @@ def check_base58check(addr: str, expect_prefix: Optional[bytes] = None) -> Tuple
|
|
|
82
82
|
return True, "base58check checksum valid"
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def _versioned(addr: str, versions: dict, name: str) -> Tuple[bool, str]:
|
|
86
|
+
"""base58check plus the chain's version byte — a Bitcoin '3…' address passes the
|
|
87
|
+
checksum too, so the version is what tells the chains apart."""
|
|
88
|
+
ok, why = check_base58check(addr)
|
|
89
|
+
if not ok:
|
|
90
|
+
return ok, why
|
|
91
|
+
v = _b58_decode(addr)[0]
|
|
92
|
+
if v not in versions:
|
|
93
|
+
return False, f"version byte {v:#04x} is not a {name} address"
|
|
94
|
+
return True, "base58check checksum valid"
|
|
95
|
+
|
|
96
|
+
|
|
85
97
|
# ---------------------------------------------------------------- per chain
|
|
86
98
|
|
|
87
99
|
def validate(address: str, chain: str) -> Tuple[bool, str]:
|
|
@@ -108,6 +120,18 @@ def validate(address: str, chain: str) -> Tuple[bool, str]:
|
|
|
108
120
|
return check_base58check(a)
|
|
109
121
|
return False, "not a recognised Bitcoin address format"
|
|
110
122
|
|
|
123
|
+
if chain == "ltc":
|
|
124
|
+
if a.lower().startswith("ltc1"):
|
|
125
|
+
return check_bech32(a, "ltc")
|
|
126
|
+
if a.startswith(("L", "M", "3")):
|
|
127
|
+
return _versioned(a, {0x30: "L", 0x32: "M", 0x05: "3"}, "Litecoin")
|
|
128
|
+
return False, "not a recognised Litecoin address format (L…, M…, 3… or ltc1…)"
|
|
129
|
+
|
|
130
|
+
if chain == "doge":
|
|
131
|
+
if a.startswith(("D", "A", "9")):
|
|
132
|
+
return _versioned(a, {0x1E: "D", 0x16: "A/9"}, "Dogecoin")
|
|
133
|
+
return False, "not a recognised Dogecoin address format (D…, A… or 9…)"
|
|
134
|
+
|
|
111
135
|
if chain == "tron":
|
|
112
136
|
if not a.startswith("T") or len(a) != 34:
|
|
113
137
|
return False, "Tron addresses start with T and are 34 characters"
|
|
@@ -22,7 +22,7 @@ from crypttrace.labels import labels
|
|
|
22
22
|
# Amounts below these are treated as dust: spam sent to well-known addresses to
|
|
23
23
|
# pollute their history (and sometimes to deanonymise the owner). Left in, dust
|
|
24
24
|
# drowns out the transfers an investigation is actually about.
|
|
25
|
-
DUST = {"btc": 0.0005, "eth": 0.002, "bsc": 0.005, "polygon": 5.0,
|
|
25
|
+
DUST = {"btc": 0.0005, "ltc": 0.001, "doge": 5.0, "eth": 0.002, "bsc": 0.005, "polygon": 5.0,
|
|
26
26
|
"arbitrum": 0.002, "optimism": 0.002, "base": 0.002,
|
|
27
27
|
"tron": 5.0, "sol": 0.01, "ton": 0.01}
|
|
28
28
|
|
|
@@ -69,8 +69,8 @@ def resolve_asset(asset: Optional[str], chain: str = "eth") -> Optional[dict]:
|
|
|
69
69
|
elif a.startswith("0x") and len(a) == 42:
|
|
70
70
|
return {"contract": a, "symbol": asset.upper()[:8], "stable": False}
|
|
71
71
|
|
|
72
|
-
if chain
|
|
73
|
-
raise ValueError("
|
|
72
|
+
if chain in ("btc", "ltc", "doge"):
|
|
73
|
+
raise ValueError(f"{chain.upper()} has no tokens — omit --asset (or use --asset {chain}).")
|
|
74
74
|
|
|
75
75
|
opts = ", ".join(known) or "none registered"
|
|
76
76
|
raise ValueError(f"Unknown asset '{asset}' on {chain}. Known symbols: {opts}. "
|
|
@@ -19,17 +19,19 @@ from crypttrace.labels import labels as _labels
|
|
|
19
19
|
from crypttrace.fetchers import etherscan, bitcoin, http as _http, tron, solana, ton
|
|
20
20
|
|
|
21
21
|
EVM_CHAINS = set(config.CHAINS)
|
|
22
|
-
NON_EVM = {"btc", "tron", "sol", "ton"}
|
|
22
|
+
NON_EVM = {"btc", "ltc", "doge", "tron", "sol", "ton"}
|
|
23
23
|
ALL_CHAINS = sorted(EVM_CHAINS | NON_EVM)
|
|
24
24
|
|
|
25
25
|
SYMBOL = {"eth": "ETH", "bsc": "BNB", "polygon": "MATIC", "arbitrum": "ETH",
|
|
26
|
-
"optimism": "ETH", "base": "ETH", "btc": "BTC", "
|
|
26
|
+
"optimism": "ETH", "base": "ETH", "btc": "BTC", "ltc": "LTC", "doge": "DOGE", "tron": "TRX", "sol": "SOL",
|
|
27
|
+
"ton": "TON"}
|
|
27
28
|
|
|
28
29
|
EXPLORER = {
|
|
29
30
|
"eth": "https://etherscan.io/address/{}", "bsc": "https://bscscan.com/address/{}",
|
|
30
31
|
"polygon": "https://polygonscan.com/address/{}", "arbitrum": "https://arbiscan.io/address/{}",
|
|
31
32
|
"optimism": "https://optimistic.etherscan.io/address/{}", "base": "https://basescan.org/address/{}",
|
|
32
|
-
"btc": "https://mempool.space/address/{}", "
|
|
33
|
+
"btc": "https://mempool.space/address/{}", "ltc": "https://litecoinspace.org/address/{}",
|
|
34
|
+
"doge": "https://blockchair.com/dogecoin/address/{}", "tron": "https://tronscan.org/#/address/{}",
|
|
33
35
|
"sol": "https://solscan.io/account/{}", "ton": "https://tonviewer.com/{}",
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -37,7 +39,8 @@ TX_EXPLORER = {
|
|
|
37
39
|
"eth": "https://etherscan.io/tx/{}", "bsc": "https://bscscan.com/tx/{}",
|
|
38
40
|
"polygon": "https://polygonscan.com/tx/{}", "arbitrum": "https://arbiscan.io/tx/{}",
|
|
39
41
|
"optimism": "https://optimistic.etherscan.io/tx/{}", "base": "https://basescan.org/tx/{}",
|
|
40
|
-
"btc": "https://mempool.space/tx/{}", "
|
|
42
|
+
"btc": "https://mempool.space/tx/{}", "ltc": "https://litecoinspace.org/tx/{}",
|
|
43
|
+
"doge": "https://blockchair.com/dogecoin/transaction/{}", "tron": "https://tronscan.org/#/transaction/{}",
|
|
41
44
|
"sol": "https://solscan.io/tx/{}", "ton": "https://tonviewer.com/transaction/{}",
|
|
42
45
|
}
|
|
43
46
|
|
|
@@ -53,6 +56,11 @@ def is_evm(chain: str) -> bool:
|
|
|
53
56
|
return chain in EVM_CHAINS
|
|
54
57
|
|
|
55
58
|
|
|
59
|
+
def is_utxo(chain: str) -> bool:
|
|
60
|
+
"""Bitcoin, Litecoin, Dogecoin: outputs and inputs, no tokens."""
|
|
61
|
+
return chain in bitcoin.UTXO_CHAINS
|
|
62
|
+
|
|
63
|
+
|
|
56
64
|
def case_sensitive(chain: str) -> bool:
|
|
57
65
|
"""Bitcoin/Tron/Solana use base58 — address case is significant."""
|
|
58
66
|
return chain in NON_EVM
|
|
@@ -86,8 +94,8 @@ def balance(address: str, chain: str = "eth") -> float:
|
|
|
86
94
|
try:
|
|
87
95
|
if is_evm(chain):
|
|
88
96
|
return etherscan.get_balance(address, chain)
|
|
89
|
-
if chain
|
|
90
|
-
return bitcoin.balance(address)
|
|
97
|
+
if is_utxo(chain):
|
|
98
|
+
return bitcoin.balance(address, chain)
|
|
91
99
|
if chain == "tron":
|
|
92
100
|
return tron.balance(address)
|
|
93
101
|
if chain == "sol":
|
|
@@ -221,10 +229,10 @@ def transfers(address: str, chain: str = "eth", limit: int = 1000,
|
|
|
221
229
|
if is_evm(chain):
|
|
222
230
|
rows = _evm_token(address, chain, contract, limit) if asset \
|
|
223
231
|
else _evm_native(address, chain, limit)
|
|
224
|
-
elif chain
|
|
232
|
+
elif is_utxo(chain):
|
|
225
233
|
if asset:
|
|
226
|
-
raise ChainError("
|
|
227
|
-
rows = bitcoin.transfers(address, limit)
|
|
234
|
+
raise ChainError(f"{bitcoin.NETWORKS[chain]['name']} has no tokens.")
|
|
235
|
+
rows = bitcoin.transfers(address, limit, chain=chain)
|
|
228
236
|
elif chain == "tron":
|
|
229
237
|
# keep TRX and TRC20 strictly separate
|
|
230
238
|
rows = _tron_token(address, contract, limit) if asset \
|
|
@@ -337,7 +345,7 @@ def inflows(address: str, chain: str, top: int, limit: int = 1000, asset=None):
|
|
|
337
345
|
def token_holdings(address: str, chain: str = "eth", limit: int = 1000) -> List[Dict]:
|
|
338
346
|
"""Approximate token holdings from transfer history (net in − out per token)."""
|
|
339
347
|
check(chain)
|
|
340
|
-
if chain
|
|
348
|
+
if is_utxo(chain):
|
|
341
349
|
return []
|
|
342
350
|
me = norm_addr(address, chain)
|
|
343
351
|
try:
|
|
@@ -762,7 +762,7 @@ def verify(
|
|
|
762
762
|
style = _verdict_style(v["status"])
|
|
763
763
|
console.print(f"\n[{style}]{v['status'].upper()}[/{style}] {verify_mod.headline(v)}\n")
|
|
764
764
|
|
|
765
|
-
if v
|
|
765
|
+
if v.get("checks"): # absent when the chain could not be read at all
|
|
766
766
|
t = render.Table(title="Cross-check against the chain", header_style="bold")
|
|
767
767
|
t.add_column("Figure"); t.add_column("Computed", justify="right")
|
|
768
768
|
t.add_column("Chain", justify="right"); t.add_column("", justify="center")
|
|
@@ -803,12 +803,12 @@ def cluster(
|
|
|
803
803
|
"funder set it up. Wallets set up by the same person's wallet are likely one party; a "
|
|
804
804
|
"funding from an exchange, mixer, bridge or any contract links nothing. A lead, not proof.[/dim]")
|
|
805
805
|
return
|
|
806
|
-
if chain
|
|
807
|
-
console.print("[red]Error:[/red] cluster works on Bitcoin and EVM chains")
|
|
806
|
+
if not chains_mod.is_utxo(chain):
|
|
807
|
+
console.print("[red]Error:[/red] cluster works on Bitcoin, Litecoin, Dogecoin and EVM chains")
|
|
808
808
|
raise typer.Exit(1)
|
|
809
809
|
from crypttrace.fetchers import bitcoin
|
|
810
810
|
try:
|
|
811
|
-
peers = bitcoin.cluster(address)
|
|
811
|
+
peers = bitcoin.cluster(address, chain)
|
|
812
812
|
except bitcoin.BitcoinError as e:
|
|
813
813
|
console.print(f"[red]Error:[/red] {e}")
|
|
814
814
|
raise typer.Exit(1)
|
|
@@ -884,6 +884,8 @@ def chains():
|
|
|
884
884
|
console.print(f" {name:<10} chainid {cid}{paid}")
|
|
885
885
|
console.print("\n[bold]Non-EVM[/bold] (no API key needed):")
|
|
886
886
|
console.print(" btc Bitcoin — mempool.space (UTXO)")
|
|
887
|
+
console.print(" ltc Litecoin — litecoinspace.org (UTXO)")
|
|
888
|
+
console.print(" doge Dogecoin — BlockCypher (UTXO; free tier ~100 requests an hour)")
|
|
887
889
|
console.print(" tron Tron — TronGrid (TRX + USDT-TRC20)")
|
|
888
890
|
console.print(" sol Solana — public JSON-RPC")
|
|
889
891
|
console.print(" ton TON — toncenter (TON + jettons such as USDT)")
|
|
@@ -29,7 +29,7 @@ _ALL_TOKENS = {"contract": None, "symbol": "*"}
|
|
|
29
29
|
|
|
30
30
|
def _history(address: str, chain: str, limit: int) -> List[dict]:
|
|
31
31
|
rows = list(chains.transfers(address, chain, limit))
|
|
32
|
-
if chain
|
|
32
|
+
if not chains.is_utxo(chain):
|
|
33
33
|
try:
|
|
34
34
|
rows += [r for r in chains.transfers(address, chain, limit, asset=_ALL_TOKENS)
|
|
35
35
|
if not spam.verdict(chain, r.get("contract", ""), r.get("symbol", ""),
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""UTXO chains — Bitcoin, Litecoin, Dogecoin — with no API key required.
|
|
2
|
+
|
|
3
|
+
Bitcoin reads mempool.space and Litecoin its twin litecoinspace.org (both
|
|
4
|
+
Esplora). Dogecoin has no public Esplora, so it reads BlockCypher, whose
|
|
5
|
+
answers are turned into the same shape; its free tier allows about 100
|
|
6
|
+
requests an hour, which is enough for a check and a few hops of tracing.
|
|
7
|
+
|
|
8
|
+
These chains use the UTXO model, not accounts: a transaction consumes previous
|
|
9
|
+
outputs and creates new ones, so there is no single "from" or "to". To fit the
|
|
10
|
+
same tracing engine used for EVM chains, transactions are normalized into
|
|
11
|
+
directional transfer rows:
|
|
12
|
+
|
|
13
|
+
* if our address signed an input, each output that isn't ours is an outflow
|
|
14
|
+
(outputs back to ourselves are change and are skipped);
|
|
15
|
+
* otherwise, outputs paying us are inflows, attributed to the first input.
|
|
16
|
+
|
|
17
|
+
UTXO chains also enable the strongest clustering heuristic in blockchain
|
|
18
|
+
forensics: common-input-ownership. If several addresses sign inputs of the
|
|
19
|
+
same transaction, one party almost certainly controls all of them.
|
|
20
|
+
"""
|
|
21
|
+
import datetime as dt
|
|
22
|
+
from typing import List, Dict, Optional
|
|
23
|
+
|
|
24
|
+
import requests
|
|
25
|
+
|
|
26
|
+
NETWORKS = {
|
|
27
|
+
"btc": {"name": "Bitcoin", "symbol": "BTC", "api": "esplora", "base": "https://mempool.space/api",
|
|
28
|
+
"host": "mempool.space"},
|
|
29
|
+
"ltc": {"name": "Litecoin", "symbol": "LTC", "api": "esplora", "base": "https://litecoinspace.org/api",
|
|
30
|
+
"host": "litecoinspace.org"},
|
|
31
|
+
"doge": {"name": "Dogecoin", "symbol": "DOGE", "api": "blockcypher",
|
|
32
|
+
"base": "https://api.blockcypher.com/v1/doge/main", "host": "BlockCypher"},
|
|
33
|
+
}
|
|
34
|
+
UTXO_CHAINS = set(NETWORKS)
|
|
35
|
+
BASE = NETWORKS["btc"]["base"]
|
|
36
|
+
SATS = 100_000_000 # satoshis, litoshis and koinus alike
|
|
37
|
+
BLOCKCYPHER_MAX_TXS = 200 # history read per Dogecoin address: the free tier is ~100 calls an hour
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BitcoinError(RuntimeError):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _net(chain: str) -> dict:
|
|
45
|
+
if chain not in NETWORKS:
|
|
46
|
+
raise BitcoinError(f"not a UTXO chain: {chain}")
|
|
47
|
+
return NETWORKS[chain]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _get(path: str, timeout: int = 30, chain: str = "btc", params: Optional[dict] = None):
|
|
51
|
+
"""Cached, throttled GET against the chain's explorer API."""
|
|
52
|
+
from crypttrace.fetchers import http
|
|
53
|
+
n = _net(chain)
|
|
54
|
+
try:
|
|
55
|
+
data = http.request_json(f"{n['base']}{path}", params or {}, timeout=timeout)
|
|
56
|
+
except http.RateLimited as e:
|
|
57
|
+
raise BitcoinError(str(e) + (" (BlockCypher's free tier allows about 100 requests an hour)"
|
|
58
|
+
if n["api"] == "blockcypher" else ""))
|
|
59
|
+
except requests.RequestException as e:
|
|
60
|
+
raise BitcoinError(f"{n['host']} request failed: {e}")
|
|
61
|
+
except ValueError as e:
|
|
62
|
+
raise BitcoinError(f"bad response from {n['host']}: {e}")
|
|
63
|
+
if isinstance(data, dict) and "__status__" in data:
|
|
64
|
+
if data["__status__"] == 400:
|
|
65
|
+
raise BitcoinError(f"{n['name']} address rejected by {n['host']} — check it is "
|
|
66
|
+
"exact (these addresses are case-sensitive).")
|
|
67
|
+
raise BitcoinError(f"Address not found on the {n['name']} chain.")
|
|
68
|
+
return data
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def stats(address: str, chain: str = "btc") -> Dict:
|
|
72
|
+
"""Authoritative totals straight from the chain index.
|
|
73
|
+
|
|
74
|
+
These are the numbers our own parsing must agree with — the reference for
|
|
75
|
+
checking that a trace didn't miscount or silently miss history.
|
|
76
|
+
"""
|
|
77
|
+
if _net(chain)["api"] == "blockcypher":
|
|
78
|
+
d = _get(f"/addrs/{address}/balance", chain=chain)
|
|
79
|
+
recv, sent = (d.get("total_received") or 0) / SATS, (d.get("total_sent") or 0) / SATS
|
|
80
|
+
return {"received": recv, "sent": sent, "balance": recv - sent,
|
|
81
|
+
"tx_count": d.get("final_n_tx") or d.get("n_tx") or 0, "funded_outputs": None}
|
|
82
|
+
d = _get(f"/address/{address}", chain=chain)
|
|
83
|
+
cs = d.get("chain_stats", {}) or {}
|
|
84
|
+
ms = d.get("mempool_stats", {}) or {}
|
|
85
|
+
recv = (cs.get("funded_txo_sum", 0) + ms.get("funded_txo_sum", 0)) / SATS
|
|
86
|
+
sent = (cs.get("spent_txo_sum", 0) + ms.get("spent_txo_sum", 0)) / SATS
|
|
87
|
+
return {
|
|
88
|
+
"received": recv,
|
|
89
|
+
"sent": sent,
|
|
90
|
+
"balance": recv - sent,
|
|
91
|
+
"tx_count": (cs.get("tx_count", 0) + ms.get("tx_count", 0)),
|
|
92
|
+
"funded_outputs": (cs.get("funded_txo_count", 0) + ms.get("funded_txo_count", 0)),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def balance(address: str, chain: str = "btc") -> float:
|
|
97
|
+
return stats(address, chain)["balance"]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _bc_time(tx: dict) -> int:
|
|
101
|
+
s = tx.get("confirmed") or tx.get("received") or ""
|
|
102
|
+
try:
|
|
103
|
+
return int(dt.datetime.fromisoformat(s.replace("Z", "+00:00")).timestamp())
|
|
104
|
+
except ValueError:
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _from_blockcypher(tx: dict) -> dict:
|
|
109
|
+
"""A BlockCypher transaction in Esplora's shape, so one parser serves every chain."""
|
|
110
|
+
first = lambda xs: (xs or [None])[0]
|
|
111
|
+
return {
|
|
112
|
+
"txid": tx.get("hash", ""),
|
|
113
|
+
"status": {"block_time": _bc_time(tx)},
|
|
114
|
+
"vin": [{"prevout": {"scriptpubkey_address": first(i.get("addresses")),
|
|
115
|
+
"value": i.get("output_value") or 0}} for i in tx.get("inputs") or []],
|
|
116
|
+
"vout": [{"scriptpubkey_address": first(o.get("addresses")), "value": o.get("value") or 0}
|
|
117
|
+
for o in tx.get("outputs") or []],
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def raw_txs(address: str, max_txs: int = 500, chain: str = "btc") -> List[dict]:
|
|
122
|
+
"""Transactions touching this address, paging back through history.
|
|
123
|
+
|
|
124
|
+
The explorers return ~50 per call. A single page is not enough for
|
|
125
|
+
investigations: famous addresses get spammed with dust, which pushes the
|
|
126
|
+
transactions that actually matter out of the recent window. So we follow
|
|
127
|
+
the cursor back until we have enough history.
|
|
128
|
+
"""
|
|
129
|
+
out: List[dict] = []
|
|
130
|
+
if _net(chain)["api"] == "blockcypher":
|
|
131
|
+
seen, before = set(), None
|
|
132
|
+
while len(out) < max_txs:
|
|
133
|
+
params = {"limit": 50, "txlimit": 500} # txlimit: inputs/outputs per tx (default 20)
|
|
134
|
+
if before is not None:
|
|
135
|
+
params["before"] = before
|
|
136
|
+
d = _get(f"/addrs/{address}/full", chain=chain, params=params)
|
|
137
|
+
batch = [t for t in (d.get("txs") or []) if t.get("hash") not in seen]
|
|
138
|
+
if not batch:
|
|
139
|
+
break
|
|
140
|
+
for t in batch:
|
|
141
|
+
seen.add(t.get("hash"))
|
|
142
|
+
out.append(_from_blockcypher(t))
|
|
143
|
+
heights = [t.get("block_height") for t in batch if (t.get("block_height") or -1) > 0]
|
|
144
|
+
if not d.get("hasMore") or not heights:
|
|
145
|
+
break
|
|
146
|
+
before = min(heights)
|
|
147
|
+
return out[:max_txs]
|
|
148
|
+
last: Optional[str] = None
|
|
149
|
+
while len(out) < max_txs:
|
|
150
|
+
path = f"/address/{address}/txs" if last is None \
|
|
151
|
+
else f"/address/{address}/txs/chain/{last}"
|
|
152
|
+
batch = _get(path, chain=chain)
|
|
153
|
+
if not isinstance(batch, list) or not batch:
|
|
154
|
+
break
|
|
155
|
+
out.extend(batch)
|
|
156
|
+
if len(batch) < 25: # last page
|
|
157
|
+
break
|
|
158
|
+
nxt = batch[-1].get("txid")
|
|
159
|
+
if not nxt or nxt == last:
|
|
160
|
+
break
|
|
161
|
+
last = nxt
|
|
162
|
+
return out[:max_txs]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _in_addrs(tx: dict) -> List[str]:
|
|
166
|
+
out = []
|
|
167
|
+
for v in tx.get("vin", []) or []:
|
|
168
|
+
a = (v.get("prevout") or {}).get("scriptpubkey_address")
|
|
169
|
+
if a:
|
|
170
|
+
out.append(a)
|
|
171
|
+
return out
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _in_pairs(tx: dict):
|
|
175
|
+
"""(address, value) for every input — a UTXO tx can be funded by many parties."""
|
|
176
|
+
for v in tx.get("vin", []) or []:
|
|
177
|
+
p = v.get("prevout") or {}
|
|
178
|
+
a = p.get("scriptpubkey_address")
|
|
179
|
+
if a:
|
|
180
|
+
yield a, (p.get("value", 0) or 0) / SATS
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _out_pairs(tx: dict):
|
|
184
|
+
for o in tx.get("vout", []) or []:
|
|
185
|
+
a = o.get("scriptpubkey_address")
|
|
186
|
+
if a:
|
|
187
|
+
yield a, o.get("value", 0) / SATS
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _depth(limit: int, chain: str) -> int:
|
|
191
|
+
"""How many transactions of history to read for `limit` transfer rows."""
|
|
192
|
+
depth = max(limit, 200)
|
|
193
|
+
if _net(chain)["api"] == "blockcypher":
|
|
194
|
+
depth = min(depth, BLOCKCYPHER_MAX_TXS) # 4 calls of the ~100 an hour
|
|
195
|
+
return depth
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def change(address: str, limit: int = 1000, chain: str = "btc") -> Dict[str, float]:
|
|
199
|
+
"""What the address paid back to itself as change, over the same history
|
|
200
|
+
`transfers` reads. Transfers leave change out — it is not money moving — but
|
|
201
|
+
the chain's totals count it twice, so a check against them adds it back:
|
|
202
|
+
in full to "received" (the change output is the address's), and in the
|
|
203
|
+
address's share of the inputs to "sent" (when others funded the transaction
|
|
204
|
+
too, only that share of what came back was the address's own spending)."""
|
|
205
|
+
received = sent = 0.0
|
|
206
|
+
for tx in raw_txs(address, max_txs=_depth(limit, chain), chain=chain):
|
|
207
|
+
ins = list(_in_pairs(tx))
|
|
208
|
+
mine_in = sum(v for a, v in ins if a == address)
|
|
209
|
+
if mine_in <= 0:
|
|
210
|
+
continue
|
|
211
|
+
back = sum(v for a, v in _out_pairs(tx) if a == address)
|
|
212
|
+
received += back
|
|
213
|
+
sent += back * mine_in / sum(v for _, v in ins)
|
|
214
|
+
return {"received": received, "sent": sent}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def transfers(address: str, limit: int = 1000, chain: str = "btc") -> List[Dict]:
|
|
218
|
+
"""Normalized {from,to,value,timestamp,hash,symbol} rows.
|
|
219
|
+
|
|
220
|
+
A UTXO transaction has no single sender: it can be funded by many inputs
|
|
221
|
+
from different owners and pay many outputs. Attributing a whole transfer to
|
|
222
|
+
the first input address — the naive shortcut — badly misreports
|
|
223
|
+
consolidations, e.g. crediting one wallet with funds a hundred others
|
|
224
|
+
supplied. So value is split in proportion to what each side actually
|
|
225
|
+
contributed or received.
|
|
226
|
+
"""
|
|
227
|
+
me = address
|
|
228
|
+
sym = _net(chain)["symbol"]
|
|
229
|
+
rows: List[Dict] = []
|
|
230
|
+
for tx in raw_txs(address, max_txs=_depth(limit, chain), chain=chain):
|
|
231
|
+
ts = int((tx.get("status") or {}).get("block_time") or 0)
|
|
232
|
+
h = tx.get("txid", "")
|
|
233
|
+
ins = list(_in_pairs(tx))
|
|
234
|
+
outs = list(_out_pairs(tx))
|
|
235
|
+
total_in = sum(v for _, v in ins)
|
|
236
|
+
if total_in <= 0:
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
if me in (a for a, _ in ins):
|
|
240
|
+
# outgoing: our share of the inputs funds our share of each output
|
|
241
|
+
mine_in = sum(v for a, v in ins if a == me)
|
|
242
|
+
share = mine_in / total_in
|
|
243
|
+
# The chain counts a spent output at full value; the recipient gets
|
|
244
|
+
# less, because the miner fee comes out in between. Carry our share
|
|
245
|
+
# of that fee so totals can be reconciled exactly rather than
|
|
246
|
+
# hidden inside a tolerance.
|
|
247
|
+
fee_share = (total_in - sum(v for _, v in outs)) * share
|
|
248
|
+
# one row per recipient: several equal outputs to one address in one
|
|
249
|
+
# transaction would otherwise be identical rows, and the store keeps one
|
|
250
|
+
paid: Dict[str, float] = {}
|
|
251
|
+
for a, v in outs:
|
|
252
|
+
if a == me or v <= 0:
|
|
253
|
+
continue # change back to self
|
|
254
|
+
paid[a] = paid.get(a, 0.0) + v
|
|
255
|
+
for a, v in paid.items():
|
|
256
|
+
rows.append({"from": me, "to": a, "value": v * share,
|
|
257
|
+
"timestamp": ts, "hash": h, "symbol": sym,
|
|
258
|
+
"fee_share": fee_share})
|
|
259
|
+
else:
|
|
260
|
+
# incoming: credit every funder in proportion to what it put in
|
|
261
|
+
mine_out = sum(v for a, v in outs if a == me)
|
|
262
|
+
if mine_out <= 0:
|
|
263
|
+
continue
|
|
264
|
+
merged: Dict[str, float] = {}
|
|
265
|
+
for a, v in ins:
|
|
266
|
+
merged[a] = merged.get(a, 0.0) + v
|
|
267
|
+
for a, v in merged.items():
|
|
268
|
+
rows.append({"from": a, "to": me, "value": mine_out * (v / total_in),
|
|
269
|
+
"timestamp": ts, "hash": h, "symbol": sym})
|
|
270
|
+
if len(rows) >= limit:
|
|
271
|
+
break
|
|
272
|
+
return rows
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def cluster(address: str, chain: str = "btc") -> List[tuple]:
|
|
276
|
+
"""Common-input-ownership: addresses that co-signed inputs with `address`.
|
|
277
|
+
|
|
278
|
+
Returns [(address, times_seen_together)] — likely the same owner's wallets.
|
|
279
|
+
"""
|
|
280
|
+
peers: Dict[str, int] = {}
|
|
281
|
+
for tx in raw_txs(address, chain=chain):
|
|
282
|
+
ins = _in_addrs(tx)
|
|
283
|
+
if address in ins and len(set(ins)) > 1:
|
|
284
|
+
for a in set(ins):
|
|
285
|
+
if a != address:
|
|
286
|
+
peers[a] = peers.get(a, 0) + 1
|
|
287
|
+
return sorted(peers.items(), key=lambda kv: kv[1], reverse=True)
|
|
@@ -24,6 +24,8 @@ from crypttrace import config
|
|
|
24
24
|
MIN_INTERVAL = {
|
|
25
25
|
"api.trongrid.io": 0.4,
|
|
26
26
|
"mempool.space": 0.25,
|
|
27
|
+
"litecoinspace.org": 0.25,
|
|
28
|
+
"api.blockcypher.com": 0.4, # free tier: 3 a second, ~100 an hour
|
|
27
29
|
"api.mainnet-beta.solana.com": 0.3,
|
|
28
30
|
}
|
|
29
31
|
DEFAULT_INTERVAL = 0.25
|
|
@@ -711,6 +711,78 @@
|
|
|
711
711
|
"TT1DyeqXaaJkt6UhVYFWUXBXknaXnBudTK": {"name": "Binance reserve wallet", "type": "exchange", "chain": "tron", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
712
712
|
"TTU4bhRuPEtaSWPXvvRQ7CVnyHs6cGu24e": {"name": "Binance reserve wallet", "type": "exchange", "chain": "tron", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2026-11-01", "source_kind": "self-published", "added": "2026-11-07"},
|
|
713
713
|
"TYLRgzUKgLHw5wCYox2xRUkCQaWo9koP6K": {"name": "Binance reserve wallet", "type": "exchange", "chain": "tron", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2026-11-01", "source_kind": "self-published", "added": "2026-11-07"},
|
|
714
|
+
"DGmzv39riELTuigZCUD6sWoHEHPdSbxdUB": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
715
|
+
"MQSs17ECe51kqhkFPQY98wiyPvGuK5iCB6": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
716
|
+
"D59dre2oAFac8UYqwrSCC4rcQapcC2PQA5": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2026-07-20", "source_kind": "self-published", "added": "2026-11-07"},
|
|
717
|
+
"D6e11vLvkgjnkvNbViSk8PBNSKVN4dupx8": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-12-18", "source_kind": "self-published", "added": "2026-11-07"},
|
|
718
|
+
"D7BrtudMAdwz2U7vSGumVDuxZsZNibJuzw": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
719
|
+
"D7D2f2WuEkwhTWqBkisZ5sM3JagaBcNWBs": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
720
|
+
"D7LBW9Kk3Qe9iDQVGarCf8XSSs4U46rEWD": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-12-19", "source_kind": "self-published", "added": "2026-11-07"},
|
|
721
|
+
"D7bg2iUG3UiHuTWjRqcjdJiEjHrsFASsjw": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
722
|
+
"DA4kkg23oQ2wS4UPMfdL9jbDuoLgk7KiTW": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2026-06-16", "source_kind": "self-published", "added": "2026-11-07"},
|
|
723
|
+
"DA9TU5Gtd4VM8petBvf8ecv9u13NSTYzzz": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
724
|
+
"DAYthKnkrWUHRENvtBwjemi6kFzk4K9SMr": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
725
|
+
"DBTrXhmzMbo1bkYAv6ZYodtwvTahhApf7y": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2026-11-01", "source_kind": "self-published", "added": "2026-11-07"},
|
|
726
|
+
"DBrm7F6i8YEVX7STwYfe27RVZD9x1jGRDR": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2026-11-02", "source_kind": "self-published", "added": "2026-11-07"},
|
|
727
|
+
"DCqD9gyq3qumbeEJZKc2Tm2R96JEd9dSDe": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
728
|
+
"DD1h9ojoyAdAGLXaqgqZP3j86AtxZs6jCn": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
729
|
+
"DD2AVpbD5aybXQC9iVRrzVyoszp5UQCRUJ": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2026-11-04", "source_kind": "self-published", "added": "2026-11-07"},
|
|
730
|
+
"DD7W4ddD7wBeMVhJmyPAy9qcDp9ypwbxpD": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
731
|
+
"DE5opaXjFgDhFBqL6tBDxTAQ56zkX6EToX": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
732
|
+
"DEth4cF3doxmbcv9ofH3vm4j2NCozn8DL6": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
733
|
+
"DF4snE9zVmwQ3nMLjLwKyVehoz9ZH7vBKj": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-12-19", "source_kind": "self-published", "added": "2026-11-07"},
|
|
734
|
+
"DF6RgUzMsp7na9PX9rKjUQeXA7HEreBot6": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-12-19", "source_kind": "self-published", "added": "2026-11-07"},
|
|
735
|
+
"DFoQ1qrgDM7D9tAhF7Mahbp6zLv8CzK3XH": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
736
|
+
"DGmd2jn1D7oLXpdpcGkLaGLDMbnRt4qkH3": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
737
|
+
"DHFu8WjwXzHVy9pknMrxdQpePFir2FmiuG": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-12-19", "source_kind": "self-published", "added": "2026-11-07"},
|
|
738
|
+
"DHnhi38hJyTVP6hCtTeH2dkjLU7Acwfdbd": {"name": "Binance reserve wallet (custodied by Ceffu)", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-12-19", "source_kind": "self-published", "added": "2026-11-07"},
|
|
739
|
+
"DJfU2p6woQ9GiBdiXsWZWJnJ9uDdZfSSNC": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
740
|
+
"DKjcdPpQLoVfkZsN4wJr2RFuyFCoAVvoRe": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
741
|
+
"DL7jHHaHHF5z2ap2eVMhZe8fZqVvPdAkEZ": {"name": "Binance reserve wallet (custodied by Ceffu)", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-12-19", "source_kind": "self-published", "added": "2026-11-07"},
|
|
742
|
+
"DLqwvgk27ACJnHPEGxyDfspieHBVYJRZiC": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
743
|
+
"DMURD7AGQwQhVLzz53QzQ8VsmmZShycpaz": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
744
|
+
"DMcrq3jy8MVYTBM9tGCjFZerpKXpW91WXx": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
745
|
+
"DP627ydcvYTgkTGboYh3xQhMpF6ph7F2b9": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-23", "source_kind": "self-published", "added": "2026-11-07"},
|
|
746
|
+
"DPAMdZsSirVBGeDR9sE1LJXkwG6AQSNAMW": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
747
|
+
"DPNqMsW1rotjhCzcbBEXzK3W73DiyzVLYc": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
748
|
+
"DPcqvZT8SbQ2dZKzZe2VXTpaKGzGHqQWoy": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2026-07-20", "source_kind": "self-published", "added": "2026-11-07"},
|
|
749
|
+
"DQ3VG1e7rJCGafx38cUVMez5nVpQPAdvna": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-12-18", "source_kind": "self-published", "added": "2026-11-07"},
|
|
750
|
+
"DQYqMYmQCfL2eDLmMXV5uvCPkN6Vv4KtSx": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
751
|
+
"DQkwksfYrAWAR9tRcqygA5pihmnURozxzo": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
752
|
+
"DQtm9uQ9gKmUDk5Pdg8pocv1aiwgH949YX": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-12-19", "source_kind": "self-published", "added": "2026-11-07"},
|
|
753
|
+
"DT3N6kG5ewsCp6zA7hUhXW7F3dzqsRTre8": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2026-10-22", "source_kind": "self-published", "added": "2026-11-07"},
|
|
754
|
+
"DTGrjpvJXQU2zSpUDsnk9b4PAQW5Pwtg4K": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
755
|
+
"DTSop4ycWkpmDqpncVZ3jwKp3z7B4kW4T3": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
756
|
+
"DTXbttzfvPr4t6CPEAtKKUrHpm9hpfgahu": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
757
|
+
"DTbG3RJRejnM99g8MSnhSbJxJVW8X7YXy9": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
758
|
+
"DU8gPC5mh4KxWJARQRxoESFark2jAguBr5": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
759
|
+
"DUMB7o1jqJvpriAAExmEFi1Y88s5q94fi9": {"name": "Binance reserve wallet", "type": "exchange", "chain": "doge", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
760
|
+
"LXTras9zXwkUvmsAF15hUu86by2UdNLSWk": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
761
|
+
"LZEjckteAtWrugbsy9zU8VHEZ4iUiXo9Nm": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
762
|
+
"LazqPDHyo1nqwaYLc8WSDtG1mRj3anFfbX": {"name": "Binance reserve wallet (custodied by Ceffu)", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
763
|
+
"LbmGksLBwtwRXyxeazCZqKiAHX6cWN2AzN": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
764
|
+
"Le8Lyy1JdYY3JUTbaAUE5utF7aTP8rYRuS": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-12-18", "source_kind": "self-published", "added": "2026-11-07"},
|
|
765
|
+
"LhcTcrMrheBqbthwfit43jkT9qnge5wSs1": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
766
|
+
"LhzEoDXHXASi4hSMxrKeVoSGrED9QsBpPq": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
767
|
+
"LiDY79SxUJfjE6fyqyxXxh9kBpfi4sRoKf": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
768
|
+
"M8T1B2Z97gVdvmfkQcAtYbEepune1tzGua": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
769
|
+
"M9vsPE9LLEgtYduCr2gktfT35uu7eQeDgr": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2026-08-24", "source_kind": "self-published", "added": "2026-11-07"},
|
|
770
|
+
"MB6LrCx4qjEFpU9U1WBrgPrntkfMR5GtCA": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
771
|
+
"MF34ww6PuT3eRh8iTdwawJrqe6fd9QNZ9F": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2026-08-24", "source_kind": "self-published", "added": "2026-11-07"},
|
|
772
|
+
"MFWNdXE9Cz6MjVDcCLaA2MaXbkwRVUe3TZ": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-12-19", "source_kind": "self-published", "added": "2026-11-07"},
|
|
773
|
+
"MGfpYF7kPpoWmVXxSQCmvgb3SedtvDdV47": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2026-02-22", "source_kind": "self-published", "added": "2026-11-07"},
|
|
774
|
+
"MHS3wszxaspPAeJYC7p9fpDp5mwvcaUpZL": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2026-07-20", "source_kind": "self-published", "added": "2026-11-07"},
|
|
775
|
+
"MJnkrsBX2o7Hv1PU3PEbLJACFt6WvdaxRC": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2026-10-22", "source_kind": "self-published", "added": "2026-11-07"},
|
|
776
|
+
"MLj1bgnNs8iAmNxhZNnrymQaAZNqnXXV3E": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
777
|
+
"MNKgpjAqG2fq6WLkpc5mw6mavGNhwCx8tq": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2026-02-22", "source_kind": "self-published", "added": "2026-11-07"},
|
|
778
|
+
"MPYLuihDRvAr1YhoxP2tG29y38myBufRF2": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2026-07-01", "source_kind": "self-published", "added": "2026-11-07"},
|
|
779
|
+
"MQd1fJwqBJvwLuyhr17PhEFx1swiqDbPQS": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
780
|
+
"MRNXY6zo4j1rTqXmpzLaXWuF8sAuLjNUpY": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2026-10-22", "source_kind": "self-published", "added": "2026-11-07"},
|
|
781
|
+
"MS56eJAupdvN5bYo6ythw784qahCpTzZ5V": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
782
|
+
"MSyWrzBejZpuC6h8trjadG4Vk7XRky1Lhq": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2026-08-24", "source_kind": "self-published", "added": "2026-11-07"},
|
|
783
|
+
"MW87w56tHzzBYuHS4tfTQ1aYN7Kz6iuSra": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
784
|
+
"ltc1q32lyrhp9zpww22phqjwwmelta0c8a5q9pnjng2": {"name": "Binance reserve wallet (custodied by Ceffu)", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-10-15", "source_kind": "self-published", "added": "2026-11-07"},
|
|
785
|
+
"ltc1qzphegzz94l3mr9w56yq6845wfcfllmrd89cd69": {"name": "Binance reserve wallet", "type": "exchange", "chain": "ltc", "source": "Binance proof-of-reserves address list (binance.com/bapi/apex/v1/public/apex/market/por/address), fetched 2026-11-07; listed since 2025-12-18", "source_kind": "self-published", "added": "2026-11-07"},
|
|
714
786
|
|
|
715
787
|
"0x9b64203878f24eb0cdf55c8c6fa7d08ba0cf77e5": {"name": "MEXC reserve wallet", "type": "exchange", "source": "MEXC proof-of-reserves audit by Hacken, 'Verified wallets' table (ownership proven by a test transaction from each address); MEXC_PoR_Audit_20261009.pdf, listed at mexc.com/proof-of-reserve/audit-report; snapshot 2026-10-09; listed for Arbitrum", "source_kind": "self-published", "added": "2026-10-23"},
|
|
716
788
|
"0xb86f1061e0d79e8319339d5fdbb187d4e7ad3300": {"name": "MEXC reserve wallet", "type": "exchange", "source": "MEXC proof-of-reserves audit by Hacken, 'Verified wallets' table (ownership proven by a test transaction from each address); MEXC_PoR_Audit_20261009.pdf, listed at mexc.com/proof-of-reserve/audit-report; snapshot 2026-10-09; listed for Arbitrum", "source_kind": "self-published", "added": "2026-10-23"},
|
|
@@ -94,7 +94,7 @@ def resemblance(a: str, b: str, chain: str) -> Optional[str]:
|
|
|
94
94
|
def _history(address: str, chain: str, limit: int) -> List[dict]:
|
|
95
95
|
"""Native and every-token transfers, fakes included — they are the bait."""
|
|
96
96
|
rows = list(chains.transfers(address, chain, limit))
|
|
97
|
-
if chain
|
|
97
|
+
if not chains.is_utxo(chain):
|
|
98
98
|
try:
|
|
99
99
|
rows += chains.transfers(address, chain, limit, asset=_ALL_TOKENS)
|
|
100
100
|
except chains.ChainError:
|
|
@@ -19,7 +19,8 @@ _PLATFORM = {"eth": "ethereum", "bsc": "binance-smart-chain", "polygon": "polygo
|
|
|
19
19
|
"tron": "tron", "sol": "solana", "ton": "the-open-network"}
|
|
20
20
|
_COINGECKO_NATIVE = {"eth": "ethereum", "bsc": "binancecoin", "polygon": "matic-network",
|
|
21
21
|
"arbitrum": "ethereum", "optimism": "ethereum", "base": "ethereum",
|
|
22
|
-
"btc": "bitcoin", "
|
|
22
|
+
"btc": "bitcoin", "ltc": "litecoin", "doge": "dogecoin",
|
|
23
|
+
"tron": "tron", "sol": "solana", "ton": "the-open-network"}
|
|
23
24
|
|
|
24
25
|
_cache: Dict[str, Optional[float]] = {}
|
|
25
26
|
|
|
@@ -112,6 +112,13 @@ def _conn() -> sqlite3.Connection:
|
|
|
112
112
|
c.execute("DELETE FROM fetched WHERE chain='sol'")
|
|
113
113
|
c.execute("PRAGMA user_version=6")
|
|
114
114
|
c.commit()
|
|
115
|
+
if c.execute("PRAGMA user_version").fetchone()[0] < 7:
|
|
116
|
+
# 0.31: equal outputs to one recipient in one Bitcoin transaction were
|
|
117
|
+
# identical rows, and only one was kept; they are one summed row now
|
|
118
|
+
c.execute("DELETE FROM transfers WHERE chain='btc'")
|
|
119
|
+
c.execute("DELETE FROM fetched WHERE chain='btc'")
|
|
120
|
+
c.execute("PRAGMA user_version=7")
|
|
121
|
+
c.commit()
|
|
115
122
|
return c
|
|
116
123
|
|
|
117
124
|
|
|
@@ -60,13 +60,26 @@ def reconcile(address: str, chain: str = "btc", asset: Optional[dict] = None,
|
|
|
60
60
|
ref_in = ref_out = ref_balance = None
|
|
61
61
|
ref_tx = None
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
change = {"received": 0.0, "sent": 0.0}
|
|
64
|
+
read_txs = limit
|
|
65
|
+
if chains.is_utxo(chain) and asset is None:
|
|
66
|
+
read_txs = bitcoin._depth(limit, chain)
|
|
64
67
|
try:
|
|
65
|
-
s = bitcoin.stats(address)
|
|
68
|
+
s = bitcoin.stats(address, chain)
|
|
66
69
|
ref_in, ref_out = s["received"], s["sent"]
|
|
67
70
|
ref_balance, ref_tx = s["balance"], s["tx_count"]
|
|
68
71
|
except bitcoin.BitcoinError as e:
|
|
69
72
|
notes.append(f"chain totals unavailable: {e}")
|
|
73
|
+
if ref_in is not None:
|
|
74
|
+
# change back to itself is in both of the chain's totals and in neither of ours
|
|
75
|
+
try:
|
|
76
|
+
change = bitcoin.change(address, limit, chain)
|
|
77
|
+
except bitcoin.BitcoinError as e:
|
|
78
|
+
notes.append(f"change outputs unavailable: {e}")
|
|
79
|
+
if change["received"] > 0:
|
|
80
|
+
notes.append(f"{change['received']:.8f} came back to this address as change — the "
|
|
81
|
+
"chain counts it as received and as spent; it is added to both computed "
|
|
82
|
+
"totals (to the spent side in this address's share of the inputs)")
|
|
70
83
|
else:
|
|
71
84
|
try:
|
|
72
85
|
ref_balance = chains.balance(address, chain)
|
|
@@ -77,10 +90,10 @@ def reconcile(address: str, chain: str = "btc", asset: Optional[dict] = None,
|
|
|
77
90
|
complete = True
|
|
78
91
|
if ref_tx is not None:
|
|
79
92
|
# rows are per-transfer, a tx can produce several — compare loosely
|
|
80
|
-
if ref_tx >
|
|
93
|
+
if ref_tx > read_txs:
|
|
81
94
|
complete = False
|
|
82
95
|
notes.append(f"address has {ref_tx} transactions on chain but only "
|
|
83
|
-
f"{
|
|
96
|
+
f"{read_txs} were fetched — this is a partial view")
|
|
84
97
|
elif len(rows) >= limit:
|
|
85
98
|
complete = False
|
|
86
99
|
notes.append(f"hit the {limit}-transfer fetch limit — history may be truncated")
|
|
@@ -88,11 +101,11 @@ def reconcile(address: str, chain: str = "btc", asset: Optional[dict] = None,
|
|
|
88
101
|
# --- arithmetic ---------------------------------------------------
|
|
89
102
|
checks = []
|
|
90
103
|
if ref_in is not None:
|
|
91
|
-
ok = _within(got_in, ref_in) or not complete
|
|
92
|
-
checks.append(("received", got_in, ref_in, ok))
|
|
104
|
+
ok = _within(got_in + change["received"], ref_in) or not complete
|
|
105
|
+
checks.append(("received", got_in + change["received"], ref_in, ok))
|
|
93
106
|
if ref_out is not None:
|
|
94
107
|
# compare like with like: what recipients got, plus the fees we paid
|
|
95
|
-
sent_incl_fees = got_out + fees
|
|
108
|
+
sent_incl_fees = got_out + fees + change["sent"]
|
|
96
109
|
ok = _within(sent_incl_fees, ref_out) or not complete
|
|
97
110
|
checks.append(("sent (incl. fees)", sent_incl_fees, ref_out, ok))
|
|
98
111
|
if fees > 0:
|
|
@@ -55,7 +55,7 @@ def _rows(address: str, chain: str, limit: int) -> List[dict]:
|
|
|
55
55
|
Tokens are read in one request and kept only when their contract is the real
|
|
56
56
|
stablecoin's — counterfeit "USDT" sent by address poisoners must not alert."""
|
|
57
57
|
rows = [{**r, "_asset": None} for r in chains.transfers(address, chain, limit, fresh=True)]
|
|
58
|
-
if chain
|
|
58
|
+
if chains.is_utxo(chain):
|
|
59
59
|
return rows
|
|
60
60
|
try:
|
|
61
61
|
tokens = chains.transfers(address, chain, limit, asset=_ALL_TOKENS, fresh=True)
|
|
@@ -217,7 +217,7 @@
|
|
|
217
217
|
<option value="eth">eth</option><option value="bsc">bsc</option>
|
|
218
218
|
<option value="polygon">polygon</option><option value="arbitrum">arbitrum</option>
|
|
219
219
|
<option value="optimism">optimism</option><option value="base">base</option>
|
|
220
|
-
<option value="btc">btc</option><option value="tron">tron</option><option value="sol">sol</option><option value="ton">ton</option>
|
|
220
|
+
<option value="btc">btc</option><option value="ltc">ltc</option><option value="doge">doge</option><option value="tron">tron</option><option value="sol">sol</option><option value="ton">ton</option>
|
|
221
221
|
</select>
|
|
222
222
|
<select id="asset" title="Asset to trace">
|
|
223
223
|
<option value="native">ETH (native)</option>
|
|
@@ -37,6 +37,11 @@ def validate(address: str, chain: str):
|
|
|
37
37
|
elif chain == "btc":
|
|
38
38
|
if not (a.startswith(("1", "3", "bc1", "tb1")) and 25 <= len(a) <= 62):
|
|
39
39
|
return f"'{a[:14]}…' is not a Bitcoin address (expected 1…, 3… or bc1…)."
|
|
40
|
+
elif chains.is_utxo(chain):
|
|
41
|
+
from crypttrace import addresses as _a
|
|
42
|
+
ok, why = _a.validate(a, chain)
|
|
43
|
+
if not ok:
|
|
44
|
+
return f"'{a[:14]}…' is not a {chain.upper()} address: {why}."
|
|
40
45
|
elif chain == "tron":
|
|
41
46
|
if not (a.startswith("T") and len(a) == 34):
|
|
42
47
|
return f"'{a[:14]}…' is not a Tron address (expected T…, 34 chars)."
|
|
@@ -80,7 +85,7 @@ def create_app() -> Flask:
|
|
|
80
85
|
for sym, meta in assets.tokens_for(chain).items():
|
|
81
86
|
opts.append({"value": sym, "label": meta["symbol"]})
|
|
82
87
|
return jsonify({"chain": chain, "assets": opts,
|
|
83
|
-
"tokens_supported": chain
|
|
88
|
+
"tokens_supported": not chains.is_utxo(chain)})
|
|
84
89
|
|
|
85
90
|
@app.route("/api/label")
|
|
86
91
|
def api_label():
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crypttrace
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.31.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
|
|
@@ -42,7 +42,7 @@ Dynamic: license-file
|
|
|
42
42
|
Open-source OSINT toolkit for crypto investigations. Give it a suspicious
|
|
43
43
|
address; it pulls the public on-chain history, labels known entities (exchanges,
|
|
44
44
|
mixers, sanctioned wallets), scores risk, and maps where the funds moved —
|
|
45
|
-
across Ethereum, Bitcoin, Tron, Solana, TON and more.
|
|
45
|
+
across Ethereum, Bitcoin, Tron, Solana, TON, Litecoin, Dogecoin and more.
|
|
46
46
|
|
|
47
47
|
Built for the investigation that actually happens: *someone's crypto was stolen,
|
|
48
48
|
here's the wallet — where did the money go, and where can it still be stopped?*
|
|
@@ -80,7 +80,7 @@ python selftest.py # offline checks, no API keys needed
|
|
|
80
80
|
| | Needed? | How to get it |
|
|
81
81
|
|---|---|---|
|
|
82
82
|
| **EVM chains** (Ethereum, BSC, Polygon, Arbitrum, Optimism, Base) | **Required** | Free key at [etherscan.io/myapikey](https://etherscan.io/myapikey) covers Ethereum, Polygon and Arbitrum. BSC, Optimism and Base need a paid Etherscan plan; on a free key crypttrace says so instead of showing an empty wallet. |
|
|
83
|
-
| **Bitcoin, Tron, Solana, TON** | **Not required** | Work out of the box via public endpoints. |
|
|
83
|
+
| **Bitcoin, Litecoin, Dogecoin, Tron, Solana, TON** | **Not required** | Work out of the box via public endpoints. Dogecoin reads BlockCypher's free tier, about 100 requests an hour, so it reads the latest 200 transactions of an address. |
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
86
|
export ETHERSCAN_API_KEY=xxxx # required for EVM chains only
|
|
@@ -122,6 +122,8 @@ the number either way.
|
|
|
122
122
|
|---|---|---|---|
|
|
123
123
|
| Ethereum, BSC, Polygon, Arbitrum, Optimism, Base | `eth` `bsc` `polygon` `arbitrum` `optimism` `base` | Etherscan v2 (BSC, Optimism, Base: paid plan) | account |
|
|
124
124
|
| Bitcoin | `btc` | mempool.space | UTXO |
|
|
125
|
+
| Litecoin | `ltc` | litecoinspace.org | UTXO |
|
|
126
|
+
| Dogecoin | `doge` | BlockCypher | UTXO |
|
|
125
127
|
| Tron (TRX + USDT-TRC20) | `tron` | TronGrid | account |
|
|
126
128
|
| Solana (SOL + SPL) | `sol` | public JSON-RPC | account |
|
|
127
129
|
| TON (TON + jettons, e.g. USDT) | `ton` | toncenter v3 | account |
|
|
@@ -472,7 +474,7 @@ with an `nfts` column in the CSV. Failed (aborted) NFT transfers are not counted
|
|
|
472
474
|
|
|
473
475
|
### Bitcoin clustering (common-input-ownership)
|
|
474
476
|
|
|
475
|
-
Bitcoin's UTXO model enables the strongest clustering heuristic in blockchain
|
|
477
|
+
Bitcoin's UTXO model (Litecoin's and Dogecoin's too) enables the strongest clustering heuristic in blockchain
|
|
476
478
|
forensics: if several addresses sign the inputs of one transaction, one party
|
|
477
479
|
almost certainly controls all of them. `cluster` surfaces those co-signers,
|
|
478
480
|
turning a single address into a set of wallets belonging to the same owner. A
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
"""Bitcoin fetcher via mempool.space — no API key required.
|
|
2
|
-
|
|
3
|
-
Bitcoin uses the UTXO model, not accounts: a transaction consumes previous
|
|
4
|
-
outputs and creates new ones, so there is no single "from" or "to". To fit the
|
|
5
|
-
same tracing engine used for EVM chains, transactions are normalized into
|
|
6
|
-
directional transfer rows:
|
|
7
|
-
|
|
8
|
-
* if our address signed an input, each output that isn't ours is an outflow
|
|
9
|
-
(outputs back to ourselves are change and are skipped);
|
|
10
|
-
* otherwise, outputs paying us are inflows, attributed to the first input.
|
|
11
|
-
|
|
12
|
-
Bitcoin also enables the strongest clustering heuristic in blockchain forensics:
|
|
13
|
-
common-input-ownership. If several addresses sign inputs of the same
|
|
14
|
-
transaction, one party almost certainly controls all of them.
|
|
15
|
-
"""
|
|
16
|
-
from typing import List, Dict, Optional
|
|
17
|
-
|
|
18
|
-
import requests
|
|
19
|
-
|
|
20
|
-
BASE = "https://mempool.space/api"
|
|
21
|
-
SATS = 100_000_000
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class BitcoinError(RuntimeError):
|
|
25
|
-
pass
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _get(path: str, timeout: int = 30):
|
|
29
|
-
"""Cached, throttled GET against mempool.space."""
|
|
30
|
-
from crypttrace.fetchers import http
|
|
31
|
-
try:
|
|
32
|
-
data = http.request_json(f"{BASE}{path}", timeout=timeout)
|
|
33
|
-
except http.RateLimited as e:
|
|
34
|
-
raise BitcoinError(str(e))
|
|
35
|
-
except requests.RequestException as e:
|
|
36
|
-
raise BitcoinError(f"mempool.space request failed: {e}")
|
|
37
|
-
except ValueError as e:
|
|
38
|
-
raise BitcoinError(f"bad response from mempool.space: {e}")
|
|
39
|
-
if isinstance(data, dict) and "__status__" in data:
|
|
40
|
-
if data["__status__"] == 400:
|
|
41
|
-
raise BitcoinError("Bitcoin address rejected by mempool.space — check it is "
|
|
42
|
-
"exact (BTC addresses are case-sensitive).")
|
|
43
|
-
raise BitcoinError("Address not found on the Bitcoin chain.")
|
|
44
|
-
return data
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def stats(address: str) -> Dict:
|
|
48
|
-
"""Authoritative totals straight from the chain index.
|
|
49
|
-
|
|
50
|
-
These are the numbers our own parsing must agree with — the reference for
|
|
51
|
-
checking that a trace didn't miscount or silently miss history.
|
|
52
|
-
"""
|
|
53
|
-
d = _get(f"/address/{address}")
|
|
54
|
-
cs = d.get("chain_stats", {}) or {}
|
|
55
|
-
ms = d.get("mempool_stats", {}) or {}
|
|
56
|
-
recv = (cs.get("funded_txo_sum", 0) + ms.get("funded_txo_sum", 0)) / SATS
|
|
57
|
-
sent = (cs.get("spent_txo_sum", 0) + ms.get("spent_txo_sum", 0)) / SATS
|
|
58
|
-
return {
|
|
59
|
-
"received": recv,
|
|
60
|
-
"sent": sent,
|
|
61
|
-
"balance": recv - sent,
|
|
62
|
-
"tx_count": (cs.get("tx_count", 0) + ms.get("tx_count", 0)),
|
|
63
|
-
"funded_outputs": (cs.get("funded_txo_count", 0) + ms.get("funded_txo_count", 0)),
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def balance(address: str) -> float:
|
|
68
|
-
return stats(address)["balance"]
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def raw_txs(address: str, max_txs: int = 500) -> List[dict]:
|
|
72
|
-
"""Transactions touching this address, paging back through history.
|
|
73
|
-
|
|
74
|
-
mempool.space returns ~50 per call. A single page is not enough for
|
|
75
|
-
investigations: famous addresses get spammed with dust, which pushes the
|
|
76
|
-
transactions that actually matter out of the recent window. So we follow
|
|
77
|
-
the /txs/chain/:last_txid cursor until we have enough history.
|
|
78
|
-
"""
|
|
79
|
-
out: List[dict] = []
|
|
80
|
-
last: Optional[str] = None
|
|
81
|
-
while len(out) < max_txs:
|
|
82
|
-
path = f"/address/{address}/txs" if last is None \
|
|
83
|
-
else f"/address/{address}/txs/chain/{last}"
|
|
84
|
-
batch = _get(path)
|
|
85
|
-
if not isinstance(batch, list) or not batch:
|
|
86
|
-
break
|
|
87
|
-
out.extend(batch)
|
|
88
|
-
if len(batch) < 25: # last page
|
|
89
|
-
break
|
|
90
|
-
nxt = batch[-1].get("txid")
|
|
91
|
-
if not nxt or nxt == last:
|
|
92
|
-
break
|
|
93
|
-
last = nxt
|
|
94
|
-
return out[:max_txs]
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def _in_addrs(tx: dict) -> List[str]:
|
|
98
|
-
out = []
|
|
99
|
-
for v in tx.get("vin", []) or []:
|
|
100
|
-
a = (v.get("prevout") or {}).get("scriptpubkey_address")
|
|
101
|
-
if a:
|
|
102
|
-
out.append(a)
|
|
103
|
-
return out
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def _in_pairs(tx: dict):
|
|
107
|
-
"""(address, value) for every input — a UTXO tx can be funded by many parties."""
|
|
108
|
-
for v in tx.get("vin", []) or []:
|
|
109
|
-
p = v.get("prevout") or {}
|
|
110
|
-
a = p.get("scriptpubkey_address")
|
|
111
|
-
if a:
|
|
112
|
-
yield a, (p.get("value", 0) or 0) / SATS
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _out_pairs(tx: dict):
|
|
116
|
-
for o in tx.get("vout", []) or []:
|
|
117
|
-
a = o.get("scriptpubkey_address")
|
|
118
|
-
if a:
|
|
119
|
-
yield a, o.get("value", 0) / SATS
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def transfers(address: str, limit: int = 1000) -> List[Dict]:
|
|
123
|
-
"""Normalized {from,to,value,timestamp,hash,symbol} rows.
|
|
124
|
-
|
|
125
|
-
A Bitcoin transaction has no single sender: it can be funded by many inputs
|
|
126
|
-
from different owners and pay many outputs. Attributing a whole transfer to
|
|
127
|
-
the first input address — the naive shortcut — badly misreports
|
|
128
|
-
consolidations, e.g. crediting one wallet with funds a hundred others
|
|
129
|
-
supplied. So value is split in proportion to what each side actually
|
|
130
|
-
contributed or received.
|
|
131
|
-
"""
|
|
132
|
-
me = address
|
|
133
|
-
rows: List[Dict] = []
|
|
134
|
-
for tx in raw_txs(address, max_txs=max(limit, 200)):
|
|
135
|
-
ts = int((tx.get("status") or {}).get("block_time") or 0)
|
|
136
|
-
h = tx.get("txid", "")
|
|
137
|
-
ins = list(_in_pairs(tx))
|
|
138
|
-
outs = list(_out_pairs(tx))
|
|
139
|
-
total_in = sum(v for _, v in ins)
|
|
140
|
-
if total_in <= 0:
|
|
141
|
-
continue
|
|
142
|
-
|
|
143
|
-
if me in (a for a, _ in ins):
|
|
144
|
-
# outgoing: our share of the inputs funds our share of each output
|
|
145
|
-
mine_in = sum(v for a, v in ins if a == me)
|
|
146
|
-
share = mine_in / total_in
|
|
147
|
-
# The chain counts a spent output at full value; the recipient gets
|
|
148
|
-
# less, because the miner fee comes out in between. Carry our share
|
|
149
|
-
# of that fee so totals can be reconciled exactly rather than
|
|
150
|
-
# hidden inside a tolerance.
|
|
151
|
-
fee_share = (total_in - sum(v for _, v in outs)) * share
|
|
152
|
-
for a, v in outs:
|
|
153
|
-
if a == me or v <= 0:
|
|
154
|
-
continue # change back to self
|
|
155
|
-
rows.append({"from": me, "to": a, "value": v * share,
|
|
156
|
-
"timestamp": ts, "hash": h, "symbol": "BTC",
|
|
157
|
-
"fee_share": fee_share})
|
|
158
|
-
else:
|
|
159
|
-
# incoming: credit every funder in proportion to what it put in
|
|
160
|
-
mine_out = sum(v for a, v in outs if a == me)
|
|
161
|
-
if mine_out <= 0:
|
|
162
|
-
continue
|
|
163
|
-
merged: Dict[str, float] = {}
|
|
164
|
-
for a, v in ins:
|
|
165
|
-
merged[a] = merged.get(a, 0.0) + v
|
|
166
|
-
for a, v in merged.items():
|
|
167
|
-
rows.append({"from": a, "to": me, "value": mine_out * (v / total_in),
|
|
168
|
-
"timestamp": ts, "hash": h, "symbol": "BTC"})
|
|
169
|
-
if len(rows) >= limit:
|
|
170
|
-
break
|
|
171
|
-
return rows
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def cluster(address: str) -> List[tuple]:
|
|
175
|
-
"""Common-input-ownership: addresses that co-signed inputs with `address`.
|
|
176
|
-
|
|
177
|
-
Returns [(address, times_seen_together)] — likely the same owner's wallets.
|
|
178
|
-
"""
|
|
179
|
-
peers: Dict[str, int] = {}
|
|
180
|
-
for tx in raw_txs(address):
|
|
181
|
-
ins = _in_addrs(tx)
|
|
182
|
-
if address in ins and len(set(ins)) > 1:
|
|
183
|
-
for a in set(ins):
|
|
184
|
-
if a != address:
|
|
185
|
-
peers[a] = peers.get(a, 0) + 1
|
|
186
|
-
return sorted(peers.items(), key=lambda kv: kv[1], reverse=True)
|
|
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
|