大数跨境

第10章 以太坊钱包开发

第10章 以太坊钱包开发 数组智控产业发展科技院
2023-05-24
2
导读:数字钱包是我们和区块链世界交互的媒介,这一章,我们来探索实现一个简单的钱包。开头部分会先介绍钱包的理论知识,

数字钱包是我们和区块链世界交互的媒介,这一章,我们来探索实现一个简单的钱包。

开头部分会先介绍钱包的理论知识,后面带大家一起来实现一个Web版本的钱包。

10.1 数字钱包基础

我们早就接受了“钱包是用来存钱的”这个概念,然而,区块链中的钱包却有一点点不一样,钱包是用来“管”钱(数字资产)的,而不是存钱的,这是怎么回事呢?

链上的数字资产都会对应到一个账号地址上,只有拥有账号的钥匙(私钥)才可以对资产进行消费(用私钥对消费交易进行签名)。

私钥和地址的关系如图10-1所示。

图10-1 私钥、公钥及地址关系

提示:在第4章,我们也介绍过账号的概念,不过在第4章使用的词是账户,本章更多使用的是账号,它们其实并没有严格的区分,在第4章我们侧重介绍其在系统内的形式,本章中我们侧重介绍其地址形式:0xea674...ec8,看上去就是一串号码,因此用账号。

一句话概括就是:私钥通过椭圆曲线生成公钥,公钥通过哈希函数生成地址,这两个过程都是单向的。

实际上,数字钱包是一个管理私钥的工具,例如生成一个新私钥(即创建账号)、加密存储私钥、用私钥签名等。

钱包并不保存数字资产,所有的数字资产都存储在链上。

私钥

钱包的功能都是基本围绕着私钥的,私钥是一个32字节的数,生成一个私钥本质上是在1到2256之间随机选一个数字。

因此生成密钥的第一步,也是最重要的一步,是要找到足够安全的随机源。

密码学认为,随机应该是不可预测及不可重复的,比如可以掷硬币256次,用纸和笔记录正反面并转换为0和1,随机得到的256位二进制数字可作为钱包的私钥。

从编程的角度来看,一般是通过在一个密码学安全的随机源(不建议大家自己去写一个随机数)中取出一长串随机字节,对其使用SHA256哈希算法进行运算,这样就可以方便地产生一个256位的数字。

10.2 钱包相关提案

钱包实际上也是一个私钥的容器。

通常,为了更好地保护隐私,一个人会有很多账号的需求,那么就有一堆私钥需要维护管理,加大了钱包及持有人的负担(例如,每个私钥都是完全随机的,备份私钥就会特别麻烦),比特币社区因此提出了一系列改进提案(Bitcoin Improvement Proposal,简称BIP,如BIP32表示第32个改进提案)来方便及规范钱包管理私钥,以太坊也继承了这些提案,因此不要奇怪为什么要介绍比特币提案。

10.2.1 BIP32分层推导

最早期的比特币钱包其实就是一堆相互毫无关系的私钥,还有一个昵称:Just a Bunch Of Keys(一堆私钥)。

BIP32提案[插图]为了解决这种混乱,提出一个办法,它根据一个随机数种子通过分层确定性推导的方式得到n个私钥。

这样,在保存的时候,只需要保存一个种子就可以推导出私钥,如图10-2所示。

图10-2 BIP32推导图(1)

图10-2中的孙密钥可以用来签发交易,BIP32提案的全称是Hierarchical Deterministic Wallets,也就是我们所说的HD钱包

BIP32分层推导的过程是这样的:

第一步,利用随机方式选择一个根种子,再用哈希计算推导出主密钥,如图10-3所示。

图10-3 BIP32推导图(2)

根种子通过HMAC-SHA512算法哈希计算后的结果分为两部分,一边的256用来作为主私钥(m),另一边的256作为主链编码(a master chain code)。

接着是第二步,用生成的密钥(由私钥或公钥)及主链编码再加上一个索引号,将作为HMAC-SHA512算法的输入继续衍生出下一层的私钥及链编码,如图10-4所示。

图10-4 BIP32推导图(3)

衍生推导的方案其实有两个:一个是用父公钥推导,一个是用父私钥推导(被称作强化衍生方程)。


同时,为了区分这两种不同的衍生推导方案,在索引号上也进行了区分,索引号小于231用于常规衍生,而231到232-1之间用于强化衍生,为了方便表示,索引号i’,表示231+i。

