-
Notifications
You must be signed in to change notification settings - Fork 6
/
getting-started.wrm
414 lines (270 loc) · 15.4 KB
/
getting-started.wrm
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
_section: 入门指南@<新手入门> @priority<100>
这是一份关于 Ethers 的简短介绍,但涵盖了许多开发人员需要的常见操作,并为 Ethereum 新手提供了一个入门指南。
_heading: 安装 Ethers
如果使用 NPM,您必须首先安装 Ethers。
_code: 通过 NPM 安装 @lang<shell>
# 安装 ethers
/home/ricmoo/test-ethers> npm install ethers@beta-exports
在 Ethers 中的所有内容都从根目录导出,并在``ethers``对象上导出。在``package.json``中也有``exports``,以方便更细粒度的导入。
通常情况下,本文档假定在代码示例中已经导入了来自 ethers 的所有导出项,但您可以以任意方式导入必要的对象。
_code: 在 Node.js 中导入 @lang<script>
// 导入所有
import { ethers } from "ethers";
// 只导入一些选定的项目
import { BrowserProvider, parseUnits } from "ethers";
// 从特定的导出项导入
import { HDNodeWallet } from "ethers/wallet";
_code: 从浏览器中导入 ESM @lang<script>
<script type="module">
import { ethers } from "https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.min.js";
// 在这里继续写代码...
</script>
_subsection: 常见术语 @<starting-glossary>
开始前,有必要在抽象层面上了解可用的对象类型以及它们的含义。
_heading: Provider
[[Provider]] 是对区块链的只读连接,允许查询区块链状态,例如账户、区块或交易详情、查询事件日志或使用 call 评估只读代码。
如果您之前学习过 Web3.js,已经习惯于 **Provider** 提供读写访问。在 Ethers 中,所有写操作都被进一步抽象成另一个对象: **Signer** 。
_heading: Signer
[[Signer]] 包装了与账户交互的所有操作。通常,账户具有位于//某处//的私钥,可用于对各种类型的载荷进行签名。
私钥可以位于内存中(使用 [[Wallet]] 时),或通过某些 IPC 层保护,例如 MetaMask,它代理来自网站的交互到浏览器插件,使私钥不受网站影响,并且仅在请求用户的许可并收到授权后才允许交互。
_heading: Transaction
若要对区块链进行任何状态更改,需进行一笔交易,其中需要支付费用,费用涵盖执行交易(如读取磁盘和执行数学运算)和存储更新信息的相关费用。
如果交易回滚,仍然必须支付费用,因为验证器仍然必须花费资源尝试运行交易以确定它已回滚,并记录其失败的详细信息。
交易包括从一个用户向另一个用户发送以太币,部署 **Contract** 或对 **Contract** 执行更改状态的操作。
_heading: Contract
[[Contract]] 是已部署到区块链上的程序,其包含一些代码并已分配存储空间,可以对其进行读取和写入。
当连接到 [[Provider]] 时可以读取它,当连接到 [[Signer]] 时可以调用更改状态的操作。
_heading: Receipt
一旦 **Trnsaction** 已提交到区块链,它将被放入内存池(mempool),直到验证器将其打包上链。
交易上链时,交易的更改生效,此时可以获得一份收据,其中包括关于交易的详细信息,例如它被包含在哪个区块中,实际支付的费用,使用的 gas ,它发出的所有事件以及它是否成功或回滚。
_subsection: 连接到 Ethereum @<starting-connecting>
与区块链进行交互所需的第一件事就是使用 [[Provider]] 连接到它。
_heading: MetaMask (和其他内置的服务提供商)
最快、最简洁的尝试和开发以太坊的方法是使用 [[link-metamask]],它是一个浏览器扩展,将对象注入 window,提供:
- 对以太坊网络的只读访问([[Provider]])
- 由私钥支持的基于请求许可的写访问([[Signer]])
当请求访问身份验证方法(如发送交易或请求私钥地址)时,MetaMask会向用户显示一个弹出窗口,请求许可。
_code: @lang<script>
let signer = null;
let provider;
if (window.ethereum == null) {
//如果未安装 MetaMask,我们使用默认提供者
//它由各种第三方服务(例如 INFURA )支持。
//它们没有安装私钥,因此只具有只读访问权限。
console.log("MetaMask not installed; using read-only defaults")
provider = ethers.getDefaultProvider()
} else {
// 连接到 MetaMask EIP-1193 对象。这是一个标准协议,允许 Ethers 访问通过 MetaMask 进行所有只读请求。
provider = new ethers.BrowserProvider(window.ethereum)
// 还提供了请求访问写操作的可能,
// 这些操作将由 MetaMask 为用户管理的私钥执行。
signer = await provider.getSigner();
}
_heading: 自定义 RPC 后端
如果您正在运行自己的以太坊节点(例如 [[link-geth]] )或使用自定义的第三方服务(例如 [[link-infura]] ),您可以直接使用 [[JsonRpcProvider]] ,该对象使用[[link-jsonrpc]]协议进行通信。
如果使用自己的以太坊节点或基于开发人员的区块链,例如 Hardhat 或 Ganache,您可以使用 [[JsonRpcProvider-getSigner]] 获取帐户。
_code: 连接到一个 JSON-RPC URL @lang<script>
// If no %%url%% is provided, it connects to the default
// http://localhost:8545, which most nodes use.
// 如果未提供 %%url%% ,它将连接到默认的http://localhost:8545,这是大多数节点所使用的。
provider = new ethers.JsonRpcProvider(url)
// 通过获取签名作为账户获取写权限。
signer = await provider.getSigner()
_subsection: 用户交互 @<starting-display>
在 Ethereum 中,所有单位通常都是整数值,因为处理小数和浮点数可能导致在执行数学运算时不精确和不显然的结果。
因此,在以太坊内部使用的单位(例如wei)适用于机器可读的目的和数学计算,通常非常大且可读性较差。
例如,想象处理元和分;您将显示像``“2.56元”``的值。在区块链世界中,我们将所有值保留为分,因此在程序内部将保存为``256``分。
因此,当接受用户输入的数据时,必须将其从小数字符串表示(例如``"2.56"``)转换为最低单位整数表示(例如``256``)。当向用户显示值时,需要进行相反的操作。
在以太坊中,//一个ether//等于``10 *\* 18``个wei,一个gwei等于``10 *\* 9``个wei,所以数值很快就会变得很大,因此提供了一些方便的函数来帮助在表示这之间转换。
_code: @lang<javascript>
// 转换用户提供的以ether为单位的字符串为以wei为单位的数量。
eth = parseEther("1.0")
//_result:
// 转换用户提供的gwei为单位的字符串为以wei为单位的max base fee
feePerGas = parseUnits("4.5", "gwei")
//_result:
//将以wei为单位的值转换为以ether为单位字符串,以便UI中显示
formatEther(eth)
//_result:
//将以wei为单位的值转换为以gwei为单位的字符串,以便UI中显示
formatUnits(feePerGas, "gwei")
//_result:
_subsection: 与区块链交互 @<starting-blockchain>
_heading: 查询状态
有了 [[Provider]] ,就可以对区块链上的数据进行只读连接。这可以用来查询当前账户状态,获取历史日志,查找合约代码等。
_code: @lang<javascript>
//_hide: provider = new InfuraProvider();
// 查询当前的区块高度
await provider.getBlockNumber()
//_result:
// 查询帐户的当前余额(通过地址或ENS名称)。
balance = await provider.getBalance("ethers.eth")
//_result:
// 因为余额是以 wei 为单位的,所以您可能希望将其以
// 以太 (ether) 的形式显示。
formatEther(balance)
//_result:
// 获取发送交易所需的下一个 nonce 值
await provider.getTransactionCount("ethers.eth")
//_result:
_heading: 发送交易
要写入区块链,您需要访问控制某个帐户的私钥。在大多数情况下,代码不能直接访问这些私钥,而是通过 [[Signer]] 发出请求,该 Signer 将请求分派给提供严格的门户访问的服务(如 [[link-metamask]] ),并要求用户批准或拒绝操作。
_code: @lang<script>
//_hide: provider = new JsonRpcProvider("http:/\/localhost:8545")
//_hide: provider.resolveName = () => "0x643aA0A61eADCC9Cc202D1915D942d35D005400C";
//_hide: signer = new Wallet(id("test"), provider);
// 当发送交易时,传入的值需要以 wei 为单位,所以要使用 parseEther
// 将 ether 转换为 wei
tx = await signer.sendTransaction({
to: "ethers.eth",
value: parseEther("1.0")
});
//_result:
// 你可能常常会希望等候交易打包出块(再继续下一步)
receipt = await tx.wait();
//_result:
_subsection: 合约 @<starting-contracts>
**合约**是一种元类,这意味着它的定义是在运行时基于传递的 ABI 派生的,同时决定其可用的方法和属性。
_heading: 应用程序二进制接口 (ABI)
由于在区块链上进行的所有操作都必须编码为二进制数据,我们需要一种简洁的方式来定义如何在常见对象(如字符串和数字)与二进制数据之间进行转换,以及合约编码、调用和解释的方式。
对于你想使用的任何方法、事件或错误类型,你必须通过一个 [[Fragment]] 来告知 Ethers 如何对请求进行编码和解码结果。
可以安全地排除任何你不需要的方法或事件。
有几种常见的格式可以描述 ABI,Solidity 编译器通常会转储一个 JSON 格式,但是当手动输入 ABI 时,使用 Solidity 签名这种可读 ABI 格式通常更容易,也具有更强的可读性。
_code: 简化版 ERC-20 ABI @lang<script>
abi = [
"function decimals() returns (string)",
"function symbol() returns (string)",
"function balanceOf(address addr) returns (uint)"
]
// 创建一个合约
contract = new Contract("dai.tokens.ethers.eth", abi, provider)
_heading: 只读方法 (例如 ``view`` 和 ``pure``)
只读方法是一种不能改变区块链的状态,但通常能够提供一个简单的接口来获取合约重要数据的方法类别
_code: 阅读 DAI ERC-20 合约 @lang<javascript>
// 合约 ABI(我们关心的代码片段)
abi = [
"function decimals() view returns (uint8)",
"function symbol() view returns (string)",
"function balanceOf(address a) view returns (uint)"
]
// 创建一个合约;连接到一个 Provider,它只能
// 访问只读的方法(如 view 和 pure)
contract = new Contract("dai.tokens.ethers.eth", abi, provider)
// Token 的符号
sym = await contract.symbol()
//_result:
// Token 的精度
decimals = await contract.decimals()
//_result:
// 读取特定账户的 Token 余额
balance = await contract.balanceOf("ethers.eth")
//_result:
// 对余额进行格式化,例如用于用户界面中
formatUnits(balance, decimals)
//_result:
_heading: 能改变状态的方法
_code: 改变一个 ERC-20 合约的状态 @lang<script>
abi = [
"function transfer(address to, uint amount)"
]
// 连接到一个 Signer;触发改变状态的交易
// 这将花费账户的以太币
contract = new Contract("dai.tokens.ethers.eth", abi, signer)
// 发送 1 DAI
amount = parseUnits("1.0", 18);
// 发送交易
tx = await contract.transfer("ethers.eth", amount)
//_result: @TODO
// 目前,交易被发送至内存池了
// 但其尚未被区块所打包。所以,我们...
// ...等待交易被打包
await tx.wait()
//_result: @TODO
_code: 强制调用(模拟)一个会改变状态的方法 @lang<javascript>
abi = [
"function transfer(address to, uint amount) returns (bool)"
]
// 连接到 Provider,因为我们只需要读取权限
contract = new Contract("dai.tokens.ethers.eth", abi, provider)
amount = parseUnits("1.0", 18)
// 使用静态调用有很多限制,但
// 对于预执行交易这种场景往往很实用
await contract.transfer.staticCall("ethers.eth", amount)
//_result:
// 我们也可以模拟另一个账户发起调用
other = new VoidSigner("0x643aA0A61eADCC9Cc202D1915D942d35D005400C")
contractAsOther = contract.connect(other.connect(provider))
await contractAsOther.transfer.staticCall("ethers.eth", amount)
//_result:
_heading: 事件监听
当为一个已定义的事件添加监听器时,事件参数会被监听器解构。
总是会传递一个额外的参数给监听器,它是一个[[EventPayload]],其中包含了事件的更多信息,比如过滤器与删除该监听器的方法。
_code: 监听 ERC-20 合约事件 @lang<script>
abi = [
"event Transfer(address indexed from, address indexed to, uint amount)"
]
// 创建一个合约;连接到一个 Provider,它只能
// 访问只读的方法(如 view 和 pure)
contract = new Contract("dai.tokens.ethers.eth", abi, provider)
// 开始监听任何转账事件
contract.on("Transfer", (from, to, _amount, event) => {
const amount = formatEther(_amount, 18)
console.log(`${ from } => ${ to }: ${ amount }`);
// `event.log` 包含了完整的事件日志
// (可选)便捷的停止监听的方法
event.removeListener();
});
// 同上
contract.on(contract.filters.Transfer, (from, to, amount, event) => {
// 见上文
})
// 监听任何 "ethers.eth" 的转账事件
filter = contract.filters.Transfer("ethers.eth")
contract.on(filter, (from, to, amount, event) => {
// `to` will always be equal to the address of "ethers.eth"
});
// 监听任何事件,不论其是否在 ABI 中有定义
// 未知事件也可以被监听器获取
// 但参数无法被解构
contract.on("*", (event) => {
// `event.log` 包含了完整的事件日志
});
_heading: 查询历史事件
当在一个较大的区块范围内进行查询时,某些后台可能会运行过慢,也可能会返回一个错误或截断结果而没有任何提示,这取决于不同后台实现。
_code: query historic ERC-20 events @lang<javascript>
abi = [
"event Transfer(address indexed from, address indexed to, uint amount)"
]
// 创建一个合约;连接到一个 Provider,它只能
// 访问只读的方法(如 view 和 pure)
contract = new Contract("dai.tokens.ethers.eth", abi, provider)
// 查询最近 100 个区块的任何转账事件
filter = contract.filters.Transfer
events = await contract.queryFilter(filter, -100)
// 这些事件是一个正常的数组
events.length
//_result:
// 第一个符合条件的事件
events[0]
//_result:
// 查询目标为 ethers.eth 的所有转账事件
filter = contract.filters.Transfer("ethers.eth")
events = await contract.queryFilter(filter)
// 第一个符合条件的事件
events[0]
//_result:
_subsection: 消息签名 @<starting-signing>
私钥除了用于交易签名授权外,还可以做很多事情。它还可以对其他形式的数据进行签名,用于其他验证目的。
例如,对**消息**进行签名可以用来证明一个账户的所有权,网站可以用它来对用户进行登录鉴权。
_code: @lang<javascript>
// Signer;对消息进行签名不需要 Provider
signer = new Wallet(id("test"))
//_result:
message = "sign into ethers.org?"
// 对消息进行签名
sig = await signer.signMessage(message);
//_result:
// 验证消息;会发现地址与 Signer 相符
verifyMessage(message, sig)
//_result:
许多其他更高级的基于消息签名的协议被用来允许通过私钥授权其他用户转移他们的 Token,并允许转账手续费由其他人支付。