@sip-protocol/sdk 0.3.2 → 0.5.0

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 (64) hide show
  1. package/dist/browser.d.mts +2 -2
  2. package/dist/browser.d.ts +2 -2
  3. package/dist/browser.js +2881 -295
  4. package/dist/browser.mjs +62 -2
  5. package/dist/chunk-AOZIY3GU.mjs +12995 -0
  6. package/dist/chunk-BCLIX5T2.mjs +12940 -0
  7. package/dist/chunk-DMHBKRWV.mjs +14712 -0
  8. package/dist/chunk-FKXPHKYD.mjs +12955 -0
  9. package/dist/chunk-HGU6HZRC.mjs +231 -0
  10. package/dist/chunk-J4Q4NJ2U.mjs +13544 -0
  11. package/dist/chunk-OPQ2GQIO.mjs +13013 -0
  12. package/dist/chunk-W2B7T6WU.mjs +14714 -0
  13. package/dist/index-5jAdWMA-.d.ts +8973 -0
  14. package/dist/index-B9Vkpaao.d.mts +8973 -0
  15. package/dist/index-BcWNakUD.d.ts +7990 -0
  16. package/dist/index-BsKY3Hr0.d.mts +7990 -0
  17. package/dist/index.d.mts +2 -2
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +2852 -266
  20. package/dist/index.mjs +62 -2
  21. package/dist/proofs/noir.mjs +1 -1
  22. package/package.json +2 -1
  23. package/src/adapters/near-intents.ts +8 -0
  24. package/src/bitcoin/index.ts +51 -0
  25. package/src/bitcoin/silent-payments.ts +865 -0
  26. package/src/bitcoin/taproot.ts +590 -0
  27. package/src/compliance/compliance-manager.ts +87 -0
  28. package/src/compliance/conditional-threshold.ts +379 -0
  29. package/src/compliance/conditional.ts +382 -0
  30. package/src/compliance/derivation.ts +489 -0
  31. package/src/compliance/index.ts +50 -8
  32. package/src/compliance/pdf.ts +365 -0
  33. package/src/compliance/reports.ts +644 -0
  34. package/src/compliance/threshold.ts +529 -0
  35. package/src/compliance/types.ts +223 -0
  36. package/src/cosmos/ibc-stealth.ts +825 -0
  37. package/src/cosmos/index.ts +83 -0
  38. package/src/cosmos/stealth.ts +487 -0
  39. package/src/errors.ts +8 -0
  40. package/src/index.ts +80 -1
  41. package/src/move/aptos.ts +369 -0
  42. package/src/move/index.ts +35 -0
  43. package/src/move/sui.ts +367 -0
  44. package/src/oracle/types.ts +8 -0
  45. package/src/settlement/backends/direct-chain.ts +8 -0
  46. package/src/stealth.ts +3 -3
  47. package/src/validation.ts +42 -1
  48. package/src/wallet/aptos/adapter.ts +422 -0
  49. package/src/wallet/aptos/index.ts +10 -0
  50. package/src/wallet/aptos/mock.ts +410 -0
  51. package/src/wallet/aptos/types.ts +278 -0
  52. package/src/wallet/bitcoin/adapter.ts +470 -0
  53. package/src/wallet/bitcoin/index.ts +38 -0
  54. package/src/wallet/bitcoin/mock.ts +516 -0
  55. package/src/wallet/bitcoin/types.ts +274 -0
  56. package/src/wallet/cosmos/adapter.ts +484 -0
  57. package/src/wallet/cosmos/index.ts +63 -0
  58. package/src/wallet/cosmos/mock.ts +596 -0
  59. package/src/wallet/cosmos/types.ts +462 -0
  60. package/src/wallet/index.ts +127 -0
  61. package/src/wallet/sui/adapter.ts +471 -0
  62. package/src/wallet/sui/index.ts +10 -0
  63. package/src/wallet/sui/mock.ts +439 -0
  64. package/src/wallet/sui/types.ts +245 -0
