以太坊钱包开发

  目录

使用vue开发以太坊钱包

准备工作

项目创建

版本信息:

Node 16.14

安装vue-cli

1
npm install -g @vue/cli

验证vue-cli 安装

1
2
3
$ vue -V
// 结果 出现vue版本号 安装成功
@vue/cli 5.0.8

通过vue-cli创建项目

1
2
3
4
5
6
7
$ vue create <项目名称>
// 选择配置
Vue CLI v5.0.8
? Please pick a preset:
Default ([Vue 3] babel, eslint)
Default ([Vue 2] babel, eslint)
❯ Manually select features `选择该项`

选择自定义配置

1
2
3
4
5
6
7
8
9
10
11
? Check the features needed for your project: (Press <space> to select, <a> to 
toggle all, <i> to invert selection, and <enter> to proceed)
◉ Babel `选择该项`
◯ TypeScript
◯ Progressive Web App (PWA) Support
◉ Router `选择该项`
◯ Vuex
❯◉ CSS Pre-processors `选择该项`
◯ Linter / Formatter
◯ Unit Testing
◯ E2E Testing

选择vue版本

1
2
3
4
? Choose a version of Vue.js that you want to start the project with (Use arrow 
keys)
❯ 3.x
2.x

选择路由设置

1
2
? Use history mode for router? (Requires proper server setup for index fallback 
in production) (Y/n) y `输入y或者n`

选择预处理语言

1
2
3
4
5
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported 
by default):
Sass/SCSS (with dart-sass)
❯ Less
Stylus

选择配置文件存放目录

1
2
3
? Where do you prefer placing config for Babel, ESLint, etc.? 
❯ In dedicated config files
In package.json

安装成功后 运行测试

1
2
$ cd 项目名称
$ npm run serve

成功界面

image-20230102200435157

第三方包安装

web3相关第三方包

1
npm install web3 bip39 ethereumjs-tx@1.3.7 ethereumjs-util ethereumjs-wallet

注: ethereumjs-tx 使用1.3.7 版本

node-polyfill 兼容文件配置

  1. 下载polyfill 插件

    1
    npm install node-polyfill-webpack-plugin -D
  2. 在vue.config.js 文件中配置插件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const { defineConfig } = require('@vue/cli-service')
    // 引入插件
    ++ const NodePolyfillWebpackPlugin = require("node-polyfill-webpack-plugin");
    module.exports = defineConfig({
    transpileDependencies: true,
    ++ configureWebpack: {
    ++ plugins: [
    ++ new NodePolyfillWebpackPlugin()
    ],
    },
    })

配置vant-ui ui组件库

https://vant-contrib.gitee.io/vant/#/zh-CN

  1. 安装
1
2
$ npm i vant
$ npm i unplugin-vue-components -D
  1. 在vue.config.js 文件中配置插件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const { defineConfig } = require("@vue/cli-service");
// 引入插件
const NodePolyfillWebpackPlugin = require("node-polyfill-webpack-plugin");
// vant
++ const { VantResolver } = require('unplugin-vue-components/resolvers');
++ const ComponentsPlugin = require('unplugin-vue-components/webpack');
module.exports = defineConfig({
transpileDependencies: true,
configureWebpack: {
plugins: [
new NodePolyfillWebpackPlugin(),
++ ComponentsPlugin({ resolvers: [VantResolver()]}),
],
},
});
  1. 测试 在app.vue文件添加代码
1
2
3
4
<template>
<van-button type="primary">test</van-button>
<van-button type="success">test</van-button>
</template>

image-20230102202255947

通过vw配置响应式

https://www.cnblogs.com/hongrun/p/16130707.html

  1. 安装
1
npm install postcss-px-to-viewport -D
  1. 在根目录下创建 名为 postcss.config.js 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
plugins: {
"postcss-px-to-viewport": {
unitToConvert: "px", // 要转化的单位
viewportWidth: 375, // UI设计稿的宽度
unitPrecision: 6, // 转换后的精度,即小数点位数
propList: ["*"], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
viewportUnit: "vw", // 指定需要转换成的视窗单位,默认vw
fontViewportUnit: "vw", // 指定字体需要转换成的视窗单位,默认vw selectorBlackList: ["wrap"], // 指定不转换为视窗单位的类名,
minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
replace: true, // 是否转换后直接更换属性值
exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配
},
},
};

web3连接到以太坊网络(测试网、主网)

  1. 什么是web3
    web3是以太坊官方开提供的一个连接以太坊区块链的模块,允许您使用HTTP或IPC与本地或远程以太坊节点进行交互,它包含以太坊生态系统的几乎所有功能。web3模块主要连接以太坊暴露出来的RPC层。开发者利用web3连接RPC层,可以连接任何暴露了RPC接口的节点,从而与区块链交互。web3是一个集合库,支持多种开发语言使用wbe3,其中的JavaScript API叫做web3.js、另外还有web3.py、web3j,web3.js将是我们钱包开发项目的重点。

