ethers v6 里 provider / signer 怎么拿,以及手写一个 signer

前端 dApp 连 MetaMask 用 ethers.js,v5 和 v6 API 变了。写代码总卡在”provider 怎么初始化”这一步。

ethers v6 的最小骨架

<script src="https://cdn.jsdelivr.net/npm/ethers@6.7.1/dist/ethers.min.js"></script>
<script>
let provider, signer;

if (window.ethereum) {
  provider = new ethers.BrowserProvider(window.ethereum);
} else {
  alert("请先安装 MetaMask");
}

document.getElementById("connect").addEventListener("click", async () => {
  await provider.send("eth_requestAccounts", []);
  signer = await provider.getSigner();
  const address = await signer.getAddress();
  const balance = await provider.getBalance(address);
  console.log(address, ethers.formatEther(balance), "ETH");
});
</script>

要点:

  • v6 用 ethers.BrowserProvider(v5 是 ethers.providers.Web3Provider
  • getSigner() 现在返回 Promise,要 await
  • ethers.formatEther(v6)替代了 ethers.utils.formatEther(v5)

v5 vs v6 对照

场景v5v6
浏览器 Providernew ethers.providers.Web3Provider(eth)new ethers.BrowserProvider(eth)
RPC Providerethers.providers.JsonRpcProvider(url)ethers.JsonRpcProvider(url)
formatEtherethers.utils.formatEther(v)ethers.formatEther(v)
parseEtherethers.utils.parseEther(s)ethers.parseEther(s)
BigNumberethers.BigNumber.from(x)原生 BigInt

手写一个 signer(不依赖 ethers)

有些教学场景或轻量项目不想引入整个 ethers.js,直接靠 window.ethereum 也能拼出一个 signer:

async function myGetSigner() {
  const accounts = await window.ethereum.request({
    method: "eth_requestAccounts",
  });
  const address = accounts[0];

  return {
    address,
    async getAddress() {
      return address;
    },

    async signMessage(message) {
      const hex = typeof message === "string" ? toHex(message) : message;
      return window.ethereum.request({
        method: "personal_sign",
        params: [hex, address],
      });
    },

    async sendTransaction(tx) {
      const params = {
        from: address,
        to: tx.to,
        value: tx.value ? "0x" + BigInt(tx.value).toString(16) : "0x0",
        gas:  tx.gas   ? "0x" + BigInt(tx.gas).toString(16)  : undefined,
        data: tx.data || "0x",
      };
      return window.ethereum.request({
        method: "eth_sendTransaction",
        params: [params],
      });
    },
  };
}

// 浏览器没有 Buffer,自己写一个字符串转 hex
function toHex(str) {
  let hex = "";
  for (const ch of str) {
    hex += ch.charCodeAt(0).toString(16).padStart(2, "0");
  }
  return "0x" + hex;
}

常见踩坑

Buffer is not defined

从 Node 教程抄的 Buffer.from(...).toString('hex') 在浏览器不能用。用上面的 toHexTextEncoder

const hex = "0x" + Array.from(new TextEncoder().encode(str))
  .map(b => b.toString(16).padStart(2, "0")).join("");

BigInt 序列化

v6 起金额和 nonce 都是原生 BigInt,直接 JSON.stringify 会崩:

TypeError: Do not know how to serialize a BigInt

要么手动转字符串,要么全局 patch:

BigInt.prototype.toJSON = function () {
  return this.toString();
};

window.ethereum 是数组

装了多个钱包(MetaMask + Coinbase + OKX)后,window.ethereum 可能变成聚合对象,isMetaMask 判断失效。EIP-6963 出来后的做法:

window.addEventListener("eip6963:announceProvider", (event) => {
  console.log(event.detail.info.name, event.detail.provider);
});
window.dispatchEvent(new Event("eip6963:requestProvider"));

一句话总结

ethers v6:new BrowserProvider(window.ethereum) + await provider.getSigner();不想引 ethers 时手写 signer 也不难,注意浏览器里没 Buffer 就行。