一文解决 web3 合约 ethers 交互基础
文章参考:https://wtf.academy/ether-start/ReadContract/
安装好 erthers 后,通过 import 对其引入:
import { ethers } from "ethers";
引入完毕后,可以 get 一个 Provider 对以太坊网络进行只读访问;通过 ethers 获取 Provider 对象:
const provider = ethers.getDefaultProvider();
获得了对应的 provider 之后,接下来可以使用 async await 语法糖创建一个异步函数:
const main=async()=>{
}
接着可以在函数中通过 provider 调用 getBalance 方法去获取某个地址的余额:
//查询某个地址的余额
const balance = await provider.getBalance(`0xeB869f9835006A829120cC62d995570997e6322F`);
由于这个 balance 是一个 bigNumber 对象,我们需要通过 ethers 提供的 formatEther 方法将其转化为 eth 单位才可以获得准确的 eth 单位的数值,此时若你直接输出将会显示如下:
此时代码应为:
console.log(`0xcEb1C2fB4198E865B33d9cEaDc7d3bbEcD759aB3: ` + ethers.utils.formatEther(balance));
完整代码如下:
import { ethers } from "ethers";
const provider = ethers.getDefaultProvider();
const main = async () => {
const balance = await provider.getBalance("0xcEb1C2fB4198E865B33d9cEaDc7d3bbEcD759aB3");
console.log(`0xcEb1C2fB4198E865B33d9cEaDc7d3bbEcD759aB3: ` + ethers.utils.formatEther(balance));
}
main()
输出结果为:
在正式发布主网合约前我们一般是在测试网络上部署对应的合约,有时候需要查询测试网络的数据,那么就需要使用 providers 下的 JsonRpcProvider 指定网络,代码如下:
const providerLocal = new ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`)
这个地址是我本地的测试网络,我是使用 Ganache 搭建的本地网络:
此时的地址是 HTTP://127.0.0.1:7545
,我们指定后即可链接到本地测试网。
此时我们复制其中一个地址对其进行查询余额: 修改第一点中的代码中查询的地址以及 provider 对象:
const balance = await providerLocal.getBalance("0x4c63Fd8500Dc73dd72cB999c5Cf305F2c846b2f8");
此时完整代码如下:
import { ethers } from "ethers";
const providerLocal = new ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`)
const main = async () => {
const balance = await providerLocal.getBalance("0x4c63Fd8500Dc73dd72cB999c5Cf305F2c846b2f8");
console.log(`0x4c63Fd8500Dc73dd72cB999c5Cf305F2c846b2f8: ` + ethers.utils.formatEther(balance));
}
main()
结果如下:
我们可以通过 provider 提供的 getNetwork 方法查询对应的provider 所链接的网络,在此我们在一个示例中同时链接本地网络以及主网,直接通过对应的 provider 调用 getNetwork 方法可以得到网络标识:
import { ethers } from "ethers";
const providerLocal = new ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`)
const provider = ethers.getDefaultProvider();
const main = async () => {
const balance = await providerLocal.getBalance("0x4c63Fd8500Dc73dd72cB999c5Cf305F2c846b2f8");
console.log(`0x4c63Fd8500Dc73dd72cB999c5Cf305F2c846b2f8: ` + ethers.utils.formatEther(balance));
const networkLocal = await providerLocal.getNetwork();//getNetwork
const networkMain = await provider.getNetwork();//getNetwork
console.log("network:", networkLocal);
console.log("networkMain:", networkMain);
}
main()
此时结果如下:
此时可以看到对应的网络。
这些查询都是通过 provider 查询的,provider 可查询在区块网络中的只读信息,查询标题所述的内容只需要调用以下方法:
具体使用如下:
const blockNumber = await provider.getBlockNumber();
const gasPrice = await provider.getGasPrice();
const feeData = await provider.getFeeData();
const block = await provider.getBlock([某个区块号]);
const code = await provider.getCode([合约地址]]);
在这里需要注意 gasPrice 是一个 bigNumber 需要使用 erthers 中的 format 对其格式化:
console.log("gas 价:", ethers.utils.formatUnits(gasPrice));
而 feeData 是一个对象数组:
可以使用不同的值,例如:
console.log("建议 gas 价:", ethers.utils.formatUnits(feeData.gasPrice));
在使用合约中的方法时,有不消耗 gas 的只读方法,也有修改状态变量需要 gas 的方法,那么在 ethers 中由于两者的特性不同,分为了只读合约和可读写合约,只读合约不需要钱包对其进行操作,可读写合约需要钱包进行操作。
只读合约是创建一个对合约操作的对象后,只能对合约的只读方法进行调用。
首先指定链接的节点:
import { ethers } from "ethers";
const providerLocal = new ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`);
接着需要我们获取合约的 abi:
const ContractAbi = '[{"inputs": [{"internalType": "address","name": "_owner","type": "address"}],"name": "setOwner","outputs": [],"stateMutability": "nonpayable","type": "function"},{"inputs": [],"stateMutability": "nonpayable","type": "constructor"},{"inputs": [],"name": "getOwner","outputs": [{"internalType": "address","name": "","type": "address"}],"stateMutability": "view","type": "function"}]';
合约的 abi 若你是 remix 进行部署的,可以在这里找到: 有了 abi 后我们还需要你部署后的合约的地址:
const ContractAddress = '0x4562645a0d04a536000d5E10a494C6d07Be1e314';
最后我们使用 ethers 的 Contract 创建合约对象,需要传入合约地址、abi 以及 provider:
const _Contract = new ethers.Contract(ContractAddress, ContractAbi, providerLocal);
此时我们就拥有了一个 Contract 对象,可以使用这个 Contract 对象对里面的方法进行调用了:
const ownerVal = await _Contract.getOwner();
console.log("ownerVal:", ownerVal);
完整代码如下:
import { ethers } from "ethers";
const providerLocal = new ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`);
//创建合约对象
const ContractAbi = '[{"inputs": [{"internalType": "address","name": "_owner","type": "address"}],"name": "setOwner","outputs": [],"stateMutability": "nonpayable","type": "function"},{"inputs": [],"stateMutability": "nonpayable","type": "constructor"},{"inputs": [],"name": "getOwner","outputs": [{"internalType": "address","name": "","type": "address"}],"stateMutability": "view","type": "function"}]';
const ContractAddress = '0x4562645a0d04a536000d5E10a494C6d07Be1e314';
const _Contract = new ethers.Contract(ContractAddress, ContractAbi, providerLocal);
const main = async () => {
const ownerVal = await _Contract.getOwner();
console.log("ownerVal:", ownerVal);
}
main()
在上一点中,我们使用的 abi 并不是非常友好,咱们编写 abi 还有一种比较简单的 函数签名的方式 编写abi:
const ContractAbi = [
"function getOwner()view public returns(address)",
];
其实简单点来说你就把函数签名进行复制过来就ok了,由于我们只是一个只读合约,复制一个只读方法即可,这样也可以完成上一点的内容。
此时运行代码后:
进行可写合约时,我们需要某一个账户去支付消耗的 gas,那么此时需要创建一个账户。
创建一个账户可以使用私钥进行创建,此时我们使用 ganache 打开后随便选择一个账户的私钥信息复制: 随后在接着上一点只读合约代码中进行更改,添加以下代码记录私钥内容:
//私钥
const privateKey = 'bde959799bd8bd8e3d27686087a53b121276f2ea8edff5f213ef799adff9ffee'
随后使用 eth 的 Wallet 方法创建钱包对象,需要传入一个私钥以及一个对应的 provider:
const wallet = new ethers.Wallet(privateKey, providerLocal)
接着我们把原来创建只读合约时指定的 provider 替换成 钱包对象:
此时即可创建一个读写合约,不过此时我们还需要更改 ABI 内容,毕竟现在的 ABI 只有一个只读方法:
const ContractAbi = [
"function getOwner()view public returns(address)",
"function setOwner(address _owner) public"
];
接着我们可以调用 setOwner 设置 owner 的值:
await _Contract.setOwner("0x503063dD8f114059B09FD5bC953E71fc14a1d672");
完整代码如下:
import { ethers } from "ethers";
const providerLocal = new ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`);
//私钥
const privateKey = 'bde959799bd8bd8e3d27686087a53b121276f2ea8edff5f213ef799adff9ffee'
const wallet = new ethers.Wallet(privateKey, providerLocal)
//读写合约
const ContractAddress = '0x4562645a0d04a536000d5E10a494C6d07Be1e314';
const ContractAbi = [
"function getOwner()view public returns(address)",
"function setOwner(address _owner) public"
];
const _Contract = new ethers.Contract(ContractAddress, ContractAbi, wallet);
const main = async () => {
//显示原本的 owner
let ownerVal = await _Contract.getOwner();
console.log("原本的 owner:", ownerVal);
//调用 setOwner 更改 owner
await _Contract.setOwner("0x503063dD8f114059B09FD5bC953E71fc14a1d672");
//更改后的 owner
ownerVal = await _Contract.getOwner();
console.log("更改后的 owner:", ownerVal);
}
main()
最后结果如下:
合约部署主要是通过合约工厂方法 ContractFactory 对合约进行部署,需要传入部署合约的 ABI、字节码、以及一个钱包对象(毕竟需要花钱)。
既然已经知道了合约部署的主要方法,那么接下来操作就简单了,首先指定 provider:
import { ethers } from "ethers";
const providerLocal = new ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`);
接着创建钱包:
//创建钱包
const privateKey = 'bde959799bd8bd8e3d27686087a53b121276f2ea8edff5f213ef799adff9ffee';
const wallet = new ethers.Wallet(privateKey, providerLocal);
得到 ABI 和字节码:
//若构造函数有参数那么需要在 abi 中加上构造函数
const abi = [
"function getOwner()view public returns(address)",
"function setOwner(address _owner) public"
];
//字节码
const bytecode = '608060405234801561001057600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506101d1806100606000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c806313af40351461003b578063893d20e814610057575b600080fd5b61005560048036038101906100509190610144565b610075565b005b61005f6100b8565b60405161006c9190610180565b60405180910390f35b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610111826100e6565b9050919050565b61012181610106565b811461012c57600080fd5b50565b60008135905061013e81610118565b92915050565b60006020828403121561015a576101596100e1565b5b60006101688482850161012f565b91505092915050565b61017a81610106565b82525050565b60006020820190506101956000830184610171565b9291505056fea2646970667358221220486a69de337d6a3ede1bd3652b7f28e79df06d6e07dfc307e656a54ff44c42f464736f6c63430008110033';
字节码在这里找: 接着直接调用工厂合约:
//工厂合约部署
const _Contract = await new ethers.ContractFactory(abi, bytecode, wallet).deploy();
deploy() 方法表示部署,之后就等链上确认,部署完毕:
await _Contract.deployed();//等待合约部署完毕
可以查看当前合约的地址和一些部署详情:
console.log("合约地址:", _Contract.address);
console.log("部署合约后的交易详情:", _Contract.deployTransaction);
最后还可以调用以下方法,完整代码如下:
import { ethers } from "ethers";
const providerLocal = new ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`);
//创建钱包
const privateKey = 'bde959799bd8bd8e3d27686087a53b121276f2ea8edff5f213ef799adff9ffee';
const wallet = new ethers.Wallet(privateKey, providerLocal);
//若构造函数有参数那么需要在 abi 中加上构造函数
const abi = [
"function getOwner()view public returns(address)",
"function setOwner(address _owner) public"
];
//字节码
const bytecode = '608060405234801561001057600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506101d1806100606000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c806313af40351461003b578063893d20e814610057575b600080fd5b61005560048036038101906100509190610144565b610075565b005b61005f6100b8565b60405161006c9190610180565b60405180910390f35b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610111826100e6565b9050919050565b61012181610106565b811461012c57600080fd5b50565b60008135905061013e81610118565b92915050565b60006020828403121561015a576101596100e1565b5b60006101688482850161012f565b91505092915050565b61017a81610106565b82525050565b60006020820190506101956000830184610171565b9291505056fea2646970667358221220486a69de337d6a3ede1bd3652b7f28e79df06d6e07dfc307e656a54ff44c42f464736f6c63430008110033';
const main = async () => {
//工厂合约部署
const _Contract = await new ethers.ContractFactory(abi, bytecode, wallet).deploy();
await _Contract.deployed();//等待合约部署完毕
console.log("合约地址:", _Contract.address);
console.log("部署合约后的交易详情:", _Contract.deployTransaction);
//调用方法
//显示原本的 owner
let ownerVal = await _Contract.getOwner();
console.log("原本的 owner:", ownerVal);
//调用 setOwner 更改 owner
await _Contract.setOwner("0x7A85F346BbC42769cE13910fCF878211A767FF1F");
//更改后的 owner
ownerVal = await _Contract.getOwner();
console.log("更改后的 owner:", ownerVal);
}
main()
结果如下:
上一点我们已经知道如何创建钱包,那么接下来就通过钱包对某个地址发起转账。
创建一个转账的交易只需要在对应的一个钱包基础上创建一个 tx 对象:
const tx = {
to: "0xB464C27f8d481D3a733f91B6059A2AF26CC9e5AB",
value: ethers.utils.parseEther("30");//此时是 eth 作为单位
}
这个 tx 对象中的 to 表示向某个地址转账,value 表示转账的金额,在此是用 utils 中的 parseEther 将转账的金额单位设置为 eth,转账 30 个eth。
完整代码如下:
import { ethers } from "ethers";
const providerLocal = new ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`);
//私钥
const privateKey = 'bde959799bd8bd8e3d27686087a53b121276f2ea8edff5f213ef799adff9ffee'
const wallet = new ethers.Wallet(privateKey, providerLocal)
const tx = {
to: "0xB464C27f8d481D3a733f91B6059A2AF26CC9e5AB",
value: ethers.utils.parseEther("30")//此时是 eth 作为单位
}
const main = async () => {
//发送交易 返回一个交易详情
const receipt = await wallet.sendTransaction(tx);
//等待确认
await receipt.wait();
console.log(receipt);
}
main()
此时我设置的账户是第一个: 运行后交易完毕:
在 ganache 中查看对应的 eth 余额,发现另一个账户新增了 30,我选择的账户减少了 30:
你也可以打印对应的交易详情:
账户还可以查看对应的信息:
> import { ethers } from "ethers"; const providerLocal = new
> ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`);
>
> //私钥 const privateKey =
> 'bde959799bd8bd8e3d27686087a53b121276f2ea8edff5f213ef799adff9ffee'
> const wallet = new ethers.Wallet(privateKey, providerLocal) const tx =
> {
> to: "0xB464C27f8d481D3a733f91B6059A2AF26CC9e5AB",
> value: ethers.utils.parseEther("30")//此时是 eth 作为单位 }
>
> console.log("私钥:" + wallet.privateKey); console.log("交易次数:" + (await
> wallet.getTransactionCount()));
在平常的开发测试中,使用 alchemy 可以使我们在连接到各个节点,例如 goerli 测试网。
alchemy 的 官网:alchemy.com
注册之后,打开 Dashboard 点击 create App 创建应用: 输入对应的项目名信息,选择对应的网络即可创建: 在你创建的内容中,点击 view key 可以查看链接的 PRC: 等下我们就在 ethers 中使用 https 的链接方式:
首先我们需要将一个合约部署在测试网:
// SPDX-License-Identifier:MIT
pragma solidity ^0.8.17;
contract test{
address owner;
constructor(){
owner=msg.sender;
}
event Transfer(address indexed msg, address indexed to, uint256 indexed number);
function getOwner()view public returns(address){
return owner;
}
function setOwner(address _owner) public{
owner=_owner;
emit Transfer(msg.sender, _owner, 10);
}
}
部署完毕后开始测试。
刚刚部署的合约中有一个事件:
event Transfer(address indexed msg, address indexed to, uint256 indexed number);
我们可以调用 setOwner 方法对其触发,触发完毕后,开始编写 ethers 代码。
首先引入 eth 并且指定对应的网络,这个时候你就需要拿你在 alchemy 中得到的 https 链接过来了:
import { ethers } from "ethers";
const provider = new ethers.providers.JsonRpcProvider('这里是你的alchemy 链接');
指定完毕后,需要你的这个事件的 abi:
const abi = [
"event Transfer(address indexed msg, address indexed to, uint256 indexed number)"
];
接着是你这个合约部署的地址:
const address = '0x37F64f179e4549580a15e20c2Ee89B776003Ca30';
创建一个合约的操作对象:
const contract = new ethers.Contract(address, abi, provider);
此时我们就需要准备使用 contract 的 queryFilter 获取当前合约的事件了。queryFilter 接收 3 个参数,分别是 事件名、起始区块和结束区块。此时我们就需要得到对应的起始区块和结束区块,我们可以对这个范围内的事件信息做检索,找到我们当前合约所释放的时间。(注意,这个区块范围是你查找的这个事件的区间)
接着通过 getBlockNumber 获取当前区块高度:
const height= await provider.getBlockNumber();
随后我们可以通过 queryFilter 检索这个区块区间内的时间事件了:
const transferEvents = await contract.queryFilter('Transfer', height - 5000, height);
随后可以打印出第一个,当然你也可以遍历输出:
console.log(transferEvents[0]);
由于我是昨天晚上释放的事件,所以我就减去了 5000 的区块范围,否则就找不到了。
完整代码如下:
import { ethers } from "ethers";
const provider = new ethers.providers.JsonRpcProvider(`https://eth-goerli.g.alchemy.com/v2/1YDyu2dfaCyIDHFsVfI_0qzBDYC19qAA`)
const abi = [
"event Transfer(address indexed msg, address indexed to, uint256 indexed number)"
];
const address = '0x37F64f179e4549580a15e20c2Ee89B776003Ca30';
const contract = new ethers.Contract(address, abi, provider);
const height = await provider.getBlockNumber();
const transferEvents = await contract.queryFilter('Transfer', height - 5000, height);
console.log('事件:');
console.log(transferEvents[0]);
监听合约事件我们使用 contract.on 或者 contract.once,一个是监听一个是只监听一次。
我们可以通过这个监听事件监听某一个交易所的充值提现,例如监听币安的提现充值。
咱们可以在币安的帮助文档可以找到合约地址:https://www.binance.com/zh-CN/support/faq/360040487711
进去查看后可以找到事件:
我们可以查看币安的合约事件,从而得到 abi:
那么此时已经得到了地址和abi,那么我们开始编写对应监听币安的 transfer 事件。首先我们可以在 alchemy.com 中创建对应的网络 RPC 地址供我们链接到以太坊网络,复制后编写代码:
import { ethers } from "ethers";
const provider = new ethers.providers.JsonRpcProvider('更改为你的 RPC 也就是那个 HTTPS(这里是主网)');
接着弄一个变量存那个合约地址:
const address = '0xdac17f958d2ee523a2206206994597c13d831ec7';
创建 abi:
const abi = [
"event Transfer(address indexed from, address indexed to, uint value)"
];
接着创建一个对应的合约对象:
const contract = new ethers.Contract(address, abi, provider);
接着开始使用 contract.on 对合约进行监听,contract.on 第一个参数是你的事件名,接着是一个function,这个function接收的参数是对应的事件参数,可以得到该事件参数的值:
console.log("币安的合约 transfer 监听:");
contract.on('Transfer', (from, to, value) => {
console.log(
'从 ' + from + ' 转账到' + to + ' ' + ethers.utils.formatUnits(ethers.BigNumber.from(value))
);
});
完整代码如下:
import { ethers } from "ethers";
const provider = new ethers.providers.JsonRpcProvider('https://eth-mainnet.g.alchemy.com/v2/OcBb0EJSh2QoQQoaAO30ndviX4r6GRd0');
const address = '0xdac17f958d2ee523a2206206994597c13d831ec7';
const abi = [
"event Transfer(address indexed from, address indexed to, uint value)"
];
const contract = new ethers.Contract(address, abi, provider);
console.log("币安的合约 transfer 监听:");
contract.on('Transfer', (from, to, value) => {
console.log(
'从 ' + from + ' 转账到' + to + ' ' + ethers.utils.formatUnits(ethers.BigNumber.from(value),6)
//引:6为token有效小数位。查到的余额除以有效小数位才是实际余额
);
});
结果如下:
在以太坊中 1 个 eth 等于 10^18 wei,在 ethers 中提供了相关的函数给我们互相之间进行转化。
查看合约是不是 ERC721 其实简单,直接通过provider 指定了 RPC 后,创建合约对象去调用 supportsInterface 接口即可。
首先指定 provider:
import { ethers } from "ethers";
const provider = new ethers.providers.JsonRpcProvider([这里改成你指定的RPC]);
接着是对应的 abi 和 address :
const abi = [
"function supportsInterface(bytes4) public view returns(bool)",//改成自己的
];
const address = "合约地址";
接着创建合约以及对应的传入接口id:
const contract = new ethers.Contract(address, abi, provider);
const isERC721 = await contract.supportsInterface('0x80ac58cd');
最后打印即可:
console.log('isERC721?', isERC721)
接口 id 我们可以在以太坊改进建议 EIP 中得到,例如你要得到 721 的就去看 721的: https://eips.ethereum.org/EIPS/eip-721:
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!