Ethers.js 极简入门
01 RPC
1、infura 注册账号,create API KEY,申请自己的 RPC 节点,避免网络拥堵;
2、连接以太坊
2.1 使用公共 RPC:ethers.getDefaultProvider()
2.2 使用个人 RPC:
const INFURA_MAINNET_URL = "RPC_ADDRESS";
const provider = new ethers.JsonRpcProvider(INFURA_MAINNET_URL);
provider 方法和区块链的交互是异步的;
获取某个地址的 ETH 余额:provider.getBakance('vitalik.eth')
02 Provider
Provider 类是对以太坊账户的抽象,它不接触用户私钥,只能读取链上信息,这一点比 web3.js 更安全。
常用方法:
- getBalance():查询余额
- getNetwork():查询 provider 连接到了哪条链
- getBlockNumber():查询当前区块高度
- getTransactionCount():查询某个钱包的历史交易次数
- getFeeData():查询当前建议的 gas 设置
- getBlock():查询区块信息
- getCode():查询某个地址的合约 bytecode,参数为合约地址
03 合约交互
Contract 类是以太坊合约(EVM 字节码)的抽象,开发者可以通过 Contract 进行读取和交易(call and transcation),并获得交易的结果和事件。
创建合约变量
1.1 只读合约:const contract = new ethers.Contract(address, abi, provider)
1.2 可读写合约:const contract = new ethers.Contract(address, abi, signer)
2. 读取合约信息
2,1 创建只读 Contract 实例
方法一:直接输入合约 abi,可以直接去 etherscan 的合约代码里复制,也可以从 remix 的编译页面里复制
方法二:ethers 引入了 Human-Readable Abi,开发者可以通过 function signature
和 event signature
来写 abi
2.2 读取链上信息
示例代码:
// 方法一
const abiWETH =
'[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view",...太长后面省略...';
const addressWETH = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; // WETH Contract
const contractWETH = new ethers.Contract(addressWETH, abiWETH, provider);
// 方法二
const abiERC20 = [
"function name() view returns (string)",
"function symbol() view returns (string)",
"function totalSupply() view returns (uint256)",
"function balanceOf(address) view returns (uint)",
];
const addressDAI = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; // DAI Contract
const contractDAI = new ethers.Contract(addressDAI, abiERC20, provider);
// 读取链上信息
const main = async () => {
const nameWETH = await contractWETH.name();
const symbolWETH = await contractWETH.symbol();
const totalSupplyWETH = await contractWETH.totalSupply();
console.log(`合约名称:${nameWETH}`);
console.log(`代号:${symbolWETH}`);
const balanceWETH = await contractWETH.balanceOf(myAddress);
console.log(`我的持仓: ${ethers.formatEther(balanceWETH)}\n`);
};
04 发送 ETH
签名者类(Signer)是以太坊账户的抽象,可用于对消息和交易进行签名,并将签名的交易发送到以太坊网络,并更改区块链状态。
钱包类(Wallet),继承签名者类,开发者可以向包含私钥的 EOA 账户一样,用它对交易和消息进行签名。
创建 Wallet 实例
// 随机创建 wallet 对象
const wallet1 = new ethers.Wallet.createRandom();
// 私钥创建 wallet 对象
const privateKey = PRIVATE_KEY;
const wallet2 = new ethers.Wallet(privateKey, provider);
// 助记词创建 wallet 对象
const wallet3 = new ethers.Wallet.fromMnemonic(mnemonic.phrase);
// JSON 文件创建 wallet 对象(keystore 文件)
发送 ETH
const tx = { to: address1, value: ethers.parseEther("0.001") };
const receipt = await wallet2.sendTransacion(tx); // 发送交易
await receipt.wait(); // 等待链上交易确认
console.log(receipt); // 打印交易详情
05 合约交互
const provider = new ethers.JsonRpcProvider(ALCHEMY_TESTNET_URL);
const dev1_private_key = DEV1_PRIVATE_KEY;
const dev1_wallet = new ethers.Wallet(dev1_private_key, provider);
// 定义合约 abi(需要用哪些合约方法就定义什么)和合约地址
const abiWETH = [
"function balanceOf(address) public view returns(uint)",
"function deposit() public payable",
"function transfer(address, uint) public returns (bool)",
"function withdraw(uint) public",
];
// WETH Contract
const addressWETH = "0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9";
// 构建合约对象
const contractWETH = new ethers.Contract(addressWETH, abiWETH, dev1_wallet);
const main = async () => {
const dev1_address = await dev1_wallet.getAddress();
const balance = await contractWETH.balanceOf(dev1_address);
console.log(ethers.formatEther(balance));
// 调用 deposit 方法:和合约方法里接收的参数相同
const tx = await contractWETH.deposit({ value: ethers.parseEther("0.001") });
await tx.wait(); // 等待交易完成,tx 是交易详情
const balanceWETH_deposit = await contractWETH.balanceOf(dev1_address);
console.log(balanceWETH_deposit);
};
main();
06 检索事件
事件(Event):相当于合约向外部世界发送通知的机制。事件会在合约的不同的函数或操作中被触发,当事件被触发时,相关数据会被记录在区块链上(可以在 etherscan 里查询到),供外部应用程序监听和处理。
function vote(unit256 candidateId) public {
// 投票逻辑
// 触发投票事件
emit VoteCast(msg.sender, candidateId)
}
代码示例:
const provider = new ethers.JsonRpcProvider(ALCHEMY_TESTNET_URL);
// 要检索的事件必须包含在合约的 abi 中
const abiWETH = [
"event Transfer(address indexed from, address indexed to, uint amount)",
];
const addressWETH = WETH_SEPOLIA_ADDRESS;
const contract = new ethers.Contract(addressWETH, abiWETH, provider);
const main = async () => {
const block = await provider.getBlockNumber();
console.log(`当前区块高度${block}`); // 获取过去10个区块内的Transfer事件,并打印出第1个
// contract.queryFilter('事件名',起始区块,结束区块)
const transferEvents = await contract.queryFilter(
"Transfer",
block - 10,
block + 10
);
const amount = ethers.formatUnits(
ethers.getBigInt(transferEvents[0].args["amount"]),
"ether"
); // 读取事件的解析结果。
console.log(
`地址 ${transferEvents[0].args["from"]} 转账${amount} WETH 到地址 ${transferEvents[0].args["to"]}`
);
};
main();
08 监听智能合约
// 监听 USDT 的实时交易数据
const provider = new ethers.JsonRpcProvider(ALCHEMY_MAINNET_URL);
const abi = [
"event Transfer(address indexed from, address indexed to, uint value)",
];
const contractUSDT = new ethers.Contract(USDT_MAIN_ADDRESS, abi, provider);
const main = async () => {
console.log("\n2. 利用contract.on(),持续监听Transfer事件");
// contract.on('eventName', function) - 持续监听
// contract.once - 监听一次
contractUSDT.on("Transfer", (from, to, value) => {
console.log(
// 打印结果
`${from} -> ${to} ${ethers.formatUnits(ethers.getBigInt(value), 6)}`
);
});
};
09 事件过滤
构建过滤器:const filter = contract.filters.EVENT_NAME(...args)
// 监听交易所的 USDT 转账
const provider = new ethers.JsonRpcProvider(ALCHEMY_MAINNET_URL);
// USDT 合约地址
const addressUSDT = "0xdac17f958d2ee523a2206206994597c13d831ec7";
// 交易所钱包地址
const accountBinance = "0x28C6c06298d514Db089934071355E5743bf21d60";
// 构建 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);
const balanceUSDT = await contractUSDT.balanceOf(accountBinance);
console.log(`USDT余额: ${ethers.formatUnits(balanceUSDT, 6)}\n`);
// 创建过滤器,监听转移USDT进交易所
console.log("\n2. 创建过滤器,监听USDT转进交易所");
let filterBinanceIn = contractUSDT.filters.Transfer(null, accountBinance);
console.log("过滤器详情:");
console.log(filterBinanceIn);
contractUSDT.on(filterBinanceIn, (res) => {
console.log("---------监听USDT进入交易所--------");
console.log(
`${res.args[0]} -> ${res.args[1]} ${ethers.formatUnits(res.args[2], 6)}`
);
});
// 创建过滤器,监听交易所转出USDT
let filterToBinanceOut = contractUSDT.filters.Transfer(accountBinance);
console.log("\n3. 创建过滤器,监听USDT转出交易所");
console.log("过滤器详情:");
console.log(filterToBinanceOut);
contractUSDT.on(filterToBinanceOut, (res) => {
console.log("---------监听USDT转出交易所--------");
console.log(
`${res.args[0]} -> ${res.args[1]} ${ethers.formatUnits(res.args[2], 6)}`
);
});
10 单位转换
Name | Decimals |
---|---|
wei | 0 |
kwei | 3 |
mwei | 6 |
gwei | 9 |
szabo | 12 |
finney | 15 |
ether | 18 |
formatUnits(变量, 单位):小单位转大单位,比如wei -> ether
,在显示余额时很有用
console.group("\n2. 格式化:小单位转大单位,formatUnits");
console.log(ethers.formatUnits(oneGwei, 0));
// '1000000000'
console.log(ethers.formatUnits(oneGwei, "gwei"));
// '1.0'
console.log(ethers.formatUnits(oneGwei, 9));
// '1.0'
console.log(ethers.formatUnits(oneGwei, "ether"));
// `0.000000001`
console.log(ethers.formatUnits(1000000000, "gwei"));
// '1.0'
console.log(ethers.formatEther(oneGwei));
// `0.000000001` 等同于formatUnits(value, "ether")
console.groupEnd();
parseUnits(变量, 单位):大单位转小单位,比如 ether -> wei,在将用户输入的值转为 wei 为单位的数值很有用
// 例如将ether转换为wei:parseUnits(变量, 单位),parseUnits默认单位是 ether
// 代码参考:https://docs.ethers.org/v6/api/utils/#about-units
console.group("\n3. 解析:大单位转小单位,parseUnits");
console.log(ethers.parseUnits("1.0").toString());
// { BigNumber: "1000000000000000000" }
console.log(ethers.parseUnits("1.0", "ether").toString());
// { BigNumber: "1000000000000000000" }
console.log(ethers.parseUnits("1.0", 18).toString());
// { BigNumber: "1000000000000000000" }
console.log(ethers.parseUnits("1.0", "gwei").toString());
// { BigNumber: "1000000000" }
console.log(ethers.parseUnits("1.0", 9).toString());
// { BigNumber: "1000000000" }
console.log(ethers.parseEther("1.0").toString());
// { BigNumber: "1000000000000000000" } 等同于parseUnits(value, "ether")
console.groupEnd();