因此增加索引(水平扩展)及通过子密钥向下一层(深度扩展)可以无限生成私钥。

并且这个推导过程具备确定性及单向性的特点:

确定性是指相同的输入,总是有相同的输出;

单向性是指子密钥不能推导出同层级的兄弟密钥,也不能推出父密钥。

如果没有子链编码也不能推导出孙密钥。

现在我们已经对分层推导有了初步的认识,一句话概括BIP32就是:

分层推导方案主要为了避免管理一堆私钥的麻烦。

10.2.2 密钥路径及BIP44

通过BIP32分层(树状结构)推导出来的密钥,通常用路径来表示,每个级别之间用斜杠“/”来分隔,由主私钥衍生出的私钥起始以“m”打头。

因此,第一个主私钥生成的子私钥是m/0。

第一个公钥是M/0。

第一个子密钥的子密钥就是m/0/1,依此类推。

BIP44则是为这个路径约定了一个规范的含义(也扩展了对多币种的支持),BIP44指定了包含5个预定义树状层级的结构:

purpose:Purpose是固定的,值为44(或者0x8000002C),即当前提案的编号。

Coin:代表币种,0代表比特币,1代表比特币测试链,60代表以太坊[插图]。

Account:代表这个币的账号编号,从0开始。

Change:常量0用于外部可见地址(如收款地址),常量1用于内部(如找零地址)。

address_index:地址索引,从0开始,代表生成第几个地址,官方建议每个account下的address_index不要超过20。

以太坊钱包也遵循BIP44标准,使用的路径是m/44'/60'/a'/0/n。

a表示账号(通常为0),n是生成的第n个地址,60是在SLIP44提案中确定的以太坊的编码。

所以我们要开发以太坊钱包同样需要对比特币的钱包提案——BIP32、BIP39有所了解。

一句话概括BIP44就是:给BIP32的分层路径定义规范。

10.2.3 BIP39

BIP32提案可以让我们保存一个随机数种子(通常用16进制数表示),而不是一堆密钥,确实方便一些,不过用户备份它时依旧要小心翼翼,千万不能抄错一个字母。

此时使用BIP39[插图]就方便很多,它用助记词的方式生成种子,这样用户只需要记住12(或24)个单词,然后让单词序列通过PBKDF2与HMAC-SHA512函数,进而创建出随机种子作为BIP32的种子。

可以简单作一个对比,下面哪一个种子备份起来更友好:

使用助记词作为种子其实包含两个部分:生成助记词以及用助记词推导出随机种子。

下面分析这整个过程。

10.2.4 生成助记词

生成助记词的过程是这样的:

先生成一个128位的随机数,再加上随机数校验码占4位,得到132位的一个数,然后按每11位做切分,这样就有了12个二进制数,然后用每个数去查BIP39定义的单词表,这样就得到12个助记词,这个过程如图10-5所示。

图10-5 助记词推导

(1)下面是使用BIP39生成助记词的一段代码:

10.2.5 用助记词推导出种子

这个过程使用密钥拉伸(Key stretching)函数,这个函数被用来增强弱密钥的安全性,PBKDF2是常用的密钥拉伸算法中的一种。

PBKDF2的基本原理是通过一个伪随机函数(例如HMAC函数),把助记词明文和盐作为输入参数,然后进行重复运算最终生成一个更长的(512位)密钥种子。

这个种子再构建出一个确定性钱包并派生出它的密钥。密钥拉伸函数需要两个参数:助记词和盐。

盐由常量字符串“mnemonic”及一个可选的密码组成,可以用来增加暴力破解的难度。

注意使用不同密码,拉伸函数就可以在使用同一个助记词的情况下产生若干个不同的种子,这个过程如图10-6所示。

图10-6 助记词推导(2)

密码可以作为附加的安全因子来保护种子,即使助记词的备份被窃取,也可以保证钱包的安全(这要求密码有足够的复杂度和长度),不过从另外一方面来说,如果我们忘记密码,那么将无法恢复我们的数字资产。

下面是一段JavaScript代码,完整地表示账号推导过程:

校验和地址是EIP-55中定义的对大小写有要求的一种地址形式。

一句话概括BIP39就是:通过定义助记词让种子的备份更加友好。

10.3 钱包功能

了解完前面关于钱包的基础理论之后,我们进入开发部分,开发一个去中心化的数字钱包。

