走进web3的世界从Ethers开始(二)

300 阅读5分钟

最近在学习web3所以根据自己的学习内容每天写一篇笔记出来,算是联系也算是巩固! 希望自己早日可以找到web3的工作。 image.png

在我看来ethers就是传统开发中的axios,他使得我们可以非常快速简单的访问区块链上的信息。那么ethers究竟为我们提供了哪些功能呢?让我们来一起学习一下吧!

今天我们来学习ethers为我们提供的另一个类Contract类:

Contract

ethers中,Contract类是部署在以太坊网络上的合约(EVM字节码)的抽象。通过它,我们可以非常容易的对合约进行读取call和交易transaction,并可以获得交易的结果和事件。上面解释有一点官方,那么我再说一下比较易懂解释:其实Contract就是给我们提供了一个可以与链上合约交互的一个类。那么他是怎么交互的呢,和链上的合约交互都需要我们提前知道哪些信息呢?下面让我们一起研究一下吧。

Contract对象分为两类,只读和可读写。只读Contract只能读取链上合约信息,执行call操作,即调用合约中viewpure的函数,而不能执行交易transaction。创建这两种Contract变量的方法有所不同:

  • 只读Contract:参数分别是合约地址,合约abiprovider变量(只读)。
const contract = new ethers.Contract(`address`, `abi`, `provider`);
  • 可读写Contract:参数分别是合约地址,合约abisigner变量。Signer签名者是ethers中的另一个类,用于签名交易,之后我们会讲到。
const contract = new ethers.Contract(`address`, `abi`, `signer`);

注意 ethers中的call指的是只读操作,与solidity中的call不同。

那么我们开始进行第一步:读取合约信息

1.创建provider:

// 声明只读合约的规则:
// 参数分别为合约地址`address`,合约ABI `abi`,Provider变量`provider`
// const contract = new ethers.Contract(`address`, `abi`, `provider`);
import { ethers } from "ethers";

// 利用Alchemy的rpc节点连接以太坊网络
// 准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 
const ALCHEMY_MAINNET_URL = '你的API Key';
const provider = new ethers.JsonRpcProvider(ALCHEMY_MAINNET_URL);

2.获取要读取合约的地址和合约的ABI(Application Binary Interface)

在这里我要解释一下什么是ABI呢,官方一点的解释为:ABI (Application Binary Interface) 是与以太坊智能合约交互的标准。其实就有点类似我们传统开发中的接口,告诉我们合约都有哪些方法,然后他的入参和出参等等一些信息(我是这样理解的,瞎bb)。如果你对与ABI不是很了解,请移步这里WTF Solidity教程第27讲: ABI编码

ethers中支持两种abi填法:

  • 方法1.  直接输入合约abi。我们可以从remix的编译页面中复制,在本地编译合约时生成的artifact文件夹的json文件中得到,或者从etherscan开源合约的代码页面得到。(单是这种方法获得ABI我们人类几乎看不懂,所以我更推荐第二种)
  • 方法2.  由于abi可读性太差,ethers创新的引入了Human-Readable Abi(人类可读abi)。我们可以通过function signatureevent signature来写abi

好了,到目前为止我们已经集齐了创建一个合约对象的所有参数啦,那么让我们动手创建一下吧:

My Web3文件夹中创建03_ReadContract文件夹,并在其中创建文件ReadContract.js

image.png

// 声明只读合约的规则:
// 参数分别为合约地址`address`,合约ABI `abi`,Provider变量`provider`
// const contract = new ethers.Contract(`address`, `abi`, `provider`);
import { ethers } from "ethers";

// 利用Alchemy的rpc节点连接以太坊网络
// 准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 
const ALCHEMY_MAINNET_URL = '你的API Key';
const provider = new ethers.JsonRpcProvider(ALCHEMY_MAINNET_URL);

