@sip-protocol/sdk 0.7.2 → 0.7.3

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 (50) hide show
  1. package/dist/browser.d.mts +1 -1
  2. package/dist/browser.d.ts +1 -1
  3. package/dist/browser.js +2926 -341
  4. package/dist/browser.mjs +48 -2
  5. package/dist/chunk-2XIVXWHA.mjs +1930 -0
  6. package/dist/chunk-3M3HNQCW.mjs +18253 -0
  7. package/dist/chunk-7RFRWDCW.mjs +1504 -0
  8. package/dist/chunk-F6F73W35.mjs +16166 -0
  9. package/dist/chunk-OFDBEIEK.mjs +16166 -0
  10. package/dist/chunk-SF7YSLF5.mjs +1515 -0
  11. package/dist/chunk-WWUSGOXE.mjs +17129 -0
  12. package/dist/index-8MQz13eJ.d.mts +13746 -0
  13. package/dist/index-B71aXVzk.d.ts +13264 -0
  14. package/dist/index-DIBZHOOQ.d.ts +13746 -0
  15. package/dist/index-pOIIuwfV.d.mts +13264 -0
  16. package/dist/index.d.mts +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +2911 -326
  19. package/dist/index.mjs +48 -2
  20. package/dist/solana-4O4K45VU.mjs +46 -0
  21. package/dist/solana-NDABAZ6P.mjs +56 -0
  22. package/dist/solana-ZYO63LY5.mjs +46 -0
  23. package/package.json +2 -2
  24. package/src/chains/solana/index.ts +24 -0
  25. package/src/chains/solana/providers/generic.ts +160 -0
  26. package/src/chains/solana/providers/helius.ts +249 -0
  27. package/src/chains/solana/providers/index.ts +54 -0
  28. package/src/chains/solana/providers/interface.ts +178 -0
  29. package/src/chains/solana/providers/webhook.ts +519 -0
  30. package/src/chains/solana/scan.ts +88 -8
  31. package/src/chains/solana/types.ts +20 -1
  32. package/src/compliance/index.ts +14 -0
  33. package/src/compliance/range-sas.ts +591 -0
  34. package/src/index.ts +99 -0
  35. package/src/privacy-backends/index.ts +86 -0
  36. package/src/privacy-backends/interface.ts +263 -0
  37. package/src/privacy-backends/privacycash-types.ts +278 -0
  38. package/src/privacy-backends/privacycash.ts +460 -0
  39. package/src/privacy-backends/registry.ts +278 -0
  40. package/src/privacy-backends/router.ts +346 -0
  41. package/src/privacy-backends/sip-native.ts +253 -0
  42. package/src/proofs/noir.ts +1 -1
  43. package/src/surveillance/algorithms/address-reuse.ts +143 -0
  44. package/src/surveillance/algorithms/cluster.ts +247 -0
  45. package/src/surveillance/algorithms/exchange.ts +295 -0
  46. package/src/surveillance/algorithms/temporal.ts +337 -0
  47. package/src/surveillance/analyzer.ts +442 -0
  48. package/src/surveillance/index.ts +64 -0
  49. package/src/surveillance/scoring.ts +372 -0
  50. package/src/surveillance/types.ts +264 -0