去中心化钱包指的是账号密钥的管理、交易的签名都是在客户端完成,即私钥的信息都是在用户手中,钱包的开发者(项目方)接触不到私钥的信息。

相应地,如果私钥保存在项目方的服务器中,则称为中心化钱包。

梳理一下钱包通常包含的功能:

·账号管理(主要是私钥的管理),如创建账号、账号导入导出。

·账号信息展示:如以太币余额、Token(代币)余额。

·转账功能:发送以太币及发送Token(代币)。

接下来,我们会逐一介绍如何实现这些功能,我们选择了基于ethers.js[插图]库来开发这款钱包。

ethers.js和web3.js一样,也是一套和以太坊区块链进行交互的库,选择ethers.js的原因是,ethers.js对账号相关的提案进行了实现。

钱包的完整代码上传在作者的Github,代码地址:https://github.com/xilibi2003/EthWebWallet。

本章我们开发的是一个Web钱包,不过对于一般用户而言,使用最多的是移动端(安卓及iOS)的钱包,选择以Web钱包为案例,是想在尽可能少的技术路线依赖下,把钱包原理及相关技术点介绍清楚。


如果读者需要实现一个移动端钱包,作者开源了一个颇受欢迎的安卓版钱包可供参考,地址为:https://github.com/xilibi2003/Upchain-wallet。

10.4 创建钱包账号

通过前面的介绍,我们知道创建账号可以有两种方式。

·方式一:随机生成一个32字节的数当成私钥。

·方式二:通过助记词进行确定性推导得出私钥。

10.4.1 随机数为私钥创建账号

即方式一,可以使用ethers.utils.randomBytes生成一个随机数,然后使用这个随机数来创建钱包,代码如下:

上面代码的wallet是ethers中的一个钱包对象,它除了有代码中出现的.address属性之外,还有如获取余额、发送交易等方法,我们在后面会作进一步介绍。

ethers.utils.randomBytes生成的是一个字节数组,如果想用十六进制数显示,需要转化为BigNumber,代码如下:

现在我们结合界面(如图10-7所示),完整地实现通过加载私钥创建账号。

图10-7 加载私钥UI

HTML界面代码如下:

以上代码在table表格中定义一个输入框和按钮,对应的JavaScript逻辑代码如下:

在以上代码中,我们会为用户默认生成一个随机的私钥,但是用户依旧可以在输入框填入一个已有账号的私钥,此时ethers.js会导入对应的账号。

10.4.2 助记词创建账号

前面已经介绍过助记词的推导过程,通过助记词创建账号是目前最主流的方式。

我们需要先生成一个随机数,然后用随机数生成助记词,最后用助记词创建钱包账号,关键代码如下:

结合界面来实现通过助记词的方式创建钱包账号,效果如图10-8所示,支持用户输入助记词及路径。

图10-8 加载助记词UI

界面的HTML代码如下(代码中定义了两个输入框和一个按钮):

对应的逻辑代码(JavaScript)如下:

同样,用户可以提供一个保存好的助记词来导入其钱包。

其实,ethers.js也提供了其他的方式创建账号,例如直接创建一个随机钱包:

以及接下来10.5节的通过keystore文件来创建账号。

10.5 导入账号

一个钱包除了自身可以创建账号,还应当可以导入其他钱包创建的账号,前面10.4节我们使用私钥及助记词来创建账号,如果是使用已有的私钥及助记词,这其实也是账号导入的过程。

第4章我们还提到过以太坊客户端Geth,Geth也可以创建钱包,实际用过Geth的同学会知道,在创建钱包时需要输入一个密码,这个密码并不是私钥,而是用来加密私钥。

Geth在创建账号时会生成一个名为keystore的JSON文件,这个文件通常在同步区块数据的目录下的keystore文件夹(如:~/.ethereum/keystore)中,keystore文件存储着加密后的私钥信息。

一个功能完善的钱包,应该需要支持导入keystore文件加密的账号。

在介绍如何导入keystore文件之前,有必要先理解keystore文件的作用及原理。

10.5.1 keystore文件

我们已经知道,私钥其实就代表一个账号,最简单的保管账号的方式就是直接把私钥保存起来,如果私钥文件被人盗取,我们的数字资产将被洗劫一空。

keystore文件就是一种以加密的方式存储密钥的文件,发起交易的时候,钱包得先从keystore文件中通过输入密码得到私钥,然后进行签名交易。

这样做之后就会安全得多,因为只有黑客同时盗取keystore文件和密码才能盗取我们的数字资产,相比明文私钥,这会大大提高安全性。

