抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

ethers.js_day01

1. ethers.js的安装

1
npm install --save ethers
  • 出现的问题:

a. 按照 B站教程来的话,需要修改package.js中的配置,如:

1
2
3
4
5
6
{
"type": "module",
"dependencies": {
"ethers": "^5.6.9"
}
}

在这里:加入了 ''type": "module"之后,require()这种引包的方式就不能使用了,网上搜了一下说是 ES6模块(没系统学过JavaScript,不是很懂,只是记录一下,避免继续犯错),得需要使用 import的方式导入 ethers

1
const { ethers } = require("ethers");
1
import { ethers } from "ethers";

b. B站视频中在vscode上安装的ethers包的命令为:

1
npm install ethers@5.6.9

这行代码会让代码无法运行,报错如下:

image-20230512211104606

2. 编写 Hello Vitalik

2.1 编译器:

  • 在线编译器 —– playcode
  • 本地编译器 —– VSCode

2.2 第一段代码

  • 示例代码:
1
2
3
4
5
6
7
import { ethers } from "ethers";
const provider = ethers.getDefaultProvider();
const main = async () => {
const balance = await provider.getBalance(`vitalik.eth`);
console.log(`ETH Balance of vitalik: ${ethers.utils.formatEther(balance)} ETH`);
}
main()
  • 解读代码
1
2
3
4
const main = async () => {
const balance = await provider.getBalance(`vitalik.eth`);
console.log(`ETH Balance of vitalik: ${ethers.utils.formatEther(balance)} ETH`);
}

代码的意思:

这是一段使用 JavaScript 编写的代码,使用了 ethers.js 库与以太坊节点进行交互。

这段代码定义了一个异步函数 main(),在函数中:

  1. 使用 provider.getBalance() 函数获取 vitalik.eth 地址上的以太币余额,并将结果存储在 balance 变量中。这里的 provider 是一个 ethers.js 库中提供的以太坊节点对象,它用于与以太坊网络进行通信。
  2. 使用 ethers.utils.formatEther() 函数将以太币余额从 wei 单位转换为以太币单位,并将结果打印到控制台中。

因为 getBalance() 函数是异步函数,它会从以太坊网络中读取数据,因此需要使用 await 关键字等待它完成读取操作。在等待 getBalance() 函数完成后,balance 变量将包含返回的余额值,可以对其进行处理并打印到控制台中。

ethers.js_day02

3. Provider 提供器

3.1 运行如下代码报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 导入ethers包
import { ethers } from "ethers";
// playcode免费版不能安装ethers,用这条命令,需要从网络上import包(把上面这行注释掉)
// import { ethers } from "https://cdn-cors.ethers.io/lib/ethers-5.6.9.esm.min.js";

// 利用Alchemy的rpc节点连接以太坊网络
// 准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md
const ALCHEMY_MAINNET_URL = 'https://eth-mainnet.g.alchemy.com/v2/oKmOQKbneVkxgHZfibs-iFhIlIAl6HDN';
const ALCHEMY_GOERLI_URL = 'https://eth-goerli.alchemyapi.io/v2/GlaeWuylnNM3uuOo-SAwJxuwTdqHaY5l';
// 连接以太坊主网
const providerETH = new ethers.JsonRpcProvider(ALCHEMY_MAINNET_URL)
// 连接Goerli测试网
const providerGoerli = new ethers.JsonRpcProvider(ALCHEMY_GOERLI_URL)

const main = async () => {
// 利用provider读取链上信息
// 1. 查询vitalik在主网和Goerli测试网的ETH余额
console.log("1. 查询vitalik在主网和Goerli测试网的ETH余额");
const balance = await providerETH.getBalance(`vitalik.eth`);
const balanceGoerli = await providerGoerli.getBalance(`vitalik.eth`);
// 将余额输出在console(主网)
console.log(`ETH Balance of vitalik: ${ethers.formatEther(balance)} ETH`);
// 输出Goerli测试网ETH余额
console.log(`Goerli ETH Balance of vitalik: ${ethers.formatEther(balanceGoerli)} ETH`);

// 2. 查询provider连接到了哪条链
console.log("\n2. 查询provider连接到了哪条链")
const network = await providerETH.getNetwork();
console.log(network.toJSON());

// 3. 查询区块高度
console.log("\n3. 查询区块高度")
const blockNumber = await providerETH.getBlockNumber();
console.log(blockNumber);

// 4. 查询 vitalik 钱包历史交易次数
console.log("\n4. 查询 vitalik 钱包历史交易次数")
const txCount = await providerETH.getTransactionCount("vitalik.eth");
console.log(txCount);

// 5. 查询当前建议的gas设置
console.log("\n5. 查询当前建议的gas设置")
const feeData = await providerETH.getFeeData();
console.log(feeData);

// 6. 查询区块信息
console.log("\n6. 查询区块信息")
const block = await providerETH.getBlock(0);
console.log(block);

// 7. 给定合约地址查询合约bytecode,例子用的WETH地址
console.log("\n7. 给定合约地址查询合约bytecode,例子用的WETH地址")
const code = await providerETH.getCode("0xc778417e063141139fce010982780140aa0cd5ab");
console.log(code);

}

