@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.
package/README.md CHANGED
@@ -6,11 +6,11 @@ React authentication and wallet connectivity library for the [Hyperliquid](https
6
6
 
7
7
  - **Dual auth flows** — Telegram OAuth (server-side signing) and EOA wallet (client-side agent key signing)
8
8
  - **Unified exchange client** — Same `ExchangeClient` interface regardless of auth method
9
+ - **Multi-wallet management** — Switch between wallets, create/delete wallets, manage wallet packs and labels (Telegram users)
9
10
  - **gRPC transport** — Custom transport that routes exchange actions through the Hypurr backend for Telegram users
10
- - **Agent key management** — Automatic generation, on-chain approval, and localStorage persistence of agent keys for EOA wallets
11
+ - **Agent key management** — Named agent keys (`"hypurr-connect"`) with on-chain approval, `extraAgents` validation, expiry-aware caching, and automatic dead-agent recovery
11
12
  - **Session persistence** — Auth state survives page reloads via localStorage
12
13
  - **Responsive login modal** — Centered modal on desktop, bottom drawer on mobile with framer-motion animations
13
- - **USDC balance tracking** — Built-in clearinghouse balance fetching with manual refresh
14
14
 
15
15
  ## Installation
16
16
 
@@ -64,7 +64,7 @@ function App() {
64
64
  import { useHypurrConnect } from "@hfunlabs/hypurr-connect";
65
65
 
66
66
  function TradingPanel() {
67
- const { user, isLoggedIn, exchange, usdcBalance, openLoginModal, logout } =
67
+ const { user, isLoggedIn, exchange, openLoginModal, logout } =
68
68
  useHypurrConnect();
69
69
 
70
70
  if (!isLoggedIn) {
@@ -74,7 +74,6 @@ function TradingPanel() {
74
74
  return (
75
75
  <div>
76
76
  <p>Welcome, {user.displayName}</p>
77
- <p>Balance: {usdcBalance} USDC</p>
78
77
  <button onClick={logout}>Disconnect</button>
79
78
  </div>
80
79
  );
@@ -138,22 +137,42 @@ This package depends on [`hypurr-grpc`](https://gitlab.com/hypurr/hypurr-grpc) (
138
137
 
139
138
  ### EOA Wallet Login
140
139
 
140
+ The EOA flow is split into two steps — **connect** and **approve** — so that the synchronous wallet connection never blocks on the signing prompt:
141
+
141
142
  1. User clicks "Wallet" in the `LoginModal`; the `onConnectWallet` callback fires
142
- 2. Your app connects the wallet (e.g., via wagmi) and calls `loginEoa(address)`
143
- 3. The user must approve an agent key before trading:
144
- - Call `approveAgent(signTypedDataAsync)` with the wallet's signing function
145
- - A random agent key pair is generated locally
146
- - The wallet signs an EIP-712 `ApproveAgent` message
147
- - The approval is submitted to Hyperliquid
148
- - The agent key is persisted in localStorage (`hypurr-connect-agent:{address}`)
149
- 4. An `ExchangeClient` is created with `HttpTransport` + `PrivateKeySigner` — all exchange actions are signed **client-side** with the agent key
143
+ 2. Your app connects the wallet (e.g., via wagmi) and calls `connectEoa(address)` — this is **synchronous**, sets the user immediately, and restores a cached agent from localStorage if one is still valid
144
+ 3. When you need exchange functionality, call `approveAgent(signTypedDataAsync, chainId)`:
145
+ - If a valid cached agent exists, it is validated against Hyperliquid's `extraAgents` endpoint and its `validUntil` timestamp — reused only if still active
146
+ - If no valid agent exists, a fresh key pair is generated and approved on-chain as a **named agent** (`"hypurr-connect"`) via the SDK's `approveAgent`. Named agents are only replaced when another `ApproveAgent` is sent with the same name, so they won't be pruned by unrelated unnamed agent registrations
147
+ - The agent key and its `validUntil` are persisted in localStorage (`hypurr-connect-agent:{address}`)
148
+ 4. Check `agentReady` to know whether the exchange client can sign transactions. If `agentReady` is `false` and you call any method on `exchange`, the SDK throws an explicit error:
149
+ > `[HypurrConnect] No agent key approved. Call approveAgent(signTypedDataAsync) before using the exchange client.`
150
+ 5. Once the agent is approved, the `ExchangeClient` uses `HttpTransport` + `PrivateKeySigner` — all exchange actions are signed **client-side** with the agent key
151
+ 6. If an exchange action fails because the agent was pruned or expired, the SDK automatically clears the dead agent and surfaces the error via `error` so the consumer can prompt re-login
152
+
153
+ ```tsx
154
+ // Step 1: Connect wallet (synchronous — safe to call from useEffect)
155
+ const { connectEoa, approveAgent, agentReady, exchange } = useHypurrConnect();
156
+
157
+ // Call when wallet connects (e.g., wagmi onConnect callback)
158
+ connectEoa(address);
159
+
160
+ // Step 2: Approve agent (async — triggers wallet signing prompt)
161
+ // Pass the connected wallet's chain ID so the EIP-712 domain matches
162
+ await approveAgent(signTypedDataAsync, chainId);
163
+
164
+ // Now exchange is ready to use
165
+ if (agentReady && exchange) {
166
+ await exchange.placeOrder(/* ... */);
167
+ }
168
+ ```
150
169
 
151
170
  ### Using the Exchange Client
152
171
 
153
- Once authenticated (and agent approved for EOA), the `exchange` object from `useHypurrConnect()` is a fully functional `ExchangeClient` from `@hfunlabs/hyperliquid`. Use it for any L1 action:
172
+ Once authenticated, the `exchange` object from `useHypurrConnect()` is a fully functional `ExchangeClient` from `@hfunlabs/hyperliquid`. Use it for any L1 action:
154
173
 
155
174
  ```tsx
156
- const { exchange, agentReady } = useHypurrConnect();
175
+ const { exchange } = useHypurrConnect();
157
176
 
158
177
  // Place an order (works identically for Telegram and EOA users)
159
178
  if (exchange) {
@@ -167,6 +186,74 @@ if (exchange) {
167
186
  }
168
187
  ```
169
188
 
189
+ ### Multi-Wallet Management (Telegram)
190
+
191
+ Telegram users can have multiple wallets. The library exposes the full wallet list and lets you switch the active wallet — the `exchange` client and `user` automatically update.
192
+
193
+ ```tsx
194
+ const {
195
+ wallets,
196
+ selectedWalletId,
197
+ selectWallet,
198
+ createWallet,
199
+ deleteWallet,
200
+ refreshWallets,
201
+ } = useHypurrConnect();
202
+
203
+ // List wallets
204
+ wallets.map((w) => (
205
+ <button
206
+ key={w.id}
207
+ onClick={() => selectWallet(w.id)}
208
+ style={{ fontWeight: w.id === selectedWalletId ? "bold" : "normal" }}
209
+ >
210
+ {w.name || w.ethereumAddress}
211
+ </button>
212
+ ));
213
+
214
+ // Create a new wallet
215
+ const newWallet = await createWallet("Trading");
216
+
217
+ // Delete a wallet (auto-selects another if the deleted one was active)
218
+ await deleteWallet(walletId);
219
+ ```
220
+
221
+ #### Wallet Packs & Labels
222
+
223
+ Organize watched wallets into named packs with labels:
224
+
225
+ ```tsx
226
+ const {
227
+ packs,
228
+ createWalletPack,
229
+ addPackLabel,
230
+ modifyPackLabel,
231
+ removePackLabel,
232
+ } = useHypurrConnect();
233
+
234
+ // Create a pack
235
+ const packId = await createWalletPack("Whales");
236
+
237
+ // Add a labeled wallet to the pack
238
+ await addPackLabel({
239
+ walletAddress: "0x...",
240
+ walletLabel: "Big trader",
241
+ packId,
242
+ });
243
+
244
+ // Rename a label
245
+ await modifyPackLabel({
246
+ walletLabelOld: "Big trader",
247
+ walletLabelNew: "Whale #1",
248
+ packId,
249
+ });
250
+
251
+ // Remove a label from the pack
252
+ await removePackLabel({ walletLabel: "Whale #1", packId });
253
+ ```
254
+
255
+ All wallet management functions automatically refresh the wallet list from the server after mutations.
256
+
170
257
  ## API Reference
171
258
 
172
259
  ### Components
@@ -179,7 +266,7 @@ if (exchange) {
179
266
  </HypurrConnectProvider>
180
267
  ```
181
268
 
182
- Context provider that manages all auth state, gRPC clients, exchange clients, and balance tracking. Must wrap any component that uses `useHypurrConnect`.
269
+ Context provider that manages all auth state, gRPC clients, and exchange clients. Must wrap any component that uses `useHypurrConnect`.
183
270
 
184
271
  #### `LoginModal`
185
272
 
@@ -200,31 +287,38 @@ Returns the full auth and exchange state. Throws if used outside `HypurrConnectP
200
287
 
201
288
  ### `HypurrConnectState`
202
289
 
203
- | Property | Type | Description |
204
- | -------------------- | -------------------------------------------------------- | ---------------------------------------------- |
205
- | `user` | `HypurrUser \| null` | Current authenticated user |
206
- | `isLoggedIn` | `boolean` | Whether a user is authenticated |
207
- | `isLoading` | `boolean` | Whether auth is in progress |
208
- | `error` | `string \| null` | Last auth error message |
209
- | `authMethod` | `AuthMethod` | `"telegram"`, `"eoa"`, or `null` |
210
- | `exchange` | `ExchangeClient \| null` | Hyperliquid exchange client for L1 actions |
211
- | `usdcBalance` | `string \| null` | User's USDC balance from the clearinghouse |
212
- | `usdcBalanceLoading` | `boolean` | Whether balance is being fetched |
213
- | `refreshBalance` | `() => void` | Manually re-fetch USDC balance |
214
- | `loginModalOpen` | `boolean` | Whether the login modal is visible |
215
- | `openLoginModal` | `() => void` | Show the login modal |
216
- | `closeLoginModal` | `() => void` | Hide the login modal |
217
- | `loginTelegram` | `(data: TelegramLoginData) => void` | Authenticate with Telegram OAuth data |
218
- | `loginEoa` | `(address: \`0x\${string}\`) => void` | Authenticate with an EOA wallet address |
219
- | `logout` | `() => void` | Clear all auth state and localStorage |
220
- | `agent` | `StoredAgent \| null` | Current agent key (EOA flow only) |
221
- | `agentReady` | `boolean` | Whether the agent key is approved and ready |
222
- | `approveAgent` | `(signTypedDataAsync: SignTypedDataFn) => Promise<void>` | Generate and approve a new agent key |
223
- | `clearAgent` | `() => void` | Remove the agent key from state and storage |
224
- | `botId` | `string` | Telegram bot ID from config |
225
- | `authDataMap` | `Record<string, string>` | Raw Telegram auth data as key-value pairs |
226
- | `telegramClient` | `TelegramClient` | Low-level gRPC client for the Telegram service |
227
- | `staticClient` | `StaticClient` | Low-level gRPC client for the Static service |
290
+ | Property | Type | Description |
291
+ | ------------------ | ------------------------------------------------------------------------- | ---------------------------------------------------------------- |
292
+ | `user` | `HypurrUser \| null` | Current authenticated user (reflects selected wallet) |
293
+ | `isLoggedIn` | `boolean` | Whether a user is authenticated |
294
+ | `isLoading` | `boolean` | Whether auth is in progress |
295
+ | `error` | `string \| null` | Last auth or dead-agent error message |
296
+ | `authMethod` | `AuthMethod` | `"telegram"`, `"eoa"`, or `null` |
297
+ | `exchange` | `ExchangeClient \| null` | Hyperliquid exchange client for L1 actions |
298
+ | `wallets` | `HyperliquidWallet[]` | All wallets for the Telegram user (empty for EOA) |
299
+ | `selectedWalletId` | `number` | ID of the currently active wallet |
300
+ | `selectWallet` | `(walletId: number) => void` | Switch the active wallet |
301
+ | `createWallet` | `(name: string) => Promise<HyperliquidWallet>` | Create a new wallet (Telegram only) |
302
+ | `deleteWallet` | `(walletId: number) => Promise<void>` | Delete a wallet (Telegram only) |
303
+ | `refreshWallets` | `() => void` | Re-fetch wallets and packs from the server |
304
+ | `packs` | `TelegramChatWalletPack[]` | Wallet packs for the Telegram user |
305
+ | `createWalletPack` | `(name: string) => Promise<number>` | Create a wallet pack; returns the new pack ID |
306
+ | `addPackLabel` | `(params) => Promise<void>` | Add a labeled wallet to a pack |
307
+ | `modifyPackLabel` | `(params) => Promise<void>` | Rename a label within a pack |
308
+ | `removePackLabel` | `(params) => Promise<void>` | Remove a label from a pack |
309
+ | `loginModalOpen` | `boolean` | Whether the login modal is visible |
310
+ | `openLoginModal` | `() => void` | Show the login modal |
311
+ | `closeLoginModal` | `() => void` | Hide the login modal |
312
+ | `connectEoa` | `(address: \`0x\${string}\`) => void` | Connect EOA wallet (sync, no signing) |
313
+ | `approveAgent` | `(signTypedDataAsync: SignTypedDataFn, chainId: number) => Promise<void>` | Approve a named agent key (async, triggers wallet prompt) |
314
+ | `logout` | `() => void` | Clear all auth state and localStorage |
315
+ | `agent` | `StoredAgent \| null` | Current agent key (EOA flow only) |
316
+ | `agentReady` | `boolean` | Whether the exchange client can sign (true for TG, or EOA+agent) |
317
+ | `clearAgent` | `() => void` | Remove the agent key from state and storage |
318
+ | `botId` | `string` | Telegram bot ID from config |
319
+ | `authDataMap` | `Record<string, string>` | Raw Telegram auth data as key-value pairs |
320
+ | `telegramClient` | `TelegramClient` | Low-level gRPC client for the Telegram service |
321
+ | `staticClient` | `StaticClient` | Low-level gRPC client for the Static service |
228
322
 
229
323
  ### Types
230
324
 
@@ -268,6 +362,34 @@ interface StoredAgent {
268
362
  privateKey: `0x${string}`;
269
363
  address: `0x${string}`;
270
364
  approvedAt: number; // Timestamp when approved on-chain
365
+ validUntil: number; // Epoch ms from extraAgents; agent is invalid after this
366
+ }
367
+ ```
368
+
369
+ #### `HyperliquidWallet`
370
+
371
+ Re-exported from `hypurr-grpc`:
372
+
373
+ ```typescript
374
+ interface HyperliquidWallet {
375
+ id: number;
376
+ name: string;
377
+ ethereumAddress: string;
378
+ isAgent: boolean;
379
+ isReadOnly: boolean;
380
+ // ... plus balances, movements, sessions
381
+ }
382
+ ```
383
+
384
+ #### `TelegramChatWalletPack`
385
+
386
+ Re-exported from `hypurr-grpc`:
387
+
388
+ ```typescript
389
+ interface TelegramChatWalletPack {
390
+ id: number;
391
+ telegramChatId: number;
392
+ name: string;
271
393
  }
272
394
  ```
273
395
 
package/dist/index.d.ts CHANGED
@@ -3,6 +3,10 @@ import { ReactNode } from 'react';
3
3
  import { ExchangeClient, IRequestTransport } from '@hfunlabs/hyperliquid';
4
4
  import { StaticClient } from 'hypurr-grpc/ts/hypurr/static/static_service.client';
5
5
  import { TelegramClient } from 'hypurr-grpc/ts/hypurr/telegram/telegram_service.client';
6
+ import { HyperliquidWallet } from 'hypurr-grpc/ts/hypurr/wallet';
7
+ export { HyperliquidWallet } from 'hypurr-grpc/ts/hypurr/wallet';
8
+ import { TelegramChatWalletPack } from 'hypurr-grpc/ts/hypurr/user';
9
+ export { TelegramChatWalletPack } from 'hypurr-grpc/ts/hypurr/user';
6
10
 
7
11
  interface HypurrConnectConfig {
8
12
  grpcTimeout?: number;
@@ -29,11 +33,15 @@ interface HypurrUser {
29
33
  photoUrl?: string;
30
34
  authMethod: AuthMethod;
31
35
  telegramId?: string;
36
+ hfunScore?: number;
37
+ reputationScore?: number;
32
38
  }
33
39
  interface StoredAgent {
34
40
  privateKey: `0x${string}`;
35
41
  address: `0x${string}`;
36
42
  approvedAt: number;
43
+ /** Epoch ms from the `extraAgents` response; agent is invalid after this time. */
44
+ validUntil: number;
37
45
  }
38
46
  type SignTypedDataFn = (params: {
39
47
  domain: Record<string, unknown>;
@@ -51,18 +59,36 @@ interface HypurrConnectState {
51
59
  error: string | null;
52
60
  authMethod: AuthMethod;
53
61
  exchange: ExchangeClient<any> | null;
54
- usdcBalance: string | null;
55
- usdcBalanceLoading: boolean;
56
- refreshBalance: () => void;
62
+ wallets: HyperliquidWallet[];
63
+ selectedWalletId: number;
64
+ selectWallet: (walletId: number) => void;
65
+ createWallet: (name: string) => Promise<HyperliquidWallet>;
66
+ deleteWallet: (walletId: number) => Promise<void>;
67
+ refreshWallets: () => void;
68
+ packs: TelegramChatWalletPack[];
69
+ createWalletPack: (name: string) => Promise<number>;
70
+ addPackLabel: (params: {
71
+ walletAddress: string;
72
+ walletLabel: string;
73
+ packId: number;
74
+ }) => Promise<void>;
75
+ modifyPackLabel: (params: {
76
+ walletLabelOld: string;
77
+ walletLabelNew: string;
78
+ packId: number;
79
+ }) => Promise<void>;
80
+ removePackLabel: (params: {
81
+ walletLabel: string;
82
+ packId: number;
83
+ }) => Promise<void>;
57
84
  loginModalOpen: boolean;
58
85
  openLoginModal: () => void;
59
86
  closeLoginModal: () => void;
60
- loginTelegram: (data: TelegramLoginData) => void;
61
- loginEoa: (address: `0x${string}`) => void;
87
+ connectEoa: (address: `0x${string}`) => void;
88
+ approveAgent: (signTypedDataAsync: SignTypedDataFn, chainId: number) => Promise<void>;
62
89
  logout: () => void;
63
90
  agent: StoredAgent | null;
64
91
  agentReady: boolean;
65
- approveAgent: (signTypedDataAsync: SignTypedDataFn) => Promise<void>;
66
92
  clearAgent: () => void;
67
93
  botId: string;
68
94
  authDataMap: Record<string, string>;