web3.eth:用于与以太坊区块链和智能合约之间的交互。

web3.utils:包含一些辅助方法。

web3.shh:用于协议进行通信的P2P和广播。

web3.bzz:用于与群网络交互的Bzz模块。

github地址:https://github.com/web3/web3.js/tree/v1.0.0-beta.34

web3.js开发文档:https://web3js.readthedocs.io/en/v1.8.1/

web3.js 中文文档 : https://learnblockchain.cn/docs/web3.js/

  1. 实例化web3对象
    web3要与以坊节点进行交互,需要创建一个web3对象,下面看看如何创建。
1
2
3
var Web3 = require('web3');
// "Web3.providers.givenProvider" will be set if in an Ethereum supported browser.
var web3 = new Web3(Web3.givenProvider || 'ws://some.local-or-remote.node:8546');

根据API可知需要指定节点地址,我们将ws://some.local-or-remote.node:8546换成其它连接到以太坊网络的节点的地址,以此来确定连接的以太坊的网络。那么连接到以太坊网络的节点的地址是多少呢?这里我们需要使用到infura。

  1. 获取连接到以太坊网络的节点地址
    infura提供公开的 Ethereum主网和测试网络节点,到infura.io网站注册后即可获取各个网络的地址。请按照如下步骤获取地址。

第一步:打开 infura网站地址:https://infura.io/dashboard,使用邮箱注册后登陆

第二步:点击上图标记的“create new project”按钮创建一个新项目。然后弹出如下弹框,在输入框输入项目名,如”MyEtherWallet“,然后点击“create project”按钮创建。

第三步:然后会显示如下界面,点击下图中的选择框,可以看到提供主网、Kovan测试网络、Ropsten测试网络、GoerLi测试网络的节点地址。

image.png

第四步:选择GoerLi测试网络,然后复制地址,将获取到类似这样的地址:https://kovan.infura.io/v3/d93f......cd67,如下。

image.png

  1. 连接到以太坊GoerLi测试网络
    现在将复制的地址替换掉实例化web对象的地址,如下
1
2
3
var Web3 = require("web3")
var web3 = new Web3(Web3.givenProvider || 'wss://goerli.infura.io/ws/v3/cb7e63cf28244e4499b4b6fb6162e746');
console.log("Web3:", web3)

连接到以太坊主网与GoerLi测试网络一样的,只需复制主网节点的地址去实例化web3即可。由于在主网上交易需要花费gas,因此我们基于GoerLi测试网络进行开发,后续开发完成后可再切换到主网。

image-20230102215334761

Web3js 高频 Api

账号创建

  1. 创建账号需要使用web3.js的如下API

API

1
web3.eth.accounts.create([entropy]);

image.png
参数:

entropy - String (可选): 它是一个可选项,是一个随机字符串,将作为解锁账号的密码。如果没有传递字符串,则使用random生成随机字符串。

返回值:

Object:包含以下字段的一个帐户对象:

address- string:帐户地址。

privateKey- string:帐户私钥。前端永远不应该在localstorage中以未加密的方式共享或存储!

signTransaction(tx [, callback])- Function:签名交易的方法。

sign(data)- Function:签名二进制交易的方法。

例子

1
web3.eth.accounts.create('2435@#@#@±±±±!!!!678543213456764321§34567543213456785432134567');
1
2
3
4
5
6
7
8
web3.eth.accounts.create('2435@#@#@±±±±!!!!678543213456764321§34567543213456785432134567');
{
address: "0xF2CD2AA0c7926743B1D4310b2BC984a0a453c3d4",
privateKey: "0xd7325de5c2c1cf0009fac77d3d04a9c004b038883446b065871bc3e831dcd098",
signTransaction: function(tx){...},
sign: function(data){...},
encrypt: function(password){...}
}
  1. 获取地址
    使用APIweb3.eth.accounts.create()创建了新账户后生成了一个账户对象,在该对象中拥有addreds属性,即账户的私钥。
1
2
3
let account = web3.eth.accounts.create("123456")
let address = account.address
//address:0xfF0B5A0AA68249cD161b606679DB49CBD9a12cd0

3.获取私钥

使用APIweb3.eth.accounts.create()创建了新账户后生成了一个账户对象,在该对象中拥有privateKey属性,即账户的私钥。

1
2
3
let account = web3.eth.accounts.create()
let privateKey = account.privateKey
//privateKey:0x348ce564d427a3311b6536bbcff9390d69395b06ed6c486954e971d960fe8709

余额获取

根据地址获取以wei为单位余额

1
2
3
web3.eth.getBalance(address).then((ret) => {
console.log(ret)
});

单位转换

  1. Eth 转为 wei
1
2
3
4
5
# Web3 或者实例后的web3对象都可以
const num = Web3.utils.toWei("0.3");
const num = web3.utils.toWei("0.3");
console.log(num)
// 300000000000000000
  1. wei 转为Eth
1
2
3
# Web3 或者实例后的web3对象都可以
this.balance = Web3.utils.fromWei(ret, "ether");
this.balance = web3.utils.fromWei(ret, "ether");

