@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.
@@ -0,0 +1,129 @@
1
+ import {
2
+ useCallback,
3
+ useRef,
4
+ useState,
5
+ type CSSProperties,
6
+ type RefCallback,
7
+ } from "react";
8
+ import { AlertTriangle } from "./icons/lucide";
9
+
10
+ const EXPIRED_AGENT_COLOR = "#f59e0b";
11
+
12
+ interface TooltipPosition {
13
+ left: number;
14
+ top: number;
15
+ }
16
+
17
+ export function AgentExpiryWarningIcon({
18
+ message,
19
+ onClick,
20
+ size = 13,
21
+ }: {
22
+ message: string;
23
+ onClick?: () => void;
24
+ size?: number;
25
+ }) {
26
+ const triggerRef = useRef<HTMLElement | null>(null);
27
+ const [tooltipPosition, setTooltipPosition] =
28
+ useState<TooltipPosition | null>(null);
29
+
30
+ const setTriggerRef: RefCallback<HTMLElement> = useCallback((node) => {
31
+ triggerRef.current = node;
32
+ }, []);
33
+
34
+ const showTooltip = useCallback(() => {
35
+ const rect = triggerRef.current?.getBoundingClientRect();
36
+ if (!rect) return;
37
+
38
+ const tooltipWidth = 260;
39
+ const left = Math.min(
40
+ Math.max(rect.left + rect.width / 2, tooltipWidth / 2 + 8),
41
+ window.innerWidth - tooltipWidth / 2 - 8,
42
+ );
43
+ const top =
44
+ rect.bottom + 8 < window.innerHeight - 48
45
+ ? rect.bottom + 8
46
+ : Math.max(8, rect.top - 44);
47
+
48
+ setTooltipPosition({ left, top });
49
+ }, []);
50
+
51
+ const hideTooltip = useCallback(() => {
52
+ setTooltipPosition(null);
53
+ }, []);
54
+
55
+ const triggerStyle: CSSProperties = {
56
+ width: size + 5,
57
+ height: size + 5,
58
+ padding: 0,
59
+ border: "none",
60
+ borderRadius: 4,
61
+ background: "transparent",
62
+ color: EXPIRED_AGENT_COLOR,
63
+ display: "inline-flex",
64
+ alignItems: "center",
65
+ justifyContent: "center",
66
+ flexShrink: 0,
67
+ cursor: onClick ? "pointer" : "help",
68
+ };
69
+
70
+ const sharedProps = {
71
+ ref: setTriggerRef,
72
+ "aria-label": "Agent approval expired",
73
+ onMouseEnter: showTooltip,
74
+ onMouseLeave: hideTooltip,
75
+ onFocus: showTooltip,
76
+ onBlur: hideTooltip,
77
+ style: triggerStyle,
78
+ };
79
+
80
+ return (
81
+ <>
82
+ {onClick ? (
83
+ <button
84
+ type="button"
85
+ {...sharedProps}
86
+ onClick={(event) => {
87
+ event.stopPropagation();
88
+ onClick();
89
+ }}
90
+ >
91
+ <AlertTriangle size={size} color={EXPIRED_AGENT_COLOR} />
92
+ </button>
93
+ ) : (
94
+ <span {...sharedProps}>
95
+ <AlertTriangle size={size} color={EXPIRED_AGENT_COLOR} />
96
+ </span>
97
+ )}
98
+ {tooltipPosition && (
99
+ <div
100
+ role="tooltip"
101
+ style={{
102
+ position: "fixed",
103
+ left: tooltipPosition.left,
104
+ top: tooltipPosition.top,
105
+ transform: "translateX(-50%)",
106
+ zIndex: 10_000,
107
+ width: 260,
108
+ maxWidth: "calc(100vw - 16px)",
109
+ padding: "7px 9px",
110
+ borderRadius: 6,
111
+ border: "1px solid rgba(255,255,255,0.12)",
112
+ background: "#111827",
113
+ boxShadow: "0 10px 28px rgba(0,0,0,0.45)",
114
+ color: "#e5e7eb",
115
+ fontSize: 12,
116
+ lineHeight: "1rem",
117
+ fontWeight: 500,
118
+ pointerEvents: "none",
119
+ whiteSpace: "normal",
120
+ }}
121
+ >
122
+ {message}
123
+ </div>
124
+ )}
125
+ </>
126
+ );
127
+ }
128
+
129
+ export { EXPIRED_AGENT_COLOR };
@@ -132,6 +132,10 @@ export function DeleteWalletModal({
132
132
  const [deleteHovered, setDeleteHovered] = useState(false);
133
133
 
134
134
  const walletName = wallet?.name || "Unnamed Wallet";
135
+ const displayAddress =
136
+ wallet?.isAgent && wallet.agentEthereumAddress?.value
137
+ ? wallet.agentEthereumAddress.value
138
+ : wallet?.ethereumAddress;
135
139
  const isNameMatch = confirmName === walletName;
136
140
  const canDelete = isNameMatch && !isDeleting;
137
141
 
@@ -258,7 +262,7 @@ export function DeleteWalletModal({
258
262
  wordBreak: "break-all",
259
263
  }}
260
264
  >
261
- {wallet.ethereumAddress}
265
+ {displayAddress}
262
266
  </p>
263
267
  </div>
264
268
 
@@ -27,6 +27,7 @@ import {
27
27
  import {
28
28
  AGENT_NAME,
29
29
  clearAgent as clearStoredAgent,
30
+ fetchAgentByAddress,
30
31
  fetchActiveAgent,
31
32
  generateAgentKey,
32
33
  isAgentValid,
@@ -37,6 +38,7 @@ import {
37
38
  import { createStaticClient, createTelegramClient } from "./grpc";
38
39
  import { GrpcExchangeTransport } from "./GrpcExchangeTransport";
39
40
  import { PrivateKeySigner } from "./privateKeySigner";
41
+ import { createTelegramAgentApprovalName } from "./agentWallet";
40
42
  import type {
41
43
  AuthMethod,
42
44
  EoaSigner,
@@ -45,6 +47,7 @@ import type {
45
47
  HypurrConnectConfig,
46
48
  HypurrConnectState,
47
49
  HypurrUser,
50
+ RenewAgentWalletParams,
48
51
  SignTypedDataFn,
49
52
  StoredAgent,
50
53
  } from "./types";
@@ -60,6 +63,23 @@ const TELEGRAM_AUTH_STATE_KEY = "hypurr-connect-auth-state";
60
63
  const TELEGRAM_AUTH_MESSAGE = "hypurr-connect:telegram-auth";
61
64
  const DEFAULT_AUTH_HUB_URL = "https://auth.hypurr.fun/login";
62
65
  const DEFAULT_MEDIA_URL = "https://media.hypurr.fun";
66
+ const USER_SIGNED_DOMAIN_NAME = "HyperliquidSignTransaction";
67
+ const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" as const;
68
+ const APPROVE_AGENT_PRIMARY_TYPE = "HyperliquidTransaction:ApproveAgent";
69
+ const APPROVE_AGENT_TYPES = {
70
+ [APPROVE_AGENT_PRIMARY_TYPE]: [
71
+ { name: "hyperliquidChain", type: "string" },
72
+ { name: "agentAddress", type: "address" },
73
+ { name: "agentName", type: "string" },
74
+ { name: "nonce", type: "uint64" },
75
+ ],
76
+ };
77
+ const EIP712_DOMAIN_TYPES = [
78
+ { name: "name", type: "string" },
79
+ { name: "version", type: "string" },
80
+ { name: "chainId", type: "uint256" },
81
+ { name: "verifyingContract", type: "address" },
82
+ ];
63
83
  const IGNORED_EXTERNAL_SIGNATURE =
64
84
  "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b" as const;
65
85
  const DEFAULT_TELEGRAM_SCOPES = [
@@ -105,6 +125,67 @@ function isAddress(value?: string | null): value is Hex {
105
125
  return !!value && /^0x[a-fA-F0-9]{40}$/.test(value);
106
126
  }
107
127
 
128
+ function createApproveAgentAction(params: {
129
+ agentAddress: Hex;
130
+ agentName: string;
131
+ chainId: number;
132
+ isTestnet: boolean;
133
+ }) {
134
+ const nonce = Date.now();
135
+ return {
136
+ action: {
137
+ type: "approveAgent" as const,
138
+ signatureChainId: `0x${params.chainId.toString(16)}` as Hex,
139
+ hyperliquidChain: (params.isTestnet ? "Testnet" : "Mainnet") as
140
+ | "Testnet"
141
+ | "Mainnet",
142
+ agentAddress: params.agentAddress.toLowerCase() as Hex,
143
+ agentName: params.agentName,
144
+ nonce,
145
+ },
146
+ nonce,
147
+ };
148
+ }
149
+
150
+ type ApproveAgentAction = ReturnType<typeof createApproveAgentAction>["action"];
151
+
152
+ async function signApproveAgentAction(params: {
153
+ signTypedDataAsync: SignTypedDataFn;
154
+ agentAddress: Hex;
155
+ agentName: string;
156
+ chainId: number;
157
+ isTestnet: boolean;
158
+ }): Promise<{
159
+ action: ApproveAgentAction;
160
+ signatureHex: Hex;
161
+ }> {
162
+ const { action } = createApproveAgentAction(params);
163
+ const signatureHex = await params.signTypedDataAsync({
164
+ domain: {
165
+ name: USER_SIGNED_DOMAIN_NAME,
166
+ version: "1",
167
+ chainId: params.chainId,
168
+ verifyingContract: ZERO_ADDRESS,
169
+ },
170
+ types: {
171
+ EIP712Domain: EIP712_DOMAIN_TYPES,
172
+ ...APPROVE_AGENT_TYPES,
173
+ },
174
+ primaryType: APPROVE_AGENT_PRIMARY_TYPE,
175
+ message: {
176
+ hyperliquidChain: action.hyperliquidChain,
177
+ agentAddress: action.agentAddress,
178
+ agentName: action.agentName,
179
+ nonce: action.nonce,
180
+ },
181
+ });
182
+
183
+ return {
184
+ action,
185
+ signatureHex,
186
+ };
187
+ }
188
+
108
189
  function getRawSignedTransaction(result: unknown): Hex | null {
109
190
  if (typeof result === "string" && result.startsWith("0x")) {
110
191
  return result as Hex;
@@ -928,6 +1009,113 @@ export function HypurrConnectProvider({
928
1009
  [tgClient, telegramRpcOptions, refreshWallets],
929
1010
  );
930
1011
 
1012
+ const renewAgentWallet = useCallback(
1013
+ async ({
1014
+ walletId,
1015
+ ownerAddress,
1016
+ signTypedDataAsync,
1017
+ chainId,
1018
+ approvalDurationMs,
1019
+ agentName,
1020
+ }: RenewAgentWalletParams): Promise<void> => {
1021
+ if (authMethod !== "telegram") {
1022
+ throw new Error(
1023
+ "[HypurrConnect] Agent wallet renewal is only available for Telegram wallets.",
1024
+ );
1025
+ }
1026
+ if (!isAddress(ownerAddress)) {
1027
+ throw new Error(
1028
+ "[HypurrConnect] Connect the owner EOA wallet before renewing this agent.",
1029
+ );
1030
+ }
1031
+ if (!telegramRpcOptions) {
1032
+ throw new Error("[HypurrConnect] No Telegram RPC session available.");
1033
+ }
1034
+
1035
+ const wallet = wallets.find((w) => w.id === walletId);
1036
+ if (!wallet) {
1037
+ throw new Error("[HypurrConnect] Agent wallet not found.");
1038
+ }
1039
+ if (!wallet.isAgent) {
1040
+ throw new Error(
1041
+ "[HypurrConnect] Selected wallet is not an agent wallet.",
1042
+ );
1043
+ }
1044
+ if (!isAddress(wallet.ethereumAddress)) {
1045
+ throw new Error(
1046
+ "[HypurrConnect] Agent wallet does not have a valid owner EVM address.",
1047
+ );
1048
+ }
1049
+ if (ownerAddress.toLowerCase() !== wallet.ethereumAddress.toLowerCase()) {
1050
+ throw new Error(
1051
+ "[HypurrConnect] Connect the owner EOA for this agent wallet before renewing.",
1052
+ );
1053
+ }
1054
+
1055
+ const agentAddress = wallet.agentEthereumAddress?.value;
1056
+ if (!isAddress(agentAddress)) {
1057
+ throw new Error(
1058
+ "[HypurrConnect] Agent wallet does not have a valid agent EVM address.",
1059
+ );
1060
+ }
1061
+
1062
+ const isTestnet = config.isTestnet ?? false;
1063
+ const approvalAgentName =
1064
+ agentName ?? createTelegramAgentApprovalName(approvalDurationMs);
1065
+ setTgError(null);
1066
+
1067
+ try {
1068
+ const { action, signatureHex } = await signApproveAgentAction({
1069
+ signTypedDataAsync,
1070
+ agentAddress,
1071
+ agentName: approvalAgentName,
1072
+ chainId,
1073
+ isTestnet,
1074
+ });
1075
+
1076
+ await tgClient.hyperliquidAgentWalletRenew(
1077
+ {
1078
+ authData: {},
1079
+ address: { value: wallet.ethereumAddress },
1080
+ signature: {
1081
+ agentAddress: action.agentAddress,
1082
+ agentName: action.agentName,
1083
+ nonce: action.nonce,
1084
+ chainId,
1085
+ signature: signatureHex,
1086
+ },
1087
+ },
1088
+ telegramRpcOptions,
1089
+ );
1090
+
1091
+ const remote = await fetchAgentByAddress(
1092
+ ownerAddress,
1093
+ agentAddress,
1094
+ isTestnet,
1095
+ );
1096
+ if (remote && remote.validUntil <= Date.now()) {
1097
+ throw new Error(
1098
+ "[HypurrConnect] Agent renewal was submitted, but the agent is still expired.",
1099
+ );
1100
+ }
1101
+
1102
+ refreshWallets();
1103
+ } catch (err) {
1104
+ console.error("[HypurrConnect] Agent wallet renewal failed:", err);
1105
+ setTgError(err instanceof Error ? err.message : String(err));
1106
+ throw err;
1107
+ }
1108
+ },
1109
+ [
1110
+ authMethod,
1111
+ config.isTestnet,
1112
+ refreshWallets,
1113
+ telegramRpcOptions,
1114
+ tgClient,
1115
+ wallets,
1116
+ ],
1117
+ );
1118
+
931
1119
  const createWalletPack = useCallback(
932
1120
  async (name: string): Promise<number> => {
933
1121
  const { response } = await tgClient.telegramChatWalletPackCreate(
@@ -1155,7 +1343,9 @@ export function HypurrConnectProvider({
1155
1343
  ? configuredReturnTo()
1156
1344
  : configuredReturnTo || currentReturnTo();
1157
1345
 
1158
- const authUrl = new URL(config.telegram?.authHubUrl || DEFAULT_AUTH_HUB_URL);
1346
+ const authUrl = new URL(
1347
+ config.telegram?.authHubUrl || DEFAULT_AUTH_HUB_URL,
1348
+ );
1159
1349
  authUrl.searchParams.set("return_to", returnTo);
1160
1350
  authUrl.searchParams.set("state", state);
1161
1351
  authUrl.searchParams.set("scope", normalizeScopes(config.telegram?.scope));
@@ -1350,6 +1540,7 @@ export function HypurrConnectProvider({
1350
1540
  createWallet,
1351
1541
  deleteWallet,
1352
1542
  renameWallet,
1543
+ renewAgentWallet,
1353
1544
  refreshWallets,
1354
1545
 
1355
1546
  packs,
@@ -1399,6 +1590,7 @@ export function HypurrConnectProvider({
1399
1590
  createWallet,
1400
1591
  deleteWallet,
1401
1592
  renameWallet,
1593
+ renewAgentWallet,
1402
1594
  refreshWallets,
1403
1595
  packs,
1404
1596
  createWalletPack,
@@ -17,10 +17,14 @@ import { TelegramColorIcon } from "./icons/TelegramColorIcon";
17
17
  export interface LoginModalProps {
18
18
  onConnectWallet: () => void;
19
19
  walletIcon?: ReactNode;
20
+ /** CSS color used as the modal/drawer background. Defaults to `rgba(20,20,20,0.95)`. */
21
+ backgroundColor?: string;
20
22
  }
21
23
 
22
24
  const MOBILE_BREAKPOINT = 640;
23
25
 
26
+ const DEFAULT_BACKGROUND_COLOR = "rgba(20,20,20,0.95)";
27
+
24
28
  const btnStyle: CSSProperties = {
25
29
  display: "flex",
26
30
  height: 53,
@@ -28,19 +32,19 @@ const btnStyle: CSSProperties = {
28
32
  alignItems: "center",
29
33
  gap: 12,
30
34
  overflow: "hidden",
31
- borderRadius: 6,
32
- background: "rgba(255,255,255,0.05)",
33
- padding: "0 24px",
35
+ borderRadius: 4,
36
+ background: "rgba(255,255,255,0.1)",
37
+ padding: "4px 24px",
34
38
  fontSize: 14,
35
- fontWeight: 600,
36
- letterSpacing: "-0.01em",
39
+ fontWeight: 500,
40
+ letterSpacing: "-0.03em",
37
41
  color: "#fff",
38
42
  cursor: "pointer",
39
43
  border: "none",
40
44
  transition: "background 150ms",
41
45
  };
42
46
 
43
- const btnHoverBg = { background: "rgba(255,255,255,0.1)" };
47
+ const btnHoverBg = { background: "rgba(255,255,255,0.15)" };
44
48
 
45
49
  const backdropStyle: CSSProperties = {
46
50
  position: "fixed",
@@ -70,14 +74,13 @@ const modalBoxStyle: CSSProperties = {
70
74
  overflow: "hidden",
71
75
  borderRadius: 12,
72
76
  border: "1px solid rgba(255,255,255,0.1)",
73
- background: "#282828",
74
77
  padding: 24,
75
78
  };
76
79
 
77
80
  const headingStyle: CSSProperties = {
78
- fontSize: 16,
79
- fontWeight: 700,
80
- letterSpacing: "-0.025em",
81
+ fontSize: 20,
82
+ fontWeight: 500,
83
+ letterSpacing: "-0.03em",
81
84
  color: "#fff",
82
85
  margin: 0,
83
86
  };
@@ -88,7 +91,7 @@ const dividerStyle: CSSProperties = {
88
91
  background: "rgba(255,255,255,0.05)",
89
92
  };
90
93
 
91
- const iconSize: CSSProperties = { width: 20, height: 20 };
94
+ const iconSize: CSSProperties = { width: 26, height: 26 };
92
95
 
93
96
  const mobileQuery =
94
97
  typeof window !== "undefined"
@@ -127,7 +130,11 @@ function HoverButton({
127
130
  );
128
131
  }
129
132
 
130
- export function LoginModal({ onConnectWallet, walletIcon }: LoginModalProps) {
133
+ export function LoginModal({
134
+ onConnectWallet,
135
+ walletIcon,
136
+ backgroundColor = DEFAULT_BACKGROUND_COLOR,
137
+ }: LoginModalProps) {
131
138
  const { loginTelegram, loginModalOpen, closeLoginModal } =
132
139
  useHypurrConnectInternal();
133
140
 
@@ -174,7 +181,11 @@ export function LoginModal({ onConnectWallet, walletIcon }: LoginModalProps) {
174
181
  <AnimatePresence>
175
182
  {loginModalOpen &&
176
183
  (isMobile ? (
177
- <MobileDrawer key="drawer" onClose={closeLoginModal}>
184
+ <MobileDrawer
185
+ key="drawer"
186
+ onClose={closeLoginModal}
187
+ backgroundColor={backgroundColor}
188
+ >
178
189
  {modalContent}
179
190
  </MobileDrawer>
180
191
  ) : (
@@ -198,7 +209,7 @@ export function LoginModal({ onConnectWallet, walletIcon }: LoginModalProps) {
198
209
  onClick={closeLoginModal}
199
210
  >
200
211
  <motion.div
201
- style={modalBoxStyle}
212
+ style={{ ...modalBoxStyle, background: backgroundColor }}
202
213
  initial={{ scale: 0.96, opacity: 0, y: 8 }}
203
214
  animate={{ scale: 1, opacity: 1, y: 0 }}
204
215
  exit={{ scale: 0.96, opacity: 0, y: 8 }}
@@ -230,7 +241,6 @@ const drawerSheetStyle: CSSProperties = {
230
241
  borderLeft: "1px solid rgba(255,255,255,0.1)",
231
242
  borderRight: "1px solid rgba(255,255,255,0.1)",
232
243
  borderTop: "1px solid rgba(255,255,255,0.1)",
233
- background: "#282828",
234
244
  padding: "12px 24px max(24px, env(safe-area-inset-bottom))",
235
245
  };
236
246
 
@@ -241,7 +251,6 @@ const drawerBgStyle: CSSProperties = {
241
251
  top: 0,
242
252
  bottom: "-100vh",
243
253
  zIndex: -1,
244
- background: "#282828",
245
254
  borderTopLeftRadius: 12,
246
255
  borderTopRightRadius: 12,
247
256
  };
@@ -256,16 +265,18 @@ const grabHandleStyle: CSSProperties = {
256
265
  margin: "0 auto",
257
266
  height: 4,
258
267
  width: 100,
259
- borderRadius: 9999,
268
+ borderRadius: 4,
260
269
  background: "rgba(255,255,255,0.05)",
261
270
  };
262
271
 
263
272
  function MobileDrawer({
264
273
  children,
265
274
  onClose,
275
+ backgroundColor,
266
276
  }: {
267
277
  children: ReactNode;
268
278
  onClose: () => void;
279
+ backgroundColor: string;
269
280
  }) {
270
281
  const controls = useAnimationControls();
271
282
 
@@ -294,7 +305,7 @@ function MobileDrawer({
294
305
 
295
306
  <motion.div
296
307
  key="drawer-sheet"
297
- style={drawerSheetStyle}
308
+ style={{ ...drawerSheetStyle, background: backgroundColor }}
298
309
  initial={{ y: "100%" }}
299
310
  animate={{ y: 0 }}
300
311
  exit={{ y: "100%" }}
@@ -304,7 +315,7 @@ function MobileDrawer({
304
315
  dragElastic={{ top: 0, bottom: 0.4 }}
305
316
  onDragEnd={handleDragEnd}
306
317
  >
307
- <div style={drawerBgStyle} />
318
+ <div style={{ ...drawerBgStyle, background: backgroundColor }} />
308
319
 
309
320
  <div style={grabHandleAreaStyle}>
310
321
  <div style={grabHandleStyle} />
@@ -125,6 +125,10 @@ export function RenameWalletModal({
125
125
  const [saveHovered, setSaveHovered] = useState(false);
126
126
 
127
127
  const currentName = wallet?.name || "Unnamed";
128
+ const displayAddress =
129
+ wallet?.isAgent && wallet.agentEthereumAddress?.value
130
+ ? wallet.agentEthereumAddress.value
131
+ : wallet?.ethereumAddress;
128
132
  const trimmedName = name.trim();
129
133
  const isNameChanged = trimmedName !== currentName;
130
134
  const isNameValid = WALLET_NAME_REGEX.test(trimmedName);
@@ -233,7 +237,7 @@ export function RenameWalletModal({
233
237
  wordBreak: "break-all",
234
238
  }}
235
239
  >
236
- {wallet.ethereumAddress}
240
+ {displayAddress}
237
241
  </p>
238
242
  </div>
239
243