@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.
@@ -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
+ }
@@ -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: profileColors.text,
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: hovered
836
- ? profileColors.surfaceBtnHover
837
- : profileColors.surfaceBtn,
838
- borderColor: hovered
839
- ? profileColors.surfaceBdHover
840
- : profileColors.surfaceBd,
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: colors.accentBackground,
851
- border: `1px solid ${colors.accentBorder}`,
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
- <p
966
+ <div
871
967
  style={{
872
- margin: 0,
873
- fontSize: 12.5,
874
- lineHeight: "1rem",
875
- fontWeight: 500,
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
- {wallet.name || "Unnamed"}
883
- </p>
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: profileColors.muted,
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}