@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.
Files changed (262) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +267 -0
  3. package/dist/{TransportWebUSB-TQ7WZ4LE.mjs → TransportWebUSB-YQMAGJAJ.mjs} +12 -9
  4. package/dist/browser.d.mts +10 -4
  5. package/dist/browser.d.ts +10 -4
  6. package/dist/browser.js +48874 -18336
  7. package/dist/browser.mjs +674 -48
  8. package/dist/chunk-4GRJ5MAW.mjs +152 -0
  9. package/dist/chunk-5D7A3L3W.mjs +717 -0
  10. package/dist/chunk-64AYA5F5.mjs +7834 -0
  11. package/dist/chunk-GMDGB22A.mjs +379 -0
  12. package/dist/chunk-I534WKN7.mjs +328 -0
  13. package/dist/chunk-IBZVA5Y7.mjs +1003 -0
  14. package/dist/chunk-PRRZAWJE.mjs +223 -0
  15. package/dist/{chunk-UJCSKKID.mjs → chunk-XGB3TDIC.mjs} +13 -1
  16. package/dist/chunk-YWGJ77A2.mjs +33806 -0
  17. package/dist/{chunk-6WGN57S2.mjs → chunk-Z3K7W5S3.mjs} +48 -0
  18. package/dist/constants-LHAAUC2T.mjs +51 -0
  19. package/dist/dist-2OGQ7FED.mjs +3957 -0
  20. package/dist/dist-IFHPYLDX.mjs +254 -0
  21. package/dist/fulfillment_proof-ANHVPKTB.mjs +21 -0
  22. package/dist/funding_proof-ICFZ5LHY.mjs +21 -0
  23. package/dist/index-DXh2IGkz.d.ts +24681 -0
  24. package/dist/index-DeE1ZzA4.d.mts +24681 -0
  25. package/dist/index.d.mts +9 -3
  26. package/dist/index.d.ts +9 -3
  27. package/dist/index.js +48676 -17318
  28. package/dist/index.mjs +583 -19
  29. package/dist/interface-Bf7w1PLW.d.mts +679 -0
  30. package/dist/interface-Bf7w1PLW.d.ts +679 -0
  31. package/dist/{noir-DKfEzWy9.d.mts → noir-kzbLVTei.d.mts} +31 -21
  32. package/dist/{noir-DKfEzWy9.d.ts → noir-kzbLVTei.d.ts} +31 -21
  33. package/dist/proofs/halo2.d.mts +151 -0
  34. package/dist/proofs/halo2.d.ts +151 -0
  35. package/dist/proofs/halo2.js +350 -0
  36. package/dist/proofs/halo2.mjs +11 -0
  37. package/dist/proofs/kimchi.d.mts +160 -0
  38. package/dist/proofs/kimchi.d.ts +160 -0
  39. package/dist/proofs/kimchi.js +431 -0
  40. package/dist/proofs/kimchi.mjs +13 -0
  41. package/dist/proofs/noir.d.mts +1 -1
  42. package/dist/proofs/noir.d.ts +1 -1
  43. package/dist/proofs/noir.js +74 -18
  44. package/dist/proofs/noir.mjs +84 -24
  45. package/dist/solana-U3MEGU7W.mjs +280 -0
  46. package/dist/validity_proof-3POXLPNY.mjs +21 -0
  47. package/package.json +54 -21
  48. package/src/adapters/index.ts +41 -0
  49. package/src/adapters/jupiter.ts +571 -0
  50. package/src/adapters/near-intents.ts +135 -0
  51. package/src/advisor/advisor.ts +653 -0
  52. package/src/advisor/index.ts +54 -0
  53. package/src/advisor/tools.ts +303 -0
  54. package/src/advisor/types.ts +164 -0
  55. package/src/chains/ethereum/announcement.ts +536 -0
  56. package/src/chains/ethereum/bnb-optimizations.ts +474 -0
  57. package/src/chains/ethereum/commitment.ts +522 -0
  58. package/src/chains/ethereum/constants.ts +462 -0
  59. package/src/chains/ethereum/deployment.ts +596 -0
  60. package/src/chains/ethereum/gas-estimation.ts +538 -0
  61. package/src/chains/ethereum/index.ts +268 -0
  62. package/src/chains/ethereum/optimizations.ts +614 -0
  63. package/src/chains/ethereum/privacy-adapter.ts +855 -0
  64. package/src/chains/ethereum/registry.ts +584 -0
  65. package/src/chains/ethereum/rpc.ts +905 -0
  66. package/src/chains/ethereum/stealth.ts +491 -0
  67. package/src/chains/ethereum/token.ts +790 -0
  68. package/src/chains/ethereum/transfer.ts +637 -0
  69. package/src/chains/ethereum/types.ts +456 -0
  70. package/src/chains/ethereum/viewing-key.ts +455 -0
  71. package/src/chains/near/commitment.ts +608 -0
  72. package/src/chains/near/constants.ts +284 -0
  73. package/src/chains/near/function-call.ts +871 -0
  74. package/src/chains/near/history.ts +654 -0
  75. package/src/chains/near/implicit-account.ts +840 -0
  76. package/src/chains/near/index.ts +393 -0
  77. package/src/chains/near/native-transfer.ts +658 -0
  78. package/src/chains/near/nep141.ts +775 -0
  79. package/src/chains/near/privacy-adapter.ts +889 -0
  80. package/src/chains/near/resolver.ts +971 -0
  81. package/src/chains/near/rpc.ts +1016 -0
  82. package/src/chains/near/stealth.ts +419 -0
  83. package/src/chains/near/types.ts +317 -0
  84. package/src/chains/near/viewing-key.ts +876 -0
  85. package/src/chains/solana/anchor-transfer.ts +386 -0
  86. package/src/chains/solana/commitment.ts +577 -0
  87. package/src/chains/solana/constants.ts +126 -12
  88. package/src/chains/solana/ephemeral-keys.ts +543 -0
  89. package/src/chains/solana/index.ts +276 -1
  90. package/src/chains/solana/key-derivation.ts +418 -0
  91. package/src/chains/solana/kit-compat.ts +334 -0
  92. package/src/chains/solana/optimizations.ts +560 -0
  93. package/src/chains/solana/privacy-adapter.ts +605 -0
  94. package/src/chains/solana/providers/generic.ts +201 -0
  95. package/src/chains/solana/providers/helius-enhanced-types.ts +336 -0
  96. package/src/chains/solana/providers/helius-enhanced.ts +623 -0
  97. package/src/chains/solana/providers/helius.ts +402 -0
  98. package/src/chains/solana/providers/index.ts +85 -0
  99. package/src/chains/solana/providers/interface.ts +221 -0
  100. package/src/chains/solana/providers/quicknode.ts +409 -0
  101. package/src/chains/solana/providers/triton.ts +426 -0
  102. package/src/chains/solana/providers/webhook.ts +790 -0
  103. package/src/chains/solana/rpc-client.ts +1150 -0
  104. package/src/chains/solana/scan.ts +170 -73
  105. package/src/chains/solana/sol-transfer.ts +732 -0
  106. package/src/chains/solana/spl-transfer.ts +886 -0
  107. package/src/chains/solana/stealth-scanner.ts +703 -0
  108. package/src/chains/solana/sunspot-verifier.ts +453 -0
  109. package/src/chains/solana/transaction-builder.ts +755 -0
  110. package/src/chains/solana/transfer.ts +74 -5
  111. package/src/chains/solana/types.ts +77 -7
  112. package/src/chains/solana/utils.ts +110 -0
  113. package/src/chains/solana/viewing-key.ts +807 -0
  114. package/src/compliance/fireblocks.ts +921 -0
  115. package/src/compliance/index.ts +37 -0
  116. package/src/compliance/range-sas.ts +956 -0
  117. package/src/config/endpoints.ts +100 -0
  118. package/src/crypto.ts +11 -8
  119. package/src/errors.ts +82 -0
  120. package/src/evm/erc4337-relayer.ts +830 -0
  121. package/src/evm/index.ts +47 -0
  122. package/src/fees/calculator.ts +396 -0
  123. package/src/fees/index.ts +87 -0
  124. package/src/fees/near-contract.ts +429 -0
  125. package/src/fees/types.ts +268 -0
  126. package/src/index.ts +785 -1
  127. package/src/intent.ts +6 -3
  128. package/src/logger.ts +324 -0
  129. package/src/network/index.ts +80 -0
  130. package/src/network/proxy.ts +691 -0
  131. package/src/optimizations/index.ts +541 -0
  132. package/src/oracle/types.ts +1 -0
  133. package/src/privacy-backends/arcium-types.ts +727 -0
  134. package/src/privacy-backends/arcium.ts +719 -0
  135. package/src/privacy-backends/combined-privacy.ts +866 -0
  136. package/src/privacy-backends/cspl-token.ts +595 -0
  137. package/src/privacy-backends/cspl-types.ts +512 -0
  138. package/src/privacy-backends/cspl.ts +907 -0
  139. package/src/privacy-backends/health.ts +488 -0
  140. package/src/privacy-backends/inco-types.ts +323 -0
  141. package/src/privacy-backends/inco.ts +616 -0
  142. package/src/privacy-backends/index.ts +336 -0
  143. package/src/privacy-backends/interface.ts +906 -0
  144. package/src/privacy-backends/lru-cache.ts +343 -0
  145. package/src/privacy-backends/magicblock.ts +458 -0
  146. package/src/privacy-backends/mock.ts +258 -0
  147. package/src/privacy-backends/privacycash-types.ts +278 -0
  148. package/src/privacy-backends/privacycash.ts +456 -0
  149. package/src/privacy-backends/private-swap.ts +570 -0
  150. package/src/privacy-backends/rate-limiter.ts +683 -0
  151. package/src/privacy-backends/registry.ts +690 -0
  152. package/src/privacy-backends/router.ts +626 -0
  153. package/src/privacy-backends/shadowwire.ts +449 -0
  154. package/src/privacy-backends/sip-native.ts +256 -0
  155. package/src/privacy-logger.ts +191 -0
  156. package/src/production-safety.ts +373 -0
  157. package/src/proofs/aggregator.ts +1029 -0
  158. package/src/proofs/browser-composer.ts +1150 -0
  159. package/src/proofs/browser.ts +113 -25
  160. package/src/proofs/cache/index.ts +127 -0
  161. package/src/proofs/cache/interface.ts +545 -0
  162. package/src/proofs/cache/key-generator.ts +188 -0
  163. package/src/proofs/cache/lru-cache.ts +481 -0
  164. package/src/proofs/cache/multi-tier-cache.ts +575 -0
  165. package/src/proofs/cache/persistent-cache.ts +788 -0
  166. package/src/proofs/compliance-proof.ts +872 -0
  167. package/src/proofs/composer/base.ts +923 -0
  168. package/src/proofs/composer/index.ts +25 -0
  169. package/src/proofs/composer/interface.ts +518 -0
  170. package/src/proofs/composer/types.ts +383 -0
  171. package/src/proofs/converters/halo2.ts +452 -0
  172. package/src/proofs/converters/index.ts +208 -0
  173. package/src/proofs/converters/interface.ts +363 -0
  174. package/src/proofs/converters/kimchi.ts +462 -0
  175. package/src/proofs/converters/noir.ts +451 -0
  176. package/src/proofs/fallback.ts +888 -0
  177. package/src/proofs/halo2.ts +42 -0
  178. package/src/proofs/index.ts +471 -0
  179. package/src/proofs/interface.ts +13 -0
  180. package/src/proofs/kimchi.ts +42 -0
  181. package/src/proofs/lazy.ts +1004 -0
  182. package/src/proofs/mock.ts +25 -1
  183. package/src/proofs/noir.ts +111 -30
  184. package/src/proofs/orchestrator.ts +960 -0
  185. package/src/proofs/parallel/concurrency.ts +297 -0
  186. package/src/proofs/parallel/dependency-graph.ts +602 -0
  187. package/src/proofs/parallel/executor.ts +420 -0
  188. package/src/proofs/parallel/index.ts +131 -0
  189. package/src/proofs/parallel/interface.ts +685 -0
  190. package/src/proofs/parallel/worker-pool.ts +644 -0
  191. package/src/proofs/providers/halo2.ts +560 -0
  192. package/src/proofs/providers/index.ts +34 -0
  193. package/src/proofs/providers/kimchi.ts +641 -0
  194. package/src/proofs/validator.ts +881 -0
  195. package/src/proofs/verifier.ts +867 -0
  196. package/src/quantum/index.ts +112 -0
  197. package/src/quantum/winternitz-vault.ts +639 -0
  198. package/src/quantum/wots.ts +611 -0
  199. package/src/settlement/backends/direct-chain.ts +1 -0
  200. package/src/settlement/index.ts +9 -0
  201. package/src/settlement/router.ts +732 -46
  202. package/src/solana/index.ts +72 -0
  203. package/src/solana/jito-relayer.ts +687 -0
  204. package/src/solana/noir-verifier-types.ts +430 -0
  205. package/src/solana/noir-verifier.ts +816 -0
  206. package/src/stealth/address-derivation.ts +193 -0
  207. package/src/stealth/ed25519.ts +431 -0
  208. package/src/stealth/index.ts +233 -0
  209. package/src/stealth/meta-address.ts +221 -0
  210. package/src/stealth/secp256k1.ts +368 -0
  211. package/src/stealth/utils.ts +194 -0
  212. package/src/stealth.ts +50 -1504
  213. package/src/surveillance/algorithms/address-reuse.ts +143 -0
  214. package/src/surveillance/algorithms/cluster.ts +247 -0
  215. package/src/surveillance/algorithms/exchange.ts +295 -0
  216. package/src/surveillance/algorithms/temporal.ts +337 -0
  217. package/src/surveillance/analyzer.ts +442 -0
  218. package/src/surveillance/index.ts +64 -0
  219. package/src/surveillance/scoring.ts +372 -0
  220. package/src/surveillance/types.ts +264 -0
  221. package/src/sync/index.ts +106 -0
  222. package/src/sync/manager.ts +504 -0
  223. package/src/sync/mock-provider.ts +318 -0
  224. package/src/sync/oblivious.ts +625 -0
  225. package/src/tokens/index.ts +15 -0
  226. package/src/tokens/registry.ts +301 -0
  227. package/src/utils/deprecation.ts +94 -0
  228. package/src/utils/index.ts +9 -0
  229. package/src/wallet/ethereum/index.ts +68 -0
  230. package/src/wallet/ethereum/metamask-privacy.ts +420 -0
  231. package/src/wallet/ethereum/multi-wallet.ts +646 -0
  232. package/src/wallet/ethereum/privacy-adapter.ts +700 -0
  233. package/src/wallet/ethereum/types.ts +3 -1
  234. package/src/wallet/ethereum/walletconnect-adapter.ts +675 -0
  235. package/src/wallet/hardware/index.ts +10 -0
  236. package/src/wallet/hardware/ledger-privacy.ts +414 -0
  237. package/src/wallet/index.ts +71 -0
  238. package/src/wallet/near/adapter.ts +626 -0
  239. package/src/wallet/near/index.ts +86 -0
  240. package/src/wallet/near/meteor-wallet.ts +1153 -0
  241. package/src/wallet/near/my-near-wallet.ts +790 -0
  242. package/src/wallet/near/wallet-selector.ts +702 -0
  243. package/src/wallet/solana/adapter.ts +6 -4
  244. package/src/wallet/solana/index.ts +13 -0
  245. package/src/wallet/solana/privacy-adapter.ts +567 -0
  246. package/src/wallet/sui/types.ts +6 -4
  247. package/src/zcash/rpc-client.ts +13 -6
  248. package/dist/chunk-3INS3PR5.mjs +0 -884
  249. package/dist/chunk-3OVABDRH.mjs +0 -17096
  250. package/dist/chunk-DLDWZFYC.mjs +0 -1495
  251. package/dist/chunk-E6SZWREQ.mjs +0 -57
  252. package/dist/chunk-G33LB27A.mjs +0 -16166
  253. package/dist/chunk-HGU6HZRC.mjs +0 -231
  254. package/dist/chunk-L2K34JCU.mjs +0 -1496
  255. package/dist/chunk-SN4ZDTVW.mjs +0 -16166
  256. package/dist/constants-VOI7BSLK.mjs +0 -27
  257. package/dist/index-BYZbDjal.d.ts +0 -11390
  258. package/dist/index-CHB3KuOB.d.mts +0 -11859
  259. package/dist/index-CzWPI6Le.d.ts +0 -11859
  260. package/dist/index-xbWjohNq.d.mts +0 -11390
  261. package/dist/solana-5EMCTPTS.mjs +0 -46
  262. 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
+ }