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

> 原文地址: https://docs.blockvectra.com/zh/guides/ink-pool-discovery/

## 直接回答

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

key 只放在本机的 `BLOCKVECTRA_API_KEY` 环境变量里，不要写进 URL、聊天消息、源码或日志。获取方式：[登录控制台](https://console.blockvectra.com/login/?next=%2Fkeys%2F)创建，或由 Agent / 脚本按[程序化开户指南](https://docs.blockvectra.com/zh/guides/programmatic-signup/)获得。本页不会替你开户或创建 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`：

```bash
export BLOCKVECTRA_API_KEY='replace-with-your-key'
node ink-pools.mjs
```

```js
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 id                 | `0xdef1`（57073）                                                                  |
| 区块                       | 60189593，哈希 `0xc3b4349618611b7dfd979140a70087920cb8416eb1c8eff82cb46332494b7617` |
| 该哈希下的 `allPoolsLength()` | 17                                                                               |
| 该哈希下的 `aggregate3`       | 17 个结果，全部成功、非零且互不重复                                                              |

这只是一次 QA 运行，不代表当前值：链头会前进，工厂合约的池数量也可能增加，运行时请重新读取。它不能证明完整的 Pharos 流程、150 个子调用的批次、池余额或 TVL、持续使用，也不代表任何团队已经采用。[保存下来的请求与响应](https://gist.github.com/gjwang/5ce60ad10587040cbe2d743dd0a3e149/a68a3c917e4a272b9131bd42c30c875b55ff27a5#file-20261103-keyed-hash-pool-discovery-json)是公开的。

## 哈希固定读取被拒绝时

2026-11-03 免 key 路由返回了链头，随后拒绝了带 `{blockHash, requireCanonical: true}` 的 `eth_call`，错误为 JSON-RPC `-32011`、`error.data.reason: state_window`。该错误不可重试（见 [state\_window](https://docs.blockvectra.com/zh/errors/#state_window)与[历史状态窗口](https://docs.blockvectra.com/zh/guides/evm-historical-state/)）。带 key 的路由和免 key 路由是两个不同的入口；请在 [GET /v1/chains](https://api.blockvectra.com/v1/chains) 里查看 `public.methods` 和状态窗口，不要假设两者表现一致。

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

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

同一个哈希下两次读取被接受，并不能证明该区块以后绝不会被重组，也不能代替对整个应用的验收。请把输出当作该哈希处的快照。方法说明见 [eth\_call](https://docs.blockvectra.com/zh/api/json-rpc/methods/eth_call/)，Ink Mainnet 的参数与接入方式见 [Ink 链页](https://blockvectra.com/zh/chains/ink_mainnet/)。

## 公共 RPC 的独立观测

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

## 下一步

* [查看免费额度与定价](https://blockvectra.com/zh/pricing/#free)，确认账户可用的方案。
* [登录控制台](https://console.blockvectra.com/login/?next=%2Fkeys%2F)创建 API key。
