@sip-protocol/react 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/README.md +124 -0
- package/dist/index.d.mts +404 -0
- package/dist/index.d.ts +404 -0
- package/dist/index.js +389 -0
- package/dist/index.mjs +358 -0
- package/package.json +64 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/use-private-swap.ts +268 -0
- package/src/hooks/use-sip.ts +188 -0
- package/src/hooks/use-stealth-address.ts +184 -0
- package/src/hooks/use-viewing-key.ts +190 -0
- package/src/index.ts +10 -0
- package/src/providers/sip-provider.tsx +50 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
// src/providers/sip-provider.tsx
|
|
2
|
+
import React, { createContext, useContext, useMemo } from "react";
|
|
3
|
+
import { SIP } from "@sip-protocol/sdk";
|
|
4
|
+
var SIPContext = createContext(void 0);
|
|
5
|
+
function SIPProvider({ config, children }) {
|
|
6
|
+
const client = useMemo(() => new SIP(config), [config]);
|
|
7
|
+
const value = useMemo(() => ({ client, config }), [client, config]);
|
|
8
|
+
return /* @__PURE__ */ React.createElement(SIPContext.Provider, { value }, children);
|
|
9
|
+
}
|
|
10
|
+
function useSIPContext() {
|
|
11
|
+
const context = useContext(SIPContext);
|
|
12
|
+
if (!context) {
|
|
13
|
+
throw new Error("useSIPContext must be used within SIPProvider");
|
|
14
|
+
}
|
|
15
|
+
return context;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/hooks/use-sip.ts
|
|
19
|
+
import { useState, useCallback } from "react";
|
|
20
|
+
import { SIP as SIP2 } from "@sip-protocol/sdk";
|
|
21
|
+
function useSIP() {
|
|
22
|
+
const [standaloneClient, setStandaloneClient] = useState(null);
|
|
23
|
+
const [standaloneReady, setStandaloneReady] = useState(false);
|
|
24
|
+
const [standaloneError, setStandaloneError] = useState(null);
|
|
25
|
+
let providerContext;
|
|
26
|
+
try {
|
|
27
|
+
providerContext = useSIPContext();
|
|
28
|
+
} catch {
|
|
29
|
+
providerContext = null;
|
|
30
|
+
}
|
|
31
|
+
const standaloneInitialize = useCallback(async (config) => {
|
|
32
|
+
if (standaloneClient && standaloneReady) {
|
|
33
|
+
console.warn("SIP client already initialized. Call will be ignored.");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
setStandaloneError(null);
|
|
38
|
+
setStandaloneReady(false);
|
|
39
|
+
const newClient = new SIP2(config);
|
|
40
|
+
setStandaloneClient(newClient);
|
|
41
|
+
setStandaloneReady(true);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
44
|
+
setStandaloneError(error);
|
|
45
|
+
setStandaloneReady(false);
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}, [standaloneClient, standaloneReady]);
|
|
49
|
+
if (providerContext) {
|
|
50
|
+
return {
|
|
51
|
+
client: providerContext.client,
|
|
52
|
+
isReady: true,
|
|
53
|
+
// Provider always provides ready client
|
|
54
|
+
error: null,
|
|
55
|
+
// Provider throws on error, doesn't expose it
|
|
56
|
+
initialize: async () => {
|
|
57
|
+
console.warn("initialize() called but SIPProvider is already providing a client. This call will be ignored.");
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
client: standaloneClient,
|
|
63
|
+
isReady: standaloneReady,
|
|
64
|
+
error: standaloneError,
|
|
65
|
+
initialize: standaloneInitialize
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/hooks/use-stealth-address.ts
|
|
70
|
+
import { useState as useState2, useEffect, useCallback as useCallback2 } from "react";
|
|
71
|
+
import {
|
|
72
|
+
generateStealthMetaAddress,
|
|
73
|
+
generateStealthAddress,
|
|
74
|
+
generateEd25519StealthMetaAddress,
|
|
75
|
+
generateEd25519StealthAddress,
|
|
76
|
+
encodeStealthMetaAddress,
|
|
77
|
+
isEd25519Chain
|
|
78
|
+
} from "@sip-protocol/sdk";
|
|
79
|
+
function useStealthAddress(chain) {
|
|
80
|
+
const [metaAddress, setMetaAddress] = useState2(null);
|
|
81
|
+
const [stealthAddress, setStealthAddress] = useState2(null);
|
|
82
|
+
const [isGenerating, setIsGenerating] = useState2(false);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
let cancelled = false;
|
|
85
|
+
setIsGenerating(true);
|
|
86
|
+
const timer = setTimeout(() => {
|
|
87
|
+
if (cancelled) return;
|
|
88
|
+
try {
|
|
89
|
+
const isEd25519 = isEd25519Chain(chain);
|
|
90
|
+
const metaAddressData = isEd25519 ? generateEd25519StealthMetaAddress(chain) : generateStealthMetaAddress(chain);
|
|
91
|
+
const encoded = encodeStealthMetaAddress(metaAddressData.metaAddress);
|
|
92
|
+
if (cancelled) return;
|
|
93
|
+
setMetaAddress(encoded);
|
|
94
|
+
const stealthData = isEd25519 ? generateEd25519StealthAddress(metaAddressData.metaAddress) : generateStealthAddress(metaAddressData.metaAddress);
|
|
95
|
+
if (cancelled) return;
|
|
96
|
+
setStealthAddress(stealthData.stealthAddress.address);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error("Failed to generate stealth addresses:", error);
|
|
99
|
+
if (cancelled) return;
|
|
100
|
+
setMetaAddress(null);
|
|
101
|
+
setStealthAddress(null);
|
|
102
|
+
} finally {
|
|
103
|
+
if (!cancelled) {
|
|
104
|
+
setIsGenerating(false);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}, 0);
|
|
108
|
+
return () => {
|
|
109
|
+
cancelled = true;
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
};
|
|
112
|
+
}, [chain]);
|
|
113
|
+
const regenerate = useCallback2(() => {
|
|
114
|
+
if (!metaAddress) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
setIsGenerating(true);
|
|
118
|
+
setTimeout(() => {
|
|
119
|
+
try {
|
|
120
|
+
const parts = metaAddress.split(":");
|
|
121
|
+
if (parts.length < 4) {
|
|
122
|
+
throw new Error("Invalid meta-address format");
|
|
123
|
+
}
|
|
124
|
+
const [, chainId, spendingKey, viewingKey] = parts;
|
|
125
|
+
const metaAddressObj = {
|
|
126
|
+
chain: chainId,
|
|
127
|
+
spendingKey: spendingKey.startsWith("0x") ? spendingKey : `0x${spendingKey}`,
|
|
128
|
+
viewingKey: viewingKey.startsWith("0x") ? viewingKey : `0x${viewingKey}`
|
|
129
|
+
};
|
|
130
|
+
const isEd25519 = isEd25519Chain(chain);
|
|
131
|
+
const stealthData = isEd25519 ? generateEd25519StealthAddress(metaAddressObj) : generateStealthAddress(metaAddressObj);
|
|
132
|
+
setStealthAddress(stealthData.stealthAddress.address);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error("Failed to regenerate stealth address:", error);
|
|
135
|
+
} finally {
|
|
136
|
+
setIsGenerating(false);
|
|
137
|
+
}
|
|
138
|
+
}, 0);
|
|
139
|
+
}, [metaAddress, chain]);
|
|
140
|
+
const copyToClipboard = useCallback2(async () => {
|
|
141
|
+
if (!stealthAddress) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
await navigator.clipboard.writeText(stealthAddress);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error("Failed to copy to clipboard:", error);
|
|
148
|
+
const textArea = document.createElement("textarea");
|
|
149
|
+
textArea.value = stealthAddress;
|
|
150
|
+
textArea.style.position = "fixed";
|
|
151
|
+
textArea.style.left = "-999999px";
|
|
152
|
+
document.body.appendChild(textArea);
|
|
153
|
+
textArea.select();
|
|
154
|
+
try {
|
|
155
|
+
document.execCommand("copy");
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.error("Fallback copy failed:", err);
|
|
158
|
+
} finally {
|
|
159
|
+
document.body.removeChild(textArea);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}, [stealthAddress]);
|
|
163
|
+
return {
|
|
164
|
+
metaAddress,
|
|
165
|
+
stealthAddress,
|
|
166
|
+
isGenerating,
|
|
167
|
+
regenerate,
|
|
168
|
+
copyToClipboard
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/hooks/use-private-swap.ts
|
|
173
|
+
import { useState as useState3, useCallback as useCallback3 } from "react";
|
|
174
|
+
function usePrivateSwap() {
|
|
175
|
+
const { client: sip } = useSIP();
|
|
176
|
+
const [quote, setQuote] = useState3(null);
|
|
177
|
+
const [status, setStatus] = useState3("idle");
|
|
178
|
+
const [isLoading, setIsLoading] = useState3(false);
|
|
179
|
+
const [error, setError] = useState3(null);
|
|
180
|
+
const fetchQuote = useCallback3(async (params) => {
|
|
181
|
+
if (!sip) {
|
|
182
|
+
throw new Error("SIP client not initialized. Wrap your app with SIPProvider or call initialize().");
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
setStatus("fetching_quote");
|
|
186
|
+
setIsLoading(true);
|
|
187
|
+
setError(null);
|
|
188
|
+
const intentParams = {
|
|
189
|
+
input: {
|
|
190
|
+
asset: {
|
|
191
|
+
chain: params.inputChain,
|
|
192
|
+
symbol: params.inputToken,
|
|
193
|
+
address: null,
|
|
194
|
+
decimals: 9
|
|
195
|
+
// Default, should be configurable
|
|
196
|
+
},
|
|
197
|
+
amount: BigInt(params.inputAmount)
|
|
198
|
+
},
|
|
199
|
+
output: {
|
|
200
|
+
asset: {
|
|
201
|
+
chain: params.outputChain,
|
|
202
|
+
symbol: params.outputToken,
|
|
203
|
+
address: null,
|
|
204
|
+
decimals: 18
|
|
205
|
+
// Default, should be configurable
|
|
206
|
+
},
|
|
207
|
+
minAmount: 0n,
|
|
208
|
+
maxSlippage: params.maxSlippage ?? 0.01
|
|
209
|
+
},
|
|
210
|
+
privacy: params.privacyLevel ?? "shielded"
|
|
211
|
+
};
|
|
212
|
+
const quotes = await sip.getQuotes(intentParams);
|
|
213
|
+
if (quotes.length === 0) {
|
|
214
|
+
throw new Error("No quotes available");
|
|
215
|
+
}
|
|
216
|
+
setQuote(quotes[0]);
|
|
217
|
+
setStatus("idle");
|
|
218
|
+
} catch (err) {
|
|
219
|
+
setError(err instanceof Error ? err : new Error("Failed to fetch quote"));
|
|
220
|
+
setStatus("failed");
|
|
221
|
+
throw err;
|
|
222
|
+
} finally {
|
|
223
|
+
setIsLoading(false);
|
|
224
|
+
}
|
|
225
|
+
}, [sip]);
|
|
226
|
+
const swap = useCallback3(async (params) => {
|
|
227
|
+
if (!sip) {
|
|
228
|
+
throw new Error("SIP client not initialized. Wrap your app with SIPProvider or call initialize().");
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
setStatus("pending");
|
|
232
|
+
setIsLoading(true);
|
|
233
|
+
setError(null);
|
|
234
|
+
const intentParams = {
|
|
235
|
+
input: {
|
|
236
|
+
asset: {
|
|
237
|
+
chain: params.input.chain,
|
|
238
|
+
symbol: params.input.token,
|
|
239
|
+
address: null,
|
|
240
|
+
decimals: 9
|
|
241
|
+
// Default, should be configurable
|
|
242
|
+
},
|
|
243
|
+
amount: params.input.amount
|
|
244
|
+
},
|
|
245
|
+
output: {
|
|
246
|
+
asset: {
|
|
247
|
+
chain: params.output.chain,
|
|
248
|
+
symbol: params.output.token,
|
|
249
|
+
address: null,
|
|
250
|
+
decimals: 18
|
|
251
|
+
// Default, should be configurable
|
|
252
|
+
},
|
|
253
|
+
minAmount: params.output.minAmount,
|
|
254
|
+
maxSlippage: params.maxSlippage ?? 0.01
|
|
255
|
+
},
|
|
256
|
+
privacy: params.privacyLevel
|
|
257
|
+
};
|
|
258
|
+
const intent = await sip.createIntent(intentParams);
|
|
259
|
+
let swapQuote = quote;
|
|
260
|
+
if (!swapQuote) {
|
|
261
|
+
const quotes = await sip.getQuotes(intentParams);
|
|
262
|
+
if (quotes.length === 0) {
|
|
263
|
+
throw new Error("No quotes available");
|
|
264
|
+
}
|
|
265
|
+
swapQuote = quotes[0];
|
|
266
|
+
}
|
|
267
|
+
setStatus("confirming");
|
|
268
|
+
const result = await sip.execute(intent, swapQuote);
|
|
269
|
+
setStatus("completed");
|
|
270
|
+
return {
|
|
271
|
+
txHash: result.txHash,
|
|
272
|
+
status: result.status,
|
|
273
|
+
outputAmount: result.outputAmount,
|
|
274
|
+
intentId: result.intentId
|
|
275
|
+
};
|
|
276
|
+
} catch (err) {
|
|
277
|
+
setError(err instanceof Error ? err : new Error("Swap failed"));
|
|
278
|
+
setStatus("failed");
|
|
279
|
+
throw err;
|
|
280
|
+
} finally {
|
|
281
|
+
setIsLoading(false);
|
|
282
|
+
}
|
|
283
|
+
}, [sip, quote]);
|
|
284
|
+
const reset = useCallback3(() => {
|
|
285
|
+
setQuote(null);
|
|
286
|
+
setStatus("idle");
|
|
287
|
+
setIsLoading(false);
|
|
288
|
+
setError(null);
|
|
289
|
+
}, []);
|
|
290
|
+
return {
|
|
291
|
+
quote,
|
|
292
|
+
fetchQuote,
|
|
293
|
+
swap,
|
|
294
|
+
status,
|
|
295
|
+
isLoading,
|
|
296
|
+
error,
|
|
297
|
+
reset
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/hooks/use-viewing-key.ts
|
|
302
|
+
import { useState as useState4, useCallback as useCallback4 } from "react";
|
|
303
|
+
import {
|
|
304
|
+
generateViewingKey as sdkGenerateViewingKey,
|
|
305
|
+
decryptWithViewing
|
|
306
|
+
} from "@sip-protocol/sdk";
|
|
307
|
+
function useViewingKey() {
|
|
308
|
+
const [viewingKey, setViewingKey] = useState4(null);
|
|
309
|
+
const [sharedWith, setSharedWith] = useState4([]);
|
|
310
|
+
const generate = useCallback4((path) => {
|
|
311
|
+
const key = sdkGenerateViewingKey(path);
|
|
312
|
+
setViewingKey(key);
|
|
313
|
+
setSharedWith([]);
|
|
314
|
+
return key;
|
|
315
|
+
}, []);
|
|
316
|
+
const decrypt = useCallback4(
|
|
317
|
+
async (encrypted) => {
|
|
318
|
+
if (!viewingKey) {
|
|
319
|
+
throw new Error("No viewing key available. Call generate() first.");
|
|
320
|
+
}
|
|
321
|
+
return decryptWithViewing(encrypted, viewingKey);
|
|
322
|
+
},
|
|
323
|
+
[viewingKey]
|
|
324
|
+
);
|
|
325
|
+
const share = useCallback4(
|
|
326
|
+
async (auditorId) => {
|
|
327
|
+
if (!viewingKey) {
|
|
328
|
+
throw new Error("No viewing key available. Call generate() first.");
|
|
329
|
+
}
|
|
330
|
+
const shareEntry = {
|
|
331
|
+
auditorId,
|
|
332
|
+
viewingKeyHash: viewingKey.hash,
|
|
333
|
+
sharedAt: Date.now()
|
|
334
|
+
};
|
|
335
|
+
setSharedWith((prev) => [...prev, shareEntry]);
|
|
336
|
+
},
|
|
337
|
+
[viewingKey]
|
|
338
|
+
);
|
|
339
|
+
return {
|
|
340
|
+
/** Current viewing key (null if not generated) */
|
|
341
|
+
viewingKey,
|
|
342
|
+
/** List of auditors who have been given access */
|
|
343
|
+
sharedWith,
|
|
344
|
+
/** Generate a new viewing key */
|
|
345
|
+
generate,
|
|
346
|
+
/** Decrypt encrypted transaction data */
|
|
347
|
+
decrypt,
|
|
348
|
+
/** Share viewing key with an auditor */
|
|
349
|
+
share
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
export {
|
|
353
|
+
SIPProvider,
|
|
354
|
+
usePrivateSwap,
|
|
355
|
+
useSIP,
|
|
356
|
+
useStealthAddress,
|
|
357
|
+
useViewingKey
|
|
358
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sip-protocol/react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React hooks for Shielded Intents Protocol",
|
|
5
|
+
"author": "SIP Protocol <hello@sip-protocol.org>",
|
|
6
|
+
"homepage": "https://sip-protocol.org",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/sip-protocol/sip-protocol.git",
|
|
10
|
+
"directory": "packages/react"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/sip-protocol/sip-protocol/issues"
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"module": "./dist/index.mjs",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.mjs",
|
|
22
|
+
"require": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@sip-protocol/sdk": "^0.6.0",
|
|
31
|
+
"@sip-protocol/types": "^0.2.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": "^18.0.0",
|
|
35
|
+
"react-dom": "^18.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@testing-library/react": "^16.3.0",
|
|
39
|
+
"@testing-library/react-hooks": "^8.0.1",
|
|
40
|
+
"@types/react": "^18.2.0",
|
|
41
|
+
"@types/react-dom": "^18.2.0",
|
|
42
|
+
"jsdom": "^27.2.0",
|
|
43
|
+
"tsup": "^8.0.0",
|
|
44
|
+
"typescript": "^5.3.0",
|
|
45
|
+
"vitest": "^1.1.0"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"sip",
|
|
49
|
+
"privacy",
|
|
50
|
+
"intents",
|
|
51
|
+
"cross-chain",
|
|
52
|
+
"react",
|
|
53
|
+
"hooks"
|
|
54
|
+
],
|
|
55
|
+
"license": "MIT",
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
58
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
59
|
+
"lint": "eslint --ext .ts,.tsx src/",
|
|
60
|
+
"typecheck": "tsc --noEmit",
|
|
61
|
+
"clean": "rm -rf dist",
|
|
62
|
+
"test": "vitest"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { useSIP, type UseSIPReturn } from './use-sip'
|
|
2
|
+
export { useStealthAddress } from './use-stealth-address'
|
|
3
|
+
export {
|
|
4
|
+
usePrivateSwap,
|
|
5
|
+
type SwapStatus,
|
|
6
|
+
type QuoteParams,
|
|
7
|
+
type SwapParams,
|
|
8
|
+
type SwapResult,
|
|
9
|
+
} from './use-private-swap'
|
|
10
|
+
export { useViewingKey } from './use-viewing-key'
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react'
|
|
2
|
+
import { useSIP } from './use-sip'
|
|
3
|
+
import type { Quote, PrivacyLevel, CreateIntentParams, TrackedIntent, FulfillmentResult } from '@sip-protocol/types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Status of the swap lifecycle
|
|
7
|
+
*/
|
|
8
|
+
export type SwapStatus =
|
|
9
|
+
| 'idle'
|
|
10
|
+
| 'fetching_quote'
|
|
11
|
+
| 'pending'
|
|
12
|
+
| 'confirming'
|
|
13
|
+
| 'completed'
|
|
14
|
+
| 'failed'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parameters for fetching a quote
|
|
18
|
+
*/
|
|
19
|
+
export interface QuoteParams {
|
|
20
|
+
/** Input chain */
|
|
21
|
+
inputChain: string
|
|
22
|
+
/** Output chain */
|
|
23
|
+
outputChain: string
|
|
24
|
+
/** Input token symbol */
|
|
25
|
+
inputToken: string
|
|
26
|
+
/** Output token symbol */
|
|
27
|
+
outputToken: string
|
|
28
|
+
/** Input amount (as string, in smallest unit) */
|
|
29
|
+
inputAmount: string
|
|
30
|
+
/** Privacy level (optional) */
|
|
31
|
+
privacyLevel?: PrivacyLevel
|
|
32
|
+
/** Maximum acceptable slippage (0-1, e.g. 0.01 = 1%) */
|
|
33
|
+
maxSlippage?: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parameters for executing a swap
|
|
38
|
+
*/
|
|
39
|
+
export interface SwapParams {
|
|
40
|
+
/** Input asset details */
|
|
41
|
+
input: {
|
|
42
|
+
chain: string
|
|
43
|
+
token: string
|
|
44
|
+
amount: bigint
|
|
45
|
+
}
|
|
46
|
+
/** Output asset details */
|
|
47
|
+
output: {
|
|
48
|
+
chain: string
|
|
49
|
+
token: string
|
|
50
|
+
minAmount: bigint
|
|
51
|
+
}
|
|
52
|
+
/** Privacy level */
|
|
53
|
+
privacyLevel: PrivacyLevel
|
|
54
|
+
/** Maximum acceptable slippage (0-1, e.g. 0.01 = 1%) */
|
|
55
|
+
maxSlippage?: number
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Result of a swap execution
|
|
60
|
+
*/
|
|
61
|
+
export interface SwapResult {
|
|
62
|
+
/** Transaction hash (if available) */
|
|
63
|
+
txHash?: string
|
|
64
|
+
/** Status of the swap */
|
|
65
|
+
status: string
|
|
66
|
+
/** Output amount received */
|
|
67
|
+
outputAmount?: bigint
|
|
68
|
+
/** Intent ID */
|
|
69
|
+
intentId: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* usePrivateSwap - Execute private swaps with shielded intents
|
|
74
|
+
*
|
|
75
|
+
* @remarks
|
|
76
|
+
* Hook for managing the complete lifecycle of a private swap:
|
|
77
|
+
* - Fetch quotes from solvers
|
|
78
|
+
* - Execute swaps with privacy
|
|
79
|
+
* - Track swap status through completion
|
|
80
|
+
* - Handle errors gracefully
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```tsx
|
|
84
|
+
* import { usePrivateSwap } from '@sip-protocol/react'
|
|
85
|
+
* import { PrivacyLevel } from '@sip-protocol/types'
|
|
86
|
+
*
|
|
87
|
+
* function MyComponent() {
|
|
88
|
+
* const { quote, fetchQuote, swap, status, isLoading, error, reset } = usePrivateSwap()
|
|
89
|
+
*
|
|
90
|
+
* // Fetch a quote
|
|
91
|
+
* const handleGetQuote = async () => {
|
|
92
|
+
* await fetchQuote({
|
|
93
|
+
* inputChain: 'solana',
|
|
94
|
+
* outputChain: 'ethereum',
|
|
95
|
+
* inputToken: 'SOL',
|
|
96
|
+
* outputToken: 'ETH',
|
|
97
|
+
* inputAmount: '1000000000', // 1 SOL
|
|
98
|
+
* })
|
|
99
|
+
* }
|
|
100
|
+
*
|
|
101
|
+
* // Execute the swap
|
|
102
|
+
* const handleSwap = async () => {
|
|
103
|
+
* const result = await swap({
|
|
104
|
+
* input: { chain: 'solana', token: 'SOL', amount: 1000000000n },
|
|
105
|
+
* output: { chain: 'ethereum', token: 'ETH', minAmount: 0n },
|
|
106
|
+
* privacyLevel: PrivacyLevel.SHIELDED,
|
|
107
|
+
* maxSlippage: 0.01,
|
|
108
|
+
* })
|
|
109
|
+
* }
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export function usePrivateSwap() {
|
|
114
|
+
const { client: sip } = useSIP()
|
|
115
|
+
|
|
116
|
+
// State
|
|
117
|
+
const [quote, setQuote] = useState<Quote | null>(null)
|
|
118
|
+
const [status, setStatus] = useState<SwapStatus>('idle')
|
|
119
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
120
|
+
const [error, setError] = useState<Error | null>(null)
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Fetch a quote for the given parameters
|
|
124
|
+
*/
|
|
125
|
+
const fetchQuote = useCallback(async (params: QuoteParams): Promise<void> => {
|
|
126
|
+
if (!sip) {
|
|
127
|
+
throw new Error('SIP client not initialized. Wrap your app with SIPProvider or call initialize().')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
setStatus('fetching_quote')
|
|
132
|
+
setIsLoading(true)
|
|
133
|
+
setError(null)
|
|
134
|
+
|
|
135
|
+
// Build CreateIntentParams from QuoteParams
|
|
136
|
+
const intentParams: CreateIntentParams = {
|
|
137
|
+
input: {
|
|
138
|
+
asset: {
|
|
139
|
+
chain: params.inputChain as any,
|
|
140
|
+
symbol: params.inputToken,
|
|
141
|
+
address: null,
|
|
142
|
+
decimals: 9, // Default, should be configurable
|
|
143
|
+
},
|
|
144
|
+
amount: BigInt(params.inputAmount),
|
|
145
|
+
},
|
|
146
|
+
output: {
|
|
147
|
+
asset: {
|
|
148
|
+
chain: params.outputChain as any,
|
|
149
|
+
symbol: params.outputToken,
|
|
150
|
+
address: null,
|
|
151
|
+
decimals: 18, // Default, should be configurable
|
|
152
|
+
},
|
|
153
|
+
minAmount: 0n,
|
|
154
|
+
maxSlippage: params.maxSlippage ?? 0.01,
|
|
155
|
+
},
|
|
156
|
+
privacy: params.privacyLevel ?? 'shielded' as PrivacyLevel,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Get quotes from SIP client
|
|
160
|
+
const quotes = await sip.getQuotes(intentParams)
|
|
161
|
+
|
|
162
|
+
if (quotes.length === 0) {
|
|
163
|
+
throw new Error('No quotes available')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Use the best quote (first one, assuming they're sorted)
|
|
167
|
+
setQuote(quotes[0])
|
|
168
|
+
setStatus('idle')
|
|
169
|
+
} catch (err) {
|
|
170
|
+
setError(err instanceof Error ? err : new Error('Failed to fetch quote'))
|
|
171
|
+
setStatus('failed')
|
|
172
|
+
throw err
|
|
173
|
+
} finally {
|
|
174
|
+
setIsLoading(false)
|
|
175
|
+
}
|
|
176
|
+
}, [sip])
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Execute a swap with the given parameters
|
|
180
|
+
*/
|
|
181
|
+
const swap = useCallback(async (params: SwapParams): Promise<SwapResult> => {
|
|
182
|
+
if (!sip) {
|
|
183
|
+
throw new Error('SIP client not initialized. Wrap your app with SIPProvider or call initialize().')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
setStatus('pending')
|
|
188
|
+
setIsLoading(true)
|
|
189
|
+
setError(null)
|
|
190
|
+
|
|
191
|
+
// Create the shielded intent
|
|
192
|
+
const intentParams: CreateIntentParams = {
|
|
193
|
+
input: {
|
|
194
|
+
asset: {
|
|
195
|
+
chain: params.input.chain as any,
|
|
196
|
+
symbol: params.input.token,
|
|
197
|
+
address: null,
|
|
198
|
+
decimals: 9, // Default, should be configurable
|
|
199
|
+
},
|
|
200
|
+
amount: params.input.amount,
|
|
201
|
+
},
|
|
202
|
+
output: {
|
|
203
|
+
asset: {
|
|
204
|
+
chain: params.output.chain as any,
|
|
205
|
+
symbol: params.output.token,
|
|
206
|
+
address: null,
|
|
207
|
+
decimals: 18, // Default, should be configurable
|
|
208
|
+
},
|
|
209
|
+
minAmount: params.output.minAmount,
|
|
210
|
+
maxSlippage: params.maxSlippage ?? 0.01,
|
|
211
|
+
},
|
|
212
|
+
privacy: params.privacyLevel,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const intent: TrackedIntent = await sip.createIntent(intentParams)
|
|
216
|
+
|
|
217
|
+
// Get quotes if we don't have one cached
|
|
218
|
+
let swapQuote = quote
|
|
219
|
+
if (!swapQuote) {
|
|
220
|
+
const quotes = await sip.getQuotes(intentParams)
|
|
221
|
+
if (quotes.length === 0) {
|
|
222
|
+
throw new Error('No quotes available')
|
|
223
|
+
}
|
|
224
|
+
swapQuote = quotes[0]
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
setStatus('confirming')
|
|
228
|
+
|
|
229
|
+
// Execute the swap
|
|
230
|
+
const result: FulfillmentResult = await sip.execute(intent, swapQuote)
|
|
231
|
+
|
|
232
|
+
setStatus('completed')
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
txHash: result.txHash,
|
|
236
|
+
status: result.status,
|
|
237
|
+
outputAmount: result.outputAmount,
|
|
238
|
+
intentId: result.intentId,
|
|
239
|
+
}
|
|
240
|
+
} catch (err) {
|
|
241
|
+
setError(err instanceof Error ? err : new Error('Swap failed'))
|
|
242
|
+
setStatus('failed')
|
|
243
|
+
throw err
|
|
244
|
+
} finally {
|
|
245
|
+
setIsLoading(false)
|
|
246
|
+
}
|
|
247
|
+
}, [sip, quote])
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Reset all state to initial values
|
|
251
|
+
*/
|
|
252
|
+
const reset = useCallback(() => {
|
|
253
|
+
setQuote(null)
|
|
254
|
+
setStatus('idle')
|
|
255
|
+
setIsLoading(false)
|
|
256
|
+
setError(null)
|
|
257
|
+
}, [])
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
quote,
|
|
261
|
+
fetchQuote,
|
|
262
|
+
swap,
|
|
263
|
+
status,
|
|
264
|
+
isLoading,
|
|
265
|
+
error,
|
|
266
|
+
reset,
|
|
267
|
+
}
|
|
268
|
+
}
|