winston

winston

ethers.js入门指南

这是关于 Ethers 非常简短的介绍,但它涵盖了开发人员通常需要执行的许多操作。

获取 Ethers#

先安装 Ethers 包
使用 npm :

npm i ethers

使用 yarn:

yarn add ethers

使用 pnpm:

pnpm i ethers

Ethers 中的所有内容都从其根目录和 ethers 对象中导出。 package.json 也包含了更细粒度的导入配置。

通常,此文档假定代码示例已经导入了所有来自 ethers 的导出对象,但您可以按任意方式导入所需的对象。

在 Node.js 中进行导入:

// 导入所有内容
import { ethers } from "ethers";

// 仅导入部分对象
import { BrowserProvider, parseUnits } from "ethers";

// 在特定模块中导入对象
import { HDNodeWallet } from "ethers/wallet";

在浏览器中导入 ESM:

<script type="module">
  import { ethers } from "https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.min.js";
  // 在此处编写您的代码...
</script>

一些常见术语#

为了开始接触此领域,理解可用对象类型及其职责是很有用的。

Provider#

Provider 是与区块链建立的只读连接,允许查询区块链状态,例如帐户、块或交易详情,查询事件日志或使用 call 评估只读代码。

如果您熟悉 Web3.js,则应该习惯 Provider 提供了读写访问权限。但在 Ethers 中,所有写操作都被进一步抽象化成另一个对象:Signer。

Signer#

Signer 封装了所有与账户交互的操作。帐户通常具有某个位置上的私钥,可以用于签署各种类型的有效载荷。

私钥可能位于内存中(使用 Wallet),也可以通过某个 IPC 层进行保护,例如 MetaMask,它将网站上的交互代理到浏览器插件中,使得私钥不会被网站获取,并且仅在请求用户授权时才允许进行交互。

Transaction#

要对区块链进行任何状态更改,都需要执行事务,而每次执行事务都需要支付费用。此处的 “费用” 涵盖了执行事务所需的相关成本(例如读取磁盘和执行数学运算)以及存储更新信息的成本。

如果事务回滚,则仍需要支付费用,因为验证者仍然必须花费资源尝试运行该事务以确定其是否已回滚,并且其失败的详细信息仍将被记录下来。

交易包括:将以太币发送给其他用户、部署合约或对合约进行状态更改操作。

Contract#

合约是一个已经部署到区块链上的程序,其中包含一些代码,分配了可以进行读写操作的存储单元。

当与 Provider 连接时,可以从中进行读取;如果连接到 Signer,则可以调用状态更改操作。

Receipt#

一旦将 Transaction 提交到区块链中,它将被放置在内存池(mempool)中,直到验证器决定将其包括进去。

交易的修改只有在其已包含在区块链中并生成了收据时才会生效。此时,会提供一个包括交易详细信息的收据,例如收据所属的区块,实际付费金额、使用燃料总量、所有由交易引发的事件以及它是成功还是回滚等。

连接以太坊#

与以太坊进行交互的第一步是使用 Provider 与其建立连接。

MetaMask(和其他注入程序)#

在以太坊上尝试和开始开发最快,最简便的方式是使用 MetaMask,它是一个浏览器扩展程序,可以将对象注入到窗口中,提供:

  • 对以太坊网络的只读访问权限(Provider)
  • 基于私钥的授权写入访问权限(Signer)

在请求授权方法,例如发送交易或甚至请求私钥地址时,MetaMask 会弹出一个提示用户授权的窗口。

let signer = null;

let provider;
if (window.ethereum == null) {

    // 如果没有安装 MetaMask,我们使用默认的 Provider,
    // 它由多个第三方服务支持(例如 INFURA),它们没有安装私钥,
    // 所以只有只读访问权限
    console.log("MetaMask not installed; using read-only defaults")
    provider = ethers.getDefaultProvider()

} else {

    // 连接到 MetaMask 的 EIP-1193 对象,这是一个标准的
    // 协议,允许 Ethers 通过 MetaMask 进行所有只读请求。
    provider = new ethers.BrowserProvider(window.ethereum)

    // 这也提供了一个机会来请求写操作的访问权限,
    // 这些操作将由 MetaMask 为用户管理的私钥执行。
    signer = await provider.getSigner();
}

自定义 RPC 后端#

如果您正在运行自己的以太坊节点(例如 Geth)或使用自定义第三方服务(例如 INFURA),则可以直接采用 JsonRpcProvider 通信,他们使用 link-jsonrpc 协议。

当使用自己的 Ethereum 节点或开发者基于区块链项目(如 HardhatGanache)时,可以通过 JsonRpcProvider.getSigner 方法获取访问权限。

连接到 JSON-RPC URL

// 如果不提供 url,它将连接到大多数默认节点
// http://localhost:8545。
provider = new ethers.JsonRpcProvider(url)

