@hfunlabs/hypurr-connect
v0.1.3
Published
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
Readme
@hfunlabs/hypurr-connect
React authentication and wallet connectivity library for the Hyperliquid decentralized exchange via the Hypurr 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
ExchangeClientinterface 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
approveAgentstep 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,extraAgentsvalidation, 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
pnpm add @hfunlabs/hypurr-connectPeer Dependencies
Install the required peer dependencies:
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
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
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
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
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 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.
When useWidget is false (or omitted), the popup OAuth flow is used, which requires botId.
Dependencies
This package depends on 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:
- The
LoginModalrenders Telegram's official Login Widget inline - User clicks the widget button and authorizes with Telegram
- The widget calls the
onAuthcallback with the user's auth data - The provider calls the Hypurr gRPC backend (
telegramUser) to fetch the user's wallet address and ID - An
ExchangeClientis created withGrpcExchangeTransport— all exchange actions are signed server-side by the Hypurr backend - Session is persisted in localStorage (
hypurr-connect-tg-user)
Popup mode (useWidget: false):
- User clicks "Telegram" in the
LoginModal - A popup opens to
oauth.telegram.orgwith the configured bot - User authorizes; Telegram posts auth data back via
postMessage - 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:
- User clicks "Wallet" in the
LoginModal; theonConnectWalletcallback fires - Your app connects the wallet (e.g., via wagmi) and calls
connectEoa(address, signer)with anEoaSigner— this sets the user immediately and restores a cached agent from localStorage if one is still valid - The
exchangeclient 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
approveAgentsignature, then the original action proceeds - Named agent keys (
"hypurr-connect") are cached in localStorage with theirvalidUntiltimestamp and revalidated against Hyperliquid'sextraAgentsendpoint
- 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:
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:
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:
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.
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:
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
<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
<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| 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
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
type AuthMethod = "telegram" | "eoa" | null;TelegramLoginData
interface TelegramLoginData {
id: number;
first_name: string;
last_name?: string;
username?: string;
photo_url?: string;
auth_date: number;
hash: string;
}StoredAgent
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:
interface HyperliquidWallet {
id: number;
name: string;
ethereumAddress: string;
isAgent: boolean;
isReadOnly: boolean;
// ... plus balances, movements, sessions
}TelegramChatWalletPack
Re-exported from hypurr-grpc:
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.
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:
// 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
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).
class GrpcExchangeTransport implements IRequestTransport {
isTestnet: boolean;
constructor(config: GrpcExchangeTransportConfig);
request<T>(
endpoint: "info" | "exchange" | "explorer",
payload: unknown,
signal?: AbortSignal,
): Promise<T>;
}exchangeendpoint — Serializes the action to bytes and callstelegramClient.hyperliquidCoreAction()via gRPC. The Hypurr backend validates the auth data and signs the action server-side.info/explorerendpoints — Proxied directly to the Hyperliquid HTTP API.
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