Eth转账

API

1
web3.eth.sendSignedTransaction(signedTransactionData [, callback])

参数

  • signedTransactionData-String:以HEX格式签名的交易数据。

    交易数据对象可以包含如下字段:

    • from- String|Number:发送帐户的地址。如果未指定,则使用web3.eth.defaultAccount属性。或web3.eth.accounts.wallet中本地钱包的地址。
    • to- String:(可选)消息的目标地址,若未定义则为合同发送消息。
    • value- Number|String|BN|BigNumber:(可选)为wei中的交易转移的数量,如果是合约发送消息,则是捐赠给合约地址。
    • gas - Number:(可选,默认:待定)用于交易的gas(未使用的gas会退还)。
    • gasPrice- Number|String|BN|BigNumber:(可选)此交易的gas价格,以wei为单位,默认为web3.eth.gasPrice
    • data- String:(可选)包含合同上函数调用数据的ABI字节字符串
    • nonce- Number:(可选)随机数的整数。
  • callback-Function:(可选)可选回调,将错误对象作为第一个参数返回,结果作为第二个参数返回。

返回

PromiEvent:promise组合的事件,将在交易完成时调用。包含以下事件

  • "transactionHash"返回String:在发送事务并且事务哈希可用之后立即触发。
  • "receipt"返回Object:在交易确认时触发。
  • "confirmation"返回NumberObject:每次确认都会被调用,直到第12次确认。接收确认编号作为第一个参数,将数据作为第二个参数。
  • "error"返回Error:如果在发送过程中发生错误,则会触发。
  1. 构建转账参数

    区块链转账和支付宝转账类似,需要 发送方接收方金额密码

    另外需要添加部分区块链参数:矿工费gas地址转账交易次数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 获取账户交易次数
    let nonce = await web3.eth.getTransactionCount(fromaddress);
    // 获取预计转账gas费
    let gasPrice = await web3.eth.getGasPrice();
    // 转账金额以wei为单位
    let balance = await web3.utils.toWei(number);
    var rawTx = {
    from: fromaddress,
    nonce: nonce,
    gasPrice: gasPrice,
    to: toaddress,
    value: balance,
    data: "0x00", //转Token代币会用到的一个字段
    };
  2. 通过转账参数计算最终gas费用,并将通过私钥将转账参数进行编码加密

    ethereumjs-tx 第三方库请选择1.3.7版本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import Tx from "ethereumjs-tx";  
    // 将私钥去除“ox”后进行hex转化
    var privateKey = new Buffer(privatekey.slice(2), "hex");
    //需要将交易的数据进行预估gas计算,然后将gas值设置到数据参数中
    let gas = await web3.eth.estimateGas(rawTx);
    rawTx.gas = gas;
    // 通过 ethereumjs-tx 实现私钥加密Ï
    var tx = new Tx(rawTx);
    tx.sign(privateKey);
    var serializedTx = tx.serialize();
  3. 通过 sendSignedTransaction api发送转账交易,并且获取交易id

1
2
3
4
5
6
7
8
9
10
11
12
13

web3.eth
.sendSignedTransaction("0x" + serializedTx.toString("hex"))
.on("transactionHash", (txid) => {
console.log("交易成功,请在区块链浏览器查看");
console.log("交易id", txid);
console.log(`https://goerli.etherscan.io/tx/${txid}`);
})
// .on('receipt', (ret)=>{console.log('receipt')})
// .on('confirmation', (ret)=>{console.log('confirmation')})
.on("error", (err) => {
console.log("error:" + err);
});
  1. 区块链浏览器或者目标钱包产看转账结果

    goerli区块链浏览器 https://goerli.etherscan.io/tx/交易id

账户系统

简介

在前面的教程中我们对以太坊钱包已经有了一定的认识,上一章也重点介绍了账号地址的生成过程,在以太坊钱包中一个重点就是账户系统,在这个模块中很多初学同学不是很清楚密码、keystore、助记词与私钥它们之间的关系。下面我们来看看它们之间到底有着怎样的爱恨情仇,让大家琢磨不透。

密码

密码不是私钥,它是在创建账户时候的密码(可以修改)

密码在以下情况下会使用到:

  1. 作为转账的支付密码
  2. 用keystore导入钱包的时候需要输入的密码,用来解锁keystore的

私钥 Private Key

私钥由64位长度的十六进制的字符组成,比如:0xA4356E49C88C8B7AB370AF7D5C0C54F0261AAA006F6BDE09CD4745CF54E0115A,一个账户只有一个私钥且不能修改。
通常一个钱包中私钥和公钥是成对出现的,有了私钥,我们就可以通过一定的算法生成公钥,再通过公钥经过一定的算法生成地址,这一过程都是不可逆的。私钥一定要妥善保管,若被泄漏别人可以通过私钥解锁账号转出你的该账号的数字货币。

公钥 Public Key

