跳转至

Ethers

推荐使用较新的 v6 版本:https://docs.ethers.org/v6/getting-started/

npm install ethers --save

与 v5 相比改动较大,改动或者迁移可参考:

导入

  • CommonJS 方式导入

在 Node 环境下导入

const ethers = require('ethers');
  • ES6 方式导入

在 Node 环境下导入,需要在 package.json 中添加配置

import { ethers } from 'ethers';

const main = async () => {
    // code
};

main();

// 运行:node hello.js
{
  "type": "module"
}

在 Web 中导入需要加 module 属性

<script type="module">
    import { ethers } from "https://cdnjs.cloudflare.com/ajax/libs/ethers/6.7.0/ethers.min.js";
    // Your code here...
</script>

Provider

Provider 类是对以太坊网络连接的抽象,提用于连接以及访问(只读)区块链及其状态

  • getDefaultProvider

ethers 内置了一些公用的 RPC 方便测试使用,但访问速度有限

const provider = ethers.getDefaultProvider();
  • jsonRpcProvider

可以通过 Infura 或 Alchemy 等节点服务商获取个人的URL,更快的连接以太坊网络

const INFURA_URL = 'https://sepolia.infura.io/v3/xxx';
const provider = new ethers.JsonRpcProvider(INFURA_URL)

方法

  • getNetwork() 查询当前连接网络
const network = await provider.getNetwork();
console.log(network);  // Network {},不能直接打印
console.log(network.toJSON());  // { name: 'mainnet', chainId: '1' }
  • getBlockNumber() 查询当前区块高度
const blockNumber = await provider.getBlockNumber();
console.log(blockNumber);  // 18363810
  • getBlock() 查询指定区块信息
const block = await provider.getBlock(blockNumber);  // 指定区块高度
console.log(block);

// Block {
//   provider: JsonRpcProvider {},
//   number: 18363810,
//   hash: '0x5b7eeece2fce2816d7bd65d3a5833150adc25f9cf974ab254a9f4cf32c66b69e',
//   timestamp: 1697469815,
//   parentHash: '0xd64a0639df5147b01ed3d9488f10df1456190902a3b60062f4ca69fad5bfcafb',
//   nonce: '0x0000000000000000',
//   difficulty: 0n,
//   gasLimit: 30000000n,
//   gasUsed: 13188166n,
//   miner: '0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5',
//   extraData: '0x6265617665726275696c642e6f7267',
//   baseFeePerGas: 13127404159n
// }
  • getFeeData() 查询当前建议的Gas设置
const feeData = await provider.getFeeData();
console.log(feeData);  // 返回数据值类型为bigint

// FeeData {
//   gasPrice: 10734724525n,
//   maxFeePerGas: 22439449050n,
//   maxPriorityFeePerGas: 1000000000n
// }
  • getBalance() 获取指定地址余额
const balanceWei = await provider.getBalance(`vitalik.eth`);  // 原生支持ENS
const balanceETH = ethers.formatEther(balanceWei);  // 将单位从默认的wei转换为ETH
console.log(`ETH Balance of vitalik: ${balanceETH} ETH`);
  • getTransactionCount() 查询指定地址历史交易次数
const txCount = await provider.getTransactionCount("vitalik.eth");
console.log(txCount);  // 1142
  • getCode() 查询指定合约的 bytecode
const code = await provider.getCode("0xc778417e063141139fce010982780140aa0cd5ab");
console.log(code);

// 0x6060604052361561010f5763ffffffff7c010000......0000060003504166306fdde03811461011157806307......d59296451a3a0c1683c70029

Signer & Wallet

Signer 类是对以太坊账户的抽象,可用于给消息和交易签名并发送到网络更改区块链状态

Signer 类是抽象类,不能直接实例化,需要用它的子类:Wallet

创建wallet对象

  • 随机私钥
// 私钥由加密安全的熵源生成
const wallet1 = ethers.Wallet.createRandom()
const wallet1WithProvider = wallet1.connect(provider)  // 否则provider为null

