npm package: @hfunlabs/hypurr-connect
Description: 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** (MetaMa
Latest version: 0.1.9

# @hfunlabs/hypurr-connect

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.

## Features

- **Dual auth flows** — Telegram OAuth (server-side signing) and EOA wallet (client-side signing)
- **Unified exchange client** — Same `ExchangeClient` interface regardless of auth method; handles both L1 actions (orders, cancels, leverage) and user-signed actions (transfers, withdrawals, staking)
- **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)
- **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
- **Multi-wallet management** — Switch between wallets, create/delete wallets, manage wallet packs and labels (Telegram users)
- **gRPC transport** — Custom transport that routes exchange actions through the Hypurr backend for Telegram users
- **Agent key management** — Named agent keys (`"hypurr-connect"`) with on-chain approval, `extraAgents` validation, expiry-aware caching, and automatic dead-agent recovery
- **Session persistence** — Auth state survives page reloads via localStorage
- **Responsive login modal** — Centered modal on desktop, bottom drawer on mobile with framer-motion animations

## Installation

```bash
pnpm add @hfunlabs/hypurr-connect
```

### Peer Dependencies

Install the required peer dependencies:

```bash
pnpm add @hfunlabs/hyperliquid @protobuf-ts/grpcweb-transport @protobuf-ts/runtime-rpc framer-motion react
```

| Peer Dependency                  | Version             |
| -------------------------------- | ------------------- |
| `@hfunlabs/hyperliquid`          | `0.30.2-hfunlabs.2` |
| `@protobuf-ts/grpcweb-transport` | `>=2.0.0`           |
| `@protobuf-ts/runtime-rpc`       | `>=2.0.0`           |
| `framer-motion`                  | `>=10.0.0`          |
| `react`                          | `>=18.0.0`          |

## Quick Start

### 1. Wrap your app with the provider

```tsx
import { HypurrConnectProvider } from "@hfunlabs/hypurr-connect";

const config = {
  isTestnet: false,
  telegram: {
    botUsername: "YourBot",
    botId: "123456789",
    useWidget: true,
  },
};

function App() {
  return (
    <HypurrConnectProvider config={config}>
      <YourApp />
    </HypurrConnectProvider>
  );
}
```

### 2. Use the hook anywhere in your app

```tsx
import { useHypurrConnect } from "@hfunlabs/hypurr-connect";

function TradingPanel() {
  const { user, isLoggedIn, exchange, openLoginModal, logout } =
    useHypurrConnect();

  if (!isLoggedIn) {
    return <button onClick={openLoginModal}>Connect</button>;
  }

  return (
    <div>
      <p>Welcome, {user.displayName}</p>
      <button onClick={logout}>Disconnect</button>
    </div>
  );
}
```

### 3. Add the login modal

```tsx
import { LoginModal, useHypurrConnect } from "@hfunlabs/hypurr-connect";

function AppShell() {
  const { loginModalOpen, closeLoginModal } = useHypurrConnect();

  return (
    <>
      {loginModalOpen && (
        <LoginModal
          onConnectWallet={() => {
            // Open your wagmi/RainbowKit wallet modal here
          }}
        />
      )}
      <MainContent />
    </>
  );
}
```

## Configuration

### `HypurrConnectConfig`

```typescript
interface HypurrConnectConfig {
  grpcTimeout?: number; // Request timeout in ms (default: 15000)
  isTestnet?: boolean; // Use testnet endpoints (default: false)
  telegram: {
    botUsername: string; // Telegram bot username (required for the widget)
    botId?: string; // Telegram bot ID (required for the popup OAuth flow)
    useWidget: boolean; // true = inline Telegram Login Widget, false = popup OAuth
  };
}
```

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).

When `useWidget` is `false` (or omitted), the popup OAuth flow is used, which requires `botId`.

### Dependencies