公钥(Public Key)是和私钥成对出现的,和私钥一起组成一个密钥对,保存在钱包中。公钥由私钥生成,但是无法通过公钥倒推得到私钥。公钥能够通过一系列算法运算得到钱包的地址,因此可以作为拥有这个钱包地址的凭证。

Keystore

Keystore常见于以太坊钱包,它是将私钥以加密的方式保存为一份 JSON 文件,这份 JSON 文件就是 keystore,所以它就是加密后的私钥。Keystore必须配合钱包密码才能导入并使用该账号。当黑客盗取 Keystore 后,在没有密码情况下, 有可能通过暴力破解 Keystore 密码解开 Keystore,所以建议使用者在设置密码时稍微复杂些,比如带上特殊字符,至少 8 位以上,并安全存储。

助记词 Mnemonic

私钥是64位长度的十六进制的字符,不利于记录且容易记错,所以用算法将一串随机数转化为了一串12 ~ 24个容易记住的单词,方便保存记录。注意:

  1. 助记词是私钥的另一种表现形式
  2. 助记词=私钥,这是不正确的说法,通过助记词可以获取相关联的多个私钥,但是通过其中一个私钥是不能获取助记词的,因此助记词≠私钥

BIP

要弄清楚助记词与私钥的关系,得清楚BIP协议,是Bitcoin Improvement Proposals的缩写,意思是Bitcoin 的改进建议,用于提出 Bitcoin 的新功能或改进措施。BIP协议衍生了很多的版本,主要有BIP32、BIP39、BIP44。

BIP32

BIP32是 HD钱包的核心提案,通过种子来生成主私钥,然后派生海量的子私钥和地址,种子是一串很长的随机数。

BIP39

由于种子是一串很长的随机数,不利于记录,所以我们用算法将种子转化为一串12 ~ 24个的单词,方便保存记录,这就是BIP39,它扩展了 HD钱包种子的生成算法。

BIP44

BIP44 是在 BIP32 和 BIP43 的基础上增加多币种,提出的层次结构非常全面,它允许处理多个币种,多个帐户,每个帐户有数百万个地址。

在BIP32路径中定义以下5个级别:

1
m/purpse'/coin_type'/account'/change/address_index
  • purpose:在BIP43之后建议将常数设置为44’。表示根据BIP44规范使用该节点的子树。
  • Coin_type:币种,代表一个主节点(种子)可用于无限数量的独立加密币,如比特币,Litecoin或Namecoin。此级别为每个加密币创建一个单独的子树,避免重用已经在其它链上存在的地址。开发人员可以为他们的项目注册未使用的号码。
  • Account:账户,此级别为了设置独立的用户身份可以将所有币种放在一个的帐户中,从0开始按顺序递增。
  • Change:常量0用于外部链,常量1用于内部链,外部链用于钱包在外部用于接收和付款。内部链用于在钱包外部不可见的地址,如返回交易变更。
  • Address_index:地址索引,按顺序递增的方式从索引0开始编号。

BIP44的规则使得 HD钱包非常强大,用户只需要保存一个种子,就能控制所有币种,所有账户的钱包,因此由BIP39 生成的助记词非常重要,所以一定安全妥善保管,那么会不会被破解呢?如果一个 HD 钱包助记词是 12 个单词,一共有 2048 个单词可能性,那么随机的生成的助记词所有可能性大概是5e+39,因此几乎不可能被破解。

HD钱包

通过BIP协议生成账号的钱包叫做HD钱包。这个HD钱包,并不是Hardware Wallet硬件钱包,这里的 HD 是Hierarchical Deterministic的缩写,意思是分层确定性,所以HD钱包的全称为比特币分成确定性钱包 。

密码、私钥、keystore与助记词的关系

img

钱包的核心:私钥

基于以上的分析,我们对以太坊钱包的账号系统有了一个很好的认识,那么我们在使用钱包的过程中,该如何保管自己的钱包呢?主要包含以下几种方式:

  • 私钥(Private Key)
  • Keystore+密码(Keystore+Password)
  • 助记词(Mnemonic code)

通过以上三种中的一种方式都可以解锁账号,然后掌控它,所以对于每种方式中的数据都必须妥善包括,如有泄漏,请尽快转移数字资产。

我们可以得到以下总结:

  • 通过私钥+密码可以生成keystore,即加密私钥;
  • 通过keystore+密码可以获取私钥,即解密keystore。
  • 通过助记词根据不同的路径获取不同的私钥,即使用HD钱包将助记词转化成种子来生成主私钥,然后派生海量的子私钥和地址。

可以看出这几种方式的核心其实都是为了获得私钥,然后去解锁账号,因此钱包的核心功能是私钥的创建、存储和使用。

参考资料

https://web3js.readthedocs.io/en/1.0/web3-eth-accounts.html
https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
https://github.com/ethereum/EIPs/issues/84
https://github.com/ethereum/EIPs/issues/85

创建账户

从无到有创建一个新的账户

web3 直接创建账户

1
web3.eth.accounts.create('2435@#@#@±±±±!!!!678543213456764321§34567543213456785432134567');

