• 以太坊钱包开发系列


    1 - 创建钱包账号

    以太坊去中心化网页钱包开发系列,将从零开始开发出一个可以实际使用的钱包,本系列文章是理论与实战相结合,一共有四篇:创建钱包账号账号Keystore文件导入导出展示钱包信息及发起签名交易发送Token(代币),这是第一篇,主要介绍钱包将实现哪些功能及怎么创建钱包账号,本钱包是基于ethers.js 进行开发。

    去中心化网页钱包

    先明确一下定义,什么是去中心化钱包,账号秘钥的管理,交易的签名,都是在客户端完成, 即私钥相关的信息都是在用户手中,钱包的开发者接触不到私钥信息。

    对应的中心化钱包则是私钥由中心服务器托管,如交易所的钱包就是这种。

    网页钱包,或者叫web钱包,是指钱包以网页的形式展现,去中心化网页钱包则交易的签名等操作是在浏览器里完成。
    其他形式的钱包,如Android钱包或iOS钱包其开发思路和web钱包一样,因此文本对开发其他平台的钱包也有参考意义,不过本系列文章主要侧重在钱包功能的实现,并未过多考虑用户体验。

    钱包功能

    一个钱包通常主要包含的功能有:

    • 账号管理(主要是私钥的管理):创建账号、账号导入导出
    • 账号信息展示:如以太币余额、Token(代币)余额。
    • 转账功能:发送以太币及发送Token(代币)

    这些功能将基于 ethers.js 进行开发, ethers.js 和web3.js 一样,也是一套和以太坊区块链进行交互的库,不仅如此,ethers.js 还对BIP 39等相关的提案进行了实现,可以在这个链接阅读其文档。

    这些功能主要表现为钱包的两个界面,一个界面是:账号管理,一个界面是进行账号信息展示及转账。下面逐个进行介绍

    创建钱包账号

    读过上一篇文章理解开发HD 钱包涉及的 BIP32、BIP44、BIP39的同学,会知道创建账号,可以有两种方式:

    • 直接生成32个字节的数当成私钥
    • 通过助记词进行确定性推导出私钥

    使用随机数作为私钥创建钱包账号

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

    var privateKey = ethers.utils.randomBytes(32);
    var wallet = new ethers.Wallet(privateKey);
    console.log("账号地址: " + wallet.address);

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

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

    let keyNumber = ethers.utils.bigNumberify(privateKey);
    console.log(randomNumber._hex);

    现在我们结合界面,完整的实现创建账号,其效果图如下,加载私钥时创建账号。

    界面代码(HTML)代码如下(主要是在表格中定义个一个输入框及一个按钮):

                    <table>
                        <tr>
                            <th>私钥:</th>
                            <td><input type="text" placeholder="(private key)" id="select-privatekey" /></td>
                        </tr>
                        <tr>
                            <td> </td>
                            <td>
                                <div id="select-submit-privatekey" class="submit">加载私钥</div>
                            </td>
                        </tr>
                    </table>

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

    // 使用JQuery获取两个UI标签
        var inputPrivatekey = $('#select-privatekey');
        var submit = $('#select-submit-privatekey');
    
    // 生成一个默认的私钥
        let randomNumber = ethers.utils.bigNumberify(ethers.utils.randomBytes(32));
        inputPrivatekey.val(randomNumber._hex);
    
    // 点击“加载私钥”时, 创建对应的钱包
        submit.click(function() {
            var privateKey = inputPrivatekey.val();
            if (privateKey.substring(0, 2) !== '0x') { privateKey = '0x' + privateKey; }
           var wallet = new ethers.Wallet(privateKey));
    
        });

    如果用户提供一个已有账号的私钥,则会导入其原有账号。

    通过助记词方式创建钱包账号

    这是目前主流常见钱包的方式,关于助记词推导过程请阅读理解开发HD 钱包涉及的 BIP32、BIP44、BIP39

    我们需要先生成一个随机数,然后用随机数生成助记词,随后用助记词创建钱包账号,设计到的API有:

    
    var rand = ethers.utils.randomBytes(16);
    
    // 生成助记词
    var mnemonic = ethers.utils.HDNode.entropyToMnemonic(rand);
    
    var path = "m/44'/60'/0'/0/0";
    
    // 通过助记词创建钱包
    ethers.Wallet.fromMnemonic(mnemonic, path);

    现在我们结合界面来实现一下通过助记词方式创建钱包账号,其效果图如下:

    界面代码(HTML)代码如下(主要是在表格中定义个两个输入框及一个按钮):

        <table>
            <tr>
                <th>助记词:</th>
                <td><input type="text" placeholder="(mnemonic phrase)" id="select-mnemonic-phrase" /></td>
            </tr>
            <tr>
                <th>Path:</th>
                <td><input type="text" placeholder="(path)" id="select-mnemonic-path" value="m/44'/60'/0'/0/0" /></td>
            </tr>
            <tr>
                <td> </td>
                <td>
                    <div id="select-submit-mnemonic" class="submit">推倒</div>
                </td>
            </tr>
        </table>

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

        var inputPhrase = $('#select-mnemonic-phrase');
        var inputPath = $('#select-mnemonic-path');
        var submit = $('#select-submit-mnemonic');
    
    // 生成助记词
        var mnemonic = ethers.utils.HDNode.entropyToMnemonic(ethers.utils.randomBytes(16));
        inputPhrase.val(mnemonic);
    
        submit.click(function() {
        // 检查助记词是否有效。
            if (!ethers.utils.HDNode.isValidMnemonic(inputPhrase.val())) {
                return;
            }
    
    // 通过助记词创建钱包对象
           var wallet = ethers.Wallet.fromMnemonic(inputPhrase.val(), inputPath.val());
        });

    同样用户可以提供一个其保存的助记词来导入其钱包,有一些遗憾的是,ethers.js 暂时不支持通过添加密码作为Salt来保护种子(也可能是我没有找到,如果知道的同学,希望反馈下),如果需要此功能可以引入bip39 和 ethereumjs-wallet 库来实现,代码可参考理解开发HD 钱包涉及的 BIP32、BIP44、BIP39

    小结

    其实 ethers 还提供了一个更简单的方法来创建钱包:

       // 直接创建一个随机钱包
       ethers.Wallet.createRandom();

    完整源码请订阅深入浅出区块链技术小专栏查看, 哈哈,是不是有一点鸡贼,创作不易呀。
    戳链接收看详细的视频课程讲解

    参考文档:
    ethers.js

    2 - 账号Keystore文件导入导出

    以太坊去中心化网页钱包开发系列,将从零开始开发出一个可以实际使用的钱包,本系列文章是理论与实战相结合,一共有四篇:创建钱包账号账号Keystore文件导入导出展示钱包信息及发起签名交易发送Token(代币),这是第二篇,主要介绍钱包账号导出与导入,将对Keystore文件的生成的原理进行介绍。

    如何导入Geth创建的账号?

    上一篇文章,介绍了如何使用私钥及助记词来创建账号,如果是使用已有的私钥及助记词,这其实也是账号导入的过程。

    有一些同学会问,我的账号是Geth生成的,如何导入到钱包呢?使用Geth的同学,应该知道Geth在创建账号时会生成一个对应keystore JSON文件,Keystore文件存储加密后的私钥信息,因此我们需要做的就是导入这个Keystore文件,这个文件通常在同步区块数据的目录下的keystore文件夹(如: ~/.ethereum/keystore)里。

    尽管在ethers.js 中,简单的使用一个函数就可以完成keystore文件的导入,不过理解Keystore 文件的作用及原理还是非常有必要的,当然如果你是在没有兴趣,可以直接跳到本文最后一节:使用ethers.js 实现账号导出导入。

    详细解读 Keystore 文件

    为什么需要 Keystore 文件

    通过这篇文章理解开发HD 钱包涉及的 BIP32、BIP44、BIP39,私钥其实就代表了一个账号,最简单的保管账号的方式就是直接把私钥保存起来,如果私钥文件被人盗取,我们的数字资产将洗劫一空。

    Keystore 文件就是一种以加密的方式存储密钥的文件,这样的发起交易的时候,先从Keystore 文件是使用密码解密出私钥,然后进行签名交易。这样做之后就会安全的多,因为只有黑客同时盗取 keystore 文件和密码才能盗取我们的数字资产。

    Keystore 文件如何生成的

    以太坊是使用对称加密算法来加密私钥生成Keystore文件,因此对称加密秘钥(注意它其实也是发起交易时需要的解密秘钥)的选择就非常关键,这个秘钥是使用KDF算法推导派生而出。因此在完整介绍Keystore 文件如何生成前,有必要先介绍一下KDF。

    使用 KDF 生成秘钥

    密码学KDF(key derivation functions),其作用是通过一个密码派生出一个或多个秘钥,即从 password 生成加密用的 key。

    其实在理解开发HD 钱包涉及的 BIP32、BIP44、BIP39中介绍助记词推导出种子的PBKDF2算法就是一种KDF函数,其原理是加盐以及增加哈希迭代次数。

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

    DK = Scrypt(salt, dk_len, n, r, p)

    其中的 salt 是一段随机的盐,dk_len 是输出的哈希值的长度。n 是 CPU/Memory 开销值,越高的开销值,计算就越困难。r 表示块大小,p 表示并行度。

    Litecoin 就使用 scrypt 作为它的 POW 算法

    实际使用中,还会加上一个密码进行计算,用一张图来表示这个过程就是:

    对私钥进行对称加密

    上面已经用KDF算法生成了一个秘钥,这个秘钥就是接着进行对称加密的秘钥,这里使用的对称加密算法是 aes-128-ctr,aes-128-ctr 加密算法还需要用到一个参数初始化向量 iv。

    Keystore文件

    好了,我们现在结合具体 Keystore文件的内容,就很容易理解了Keystore 文件怎么产生的了。

    {  
       "address":"856e604698f79cef417aab...",
       "crypto":{  
          "cipher":"aes-128-ctr",
          "ciphertext":"13a3ad2135bef1ff228e399dfc8d7757eb4bb1a81d1b31....",
          "cipherparams":{  
             "iv":"92e7468e8625653f85322fb3c..."
          },
          "kdf":"scrypt",
          "kdfparams":{  
             "dklen":32,
             "n":262144,
             "p":1,
             "r":8,
             "salt":"3ca198ce53513ce01bd651aee54b16b6a...."
          },
          "mac":"10423d837830594c18a91097d09b7f2316..."
       },
       "id":"5346bac5-0a6f-4ac6-baba-e2f3ad464f3f",
       "version":3
    }
    
    

    来解读一下各个字段:

    • address: 账号地址
    • version: Keystore文件的版本,目前为第3版,也称为V3 KeyStore。
    • id : uuid
    • crypto: 加密推倒的相关配置.
      • cipher 是用于加密以太坊私钥的对称加密算法。用的是 aes-128-ctr 。
      • cipherparams 是 aes-128-ctr 加密算法需要的参数。在这里,用到的唯一的参数 iv。
      • ciphertext 是加密算法输出的密文,也是将来解密时的需要的输入。
      • kdf: 指定使用哪一个算法,这里使用的是 scrypt。
      • kdfparams: scrypt函数需要的参数
      • mac: 用来校验密码的正确性, mac= sha3(DK[16:32], ciphertext) 下面一个小节单独分析。

    我们来完整梳理一下 Keystore 文件的产生:
    1. 使用scrypt函数 (根据密码 和 相应的参数) 生成秘钥
    2. 使用上一步生成的秘钥 + 账号私钥 + 参数 进行对称加密。
    3. 把相关的参数 和 输出的密文 保存为以上格式的 JSON 文件

    如何确保密码是对的?

    当我们在使用Keystore文件来还原私钥时,依然是使用kdf生成一个秘钥,然后用秘钥对ciphertext进行解密,其过程如下:

    此时细心的同学会发现,无论使用说明密码,来进行这个操作,都会生成一个私钥,但是最终计算的以太坊私钥到底是不是正确的,却不得而知。

    这就是 keystore 文件中 mac 值的作用。mac 值是 kdf输出 和 ciphertext 密文进行SHA3-256运算的结果,显然密码不同,计算的mac 值也不同,因此可以用来检验密码的正确性。检验过程用图表示如下:

    现在我们以解密的角度完整的梳理下流程,就可以得到以下图:

    用ethers.js 实现账号导出导入

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

    // 导入keystore Json
        ethers.Wallet.fromEncryptedJson(json, password, [progressCallback]).then(function(wallet) {
           // wallet
        });
    
        // 使用钱包对象 导出keystore Json
        wallet.encrypt(pwd, [progressCallback].then(function(json) {
            // 保存json
        });

    现在结合界面来完整的实现账号导出及导入,先看看导出,UI图如下:

    HTML 代码如下:

        <h3>KeyStore 导出:</h3>
        <table>
            <tr>
                <th>密码:</th>
                <td><input type="text" placeholder="(password)" id="save-keystore-file-pwd" /></td>
            </tr>
    
            <tr>
                <td> </td>
                <td>
                    <div id="save-keystore" class="submit">导出</div>
                </td>
            </tr>
        </table>

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

    // "导出" 按钮,执行exportKeystore函数
      $('#save-keystore').click(exportKeystore);
    
      exportKeystore: function() {
        // 获取密码
        var pwd = $('#save-keystore-file-pwd');
    
        // wallet 是上一篇文章中生成的钱包对象
        wallet.encrypt(pwd.val()).then(function(json) {
          var blob = new Blob([json], {type: "text/plain;charset=utf-8"});
    
          // 使用了FileSaver.js 进行文件保存
          saveAs(blob, "keystore.json");
    
        });
      }

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

    再来看看导入keystore 文件, UI图如下:

     <h2>加载账号Keystore文件</h2>
    <table>
        <tr>
            <th>Keystore:</th>
            <td><div class="file" id="select-wallet-drop">把Json文件拖动到这里</div><input type="file" id="select-wallet-file" /></td>
        </tr>
        <tr>
            <th>密码:</th>
            <td><input type="password" placeholder="(password)" id="select-wallet-password" /></td>
        </tr>
        <tr>
            <td> </td>
            <td>
                <div id="select-submit-wallet" class="submit disable">解密</div>
            </td>
        </tr>
    </table>

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

     // 使用FileReader读取文件,
    
    var fileReader = new FileReader();
      fileReader.onload = function(e) {
        var json = e.target.result;
    
        // 从加载
        ethers.Wallet.fromEncryptedJson(json, password).then(function(wallet) {
    
        }, function(error) {
    
        });
    
      };
    fileReader.readAsText(inputFile.files[0]);

    哈哈哈,有到了推广时间了,完整源码请订阅深入浅出区块链技术小专栏查看,赶紧订阅吧,走过路过,不容错过。

    参考文档

    what-is-an-ethereum-keystore-file

    3 - 展示钱包信息及发起签名交易

    以太坊去中心化网页钱包开发系列,将从零开始开发出一个可以实际使用的钱包,本系列文章是理论与实战相结合,一共有四篇:创建钱包账号账号Keystore文件导入导出展示钱包信息及发起签名交易发送Token(代币),这是第三篇介绍使用ethers.js的钱包对象获取相关信息及发起你离线交易。

    使用 Provider 连接以太坊网络

    我们前面两篇文章介绍创建(或导入)钱包账号的过程都是是离线的,即不需要依赖以太坊网络即可创建钱包账号,但如果想获取钱包账号的相关信息,比如余额、交易记录,发起交易的话,就需要让钱包连上以太坊的网络。

    不管是在 Web3 中,还是Ethers.js 都是使用 Provider 来进行网络连接的,Ethers.js 提供了集成多种 Provider 的方式:

    • Web3Provider: 使用一个已有的web3 兼容的Provider,如有MetaMask 或 Mist提供。

    • EtherscanProvider 及 InfuraProvider: 如果没有自己的节点,可以使用Etherscan 及 Infura 的Provider,他们都是以太坊的基础设施服务提供商,Ethers.js 还提供了一种更简单的方式:使用一个默认的provider, 他会自动帮我们连接Etherscan 及 Infura。

      let defaultProvider = ethers.getDefaultProvider('ropsten');

      连接Provider, 通常有一个参数network网络名称,取值有: homesteadrinkebyropstenkovan, 关于Provider的更多用法,可以参考Ethers.js Provider

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

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

    // 连接本地的geth 节点,8545是geth 的端口
    var provider = new ethers.providers.JsonRpcProvider("http://127.0.0.1:8545");
    
    // wallet 为前两篇文章中生成的钱包对象, activeWallet就是后面可以用来请求余额发送交易的对象
    var activeWallet = wallet.connect(App.provider);

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

    展示钱包详情:查询余额及Nonce

    连接到以太坊网络之后,就可以向网络请求余额以及获取账号交易数量,使用一下API:

    activeWallet.getBalance().then(function(balance) {
    });
    
    activeWallet.getTransactionCount().then(function(transactionCount) {
    });

    activeWallet就是后面可以用来请求发送交易的对象

    <h3>钱包详情:</h3>
    <table>
        <tr><th>地址:</th>
            <td>
                <input type="text" readonly="readonly" class="readonly" id="wallet-address" value="" /></div>
            </td>
        </tr>
        <tr><th>余额:</th>
            <td>
                <input type="text" readonly="readonly" class="readonly" id="wallet-balance" value="0.0" /></div>
            </td>
        </tr>
        <tr><th>Nonce:</th>
            <td>
                <input type="text" readonly="readonly" class="readonly" id="wallet-transaction-count" value="0" /></div>
            </td>
        </tr>
        <tr><td> </td>
            <td>
                <div id="wallet-submit-refresh" class="submit">刷新</div>
            </td>
        </tr>
    </table>
    

    js处理的逻辑就是获取信息之后,填充相应的控件,代码如下:

    var inputBalance = $('#wallet-balance');
    var inputTransactionCount = $('#wallet-transaction-count');
    
    $("#wallet-submit-refresh").click(function() {
    
    // 获取余额时, 包含当前正在打包的区块
       activeWallet.getBalance('pending').then(function(balance) {
              // 单位转换 wei -> ether
              inputBalance.val(ethers.utils.formatEther(balance, { commify: true }));
          }, function(error) {
          });
    
       activeWallet.getTransactionCount('pending').then(function(transactionCount) {
              inputTransactionCount.val(transactionCount);
          }, function(error) {
          });
    });
    
    // 模拟一次点击获取数据
    $("#wallet-submit-refresh").click();

    发送签名交易

    之前我们有一篇文章:如何使用Web3.js API 在页面中进行转账介绍过发起交易,不过当时的签名是利用MetaMask来完成的,现在我们要完成一个钱包,必须要发送一个签名交易,签名交易也称为离线交易(因为这个过程可以离线进行:在离线状态下对交易进行签名,然后把签名后的交易进行广播)。

    尽管 Ethers.js 提供了非常简洁的API来发送签名交易,但是探究下简洁API背后的细节依然会对我们有帮助,这个过程大致可分为三步:

    1. 构造交易
    2. 交易签名
    3. 发送(广播)交易

    构造交易

    先来看看一个交易长什么样子:

    const txParams = {
      nonce: '0x00',
      gasPrice: '0x09184e72a000',
      gasLimit: '0x2710',
      to: '0x0000000000000000000000000000000000000000',
      value: '0x00',
      data: '0x7f7465737432000000000000000000000000000000000000000000000000000000600057',
      // EIP 155 chainId - mainnet: 1, ropsten: 3
      chainId: 3
    }

    发起交易的时候,就是需要填充每一个字段,构建这样一个交易结构。
    to 和 value: 很好理解,就是用户要转账的目标及金额。
    data: 是交易时附加的消息,如果是对合约地址发起交易,这会转化为对合约函数的执行,可参考:如何理解以太坊ABI
    nonce: 交易序列号
    chainId: 链id,用来去区分不同的链(分叉链)id可在EIP-55查询。

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

    gasPricegasLimit: Gas是以太坊的工作计费机制,是由交易发起者给矿工打包的费用。上面几个参数的设置比较固定,Gas的设置(尤其是gasPrice)则灵活的多。

    gasLimit 表示预计的指令和存储空间的工作量,如果工作量没有用完,会退回交易发起者,如果不够会发生out-of-gas 错误。
    一个普通转账的交易,工作量是固定的,gasLimit为21000,合约执行gasLimit则是变化的,也许有一些人会认为直接设置为高一点,反正会退回,但如果合约执行出错,就会吃掉所有的gas。幸运的是web3 和 ethers.js 都提供了测算Gas Limit的方法,下一遍发送代币

    gasPrice是交易发起者是愿意为工作量支付的单位费用,矿工在选择交易的时候,是按照gasPrice进行排序,先服务高出价者,因此如果出价过低会导致交易迟迟不能打包确认,出价过高对发起者又比较亏。

    web3 和 ethers.js 提供一个方法 getGasPrice() 用来获取最近几个历史区块gas price的中位数,也有一些第三方提供预测gas price的接口,如:gasPriceOracle 、 ethgasAPI、 etherscan gastracker,这些服务通常还会参考当前交易池内交易数量及价格,可参考性更强,

    常规的一个做法是利用这些接口给用户一个参考值,然后用户可以根据参考值进行微调。

    交易签名

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

    const tx = new EthereumTx(txParams)
    tx.sign(privateKey)
    const serializedTx = tx.serialize()

    代码参考ethereumjs-tx

    发送(广播)交易

    然后就是发送(广播)交易,代码如下:

    web3.eth.sendRawTransaction(serializedTx, function (err, transactionHash) {
        console.log(err);
        console.log(transactionHash);
    });

    通过这三步就完成了发送签名交易的过程,ethers.js 里提供了一个简洁的接口,来完成所有这三步操作(强调一下,签名已经在接口里帮我们完成了),接口如下:

     activeWallet.sendTransaction({
                to: targetAddress,
                value: amountWei,
                gasPrice: activeWallet.provider.getGasPrice(),
                gasLimit: 21000,
            }).then(function(tx) {
            });

    用ethers.js 实现发送交易

    先来看看发送交易的UI界面:

    <h3>以太转账:</h3>
    <table>
        <tr> <th>发送至:</th>
            <td><input type="text" placeholder="(target address)" id="wallet-send-target-address" /></td>
        </tr>
        <tr> <th>金额:</th>
            <td><input type="text" placeholder="(amount)" id="wallet-send-amount" /></td>
        </tr>
        <tr> <td> </td>
            <td>
                <div id="wallet-submit-send" class="submit disable">发送</div>
            </td>
        </tr>
    </table>

    上面主要定义了两个文本输入框及一个“发送“按钮,在点击发送时运行一下(关键)代码:

        var inputTargetAddress = $('#wallet-send-target-address');
        var inputAmount = $('#wallet-send-amount');
        var submit = $('#wallet-submit-send');
    
        submit.click(function() {
        // 得到一个checksum 地址
            var targetAddress = ethers.utils.getAddress(inputTargetAddress.val());
        // ether -> wei
            var amountWei = ethers.utils.parseEther(inputAmount.val());
            activeWallet.sendTransaction({
                to: targetAddress,
                value: amountWei,
                // gasPrice: activeWallet.provider.getGasPrice(),  (可用默认值)
                // gasLimit: 21000,
            }).then(function(tx) {
                console.log(tx);
            });
        })

    哈哈哈~, 干活介绍到这里,现在夹带一点私货,有到了推广时间了,完整源码请订阅深入浅出区块链技术小专栏查看,赶紧订阅吧,走过路过,不容错过。

    参考文档

    1. ethereum-tx
    2. EIP-55
    3. Ethers.js

     

  • 相关阅读:
    Gradle构建模块化项目
    线程池的理解与应用
    Redis理解
    kafka监听出现的问题,解决和剖析
    shiro利用过期时间,解决用户冻结踢出问题
    信息系统的运行与维护包含的主要内容
    软件维护的内容是什么
    执行顺序
    Chrome/Edge 91版本SameSite by default cookies被移除后的解决方案
    公从号编程
  • 原文地址:https://www.cnblogs.com/hzcya1995/p/13312866.html
Copyright © 2020-2023  润新知