console.log(wallet1)
// HDNodeWallet {
//   provider: null,
//   address: '0x7E86Fde7fAEF38c45e4F7f0f40B46987A0F25Da9',
//   publicKey: '0x03d6ab9b45bbb2bbd57b4401f2c3899f4dcf76de1b5472c9d9f19761860da72367',
//   fingerprint: '0xd946a991',
//   parentFingerprint: '0x618c5125',
//   mnemonic: Mnemonic {
//     phrase: 'report edge fatigue embark chef obtain craft always address faith solar cluster',
//     password: '',
//     wordlist: LangEn { locale: 'en' },
//     entropy: '0xb6e8c94f242275310c803c036a433996'
//   },
//   chainCode: '0x720f1b0f727938711cb8b83c44c1e66a6b7f9a50fe9f9942909580f77bb64667',
//   path: "m/44'/60'/0'/0/0",
//   index: 0,
//   depth: 5
// }
  • 指定私钥
// 指定私钥和provider
const privateKey = '0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b'
const wallet2 = new ethers.Wallet(privateKey, provider)

console.log(wallet2)
// Wallet {
//   provider: JsonRpcProvider {},
//   address: '0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2'
// }

// 这种方式不能获取助记词
const mnemonic = wallet2.mnemonic  // undefined
  • 从助记词创建
const wallet3 = ethers.Wallet.fromPhrase(mnemonic.phrase)
  • 从keystore文件创建
const wallet4 = ethers.Wallet.fromEncryptedJson(keystore.json)

获取钱包信息

const address = await wallet.getAddress()  // 获取钱包地址
const pk = wallet2.privateKey  // 获取私钥
const phrase = wallet.mnemonic.phrase  // wallet2不能获取

// 获取交易次数
const txCount = await provider.getTransactionCount(wallet)  // 参数也可以是address

发送交易

const tx = {
    to: address1,  // 接收地址
    value: ethers.parseEther("0.001")  // 发送数额
}
// sendTransaction包含发送地址from、请求数据data、nonce等信息
const receipt = await wallet2.sendTransaction(tx)
await receipt.wait()  // 等待链上确认交易

// 打印交易详情
console.log(receipt)
// TransactionResponse {
//   provider: JsonRpcProvider {},
//   blockNumber: null,
//   blockHash: null,
//   index: undefined,
//   hash: '0x20282a0bfcfa53adfe8bc354f14b67952bbec8c50058f600233334efc05a1c46',
//   type: 2,
//   to: '0x6dC688B011ca7d4BdbcdB3Cf42E7F934A44835d6',
//   from: '0xE8187F49c358B3989Be39D34312C6fb421Cc4341',
//   nonce: 82,
//   gasLimit: 21000n,
//   gasPrice: undefined,
//   maxPriorityFeePerGas: 1000000000n,
//   maxFeePerGas: 1000001864n,
//   data: '0x',
//   value: 1000000000000000n,
//   chainId: 11155111n,
//   signature: Signature { r: "0x4d292bf9fedabd389f3844965d5ea41450010bf40ac0669d2e0f7f58566fa3b9", s: "0x0403c675c0c44dd4b7f5ba369e6e16c04fe0827d40cbf3b7b23e83908ad8b1f7", yParity: 1, networkV: null },
//   accessList: []
// }

Contract

Contract 类是对合约(EVM字节码)的抽象,用于与合约交互

  • 只读

只能调用合约中的 view 和 pure 函数

const contract = new ethers.Contract('address', 'abi', 'provider');

// 比如读取WETH合约链上信息
const main = async () => {
    const nameWETH = await contractWETH.name()
    const symbolWETH = await contractWETH.symbol()
    const totalSupplyWETH = await contractWETH.totalSupply()
}
main()

ABI(Application Binary Interface) 是与以太坊智能合约交互的标准接口,类似于API