助记词 创建账户

需要使用bip39协议将助记词转换成种子,再通过ethereumjs-wallet库生成hd钱包,根据路径的不同从hd钱包中获取不同的keypair,keypair中就包含有公钥、私钥,再通过ethereumjs-util库将公钥生成地址,从而根据助记词获取所有关联的账号,能获取到公钥、私钥、地址等数据信息。

1. 依赖库

需要用到三个库:bip39、ethereumjs-wallet/hdkey、ethereumjs-util。先安装依赖库,cd到项目跟路径运行命令npm i bip39 ethereumjs-wallet ethereumjs-util

2. 通过助记词创建账号

  • 创建助记词

    1
    2
    3
    4
    5
    6
    7
    // 引入bip39模块
    import * as bip39 from "bip39";
    // 创建助记词
    let mnemonic = bip39.generateMnemonic();
    console.log(mnemonic);
    // 结果 12位助记词
    // vote select solar shy embrace immense lizard stamp scrub vague negative forward
  • 根据助记词生成密钥对 keypair

1
2
3
4
5
6
7
8
9
      // 导入分层钱包模块
import { hdkey } from "ethereumjs-wallet";
//1.将助记词转成seed
let seed = await bip39.mnemonicToSeed("12位助记词");
//3.通过hdkey将seed生成HD Wallet
let hdWallet = hdkey.fromMasterSeed(seed);
//4.生成钱包中在m/44'/60'/0'/0/i路径的keypair
let keypair = hdWallet.derivePath("m/44'/60'/0'/0/0");
console.log(keypair);

keypair 密钥对

image-20221208222354836

3. 由keypair 获取钱包地址和私钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 获取钱包对象
let wallet = keypair.getWallet();
// 获取钱包地址
let lowerCaseAddress = wallet.getAddressString();
// 获取钱包校验地址
let CheckSumAddress = wallet.getChecksumAddressString();
// 获取私钥
let prikey = wallet.getPrivateKey().toString("hex");
console.log("lowerCaseAddress", lowerCaseAddress);
console.log("CheckSumAddress", CheckSumAddress);
console.log("prikey", prikey);
/*
lowerCaseAddress 0xd9fc0fd4412616c7075e68b151ab3a7bcb9a3f54
CheckSumAddress 0xd9fC0FD4412616c7075E68b151ab3A7bcB9A3f54
prikey 3fc11495517f1f015bbcb6c311da66e3b26b23e4c91c1285ccc4b69d9d274002
*/

导出账户

一个已经存在的账户导出 私钥 和 keystore

  1. 通过分层钱包对象 + 密码 创建keystore
1
let keystore = await wallet.toV3(data.pass1); // 参数必须为 字符串
  1. 通过私钥和密码创建 keystore
1
const keystore = await web3.eth.accounts.encrypt("账户私钥","密码");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 模拟keystore数据
const keystoreJsonV3 = {
version: 3,
id: "dbb70fb2-52ad-4e1f-9c19-0b50329f89c3",
address: "445b469888528dacd9b87246c5ce70407adaa411",
crypto: {
ciphertext:
"1e53e7e775644422600188b8134907992db40d278064ea3a966da4dcdf80db64",
cipherparams: { iv: "f9d2b047019674eee449b316f4a21491" },
cipher: "aes-128-ctr",
kdf: "scrypt",
kdfparams: {
dklen: 32,
salt: "153e074d78d0ba36fae3e46e582c42e53f61653cb5d4f1a3a3f68094e6ca0160",
n: 8192,
r: 8,
p: 1,
},
mac: "e91456c59b2505c16b80c2495ab7b4633273c2ae366cb6953f27de8cfebad629",
},
};
const res = web3.eth.accounts.decrypt(keystoreJsonV3, "1235");
console.log(res);
  1. 通过keystore解密私钥
1
2
3
4
5
6
7
8
9
10
import ethwallet from "ethereumjs-wallet";  
let pass = prompt("请输入密码");
let wallet;
try {
wallet = await ethwallet.fromV3(keystore, pass);
} catch (error) {
alert("密码错误");
return false;
}
let key = wallet.getPrivateKey().toString("hex");

导入账户

通过 私钥、助记词、keystore 导入一个已经存在的钱包账户 地址 和 私钥

  1. 通过keystore获取 私钥和地址
1
2
3
4
5
6
7
8
9
10
11
import ethwallet from "ethereumjs-wallet";  
let pass = prompt("请输入密码");
let wallet;
try {
wallet = await ethwallet.fromV3(keystore, pass);
} catch (error) {
alert("密码错误");
return false;
}
let key = wallet.getPrivateKey().toString("hex");
let address = wallet.getAddressString()
  1. 通过助记词 获取地址和私钥
