@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,306 @@
1
+ /**
2
+ * Stablecoin Registry for SIP Protocol
3
+ *
4
+ * Provides a comprehensive registry of supported stablecoins across chains.
5
+ * All addresses are verified contract addresses from official sources.
6
+ */
7
+
8
+ import type { Asset, ChainId, StablecoinSymbol } from '@sip-protocol/types'
9
+
10
+ /**
11
+ * Stablecoin metadata
12
+ */
13
+ export interface StablecoinInfo {
14
+ /** Token symbol */
15
+ symbol: StablecoinSymbol
16
+ /** Full name */
17
+ name: string
18
+ /** Issuer/protocol */
19
+ issuer: string
20
+ /** Whether it's fiat-backed, crypto-backed, or algorithmic */
21
+ type: 'fiat-backed' | 'crypto-backed' | 'algorithmic'
22
+ /** Description */
23
+ description: string
24
+ }
25
+
26
+ /**
27
+ * Stablecoin metadata registry
28
+ */
29
+ export const STABLECOIN_INFO: Record<StablecoinSymbol, StablecoinInfo> = {
30
+ USDC: {
31
+ symbol: 'USDC',
32
+ name: 'USD Coin',
33
+ issuer: 'Circle',
34
+ type: 'fiat-backed',
35
+ description: 'Fully-reserved US dollar stablecoin by Circle',
36
+ },
37
+ USDT: {
38
+ symbol: 'USDT',
39
+ name: 'Tether USD',
40
+ issuer: 'Tether',
41
+ type: 'fiat-backed',
42
+ description: 'Largest stablecoin by market cap',
43
+ },
44
+ DAI: {
45
+ symbol: 'DAI',
46
+ name: 'Dai Stablecoin',
47
+ issuer: 'MakerDAO',
48
+ type: 'crypto-backed',
49
+ description: 'Decentralized crypto-collateralized stablecoin',
50
+ },
51
+ BUSD: {
52
+ symbol: 'BUSD',
53
+ name: 'Binance USD',
54
+ issuer: 'Paxos/Binance',
55
+ type: 'fiat-backed',
56
+ description: 'Regulated stablecoin by Paxos',
57
+ },
58
+ FRAX: {
59
+ symbol: 'FRAX',
60
+ name: 'Frax',
61
+ issuer: 'Frax Finance',
62
+ type: 'algorithmic',
63
+ description: 'Fractional-algorithmic stablecoin',
64
+ },
65
+ LUSD: {
66
+ symbol: 'LUSD',
67
+ name: 'Liquity USD',
68
+ issuer: 'Liquity',
69
+ type: 'crypto-backed',
70
+ description: 'ETH-backed stablecoin with 0% interest loans',
71
+ },
72
+ PYUSD: {
73
+ symbol: 'PYUSD',
74
+ name: 'PayPal USD',
75
+ issuer: 'PayPal/Paxos',
76
+ type: 'fiat-backed',
77
+ description: 'PayPal\'s regulated stablecoin',
78
+ },
79
+ }
80
+
81
+ /**
82
+ * Contract addresses by chain
83
+ * Note: null means native or not available on that chain
84
+ *
85
+ * Addresses verified from:
86
+ * - USDC: https://www.circle.com/en/usdc
87
+ * - USDT: https://tether.to/en/transparency
88
+ * - DAI: https://docs.makerdao.com/
89
+ * - Others: Official protocol documentation
90
+ */
91
+ export const STABLECOIN_ADDRESSES: Record<StablecoinSymbol, Partial<Record<ChainId, string>>> = {
92
+ USDC: {
93
+ ethereum: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
94
+ polygon: '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359', // Native USDC
95
+ arbitrum: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', // Native USDC
96
+ optimism: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', // Native USDC
97
+ base: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', // Native USDC
98
+ solana: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // SPL token
99
+ near: 'a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.factory.bridge.near', // Bridged
100
+ },
101
+ USDT: {
102
+ ethereum: '0xdac17f958d2ee523a2206206994597c13d831ec7',
103
+ polygon: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', // PoS USDT
104
+ arbitrum: '0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9',
105
+ optimism: '0x94b008aa00579c1307b0ef2c499ad98a8ce58e58',
106
+ base: '0xfde4c96c8593536e31f229ea8f37b2ada2699bb2',
107
+ solana: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', // SPL token
108
+ near: 'dac17f958d2ee523a2206206994597c13d831ec7.factory.bridge.near', // Bridged
109
+ },
110
+ DAI: {
111
+ ethereum: '0x6b175474e89094c44da98b954eedeac495271d0f',
112
+ polygon: '0x8f3cf7ad23cd3cadbd9735aff958023239c6a063', // PoS DAI
113
+ arbitrum: '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1',
114
+ optimism: '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1',
115
+ base: '0x50c5725949a6f0c72e6c4a641f24049a917db0cb',
116
+ },
117
+ BUSD: {
118
+ ethereum: '0x4fabb145d64652a948d72533023f6e7a623c7c53',
119
+ // Note: BUSD is being phased out, limited chain support
120
+ },
121
+ FRAX: {
122
+ ethereum: '0x853d955acef822db058eb8505911ed77f175b99e',
123
+ polygon: '0x45c32fa6df82ead1e2ef74d17b76547eddfaff89',
124
+ arbitrum: '0x17fc002b466eec40dae837fc4be5c67993ddbd6f',
125
+ optimism: '0x2e3d870790dc77a83dd1d18184acc7439a53f475',
126
+ },
127
+ LUSD: {
128
+ ethereum: '0x5f98805a4e8be255a32880fdec7f6728c6568ba0',
129
+ arbitrum: '0x93b346b6bc2548da6a1e7d98e9a421b42541425b',
130
+ optimism: '0xc40f949f8a4e094d1b49a23ea9241d289b7b2819',
131
+ },
132
+ PYUSD: {
133
+ ethereum: '0x6c3ea9036406852006290770bedfcaba0e23a0e8',
134
+ // PYUSD is relatively new, limited chain support
135
+ },
136
+ }
137
+
138
+ /**
139
+ * Decimals for each stablecoin
140
+ * Most use 6 decimals (USDC, USDT), but some use 18 (DAI)
141
+ */
142
+ export const STABLECOIN_DECIMALS: Record<StablecoinSymbol, number> = {
143
+ USDC: 6,
144
+ USDT: 6,
145
+ DAI: 18,
146
+ BUSD: 18,
147
+ FRAX: 18,
148
+ LUSD: 18,
149
+ PYUSD: 6,
150
+ }
151
+
152
+ /**
153
+ * Get stablecoin asset for a specific chain
154
+ *
155
+ * @param symbol - Stablecoin symbol (e.g., 'USDC')
156
+ * @param chain - Target chain
157
+ * @returns Asset object or null if not available on chain
158
+ *
159
+ * @example
160
+ * ```typescript
161
+ * const usdc = getStablecoin('USDC', 'ethereum')
162
+ * // { chain: 'ethereum', symbol: 'USDC', address: '0xa0b8...', decimals: 6 }
163
+ *
164
+ * const usdcSol = getStablecoin('USDC', 'solana')
165
+ * // { chain: 'solana', symbol: 'USDC', address: 'EPjF...', decimals: 6 }
166
+ * ```
167
+ */
168
+ export function getStablecoin(symbol: StablecoinSymbol, chain: ChainId): Asset | null {
169
+ const address = STABLECOIN_ADDRESSES[symbol]?.[chain]
170
+ if (!address) {
171
+ return null
172
+ }
173
+
174
+ return {
175
+ chain,
176
+ symbol,
177
+ address: address as `0x${string}`,
178
+ decimals: STABLECOIN_DECIMALS[symbol],
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Get all supported stablecoins for a chain
184
+ *
185
+ * @param chain - Target chain
186
+ * @returns Array of available stablecoin assets
187
+ *
188
+ * @example
189
+ * ```typescript
190
+ * const ethStables = getStablecoinsForChain('ethereum')
191
+ * // [USDC, USDT, DAI, BUSD, FRAX, LUSD, PYUSD]
192
+ * ```
193
+ */
194
+ export function getStablecoinsForChain(chain: ChainId): Asset[] {
195
+ const stables: Asset[] = []
196
+
197
+ for (const symbol of Object.keys(STABLECOIN_ADDRESSES) as StablecoinSymbol[]) {
198
+ const asset = getStablecoin(symbol, chain)
199
+ if (asset) {
200
+ stables.push(asset)
201
+ }
202
+ }
203
+
204
+ return stables
205
+ }
206
+
207
+ /**
208
+ * Check if a token symbol is a supported stablecoin
209
+ */
210
+ export function isStablecoin(symbol: string): symbol is StablecoinSymbol {
211
+ return symbol in STABLECOIN_ADDRESSES
212
+ }
213
+
214
+ /**
215
+ * Get stablecoin info (metadata)
216
+ */
217
+ export function getStablecoinInfo(symbol: StablecoinSymbol): StablecoinInfo {
218
+ return STABLECOIN_INFO[symbol]
219
+ }
220
+
221
+ /**
222
+ * Get all supported stablecoin symbols
223
+ */
224
+ export function getSupportedStablecoins(): StablecoinSymbol[] {
225
+ return Object.keys(STABLECOIN_ADDRESSES) as StablecoinSymbol[]
226
+ }
227
+
228
+ /**
229
+ * Check if a stablecoin is available on a specific chain
230
+ */
231
+ export function isStablecoinOnChain(symbol: StablecoinSymbol, chain: ChainId): boolean {
232
+ return !!STABLECOIN_ADDRESSES[symbol]?.[chain]
233
+ }
234
+
235
+ /**
236
+ * Get all chains where a stablecoin is available
237
+ */
238
+ export function getChainsForStablecoin(symbol: StablecoinSymbol): ChainId[] {
239
+ const addresses = STABLECOIN_ADDRESSES[symbol]
240
+ if (!addresses) return []
241
+ return Object.keys(addresses) as ChainId[]
242
+ }
243
+
244
+ /**
245
+ * Convert human-readable amount to smallest units
246
+ *
247
+ * @param amount - Human-readable amount (e.g., 100.50)
248
+ * @param symbol - Stablecoin symbol
249
+ * @returns Amount in smallest units (e.g., 100500000 for USDC)
250
+ *
251
+ * @example
252
+ * ```typescript
253
+ * toStablecoinUnits(100.50, 'USDC') // 100500000n (6 decimals)
254
+ * toStablecoinUnits(100.50, 'DAI') // 100500000000000000000n (18 decimals)
255
+ * ```
256
+ */
257
+ export function toStablecoinUnits(amount: number, symbol: StablecoinSymbol): bigint {
258
+ const decimals = STABLECOIN_DECIMALS[symbol]
259
+ const factor = 10 ** decimals
260
+ return BigInt(Math.floor(amount * factor))
261
+ }
262
+
263
+ /**
264
+ * Convert smallest units to human-readable amount
265
+ *
266
+ * @param units - Amount in smallest units
267
+ * @param symbol - Stablecoin symbol
268
+ * @returns Human-readable amount
269
+ *
270
+ * @example
271
+ * ```typescript
272
+ * fromStablecoinUnits(100500000n, 'USDC') // 100.5
273
+ * fromStablecoinUnits(100500000000000000000n, 'DAI') // 100.5
274
+ * ```
275
+ */
276
+ export function fromStablecoinUnits(units: bigint, symbol: StablecoinSymbol): number {
277
+ const decimals = STABLECOIN_DECIMALS[symbol]
278
+ const factor = 10 ** decimals
279
+ return Number(units) / factor
280
+ }
281
+
282
+ /**
283
+ * Format stablecoin amount for display
284
+ *
285
+ * @param units - Amount in smallest units
286
+ * @param symbol - Stablecoin symbol
287
+ * @param options - Formatting options
288
+ * @returns Formatted string (e.g., "100.50 USDC")
289
+ */
290
+ export function formatStablecoinAmount(
291
+ units: bigint,
292
+ symbol: StablecoinSymbol,
293
+ options?: {
294
+ includeSymbol?: boolean
295
+ minimumFractionDigits?: number
296
+ maximumFractionDigits?: number
297
+ }
298
+ ): string {
299
+ const amount = fromStablecoinUnits(units, symbol)
300
+ const formatted = amount.toLocaleString('en-US', {
301
+ minimumFractionDigits: options?.minimumFractionDigits ?? 2,
302
+ maximumFractionDigits: options?.maximumFractionDigits ?? 2,
303
+ })
304
+
305
+ return options?.includeSymbol !== false ? `${formatted} ${symbol}` : formatted
306
+ }
package/src/privacy.ts CHANGED
@@ -25,6 +25,7 @@ import { hkdf } from '@noble/hashes/hkdf'
25
25
  import { bytesToHex, hexToBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils'
26
26
  import { xchacha20poly1305 } from '@noble/ciphers/chacha.js'
27
27
  import { ValidationError, CryptoError, ErrorCode } from './errors'
28
+ import { secureWipe } from './secure-memory'
28
29
 
29
30
  /**
30
31
  * Maximum size for decrypted transaction data (1MB)
@@ -99,13 +100,19 @@ export function getPrivacyConfig(
99
100
  */
100
101
  export function generateViewingKey(path: string = 'm/0'): ViewingKey {
101
102
  const keyBytes = randomBytes(32)
102
- const key = `0x${bytesToHex(keyBytes)}` as HexString
103
- const hashBytes = sha256(keyBytes)
104
103
 
105
- return {
106
- key,
107
- path,
108
- hash: `0x${bytesToHex(hashBytes)}` as Hash,
104
+ try {
105
+ const key = `0x${bytesToHex(keyBytes)}` as HexString
106
+ const hashBytes = sha256(keyBytes)
107
+
108
+ return {
109
+ key,
110
+ path,
111
+ hash: `0x${bytesToHex(hashBytes)}` as Hash,
112
+ }
113
+ } finally {
114
+ // Securely wipe key bytes after converting to hex
115
+ secureWipe(keyBytes)
109
116
  }
110
117
  }
111
118
 
@@ -138,17 +145,28 @@ export function deriveViewingKey(
138
145
  // This follows BIP32-style hierarchical derivation
139
146
  const derivedFull = hmac(sha512, masterKeyBytes, childPathBytes)
140
147
 
141
- // Take first 32 bytes as the derived key (standard practice)
142
- const derivedBytes = derivedFull.slice(0, 32)
143
- const derived = `0x${bytesToHex(derivedBytes)}` as HexString
148
+ try {
149
+ // Take first 32 bytes as the derived key (standard practice)
150
+ const derivedBytes = derivedFull.slice(0, 32)
151
+ const derived = `0x${bytesToHex(derivedBytes)}` as HexString
144
152
 
145
- // Compute hash of the derived key for identification
146
- const hashBytes = sha256(derivedBytes)
153
+ // Compute hash of the derived key for identification
154
+ const hashBytes = sha256(derivedBytes)
147
155
 
148
- return {
149
- key: derived,
150
- path: `${masterKey.path}/${childPath}`,
151
- hash: `0x${bytesToHex(hashBytes)}` as Hash,
156
+ const result = {
157
+ key: derived,
158
+ path: `${masterKey.path}/${childPath}`,
159
+ hash: `0x${bytesToHex(hashBytes)}` as Hash,
160
+ }
161
+
162
+ // Wipe derived bytes after conversion to hex
163
+ secureWipe(derivedBytes)
164
+
165
+ return result
166
+ } finally {
167
+ // Securely wipe master key bytes and full derivation output
168
+ secureWipe(masterKeyBytes)
169
+ secureWipe(derivedFull)
152
170
  }
153
171
  }
154
172
 
@@ -172,7 +190,7 @@ const NONCE_SIZE = 24
172
190
  * Uses HKDF-SHA256 with domain separation for security.
173
191
  *
174
192
  * @param viewingKey - The viewing key to derive from
175
- * @returns 32-byte encryption key
193
+ * @returns 32-byte encryption key (caller must wipe after use)
176
194
  */
177
195
  function deriveEncryptionKey(viewingKey: ViewingKey): Uint8Array {
178
196
  // Extract the raw key bytes (remove 0x prefix)
@@ -181,12 +199,17 @@ function deriveEncryptionKey(viewingKey: ViewingKey): Uint8Array {
181
199
  : viewingKey.key
182
200
  const keyBytes = hexToBytes(keyHex)
183
201
 
184
- // Use HKDF to derive a proper encryption key
185
- // HKDF(SHA256, ikm=viewingKey, salt=domain, info=path, length=32)
186
- const salt = utf8ToBytes(ENCRYPTION_DOMAIN)
187
- const info = utf8ToBytes(viewingKey.path)
188
-
189
- return hkdf(sha256, keyBytes, salt, info, 32)
202
+ try {
203
+ // Use HKDF to derive a proper encryption key
204
+ // HKDF(SHA256, ikm=viewingKey, salt=domain, info=path, length=32)
205
+ const salt = utf8ToBytes(ENCRYPTION_DOMAIN)
206
+ const info = utf8ToBytes(viewingKey.path)
207
+
208
+ return hkdf(sha256, keyBytes, salt, info, 32)
209
+ } finally {
210
+ // Securely wipe source key bytes
211
+ secureWipe(keyBytes)
212
+ }
190
213
  }
191
214
 
192
215
  // ─── Transaction Data Type ────────────────────────────────────────────────────
@@ -233,20 +256,25 @@ export function encryptForViewing(
233
256
  // Derive encryption key from viewing key
234
257
  const key = deriveEncryptionKey(viewingKey)
235
258
 
236
- // Generate random nonce (24 bytes for XChaCha20)
237
- const nonce = randomBytes(NONCE_SIZE)
259
+ try {
260
+ // Generate random nonce (24 bytes for XChaCha20)
261
+ const nonce = randomBytes(NONCE_SIZE)
238
262
 
239
- // Serialize data to JSON
240
- const plaintext = utf8ToBytes(JSON.stringify(data))
263
+ // Serialize data to JSON
264
+ const plaintext = utf8ToBytes(JSON.stringify(data))
241
265
 
242
- // Encrypt with XChaCha20-Poly1305
243
- const cipher = xchacha20poly1305(key, nonce)
244
- const ciphertext = cipher.encrypt(plaintext)
266
+ // Encrypt with XChaCha20-Poly1305
267
+ const cipher = xchacha20poly1305(key, nonce)
268
+ const ciphertext = cipher.encrypt(plaintext)
245
269
 
246
- return {
247
- ciphertext: `0x${bytesToHex(ciphertext)}` as HexString,
248
- nonce: `0x${bytesToHex(nonce)}` as HexString,
249
- viewingKeyHash: viewingKey.hash,
270
+ return {
271
+ ciphertext: `0x${bytesToHex(ciphertext)}` as HexString,
272
+ nonce: `0x${bytesToHex(nonce)}` as HexString,
273
+ viewingKeyHash: viewingKey.hash,
274
+ }
275
+ } finally {
276
+ // Securely wipe encryption key after use
277
+ secureWipe(key)
250
278
  }
251
279
  }
252
280
 
@@ -287,76 +315,81 @@ export function decryptWithViewing(
287
315
  // Derive encryption key from viewing key
288
316
  const key = deriveEncryptionKey(viewingKey)
289
317
 
290
- // Parse nonce and ciphertext
291
- const nonceHex = encrypted.nonce.startsWith('0x')
292
- ? encrypted.nonce.slice(2)
293
- : encrypted.nonce
294
- const nonce = hexToBytes(nonceHex)
295
-
296
- const ciphertextHex = encrypted.ciphertext.startsWith('0x')
297
- ? encrypted.ciphertext.slice(2)
298
- : encrypted.ciphertext
299
- const ciphertext = hexToBytes(ciphertextHex)
300
-
301
- // Decrypt with XChaCha20-Poly1305
302
- // This will throw if authentication fails (wrong key or tampered data)
303
- const cipher = xchacha20poly1305(key, nonce)
304
- let plaintext: Uint8Array
305
-
306
318
  try {
307
- plaintext = cipher.decrypt(ciphertext)
308
- } catch (e) {
309
- throw new CryptoError(
310
- 'Decryption failed - authentication tag verification failed. ' +
311
- 'Either the viewing key is incorrect or the data has been tampered with.',
312
- ErrorCode.DECRYPTION_FAILED,
313
- {
314
- cause: e instanceof Error ? e : undefined,
315
- operation: 'decryptWithViewing',
316
- }
317
- )
318
- }
319
+ // Parse nonce and ciphertext
320
+ const nonceHex = encrypted.nonce.startsWith('0x')
321
+ ? encrypted.nonce.slice(2)
322
+ : encrypted.nonce
323
+ const nonce = hexToBytes(nonceHex)
324
+
325
+ const ciphertextHex = encrypted.ciphertext.startsWith('0x')
326
+ ? encrypted.ciphertext.slice(2)
327
+ : encrypted.ciphertext
328
+ const ciphertext = hexToBytes(ciphertextHex)
329
+
330
+ // Decrypt with XChaCha20-Poly1305
331
+ // This will throw if authentication fails (wrong key or tampered data)
332
+ const cipher = xchacha20poly1305(key, nonce)
333
+ let plaintext: Uint8Array
334
+
335
+ try {
336
+ plaintext = cipher.decrypt(ciphertext)
337
+ } catch (e) {
338
+ throw new CryptoError(
339
+ 'Decryption failed - authentication tag verification failed. ' +
340
+ 'Either the viewing key is incorrect or the data has been tampered with.',
341
+ ErrorCode.DECRYPTION_FAILED,
342
+ {
343
+ cause: e instanceof Error ? e : undefined,
344
+ operation: 'decryptWithViewing',
345
+ }
346
+ )
347
+ }
319
348
 
320
- // Parse JSON
321
- const textDecoder = new TextDecoder()
322
- const jsonString = textDecoder.decode(plaintext)
323
-
324
- // Validate size before parsing to prevent DoS
325
- if (jsonString.length > MAX_TRANSACTION_DATA_SIZE) {
326
- throw new ValidationError(
327
- `decrypted data exceeds maximum size limit (${MAX_TRANSACTION_DATA_SIZE} bytes)`,
328
- 'transactionData',
329
- { received: jsonString.length, max: MAX_TRANSACTION_DATA_SIZE },
330
- ErrorCode.INVALID_INPUT
331
- )
332
- }
349
+ // Parse JSON
350
+ const textDecoder = new TextDecoder()
351
+ const jsonString = textDecoder.decode(plaintext)
333
352
 
334
- try {
335
- const data = JSON.parse(jsonString) as TransactionData
336
- // Validate required fields
337
- if (
338
- typeof data.sender !== 'string' ||
339
- typeof data.recipient !== 'string' ||
340
- typeof data.amount !== 'string' ||
341
- typeof data.timestamp !== 'number'
342
- ) {
353
+ // Validate size before parsing to prevent DoS
354
+ if (jsonString.length > MAX_TRANSACTION_DATA_SIZE) {
343
355
  throw new ValidationError(
344
- 'invalid transaction data format',
356
+ `decrypted data exceeds maximum size limit (${MAX_TRANSACTION_DATA_SIZE} bytes)`,
345
357
  'transactionData',
346
- { received: data },
358
+ { received: jsonString.length, max: MAX_TRANSACTION_DATA_SIZE },
347
359
  ErrorCode.INVALID_INPUT
348
360
  )
349
361
  }
350
- return data
351
- } catch (e) {
352
- if (e instanceof SyntaxError) {
353
- throw new CryptoError(
354
- 'Decryption succeeded but data is malformed JSON',
355
- ErrorCode.DECRYPTION_FAILED,
356
- { cause: e, operation: 'decryptWithViewing' }
357
- )
362
+
363
+ try {
364
+ const data = JSON.parse(jsonString) as TransactionData
365
+ // Validate required fields
366
+ if (
367
+ typeof data.sender !== 'string' ||
368
+ typeof data.recipient !== 'string' ||
369
+ typeof data.amount !== 'string' ||
370
+ typeof data.timestamp !== 'number'
371
+ ) {
372
+ throw new ValidationError(
373
+ 'invalid transaction data format',
374
+ 'transactionData',
375
+ { received: data },
376
+ ErrorCode.INVALID_INPUT
377
+ )
378
+ }
379
+ return data
380
+ } catch (e) {
381
+ if (e instanceof SyntaxError) {
382
+ throw new CryptoError(
383
+ 'Decryption succeeded but data is malformed JSON',
384
+ ErrorCode.DECRYPTION_FAILED,
385
+ { cause: e, operation: 'decryptWithViewing' }
386
+ )
387
+ }
388
+ throw e
358
389
  }
359
- throw e
390
+ } finally {
391
+ // Securely wipe encryption key after use
392
+ secureWipe(key)
360
393
  }
361
394
  }
362
395