@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,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,7 @@ export function DeleteWalletModal({
132
132
  const [deleteHovered, setDeleteHovered] = useState(false);
133
133
 
134
134
  const walletName = wallet?.name || "Unnamed Wallet";
135
+ const displayAddress = wallet?.ethereumAddress;
135
136
  const isNameMatch = confirmName === walletName;
136
137
  const canDelete = isNameMatch && !isDeleting;
137
138
 
@@ -258,7 +259,7 @@ export function DeleteWalletModal({
258
259
  wordBreak: "break-all",
259
260
  }}
260
261
  >
261
- {wallet.ethereumAddress}
262
+ {displayAddress}
262
263
  </p>
263
264
  </div>
264
265
 
@@ -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(
@@ -1352,6 +1540,7 @@ export function HypurrConnectProvider({
1352
1540
  createWallet,
1353
1541
  deleteWallet,
1354
1542
  renameWallet,
1543
+ renewAgentWallet,
1355
1544
  refreshWallets,
1356
1545
 
1357
1546
  packs,
@@ -1401,6 +1590,7 @@ export function HypurrConnectProvider({
1401
1590
  createWallet,
1402
1591
  deleteWallet,
1403
1592
  renameWallet,
1593
+ renewAgentWallet,
1404
1594
  refreshWallets,
1405
1595
  packs,
1406
1596
  createWalletPack,
@@ -32,19 +32,19 @@ const btnStyle: CSSProperties = {
32
32
  alignItems: "center",
33
33
  gap: 12,
34
34
  overflow: "hidden",
35
- borderRadius: 6,
36
- background: "rgba(255,255,255,0.05)",
37
- padding: "0 24px",
35
+ borderRadius: 4,
36
+ background: "rgba(255,255,255,0.1)",
37
+ padding: "4px 24px",
38
38
  fontSize: 14,
39
- fontWeight: 600,
40
- letterSpacing: "-0.01em",
39
+ fontWeight: 500,
40
+ letterSpacing: "-0.03em",
41
41
  color: "#fff",
42
42
  cursor: "pointer",
43
43
  border: "none",
44
44
  transition: "background 150ms",
45
45
  };
46
46
 
47
- const btnHoverBg = { background: "rgba(255,255,255,0.1)" };
47
+ const btnHoverBg = { background: "rgba(255,255,255,0.15)" };
48
48
 
49
49
  const backdropStyle: CSSProperties = {
50
50
  position: "fixed",
@@ -78,9 +78,9 @@ const modalBoxStyle: CSSProperties = {
78
78
  };
79
79
 
80
80
  const headingStyle: CSSProperties = {
81
- fontSize: 16,
82
- fontWeight: 700,
83
- letterSpacing: "-0.025em",
81
+ fontSize: 20,
82
+ fontWeight: 500,
83
+ letterSpacing: "-0.03em",
84
84
  color: "#fff",
85
85
  margin: 0,
86
86
  };
@@ -91,7 +91,7 @@ const dividerStyle: CSSProperties = {
91
91
  background: "rgba(255,255,255,0.05)",
92
92
  };
93
93
 
94
- const iconSize: CSSProperties = { width: 20, height: 20 };
94
+ const iconSize: CSSProperties = { width: 26, height: 26 };
95
95
 
96
96
  const mobileQuery =
97
97
  typeof window !== "undefined"
@@ -265,7 +265,7 @@ const grabHandleStyle: CSSProperties = {
265
265
  margin: "0 auto",
266
266
  height: 4,
267
267
  width: 100,
268
- borderRadius: 9999,
268
+ borderRadius: 4,
269
269
  background: "rgba(255,255,255,0.05)",
270
270
  };
271
271
 
@@ -125,6 +125,7 @@ export function RenameWalletModal({
125
125
  const [saveHovered, setSaveHovered] = useState(false);
126
126
 
127
127
  const currentName = wallet?.name || "Unnamed";
128
+ const displayAddress = wallet?.ethereumAddress;
128
129
  const trimmedName = name.trim();
129
130
  const isNameChanged = trimmedName !== currentName;
130
131
  const isNameValid = WALLET_NAME_REGEX.test(trimmedName);
@@ -233,7 +234,7 @@ export function RenameWalletModal({
233
234
  wordBreak: "break-all",
234
235
  }}
235
236
  >
236
- {wallet.ethereumAddress}
237
+ {displayAddress}
237
238
  </p>
238
239
  </div>
239
240