1
2
3
4
5
6
7
8
9
10
11
12
let mnemonic=prompt("请输入助记词")
let seed = bip39.mnemonicToSeed(mnemonic)
let hdwallet = hdkey.fromMasterSeed(seed)
let keypair = hdWallet.derivePath("m/44'/60'/0'/0/0");
// 获取钱包对象
let wallet = keypair.getWallet();
// 获取钱包地址
let lowerCaseAddress = wallet.getAddressString();
// 获取钱包校验地址
let CheckSumAddress = wallet.getChecksumAddressString();
// 获取私钥
let prikey = wallet.getPrivateKey().toString("hex");
  1. 通过私钥获取 地址

    1
    2
    3
    4
    5
    import ethwallet from "ethereumjs-wallet";     
    let privatekey=new Buffer( prompt("请输入私钥"), 'hex' )
    let wallet = ethwallet.fromPrivateKey(privatekey)
    // 获取钱包地址
    let lowerCaseAddress = wallet.getAddressString();

区块链钱包项目流程

image-20221209110335777

1.项目准备

​ 直接采用随堂demo创建的项目 不需要重新创建

web3相关第三方包

1
npm install web3 bip39 ethereumjs-tx@1.3.7 ethereumjs-util ethereumjs-wallet

注: ethereumjs-tx 使用1.3.7 版本

node-polyfill 兼容文件配置

  1. 下载polyfill 插件

    1
    npm install node-polyfill-webpack-plugin -D
  2. 在vue.config.js 文件中配置插件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const { defineConfig } = require('@vue/cli-service')
    // 引入插件
    ++ const NodePolyfillWebpackPlugin = require("node-polyfill-webpack-plugin");
    module.exports = defineConfig({
    transpileDependencies: true,
    ++ configureWebpack: {
    ++ plugins: [
    ++ new NodePolyfillWebpackPlugin()
    ],
    },
    })

配置vant-ui ui组件库

https://vant-contrib.gitee.io/vant/#/zh-CN

  1. 安装
1
2
$ npm i vant
$ npm i unplugin-vue-components -D
  1. 在vue.config.js 文件中配置插件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const { defineConfig } = require("@vue/cli-service");
// 引入插件
const NodePolyfillWebpackPlugin = require("node-polyfill-webpack-plugin");
// vant
++ const { VantResolver } = require('unplugin-vue-components/resolvers');
++ const ComponentsPlugin = require('unplugin-vue-components/webpack');
module.exports = defineConfig({
transpileDependencies: true,
configureWebpack: {
plugins: [
new NodePolyfillWebpackPlugin(),
++ ComponentsPlugin({ resolvers: [VantResolver()]}),
],
},
});
  1. 测试 在app.vue文件添加代码
1
2
3
4
<template>
<van-button type="primary">test</van-button>
<van-button type="success">test</van-button>
</template>

image-20230102202255947

通过vw配置响应式

https://www.cnblogs.com/hongrun/p/16130707.html

  1. 安装
1
npm install postcss-px-to-viewport -D
  1. 在根目录下创建 名为 postcss.config.js 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
plugins: {
"postcss-px-to-viewport": {
unitToConvert: "px", // 要转化的单位
viewportWidth: 375, // UI设计稿的宽度
unitPrecision: 6, // 转换后的精度,即小数点位数
propList: ["*"], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
viewportUnit: "vw", // 指定需要转换成的视窗单位,默认vw
fontViewportUnit: "vw", // 指定字体需要转换成的视窗单位,默认vw selectorBlackList: ["wrap"], // 指定不转换为视窗单位的类名,
minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
replace: true, // 是否转换后直接更换属性值
exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配
},
},
};

封装缓存函数

整个项目为了保证钱包的安全性,所有账户相关的操作,不经过中心化服务器,只在缓存使用

在这里可以参考webstorage增加过期时间,cookie 等的封装

创建 src/utils/storage.js 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Storage {
setItem(key, val) {
localStorage.setItem(key, JSON.stringify(val || ""));
}
getItem(key) {
let val;
try {
val = JSON.parse(localStorage.getItem(key));
} catch {
val = null;
}
return val;
}
}
export default new Storage();

app.vue 文件引入

在app.vue 引入文件并且初始化web3.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="app">
<h3> qf - eth -wallet</h3>
</div>
</template>
<script setup>
// import * as util from "ethereumjs-util";
import ethwallet from "ethereumjs-wallet";
import { hdkey } from "ethereumjs-wallet";
import * as bip39 from "bip39";
import Tx from "ethereumjs-tx";
import Web3 from "web3";
import storage from "@/utils/storage";
var web3 = new Web3(
Web3.givenProvider ||
"https://goerli.infura.io/v3/e4f789009c9245eeaad1d93ce9d059bb"
);

</script>

2.通过Mnemonic助记词创建钱包

判断缓存是否有 钱包对象

  1. 有钱包对象显示钱包信息 地址 私钥 余额
  2. 没有钱包对象显示创建钱包按钮
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
<template>
<div class="app">
<h3>qf - eth -wallet</h3>
<h4>创建钱包</h4>
<van-button type="primary" @click="createWallet"> 创建钱包 </van-button>
<van-button type="primary" @click="importWallet">
导入钱包-助记词
</van-button>
<h4>钱包信息</h4>
{{ wallet }}
</div>
</template>
<script setup>