// 通过获取 signer 来作为账户获得写入访问权限
signer = await provider.getSigner()

用户交互#

在以太坊中,所有单位都倾向于是整数值,因为使用小数和浮点数会导致执行数学操作时出现不精确和不明显的结果。
因此,内部使用的单位(例如 wei)适用于机器可读的目的和数学计算,它们通常非常大且不太适合人类阅读。
例如,想象一下处理美元和美分;您将显示像 "$ 2.56" 这样的值。在区块链世界中,我们会将所有值保留为分(cents),以便在内部表示为 256 分。
因此,在接受用户键入的数据时,必须将其从其十进制字符串表示法(例如 "2.56")转换为最低单位整数表示法(例如 256)。当向用户显示值时则需要相反的操作。
在 Ethereum 中,一个以太币等于 10 ** 18 个 wei,一个 gwei 等于 10 ** 9 个 wei,因此值会很快变得非常大,所以提供了一些方便的函数来帮助在各种表示之间进行转换。

// 将以太中用户提供的字符串转换为 wei 值
eth = parseEther("1.0")
// 1000000000000000000n

// 将 gwei 中用户提供的字符串转换为最大基础费用对应的 wei 值
feePerGas = parseUnits("4.5", "gwei")
// 4500000000n

// 将 wei 值转换为显示在 UI 中的 ether 字符串值
formatEther(eth)
// '1.0'

// 将 wei 值转换为显示在 UI 中的 gwei 字符串值
formatUnits(feePerGas, "gwei")
// '4.5'

与区块链进行交互#

查询状态#

一旦您拥有了 Provider,您就可以读取并查询区块链上的数据。这可以用于查询当前账户状态、获取历史日志、查找合约代码等。

// 查看当前块数
await provider.getBlockNumber()
// 16988474

// 获取地址或 ENS 不可变域名对应的当前余额
balance = await provider.getBalance("ethers.eth")
// 182334002436162568n

// 由于余额是以 wei 为单位的,您可能希望将其显示
// 为 ether 的形式。
formatEther(balance)
// '0.182334002436162568'

// 获取发送事务所需的下一个 nonce
await provider.getTransactionCount("ethers.eth")
// 3

发送交易#

要向区块链写入内容,您需要访问控制某个账户的私钥。在大多数情况下,这些私钥不能直接访问,而是通过 Signer 发出请求,然后分派给一个服务(例如 MetaMask),该服务提供严格的门禁访问,并要求用户批准或拒绝操作。

// 发送交易时,value 是以 wei 为单位的,因此 parseEther
// 将以太转换为 wei。
tx = await signer.sendTransaction({
  to: "ethers.eth",
  value: parseEther("1.0")
});

// 通常您可能希望等待事务被挖掘
receipt = await tx.wait();

合约#

Contract 是一个元类,这意味着它的定义是在运行时根据传递给它的 ABI(应用二进制接口)派生出来的,从而确定可用的方法和属性。

应用二进制接口(Application Binary Interface,ABI)

由于在区块链上发生的所有操作都必须编码为二进制数据,我们需要一种简洁的方式来定义如何将通用对象(例如字符串和数字)及其二进制表示之间进行转换,以及编码调用和解释合约的方式。

对于任何您要使用的方法、事件或错误,都必须包含一个 Fragment(片段)来告诉 Ethers 它应该如何编码请求和解码结果。可以安全地排除不需要的任何方法或事件。

有几种常见的格式可用于描述 ABI。Solidity 编译器通常会倾向于转储 JSON 表示法,但手动输入 ABI 时,通常更易于使用(并且易于阅读)人类可读的 ABI 表示法,这仅是 Solidity 签名。

简化的 ERC-20 ABI 示例:

abi = [
  "function decimals() returns (string)",
  "function symbol() returns (string)",
  "function balanceOf(address addr) returns (uint)"
]

// 创建一个合约
contract = new Contract("dai.tokens.ethers.eth", abi, provider)

只读方法(即:"view" 和 "pure" 方法)#

只读方法是不能改变区块链状态的方法,但通常提供了一个简单的接口来获取与合约相关的重要数据。

以下是读取 DAI ERC-20 合同的示例:

// 合约 ABI(我们关心的片段)
abi = [
  "function decimals() view returns (uint8)",
  "function symbol() view returns (string)",
  "function balanceOf(address a) view returns (uint)"
]

// 创建一个合约;连接到 Provider,因此它只能访问只读方法(如 view 和 pure)
contract = new Contract("dai.tokens.ethers.eth", abi, provider)

// 符号名称
sym = await contract.symbol()
// 'DAI'

// 代币使用的小数位数
decimals = await contract.decimals()
// 18n

// 读取账户的代币余额
balance = await contract.balanceOf("ethers.eth")
// 201469770000000000000000n

