@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.
- package/dist/index.d.mts +3236 -1554
- package/dist/index.d.ts +3236 -1554
- package/dist/index.js +9185 -3521
- package/dist/index.mjs +8995 -3376
- package/package.json +5 -2
- package/src/adapters/near-intents.ts +48 -35
- package/src/adapters/oneclick-client.ts +9 -1
- package/src/compliance/compliance-manager.ts +1035 -0
- package/src/compliance/index.ts +43 -0
- package/src/index.ts +129 -2
- package/src/payment/index.ts +54 -0
- package/src/payment/payment.ts +623 -0
- package/src/payment/stablecoins.ts +306 -0
- package/src/privacy.ts +127 -94
- package/src/proofs/circuits/fulfillment_proof.json +1 -0
- package/src/proofs/circuits/funding_proof.json +1 -0
- package/src/proofs/circuits/validity_proof.json +1 -0
- package/src/proofs/interface.ts +13 -1
- package/src/proofs/noir.ts +967 -97
- package/src/secure-memory.ts +147 -0
- package/src/sip.ts +399 -37
- package/src/stealth.ts +116 -84
- package/src/treasury/index.ts +43 -0
- package/src/treasury/treasury.ts +911 -0
- package/src/wallet/hardware/index.ts +87 -0
- package/src/wallet/hardware/ledger.ts +628 -0
- package/src/wallet/hardware/mock.ts +667 -0
- package/src/wallet/hardware/trezor.ts +657 -0
- package/src/wallet/hardware/types.ts +317 -0
- package/src/wallet/index.ts +40 -0
- package/src/zcash/shielded-service.ts +59 -1
|
@@ -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
|
-
|
|
106
|
-
key
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
146
|
-
|
|
153
|
+
// Compute hash of the derived key for identification
|
|
154
|
+
const hashBytes = sha256(derivedBytes)
|
|
147
155
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
237
|
-
|
|
259
|
+
try {
|
|
260
|
+
// Generate random nonce (24 bytes for XChaCha20)
|
|
261
|
+
const nonce = randomBytes(NONCE_SIZE)
|
|
238
262
|
|
|
239
|
-
|
|
240
|
-
|
|
263
|
+
// Serialize data to JSON
|
|
264
|
+
const plaintext = utf8ToBytes(JSON.stringify(data))
|
|
241
265
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
266
|
+
// Encrypt with XChaCha20-Poly1305
|
|
267
|
+
const cipher = xchacha20poly1305(key, nonce)
|
|
268
|
+
const ciphertext = cipher.encrypt(plaintext)
|
|
245
269
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
335
|
-
|
|
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
|
-
|
|
356
|
+
`decrypted data exceeds maximum size limit (${MAX_TRANSACTION_DATA_SIZE} bytes)`,
|
|
345
357
|
'transactionData',
|
|
346
|
-
{ received:
|
|
358
|
+
{ received: jsonString.length, max: MAX_TRANSACTION_DATA_SIZE },
|
|
347
359
|
ErrorCode.INVALID_INPUT
|
|
348
360
|
)
|
|
349
361
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
390
|
+
} finally {
|
|
391
|
+
// Securely wipe encryption key after use
|
|
392
|
+
secureWipe(key)
|
|
360
393
|
}
|
|
361
394
|
}
|
|
362
395
|
|