@hfunlabs/hypurr-connect 0.1.0 → 0.1.2

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.
@@ -8,9 +8,10 @@ import {
8
8
  useCallback,
9
9
  useEffect,
10
10
  useSyncExternalStore,
11
+ type CSSProperties,
11
12
  type ReactNode,
12
13
  } from "react";
13
- import { useHypurrConnect } from "./HypurrConnectProvider";
14
+ import { useHypurrConnectInternal } from "./HypurrConnectProvider";
14
15
  import { MetaMaskColorIcon } from "./icons/MetaMaskColorIcon";
15
16
  import { TelegramColorIcon } from "./icons/TelegramColorIcon";
16
17
  import type { TelegramLoginData } from "./types";
@@ -22,8 +23,74 @@ export interface LoginModalProps {
22
23
 
23
24
  const MOBILE_BREAKPOINT = 640;
24
25
 
25
- const btnClass =
26
- "flex h-[53px] w-full items-center gap-3 overflow-hidden rounded bg-white/5 px-6 text-sm font-semibold tracking-tight text-white cursor-pointer transition-colors duration-150 hover:bg-white/10";
26
+ const btnStyle: CSSProperties = {
27
+ display: "flex",
28
+ height: 53,
29
+ width: "100%",
30
+ alignItems: "center",
31
+ gap: 12,
32
+ overflow: "hidden",
33
+ borderRadius: 6,
34
+ background: "rgba(255,255,255,0.05)",
35
+ padding: "0 24px",
36
+ fontSize: 14,
37
+ fontWeight: 600,
38
+ letterSpacing: "-0.01em",
39
+ color: "#fff",
40
+ cursor: "pointer",
41
+ border: "none",
42
+ transition: "background 150ms",
43
+ };
44
+
45
+ const btnHoverBg = { background: "rgba(255,255,255,0.1)" };
46
+
47
+ const backdropStyle: CSSProperties = {
48
+ position: "fixed",
49
+ inset: 0,
50
+ zIndex: 100,
51
+ background: "rgba(0,0,0,0.6)",
52
+ backdropFilter: "blur(2px)",
53
+ WebkitBackdropFilter: "blur(2px)",
54
+ };
55
+
56
+ const modalWrapperStyle: CSSProperties = {
57
+ position: "fixed",
58
+ inset: 0,
59
+ zIndex: 101,
60
+ display: "flex",
61
+ alignItems: "center",
62
+ justifyContent: "center",
63
+ padding: 16,
64
+ };
65
+
66
+ const modalBoxStyle: CSSProperties = {
67
+ display: "flex",
68
+ width: 400,
69
+ flexDirection: "column",
70
+ alignItems: "center",
71
+ gap: 16,
72
+ overflow: "hidden",
73
+ borderRadius: 12,
74
+ border: "1px solid rgba(255,255,255,0.1)",
75
+ background: "#282828",
76
+ padding: 24,
77
+ };
78
+
79
+ const headingStyle: CSSProperties = {
80
+ fontSize: 16,
81
+ fontWeight: 700,
82
+ letterSpacing: "-0.025em",
83
+ color: "#fff",
84
+ margin: 0,
85
+ };
86
+
87
+ const dividerStyle: CSSProperties = {
88
+ height: 1,
89
+ width: "100%",
90
+ background: "rgba(255,255,255,0.05)",
91
+ };
92
+
93
+ const iconSize: CSSProperties = { width: 20, height: 20 };
27
94
 
28
95
  const mobileQuery =
29
96
  typeof window !== "undefined"
@@ -43,9 +110,28 @@ function useIsMobile() {
43
110
  return useSyncExternalStore(subscribeMobile, getSnapshotMobile, () => false);
44
111
  }
45
112
 
113
+ function HoverButton({
114
+ onClick,
115
+ children,
116
+ }: {
117
+ onClick: () => void;
118
+ children: ReactNode;
119
+ }) {
120
+ return (
121
+ <motion.button
122
+ type="button"
123
+ onClick={onClick}
124
+ style={btnStyle}
125
+ whileHover={btnHoverBg}
126
+ >
127
+ {children}
128
+ </motion.button>
129
+ );
130
+ }
131
+
46
132
  export function LoginModal({ onConnectWallet, walletIcon }: LoginModalProps) {
47
133
  const { loginTelegram, loginModalOpen, closeLoginModal, botId } =
48
- useHypurrConnect();
134
+ useHypurrConnectInternal();
49
135
 
50
136
  const handleTelegramAuth = useCallback(
51
137
  (user: TelegramLoginData) => {
@@ -99,26 +185,33 @@ export function LoginModal({ onConnectWallet, walletIcon }: LoginModalProps) {
99
185
 
100
186
  const modalContent = (
101
187
  <>
102
- <div className="flex w-full flex-col items-center gap-2 overflow-hidden">
103
- <button type="button" onClick={openTelegramOAuth} className={btnClass}>
104
- <TelegramColorIcon className="size-5" />
188
+ <div
189
+ style={{
190
+ display: "flex",
191
+ width: "100%",
192
+ flexDirection: "column",
193
+ alignItems: "center",
194
+ gap: 8,
195
+ overflow: "hidden",
196
+ }}
197
+ >
198
+ <HoverButton onClick={openTelegramOAuth}>
199
+ <TelegramColorIcon style={iconSize} />
105
200
  Telegram
106
- </button>
201
+ </HoverButton>
107
202
  </div>
108
203
 
109
- <div className="h-px w-full bg-white/5" />
204
+ <div style={dividerStyle} />
110
205
 
111
- <button
112
- type="button"
206
+ <HoverButton
113
207
  onClick={() => {
114
208
  closeLoginModal();
115
209
  onConnectWallet();
116
210
  }}
117
- className={btnClass}
118
211
  >
119
- {walletIcon ?? <MetaMaskColorIcon className="size-5" />}
212
+ {walletIcon ?? <MetaMaskColorIcon style={iconSize} />}
120
213
  Wallet
121
- </button>
214
+ </HoverButton>
122
215
  </>
123
216
  );
124
217
 
@@ -133,7 +226,7 @@ export function LoginModal({ onConnectWallet, walletIcon }: LoginModalProps) {
133
226
  <>
134
227
  <motion.div
135
228
  key="backdrop"
136
- className="fixed inset-0 z-[100] bg-black/60 backdrop-blur-[2px]"
229
+ style={backdropStyle}
137
230
  initial={{ opacity: 0 }}
138
231
  animate={{ opacity: 1 }}
139
232
  exit={{ opacity: 0 }}
@@ -142,7 +235,7 @@ export function LoginModal({ onConnectWallet, walletIcon }: LoginModalProps) {
142
235
  />
143
236
  <motion.div
144
237
  key="modal-wrapper"
145
- className="fixed inset-0 z-[101] flex items-center justify-center p-4"
238
+ style={modalWrapperStyle}
146
239
  initial={{ opacity: 0 }}
147
240
  animate={{ opacity: 1 }}
148
241
  exit={{ opacity: 0 }}
@@ -150,16 +243,14 @@ export function LoginModal({ onConnectWallet, walletIcon }: LoginModalProps) {
150
243
  onClick={closeLoginModal}
151
244
  >
152
245
  <motion.div
153
- className="flex w-[400px] flex-col items-center gap-4 overflow-hidden rounded-xl border border-white/10 bg-[#282828] p-6"
246
+ style={modalBoxStyle}
154
247
  initial={{ opacity: 0, scale: 0.95, y: 10 }}
155
248
  animate={{ opacity: 1, scale: 1, y: 0 }}
156
249
  exit={{ opacity: 0, scale: 0.95, y: 10 }}
157
250
  transition={{ duration: 0.2, ease: "easeOut" }}
158
251
  onClick={(e) => e.stopPropagation()}
159
252
  >
160
- <p className="text-base font-bold tracking-tight text-white">
161
- Connect
162
- </p>
253
+ <p style={headingStyle}>Connect</p>
163
254
  {modalContent}
164
255
  </motion.div>
165
256
  </motion.div>
@@ -169,6 +260,51 @@ export function LoginModal({ onConnectWallet, walletIcon }: LoginModalProps) {
169
260
  );
170
261
  }
171
262
 
263
+ const drawerSheetStyle: CSSProperties = {
264
+ position: "fixed",
265
+ left: 0,
266
+ right: 0,
267
+ bottom: 0,
268
+ zIndex: 101,
269
+ display: "flex",
270
+ flexDirection: "column",
271
+ alignItems: "center",
272
+ gap: 16,
273
+ borderTopLeftRadius: 12,
274
+ borderTopRightRadius: 12,
275
+ borderLeft: "1px solid rgba(255,255,255,0.1)",
276
+ borderRight: "1px solid rgba(255,255,255,0.1)",
277
+ borderTop: "1px solid rgba(255,255,255,0.1)",
278
+ background: "#282828",
279
+ padding: "12px 24px max(24px, env(safe-area-inset-bottom))",
280
+ };
281
+
282
+ const drawerBgStyle: CSSProperties = {
283
+ position: "absolute",
284
+ left: 0,
285
+ right: 0,
286
+ top: 0,
287
+ bottom: "-100vh",
288
+ zIndex: -1,
289
+ background: "#282828",
290
+ borderTopLeftRadius: 12,
291
+ borderTopRightRadius: 12,
292
+ };
293
+
294
+ const grabHandleAreaStyle: CSSProperties = {
295
+ width: "100%",
296
+ cursor: "grab",
297
+ paddingBottom: 4,
298
+ };
299
+
300
+ const grabHandleStyle: CSSProperties = {
301
+ margin: "0 auto",
302
+ height: 4,
303
+ width: 100,
304
+ borderRadius: 9999,
305
+ background: "rgba(255,255,255,0.05)",
306
+ };
307
+
172
308
  function MobileDrawer({
173
309
  children,
174
310
  onClose,
@@ -193,7 +329,7 @@ function MobileDrawer({
193
329
  <>
194
330
  <motion.div
195
331
  key="drawer-backdrop"
196
- className="fixed inset-0 z-[100] bg-black/60 backdrop-blur-[2px]"
332
+ style={backdropStyle}
197
333
  initial={{ opacity: 0 }}
198
334
  animate={{ opacity: 1 }}
199
335
  exit={{ opacity: 0 }}
@@ -203,7 +339,7 @@ function MobileDrawer({
203
339
 
204
340
  <motion.div
205
341
  key="drawer-sheet"
206
- className="fixed inset-x-0 bottom-0 z-[101] flex flex-col items-center gap-4 rounded-t-xl border-x border-t border-white/10 bg-[#282828] px-6 pb-[max(24px,env(safe-area-inset-bottom))] pt-3"
342
+ style={drawerSheetStyle}
207
343
  initial={{ y: "100%" }}
208
344
  animate={{ y: 0 }}
209
345
  exit={{ y: "100%" }}
@@ -213,15 +349,13 @@ function MobileDrawer({
213
349
  dragElastic={{ top: 0, bottom: 0.4 }}
214
350
  onDragEnd={handleDragEnd}
215
351
  >
216
- <div className="absolute inset-x-0 top-0 bottom-[-100vh] -z-10 bg-[#282828] rounded-t-xl" />
352
+ <div style={drawerBgStyle} />
217
353
 
218
- <div className="w-full cursor-grab pt-0 pb-1 active:cursor-grabbing">
219
- <div className="mx-auto h-1 w-[100px] rounded-full bg-white/5" />
354
+ <div style={grabHandleAreaStyle}>
355
+ <div style={grabHandleStyle} />
220
356
  </div>
221
357
 
222
- <p className="text-base font-bold tracking-tight text-white">
223
- Connect
224
- </p>
358
+ <p style={headingStyle}>Connect</p>
225
359
 
226
360
  {children}
227
361
  </motion.div>
package/src/agent.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { StoredAgent } from "./types";
2
2
 
3
+ export const AGENT_NAME = "hypurr-connect";
4
+
3
5
  const AGENT_STORAGE_PREFIX = "hypurr-connect-agent";
4
6
 
5
7
  function storageKey(masterAddress: string): string {
@@ -41,3 +43,81 @@ export async function generateAgentKey(): Promise<{
41
43
  const signer = new PrivateKeySigner(privateKey);
42
44
  return { privateKey, address: signer.address };
43
45
  }
46
+
47
+ interface ExtraAgent {
48
+ address: string;
49
+ name: string;
50
+ validUntil: number;
51
+ }
52
+
53
+ /**
54
+ * Query the Hyperliquid info API for the named agents registered to a user.
55
+ * Returns the matching entry for AGENT_NAME if it exists and is still valid.
56
+ */
57
+ export async function fetchActiveAgent(
58
+ userAddress: string,
59
+ isTestnet: boolean,
60
+ ): Promise<ExtraAgent | null> {
61
+ const url = isTestnet
62
+ ? "https://api.hyperliquid-testnet.xyz/info"
63
+ : "https://api.hyperliquid.xyz/info";
64
+
65
+ const res = await fetch(url, {
66
+ method: "POST",
67
+ headers: { "Content-Type": "application/json" },
68
+ body: JSON.stringify({ type: "extraAgents", user: userAddress }),
69
+ });
70
+
71
+ if (!res.ok) return null;
72
+
73
+ const agents: unknown = await res.json();
74
+ if (!Array.isArray(agents)) return null;
75
+
76
+ const nowMs = Date.now();
77
+ const match = (agents as ExtraAgent[]).find(
78
+ (a) => a.name === AGENT_NAME && a.validUntil * 1000 > nowMs,
79
+ );
80
+ if (!match) return null;
81
+ return { ...match, validUntil: match.validUntil * 1000 };
82
+ }
83
+
84
+ /**
85
+ * Checks whether a stored agent is still valid: the address must appear in the
86
+ * on-chain `extraAgents` list and not be expired.
87
+ */
88
+ export async function isAgentValid(
89
+ stored: StoredAgent,
90
+ userAddress: string,
91
+ isTestnet: boolean,
92
+ ): Promise<boolean> {
93
+ if (stored.validUntil <= Date.now()) return false;
94
+
95
+ const remote = await fetchActiveAgent(userAddress, isTestnet);
96
+ if (!remote) return false;
97
+
98
+ return (
99
+ remote.address.toLowerCase() === stored.address.toLowerCase() &&
100
+ remote.validUntil > Date.now()
101
+ );
102
+ }
103
+
104
+ const DEAD_AGENT_PATTERNS = [
105
+ /agent address .+ is not valid/i,
106
+ /unknown signer/i,
107
+ /not authorized/i,
108
+ /not an agent/i,
109
+ ];
110
+
111
+ /**
112
+ * Returns true if the error indicates the agent has been pruned, expired,
113
+ * or is otherwise no longer registered on-chain.
114
+ */
115
+ export function isDeadAgentError(err: unknown): boolean {
116
+ const msg =
117
+ err instanceof Error
118
+ ? err.message
119
+ : typeof err === "object" && err !== null && "message" in err
120
+ ? String((err as { message: unknown }).message)
121
+ : String(err);
122
+ return DEAD_AGENT_PATTERNS.some((p) => p.test(msg));
123
+ }
@@ -1,14 +1,16 @@
1
- export function MetaMaskColorIcon({ className }: { className?: string }) {
1
+ import type { CSSProperties } from "react";
2
+
3
+ export function MetaMaskColorIcon({ style }: { style?: CSSProperties }) {
2
4
  return (
3
5
  <svg
4
6
  width="24"
5
7
  height="24"
6
8
  viewBox="0 0 24 24"
7
- className={className}
9
+ style={style}
8
10
  fill="none"
9
11
  xmlns="http://www.w3.org/2000/svg"
10
12
  >
11
- <g clip-path="url(#clip0_2567_1088)">
13
+ <g clipPath="url(#clip0_2567_1088)">
12
14
  <path
13
15
  d="M19.8188 19.418L15.9421 18.2871L13.0186 19.9994L10.9788 19.9985L8.05356 18.2871L4.17862 19.418L3 15.5193L4.17875 11.1924L3 7.5341L4.17875 3L10.2336 6.54437H13.7639L19.8188 3L20.9976 7.5341L19.8188 11.1924L20.9976 15.5193L19.8188 19.418Z"
14
16
  fill="#FF5C16"
@@ -1,10 +1,12 @@
1
- export function TelegramColorIcon({ className }: { className?: string }) {
1
+ import type { CSSProperties } from "react";
2
+
3
+ export function TelegramColorIcon({ style }: { style?: CSSProperties }) {
2
4
  return (
3
5
  <svg
4
6
  width="24"
5
7
  height="24"
6
8
  viewBox="0 0 24 24"
7
- className={className}
9
+ style={style}
8
10
  fill="none"
9
11
  xmlns="http://www.w3.org/2000/svg"
10
12
  >
@@ -13,8 +15,8 @@ export function TelegramColorIcon({ className }: { className?: string }) {
13
15
  fill="url(#paint0_linear_2571_1084)"
14
16
  />
15
17
  <path
16
- fill-rule="evenodd"
17
- clip-rule="evenodd"
18
+ fillRule="evenodd"
19
+ clipRule="evenodd"
18
20
  d="M7.07426 11.905C9.69794 10.7619 11.4475 10.0083 12.3229 9.64417C14.8222 8.60458 15.3416 8.424 15.6801 8.41803C15.7546 8.41672 15.921 8.43517 16.0289 8.52267C16.1199 8.59655 16.145 8.69635 16.1569 8.7664C16.1689 8.83645 16.1839 8.99602 16.172 9.1207C16.0366 10.5438 15.4505 13.9973 15.1523 15.5912C15.0262 16.2657 14.7778 16.4918 14.5373 16.514C14.0146 16.562 13.6178 16.1686 13.1115 15.8367C12.3194 15.3175 11.8719 14.9943 11.103 14.4876C10.2145 13.902 10.7905 13.5802 11.2969 13.0542C11.4294 12.9166 13.7322 10.822 13.7768 10.632C13.7824 10.6082 13.7875 10.5196 13.7349 10.4729C13.6823 10.4261 13.6046 10.4421 13.5486 10.4548C13.4691 10.4728 12.2037 11.3092 9.75232 12.964C9.39313 13.2106 9.06779 13.3308 8.7763 13.3245C8.45496 13.3176 7.83681 13.1428 7.37729 12.9934C6.81366 12.8102 6.3657 12.7134 6.40471 12.4022C6.42503 12.2401 6.64821 12.0744 7.07426 11.905Z"
19
21
  fill="white"
20
22
  />
@@ -27,8 +29,8 @@ export function TelegramColorIcon({ className }: { className?: string }) {
27
29
  y2="1789.65"
28
30
  gradientUnits="userSpaceOnUse"
29
31
  >
30
- <stop stop-color="#2AABEE" />
31
- <stop offset="1" stop-color="#229ED9" />
32
+ <stop stopColor="#2AABEE" />
33
+ <stop offset="1" stopColor="#229ED9" />
32
34
  </linearGradient>
33
35
  </defs>
34
36
  </svg>
package/src/index.ts CHANGED
@@ -16,3 +16,7 @@ export type {
16
16
  StoredAgent,
17
17
  TelegramLoginData,
18
18
  } from "./types";
19
+
20
+ // Re-export wallet types from hypurr-grpc so consumers don't need the dependency
21
+ export type { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
22
+ export type { TelegramChatWalletPack } from "hypurr-grpc/ts/hypurr/user";
package/src/types.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { ExchangeClient } from "@hfunlabs/hyperliquid";
2
2
  import type { StaticClient } from "hypurr-grpc/ts/hypurr/static/static_service.client";
3
3
  import type { TelegramClient } from "hypurr-grpc/ts/hypurr/telegram/telegram_service.client";
4
+ import type { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
5
+ import type { TelegramChatWalletPack } from "hypurr-grpc/ts/hypurr/user";
4
6
 
5
7
  // ─── Config ──────────────────────────────────────────────────────
6
8
 
@@ -36,6 +38,8 @@ export interface HypurrUser {
36
38
  photoUrl?: string;
37
39
  authMethod: AuthMethod;
38
40
  telegramId?: string;
41
+ hfunScore?: number;
42
+ reputationScore?: number;
39
43
  }
40
44
 
41
45
  // ─── Agent (EOA flow) ────────────────────────────────────────────
@@ -44,6 +48,8 @@ export interface StoredAgent {
44
48
  privateKey: `0x${string}`;
45
49
  address: `0x${string}`;
46
50
  approvedAt: number;
51
+ /** Epoch ms from the `extraAgents` response; agent is invalid after this time. */
52
+ validUntil: number;
47
53
  }
48
54
 
49
55
  export type SignTypedDataFn = (params: {
@@ -69,10 +75,33 @@ export interface HypurrConnectState {
69
75
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
76
  exchange: ExchangeClient<any> | null;
71
77
 
72
- // USDC balance from Hyperliquid perps clearinghouse
73
- usdcBalance: string | null;
74
- usdcBalanceLoading: boolean;
75
- refreshBalance: () => void;
78
+ // Multi-wallet (Telegram only EOA has a single wallet)
79
+ wallets: HyperliquidWallet[];
80
+ selectedWalletId: number;
81
+ selectWallet: (walletId: number) => void;
82
+
83
+ // Wallet management (Telegram only)
84
+ createWallet: (name: string) => Promise<HyperliquidWallet>;
85
+ deleteWallet: (walletId: number) => Promise<void>;
86
+ refreshWallets: () => void;
87
+
88
+ // Wallet packs & labels (Telegram only)
89
+ packs: TelegramChatWalletPack[];
90
+ createWalletPack: (name: string) => Promise<number>;
91
+ addPackLabel: (params: {
92
+ walletAddress: string;
93
+ walletLabel: string;
94
+ packId: number;
95
+ }) => Promise<void>;
96
+ modifyPackLabel: (params: {
97
+ walletLabelOld: string;
98
+ walletLabelNew: string;
99
+ packId: number;
100
+ }) => Promise<void>;
101
+ removePackLabel: (params: {
102
+ walletLabel: string;
103
+ packId: number;
104
+ }) => Promise<void>;
76
105
 
77
106
  // Login modal
78
107
  loginModalOpen: boolean;
@@ -80,14 +109,16 @@ export interface HypurrConnectState {
80
109
  closeLoginModal: () => void;
81
110
 
82
111
  // Auth actions
83
- loginTelegram: (data: TelegramLoginData) => void;
84
- loginEoa: (address: `0x${string}`) => void;
112
+ connectEoa: (address: `0x${string}`) => void;
113
+ approveAgent: (
114
+ signTypedDataAsync: SignTypedDataFn,
115
+ chainId: number,
116
+ ) => Promise<void>;
85
117
  logout: () => void;
86
118
 
87
119
  // EOA agent management
88
120
  agent: StoredAgent | null;
89
121
  agentReady: boolean;
90
- approveAgent: (signTypedDataAsync: SignTypedDataFn) => Promise<void>;
91
122
  clearAgent: () => void;
92
123
 
93
124
  // Telegram config