@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,971 @@
1
+ /**
2
+ * NEAR Stealth Address Resolver
3
+ *
4
+ * Scans NEAR blockchain for stealth address announcements and identifies
5
+ * addresses belonging to a user's viewing key for wallet balance discovery.
6
+ *
7
+ * ## Architecture
8
+ *
9
+ * ```
10
+ * NEAR Blockchain
11
+ * │
12
+ * ▼ RPC / Indexer
13
+ * Transaction Logs
14
+ * │
15
+ * ▼ Parse SIP: prefixed memos
16
+ * Announcements
17
+ * │
18
+ * ▼ Check against viewing keys
19
+ * Detected Payments
20
+ * ```
21
+ *
22
+ * ## Features
23
+ *
24
+ * - Historical scanning with pagination
25
+ * - Real-time scanning placeholder (WebSocket planned)
26
+ * - Batch scanning for multiple recipients
27
+ * - View tag filtering for efficient scanning
28
+ * - Announcement caching layer
29
+ *
30
+ * @module chains/near/resolver
31
+ */
32
+
33
+ import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
34
+ import type { HexString, StealthAddress } from '@sip-protocol/types'
35
+ import { ValidationError } from '../../errors'
36
+ import { isValidHex } from '../../validation'
37
+ import { checkNEARStealthAddress, implicitAccountToEd25519PublicKey } from './stealth'
38
+ import { parseAnnouncement, type NEARAnnouncement } from './types'
39
+ import {
40
+ SIP_MEMO_PREFIX,
41
+ VIEW_TAG_MIN,
42
+ VIEW_TAG_MAX,
43
+ isImplicitAccount,
44
+ } from './constants'
45
+ import type { NEARViewingKey } from './viewing-key'
46
+
47
+ // ─── Types ────────────────────────────────────────────────────────────────────
48
+
49
+ /**
50
+ * A recipient to scan for (viewing + spending key pair)
51
+ */
52
+ export interface NEARScanRecipient {
53
+ /**
54
+ * Viewing private key (hex)
55
+ * @security SENSITIVE - enables scanning for payments
56
+ */
57
+ viewingPrivateKey: HexString
58
+
59
+ /**
60
+ * Spending private key (hex)
61
+ * @security SENSITIVE - required for DKSAP shared secret computation
62
+ */
63
+ spendingPrivateKey: HexString
64
+
65
+ /**
66
+ * Optional label for this recipient
67
+ */
68
+ label?: string
69
+ }
70
+
71
+ /**
72
+ * Options for the NEAR stealth scanner
73
+ */
74
+ export interface NEARStealthScannerOptions {
75
+ /**
76
+ * NEAR RPC URL
77
+ */
78
+ rpcUrl: string
79
+
80
+ /**
81
+ * Optional network for explorer links
82
+ * @default 'mainnet'
83
+ */
84
+ network?: 'mainnet' | 'testnet'
85
+
86
+ /**
87
+ * Maximum results per scan batch
88
+ * @default 100
89
+ */
90
+ batchSize?: number
91
+
92
+ /**
93
+ * Enable view tag filtering for efficient scanning
94
+ * @default true
95
+ */
96
+ useViewTagFilter?: boolean
97
+
98
+ /**
99
+ * Request timeout in milliseconds
100
+ * @default 30000
101
+ */
102
+ timeout?: number
103
+ }
104
+
105
+ /**
106
+ * Options for historical scanning
107
+ */
108
+ export interface NEARHistoricalScanOptions {
109
+ /**
110
+ * Account ID to scan for announcements
111
+ * This is typically the SIP registry contract or a specific stealth address
112
+ */
113
+ accountId?: string
114
+
115
+ /**
116
+ * Start block height for scanning
117
+ */
118
+ fromBlock?: number
119
+
120
+ /**
121
+ * End block height for scanning
122
+ */
123
+ toBlock?: number
124
+
125
+ /**
126
+ * Maximum number of transactions to scan
127
+ * @default 1000
128
+ */
129
+ limit?: number
130
+
131
+ /**
132
+ * Cursor for pagination
133
+ */
134
+ cursor?: string
135
+ }
136
+
137
+ /**
138
+ * A detected stealth payment on NEAR
139
+ */
140
+ export interface NEARDetectedPaymentResult {
141
+ /**
142
+ * Stealth address (implicit account ID - 64 hex chars)
143
+ */
144
+ stealthAddress: string
145
+
146
+ /**
147
+ * Ed25519 public key for the stealth address (hex)
148
+ */
149
+ stealthPublicKey: HexString
150
+
151
+ /**
152
+ * Ephemeral public key from the sender (hex)
153
+ */
154
+ ephemeralPublicKey: HexString
155
+
156
+ /**
157
+ * View tag for efficient scanning
158
+ */
159
+ viewTag: number
160
+
161
+ /**
162
+ * Amount in yoctoNEAR
163
+ */
164
+ amount: bigint
165
+
166
+ /**
167
+ * Token contract (null for native NEAR)
168
+ */
169
+ tokenContract: string | null
170
+
171
+ /**
172
+ * Token decimals
173
+ */
174
+ decimals: number
175
+
176
+ /**
177
+ * Transaction hash
178
+ */
179
+ txHash: string
180
+
181
+ /**
182
+ * Block height
183
+ */
184
+ blockHeight: number
185
+
186
+ /**
187
+ * Block timestamp (nanoseconds)
188
+ */
189
+ timestamp: number
190
+
191
+ /**
192
+ * Label of the recipient this payment was detected for
193
+ */
194
+ recipientLabel?: string
195
+ }
196
+
197
+ /**
198
+ * Result of a historical scan
199
+ */
200
+ export interface NEARHistoricalScanResult {
201
+ /**
202
+ * Detected payments
203
+ */
204
+ payments: NEARDetectedPaymentResult[]
205
+
206
+ /**
207
+ * Total transactions scanned
208
+ */
209
+ scannedCount: number
210
+
211
+ /**
212
+ * Whether more results are available
213
+ */
214
+ hasMore: boolean
215
+
216
+ /**
217
+ * Cursor for next page
218
+ */
219
+ nextCursor?: string
220
+
221
+ /**
222
+ * Last block height scanned
223
+ */
224
+ lastBlockHeight?: number
225
+ }
226
+
227
+ /**
228
+ * Callback for real-time payment detection
229
+ */
230
+ export type NEARPaymentCallback = (payment: NEARDetectedPaymentResult) => void
231
+
232
+ /**
233
+ * Callback for scan errors
234
+ */
235
+ export type NEARErrorCallback = (error: Error) => void
236
+
237
+ /**
238
+ * Announcement cache entry
239
+ */
240
+ interface CacheEntry {
241
+ announcement: NEARAnnouncement
242
+ txHash: string
243
+ blockHeight: number
244
+ timestamp: number
245
+ }
246
+
247
+ /**
248
+ * Announcement cache interface
249
+ */
250
+ export interface NEARAnnouncementCache {
251
+ /**
252
+ * Get cached announcements for a block range
253
+ */
254
+ get(fromBlock: number, toBlock: number): CacheEntry[]
255
+
256
+ /**
257
+ * Add announcements to cache
258
+ */
259
+ add(entries: CacheEntry[]): void
260
+
261
+ /**
262
+ * Get the highest cached block
263
+ */
264
+ getHighestBlock(): number | null
265
+
266
+ /**
267
+ * Clear cache for reorg handling
268
+ */
269
+ clearFrom(blockHeight: number): void
270
+
271
+ /**
272
+ * Get total cached count
273
+ */
274
+ size(): number
275
+ }
276
+
277
+ // ─── In-Memory Cache Implementation ───────────────────────────────────────────
278
+
279
+ /**
280
+ * Create an in-memory announcement cache
281
+ */
282
+ export function createNEARAnnouncementCache(): NEARAnnouncementCache {
283
+ const entries: CacheEntry[] = []
284
+
285
+ return {
286
+ get(fromBlock: number, toBlock: number): CacheEntry[] {
287
+ return entries.filter(
288
+ e => e.blockHeight >= fromBlock && e.blockHeight <= toBlock
289
+ )
290
+ },
291
+
292
+ add(newEntries: CacheEntry[]): void {
293
+ for (const entry of newEntries) {
294
+ // Avoid duplicates
295
+ const exists = entries.some(
296
+ e => e.txHash === entry.txHash
297
+ )
298
+ if (!exists) {
299
+ entries.push(entry)
300
+ }
301
+ }
302
+ // Sort by block height
303
+ entries.sort((a, b) => a.blockHeight - b.blockHeight)
304
+ },
305
+
306
+ getHighestBlock(): number | null {
307
+ if (entries.length === 0) return null
308
+ return entries[entries.length - 1].blockHeight
309
+ },
310
+
311
+ clearFrom(blockHeight: number): void {
312
+ const idx = entries.findIndex(e => e.blockHeight >= blockHeight)
313
+ if (idx !== -1) {
314
+ entries.splice(idx)
315
+ }
316
+ },
317
+
318
+ size(): number {
319
+ return entries.length
320
+ },
321
+ }
322
+ }
323
+
324
+ // ─── NEAR RPC Helper ──────────────────────────────────────────────────────────
325
+
326
+ /**
327
+ * Simple NEAR RPC client for scanning
328
+ */
329
+ class NEARRpcClient {
330
+ constructor(
331
+ private rpcUrl: string,
332
+ private timeout: number = 30000
333
+ ) {}
334
+
335
+ async call<T>(method: string, params: unknown[]): Promise<T> {
336
+ const controller = new AbortController()
337
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout)
338
+
339
+ try {
340
+ const response = await fetch(this.rpcUrl, {
341
+ method: 'POST',
342
+ headers: { 'Content-Type': 'application/json' },
343
+ body: JSON.stringify({
344
+ jsonrpc: '2.0',
345
+ id: Date.now(),
346
+ method,
347
+ params,
348
+ }),
349
+ signal: controller.signal,
350
+ })
351
+
352
+ if (!response.ok) {
353
+ throw new Error(`RPC request failed: ${response.status} ${response.statusText}`)
354
+ }
355
+
356
+ const json = await response.json() as { result?: T; error?: { message: string } }
357
+
358
+ if (json.error) {
359
+ throw new Error(`RPC error: ${json.error.message}`)
360
+ }
361
+
362
+ return json.result as T
363
+ } finally {
364
+ clearTimeout(timeoutId)
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Get account balance
370
+ */
371
+ async getBalance(accountId: string): Promise<bigint> {
372
+ interface AccountView {
373
+ amount: string
374
+ locked: string
375
+ code_hash: string
376
+ storage_usage: number
377
+ storage_paid_at: number
378
+ }
379
+
380
+ const result = await this.call<AccountView>('query', [{
381
+ request_type: 'view_account',
382
+ finality: 'final',
383
+ account_id: accountId,
384
+ }])
385
+
386
+ return BigInt(result.amount)
387
+ }
388
+
389
+ /**
390
+ * Get block info
391
+ */
392
+ async getBlock(blockId: number | string = 'final'): Promise<{
393
+ header: {
394
+ height: number
395
+ timestamp: number
396
+ hash: string
397
+ }
398
+ }> {
399
+ if (typeof blockId === 'number') {
400
+ return this.call('block', [{ block_id: blockId }])
401
+ }
402
+ return this.call('block', [{ finality: blockId }])
403
+ }
404
+
405
+ /**
406
+ * Get transaction status
407
+ */
408
+ async getTxStatus(txHash: string, senderId: string): Promise<{
409
+ transaction: {
410
+ hash: string
411
+ signer_id: string
412
+ receiver_id: string
413
+ actions: Array<{
414
+ FunctionCall?: {
415
+ method_name: string
416
+ args: string
417
+ }
418
+ Transfer?: {
419
+ deposit: string
420
+ }
421
+ }>
422
+ }
423
+ receipts_outcome: Array<{
424
+ outcome: {
425
+ logs: string[]
426
+ status: unknown
427
+ }
428
+ }>
429
+ }> {
430
+ return this.call('tx', [txHash, senderId])
431
+ }
432
+ }
433
+
434
+ // ─── NEARStealthScanner Class ─────────────────────────────────────────────────
435
+
436
+ /**
437
+ * NEAR Stealth Address Scanner/Resolver
438
+ *
439
+ * Scans NEAR blockchain for stealth address announcements and identifies
440
+ * which addresses belong to a user's viewing key.
441
+ *
442
+ * @example Basic usage
443
+ * ```typescript
444
+ * const scanner = new NEARStealthScanner({
445
+ * rpcUrl: 'https://rpc.mainnet.near.org',
446
+ * })
447
+ *
448
+ * // Add recipients to scan for
449
+ * scanner.addRecipient({
450
+ * viewingPrivateKey: '0x...',
451
+ * spendingPublicKey: '0x...',
452
+ * label: 'Wallet 1',
453
+ * })
454
+ *
455
+ * // Scan announcements
456
+ * const result = await scanner.scanAnnouncements([
457
+ * { stealthAddress: '...', ephemeralPublicKey: '0x...', viewTag: 42 }
458
+ * ])
459
+ *
460
+ * console.log(`Found ${result.payments.length} payments`)
461
+ * ```
462
+ */
463
+ export class NEARStealthScanner {
464
+ private rpc: NEARRpcClient
465
+ private recipients: NEARScanRecipient[] = []
466
+ private _batchSize: number
467
+ private cache: NEARAnnouncementCache | null = null
468
+ private network: 'mainnet' | 'testnet'
469
+
470
+ constructor(options: NEARStealthScannerOptions) {
471
+ this.rpc = new NEARRpcClient(options.rpcUrl, options.timeout)
472
+ this._batchSize = options.batchSize ?? 100
473
+ this.network = options.network ?? 'mainnet'
474
+ }
475
+
476
+ /**
477
+ * Get the batch size for scanning
478
+ */
479
+ get batchSize(): number {
480
+ return this._batchSize
481
+ }
482
+
483
+ /**
484
+ * Get the network
485
+ */
486
+ getNetwork(): 'mainnet' | 'testnet' {
487
+ return this.network
488
+ }
489
+
490
+ /**
491
+ * Enable caching for announcements
492
+ */
493
+ enableCache(cache?: NEARAnnouncementCache): void {
494
+ this.cache = cache ?? createNEARAnnouncementCache()
495
+ }
496
+
497
+ /**
498
+ * Disable caching
499
+ */
500
+ disableCache(): void {
501
+ this.cache = null
502
+ }
503
+
504
+ /**
505
+ * Get the current cache
506
+ */
507
+ getCache(): NEARAnnouncementCache | null {
508
+ return this.cache
509
+ }
510
+
511
+ /**
512
+ * Add a recipient to scan for
513
+ *
514
+ * @param recipient - Recipient with viewing/spending keys
515
+ */
516
+ addRecipient(recipient: NEARScanRecipient): void {
517
+ // Validate keys
518
+ if (!isValidHex(recipient.viewingPrivateKey)) {
519
+ throw new ValidationError('Invalid viewingPrivateKey', 'viewingPrivateKey')
520
+ }
521
+ if (!isValidHex(recipient.spendingPrivateKey)) {
522
+ throw new ValidationError('Invalid spendingPrivateKey', 'spendingPrivateKey')
523
+ }
524
+
525
+ this.recipients.push(recipient)
526
+ }
527
+
528
+ /**
529
+ * Add recipient from a NEARViewingKey
530
+ */
531
+ addRecipientFromViewingKey(
532
+ viewingKey: NEARViewingKey,
533
+ spendingPrivateKey: HexString
534
+ ): void {
535
+ this.addRecipient({
536
+ viewingPrivateKey: viewingKey.privateKey,
537
+ spendingPrivateKey,
538
+ label: viewingKey.label,
539
+ })
540
+ }
541
+
542
+ /**
543
+ * Remove a recipient by label
544
+ *
545
+ * @param label - Recipient label to remove
546
+ */
547
+ removeRecipient(label: string): void {
548
+ this.recipients = this.recipients.filter(r => r.label !== label)
549
+ }
550
+
551
+ /**
552
+ * Clear all recipients
553
+ */
554
+ clearRecipients(): void {
555
+ this.recipients = []
556
+ }
557
+
558
+ /**
559
+ * Get current recipients (labels only, keys are sensitive)
560
+ */
561
+ getRecipients(): Array<{ label?: string }> {
562
+ return this.recipients.map(r => ({
563
+ label: r.label,
564
+ }))
565
+ }
566
+
567
+ /**
568
+ * Get the current block height
569
+ */
570
+ async getCurrentBlockHeight(): Promise<number> {
571
+ const block = await this.rpc.getBlock('final')
572
+ return block.header.height
573
+ }
574
+
575
+ /**
576
+ * Get balance of a stealth address
577
+ */
578
+ async getStealthAddressBalance(stealthAddress: string): Promise<bigint> {
579
+ if (!isImplicitAccount(stealthAddress)) {
580
+ throw new ValidationError(
581
+ 'stealthAddress must be a valid implicit account (64 hex chars)',
582
+ 'stealthAddress'
583
+ )
584
+ }
585
+ return this.rpc.getBalance(stealthAddress)
586
+ }
587
+
588
+ /**
589
+ * Scan a list of announcements against configured recipients
590
+ *
591
+ * This is the core scanning function. Use this when you have already
592
+ * fetched announcements from the chain or an indexer.
593
+ *
594
+ * @param announcements - Announcements to check
595
+ * @param txMetadata - Optional transaction metadata for each announcement
596
+ * @returns Detected payments
597
+ */
598
+ async scanAnnouncements(
599
+ announcements: NEARAnnouncement[],
600
+ txMetadata?: Array<{
601
+ txHash: string
602
+ blockHeight: number
603
+ timestamp: number
604
+ amount?: bigint
605
+ tokenContract?: string
606
+ decimals?: number
607
+ }>
608
+ ): Promise<NEARDetectedPaymentResult[]> {
609
+ if (this.recipients.length === 0) {
610
+ return []
611
+ }
612
+
613
+ const payments: NEARDetectedPaymentResult[] = []
614
+
615
+ for (let i = 0; i < announcements.length; i++) {
616
+ const announcement = announcements[i]
617
+ const metadata = txMetadata?.[i]
618
+
619
+ // Validate announcement - viewTag is already a number from parseAnnouncement
620
+ const viewTag = announcement.viewTag
621
+ if (!Number.isInteger(viewTag) || viewTag < VIEW_TAG_MIN || viewTag > VIEW_TAG_MAX) {
622
+ continue
623
+ }
624
+
625
+ // Get stealth address as ed25519 public key
626
+ let stealthPublicKey: HexString
627
+ try {
628
+ if (isImplicitAccount(announcement.stealthAddress)) {
629
+ stealthPublicKey = implicitAccountToEd25519PublicKey(announcement.stealthAddress)
630
+ } else if (announcement.stealthAddress.startsWith('0x')) {
631
+ stealthPublicKey = announcement.stealthAddress as HexString
632
+ } else {
633
+ continue
634
+ }
635
+ } catch {
636
+ continue
637
+ }
638
+
639
+ // Validate ephemeral public key
640
+ if (!isValidHex(announcement.ephemeralPublicKey)) {
641
+ continue
642
+ }
643
+
644
+ const stealthAddressToCheck: StealthAddress = {
645
+ address: stealthPublicKey,
646
+ ephemeralPublicKey: announcement.ephemeralPublicKey,
647
+ viewTag,
648
+ }
649
+
650
+ // Check against each recipient
651
+ for (const recipient of this.recipients) {
652
+ try {
653
+ const isMatch = checkNEARStealthAddress(
654
+ stealthAddressToCheck,
655
+ recipient.spendingPrivateKey,
656
+ recipient.viewingPrivateKey
657
+ )
658
+
659
+ if (isMatch) {
660
+ // Get stealth address as implicit account
661
+ const stealthAddress = isImplicitAccount(announcement.stealthAddress)
662
+ ? announcement.stealthAddress
663
+ : bytesToHex(hexToBytes(stealthPublicKey.slice(2)))
664
+
665
+ // Get balance if not provided
666
+ let amount = metadata?.amount ?? 0n
667
+ if (amount === 0n && isImplicitAccount(stealthAddress)) {
668
+ try {
669
+ amount = await this.getStealthAddressBalance(stealthAddress)
670
+ } catch {
671
+ // Account might not exist yet
672
+ }
673
+ }
674
+
675
+ payments.push({
676
+ stealthAddress,
677
+ stealthPublicKey,
678
+ ephemeralPublicKey: announcement.ephemeralPublicKey,
679
+ viewTag,
680
+ amount,
681
+ tokenContract: metadata?.tokenContract ?? null,
682
+ decimals: metadata?.decimals ?? 24,
683
+ txHash: metadata?.txHash ?? '',
684
+ blockHeight: metadata?.blockHeight ?? 0,
685
+ timestamp: metadata?.timestamp ?? Date.now(),
686
+ recipientLabel: recipient.label,
687
+ })
688
+
689
+ // Only one recipient can match per announcement
690
+ break
691
+ }
692
+ } catch {
693
+ // Invalid keys or malformed data, try next recipient
694
+ }
695
+ }
696
+ }
697
+
698
+ return payments
699
+ }
700
+
701
+ /**
702
+ * Parse announcements from transaction logs
703
+ *
704
+ * @param logs - Transaction log strings
705
+ * @returns Parsed announcements
706
+ */
707
+ parseAnnouncementsFromLogs(logs: string[]): NEARAnnouncement[] {
708
+ const announcements: NEARAnnouncement[] = []
709
+
710
+ for (const log of logs) {
711
+ if (!log.includes(SIP_MEMO_PREFIX)) {
712
+ continue
713
+ }
714
+
715
+ const parsed = parseAnnouncement(log)
716
+ // parseAnnouncement only returns ephemeralPublicKey and viewTag from the memo
717
+ // stealthAddress must be added separately from the transaction receiver
718
+ if (parsed && parsed.ephemeralPublicKey && typeof parsed.viewTag === 'number') {
719
+ announcements.push({
720
+ ephemeralPublicKey: parsed.ephemeralPublicKey,
721
+ viewTag: parsed.viewTag,
722
+ stealthAddress: '' as HexString, // Must be filled in by caller
723
+ stealthAccountId: '', // Must be filled in by caller
724
+ })
725
+ }
726
+ }
727
+
728
+ return announcements
729
+ }
730
+
731
+ /**
732
+ * Verify a specific stealth address belongs to a recipient
733
+ *
734
+ * @param stealthAddress - Stealth address (implicit account or hex)
735
+ * @param ephemeralPublicKey - Ephemeral public key from sender
736
+ * @param viewTag - View tag for filtering
737
+ * @param viewingPrivateKey - Viewing private key to check
738
+ * @param spendingPrivateKey - Spending private key to check
739
+ * @returns True if the stealth address belongs to the recipient
740
+ */
741
+ verifyStealthAddressOwnership(
742
+ stealthAddress: string,
743
+ ephemeralPublicKey: HexString,
744
+ viewTag: number,
745
+ viewingPrivateKey: HexString,
746
+ spendingPrivateKey: HexString
747
+ ): boolean {
748
+ // Get stealth address as ed25519 public key
749
+ let stealthPublicKey: HexString
750
+ if (isImplicitAccount(stealthAddress)) {
751
+ stealthPublicKey = implicitAccountToEd25519PublicKey(stealthAddress)
752
+ } else if (stealthAddress.startsWith('0x')) {
753
+ stealthPublicKey = stealthAddress as HexString
754
+ } else {
755
+ return false
756
+ }
757
+
758
+ const stealthAddressToCheck: StealthAddress = {
759
+ address: stealthPublicKey,
760
+ ephemeralPublicKey,
761
+ viewTag,
762
+ }
763
+
764
+ try {
765
+ return checkNEARStealthAddress(
766
+ stealthAddressToCheck,
767
+ spendingPrivateKey,
768
+ viewingPrivateKey
769
+ )
770
+ } catch {
771
+ return false
772
+ }
773
+ }
774
+
775
+ /**
776
+ * Batch check multiple announcements efficiently
777
+ *
778
+ * @param announcements - Announcements to check
779
+ * @returns Map of stealth address to recipient label for matches
780
+ */
781
+ batchCheckAnnouncements(
782
+ announcements: NEARAnnouncement[]
783
+ ): Map<string, string | undefined> {
784
+ const matches = new Map<string, string | undefined>()
785
+
786
+ for (const announcement of announcements) {
787
+ // viewTag is already a number from parseAnnouncement
788
+ const viewTag = announcement.viewTag
789
+ if (!Number.isInteger(viewTag) || viewTag < VIEW_TAG_MIN || viewTag > VIEW_TAG_MAX) {
790
+ continue
791
+ }
792
+
793
+ // Get stealth public key
794
+ let stealthPublicKey: HexString
795
+ try {
796
+ if (isImplicitAccount(announcement.stealthAddress)) {
797
+ stealthPublicKey = implicitAccountToEd25519PublicKey(announcement.stealthAddress)
798
+ } else if (announcement.stealthAddress.startsWith('0x')) {
799
+ stealthPublicKey = announcement.stealthAddress as HexString
800
+ } else {
801
+ continue
802
+ }
803
+ } catch {
804
+ continue
805
+ }
806
+
807
+ const stealthAddressToCheck: StealthAddress = {
808
+ address: stealthPublicKey,
809
+ ephemeralPublicKey: announcement.ephemeralPublicKey,
810
+ viewTag,
811
+ }
812
+
813
+ for (const recipient of this.recipients) {
814
+ try {
815
+ const isMatch = checkNEARStealthAddress(
816
+ stealthAddressToCheck,
817
+ recipient.spendingPrivateKey,
818
+ recipient.viewingPrivateKey
819
+ )
820
+
821
+ if (isMatch) {
822
+ matches.set(announcement.stealthAddress, recipient.label)
823
+ break
824
+ }
825
+ } catch {
826
+ continue
827
+ }
828
+ }
829
+ }
830
+
831
+ return matches
832
+ }
833
+ }
834
+
835
+ // ─── Factory Function ─────────────────────────────────────────────────────────
836
+
837
+ /**
838
+ * Create a new NEAR stealth scanner
839
+ *
840
+ * @param options - Scanner options
841
+ * @returns Configured stealth scanner
842
+ *
843
+ * @example
844
+ * ```typescript
845
+ * const scanner = createNEARStealthScanner({
846
+ * rpcUrl: 'https://rpc.mainnet.near.org',
847
+ * })
848
+ * ```
849
+ */
850
+ export function createNEARStealthScanner(
851
+ options: NEARStealthScannerOptions
852
+ ): NEARStealthScanner {
853
+ return new NEARStealthScanner(options)
854
+ }
855
+
856
+ // ─── Batch Scanning Utilities ─────────────────────────────────────────────────
857
+
858
+ /**
859
+ * Batch scan announcements for multiple recipients
860
+ *
861
+ * @param options - Scanner options
862
+ * @param recipients - Recipients to scan for
863
+ * @param announcements - Announcements to check
864
+ * @returns All detected payments grouped by recipient
865
+ *
866
+ * @example
867
+ * ```typescript
868
+ * const results = await batchScanNEARAnnouncements(
869
+ * { rpcUrl: 'https://rpc.mainnet.near.org' },
870
+ * [
871
+ * { viewingPrivateKey: '0x...', spendingPublicKey: '0x...', label: 'Wallet 1' },
872
+ * { viewingPrivateKey: '0x...', spendingPublicKey: '0x...', label: 'Wallet 2' },
873
+ * ],
874
+ * announcements
875
+ * )
876
+ *
877
+ * for (const [label, payments] of Object.entries(results)) {
878
+ * console.log(`${label}: ${payments.length} payments`)
879
+ * }
880
+ * ```
881
+ */
882
+ export async function batchScanNEARAnnouncements(
883
+ options: NEARStealthScannerOptions,
884
+ recipients: NEARScanRecipient[],
885
+ announcements: NEARAnnouncement[],
886
+ txMetadata?: Array<{
887
+ txHash: string
888
+ blockHeight: number
889
+ timestamp: number
890
+ amount?: bigint
891
+ tokenContract?: string
892
+ decimals?: number
893
+ }>
894
+ ): Promise<Record<string, NEARDetectedPaymentResult[]>> {
895
+ const scanner = createNEARStealthScanner(options)
896
+
897
+ for (const recipient of recipients) {
898
+ scanner.addRecipient(recipient)
899
+ }
900
+
901
+ const payments = await scanner.scanAnnouncements(announcements, txMetadata)
902
+
903
+ // Group by recipient label
904
+ const grouped: Record<string, NEARDetectedPaymentResult[]> = {}
905
+
906
+ for (const recipient of recipients) {
907
+ const label = recipient.label || 'unknown'
908
+ grouped[label] = payments.filter(p => p.recipientLabel === label)
909
+ }
910
+
911
+ return grouped
912
+ }
913
+
914
+ /**
915
+ * Quick check if any announcement matches any recipient
916
+ *
917
+ * Useful for efficient initial filtering before detailed processing.
918
+ *
919
+ * @param recipients - Recipients to check
920
+ * @param announcements - Announcements to check
921
+ * @returns True if any announcement matches any recipient
922
+ */
923
+ export function hasNEARAnnouncementMatch(
924
+ recipients: NEARScanRecipient[],
925
+ announcements: NEARAnnouncement[]
926
+ ): boolean {
927
+ for (const announcement of announcements) {
928
+ // viewTag is already a number from parseAnnouncement
929
+ const viewTag = announcement.viewTag
930
+ if (!Number.isInteger(viewTag) || viewTag < VIEW_TAG_MIN || viewTag > VIEW_TAG_MAX) {
931
+ continue
932
+ }
933
+
934
+ let stealthPublicKey: HexString
935
+ try {
936
+ if (isImplicitAccount(announcement.stealthAddress)) {
937
+ stealthPublicKey = implicitAccountToEd25519PublicKey(announcement.stealthAddress)
938
+ } else if (announcement.stealthAddress.startsWith('0x')) {
939
+ stealthPublicKey = announcement.stealthAddress as HexString
940
+ } else {
941
+ continue
942
+ }
943
+ } catch {
944
+ continue
945
+ }
946
+
947
+ const stealthAddressToCheck: StealthAddress = {
948
+ address: stealthPublicKey,
949
+ ephemeralPublicKey: announcement.ephemeralPublicKey,
950
+ viewTag,
951
+ }
952
+
953
+ for (const recipient of recipients) {
954
+ try {
955
+ const isMatch = checkNEARStealthAddress(
956
+ stealthAddressToCheck,
957
+ recipient.spendingPrivateKey,
958
+ recipient.viewingPrivateKey
959
+ )
960
+
961
+ if (isMatch) {
962
+ return true
963
+ }
964
+ } catch {
965
+ continue
966
+ }
967
+ }
968
+ }
969
+
970
+ return false
971
+ }