@hfunlabs/hypurr-connect 0.1.1 → 0.1.3

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.
@@ -11,9 +11,10 @@ import {
11
11
  type CSSProperties,
12
12
  type ReactNode,
13
13
  } from "react";
14
- import { useHypurrConnect } from "./HypurrConnectProvider";
14
+ import { useHypurrConnectInternal } from "./HypurrConnectProvider";
15
15
  import { MetaMaskColorIcon } from "./icons/MetaMaskColorIcon";
16
16
  import { TelegramColorIcon } from "./icons/TelegramColorIcon";
17
+ import { TelegramLoginWidget } from "./TelegramLoginWidget";
17
18
  import type { TelegramLoginData } from "./types";
18
19
 
19
20
  export interface LoginModalProps {
@@ -130,8 +131,14 @@ function HoverButton({
130
131
  }
131
132
 
132
133
  export function LoginModal({ onConnectWallet, walletIcon }: LoginModalProps) {
133
- const { loginTelegram, loginModalOpen, closeLoginModal, botId } =
134
- useHypurrConnect();
134
+ const {
135
+ loginTelegram,
136
+ loginModalOpen,
137
+ closeLoginModal,
138
+ botId,
139
+ botUsername,
140
+ useWidget,
141
+ } = useHypurrConnectInternal();
135
142
 
136
143
  const handleTelegramAuth = useCallback(
137
144
  (user: TelegramLoginData) => {
@@ -195,10 +202,17 @@ export function LoginModal({ onConnectWallet, walletIcon }: LoginModalProps) {
195
202
  overflow: "hidden",
196
203
  }}
197
204
  >
198
- <HoverButton onClick={openTelegramOAuth}>
199
- <TelegramColorIcon style={iconSize} />
200
- Telegram
201
- </HoverButton>
205
+ {useWidget && botUsername ? (
206
+ <TelegramLoginWidget
207
+ botUsername={botUsername}
208
+ onAuth={handleTelegramAuth}
209
+ />
210
+ ) : (
211
+ <HoverButton onClick={openTelegramOAuth}>
212
+ <TelegramColorIcon style={iconSize} />
213
+ Telegram
214
+ </HoverButton>
215
+ )}
202
216
  </div>
203
217
 
204
218
  <div style={dividerStyle} />
@@ -0,0 +1,62 @@
1
+ import { useEffect, useRef } from "react";
2
+ import type { TelegramLoginData } from "./types";
3
+
4
+ const WIDGET_SCRIPT_URL = "https://telegram.org/js/telegram-widget.js?22";
5
+ const CALLBACK_NAME = "__hypurrConnectTelegramAuth";
6
+
7
+ export interface TelegramLoginWidgetProps {
8
+ botUsername: string;
9
+ onAuth: (data: TelegramLoginData) => void;
10
+ buttonSize?: "large" | "medium" | "small";
11
+ cornerRadius?: number;
12
+ showUserPhoto?: boolean;
13
+ requestAccess?: boolean;
14
+ }
15
+
16
+ export function TelegramLoginWidget({
17
+ botUsername,
18
+ onAuth,
19
+ buttonSize = "large",
20
+ cornerRadius,
21
+ showUserPhoto = true,
22
+ requestAccess = true,
23
+ }: TelegramLoginWidgetProps) {
24
+ const containerRef = useRef<HTMLDivElement>(null);
25
+ const onAuthRef = useRef(onAuth);
26
+ onAuthRef.current = onAuth;
27
+
28
+ useEffect(() => {
29
+ const container = containerRef.current;
30
+ if (!container) return;
31
+
32
+ (window as unknown as Record<string, unknown>)[CALLBACK_NAME] = (
33
+ user: TelegramLoginData,
34
+ ) => {
35
+ onAuthRef.current(user);
36
+ };
37
+
38
+ const script = document.createElement("script");
39
+ script.src = WIDGET_SCRIPT_URL;
40
+ script.async = true;
41
+ script.setAttribute("data-telegram-login", botUsername);
42
+ script.setAttribute("data-size", buttonSize);
43
+ script.setAttribute("data-onauth", `${CALLBACK_NAME}(user)`);
44
+ script.setAttribute("data-userpic", String(showUserPhoto));
45
+ if (requestAccess) {
46
+ script.setAttribute("data-request-access", "write");
47
+ }
48
+ if (cornerRadius !== undefined) {
49
+ script.setAttribute("data-radius", String(cornerRadius));
50
+ }
51
+
52
+ container.innerHTML = "";
53
+ container.appendChild(script);
54
+
55
+ return () => {
56
+ container.innerHTML = "";
57
+ delete (window as unknown as Record<string, unknown>)[CALLBACK_NAME];
58
+ };
59
+ }, [botUsername, buttonSize, cornerRadius, showUserPhoto, requestAccess]);
60
+
61
+ return <div ref={containerRef} />;
62
+ }
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
+ }
package/src/index.ts CHANGED
@@ -4,11 +4,15 @@ export {
4
4
  } from "./HypurrConnectProvider";
5
5
  export { LoginModal } from "./LoginModal";
6
6
  export type { LoginModalProps } from "./LoginModal";
7
+ export { TelegramLoginWidget } from "./TelegramLoginWidget";
8
+ export type { TelegramLoginWidgetProps } from "./TelegramLoginWidget";
7
9
  export { GrpcExchangeTransport } from "./GrpcExchangeTransport";
8
10
  export type { GrpcExchangeTransportConfig } from "./GrpcExchangeTransport";
9
11
  export { createTelegramClient, createStaticClient } from "./grpc";
12
+ export { createEoaSigner } from "./types";
10
13
  export type {
11
14
  AuthMethod,
15
+ EoaSigner,
12
16
  HypurrConnectConfig,
13
17
  HypurrConnectState,
14
18
  HypurrUser,
@@ -16,3 +20,7 @@ export type {
16
20
  StoredAgent,
17
21
  TelegramLoginData,
18
22
  } from "./types";
23
+
24
+ // Re-export wallet types from hypurr-grpc so consumers don't need the dependency
25
+ export type { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
26
+ export type { TelegramChatWalletPack } from "hypurr-grpc/ts/hypurr/user";
package/src/types.ts CHANGED
@@ -1,15 +1,18 @@
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 { TelegramChatWalletPack } from "hypurr-grpc/ts/hypurr/user";
5
+ import type { HyperliquidWallet } from "hypurr-grpc/ts/hypurr/wallet";
4
6
 
5
7
  // ─── Config ──────────────────────────────────────────────────────
6
8
 
7
9
  export interface HypurrConnectConfig {
8
10
  grpcTimeout?: number;
9
11
  isTestnet?: boolean;
10
- telegram?: {
12
+ telegram: {
11
13
  botUsername: string;
12
- botId: string;
14
+ botId?: string;
15
+ useWidget: boolean;
13
16
  };
14
17
  }
15
18
 
@@ -36,6 +39,8 @@ export interface HypurrUser {
36
39
  photoUrl?: string;
37
40
  authMethod: AuthMethod;
38
41
  telegramId?: string;
42
+ hfunScore?: number;
43
+ reputationScore?: number;
39
44
  }
40
45
 
41
46
  // ─── Agent (EOA flow) ────────────────────────────────────────────
@@ -44,6 +49,8 @@ export interface StoredAgent {
44
49
  privateKey: `0x${string}`;
45
50
  address: `0x${string}`;
46
51
  approvedAt: number;
52
+ /** Epoch ms from the `extraAgents` response; agent is invalid after this time. */
53
+ validUntil: number;
47
54
  }
48
55
 
49
56
  export type SignTypedDataFn = (params: {
@@ -53,6 +60,51 @@ export type SignTypedDataFn = (params: {
53
60
  message: Record<string, unknown>;
54
61
  }) => Promise<`0x${string}`>;
55
62
 
63
+ /** Wallet signer provided at EOA connect time for user-signed actions. */
64
+ export interface EoaSigner {
65
+ signTypedData: SignTypedDataFn;
66
+ chainId: number;
67
+ }
68
+
69
+ /**
70
+ * Create an {@link EoaSigner} from any EIP-712 signing function.
71
+ *
72
+ * Accepts either a direct function or a `{ current }` ref object so the
73
+ * signer always calls through to the latest function (avoids stale closures
74
+ * with React hooks like wagmi's `useSignTypedData`).
75
+ *
76
+ * @example wagmi v2 — ref pattern (recommended)
77
+ * ```ts
78
+ * const { signTypedDataAsync } = useSignTypedData();
79
+ * const chainId = useChainId();
80
+ * const signerRef = useRef(signTypedDataAsync);
81
+ * signerRef.current = signTypedDataAsync; // stays fresh every render
82
+ *
83
+ * // call once — the ref keeps it up to date
84
+ * connectEoa(address, createEoaSigner(signerRef, chainId));
85
+ * ```
86
+ *
87
+ * @example direct function (e.g. from viem WalletClient)
88
+ * ```ts
89
+ * connectEoa(address, createEoaSigner(client.signTypedData, chainId));
90
+ * ```
91
+ */
92
+ export function createEoaSigner(
93
+ signTypedDataAsync:
94
+ | ((args: Record<string, unknown>) => Promise<`0x${string}`>)
95
+ | { current: (args: Record<string, unknown>) => Promise<`0x${string}`> },
96
+ chainId: number,
97
+ ): EoaSigner {
98
+ const resolve =
99
+ typeof signTypedDataAsync === "function"
100
+ ? signTypedDataAsync
101
+ : (args: Record<string, unknown>) => signTypedDataAsync.current(args);
102
+ return {
103
+ signTypedData: (params) => resolve(params),
104
+ chainId,
105
+ };
106
+ }
107
+
56
108
  // ─── Context state ───────────────────────────────────────────────
57
109
 
58
110
  export interface HypurrConnectState {
@@ -63,16 +115,40 @@ export interface HypurrConnectState {
63
115
  error: string | null;
64
116
  authMethod: AuthMethod;
65
117
 
66
- // SDK ExchangeClient — works for all L1 actions
118
+ // SDK ExchangeClient — handles both L1 (agent-signed) and user-signed actions.
67
119
  // Telegram: backed by GrpcExchangeTransport (HyperliquidCoreAction)
68
- // EOA: backed by HttpTransport + agent wallet
120
+ // EOA: uses a dual wallet agent key for L1 actions, master wallet for
121
+ // user-signed actions (transfers, withdrawals, etc.) when a signer is provided.
69
122
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
123
  exchange: ExchangeClient<any> | null;
71
124
 
72
- // USDC balance from Hyperliquid perps clearinghouse
73
- usdcBalance: string | null;
74
- usdcBalanceLoading: boolean;
75
- refreshBalance: () => void;
125
+ // Multi-wallet (Telegram only EOA has a single wallet)
126
+ wallets: HyperliquidWallet[];
127
+ selectedWalletId: number;
128
+ selectWallet: (walletId: number) => void;
129
+
130
+ // Wallet management (Telegram only)
131
+ createWallet: (name: string) => Promise<HyperliquidWallet>;
132
+ deleteWallet: (walletId: number) => Promise<void>;
133
+ refreshWallets: () => void;
134
+
135
+ // Wallet packs & labels (Telegram only)
136
+ packs: TelegramChatWalletPack[];
137
+ createWalletPack: (name: string) => Promise<number>;
138
+ addPackLabel: (params: {
139
+ walletAddress: string;
140
+ walletLabel: string;
141
+ packId: number;
142
+ }) => Promise<void>;
143
+ modifyPackLabel: (params: {
144
+ walletLabelOld: string;
145
+ walletLabelNew: string;
146
+ packId: number;
147
+ }) => Promise<void>;
148
+ removePackLabel: (params: {
149
+ walletLabel: string;
150
+ packId: number;
151
+ }) => Promise<void>;
76
152
 
77
153
  // Login modal
78
154
  loginModalOpen: boolean;
@@ -80,14 +156,16 @@ export interface HypurrConnectState {
80
156
  closeLoginModal: () => void;
81
157
 
82
158
  // Auth actions
83
- loginTelegram: (data: TelegramLoginData) => void;
84
- loginEoa: (address: `0x${string}`) => void;
159
+ connectEoa: (address: `0x${string}`, signer?: EoaSigner) => void;
160
+ approveAgent: (
161
+ signTypedDataAsync: SignTypedDataFn,
162
+ chainId: number,
163
+ ) => Promise<void>;
85
164
  logout: () => void;
86
165
 
87
166
  // EOA agent management
88
167
  agent: StoredAgent | null;
89
168
  agentReady: boolean;
90
- approveAgent: (signTypedDataAsync: SignTypedDataFn) => Promise<void>;
91
169
  clearAgent: () => void;
92
170
 
93
171
  // Telegram config