构建一个实时 Hyperliquid 投资组合跟踪器

本文介绍了如何使用 QuickNode 的 Hyperliquid info endpoint 构建一个 Hyperliquid 投资组合跟踪器,该跟踪器可以实时监控用户的仓位、盈亏和保证金使用情况。文章详细阐述了如何设置 QuickNode endpoint 和 Supabase 账户,搭建数据库 schema,创建索引器,以及构建一个可以展示实时交易数据的仪表盘。

概述

作为 Hyperliquid 上的永续交易者,拥有一个全面的投资组合跟踪器对于实时监控你的仓位、盈亏和保证金利用率至关重要。本指南将向你展示如何使用 QuickNode 的 Hyperliquid info endpoint 构建一个强大的投资组合跟踪器,该跟踪器可以监控任何 Hyperliquid 钱包。除了创建一个有用的交易工具之外,本教程还将揭秘如何获取、构建和利用 HyperCore 数据来构建应用程序。该应用程序展示了:

  • 实时仓位跟踪 - 实时更新永续仓位及盈亏
  • 投资组合分析 - 账户价值、保证金使用和风险指标
  • 金库管理 - 跟踪金库价值和锁仓计划
  • 现货持有 - 监控代币余额和美元价值
  • 搜索任何钱包 - 在不同的交易账户之间切换

主页

仪表盘页面

你将做什么

分 4 个阶段构建一个完整的投资组合跟踪器:

  1. 设置 QuickNode endpoint 和 Supabase 账户
  2. 设置你的数据库 schema
  3. 创建一个每 500 毫秒获取一次 HyperCore 数据的 indexer
  4. 使用现代 UI 库构建一个显示实时交易数据的仪表盘

你将需要什么

为什么选择 QuickNode Endpoint?

QuickNode 提供专用的 Hyperliquid API endpoint,无需运行你自己的节点:

  • 预配置的 endpoint,无需设置
  • 处理连接管理和故障转移
  • 无需额外基础设施即可直接访问 HyperCore 数据

架构概览

投资组合跟踪器由三个组件组成,这些组件通过 PostgreSQL 数据库进行通信。 Indexer 从 Hyperliquid 获取数据,将其存储在数据库中,前端查询数据库以进行显示。

技术栈组件

  • 前端:React + TypeScript + Tailwind CSS + shadcn/ui & Radix UI
    • 在响应式界面中显示交易数据
    • 每 1000 毫秒轮询一次数据库以进行更新
    • 通过数据库请求处理钱包切换
  • 后端:Node.js indexer,轮询间隔为 500 毫秒
    • 从 5 个不同的 Hyperliquid endpoint 获取数据
    • 使用适当的精度处理将数据存储在 PostgreSQL 中
    • 管理来自前端的钱包切换请求

轮询注意事项

本指南使用较短的轮询间隔(Indexer 为 500 毫秒,前端为 1000 毫秒)来演示实时更新。你可以根据需要调整这些间隔:

  • 前端:src/Dashboard.tsx 第 260-264 行 - 在以下代码中更改 1000 值:
const interval = setInterval(async () => {
    await fetchData(currentWallet);
}, 1000);
  • Indexer:src/indexer/indexer.ts 第 623-630 行 - 在以下代码中更改 500 值:
setInterval(async () => {
    await indexer.checkForWalletSwitch();
    await indexer.indexData();
}, 500);

监控你的 QuickNode 和 Supabase 使用情况以优化成本。

  • 数据库:Supabase PostgreSQL
    • 在 6 个表中以财务精度(DECIMAL 类型)存储交易数据
    • 通过 wallet_switch_requests 表处理前端和 indexer 之间的通信
    • 使用唯一约束来防止重复条目
  • 数据源:通过 QuickNode 的 Hyperliquid info endpoint
    • 提供账户数据、仓位、金库存有量、现货余额和委托
    • 以 JSON 格式返回数据,并使用字符串数字表示精度
    • 通过带有钱包地址参数的 HTTP POST 请求访问
                    ┌─────────────────┐
                    │   Perp 交易者   │
                    └─────────┬───────┘
                              │ 1. 输入钱包地址
                              ▼
                    ┌─────────────────┐
                    │ React 仪表盘 │◄─────────────────┐
                    └─────────┬───────┘                  │
                              │ 2. 存储请求         │ 6. 读取 & 显示数据
                              ▼                          │
                    ┌─────────────────┐                  │
                    │    Supabase     │◄─────────────────┤
                    │   PostgreSQL    │                  │
                    └─────────┬───────┘                  │
                              │ 3. 检测到请求        │ 5. 存储数据
                              ▼                          │
                    ┌─────────────────┐                  │
                    │     Indexer     │──────────────────┘
                    │   (500ms 轮询)  │
                    └─────────┬───────┘
                              │ 4. 获取 HyperCore 数据
                              ▼
                    ┌─────────────────┐
                    │   QuickNode     │
                    │  Hyperliquid    │
                    │   Endpoint      │
                    └─────────────────┘