// 第2种输入abi的方式:输入程序需要用到的函数,逗号分隔,ethers会自动帮你转换成相应的abi
// 人类可读abi,以ERC20合约为例
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 () => {
    // 2. 读取DAI合约的链上信息(IERC20接口合约)
    const nameDAI = await contractDAI.name()
    const symbolDAI = await contractDAI.symbol()
    const totalSupplDAI = await contractDAI.totalSupply()
    console.log("\n2. 读取DAI合约信息")
    console.log(`合约地址: ${addressDAI}`)
    console.log(`名称: ${nameDAI}`)
    console.log(`代号: ${symbolDAI}`)
    console.log(`总供给: ${ethers.formatEther(totalSupplDAI)}`)
    const balanceDAI = await contractDAI.balanceOf('vitalik.eth')
    console.log(`Vitalik持仓: ${ethers.formatEther(balanceDAI)}\n`)
}

main()

然后在控制台中运行命令node 03_ReadContract/ReadContract.js

image.png

这样我们就大功告成啦,成功的访问了链上的DAI合约的信息。

上面我们已经学习到了如何使用只读的Contract对象,那么我们接下来再去学习一下如何在两个地址发送ETH。

我们需要先在alchemy创建一个链接测试网络的App。如下图 image.png

然后我们我们先去学习一下两个新的类Signer签名者类和Wallet钱包类。

ethers中,Signer签名者类是以太坊账户的抽象,可用于对消息和交易进行签名,并将签名的交易发送到以太坊网络,并更改区块链状态。Signer类是抽象类,不能直接实例化,我们需要使用它的子类:Wallet钱包类。Wallet类继承了Signer类,并且开发者可以像包含私钥的外部拥有帐户(EOA)一样,用它对交易和消息进行签名。

ethers为我们提供了一些创建Wallet实例的方法让我们一起来看一下:

方法1:创建随机的wallet对象

我们可以利用ethers.Wallet.createRandom()函数创建带有随机私钥的Wallet对象。该私钥由加密安全的熵源生成,如果当前环境没有安全的熵源,则会引发错误。

// 创建随机的wallet对象
const wallet1 = ethers.Wallet.createRandom()

方法2:用私钥创建wallet对象

私钥的获取可以从我们创建的wallet1中取得wallet1.privateKey

// 利用私钥和provider创建wallet对象
const privateKey = 'wallet1.privateKey | 你的私钥'
const wallet2 = new ethers.Wallet(privateKey, provider)

方法3:从助记词创建wallet对象

私钥的获取可以从我们创建的wallet1中取得wallet1.mnemonic.phrase

我们已知助记词的情况下,可以利用ethers.Wallet.fromPhrase()函数创建Wallet对象。

// 从助记词创建wallet对象
const wallet3 = ethers.Wallet.fromPhrase(wallet1.mnemonic.phrase | 你的助记词)

其他方法:通过JSON文件创建wallet对象

以上三种方法即可满足大部分需求,当然还可以通过ethers.Wallet.fromEncryptedJson解密一个JSON钱包文件创建钱包实例,JSON文件即keystore文件,通常来自GethParity等钱包,通过Geth搭建过以太坊节点的个人对keystore文件不会陌生。

到这里相信我们都能创建出来一个wallet实例了,接下来我们就可以去使用此实例去实现发送ETH操作啦

发送ETH

我们可以利用Wallet实例来发送ETH。首先,我们要构造一个交易请求,在里面声明接收地址to和发送的ETH数额value。交易请求TransactionRequest类型可以包含发送方from,nonce值nounce,请求数据data等信息,之后会更详细介绍。

    // 创建交易请求,参数:to为接收地址,value为ETH数额
    const tx = {
        to: address1,
        value: ethers.parseEther("0.001")
    }

然后,我们就可以利用Wallet类的sendTransaction来发送交易,等待交易上链,并获得交易的收据,非常简单。

    //发送交易,获得收据
    const txRes = await wallet2.sendTransaction(tx)
    const receipt = await txRes.wait() // 等待链上确认交易
    console.log(receipt) // 打印交易的收据

