最近在学习web3所以根据自己的学习内容每天写一篇笔记出来,算是联系也算是巩固! 希望自己早日可以找到web3的工作。
在我看来ethers
就是传统开发中的axios
,他使得我们可以非常快速简单的访问区块链上的信息。那么ethers
究竟为我们提供了哪些功能呢?让我们来一起学习一下吧!
今天我们来学习ethers
为我们提供的另一个类Contract
类:
Contract
类
在ethers
中,Contract
类是部署在以太坊网络上的合约(EVM
字节码)的抽象。通过它,我们可以非常容易的对合约进行读取call
和交易transaction
,并可以获得交易的结果和事件。上面解释有一点官方,那么我再说一下比较易懂解释:其实Contract
就是给我们提供了一个可以与链上合约交互的一个类。那么他是怎么交互的呢,和链上的合约交互都需要我们提前知道哪些信息呢?下面让我们一起研究一下吧。
Contract
对象分为两类,只读和可读写。只读Contract
只能读取链上合约信息,执行call
操作,即调用合约中view
和pure
的函数,而不能执行交易transaction
。创建这两种Contract
变量的方法有所不同:
- 只读
Contract
:参数分别是合约地址,合约abi
和provider
变量(只读)。
const contract = new ethers.Contract(`address`, `abi`, `provider`);
- 可读写
Contract
:参数分别是合约地址,合约abi
和signer
变量。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 signature
和event signature
来写abi
。
好了,到目前为止我们已经集齐了创建一个合约对象的所有参数啦,那么让我们动手创建一下吧:
在My Web3
文件夹中创建03_ReadContract
文件夹,并在其中创建文件ReadContract.js
// 声明只读合约的规则:
// 参数分别为合约地址`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
这样我们就大功告成啦,成功的访问了链上的DAI合约的信息。
上面我们已经学习到了如何使用只读的Contract
对象,那么我们接下来再去学习一下如何在两个地址发送ETH。
我们需要先在alchemy
创建一个链接测试网络的App
。如下图
然后我们我们先去学习一下两个新的类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
文件,通常来自Geth
, Parity
等钱包,通过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
// 利用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
wallet2
实例连接到了MetaMask上 所以也可以在MetaMask看一下交易后的余额:
我们从水龙头领取了0.00000001个ETH交易后剩下了0.0009...个,大功告成啦!