This package depends on [`hypurr-grpc`](https://gitlab.com/hypurr/hypurr-grpc) (public GitLab repo) for generated protobuf service clients. It is installed directly from Git — no registry auth is needed.

## Authentication Flows

### Telegram Login

Two modes are available, controlled by `config.telegram.useWidget`:

**Widget mode** (`useWidget: true`) — recommended:

1. The `LoginModal` renders Telegram's official [Login Widget](https://core.telegram.org/widgets/login) inline
2. User clicks the widget button and authorizes with Telegram
3. The widget calls the `onAuth` callback with the user's auth data
4. The provider calls the Hypurr gRPC backend (`telegramUser`) to fetch the user's wallet address and ID
5. An `ExchangeClient` is created with `GrpcExchangeTransport` — all exchange actions are signed **server-side** by the Hypurr backend
6. Session is persisted in localStorage (`hypurr-connect-tg-user`)

**Popup mode** (`useWidget: false`):

1. User clicks "Telegram" in the `LoginModal`
2. A popup opens to `oauth.telegram.org` with the configured bot
3. User authorizes; Telegram posts auth data back via `postMessage`
4. Steps 4–6 are identical to widget mode

### EOA Wallet Login

When a signer is provided via `connectEoa`, the exchange client works immediately — no manual agent approval step is needed:

1. User clicks "Wallet" in the `LoginModal`; the `onConnectWallet` callback fires
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
3. The `exchange` client is ready to use right away:
   - **User-signed actions** (transfers, withdrawals, staking) are routed directly to the master wallet — the wallet popup appears for the user to approve
   - **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
   - Named agent keys (`"hypurr-connect"`) are cached in localStorage with their `validUntil` timestamp and revalidated against Hyperliquid's `extraAgents` endpoint
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`

Use the `createEoaSigner` helper to adapt wagmi's `signTypedDataAsync` to the `EoaSigner` interface. Pass a ref to avoid stale closure issues with React hooks:

```tsx
import { useHypurrConnect, createEoaSigner } from "@hfunlabs/hypurr-connect";
import { useSignTypedData, useChainId, useAccountEffect } from "wagmi";

function ConnectWallet() {
  const { connectEoa, logout, exchange } = useHypurrConnect();
  const { signTypedDataAsync } = useSignTypedData();
  const chainId = useChainId();

  // Keep a ref so the signer always calls the latest signTypedDataAsync
  const signerRef = useRef(signTypedDataAsync);
  signerRef.current = signTypedDataAsync;

  useAccountEffect({
    onConnect({ address }) {
      connectEoa(address, createEoaSigner(signerRef, chainId));
    },
    onDisconnect() {
      logout();
    },
  });

  // exchange is ready — L1 and user-signed actions both work
  if (exchange) {
    // L1 action — agent signs silently (auto-provisioned if needed)
    await exchange.order({ orders: [/* ... */], grouping: "na" });

    // User-signed action — wallet popup appears
    await exchange.usdSend({ destination: "0x...", amount: "100" });
  }
}
```

If you prefer explicit control over agent approval (or don't need user-signed actions), you can omit the signer and call `approveAgent` manually:

```tsx
connectEoa(address); // no signer — only L1 actions after manual approval
await approveAgent(signTypedDataAsync, chainId);
```

### Using the Exchange Client

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:

```tsx
const { exchange } = useHypurrConnect();

if (exchange) {
  // L1 actions — signed by the agent key (no wallet popup)
  await exchange.order({
    orders: [
      {
        a: 0,
        b: true,
        p: "50000",
        s: "0.001",
        t: { limit: { tif: "Gtc" } },
      },
    ],
    grouping: "na",
  });

  await exchange.cancelOrder({ asset: 0, oid: 12345 });

  // User-signed actions — signed by the master wallet (wallet popup)
  await exchange.usdSend({ destination: "0x...", amount: "100" });
  await exchange.withdraw({ destination: "0x...", amount: "50" });
  await exchange.spotSend({
    destination: "0x...",
    token: "PURR:0x...",
    amount: "10",
  });
}
```

The routing is automatic — no need to call different methods or switch signers.

### Multi-Wallet Management (Telegram)

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.

```tsx
const {
  wallets,
  selectedWalletId,
  selectWallet,
  createWallet,
  deleteWallet,
  refreshWallets,
} = useHypurrConnect();

// List wallets
wallets.map((w) => (
  <button
    key={w.id}
    onClick={() => selectWallet(w.id)}
    style={{ fontWeight: w.id === selectedWalletId ? "bold" : "normal" }}
  >
    {w.name || w.ethereumAddress}
  </button>
));

// Create a new wallet
const newWallet = await createWallet("Trading");

// Delete a wallet (auto-selects another if the deleted one was active)
await deleteWallet(walletId);
```

#### Wallet Packs & Labels

Organize watched wallets into named packs with labels:

```tsx
const {
  packs,
  createWalletPack,
  addPackLabel,
  modifyPackLabel,
  removePackLabel,
} = useHypurrConnect();

// Create a pack
const packId = await createWalletPack("Whales");

// Add a labeled wallet to the pack
await addPackLabel({
  walletAddress: "0x...",
  walletLabel: "Big trader",
  packId,
});

// Rename a label
await modifyPackLabel({
  walletLabelOld: "Big trader",
  walletLabelNew: "Whale #1",
  packId,
});

// Remove a label from the pack
await removePackLabel({ walletLabel: "Whale #1", packId });
```

All wallet management functions automatically refresh the wallet list from the server after mutations.

## API Reference

### Components

#### `HypurrConnectProvider`

```tsx
<HypurrConnectProvider config={HypurrConnectConfig}>
  {children}
</HypurrConnectProvider>
```

Context provider that manages all auth state, gRPC clients, and exchange clients. Must wrap any component that uses `useHypurrConnect`.

#### `LoginModal`

```tsx
<LoginModal
  onConnectWallet={() => void}
  walletIcon?: ReactNode       // Custom icon for the wallet button (defaults to MetaMask icon)
/>
```

Animated modal with Telegram and Wallet login buttons. Renders as a centered modal on desktop (>=640px) and a bottom drawer on mobile. Uses the `closeLoginModal` function from context to dismiss.

### Hook

#### `useHypurrConnect(): HypurrConnectState`

Returns the full auth and exchange state. Throws if used outside `HypurrConnectProvider`.

### `HypurrConnectState`

| Property           | Type                                                                      | Description                                                      |
| ------------------ | ------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `user`             | `HypurrUser \| null`                                                      | Current authenticated user (reflects selected wallet)            |
| `isLoggedIn`       | `boolean`                                                                 | Whether a user is authenticated                                  |
| `isLoading`        | `boolean`                                                                 | Whether auth is in progress                                      |
| `error`            | `string \| null`                                                          | Last auth or dead-agent error message                            |
| `authMethod`       | `AuthMethod`                                                              | `"telegram"`, `"eoa"`, or `null`                                 |
| `exchange`         | `ExchangeClient \| null`                                                  | Hyperliquid exchange client (L1 + user-signed actions for EOA)   |
| `wallets`          | `HyperliquidWallet[]`                                                     | All wallets for the Telegram user (empty for EOA)                |
| `selectedWalletId` | `number`                                                                  | ID of the currently active wallet                                |
| `selectWallet`     | `(walletId: number) => void`                                              | Switch the active wallet                                         |
| `createWallet`     | `(name: string) => Promise<HyperliquidWallet>`                            | Create a new wallet (Telegram only)                              |
| `deleteWallet`     | `(walletId: number) => Promise<void>`                                     | Delete a wallet (Telegram only)                                  |
| `refreshWallets`   | `() => void`                                                              | Re-fetch wallets and packs from the server                       |
| `packs`            | `TelegramChatWalletPack[]`                                                | Wallet packs for the Telegram user                               |
| `createWalletPack` | `(name: string) => Promise<number>`                                       | Create a wallet pack; returns the new pack ID                    |
| `addPackLabel`     | `(params) => Promise<void>`                                               | Add a labeled wallet to a pack                                   |
| `modifyPackLabel`  | `(params) => Promise<void>`                                               | Rename a label within a pack                                     |
| `removePackLabel`  | `(params) => Promise<void>`                                               | Remove a label from a pack                                       |
| `loginModalOpen`   | `boolean`                                                                 | Whether the login modal is visible                               |
| `openLoginModal`   | `() => void`                                                              | Show the login modal                                             |
| `closeLoginModal`  | `() => void`                                                              | Hide the login modal                                             |
| `connectEoa`       | `(address: \`0x\${string}\`, signer?: EoaSigner) => void`                 | Connect EOA wallet (sync); pass signer to enable user-signed actions and auto-provisioning |
| `approveAgent`     | `(signTypedDataAsync: SignTypedDataFn, chainId: number) => Promise<void>` | Approve a named agent key (async, triggers wallet prompt)        |
| `logout`           | `() => void`                                                              | Clear all auth state and localStorage                            |
| `agent`            | `StoredAgent \| null`                                                     | Current agent key (EOA flow only)                                |
| `agentReady`       | `boolean`                                                                 | Whether the exchange client can sign (true for TG, or EOA+agent) |
| `clearAgent`       | `() => void`                                                              | Remove the agent key from state and storage                      |
| `botId`            | `string`                                                                  | Telegram bot ID from config                                      |
| `authDataMap`      | `Record<string, string>`                                                  | Raw Telegram auth data as key-value pairs                        |
| `telegramClient`   | `TelegramClient`                                                          | Low-level gRPC client for the Telegram service                   |
| `staticClient`     | `StaticClient`                                                            | Low-level gRPC client for the Static service                     |

### Types

#### `HypurrUser`

```typescript
interface HypurrUser {
  address: string; // Ethereum address
  walletId: number; // Hypurr wallet ID
  displayName: string; // "@username", first_name, or truncated address
  photoUrl?: string; // Telegram profile photo URL
  authMethod: AuthMethod; // "telegram" | "eoa"
  telegramId?: string; // Telegram user ID (string)
}
```

#### `AuthMethod`

```typescript
type AuthMethod = "telegram" | "eoa" | null;
```

#### `TelegramLoginData`

```typescript
interface TelegramLoginData {
  id: number;
  first_name: string;
  last_name?: string;
  username?: string;
  photo_url?: string;
  auth_date: number;
  hash: string;
}
```

#### `StoredAgent`

```typescript
interface StoredAgent {
  privateKey: `0x${string}`;
  address: `0x${string}`;
  approvedAt: number; // Timestamp when approved on-chain
  validUntil: number; // Epoch ms from extraAgents; agent is invalid after this
}
```

#### `HyperliquidWallet`

Re-exported from `hypurr-grpc`:

```typescript
interface HyperliquidWallet {
  id: number;
  name: string;
  ethereumAddress: string;
  isAgent: boolean;
  isReadOnly: boolean;
  // ... plus balances, movements, sessions
}
```

#### `TelegramChatWalletPack`

Re-exported from `hypurr-grpc`:

```typescript
interface TelegramChatWalletPack {
  id: number;
  telegramChatId: number;
  name: string;
}
```

#### `EoaSigner`

Encapsulates the master wallet's EIP-712 signing function and chain ID. Pass to `connectEoa` to enable user-signed actions and auto agent provisioning.

```typescript
interface EoaSigner {
  signTypedData: SignTypedDataFn;
  chainId: number;
}
```

#### `createEoaSigner(signTypedData, chainId): EoaSigner`

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:

```typescript
// With a ref (recommended for React — always calls the latest function)
const signerRef = useRef(signTypedDataAsync);
signerRef.current = signTypedDataAsync;
const signer = createEoaSigner(signerRef, chainId);

// With a direct function (fine for stable references)
const signer = createEoaSigner(signTypedDataAsync, chainId);
```

#### `SignTypedDataFn`

```typescript
type SignTypedDataFn = (params: {
  domain: Record<string, unknown>;
  types: Record<string, { name: string; type: string }[]>;
  primaryType: string;
  message: Record<string, unknown>;
}) => Promise<`0x${string}`>;
```

### Classes

#### `GrpcExchangeTransport`

Custom `IRequestTransport` implementation that routes exchange actions through the Hypurr gRPC backend (used by Telegram auth).

```typescript
class GrpcExchangeTransport implements IRequestTransport {
  isTestnet: boolean;
  constructor(config: GrpcExchangeTransportConfig);
  request<T>(
    endpoint: "info" | "exchange" | "explorer",
    payload: unknown,
    signal?: AbortSignal,
  ): Promise<T>;
}
```

- **`exchange` endpoint** — Serializes the action to bytes and calls `telegramClient.hyperliquidCoreAction()` via gRPC. The Hypurr backend validates the auth data and signs the action server-side.
- **`info` / `explorer` endpoints** — Proxied directly to the Hyperliquid HTTP API.

```typescript
interface GrpcExchangeTransportConfig {
  isTestnet?: boolean;
  telegramClient: TelegramClient;
  authDataMap: Record<string, string>;
  walletId: number;
}
```

### Factory Functions

#### `createTelegramClient(config: HypurrConnectConfig): TelegramClient`

Creates a gRPC-Web client for the Telegram service.

#### `createStaticClient(config: HypurrConnectConfig): StaticClient`

Creates a gRPC-Web client for the Static service.

Both use `GrpcWebFetchTransport` with the configured `baseUrl`, `timeout`, and `origin` metadata.

## localStorage Keys

| Key                              | Content                                                        |
| -------------------------------- | -------------------------------------------------------------- |
| `hypurr-connect-tg-user`         | Serialized `TelegramLoginData` (persists Telegram session)     |
| `hypurr-connect-agent:{address}` | Serialized `StoredAgent` (persists EOA agent keys per address) |

## Architecture

```
┌──────────────────────────────────────────────────────────────────┐
│                      HypurrConnectProvider                        │
│                                                                  │
│  ┌──────────────────┐       ┌──────────────────────────────┐     │
│  │  Telegram Auth    │─gRPC─▶│  GrpcExchangeTransport        │     │
│  │  (server-signed)  │       │  telegramClient                │     │
│  └──────────────────┘       │    .hyperliquidCoreAction()    │     │
│                              └──────────────────────────────┘     │
│                                                                  │
│  ┌──────────────────┐       ┌──────────────────────────────┐     │
│  │    EOA Auth       │─HTTP─▶│  HttpTransport + Dual Wallet   │     │
│  │  (client-signed)  │       │                                │     │
│  └──────────────────┘       │  signTypedData(params) ─┐      │     │
│                              │                         │      │     │
│                              │  domain = "Exchange"    │      │     │
│                              │    └─▶ Agent Key        │      │     │
│                              │        (auto-provision  │      │     │
│                              │         if needed)      │      │     │
│                              │                         │      │     │
│                              │  domain = "Hyperliquid  │      │     │
│                              │   SignTransaction"      │      │     │
│                              │    └─▶ Master Wallet    │      │     │
│                              │        (wallet popup)   │      │     │
│                              └──────────────────────────────┘     │
│                                          │                        │
│                                          ▼                        │
│                                 ┌──────────────┐                 │
│                                 │ExchangeClient│                 │
│                                 └──────────────┘                 │
│                                          │                        │
│                                          ▼                        │
│                                  Hyperliquid L1                   │
└──────────────────────────────────────────────────────────────────┘
```

## License

MIT