// import * as util from "ethereumjs-util";
import ethwallet from "ethereumjs-wallet";
import { hdkey } from "ethereumjs-wallet";
import * as bip39 from "bip39";
import Tx from "ethereumjs-tx";
import storage from "@/utils/storage";
import Web3 from "web3";
import { reactive } from "vue";
var web3 = new Web3(
Web3.givenProvider ||
"https://goerli.infura.io/v3/e4f789009c9245eeaad1d93ce9d059bb"
);
const wallet = reactive(storage.getItem("wallet") || {});
console.log(wallet);
async function createWallet() {
const pass = prompt("请输入您的钱包密码");
if (!pass) return false;
let mnemonic = bip39.generateMnemonic();
alert("您的助记词为:" + mnemonic);
const checkMnemonic = prompt("请输入您的助记词");
if (mnemonic === checkMnemonic) {
//1.将助记词转成seed
let seed = await bip39.mnemonicToSeed(mnemonic);
//3.通过hdkey将seed生成HD Wallet
let hdWallet = hdkey.fromMasterSeed(seed);
//4.生成钱包中在m/44'/60'/0'/0/i路径的keypair
let keyPair = hdWallet.derivePath("m/44'/60'/0'/0/0");
// 获取钱包对象
let wallet = keyPair.getWallet();
// 获取钱包地址
let lowerCaseAddress = wallet.getAddressString();
// 获取钱包校验地址
let CheckSumAddress = wallet.getChecksumAddressString();
// 获取私钥
let prikey = wallet.getPrivateKey().toString("hex");
let keystore = await wallet.toV3(pass);
console.log(keystore);
// 保存钱包信息
const walletInfo = {
address: lowerCaseAddress,
prikey,
keystore,
balance: 0,
mnemonic, // 助记词不应该记录下来仅仅是为了便于演示
};
storage.setItem("wallet", walletInfo);
wallet = walletInfo;
} else {
alert("助记词错误请重新输入");
}
}

</script>

3.显示余额

1
2
3
4
5
6
7
8
9
10
11
const balance = ref("0");

// 获取余额
async function getBalance() {
if(!wallet.address) return false;
// 根据地址查询余额
web3.eth.getBalance(wallet.address).then((ret) => {
balance.value = web3.utils.fromWei(ret, "ether");
});
}
getBalance()

4.转账交易

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
1. 获取用户输入金额 与 地址 
2. 调用send 方法

async function send() {
const keystore = storage.getItem("wallet").keystore;
let pass = prompt("请输入密码");
let walletobj;
try {
walletobj = await ethwallet.fromV3(keystore, pass);
} catch (error) {
alert("密码错误");
return false;
}
let key = walletobj.getPrivateKey().toString("hex");
var privateKey = new Buffer(key, "hex");
// console.log(privateKey);
const fromaddress = wallet.address;
// 获取账户交易次数
let nonce = await web3.eth.getTransactionCount(fromaddress);
// 获取预计转账gas费
let gasPrice = await web3.eth.getGasPrice();
// 转账金额以wei为单位
console.log(number.value, typeof number.value);
let balance = Web3.utils.toWei(number.value);
console.log(222);
var rawTx = {
from: fromaddress,
nonce: nonce,
gasPrice: gasPrice,
to: toaddress.value,
value: balance,
data: "0x00", //转Token代币会用到的一个字段
};

//需要将交易的数据进行预估gas计算,然后将gas值设置到数据参数中
let gas = await web3.eth.estimateGas(rawTx);
rawTx.gas = gas;
// 通过 ethereumjs-tx 实现私钥加密Ï
var tx = new Tx(rawTx);
tx.sign(privateKey);
var serializedTx = tx.serialize();

web3.eth
.sendSignedTransaction("0x" + serializedTx.toString("hex"))
.on("transactionHash", (txid) => {
console.log("交易成功,请在区块链浏览器查看");
console.log("交易id", txid);
console.log(`https://goerli.etherscan.io/tx/${txid}`);
})
// .on('receipt', (ret)=>{console.log('receipt')})
// .on('confirmation', (ret)=>{console.log('confirmation')})
.on("error", (err) => {
console.log("error:" + err);
});
}

5.导出账户信息

  • 导出私钥
1
2
3
4
5
6
7
8
9
10
11
12
async function exportKey() {
let pass = prompt("请输入密码");
let walletObj;
try {
walletObj = await ethwallet.fromV3(wallet.keystore, pass);
} catch (error) {
alert("密码错误");
return false;
}
let key = walletObj.getPrivateKey().toString("hex");
alert(key)
}
  • 导出keystore
1
2
3
4
缓存对象
function exportKeyStore() {
alert(JSON.stringify(wallet.keystore))
}

