在同一个区块哈希下列出 Ink 池:EIP-1898 eth_call 与 Multicall3

把 Ink 工厂合约的池列表固定在同一个 canonical 区块哈希读取:eth_chainId、最新区块哈希、allPoolsLength,再用 Multicall3 aggregate3,使用 x-api-key 请求头,附 Node.js viem 示例。

直接回答

要在 Ink Mainnet 的某一个区块上发现池,请使用需要认证的端点 https://api.blockvectra.com/v1/ink_mainnet,把 key 放在 x-api-key 请求头里。先读一次最新区块,之后每个 eth_call 都用该区块的哈希作为 EIP-1898 选择器:{"blockHash": "0x…", "requireCanonical": true}。免 key 的 …/v1/ink_mainnet/public 路由对这种按哈希固定的 eth_call 会返回 -32011,reason 为 state_window(见哈希固定读取被拒绝时),所以这个任务需要 key。

key 只放在本机的 BLOCKVECTRA_API_KEY 环境变量里,不要写进 URL、聊天消息、源码或日志。获取方式:登录控制台创建,或由 Agent / 脚本按程序化开户指南获得。本页不会替你开户或创建 key。

  • 第一步: 确认 eth_chainId 返回 0xdef1(57073)。
  • 完成标志: 脚本输出一个区块哈希、该哈希下工厂合约的池数量,以及与数量相同的池地址。

四个请求

  1. eth_chainId 必须等于 57073,否则停止。
  2. eth_getBlockByNumber 传 latest 取得链头,只保留它的 hash;此后不再使用区块号。
  3. 向工厂合约 0x7660BF2aDefC687C22906112ca323d60A00358cF 发 eth_call,调用 allPoolsLength()(选择器 0xefde4e64),块参数为 {blockHash, requireCanonical: true},得到池数量。
  4. 向 Multicall3 0xcA11bde05977b3631167028862bE2a173976CA11 发一次使用同一哈希选择器的 eth_call,用 aggregate3 批量调用 allPools(0) … allPools(count − 1)(选择器 0x41d1de97)。调用列表按刚读到的数量生成,不要写死。

示例:Node.js 与 viem

示例需要 Node.js 20 或更高版本和 viem 2.x。请在自己的项目里用 npm install viem 自行安装 viem,除此之外不需要别的依赖。把脚本保存为 ink-pools.mjs:

export BLOCKVECTRA_API_KEY='replace-with-your-key'
node ink-pools.mjs
import { decodeAbiParameters, decodeFunctionResult, encodeFunctionData, parseAbi, zeroAddress } from "viem";

const ENDPOINT = "https://api.blockvectra.com/v1/ink_mainnet";
const CHAIN_ID = 57073n;
const FACTORY = "0x7660BF2aDefC687C22906112ca323d60A00358cF";
const MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11";
const MAX_POOLS = 150n; // example budget for this script, not a product limit
const TIMEOUT_MS = 15_000;
const GAP_MS = 1_100;

const apiKey = process.env.BLOCKVECTRA_API_KEY;
if (!apiKey || apiKey === "replace-with-your-key") throw new Error("Set BLOCKVECTRA_API_KEY in your local environment");

const factoryAbi = parseAbi([
  "function allPoolsLength() view returns (uint256)",
  "function allPools(uint256) view returns (address)",
]);
const multicallAbi = parseAbi([
  "function aggregate3((address target, bool allowFailure, bytes callData)[] calls) payable returns ((bool success, bytes returnData)[] returnData)",
]);

// One request at a time, no retry, no redirect, no fallback to another endpoint.
let lastSentAt = 0;
async function rpc(method, params) {
  const wait = lastSentAt + GAP_MS - Date.now();
  if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
  lastSentAt = Date.now();
  const response = await fetch(ENDPOINT, {
    method: "POST",
    headers: { "content-type": "application/json", "x-api-key": apiKey },
    body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
    redirect: "error",
    signal: AbortSignal.timeout(TIMEOUT_MS),
  });
  if (response.status !== 200) throw new Error(`${method}: HTTP ${response.status}`);
  const body = await response.json();
  if (body?.jsonrpc !== "2.0" || body.id !== 1) throw new Error(`${method}: invalid JSON-RPC response`);
  if (body.error) {
    const reason = String(body.error.data?.reason ?? "").slice(0, 64);
    throw new Error(`${method}: JSON-RPC error ${body.error.code} ${reason}`.trim());
  }
  return body.result;
}

function requireData(value, bytes) {
  const valid = typeof value === "string" && /^0x(?:[0-9a-f]{2})*$/i.test(value)
    && (bytes === undefined || value.length === 2 + bytes * 2);
  if (!valid) throw new Error("Invalid eth_call result");
  return value;
}

// 1. Chain id
const chainIdHex = await rpc("eth_chainId", []);
if (typeof chainIdHex !== "string" || !/^0x(?:0|[1-9a-f][0-9a-f]*)$/i.test(chainIdHex)) throw new Error("Invalid chain id");
if (BigInt(chainIdHex) !== CHAIN_ID) throw new Error(`Expected chain ${CHAIN_ID}, got ${BigInt(chainIdHex)}`);