以太坊使用对称加密算法来加密私钥生成keystore文件,因此对称加密密钥(其实也是发起交易时进行解密的密钥)的选择就非常关键,这个密钥是使用KDF算法推导派生而出的。

KDF生成密钥

KDF(key derivation functions)密钥衍生算法,它的作用是通过一个密码派生出一个或多个密钥,即从用户密码生成加密私钥用的密钥。

其实10.2.5节介绍的助记词推导种子的PBKDF2算法就是一种KDF算法,其原理是加入一个随机数作为盐以及增加哈希迭代次数以增加复杂度。

而在keystore中用的是Scrypt算法,用一个公式来表示的话,派生的Key生成方程为:

其中的salt是一段随机的盐,dk_len是输出的哈希值的长度。

n是CPU/Memory开销值,开销值越高,计算就越困难。

r表示块大小,p表示并行度。

顺带说一句:莱特币(Litecoin)就使用Scrypt作为它的POW算法。

实际使用中,还会给Scrypt运算输入一个用户密码,可以用图10-9来表示这个过程。

图10-9 keystore对称加密密钥的生成过程

对称加密私钥

上面用KDF算法生成了一个密钥,在生成keystore文件时,就是使用这个密钥来进行对称加密,keystore目前的版本中选择的对称加密算法是aes-128-ctr,加密后,生成的keystore文件的内容如下:

解释一下各个字段,其中最关键的信息是crypto字段。

·crypto:密钥推导算法相关配置。

〇cipher:用于加密以太坊私钥的对称加密算法。

上述文件用的是aes-128-ctr加密算法。

〇cipherparams:aes-128-ctr加密算法需要的参数。

aes-128-ctr加密算法需要用到一个参数来初始化向量iv。

〇ciphertext:加密算法输出的密文,也是将来解密时需要的输入内容。

〇kdf:指定使用哪一个算法,这里使用的是scrypt算法。

〇kdfparams:Scrypt函数需要的参数。

〇mac:用来校验密码的正确性,下面一个小节单独分析。

·address:表示账号地址。·version:keystore文件的版本号。

·id:uuid(通用唯一识别码)编号。

我们来完整梳理一下keystore文件的产生过程:

(1)使用Scrypt算法(根据密码和相应的参数)生成密钥;

(2)使用上一步生成的密钥,加上账号私钥、参数进行对称加密;

(3)把相关的参数和输出的密文保存为JSON格式的文件。

keystore还原出私钥

当我们在使用keystore文件来还原私钥时,依然是使用KDF生成一个密钥,然后用密钥对keystore文件中的密文ciphertext进行解密,其过程如图10-10所示。

图10-10 还原私钥

在对称加密算法中,加密和解密其实是一样的,只不过加密的输出是解密的输入。

细心的读者会发现,无论使用什么密钥(即便使用错误的密码衍生出来的密钥)来进行解密,都会生成一个私钥,那么要怎么确认解密出来的私钥是之前保存的?

这就是keystore文件中mac字段的作用。

mac值是KDF输出和ciphertext密文进行SHA3-256运算的结果:

显然,密码不同,KDF输出就会不同,计算的mac值也会不同,因此可以通过比对mac值是否相同来检验密码的正确性。

检验过程如图10-11所示。

图10-11 校验私钥

因此解密出私钥的流程如图10-12所示。

图10-12 keystore解密

通过对keystrore原理的介绍,我们更能理解它的作用,接下来继续完成通过keystrore文件实现导入账号。

10.5.2 导出和导入keystore

ethers.js直接提供了加载keystore JSON文件来创建钱包对象以及加密生成keystore文件的方法,代码如下:

结合界面来完整地实现keystore文件的导出及导入,先实现导出功能,UI界面如图10-13所示。

图10-13 钱包UI-导出keystore

HTML代码如下:

上面主要定义了一个密码输入框和一个导出按钮,单击“导出”按钮后,逻辑处理代码如下:

FileSaver.js是可以用来在页面保存文件的一个库。

再来看看如何实现导入keystore文件,UI界面如图10-14所示。

图10-14 钱包UI-加载keystore

HTML代码如下:

上面主要定义了一个文件输入框、一个密码输入框以及一个“解密“按钮,因此处理逻辑包含两部分:一是读取文件,二是解析加载账号,关键代码如下:

10.6 获取钱包余额