main()
  • 报错结果如下:

  • image-20230513145732581

  • 报错原因是说 JsonRpcProvider 不是一个构造器,到官方文档上查看,JsonRpcProvider的用法是:

  • image-20230513145925225

    1
    2
    3
    4
    // 连接以太坊主网
    const providerETH = new ethers.providers.JsonRpcProvider(ALCHEMY_MAINNET_URL)
    // 连接Goerli测试网
    const providerGoerli = new ethers.providers.JsonRpcProvider(ALCHEMY_GOERLI_URL)
  • RPC的连接

3.2 与本地的ganache获取连接

  • 代码:
1
2
3
4
5
6
7
8
9
import { ethers } from "ethers";
const provider = ethers.getDefaultProvider("HTTP://127.0.0.1:8545");// 我设置的端口号为8545

const address = ''; // 替换为需要查询余额的账户地址
provider.getBalance(address).then((balance) => {
console.log(`Account balance: ${ethers.utils.formatEther(balance)} ETH`);
}).catch((err) => {
console.error('Error:', err);
});

image-20230514194933229

3.3 与 metamask(小狐狸🦊钱包)建立连接

  • 3.3.1 需要到 Alchemy 上获取RPC 的节点

    image-20230514195349665

1
2
3
4
5
6
7
8
const provider = ethers.getDefaultProvider(""); // 从Alchemy获取

const address = ''; // 替换为需要查询余额的账户地址
provider.getBalance(address).then((balance) => {
console.log(`Account balance: ${ethers.utils.formatEther(balance)} ETH`);
}).catch((err) => {
console.error('Error:', err);
});

image-20230514195525076

3.4 利用Provider读取链上数据

tips:const provider = new ethers.providers.JsonRpcProvider("HTTP://127.0.0.1:8545"); 这个的连接速度要比const provider = ethers.getDefaultProvider("HTTP://127.0.0.1:8545");的连接速度更快

3.5 获取指定地址的余额 – provider.getBalance(address)

代码:

1
2
3
4
5
>provider.getBalance(address).then((balance) => {
console.log(`Account balance: ${ethers.utils.formatEther(balance)} ETH`);
>}).catch((err) => {
console.error('Error:', err);
>});

解释:

这段代码是使用以太坊 JavaScript 库 Ethers.js 来查询指定以太坊地址的余额,并将其以以太币(ETH)的形式打印到控制台上。

具体来说,这段代码执行了以下步骤:

  1. 调用 provider.getBalance(address) 方法,其中 provider 是一个以太坊节点提供商,address 是要查询的以太坊地址。
  2. getBalance() 方法返回一个 Promise,该 Promise 在查询完成后返回以太坊地址的余额。
  3. 当 Promise 被解析时,then() 方法中的回调函数会被执行。该回调函数使用 ethers.utils.formatEther() 方法将余额从 wei 转换为以太,并将结果打印到控制台上。
  4. 如果 Promise 被拒绝(即查询失败),则 catch() 方法中的回调函数会被执行,该回调函数将错误信息打印到控制台上

3.6 查询provider连接到那条链 – provider.getNetwork()

代码:

1
const netWork = await provider.getNetwork();

查询结果:

image-20230515105317448

3.7 查询当前 gas price — provider.getGasPrice()

tips:返回的数据格式为BigNumber,可以用BigNumber类的toNumber()或toString() 方法转换成数字或者字符串

代码:

1
2
>const gas_price = await provider.getGasPrice();
>console.log(gas_price.toString());

运行结果:

image-20230515110805460

3.8 查询区块信息 – provider.getBlock()

代码:

1
const block = await provider.getBlock(2);

查询结果:

image-20230515111721033

3.9 查询地址的合约bytecode — provider.getCode(address)

代码:

1
2
const address = "0x44f2A5d2CFf45111E5FdcBE600CfA62Ea0386E7f"; //合约地址
const code = await providerETH.getCode(address);

查询结果:

image-20230515112347262

4.读取合约信息

4.1 创建Contract变量

4.1.1 只读Contract

tips:参数分别是合约地址,合约的abi和provider变量(只读)

代码:

1
const contract = new ethers.Contract(`address`,`abi`,`provider`)

4.1.2 可读写 Contract

tips: 参数分别是合约地址,合约的abi和singer变量。Singer签名者是ethers中另一个类,用于签名交易。

代码:

1
const contract = new ethers.Contract(`address`, `abi`, `signer`);

4.2 创建只读 Contract实例

tips:创建Contract实例需要填入三个参数,分别是合约地址,合约abi和provider变量。

tips:ehters支持两种abi填法

方法一:直接输出合约的abi。也可以像之前web3中那样使用 fs包读取 .abi文件,我觉得这种可读性更强一些。

1
2
3
4
var fs = require("fs");
var path = "";//这里是你本地abi的文件地址
var contractABI = JSON.parse(fs.readFileSync(path),toString());

参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// SPDX-License-Identifier: MIT
pragma solidity ^0.4.21;

