在同一个区块哈希下列出 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)。 - 完成标志: 脚本输出一个区块哈希、该哈希下工厂合约的池数量,以及与数量相同的池地址。
四个请求
eth_chainId必须等于 57073,否则停止。eth_getBlockByNumber传latest取得链头,只保留它的hash;此后不再使用区块号。- 向工厂合约
0x7660BF2aDefC687C22906112ca323d60A00358cF发eth_call,调用allPoolsLength()(选择器0xefde4e64),块参数为{blockHash, requireCanonical: true},得到池数量。 - 向 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.mjsimport { 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、持续使用,也不代表任何团队已经采用。保存下来的请求与响应是公开的。
哈希固定读取被拒绝时
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 运行示例后再下判断。
下一步
最后更新: