本文介绍了如何使用QuickNode的Hyperliquid信息端点构建一个Hyperliquid投资组合跟踪器,该跟踪器可以实时监控任何Hyperliquid钱包的持仓、盈亏和保证金利用率。文章详细阐述了如何获取、构建和利用HyperCore数据,并展示了实时持仓跟踪、投资组合分析、保险库管理和现货持有等功能的实现。
作为 Hyperliquid 上的永续合约交易者,拥有一个全面的投资组合跟踪器对于实时监控你的仓位、盈亏和保证金利用率至关重要。本指南将向你展示如何使用 QuickNode 的 Hyperliquid 信息端点构建一个强大的投资组合跟踪器,以监控任何 Hyperliquid 钱包。除了创建一个有用的交易工具外,本教程还将揭秘如何获取、构建和利用 HyperCore 数据来构建应用程序。该应用程序展示了:
分 4 个阶段构建一个完整的投资组合跟踪器:
为什么选择 QuickNode 端点?
QuickNode 提供专用的 Hyperliquid API 端点,无需运行你自己的节点:
投资组合跟踪器由三个组件组成,它们通过 PostgreSQL 数据库进行通信。索引器从 Hyperliquid 获取数据,将其存储在数据库中,前端查询数据库以进行显示。
轮询注意事项
本指南使用积极的轮询间隔(索引器 500 毫秒,前端 1000 毫秒)来演示实时更新。如果需要,你可以调整这些间隔:
src/Dashboard.tsx
第 260-264 行 - 更改以下代码中的 1000
值:const interval = setInterval(async () => {
await fetchData(currentWallet);
}, 1000);
src/indexer/indexer.ts
第 623-630 行 - 更改以下代码中的 500
值:setInterval(async () => {
await indexer.checkForWalletSwitch();
await indexer.indexData();
}, 500);
监控你的 QuickNode 和 Supabase 使用情况以优化成本。
DECIMAL
类型)将交易数据存储在 6 个表中wallet_switch_requests
表处理前端和索引器之间的通信info
端点
┌─────────────────┐
│ Perp Trader │
└─────────┬───────┘
│ 1. Enter wallet address (输入钱包地址)
▼
┌─────────────────┐
│ React Dashboard │◄─────────────────┐
└─────────┬───────┘ │
│ 2. Store request (存储请求) │ 6. Read & display data (读取并显示数据)
▼ │
┌─────────────────┐ │
│ Supabase │◄─────────────────┤
│ PostgreSQL │ │
└─────────┬───────┘ │
│ 3. Detect request (检测请求) │ 5. Store data (存储数据)
▼ │
┌─────────────────┐ │
│ Indexer │──────────────────┘
│ (500ms poll) │
└─────────┬───────┘
│ 4. Fetch HyperCore data (获取 HyperCore 数据)
▼
┌─────────────────┐
│ QuickNode │
│ Hyperliquid │
│ Endpoint │
└─────────────────┘
第一次钱包搜索:
第二次及以后的钱包搜索:
该项目遵循一个清晰的、模块化的架构,该架构将数据收集、UI 组件和业务逻辑之间的关注点分离开来。这种结构使代码库易于维护,并可以轻松扩展功能。
├── src/
│ ├── indexer/
│ │ ├── indexer.ts # Main indexer orchestration & wallet management (主索引器编排和钱包管理)
│ │ └── apicalls.ts # Hyperliquid info endpoint queries (Hyperliquid 信息端点查询)
│ ├── components/
│ │ ├── ui/ # shadcn/ui components (Button, Input, Card, etc.)
│ │ └── dashboard/ # Dashboard components (WalletHeader, PortfolioMetrics, etc.)
│ ├── shared/
│ │ ├── types.ts # TypeScript interfaces & types (TypeScript 接口和类型)
│ │ ├── utils.ts # Formatting, calculations & utility functions (格式化、计算和实用功能)
│ │ ├── constants.ts # UI constants for the dashboard (仪表板的 UI 常量)
│ │ └── supabase.ts # Supabase client instance for frontend access (用于前端访问的 Supabase 客户端实例)
│ ├── Dashboard.tsx # Main Dashboard logic (主仪表板逻辑)
│ └── main.tsx
├── supabase/
│ └── schema.sql # Complete database schema (完整的数据库模式)
├── package.json
└── .env
首先,克隆项目存储库并导航到项目目录:
git clone https://github.com/quiknode-labs/qn-guide-examples.git
cd qn-guide-examples/sample-dapps/hyperliquid-portfolio-tracker
通过运行以下命令创建你的 .env
文件:
cp .env.example .env
在 Supabase 网站 上创建一个新的 Supabase 账户或登录到你现有的 Supabase 账户。
创建一个新项目,然后单击“连接”按钮。
在“应用程序框架”部分中,选择 React 并将 using
字段更改为 Vite。复制 VITE_SUPABASE_URL
和 VITE_SUPABASE_ANON_KEY
值并将它们添加到你的 .env
文件中。
最后,导航到右上角的 SQL 编辑器,粘贴 schema.sql
文件的内容,然后单击运行。这将创建所有必需的表和函数,我们需要这些表和函数来存储和获取前端的数据。
创建你的免费试用 QuickNode 帐户,然后创建你的第一个 Hyperliquid RPC 端点并将其粘贴到你的 .env
文件中。
info
确保删除现有的 /evm
并在你的 QuickNode 端点 URL 的末尾添加 /info
,以获得对 Hyperliquid 信息端点的访问权限。
一旦创建了表并配置了你的环境,你就可以通过在根目录中运行以下命令来启动项目:
npm install && npm run dev:both
这将同时运行前端应用程序和索引器。
当索引器开始运行时,它将等待你搜索有效的钱包地址:
在 localhost URL 上打开你的前端页面,然后单击演示钱包按钮以获取示例钱包地址:
搜索钱包后,索引器将开始每 500 毫秒获取该地址的数据:
仪表板将显示钱包的实时统计信息:
你现在可以实时访问帐户价值、活跃仓位和其他交易数据。
在使用索引器时,你可能会在设置或运行时遇到以下问题:
解决方案:
通过运行以下命令重新启动索引器:
npm run dev:indexer
然后通过输入有效的钱包地址再次尝试搜索。
现在你已经启动并运行了portfolio tracker,让我们来深入探讨它的工作原理。本节将深入探讨使实时投资组合跟踪成为可能的三大核心组件:
索引器- 通过 apicalls.ts
处理 Hyperliquid 的 info
端点,检测钱包切换请求,并每 500 毫秒编排来自 5 个不同 Hyperliquid 端点的数据收集。
模式和数据库- 演示了 PostgreSQL 表结构,这些表结构以财务精度存储交易数据,包括前端-索引器通信的协调机制。
仪表板(前端)- 涵盖 React 状态管理、实时数据库轮询、钱包地址验证以及显示实时交易数据的用户界面。
索引器作为单独的 Node.js 进程运行,每 500 毫秒轮询 QuickNode 端点。它获取当前钱包地址的交易数据并将其存储在数据库中。索引器等待来自前端的钱包切换请求,并使用锁定文件处理进程隔离。
索引器每 500 毫秒轮询 wallet_switch_requests
表,以检测前端钱包切换,使用状态字段以防止切换过程中的竞争条件。
info
端点集成索引器使用 apicalls.ts
中的 HyperliquidAPI
类与 QuickNode 的 Hyperliquid 端点进行通信。每个 API 方法都遵循一致的模式来获取不同类型的交易数据:
// From apicalls.ts - Main account data fetching (来自 apicalls.ts - 主要账户数据获取)
async getClearinghouseState(walletAddress: string): Promise<ClearinghouseStateResponse> {
const payload = {
type: 'clearinghouseState',
user: walletAddress
};
const response = await fetch(QUICKNODE_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
return await response.json();
}
getUserVaultEquities()
方法遵循相同的结构,但 type: 'userVaultEquities'
,这表明每个端点只需要钱包地址和端点类型。所有方法都包括全面的错误处理和日志记录,以调试交易数据问题。
主索引循环调用 5 个不同的 info
端点,并将结果存储在单独的数据库表中:
// From indexer.ts - Core data fetching and storage (来自 indexer.ts - 核心数据获取和存储)
const data = await hyperliquidAPI.getClearinghouseState(CURRENT_WALLET_ADDRESS);
const stateId = await this.storeClearinghouseState(data);
// Store positions with atomic replacement to prevent UI flickering (使用原子替换存储仓位以防止 UI 闪烁)
await this.storeAssetPositions(data.assetPositions, data.time);
// Fetch additional data types with error handling (获取其他数据类型并进行错误处理)
try {
const rateLimitData = await hyperliquidAPI.getUserRateLimit(CURRENT_WALLET_ADDRESS);
await this.storeUserRateLimit(rateLimitData, timestamp);
} catch (error) {
console.log(`No rate limit data available for ${CURRENT_WALLET_ADDRESS}`);
}
索引器可以优雅地处理单个端点故障 - 如果一个数据源失败,其他数据源可以继续正常工作。
索引器使用基于文件的锁来防止多个实例同时运行:
// From indexer.ts - Lock file creation and process checking (来自 indexer.ts - 锁定文件创建和进程检查)
function createLock(): boolean {
if (fs.existsSync(LOCK_FILE)) {
const { pid } = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf8'));
try {
process.kill(pid, 0);
console.error(`❌ Another indexer is already running (PID: ${pid})`);
return false;
} catch (e) {
fs.unlinkSync(LOCK_FILE); // Remove stale lock (删除过时的锁)
}
}
fs.writeFileSync(LOCK_FILE, JSON.stringify({ pid: process.pid }));
return true;
}
索引器使用锁定文件来防止多个实例同时运行,从而确数据一致性。
现在我们已经了解了索引器如何收集数据,让我们检查一下如何存储和构造数据。PostgreSQL 数据库将交易数据存储在 6 个表中,并处理前端和索引器之间的通信。每个表都使用 DECIMAL
类型来获得财务精度和唯一约束,以防止重复条目。
资产仓位表- 存储具有财务精度的永续合约交易仓位:
-- From schema.sql - Core trading data storage (来自 schema.sql - 核心交易数据存储)
CREATE TABLE asset_positions (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
wallet_address TEXT NOT NULL,
coin TEXT NOT NULL, -- Asset symbol (e.g., 'BTC', 'ETH', 'SOL') (资产符号(例如,“BTC”、“ETH”、“SOL”))
size DECIMAL(20, 5) NOT NULL, -- Position size: 20 digits total, 5 after decimal (仓位大小:总共 20 位数字,小数点后 5 位)
leverage_type TEXT NOT NULL, -- 'cross' or 'isolated' margin mode ('cross' 或 'isolated' 保证金模式)
leverage_value INTEGER NOT NULL, -- Leverage multiplier (1x, 5x, 10x, etc.) (杠杆倍数(1x、5x、10x 等))
entry_price DECIMAL(20, 5), -- Average entry price with 5 decimal precision (平均入场价格,精度为 5 位小数)
position_value DECIMAL(20, 5), -- Current USD value of the position (仓位的当前美元价值)
unrealized_pnl DECIMAL(20, 5), -- Profit/loss before closing position (平仓前的盈/亏)
liquidation_price DECIMAL(20, 5), -- Price at which position gets liquidated (仓位被清算的价格)
margin_used DECIMAL(20, 5), -- Amount of margin allocated to this position (分配给此仓位的保证金金额)
timestamp BIGINT NOT NULL, -- Unix timestamp in milliseconds from HyperCore (来自 HyperCore 的 Unix 时间戳,以毫秒为单位)
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -- Database insertion time (数据库插入时间)
);
-- Unique constraint prevents duplicate positions per wallet-coin pair (唯一约束防止每个钱包-币对出现重复仓位)
ALTER TABLE asset_positions ADD CONSTRAINT unique_position_per_wallet
UNIQUE (wallet_address, coin);
DECIMAL(20, 5)
类型提供 20 个总位数,小数点后 5 位。
钱包切换请求表- 协调前端和索引器之间的通信:
-- From schema.sql - Frontend-indexer coordination (来自 schema.sql - 前端-索引器协调)
CREATE TABLE wallet_switch_requests (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
requested_wallet_address TEXT NOT NULL, -- Ethereum address (0x format) (以太坊地址(0x 格式))
status TEXT NOT NULL DEFAULT 'pending', -- State machine: pending → processing → completed (状态机:pending → processing → completed)
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Index for efficient status-based queries by indexer (索引器用于有效的基于状态的查询的索引)
CREATE INDEX idx_wallet_switch_requests_status ON wallet_switch_requests(status);
此表充当 React 前端和 Node.js 索引器之间的消息队列。当用户输入新的钱包地址时,前端会插入一个 "pending"
请求。索引器每 500 毫秒轮询一次 "pending"
请求,将状态更新为 "processing"
以防止竞争情况,然后执行钱包切换。状态进展确保每次只发生一个钱包切换,即使在用户快速搜索不同的地址时也是如此。
在有了索引器收集数据和数据库存储数据的情况下,最后一部分就是用户界面。React 前端每 1000 毫秒轮询一次数据库以获取更新的交易数据,并使用模块化组件显示它。它通过将请求插入数据库并立即清除本地状态来管理钱包切换。
仪表板架构围绕着中心化状态管理和模块化组件组合:
// From Dashboard.tsx - Centralized state management (来自 Dashboard.tsx - 中心化状态管理)
const [latestState, setLatestState] = useState<ClearinghouseState | null>(null);
const [positions, setPositions] = useState<AssetPosition[]>([]);
const [vaultEquities, setVaultEquities] = useState<UserVaultEquity[]>([]);
const [spotBalances, setSpotBalances] = useState<SpotBalance[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [hasInitialData, setHasInitialData] = useState(false);
Dashboard
组件将所有交易数据保存在 React 状态中,并将其作为 props 传递给子组件。这种模式使状态管理变得简单,并使在调试期间跟踪数据流变得容易。
前端使用具有适当清除的 setInterval
循环每 1000 毫秒轮询数据库:
// From Dashboard.tsx - Auto-refresh with cleanup (来自 Dashboard.tsx - 自动刷新并清除)
useEffect(() => {
if (currentWallet && hasStarted && hasInitialData) {
const interval = setInterval(async () => {
await fetchData(currentWallet);
}, 1000);
return () => clearInterval(interval);
}
}, [currentWallet, hasStarted, hasInitialData, fetchData]);
// Data freshness detection provides visual feedback (数据新鲜度检测提供视觉反馈)
const isDataStale = latestState && (Date.now() - latestState.timestamp > 3000);
轮询会根据用户操作自动启动和停止,仅在需要时运行以节省资源。该界面会在数据陈旧(超过 3 秒)时显示,以使有关数据新鲜度的用户随时了解情况。
前端使用优化的查询模式从 Supabase 实时获取交易数据:
// From Dashboard.tsx - Real-time data queries (来自 Dashboard.tsx - 实时数据查询)
const { data: latestData, error: latestError } = await supabase
.from('clearinghouse_states')
.select('*')
.eq('wallet_address', walletAddress)
.order('timestamp', { ascending: false })
.limit(1)
.maybeSingle();
// Get all positions for this wallet (获取此钱包的所有仓位)
const { data: positionsData, error: positionsError } = await supabase
.from('asset_positions')
.select('*')
.eq('wallet_address', walletAddress)
.order('timestamp', { ascending: false });
if (positionsError && positionsError.code !== 'PGRST116') throw positionsError;
这些查询可以很好地处理钱包没有交易数据的情况,显示空结果而不是错误。
前端验证钱包地址并在切换时立即清除状态:
// From Dashboard.tsx - Wallet validation and switching (来自 Dashboard.tsx - 钱包验证和切换)
const isValidWalletAddress = (address: string): boolean => {
return /^0x[a-fA-F0-9]{40}$/.test(address);
};
const handleWalletSearch = async () => {
if (!isValidWalletAddress(address)) {
setError('Invalid wallet address format');
return;
}
// Clear ALL old data immediately when switching wallets (切换钱包时立即清除所有旧数据)
setLatestState(null);
setPositions([]);
setVaultEquities([]);
setSpotBalances([]);
setIsSearching(true);
// Signal indexer to switch (向索引器发出切换信号)
await switchIndexerWallet(address);
setCurrentWallet(address);
};
该界面验证钱包地址并在切换时立即清除旧数据,显示加载状态直到加载新数据。
交易数据从 React 状态流向专门的 UI 组件,这些组件格式化并呈现信息:
// From PortfolioMetrics.tsx - Portfolio overview card component (来自 PortfolioMetrics.tsx - 投资组合概述卡组件)
interface PortfolioMetricsProps {
totalAccountValue: number;
totalUnrealizedPnl: number;
userRateLimit: UserRateLimit | null;
vaultEquities: UserVaultEquity[];
delegations: Delegation[];
formatCurrency: (value: number) => string;
}
export const PortfolioMetrics: React.FC<PortfolioMetricsProps> = ({
totalAccountValue,
totalUnrealizedPnl,
formatCurrency
}) => {
return (
<Card className="bg-slate-900/50 border-slate-700/50 backdrop-blur-sm mb-6">
<CardContent className="p-4">
<div className="text-xs text-slate-400 mb-3 font-medium tracking-wide uppercase">
Perp Account Value
</div>
<div className="text-2xl font-bold text-white">
{formatCurrency(totalAccountValue)}
</div>
</CardContent>
</Card>
);
};
Dashboard
组件将计算出的值和格式化函数传递给子组件,这些子组件处理交易数据的可视化呈现和样式设置。
恭喜!你已经成功构建了一个使用 QuickNode 的 Hyperliquid 信息端点的实时 Hyperliquid 投资组合跟踪器。你已经学习了如何获取永续合约交易数据、构建用于财务精度的 PostgreSQL 数据库以及构建实时更新的响应式仪表板。这个基础解锁了高级交易工具的可能性,例如自动风险监控和多钱包比较。
你可以通过集成图表库(如 Recharts)或使用你喜欢的通知服务构建交易警报来进一步扩展此项目。查看我们的其他 Hyperliquid 指南,以探索更多在 Hyperliquid 上构建的方法。
现在你已经有了一个可用的投资组合跟踪器,以下是扩展和改进应用程序的几种方法:
如果你遇到困难或有疑问,请在我们的 Discord 中提出。通过在 Twitter (@QuickNode) 或我们的 Telegram 公告频道 上关注我们,及时了解最新信息。
如果你对新主题有任何反馈或要求,请告诉我们。我们很乐意听取你的意见。
- 原文链接: quicknode.com/guides/hyp...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!