contract CallMeChallenge {
int public value = 5;
bool public isComplete = false;

function callme() public {
isComplete = true;
}

function getValue() external view returns(int) {
return value;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { Contract, ethers } from "ethers"; // ES6模块下引入ethers包
// var fs = require("fs")
import fs from "fs"; // ES6模块下引入fs包

const provider = new ethers.providers.JsonRpcProvider("HTTP://127.0.0.1:8545");

// 读取文件的abi
let con_ABI = JSON.parse(fs.readFileSync("D:\\VSCode\\VSCode_Data\\ethersjsWorkspace\\LearningSelf\\abi\\CallMeChallenge.abi").toString())
// 合约地址
let con_Address = "0xCfD32B1C6C1b2Fb425F3d6a86F8E2ebB363b5d96";
// 创建合约
let contract = new ethers.Contract(con_Address,con_ABI,provider);
async function main() {
let slot0 = await provider.getStorageAt(con_Address,0);
console.log("slot0 = >" + slot0);

let slot1 = await provider.getStorageAt(con_Address,0);
console.log("slot1 = >" + slot1);
}

// 调用函数
main()

// 调用合约的只读函数
let value = await contract.getValue();
console.log("value => " + value);



let address = "0xfDc84b042c01F01C7cC6eF08452BA156844c3A5C";
provider.getBalance(address).then((balance)=>{
console.log(`Account balance: ${ethers.utils.formatEther(balance)} ETH`);
})

运行结果:

image-20230516120223937

注: 在Solidity中,布尔类型(bool)的默认值是false。在你的智能合约中,存储槽(slot)的位置为0,存储的变量是布尔类型(isComplete),因此默认值为false。

当你部署合约时,存储槽的初始值会被设置为false。如果在之后合约的执行过程中,调用了callme()函数,isComplete变量的值会被设置为true,存储槽的值也会相应地被更新为true。

在使用ethers查询存储槽的值时,如果存储槽的值为false,查询结果会显示为0x0。如果存储槽的值为true,查询结果会显示为0x1。

因此,如果你在查询智能合约的存储槽时得到的结果是0x0,这意味着存储槽的值为false,即isComplete变量还没有被设置为true。如果你想要获取存储槽的实际值,可以将查询结果转换为布尔类型,如下所示:

1
2
3
const slot0 = await provider.getStorageAt(contractAddress, 0);
const isComplete = ethers.utils.bigNumberify(slot0).toNumber() !== 0;
console.log(`isComplete: ${isComplete}`);

这段代码将查询结果转换为BigNumber类型,并将其转换为数字类型。如果数字不为0,isComplete变量的值为true,否则为false。

方法二:由于abi可读性太差,ethers创新的引入了人类可读ABI。开发者可以通过function signature和event signature来写ABI。

参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { Contract, ethers } from "ethers"; // ES6模块下引入ethers包
// var fs = require("fs")
import fs from "fs"; // ES6模块下引入fs包

const provider = new ethers.providers.JsonRpcProvider("HTTP://127.0.0.1:8545");

// 读取文件的abi
let con_ABI = JSON.parse(fs.readFileSync("D:\\VSCode\\VSCode_Data\\ethersjsWorkspace\\LearningSelf\\abi\\CallMeChallenge.abi").toString())
// 合约地址
let con_Address = "0xCfD32B1C6C1b2Fb425F3d6a86F8E2ebB363b5d96";
// 创建合约
let contract = new ethers.Contract(con_Address,con_ABI,provider);

let abi = [
"function callme() public",
"function getValue() external view returns(int)",
]

let contract1 = new ethers.Contract(con_Address,abi,provider);

async function demo1(){
// 调用合约的只读函数
let value = await contract1.getValue();
console.log("demo1's value => " + value);
}

demo1()

运行结果:

image-20230516121226933

4.3 更多的读取合约信息可到 官方文档 查看

5.发送ETH

5.1 Signer签名类

在ethers中,Signer签名者类是以太坊账户的抽象类,可用于对消息和交易进行签名,并将签名的交易发送到以太坊网络,并更改区块链状态。Signer类是抽象类,不能直接实例化,我们需要使用它的子类:Wallet钱包类。

5.2 Wallet钱包类

Wallet类继承了Signe类,并且开发者可以像包含私钥的外部拥有者账户(EOA)一样,用它对交易和消息进行签名。

5.2.1方法一:创建随机的wallet对象

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

tips:在密码学中,熵(entropy)是指随机性的度量。在智能合约中,熵源(entropy source)是指用于生成随机数或随机种子的源头。

在智能合约中,为了确保随机数的安全性和不可预测性,需要使用熵源来生成随机数或随机种子。通常情况下,熵源可以是外部的数据源,例如区块哈希或者时间戳等,也可以是合约内部的数据源,例如合约的存储槽或者合约的执行结果等。

需要注意的是,熵源的质量和安全性对于密码学应用非常重要。如果熵源不够随机或者不够安全,可能会导致随机数的可预测性,从而破坏密码学应用的安全性。因此,在使用熵源生成随机数或随机种子时,需要选择合适的熵源,并对其进行充分的测试和验证,以确保其安全性和不可预测性。

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

5.2.2方法二:用私钥创建wallet对象

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

Ganache中获取privateKey

image-20230516124049554

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

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

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

1
2
// 从助记词创建wallet对象
const wallet3 = new ethers.Wallet.fromMnemonic(mnemonic.phrase)

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

通过ethers.Wallet.fromEncryptedJson解密一个JSON钱包文件创建钱包实例,JSON文件即keystore文件,通常来自Geth, Parity等钱包

5.3 发送ETH

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

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

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

1
2
3
4
//发送交易,获得收据
const receipt = await wallet2.sendTransaction(tx)
await receipt.wait() // 等待链上确认交易
console.log(receipt) // 打印交易详情

5.4 代码实例

5.4.1创建Wallet实例

三种方法的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { ethers } from "ethers";

const netWork = "HTTP://127.0.0.1:8545";
const provider = new ethers.providers.JsonRpcProvider(netWork);


// let balance = await provider.getBalance("0xfDc84b042c01F01C7cC6eF08452BA156844c3A5C");
// console.log(`Account balance: ${ethers.utils.formatEther(balance)} ETH`);

/**
方法一:创建随机私钥的Wallet对象。
这种方法创建的钱包是单机的,我们需要用connect(provider)函数,连接到以太坊节点。这种方法创建的钱包可以用mnemonic获取助记词
*/

const wallet1 = ethers.Wallet.createRandom();
console.log("wallet1 =>\n",wallet1);
const wallet1WithProvider = wallet1.connect(provider);
const mnemonic = wallet1.mnemonic;
console.log("助记词:\n" , mnemonic);

/**
* 方法二:利用私钥和provider实例创建Wallet对象
* 这种方法不能获取助记词
*/

const privateKey = "0x9b139483dc6f6c8782fda2301e9b65a75b3d084d79e79e04c64683bebfa1741d";
const wallet2 = new ethers.Wallet(privateKey, provider);
console.log("wallet2 =>\n",wallet2);


/**
* 方法三:利用助记词创建Wallet对象
*/

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

console.log("wallet3 => ",wallet3);

运行结果:

image-20230516181207290

image-20230516181225239

5.4.2 获取钱包地址

代码:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 获取钱包地址
*/

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}`);

运行结果:

image-20230516181912331

5.4.3 获取助记词

利用钱包对象的mnemonic成员获取助记词:

1
console.log(`钱包1助记词: ${wallet1.mnemonic.phrase}`)

结果:

image-20230516182108758

5.4.4 利用钱包对象的 privateKey 成员获取私钥:

1
console.log(`钱包2私钥: ${wallet2.privateKey}`)

运行结果:

image-20230516182407128

到ganache上查看:

image-20230516182437775

结果是相同的。

5.4.5 获取钱包在链上的交互次数

利用 getTransactionCount()函数获取钱包在链上的交互次数

1
2
3
4
const txCount1 = await wallet1WithProvider.getTransactionCount()
const txCount2 = await wallet2.getTransactionCount()
console.log(`钱包1发送交易次数: ${txCount1}`)
console.log(`钱包2发送交易次数: ${txCount2}`)

运行结果:

image-20230516182720008

ganache上查看:

image-20230516182746669

5.4.6 发送ETH

我们用wallet2wallet1发送0.001 ETH,并打印交易前后的钱包余额。由于wallet1是新建的随机私钥钱包,因此交易前余额为0,而交易后余额为0.001 ETH

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const wallet1 = ethers.Wallet.createRandom();
const wallet1WithProvider = wallet1.connect(provider);
const mnemonic = wallet1.mnemonic;

const privateKey = "0x9b139483dc6f6c8782fda2301e9b65a75b3d084d79e79e04c64683bebfa1741d";
const wallet2 = new ethers.Wallet(privateKey, provider);

/**
* 获取钱包地址
*/

const address1 = await wallet1.getAddress()
const address2 = await wallet2.getAddress()

/**
* 发送ETH
*/

console.log(" wallet2 给 wallet1 转账0.01ETH");
//交易前余额
console.log(`钱包1: ${ethers.utils.formatEther(await wallet1WithProvider.getBalance())} ETH`);
console.log(`钱包2: ${ethers.utils.formatEther(await wallet2.getBalance())} ETH`);

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

// 发送交易,获得收据
const recepit = await wallet2.sendTransaction(TX);
await recepit.wait();
console.log("receipt => ",recepit);

console.log("发送后的账户余额");

console.log(`钱包1: ${ethers.utils.formatEther(await wallet1WithProvider.getBalance())} ETH`);
console.log(`钱包2: ${ethers.utils.formatEther(await wallet2.getBalance())} ETH`);

运行结果:

image-20230516234707193

ethers.js_day03

6. 合约交互

6.1 创建可写Contract变量

声明可写的Contract变量的规则:

1
const contract = new ethers.Contract(address, abi, signer)

解读:其中 address 是合约地址,abi是合约的abi接口,singer是wallet对象。声明可写contract需要提供signer,声明可读contract只需要provider

将可读合约转换为可写合约:

1
const contract2 = contract.connect(signer)

6.2 合约交互

读合约信息不需要gas。写入合约需要接入合约信息,构建交易,并支付gas。该交易将由整个网络的每个节点以及矿工验证,并改变区块链状态。

合约交互的方法如下:

1
2
3
4
// 发送交易
const tx = await contract.METHOD_NAME(args [, overrides])
// 等待链上确认交易
await tx.wait()

解读代码:其中METHOD_NAME为调用的函数名,args为函数参数,[, overrides]是可以选择传入的数据,包括:

  • gasPrice:gas价格
  • gasLimit:gas上限
  • value:调用时传入的ether(单位是wei)
  • nonce:nonce(随机数?)

注:此方法不能获取合约运行的返回值,如若需要获取合约运行你的返回值,要使用Solidity事件记录,然后利用交易收据去查询。

6.3 例子:与本地ganache合约交互

  1. 创建provider,wallet变量
1
2
3
4
5
6
7
8
9
>import { ethers } from "ethers";

>// 获取本地的provider
>const provider = new ethers.providers.JsonRpcProvider("HTTP://127.0.0.1:8545");

>// 根据私钥创建 wallet对象
>const privateKey = "0x9b139483dc6f6c8782fda2301e9b65a75b3d084d79e79e04c64683bebfa1741d";
>const wallet = new ethers.Wallet(privateKey, provider);

  1. 创建可写合约变量,在我自己 domo1.sol中调用4个函数
  • getSlot0():查找插槽0位置的值;
  • getSlot1():查找插槽1位置的值;
  • getSlot2():查找插槽2位置的值;
  • depoist():转账;
  • balanceOf():查询地址余额。

tips:这里我使用的是方法是传入remix上获取的abi

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 创建可写的合约变量
*/

// 读取文件的abi
const ABI = JSON.parse(fs.readFileSync("LearningSelf\\day03\\demo1.abi").toString());

// 在remix上部署的合约地址
const contractAddress = "0x5B1557A5AD3cfAC88F3624743BF67baB28723899";

// 声明可写合约
const constract = new ethers.Contract(contractAddress,ABI,wallet);

3.调用getSlot0、1、2()三个函数,查看其值

1
2
3
4
5
6
7
8
9
10
// let slot0 = await provider.getStorageAt(contractAddress,0);
let slot0 = await constract.getSlot0();
// console.log(slot0);
console.log(`slot0's value is => ${slot0}`);
let slot1 = await constract.getSlot1();
// console.log(slot1);
console.log(`slot1's value is => ${slot1}`);
let slot2 = await constract.getSlot2();
// console.log(slot2);
console.log(`slot2's value is => ${slot2}`)

运行结果:

image-20230517130710866

4.查询账户的余额

1
2
3
4
5
6
// 需要查询余额的账户
let test_address = "0x753f06cD09C531Ef83b84C5147E812519BB65efd";

let balance = await constract.balanceOf(test_address);
console.log(`ETH balance is => ${ethers.utils.formatEther(balance)}`)

运行结果:

image-20230517130751856

5.转账操作,打印交易详情和余额。

1
2
3
4
5
6
//转账
let balance1 = await provider.getBalance(test_address);
console.log(`转账前的余额为:=> ${ethers.utils.formatEther(balance1)}`)
constract.depoist(1, test_address);
console.log(`转账后的余额为:=> ${ethers.utils.formatEther(balance1)}`)

运行结果:

image-20230517135419658

由于vscode显示数位的问题,使用在remix上查询显示的位数更多

image-20230517135516511

注:对于非pure/view函数的调用,会返回交易的信息。如果想知道函数执行过程中合约变量的变化,可以在合约中使用emit输出事件,并在返回的transaction信息中读取事件信息来获取对应的值

完整的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import { ethers } from "ethers";
import fs from "fs";

// 获取本地的provider
const provider = new ethers.providers.JsonRpcProvider("HTTP://127.0.0.1:8545");

// 根据私钥创建 wallet对象
const privateKey = "0x9b139483dc6f6c8782fda2301e9b65a75b3d084d79e79e04c64683bebfa1741d";
const wallet = new ethers.Wallet(privateKey, provider);


/**
* 创建可写的合约变量
*/

// 读取文件的abi
const ABI = JSON.parse(fs.readFileSync("LearningSelf\\day03\\demo1.abi").toString());

// 在remix上部署的合约地址
const contractAddress = "0xf8fe22cA93bD2416a106dFB62168C0D7eBF557FE";

// 声明可写合约
const constract = new ethers.Contract(contractAddress,ABI,wallet);


/**
* 调用合约的函数
*/

// let slot0 = await provider.getStorageAt(contractAddress,0);
let slot0 = await constract.getSlot0();
// console.log(slot0);
console.log(`slot0's value is => ${slot0}`);
let slot1 = await constract.getSlot1();
// console.log(slot1);
console.log(`slot1's value is => ${slot1}`);
let slot2 = await constract.getSlot2();
// console.log(slot2);
console.log(`slot2's value is => ${slot2}`);

// 需要查询余额的账户
let test_address = "0x753f06cD09C531Ef83b84C5147E812519BB65efd";

let balance = await constract.balanceOf(test_address);
console.log(`ETH balance is => ${ethers.utils.formatEther(balance)}`)

//转账
let balance1 = await provider.getBalance(test_address);
console.log(`转账前的余额为:=> ${ethers.utils.formatEther(balance1)}`)
constract.depoist(1, test_address);
console.log(`转账后的余额为:=> ${ethers.utils.formatEther(balance1)}`)


7.部署合约

7.1 合约工厂

ethers.js 创造了合约工厂 ContractFactory类型,方便开发者部署合约。你可以利用合约abi,编译得到的字节码bytecode和签名者变量 singer 来创建合约工厂实例,为部署合约做准备。

1
const contractFactory = new ethers.ContractFactory(abi, bytecode, signer);

注:如果合约的构造函数有参数,那么在abi中必须包含构造函数。

在创建好合约工厂之后,可以调用它的deploy函数,并传入合约构造函数的参数args来部署并得到合约实例:

1
const contract = await contractFactory.deploy(args)

可以使用两种命令,等待合约部署在链上确认,然后再进行交互

1
2
await contractERC20.deployed()
//或者 await contract.deployTransaction.wait()

7.2 例子:部署我自己编写的demo1.sol合约

demo2.sol 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract demo2 {
uint slot0;
address slot1;
string public slot2;

// 初始化一些变量

constructor(uint _slot0, address _slot1, string memory _slot2) payable{
slot0 = _slot0;
slot1 = _slot1;
slot2 = _slot2;
}

function setSlot0(uint num) public {
slot0 = num;
}

function getSlot0() external view returns(uint){
return slot0;

}

function setSlot1(address _address) public {
slot1 = _address;
}

function getSlot1() external view returns(address){
return slot1;
}

function setSlot2(string memory _str) public {
slot2 = _str;
}

function getSlot2() external view returns(string memory){
return slot2;
}

function depoist(uint _amount, address payable _to) external {
uint balance = address(this).balance;
require(balance > _amount, "You balance is less than your address(this).balance");
_to.transfer(_amount);
}

function balanceOf(address _address) external view returns(uint) {
return address(_address).balance;
}

}
  1. 创建provider和wallet变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import { ethers } from "ethers";
    import fs from "fs";

    // 获取本地的provider
    const provider = new ethers.providers.JsonRpcProvider("HTTP://127.0.0.1:8545");

    // 根据私钥创建 wallet对象
    const privateKey = "0x9b139483dc6f6c8782fda2301e9b65a75b3d084d79e79e04c64683bebfa1741d";
    const wallet = new ethers.Wallet(privateKey, provider);
  2. 准备demo1合约的字节码和ABI(这里我使用remix生成的ABI)。因为demo2的构造函数含有参数,因此我们需要把它包含再ABI中。合约的字节码可以从remix的编译面板中点击bytecode按钮拷贝下来(为了代码的可读性,将拷贝的字节码新建一个 .code文件存放,获取的时候 使用 fs来读取),其中”object”字段对应的数据就是字节码。如果部署在链上的合约,你可以在etherscan的Contract页面的Contract Creation Code中找到。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
       // demo2人类可读abi
    /**
    const demo2ABI = [
    "constructor(uint _slot0, address _slot1, string memory _slot2) payable",
    .......
    ]
    */
    // 读取文件的abi
    const ABI = JSON.parse(fs.readFileSync("LearningSelf\\day03\\demo2.abi").toString());

    // 读取文件的bytecode
    const byteCode = fs.readFileSync("LearningSelf\\day03\\demo2.code").toString();




    3. 创建合约工厂ContractFactory实例

    ```js
    // 创建合约工厂 ContractFactory 实例
    const factoryDemo2 = new ethers.ContractFactory(ABI, byteCode, wallet);



    4. 调用工厂合约的deploy() 函数并填入构造函数的参数,部署demo2合约并获取合约实例。有写常用方法:

    - `contract.address`获取合约地址,
    - `contract.deployTransaction`获取部署详情,
    - `contractERC20.deployed()`等待合约部署在链上确认。

    ```js
    // 利用 factoryDemo2 部署demo2 合约
    const gasPrice = await provider.getGasPrice();
    const transaction = { value: ethers.utils.parseEther('1'), gasPrice };
    const constract = await factoryDemo2.deploy(9,"0xfDc84b042c01F01C7cC6eF08452BA156844c3A5C","biyou",transaction);
    console.log(`部署得到的合约地址:${constract.address}`);

    console.log("部署合约的交易详情");
    console.log(constract.deployTransaction);
  3. 调用合约函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * 调用合约的函数
    */
    let slot0 = await constract.getSlot0();
    console.log(`slot0's value is => ${slot0}`);
    let slot1 = await constract.getSlot1();
    console.log(`slot1's value is => ${slot1}`);
    let slot2 = await constract.getSlot2();
    console.log(`slot2's value is => ${slot2}`);

    let balance = await constract.balanceOf("0x643bB521371F66E271937C6365F5812118e58a6D");
    console.log(`ETH balance is => ${balance}`);

    执行结果:

    image-20230517152117690

    image-20230517152152581

完整的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { ethers } from "ethers";
import fs from "fs";

// 获取本地的provider
const provider = new ethers.providers.JsonRpcProvider("HTTP://127.0.0.1:8545");

// 根据私钥创建 wallet对象
const privateKey = "0x9b139483dc6f6c8782fda2301e9b65a75b3d084d79e79e04c64683bebfa1741d";
const wallet = new ethers.Wallet(privateKey, provider);

// 读取文件的abi
const ABI = JSON.parse(fs.readFileSync("LearningSelf\\day03\\demo2.abi").toString());

// 读取文件的bytecode
const byteCode = fs.readFileSync("LearningSelf\\day03\\demo2.code").toString();

// 创建合约工厂 ContractFactory 实例
const factoryDemo2 = new ethers.ContractFactory(ABI, byteCode, wallet);


// 利用 factoryDemo2 部署demo2 合约
const gasPrice = await provider.getGasPrice();
const transaction = { value: ethers.utils.parseEther('1'), gasPrice };
const constract = await factoryDemo2.deploy(9,"0xfDc84b042c01F01C7cC6eF08452BA156844c3A5C","biyou",transaction);
console.log(`部署得到的合约地址:${constract.address}`);

console.log("部署合约的交易详情");
console.log(constract.deployTransaction);

/**
* 调用合约的函数
*/
let slot0 = await constract.getSlot0();
console.log(`slot0's value is => ${slot0}`);
let slot1 = await constract.getSlot1();
console.log(`slot1's value is => ${slot1}`);
let slot2 = await constract.getSlot2();
console.log(`slot2's value is => ${slot2}`);

let balance = await constract.balanceOf("0x643bB521371F66E271937C6365F5812118e58a6D");
console.log(`ETH balance is => ${balance}`);

8.检索事件 — 很有用

8.1 参考博客:请移步 这里

8.2 检索事件

可以利用Ethers中合约类型的queryFilter()函数读取合约释放的事件

1
const transferEvents = await contract.queryFilter('事件名',起始区块,结束区块)

queryFilter() 包含3个参数,分别是事件名(必填),起始区块(选填),和结束区块(选填)。检索结果会以数组的形式返回。

注:要检索的事件必须包含在合约的abi中。

9. 监听合约事件

9.1 监听合约事件

contract.on

在ethersjs中,合约对象有一个contract.on 的监听方法,让我们持续监听合约的事件:

1
contract.on("eventName", function)

contract.on 有两个参数,一个是要监听的事件名称”eventName”,需要包含合约abi中;另一个是我们在事件发生时调用的函数

contract.once

合约对象有一个contract.once的监听方法,让我们只监听一次合约释放事件,它的参数与contract.on一样:

1
contract.once("eventName",function)

9.2 监听合约示例

在这里 —-> here

…………………

10. 事件过滤

现阶段的我还不适合考虑这些,日后再学 博客链接

…………………

11. BigNumber 和 单位转换

11.1 BigNumber

在以太坊中,许多计算都对JavaScript整数的安全值(js中最大安全数为9007199254740991)。因此,ethers.js使用BigNumber 类安全地对任何数量级的数字进行数学运算。在ethers.js中,大多数需要返回值的操作将返回BigNumber,而接受值的参数也会接受他们。

BigNumber官方文档

11.2 创建BigNumber实例

可以利用 ethers.BigNumber.from() 函数将string,number,BigNumber等类型转换为BigNumber。

注:超过js最大安全整数的数值将不能转换

1
2
3
4
5
6
7
const oneGwei = ethers.BigNumber.from("1000000000"); // 从十进制字符串生成
console.log(oneGwei)
console.log(ethers.BigNumber.from("0x3b9aca00")) // 从hex字符串生成
console.log(ethers.BigNumber.from(1000000000)) // 从数字生成
// 不能从js最大的安全整数之外的数字生成BigNumber,下面代码会报错
// ethers.BigNumber.from(Number.MAX_SAFE_INTEGER);
console.log("js中最大安全整数:", Number.MAX_SAFE_INTEGER)

运行结果:

image-20230517171659607

11.3 BigNumber运算

BigNumber支持很多运算,例如加减乘除、取模mod,幂运算pow,绝对值abs等运算:

1
2
3
4
5
6
7
// 运算
console.log("加法:", oneGwei.add(1).toString())
console.log("减法:", oneGwei.sub(1).toString())
console.log("乘法:", oneGwei.mul(2).toString())
console.log("除法:", oneGwei.div(2).toString())
// 比较
console.log("是否相等:", oneGwei.eq("1000000000"))

运行结果:

image-20230517171933283

11. 4 单位转换

以太坊中,1 ethers 等于 10^18wei。下面是一些常用的单位:

image-20230517172228183

在应用中,我们经常将数值在用户可读的字符串(以ether为单位)和机器可读的数值(以wei为单位)之间转换。例如,钱包可以为用户界面指定余额(以ether为单位)和gas价格(以gwei为单位),但是在发送交易时,两者都必须转换成以wei为单位的数值。ethers.js提供了一些功能函数,方便这类转换。

  • formatUnits(变量, 单位):格式化,小单位转大单位,比如wei -> ether,在显示余额时很有用。参数中,单位填位数(数字)或指定的单位(字符串)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //代码参考:https://docs.ethers.io/v5/api/utils/display-logic/#utils-parseUnits
    console.group('\n2. 格式化:小单位转大单位,formatUnits');
    console.log(ethers.utils.formatUnits(oneGwei, 0));
    // '1000000000'
    console.log(ethers.utils.formatUnits(oneGwei, "gwei"));
    // '1.0'
    console.log(ethers.utils.formatUnits(oneGwei, 9));
    // '1.0'
    console.log(ethers.utils.formatUnits(oneGwei, "ether"));
    // `0.000000001`
    console.log(ethers.utils.formatUnits(1000000000, "gwei"));
    // '1.0'
    console.log(ethers.utils.formatEther(oneGwei));
    // `0.000000001` 等同于formatUnits(value, "ether")
    console.groupEnd();

    运行结果:

    image-20230517172556364

  • parseUnits:解析,大单位转小单位,比如ether -> wei,在将用户输入的值转为wei为单位的数值很有用。参数中,单位填位数(数字)或指定的单位(字符串)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 3. 解析:大单位转小单位
    // 例如将ether转换为wei:parseUnits(变量, 单位),parseUnits默认单位是 ether
    // 代码参考:https://docs.ethers.io/v5/api/utils/display-logic/#utils-parseUnits
    console.group('\n3. 解析:大单位转小单位,parseUnits');
    console.log(ethers.utils.parseUnits("1.0").toString());
    // { BigNumber: "1000000000000000000" }
    console.log(ethers.utils.parseUnits("1.0", "ether").toString());
    // { BigNumber: "1000000000000000000" }
    console.log(ethers.utils.parseUnits("1.0", 18).toString());
    // { BigNumber: "1000000000000000000" }
    console.log(ethers.utils.parseUnits("1.0", "gwei").toString());
    // { BigNumber: "1000000000" }
    console.log(ethers.utils.parseUnits("1.0", 9).toString());
    // { BigNumber: "1000000000" }
    console.log(ethers.utils.parseEther("1.0").toString());
    // { BigNumber: "1000000000000000000" } 等同于parseUnits(value, "ether")
    console.groupEnd();

    运行结果:

    image-20230517172937424

12. CallStatic

callStatic方法是属于ethers.Contract类的编写方法分析,同类的还有populateTransaction和estimateGas方法

12.1 可能失败的交易

在以太坊上交易需要支付昂贵的gas,并且有失败的风险,发送失败的交易并不会把gas返还给你。因此,在发送交易前知道哪些交易可能会失败非常重要。以小狐狸钱包(metamask)为例。

如果交易将失败,小狐狸会告诉你 this transaction may fail,翻译过来就是”这笔交易可能会失败”。当用户看到提示的时候,就可以取消交易了。

实现的原理:以太坊节点有一个eth_call方法,让用户可以模拟一笔交易,并返回可能的交易结果,但不是真正的在区块链上执行(交易不上链)。

12.2 callStatic

ethers.js中你可以利用contract对象的callStatic()来调用以太坊节点的eth_call。如果调用成功,则返回ture;如果失败,则报错并返回失败原因。方法:

1
2
const tx = await contract.callStatic.函数名( 参数, {override})
console.log(`交易会成功吗?:`, tx)
  • 函数名:为模拟调用的函数名。
  • 参数:调用函数的参数。
  • {override}:选填,可包含一下参数:
    • from:执行时的msg.sender,也就是你可以模拟任何一个人的调用,比如V神。
    • value:执行时的msg.value
    • blockTag:执行时的区块高度。
    • gasPrice
    • gasLimit
    • nonce

12.3 用callStatic 模拟DAI转账

演示过程—-》博客

13. 编码 calldata

13.1 接口类Interface

ethers.js 的接口类抽象了与以太坊网络的合约交互所需的ABI编码和解码。ABI与API类似,是一格式,用于对合约可以处理的各种类型的数据进行编码,以便它们可以交互。

可以利用abi生成或者直接从合约中获取interface变量:

1
2
3
4
// 利用abi生成
const interface = new ethers.utils.Interface(abi)
// 直接从contract中获取
const interface2 = contract.interface

接口类封装了一些编码解码的方法。与一些特殊的合约交互时(比如代理 合约),你需要编码参数、解码返回值:

注:相关函数必须包含在abi中。

  • getSighash():获取函数选择器(function selector),参数为函数名或函数签名。

    至于函数签名如何获取,在remix上使用keccak256()或的和ethersjs使用keccak256()获取的结果完全不一样,日后空闲了再仔细深入学习

    1
    2
    interface.getSighash("balanceOf");
    // '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
  • encodeDeploy():编码构造器的参数,然后可以附在合约字节码的后面。

    1
    interface.encodeDeploy("Wrapped ETH", "WETH");
  • encodeFunctionData():编码函数的calldata

    1
    interface.encodeFunctionData("balanceOf", ["0xc778417e063141139fce010982780140aa0cd5ab"]);
  • decodeFunctionResult():解码函数的返回值。

    1
    interface.decodeFunctionResult("balanceOf", resultData)

评论



政策 · 统计 | 本站使用 Volantis 主题设计