@hfunlabs/hypurr-connect 0.1.14 → 0.1.16
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 +86 -3
- package/dist/index.js +1827 -333
- package/dist/index.js.map +1 -1
- package/package.json +21 -6
- package/src/AddWalletModal.tsx +744 -0
- package/src/AgentExpiryWarning.tsx +129 -0
- package/src/DeleteWalletModal.tsx +2 -1
- package/src/HypurrConnectProvider.tsx +190 -0
- package/src/LoginModal.tsx +11 -11
- package/src/RenameWalletModal.tsx +2 -1
- package/src/RenewAgentModal.tsx +380 -0
- package/src/UserProfileModal.tsx +151 -23
- package/src/WalletSelectorDropdown.tsx +138 -4
- 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,380 @@
|
|
|
1
|
+
import { AnimatePresence, motion } from "framer-motion";
|
|
2
|
+
import type { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
|
|
3
|
+
import { useCallback, useMemo, useState, type ReactNode } from "react";
|
|
4
|
+
import { useHypurrConnectInternal } from "./HypurrConnectProvider";
|
|
5
|
+
import {
|
|
6
|
+
AGENT_APPROVAL_DURATION_OPTIONS,
|
|
7
|
+
DEFAULT_AGENT_APPROVAL_DURATION_MS,
|
|
8
|
+
agentExpiresAtMs,
|
|
9
|
+
formatAgentExpiry,
|
|
10
|
+
getAgentExpiryTitle,
|
|
11
|
+
type AgentApprovalDurationOption,
|
|
12
|
+
} from "./agentWallet";
|
|
13
|
+
import {
|
|
14
|
+
AlertTriangle,
|
|
15
|
+
Check,
|
|
16
|
+
Loader2,
|
|
17
|
+
SpinKeyframes,
|
|
18
|
+
Wallet,
|
|
19
|
+
X,
|
|
20
|
+
} from "./icons/lucide";
|
|
21
|
+
import type { Hex, SignTypedDataFn } from "./types";
|
|
22
|
+
|
|
23
|
+
export interface RenewAgentModalProps {
|
|
24
|
+
isOpen: boolean;
|
|
25
|
+
onClose: () => void;
|
|
26
|
+
wallet: HyperliquidWallet | null;
|
|
27
|
+
ownerAddress?: string | null;
|
|
28
|
+
isWalletConnected?: boolean;
|
|
29
|
+
chainId?: number;
|
|
30
|
+
signTypedDataAsync?: SignTypedDataFn;
|
|
31
|
+
onConnectWallet?: () => void;
|
|
32
|
+
onDisconnectWallet?: () => void;
|
|
33
|
+
onRenewed?: (wallet: HyperliquidWallet) => void | Promise<void>;
|
|
34
|
+
onNotify?: (n: { type: "success" | "error"; message: string }) => void;
|
|
35
|
+
/** Expiration choices for the renewed owner approval. Defaults to 1/7/30/90 days. */
|
|
36
|
+
approvalDurationOptions?: AgentApprovalDurationOption[];
|
|
37
|
+
/** Initial renewal approval duration. Defaults to 1 day. */
|
|
38
|
+
defaultApprovalDurationMs?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isHexAddress = (address?: string | null): address is Hex =>
|
|
42
|
+
!!address && /^0x[a-fA-F0-9]{40}$/.test(address);
|
|
43
|
+
|
|
44
|
+
const formatAddress = (address?: string | null) =>
|
|
45
|
+
address ? `${address.slice(0, 6)}...${address.slice(-4)}` : "";
|
|
46
|
+
|
|
47
|
+
const isSameAddress = (a?: string | null, b?: string | null) =>
|
|
48
|
+
isHexAddress(a) && isHexAddress(b) && a.toLowerCase() === b.toLowerCase();
|
|
49
|
+
|
|
50
|
+
export function RenewAgentModal({
|
|
51
|
+
isOpen,
|
|
52
|
+
onClose,
|
|
53
|
+
wallet,
|
|
54
|
+
ownerAddress,
|
|
55
|
+
isWalletConnected,
|
|
56
|
+
chainId,
|
|
57
|
+
signTypedDataAsync,
|
|
58
|
+
onConnectWallet,
|
|
59
|
+
onDisconnectWallet,
|
|
60
|
+
onRenewed,
|
|
61
|
+
onNotify,
|
|
62
|
+
approvalDurationOptions,
|
|
63
|
+
defaultApprovalDurationMs = DEFAULT_AGENT_APPROVAL_DURATION_MS,
|
|
64
|
+
}: RenewAgentModalProps): ReactNode {
|
|
65
|
+
const { renewAgentWallet } = useHypurrConnectInternal();
|
|
66
|
+
const [isRenewing, setIsRenewing] = useState(false);
|
|
67
|
+
const [approvalDurationMs, setApprovalDurationMs] = useState(
|
|
68
|
+
defaultApprovalDurationMs,
|
|
69
|
+
);
|
|
70
|
+
const [error, setError] = useState<string | null>(null);
|
|
71
|
+
|
|
72
|
+
const expiryMessage = useMemo(
|
|
73
|
+
() => (wallet ? getAgentExpiryTitle(wallet) : null),
|
|
74
|
+
[wallet],
|
|
75
|
+
);
|
|
76
|
+
const currentExpiryMs = useMemo(
|
|
77
|
+
() => (wallet ? agentExpiresAtMs(wallet) : null),
|
|
78
|
+
[wallet],
|
|
79
|
+
);
|
|
80
|
+
const durationOptions =
|
|
81
|
+
approvalDurationOptions && approvalDurationOptions.length > 0
|
|
82
|
+
? approvalDurationOptions
|
|
83
|
+
: AGENT_APPROVAL_DURATION_OPTIONS;
|
|
84
|
+
const expectedOwnerAddress = wallet?.ethereumAddress ?? null;
|
|
85
|
+
const agentAddress = wallet?.agentEthereumAddress?.value ?? null;
|
|
86
|
+
const hasExpectedOwner = isHexAddress(expectedOwnerAddress);
|
|
87
|
+
const hasAgentAddress = isHexAddress(agentAddress);
|
|
88
|
+
const hasConnectedOwner = isWalletConnected && isHexAddress(ownerAddress);
|
|
89
|
+
const ownerMatches = isSameAddress(ownerAddress, expectedOwnerAddress);
|
|
90
|
+
const ownerReady = ownerMatches && !!chainId;
|
|
91
|
+
const canRenew =
|
|
92
|
+
!!wallet &&
|
|
93
|
+
hasAgentAddress &&
|
|
94
|
+
ownerReady &&
|
|
95
|
+
!!signTypedDataAsync &&
|
|
96
|
+
!isRenewing;
|
|
97
|
+
|
|
98
|
+
const handleClose = useCallback(() => {
|
|
99
|
+
if (isRenewing) return;
|
|
100
|
+
setError(null);
|
|
101
|
+
onClose();
|
|
102
|
+
}, [isRenewing, onClose]);
|
|
103
|
+
|
|
104
|
+
const handleRenew = useCallback(async () => {
|
|
105
|
+
if (!wallet) return;
|
|
106
|
+
if (!isHexAddress(agentAddress)) {
|
|
107
|
+
setError("This wallet is missing its agent address.");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!isHexAddress(expectedOwnerAddress)) {
|
|
111
|
+
setError("This wallet is missing its owner address.");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (!isHexAddress(ownerAddress)) {
|
|
115
|
+
onConnectWallet?.();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (!isSameAddress(ownerAddress, expectedOwnerAddress)) {
|
|
119
|
+
setError(
|
|
120
|
+
`Connect the owner wallet ${formatAddress(expectedOwnerAddress)} to renew this agent.`,
|
|
121
|
+
);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!chainId || !signTypedDataAsync) {
|
|
125
|
+
setError("Wallet signing is not ready yet");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
setError(null);
|
|
130
|
+
setIsRenewing(true);
|
|
131
|
+
try {
|
|
132
|
+
await renewAgentWallet({
|
|
133
|
+
walletId: wallet.id,
|
|
134
|
+
ownerAddress,
|
|
135
|
+
signTypedDataAsync,
|
|
136
|
+
chainId,
|
|
137
|
+
approvalDurationMs,
|
|
138
|
+
});
|
|
139
|
+
await onRenewed?.(wallet);
|
|
140
|
+
onNotify?.({ type: "success", message: "Agent wallet renewed" });
|
|
141
|
+
onClose();
|
|
142
|
+
} catch (e: unknown) {
|
|
143
|
+
const message =
|
|
144
|
+
e instanceof Error ? e.message : "Failed to renew agent wallet";
|
|
145
|
+
setError(message);
|
|
146
|
+
onNotify?.({ type: "error", message });
|
|
147
|
+
} finally {
|
|
148
|
+
setIsRenewing(false);
|
|
149
|
+
}
|
|
150
|
+
}, [
|
|
151
|
+
agentAddress,
|
|
152
|
+
approvalDurationMs,
|
|
153
|
+
chainId,
|
|
154
|
+
expectedOwnerAddress,
|
|
155
|
+
onClose,
|
|
156
|
+
onConnectWallet,
|
|
157
|
+
onNotify,
|
|
158
|
+
onRenewed,
|
|
159
|
+
ownerAddress,
|
|
160
|
+
renewAgentWallet,
|
|
161
|
+
signTypedDataAsync,
|
|
162
|
+
wallet,
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<AnimatePresence>
|
|
167
|
+
{isOpen && wallet && (
|
|
168
|
+
<motion.div className="hypurr-connect" style={{ display: "contents" }}>
|
|
169
|
+
<SpinKeyframes />
|
|
170
|
+
<motion.div
|
|
171
|
+
className="fixed inset-0 z-[110] bg-black/70 backdrop-blur-sm"
|
|
172
|
+
initial={{ opacity: 0 }}
|
|
173
|
+
animate={{ opacity: 1 }}
|
|
174
|
+
exit={{ opacity: 0 }}
|
|
175
|
+
transition={{ duration: 0.15 }}
|
|
176
|
+
onClick={handleClose}
|
|
177
|
+
/>
|
|
178
|
+
<div className="fixed inset-0 z-[111] flex items-center justify-center p-4">
|
|
179
|
+
<motion.div
|
|
180
|
+
className="relative w-full max-w-md overflow-hidden rounded-lg border border-surface-bd bg-surface-modal font-sans shadow-modal"
|
|
181
|
+
initial={{ opacity: 0, y: 8 }}
|
|
182
|
+
animate={{ opacity: 1, y: 0 }}
|
|
183
|
+
exit={{ opacity: 0, y: 8 }}
|
|
184
|
+
transition={{ duration: 0.18, ease: "easeOut" }}
|
|
185
|
+
onClick={(event) => event.stopPropagation()}
|
|
186
|
+
>
|
|
187
|
+
<div className="relative flex items-center justify-center border-b border-white/[0.06] px-6 pb-5 pt-6">
|
|
188
|
+
<h3 className="text-lg font-semibold text-white">
|
|
189
|
+
Renew Agent Wallet
|
|
190
|
+
</h3>
|
|
191
|
+
<button
|
|
192
|
+
onClick={handleClose}
|
|
193
|
+
disabled={isRenewing}
|
|
194
|
+
className="absolute right-6 text-gray-400 transition-colors hover:text-white disabled:cursor-not-allowed disabled:opacity-40"
|
|
195
|
+
aria-label="Close"
|
|
196
|
+
>
|
|
197
|
+
<X size={16} />
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<div className="space-y-4 px-6 py-5">
|
|
202
|
+
<div className="flex items-start gap-2 rounded-lg border border-amber-500/25 bg-amber-500/[0.08] p-3">
|
|
203
|
+
<AlertTriangle
|
|
204
|
+
size={14}
|
|
205
|
+
className="mt-0.5 flex-shrink-0 text-amber-400"
|
|
206
|
+
/>
|
|
207
|
+
<p className="text-base text-amber-200">
|
|
208
|
+
{expiryMessage ??
|
|
209
|
+
(currentExpiryMs
|
|
210
|
+
? `Current approval expires ${formatAgentExpiry(
|
|
211
|
+
currentExpiryMs,
|
|
212
|
+
)}. Renew to extend this agent wallet.`
|
|
213
|
+
: "Renew this agent wallet with a fresh owner approval.")}
|
|
214
|
+
</p>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div className="rounded-lg border border-white/[0.06] bg-white/[0.03] p-3">
|
|
218
|
+
<p className="mb-1.5 text-base uppercase tracking-[0.1em] text-gray-400">
|
|
219
|
+
Agent address
|
|
220
|
+
</p>
|
|
221
|
+
<div className="flex items-center justify-between gap-3">
|
|
222
|
+
<div className="min-w-0">
|
|
223
|
+
<p className="truncate text-base font-medium text-white">
|
|
224
|
+
{wallet.name || "Unnamed Wallet"}
|
|
225
|
+
</p>
|
|
226
|
+
<p className="font-mono text-base text-gray-400">
|
|
227
|
+
{hasAgentAddress
|
|
228
|
+
? formatAddress(agentAddress)
|
|
229
|
+
: "Missing agent address"}
|
|
230
|
+
</p>
|
|
231
|
+
</div>
|
|
232
|
+
<AlertTriangle
|
|
233
|
+
size={16}
|
|
234
|
+
className="flex-shrink-0 text-amber-400"
|
|
235
|
+
/>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
{ownerReady ? (
|
|
240
|
+
<div className="flex items-center gap-3 rounded-lg border border-white/[0.06] bg-white/[0.03] px-3 py-2.5">
|
|
241
|
+
<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]">
|
|
242
|
+
<Wallet size={14} className="text-gray-400" />
|
|
243
|
+
</div>
|
|
244
|
+
<div className="min-w-0 flex-1">
|
|
245
|
+
<p className="text-base text-gray-400">
|
|
246
|
+
Owner wallet connected
|
|
247
|
+
</p>
|
|
248
|
+
<p className="font-mono text-base text-white">
|
|
249
|
+
{formatAddress(ownerAddress)}
|
|
250
|
+
</p>
|
|
251
|
+
</div>
|
|
252
|
+
<Check size={16} className="flex-shrink-0 text-trade-up" />
|
|
253
|
+
</div>
|
|
254
|
+
) : hasConnectedOwner && !ownerMatches ? (
|
|
255
|
+
<div className="space-y-3 rounded-lg border border-trade-down/20 bg-trade-down/[0.08] p-3">
|
|
256
|
+
<div className="flex items-start gap-2">
|
|
257
|
+
<AlertTriangle
|
|
258
|
+
size={14}
|
|
259
|
+
className="mt-0.5 flex-shrink-0 text-trade-down"
|
|
260
|
+
/>
|
|
261
|
+
<div className="min-w-0">
|
|
262
|
+
<p className="text-base text-trade-down">
|
|
263
|
+
Wrong owner wallet connected
|
|
264
|
+
</p>
|
|
265
|
+
<p className="mt-1 font-mono text-base text-gray-400">
|
|
266
|
+
Connected {formatAddress(ownerAddress)}
|
|
267
|
+
</p>
|
|
268
|
+
<p className="font-mono text-base text-gray-400">
|
|
269
|
+
Required {formatAddress(expectedOwnerAddress)}
|
|
270
|
+
</p>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
{onDisconnectWallet && (
|
|
274
|
+
<button
|
|
275
|
+
onClick={onDisconnectWallet}
|
|
276
|
+
disabled={isRenewing}
|
|
277
|
+
className="btn-raised flex w-full items-center justify-center gap-2 rounded-lg py-2 text-base font-medium disabled:cursor-not-allowed disabled:opacity-40"
|
|
278
|
+
>
|
|
279
|
+
Disconnect Wallet
|
|
280
|
+
</button>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
) : (
|
|
284
|
+
<button
|
|
285
|
+
onClick={onConnectWallet}
|
|
286
|
+
className="btn-raised flex w-full items-center justify-center gap-2 rounded-lg py-2 text-base font-medium"
|
|
287
|
+
>
|
|
288
|
+
<Wallet size={14} /> Connect Owner Wallet
|
|
289
|
+
{hasExpectedOwner
|
|
290
|
+
? ` ${formatAddress(expectedOwnerAddress)}`
|
|
291
|
+
: ""}
|
|
292
|
+
</button>
|
|
293
|
+
)}
|
|
294
|
+
|
|
295
|
+
<ApprovalDurationPicker
|
|
296
|
+
value={approvalDurationMs}
|
|
297
|
+
onChange={setApprovalDurationMs}
|
|
298
|
+
options={durationOptions}
|
|
299
|
+
disabled={isRenewing}
|
|
300
|
+
/>
|
|
301
|
+
|
|
302
|
+
{error && (
|
|
303
|
+
<div className="flex items-start gap-2 rounded-lg border border-trade-down/20 bg-trade-down/[0.08] p-3">
|
|
304
|
+
<AlertTriangle
|
|
305
|
+
size={14}
|
|
306
|
+
className="mt-0.5 flex-shrink-0 text-trade-down"
|
|
307
|
+
/>
|
|
308
|
+
<p className="text-base text-trade-down">{error}</p>
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<div className="space-y-2 px-6 pb-6">
|
|
314
|
+
<button
|
|
315
|
+
onClick={handleRenew}
|
|
316
|
+
disabled={!canRenew}
|
|
317
|
+
className={`flex w-full items-center justify-center gap-2 rounded-lg py-2 text-base font-medium ${
|
|
318
|
+
canRenew ? "btn-raised" : "btn-raised-disabled"
|
|
319
|
+
}`}
|
|
320
|
+
>
|
|
321
|
+
{isRenewing ? (
|
|
322
|
+
<>
|
|
323
|
+
<Loader2 size={14} /> Renewing...
|
|
324
|
+
</>
|
|
325
|
+
) : (
|
|
326
|
+
"Renew Agent Wallet"
|
|
327
|
+
)}
|
|
328
|
+
</button>
|
|
329
|
+
{ownerReady && onDisconnectWallet && (
|
|
330
|
+
<button
|
|
331
|
+
onClick={onDisconnectWallet}
|
|
332
|
+
disabled={isRenewing}
|
|
333
|
+
className="w-full py-1.5 text-base text-gray-400 transition-colors hover:text-gray-300 disabled:cursor-not-allowed disabled:opacity-40"
|
|
334
|
+
>
|
|
335
|
+
Disconnect & use different owner
|
|
336
|
+
</button>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
</motion.div>
|
|
340
|
+
</div>
|
|
341
|
+
</motion.div>
|
|
342
|
+
)}
|
|
343
|
+
</AnimatePresence>
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function ApprovalDurationPicker({
|
|
348
|
+
value,
|
|
349
|
+
onChange,
|
|
350
|
+
options,
|
|
351
|
+
disabled,
|
|
352
|
+
}: {
|
|
353
|
+
value: number;
|
|
354
|
+
onChange: (value: number) => void;
|
|
355
|
+
options: AgentApprovalDurationOption[];
|
|
356
|
+
disabled?: boolean;
|
|
357
|
+
}) {
|
|
358
|
+
return (
|
|
359
|
+
<div>
|
|
360
|
+
<label className="mb-2 block text-base uppercase tracking-[0.1em] text-gray-400">
|
|
361
|
+
Approval Duration
|
|
362
|
+
</label>
|
|
363
|
+
<div className="grid grid-cols-4 gap-1.5">
|
|
364
|
+
{options.map((option) => (
|
|
365
|
+
<button
|
|
366
|
+
key={`${option.label}-${option.durationMs}`}
|
|
367
|
+
type="button"
|
|
368
|
+
onClick={() => onChange(option.durationMs)}
|
|
369
|
+
disabled={disabled}
|
|
370
|
+
className={`rounded py-1.5 text-base font-medium disabled:cursor-not-allowed disabled:opacity-50 ${
|
|
371
|
+
value === option.durationMs ? "btn-raised-active" : "btn-raised"
|
|
372
|
+
}`}
|
|
373
|
+
>
|
|
374
|
+
{option.label}
|
|
375
|
+
</button>
|
|
376
|
+
))}
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
);
|
|
380
|
+
}
|
package/src/UserProfileModal.tsx
CHANGED
|
@@ -6,13 +6,22 @@ import {
|
|
|
6
6
|
type CSSProperties,
|
|
7
7
|
type ReactNode,
|
|
8
8
|
} from "react";
|
|
9
|
+
import {
|
|
10
|
+
AgentExpiryWarningIcon,
|
|
11
|
+
EXPIRED_AGENT_COLOR,
|
|
12
|
+
} from "./AgentExpiryWarning";
|
|
9
13
|
import { DeleteWalletModal } from "./DeleteWalletModal";
|
|
10
14
|
import { useHypurrConnectInternal } from "./HypurrConnectProvider";
|
|
11
15
|
import { RenameWalletModal } from "./RenameWalletModal";
|
|
16
|
+
import { getAgentExpiryTitle } from "./agentWallet";
|
|
12
17
|
import {
|
|
18
|
+
Bot,
|
|
13
19
|
Copy,
|
|
20
|
+
Eye,
|
|
21
|
+
KeyRound,
|
|
14
22
|
LayoutDashboard,
|
|
15
23
|
Pencil,
|
|
24
|
+
RefreshCw,
|
|
16
25
|
SpinKeyframes,
|
|
17
26
|
Star,
|
|
18
27
|
Trash2,
|
|
@@ -29,6 +38,7 @@ import {
|
|
|
29
38
|
modalHeaderStyle,
|
|
30
39
|
modalPanelStyle,
|
|
31
40
|
modalWrapperStyle,
|
|
41
|
+
pickContrastColor,
|
|
32
42
|
type PrincipalColorOverrides,
|
|
33
43
|
type PrincipalColors,
|
|
34
44
|
profileColors,
|
|
@@ -82,6 +92,8 @@ export interface UserProfileModalProps {
|
|
|
82
92
|
* portfolio UI in response.
|
|
83
93
|
*/
|
|
84
94
|
onShowPortfolio?: (wallet: HyperliquidWallet) => void;
|
|
95
|
+
/** Called when the user clicks an agent wallet renew action. Host should connect the owner wallet and call `renewAgentWallet`. */
|
|
96
|
+
onRenewAgentWallet?: (wallet: HyperliquidWallet) => void;
|
|
85
97
|
/** Optional toast callback. SDK fires success on rename/delete; errors stay inline in sub-modals. */
|
|
86
98
|
onNotify?: (n: { type: "success" | "error"; message: string }) => void;
|
|
87
99
|
/** Shorthand for `principalColors.accent`. Defaults to `#a855f7`. */
|
|
@@ -155,6 +167,44 @@ const walletRowStyle: CSSProperties = {
|
|
|
155
167
|
transition: "background-color 150ms, border-color 150ms",
|
|
156
168
|
};
|
|
157
169
|
|
|
170
|
+
type WalletTypeMeta = {
|
|
171
|
+
label: string;
|
|
172
|
+
color: string;
|
|
173
|
+
background: string;
|
|
174
|
+
border: string;
|
|
175
|
+
icon: ReactNode;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
function getWalletTypeMeta(wallet: HyperliquidWallet): WalletTypeMeta {
|
|
179
|
+
if (wallet.isAgent) {
|
|
180
|
+
return {
|
|
181
|
+
label: "Agent wallet",
|
|
182
|
+
color: "#38bdf8",
|
|
183
|
+
background: "rgba(56,189,248,0.16)",
|
|
184
|
+
border: "rgba(56,189,248,0.34)",
|
|
185
|
+
icon: <Bot size={10} />,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (wallet.isReadOnly) {
|
|
190
|
+
return {
|
|
191
|
+
label: "Read-only wallet",
|
|
192
|
+
color: "#a78bfa",
|
|
193
|
+
background: "rgba(167,139,250,0.16)",
|
|
194
|
+
border: "rgba(167,139,250,0.34)",
|
|
195
|
+
icon: <Eye size={10} />,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
label: "Private-key wallet",
|
|
201
|
+
color: "#34d399",
|
|
202
|
+
background: "rgba(52,211,153,0.16)",
|
|
203
|
+
border: "rgba(52,211,153,0.34)",
|
|
204
|
+
icon: <KeyRound size={10} />,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
158
208
|
function ToggleSwitch({
|
|
159
209
|
checked,
|
|
160
210
|
onChange,
|
|
@@ -189,9 +239,11 @@ function ToggleSwitch({
|
|
|
189
239
|
height: 12,
|
|
190
240
|
width: 12,
|
|
191
241
|
borderRadius: "50%",
|
|
192
|
-
background:
|
|
242
|
+
background: checked
|
|
243
|
+
? pickContrastColor(accentColor)
|
|
244
|
+
: profileColors.text,
|
|
193
245
|
transform: checked ? "translateX(20px)" : "translateX(4px)",
|
|
194
|
-
transition: "transform 150ms",
|
|
246
|
+
transition: "transform 150ms, background 150ms",
|
|
195
247
|
}}
|
|
196
248
|
/>
|
|
197
249
|
</button>
|
|
@@ -282,6 +334,7 @@ export function UserProfileModal({
|
|
|
282
334
|
onWalletDeleted,
|
|
283
335
|
onWalletRenamed,
|
|
284
336
|
onShowPortfolio,
|
|
337
|
+
onRenewAgentWallet,
|
|
285
338
|
onNotify,
|
|
286
339
|
accentColor,
|
|
287
340
|
principalColors,
|
|
@@ -746,6 +799,11 @@ export function UserProfileModal({
|
|
|
746
799
|
colors={colors}
|
|
747
800
|
onRename={() => setWalletToRename(wallet)}
|
|
748
801
|
onDelete={() => setWalletToDelete(wallet)}
|
|
802
|
+
onRenewAgentWallet={
|
|
803
|
+
wallet.isAgent && onRenewAgentWallet
|
|
804
|
+
? () => onRenewAgentWallet(wallet)
|
|
805
|
+
: undefined
|
|
806
|
+
}
|
|
749
807
|
onShowPortfolio={
|
|
750
808
|
onShowPortfolio
|
|
751
809
|
? () => onShowPortfolio(wallet)
|
|
@@ -819,36 +877,52 @@ function WalletRow({
|
|
|
819
877
|
colors,
|
|
820
878
|
onRename,
|
|
821
879
|
onDelete,
|
|
880
|
+
onRenewAgentWallet,
|
|
822
881
|
onShowPortfolio,
|
|
823
882
|
}: {
|
|
824
883
|
wallet: HyperliquidWallet;
|
|
825
884
|
colors: PrincipalColors;
|
|
826
885
|
onRename: () => void;
|
|
827
886
|
onDelete: () => void;
|
|
887
|
+
onRenewAgentWallet?: () => void;
|
|
828
888
|
onShowPortfolio?: () => void;
|
|
829
889
|
}) {
|
|
830
890
|
const [hovered, setHovered] = useState(false);
|
|
891
|
+
const agentExpiryTitle = getAgentExpiryTitle(wallet);
|
|
892
|
+
const isAgentExpired = !!agentExpiryTitle;
|
|
893
|
+
const typeMeta = getWalletTypeMeta(wallet);
|
|
831
894
|
return (
|
|
832
895
|
<div
|
|
833
896
|
style={{
|
|
834
897
|
...walletRowStyle,
|
|
835
|
-
background:
|
|
836
|
-
?
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
898
|
+
background: isAgentExpired
|
|
899
|
+
? hovered
|
|
900
|
+
? "rgba(245,158,11,0.12)"
|
|
901
|
+
: "rgba(245,158,11,0.07)"
|
|
902
|
+
: hovered
|
|
903
|
+
? profileColors.surfaceBtnHover
|
|
904
|
+
: profileColors.surfaceBtn,
|
|
905
|
+
borderColor: isAgentExpired
|
|
906
|
+
? "rgba(245,158,11,0.34)"
|
|
907
|
+
: hovered
|
|
908
|
+
? profileColors.surfaceBdHover
|
|
909
|
+
: profileColors.surfaceBd,
|
|
841
910
|
}}
|
|
842
911
|
onMouseEnter={() => setHovered(true)}
|
|
843
912
|
onMouseLeave={() => setHovered(false)}
|
|
844
913
|
>
|
|
845
914
|
<div
|
|
846
915
|
style={{
|
|
916
|
+
position: "relative",
|
|
847
917
|
width: 32,
|
|
848
918
|
height: 32,
|
|
849
919
|
borderRadius: 6,
|
|
850
|
-
background:
|
|
851
|
-
|
|
920
|
+
background: isAgentExpired
|
|
921
|
+
? "rgba(245,158,11,0.12)"
|
|
922
|
+
: colors.accentBackground,
|
|
923
|
+
border: `1px solid ${
|
|
924
|
+
isAgentExpired ? "rgba(245,158,11,0.34)" : colors.accentBorder
|
|
925
|
+
}`,
|
|
852
926
|
display: "flex",
|
|
853
927
|
alignItems: "center",
|
|
854
928
|
justifyContent: "center",
|
|
@@ -860,33 +934,66 @@ function WalletRow({
|
|
|
860
934
|
fontSize: 12.5,
|
|
861
935
|
lineHeight: "1rem",
|
|
862
936
|
fontWeight: 600,
|
|
863
|
-
color: colors.accentText,
|
|
937
|
+
color: isAgentExpired ? EXPIRED_AGENT_COLOR : colors.accentText,
|
|
864
938
|
}}
|
|
865
939
|
>
|
|
866
940
|
{(wallet.name || "W")[0].toUpperCase()}
|
|
867
941
|
</span>
|
|
942
|
+
<span
|
|
943
|
+
role="img"
|
|
944
|
+
aria-label={typeMeta.label}
|
|
945
|
+
title={typeMeta.label}
|
|
946
|
+
style={{
|
|
947
|
+
position: "absolute",
|
|
948
|
+
right: -5,
|
|
949
|
+
bottom: -5,
|
|
950
|
+
width: 16,
|
|
951
|
+
height: 16,
|
|
952
|
+
borderRadius: "50%",
|
|
953
|
+
display: "flex",
|
|
954
|
+
alignItems: "center",
|
|
955
|
+
justifyContent: "center",
|
|
956
|
+
color: typeMeta.color,
|
|
957
|
+
background: typeMeta.background,
|
|
958
|
+
border: `1px solid ${typeMeta.border}`,
|
|
959
|
+
boxShadow: "0 1px 4px rgba(0,0,0,0.35)",
|
|
960
|
+
}}
|
|
961
|
+
>
|
|
962
|
+
{typeMeta.icon}
|
|
963
|
+
</span>
|
|
868
964
|
</div>
|
|
869
965
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
870
|
-
<
|
|
966
|
+
<div
|
|
871
967
|
style={{
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
color: profileColors.text,
|
|
877
|
-
overflow: "hidden",
|
|
878
|
-
textOverflow: "ellipsis",
|
|
879
|
-
whiteSpace: "nowrap",
|
|
968
|
+
display: "flex",
|
|
969
|
+
alignItems: "center",
|
|
970
|
+
gap: 5,
|
|
971
|
+
minWidth: 0,
|
|
880
972
|
}}
|
|
881
973
|
>
|
|
882
|
-
|
|
883
|
-
|
|
974
|
+
<p
|
|
975
|
+
style={{
|
|
976
|
+
margin: 0,
|
|
977
|
+
fontSize: 12.5,
|
|
978
|
+
lineHeight: "1rem",
|
|
979
|
+
fontWeight: 500,
|
|
980
|
+
color: isAgentExpired ? EXPIRED_AGENT_COLOR : profileColors.text,
|
|
981
|
+
overflow: "hidden",
|
|
982
|
+
textOverflow: "ellipsis",
|
|
983
|
+
whiteSpace: "nowrap",
|
|
984
|
+
}}
|
|
985
|
+
>
|
|
986
|
+
{wallet.name || "Unnamed"}
|
|
987
|
+
</p>
|
|
988
|
+
</div>
|
|
884
989
|
<p
|
|
885
990
|
style={{
|
|
886
991
|
margin: 0,
|
|
887
992
|
fontSize: 12.5,
|
|
888
993
|
lineHeight: "1rem",
|
|
889
|
-
color:
|
|
994
|
+
color: isAgentExpired
|
|
995
|
+
? "rgba(245,158,11,0.76)"
|
|
996
|
+
: profileColors.muted,
|
|
890
997
|
fontFamily: fontFamily.mono,
|
|
891
998
|
}}
|
|
892
999
|
>
|
|
@@ -894,6 +1001,12 @@ function WalletRow({
|
|
|
894
1001
|
{wallet.ethereumAddress?.slice(-4)}
|
|
895
1002
|
</p>
|
|
896
1003
|
</div>
|
|
1004
|
+
{agentExpiryTitle && (
|
|
1005
|
+
<AgentExpiryWarningIcon
|
|
1006
|
+
message={agentExpiryTitle}
|
|
1007
|
+
onClick={onRenewAgentWallet}
|
|
1008
|
+
/>
|
|
1009
|
+
)}
|
|
897
1010
|
<div
|
|
898
1011
|
style={{
|
|
899
1012
|
display: "flex",
|
|
@@ -904,6 +1017,21 @@ function WalletRow({
|
|
|
904
1017
|
transition: "opacity 120ms",
|
|
905
1018
|
}}
|
|
906
1019
|
>
|
|
1020
|
+
{wallet.isAgent && onRenewAgentWallet && (
|
|
1021
|
+
<IconBtn
|
|
1022
|
+
color={isAgentExpired ? EXPIRED_AGENT_COLOR : colors.accent}
|
|
1023
|
+
hoverBackgroundColor={
|
|
1024
|
+
isAgentExpired
|
|
1025
|
+
? "rgba(245,158,11,0.12)"
|
|
1026
|
+
: colors.accentHoverBackground
|
|
1027
|
+
}
|
|
1028
|
+
title="Renew agent wallet"
|
|
1029
|
+
ariaLabel={`Renew agent wallet for ${wallet.name || "wallet"}`}
|
|
1030
|
+
onClick={onRenewAgentWallet}
|
|
1031
|
+
>
|
|
1032
|
+
<RefreshCw size={13} />
|
|
1033
|
+
</IconBtn>
|
|
1034
|
+
)}
|
|
907
1035
|
{onShowPortfolio && (
|
|
908
1036
|
<IconBtn
|
|
909
1037
|
color={colors.accent}
|