可以直接从编译后生成的artifact路径下的json文件中获取,如果已开源还可以从EtherScan中获取

[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":
...省略...
"type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[],"name":"Pause","type":"event"},{"anonymous":false,"inputs":[],"name":"Unpause","type":"event"}]

不过这种可读性比较差,ethers 引入了 Human-Readable ABI

const abiERC20 = [
    "function name() view returns (string)",
    "function symbol() view returns (string)",
    "function totalSupply() view returns (uint256)",
    "function balanceOf(address) view returns (uint)",
];
  • 可读写

可以执行 transaction

const contract = new ethers.Contract('address', 'abi', 'signer');

// 也可以先声明一个只读合约,然后再将只读可约转为可写合约
const contract2 = contract.connect(signer)


const tx = await contract.METHOD_NAME(args [, overrides])  // 发送交易
await tx.wait()  // 等待链上确认交易
// METHOD_NAME 为调用的函数名
// argsw 为参数
// overrides 为可选参数
//     gasPrice
//     gasLimit
//     value:调用时传入的ether(单位是wei)
//     nonce

ContractFactory

// 部署合约
const factoryERC20 = new ethers.ContractFactory(abiERC20, bytecodeERC20, wallet);  // 实例化
const contractERC20 = await factoryERC20.deploy("Test Token", "TTC")  // 部署,并传入constructor参数
console.log(contractERC20.target)  // 合约地址
console.log(contractERC20.deploymentTransaction())  // 合约部署的交易详情
await contractERC20.waitForDeployment()  // 等待合约部署上链

// 与部署成功后的合约交互
console.log(`合约名称: ${await contractERC20.name()}`)
console.log(`合约代号: ${await contractERC20.symbol()}`)
let tx = await contractERC20.mint("10000")
await tx.wait()

模拟交易

以太坊节点提供了 eth_call 方法,让用户可以模拟一笔交易,根据返回结果预知交易能否成功

Ethers 将其封装在了 contract 对象的 staticCall() 方法中方便调用

const tx = await contract.函数名.staticCall(函数参数, {override})
// override
//     from: 可以模拟任何msg.sender调用
//     value: msg.value
//     blockTag: 执行时的区块高度
//     gasPrice
//     gasLimit
//     nonce

// 模拟 addressX 给V神转账 1 DAI
const tx = await contractDAI.transfer.staticCall("vitalik.eth", ethers.parseEther("1"), {from: addressX})
// 如果交易可以成功则返回true,否则报错并返回原因

事件相关

// Solidity 定义 Event
event Transfer(address indexed from, address indexed to, uint256 amount);
// 有索引的(indexed)变量存储在 Topics,反之存在 Data
// 最多可以包含4个索引

20231021110758

如图,Address 是合约地址,Topics[0] 是事件哈希:keccak256("Transfer(address,address,uint256)"),Topics[1] 和 Topics[2] 分别是 from 和 to 地址,Data 是转账数量。

检索事件

  • queryFilter

contract.queryFilter('事件名', 起始区块, 结束区块)

const block = await provider.getBlockNumber()  // 获取当前区块高度
const transferEvents = await contract.queryFilter('Transfer', block - 10, block)
console.log(transferEvents[0])  // 打印第一个Transfer事件

监听事件

  • 持续监听:contract.on("事件名", 事件发生时要调用的函数)
  • 监听一次:contract.once("事件名", 事件发生时要调用的函数)
// 持续监听USDT合约的Transfer事件,事件发生时打印结果
contractUSDT.on('Transfer', (from, to, value)=>{
    console.log(
        `${from} -> ${to} ${ethers.formatUnits(ethers.getBigInt(value),6)}`
    )
})
  • 监听过滤

contract.filters.EVENT_NAME( ...args )

contract.filters.Transfer(fromAddress, toAddress)  // 过滤所有从a发给b的Transfer事件
contract.filters.Transfer(fromAddress)  // 过滤所有从a发出的~
contract.filters.Transfer(null, toAddress)  // 过滤所有发给b的~
contract.filters.Transfer(null, [to1Address, to2Address])  // 过滤所有发给b或c的~

实例

const provider = new ethers.JsonRpcProvider(Infura_URL);
const addressUSDT = '0xdac17f958d2ee523a2206206994597c13d831ec7'  // USDT合约地址
const filterBinanceIn = '0x28C6c06298d514Db089934071355E5743bf21d60'  // 某人Binance的热钱包地址
// 构建ABI
const abi = [
  "event Transfer(address indexed from, address indexed to, uint value)",
  "function balanceOf(address) public view returns(uint)",
];
// 构建合约对象
const contractUSDT = new ethers.Contract(addressUSDT, abi, provider);

// 查询accountA的余额
const balanceUSDT = await contractUSDT.balanceOf(filterBinanceIn)
console.log(`USDT余额: ${ethers.formatUnits(balanceUSDT,6)}\n`)

// 过滤Transfer事件:从accountBinance转出USDT
let filterToBinanceOut = contractUSDT.filters.Transfer(accountBinance);
console.log(filterToBinanceOut);
contractUSDT.on(filterToBinanceOut, (res) => {
    console.log('---------监听USDT转出交易所--------');
    console.log(
        `${res.args[0]} -> ${res.args[1]} ${ethers.formatUnits(res.args[2],6)}`
    )
});

// 过滤Transfer事件:转入accountBinance
let filterBinanceIn = contractUSDT.filters.Transfer(null, accountBinance);
console.log(filterBinanceIn);
contractUSDT.on(filterBinanceIn, (res) => {
    console.log('---------监听USDT转入交易所--------');
    console.log(
    `${res.args[0]} -> ${res.args[1]} ${ethers.formatUnits(res.args[2],6)}`
    )
});

单位转换

  • 转为ether:ethers.formatEther("变量")
ethers.formatEther("1")  // '0.000000000000000001'
ethers.formatEther("1000000000000000000")  // '1.0'
  • 任意转:ethers.utils.formatUnits("变量", "单位");

小转大输出类型为BigInt,大转小输出类型为String

// 默认转换为ether
ethers.formatUnits("1");  // 1000000000000000000n
ethers.formatUnits("1000000000000000000");  // '1.0'

// 可以指定单位
ethers.formatUnits("1", "ether");  // 1000000000000000000n
ethers.formatUnits("1", "gwei")  // 1000000000n
ethers.formatUnits("1", "wei")  // 1n

// 也可以转为任意长度
ethers.parseUnits("1", 3)  // 1000n

BigInt

在 Ethers V5 中需要引入 BigNumber 库来实现,四则运算需要使用对应方法

// import { BigNumber } from "ethers";
const { BigNumber } = require('ethers')

const HOUR = BigNumber.from(60 * 60);  // 3600s
const DAY = HOUR.mul(24);  // 86400s
const WEEK = DAY.mul(7);
const MONTH = DAY.mul(30);
const YEAR = DAY.mul(365);

const decimals = BigNumber.from("1000000000000000000");
const oneGwei = ethers.BigNumber.from("1000000000");

oneGwei.add(1).toString()  // 加
oneGwei.sub(1).toString()  // 减
oneGwei.mul(1).toString()  // 乘
oneGwei.div(1).toString()  // 除
oneGwei.mod(1).toString()  // 取模
oneGwei.pow(1).toString()  // 幂运算
oneGwei.abs(1).toString()  // 绝对值
oneGwei.eq(1000000000)  // 比较,是否相等,返回bool值

在 Ether V6 中,BigNumber 库被移除,替换为 JS ES2020 原生的 BigInt 库

const value1 = 1000n
const value2 = ethers.getBigInt("1000")  // 将其它类型转换为 BigInt
console.log(value1 + 1n)  // 基础运算直接用
console.log(value1 == value2)  // 比较是否相等

最后更新: 2023-10-25