@@ -0,0 +1,519 @@
1
+ /**
2
+ * Helius Webhook Handler
3
+ *
4
+ * Real-time stealth payment detection using Helius webhooks.
5
+ * Push-based notifications instead of polling for efficient scanning.
6
+ *
7
+ * @see https://docs.helius.dev/webhooks-and-websockets/webhooks
8
+ *
9
+ * @example Server setup (Express.js)
10
+ * ```typescript
11
+ * import express from 'express'
12
+ * import { createWebhookHandler } from '@sip-protocol/sdk'
13
+ *
14
+ * const app = express()
15
+ * app.use(express.json())
16
+ *
17
+ * const handler = createWebhookHandler({
18
+ * viewingPrivateKey: '0x...',
19
+ * spendingPublicKey: '0x...',
20
+ * onPaymentFound: (payment) => {
21
+ * console.log('Found payment!', payment)
22
+ * // Notify user, update database, etc.
23
+ * },
24
+ * })
25
+ *
26
+ * app.post('/webhook/helius', async (req, res) => {
27
+ * await handler(req.body)
28
+ * res.status(200).send('OK')
29
+ * })
30
+ * ```
31
+ *
32
+ * @example Helius webhook configuration
33
+ * ```
34
+ * Webhook URL: https://your-server.com/webhook/helius
35
+ * Transaction Type: Any (or TRANSFER for token transfers)
36
+ * Account Addresses: [MEMO_PROGRAM_ID] for memo filtering
37
+ * Webhook Type: raw (to get full transaction data)
38
+ * ```
39
+ */
40
+
41
+ import type { HexString } from '@sip-protocol/types'
42
+ import {
43
+ checkEd25519StealthAddress,
44
+ solanaAddressToEd25519PublicKey,
45
+ } from '../../../stealth'
46
+ import type { StealthAddress } from '@sip-protocol/types'
47
+ import { parseAnnouncement } from '../types'
48
+ import type { SolanaScanResult } from '../types'
49
+ import { SIP_MEMO_PREFIX, SOLANA_TOKEN_MINTS } from '../constants'
50
+ import { ValidationError } from '../../../errors'
51
+
52
+ /**
53
+ * Helius raw webhook payload for a transaction
54
+ *
55
+ * @see https://docs.helius.dev/webhooks-and-websockets/webhooks
56
+ */
57
+ export interface HeliusWebhookTransaction {
58
+ /** Block timestamp (Unix seconds) */
59
+ blockTime: number
60
+ /** Position within block */
61
+ indexWithinBlock?: number
62
+ /** Transaction metadata */
63
+ meta: {
64
+ /** Error if transaction failed */
65
+ err: unknown | null
66
+ /** Transaction fee in lamports */
67
+ fee: number
68
+ /** Inner instructions (CPI calls) */
69
+ innerInstructions: Array<{
70
+ index: number
71
+ instructions: Array<{
72
+ accounts: number[]
73
+ data: string
74
+ programIdIndex: number
75
+ }>
76
+ }>
77
+ /** Loaded address tables */
78
+ loadedAddresses?: {
79
+ readonly: string[]
80
+ writable: string[]
81
+ }
82
+ /** Program log messages */
83
+ logMessages: string[]
84
+ /** Post-transaction lamport balances */
85
+ postBalances: number[]
86
+ /** Post-transaction token balances */
87
+ postTokenBalances: Array<{
88
+ accountIndex: number
89
+ mint: string
90
+ owner?: string
91
+ programId?: string
92
+ uiTokenAmount: {
93
+ amount: string
94
+ decimals: number
95
+ uiAmount: number | null
96
+ uiAmountString: string
97
+ }
98
+ }>
99
+ /** Pre-transaction lamport balances */
100
+ preBalances: number[]
101
+ /** Pre-transaction token balances */
102
+ preTokenBalances: Array<{
103
+ accountIndex: number
104
+ mint: string
105
+ owner?: string
106
+ programId?: string
107
+ uiTokenAmount: {
108
+ amount: string
109
+ decimals: number
110
+ uiAmount: number | null
111
+ uiAmountString: string
112
+ }
113
+ }>
114
+ /** Rewards */
115
+ rewards: unknown[]
116
+ }
117
+ /** Slot number */
118
+ slot: number
119
+ /** Transaction data */
120
+ transaction: {
121
+ /** Transaction message */
122
+ message: {
123
+ /** Account keys involved */
124
+ accountKeys: string[]
125
+ /** Compiled instructions */
126
+ instructions: Array<{
127
+ accounts: number[]
128
+ data: string
129
+ programIdIndex: number
130
+ }>
131
+ /** Recent blockhash */
132
+ recentBlockhash: string
133
+ }
134
+ /** Transaction signatures */
135
+ signatures: string[]
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Helius enhanced webhook payload
141
+ *
142
+ * Enhanced webhooks provide parsed/decoded transaction data.
143
+ */
144
+ export interface HeliusEnhancedTransaction {
145
+ /** Human-readable description */
146
+ description: string
147
+ /** Transaction type (TRANSFER, NFT_SALE, etc.) */
148
+ type: string
149
+ /** Source wallet/program */
150
+ source: string
151
+ /** Transaction fee in lamports */
152
+ fee: number
153
+ /** Fee payer address */
154
+ feePayer: string
155
+ /** Transaction signature */
156
+ signature: string
157
+ /** Slot number */
158
+ slot: number
159
+ /** Block timestamp */
160
+ timestamp: number
161
+ /** Native SOL transfers */
162
+ nativeTransfers: Array<{
163
+ fromUserAccount: string
164
+ toUserAccount: string
165
+ amount: number
166
+ }>
167
+ /** Token transfers */
168
+ tokenTransfers: Array<{
169
+ fromUserAccount: string
170
+ toUserAccount: string
171
+ fromTokenAccount: string
172
+ toTokenAccount: string
173
+ tokenAmount: number
174
+ mint: string
175
+ tokenStandard: string
176
+ }>
177
+ /** Account data changes */
178
+ accountData: Array<{
179
+ account: string
180
+ nativeBalanceChange: number
181
+ tokenBalanceChanges: Array<{
182
+ userAccount: string
183
+ tokenAccount: string
184
+ mint: string
185
+ rawTokenAmount: {
186
+ tokenAmount: string
187
+ decimals: number
188
+ }
189
+ }>
190
+ }>
191
+ /** Events (NFT sales, etc.) */
192
+ events?: Record<string, unknown>
193
+ }
194
+
195
+ /**
196
+ * Webhook payload (can be single transaction or array)
197
+ */
198
+ export type HeliusWebhookPayload =
199
+ | HeliusWebhookTransaction
200
+ | HeliusWebhookTransaction[]
201
+ | HeliusEnhancedTransaction
202
+ | HeliusEnhancedTransaction[]
203
+
204
+ /**
205
+ * Configuration for webhook handler
206
+ */
207
+ export interface WebhookHandlerConfig {
208
+ /** Recipient's viewing private key (hex) */
209
+ viewingPrivateKey: HexString
210
+ /** Recipient's spending public key (hex) */
211
+ spendingPublicKey: HexString
212
+ /**
213
+ * Callback when a payment is found
214
+ *
215
+ * @param payment - The detected payment details
216
+ */
217
+ onPaymentFound: (payment: SolanaScanResult) => void | Promise<void>
218
+ /**
219
+ * Optional callback for errors
220
+ *
221
+ * @param error - The error that occurred
222
+ * @param transaction - The transaction that caused the error (if available)
223
+ */
224
+ onError?: (error: Error, transaction?: HeliusWebhookTransaction) => void
225
+ }
226
+
227
+ /**
228
+ * Result of processing a single webhook transaction
229
+ */
230
+ export interface WebhookProcessResult {
231
+ /** Whether a payment was found for us */
232
+ found: boolean
233
+ /** The payment details (if found) */
234
+ payment?: SolanaScanResult
235
+ /** Transaction signature */
236
+ signature: string
237
+ }
238
+
239
+ /**
240
+ * Create a webhook handler for processing Helius webhook payloads
241
+ *
242
+ * @param config - Handler configuration
243
+ * @returns Async function to process webhook payloads
244
+ *
245
+ * @example
246
+ * ```typescript
247
+ * const handler = createWebhookHandler({
248
+ * viewingPrivateKey: recipientKeys.viewingPrivateKey,
249
+ * spendingPublicKey: recipientKeys.spendingPublicKey,
250
+ * onPaymentFound: async (payment) => {
251
+ * await db.savePayment(payment)
252
+ * await notifyUser(payment)
253
+ * },
254
+ * })
255
+ *
256
+ * // In your Express route:
257
+ * app.post('/webhook', async (req, res) => {
258
+ * const results = await handler(req.body)
259
+ * res.json({ processed: results.length })
260
+ * })
261
+ * ```
262
+ */
263
+ export function createWebhookHandler(
264
+ config: WebhookHandlerConfig
265
+ ): (payload: HeliusWebhookPayload) => Promise<WebhookProcessResult[]> {
266
+ const { viewingPrivateKey, spendingPublicKey, onPaymentFound, onError } = config
267
+
268
+ // Validate required keys
269
+ if (!viewingPrivateKey || !viewingPrivateKey.startsWith('0x')) {
270
+ throw new ValidationError('viewingPrivateKey must be a valid hex string starting with 0x', 'viewingPrivateKey')
271
+ }
272
+ if (!spendingPublicKey || !spendingPublicKey.startsWith('0x')) {
273
+ throw new ValidationError('spendingPublicKey must be a valid hex string starting with 0x', 'spendingPublicKey')
274
+ }
275
+ if (typeof onPaymentFound !== 'function') {
276
+ throw new ValidationError('onPaymentFound callback is required', 'onPaymentFound')
277
+ }
278
+
279
+ return async (payload: HeliusWebhookPayload): Promise<WebhookProcessResult[]> => {
280
+ // Normalize to array
281
+ const transactions = Array.isArray(payload) ? payload : [payload]
282
+ const results: WebhookProcessResult[] = []
283
+
284
+ for (const tx of transactions) {
285
+ try {
286
+ // Handle raw vs enhanced webhook format
287
+ if (isRawTransaction(tx)) {
288
+ const result = await processRawTransaction(
289
+ tx,
290
+ viewingPrivateKey,
291
+ spendingPublicKey,
292
+ onPaymentFound
293
+ )
294
+ results.push(result)
295
+ } else {
296
+ // Enhanced transactions don't include log messages,
297
+ // so we can't detect SIP announcements directly
298
+ // For now, skip enhanced transactions
299
+ results.push({
300
+ found: false,
301
+ signature: (tx as HeliusEnhancedTransaction).signature,
302
+ })
303
+ }
304
+ } catch (error) {
305
+ onError?.(error as Error, isRawTransaction(tx) ? tx : undefined)
306
+ results.push({
307
+ found: false,
308
+ signature: getSignature(tx),
309
+ })
310
+ }
311
+ }
312
+
313
+ return results
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Process a single raw transaction for SIP announcements
319
+ */
320
+ async function processRawTransaction(
321
+ tx: HeliusWebhookTransaction,
322
+ viewingPrivateKey: HexString,
323
+ spendingPublicKey: HexString,
324
+ onPaymentFound: (payment: SolanaScanResult) => void | Promise<void>
325
+ ): Promise<WebhookProcessResult> {
326
+ const signature = tx.transaction?.signatures?.[0] ?? 'unknown'
327
+
328
+ // Check if transaction failed
329
+ if (tx.meta?.err) {
330
+ return { found: false, signature }
331
+ }
332
+
333
+ // Ensure log messages exist
334
+ if (!tx.meta?.logMessages) {
335
+ return { found: false, signature }
336
+ }
337
+
338
+ // Search log messages for SIP announcement
339
+ for (const log of tx.meta.logMessages) {
340
+ if (!log.includes(SIP_MEMO_PREFIX)) continue
341
+
342
+ // Extract memo content from log
343
+ const memoMatch = log.match(/Program log: (.+)/)
344
+ if (!memoMatch) continue
345
+
346
+ const memoContent = memoMatch[1]
347
+ const announcement = parseAnnouncement(memoContent)
348
+ if (!announcement) continue
349
+
350
+ // Check if this payment is for us
351
+ const ephemeralPubKeyHex = solanaAddressToEd25519PublicKey(
352
+ announcement.ephemeralPublicKey
353
+ )
354
+
355
+ // Parse view tag with bounds checking (view tags are 1 byte: 0-255)
356
+ const viewTagNumber = parseInt(announcement.viewTag, 16)
357
+ if (!Number.isFinite(viewTagNumber) || viewTagNumber < 0 || viewTagNumber > 255) {
358
+ continue // Invalid view tag, skip this announcement
359
+ }
360
+ const stealthAddressToCheck: StealthAddress = {
361
+ address: announcement.stealthAddress
362
+ ? solanaAddressToEd25519PublicKey(announcement.stealthAddress)
363
+ : ('0x' + '00'.repeat(32)) as HexString,
364
+ ephemeralPublicKey: ephemeralPubKeyHex,
365
+ viewTag: viewTagNumber,
366
+ }
367
+
368
+ // Check if this is our payment (may throw for invalid curve points)
369
+ let isOurs = false
370
+ try {
371
+ isOurs = checkEd25519StealthAddress(
372
+ stealthAddressToCheck,
373
+ viewingPrivateKey,
374
+ spendingPublicKey
375
+ )
376
+ } catch {
377
+ // Invalid ephemeral key or malformed data - not our payment
378
+ continue
379
+ }
380
+
381
+ if (isOurs) {
382
+ // Parse token transfer info
383
+ const transferInfo = parseTokenTransferFromWebhook(tx)
384
+
385
+ const payment: SolanaScanResult = {
386
+ stealthAddress: announcement.stealthAddress || '',
387
+ ephemeralPublicKey: announcement.ephemeralPublicKey,
388
+ amount: transferInfo?.amount ?? 0n,
389
+ mint: transferInfo?.mint ?? '',
390
+ tokenSymbol: transferInfo?.mint ? getTokenSymbol(transferInfo.mint) : undefined,
391
+ txSignature: signature,
392
+ slot: tx.slot,
393
+ timestamp: tx.blockTime,
394
+ }
395
+
396
+ // Call the callback (wrap in try-catch to prevent callback errors from breaking processing)
397
+ try {
398
+ await onPaymentFound(payment)
399
+ } catch {
400
+ // Callback error should not prevent returning the found payment
401
+ }
402
+
403
+ return { found: true, payment, signature }
404
+ }
405
+ }
406
+
407
+ return { found: false, signature }
408
+ }
409
+
410
+ /**
411
+ * Parse token transfer info from webhook transaction
412
+ */
413
+ function parseTokenTransferFromWebhook(
414
+ tx: HeliusWebhookTransaction
415
+ ): { mint: string; amount: bigint } | null {
416
+ const { preTokenBalances, postTokenBalances } = tx.meta
417
+
418
+ if (!postTokenBalances || !preTokenBalances) {
419
+ return null
420
+ }
421
+
422
+ // Find token balance changes
423
+ for (const post of postTokenBalances) {
424
+ const pre = preTokenBalances.find(
425
+ (p) => p.accountIndex === post.accountIndex
426
+ )
427
+
428
+ const postAmount = BigInt(post.uiTokenAmount.amount)
429
+ const preAmount = pre ? BigInt(pre.uiTokenAmount.amount) : 0n
430
+
431
+ if (postAmount > preAmount) {
432
+ return {
433
+ mint: post.mint,
434
+ amount: postAmount - preAmount,
435
+ }
436
+ }
437
+ }
438
+
439
+ return null
440
+ }
441
+
442
+ /**
443
+ * Get token symbol from mint address
444
+ */
445
+ function getTokenSymbol(mint: string): string | undefined {
446
+ for (const [symbol, address] of Object.entries(SOLANA_TOKEN_MINTS)) {
447
+ if (address === mint) {
448
+ return symbol
449
+ }
450
+ }
451
+ return undefined
452
+ }
453
+
454
+ /**
455
+ * Type guard for raw transaction
456
+ * Raw transactions have transaction.signatures array, enhanced have signature directly
457
+ */
458
+ function isRawTransaction(tx: unknown): tx is HeliusWebhookTransaction {
459
+ return (
460
+ typeof tx === 'object' &&
461
+ tx !== null &&
462
+ 'meta' in tx &&
463
+ 'transaction' in tx &&
464
+ Array.isArray((tx as HeliusWebhookTransaction).transaction?.signatures)
465
+ )
466
+ }
467
+
468
+ /**
469
+ * Get signature from either transaction type
470
+ */
471
+ function getSignature(tx: HeliusWebhookTransaction | HeliusEnhancedTransaction): string {
472
+ // Enhanced transactions have signature at top level
473
+ if ('signature' in tx && typeof (tx as HeliusEnhancedTransaction).signature === 'string') {
474
+ return (tx as HeliusEnhancedTransaction).signature
475
+ }
476
+ // Raw transactions have signatures array in transaction object
477
+ if (isRawTransaction(tx)) {
478
+ return tx.transaction?.signatures?.[0] ?? 'unknown'
479
+ }
480
+ return 'unknown'
481
+ }
482
+
483
+ /**
484
+ * Process a single webhook transaction and check if it's a payment for us
485
+ *
486
+ * Lower-level function for custom webhook handling.
487
+ *
488
+ * @param transaction - Raw Helius webhook transaction
489
+ * @param viewingPrivateKey - Recipient's viewing private key
490
+ * @param spendingPublicKey - Recipient's spending public key
491
+ * @returns Payment result if found, null otherwise
492
+ *
493
+ * @example
494
+ * ```typescript
495
+ * const payment = await processWebhookTransaction(
496
+ * webhookPayload,
497
+ * viewingPrivateKey,
498
+ * spendingPublicKey
499
+ * )
500
+ *
501
+ * if (payment) {
502
+ * console.log('Found payment:', payment.amount, payment.mint)
503
+ * }
504
+ * ```
505
+ */
506
+ export async function processWebhookTransaction(
507
+ transaction: HeliusWebhookTransaction,
508
+ viewingPrivateKey: HexString,
509
+ spendingPublicKey: HexString
510
+ ): Promise<SolanaScanResult | null> {
511
+ const result = await processRawTransaction(
512
+ transaction,
513
+ viewingPrivateKey,
514
+ spendingPublicKey,
515
+ () => {} // No-op callback
516
+ )
517
+
518
+ return result.found ? result.payment ?? null : null
519
+ }
@@ -13,7 +13,6 @@ import {
13
13
  getAssociatedTokenAddress,
14
14
  createTransferInstruction,
15
15
  getAccount,
16
- TOKEN_PROGRAM_ID,
17
16
  } from '@solana/spl-token'
18
17
  import {
19
18
  checkEd25519StealthAddress,
@@ -35,7 +34,9 @@ import {
35
34
  SOLANA_TOKEN_MINTS,
36
35
  type SolanaCluster,
37
36
  } from './constants'
38
- import { hexToBytes, bytesToHex } from '@noble/hashes/utils'
37
+ import type { SolanaRPCProvider } from './providers/interface'
38
+ import { hexToBytes } from '@noble/hashes/utils'
39
+ import { ed25519 } from '@noble/curves/ed25519'
39
40
 
40
41
  /**
41
42
  * Scan for incoming stealth payments
@@ -70,6 +71,7 @@ export async function scanForPayments(
70
71
  fromSlot,
71
72
  toSlot,
72
73
  limit = 100,
74
+ provider,
73
75
  } = params
74
76
 
75
77
  const results: SolanaScanResult[] = []
@@ -144,12 +146,32 @@ export async function scanForPayments(
144
146
  // Parse token transfer from transaction
145
147
  const transferInfo = parseTokenTransfer(tx)
146
148
  if (transferInfo) {
149
+ // If provider is available, use it for more accurate current balance
150
+ let amount = transferInfo.amount
151
+ const tokenSymbol = getTokenSymbol(transferInfo.mint)
152
+
153
+ if (provider && announcement.stealthAddress) {
154
+ try {
155
+ // Use getTokenBalance for efficient single-token query
156
+ const balance = await provider.getTokenBalance(
157
+ announcement.stealthAddress,
158
+ transferInfo.mint
159
+ )
160
+ // Only use provider balance if > 0 (confirms tokens still there)
161
+ if (balance > 0n) {
162
+ amount = balance
163
+ }
164
+ } catch {
165
+ // Fallback to parsed transfer info if provider fails
166
+ }
167
+ }
168
+
147
169
  results.push({
148
170
  stealthAddress: announcement.stealthAddress || '',
149
171
  ephemeralPublicKey: announcement.ephemeralPublicKey,
150
- amount: transferInfo.amount,
172
+ amount,
151
173
  mint: transferInfo.mint,
152
- tokenSymbol: getTokenSymbol(transferInfo.mint),
174
+ tokenSymbol,
153
175
  txSignature: sigInfo.signature,
154
176
  slot: sigInfo.slot,
155
177
  timestamp: sigInfo.blockTime || 0,
@@ -229,11 +251,31 @@ export async function claimStealthPayment(
229
251
  // The SDK returns a scalar, so we need to handle this carefully
230
252
  const stealthPrivKeyBytes = hexToBytes(recovery.privateKey.slice(2))
231
253
 
232
- // Solana keypairs expect 64 bytes (32 byte seed + 32 byte public key)
233
- // We construct this from the derived scalar
254
+ // Validate that the derived private key (scalar) produces the expected public key
255
+ // Note: SIP derives a scalar, not a seed. We use scalar multiplication to verify.
234
256
  const stealthPubkey = new PublicKey(stealthAddress)
257
+ const expectedPubKeyBytes = stealthPubkey.toBytes()
258
+
259
+ // Convert scalar bytes to bigint (little-endian for ed25519)
260
+ const scalarBigInt = bytesToBigIntLE(stealthPrivKeyBytes)
261
+ const ED25519_ORDER = 2n ** 252n + 27742317777372353535851937790883648493n
262
+ let validScalar = scalarBigInt % ED25519_ORDER
263
+ if (validScalar === 0n) validScalar = 1n
264
+
265
+ // Derive public key via scalar multiplication
266
+ const derivedPubKeyBytes = ed25519.ExtendedPoint.BASE.multiply(validScalar).toRawBytes()
267
+
268
+ if (!derivedPubKeyBytes.every((b, i) => b === expectedPubKeyBytes[i])) {
269
+ throw new Error(
270
+ 'Stealth key derivation failed: derived private key does not produce expected public key. ' +
271
+ 'This may indicate incorrect spending/viewing keys or corrupted announcement data.'
272
+ )
273
+ }
274
+
275
+ // Solana keypairs expect 64 bytes (32 byte seed + 32 byte public key)
276
+ // We construct this from the derived scalar (now validated)
235
277
  const stealthKeypair = Keypair.fromSecretKey(
236
- new Uint8Array([...stealthPrivKeyBytes, ...stealthPubkey.toBytes()])
278
+ new Uint8Array([...stealthPrivKeyBytes, ...expectedPubKeyBytes])
237
279
  )
238
280
 
239
281
  // Get token accounts
@@ -306,12 +348,39 @@ export async function claimStealthPayment(
306
348
 
307
349
  /**
308
350
  * Get token balance for a stealth address
351
+ *
352
+ * @param connection - Solana RPC connection
353
+ * @param stealthAddress - Stealth address to check (base58)
354
+ * @param mint - SPL token mint address
355
+ * @param provider - Optional RPC provider for efficient queries
356
+ * @returns Token balance in smallest unit
357
+ *
358
+ * @example
359
+ * ```typescript
360
+ * // Using standard RPC
361
+ * const balance = await getStealthBalance(connection, stealthAddr, mint)
362
+ *
363
+ * // Using Helius for efficient queries
364
+ * const helius = createProvider('helius', { apiKey })
365
+ * const balance = await getStealthBalance(connection, stealthAddr, mint, helius)
366
+ * ```
309
367
  */
310
368
  export async function getStealthBalance(
311
369
  connection: SolanaScanParams['connection'],
312
370
  stealthAddress: string,
313
- mint: PublicKey
371
+ mint: PublicKey,
372
+ provider?: SolanaRPCProvider
314
373
  ): Promise<bigint> {
374
+ // Use provider if available for efficient queries
375
+ if (provider) {
376
+ try {
377
+ return await provider.getTokenBalance(stealthAddress, mint.toBase58())
378
+ } catch {
379
+ // Fallback to standard RPC if provider fails
380
+ }
381
+ }
382
+
383
+ // Standard RPC fallback
315
384
  try {
316
385
  const stealthPubkey = new PublicKey(stealthAddress)
317
386
  const ata = await getAssociatedTokenAddress(mint, stealthPubkey, true)
@@ -380,3 +449,14 @@ function detectCluster(endpoint: string): SolanaCluster {
380
449
  }
381
450
  return 'mainnet-beta'
382
451
  }
452
+
453
+ /**
454
+ * Convert bytes to bigint (little-endian, for ed25519 scalars)
455
+ */
456
+ function bytesToBigIntLE(bytes: Uint8Array): bigint {
457
+ let result = 0n
458
+ for (let i = bytes.length - 1; i >= 0; i--) {
459
+ result = (result << 8n) | BigInt(bytes[i])
460
+ }
461
+ return result
462
+ }
@@ -5,8 +5,9 @@
5
5
  */
6
6
 
7
7
  import type { PublicKey, Connection, Transaction, VersionedTransaction } from '@solana/web3.js'
8
- import type { StealthMetaAddress, StealthAddress, HexString } from '@sip-protocol/types'
8
+ import type { StealthMetaAddress, HexString } from '@sip-protocol/types'
9
9
  import type { SolanaCluster } from './constants'
10
+ import type { SolanaRPCProvider } from './providers/interface'
10
11
 
11
12
  /**
12
13
  * Parameters for sending a private SPL token transfer
@@ -62,6 +63,24 @@ export interface SolanaScanParams {
62
63
  toSlot?: number
63
64
  /** Optional: Limit number of results */
64
65
  limit?: number
66
+ /**
67
+ * Optional: RPC provider for efficient asset queries
68
+ *
69
+ * When provided, uses provider.getAssetsByOwner() for token detection
70
+ * instead of parsing transaction logs. Recommended for production.
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * const helius = createProvider('helius', { apiKey: '...' })
75
+ * const payments = await scanForPayments({
76
+ * connection,
77
+ * provider: helius,
78
+ * viewingPrivateKey,
79
+ * spendingPublicKey,
80
+ * })
81
+ * ```
82
+ */
83
+ provider?: SolanaRPCProvider
65
84
  }
66
85
 
67
86
  /**