@sip-protocol/sdk 0.7.2 → 0.7.4
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.
- package/LICENSE +21 -0
- package/README.md +267 -0
- package/dist/{TransportWebUSB-TQ7WZ4LE.mjs → TransportWebUSB-YQMAGJAJ.mjs} +12 -9
- package/dist/browser.d.mts +10 -4
- package/dist/browser.d.ts +10 -4
- package/dist/browser.js +48874 -18336
- package/dist/browser.mjs +674 -48
- package/dist/chunk-4GRJ5MAW.mjs +152 -0
- package/dist/chunk-5D7A3L3W.mjs +717 -0
- package/dist/chunk-64AYA5F5.mjs +7834 -0
- package/dist/chunk-GMDGB22A.mjs +379 -0
- package/dist/chunk-I534WKN7.mjs +328 -0
- package/dist/chunk-IBZVA5Y7.mjs +1003 -0
- package/dist/chunk-PRRZAWJE.mjs +223 -0
- package/dist/{chunk-UJCSKKID.mjs → chunk-XGB3TDIC.mjs} +13 -1
- package/dist/chunk-YWGJ77A2.mjs +33806 -0
- package/dist/{chunk-6WGN57S2.mjs → chunk-Z3K7W5S3.mjs} +48 -0
- package/dist/constants-LHAAUC2T.mjs +51 -0
- package/dist/dist-2OGQ7FED.mjs +3957 -0
- package/dist/dist-IFHPYLDX.mjs +254 -0
- package/dist/fulfillment_proof-ANHVPKTB.mjs +21 -0
- package/dist/funding_proof-ICFZ5LHY.mjs +21 -0
- package/dist/index-DXh2IGkz.d.ts +24681 -0
- package/dist/index-DeE1ZzA4.d.mts +24681 -0
- package/dist/index.d.mts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +48676 -17318
- package/dist/index.mjs +583 -19
- package/dist/interface-Bf7w1PLW.d.mts +679 -0
- package/dist/interface-Bf7w1PLW.d.ts +679 -0
- package/dist/{noir-DKfEzWy9.d.mts → noir-kzbLVTei.d.mts} +31 -21
- package/dist/{noir-DKfEzWy9.d.ts → noir-kzbLVTei.d.ts} +31 -21
- package/dist/proofs/halo2.d.mts +151 -0
- package/dist/proofs/halo2.d.ts +151 -0
- package/dist/proofs/halo2.js +350 -0
- package/dist/proofs/halo2.mjs +11 -0
- package/dist/proofs/kimchi.d.mts +160 -0
- package/dist/proofs/kimchi.d.ts +160 -0
- package/dist/proofs/kimchi.js +431 -0
- package/dist/proofs/kimchi.mjs +13 -0
- package/dist/proofs/noir.d.mts +1 -1
- package/dist/proofs/noir.d.ts +1 -1
- package/dist/proofs/noir.js +74 -18
- package/dist/proofs/noir.mjs +84 -24
- package/dist/solana-U3MEGU7W.mjs +280 -0
- package/dist/validity_proof-3POXLPNY.mjs +21 -0
- package/package.json +54 -21
- package/src/adapters/index.ts +41 -0
- package/src/adapters/jupiter.ts +571 -0
- package/src/adapters/near-intents.ts +135 -0
- package/src/advisor/advisor.ts +653 -0
- package/src/advisor/index.ts +54 -0
- package/src/advisor/tools.ts +303 -0
- package/src/advisor/types.ts +164 -0
- package/src/chains/ethereum/announcement.ts +536 -0
- package/src/chains/ethereum/bnb-optimizations.ts +474 -0
- package/src/chains/ethereum/commitment.ts +522 -0
- package/src/chains/ethereum/constants.ts +462 -0
- package/src/chains/ethereum/deployment.ts +596 -0
- package/src/chains/ethereum/gas-estimation.ts +538 -0
- package/src/chains/ethereum/index.ts +268 -0
- package/src/chains/ethereum/optimizations.ts +614 -0
- package/src/chains/ethereum/privacy-adapter.ts +855 -0
- package/src/chains/ethereum/registry.ts +584 -0
- package/src/chains/ethereum/rpc.ts +905 -0
- package/src/chains/ethereum/stealth.ts +491 -0
- package/src/chains/ethereum/token.ts +790 -0
- package/src/chains/ethereum/transfer.ts +637 -0
- package/src/chains/ethereum/types.ts +456 -0
- package/src/chains/ethereum/viewing-key.ts +455 -0
- package/src/chains/near/commitment.ts +608 -0
- package/src/chains/near/constants.ts +284 -0
- package/src/chains/near/function-call.ts +871 -0
- package/src/chains/near/history.ts +654 -0
- package/src/chains/near/implicit-account.ts +840 -0
- package/src/chains/near/index.ts +393 -0
- package/src/chains/near/native-transfer.ts +658 -0
- package/src/chains/near/nep141.ts +775 -0
- package/src/chains/near/privacy-adapter.ts +889 -0
- package/src/chains/near/resolver.ts +971 -0
- package/src/chains/near/rpc.ts +1016 -0
- package/src/chains/near/stealth.ts +419 -0
- package/src/chains/near/types.ts +317 -0
- package/src/chains/near/viewing-key.ts +876 -0
- package/src/chains/solana/anchor-transfer.ts +386 -0
- package/src/chains/solana/commitment.ts +577 -0
- package/src/chains/solana/constants.ts +126 -12
- package/src/chains/solana/ephemeral-keys.ts +543 -0
- package/src/chains/solana/index.ts +276 -1
- package/src/chains/solana/key-derivation.ts +418 -0
- package/src/chains/solana/kit-compat.ts +334 -0
- package/src/chains/solana/optimizations.ts +560 -0
- package/src/chains/solana/privacy-adapter.ts +605 -0
- package/src/chains/solana/providers/generic.ts +201 -0
- package/src/chains/solana/providers/helius-enhanced-types.ts +336 -0
- package/src/chains/solana/providers/helius-enhanced.ts +623 -0
- package/src/chains/solana/providers/helius.ts +402 -0
- package/src/chains/solana/providers/index.ts +85 -0
- package/src/chains/solana/providers/interface.ts +221 -0
- package/src/chains/solana/providers/quicknode.ts +409 -0
- package/src/chains/solana/providers/triton.ts +426 -0
- package/src/chains/solana/providers/webhook.ts +790 -0
- package/src/chains/solana/rpc-client.ts +1150 -0
- package/src/chains/solana/scan.ts +170 -73
- package/src/chains/solana/sol-transfer.ts +732 -0
- package/src/chains/solana/spl-transfer.ts +886 -0
- package/src/chains/solana/stealth-scanner.ts +703 -0
- package/src/chains/solana/sunspot-verifier.ts +453 -0
- package/src/chains/solana/transaction-builder.ts +755 -0
- package/src/chains/solana/transfer.ts +74 -5
- package/src/chains/solana/types.ts +77 -7
- package/src/chains/solana/utils.ts +110 -0
- package/src/chains/solana/viewing-key.ts +807 -0
- package/src/compliance/fireblocks.ts +921 -0
- package/src/compliance/index.ts +37 -0
- package/src/compliance/range-sas.ts +956 -0
- package/src/config/endpoints.ts +100 -0
- package/src/crypto.ts +11 -8
- package/src/errors.ts +82 -0
- package/src/evm/erc4337-relayer.ts +830 -0
- package/src/evm/index.ts +47 -0
- package/src/fees/calculator.ts +396 -0
- package/src/fees/index.ts +87 -0
- package/src/fees/near-contract.ts +429 -0
- package/src/fees/types.ts +268 -0
- package/src/index.ts +785 -1
- package/src/intent.ts +6 -3
- package/src/logger.ts +324 -0
- package/src/network/index.ts +80 -0
- package/src/network/proxy.ts +691 -0
- package/src/optimizations/index.ts +541 -0
- package/src/oracle/types.ts +1 -0
- package/src/privacy-backends/arcium-types.ts +727 -0
- package/src/privacy-backends/arcium.ts +719 -0
- package/src/privacy-backends/combined-privacy.ts +866 -0
- package/src/privacy-backends/cspl-token.ts +595 -0
- package/src/privacy-backends/cspl-types.ts +512 -0
- package/src/privacy-backends/cspl.ts +907 -0
- package/src/privacy-backends/health.ts +488 -0
- package/src/privacy-backends/inco-types.ts +323 -0
- package/src/privacy-backends/inco.ts +616 -0
- package/src/privacy-backends/index.ts +336 -0
- package/src/privacy-backends/interface.ts +906 -0
- package/src/privacy-backends/lru-cache.ts +343 -0
- package/src/privacy-backends/magicblock.ts +458 -0
- package/src/privacy-backends/mock.ts +258 -0
- package/src/privacy-backends/privacycash-types.ts +278 -0
- package/src/privacy-backends/privacycash.ts +456 -0
- package/src/privacy-backends/private-swap.ts +570 -0
- package/src/privacy-backends/rate-limiter.ts +683 -0
- package/src/privacy-backends/registry.ts +690 -0
- package/src/privacy-backends/router.ts +626 -0
- package/src/privacy-backends/shadowwire.ts +449 -0
- package/src/privacy-backends/sip-native.ts +256 -0
- package/src/privacy-logger.ts +191 -0
- package/src/production-safety.ts +373 -0
- package/src/proofs/aggregator.ts +1029 -0
- package/src/proofs/browser-composer.ts +1150 -0
- package/src/proofs/browser.ts +113 -25
- package/src/proofs/cache/index.ts +127 -0
- package/src/proofs/cache/interface.ts +545 -0
- package/src/proofs/cache/key-generator.ts +188 -0
- package/src/proofs/cache/lru-cache.ts +481 -0
- package/src/proofs/cache/multi-tier-cache.ts +575 -0
- package/src/proofs/cache/persistent-cache.ts +788 -0
- package/src/proofs/compliance-proof.ts +872 -0
- package/src/proofs/composer/base.ts +923 -0
- package/src/proofs/composer/index.ts +25 -0
- package/src/proofs/composer/interface.ts +518 -0
- package/src/proofs/composer/types.ts +383 -0
- package/src/proofs/converters/halo2.ts +452 -0
- package/src/proofs/converters/index.ts +208 -0
- package/src/proofs/converters/interface.ts +363 -0
- package/src/proofs/converters/kimchi.ts +462 -0
- package/src/proofs/converters/noir.ts +451 -0
- package/src/proofs/fallback.ts +888 -0
- package/src/proofs/halo2.ts +42 -0
- package/src/proofs/index.ts +471 -0
- package/src/proofs/interface.ts +13 -0
- package/src/proofs/kimchi.ts +42 -0
- package/src/proofs/lazy.ts +1004 -0
- package/src/proofs/mock.ts +25 -1
- package/src/proofs/noir.ts +111 -30
- package/src/proofs/orchestrator.ts +960 -0
- package/src/proofs/parallel/concurrency.ts +297 -0
- package/src/proofs/parallel/dependency-graph.ts +602 -0
- package/src/proofs/parallel/executor.ts +420 -0
- package/src/proofs/parallel/index.ts +131 -0
- package/src/proofs/parallel/interface.ts +685 -0
- package/src/proofs/parallel/worker-pool.ts +644 -0
- package/src/proofs/providers/halo2.ts +560 -0
- package/src/proofs/providers/index.ts +34 -0
- package/src/proofs/providers/kimchi.ts +641 -0
- package/src/proofs/validator.ts +881 -0
- package/src/proofs/verifier.ts +867 -0
- package/src/quantum/index.ts +112 -0
- package/src/quantum/winternitz-vault.ts +639 -0
- package/src/quantum/wots.ts +611 -0
- package/src/settlement/backends/direct-chain.ts +1 -0
- package/src/settlement/index.ts +9 -0
- package/src/settlement/router.ts +732 -46
- package/src/solana/index.ts +72 -0
- package/src/solana/jito-relayer.ts +687 -0
- package/src/solana/noir-verifier-types.ts +430 -0
- package/src/solana/noir-verifier.ts +816 -0
- package/src/stealth/address-derivation.ts +193 -0
- package/src/stealth/ed25519.ts +431 -0
- package/src/stealth/index.ts +233 -0
- package/src/stealth/meta-address.ts +221 -0
- package/src/stealth/secp256k1.ts +368 -0
- package/src/stealth/utils.ts +194 -0
- package/src/stealth.ts +50 -1504
- package/src/surveillance/algorithms/address-reuse.ts +143 -0
- package/src/surveillance/algorithms/cluster.ts +247 -0
- package/src/surveillance/algorithms/exchange.ts +295 -0
- package/src/surveillance/algorithms/temporal.ts +337 -0
- package/src/surveillance/analyzer.ts +442 -0
- package/src/surveillance/index.ts +64 -0
- package/src/surveillance/scoring.ts +372 -0
- package/src/surveillance/types.ts +264 -0
- package/src/sync/index.ts +106 -0
- package/src/sync/manager.ts +504 -0
- package/src/sync/mock-provider.ts +318 -0
- package/src/sync/oblivious.ts +625 -0
- package/src/tokens/index.ts +15 -0
- package/src/tokens/registry.ts +301 -0
- package/src/utils/deprecation.ts +94 -0
- package/src/utils/index.ts +9 -0
- package/src/wallet/ethereum/index.ts +68 -0
- package/src/wallet/ethereum/metamask-privacy.ts +420 -0
- package/src/wallet/ethereum/multi-wallet.ts +646 -0
- package/src/wallet/ethereum/privacy-adapter.ts +700 -0
- package/src/wallet/ethereum/types.ts +3 -1
- package/src/wallet/ethereum/walletconnect-adapter.ts +675 -0
- package/src/wallet/hardware/index.ts +10 -0
- package/src/wallet/hardware/ledger-privacy.ts +414 -0
- package/src/wallet/index.ts +71 -0
- package/src/wallet/near/adapter.ts +626 -0
- package/src/wallet/near/index.ts +86 -0
- package/src/wallet/near/meteor-wallet.ts +1153 -0
- package/src/wallet/near/my-near-wallet.ts +790 -0
- package/src/wallet/near/wallet-selector.ts +702 -0
- package/src/wallet/solana/adapter.ts +6 -4
- package/src/wallet/solana/index.ts +13 -0
- package/src/wallet/solana/privacy-adapter.ts +567 -0
- package/src/wallet/sui/types.ts +6 -4
- package/src/zcash/rpc-client.ts +13 -6
- package/dist/chunk-3INS3PR5.mjs +0 -884
- package/dist/chunk-3OVABDRH.mjs +0 -17096
- package/dist/chunk-DLDWZFYC.mjs +0 -1495
- package/dist/chunk-E6SZWREQ.mjs +0 -57
- package/dist/chunk-G33LB27A.mjs +0 -16166
- package/dist/chunk-HGU6HZRC.mjs +0 -231
- package/dist/chunk-L2K34JCU.mjs +0 -1496
- package/dist/chunk-SN4ZDTVW.mjs +0 -16166
- package/dist/constants-VOI7BSLK.mjs +0 -27
- package/dist/index-BYZbDjal.d.ts +0 -11390
- package/dist/index-CHB3KuOB.d.mts +0 -11859
- package/dist/index-CzWPI6Le.d.ts +0 -11859
- package/dist/index-xbWjohNq.d.mts +0 -11390
- package/dist/solana-5EMCTPTS.mjs +0 -46
- package/dist/solana-Q4NAVBTS.mjs +0 -46
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Address Reuse Detection Algorithm
|
|
3
|
+
*
|
|
4
|
+
* Detects when the same address is used multiple times for receiving
|
|
5
|
+
* or sending transactions, which degrades privacy by creating linkability.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AnalyzableTransaction, AddressReuseResult } from '../types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Maximum score deduction for address reuse (out of 25)
|
|
14
|
+
*/
|
|
15
|
+
const MAX_DEDUCTION = 25
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Deduction per reuse instance
|
|
19
|
+
*/
|
|
20
|
+
const DEDUCTION_PER_REUSE = 2
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Threshold before counting as reuse (first use is free)
|
|
24
|
+
*/
|
|
25
|
+
const REUSE_THRESHOLD = 1
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Analyze address reuse patterns in transaction history
|
|
29
|
+
*
|
|
30
|
+
* @param transactions - Transaction history to analyze
|
|
31
|
+
* @param walletAddress - The wallet being analyzed
|
|
32
|
+
* @returns Address reuse analysis result
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* const result = analyzeAddressReuse(transactions, 'abc123...')
|
|
37
|
+
* console.log(result.totalReuseCount) // 12
|
|
38
|
+
* console.log(result.scoreDeduction) // 24 (capped at 25)
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function analyzeAddressReuse(
|
|
42
|
+
transactions: AnalyzableTransaction[],
|
|
43
|
+
walletAddress: string
|
|
44
|
+
): AddressReuseResult {
|
|
45
|
+
// Track address usage counts
|
|
46
|
+
const receiveAddresses = new Map<string, number>()
|
|
47
|
+
const sendAddresses = new Map<string, number>()
|
|
48
|
+
|
|
49
|
+
for (const tx of transactions) {
|
|
50
|
+
if (!tx.success) continue
|
|
51
|
+
|
|
52
|
+
// Count receives (wallet is recipient)
|
|
53
|
+
if (tx.recipient === walletAddress) {
|
|
54
|
+
const count = receiveAddresses.get(walletAddress) ?? 0
|
|
55
|
+
receiveAddresses.set(walletAddress, count + 1)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Count sends (wallet is sender)
|
|
59
|
+
if (tx.sender === walletAddress) {
|
|
60
|
+
const count = sendAddresses.get(walletAddress) ?? 0
|
|
61
|
+
sendAddresses.set(walletAddress, count + 1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Also track any other addresses the wallet has used
|
|
65
|
+
// (e.g., associated token accounts, derived addresses)
|
|
66
|
+
for (const addr of tx.involvedAddresses) {
|
|
67
|
+
if (addr === walletAddress) continue
|
|
68
|
+
|
|
69
|
+
// If this address appears multiple times with the wallet,
|
|
70
|
+
// it might indicate reuse of derived addresses
|
|
71
|
+
if (tx.sender === walletAddress) {
|
|
72
|
+
const count = sendAddresses.get(addr) ?? 0
|
|
73
|
+
sendAddresses.set(addr, count + 1)
|
|
74
|
+
}
|
|
75
|
+
if (tx.recipient === walletAddress) {
|
|
76
|
+
const count = receiveAddresses.get(addr) ?? 0
|
|
77
|
+
receiveAddresses.set(addr, count + 1)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Calculate reuse counts
|
|
83
|
+
let receiveReuseCount = 0
|
|
84
|
+
let sendReuseCount = 0
|
|
85
|
+
const reusedAddresses: AddressReuseResult['reusedAddresses'] = []
|
|
86
|
+
|
|
87
|
+
// Process receive addresses
|
|
88
|
+
for (const [address, count] of Array.from(receiveAddresses.entries())) {
|
|
89
|
+
if (count > REUSE_THRESHOLD) {
|
|
90
|
+
const reuseCount = count - REUSE_THRESHOLD
|
|
91
|
+
receiveReuseCount += reuseCount
|
|
92
|
+
|
|
93
|
+
const existing = reusedAddresses.find((r) => r.address === address)
|
|
94
|
+
if (existing) {
|
|
95
|
+
existing.useCount = Math.max(existing.useCount, count)
|
|
96
|
+
existing.type = 'both'
|
|
97
|
+
} else {
|
|
98
|
+
reusedAddresses.push({
|
|
99
|
+
address,
|
|
100
|
+
useCount: count,
|
|
101
|
+
type: 'receive',
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Process send addresses
|
|
108
|
+
for (const [address, count] of Array.from(sendAddresses.entries())) {
|
|
109
|
+
if (count > REUSE_THRESHOLD) {
|
|
110
|
+
const reuseCount = count - REUSE_THRESHOLD
|
|
111
|
+
sendReuseCount += reuseCount
|
|
112
|
+
|
|
113
|
+
const existing = reusedAddresses.find((r) => r.address === address)
|
|
114
|
+
if (existing) {
|
|
115
|
+
existing.useCount = Math.max(existing.useCount, count)
|
|
116
|
+
existing.type = 'both'
|
|
117
|
+
} else {
|
|
118
|
+
reusedAddresses.push({
|
|
119
|
+
address,
|
|
120
|
+
useCount: count,
|
|
121
|
+
type: 'send',
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const totalReuseCount = receiveReuseCount + sendReuseCount
|
|
128
|
+
|
|
129
|
+
// Calculate score deduction
|
|
130
|
+
const rawDeduction = totalReuseCount * DEDUCTION_PER_REUSE
|
|
131
|
+
const scoreDeduction = Math.min(rawDeduction, MAX_DEDUCTION)
|
|
132
|
+
|
|
133
|
+
// Sort reused addresses by use count (most reused first)
|
|
134
|
+
reusedAddresses.sort((a, b) => b.useCount - a.useCount)
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
receiveReuseCount,
|
|
138
|
+
sendReuseCount,
|
|
139
|
+
totalReuseCount,
|
|
140
|
+
scoreDeduction,
|
|
141
|
+
reusedAddresses: reusedAddresses.slice(0, 10), // Top 10 most reused
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cluster Detection Algorithm (Common Input Ownership Heuristic)
|
|
3
|
+
*
|
|
4
|
+
* Identifies addresses that are likely owned by the same entity
|
|
5
|
+
* by analyzing transaction patterns:
|
|
6
|
+
*
|
|
7
|
+
* 1. Common Input Heuristic: Multiple inputs in same tx = same owner
|
|
8
|
+
* 2. Change Address Detection: Change outputs likely go to owner
|
|
9
|
+
* 3. Consolidation Patterns: Merging funds indicates ownership
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { AnalyzableTransaction, ClusterResult } from '../types'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Maximum score deduction for cluster exposure (out of 25)
|
|
18
|
+
*/
|
|
19
|
+
const MAX_DEDUCTION = 25
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Deduction per linked address
|
|
23
|
+
*/
|
|
24
|
+
const DEDUCTION_PER_LINK = 3
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Minimum transactions between addresses to consider linked
|
|
28
|
+
*/
|
|
29
|
+
const MIN_LINK_THRESHOLD = 2
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Union-Find data structure for efficient cluster management
|
|
33
|
+
*/
|
|
34
|
+
class UnionFind {
|
|
35
|
+
private parent: Map<string, string>
|
|
36
|
+
private rank: Map<string, number>
|
|
37
|
+
|
|
38
|
+
constructor() {
|
|
39
|
+
this.parent = new Map()
|
|
40
|
+
this.rank = new Map()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
find(x: string): string {
|
|
44
|
+
if (!this.parent.has(x)) {
|
|
45
|
+
this.parent.set(x, x)
|
|
46
|
+
this.rank.set(x, 0)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (this.parent.get(x) !== x) {
|
|
50
|
+
this.parent.set(x, this.find(this.parent.get(x)!))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return this.parent.get(x)!
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
union(x: string, y: string): void {
|
|
57
|
+
const rootX = this.find(x)
|
|
58
|
+
const rootY = this.find(y)
|
|
59
|
+
|
|
60
|
+
if (rootX === rootY) return
|
|
61
|
+
|
|
62
|
+
const rankX = this.rank.get(rootX) ?? 0
|
|
63
|
+
const rankY = this.rank.get(rootY) ?? 0
|
|
64
|
+
|
|
65
|
+
if (rankX < rankY) {
|
|
66
|
+
this.parent.set(rootX, rootY)
|
|
67
|
+
} else if (rankX > rankY) {
|
|
68
|
+
this.parent.set(rootY, rootX)
|
|
69
|
+
} else {
|
|
70
|
+
this.parent.set(rootY, rootX)
|
|
71
|
+
this.rank.set(rootX, rankX + 1)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getClusters(): Map<string, string[]> {
|
|
76
|
+
const clusters = new Map<string, string[]>()
|
|
77
|
+
|
|
78
|
+
for (const addr of Array.from(this.parent.keys())) {
|
|
79
|
+
const root = this.find(addr)
|
|
80
|
+
if (!clusters.has(root)) {
|
|
81
|
+
clusters.set(root, [])
|
|
82
|
+
}
|
|
83
|
+
clusters.get(root)!.push(addr)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return clusters
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Detect address clusters using Common Input Ownership Heuristic
|
|
92
|
+
*
|
|
93
|
+
* @param transactions - Transaction history to analyze
|
|
94
|
+
* @param walletAddress - The wallet being analyzed
|
|
95
|
+
* @returns Cluster detection result
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* const result = detectClusters(transactions, 'abc123...')
|
|
100
|
+
* console.log(result.linkedAddressCount) // 5
|
|
101
|
+
* console.log(result.clusters[0].linkType) // 'common-input'
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export function detectClusters(
|
|
105
|
+
transactions: AnalyzableTransaction[],
|
|
106
|
+
walletAddress: string
|
|
107
|
+
): ClusterResult {
|
|
108
|
+
const uf = new UnionFind()
|
|
109
|
+
const linkCounts = new Map<string, number>()
|
|
110
|
+
const linkTypes = new Map<string, 'common-input' | 'change-address' | 'consolidation'>()
|
|
111
|
+
const txCountPerPair = new Map<string, number>()
|
|
112
|
+
|
|
113
|
+
// Always include the wallet address
|
|
114
|
+
uf.find(walletAddress)
|
|
115
|
+
|
|
116
|
+
for (const tx of transactions) {
|
|
117
|
+
if (!tx.success) continue
|
|
118
|
+
|
|
119
|
+
const involvedWithWallet = tx.involvedAddresses.filter(
|
|
120
|
+
(addr) => addr !== walletAddress
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
// Common Input Heuristic: If wallet sends with other inputs,
|
|
124
|
+
// those inputs are likely controlled by same entity
|
|
125
|
+
if (tx.sender === walletAddress && involvedWithWallet.length > 0) {
|
|
126
|
+
for (const addr of involvedWithWallet) {
|
|
127
|
+
// Count how many times we see this relationship
|
|
128
|
+
const pairKey = [walletAddress, addr].sort().join(':')
|
|
129
|
+
const count = (txCountPerPair.get(pairKey) ?? 0) + 1
|
|
130
|
+
txCountPerPair.set(pairKey, count)
|
|
131
|
+
|
|
132
|
+
if (count >= MIN_LINK_THRESHOLD) {
|
|
133
|
+
uf.union(walletAddress, addr)
|
|
134
|
+
linkCounts.set(addr, count)
|
|
135
|
+
linkTypes.set(addr, 'common-input')
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Change Address Detection: Small outputs after a large tx
|
|
141
|
+
// often go to change addresses owned by the sender
|
|
142
|
+
if (tx.sender === walletAddress && tx.recipient !== walletAddress) {
|
|
143
|
+
// Look for other outputs in the same transaction to the wallet
|
|
144
|
+
// This is a simplified heuristic
|
|
145
|
+
const otherRecipients = involvedWithWallet.filter(
|
|
146
|
+
(addr) => addr !== tx.recipient
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
for (const addr of otherRecipients) {
|
|
150
|
+
const pairKey = [walletAddress, addr].sort().join(':')
|
|
151
|
+
const count = (txCountPerPair.get(pairKey) ?? 0) + 1
|
|
152
|
+
txCountPerPair.set(pairKey, count)
|
|
153
|
+
|
|
154
|
+
if (count >= MIN_LINK_THRESHOLD) {
|
|
155
|
+
uf.union(walletAddress, addr)
|
|
156
|
+
if (!linkTypes.has(addr)) {
|
|
157
|
+
linkTypes.set(addr, 'change-address')
|
|
158
|
+
}
|
|
159
|
+
linkCounts.set(addr, count)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Consolidation Pattern: Multiple inputs merged into one output
|
|
165
|
+
// All inputs likely owned by same entity
|
|
166
|
+
if (tx.recipient === walletAddress && involvedWithWallet.length > 1) {
|
|
167
|
+
// Multiple addresses sent to this wallet in same tx = consolidation
|
|
168
|
+
for (let i = 0; i < involvedWithWallet.length; i++) {
|
|
169
|
+
for (let j = i + 1; j < involvedWithWallet.length; j++) {
|
|
170
|
+
const addr1 = involvedWithWallet[i]
|
|
171
|
+
const addr2 = involvedWithWallet[j]
|
|
172
|
+
const pairKey = [addr1, addr2].sort().join(':')
|
|
173
|
+
const count = (txCountPerPair.get(pairKey) ?? 0) + 1
|
|
174
|
+
txCountPerPair.set(pairKey, count)
|
|
175
|
+
|
|
176
|
+
if (count >= MIN_LINK_THRESHOLD) {
|
|
177
|
+
uf.union(addr1, addr2)
|
|
178
|
+
linkTypes.set(addr1, 'consolidation')
|
|
179
|
+
linkTypes.set(addr2, 'consolidation')
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Also link to wallet
|
|
184
|
+
const pairKey = [walletAddress, involvedWithWallet[i]].sort().join(':')
|
|
185
|
+
const count = (txCountPerPair.get(pairKey) ?? 0) + 1
|
|
186
|
+
txCountPerPair.set(pairKey, count)
|
|
187
|
+
|
|
188
|
+
if (count >= MIN_LINK_THRESHOLD) {
|
|
189
|
+
uf.union(walletAddress, involvedWithWallet[i])
|
|
190
|
+
linkCounts.set(involvedWithWallet[i], count)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Get clusters containing the wallet
|
|
197
|
+
const allClusters = uf.getClusters()
|
|
198
|
+
const walletRoot = uf.find(walletAddress)
|
|
199
|
+
const walletCluster = allClusters.get(walletRoot) ?? [walletAddress]
|
|
200
|
+
|
|
201
|
+
// Count linked addresses (excluding the wallet itself)
|
|
202
|
+
const linkedAddresses = walletCluster.filter((addr) => addr !== walletAddress)
|
|
203
|
+
const linkedAddressCount = linkedAddresses.length
|
|
204
|
+
|
|
205
|
+
// Calculate confidence based on transaction counts
|
|
206
|
+
const totalLinkTxs = Array.from(linkCounts.values()).reduce((a, b) => a + b, 0)
|
|
207
|
+
const confidence = Math.min(totalLinkTxs / (linkedAddressCount * 5), 1)
|
|
208
|
+
|
|
209
|
+
// Calculate score deduction
|
|
210
|
+
const rawDeduction = linkedAddressCount * DEDUCTION_PER_LINK
|
|
211
|
+
const scoreDeduction = Math.min(rawDeduction, MAX_DEDUCTION)
|
|
212
|
+
|
|
213
|
+
// Build cluster details
|
|
214
|
+
const clusters: ClusterResult['clusters'] = []
|
|
215
|
+
|
|
216
|
+
if (linkedAddressCount > 0) {
|
|
217
|
+
// Group by link type
|
|
218
|
+
const byType = new Map<string, string[]>()
|
|
219
|
+
for (const addr of linkedAddresses) {
|
|
220
|
+
const type = linkTypes.get(addr) ?? 'common-input'
|
|
221
|
+
if (!byType.has(type)) {
|
|
222
|
+
byType.set(type, [])
|
|
223
|
+
}
|
|
224
|
+
byType.get(type)!.push(addr)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const [type, addresses] of Array.from(byType.entries())) {
|
|
228
|
+
const txCount = addresses.reduce(
|
|
229
|
+
(sum, addr) => sum + (linkCounts.get(addr) ?? 0),
|
|
230
|
+
0
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
clusters.push({
|
|
234
|
+
addresses: [walletAddress, ...addresses],
|
|
235
|
+
linkType: type as 'common-input' | 'change-address' | 'consolidation',
|
|
236
|
+
transactionCount: txCount,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
linkedAddressCount,
|
|
243
|
+
confidence,
|
|
244
|
+
scoreDeduction,
|
|
245
|
+
clusters,
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exchange Exposure Detection Algorithm
|
|
3
|
+
*
|
|
4
|
+
* Detects interactions with known exchange addresses to identify
|
|
5
|
+
* KYC exposure points. Deposits to centralized exchanges are
|
|
6
|
+
* particularly privacy-degrading as they link on-chain activity
|
|
7
|
+
* to verified identities.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
AnalyzableTransaction,
|
|
14
|
+
ExchangeExposureResult,
|
|
15
|
+
KnownExchange,
|
|
16
|
+
} from '../types'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Maximum score deduction for exchange exposure (out of 20)
|
|
20
|
+
*/
|
|
21
|
+
const MAX_DEDUCTION = 20
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Deduction per unique CEX (KYC required)
|
|
25
|
+
*/
|
|
26
|
+
const DEDUCTION_PER_CEX = 8
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Deduction per unique DEX (no KYC but still traceable)
|
|
30
|
+
*/
|
|
31
|
+
const DEDUCTION_PER_DEX = 2
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Known Solana exchange addresses
|
|
35
|
+
* Sources: Arkham Intelligence, public documentation
|
|
36
|
+
*/
|
|
37
|
+
export const KNOWN_EXCHANGES: KnownExchange[] = [
|
|
38
|
+
// Centralized Exchanges (KYC Required)
|
|
39
|
+
{
|
|
40
|
+
name: 'Binance',
|
|
41
|
+
addresses: [
|
|
42
|
+
'5tzFkiKscXHK5ZXCGbXZxdw7gTjjD1mBwuoFbhUvuAi9',
|
|
43
|
+
'9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM',
|
|
44
|
+
'AC5RDfQFmDS1deWZos921JfqscXdByf8BKHs5ACWjtW2',
|
|
45
|
+
],
|
|
46
|
+
type: 'cex',
|
|
47
|
+
kycRequired: true,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'Coinbase',
|
|
51
|
+
addresses: [
|
|
52
|
+
'H8sMJSCQxfKiFTCfDR3DUMLPwcRbM61LGFJ8N4dK3WjS',
|
|
53
|
+
'2ojv9BAiHUrvsm9gxDe7fJSzbNZSJcxZvf8dqmWGHG8S',
|
|
54
|
+
'GJRs4FwHtemZ5ZE9x3FNvJ8TMwitKTh21yxdRPqn7npE',
|
|
55
|
+
],
|
|
56
|
+
type: 'cex',
|
|
57
|
+
kycRequired: true,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'Kraken',
|
|
61
|
+
addresses: [
|
|
62
|
+
'krakenmRKej41L9sX8N8Z2mhjZ8UpVHHBMzkKzfBh54',
|
|
63
|
+
],
|
|
64
|
+
type: 'cex',
|
|
65
|
+
kycRequired: true,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'FTX (Defunct)',
|
|
69
|
+
addresses: [
|
|
70
|
+
'FTXkd8cjuYGRLzPVdvqxNxNNNYBfFPPjrF3vW2Yq8p7',
|
|
71
|
+
],
|
|
72
|
+
type: 'cex',
|
|
73
|
+
kycRequired: true,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'KuCoin',
|
|
77
|
+
addresses: [
|
|
78
|
+
'BmFdpraQhkiDQE6SnfG5omcA1VwzqfXrwtNYBwWTymy6',
|
|
79
|
+
],
|
|
80
|
+
type: 'cex',
|
|
81
|
+
kycRequired: true,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'OKX',
|
|
85
|
+
addresses: [
|
|
86
|
+
'GGztQqQ6pCPaJQnNpXBgELr5cs3WwDakRbh1iEMzjgSJ',
|
|
87
|
+
],
|
|
88
|
+
type: 'cex',
|
|
89
|
+
kycRequired: true,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'Bybit',
|
|
93
|
+
addresses: [
|
|
94
|
+
'AC5RDfQFmDS1deWZos921JfqscXdByf8BKHs5ACWjtW3',
|
|
95
|
+
],
|
|
96
|
+
type: 'cex',
|
|
97
|
+
kycRequired: true,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'Gate.io',
|
|
101
|
+
addresses: [
|
|
102
|
+
'u6PJ8DtQuPFnfmwHbGFULQ4u4EgjDiyYKjVEsynXq2w',
|
|
103
|
+
],
|
|
104
|
+
type: 'cex',
|
|
105
|
+
kycRequired: true,
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// Decentralized Exchanges (No KYC but traceable)
|
|
109
|
+
{
|
|
110
|
+
name: 'Jupiter',
|
|
111
|
+
addresses: [
|
|
112
|
+
'JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4',
|
|
113
|
+
'JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB',
|
|
114
|
+
],
|
|
115
|
+
type: 'dex',
|
|
116
|
+
kycRequired: false,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'Raydium',
|
|
120
|
+
addresses: [
|
|
121
|
+
'675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8',
|
|
122
|
+
'5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1',
|
|
123
|
+
],
|
|
124
|
+
type: 'dex',
|
|
125
|
+
kycRequired: false,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'Orca',
|
|
129
|
+
addresses: [
|
|
130
|
+
'9W959DqEETiGZocYWCQPaJ6sBmUzgfxXfqGeTEdp3aQP',
|
|
131
|
+
'whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc',
|
|
132
|
+
],
|
|
133
|
+
type: 'dex',
|
|
134
|
+
kycRequired: false,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'Marinade',
|
|
138
|
+
addresses: [
|
|
139
|
+
'MarBmsSgKXdrN1egZf5sqe1TMai9K1rChYNDJgjq7aD',
|
|
140
|
+
],
|
|
141
|
+
type: 'dex',
|
|
142
|
+
kycRequired: false,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'Phantom Swap',
|
|
146
|
+
addresses: [
|
|
147
|
+
'PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY',
|
|
148
|
+
],
|
|
149
|
+
type: 'dex',
|
|
150
|
+
kycRequired: false,
|
|
151
|
+
},
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Build address lookup map for efficient detection
|
|
156
|
+
*/
|
|
157
|
+
function buildExchangeLookup(
|
|
158
|
+
exchanges: KnownExchange[]
|
|
159
|
+
): Map<string, KnownExchange> {
|
|
160
|
+
const lookup = new Map<string, KnownExchange>()
|
|
161
|
+
|
|
162
|
+
for (const exchange of exchanges) {
|
|
163
|
+
for (const address of exchange.addresses) {
|
|
164
|
+
lookup.set(address, exchange)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return lookup
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Detect exchange interactions in transaction history
|
|
173
|
+
*
|
|
174
|
+
* @param transactions - Transaction history to analyze
|
|
175
|
+
* @param walletAddress - The wallet being analyzed
|
|
176
|
+
* @param customExchanges - Optional custom exchange list
|
|
177
|
+
* @returns Exchange exposure analysis result
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```typescript
|
|
181
|
+
* const result = detectExchangeExposure(transactions, 'abc123...')
|
|
182
|
+
* console.log(result.exchangeCount) // 2
|
|
183
|
+
* console.log(result.exchanges[0].name) // 'Binance'
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
export function detectExchangeExposure(
|
|
187
|
+
transactions: AnalyzableTransaction[],
|
|
188
|
+
walletAddress: string,
|
|
189
|
+
customExchanges?: KnownExchange[]
|
|
190
|
+
): ExchangeExposureResult {
|
|
191
|
+
const exchanges = customExchanges
|
|
192
|
+
? [...KNOWN_EXCHANGES, ...customExchanges]
|
|
193
|
+
: KNOWN_EXCHANGES
|
|
194
|
+
|
|
195
|
+
const lookup = buildExchangeLookup(exchanges)
|
|
196
|
+
|
|
197
|
+
// Track interactions per exchange
|
|
198
|
+
const exchangeStats = new Map<
|
|
199
|
+
string,
|
|
200
|
+
{
|
|
201
|
+
exchange: KnownExchange
|
|
202
|
+
deposits: number
|
|
203
|
+
withdrawals: number
|
|
204
|
+
firstInteraction: number
|
|
205
|
+
lastInteraction: number
|
|
206
|
+
}
|
|
207
|
+
>()
|
|
208
|
+
|
|
209
|
+
for (const tx of transactions) {
|
|
210
|
+
if (!tx.success) continue
|
|
211
|
+
|
|
212
|
+
// Check all involved addresses
|
|
213
|
+
for (const addr of tx.involvedAddresses) {
|
|
214
|
+
const exchange = lookup.get(addr)
|
|
215
|
+
if (!exchange) continue
|
|
216
|
+
|
|
217
|
+
// Initialize stats if first interaction
|
|
218
|
+
if (!exchangeStats.has(exchange.name)) {
|
|
219
|
+
exchangeStats.set(exchange.name, {
|
|
220
|
+
exchange,
|
|
221
|
+
deposits: 0,
|
|
222
|
+
withdrawals: 0,
|
|
223
|
+
firstInteraction: tx.timestamp,
|
|
224
|
+
lastInteraction: tx.timestamp,
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const stats = exchangeStats.get(exchange.name)!
|
|
229
|
+
|
|
230
|
+
// Update timestamps
|
|
231
|
+
stats.firstInteraction = Math.min(stats.firstInteraction, tx.timestamp)
|
|
232
|
+
stats.lastInteraction = Math.max(stats.lastInteraction, tx.timestamp)
|
|
233
|
+
|
|
234
|
+
// Determine deposit vs withdrawal
|
|
235
|
+
if (tx.sender === walletAddress && tx.recipient === addr) {
|
|
236
|
+
// Wallet sent TO exchange = deposit
|
|
237
|
+
stats.deposits++
|
|
238
|
+
} else if (tx.sender === addr && tx.recipient === walletAddress) {
|
|
239
|
+
// Exchange sent TO wallet = withdrawal
|
|
240
|
+
stats.withdrawals++
|
|
241
|
+
} else if (tx.sender === walletAddress) {
|
|
242
|
+
// Wallet interacted with exchange (could be swap)
|
|
243
|
+
stats.deposits++
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Calculate totals
|
|
249
|
+
let totalDeposits = 0
|
|
250
|
+
let totalWithdrawals = 0
|
|
251
|
+
let cexCount = 0
|
|
252
|
+
let dexCount = 0
|
|
253
|
+
|
|
254
|
+
const exchangeResults: ExchangeExposureResult['exchanges'] = []
|
|
255
|
+
|
|
256
|
+
for (const [name, stats] of Array.from(exchangeStats.entries())) {
|
|
257
|
+
totalDeposits += stats.deposits
|
|
258
|
+
totalWithdrawals += stats.withdrawals
|
|
259
|
+
|
|
260
|
+
if (stats.exchange.type === 'cex') {
|
|
261
|
+
cexCount++
|
|
262
|
+
} else {
|
|
263
|
+
dexCount++
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
exchangeResults.push({
|
|
267
|
+
name,
|
|
268
|
+
type: stats.exchange.type,
|
|
269
|
+
kycRequired: stats.exchange.kycRequired,
|
|
270
|
+
deposits: stats.deposits,
|
|
271
|
+
withdrawals: stats.withdrawals,
|
|
272
|
+
firstInteraction: stats.firstInteraction,
|
|
273
|
+
lastInteraction: stats.lastInteraction,
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Sort by interaction count (most active first)
|
|
278
|
+
exchangeResults.sort(
|
|
279
|
+
(a, b) => b.deposits + b.withdrawals - (a.deposits + a.withdrawals)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
// Calculate score deduction
|
|
283
|
+
const cexDeduction = cexCount * DEDUCTION_PER_CEX
|
|
284
|
+
const dexDeduction = dexCount * DEDUCTION_PER_DEX
|
|
285
|
+
const rawDeduction = cexDeduction + dexDeduction
|
|
286
|
+
const scoreDeduction = Math.min(rawDeduction, MAX_DEDUCTION)
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
exchangeCount: exchangeStats.size,
|
|
290
|
+
depositCount: totalDeposits,
|
|
291
|
+
withdrawalCount: totalWithdrawals,
|
|
292
|
+
scoreDeduction,
|
|
293
|
+
exchanges: exchangeResults,
|
|
294
|
+
}
|
|
295
|
+
}
|