那么我们把他们整理到一起去试一下吧!

My Web3文件夹中创建04_SendETH文件夹,并在其中创建文件SendETH.js

image.png

// 利用Wallet类发送ETH
import { ethers } from "ethers";

// 利用Alchemy的rpc节点连接以太坊测试网络
const ALCHEMY_SEPOLIA_URL = '你申请的Connect to Alchemy URL';
const provider = new ethers.JsonRpcProvider(ALCHEMY_SEPOLIA_URL);

// 创建随机的wallet对象
const wallet1 = ethers.Wallet.createRandom()
const wallet1WithProvider = wallet1.connect(provider)

// 利用私钥和provider创建wallet对象
const wallet2 = new ethers.Wallet('你的私钥', provider)

// 从助记词创建wallet对象
const wallet3 = ethers.Wallet.fromPhrase(wallet1.mnemonic.phrase)

const main = async () => {
    // 1. 获取钱包地址
    const address1 = await wallet1.getAddress()
    const address2 = await wallet2.getAddress()
    const address3 = await wallet3.getAddress() // 获取地址
    console.log(`1. 获取钱包地址`);
    console.log(`钱包1地址: ${address1}`);
    console.log(`钱包2地址: ${address2}`);
    console.log(`钱包3地址: ${address3}`);
    console.log(`钱包1和钱包3的地址是否相同: ${address1 === address3}`);

    // 2. 获取助记词
    console.log(`\n2. 获取助记词`);
    console.log(`钱包1助记词: ${wallet1.mnemonic.phrase}`)
    // 注意:从private key生成的钱包没有助记词
    // console.log(wallet2.mnemonic.phrase)

    // 3. 获取私钥
    console.log(`\n3. 获取私钥`);
    console.log(`钱包1私钥: ${wallet1.privateKey}`)
    console.log(`钱包2私钥: ${wallet2.privateKey}`)

    // 4. 获取链上发送交易次数    
    console.log(`\n4. 获取链上交易次数`);
    const txCount1 = await provider.getTransactionCount(wallet1WithProvider)
    const txCount2 = await provider.getTransactionCount(wallet2)
    console.log(`钱包1发送交易次数: ${txCount1}`)
    console.log(`钱包2发送交易次数: ${txCount2}`)

    // 5. 发送ETH
    // 我们可以事先去一些网站水龙头领取一些测试网代币
    // https://testnet.help/en/ethfaucet/sepolia#log 领取地址
    console.log(`\n5. 发送ETH(测试网)`);
    // i. 打印交易前余额
    console.log(`i. 发送前余额`)
    console.log(`钱包1: ${ethers.formatEther(await provider.getBalance(wallet1WithProvider))} ETH`)
    console.log(`钱包2: ${ethers.formatEther(await provider.getBalance(wallet2))} ETH`)
    // ii. 构造交易请求,参数:to为接收地址,value为ETH数额
    const tx = {
        to: address1,
        value: ethers.parseEther("0.00000001")
    }
    // iii. 发送交易,获得收据
    console.log(`\nii. 等待交易在区块链确认(需要几分钟)`)
    const receipt = await wallet2.sendTransaction(tx)
    await receipt.wait() // 等待链上确认交易
    console.log(receipt) // 打印交易详情
    // iv. 打印交易后余额
    console.log(`\niii. 发送后余额`)
    console.log(`钱包1: ${ethers.formatEther(await provider.getBalance(wallet1WithProvider))} ETH`)
    console.log(`钱包2: ${ethers.formatEther(await provider.getBalance(wallet2))} ETH`)
}

main()

然后在控制台中运行命令node 04_SendETH/SendETH.js

image.png

wallet2实例连接到了MetaMask上 所以也可以在MetaMask看一下交易后的余额:

image.png

我们从水龙头领取了0.00000001个ETH交易后剩下了0.0009...个,大功告成啦!