@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.
- package/dist/browser.d.mts +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +2926 -341
- package/dist/browser.mjs +48 -2
- package/dist/chunk-2XIVXWHA.mjs +1930 -0
- package/dist/chunk-3M3HNQCW.mjs +18253 -0
- package/dist/chunk-7RFRWDCW.mjs +1504 -0
- package/dist/chunk-F6F73W35.mjs +16166 -0
- package/dist/chunk-OFDBEIEK.mjs +16166 -0
- package/dist/chunk-SF7YSLF5.mjs +1515 -0
- package/dist/chunk-WWUSGOXE.mjs +17129 -0
- package/dist/index-8MQz13eJ.d.mts +13746 -0
- package/dist/index-B71aXVzk.d.ts +13264 -0
- package/dist/index-DIBZHOOQ.d.ts +13746 -0
- package/dist/index-pOIIuwfV.d.mts +13264 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2911 -326
- package/dist/index.mjs +48 -2
- package/dist/solana-4O4K45VU.mjs +46 -0
- package/dist/solana-NDABAZ6P.mjs +56 -0
- package/dist/solana-ZYO63LY5.mjs +46 -0
- package/package.json +2 -2
- package/src/chains/solana/index.ts +24 -0
- package/src/chains/solana/providers/generic.ts +160 -0
- package/src/chains/solana/providers/helius.ts +249 -0
- package/src/chains/solana/providers/index.ts +54 -0
- package/src/chains/solana/providers/interface.ts +178 -0
- package/src/chains/solana/providers/webhook.ts +519 -0
- package/src/chains/solana/scan.ts +88 -8
- package/src/chains/solana/types.ts +20 -1
- package/src/compliance/index.ts +14 -0
- package/src/compliance/range-sas.ts +591 -0
- package/src/index.ts +99 -0
- package/src/privacy-backends/index.ts +86 -0
- package/src/privacy-backends/interface.ts +263 -0
- package/src/privacy-backends/privacycash-types.ts +278 -0
- package/src/privacy-backends/privacycash.ts +460 -0
- package/src/privacy-backends/registry.ts +278 -0
- package/src/privacy-backends/router.ts +346 -0
- package/src/privacy-backends/sip-native.ts +253 -0
- package/src/proofs/noir.ts +1 -1
- package/src/surveillance/algorithms/address-reuse.ts +143 -0
- package/src/surveillance/algorithms/cluster.ts +247 -0
- package/src/surveillance/algorithms/exchange.ts +295 -0
- package/src/surveillance/algorithms/temporal.ts +337 -0
- package/src/surveillance/analyzer.ts +442 -0
- package/src/surveillance/index.ts +64 -0
- package/src/surveillance/scoring.ts +372 -0
- package/src/surveillance/types.ts +264 -0
|
@@ -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 {
|
|
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
|
|
172
|
+
amount,
|
|
151
173
|
mint: transferInfo.mint,
|
|
152
|
-
tokenSymbol
|
|
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
|
-
//
|
|
233
|
-
//
|
|
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, ...
|
|
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,
|
|
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
|
/**
|