@hfunlabs/hypurr-connect 0.1.2 → 0.1.4

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
@@ -1,11 +1,13 @@
1
1
  # @hfunlabs/hypurr-connect
2
2
 
3
- React authentication and wallet connectivity library for the [Hyperliquid](https://hyperliquid.xyz) decentralized exchange via the [Hypurr](https://hypurr.fun) gRPC backend. Provides two authentication paths — **Telegram OAuth** and **EOA wallet** (MetaMask / browser wallet) — with a unified `ExchangeClient` API for placing orders, managing positions, and executing any L1 action.
3
+ React authentication and wallet connectivity library for the [Hyperliquid](https://hyperliquid.xyz) decentralized exchange via the [Hypurr](https://hypurr.fun) gRPC backend. Provides two authentication paths — **Telegram OAuth** and **EOA wallet** (MetaMask / browser wallet) — with a unified `ExchangeClient` API for placing orders, managing positions, and executing both L1 and user-signed actions.
4
4
 
5
5
  ## Features
6
6
 
7
- - **Dual auth flows** — Telegram OAuth (server-side signing) and EOA wallet (client-side agent key signing)
8
- - **Unified exchange client** — Same `ExchangeClient` interface regardless of auth method
7
+ - **Dual auth flows** — Telegram OAuth (server-side signing) and EOA wallet (client-side signing)
8
+ - **Unified exchange client** — Same `ExchangeClient` interface regardless of auth method; handles both L1 actions (orders, cancels, leverage) and user-signed actions (transfers, withdrawals, staking)
9
+ - **Dual wallet routing** — EOA exchange client automatically routes L1 actions through the agent key (silent) and user-signed actions through the master wallet (wallet popup)
10
+ - **Auto agent provisioning** — Agent keys are created on-the-fly the first time an L1 action is executed; no manual `approveAgent` step required when a signer is provided
9
11
  - **Multi-wallet management** — Switch between wallets, create/delete wallets, manage wallet packs and labels (Telegram users)
10
12
  - **gRPC transport** — Custom transport that routes exchange actions through the Hypurr backend for Telegram users
11
13
  - **Agent key management** — Named agent keys (`"hypurr-connect"`) with on-chain approval, `extraAgents` validation, expiry-aware caching, and automatic dead-agent recovery
@@ -46,6 +48,7 @@ const config = {
46
48
  telegram: {
47
49
  botUsername: "YourBot",
48
50
  botId: "123456789",
51
+ useWidget: true,
49
52
  },
50
53
  };
51
54
 
@@ -111,14 +114,17 @@ function AppShell() {
111
114
  interface HypurrConnectConfig {
112
115
  grpcTimeout?: number; // Request timeout in ms (default: 15000)
113
116
  isTestnet?: boolean; // Use testnet endpoints (default: false)
114
- telegram?: {
115
- botUsername: string; // Telegram bot username for the login widget
116
- botId: string; // Telegram bot ID for the OAuth URL
117
+ telegram: {
118
+ botUsername: string; // Telegram bot username (required for the widget)
119
+ botId?: string; // Telegram bot ID (required for the popup OAuth flow)
120
+ useWidget: boolean; // true = inline Telegram Login Widget, false = popup OAuth
117
121
  };
118
122
  }
119
123
  ```
120
124
 
121
- The `telegram` block is only required if you want to support Telegram login.
125
+ When `useWidget` is `true`, the login modal renders Telegram's official [Login Widget](https://core.telegram.org/widgets/login) inline — no popup window is opened. This avoids popup-blocker issues and shows users the familiar Telegram button directly inside the modal. The widget requires `botUsername` and that your domain is linked to the bot via `/setdomain` in [@BotFather](https://t.me/botfather).
126
+
127
+ When `useWidget` is `false` (or omitted), the popup OAuth flow is used, which requires `botId`.
122
128
 
123
129
  ### Dependencies
124
130
 
@@ -128,64 +134,115 @@ This package depends on [`hypurr-grpc`](https://gitlab.com/hypurr/hypurr-grpc) (
128
134
 
129
135
  ### Telegram Login
130
136
 
131
- 1. User clicks "Telegram" in the `LoginModal`
132
- 2. A popup opens to `oauth.telegram.org` with the configured bot
133
- 3. User authorizes; Telegram posts auth data back via `postMessage`
137
+ Two modes are available, controlled by `config.telegram.useWidget`:
138
+
139
+ **Widget mode** (`useWidget: true`) recommended:
140
+
141
+ 1. The `LoginModal` renders Telegram's official [Login Widget](https://core.telegram.org/widgets/login) inline
142
+ 2. User clicks the widget button and authorizes with Telegram
143
+ 3. The widget calls the `onAuth` callback with the user's auth data
134
144
  4. The provider calls the Hypurr gRPC backend (`telegramUser`) to fetch the user's wallet address and ID
135
145
  5. An `ExchangeClient` is created with `GrpcExchangeTransport` — all exchange actions are signed **server-side** by the Hypurr backend
136
146
  6. Session is persisted in localStorage (`hypurr-connect-tg-user`)
137
147
 
148
+ **Popup mode** (`useWidget: false`):
149
+
150
+ 1. User clicks "Telegram" in the `LoginModal`
151
+ 2. A popup opens to `oauth.telegram.org` with the configured bot
152
+ 3. User authorizes; Telegram posts auth data back via `postMessage`
153
+ 4. Steps 4–6 are identical to widget mode
154
+
138
155
  ### EOA Wallet Login
139
156
 
140
- The EOA flow is split into two steps **connect** and **approve**so that the synchronous wallet connection never blocks on the signing prompt:
157
+ When a signer is provided via `connectEoa`, the exchange client works immediatelyno manual agent approval step is needed:
141
158
 
142
159
  1. User clicks "Wallet" in the `LoginModal`; the `onConnectWallet` callback fires
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
160
+ 2. Your app connects the wallet (e.g., via wagmi) and calls `connectEoa(address, signer)` with an `EoaSigner` — this sets the user immediately and restores a cached agent from localStorage if one is still valid
161
+ 3. The `exchange` client is ready to use right away:
162
+ - **User-signed actions** (transfers, withdrawals, staking) are routed directly to the master wallet the wallet popup appears for the user to approve
163
+ - **L1 actions** (orders, cancels, leverage) use the agent key. If no agent exists yet, one is **auto-provisioned** on the first L1 call this triggers a single wallet popup for the `approveAgent` signature, then the original action proceeds
164
+ - Named agent keys (`"hypurr-connect"`) are cached in localStorage with their `validUntil` timestamp and revalidated against Hyperliquid's `extraAgents` endpoint
165
+ 4. 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`
166
+
167
+ Use the `createEoaSigner` helper to adapt wagmi's `signTypedDataAsync` to the `EoaSigner` interface. Pass a ref to avoid stale closure issues with React hooks:
152
168
 
153
169
  ```tsx
154
- // Step 1: Connect wallet (synchronous — safe to call from useEffect)
155
- const { connectEoa, approveAgent, agentReady, exchange } = useHypurrConnect();
170
+ import { useHypurrConnect, createEoaSigner } from "@hfunlabs/hypurr-connect";
171
+ import { useSignTypedData, useChainId, useAccountEffect } from "wagmi";
172
+
173
+ function ConnectWallet() {
174
+ const { connectEoa, logout, exchange } = useHypurrConnect();
175
+ const { signTypedDataAsync } = useSignTypedData();
176
+ const chainId = useChainId();
177
+
178
+ // Keep a ref so the signer always calls the latest signTypedDataAsync
179
+ const signerRef = useRef(signTypedDataAsync);
180
+ signerRef.current = signTypedDataAsync;
181
+
182
+ useAccountEffect({
183
+ onConnect({ address }) {
184
+ connectEoa(address, createEoaSigner(signerRef, chainId));
185
+ },
186
+ onDisconnect() {
187
+ logout();
188
+ },
189
+ });
156
190
 
157
- // Call when wallet connects (e.g., wagmi onConnect callback)
158
- connectEoa(address);
191
+ // exchange is ready L1 and user-signed actions both work
192
+ if (exchange) {
193
+ // L1 action — agent signs silently (auto-provisioned if needed)
194
+ await exchange.order({ orders: [/* ... */], grouping: "na" });
159
195
 
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(/* ... */);
196
+ // User-signed action — wallet popup appears
197
+ await exchange.usdSend({ destination: "0x...", amount: "100" });
198
+ }
167
199
  }
168
200
  ```
169
201
 
202
+ If you prefer explicit control over agent approval (or don't need user-signed actions), you can omit the signer and call `approveAgent` manually:
203
+
204
+ ```tsx
205
+ connectEoa(address); // no signer — only L1 actions after manual approval
206
+ await approveAgent(signTypedDataAsync, chainId);
207
+ ```
208
+
170
209
  ### Using the Exchange Client
171
210
 
172
- Once authenticated, the `exchange` object from `useHypurrConnect()` is a fully functional `ExchangeClient` from `@hfunlabs/hyperliquid`. Use it for any L1 action:
211
+ Once authenticated, the `exchange` object from `useHypurrConnect()` is a fully functional `ExchangeClient` from `@hfunlabs/hyperliquid`. For EOA users with a signer, it handles **both** L1 and user-signed actions transparently:
173
212
 
174
213
  ```tsx
175
214
  const { exchange } = useHypurrConnect();
176
215
 
177
- // Place an order (works identically for Telegram and EOA users)
178
216
  if (exchange) {
179
- const result = await exchange.placeOrder({
180
- asset: 0,
181
- isBuy: true,
182
- limitPx: "50000",
183
- sz: "0.001",
184
- orderType: { limit: { tif: "Gtc" } },
217
+ // L1 actions signed by the agent key (no wallet popup)
218
+ await exchange.order({
219
+ orders: [
220
+ {
221
+ a: 0,
222
+ b: true,
223
+ p: "50000",
224
+ s: "0.001",
225
+ t: { limit: { tif: "Gtc" } },
226
+ },
227
+ ],
228
+ grouping: "na",
229
+ });
230
+
231
+ await exchange.cancelOrder({ asset: 0, oid: 12345 });
232
+
233
+ // User-signed actions — signed by the master wallet (wallet popup)
234
+ await exchange.usdSend({ destination: "0x...", amount: "100" });
235
+ await exchange.withdraw({ destination: "0x...", amount: "50" });
236
+ await exchange.spotSend({
237
+ destination: "0x...",
238
+ token: "PURR:0x...",
239
+ amount: "10",
185
240
  });
186
241
  }
187
242
  ```
188
243
 
244
+ The routing is automatic — no need to call different methods or switch signers.
245
+
189
246
  ### Multi-Wallet Management (Telegram)
190
247
 
191
248
  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.
@@ -294,7 +351,7 @@ Returns the full auth and exchange state. Throws if used outside `HypurrConnectP
294
351
  | `isLoading` | `boolean` | Whether auth is in progress |
295
352
  | `error` | `string \| null` | Last auth or dead-agent error message |
296
353
  | `authMethod` | `AuthMethod` | `"telegram"`, `"eoa"`, or `null` |
297
- | `exchange` | `ExchangeClient \| null` | Hyperliquid exchange client for L1 actions |
354
+ | `exchange` | `ExchangeClient \| null` | Hyperliquid exchange client (L1 + user-signed actions for EOA) |
298
355
  | `wallets` | `HyperliquidWallet[]` | All wallets for the Telegram user (empty for EOA) |
299
356
  | `selectedWalletId` | `number` | ID of the currently active wallet |
300
357
  | `selectWallet` | `(walletId: number) => void` | Switch the active wallet |
@@ -309,7 +366,7 @@ Returns the full auth and exchange state. Throws if used outside `HypurrConnectP
309
366
  | `loginModalOpen` | `boolean` | Whether the login modal is visible |
310
367
  | `openLoginModal` | `() => void` | Show the login modal |
311
368
  | `closeLoginModal` | `() => void` | Hide the login modal |
312
- | `connectEoa` | `(address: \`0x\${string}\`) => void` | Connect EOA wallet (sync, no signing) |
369
+ | `connectEoa` | `(address: \`0x\${string}\`, signer?: EoaSigner) => void` | Connect EOA wallet (sync); pass signer to enable user-signed actions and auto-provisioning |
313
370
  | `approveAgent` | `(signTypedDataAsync: SignTypedDataFn, chainId: number) => Promise<void>` | Approve a named agent key (async, triggers wallet prompt) |
314
371
  | `logout` | `() => void` | Clear all auth state and localStorage |
315
372
  | `agent` | `StoredAgent \| null` | Current agent key (EOA flow only) |
@@ -393,6 +450,31 @@ interface TelegramChatWalletPack {
393
450
  }
394
451
  ```
395
452
 
453
+ #### `EoaSigner`
454
+
455
+ Encapsulates the master wallet's EIP-712 signing function and chain ID. Pass to `connectEoa` to enable user-signed actions and auto agent provisioning.
456
+
457
+ ```typescript
458
+ interface EoaSigner {
459
+ signTypedData: SignTypedDataFn;
460
+ chainId: number;
461
+ }
462
+ ```
463
+
464
+ #### `createEoaSigner(signTypedData, chainId): EoaSigner`
465
+
466
+ Helper to create an `EoaSigner` from wagmi's `signTypedDataAsync` (or any compatible function). Accepts either a direct function or a `{ current: Function }` ref to avoid stale closures with React hooks:
467
+
468
+ ```typescript
469
+ // With a ref (recommended for React — always calls the latest function)
470
+ const signerRef = useRef(signTypedDataAsync);
471
+ signerRef.current = signTypedDataAsync;
472
+ const signer = createEoaSigner(signerRef, chainId);
473
+
474
+ // With a direct function (fine for stable references)
475
+ const signer = createEoaSigner(signTypedDataAsync, chainId);
476
+ ```
477
+
396
478
  #### `SignTypedDataFn`
397
479
 
398
480
  ```typescript
@@ -456,28 +538,39 @@ Both use `GrpcWebFetchTransport` with the configured `baseUrl`, `timeout`, and `
456
538
  ## Architecture
457
539
 
458
540
  ```
459
- ┌─────────────────────────────────────────────────────────┐
460
- HypurrConnectProvider
461
-
462
- ┌───────────────┐ ┌────────────────────────┐
463
- │ │ Telegram Auth │──gRPC──▶ │ GrpcExchangeTransport
464
- │ │ (server- ┌──────────────────┐
465
- │ │ signed) │ │ │ telegramClient
466
- └───────────────┘ │ .hyperliquidCore │ │ │
467
- │ Action() │ │ │
468
- ┌───────────────┐ └──────────────────┘ │ │
469
- │ │ EOA Auth │──HTTP──▶ │ HttpTransport
470
- │ │ (agent key + PrivateKeySigner
471
- │ │ signed) │ └────────────────────────┘ │
472
- └───────────────┘
473
-
474
- ┌──────────────┐
475
- │ │ExchangeClient
476
- └──────────────┘
477
-
478
-
479
- Hyperliquid L1
480
- └─────────────────────────────────────────────────────────┘
541
+ ┌──────────────────────────────────────────────────────────────────┐
542
+ HypurrConnectProvider
543
+
544
+ ┌──────────────────┐ ┌──────────────────────────────┐
545
+ │ │ Telegram Auth │─gRPC─▶│ GrpcExchangeTransport
546
+ │ │ (server-signed) telegramClient
547
+ └──────────────────┘ .hyperliquidCoreAction() │
548
+ └──────────────────────────────┘
549
+
550
+ ┌──────────────────┐ ┌──────────────────────────────┐
551
+ │ │ EOA Auth │─HTTP─▶│ HttpTransport + Dual Wallet
552
+ │ │ (client-signed)
553
+ └──────────────────┘ signTypedData(params) ─┐ │
554
+ │ │
555
+ domain = "Exchange" │ │ │
556
+ │ │ └─▶ Agent Key │ │ │
557
+ │ │ (auto-provision
558
+ │ │ if needed) │ │ │
559
+ │ │
560
+ domain = "Hyperliquid │ │ │
561
+ │ │ SignTransaction" │ │ │
562
+ │ │ └─▶ Master Wallet │ │ │
563
+ │ │ (wallet popup) │ │ │
564
+ │ └──────────────────────────────┘ │
565
+ │ │ │
566
+ │ ▼ │
567
+ │ ┌──────────────┐ │
568
+ │ │ExchangeClient│ │
569
+ │ └──────────────┘ │
570
+ │ │ │
571
+ │ ▼ │
572
+ │ Hyperliquid L1 │
573
+ └──────────────────────────────────────────────────────────────────┘
481
574
  ```
482
575
 
483
576
  ## License
package/dist/index.d.ts CHANGED
@@ -3,17 +3,19 @@ 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
6
  import { TelegramChatWalletPack } from 'hypurr-grpc/ts/hypurr/user';
9
7
  export { TelegramChatWalletPack } from 'hypurr-grpc/ts/hypurr/user';
8
+ import { HyperliquidWallet } from 'hypurr-grpc/ts/hypurr/wallet';
9
+ export { HyperliquidWallet } from 'hypurr-grpc/ts/hypurr/wallet';
10
+ export { HyperliquidWalletScaleSession, HyperliquidWalletTwapSession } from 'hypurr-grpc/ts/hypurr/tools';
10
11
 
11
12
  interface HypurrConnectConfig {
12
13
  grpcTimeout?: number;
13
14
  isTestnet?: boolean;
14
- telegram?: {
15
+ telegram: {
15
16
  botUsername: string;
16
- botId: string;
17
+ botId?: string;
18
+ useWidget: boolean;
17
19
  };
18
20
  }
19
21
  interface TelegramLoginData {
@@ -52,6 +54,37 @@ type SignTypedDataFn = (params: {
52
54
  primaryType: string;
53
55
  message: Record<string, unknown>;
54
56
  }) => Promise<`0x${string}`>;
57
+ /** Wallet signer provided at EOA connect time for user-signed actions. */
58
+ interface EoaSigner {
59
+ signTypedData: SignTypedDataFn;
60
+ chainId: number;
61
+ }
62
+ /**
63
+ * Create an {@link EoaSigner} from any EIP-712 signing function.
64
+ *
65
+ * Accepts either a direct function or a `{ current }` ref object so the
66
+ * signer always calls through to the latest function (avoids stale closures
67
+ * with React hooks like wagmi's `useSignTypedData`).
68
+ *
69
+ * @example wagmi v2 — ref pattern (recommended)
70
+ * ```ts
71
+ * const { signTypedDataAsync } = useSignTypedData();
72
+ * const chainId = useChainId();
73
+ * const signerRef = useRef(signTypedDataAsync);
74
+ * signerRef.current = signTypedDataAsync; // stays fresh every render
75
+ *
76
+ * // call once — the ref keeps it up to date
77
+ * connectEoa(address, createEoaSigner(signerRef, chainId));
78
+ * ```
79
+ *
80
+ * @example direct function (e.g. from viem WalletClient)
81
+ * ```ts
82
+ * connectEoa(address, createEoaSigner(client.signTypedData, chainId));
83
+ * ```
84
+ */
85
+ declare function createEoaSigner(signTypedDataAsync: ((args: Record<string, unknown>) => Promise<`0x${string}`>) | {
86
+ current: (args: Record<string, unknown>) => Promise<`0x${string}`>;
87
+ }, chainId: number): EoaSigner;
55
88
  interface HypurrConnectState {
56
89
  user: HypurrUser | null;
57
90
  isLoggedIn: boolean;
@@ -84,7 +117,7 @@ interface HypurrConnectState {
84
117
  loginModalOpen: boolean;
85
118
  openLoginModal: () => void;
86
119
  closeLoginModal: () => void;
87
- connectEoa: (address: `0x${string}`) => void;
120
+ connectEoa: (address: `0x${string}`, signer?: EoaSigner) => void;
88
121
  approveAgent: (signTypedDataAsync: SignTypedDataFn, chainId: number) => Promise<void>;
89
122
  logout: () => void;
90
123
  agent: StoredAgent | null;
@@ -108,6 +141,16 @@ interface LoginModalProps {
108
141
  }
109
142
  declare function LoginModal({ onConnectWallet, walletIcon }: LoginModalProps): react_jsx_runtime.JSX.Element;
110
143
 
144
+ interface TelegramLoginWidgetProps {
145
+ botUsername: string;
146
+ onAuth: (data: TelegramLoginData) => void;
147
+ buttonSize?: "large" | "medium" | "small";
148
+ cornerRadius?: number;
149
+ showUserPhoto?: boolean;
150
+ requestAccess?: boolean;
151
+ }
152
+ declare function TelegramLoginWidget({ botUsername, onAuth, buttonSize, cornerRadius, showUserPhoto, requestAccess, }: TelegramLoginWidgetProps): react_jsx_runtime.JSX.Element;
153
+
111
154
  interface GrpcExchangeTransportConfig {
112
155
  isTestnet?: boolean;
113
156
  telegramClient: TelegramClient;
@@ -136,4 +179,4 @@ declare class GrpcExchangeTransport implements IRequestTransport {
136
179
  declare function createTelegramClient(config: HypurrConnectConfig): TelegramClient;
137
180
  declare function createStaticClient(config: HypurrConnectConfig): StaticClient;
138
181
 
139
- export { type AuthMethod, GrpcExchangeTransport, type GrpcExchangeTransportConfig, type HypurrConnectConfig, HypurrConnectProvider, type HypurrConnectState, type HypurrUser, LoginModal, type LoginModalProps, type SignTypedDataFn, type StoredAgent, type TelegramLoginData, createStaticClient, createTelegramClient, useHypurrConnect };
182
+ export { type AuthMethod, type EoaSigner, GrpcExchangeTransport, type GrpcExchangeTransportConfig, type HypurrConnectConfig, HypurrConnectProvider, type HypurrConnectState, type HypurrUser, LoginModal, type LoginModalProps, type SignTypedDataFn, type StoredAgent, type TelegramLoginData, TelegramLoginWidget, type TelegramLoginWidgetProps, createEoaSigner, createStaticClient, createTelegramClient, useHypurrConnect };