@hfunlabs/hypurr-connect 0.1.14 → 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.
@@ -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,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: hovered
836
- ? profileColors.surfaceBtnHover
837
- : profileColors.surfaceBtn,
838
- borderColor: hovered
839
- ? profileColors.surfaceBdHover
840
- : profileColors.surfaceBd,
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: colors.accentBackground,
851
- border: `1px solid ${colors.accentBorder}`,
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
- <p
970
+ <div
871
971
  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",
972
+ display: "flex",
973
+ alignItems: "center",
974
+ gap: 5,
975
+ minWidth: 0,
880
976
  }}
881
977
  >
882
- {wallet.name || "Unnamed"}
883
- </p>
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: profileColors.muted,
998
+ color: isAgentExpired
999
+ ? "rgba(245,158,11,0.76)"
1000
+ : profileColors.muted,
890
1001
  fontFamily: fontFamily.mono,
891
1002
  }}
892
1003
  >
893
- {wallet.ethereumAddress?.slice(0, 6)}...
894
- {wallet.ethereumAddress?.slice(-4)}
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}