6.解锁账户信息

  • 助记词解锁

    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
    添加助记词导入钱包方法
    async function importWallet() {
    const mnemonic = prompt("请输入助记词");
    const pass = prompt("请输入您的钱包密码");
    if (!pass) return false;
    //1.将助记词转成seed
    let seed = await bip39.mnemonicToSeed(mnemonic);
    //3.通过hdkey将seed生成HD Wallet
    let hdWallet = hdkey.fromMasterSeed(seed);
    //4.生成钱包中在m/44'/60'/0'/0/i路径的keypair
    let keyPair = hdWallet.derivePath("m/44'/60'/0'/0/0");
    // 获取钱包对象
    let wallet = keyPair.getWallet();
    // 获取钱包地址
    let lowerCaseAddress = wallet.getAddressString();
    // 获取钱包校验地址
    let CheckSumAddress = wallet.getChecksumAddressString();
    // 获取私钥
    let prikey = wallet.getPrivateKey().toString("hex");
    let keystore = await wallet.toV3(pass);
    console.log(keystore);
    // 保存钱包信息
    const walletInfo = {
    address: lowerCaseAddress,
    prikey,
    keystore,
    balance: 0,
    mnemonic, // 助记词不应该记录下来仅仅是为了便于演示
    };
    storage.setItem("wallet", walletInfo);
    wallet = walletInfo;
    }
  • 私钥解锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async function importByPrivateKey() { 
const key = prompt("请输入私钥")
let privatekey=new Buffer(key , 'hex' )
let pass = prompt("请输入密码");
let wallet = ethwallet.fromPrivateKey(privatekey)
let keystore = await wallet.toV3(pass);
// 获取钱包地址
let lowerCaseAddress = wallet.getAddressString();
const walletInfo = {
address: lowerCaseAddress,
prikey: key,
keystore,
balance: 0,
};
console.log(walletInfo)
storage.setItem("wallet", walletInfo);
}
  1. 未来展望
    • 通过uniapp 、rn、electron 将项目变为app和桌面端应用
    • app添加扫码转账功能
    • 增加erc20代币转账功能
    • 增加nft数字藏品商城功能
    • 增加Dao 应用

智能合约

  1. 通过智能合约文件 获取abi

    1
    import { abi } from "@/contract/HHC.json";
  2. 在web3实例的基础上创建智能合约实例

1
2
3
4
const hhc = new this.web3.eth.Contract(
abi,
"0x18dAaC8e2E422fDB5e26715eF1EcDca11F78eDE5"
);
  1. 通过智能合约实例获取代币余额
1
2
 let num = await hhc.methods.balanceOf(address).call();
Web3.utils.fromWei(num, "ether");
  1. 智能合约交易hash 创建
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
async createCoinTransationHx(contractInstance, method, to, value) {
let pass = prompt("请输入密码");
let wallet;
try {
wallet = await ethwallet.fromV3(keystore, pass);
} catch (error) {
alert("密码错误");
return false;
}
let key = wallet.getPrivateKey().toString("hex");
const from = this.ownerAddress;
// 当前地址交易次数
const nonce = await this.instance.eth.getTransactionCount(from);
var privateKey = new Buffer(key, "hex");
// 获取预计转账gas费
let gasPrice = await this.instance.eth.getGasPrice();
// 转账金额以wei为单位
let weiValue = await Web3.utils.toWei(value);
// 转账的记录对象
// 代币转账
// this.HccCont = new this.web3.eth.Contract(
// abi,
// "0x18dAaC8e2E422fDB5e26715eF1EcDca11F78eDE5"
// );
const contractAbi = await contractInstance.methods[method](
to,
weiValue
).encodeABI();
// console.log(contractAbi);
// console.log(contractInstance._address);
// return false;
var rawTx = {
from: this.ownerAddress,
nonce: nonce,
gasPrice: gasPrice,
to: contractInstance._address, //eth 转账 to 目标地址 ,智能合约 to 智能合约地址
value: 0, //eth 转账 数量
data: contractAbi, //智能合约方法的abi编码
};
//需要将交易的数据进行预估gas计算,然后将gas值设置到数据参数中
let gas = await this.instance.eth.estimateGas(rawTx);
rawTx.gas = gas;
// 通过tx实现交易对象的加密操作
var tx = new Tx(rawTx);
tx.sign(privateKey);
var serializedTx = tx.serialize();
var transationHx = "0x" + serializedTx.toString("hex");
return transationHx;
}
  1. 智能合约代币转账

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    this.web3.eth
    .sendSignedTransaction(hx)
    .on("transactionHash", (txid) => {
    console.log("交易成功,请在区块链浏览器查看");
    console.log("交易id", txid);
    console.log(`https://goerli.etherscan.io/tx/${txid}`);
    })
    .on("receipt", (ret) => {
    cb && cb(ret);
    console.log("receipt", ret);
    const { transactionHash } = ret;
    this.createOrderData(transactionHash);
    })
    .on("latestBlockHash", (...arg) => {
    console.log("latestBlockHash", arg);
    })
    .on("error", (err) => {
    console.log("error:");
    console.log(err);
    });

以上内容来自千锋教育,并无商业用途,仅供学习使用。