// 2. Read the head once and keep only its hash
const head = await rpc("eth_getBlockByNumber", ["latest", false]);
const blockHash = head?.hash;
if (typeof blockHash !== "string" || !/^0x[0-9a-f]{64}$/i.test(blockHash)) throw new Error("Invalid block hash");
const at = { blockHash, requireCanonical: true };

// 3. Pool count at that hash
const countData = await rpc("eth_call", [
  { to: FACTORY, data: encodeFunctionData({ abi: factoryAbi, functionName: "allPoolsLength" }) },
  at,
]);
const count = decodeFunctionResult({ abi: factoryAbi, functionName: "allPoolsLength", data: requireData(countData, 32) });
if (count < 1n || count > MAX_POOLS) throw new Error(`allPoolsLength ${count} is outside 1..${MAX_POOLS}`);
const size = Number(count);

// 4. allPools(i) for every i below the count, in one aggregate3 at the same hash
const calls = Array.from({ length: size }, (_, i) => ({
  target: FACTORY,
  allowFailure: true,
  callData: encodeFunctionData({ abi: factoryAbi, functionName: "allPools", args: [BigInt(i)] }),
}));
const listData = await rpc("eth_call", [
  { to: MULTICALL3, data: encodeFunctionData({ abi: multicallAbi, functionName: "aggregate3", args: [calls] }) },
  at,
]);
const results = decodeFunctionResult({ abi: multicallAbi, functionName: "aggregate3", data: requireData(listData) });
if (results.length !== size) throw new Error(`aggregate3 returned ${results.length} results for ${size} calls`);

const pools = results.map(({ success, returnData }, i) => {
  if (!success) throw new Error(`allPools(${i}) failed inside Multicall3`);
  const word = requireData(returnData, 32);
  if (!/^0x0{24}[0-9a-f]{40}$/i.test(word)) throw new Error(`allPools(${i}) returned non-zero address padding`);
  const [pool] = decodeAbiParameters([{ type: "address" }], word);
  if (pool === zeroAddress) throw new Error(`allPools(${i}) returned the zero address`);
  return pool;
});
if (new Set(pools.map((pool) => pool.toLowerCase())).size !== pools.length) throw new Error("Duplicate pool address");

console.log(JSON.stringify({ blockHash, count: size, pools }, null, 2));

出现下列任一情况,脚本会以非零退出码停止,不输出部分结果:

  • HTTP 状态码不是 200、JSON-RPC 返回 error、15 秒超时或发生重定向;
  • chain id 不是 57073、区块哈希格式不对,或结果不是有效的十六进制 / ABI 数据;
  • 数量不在 1 到 150 之间。150 只是本示例的有界预算,不是产品上限,我们也没有在这里测试过 150 个子调用的批次;
  • 内部某个 allPools(i) 调用失败,或出现零地址、重复的池地址。

它一共发 4 个请求,间隔约 1.1 秒,从不重试、不广播交易、不分页,也不会用 latest 或区块号替代那个哈希。输出只有哈希、数量和池地址,不输出请求头,也不输出 key。

样本结果

我们的 QA 于 2026-11-03 06:40 UTC 带 key 对 ink_mainnet 发了这四个请求,每个响应都是 HTTP 200,没有重试:

项目观察结果
Chain id0xdef1(57073)
区块60189593,哈希 0xc3b4349618611b7dfd979140a70087920cb8416eb1c8eff82cb46332494b7617
该哈希下的 allPoolsLength()17
该哈希下的 aggregate317 个结果,全部成功、非零且互不重复

这只是一次 QA 运行,不代表当前值:链头会前进,工厂合约的池数量也可能增加,运行时请重新读取。它不能证明完整的 Pharos 流程、150 个子调用的批次、池余额或 TVL、持续使用,也不代表任何团队已经采用。保存下来的请求与响应是公开的。

哈希固定读取被拒绝时

2026-11-03 免 key 路由返回了链头,随后拒绝了带 {blockHash, requireCanonical: true} 的 eth_call,错误为 JSON-RPC -32011、error.data.reason: state_window。该错误不可重试(见 state_window与历史状态窗口)。带 key 的路由和免 key 路由是两个不同的入口;请在 GET /v1/chains 里查看 public.methods 和状态窗口,不要假设两者表现一致。

如果按哈希固定的调用被拒绝,原因可能是该区块已不再属于 canonical 链,也可能是提供服务的节点无法读取它:

  • 立即停止。不要重试同一个哈希,也不要换成 latest 或区块号来拿到结果,否则数量和池列表就来自不同区块。
  • 从第 2 步重新开始。新的输出是另一个快照,有它自己的哈希。

同一个哈希下两次读取被接受,并不能证明该区块以后绝不会被重组,也不能代替对整个应用的验收。请把输出当作该哈希处的快照。方法说明见 eth_call,Ink Mainnet 的参数与接入方式见 Ink 链页。

公共 RPC 的独立观测

如果想从外部看看 Ink 公共 RPC 端点的表现,OpenChainBench 有一个公开榜单 openchainbench.com/benchmarks/ink-rpc,同样的数据也提供 JSON:openchainbench.com/api/stat/ink-rpc。它每 60 秒从三个地域各测一次免 key 的 eth_getBlockByNumber("latest", false)。这与本指南带 key、按哈希固定的 eth_call 和 Multicall3 请求不是同一种负载,也不说明池发现是否成功,所以请用你自己的 key 运行示例后再下判断。

下一步

最后更新:

本页目录