应用程序流程

第一次钱包搜索:

  1. 用户输入钱包 → 前端验证并将请求存储在数据库中
  2. Indexer 检测到请求(每 500 毫秒轮询一次)→ 切换到新的钱包
  3. 数据收集开始 → 从 5 个给定的 endpoint 独立获取数据并将其存储到数据库中
  4. 前端轮询数据库(每 1000 毫秒)→ UI 使用从 indexer 获取的新鲜数据进行更新

第二次及以后的钱包搜索:

  1. 用户输入新钱包 → 前端调用以清除任何先前钱包的数据,并命令 indexer 切换到新钱包
  2. Indexer 检测到新请求 → 立即切换到新钱包,并开始使用新的钱包地址获取数据
  3. 数据实时更新 → 仪表盘开始获取新钱包的仓位和指标

项目结构

该项目遵循一个清晰的、模块化的架构,该架构分离了数据收集、UI 组件和业务逻辑之间的关注点。这种结构使代码库可维护,并允许轻松扩展功能。

目录分解

├── src/
│   ├── indexer/
│   │   ├── indexer.ts          # 主要的 indexer 编排和钱包管理
│   │   └── apicalls.ts         # Hyperliquid info endpoint 查询
│   ├── components/
│   │   ├── ui/                 # shadcn/ui 组件(Button, Input, Card, 等)
│   │   └── dashboard/          # 仪表盘组件(WalletHeader, PortfolioMetrics, 等)
│   ├── shared/
│   │   ├── types.ts            # TypeScript 接口 & 类型
│   │   ├── utils.ts            # 格式化、计算 & 实用函数
│   │   ├── constants.ts        # 仪表盘的 UI 常量
│   │   └── supabase.ts         # 前端访问的 Supabase 客户端实例
│   ├── Dashboard.tsx           # 主要的仪表盘逻辑
│   └── main.tsx
├── supabase/
│   └── schema.sql              # 完整的数据库 schema
├── package.json
└── .env

运行项目

步骤 1:克隆存储库

首先,克隆项目存储库并导航到项目目录:

git clone https://github.com/quiknode-labs/qn-guide-examples.git
cd qn-guide-examples/sample-dapps/hyperliquid-portfolio-tracker

步骤 2:设置环境变量文件

通过运行以下命令创建你的 .env 文件:

cp .env.example .env

步骤 3:Supabase 数据库设置

Supabase 网站 上创建一个新的 Supabase 账户或登录到你现有的 Supabase 账户。

创建一个新项目,然后单击 Connect 按钮。

数据库连接

在 App Frameworks 部分,选择 React 并将 using 字段更改为 Vite。复制 VITE_SUPABASE_URLVITE_SUPABASE_ANON_KEY 的值并将它们添加到你的 .env 文件中。

数据库选择React

最后,导航到右上角的 SQL Editor,粘贴 schema.sql 文件的内容,然后单击 run。这将创建所有必要的表和函数,我们需要存储 & 获取前端的数据。

SQL 编辑器

步骤 4:QuickNode 设置

创建你的免费试用 QuickNode 账户,然后创建你的第一个 Hyperliquid RPC endpoint 并将其粘贴到你的 .env 文件中。

info

确保删除现有的 /evm 并在你的 QuickNode endpoint URL 的末尾添加 /info,以获取对 Hyperliquid info endpoint 的访问权限。

步骤 5:启动应用程序

创建表并配置环境后,你可以通过在根目录中运行以下命令来启动项目:

npm install && npm run dev:both

这将在运行前端应用程序的同时运行 indexer。

当 indexer 开始运行时,它将等待你搜索有效的钱包地址:

Indexer 启动

在 localhost URL 上打开你的前端页面,然后单击演示钱包按钮以获取示例钱包地址:

钱包搜索

搜索钱包后,indexer 将开始每 500 毫秒获取该地址的数据:

Indexer 运行

仪表盘将显示钱包的实时统计信息:

仪表盘页面

你现在可以实时访问账户价值、活跃仓位和其他交易数据。

故障排除

在使用 indexer 时,你可能会在设置或运行时遇到以下问题:

  • Indexer 停止响应
  • 钱包搜索后未显示数据
  • 意外停止了 indexer

解决方案:

通过运行以下命令重新启动 indexer:

npm run dev:indexer

然后尝试通过输入有效的钱包地址再次搜索。

深入了解代码库

