@sip-protocol/sdk 0.4.0 → 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.
- package/dist/browser.d.mts +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +1866 -153
- package/dist/browser.mjs +14 -2
- package/dist/chunk-DMHBKRWV.mjs +14712 -0
- package/dist/chunk-HGU6HZRC.mjs +231 -0
- package/dist/chunk-J4Q4NJ2U.mjs +13544 -0
- package/dist/chunk-W2B7T6WU.mjs +14714 -0
- package/dist/index-5jAdWMA-.d.ts +8973 -0
- package/dist/index-B9Vkpaao.d.mts +8973 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1851 -138
- package/dist/index.mjs +14 -2
- package/dist/proofs/noir.mjs +1 -1
- package/package.json +1 -1
- package/src/compliance/compliance-manager.ts +87 -0
- package/src/compliance/conditional-threshold.ts +379 -0
- package/src/compliance/conditional.ts +382 -0
- package/src/compliance/derivation.ts +489 -0
- package/src/compliance/index.ts +50 -8
- package/src/compliance/pdf.ts +365 -0
- package/src/compliance/reports.ts +644 -0
- package/src/compliance/threshold.ts +529 -0
- package/src/compliance/types.ts +223 -0
- package/src/errors.ts +8 -0
- package/src/index.ts +29 -1
|
@@ -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
|
+
}
|