第3节:读合约

小白入门:https://github.com/dukedaily/solidity-expert ,欢迎star转发,文末加V入群。

职场进阶: https://dukeweb3.com

ethers 的 Contract 对象把 ABI + 地址 + Provider 绑定起来,直接像调用对象方法一样读链上数据。

最小示例

读取 USDC 的总供应量与 vitalik.eth 的余额:

import { ethers } from "ethers";

const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");

const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const abi = [
  "function totalSupply() view returns (uint256)",
  "function balanceOf(address) view returns (uint256)",
  "function decimals() view returns (uint8)",
];

const usdc = new ethers.Contract(USDC, abi, provider);

const total = await usdc.totalSupply();
const decimals = await usdc.decimals();
const vbal = await usdc.balanceOf(await provider.resolveName("vitalik.eth"));

console.log("USDC 总供应:", ethers.formatUnits(total, decimals));
console.log("Vitalik 余额:", ethers.formatUnits(vbal, decimals));

ABI 的三种写法

// 1) Human-readable ABI(推荐,简洁)
const abi = [
  "function balanceOf(address owner) view returns (uint256)",
  "event Transfer(address indexed from, address indexed to, uint256 value)",
];

// 2) JSON ABI(编译 artifact 输出的格式)
import abiJson from "./Usdc.json";
const contract = new ethers.Contract(addr, abiJson.abi, provider);

// 3) 通过 Interface 构造(便于 selector / 编码工作)
const iface = new ethers.Interface(abi);

staticCall:不发交易的预执行

要预执行一个会改状态的方法但不真的上链,使用 staticCall

// 试调 transfer,不发交易,但会检查 revert
try {
  const ok = await usdc.transfer.staticCall(
    "0xSomeRecipient",
    ethers.parseUnits("100", 6),
  );
  console.log("预检通过:", ok);
} catch (err) {
  console.log("预检失败:", err.reason ?? err.message);
}

调用失败的错误结构

v6 的 CALL_EXCEPTION 错误对象携带结构化信息:

try {
  await usdc.transfer.staticCall("0xSomeRecipient", 10n ** 30n);
} catch (err) {
  // err.code === "CALL_EXCEPTION"
  console.log("reason:", err.reason); // 字符串 message
  console.log("revert:", err.revert); // custom error 数据
}

小结

Contract + Provider 就是只读的"合约查询客户端"。下一节把 Signer 接入,开始写链。