@hfunlabs/hypurr-connect 0.1.13 → 0.1.15
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/README.md +12 -0
- package/dist/index.d.ts +89 -4
- package/dist/index.js +1858 -348
- package/dist/index.js.map +1 -1
- package/package.json +17 -6
- package/src/AddWalletModal.tsx +744 -0
- package/src/AgentExpiryWarning.tsx +129 -0
- package/src/DeleteWalletModal.tsx +5 -1
- package/src/HypurrConnectProvider.tsx +193 -1
- package/src/LoginModal.tsx +30 -19
- package/src/RenameWalletModal.tsx +5 -1
- package/src/RenewAgentModal.tsx +380 -0
- package/src/UserProfileModal.tsx +157 -25
- package/src/WalletSelectorDropdown.tsx +146 -6
- package/src/agent.ts +38 -12
- package/src/agentWallet.ts +86 -0
- package/src/css.d.ts +1 -0
- package/src/icons/lucide.tsx +61 -0
- package/src/index.ts +17 -1
- package/src/profileStyles.ts +58 -0
- package/src/styles.css +1 -0
- package/src/tailwind.css +77 -0
- package/src/types.ts +13 -0
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
import { AnimatePresence, motion } from "framer-motion";
|
|
2
|
+
import type { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
|
|
3
|
+
import { useCallback, useState, type ReactNode } from "react";
|
|
4
|
+
import { useHypurrConnectInternal } from "./HypurrConnectProvider";
|
|
5
|
+
import {
|
|
6
|
+
AGENT_APPROVAL_DURATION_OPTIONS,
|
|
7
|
+
DEFAULT_AGENT_APPROVAL_DURATION_MS,
|
|
8
|
+
createTelegramAgentApprovalName,
|
|
9
|
+
type AgentApprovalDurationOption,
|
|
10
|
+
} from "./agentWallet";
|
|
11
|
+
import {
|
|
12
|
+
AlertTriangle,
|
|
13
|
+
Check,
|
|
14
|
+
Eye,
|
|
15
|
+
EyeOff,
|
|
16
|
+
KeyRound,
|
|
17
|
+
Loader2,
|
|
18
|
+
Plus,
|
|
19
|
+
SpinKeyframes,
|
|
20
|
+
Wallet,
|
|
21
|
+
X,
|
|
22
|
+
} from "./icons/lucide";
|
|
23
|
+
import type { Hex, SignTypedDataFn } from "./types";
|
|
24
|
+
|
|
25
|
+
export interface AddWalletModalProps {
|
|
26
|
+
isOpen: boolean;
|
|
27
|
+
onClose: () => void;
|
|
28
|
+
ownerAddress?: string | null;
|
|
29
|
+
isWalletConnected?: boolean;
|
|
30
|
+
chainId?: number;
|
|
31
|
+
signTypedDataAsync?: SignTypedDataFn;
|
|
32
|
+
onConnectWallet?: () => void;
|
|
33
|
+
onDisconnectWallet?: () => void;
|
|
34
|
+
onWalletAdded?: (wallet?: HyperliquidWallet) => void | Promise<void>;
|
|
35
|
+
onAddReadOnlyWallet?: (address: string) => void | Promise<void>;
|
|
36
|
+
onNotify?: (n: { type: "success" | "error"; message: string }) => void;
|
|
37
|
+
hyperliquidChain?: "Mainnet" | "Testnet" | string;
|
|
38
|
+
/** Expiration choices for owner-approved agent wallets. Defaults to 1/7/30/90 days. */
|
|
39
|
+
agentApprovalDurationOptions?: AgentApprovalDurationOption[];
|
|
40
|
+
/** Initial owner approval duration. Defaults to 1 day. */
|
|
41
|
+
defaultAgentApprovalDurationMs?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type TabKey = "generate" | "import" | "api" | "readonly";
|
|
45
|
+
|
|
46
|
+
const WALLET_NAME_REGEX = /^[a-zA-Z0-9/]*$/;
|
|
47
|
+
const PRIVATE_KEY_REGEX = /^(0x)?[a-fA-F0-9]{64}$/;
|
|
48
|
+
const BASE_TAB_KEYS: TabKey[] = ["generate", "import", "api"];
|
|
49
|
+
|
|
50
|
+
function createAuthSignatureRequest(
|
|
51
|
+
chainId: number,
|
|
52
|
+
hyperliquidChain: string,
|
|
53
|
+
agentAddress: string,
|
|
54
|
+
approvalDurationMs: number,
|
|
55
|
+
) {
|
|
56
|
+
const nonce = Date.now();
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
domain: {
|
|
60
|
+
name: "HyperliquidSignTransaction",
|
|
61
|
+
version: "1",
|
|
62
|
+
chainId,
|
|
63
|
+
verifyingContract: "0x0000000000000000000000000000000000000000" as const,
|
|
64
|
+
},
|
|
65
|
+
types: {
|
|
66
|
+
"HyperliquidTransaction:ApproveAgent": [
|
|
67
|
+
{ name: "hyperliquidChain", type: "string" },
|
|
68
|
+
{ name: "agentAddress", type: "address" },
|
|
69
|
+
{ name: "agentName", type: "string" },
|
|
70
|
+
{ name: "nonce", type: "uint64" },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
primaryType: "HyperliquidTransaction:ApproveAgent",
|
|
74
|
+
message: {
|
|
75
|
+
hyperliquidChain,
|
|
76
|
+
agentAddress: agentAddress.toLowerCase() as Hex,
|
|
77
|
+
agentName: createTelegramAgentApprovalName(approvalDurationMs),
|
|
78
|
+
nonce,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const tabClass = (selected: boolean) =>
|
|
84
|
+
`flex-1 py-1.5 rounded text-base font-medium flex items-center justify-center gap-1.5 ${
|
|
85
|
+
selected ? "btn-raised-active" : "btn-raised"
|
|
86
|
+
}`;
|
|
87
|
+
|
|
88
|
+
const inputClass = (hasError?: boolean) =>
|
|
89
|
+
`w-full bg-surface-btn/90 border ${
|
|
90
|
+
hasError ? "border-trade-down/50" : "border-surface-bd"
|
|
91
|
+
} rounded-lg px-3 py-2.5 text-white placeholder-gray-600 focus:outline-none font-mono text-base`;
|
|
92
|
+
|
|
93
|
+
const formatAddress = (address: string) =>
|
|
94
|
+
`${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
95
|
+
|
|
96
|
+
export function AddWalletModal({
|
|
97
|
+
isOpen,
|
|
98
|
+
onClose,
|
|
99
|
+
ownerAddress,
|
|
100
|
+
isWalletConnected,
|
|
101
|
+
chainId,
|
|
102
|
+
signTypedDataAsync,
|
|
103
|
+
onConnectWallet,
|
|
104
|
+
onDisconnectWallet,
|
|
105
|
+
onWalletAdded,
|
|
106
|
+
onAddReadOnlyWallet,
|
|
107
|
+
onNotify,
|
|
108
|
+
hyperliquidChain = "Mainnet",
|
|
109
|
+
agentApprovalDurationOptions,
|
|
110
|
+
defaultAgentApprovalDurationMs = DEFAULT_AGENT_APPROVAL_DURATION_MS,
|
|
111
|
+
}: AddWalletModalProps): ReactNode {
|
|
112
|
+
const tabKeys = onAddReadOnlyWallet
|
|
113
|
+
? [...BASE_TAB_KEYS, "readonly" as const]
|
|
114
|
+
: BASE_TAB_KEYS;
|
|
115
|
+
|
|
116
|
+
const [activeTab, setActiveTab] = useState<TabKey>("generate");
|
|
117
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
118
|
+
const [walletName, setWalletName] = useState("");
|
|
119
|
+
const [newWalletName, setNewWalletName] = useState("");
|
|
120
|
+
const [importWalletName, setImportWalletName] = useState("");
|
|
121
|
+
const [importPrivateKey, setImportPrivateKey] = useState("");
|
|
122
|
+
const [showImportPrivateKey, setShowImportPrivateKey] = useState(false);
|
|
123
|
+
const [readOnlyAddress, setReadOnlyAddress] = useState("");
|
|
124
|
+
const [agentApprovalDurationMs, setAgentApprovalDurationMs] = useState(
|
|
125
|
+
defaultAgentApprovalDurationMs,
|
|
126
|
+
);
|
|
127
|
+
const [error, setError] = useState<string | null>(null);
|
|
128
|
+
|
|
129
|
+
const {
|
|
130
|
+
authMethod,
|
|
131
|
+
createWallet,
|
|
132
|
+
isLoading: userLoading,
|
|
133
|
+
refreshWallets,
|
|
134
|
+
telegramClient,
|
|
135
|
+
telegramRpcOptions,
|
|
136
|
+
} = useHypurrConnectInternal();
|
|
137
|
+
|
|
138
|
+
const isValidWalletName =
|
|
139
|
+
walletName.length > 0 && WALLET_NAME_REGEX.test(walletName);
|
|
140
|
+
const isValidNewWalletName =
|
|
141
|
+
newWalletName.length > 0 && WALLET_NAME_REGEX.test(newWalletName);
|
|
142
|
+
const isValidImportWalletName =
|
|
143
|
+
importWalletName.length > 0 && WALLET_NAME_REGEX.test(importWalletName);
|
|
144
|
+
const trimmedImportPrivateKey = importPrivateKey.trim();
|
|
145
|
+
const isValidImportPrivateKey = PRIVATE_KEY_REGEX.test(
|
|
146
|
+
trimmedImportPrivateKey,
|
|
147
|
+
);
|
|
148
|
+
const isProcessing = isLoading || userLoading;
|
|
149
|
+
const durationOptions =
|
|
150
|
+
agentApprovalDurationOptions && agentApprovalDurationOptions.length > 0
|
|
151
|
+
? agentApprovalDurationOptions
|
|
152
|
+
: AGENT_APPROVAL_DURATION_OPTIONS;
|
|
153
|
+
|
|
154
|
+
const handleClose = useCallback(() => {
|
|
155
|
+
if (isProcessing) return;
|
|
156
|
+
setError(null);
|
|
157
|
+
setImportPrivateKey("");
|
|
158
|
+
setShowImportPrivateKey(false);
|
|
159
|
+
onClose();
|
|
160
|
+
}, [isProcessing, onClose]);
|
|
161
|
+
|
|
162
|
+
const assertTelegramReady = useCallback(() => {
|
|
163
|
+
if (authMethod !== "telegram" || !telegramRpcOptions) {
|
|
164
|
+
throw new Error("Telegram login is required to add wallets");
|
|
165
|
+
}
|
|
166
|
+
}, [authMethod, telegramRpcOptions]);
|
|
167
|
+
|
|
168
|
+
const completeWalletAdded = useCallback(
|
|
169
|
+
async (wallet?: HyperliquidWallet) => {
|
|
170
|
+
refreshWallets();
|
|
171
|
+
await onWalletAdded?.(wallet);
|
|
172
|
+
},
|
|
173
|
+
[onWalletAdded, refreshWallets],
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const handleCreateWallet = useCallback(async () => {
|
|
177
|
+
if (!isValidNewWalletName) {
|
|
178
|
+
setError("Wallet name may only contain letters, numbers, and /");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
setError(null);
|
|
182
|
+
setIsLoading(true);
|
|
183
|
+
try {
|
|
184
|
+
assertTelegramReady();
|
|
185
|
+
const wallet = await createWallet(newWalletName);
|
|
186
|
+
await completeWalletAdded(wallet);
|
|
187
|
+
onNotify?.({ type: "success", message: "Wallet created successfully" });
|
|
188
|
+
setNewWalletName("");
|
|
189
|
+
handleClose();
|
|
190
|
+
} catch (e: unknown) {
|
|
191
|
+
const message =
|
|
192
|
+
e instanceof Error ? e.message : "Failed to create wallet";
|
|
193
|
+
setError(message);
|
|
194
|
+
onNotify?.({ type: "error", message });
|
|
195
|
+
} finally {
|
|
196
|
+
setIsLoading(false);
|
|
197
|
+
}
|
|
198
|
+
}, [
|
|
199
|
+
assertTelegramReady,
|
|
200
|
+
completeWalletAdded,
|
|
201
|
+
createWallet,
|
|
202
|
+
handleClose,
|
|
203
|
+
isValidNewWalletName,
|
|
204
|
+
newWalletName,
|
|
205
|
+
onNotify,
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
const handleImportWallet = useCallback(async () => {
|
|
209
|
+
if (!isValidImportWalletName) {
|
|
210
|
+
setError("Wallet name may only contain letters, numbers, and /");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (!isValidImportPrivateKey) {
|
|
214
|
+
setError("Please enter a valid 64-character hex private key");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
setError(null);
|
|
218
|
+
setIsLoading(true);
|
|
219
|
+
try {
|
|
220
|
+
assertTelegramReady();
|
|
221
|
+
const response = await telegramClient.hyperliquidWalletImport(
|
|
222
|
+
{
|
|
223
|
+
authData: {},
|
|
224
|
+
name: importWalletName,
|
|
225
|
+
privateKey: trimmedImportPrivateKey,
|
|
226
|
+
},
|
|
227
|
+
telegramRpcOptions,
|
|
228
|
+
);
|
|
229
|
+
await completeWalletAdded(response.response.wallet);
|
|
230
|
+
onNotify?.({ type: "success", message: "Wallet imported successfully" });
|
|
231
|
+
setImportWalletName("");
|
|
232
|
+
setImportPrivateKey("");
|
|
233
|
+
handleClose();
|
|
234
|
+
} catch (e: unknown) {
|
|
235
|
+
const message =
|
|
236
|
+
e instanceof Error ? e.message : "Failed to import wallet";
|
|
237
|
+
setError(message);
|
|
238
|
+
onNotify?.({ type: "error", message });
|
|
239
|
+
} finally {
|
|
240
|
+
setIsLoading(false);
|
|
241
|
+
}
|
|
242
|
+
}, [
|
|
243
|
+
assertTelegramReady,
|
|
244
|
+
completeWalletAdded,
|
|
245
|
+
handleClose,
|
|
246
|
+
importWalletName,
|
|
247
|
+
isValidImportPrivateKey,
|
|
248
|
+
isValidImportWalletName,
|
|
249
|
+
onNotify,
|
|
250
|
+
telegramClient,
|
|
251
|
+
telegramRpcOptions,
|
|
252
|
+
trimmedImportPrivateKey,
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
const handleSignAndAdd = useCallback(async () => {
|
|
256
|
+
if (!ownerAddress) {
|
|
257
|
+
onConnectWallet?.();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (!signTypedDataAsync || !chainId) {
|
|
261
|
+
setError("Wallet signing is not ready yet");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (!isValidWalletName) {
|
|
265
|
+
setError("Wallet name may only contain letters, numbers, and /");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
setError(null);
|
|
269
|
+
setIsLoading(true);
|
|
270
|
+
try {
|
|
271
|
+
assertTelegramReady();
|
|
272
|
+
const res = await telegramClient.hyperliquidAgentSignatureCreate(
|
|
273
|
+
{ authData: {}, address: ownerAddress },
|
|
274
|
+
telegramRpcOptions,
|
|
275
|
+
);
|
|
276
|
+
const agentAddress = res.response.agent;
|
|
277
|
+
const request = createAuthSignatureRequest(
|
|
278
|
+
chainId,
|
|
279
|
+
hyperliquidChain,
|
|
280
|
+
agentAddress,
|
|
281
|
+
agentApprovalDurationMs,
|
|
282
|
+
);
|
|
283
|
+
const signature = await signTypedDataAsync(request);
|
|
284
|
+
|
|
285
|
+
await telegramClient.hyperliquidAgentWalletCreate(
|
|
286
|
+
{
|
|
287
|
+
authData: {},
|
|
288
|
+
name: walletName,
|
|
289
|
+
signature: {
|
|
290
|
+
agentAddress: request.message.agentAddress,
|
|
291
|
+
agentName: request.message.agentName,
|
|
292
|
+
nonce: request.message.nonce,
|
|
293
|
+
chainId,
|
|
294
|
+
signature,
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
telegramRpcOptions,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
await completeWalletAdded();
|
|
301
|
+
onNotify?.({ type: "success", message: "Wallet added successfully" });
|
|
302
|
+
onDisconnectWallet?.();
|
|
303
|
+
setWalletName("");
|
|
304
|
+
handleClose();
|
|
305
|
+
} catch (e: unknown) {
|
|
306
|
+
const message = e instanceof Error ? e.message : "Failed to add wallet";
|
|
307
|
+
setError(message);
|
|
308
|
+
onNotify?.({ type: "error", message });
|
|
309
|
+
} finally {
|
|
310
|
+
setIsLoading(false);
|
|
311
|
+
}
|
|
312
|
+
}, [
|
|
313
|
+
assertTelegramReady,
|
|
314
|
+
agentApprovalDurationMs,
|
|
315
|
+
chainId,
|
|
316
|
+
completeWalletAdded,
|
|
317
|
+
handleClose,
|
|
318
|
+
hyperliquidChain,
|
|
319
|
+
isValidWalletName,
|
|
320
|
+
onConnectWallet,
|
|
321
|
+
onDisconnectWallet,
|
|
322
|
+
onNotify,
|
|
323
|
+
ownerAddress,
|
|
324
|
+
signTypedDataAsync,
|
|
325
|
+
telegramClient,
|
|
326
|
+
telegramRpcOptions,
|
|
327
|
+
walletName,
|
|
328
|
+
]);
|
|
329
|
+
|
|
330
|
+
const handleAddReadOnly = useCallback(async () => {
|
|
331
|
+
const trimmed = readOnlyAddress.trim();
|
|
332
|
+
if (!trimmed) {
|
|
333
|
+
setError("Please enter a wallet address");
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(trimmed)) {
|
|
337
|
+
setError("Invalid Ethereum address format");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
setError(null);
|
|
341
|
+
setIsLoading(true);
|
|
342
|
+
try {
|
|
343
|
+
await onAddReadOnlyWallet?.(trimmed);
|
|
344
|
+
onNotify?.({ type: "success", message: "Read-only wallet added" });
|
|
345
|
+
setReadOnlyAddress("");
|
|
346
|
+
handleClose();
|
|
347
|
+
} catch (e: unknown) {
|
|
348
|
+
const message = e instanceof Error ? e.message : "Failed to add wallet";
|
|
349
|
+
setError(message);
|
|
350
|
+
onNotify?.({ type: "error", message });
|
|
351
|
+
} finally {
|
|
352
|
+
setIsLoading(false);
|
|
353
|
+
}
|
|
354
|
+
}, [handleClose, onAddReadOnlyWallet, onNotify, readOnlyAddress]);
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
<AnimatePresence>
|
|
358
|
+
{isOpen && (
|
|
359
|
+
<motion.div className="hypurr-connect" style={{ display: "contents" }}>
|
|
360
|
+
<SpinKeyframes />
|
|
361
|
+
<motion.div
|
|
362
|
+
className="fixed inset-0 z-[100] bg-black/70 backdrop-blur-sm"
|
|
363
|
+
initial={{ opacity: 0 }}
|
|
364
|
+
animate={{ opacity: 1 }}
|
|
365
|
+
exit={{ opacity: 0 }}
|
|
366
|
+
transition={{ duration: 0.15 }}
|
|
367
|
+
onClick={handleClose}
|
|
368
|
+
/>
|
|
369
|
+
<div className="fixed inset-0 z-[101] flex items-center justify-center p-4">
|
|
370
|
+
<motion.div
|
|
371
|
+
className="relative w-full max-w-md overflow-hidden rounded-lg border border-surface-bd bg-surface-modal font-sans shadow-modal"
|
|
372
|
+
initial={{ opacity: 0, y: 8 }}
|
|
373
|
+
animate={{ opacity: 1, y: 0 }}
|
|
374
|
+
exit={{ opacity: 0, y: 8 }}
|
|
375
|
+
transition={{ duration: 0.18, ease: "easeOut" }}
|
|
376
|
+
onClick={(event) => event.stopPropagation()}
|
|
377
|
+
>
|
|
378
|
+
<div className="relative flex items-center justify-center border-b border-white/[0.06] px-6 pb-5 pt-6">
|
|
379
|
+
<h3 className="text-lg font-semibold text-white">Add Wallet</h3>
|
|
380
|
+
<button
|
|
381
|
+
onClick={handleClose}
|
|
382
|
+
disabled={isProcessing}
|
|
383
|
+
className="absolute right-6 text-gray-400 transition-colors hover:text-white disabled:cursor-not-allowed disabled:opacity-40"
|
|
384
|
+
aria-label="Close"
|
|
385
|
+
>
|
|
386
|
+
<X size={16} />
|
|
387
|
+
</button>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<div className="flex gap-1.5 px-6 pt-5">
|
|
391
|
+
{tabKeys.map((tab) => (
|
|
392
|
+
<button
|
|
393
|
+
key={tab}
|
|
394
|
+
type="button"
|
|
395
|
+
onClick={() => {
|
|
396
|
+
setError(null);
|
|
397
|
+
setActiveTab(tab);
|
|
398
|
+
}}
|
|
399
|
+
className={tabClass(activeTab === tab)}
|
|
400
|
+
>
|
|
401
|
+
{tab === "generate" ? (
|
|
402
|
+
<>
|
|
403
|
+
<Plus size={13} /> New
|
|
404
|
+
</>
|
|
405
|
+
) : tab === "import" ? (
|
|
406
|
+
<>
|
|
407
|
+
<KeyRound size={13} /> Import
|
|
408
|
+
</>
|
|
409
|
+
) : tab === "api" ? (
|
|
410
|
+
<>
|
|
411
|
+
<Wallet size={13} /> Link
|
|
412
|
+
</>
|
|
413
|
+
) : (
|
|
414
|
+
<>
|
|
415
|
+
<Eye size={13} /> Watch
|
|
416
|
+
</>
|
|
417
|
+
)}
|
|
418
|
+
</button>
|
|
419
|
+
))}
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<div className="px-6 pb-6 pt-5">
|
|
423
|
+
{activeTab === "generate" && (
|
|
424
|
+
<section className="space-y-4">
|
|
425
|
+
<p className="text-base text-gray-400">
|
|
426
|
+
Create a new Hyperliquid wallet.
|
|
427
|
+
</p>
|
|
428
|
+
<WalletNameInput
|
|
429
|
+
value={newWalletName}
|
|
430
|
+
setValue={setNewWalletName}
|
|
431
|
+
isValid={isValidNewWalletName}
|
|
432
|
+
clearError={() => setError(null)}
|
|
433
|
+
placeholder="MyNewWallet"
|
|
434
|
+
/>
|
|
435
|
+
<button
|
|
436
|
+
onClick={handleCreateWallet}
|
|
437
|
+
disabled={isProcessing || !isValidNewWalletName}
|
|
438
|
+
className={`flex w-full items-center justify-center gap-2 rounded-lg py-2 text-base font-medium ${
|
|
439
|
+
!isProcessing && isValidNewWalletName
|
|
440
|
+
? "btn-raised"
|
|
441
|
+
: "btn-raised-disabled"
|
|
442
|
+
}`}
|
|
443
|
+
>
|
|
444
|
+
{isProcessing ? (
|
|
445
|
+
<>
|
|
446
|
+
<Loader2 size={14} /> Creating...
|
|
447
|
+
</>
|
|
448
|
+
) : (
|
|
449
|
+
<>
|
|
450
|
+
<Plus size={14} /> Create Wallet
|
|
451
|
+
</>
|
|
452
|
+
)}
|
|
453
|
+
</button>
|
|
454
|
+
{error && <ErrorBox message={error} />}
|
|
455
|
+
</section>
|
|
456
|
+
)}
|
|
457
|
+
|
|
458
|
+
{activeTab === "import" && (
|
|
459
|
+
<section className="space-y-4">
|
|
460
|
+
<p className="text-base text-gray-400">
|
|
461
|
+
Import a Hyperliquid wallet with its private key.
|
|
462
|
+
</p>
|
|
463
|
+
<WalletNameInput
|
|
464
|
+
value={importWalletName}
|
|
465
|
+
setValue={setImportWalletName}
|
|
466
|
+
isValid={isValidImportWalletName}
|
|
467
|
+
clearError={() => setError(null)}
|
|
468
|
+
placeholder="MyImportedWallet"
|
|
469
|
+
/>
|
|
470
|
+
<div>
|
|
471
|
+
<label className="mb-2 block text-base uppercase tracking-[0.1em] text-gray-400">
|
|
472
|
+
Private Key
|
|
473
|
+
</label>
|
|
474
|
+
<div className="relative">
|
|
475
|
+
<input
|
|
476
|
+
type={showImportPrivateKey ? "text" : "password"}
|
|
477
|
+
value={importPrivateKey}
|
|
478
|
+
onChange={(event) => {
|
|
479
|
+
setImportPrivateKey(event.target.value);
|
|
480
|
+
setError(null);
|
|
481
|
+
}}
|
|
482
|
+
placeholder="0x..."
|
|
483
|
+
spellCheck={false}
|
|
484
|
+
autoComplete="off"
|
|
485
|
+
className={`${inputClass(
|
|
486
|
+
!!importPrivateKey && !isValidImportPrivateKey,
|
|
487
|
+
)} pr-11`}
|
|
488
|
+
/>
|
|
489
|
+
<button
|
|
490
|
+
type="button"
|
|
491
|
+
onClick={() =>
|
|
492
|
+
setShowImportPrivateKey((visible) => !visible)
|
|
493
|
+
}
|
|
494
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-gray-500 transition-colors hover:text-gray-300"
|
|
495
|
+
aria-label={
|
|
496
|
+
showImportPrivateKey
|
|
497
|
+
? "Hide private key"
|
|
498
|
+
: "Show private key"
|
|
499
|
+
}
|
|
500
|
+
>
|
|
501
|
+
{showImportPrivateKey ? (
|
|
502
|
+
<EyeOff size={15} />
|
|
503
|
+
) : (
|
|
504
|
+
<Eye size={15} />
|
|
505
|
+
)}
|
|
506
|
+
</button>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
<button
|
|
510
|
+
onClick={handleImportWallet}
|
|
511
|
+
disabled={
|
|
512
|
+
isProcessing ||
|
|
513
|
+
!isValidImportWalletName ||
|
|
514
|
+
!isValidImportPrivateKey
|
|
515
|
+
}
|
|
516
|
+
className={`flex w-full items-center justify-center gap-2 rounded-lg border bg-transparent py-2 text-base font-medium transition-colors ${
|
|
517
|
+
!isProcessing &&
|
|
518
|
+
isValidImportWalletName &&
|
|
519
|
+
isValidImportPrivateKey
|
|
520
|
+
? "border-purple-500 text-purple-400 hover:bg-purple-500/10"
|
|
521
|
+
: "cursor-not-allowed border-gray-700 text-gray-600"
|
|
522
|
+
}`}
|
|
523
|
+
>
|
|
524
|
+
{isProcessing ? (
|
|
525
|
+
<>
|
|
526
|
+
<Loader2 size={14} /> Importing...
|
|
527
|
+
</>
|
|
528
|
+
) : (
|
|
529
|
+
<>
|
|
530
|
+
<KeyRound size={14} /> Import Wallet
|
|
531
|
+
</>
|
|
532
|
+
)}
|
|
533
|
+
</button>
|
|
534
|
+
{error && <ErrorBox message={error} />}
|
|
535
|
+
</section>
|
|
536
|
+
)}
|
|
537
|
+
|
|
538
|
+
{activeTab === "api" && (
|
|
539
|
+
<section className="space-y-4">
|
|
540
|
+
<p className="text-base text-gray-400">
|
|
541
|
+
Connect your wallet and sign to link it for trading.
|
|
542
|
+
</p>
|
|
543
|
+
{!isWalletConnected || !ownerAddress ? (
|
|
544
|
+
<button
|
|
545
|
+
onClick={onConnectWallet}
|
|
546
|
+
className="btn-raised flex w-full items-center justify-center gap-2 rounded-lg py-2 text-base font-medium"
|
|
547
|
+
>
|
|
548
|
+
<Wallet size={14} /> Connect Wallet
|
|
549
|
+
</button>
|
|
550
|
+
) : (
|
|
551
|
+
<>
|
|
552
|
+
<div className="flex items-center gap-3 rounded-lg border border-white/[0.06] bg-white/[0.03] px-3 py-2.5">
|
|
553
|
+
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-white/[0.08] bg-white/[0.06]">
|
|
554
|
+
<Wallet size={14} className="text-gray-400" />
|
|
555
|
+
</div>
|
|
556
|
+
<div className="min-w-0 flex-1">
|
|
557
|
+
<p className="text-base text-gray-400">Connected</p>
|
|
558
|
+
<p className="font-mono text-base text-white">
|
|
559
|
+
{formatAddress(ownerAddress)}
|
|
560
|
+
</p>
|
|
561
|
+
</div>
|
|
562
|
+
<Check
|
|
563
|
+
size={16}
|
|
564
|
+
className="flex-shrink-0 text-trade-up"
|
|
565
|
+
/>
|
|
566
|
+
</div>
|
|
567
|
+
<WalletNameInput
|
|
568
|
+
value={walletName}
|
|
569
|
+
setValue={setWalletName}
|
|
570
|
+
isValid={isValidWalletName}
|
|
571
|
+
clearError={() => setError(null)}
|
|
572
|
+
placeholder="MyWallet"
|
|
573
|
+
/>
|
|
574
|
+
<ApprovalDurationPicker
|
|
575
|
+
value={agentApprovalDurationMs}
|
|
576
|
+
onChange={setAgentApprovalDurationMs}
|
|
577
|
+
options={durationOptions}
|
|
578
|
+
disabled={isProcessing}
|
|
579
|
+
/>
|
|
580
|
+
<button
|
|
581
|
+
onClick={handleSignAndAdd}
|
|
582
|
+
disabled={isProcessing || !isValidWalletName}
|
|
583
|
+
className={`flex w-full items-center justify-center gap-2 rounded-lg py-2 text-base font-medium ${
|
|
584
|
+
!isProcessing && isValidWalletName
|
|
585
|
+
? "btn-raised"
|
|
586
|
+
: "btn-raised-disabled"
|
|
587
|
+
}`}
|
|
588
|
+
>
|
|
589
|
+
{isProcessing ? (
|
|
590
|
+
<>
|
|
591
|
+
<Loader2 size={14} /> Signing...
|
|
592
|
+
</>
|
|
593
|
+
) : (
|
|
594
|
+
"Sign & Add Wallet"
|
|
595
|
+
)}
|
|
596
|
+
</button>
|
|
597
|
+
{onDisconnectWallet && (
|
|
598
|
+
<button
|
|
599
|
+
onClick={onDisconnectWallet}
|
|
600
|
+
className="w-full py-1.5 text-base text-gray-400 transition-colors hover:text-gray-300"
|
|
601
|
+
>
|
|
602
|
+
Disconnect & use different wallet
|
|
603
|
+
</button>
|
|
604
|
+
)}
|
|
605
|
+
</>
|
|
606
|
+
)}
|
|
607
|
+
{error && <ErrorBox message={error} />}
|
|
608
|
+
</section>
|
|
609
|
+
)}
|
|
610
|
+
|
|
611
|
+
{activeTab === "readonly" && onAddReadOnlyWallet && (
|
|
612
|
+
<section className="space-y-4">
|
|
613
|
+
<p className="text-base text-gray-400">
|
|
614
|
+
View positions and balances without trading access.
|
|
615
|
+
</p>
|
|
616
|
+
<div>
|
|
617
|
+
<label className="mb-2 block text-base uppercase tracking-[0.1em] text-gray-400">
|
|
618
|
+
Wallet Address
|
|
619
|
+
</label>
|
|
620
|
+
<input
|
|
621
|
+
type="text"
|
|
622
|
+
value={readOnlyAddress}
|
|
623
|
+
onChange={(event) =>
|
|
624
|
+
setReadOnlyAddress(event.target.value)
|
|
625
|
+
}
|
|
626
|
+
placeholder="0x..."
|
|
627
|
+
className={inputClass()}
|
|
628
|
+
/>
|
|
629
|
+
</div>
|
|
630
|
+
<button
|
|
631
|
+
onClick={handleAddReadOnly}
|
|
632
|
+
disabled={isProcessing || !readOnlyAddress.trim()}
|
|
633
|
+
className={`flex w-full items-center justify-center gap-2 rounded-lg py-2 text-base font-medium ${
|
|
634
|
+
!isProcessing && readOnlyAddress.trim()
|
|
635
|
+
? "btn-raised"
|
|
636
|
+
: "btn-raised-disabled"
|
|
637
|
+
}`}
|
|
638
|
+
>
|
|
639
|
+
{isProcessing ? (
|
|
640
|
+
<>
|
|
641
|
+
<Loader2 size={14} /> Adding...
|
|
642
|
+
</>
|
|
643
|
+
) : (
|
|
644
|
+
<>
|
|
645
|
+
<Eye size={14} /> Add Watch Wallet
|
|
646
|
+
</>
|
|
647
|
+
)}
|
|
648
|
+
</button>
|
|
649
|
+
{error && <ErrorBox message={error} />}
|
|
650
|
+
</section>
|
|
651
|
+
)}
|
|
652
|
+
</div>
|
|
653
|
+
</motion.div>
|
|
654
|
+
</div>
|
|
655
|
+
</motion.div>
|
|
656
|
+
)}
|
|
657
|
+
</AnimatePresence>
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function WalletNameInput({
|
|
662
|
+
value,
|
|
663
|
+
setValue,
|
|
664
|
+
isValid,
|
|
665
|
+
clearError,
|
|
666
|
+
placeholder,
|
|
667
|
+
}: {
|
|
668
|
+
value: string;
|
|
669
|
+
setValue: (value: string) => void;
|
|
670
|
+
isValid: boolean;
|
|
671
|
+
clearError: () => void;
|
|
672
|
+
placeholder: string;
|
|
673
|
+
}) {
|
|
674
|
+
return (
|
|
675
|
+
<div>
|
|
676
|
+
<label className="mb-2 block text-base uppercase tracking-[0.1em] text-gray-400">
|
|
677
|
+
Wallet Name
|
|
678
|
+
</label>
|
|
679
|
+
<input
|
|
680
|
+
type="text"
|
|
681
|
+
value={value}
|
|
682
|
+
onChange={(event) => {
|
|
683
|
+
const nextValue = event.target.value;
|
|
684
|
+
if (WALLET_NAME_REGEX.test(nextValue) || nextValue === "") {
|
|
685
|
+
setValue(nextValue);
|
|
686
|
+
clearError();
|
|
687
|
+
}
|
|
688
|
+
}}
|
|
689
|
+
placeholder={placeholder}
|
|
690
|
+
className={inputClass(!!value && !isValid)}
|
|
691
|
+
/>
|
|
692
|
+
<p className="mt-1.5 text-base text-gray-600">
|
|
693
|
+
Letters, numbers, and / only
|
|
694
|
+
</p>
|
|
695
|
+
</div>
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function ApprovalDurationPicker({
|
|
700
|
+
value,
|
|
701
|
+
onChange,
|
|
702
|
+
options,
|
|
703
|
+
disabled,
|
|
704
|
+
}: {
|
|
705
|
+
value: number;
|
|
706
|
+
onChange: (value: number) => void;
|
|
707
|
+
options: AgentApprovalDurationOption[];
|
|
708
|
+
disabled?: boolean;
|
|
709
|
+
}) {
|
|
710
|
+
return (
|
|
711
|
+
<div>
|
|
712
|
+
<label className="mb-2 block text-base uppercase tracking-[0.1em] text-gray-400">
|
|
713
|
+
Approval Duration
|
|
714
|
+
</label>
|
|
715
|
+
<div className="grid grid-cols-4 gap-1.5">
|
|
716
|
+
{options.map((option) => (
|
|
717
|
+
<button
|
|
718
|
+
key={`${option.label}-${option.durationMs}`}
|
|
719
|
+
type="button"
|
|
720
|
+
onClick={() => onChange(option.durationMs)}
|
|
721
|
+
disabled={disabled}
|
|
722
|
+
className={`rounded py-1.5 text-base font-medium disabled:cursor-not-allowed disabled:opacity-50 ${
|
|
723
|
+
value === option.durationMs ? "btn-raised-active" : "btn-raised"
|
|
724
|
+
}`}
|
|
725
|
+
>
|
|
726
|
+
{option.label}
|
|
727
|
+
</button>
|
|
728
|
+
))}
|
|
729
|
+
</div>
|
|
730
|
+
</div>
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function ErrorBox({ message }: { message: string }) {
|
|
735
|
+
return (
|
|
736
|
+
<div className="flex items-start gap-2 rounded-lg border border-trade-down/20 bg-trade-down/[0.08] p-3">
|
|
737
|
+
<AlertTriangle
|
|
738
|
+
size={14}
|
|
739
|
+
className="mt-0.5 flex-shrink-0 text-trade-down"
|
|
740
|
+
/>
|
|
741
|
+
<p className="text-base text-trade-down">{message}</p>
|
|
742
|
+
</div>
|
|
743
|
+
);
|
|
744
|
+
}
|