现在你已经启动并运行了投资组合跟踪器,让我们探索一下它在幕后是如何工作的。本节将深入探讨三个核心组件,这些组件使实时投资组合跟踪成为可能:

Indexer - 通过 apicalls.ts 处理 Hyperliquid 的 info endpoint,检测钱包切换请求,并每 500 毫秒编排从 5 个不同的 Hyperliquid endpoint 收集数据。

Schema & DB - 演示了 PostgreSQL 表结构,这些结构以财务精度存储交易数据,包括前端-indexer 通信的协调机制。

仪表盘(前端) - 涵盖 React 状态管理、实时数据库轮询、钱包地址验证以及显示实时交易数据的用户界面。

Indexer

Indexer 作为一个单独的 Node.js 进程运行,每 500 毫秒轮询 QuickNode endpoint。它获取当前钱包地址的交易数据,并将其存储在数据库中。 Indexer 等待来自前端的钱包切换请求,并使用锁文件处理进程隔离。

钱包更改检测

Indexer 每 500 毫秒轮询 wallet_switch_requests 表,以检测前端钱包切换,使用状态字段来防止切换过程中的竞争条件。

Hyperliquid info Endpoint 集成

Indexer 使用 apicalls.ts 中的 HyperliquidAPI 类与 QuickNode 的 Hyperliquid endpoint 通信。每个 API 方法都遵循一致的模式来获取不同类型的交易数据:

// 来自 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',这表明每个 endpoint 仅需要钱包地址和 endpoint 类型。所有方法都包括全面的错误处理和日志记录,以调试交易数据问题。

深入解释 Indexer

主要的索引循环调用 5 个不同的 info endpoint,并将结果存储在单独的数据库表中:

// 来自 indexer.ts - 核心数据获取和存储
const data = await hyperliquidAPI.getClearinghouseState(CURRENT_WALLET_ADDRESS);
const stateId = await this.storeClearinghouseState(data);

// 以原子替换方式存储仓位,以防止 UI 闪烁
await this.storeAssetPositions(data.assetPositions, data.time);

// 获取其他数据类型,并进行错误处理
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}`);
}

Indexer 可以优雅地处理单个 endpoint 故障 - 如果一个数据源失败,其他数据源将继续正常工作。

Indexer 使用基于文件的锁来防止多个实例同时运行:

// 来自 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(`❌ 另一个 indexer 已经在运行 (PID: ${pid})`);
      return false;
    } catch (e) {
      fs.unlinkSync(LOCK_FILE); // 删除过时的锁
    }
  }

  fs.writeFileSync(LOCK_FILE, JSON.stringify({ pid: process.pid }));
  return true;
}

Indexer 使用锁文件来防止多个实例同时运行,从而确保数据一致性。

Schema & DB (Supabase)

现在我们已经了解了 indexer 如何收集数据,让我们检查一下如何存储和构造这些数据。 PostgreSQL 数据库在 6 个表中存储交易数据,并处理前端和 indexer 之间的通信。每个表都使用 DECIMAL 类型表示财务精度,并使用唯一约束来防止重复条目。

表结构示例

资产仓位表** - 存储具有财务精度的永续交易仓位:

-- 来自 schema.sql - 核心交易数据存储
CREATE TABLE asset_positions (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  wallet_address TEXT NOT NULL,

  coin TEXT NOT NULL,                  -- 资产符号(例如,“BTC”、“ETH”、“SOL”)
  size DECIMAL(20, 5) NOT NULL,        -- 仓位大小:总共 20 位数字,小数点后 5 位
  leverage_type TEXT NOT NULL,         -- “cross”或“isolated”保证金模式
  leverage_value INTEGER NOT NULL,     -- 杠杆倍数(1 倍、5 倍、10 倍等)

  entry_price DECIMAL(20, 5),          -- 平均入场价格,精度为小数点后 5 位
  position_value DECIMAL(20, 5),       -- 仓位当前的美元价值
  unrealized_pnl DECIMAL(20, 5),       -- 平仓前的盈/亏
  liquidation_price DECIMAL(20, 5),    -- 仓位被清算的价格
  margin_used DECIMAL(20, 5),          -- 分配给该仓位的保证金金额

  timestamp BIGINT NOT NULL,           -- 来自 HyperCore 的毫秒级 Unix 时间戳
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()  -- 数据库插入时间
);

-- 唯一约束可防止每个钱包-币种对的重复仓位
ALTER TABLE asset_positions ADD CONSTRAINT unique_position_per_wallet
UNIQUE (wallet_address, coin);

DECIMAL(20, 5) 类型提供总共 20 位数字,小数点后 5 位。

钱包切换请求表 - 协调前端和 indexer 之间的通信:

-- 来自 schema.sql - 前端-indexer 协调
CREATE TABLE wallet_switch_requests (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  requested_wallet_address TEXT NOT NULL,           -- Ethereum 地址(0x 格式)
  status TEXT NOT NULL DEFAULT 'pending',           -- 状态机:pending → processing → completed
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- 用于 indexer 基于状态的高效查询的索引
CREATE INDEX idx_wallet_switch_requests_status ON wallet_switch_requests(status);

此表充当 React 前端和 Node.js indexer 之间的消息队列。当用户输入新的钱包地址时,前端会插入一个 'pending' 请求。 Indexer 每 500 毫秒轮询一次 'pending' 请求,将状态更新为 'processing' 以防止竞争条件,然后执行钱包切换。状态进展确保一次只发生一个钱包切换,即使使用者快速搜索不同的地址也是如此。

仪表盘(前端)

Indexer 收集数据并将数据存储在数据库中后,最后一部分就是用户界面。 React 前端每 1000 毫秒轮询一次数据库,以获取更新的交易数据,并使用模块化组件显示该数据。它通过将请求插入数据库并立即清除本地状态来管理钱包切换。

概述

仪表盘架构围绕着中心化状态管理和模块化组件组合:

// 来自 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 毫秒轮询一次数据库:

// 来自 Dashboard.tsx - 使用清理自动刷新
useEffect(() => {
  if (currentWallet && hasStarted && hasInitialData) {
    const interval = setInterval(async () => {
      await fetchData(currentWallet);
    }, 1000);
    return () => clearInterval(interval);
  }
}, [currentWallet, hasStarted, hasInitialData, fetchData]);

// 数据新鲜度检测提供视觉反馈
const isDataStale = latestState && (Date.now() - latestState.timestamp > 3000);

轮询会根据用户操作自动启动和停止,仅在需要时运行以节省资源。 界面显示数据何时过时(超过 3 秒),以使用户了解数据的新鲜度。

数据库查询模式

前端使用优化的查询模式从 Supabase 实时获取交易数据:

// 来自 Dashboard.tsx - 实时数据查询
const { data: latestData, error: latestError } = await supabase
  .from('clearinghouse_states')
  .select('*')
  .eq('wallet_address', walletAddress)
  .order('timestamp', { ascending: false })
  .limit(1)
  .maybeSingle();

// 获取此钱包的所有仓位
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;

这些查询可以优雅地处理钱包没有交易数据的情况,显示空结果而不是错误。

管理钱包切换

前端验证钱包地址,并在切换时立即清除状态:

// 来自 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;
  }

  // 切换钱包时立即清除所有旧数据
  setLatestState(null);
  setPositions([]);
  setVaultEquities([]);
  setSpotBalances([]);
  setIsSearching(true);

  // 向 indexer 发出切换信号
  await switchIndexerWallet(address);
  setCurrentWallet(address);
};

界面验证钱包地址,并在切换时立即清除旧数据,显示加载状态,直到加载新数据为止。

数据显示

交易数据从 React 状态流向专用 UI 组件,这些组件格式化并呈现信息:

// 来自 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 账户价值
        </div>
        <div className="text-2xl font-bold text-white">
          {formatCurrency(totalAccountValue)}
        </div>
      </CardContent>
    </Card>
  );
};

Dashboard 组件将计算出的值和格式化函数传递给子组件,这些子组件处理交易数据的视觉呈现和样式设置。

结论

恭喜! 你已成功使用 QuickNode 的 Hyperliquid info endpoint 构建了实时 Hyperliquid 投资组合跟踪器。 你已经学习了如何获取永续交易数据、构建具有财务精度的 PostgreSQL 数据库以及构建实时更新的响应式仪表盘。 这种基础解锁了高级交易工具的可能性,例如自动风险监控和多钱包比较。

你可以通过集成 Recharts 等图表库或使用你喜欢的通知服务构建交易警报来进一步扩展此项目。 查看我们的其他 Hyperliquid 指南,以探索更多在 Hyperliquid 上构建的方法。

下一步

现在你有了一个可以正常工作的投资组合跟踪器,以下是扩展和改进应用程序的几种方法:

  • 当仓位接近危险保证金水平时发出清算警告
  • 使用图表跟踪一段时间内的投资组合绩效
  • 缩短钱包搜索之间的加载时间

更多资源

如果你遇到困难或有疑问,请将其放入我们的 Discord。 通过在 Twitter (@QuickNode) 或我们的 Telegram 公告频道 上关注我们来了解最新信息。

我们 ❤️ 反馈!

如果你对新主题有任何反馈或请求,请告诉我们。 我们很乐意听取你的意见。

  • 原文链接: quicknode.com/guides/oth...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。