前面10.4节、10.5节介绍创建(或导入)钱包账号的过程都是离线的,也就是说不需要依赖以太坊网络即可创建钱包账号,但如果想获取钱包账号的余额、交易记录以及发起交易,就需要让钱包连上以太坊的网络。

10.6.1 连接以太坊网络

在以太坊中,供用户连接到区块链网络的节点被称作节点提供者(Provider),可以把它理解为是网络连接的抽象,在连接区块链网络时就需要指定一个节点提供者,ethers.js集成多种封装以方便接入不同的节点,下面举几个例子。

·Web3Provider:使用由MetaMask等钱包注入页面的Provider。

·EtherscanProvider和InfuraProvider:如果没有自己的节点,可以使用Etherscan及Infura的Provider,它们都是以太坊的基础设施服务提供商。

·JsonRpcProvider和IpcProvider:如果有自己的节点可以使用,可以连接主网、测试网络、私有网络或Ganache,这也是本章使用的方式。

使用钱包连接Provider的方法如下:

wallet为导入或创建账号时生成的钱包对象,而activeWallet将在后面10.6节、10.7节、10.8节中被用来请求余额以及发送交易。

启动Geth的需要注意,需要使用--rpc --rpccorsdomain开启RPC通信及跨域。

其实ethers.js还提供了一种默认的Provider,它的背后对应着多个节点服务,通过指定参数,就可以连接到相应的节点,用法如下:

getDefaultProvider的第一个参数network网络名称,取值有rinkeby、ropsten、kovan等,第二个参数可以指定节点服务商的标识,如infura的projectID。

关于Provider的更多用法,可以参考ethers.js文档。

10.6.2 查询余额

连接到以太坊网络之后,就可以向网络请求余额,为了方便显示交易的情况,这里顺带获取了账号交易数量Nonce,ether.js中对应的API如下:

activeWallet是连接了Provider的钱包对象,我们要实现的功能是通过钱包对象调用相应的API,获取余额及交易数量后显示到界面中,显示效果如图10-15所示。

图10-15 钱包详情界面

HTML界面代码如下:

在上述代码中,使用getBalance()获取到的金额是以wei为单位的金额,而我们通常说的以太币一般是指以ether为单位,因此在显示时,我们对金额作了单位转换。

10.7 发送交易

发送交易是钱包中最常用的功能,在ether.js发送交易只需要调用钱包对象的sendTransaction()函数,不过为了方便读者在其他平台实现它,这里还是探究一下发送交易的细节。

发送一个交易其实包含三个动作:

·构造交易

·交易签名

·发送交易前面两步,构造交易及交易签名是可以在离线的状态下进行的,这样可以降低账号私钥及助记词被盗风险,提高安全性。

10.7.1 构造交易

发送交易的第一步是构造交易结构,我们先来看看一个交易长什么样子:

发起交易的时候,就需要填充每一个字段,构建这样一个交易结构。

·to:转账的目标,即向哪一个地址转账。

·value:转账的金额。

·data:交易时附加的消息,如果是对合约地址发起交易,这会转化为对合约函数的执行,可参考前面介绍的ABI编码。

·nonce:交易序列号。

·chainId:链id,用来区分不同的链(分叉链),id可在EIP-155查询。

补充:nonce和chainId有一个重要的作用就是防止重放攻击(一个交易被执行多次),如果没有nonce的活,收款人可能把这笔签名过的交易再次进行广播,没有chainId的话,以太坊上的交易可以拿到其他以太坊链(如以太坊经典ETC)上再次进行广播。

·gasLimit:和gasPrice一起,用来控制给矿工打包交易的费用。

gasLimit用来设置交易的预期工作量,如果实际交易运算工作量超出给定的gasLimit,则交易会触发out-of-gas错误。

一个普通转账的交易,工作量是固定的,gasLimit为21 000,而执行合约的gasLimit,取决于合约运算的复杂度,通常与链交互的库(如web3.js、ethers.js等)都会提供测算gasLimit的API。

·gasPrice:指定交易发起者愿意为单位工作量支付的费用,几个参数的设置比较固定,gasPrice则需要依赖网络的拥堵情况来设定。

因为矿工是按照gasPrice对交易排序后再打包的,gasPrice越高,就排在越靠前,越快被打包,因此如果出价过低,会导致交易迟迟不能打包确认。

