@sip-protocol/sdk 0.1.0 → 0.1.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.
@@ -0,0 +1,1035 @@
1
+ /**
2
+ * Enterprise Compliance Manager for SIP Protocol
3
+ *
4
+ * Provides compliance management, auditor access control,
5
+ * transaction disclosure, and reporting functionality.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // Initialize compliance manager
10
+ * const compliance = await ComplianceManager.create({
11
+ * organizationName: 'Acme Corp',
12
+ * riskThreshold: 70,
13
+ * highValueThreshold: 100000_000000n,
14
+ * })
15
+ *
16
+ * // Register an auditor
17
+ * const auditor = await compliance.registerAuditor({
18
+ * organization: 'Big Four Audit',
19
+ * contactName: 'John Auditor',
20
+ * contactEmail: 'john@bigfour.com',
21
+ * publicKey: '0x...',
22
+ * scope: {
23
+ * transactionTypes: ['all'],
24
+ * chains: ['ethereum'],
25
+ * tokens: [],
26
+ * startDate: Date.now() / 1000 - 365 * 24 * 60 * 60,
27
+ * },
28
+ * })
29
+ *
30
+ * // Disclose a transaction to the auditor
31
+ * const disclosed = compliance.discloseTransaction(txId, auditor.auditorId, viewingKey)
32
+ *
33
+ * // Generate a compliance report
34
+ * const report = await compliance.generateReport({
35
+ * type: 'transaction_summary',
36
+ * title: 'Q4 2024 Report',
37
+ * format: 'json',
38
+ * startDate: quarterStart,
39
+ * endDate: quarterEnd,
40
+ * })
41
+ * ```
42
+ */
43
+
44
+ import {
45
+ ReportStatus,
46
+ PrivacyLevel,
47
+ type ComplianceConfig,
48
+ type ComplianceRole,
49
+ type AuditScope,
50
+ type AuditorRegistration,
51
+ type DisclosedTransaction,
52
+ type ComplianceReport,
53
+ type ReportData,
54
+ type CreateComplianceConfigParams,
55
+ type RegisterAuditorParams,
56
+ type GenerateReportParams,
57
+ type DisclosureRequest,
58
+ type AuditLogEntry,
59
+ type ViewingKey,
60
+ type HexString,
61
+ type ChainId,
62
+ type Asset,
63
+ type ShieldedPayment,
64
+ type PaymentPurpose,
65
+ } from '@sip-protocol/types'
66
+ import { bytesToHex, randomBytes } from '@noble/hashes/utils'
67
+
68
+ import { ValidationError, ErrorCode } from '../errors'
69
+ import { generateViewingKey, deriveViewingKey, decryptWithViewing } from '../privacy'
70
+ import { isValidChainId } from '../validation'
71
+
72
+ /**
73
+ * Default configuration values
74
+ */
75
+ const DEFAULTS = {
76
+ riskThreshold: 70,
77
+ highValueThreshold: 10000_000000n, // 10,000 USDC equivalent
78
+ retentionPeriodDays: 2555, // ~7 years
79
+ }
80
+
81
+ /**
82
+ * ComplianceManager - Enterprise compliance and auditing
83
+ */
84
+ export class ComplianceManager {
85
+ private config: ComplianceConfig
86
+ private auditors: Map<string, AuditorRegistration> = new Map()
87
+ private disclosedTransactions: Map<string, DisclosedTransaction> = new Map()
88
+ private reports: Map<string, ComplianceReport> = new Map()
89
+ private disclosureRequests: Map<string, DisclosureRequest> = new Map()
90
+ private auditLog: AuditLogEntry[] = []
91
+
92
+ private constructor(config: ComplianceConfig) {
93
+ this.config = config
94
+ }
95
+
96
+ /**
97
+ * Create a new compliance manager
98
+ */
99
+ static async create(params: CreateComplianceConfigParams): Promise<ComplianceManager> {
100
+ if (!params.organizationName || params.organizationName.trim().length === 0) {
101
+ throw new ValidationError(
102
+ 'organization name is required',
103
+ 'organizationName',
104
+ undefined,
105
+ ErrorCode.MISSING_REQUIRED
106
+ )
107
+ }
108
+
109
+ const now = Math.floor(Date.now() / 1000)
110
+ const organizationId = generateId('org')
111
+
112
+ // Generate master viewing key
113
+ const masterViewingKey = generateViewingKey(`compliance/${organizationId}`)
114
+
115
+ const config: ComplianceConfig = {
116
+ organizationId,
117
+ organizationName: params.organizationName,
118
+ masterViewingKey,
119
+ defaultAuditScope: {
120
+ transactionTypes: ['all'],
121
+ chains: [],
122
+ tokens: [],
123
+ },
124
+ riskThreshold: params.riskThreshold ?? DEFAULTS.riskThreshold,
125
+ highValueThreshold: params.highValueThreshold ?? DEFAULTS.highValueThreshold,
126
+ retentionPeriodDays: params.retentionPeriodDays ?? DEFAULTS.retentionPeriodDays,
127
+ autoReporting: {
128
+ enabled: false,
129
+ frequency: 'monthly',
130
+ reportTypes: ['transaction_summary'],
131
+ },
132
+ createdAt: now,
133
+ updatedAt: now,
134
+ }
135
+
136
+ return new ComplianceManager(config)
137
+ }
138
+
139
+ /**
140
+ * Load from existing config
141
+ */
142
+ static fromConfig(config: ComplianceConfig): ComplianceManager {
143
+ return new ComplianceManager(config)
144
+ }
145
+
146
+ // ─── Getters ─────────────────────────────────────────────────────────────────
147
+
148
+ get organizationId(): string {
149
+ return this.config.organizationId
150
+ }
151
+
152
+ get organizationName(): string {
153
+ return this.config.organizationName
154
+ }
155
+
156
+ get masterViewingKey(): ViewingKey {
157
+ return this.config.masterViewingKey
158
+ }
159
+
160
+ getConfig(): ComplianceConfig {
161
+ return { ...this.config }
162
+ }
163
+
164
+ // ─── Auditor Management ──────────────────────────────────────────────────────
165
+
166
+ /**
167
+ * Register a new auditor
168
+ */
169
+ async registerAuditor(
170
+ params: RegisterAuditorParams,
171
+ registeredBy: string,
172
+ ): Promise<AuditorRegistration> {
173
+ validateRegisterAuditorParams(params)
174
+
175
+ const auditorId = generateId('auditor')
176
+ const now = Math.floor(Date.now() / 1000)
177
+
178
+ // Derive auditor-specific viewing key
179
+ const viewingKey = deriveViewingKey(
180
+ this.config.masterViewingKey,
181
+ `auditor/${auditorId}`
182
+ )
183
+
184
+ const auditor: AuditorRegistration = {
185
+ auditorId,
186
+ organization: params.organization,
187
+ contactName: params.contactName,
188
+ contactEmail: params.contactEmail,
189
+ publicKey: params.publicKey,
190
+ viewingKey,
191
+ scope: params.scope,
192
+ role: params.role ?? 'auditor',
193
+ registeredAt: now,
194
+ registeredBy,
195
+ isActive: true,
196
+ }
197
+
198
+ this.auditors.set(auditorId, auditor)
199
+
200
+ // Log the action
201
+ this.addAuditLog(registeredBy, 'auditor_registered', {
202
+ auditorId,
203
+ organization: params.organization,
204
+ role: auditor.role,
205
+ })
206
+
207
+ return auditor
208
+ }
209
+
210
+ /**
211
+ * Get an auditor by ID
212
+ */
213
+ getAuditor(auditorId: string): AuditorRegistration | undefined {
214
+ return this.auditors.get(auditorId)
215
+ }
216
+
217
+ /**
218
+ * Get all auditors
219
+ */
220
+ getAllAuditors(): AuditorRegistration[] {
221
+ return Array.from(this.auditors.values())
222
+ }
223
+
224
+ /**
225
+ * Get active auditors
226
+ */
227
+ getActiveAuditors(): AuditorRegistration[] {
228
+ return this.getAllAuditors().filter(a => a.isActive)
229
+ }
230
+
231
+ /**
232
+ * Deactivate an auditor
233
+ */
234
+ deactivateAuditor(
235
+ auditorId: string,
236
+ deactivatedBy: string,
237
+ reason: string,
238
+ ): AuditorRegistration {
239
+ const auditor = this.auditors.get(auditorId)
240
+ if (!auditor) {
241
+ throw new ValidationError(
242
+ `auditor not found: ${auditorId}`,
243
+ 'auditorId',
244
+ undefined,
245
+ ErrorCode.INVALID_INPUT
246
+ )
247
+ }
248
+
249
+ auditor.isActive = false
250
+ auditor.deactivatedAt = Math.floor(Date.now() / 1000)
251
+ auditor.deactivationReason = reason
252
+
253
+ this.addAuditLog(deactivatedBy, 'auditor_deactivated', {
254
+ auditorId,
255
+ reason,
256
+ })
257
+
258
+ return auditor
259
+ }
260
+
261
+ /**
262
+ * Update auditor scope
263
+ */
264
+ updateAuditorScope(
265
+ auditorId: string,
266
+ scope: AuditScope,
267
+ updatedBy: string,
268
+ ): AuditorRegistration {
269
+ const auditor = this.auditors.get(auditorId)
270
+ if (!auditor) {
271
+ throw new ValidationError(
272
+ `auditor not found: ${auditorId}`,
273
+ 'auditorId',
274
+ undefined,
275
+ ErrorCode.INVALID_INPUT
276
+ )
277
+ }
278
+
279
+ auditor.scope = scope
280
+
281
+ this.addAuditLog(updatedBy, 'config_updated', {
282
+ auditorId,
283
+ field: 'scope',
284
+ newScope: scope,
285
+ })
286
+
287
+ return auditor
288
+ }
289
+
290
+ // ─── Transaction Disclosure ──────────────────────────────────────────────────
291
+
292
+ /**
293
+ * Disclose a transaction to an auditor
294
+ *
295
+ * @param payment - The shielded payment to disclose
296
+ * @param auditorId - The auditor to disclose to
297
+ * @param viewingKey - The viewing key to decrypt the payment
298
+ * @param disclosedBy - Who authorized the disclosure
299
+ * @returns The disclosed transaction
300
+ */
301
+ discloseTransaction(
302
+ payment: ShieldedPayment,
303
+ auditorId: string,
304
+ viewingKey: ViewingKey,
305
+ disclosedBy: string,
306
+ additionalInfo?: {
307
+ txHash?: string
308
+ blockNumber?: number
309
+ riskScore?: number
310
+ riskFlags?: string[]
311
+ notes?: string
312
+ tags?: string[]
313
+ },
314
+ ): DisclosedTransaction {
315
+ const auditor = this.auditors.get(auditorId)
316
+ if (!auditor) {
317
+ throw new ValidationError(
318
+ `auditor not found: ${auditorId}`,
319
+ 'auditorId',
320
+ undefined,
321
+ ErrorCode.INVALID_INPUT
322
+ )
323
+ }
324
+
325
+ if (!auditor.isActive) {
326
+ throw new ValidationError(
327
+ 'auditor is not active',
328
+ 'auditorId',
329
+ undefined,
330
+ ErrorCode.INVALID_INPUT
331
+ )
332
+ }
333
+
334
+ // Check if transaction is within auditor's scope
335
+ if (!this.isWithinScope(payment, auditor.scope)) {
336
+ throw new ValidationError(
337
+ 'transaction is outside auditor scope',
338
+ 'scope',
339
+ undefined,
340
+ ErrorCode.INVALID_INPUT
341
+ )
342
+ }
343
+
344
+ const disclosureId = generateId('disc')
345
+ const now = Math.floor(Date.now() / 1000)
346
+
347
+ // Decrypt memo if available
348
+ let decryptedMemo: string | undefined
349
+ let memoDecryptionError: string | undefined
350
+ if (payment.encryptedMemo && viewingKey) {
351
+ try {
352
+ decryptedMemo = this.decryptMemoSafe(payment.encryptedMemo, viewingKey)
353
+ } catch (error) {
354
+ // Log warning for monitoring - memo decryption failure may indicate
355
+ // key mismatch, data corruption, or unauthorized viewing key
356
+ memoDecryptionError = error instanceof Error ? error.message : 'Unknown decryption error'
357
+ console.warn(
358
+ `[ComplianceManager] Failed to decrypt memo for payment ${payment.paymentId}: ${memoDecryptionError}`
359
+ )
360
+ }
361
+ }
362
+
363
+ const disclosed: DisclosedTransaction = {
364
+ transactionId: payment.paymentId,
365
+ disclosureId,
366
+ auditorId,
367
+ disclosedAt: now,
368
+ disclosedBy,
369
+ type: 'payment',
370
+ direction: 'outbound', // Payments are outbound
371
+ token: payment.token,
372
+ amount: payment.amount,
373
+ sender: payment.senderCommitment?.value ?? 'hidden',
374
+ recipient: payment.recipientStealth?.address ?? payment.recipientAddress ?? 'unknown',
375
+ txHash: additionalInfo?.txHash ?? '',
376
+ blockNumber: additionalInfo?.blockNumber ?? 0,
377
+ timestamp: payment.createdAt,
378
+ chain: payment.sourceChain,
379
+ privacyLevel: payment.privacyLevel,
380
+ memo: decryptedMemo ?? payment.memo,
381
+ purpose: payment.purpose,
382
+ riskScore: additionalInfo?.riskScore,
383
+ riskFlags: additionalInfo?.riskFlags,
384
+ notes: additionalInfo?.notes,
385
+ tags: additionalInfo?.tags,
386
+ }
387
+
388
+ this.disclosedTransactions.set(disclosureId, disclosed)
389
+
390
+ this.addAuditLog(disclosedBy, 'transaction_disclosed', {
391
+ disclosureId,
392
+ transactionId: payment.paymentId,
393
+ auditorId,
394
+ })
395
+
396
+ return disclosed
397
+ }
398
+
399
+ /**
400
+ * Get disclosed transactions for an auditor
401
+ */
402
+ getDisclosedTransactions(auditorId?: string): DisclosedTransaction[] {
403
+ const all = Array.from(this.disclosedTransactions.values())
404
+ if (auditorId) {
405
+ return all.filter(t => t.auditorId === auditorId)
406
+ }
407
+ return all
408
+ }
409
+
410
+ /**
411
+ * Get a disclosed transaction by ID
412
+ */
413
+ getDisclosedTransaction(disclosureId: string): DisclosedTransaction | undefined {
414
+ return this.disclosedTransactions.get(disclosureId)
415
+ }
416
+
417
+ // ─── Disclosure Requests ─────────────────────────────────────────────────────
418
+
419
+ /**
420
+ * Create a disclosure request
421
+ */
422
+ createDisclosureRequest(
423
+ transactionId: string,
424
+ auditorId: string,
425
+ reason: string,
426
+ ): DisclosureRequest {
427
+ const auditor = this.auditors.get(auditorId)
428
+ if (!auditor) {
429
+ throw new ValidationError(
430
+ `auditor not found: ${auditorId}`,
431
+ 'auditorId',
432
+ undefined,
433
+ ErrorCode.INVALID_INPUT
434
+ )
435
+ }
436
+
437
+ const requestId = generateId('req')
438
+ const now = Math.floor(Date.now() / 1000)
439
+
440
+ const request: DisclosureRequest = {
441
+ requestId,
442
+ transactionId,
443
+ auditorId,
444
+ reason,
445
+ requestedAt: now,
446
+ status: 'pending',
447
+ }
448
+
449
+ this.disclosureRequests.set(requestId, request)
450
+
451
+ this.addAuditLog(auditorId, 'disclosure_requested', {
452
+ requestId,
453
+ transactionId,
454
+ reason,
455
+ })
456
+
457
+ return request
458
+ }
459
+
460
+ /**
461
+ * Approve a disclosure request
462
+ */
463
+ approveDisclosureRequest(requestId: string, approvedBy: string): DisclosureRequest {
464
+ const request = this.disclosureRequests.get(requestId)
465
+ if (!request) {
466
+ throw new ValidationError(
467
+ `request not found: ${requestId}`,
468
+ 'requestId',
469
+ undefined,
470
+ ErrorCode.INVALID_INPUT
471
+ )
472
+ }
473
+
474
+ request.status = 'approved'
475
+ request.approvedBy = approvedBy
476
+ request.resolvedAt = Math.floor(Date.now() / 1000)
477
+
478
+ this.addAuditLog(approvedBy, 'disclosure_approved', {
479
+ requestId,
480
+ transactionId: request.transactionId,
481
+ auditorId: request.auditorId,
482
+ })
483
+
484
+ return request
485
+ }
486
+
487
+ /**
488
+ * Deny a disclosure request
489
+ */
490
+ denyDisclosureRequest(
491
+ requestId: string,
492
+ deniedBy: string,
493
+ reason: string,
494
+ ): DisclosureRequest {
495
+ const request = this.disclosureRequests.get(requestId)
496
+ if (!request) {
497
+ throw new ValidationError(
498
+ `request not found: ${requestId}`,
499
+ 'requestId',
500
+ undefined,
501
+ ErrorCode.INVALID_INPUT
502
+ )
503
+ }
504
+
505
+ request.status = 'denied'
506
+ request.approvedBy = deniedBy
507
+ request.resolvedAt = Math.floor(Date.now() / 1000)
508
+ request.denialReason = reason
509
+
510
+ this.addAuditLog(deniedBy, 'disclosure_denied', {
511
+ requestId,
512
+ transactionId: request.transactionId,
513
+ auditorId: request.auditorId,
514
+ reason,
515
+ })
516
+
517
+ return request
518
+ }
519
+
520
+ /**
521
+ * Get pending disclosure requests
522
+ */
523
+ getPendingRequests(): DisclosureRequest[] {
524
+ return Array.from(this.disclosureRequests.values())
525
+ .filter(r => r.status === 'pending')
526
+ }
527
+
528
+ // ─── Reporting ───────────────────────────────────────────────────────────────
529
+
530
+ /**
531
+ * Generate a compliance report
532
+ */
533
+ async generateReport(
534
+ params: GenerateReportParams,
535
+ requestedBy: string,
536
+ ): Promise<ComplianceReport> {
537
+ validateReportParams(params)
538
+
539
+ const reportId = generateId('report')
540
+ const now = Math.floor(Date.now() / 1000)
541
+
542
+ const report: ComplianceReport = {
543
+ reportId,
544
+ type: params.type,
545
+ title: params.title,
546
+ description: params.description,
547
+ format: params.format,
548
+ status: ReportStatus.GENERATING,
549
+ requestedBy,
550
+ requestedAt: now,
551
+ startDate: params.startDate,
552
+ endDate: params.endDate,
553
+ chains: params.chains ?? [],
554
+ tokens: params.tokens ?? [],
555
+ includeInbound: params.includeInbound ?? true,
556
+ includeOutbound: params.includeOutbound ?? true,
557
+ }
558
+
559
+ this.reports.set(reportId, report)
560
+
561
+ // Generate report data
562
+ try {
563
+ const transactions = this.filterTransactions(
564
+ params.startDate,
565
+ params.endDate,
566
+ params.chains,
567
+ params.tokens,
568
+ report.includeInbound,
569
+ report.includeOutbound,
570
+ )
571
+
572
+ const reportData = this.computeReportData(
573
+ transactions,
574
+ params.includeTransactions ?? false,
575
+ )
576
+
577
+ if (params.format === 'json') {
578
+ report.data = reportData
579
+ } else if (params.format === 'csv') {
580
+ report.content = this.generateCSV(transactions)
581
+ }
582
+
583
+ report.status = ReportStatus.COMPLETED
584
+ report.generatedAt = Math.floor(Date.now() / 1000)
585
+ } catch (error) {
586
+ report.status = ReportStatus.FAILED
587
+ report.error = error instanceof Error ? error.message : 'Unknown error'
588
+ }
589
+
590
+ this.addAuditLog(requestedBy, 'report_generated', {
591
+ reportId,
592
+ type: params.type,
593
+ status: report.status,
594
+ })
595
+
596
+ return report
597
+ }
598
+
599
+ /**
600
+ * Get a report by ID
601
+ */
602
+ getReport(reportId: string): ComplianceReport | undefined {
603
+ return this.reports.get(reportId)
604
+ }
605
+
606
+ /**
607
+ * Get all reports
608
+ */
609
+ getAllReports(): ComplianceReport[] {
610
+ return Array.from(this.reports.values())
611
+ }
612
+
613
+ // ─── Audit Log ───────────────────────────────────────────────────────────────
614
+
615
+ /**
616
+ * Get audit log entries
617
+ */
618
+ getAuditLog(options?: {
619
+ startDate?: number
620
+ endDate?: number
621
+ actor?: string
622
+ action?: AuditLogEntry['action']
623
+ limit?: number
624
+ }): AuditLogEntry[] {
625
+ let entries = [...this.auditLog]
626
+
627
+ if (options?.startDate) {
628
+ entries = entries.filter(e => e.timestamp >= options.startDate!)
629
+ }
630
+ if (options?.endDate) {
631
+ entries = entries.filter(e => e.timestamp <= options.endDate!)
632
+ }
633
+ if (options?.actor) {
634
+ entries = entries.filter(e => e.actor === options.actor)
635
+ }
636
+ if (options?.action) {
637
+ entries = entries.filter(e => e.action === options.action)
638
+ }
639
+ if (options?.limit) {
640
+ entries = entries.slice(-options.limit)
641
+ }
642
+
643
+ return entries
644
+ }
645
+
646
+ // ─── Export ──────────────────────────────────────────────────────────────────
647
+
648
+ /**
649
+ * Export transactions to CSV
650
+ */
651
+ exportToCSV(auditorId?: string): string {
652
+ const transactions = this.getDisclosedTransactions(auditorId)
653
+ return this.generateCSV(transactions)
654
+ }
655
+
656
+ /**
657
+ * Export transactions to JSON
658
+ */
659
+ exportToJSON(auditorId?: string): string {
660
+ const transactions = this.getDisclosedTransactions(auditorId)
661
+ return JSON.stringify(transactions, (_, value) =>
662
+ typeof value === 'bigint' ? value.toString() : value
663
+ , 2)
664
+ }
665
+
666
+ // ─── Serialization ───────────────────────────────────────────────────────────
667
+
668
+ /**
669
+ * Serialize to JSON
670
+ */
671
+ toJSON(): string {
672
+ return JSON.stringify({
673
+ config: this.config,
674
+ auditors: Array.from(this.auditors.entries()),
675
+ disclosedTransactions: Array.from(this.disclosedTransactions.entries()),
676
+ reports: Array.from(this.reports.entries()),
677
+ disclosureRequests: Array.from(this.disclosureRequests.entries()),
678
+ auditLog: this.auditLog,
679
+ }, (_, value) => typeof value === 'bigint' ? value.toString() : value)
680
+ }
681
+
682
+ /**
683
+ * Deserialize from JSON
684
+ */
685
+ static fromJSON(json: string): ComplianceManager {
686
+ const data = JSON.parse(json, (key, value) => {
687
+ if (typeof value === 'string' && /^\d+$/.test(value) &&
688
+ ['amount', 'highValueThreshold', 'minAmount', 'maxAmount'].includes(key)) {
689
+ return BigInt(value)
690
+ }
691
+ return value
692
+ })
693
+
694
+ const manager = new ComplianceManager(data.config)
695
+ manager.auditors = new Map(data.auditors)
696
+ manager.disclosedTransactions = new Map(data.disclosedTransactions)
697
+ manager.reports = new Map(data.reports)
698
+ manager.disclosureRequests = new Map(data.disclosureRequests)
699
+ manager.auditLog = data.auditLog
700
+
701
+ return manager
702
+ }
703
+
704
+ // ─── Private Helpers ─────────────────────────────────────────────────────────
705
+
706
+ private addAuditLog(
707
+ actor: string,
708
+ action: AuditLogEntry['action'],
709
+ details: Record<string, unknown>,
710
+ ): void {
711
+ this.auditLog.push({
712
+ entryId: generateId('log'),
713
+ timestamp: Math.floor(Date.now() / 1000),
714
+ actor,
715
+ action,
716
+ details,
717
+ })
718
+ }
719
+
720
+ private isWithinScope(payment: ShieldedPayment, scope: AuditScope): boolean {
721
+ // Check chains
722
+ if (scope.chains.length > 0 && !scope.chains.includes(payment.sourceChain)) {
723
+ return false
724
+ }
725
+
726
+ // Check tokens
727
+ if (scope.tokens.length > 0 && !scope.tokens.includes(payment.token.symbol)) {
728
+ return false
729
+ }
730
+
731
+ // Check date range
732
+ if (scope.startDate && payment.createdAt < scope.startDate) {
733
+ return false
734
+ }
735
+ if (scope.endDate && payment.createdAt > scope.endDate) {
736
+ return false
737
+ }
738
+
739
+ // Check amount range
740
+ if (scope.minAmount && payment.amount < scope.minAmount) {
741
+ return false
742
+ }
743
+ if (scope.maxAmount && payment.amount > scope.maxAmount) {
744
+ return false
745
+ }
746
+
747
+ return true
748
+ }
749
+
750
+ private decryptMemoSafe(encryptedMemo: HexString, viewingKey: ViewingKey): string {
751
+ // This is a simplified implementation
752
+ // In practice, you'd need proper encrypted transaction data
753
+ try {
754
+ const decrypted = decryptWithViewing(
755
+ {
756
+ ciphertext: encryptedMemo,
757
+ nonce: '0x' + '00'.repeat(24) as HexString,
758
+ viewingKeyHash: viewingKey.hash,
759
+ },
760
+ viewingKey
761
+ )
762
+ return decrypted.sender // Simplified - actual implementation varies
763
+ } catch {
764
+ throw new Error('Failed to decrypt memo')
765
+ }
766
+ }
767
+
768
+ private filterTransactions(
769
+ startDate: number,
770
+ endDate: number,
771
+ chains?: ChainId[],
772
+ tokens?: string[],
773
+ includeInbound?: boolean,
774
+ includeOutbound?: boolean,
775
+ ): DisclosedTransaction[] {
776
+ return Array.from(this.disclosedTransactions.values()).filter(tx => {
777
+ if (tx.timestamp < startDate || tx.timestamp > endDate) return false
778
+ if (chains?.length && !chains.includes(tx.chain)) return false
779
+ if (tokens?.length && !tokens.includes(tx.token.symbol)) return false
780
+ if (!includeInbound && tx.direction === 'inbound') return false
781
+ if (!includeOutbound && tx.direction === 'outbound') return false
782
+ return true
783
+ })
784
+ }
785
+
786
+ private computeReportData(
787
+ transactions: DisclosedTransaction[],
788
+ includeTransactions: boolean,
789
+ ): ReportData {
790
+ const volumeByToken: Record<string, bigint> = {}
791
+ const avgByToken: Record<string, bigint> = {}
792
+ const countByToken: Record<string, number> = {}
793
+ const byChain: Record<ChainId, number> = {} as Record<ChainId, number>
794
+ const byPrivacy: Record<PrivacyLevel, number> = {} as Record<PrivacyLevel, number>
795
+ const counterparties = new Set<string>()
796
+
797
+ let totalInbound = 0
798
+ let totalOutbound = 0
799
+ let payments = 0
800
+ let swaps = 0
801
+ let deposits = 0
802
+ let withdrawals = 0
803
+ let lowRisk = 0
804
+ let mediumRisk = 0
805
+ let highRisk = 0
806
+ let flagged = 0
807
+
808
+ const highValueTxs: DisclosedTransaction[] = []
809
+
810
+ for (const tx of transactions) {
811
+ // Volume tracking
812
+ const symbol = tx.token.symbol
813
+ volumeByToken[symbol] = (volumeByToken[symbol] ?? 0n) + tx.amount
814
+ countByToken[symbol] = (countByToken[symbol] ?? 0) + 1
815
+
816
+ // Direction
817
+ if (tx.direction === 'inbound') totalInbound++
818
+ else totalOutbound++
819
+
820
+ // Type
821
+ if (tx.type === 'payment') payments++
822
+ else if (tx.type === 'swap') swaps++
823
+ else if (tx.type === 'deposit') deposits++
824
+ else if (tx.type === 'withdrawal') withdrawals++
825
+
826
+ // Chain
827
+ byChain[tx.chain] = (byChain[tx.chain] ?? 0) + 1
828
+
829
+ // Privacy
830
+ byPrivacy[tx.privacyLevel] = (byPrivacy[tx.privacyLevel] ?? 0) + 1
831
+
832
+ // Counterparty
833
+ counterparties.add(tx.recipient)
834
+ counterparties.add(tx.sender)
835
+
836
+ // Risk
837
+ if (tx.riskScore !== undefined) {
838
+ if (tx.riskScore < 30) lowRisk++
839
+ else if (tx.riskScore < 70) mediumRisk++
840
+ else highRisk++
841
+ }
842
+ if (tx.riskFlags?.length) flagged++
843
+
844
+ // High value
845
+ if (tx.amount >= this.config.highValueThreshold) {
846
+ highValueTxs.push(tx)
847
+ }
848
+ }
849
+
850
+ // Calculate averages
851
+ for (const symbol of Object.keys(volumeByToken)) {
852
+ avgByToken[symbol] = volumeByToken[symbol] / BigInt(countByToken[symbol])
853
+ }
854
+
855
+ const data: ReportData = {
856
+ summary: {
857
+ totalTransactions: transactions.length,
858
+ totalInbound,
859
+ totalOutbound,
860
+ totalVolume: volumeByToken,
861
+ uniqueCounterparties: counterparties.size,
862
+ averageTransactionSize: avgByToken,
863
+ dateRange: {
864
+ start: Math.min(...transactions.map(t => t.timestamp)),
865
+ end: Math.max(...transactions.map(t => t.timestamp)),
866
+ },
867
+ },
868
+ byType: { payments, swaps, deposits, withdrawals },
869
+ byChain,
870
+ byPrivacyLevel: byPrivacy,
871
+ highValueTransactions: highValueTxs,
872
+ riskSummary: {
873
+ lowRisk,
874
+ mediumRisk,
875
+ highRisk,
876
+ flaggedTransactions: flagged,
877
+ },
878
+ }
879
+
880
+ if (includeTransactions) {
881
+ data.transactions = transactions
882
+ }
883
+
884
+ return data
885
+ }
886
+
887
+ private generateCSV(transactions: DisclosedTransaction[]): string {
888
+ const headers = [
889
+ 'Transaction ID',
890
+ 'Disclosure ID',
891
+ 'Type',
892
+ 'Direction',
893
+ 'Token',
894
+ 'Amount',
895
+ 'Sender',
896
+ 'Recipient',
897
+ 'Chain',
898
+ 'Privacy Level',
899
+ 'Timestamp',
900
+ 'TX Hash',
901
+ 'Block',
902
+ 'Risk Score',
903
+ 'Purpose',
904
+ 'Memo',
905
+ ]
906
+
907
+ const rows = transactions.map(tx => [
908
+ tx.transactionId,
909
+ tx.disclosureId,
910
+ tx.type,
911
+ tx.direction,
912
+ tx.token.symbol,
913
+ tx.amount.toString(),
914
+ tx.sender,
915
+ tx.recipient,
916
+ tx.chain,
917
+ tx.privacyLevel,
918
+ new Date(tx.timestamp * 1000).toISOString(),
919
+ tx.txHash,
920
+ tx.blockNumber.toString(),
921
+ tx.riskScore?.toString() ?? '',
922
+ tx.purpose ?? '',
923
+ tx.memo ?? '',
924
+ ])
925
+
926
+ // Escape CSV cells to prevent formula injection attacks
927
+ // Formulas starting with =, +, -, @, |, or tab can be malicious
928
+ const escapeForCSV = (val: string): string => {
929
+ // First handle formula injection: prefix with single quote if starts with dangerous chars
930
+ let escaped = val
931
+ if (/^[=+\-@|\t]/.test(escaped)) {
932
+ escaped = `'${escaped}`
933
+ }
934
+ // Then escape double quotes and wrap in quotes
935
+ return `"${escaped.replace(/"/g, '""')}"`
936
+ }
937
+
938
+ const csvRows = [headers, ...rows].map(row => row.map(escapeForCSV).join(','))
939
+
940
+ return csvRows.join('\n')
941
+ }
942
+ }
943
+
944
+ // ─── Helpers ───────────────────────────────────────────────────────────────────
945
+
946
+ function generateId(prefix: string): string {
947
+ return `${prefix}_${bytesToHex(randomBytes(12))}`
948
+ }
949
+
950
+ function validateRegisterAuditorParams(params: RegisterAuditorParams): void {
951
+ if (!params.organization?.trim()) {
952
+ throw new ValidationError(
953
+ 'organization is required',
954
+ 'organization',
955
+ undefined,
956
+ ErrorCode.MISSING_REQUIRED
957
+ )
958
+ }
959
+ if (!params.contactName?.trim()) {
960
+ throw new ValidationError(
961
+ 'contact name is required',
962
+ 'contactName',
963
+ undefined,
964
+ ErrorCode.MISSING_REQUIRED
965
+ )
966
+ }
967
+ if (!params.contactEmail?.trim()) {
968
+ throw new ValidationError(
969
+ 'contact email is required',
970
+ 'contactEmail',
971
+ undefined,
972
+ ErrorCode.MISSING_REQUIRED
973
+ )
974
+ }
975
+ if (!params.publicKey?.trim()) {
976
+ throw new ValidationError(
977
+ 'public key is required',
978
+ 'publicKey',
979
+ undefined,
980
+ ErrorCode.MISSING_REQUIRED
981
+ )
982
+ }
983
+ if (!params.scope) {
984
+ throw new ValidationError(
985
+ 'audit scope is required',
986
+ 'scope',
987
+ undefined,
988
+ ErrorCode.MISSING_REQUIRED
989
+ )
990
+ }
991
+ }
992
+
993
+ function validateReportParams(params: GenerateReportParams): void {
994
+ if (!params.title?.trim()) {
995
+ throw new ValidationError(
996
+ 'report title is required',
997
+ 'title',
998
+ undefined,
999
+ ErrorCode.MISSING_REQUIRED
1000
+ )
1001
+ }
1002
+ if (!params.type) {
1003
+ throw new ValidationError(
1004
+ 'report type is required',
1005
+ 'type',
1006
+ undefined,
1007
+ ErrorCode.MISSING_REQUIRED
1008
+ )
1009
+ }
1010
+ if (!params.format) {
1011
+ throw new ValidationError(
1012
+ 'report format is required',
1013
+ 'format',
1014
+ undefined,
1015
+ ErrorCode.MISSING_REQUIRED
1016
+ )
1017
+ }
1018
+ if (params.startDate === undefined || params.startDate === null ||
1019
+ params.endDate === undefined || params.endDate === null) {
1020
+ throw new ValidationError(
1021
+ 'date range is required',
1022
+ 'dateRange',
1023
+ undefined,
1024
+ ErrorCode.MISSING_REQUIRED
1025
+ )
1026
+ }
1027
+ if (params.startDate >= params.endDate) {
1028
+ throw new ValidationError(
1029
+ 'start date must be before end date',
1030
+ 'dateRange',
1031
+ undefined,
1032
+ ErrorCode.INVALID_INPUT
1033
+ )
1034
+ }
1035
+ }