@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,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,56 @@ 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 displayAddress =
|
|
894
|
+
wallet.isAgent && wallet.agentEthereumAddress?.value
|
|
895
|
+
? wallet.agentEthereumAddress.value
|
|
896
|
+
: wallet.ethereumAddress;
|
|
897
|
+
const typeMeta = getWalletTypeMeta(wallet);
|
|
831
898
|
return (
|
|
832
899
|
<div
|
|
833
900
|
style={{
|
|
834
901
|
...walletRowStyle,
|
|
835
|
-
background:
|
|
836
|
-
?
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
902
|
+
background: isAgentExpired
|
|
903
|
+
? hovered
|
|
904
|
+
? "rgba(245,158,11,0.12)"
|
|
905
|
+
: "rgba(245,158,11,0.07)"
|
|
906
|
+
: hovered
|
|
907
|
+
? profileColors.surfaceBtnHover
|
|
908
|
+
: profileColors.surfaceBtn,
|
|
909
|
+
borderColor: isAgentExpired
|
|
910
|
+
? "rgba(245,158,11,0.34)"
|
|
911
|
+
: hovered
|
|
912
|
+
? profileColors.surfaceBdHover
|
|
913
|
+
: profileColors.surfaceBd,
|
|
841
914
|
}}
|
|
842
915
|
onMouseEnter={() => setHovered(true)}
|
|
843
916
|
onMouseLeave={() => setHovered(false)}
|
|
844
917
|
>
|
|
845
918
|
<div
|
|
846
919
|
style={{
|
|
920
|
+
position: "relative",
|
|
847
921
|
width: 32,
|
|
848
922
|
height: 32,
|
|
849
923
|
borderRadius: 6,
|
|
850
|
-
background:
|
|
851
|
-
|
|
924
|
+
background: isAgentExpired
|
|
925
|
+
? "rgba(245,158,11,0.12)"
|
|
926
|
+
: colors.accentBackground,
|
|
927
|
+
border: `1px solid ${
|
|
928
|
+
isAgentExpired ? "rgba(245,158,11,0.34)" : colors.accentBorder
|
|
929
|
+
}`,
|
|
852
930
|
display: "flex",
|
|
853
931
|
alignItems: "center",
|
|
854
932
|
justifyContent: "center",
|
|
@@ -860,40 +938,79 @@ function WalletRow({
|
|
|
860
938
|
fontSize: 12.5,
|
|
861
939
|
lineHeight: "1rem",
|
|
862
940
|
fontWeight: 600,
|
|
863
|
-
color: colors.accentText,
|
|
941
|
+
color: isAgentExpired ? EXPIRED_AGENT_COLOR : colors.accentText,
|
|
864
942
|
}}
|
|
865
943
|
>
|
|
866
944
|
{(wallet.name || "W")[0].toUpperCase()}
|
|
867
945
|
</span>
|
|
946
|
+
<span
|
|
947
|
+
role="img"
|
|
948
|
+
aria-label={typeMeta.label}
|
|
949
|
+
title={typeMeta.label}
|
|
950
|
+
style={{
|
|
951
|
+
position: "absolute",
|
|
952
|
+
right: -5,
|
|
953
|
+
bottom: -5,
|
|
954
|
+
width: 16,
|
|
955
|
+
height: 16,
|
|
956
|
+
borderRadius: "50%",
|
|
957
|
+
display: "flex",
|
|
958
|
+
alignItems: "center",
|
|
959
|
+
justifyContent: "center",
|
|
960
|
+
color: typeMeta.color,
|
|
961
|
+
background: typeMeta.background,
|
|
962
|
+
border: `1px solid ${typeMeta.border}`,
|
|
963
|
+
boxShadow: "0 1px 4px rgba(0,0,0,0.35)",
|
|
964
|
+
}}
|
|
965
|
+
>
|
|
966
|
+
{typeMeta.icon}
|
|
967
|
+
</span>
|
|
868
968
|
</div>
|
|
869
969
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
870
|
-
<
|
|
970
|
+
<div
|
|
871
971
|
style={{
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
color: profileColors.text,
|
|
877
|
-
overflow: "hidden",
|
|
878
|
-
textOverflow: "ellipsis",
|
|
879
|
-
whiteSpace: "nowrap",
|
|
972
|
+
display: "flex",
|
|
973
|
+
alignItems: "center",
|
|
974
|
+
gap: 5,
|
|
975
|
+
minWidth: 0,
|
|
880
976
|
}}
|
|
881
977
|
>
|
|
882
|
-
|
|
883
|
-
|
|
978
|
+
<p
|
|
979
|
+
style={{
|
|
980
|
+
margin: 0,
|
|
981
|
+
fontSize: 12.5,
|
|
982
|
+
lineHeight: "1rem",
|
|
983
|
+
fontWeight: 500,
|
|
984
|
+
color: isAgentExpired ? EXPIRED_AGENT_COLOR : profileColors.text,
|
|
985
|
+
overflow: "hidden",
|
|
986
|
+
textOverflow: "ellipsis",
|
|
987
|
+
whiteSpace: "nowrap",
|
|
988
|
+
}}
|
|
989
|
+
>
|
|
990
|
+
{wallet.name || "Unnamed"}
|
|
991
|
+
</p>
|
|
992
|
+
</div>
|
|
884
993
|
<p
|
|
885
994
|
style={{
|
|
886
995
|
margin: 0,
|
|
887
996
|
fontSize: 12.5,
|
|
888
997
|
lineHeight: "1rem",
|
|
889
|
-
color:
|
|
998
|
+
color: isAgentExpired
|
|
999
|
+
? "rgba(245,158,11,0.76)"
|
|
1000
|
+
: profileColors.muted,
|
|
890
1001
|
fontFamily: fontFamily.mono,
|
|
891
1002
|
}}
|
|
892
1003
|
>
|
|
893
|
-
{
|
|
894
|
-
{
|
|
1004
|
+
{displayAddress?.slice(0, 6)}...
|
|
1005
|
+
{displayAddress?.slice(-4)}
|
|
895
1006
|
</p>
|
|
896
1007
|
</div>
|
|
1008
|
+
{agentExpiryTitle && (
|
|
1009
|
+
<AgentExpiryWarningIcon
|
|
1010
|
+
message={agentExpiryTitle}
|
|
1011
|
+
onClick={onRenewAgentWallet}
|
|
1012
|
+
/>
|
|
1013
|
+
)}
|
|
897
1014
|
<div
|
|
898
1015
|
style={{
|
|
899
1016
|
display: "flex",
|
|
@@ -904,6 +1021,21 @@ function WalletRow({
|
|
|
904
1021
|
transition: "opacity 120ms",
|
|
905
1022
|
}}
|
|
906
1023
|
>
|
|
1024
|
+
{wallet.isAgent && onRenewAgentWallet && (
|
|
1025
|
+
<IconBtn
|
|
1026
|
+
color={isAgentExpired ? EXPIRED_AGENT_COLOR : colors.accent}
|
|
1027
|
+
hoverBackgroundColor={
|
|
1028
|
+
isAgentExpired
|
|
1029
|
+
? "rgba(245,158,11,0.12)"
|
|
1030
|
+
: colors.accentHoverBackground
|
|
1031
|
+
}
|
|
1032
|
+
title="Renew agent wallet"
|
|
1033
|
+
ariaLabel={`Renew agent wallet for ${wallet.name || "wallet"}`}
|
|
1034
|
+
onClick={onRenewAgentWallet}
|
|
1035
|
+
>
|
|
1036
|
+
<RefreshCw size={13} />
|
|
1037
|
+
</IconBtn>
|
|
1038
|
+
)}
|
|
907
1039
|
{onShowPortfolio && (
|
|
908
1040
|
<IconBtn
|
|
909
1041
|
color={colors.accent}
|