在web3和ethers.js中,提供了getGasPrice()方法用来获取最近几个历史区块gasPrice的中位数,可以作为设定gasPrice的参考值,如果是正式产品,还可以使用第三方提供预测gasPrice的接口,(例如使用https://ethgasstation.info/index.php),第三方服务通常还会参考当前交易池内的交易数量及价格,可参考性更强一些。

10.7.2 交易签名

在构建交易之后,就是用私钥对其签名,代码如下:

代码使用了ethereumjs-tx库来实现签名。

10.7.3 发送交易

然后就是发送交易,使用web3.js完成签名的代码如下:

通过前面三步完成了交易构造、签名及发送的过程,不过ethers.js提供了非常简洁的API来完成这三步操作,我们基于ethers.js来实现发送交易并不需要这么麻烦。

10.7.4 Ethers.js发送交易

Ethers.js发送交易只需要调用Wallet对象的sendTransaction()函数,因为钱包对象在创建的时候,已经可以获得私钥相关信息,所以它可以自动帮我们完成签名。

发送交易的代码如下:

来看看发送交易的UI界面,如图10-16所示。

图10-16 以太币转账界面

界面的HTML代码如下:

上述代码定义了两个文本输入框用来输入转账的目标地址和转账金额,以及一个“发送”按钮用来触发转账,JavaScript逻辑部分的关键代码如下:

在发起转账交易时,我们应该对用户输入的目标地址做一个检查,防止用户输入错误,我们这里使用getAddress()得到一个区分大小写的地址。

转账交易的金额需要以wei为单位,因此代码使用parseEther()做了一个单位转换,gasLimit和gasPrice可以省略,这是会自动测量gasLimit,并使用getGasPrice()作为默认值。

10.8 交易ERC20代币

第8章智能合约案例介绍了如何创建ERC20代币,这一节介绍在钱包中交易ERC20代币。

钱包中发送ERC20代币需要调用ERC20合约的transfer()函数,获取代币余额调用的是ERC20合约的balanceOf()函数,调用合约的函数需要知道合约的ABI接口信息。符合ERC20标准接口的合约,其ABI信息都是一样的。

10.8.1 构造合约对象

调用合约函数需要先构造合约对象,ethers.js构造合约对象需要提供三个参数(ABI、合约地址及Provider)给ethers.Contract构造函数,代码如下:

然后就可以使用contract对象来调用Token合约的函数。

10.8.2 获取代币余额

结合用户交互界面来实现获取代币余额,界面如图10-17所示。

图10-17 钱包UI-Token余额

在HTML里,定义的标签如下:

在合约内部,余额是基于decimals来进行内部存储的(可回顾本书第8章),调用balanceOf()获取的余额需要根据小数点位数进行转换,例如代币的decimals是4,获取的余额是12 000,则需要转换为1.2显示。

10.8.2 转移代币

转移代币界面和10.7节的转账界面基本上是一样的,如图10-18所示。

图10-18 钱包UI-Token转移

界面的HTML代码如下:

上面定义了两个文本输入框和一个“发送”按钮,在逻辑处理部分,转移代币需要发起一个交易以调用合约的transfer()方法,不像以太币转账gas是固定的,代币转账在不同的合约中消耗的gas是不一样的,我们这里演示如何测量gas。

处理发送逻辑的关键代码如下:

上述有一个地方要注意,在合约调用transfer()之前,需要关联钱包对象,因为发起交易的时候需要用它来进行签名。

所有会更改区块链数据的函数都需要关联钱包对象,如果是调用视图函数(例如调用balanceOf())则只需要连接Provider(我们在构造合约对象时已经将它传入)。

代码中使用了ethers.js的Contract提供了contract.estimate.合约方法()来测量合约方法需要的gasLimit,测量gasLimit其实不是必须的,ethers.js在发起交易的时候,其实总是先进行测量。

好了,恭喜你,你已经掌握了如何实现以太坊钱包的大部分知识点。

       

【声明】内容源于网络
0
0
数组智控产业发展科技院
以AI技术为底层能力,聚焦智慧园区、城市公共安全、数智警务、健康医疗、能源电力、科研实验及平安校园等领域,提供从感知到决策的全流程软硬件一体化的国产装备智能体产品解决方案。
内容 986
粉丝 0
数组智控产业发展科技院 以AI技术为底层能力,聚焦智慧园区、城市公共安全、数智警务、健康医疗、能源电力、科研实验及平安校园等领域,提供从感知到决策的全流程软硬件一体化的国产装备智能体产品解决方案。
总阅读940
粉丝0
内容986