@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,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure Memory Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides best-effort secure memory handling for cryptographic secrets
|
|
5
|
+
* in JavaScript environments.
|
|
6
|
+
*
|
|
7
|
+
* ## What This Provides
|
|
8
|
+
* - **Explicit Cleanup**: Deterministic overwriting of sensitive buffers
|
|
9
|
+
* - **Defense in Depth**: Overwrite with random data, then zero
|
|
10
|
+
* - **API for Safe Patterns**: Helper functions for scoped secret usage
|
|
11
|
+
*
|
|
12
|
+
* ## IMPORTANT: Limitations of JavaScript Memory Cleanup
|
|
13
|
+
*
|
|
14
|
+
* JavaScript does NOT provide true secure memory guarantees. These utilities
|
|
15
|
+
* offer BEST-EFFORT cleanup only. Be aware of these fundamental limitations:
|
|
16
|
+
*
|
|
17
|
+
* 1. **Garbage Collection**: The GC may have already copied the original
|
|
18
|
+
* data to other memory locations before you call secureWipe().
|
|
19
|
+
*
|
|
20
|
+
* 2. **JIT Compilation**: Modern JS engines may optimize away "dead" writes
|
|
21
|
+
* or create copies in compiled code paths.
|
|
22
|
+
*
|
|
23
|
+
* 3. **Memory Swapping**: The OS may swap memory pages to disk before cleanup,
|
|
24
|
+
* leaving secrets in swap files or hibernation images.
|
|
25
|
+
*
|
|
26
|
+
* 4. **String Interning**: If secrets pass through strings, they may be
|
|
27
|
+
* interned and retained in the string pool.
|
|
28
|
+
*
|
|
29
|
+
* ## Recommendations for High-Security Applications
|
|
30
|
+
*
|
|
31
|
+
* For applications requiring strong memory protection:
|
|
32
|
+
* - Use hardware security modules (HSMs) for key storage
|
|
33
|
+
* - Use hardware wallets (Ledger, Trezor) for signing operations
|
|
34
|
+
* - Consider native bindings with secure memory allocators
|
|
35
|
+
* - Run in isolated environments with encrypted swap disabled
|
|
36
|
+
*
|
|
37
|
+
* This module is appropriate for:
|
|
38
|
+
* - Reducing attack surface (makes casual inspection harder)
|
|
39
|
+
* - Defense in depth (multiple security layers)
|
|
40
|
+
* - Compliance with best practices (demonstrable cleanup efforts)
|
|
41
|
+
*
|
|
42
|
+
* @see docs/security/KNOWN_LIMITATIONS.md
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { randomBytes } from '@noble/hashes/utils'
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Securely wipe a buffer containing sensitive data
|
|
49
|
+
*
|
|
50
|
+
* This performs a defense-in-depth wipe:
|
|
51
|
+
* 1. Overwrite with random data (defeats simple memory scrapers)
|
|
52
|
+
* 2. Zero the buffer (standard cleanup)
|
|
53
|
+
*
|
|
54
|
+
* Note: Due to JavaScript's garbage collection and potential JIT
|
|
55
|
+
* optimizations, this cannot guarantee complete erasure. However,
|
|
56
|
+
* it provides significant improvement over leaving secrets in memory.
|
|
57
|
+
*
|
|
58
|
+
* @param buffer - The buffer to wipe (modified in place)
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* const secretKey = randomBytes(32)
|
|
63
|
+
* // ... use the key ...
|
|
64
|
+
* secureWipe(secretKey) // Clean up when done
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function secureWipe(buffer: Uint8Array): void {
|
|
68
|
+
if (!buffer || buffer.length === 0) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Step 1: Overwrite with random data
|
|
73
|
+
// This defeats simple memory scrapers looking for zeroed patterns
|
|
74
|
+
const random = randomBytes(buffer.length)
|
|
75
|
+
buffer.set(random)
|
|
76
|
+
|
|
77
|
+
// Step 2: Zero the buffer
|
|
78
|
+
// Standard cleanup - makes the data unreadable
|
|
79
|
+
buffer.fill(0)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Execute a function with a secret buffer and ensure cleanup
|
|
84
|
+
*
|
|
85
|
+
* Provides a safer pattern for using secrets - the buffer is
|
|
86
|
+
* automatically wiped after the function completes (or throws).
|
|
87
|
+
*
|
|
88
|
+
* @param createSecret - Function to create the secret buffer
|
|
89
|
+
* @param useSecret - Function that uses the secret
|
|
90
|
+
* @returns The result of useSecret
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* const signature = await withSecureBuffer(
|
|
95
|
+
* () => generatePrivateKey(),
|
|
96
|
+
* async (privateKey) => {
|
|
97
|
+
* return signMessage(message, privateKey)
|
|
98
|
+
* }
|
|
99
|
+
* )
|
|
100
|
+
* // privateKey is automatically wiped after signing
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export async function withSecureBuffer<T>(
|
|
104
|
+
createSecret: () => Uint8Array,
|
|
105
|
+
useSecret: (secret: Uint8Array) => T | Promise<T>,
|
|
106
|
+
): Promise<T> {
|
|
107
|
+
const secret = createSecret()
|
|
108
|
+
try {
|
|
109
|
+
return await useSecret(secret)
|
|
110
|
+
} finally {
|
|
111
|
+
secureWipe(secret)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Synchronous version of withSecureBuffer
|
|
117
|
+
*
|
|
118
|
+
* @param createSecret - Function to create the secret buffer
|
|
119
|
+
* @param useSecret - Function that uses the secret (sync)
|
|
120
|
+
* @returns The result of useSecret
|
|
121
|
+
*/
|
|
122
|
+
export function withSecureBufferSync<T>(
|
|
123
|
+
createSecret: () => Uint8Array,
|
|
124
|
+
useSecret: (secret: Uint8Array) => T,
|
|
125
|
+
): T {
|
|
126
|
+
const secret = createSecret()
|
|
127
|
+
try {
|
|
128
|
+
return useSecret(secret)
|
|
129
|
+
} finally {
|
|
130
|
+
secureWipe(secret)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Wipe multiple buffers at once
|
|
136
|
+
*
|
|
137
|
+
* Convenience function for cleaning up multiple secrets.
|
|
138
|
+
*
|
|
139
|
+
* @param buffers - Array of buffers to wipe
|
|
140
|
+
*/
|
|
141
|
+
export function secureWipeAll(...buffers: (Uint8Array | undefined | null)[]): void {
|
|
142
|
+
for (const buffer of buffers) {
|
|
143
|
+
if (buffer) {
|
|
144
|
+
secureWipe(buffer)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
package/src/sip.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import {
|
|
8
8
|
PrivacyLevel,
|
|
9
9
|
IntentStatus,
|
|
10
|
+
OneClickSwapStatus,
|
|
10
11
|
type ShieldedIntent,
|
|
11
12
|
type CreateIntentParams,
|
|
12
13
|
type TrackedIntent,
|
|
@@ -14,6 +15,8 @@ import {
|
|
|
14
15
|
type FulfillmentResult,
|
|
15
16
|
type StealthMetaAddress,
|
|
16
17
|
type ViewingKey,
|
|
18
|
+
type OneClickQuoteResponse,
|
|
19
|
+
type Asset,
|
|
17
20
|
} from '@sip-protocol/types'
|
|
18
21
|
import { IntentBuilder, createShieldedIntent, trackIntent, hasRequiredProofs } from './intent'
|
|
19
22
|
import {
|
|
@@ -24,8 +27,9 @@ import {
|
|
|
24
27
|
import { generateViewingKey, deriveViewingKey } from './privacy'
|
|
25
28
|
import type { ChainId, HexString } from '@sip-protocol/types'
|
|
26
29
|
import type { ProofProvider } from './proofs'
|
|
27
|
-
import { ValidationError } from './errors'
|
|
30
|
+
import { ValidationError, IntentError, ErrorCode } from './errors'
|
|
28
31
|
import { isValidChainId } from './validation'
|
|
32
|
+
import { NEARIntentsAdapter, type NEARIntentsAdapterConfig, type SwapRequest } from './adapters'
|
|
29
33
|
|
|
30
34
|
/**
|
|
31
35
|
* SIP SDK configuration
|
|
@@ -33,6 +37,11 @@ import { isValidChainId } from './validation'
|
|
|
33
37
|
export interface SIPConfig {
|
|
34
38
|
/** Network: mainnet or testnet */
|
|
35
39
|
network: 'mainnet' | 'testnet'
|
|
40
|
+
/**
|
|
41
|
+
* Mode: 'demo' for mock data, 'production' for real NEAR Intents
|
|
42
|
+
* @default 'demo'
|
|
43
|
+
*/
|
|
44
|
+
mode?: 'demo' | 'production'
|
|
36
45
|
/** Default privacy level */
|
|
37
46
|
defaultPrivacy?: PrivacyLevel
|
|
38
47
|
/** RPC endpoints for chains */
|
|
@@ -54,6 +63,23 @@ export interface SIPConfig {
|
|
|
54
63
|
* ```
|
|
55
64
|
*/
|
|
56
65
|
proofProvider?: ProofProvider
|
|
66
|
+
/**
|
|
67
|
+
* NEAR Intents adapter configuration
|
|
68
|
+
*
|
|
69
|
+
* Required for production mode. Provides connection to 1Click API.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const sip = new SIP({
|
|
74
|
+
* network: 'mainnet',
|
|
75
|
+
* mode: 'production',
|
|
76
|
+
* intentsAdapter: {
|
|
77
|
+
* jwtToken: process.env.NEAR_INTENTS_JWT,
|
|
78
|
+
* },
|
|
79
|
+
* })
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
intentsAdapter?: NEARIntentsAdapterConfig | NEARIntentsAdapter
|
|
57
83
|
}
|
|
58
84
|
|
|
59
85
|
/**
|
|
@@ -68,13 +94,25 @@ export interface WalletAdapter {
|
|
|
68
94
|
signMessage(message: string): Promise<string>
|
|
69
95
|
/** Sign a transaction */
|
|
70
96
|
signTransaction(tx: unknown): Promise<unknown>
|
|
97
|
+
/** Send a transaction (optional) */
|
|
98
|
+
sendTransaction?(tx: unknown): Promise<string>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Extended quote with deposit info for production mode
|
|
103
|
+
*/
|
|
104
|
+
export interface ProductionQuote extends Quote {
|
|
105
|
+
/** Deposit address for input tokens (production mode only) */
|
|
106
|
+
depositAddress?: string
|
|
107
|
+
/** Raw 1Click quote response (production mode only) */
|
|
108
|
+
rawQuote?: OneClickQuoteResponse
|
|
71
109
|
}
|
|
72
110
|
|
|
73
111
|
/**
|
|
74
112
|
* Main SIP SDK class
|
|
75
113
|
*/
|
|
76
114
|
export class SIP {
|
|
77
|
-
private config: SIPConfig
|
|
115
|
+
private config: SIPConfig & { mode: 'demo' | 'production' }
|
|
78
116
|
private wallet?: WalletAdapter
|
|
79
117
|
private stealthKeys?: {
|
|
80
118
|
metaAddress: StealthMetaAddress
|
|
@@ -82,6 +120,9 @@ export class SIP {
|
|
|
82
120
|
viewingPrivateKey: HexString
|
|
83
121
|
}
|
|
84
122
|
private proofProvider?: ProofProvider
|
|
123
|
+
private intentsAdapter?: NEARIntentsAdapter
|
|
124
|
+
/** Cache of pending swaps by intent ID */
|
|
125
|
+
private pendingSwaps: Map<string, { depositAddress: string; quote: OneClickQuoteResponse }> = new Map()
|
|
85
126
|
|
|
86
127
|
constructor(config: SIPConfig) {
|
|
87
128
|
// Validate config
|
|
@@ -97,6 +138,14 @@ export class SIP {
|
|
|
97
138
|
)
|
|
98
139
|
}
|
|
99
140
|
|
|
141
|
+
if (config.mode !== undefined && config.mode !== 'demo' && config.mode !== 'production') {
|
|
142
|
+
throw new ValidationError(
|
|
143
|
+
`mode must be 'demo' or 'production'`,
|
|
144
|
+
'config.mode',
|
|
145
|
+
{ received: config.mode }
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
100
149
|
if (config.defaultPrivacy !== undefined) {
|
|
101
150
|
const validLevels = ['transparent', 'shielded', 'compliant']
|
|
102
151
|
if (!validLevels.includes(config.defaultPrivacy)) {
|
|
@@ -110,9 +159,51 @@ export class SIP {
|
|
|
110
159
|
|
|
111
160
|
this.config = {
|
|
112
161
|
...config,
|
|
162
|
+
mode: config.mode ?? 'demo',
|
|
113
163
|
defaultPrivacy: config.defaultPrivacy ?? PrivacyLevel.SHIELDED,
|
|
114
164
|
}
|
|
115
165
|
this.proofProvider = config.proofProvider
|
|
166
|
+
|
|
167
|
+
// Initialize intents adapter if provided
|
|
168
|
+
if (config.intentsAdapter) {
|
|
169
|
+
if (config.intentsAdapter instanceof NEARIntentsAdapter) {
|
|
170
|
+
this.intentsAdapter = config.intentsAdapter
|
|
171
|
+
} else {
|
|
172
|
+
this.intentsAdapter = new NEARIntentsAdapter(config.intentsAdapter)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get the current mode
|
|
179
|
+
*/
|
|
180
|
+
getMode(): 'demo' | 'production' {
|
|
181
|
+
return this.config.mode
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check if running in production mode with real NEAR Intents
|
|
186
|
+
*/
|
|
187
|
+
isProductionMode(): boolean {
|
|
188
|
+
return this.config.mode === 'production' && !!this.intentsAdapter
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get the NEAR Intents adapter
|
|
193
|
+
*/
|
|
194
|
+
getIntentsAdapter(): NEARIntentsAdapter | undefined {
|
|
195
|
+
return this.intentsAdapter
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Set the NEAR Intents adapter
|
|
200
|
+
*/
|
|
201
|
+
setIntentsAdapter(adapter: NEARIntentsAdapter | NEARIntentsAdapterConfig): void {
|
|
202
|
+
if (adapter instanceof NEARIntentsAdapter) {
|
|
203
|
+
this.intentsAdapter = adapter
|
|
204
|
+
} else {
|
|
205
|
+
this.intentsAdapter = new NEARIntentsAdapter(adapter)
|
|
206
|
+
}
|
|
116
207
|
}
|
|
117
208
|
|
|
118
209
|
/**
|
|
@@ -222,51 +313,65 @@ export class SIP {
|
|
|
222
313
|
}
|
|
223
314
|
|
|
224
315
|
/**
|
|
225
|
-
* Get quotes for an intent
|
|
316
|
+
* Get quotes for an intent
|
|
317
|
+
*
|
|
318
|
+
* In production mode: fetches real quotes from NEAR 1Click API
|
|
319
|
+
* In demo mode: returns mock quotes for testing
|
|
320
|
+
*
|
|
321
|
+
* @param params - Intent parameters (CreateIntentParams for production, ShieldedIntent/CreateIntentParams for demo)
|
|
322
|
+
* @param recipientMetaAddress - Optional stealth meta-address for privacy modes
|
|
323
|
+
* @returns Array of quotes (with deposit info in production mode)
|
|
226
324
|
*/
|
|
227
|
-
async getQuotes(
|
|
228
|
-
|
|
229
|
-
|
|
325
|
+
async getQuotes(
|
|
326
|
+
params: CreateIntentParams | ShieldedIntent,
|
|
327
|
+
recipientMetaAddress?: StealthMetaAddress | string,
|
|
328
|
+
): Promise<ProductionQuote[]> {
|
|
329
|
+
// Production mode - use real NEAR Intents
|
|
330
|
+
if (this.isProductionMode()) {
|
|
331
|
+
// Production mode requires CreateIntentParams with raw values
|
|
332
|
+
if (!('input' in params)) {
|
|
333
|
+
throw new ValidationError(
|
|
334
|
+
'Production mode requires CreateIntentParams with raw input/output values. ShieldedIntent does not expose raw values.',
|
|
335
|
+
'params'
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
return this.getQuotesProduction(params, recipientMetaAddress)
|
|
339
|
+
}
|
|
230
340
|
|
|
231
|
-
return
|
|
232
|
-
|
|
233
|
-
quoteId: `quote-${Date.now()}-1`,
|
|
234
|
-
intentId: intent.intentId,
|
|
235
|
-
solverId: 'solver-1',
|
|
236
|
-
outputAmount: baseAmount + (baseAmount * 2n) / 100n, // +2%
|
|
237
|
-
estimatedTime: 30,
|
|
238
|
-
expiry: Math.floor(Date.now() / 1000) + 60,
|
|
239
|
-
fee: baseAmount / 200n, // 0.5%
|
|
240
|
-
},
|
|
241
|
-
{
|
|
242
|
-
quoteId: `quote-${Date.now()}-2`,
|
|
243
|
-
intentId: intent.intentId,
|
|
244
|
-
solverId: 'solver-2',
|
|
245
|
-
outputAmount: baseAmount + (baseAmount * 1n) / 100n, // +1%
|
|
246
|
-
estimatedTime: 15,
|
|
247
|
-
expiry: Math.floor(Date.now() / 1000) + 60,
|
|
248
|
-
fee: baseAmount / 100n, // 1%
|
|
249
|
-
},
|
|
250
|
-
]
|
|
341
|
+
// Demo mode - return mock quotes
|
|
342
|
+
return this.getQuotesDemo(params)
|
|
251
343
|
}
|
|
252
344
|
|
|
253
345
|
/**
|
|
254
|
-
* Execute an intent with a selected quote
|
|
346
|
+
* Execute an intent with a selected quote
|
|
347
|
+
*
|
|
348
|
+
* In production mode: initiates real swap via NEAR 1Click API
|
|
349
|
+
* In demo mode: returns mock result
|
|
350
|
+
*
|
|
351
|
+
* @param intent - The intent to execute
|
|
352
|
+
* @param quote - Selected quote from getQuotes()
|
|
353
|
+
* @param options - Execution options
|
|
354
|
+
* @returns Fulfillment result with transaction hash (when available)
|
|
255
355
|
*/
|
|
256
356
|
async execute(
|
|
257
357
|
intent: TrackedIntent,
|
|
258
|
-
quote: Quote,
|
|
358
|
+
quote: Quote | ProductionQuote,
|
|
359
|
+
options?: {
|
|
360
|
+
/** Callback when deposit is required */
|
|
361
|
+
onDepositRequired?: (depositAddress: string, amount: string) => Promise<string>
|
|
362
|
+
/** Callback for status updates */
|
|
363
|
+
onStatusUpdate?: (status: OneClickSwapStatus) => void
|
|
364
|
+
/** Timeout for waiting (ms) */
|
|
365
|
+
timeout?: number
|
|
366
|
+
},
|
|
259
367
|
): Promise<FulfillmentResult> {
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
return {
|
|
264
|
-
intentId: intent.intentId,
|
|
265
|
-
status: IntentStatus.FULFILLED,
|
|
266
|
-
outputAmount: quote.outputAmount,
|
|
267
|
-
txHash: intent.privacyLevel === PrivacyLevel.TRANSPARENT ? `0x${Date.now().toString(16)}` : undefined,
|
|
268
|
-
fulfilledAt: Math.floor(Date.now() / 1000),
|
|
368
|
+
// Production mode - use real NEAR Intents
|
|
369
|
+
if (this.isProductionMode()) {
|
|
370
|
+
return this.executeProduction(intent, quote as ProductionQuote, options)
|
|
269
371
|
}
|
|
372
|
+
|
|
373
|
+
// Demo mode - return mock result
|
|
374
|
+
return this.executeDemo(intent, quote)
|
|
270
375
|
}
|
|
271
376
|
|
|
272
377
|
/**
|
|
@@ -289,6 +394,245 @@ export class SIP {
|
|
|
289
394
|
getNetwork(): 'mainnet' | 'testnet' {
|
|
290
395
|
return this.config.network
|
|
291
396
|
}
|
|
397
|
+
|
|
398
|
+
// ─── Production Mode Implementation ─────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
private async getQuotesProduction(
|
|
401
|
+
params: CreateIntentParams,
|
|
402
|
+
recipientMetaAddress?: StealthMetaAddress | string,
|
|
403
|
+
): Promise<ProductionQuote[]> {
|
|
404
|
+
if (!this.intentsAdapter) {
|
|
405
|
+
throw new ValidationError(
|
|
406
|
+
'NEAR Intents adapter not configured. Set intentsAdapter in config for production mode.',
|
|
407
|
+
'intentsAdapter'
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// For privacy modes, require stealth meta-address
|
|
412
|
+
const metaAddr = recipientMetaAddress ?? (
|
|
413
|
+
params.privacy !== PrivacyLevel.TRANSPARENT
|
|
414
|
+
? this.stealthKeys?.metaAddress
|
|
415
|
+
: undefined
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if (params.privacy !== PrivacyLevel.TRANSPARENT && !metaAddr) {
|
|
419
|
+
throw new ValidationError(
|
|
420
|
+
'Stealth meta-address required for privacy modes. Call generateStealthKeys() or provide recipientMetaAddress.',
|
|
421
|
+
'recipientMetaAddress'
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Generate a request ID for tracking
|
|
426
|
+
const requestId = `quote-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
427
|
+
|
|
428
|
+
// Build swap request from CreateIntentParams
|
|
429
|
+
const swapRequest: SwapRequest = {
|
|
430
|
+
requestId,
|
|
431
|
+
privacyLevel: params.privacy,
|
|
432
|
+
inputAsset: params.input.asset,
|
|
433
|
+
outputAsset: params.output.asset,
|
|
434
|
+
inputAmount: params.input.amount,
|
|
435
|
+
minOutputAmount: params.output.minAmount,
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
// Get quote from 1Click API
|
|
440
|
+
const prepared = await this.intentsAdapter.prepareSwap(
|
|
441
|
+
swapRequest,
|
|
442
|
+
metaAddr,
|
|
443
|
+
this.wallet?.address,
|
|
444
|
+
)
|
|
445
|
+
const rawQuote = await this.intentsAdapter.getQuote(prepared)
|
|
446
|
+
|
|
447
|
+
// Cache for execute()
|
|
448
|
+
this.pendingSwaps.set(requestId, {
|
|
449
|
+
depositAddress: rawQuote.depositAddress,
|
|
450
|
+
quote: rawQuote,
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
// Convert to SIP Quote format
|
|
454
|
+
const quote: ProductionQuote = {
|
|
455
|
+
quoteId: rawQuote.quoteId,
|
|
456
|
+
intentId: requestId,
|
|
457
|
+
solverId: 'near-1click',
|
|
458
|
+
outputAmount: BigInt(rawQuote.amountOut),
|
|
459
|
+
estimatedTime: rawQuote.timeEstimate,
|
|
460
|
+
expiry: Math.floor(new Date(rawQuote.deadline).getTime() / 1000),
|
|
461
|
+
fee: this.calculateFee(params.input.amount, BigInt(rawQuote.amountIn)),
|
|
462
|
+
depositAddress: rawQuote.depositAddress,
|
|
463
|
+
rawQuote,
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return [quote]
|
|
467
|
+
} catch (error) {
|
|
468
|
+
// If production fails, don't fall back to demo - let the error propagate
|
|
469
|
+
throw error
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private async executeProduction(
|
|
474
|
+
intent: TrackedIntent,
|
|
475
|
+
quote: ProductionQuote,
|
|
476
|
+
options?: {
|
|
477
|
+
onDepositRequired?: (depositAddress: string, amount: string) => Promise<string>
|
|
478
|
+
onStatusUpdate?: (status: OneClickSwapStatus) => void
|
|
479
|
+
timeout?: number
|
|
480
|
+
},
|
|
481
|
+
): Promise<FulfillmentResult> {
|
|
482
|
+
if (!this.intentsAdapter) {
|
|
483
|
+
throw new ValidationError(
|
|
484
|
+
'NEAR Intents adapter not configured',
|
|
485
|
+
'intentsAdapter'
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Get deposit info from quote or cache
|
|
490
|
+
const pendingSwap = this.pendingSwaps.get(quote.intentId)
|
|
491
|
+
const depositAddress = quote.depositAddress ?? pendingSwap?.depositAddress
|
|
492
|
+
const rawQuote = quote.rawQuote ?? pendingSwap?.quote
|
|
493
|
+
|
|
494
|
+
if (!depositAddress || !rawQuote) {
|
|
495
|
+
throw new IntentError(
|
|
496
|
+
'No deposit address found. Call getQuotes() first.',
|
|
497
|
+
ErrorCode.INTENT_NOT_FOUND,
|
|
498
|
+
{ intentId: intent.intentId }
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// If wallet can send transactions and callback provided, handle deposit
|
|
503
|
+
let depositTxHash: string | undefined
|
|
504
|
+
if (options?.onDepositRequired) {
|
|
505
|
+
depositTxHash = await options.onDepositRequired(depositAddress, rawQuote.amountIn)
|
|
506
|
+
|
|
507
|
+
// Validate txHash format before proceeding
|
|
508
|
+
if (!depositTxHash || !this.isValidTxHash(depositTxHash)) {
|
|
509
|
+
throw new ValidationError(
|
|
510
|
+
'Invalid deposit transaction hash. Expected 0x-prefixed hex string (32-66 bytes).',
|
|
511
|
+
'depositTxHash',
|
|
512
|
+
{ received: depositTxHash }
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Notify 1Click of deposit
|
|
517
|
+
await this.intentsAdapter.notifyDeposit(
|
|
518
|
+
depositAddress,
|
|
519
|
+
depositTxHash,
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Wait for completion
|
|
524
|
+
const finalStatus = await this.intentsAdapter.waitForCompletion(
|
|
525
|
+
depositAddress,
|
|
526
|
+
{
|
|
527
|
+
timeout: options?.timeout ?? 300000, // 5 minutes default
|
|
528
|
+
onStatus: (status) => options?.onStatusUpdate?.(status.status),
|
|
529
|
+
},
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
// Clean up cache
|
|
533
|
+
this.pendingSwaps.delete(quote.intentId)
|
|
534
|
+
|
|
535
|
+
// Convert to FulfillmentResult
|
|
536
|
+
const isSuccess = finalStatus.status === OneClickSwapStatus.SUCCESS
|
|
537
|
+
return {
|
|
538
|
+
intentId: intent.intentId,
|
|
539
|
+
status: isSuccess ? IntentStatus.FULFILLED : IntentStatus.FAILED,
|
|
540
|
+
outputAmount: quote.outputAmount,
|
|
541
|
+
txHash: finalStatus.settlementTxHash ?? depositTxHash,
|
|
542
|
+
fulfilledAt: Math.floor(Date.now() / 1000),
|
|
543
|
+
error: finalStatus.error,
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private calculateFee(inputAmount: bigint, quotedInput: bigint): bigint {
|
|
548
|
+
// Fee is the difference between what we sent and what was quoted
|
|
549
|
+
if (quotedInput > inputAmount) {
|
|
550
|
+
return quotedInput - inputAmount
|
|
551
|
+
}
|
|
552
|
+
// Estimate 0.5% fee if we can't calculate
|
|
553
|
+
return inputAmount / 200n
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Validate transaction hash format
|
|
558
|
+
*
|
|
559
|
+
* Accepts:
|
|
560
|
+
* - Ethereum-style: 0x + 64 hex chars (32 bytes)
|
|
561
|
+
* - Solana-style: base58 encoded (44-88 chars)
|
|
562
|
+
* - NEAR-style: base58 or hex with varying lengths
|
|
563
|
+
*/
|
|
564
|
+
private isValidTxHash(txHash: string): boolean {
|
|
565
|
+
if (!txHash || typeof txHash !== 'string') {
|
|
566
|
+
return false
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Check for 0x-prefixed hex (Ethereum, etc.)
|
|
570
|
+
if (txHash.startsWith('0x')) {
|
|
571
|
+
const hex = txHash.slice(2)
|
|
572
|
+
// Valid hex, 32-66 bytes (64-132 chars)
|
|
573
|
+
return /^[0-9a-fA-F]{64,132}$/.test(hex)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Base58 (Solana, NEAR)
|
|
577
|
+
if (/^[1-9A-HJ-NP-Za-km-z]{32,88}$/.test(txHash)) {
|
|
578
|
+
return true
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return false
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ─── Demo Mode Implementation ───────────────────────────────────────────────
|
|
585
|
+
|
|
586
|
+
private async getQuotesDemo(params: CreateIntentParams | ShieldedIntent): Promise<ProductionQuote[]> {
|
|
587
|
+
// Extract base amount depending on type
|
|
588
|
+
const baseAmount = 'input' in params
|
|
589
|
+
? params.output.minAmount // CreateIntentParams
|
|
590
|
+
: params.minOutputAmount // ShieldedIntent
|
|
591
|
+
|
|
592
|
+
// Generate intentId if not present
|
|
593
|
+
const intentId = 'intentId' in params
|
|
594
|
+
? params.intentId
|
|
595
|
+
: `demo-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
596
|
+
|
|
597
|
+
return [
|
|
598
|
+
{
|
|
599
|
+
quoteId: `quote-${Date.now()}-1`,
|
|
600
|
+
intentId,
|
|
601
|
+
solverId: 'demo-solver-1',
|
|
602
|
+
outputAmount: baseAmount + (baseAmount * 2n) / 100n, // +2%
|
|
603
|
+
estimatedTime: 30,
|
|
604
|
+
expiry: Math.floor(Date.now() / 1000) + 60,
|
|
605
|
+
fee: baseAmount / 200n, // 0.5%
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
quoteId: `quote-${Date.now()}-2`,
|
|
609
|
+
intentId,
|
|
610
|
+
solverId: 'demo-solver-2',
|
|
611
|
+
outputAmount: baseAmount + (baseAmount * 1n) / 100n, // +1%
|
|
612
|
+
estimatedTime: 15,
|
|
613
|
+
expiry: Math.floor(Date.now() / 1000) + 60,
|
|
614
|
+
fee: baseAmount / 100n, // 1%
|
|
615
|
+
},
|
|
616
|
+
]
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private async executeDemo(
|
|
620
|
+
intent: TrackedIntent,
|
|
621
|
+
quote: Quote,
|
|
622
|
+
): Promise<FulfillmentResult> {
|
|
623
|
+
// Simulate execution delay
|
|
624
|
+
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
625
|
+
|
|
626
|
+
return {
|
|
627
|
+
intentId: intent.intentId,
|
|
628
|
+
status: IntentStatus.FULFILLED,
|
|
629
|
+
outputAmount: quote.outputAmount,
|
|
630
|
+
txHash: intent.privacyLevel === PrivacyLevel.TRANSPARENT
|
|
631
|
+
? `0x${Date.now().toString(16)}`
|
|
632
|
+
: undefined,
|
|
633
|
+
fulfilledAt: Math.floor(Date.now() / 1000),
|
|
634
|
+
}
|
|
635
|
+
}
|
|
292
636
|
}
|
|
293
637
|
|
|
294
638
|
/**
|
|
@@ -297,3 +641,21 @@ export class SIP {
|
|
|
297
641
|
export function createSIP(network: 'mainnet' | 'testnet' = 'testnet'): SIP {
|
|
298
642
|
return new SIP({ network })
|
|
299
643
|
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Create a new SIP instance configured for production
|
|
647
|
+
*/
|
|
648
|
+
export function createProductionSIP(config: {
|
|
649
|
+
network: 'mainnet' | 'testnet'
|
|
650
|
+
jwtToken?: string
|
|
651
|
+
proofProvider?: ProofProvider
|
|
652
|
+
}): SIP {
|
|
653
|
+
return new SIP({
|
|
654
|
+
network: config.network,
|
|
655
|
+
mode: 'production',
|
|
656
|
+
proofProvider: config.proofProvider,
|
|
657
|
+
intentsAdapter: {
|
|
658
|
+
jwtToken: config.jwtToken,
|
|
659
|
+
},
|
|
660
|
+
})
|
|
661
|
+
}
|