etherjs02——Event事件、模拟转账、合约标准

329 阅读4分钟

读取Event事件,就是读取一些信息吧,不需要gas费,不需要和合约交互

读取Transfer事件

import dotenv from "dotenv";
import { ethers } from "ethers";

dotenv.config();
const provider = new ethers.JsonRpcProvider(process.env.INFURA_ID);
const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY, provider);

// WETH ABI,只包含我们关心的Transfer事件
const abiWETH = [
  "event Transfer(address indexed from, address indexed to, uint256 value)",
  "event Approval(address indexed owner, address indexed spender, uint256 value)"
];
// 测试网WETH地址
const addressWETH = '0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14'
// 声明合约实例
const contract = new ethers.Contract(addressWETH, abiWETH, provider)

async function main() {
  // 得到当前block
  const block = await provider.getBlockNumber()
  console.log(`当前区块高度: ${block}`);
  console.log(`打印事件详情:`);

  // 查询过去100个区块内的Transfer事件
  const fromBlock = block - 100;
  console.log(`查询区块范围: 从 ${fromBlock}${block}`);

  try {
    // 设置过滤器
    const filter = contract.filters.Transfer();

    // 查询过滤器内的事件
    const transferEvents = await contract.queryFilter(filter, fromBlock, block);

    // 打印事件详情
    if (transferEvents.length > 0) {
      console.log(`找到 ${transferEvents.length} 个 Transfer 事件`);
      transferEvents.forEach((event, index) => {
        console.log(`事件 ${index + 1}:`, event);
      });
    } else {
      console.log('没有找到任何 Transfer 事件');
    }
  } catch (error) {
    console.error('查询事件时发生错误:', error);
  }
}

main();

监听Transfer事件

  const WETHAddress = '0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14';

  const abi = [
    "event Transfer(address indexed from, address indexed to, uint256 value)"
  ];
  // 生成WETH合约对象
  const contractETH = new ethers.Contract(WETHAddress, abi, provider);

  // 持续监听WETH合约
  console.log("\n2. 利用contract.on(),持续监听Transfer事件");
  contractETH.on('Transfer', (from, to, value) => {
    console.log(
      // 打印结果
      `${from} -> ${to} ${ethers.formatUnits(value, 18)}`
    );
  });

image.png

创建事件过滤器

example:监听从币安交易所到USDT的合约转账事件

import dotenv from "dotenv";
import { ethers } from "ethers";

dotenv.config();
const provider = new ethers.JsonRpcProvider("https://rpc.ankr.com/eth");
// 合约地址
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);

async function main() {
    const balanceUSDT = await contractUSDT.balanceOf(accountBinance);
    console.log(`USDT余额: ${ethers.formatUnits(balanceUSDT, 6)}\n`);

    // 2. 创建过滤器,监听转移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)}`);
    });
}
main();

代码没问题,但是一到连接主网节点,就因为网络问题,程序运行失败,提供一个节点网络 chainlist.org/

单位转换

image.png 我们经常将数值在用户可读的字符串(以ether为单位)和机器可读的数值(以wei为单位)之间转换。例如,钱包可以为用户界面指定余额(以ether为单位)和gas价格(以gwei为单位),但是在发送交易时,两者都必须转换成以wei为单位的数值

formatUnits

  const oneGwei = ethers.getBigInt("1000000000"); // 从十进制字符串生成
  //代码参考:https://docs.ethers.org/v6/api/utils/#about-units
  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

  // 3. 解析:大单位转小单位
  // 例如将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();

StaticCall

在 ethers.js 中,你可以使用 contract.函数名.staticCall() 方法来模拟执行一个可能会改变状态的函数,但不实际向区块链提交这个状态改变。这相当于调用以太坊节点的 eth_call。这通常用于模拟状态改变函数的结果。如果函数调用成功,它将返回函数本身的返回值;如果函数调用失败,它将抛出异常。

在这里我们使用DAI合约来写demo

  1. 根据abi,address和provider创建合约实例
const provider = new ethers.JsonRpcProvider("https://rpc.ankr.com/eth");
const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY, provider);
// DAI的ABI
const abiDAI = [
  "function balanceOf(address) public view returns(uint)",
  "function transfer(address, uint) public returns (bool)",
];
// DAI合约地址(主网)
const addressDAI = '0x6B175474E89094C44Da98b954EedeAC495271d0F' // DAI Contract
// 创建DAI合约实例
const contractDAI = new ethers.Contract(addressDAI, abiDAI, provider)
  1. 读取钱包中的DAI持仓
const address = await wallet.getAddress()
console.log("\n1. 读取测试钱包的DAI余额")
const balanceDAI = await contractDAI.balanceOf(address)
console.log(`DAI持仓: ${ethers.formatEther(balanceDAI)}\n`)

image.png

  1. 模拟从Vitalik钱包向Vitalik账户转1个DAI,这个会成功,因为Vitalik有这么多钱。
// 发起交易 
const tx = await contractDAI.transfer.staticCall("vitalik.eth", ethers.parseEther("1"), { from: await provider.resolveName("vitalik.eth") })
console.log(`交易会成功吗?:`, tx)

image.png

4.尝试从自己的钱包转账1DAI给Vitalik,这个不会成功,因为我没有这么多DAI,报错原因应该会是

const tx2 = await contractDAI.transfer.staticCall("vitalik.eth", ethers.parseEther("10000"), { from: address })
console.log(`交易会成功吗?:`, tx2)

image.png

合约标准

我们听说过ERC20合约标准,用于创建虚拟币,比如ETH ERC721用于创建NFT等数字收藏品等

  • ERC20: 用于创建同质化代币,每个代币是相同的,适用于支付和奖励等场景。
  • ERC721: 用于创建非同质化代币,每个代币是独一无二的,适用于数字艺术品和收藏品等场景。

在这里我们尝试调用使用ERC721创建的合约

// 合约abi
const abiERC721 = [
  "function name() view returns (string)",
  "function symbol() view returns (string)",
  "function supportsInterface(bytes4) public view returns(bool)",
];
// ERC721的合约地址,这里用的BAYC
const addressBAYC = "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d"
// 创建ERC721合约实例
const contractERC721 = new ethers.Contract(addressBAYC, abiERC721, provider)

// 1. 读取ERC721合约的链上信息
const nameERC721 = await contractERC721.name()
const symbolERC721 = await contractERC721.symbol()
console.log("\n1. 读取ERC721合约信息")
console.log(`合约地址: ${addressBAYC}`)
console.log(`名称: ${nameERC721}`)
console.log(`代号: ${symbolERC721}`)

跟我们前面学习到的ERC20标准创建的合约的使用规则一样,也是通过地址、abi和provider创建合约实例,然后读取信息

ERC165检测合约接口,ERC165是一个标准接口,用于检测智能合约是否实现了某个接口。它允许合约查询另一个合约是否实现了特定的功能。

const selectorERC721 = "0x80ac58cd"
const isERC721 = await contractERC721.supportsInterface(selectorERC721)
console.log("\n2. 利用ERC165的supportsInterface,确定合约是否为ERC721标准")
console.log(`合约是否为ERC721标准: ${isERC721}`)