@hfunlabs/hypurr-connect
v0.1.11
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,
grpcUrl: "https://grpc.hypurr.fun",
telegram: {
authHubUrl: "https://auth.hypurr.fun/login",
scope: [
"telegram:user:read",
"telegram:wallet:read",
"telegram:wallet:write",
"telegram:trade:read",
"telegram:trade:write",
],
},
};
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 {
grpcUrl?: string; // gRPC-web base URL (default: https://grpc.hypurr.fun)
mediaUrl?: string; // Media base URL (default: https://media.hypurr.fun)
grpcTimeout?: number; // Request timeout in ms (default: 15000)
isTestnet?: boolean; // Use testnet endpoints (default: false)
telegram: {
authHubUrl?: string; // Auth hub URL (default: https://auth.hypurr.fun/login)
returnTo?: string | (() => string); // Callback URL (default: current page)
scope?: string | string[]; // Requested JWT scopes
};
}The SDK no longer renders Telegram's login widget or opens oauth.telegram.org
directly. Telegram login is delegated to the Hypurr auth hub in a popup, and the
popup posts the scoped JWT back to the original page.
Dependencies
This package depends on hypurr-grpc
for generated protobuf service clients.
Authentication Flows
Telegram Login
- User clicks "Telegram" in the
LoginModal. - The SDK opens the configured auth hub in a popup with
return_to,state, and requestedscope. - The auth hub performs Telegram login and redirects back to
return_towith a scoped JWT. - The popup callback page posts
{ token, state }to the opener withpostMessageand closes. - The opener validates
state, stores the JWT, and calls the Hypurr gRPC backend withAuthorization: Bearer <jwt>metadata. - An
ExchangeClientis created withGrpcExchangeTransport; exchange actions are still signed server-side by the Hypurr backend. - The JWT session is persisted in localStorage (
hypurr-connect-tg-jwt).
If the popup is blocked, the SDK falls back to a full-page redirect. The
default returnTo is the current page with auth query params removed; custom
returnTo URLs should load the app and mount HypurrConnectProvider so the
popup callback bridge can run.
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 | Deprecated legacy Telegram bot ID from config |
|authDataMap |Record<string, string> | Deprecated; empty when using hub JWT auth |
|authToken |string | null | JWT returned by the auth hub |
|telegramRpcOptions|RpcOptions | undefined | gRPC call options containingAuthorization: Bearer metadata |
|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 hub JWT and signs the action server-side.info/explorerendpoints — Proxied directly to the Hyperliquid HTTP API.
interface GrpcExchangeTransportConfig {
isTestnet?: boolean;
telegramClient: TelegramClient;
rpcOptions?: RpcOptions;
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 config.grpcUrl as baseUrl and
config.grpcTimeout as timeout.
localStorage Keys
| Key | Content |
| -------------------------------- | -------------------------------------------------------------- |
| hypurr-connect-tg-jwt | Hub-issued JWT for Telegram gRPC calls |
| 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
