@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,623 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shielded Payments for SIP Protocol
|
|
3
|
+
*
|
|
4
|
+
* Provides privacy-preserving stablecoin transfers using stealth addresses
|
|
5
|
+
* and Pedersen commitments. Optimized for P2P payments with lower latency
|
|
6
|
+
* than cross-chain swaps.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // Create a shielded USDC payment
|
|
11
|
+
* const payment = await new PaymentBuilder()
|
|
12
|
+
* .token('USDC', 'ethereum')
|
|
13
|
+
* .amount(100n * 10n ** 6n) // 100 USDC
|
|
14
|
+
* .recipient(recipientMetaAddress)
|
|
15
|
+
* .privacy('shielded')
|
|
16
|
+
* .memo('Payment for services')
|
|
17
|
+
* .build()
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
SIP_VERSION,
|
|
23
|
+
PrivacyLevel,
|
|
24
|
+
PaymentStatus,
|
|
25
|
+
type ShieldedPayment,
|
|
26
|
+
type CreatePaymentParams,
|
|
27
|
+
type TrackedPayment,
|
|
28
|
+
type Asset,
|
|
29
|
+
type ChainId,
|
|
30
|
+
type StablecoinSymbol,
|
|
31
|
+
type HexString,
|
|
32
|
+
type Hash,
|
|
33
|
+
type PaymentPurpose,
|
|
34
|
+
} from '@sip-protocol/types'
|
|
35
|
+
import { sha256 } from '@noble/hashes/sha256'
|
|
36
|
+
import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils'
|
|
37
|
+
import { xchacha20poly1305 } from '@noble/ciphers/chacha.js'
|
|
38
|
+
import { hkdf } from '@noble/hashes/hkdf'
|
|
39
|
+
|
|
40
|
+
import { generateStealthAddress, decodeStealthMetaAddress } from '../stealth'
|
|
41
|
+
import { createCommitment, generateIntentId, hash } from '../crypto'
|
|
42
|
+
import { getPrivacyConfig } from '../privacy'
|
|
43
|
+
import { ValidationError, ErrorCode } from '../errors'
|
|
44
|
+
import { isValidChainId, isValidPrivacyLevel, isValidStealthMetaAddress } from '../validation'
|
|
45
|
+
import { secureWipe } from '../secure-memory'
|
|
46
|
+
import { getStablecoin, isStablecoin, STABLECOIN_DECIMALS } from './stablecoins'
|
|
47
|
+
import type { ProofProvider } from '../proofs'
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Options for creating a shielded payment
|
|
51
|
+
*/
|
|
52
|
+
export interface CreatePaymentOptions {
|
|
53
|
+
/** Sender address (for ownership proof) */
|
|
54
|
+
senderAddress?: string
|
|
55
|
+
/** Proof provider for generating ZK proofs */
|
|
56
|
+
proofProvider?: ProofProvider
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Builder class for creating shielded payments
|
|
61
|
+
*
|
|
62
|
+
* Provides a fluent interface for constructing privacy-preserving payments.
|
|
63
|
+
*/
|
|
64
|
+
export class PaymentBuilder {
|
|
65
|
+
private _token?: Asset
|
|
66
|
+
private _amount?: bigint
|
|
67
|
+
private _recipientMetaAddress?: string
|
|
68
|
+
private _recipientAddress?: string
|
|
69
|
+
private _privacy: PrivacyLevel = PrivacyLevel.SHIELDED
|
|
70
|
+
private _viewingKey?: HexString
|
|
71
|
+
private _sourceChain?: ChainId
|
|
72
|
+
private _destinationChain?: ChainId
|
|
73
|
+
private _purpose?: PaymentPurpose
|
|
74
|
+
private _memo?: string
|
|
75
|
+
private _ttl: number = 3600 // 1 hour default
|
|
76
|
+
private _senderAddress?: string
|
|
77
|
+
private _proofProvider?: ProofProvider
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Set the token to transfer
|
|
81
|
+
*
|
|
82
|
+
* @param tokenOrSymbol - Asset object or stablecoin symbol
|
|
83
|
+
* @param chain - Chain ID (required if using symbol)
|
|
84
|
+
*/
|
|
85
|
+
token(tokenOrSymbol: Asset | StablecoinSymbol, chain?: ChainId): this {
|
|
86
|
+
if (typeof tokenOrSymbol === 'string') {
|
|
87
|
+
// It's a stablecoin symbol
|
|
88
|
+
if (!chain) {
|
|
89
|
+
throw new ValidationError(
|
|
90
|
+
'chain is required when using stablecoin symbol',
|
|
91
|
+
'chain',
|
|
92
|
+
undefined,
|
|
93
|
+
ErrorCode.MISSING_REQUIRED
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
if (!isStablecoin(tokenOrSymbol)) {
|
|
97
|
+
throw new ValidationError(
|
|
98
|
+
`unknown stablecoin: ${tokenOrSymbol}`,
|
|
99
|
+
'token',
|
|
100
|
+
{ received: tokenOrSymbol },
|
|
101
|
+
ErrorCode.INVALID_INPUT
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
const asset = getStablecoin(tokenOrSymbol, chain)
|
|
105
|
+
if (!asset) {
|
|
106
|
+
throw new ValidationError(
|
|
107
|
+
`${tokenOrSymbol} is not available on ${chain}`,
|
|
108
|
+
'token',
|
|
109
|
+
{ symbol: tokenOrSymbol, chain },
|
|
110
|
+
ErrorCode.INVALID_INPUT
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
this._token = asset
|
|
114
|
+
this._sourceChain = chain
|
|
115
|
+
} else {
|
|
116
|
+
// It's an Asset object
|
|
117
|
+
this._token = tokenOrSymbol
|
|
118
|
+
this._sourceChain = tokenOrSymbol.chain
|
|
119
|
+
}
|
|
120
|
+
return this
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Set the amount to transfer (in smallest units)
|
|
125
|
+
*
|
|
126
|
+
* @param amount - Amount in token's smallest units
|
|
127
|
+
*/
|
|
128
|
+
amount(amount: bigint): this {
|
|
129
|
+
if (amount <= 0n) {
|
|
130
|
+
throw new ValidationError(
|
|
131
|
+
'amount must be positive',
|
|
132
|
+
'amount',
|
|
133
|
+
{ received: amount.toString() },
|
|
134
|
+
ErrorCode.INVALID_INPUT
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
this._amount = amount
|
|
138
|
+
return this
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Set the amount in human-readable format
|
|
143
|
+
*
|
|
144
|
+
* @param amount - Human-readable amount (e.g., 100.50)
|
|
145
|
+
*/
|
|
146
|
+
amountHuman(amount: number): this {
|
|
147
|
+
if (!this._token) {
|
|
148
|
+
throw new ValidationError(
|
|
149
|
+
'token must be set before amountHuman',
|
|
150
|
+
'token',
|
|
151
|
+
undefined,
|
|
152
|
+
ErrorCode.MISSING_REQUIRED
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
const decimals = this._token.decimals
|
|
156
|
+
this._amount = BigInt(Math.floor(amount * (10 ** decimals)))
|
|
157
|
+
return this
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Set the recipient's stealth meta-address (for privacy modes)
|
|
162
|
+
*/
|
|
163
|
+
recipient(metaAddress: string): this {
|
|
164
|
+
if (!isValidStealthMetaAddress(metaAddress)) {
|
|
165
|
+
throw new ValidationError(
|
|
166
|
+
'invalid stealth meta-address format',
|
|
167
|
+
'recipientMetaAddress',
|
|
168
|
+
undefined,
|
|
169
|
+
ErrorCode.INVALID_INPUT
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
this._recipientMetaAddress = metaAddress
|
|
173
|
+
this._recipientAddress = undefined // Clear direct address
|
|
174
|
+
return this
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Set the recipient's direct address (for transparent mode)
|
|
179
|
+
*/
|
|
180
|
+
recipientDirect(address: string): this {
|
|
181
|
+
if (!address || address.trim().length === 0) {
|
|
182
|
+
throw new ValidationError(
|
|
183
|
+
'address must be a non-empty string',
|
|
184
|
+
'recipientAddress',
|
|
185
|
+
undefined,
|
|
186
|
+
ErrorCode.INVALID_INPUT
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
this._recipientAddress = address
|
|
190
|
+
this._recipientMetaAddress = undefined // Clear stealth address
|
|
191
|
+
return this
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Set the privacy level
|
|
196
|
+
*/
|
|
197
|
+
privacy(level: PrivacyLevel): this {
|
|
198
|
+
if (!isValidPrivacyLevel(level)) {
|
|
199
|
+
throw new ValidationError(
|
|
200
|
+
`invalid privacy level: ${level}`,
|
|
201
|
+
'privacy',
|
|
202
|
+
{ received: level },
|
|
203
|
+
ErrorCode.INVALID_PRIVACY_LEVEL
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
this._privacy = level
|
|
207
|
+
return this
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Set the viewing key (required for compliant mode)
|
|
212
|
+
*/
|
|
213
|
+
viewingKey(key: HexString): this {
|
|
214
|
+
this._viewingKey = key
|
|
215
|
+
return this
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Set the destination chain (for cross-chain payments)
|
|
220
|
+
*/
|
|
221
|
+
destinationChain(chain: ChainId): this {
|
|
222
|
+
if (!isValidChainId(chain)) {
|
|
223
|
+
throw new ValidationError(
|
|
224
|
+
`invalid chain: ${chain}`,
|
|
225
|
+
'destinationChain',
|
|
226
|
+
{ received: chain },
|
|
227
|
+
ErrorCode.INVALID_INPUT
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
this._destinationChain = chain
|
|
231
|
+
return this
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Set the payment purpose
|
|
236
|
+
*/
|
|
237
|
+
purpose(purpose: PaymentPurpose): this {
|
|
238
|
+
this._purpose = purpose
|
|
239
|
+
return this
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Set an optional memo/reference
|
|
244
|
+
*/
|
|
245
|
+
memo(memo: string): this {
|
|
246
|
+
if (memo.length > 256) {
|
|
247
|
+
throw new ValidationError(
|
|
248
|
+
'memo must be 256 characters or less',
|
|
249
|
+
'memo',
|
|
250
|
+
{ received: memo.length },
|
|
251
|
+
ErrorCode.INVALID_INPUT
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
this._memo = memo
|
|
255
|
+
return this
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Set time-to-live in seconds
|
|
260
|
+
*/
|
|
261
|
+
ttl(seconds: number): this {
|
|
262
|
+
if (seconds <= 0 || !Number.isInteger(seconds)) {
|
|
263
|
+
throw new ValidationError(
|
|
264
|
+
'ttl must be a positive integer',
|
|
265
|
+
'ttl',
|
|
266
|
+
{ received: seconds },
|
|
267
|
+
ErrorCode.INVALID_INPUT
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
this._ttl = seconds
|
|
271
|
+
return this
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Set the sender address
|
|
276
|
+
*/
|
|
277
|
+
sender(address: string): this {
|
|
278
|
+
this._senderAddress = address
|
|
279
|
+
return this
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Set the proof provider
|
|
284
|
+
*/
|
|
285
|
+
withProvider(provider: ProofProvider): this {
|
|
286
|
+
this._proofProvider = provider
|
|
287
|
+
return this
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Build the shielded payment
|
|
292
|
+
*/
|
|
293
|
+
async build(): Promise<ShieldedPayment> {
|
|
294
|
+
// Validate required fields
|
|
295
|
+
if (!this._token) {
|
|
296
|
+
throw new ValidationError(
|
|
297
|
+
'token is required',
|
|
298
|
+
'token',
|
|
299
|
+
undefined,
|
|
300
|
+
ErrorCode.MISSING_REQUIRED
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
if (this._amount === undefined) {
|
|
304
|
+
throw new ValidationError(
|
|
305
|
+
'amount is required',
|
|
306
|
+
'amount',
|
|
307
|
+
undefined,
|
|
308
|
+
ErrorCode.MISSING_REQUIRED
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Build params
|
|
313
|
+
const params: CreatePaymentParams = {
|
|
314
|
+
token: this._token,
|
|
315
|
+
amount: this._amount,
|
|
316
|
+
recipientMetaAddress: this._recipientMetaAddress,
|
|
317
|
+
recipientAddress: this._recipientAddress,
|
|
318
|
+
privacy: this._privacy,
|
|
319
|
+
viewingKey: this._viewingKey,
|
|
320
|
+
sourceChain: this._sourceChain!,
|
|
321
|
+
destinationChain: this._destinationChain,
|
|
322
|
+
purpose: this._purpose,
|
|
323
|
+
memo: this._memo,
|
|
324
|
+
ttl: this._ttl,
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return createShieldedPayment(params, {
|
|
328
|
+
senderAddress: this._senderAddress,
|
|
329
|
+
proofProvider: this._proofProvider,
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Create a shielded payment
|
|
336
|
+
*
|
|
337
|
+
* @param params - Payment creation parameters
|
|
338
|
+
* @param options - Optional configuration
|
|
339
|
+
* @returns Promise resolving to the shielded payment
|
|
340
|
+
*/
|
|
341
|
+
export async function createShieldedPayment(
|
|
342
|
+
params: CreatePaymentParams,
|
|
343
|
+
options?: CreatePaymentOptions,
|
|
344
|
+
): Promise<ShieldedPayment> {
|
|
345
|
+
const {
|
|
346
|
+
token,
|
|
347
|
+
amount,
|
|
348
|
+
recipientMetaAddress,
|
|
349
|
+
recipientAddress,
|
|
350
|
+
privacy,
|
|
351
|
+
viewingKey,
|
|
352
|
+
sourceChain,
|
|
353
|
+
destinationChain,
|
|
354
|
+
purpose,
|
|
355
|
+
memo,
|
|
356
|
+
ttl = 3600,
|
|
357
|
+
} = params
|
|
358
|
+
|
|
359
|
+
const { senderAddress, proofProvider } = options ?? {}
|
|
360
|
+
|
|
361
|
+
// Resolve token if it's a symbol
|
|
362
|
+
let resolvedToken: Asset
|
|
363
|
+
if (typeof token === 'string') {
|
|
364
|
+
if (!isStablecoin(token)) {
|
|
365
|
+
throw new ValidationError(
|
|
366
|
+
`unknown stablecoin: ${token}`,
|
|
367
|
+
'token',
|
|
368
|
+
{ received: token },
|
|
369
|
+
ErrorCode.INVALID_INPUT
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
const asset = getStablecoin(token, sourceChain)
|
|
373
|
+
if (!asset) {
|
|
374
|
+
throw new ValidationError(
|
|
375
|
+
`${token} is not available on ${sourceChain}`,
|
|
376
|
+
'token',
|
|
377
|
+
{ symbol: token, chain: sourceChain },
|
|
378
|
+
ErrorCode.INVALID_INPUT
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
resolvedToken = asset
|
|
382
|
+
} else {
|
|
383
|
+
resolvedToken = token
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Validate privacy requirements
|
|
387
|
+
if (privacy !== PrivacyLevel.TRANSPARENT && !recipientMetaAddress) {
|
|
388
|
+
throw new ValidationError(
|
|
389
|
+
'recipientMetaAddress is required for shielded/compliant privacy modes',
|
|
390
|
+
'recipientMetaAddress',
|
|
391
|
+
undefined,
|
|
392
|
+
ErrorCode.MISSING_REQUIRED
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
if (privacy === PrivacyLevel.TRANSPARENT && !recipientAddress) {
|
|
396
|
+
throw new ValidationError(
|
|
397
|
+
'recipientAddress is required for transparent mode',
|
|
398
|
+
'recipientAddress',
|
|
399
|
+
undefined,
|
|
400
|
+
ErrorCode.MISSING_REQUIRED
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
if (privacy === PrivacyLevel.COMPLIANT && !viewingKey) {
|
|
404
|
+
throw new ValidationError(
|
|
405
|
+
'viewingKey is required for compliant mode',
|
|
406
|
+
'viewingKey',
|
|
407
|
+
undefined,
|
|
408
|
+
ErrorCode.MISSING_REQUIRED
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Generate payment ID
|
|
413
|
+
const paymentId = generateIntentId()
|
|
414
|
+
|
|
415
|
+
// Calculate viewing key hash
|
|
416
|
+
let viewingKeyHash: Hash | undefined
|
|
417
|
+
if (viewingKey) {
|
|
418
|
+
const keyHex = viewingKey.startsWith('0x') ? viewingKey.slice(2) : viewingKey
|
|
419
|
+
const keyBytes = hexToBytes(keyHex)
|
|
420
|
+
viewingKeyHash = `0x${bytesToHex(sha256(keyBytes))}` as Hash
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Get privacy config
|
|
424
|
+
const privacyConfig = getPrivacyConfig(
|
|
425
|
+
privacy,
|
|
426
|
+
viewingKey ? { key: viewingKey, path: 'm/0', hash: viewingKeyHash! } : undefined,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
const now = Math.floor(Date.now() / 1000)
|
|
430
|
+
|
|
431
|
+
// Create the base payment object
|
|
432
|
+
const payment: ShieldedPayment = {
|
|
433
|
+
paymentId,
|
|
434
|
+
version: SIP_VERSION,
|
|
435
|
+
privacyLevel: privacy,
|
|
436
|
+
createdAt: now,
|
|
437
|
+
expiry: now + ttl,
|
|
438
|
+
token: resolvedToken,
|
|
439
|
+
amount,
|
|
440
|
+
sourceChain,
|
|
441
|
+
destinationChain: destinationChain ?? sourceChain,
|
|
442
|
+
purpose,
|
|
443
|
+
viewingKeyHash,
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Handle privacy-specific fields
|
|
447
|
+
if (privacy !== PrivacyLevel.TRANSPARENT && recipientMetaAddress) {
|
|
448
|
+
// Generate stealth address
|
|
449
|
+
const metaAddress = decodeStealthMetaAddress(recipientMetaAddress)
|
|
450
|
+
const { stealthAddress } = generateStealthAddress(metaAddress)
|
|
451
|
+
payment.recipientStealth = stealthAddress
|
|
452
|
+
|
|
453
|
+
// Create commitments
|
|
454
|
+
payment.amountCommitment = createCommitment(amount)
|
|
455
|
+
payment.senderCommitment = createCommitment(
|
|
456
|
+
BigInt(senderAddress ? hash(senderAddress).slice(2, 18) : '0')
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
// Encrypt memo if provided
|
|
460
|
+
if (memo && viewingKey) {
|
|
461
|
+
payment.encryptedMemo = encryptMemo(memo, viewingKey)
|
|
462
|
+
} else {
|
|
463
|
+
payment.memo = memo
|
|
464
|
+
}
|
|
465
|
+
} else {
|
|
466
|
+
// Transparent mode
|
|
467
|
+
payment.recipientAddress = recipientAddress
|
|
468
|
+
payment.memo = memo
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Generate proofs if provider available
|
|
472
|
+
if (privacy !== PrivacyLevel.TRANSPARENT && proofProvider?.isReady) {
|
|
473
|
+
const hexToUint8 = (hex: HexString): Uint8Array => {
|
|
474
|
+
const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex
|
|
475
|
+
return hexToBytes(cleanHex)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Generate funding proof
|
|
479
|
+
const fundingResult = await proofProvider.generateFundingProof({
|
|
480
|
+
balance: amount,
|
|
481
|
+
minimumRequired: amount,
|
|
482
|
+
blindingFactor: hexToUint8(payment.amountCommitment!.blindingFactor as HexString),
|
|
483
|
+
assetId: resolvedToken.symbol,
|
|
484
|
+
userAddress: senderAddress ?? '0x0',
|
|
485
|
+
ownershipSignature: new Uint8Array(64),
|
|
486
|
+
})
|
|
487
|
+
payment.fundingProof = fundingResult.proof
|
|
488
|
+
|
|
489
|
+
// Generate validity proof (as authorization)
|
|
490
|
+
const validityResult = await proofProvider.generateValidityProof({
|
|
491
|
+
intentHash: hash(paymentId) as HexString,
|
|
492
|
+
senderAddress: senderAddress ?? '0x0',
|
|
493
|
+
senderBlinding: hexToUint8(payment.senderCommitment!.blindingFactor as HexString),
|
|
494
|
+
senderSecret: new Uint8Array(32),
|
|
495
|
+
authorizationSignature: new Uint8Array(64),
|
|
496
|
+
nonce: new Uint8Array(32),
|
|
497
|
+
timestamp: now,
|
|
498
|
+
expiry: now + ttl,
|
|
499
|
+
})
|
|
500
|
+
payment.authorizationProof = validityResult.proof
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return payment
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Encrypt a memo using the viewing key
|
|
508
|
+
*/
|
|
509
|
+
function encryptMemo(memo: string, viewingKey: HexString): HexString {
|
|
510
|
+
const keyHex = viewingKey.startsWith('0x') ? viewingKey.slice(2) : viewingKey
|
|
511
|
+
const keyBytes = hexToBytes(keyHex)
|
|
512
|
+
|
|
513
|
+
// Derive encryption key using HKDF
|
|
514
|
+
const encKey = hkdf(sha256, keyBytes, new Uint8Array(0), new Uint8Array(0), 32)
|
|
515
|
+
|
|
516
|
+
try {
|
|
517
|
+
// Generate nonce
|
|
518
|
+
const nonce = randomBytes(24)
|
|
519
|
+
|
|
520
|
+
// Encrypt
|
|
521
|
+
const cipher = xchacha20poly1305(encKey, nonce)
|
|
522
|
+
const plaintext = new TextEncoder().encode(memo)
|
|
523
|
+
const ciphertext = cipher.encrypt(plaintext)
|
|
524
|
+
|
|
525
|
+
// Concatenate nonce + ciphertext
|
|
526
|
+
const result = new Uint8Array(nonce.length + ciphertext.length)
|
|
527
|
+
result.set(nonce)
|
|
528
|
+
result.set(ciphertext, nonce.length)
|
|
529
|
+
|
|
530
|
+
return `0x${bytesToHex(result)}` as HexString
|
|
531
|
+
} finally {
|
|
532
|
+
secureWipe(keyBytes)
|
|
533
|
+
secureWipe(encKey)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Decrypt a memo using the viewing key
|
|
539
|
+
*/
|
|
540
|
+
export function decryptMemo(encryptedMemo: HexString, viewingKey: HexString): string {
|
|
541
|
+
const keyHex = viewingKey.startsWith('0x') ? viewingKey.slice(2) : viewingKey
|
|
542
|
+
const keyBytes = hexToBytes(keyHex)
|
|
543
|
+
|
|
544
|
+
// Derive encryption key using HKDF
|
|
545
|
+
const encKey = hkdf(sha256, keyBytes, new Uint8Array(0), new Uint8Array(0), 32)
|
|
546
|
+
|
|
547
|
+
try {
|
|
548
|
+
// Parse encrypted data
|
|
549
|
+
const dataHex = encryptedMemo.startsWith('0x') ? encryptedMemo.slice(2) : encryptedMemo
|
|
550
|
+
const data = hexToBytes(dataHex)
|
|
551
|
+
|
|
552
|
+
// Extract nonce and ciphertext
|
|
553
|
+
const nonce = data.slice(0, 24)
|
|
554
|
+
const ciphertext = data.slice(24)
|
|
555
|
+
|
|
556
|
+
// Decrypt
|
|
557
|
+
const cipher = xchacha20poly1305(encKey, nonce)
|
|
558
|
+
const plaintext = cipher.decrypt(ciphertext)
|
|
559
|
+
|
|
560
|
+
return new TextDecoder().decode(plaintext)
|
|
561
|
+
} finally {
|
|
562
|
+
secureWipe(keyBytes)
|
|
563
|
+
secureWipe(encKey)
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Track a payment's status
|
|
569
|
+
*/
|
|
570
|
+
export function trackPayment(payment: ShieldedPayment): TrackedPayment {
|
|
571
|
+
return {
|
|
572
|
+
...payment,
|
|
573
|
+
status: PaymentStatus.DRAFT,
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Check if a payment has expired
|
|
579
|
+
*/
|
|
580
|
+
export function isPaymentExpired(payment: ShieldedPayment): boolean {
|
|
581
|
+
return Math.floor(Date.now() / 1000) > payment.expiry
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Get time remaining until payment expires (in seconds)
|
|
586
|
+
*/
|
|
587
|
+
export function getPaymentTimeRemaining(payment: ShieldedPayment): number {
|
|
588
|
+
const remaining = payment.expiry - Math.floor(Date.now() / 1000)
|
|
589
|
+
return Math.max(0, remaining)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Serialize a payment to JSON
|
|
594
|
+
*/
|
|
595
|
+
export function serializePayment(payment: ShieldedPayment): string {
|
|
596
|
+
return JSON.stringify(payment, (_, value) =>
|
|
597
|
+
typeof value === 'bigint' ? value.toString() : value
|
|
598
|
+
)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Deserialize a payment from JSON
|
|
603
|
+
*/
|
|
604
|
+
export function deserializePayment(json: string): ShieldedPayment {
|
|
605
|
+
return JSON.parse(json, (key, value) => {
|
|
606
|
+
if (typeof value === 'string' && /^\d+$/.test(value) && key === 'amount') {
|
|
607
|
+
return BigInt(value)
|
|
608
|
+
}
|
|
609
|
+
return value
|
|
610
|
+
})
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Get a human-readable summary of the payment
|
|
615
|
+
*/
|
|
616
|
+
export function getPaymentSummary(payment: ShieldedPayment): string {
|
|
617
|
+
const privacy = payment.privacyLevel.toUpperCase()
|
|
618
|
+
const amount = Number(payment.amount) / (10 ** payment.token.decimals)
|
|
619
|
+
const token = payment.token.symbol
|
|
620
|
+
const expiry = new Date(payment.expiry * 1000).toISOString()
|
|
621
|
+
|
|
622
|
+
return `[${privacy}] Payment ${payment.paymentId.slice(0, 12)}... ${amount} ${token} (expires: ${expiry})`
|
|
623
|
+
}
|