@@ -0,0 +1,644 @@
1
+ /**
2
+ * Compliance Report Generation
3
+ *
4
+ * Generates audit reports from viewing keys and encrypted transactions.
5
+ * Provides JSON-formatted reports with decrypted transaction data and
6
+ * summary statistics.
7
+ */
8
+
9
+ import type { EncryptedTransaction, ViewingKey, HexString } from '@sip-protocol/types'
10
+ import { sha256 } from '@noble/hashes/sha256'
11
+ import { hexToBytes, bytesToHex } from '@noble/hashes/utils'
12
+ import { decryptWithViewing } from '../privacy'
13
+ import { generateRandomBytes } from '../crypto'
14
+ import { ValidationError, CryptoError, ErrorCode } from '../errors'
15
+ import type {
16
+ GenerateAuditReportParams,
17
+ AuditReport,
18
+ DecryptedTransaction,
19
+ PdfExportOptions,
20
+ ExportForRegulatorParams,
21
+ RegulatoryExport,
22
+ FATFExport,
23
+ FATFTransaction,
24
+ FINCENExport,
25
+ FINCENTransaction,
26
+ CSVExport,
27
+ } from './types'
28
+ import { generatePdfReport } from './pdf'
29
+
30
+ /**
31
+ * ComplianceReporter - Generates audit reports from encrypted transactions
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * const reporter = new ComplianceReporter()
36
+ *
37
+ * const report = await reporter.generateAuditReport({
38
+ * viewingKey: myViewingKey,
39
+ * transactions: encryptedTransactions,
40
+ * startDate: new Date('2025-01-01'),
41
+ * endDate: new Date('2025-12-31'),
42
+ * format: 'json',
43
+ * })
44
+ *
45
+ * console.log(`Report contains ${report.summary.transactionCount} transactions`)
46
+ * console.log(`Total volume: ${report.summary.totalVolume}`)
47
+ * ```
48
+ */
49
+ export class ComplianceReporter {
50
+ /**
51
+ * Generate an audit report from encrypted transactions
52
+ *
53
+ * Decrypts transactions using the provided viewing key, filters by date range,
54
+ * and generates a comprehensive report with summary statistics.
55
+ *
56
+ * @param params - Report generation parameters
57
+ * @returns Audit report with decrypted transactions and statistics
58
+ * @throws {ValidationError} If parameters are invalid
59
+ * @throws {CryptoError} If decryption fails
60
+ */
61
+ async generateAuditReport(
62
+ params: GenerateAuditReportParams
63
+ ): Promise<AuditReport> {
64
+ // Validate parameters
65
+ this.validateParams(params)
66
+
67
+ // Normalize viewing key
68
+ const viewingKey = this.normalizeViewingKey(params.viewingKey)
69
+
70
+ // Decrypt and filter transactions
71
+ const decryptedTransactions = this.decryptTransactions(
72
+ params.transactions,
73
+ viewingKey,
74
+ params.startDate,
75
+ params.endDate
76
+ )
77
+
78
+ // Calculate summary statistics
79
+ const summary = this.calculateSummary(decryptedTransactions)
80
+
81
+ // Determine report period
82
+ const period = this.determinePeriod(
83
+ decryptedTransactions,
84
+ params.startDate,
85
+ params.endDate
86
+ )
87
+
88
+ // Generate report ID
89
+ const reportId = `audit_${generateRandomBytes(16).slice(2)}`
90
+
91
+ return {
92
+ reportId,
93
+ generatedAt: new Date(),
94
+ period,
95
+ transactions: decryptedTransactions,
96
+ summary,
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Validate report generation parameters
102
+ */
103
+ private validateParams(params: GenerateAuditReportParams): void {
104
+ if (!params.viewingKey) {
105
+ throw new ValidationError(
106
+ 'viewingKey is required',
107
+ 'viewingKey',
108
+ undefined,
109
+ ErrorCode.MISSING_REQUIRED
110
+ )
111
+ }
112
+
113
+ if (!params.transactions) {
114
+ throw new ValidationError(
115
+ 'transactions array is required',
116
+ 'transactions',
117
+ undefined,
118
+ ErrorCode.MISSING_REQUIRED
119
+ )
120
+ }
121
+
122
+ if (!Array.isArray(params.transactions)) {
123
+ throw new ValidationError(
124
+ 'transactions must be an array',
125
+ 'transactions',
126
+ { received: typeof params.transactions },
127
+ ErrorCode.INVALID_INPUT
128
+ )
129
+ }
130
+
131
+ if (params.format !== 'json' && params.format !== 'pdf') {
132
+ throw new ValidationError(
133
+ 'only JSON and PDF formats are supported',
134
+ 'format',
135
+ { received: params.format },
136
+ ErrorCode.INVALID_INPUT
137
+ )
138
+ }
139
+
140
+ // Validate date range
141
+ if (params.startDate && params.endDate) {
142
+ if (params.startDate > params.endDate) {
143
+ throw new ValidationError(
144
+ 'startDate must be before endDate',
145
+ 'startDate',
146
+ {
147
+ startDate: params.startDate.toISOString(),
148
+ endDate: params.endDate.toISOString(),
149
+ },
150
+ ErrorCode.INVALID_INPUT
151
+ )
152
+ }
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Normalize viewing key to ViewingKey object
158
+ */
159
+ private normalizeViewingKey(
160
+ viewingKey: ViewingKey | string
161
+ ): ViewingKey {
162
+ if (typeof viewingKey === 'string') {
163
+ // Convert string to ViewingKey object
164
+ // For string keys, we need to compute the hash
165
+ const keyHex = viewingKey.startsWith('0x') ? viewingKey.slice(2) : viewingKey
166
+ const keyBytes = hexToBytes(keyHex)
167
+ const hashBytes = sha256(keyBytes)
168
+
169
+ return {
170
+ key: `0x${keyHex}` as HexString,
171
+ path: 'm/0',
172
+ hash: `0x${bytesToHex(hashBytes)}` as HexString,
173
+ }
174
+ }
175
+
176
+ return viewingKey
177
+ }
178
+
179
+ /**
180
+ * Decrypt transactions and filter by date range
181
+ */
182
+ private decryptTransactions(
183
+ encrypted: EncryptedTransaction[],
184
+ viewingKey: ViewingKey,
185
+ startDate?: Date,
186
+ endDate?: Date
187
+ ): DecryptedTransaction[] {
188
+ const decrypted: DecryptedTransaction[] = []
189
+ const errors: Array<{ index: number; error: Error }> = []
190
+
191
+ for (let i = 0; i < encrypted.length; i++) {
192
+ try {
193
+ const txData = decryptWithViewing(encrypted[i], viewingKey)
194
+
195
+ // Filter by date range if specified
196
+ if (startDate || endDate) {
197
+ const txDate = new Date(txData.timestamp * 1000)
198
+
199
+ if (startDate && txDate < startDate) {
200
+ continue
201
+ }
202
+
203
+ if (endDate && txDate > endDate) {
204
+ continue
205
+ }
206
+ }
207
+
208
+ decrypted.push({
209
+ id: `tx_${i}`,
210
+ sender: txData.sender,
211
+ recipient: txData.recipient,
212
+ amount: txData.amount,
213
+ timestamp: txData.timestamp,
214
+ })
215
+ } catch (error) {
216
+ // Collect errors but continue processing other transactions
217
+ errors.push({ index: i, error: error as Error })
218
+ }
219
+ }
220
+
221
+ // If all transactions failed to decrypt, throw an error
222
+ if (decrypted.length === 0 && encrypted.length > 0) {
223
+ throw new CryptoError(
224
+ `Failed to decrypt any transactions. First error: ${errors[0]?.error.message}`,
225
+ ErrorCode.DECRYPTION_FAILED,
226
+ {
227
+ context: {
228
+ totalTransactions: encrypted.length,
229
+ failedCount: errors.length,
230
+ }
231
+ }
232
+ )
233
+ }
234
+
235
+ return decrypted
236
+ }
237
+
238
+ /**
239
+ * Calculate summary statistics
240
+ */
241
+ private calculateSummary(transactions: DecryptedTransaction[]): {
242
+ totalVolume: bigint
243
+ transactionCount: number
244
+ uniqueCounterparties: number
245
+ } {
246
+ let totalVolume = 0n
247
+ const counterparties = new Set<string>()
248
+
249
+ for (const tx of transactions) {
250
+ // Parse amount as bigint
251
+ try {
252
+ const amount = BigInt(tx.amount)
253
+ totalVolume += amount
254
+ } catch (error) {
255
+ // Skip invalid amounts
256
+ console.warn(`Skipping invalid amount in transaction ${tx.id}: ${tx.amount}`)
257
+ }
258
+
259
+ // Collect unique counterparties
260
+ counterparties.add(tx.sender)
261
+ counterparties.add(tx.recipient)
262
+ }
263
+
264
+ return {
265
+ totalVolume,
266
+ transactionCount: transactions.length,
267
+ uniqueCounterparties: counterparties.size,
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Determine report period from transactions and params
273
+ */
274
+ private determinePeriod(
275
+ transactions: DecryptedTransaction[],
276
+ startDate?: Date,
277
+ endDate?: Date
278
+ ): { start: Date; end: Date } {
279
+ // If dates provided, use them
280
+ if (startDate && endDate) {
281
+ return { start: startDate, end: endDate }
282
+ }
283
+
284
+ // Otherwise, derive from transactions
285
+ if (transactions.length === 0) {
286
+ // No transactions - use current date
287
+ const now = new Date()
288
+ return { start: now, end: now }
289
+ }
290
+
291
+ // Find min and max timestamps
292
+ let minTimestamp = transactions[0].timestamp
293
+ let maxTimestamp = transactions[0].timestamp
294
+
295
+ for (const tx of transactions) {
296
+ if (tx.timestamp < minTimestamp) {
297
+ minTimestamp = tx.timestamp
298
+ }
299
+ if (tx.timestamp > maxTimestamp) {
300
+ maxTimestamp = tx.timestamp
301
+ }
302
+ }
303
+
304
+ return {
305
+ start: startDate || new Date(minTimestamp * 1000),
306
+ end: endDate || new Date(maxTimestamp * 1000),
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Export audit report to PDF format
312
+ *
313
+ * Generates a professionally formatted PDF document from an audit report.
314
+ * Works in both Node.js and browser environments.
315
+ *
316
+ * @param report - The audit report to export
317
+ * @param options - PDF export options
318
+ * @returns PDF document as Uint8Array
319
+ *
320
+ * @example
321
+ * ```typescript
322
+ * const reporter = new ComplianceReporter()
323
+ * const report = await reporter.generateAuditReport({...})
324
+ *
325
+ * const pdfBytes = reporter.exportToPdf(report, {
326
+ * title: 'Q1 2025 Audit Report',
327
+ * organization: 'ACME Corp',
328
+ * })
329
+ *
330
+ * // Save to file (Node.js)
331
+ * fs.writeFileSync('report.pdf', pdfBytes)
332
+ * ```
333
+ */
334
+ exportToPdf(report: AuditReport, options?: PdfExportOptions): Uint8Array {
335
+ return generatePdfReport(report, options)
336
+ }
337
+
338
+ /**
339
+ * Export transactions to regulatory compliance formats
340
+ *
341
+ * Decrypts and exports transactions in formats required by regulators:
342
+ * - FATF: Financial Action Task Force Travel Rule format
343
+ * - FINCEN: FinCEN Suspicious Activity Report (SAR) format
344
+ * - CSV: Generic comma-separated values format
345
+ *
346
+ * @param params - Export parameters
347
+ * @returns Regulatory export in the specified format
348
+ * @throws {ValidationError} If parameters are invalid
349
+ * @throws {CryptoError} If decryption fails
350
+ *
351
+ * @example
352
+ * ```typescript
353
+ * const reporter = new ComplianceReporter()
354
+ *
355
+ * // FATF Travel Rule export
356
+ * const fatfExport = await reporter.exportForRegulator({
357
+ * viewingKey: myViewingKey,
358
+ * transactions: encryptedTxs,
359
+ * jurisdiction: 'EU',
360
+ * format: 'FATF',
361
+ * currency: 'EUR',
362
+ * })
363
+ *
364
+ * // FINCEN SAR export (US only)
365
+ * const fincenExport = await reporter.exportForRegulator({
366
+ * viewingKey: myViewingKey,
367
+ * transactions: suspiciousTxs,
368
+ * jurisdiction: 'US',
369
+ * format: 'FINCEN',
370
+ * })
371
+ *
372
+ * // CSV export
373
+ * const csvExport = await reporter.exportForRegulator({
374
+ * viewingKey: myViewingKey,
375
+ * transactions: encryptedTxs,
376
+ * jurisdiction: 'SG',
377
+ * format: 'CSV',
378
+ * })
379
+ * ```
380
+ */
381
+ async exportForRegulator(
382
+ params: ExportForRegulatorParams
383
+ ): Promise<RegulatoryExport> {
384
+ // Validate parameters
385
+ this.validateRegulatoryParams(params)
386
+
387
+ // Normalize viewing key
388
+ const viewingKey = this.normalizeViewingKey(params.viewingKey)
389
+
390
+ // Decrypt transactions
391
+ const decryptedTransactions = this.decryptTransactions(
392
+ params.transactions,
393
+ viewingKey,
394
+ params.startDate,
395
+ params.endDate
396
+ )
397
+
398
+ // Generate report ID
399
+ const reportId = `reg_${generateRandomBytes(16).slice(2)}`
400
+
401
+ // Export based on format
402
+ switch (params.format) {
403
+ case 'FATF':
404
+ return this.exportToFATF(
405
+ reportId,
406
+ decryptedTransactions,
407
+ params.jurisdiction,
408
+ params.currency || 'USD'
409
+ )
410
+ case 'FINCEN':
411
+ return this.exportToFINCEN(
412
+ reportId,
413
+ decryptedTransactions,
414
+ params.startDate,
415
+ params.endDate,
416
+ params.currency || 'USD'
417
+ )
418
+ case 'CSV':
419
+ return this.exportToCSV(
420
+ reportId,
421
+ decryptedTransactions,
422
+ params.jurisdiction,
423
+ params.currency || 'USD'
424
+ )
425
+ default:
426
+ throw new ValidationError(
427
+ `unsupported format: ${params.format}`,
428
+ 'format',
429
+ { received: params.format },
430
+ ErrorCode.INVALID_INPUT
431
+ )
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Validate regulatory export parameters
437
+ */
438
+ private validateRegulatoryParams(params: ExportForRegulatorParams): void {
439
+ if (!params.viewingKey) {
440
+ throw new ValidationError(
441
+ 'viewingKey is required',
442
+ 'viewingKey',
443
+ undefined,
444
+ ErrorCode.MISSING_REQUIRED
445
+ )
446
+ }
447
+
448
+ if (!params.transactions) {
449
+ throw new ValidationError(
450
+ 'transactions array is required',
451
+ 'transactions',
452
+ undefined,
453
+ ErrorCode.MISSING_REQUIRED
454
+ )
455
+ }
456
+
457
+ if (!Array.isArray(params.transactions)) {
458
+ throw new ValidationError(
459
+ 'transactions must be an array',
460
+ 'transactions',
461
+ { received: typeof params.transactions },
462
+ ErrorCode.INVALID_INPUT
463
+ )
464
+ }
465
+
466
+ if (!params.jurisdiction) {
467
+ throw new ValidationError(
468
+ 'jurisdiction is required',
469
+ 'jurisdiction',
470
+ undefined,
471
+ ErrorCode.MISSING_REQUIRED
472
+ )
473
+ }
474
+
475
+ const validJurisdictions = ['US', 'EU', 'UK', 'SG']
476
+ if (!validJurisdictions.includes(params.jurisdiction)) {
477
+ throw new ValidationError(
478
+ `invalid jurisdiction. Must be one of: ${validJurisdictions.join(', ')}`,
479
+ 'jurisdiction',
480
+ { received: params.jurisdiction },
481
+ ErrorCode.INVALID_INPUT
482
+ )
483
+ }
484
+
485
+ if (!params.format) {
486
+ throw new ValidationError(
487
+ 'format is required',
488
+ 'format',
489
+ undefined,
490
+ ErrorCode.MISSING_REQUIRED
491
+ )
492
+ }
493
+
494
+ const validFormats = ['FATF', 'FINCEN', 'CSV']
495
+ if (!validFormats.includes(params.format)) {
496
+ throw new ValidationError(
497
+ `invalid format. Must be one of: ${validFormats.join(', ')}`,
498
+ 'format',
499
+ { received: params.format },
500
+ ErrorCode.INVALID_INPUT
501
+ )
502
+ }
503
+
504
+ // FINCEN is US-only
505
+ if (params.format === 'FINCEN' && params.jurisdiction !== 'US') {
506
+ throw new ValidationError(
507
+ 'FINCEN format is only available for US jurisdiction',
508
+ 'format',
509
+ { jurisdiction: params.jurisdiction, format: params.format },
510
+ ErrorCode.INVALID_INPUT
511
+ )
512
+ }
513
+
514
+ // Validate date range
515
+ if (params.startDate && params.endDate) {
516
+ if (params.startDate > params.endDate) {
517
+ throw new ValidationError(
518
+ 'startDate must be before endDate',
519
+ 'startDate',
520
+ {
521
+ startDate: params.startDate.toISOString(),
522
+ endDate: params.endDate.toISOString(),
523
+ },
524
+ ErrorCode.INVALID_INPUT
525
+ )
526
+ }
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Export to FATF Travel Rule format
532
+ */
533
+ private exportToFATF(
534
+ reportId: string,
535
+ transactions: DecryptedTransaction[],
536
+ jurisdiction: string,
537
+ currency: string
538
+ ): FATFExport {
539
+ const fatfTransactions: FATFTransaction[] = transactions.map((tx) => ({
540
+ originatorAccount: tx.sender,
541
+ beneficiaryAccount: tx.recipient,
542
+ amount: tx.amount,
543
+ currency,
544
+ transactionRef: tx.id,
545
+ timestamp: new Date(tx.timestamp * 1000).toISOString(),
546
+ }))
547
+
548
+ return {
549
+ reportId,
550
+ generatedAt: new Date().toISOString(),
551
+ jurisdiction: jurisdiction as any,
552
+ transactions: fatfTransactions,
553
+ }
554
+ }
555
+
556
+ /**
557
+ * Export to FINCEN SAR format
558
+ */
559
+ private exportToFINCEN(
560
+ reportId: string,
561
+ transactions: DecryptedTransaction[],
562
+ startDate?: Date,
563
+ endDate?: Date,
564
+ currency: string = 'USD'
565
+ ): FINCENExport {
566
+ // Calculate summary
567
+ let totalAmount = 0n
568
+ for (const tx of transactions) {
569
+ try {
570
+ totalAmount += BigInt(tx.amount)
571
+ } catch (error) {
572
+ // Skip invalid amounts
573
+ }
574
+ }
575
+
576
+ // Determine period
577
+ const period = this.determinePeriod(transactions, startDate, endDate)
578
+
579
+ // Convert transactions
580
+ const fincenTransactions: FINCENTransaction[] = transactions.map((tx) => ({
581
+ transactionDate: new Date(tx.timestamp * 1000).toISOString(),
582
+ amount: tx.amount,
583
+ currency,
584
+ narrativeSummary: `Transfer from ${tx.sender} to ${tx.recipient}`,
585
+ transactionRef: tx.id,
586
+ parties: {
587
+ sender: tx.sender,
588
+ recipient: tx.recipient,
589
+ },
590
+ }))
591
+
592
+ return {
593
+ reportId,
594
+ filingType: 'SAR',
595
+ reportDate: new Date().toISOString(),
596
+ jurisdiction: 'US',
597
+ summary: {
598
+ transactionCount: transactions.length,
599
+ totalAmount: totalAmount.toString(),
600
+ period: {
601
+ start: period.start.toISOString(),
602
+ end: period.end.toISOString(),
603
+ },
604
+ },
605
+ transactions: fincenTransactions,
606
+ }
607
+ }
608
+
609
+ /**
610
+ * Export to CSV format
611
+ */
612
+ private exportToCSV(
613
+ reportId: string,
614
+ transactions: DecryptedTransaction[],
615
+ jurisdiction: string,
616
+ currency: string
617
+ ): CSVExport {
618
+ const headers = [
619
+ 'Transaction ID',
620
+ 'Timestamp',
621
+ 'Sender',
622
+ 'Recipient',
623
+ 'Amount',
624
+ 'Currency',
625
+ ]
626
+
627
+ const rows = transactions.map((tx) => [
628
+ tx.id,
629
+ new Date(tx.timestamp * 1000).toISOString(),
630
+ tx.sender,
631
+ tx.recipient,
632
+ tx.amount,
633
+ currency,
634
+ ])
635
+
636
+ return {
637
+ reportId,
638
+ generatedAt: new Date().toISOString(),
639
+ jurisdiction: jurisdiction as any,
640
+ headers,
641
+ rows,
642
+ }
643
+ }
644
+ }