@sip-protocol/sdk 0.1.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/LICENSE +21 -0
- package/dist/index.d.mts +3640 -0
- package/dist/index.d.ts +3640 -0
- package/dist/index.js +5725 -0
- package/dist/index.mjs +5606 -0
- package/package.json +61 -0
- package/src/adapters/index.ts +19 -0
- package/src/adapters/near-intents.ts +475 -0
- package/src/adapters/oneclick-client.ts +367 -0
- package/src/commitment.ts +470 -0
- package/src/crypto.ts +93 -0
- package/src/errors.ts +471 -0
- package/src/index.ts +369 -0
- package/src/intent.ts +488 -0
- package/src/privacy.ts +382 -0
- package/src/proofs/index.ts +52 -0
- package/src/proofs/interface.ts +228 -0
- package/src/proofs/mock.ts +258 -0
- package/src/proofs/noir.ts +233 -0
- package/src/sip.ts +299 -0
- package/src/solver/index.ts +25 -0
- package/src/solver/mock-solver.ts +278 -0
- package/src/stealth.ts +414 -0
- package/src/validation.ts +401 -0
- package/src/wallet/base-adapter.ts +407 -0
- package/src/wallet/errors.ts +106 -0
- package/src/wallet/ethereum/adapter.ts +655 -0
- package/src/wallet/ethereum/index.ts +48 -0
- package/src/wallet/ethereum/mock.ts +505 -0
- package/src/wallet/ethereum/types.ts +364 -0
- package/src/wallet/index.ts +116 -0
- package/src/wallet/registry.ts +207 -0
- package/src/wallet/solana/adapter.ts +533 -0
- package/src/wallet/solana/index.ts +40 -0
- package/src/wallet/solana/mock.ts +522 -0
- package/src/wallet/solana/types.ts +253 -0
- package/src/zcash/index.ts +53 -0
- package/src/zcash/rpc-client.ts +623 -0
- package/src/zcash/shielded-service.ts +641 -0
package/src/intent.ts
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShieldedIntent class for SIP Protocol
|
|
3
|
+
*
|
|
4
|
+
* Main interface for creating and managing shielded intents.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
SIP_VERSION,
|
|
9
|
+
IntentStatus,
|
|
10
|
+
PrivacyLevel as PrivacyLevelEnum,
|
|
11
|
+
type ShieldedIntent,
|
|
12
|
+
type CreateIntentParams,
|
|
13
|
+
type TrackedIntent,
|
|
14
|
+
type Quote,
|
|
15
|
+
type FulfillmentResult,
|
|
16
|
+
type StealthMetaAddress,
|
|
17
|
+
type Commitment,
|
|
18
|
+
type HexString,
|
|
19
|
+
type Hash,
|
|
20
|
+
type PrivacyLevel,
|
|
21
|
+
} from '@sip-protocol/types'
|
|
22
|
+
import { generateStealthAddress, decodeStealthMetaAddress } from './stealth'
|
|
23
|
+
import {
|
|
24
|
+
createCommitment,
|
|
25
|
+
generateIntentId,
|
|
26
|
+
hash,
|
|
27
|
+
} from './crypto'
|
|
28
|
+
import { hexToBytes, bytesToHex } from '@noble/hashes/utils'
|
|
29
|
+
import { sha256 } from '@noble/hashes/sha256'
|
|
30
|
+
import { getPrivacyConfig, generateViewingKey } from './privacy'
|
|
31
|
+
import type { ProofProvider } from './proofs'
|
|
32
|
+
import { ValidationError } from './errors'
|
|
33
|
+
import {
|
|
34
|
+
validateCreateIntentParams,
|
|
35
|
+
isValidChainId,
|
|
36
|
+
isValidAmount,
|
|
37
|
+
isValidSlippage,
|
|
38
|
+
isValidPrivacyLevel,
|
|
39
|
+
isValidStealthMetaAddress,
|
|
40
|
+
} from './validation'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Options for creating a shielded intent
|
|
44
|
+
*/
|
|
45
|
+
export interface CreateIntentOptions {
|
|
46
|
+
/** Sender address (for ownership proof) */
|
|
47
|
+
senderAddress?: string
|
|
48
|
+
/**
|
|
49
|
+
* Proof provider for generating ZK proofs
|
|
50
|
+
* If provided and privacy level requires proofs, they will be generated automatically
|
|
51
|
+
*/
|
|
52
|
+
proofProvider?: ProofProvider
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Builder class for creating shielded intents
|
|
57
|
+
*/
|
|
58
|
+
export class IntentBuilder {
|
|
59
|
+
private params: Partial<CreateIntentParams> = {}
|
|
60
|
+
private senderAddress?: string
|
|
61
|
+
private proofProvider?: ProofProvider
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Set the input for the intent
|
|
65
|
+
*
|
|
66
|
+
* @throws {ValidationError} If chain or amount is invalid
|
|
67
|
+
*/
|
|
68
|
+
input(
|
|
69
|
+
chain: string,
|
|
70
|
+
token: string,
|
|
71
|
+
amount: number | bigint,
|
|
72
|
+
sourceAddress?: string,
|
|
73
|
+
): this {
|
|
74
|
+
// Validate chain
|
|
75
|
+
if (!isValidChainId(chain)) {
|
|
76
|
+
throw new ValidationError(
|
|
77
|
+
`invalid chain '${chain}', must be one of: solana, ethereum, near, zcash, polygon, arbitrum, optimism, base`,
|
|
78
|
+
'input.chain'
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Validate token
|
|
83
|
+
if (!token || typeof token !== 'string' || token.trim().length === 0) {
|
|
84
|
+
throw new ValidationError('token must be a non-empty string', 'input.token')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Validate amount
|
|
88
|
+
const amountBigInt = typeof amount === 'number' ? BigInt(Math.floor(amount * 1e18)) : amount
|
|
89
|
+
if (!isValidAmount(amountBigInt)) {
|
|
90
|
+
throw new ValidationError('amount must be positive', 'input.amount')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.params.input = {
|
|
94
|
+
asset: {
|
|
95
|
+
chain: chain as any,
|
|
96
|
+
symbol: token,
|
|
97
|
+
address: null,
|
|
98
|
+
decimals: 18, // Default, should be looked up
|
|
99
|
+
},
|
|
100
|
+
amount: amountBigInt,
|
|
101
|
+
sourceAddress,
|
|
102
|
+
}
|
|
103
|
+
this.senderAddress = sourceAddress
|
|
104
|
+
return this
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Set the output for the intent
|
|
109
|
+
*
|
|
110
|
+
* @throws {ValidationError} If chain is invalid
|
|
111
|
+
*/
|
|
112
|
+
output(chain: string, token: string, minAmount?: number | bigint): this {
|
|
113
|
+
// Validate chain
|
|
114
|
+
if (!isValidChainId(chain)) {
|
|
115
|
+
throw new ValidationError(
|
|
116
|
+
`invalid chain '${chain}', must be one of: solana, ethereum, near, zcash, polygon, arbitrum, optimism, base`,
|
|
117
|
+
'output.chain'
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Validate token
|
|
122
|
+
if (!token || typeof token !== 'string' || token.trim().length === 0) {
|
|
123
|
+
throw new ValidationError('token must be a non-empty string', 'output.token')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const minAmountBigInt = minAmount
|
|
127
|
+
? typeof minAmount === 'number'
|
|
128
|
+
? BigInt(Math.floor(minAmount * 1e18))
|
|
129
|
+
: minAmount
|
|
130
|
+
: 0n
|
|
131
|
+
|
|
132
|
+
// minAmount can be 0 (no minimum), but not negative
|
|
133
|
+
if (minAmountBigInt < 0n) {
|
|
134
|
+
throw new ValidationError('minAmount cannot be negative', 'output.minAmount')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.params.output = {
|
|
138
|
+
asset: {
|
|
139
|
+
chain: chain as any,
|
|
140
|
+
symbol: token,
|
|
141
|
+
address: null,
|
|
142
|
+
decimals: 18,
|
|
143
|
+
},
|
|
144
|
+
minAmount: minAmountBigInt,
|
|
145
|
+
maxSlippage: 0.01, // 1% default
|
|
146
|
+
}
|
|
147
|
+
return this
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Set the privacy level
|
|
152
|
+
*
|
|
153
|
+
* @throws {ValidationError} If privacy level is invalid
|
|
154
|
+
*/
|
|
155
|
+
privacy(level: PrivacyLevel): this {
|
|
156
|
+
if (!isValidPrivacyLevel(level)) {
|
|
157
|
+
throw new ValidationError(
|
|
158
|
+
`invalid privacy level '${level}', must be one of: transparent, shielded, compliant`,
|
|
159
|
+
'privacy'
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
this.params.privacy = level
|
|
163
|
+
return this
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Set the recipient's stealth meta-address
|
|
168
|
+
*
|
|
169
|
+
* @throws {ValidationError} If stealth meta-address format is invalid
|
|
170
|
+
*/
|
|
171
|
+
recipient(metaAddress: string): this {
|
|
172
|
+
if (metaAddress && !isValidStealthMetaAddress(metaAddress)) {
|
|
173
|
+
throw new ValidationError(
|
|
174
|
+
'invalid stealth meta-address format, expected: sip:<chain>:<spendingKey>:<viewingKey>',
|
|
175
|
+
'recipientMetaAddress'
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
this.params.recipientMetaAddress = metaAddress
|
|
179
|
+
return this
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Set slippage tolerance
|
|
184
|
+
*
|
|
185
|
+
* @param percent - Slippage percentage (e.g., 1 for 1%)
|
|
186
|
+
* @throws {ValidationError} If slippage is out of range
|
|
187
|
+
*/
|
|
188
|
+
slippage(percent: number): this {
|
|
189
|
+
const slippageDecimal = percent / 100
|
|
190
|
+
if (!isValidSlippage(slippageDecimal)) {
|
|
191
|
+
throw new ValidationError(
|
|
192
|
+
'slippage must be a non-negative number less than 100%',
|
|
193
|
+
'maxSlippage',
|
|
194
|
+
{ received: percent, asDecimal: slippageDecimal }
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
if (this.params.output) {
|
|
198
|
+
this.params.output.maxSlippage = slippageDecimal
|
|
199
|
+
}
|
|
200
|
+
return this
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Set time-to-live in seconds
|
|
205
|
+
*
|
|
206
|
+
* @throws {ValidationError} If TTL is not a positive integer
|
|
207
|
+
*/
|
|
208
|
+
ttl(seconds: number): this {
|
|
209
|
+
if (typeof seconds !== 'number' || !Number.isInteger(seconds) || seconds <= 0) {
|
|
210
|
+
throw new ValidationError(
|
|
211
|
+
'ttl must be a positive integer (seconds)',
|
|
212
|
+
'ttl',
|
|
213
|
+
{ received: seconds }
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
this.params.ttl = seconds
|
|
217
|
+
return this
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Set the proof provider for automatic proof generation
|
|
222
|
+
*
|
|
223
|
+
* @param provider - The proof provider to use
|
|
224
|
+
* @returns this for chaining
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```typescript
|
|
228
|
+
* const intent = await builder
|
|
229
|
+
* .input('near', 'NEAR', 100n)
|
|
230
|
+
* .output('zcash', 'ZEC', 95n)
|
|
231
|
+
* .privacy(PrivacyLevel.SHIELDED)
|
|
232
|
+
* .withProvider(mockProvider)
|
|
233
|
+
* .build()
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
withProvider(provider: ProofProvider): this {
|
|
237
|
+
this.proofProvider = provider
|
|
238
|
+
return this
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Build the shielded intent
|
|
243
|
+
*
|
|
244
|
+
* If a proof provider is set and the privacy level requires proofs,
|
|
245
|
+
* they will be generated automatically.
|
|
246
|
+
*
|
|
247
|
+
* @returns Promise resolving to the shielded intent
|
|
248
|
+
*/
|
|
249
|
+
async build(): Promise<ShieldedIntent> {
|
|
250
|
+
return createShieldedIntent(this.params as CreateIntentParams, {
|
|
251
|
+
senderAddress: this.senderAddress,
|
|
252
|
+
proofProvider: this.proofProvider,
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Create a new shielded intent
|
|
259
|
+
*
|
|
260
|
+
* @param params - Intent creation parameters
|
|
261
|
+
* @param options - Optional configuration (sender address, proof provider)
|
|
262
|
+
* @returns Promise resolving to the shielded intent
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```typescript
|
|
266
|
+
* // Without proof provider (proofs need to be attached later)
|
|
267
|
+
* const intent = await createShieldedIntent(params)
|
|
268
|
+
*
|
|
269
|
+
* // With proof provider (proofs generated automatically for SHIELDED/COMPLIANT)
|
|
270
|
+
* const intent = await createShieldedIntent(params, {
|
|
271
|
+
* senderAddress: wallet.address,
|
|
272
|
+
* proofProvider: mockProvider,
|
|
273
|
+
* })
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
export async function createShieldedIntent(
|
|
277
|
+
params: CreateIntentParams,
|
|
278
|
+
options?: CreateIntentOptions,
|
|
279
|
+
): Promise<ShieldedIntent> {
|
|
280
|
+
// Comprehensive input validation
|
|
281
|
+
validateCreateIntentParams(params)
|
|
282
|
+
|
|
283
|
+
const { input, output, privacy, recipientMetaAddress, viewingKey, ttl = 300 } = params
|
|
284
|
+
const { senderAddress, proofProvider } = options ?? {}
|
|
285
|
+
|
|
286
|
+
// Get privacy configuration
|
|
287
|
+
// Compute viewing key hash the same way as generateViewingKey():
|
|
288
|
+
// Hash the raw key bytes, not the hex string
|
|
289
|
+
let viewingKeyHash: Hash | undefined
|
|
290
|
+
if (viewingKey) {
|
|
291
|
+
const keyHex = viewingKey.startsWith('0x') ? viewingKey.slice(2) : viewingKey
|
|
292
|
+
const keyBytes = hexToBytes(keyHex)
|
|
293
|
+
viewingKeyHash = `0x${bytesToHex(sha256(keyBytes))}` as Hash
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const privacyConfig = getPrivacyConfig(
|
|
297
|
+
privacy,
|
|
298
|
+
viewingKey ? { key: viewingKey, path: 'm/0', hash: viewingKeyHash! } : undefined,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
// Generate intent ID
|
|
302
|
+
const intentId = generateIntentId()
|
|
303
|
+
|
|
304
|
+
// Create commitments for private fields
|
|
305
|
+
const inputCommitment = createCommitment(input.amount)
|
|
306
|
+
const senderCommitment = createCommitment(
|
|
307
|
+
BigInt(senderAddress ? hash(senderAddress).slice(2, 18) : '0'),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
// Generate stealth address for recipient (if shielded)
|
|
311
|
+
let recipientStealth
|
|
312
|
+
if (privacyConfig.useStealth && recipientMetaAddress) {
|
|
313
|
+
const metaAddress = decodeStealthMetaAddress(recipientMetaAddress)
|
|
314
|
+
const { stealthAddress } = generateStealthAddress(metaAddress)
|
|
315
|
+
recipientStealth = stealthAddress
|
|
316
|
+
} else {
|
|
317
|
+
// For transparent mode, create a placeholder
|
|
318
|
+
recipientStealth = {
|
|
319
|
+
address: '0x0' as HexString,
|
|
320
|
+
ephemeralPublicKey: '0x0' as HexString,
|
|
321
|
+
viewTag: 0,
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const now = Math.floor(Date.now() / 1000)
|
|
326
|
+
|
|
327
|
+
// Generate proofs if provider is available and privacy level requires them
|
|
328
|
+
let fundingProof: import('@sip-protocol/types').ZKProof | undefined
|
|
329
|
+
let validityProof: import('@sip-protocol/types').ZKProof | undefined
|
|
330
|
+
|
|
331
|
+
const requiresProofs = privacy !== PrivacyLevelEnum.TRANSPARENT
|
|
332
|
+
|
|
333
|
+
if (requiresProofs && proofProvider && proofProvider.isReady) {
|
|
334
|
+
// Helper to convert HexString to Uint8Array
|
|
335
|
+
const hexToUint8 = (hex: HexString): Uint8Array => {
|
|
336
|
+
const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex
|
|
337
|
+
return hexToBytes(cleanHex)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Generate funding proof
|
|
341
|
+
const fundingResult = await proofProvider.generateFundingProof({
|
|
342
|
+
balance: input.amount,
|
|
343
|
+
minimumRequired: output.minAmount,
|
|
344
|
+
blindingFactor: hexToUint8(inputCommitment.blindingFactor as HexString),
|
|
345
|
+
assetId: input.asset.symbol,
|
|
346
|
+
userAddress: senderAddress ?? '0x0',
|
|
347
|
+
ownershipSignature: new Uint8Array(64), // Placeholder - would come from wallet
|
|
348
|
+
})
|
|
349
|
+
fundingProof = fundingResult.proof
|
|
350
|
+
|
|
351
|
+
// Generate validity proof
|
|
352
|
+
const validityResult = await proofProvider.generateValidityProof({
|
|
353
|
+
intentHash: hash(intentId) as HexString,
|
|
354
|
+
senderAddress: senderAddress ?? '0x0',
|
|
355
|
+
senderBlinding: hexToUint8(senderCommitment.blindingFactor as HexString),
|
|
356
|
+
senderSecret: new Uint8Array(32), // Placeholder - would come from wallet
|
|
357
|
+
authorizationSignature: new Uint8Array(64), // Placeholder - would come from wallet
|
|
358
|
+
nonce: new Uint8Array(32), // Could use randomBytes here
|
|
359
|
+
timestamp: now,
|
|
360
|
+
expiry: now + ttl,
|
|
361
|
+
})
|
|
362
|
+
validityProof = validityResult.proof
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
intentId,
|
|
367
|
+
version: SIP_VERSION,
|
|
368
|
+
privacyLevel: privacy,
|
|
369
|
+
createdAt: now,
|
|
370
|
+
expiry: now + ttl,
|
|
371
|
+
|
|
372
|
+
outputAsset: output.asset,
|
|
373
|
+
minOutputAmount: output.minAmount,
|
|
374
|
+
maxSlippage: output.maxSlippage,
|
|
375
|
+
|
|
376
|
+
inputCommitment,
|
|
377
|
+
senderCommitment,
|
|
378
|
+
recipientStealth,
|
|
379
|
+
|
|
380
|
+
// Proofs are undefined if:
|
|
381
|
+
// - TRANSPARENT mode (not required)
|
|
382
|
+
// - No proof provider given
|
|
383
|
+
// - Provider not ready
|
|
384
|
+
fundingProof: fundingProof as any,
|
|
385
|
+
validityProof: validityProof as any,
|
|
386
|
+
|
|
387
|
+
viewingKeyHash: privacyConfig.viewingKey?.hash,
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Attach proofs to a shielded intent
|
|
393
|
+
*
|
|
394
|
+
* For SHIELDED and COMPLIANT modes, proofs are required before the intent
|
|
395
|
+
* can be submitted. This function attaches the proofs to an intent.
|
|
396
|
+
*
|
|
397
|
+
* @param intent - The intent to attach proofs to
|
|
398
|
+
* @param fundingProof - The funding proof (balance >= minimum)
|
|
399
|
+
* @param validityProof - The validity proof (authorization)
|
|
400
|
+
* @returns The intent with proofs attached
|
|
401
|
+
*/
|
|
402
|
+
export function attachProofs(
|
|
403
|
+
intent: ShieldedIntent,
|
|
404
|
+
fundingProof: import('@sip-protocol/types').ZKProof,
|
|
405
|
+
validityProof: import('@sip-protocol/types').ZKProof,
|
|
406
|
+
): ShieldedIntent {
|
|
407
|
+
return {
|
|
408
|
+
...intent,
|
|
409
|
+
fundingProof,
|
|
410
|
+
validityProof,
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Check if an intent has all required proofs
|
|
416
|
+
*/
|
|
417
|
+
export function hasRequiredProofs(intent: ShieldedIntent): boolean {
|
|
418
|
+
// TRANSPARENT mode doesn't require proofs
|
|
419
|
+
if (intent.privacyLevel === 'transparent') {
|
|
420
|
+
return true
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// SHIELDED and COMPLIANT modes require both proofs
|
|
424
|
+
return !!(intent.fundingProof && intent.validityProof)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Wrap a shielded intent with status tracking
|
|
429
|
+
*/
|
|
430
|
+
export function trackIntent(intent: ShieldedIntent): TrackedIntent {
|
|
431
|
+
return {
|
|
432
|
+
...intent,
|
|
433
|
+
status: IntentStatus.PENDING,
|
|
434
|
+
quotes: [],
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Check if an intent has expired
|
|
440
|
+
*/
|
|
441
|
+
export function isExpired(intent: ShieldedIntent): boolean {
|
|
442
|
+
return Math.floor(Date.now() / 1000) > intent.expiry
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Get time remaining until intent expires (in seconds)
|
|
447
|
+
*/
|
|
448
|
+
export function getTimeRemaining(intent: ShieldedIntent): number {
|
|
449
|
+
const remaining = intent.expiry - Math.floor(Date.now() / 1000)
|
|
450
|
+
return Math.max(0, remaining)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Serialize a shielded intent to JSON
|
|
455
|
+
*/
|
|
456
|
+
export function serializeIntent(intent: ShieldedIntent): string {
|
|
457
|
+
return JSON.stringify(intent, (_, value) =>
|
|
458
|
+
typeof value === 'bigint' ? value.toString() : value,
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Deserialize a shielded intent from JSON
|
|
464
|
+
*/
|
|
465
|
+
export function deserializeIntent(json: string): ShieldedIntent {
|
|
466
|
+
return JSON.parse(json, (key, value) => {
|
|
467
|
+
// Convert string numbers back to bigint for known fields
|
|
468
|
+
if (
|
|
469
|
+
typeof value === 'string' &&
|
|
470
|
+
/^\d+$/.test(value) &&
|
|
471
|
+
['minOutputAmount', 'amount'].includes(key)
|
|
472
|
+
) {
|
|
473
|
+
return BigInt(value)
|
|
474
|
+
}
|
|
475
|
+
return value
|
|
476
|
+
})
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Get a human-readable summary of the intent
|
|
481
|
+
*/
|
|
482
|
+
export function getIntentSummary(intent: ShieldedIntent): string {
|
|
483
|
+
const privacy = intent.privacyLevel.toUpperCase()
|
|
484
|
+
const output = intent.outputAsset.symbol
|
|
485
|
+
const expiry = new Date(intent.expiry * 1000).toISOString()
|
|
486
|
+
|
|
487
|
+
return `[${privacy}] Intent ${intent.intentId.slice(0, 16)}... → ${output} (expires: ${expiry})`
|
|
488
|
+
}
|