// 格式化余额以供人类阅读,例如在 UI 中显示
formatUnits(balance, decimals)
// '201469.77'

状态改变方法#

修改 ERC-20 合约的状态

abi = [
  "function transfer(address to, uint amount)"
]

// 连接一个 Signer;可以发出更改状态的事务,
// 这将花费账户的以太币
contract = new Contract("dai.tokens.ethers.eth", abi, signer)

// 发送 1 DAI
amount = parseUnits("1.0", 18);

// 发送交易
tx = await contract.transfer("ethers.eth", amount)

// 目前该交易已发送到 mempool,
// 但尚未包含。 因此我们...

// ...等待交易被包含。
await tx.wait()

强制调用(模拟)一个状态更改方法

abi = [
  "function transfer(address to, uint amount) returns (bool)"
]

// 仅连接到 Provider,因此我们只需要读取访问权限
contract = new Contract("dai.tokens.ethers.eth", abi, provider)

amount = parseUnits("1.0", 18)

// 使用静态调用有许多限制,但通常可以用于预检查事务。
await contract.transfer.staticCall("ethers.eth", amount)
// true

// 我们还可以模拟另一个账户发起的交易
other = new VoidSigner("0x643aA0A61eADCC9Cc202D1915D942d35D005400C")
contractAsOther = contract.connect(other.connect(provider))
await contractAsOther.transfer.staticCall("ethers.eth", amount)
// true

监听事件#

当添加命名事件的监听器时,事件参数会被析构。始终有一个额外的参数传递给监听器,它是 EventPayload,其中包括有关事件的更多信息,包括过滤器和用于移除该侦听器的方法。

监听 ERC-20 事件:

abi = [
  "event Transfer(address indexed from, address indexed to, uint amount)"
]

// 创建一个合约;连接到 Provider,因此它只能访问只读方法(如 view 和 pure)
contract = new Contract("dai.tokens.ethers.eth", abi, provider)

// 开始监听任何转账事件
contract.on("Transfer", (from, to, _amount, event) => {
  const amount = formatEther(_amount, 18)
  console.log(`${ from } => ${ to }: ${ amount }`);

  // The `event.log` has the entire EventLog

  // Optionally, convenience method to stop listening
  event.removeListener();
});

// 同上
contract.on(contract.filters.Transfer, (from, to, amount, event) => {
  // See above
})

// 监听任何转账到 "ethers.eth" 的事件
filter = contract.filters.Transfer("ethers.eth")
contract.on(filter, (from, to, amount, event) => {
  // `to` will always be equal to the address of "ethers.eth"
});

// 监听任何事件,无论是否在 ABI 中,由于未知事件可能被捕获,因此不会解构参数。
contract.on("*", (event) => {
  // The `event.log` has the entire EventLog
});

查询历史事件#

在大范围的区块内查询时,某些后端可能会速度过慢,可能会返回错误或者不提供结果取决于该后端的服务器策略。

查询 ERC-20 历史事件:

abi = [
  "event Transfer(address indexed from, address indexed to, uint amount)"
]

// 创建一个合约;连接到 Provider,因此它只能访问只读方法(如 view 和 pure)
contract = new Contract("dai.tokens.ethers.eth", abi, provider)

// 查询最近的 100 个块中是否有任何转账
filter = contract.filters.Transfer
events = await contract.queryFilter(filter, -100)

// 获取事件数组大小
events.length
// 90

// 第一个匹配的事件
events[0]
// EventLog { ... }

// 查询所有时间段内的任何转账到 "ethers.eth" 的事件
filter = contract.filters.Transfer("ethers.eth")
events = await contract.queryFilter(filter)

// 第一个匹配的事件
events[0]
// EventLog { ... }

签名消息#

私钥可以做很多事情,不仅可以签署交易以授权它,还可以用于签署其他形式的数据,并且可以验证这些数据以及达到其他目的。

例如,签署消息可用于证明对账户的所有权,这样网站就可以用它来验证用户并登录。

// 我们的签名者;签署消息不需要 Provider
signer = new Wallet(id("test"))
// Wallet {
//   address: '0xC08B5542D177ac6686946920409741463a15dDdB',
//   provider: null
// }

message = "sign into ethers.org?"

// 对消息进行签名
sig = await signer.signMessage(message);
// '0xefc6e1d2f21bb22b1013d05ecf1f06fd73cdcb34388111e4deec58605f3667061783be1297d8e3bee955d5b583bac7b26789b4a4c12042d59799ca75d98d23a51c'

// 验证消息;请注意地址与签署者匹配
verifyMessage(message, sig)
// '0xC08B5542D177ac6686946920409741463a15dDdB'

在签名消息上构建的许多其他高级协议被用于允许一个私钥授权其他用户转移其代币,从而允许由其他人支付转移的交易费用。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。