以太坊HD钱包开发 一 —— 钱包概念介绍
https://blog.csdn.net/bondsui/article/details/85780452
以太坊HD钱包开发 二 —— BIP协议介绍
https://blog.csdn.net/bondsui/article/details/85780675
以太坊HD钱包开发 三 —— 代码实现
https://blog.csdn.net/bondsui/article/details/85780940
文章目录
github源码在结尾
7、web钱包
1、创建工程
create-react-app webwallet
导入引用的包 bip、ethers、file-saver、pubsub、semantic
"dependencies": {
"bip39": "^2.5.0",
"ethers": "^4.0.20",
"file-saver": "^2.0.0",
"json-format": "^1.0.1",
"pubsub-js": "^1.7.0",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-scripts": "2.1.2",
"semantic-ui-css": "^2.4.1",
"semantic-ui-react": "^0.84.0"
}
创建目录
➜ src git:(master) ✗ tree
├── App.css
├── App.js
├── service
│ └── service.js
├── serviceWorker.js
├── utils
└── view
├── login
│ ├── login.js
│ ├── tab_keystore.js
│ ├── tab_mnemonic.js
│ └── tab_private.js
└── wallet
├── tab_account.js
├── tab_settings.js
├── tab_transaction.js
└── wallet.js
2、首页
首页引入PubSub.js,事件发布订阅,如果未登录,显示导入页面,否则显示钱包页面
代码
import React, {Component} from 'react';
import PubSub from "pubsub-js";
import './App.css';
import LoginForm from './view/login/login'
import Wallet from './view/wallet/wallet'
import {Container} from "semantic-ui-react";
class App extends Component {
state = {
login: false,
loading: false,
loginEvent: '',
wallets: []
}
// 页面加载,订阅消息,保存订阅id
componentWillMount() {
let loginEvent = PubSub.subscribe("onLoginSucc", this.onLoginSucc)
this.setState({loginEvent})
}
onLoginSucc = (msg, data) => {
console.log("登陆成功")
console.log(data)
this.setState({
login: true,
wallets: data
})
}
// 页面卸载 取消消息订阅
componentWillUnmount() {
PubSub.unsubscribe(this.state.loginEvent)
}
render() {
let {login} = this.state
let content = login ? <Wallet wallets={this.state.wallets}/> : <LoginForm/>
return (
<Container>
{content}
</Container>
);
}
}
export default App;
3、登陆页面
3.1 登录首页
登陆页面为3个tab,点击切换导入方式
登陆成功后发布消息
login.js
import React,{Component} from 'react'
import {Grid,Header, Image, Tab} from 'semantic-ui-react'
import PrivateLogin from "./tab_private"
import MmicLogin from "./tab_mnemonic"
import KeyStoreLogin from "./tab_keystore"
const panes = [
{menuItem: '私钥', render: () => <Tab.Pane attached={false}><PrivateLogin/></Tab.Pane>},
{menuItem: '助记词', render: () => <Tab.Pane attached={false}><MmicLogin/></Tab.Pane>},
{menuItem: 'KeyStore', render: () => <Tab.Pane attached={false}><KeyStoreLogin/></Tab.Pane>},
]
export default class Login extends Component {
render() {
return (
<Grid textAlign='center' verticalAlign='middle'>
<Grid.Column style={{maxWidth: 450, marginTop: 100}}>
<Header as='h2' color='teal' textAlign='center'>
<Image src='images/logo.png'/> EHT钱包
</Header>
<Tab menu={{text: true}} panes={panes} style={{maxWidth: 450}}/>
</Grid.Column>
</Grid>
)
}
}
3.2 私钥新建或导入
新建账号,随机一个私钥,
对用户输入的私钥进行校验 service.checkPrivate(key)
登陆成功后发布消息
import {Button, Form, Segment} from 'semantic-ui-react'
import React, {Component} from 'react'
import PubSub from 'pubsub-js'
let service = require('../../service/service')
export default class PrivateLogin extends Component {
state = {
privateKey: "",
}
// 随机创建
handleCreateClick = () => {
let privateKey = service.newRandomKey()
this.setState({privateKey})
}
// 处理输入绑定
handleChange = (e, {name, value}) => {
this.setState({[name]: value})
}
// 私钥登陆
onPrivateLoginClick = () => {
let key = this.state.privateKey
let err = service.checkPrivate(key)
if (err !== "") {
alert(err)
return;
}
if (key.substring(0, 2).toLowerCase() !== '0x') {
key = '0x' + key;
}
console.log("开始创建钱包", key)
let wallets = service.newWalletFromPrivateKey(key)
if (wallets) {
PubSub.publish("onLoginSucc", wallets)
} else {
alert("导入出错")
}
}
render() {
return (
<Form size='large'>
<Segment>
<Form.Input
fluid icon='lock' iconPosition='left'
placeholder='private key'
name="privateKey"
value={this.state.privateKey}
onChange={this.handleChange}/>
<a href='#' onClick={this.handleCreateClick}>随机生成</a>
<br/>
<br/>
<Button
color='teal' fluid size='large'
onClick={this.onPrivateLoginClick}>
私钥导入
</Button>
</Segment>
</Form>
)
}
}
// 私钥校验
function checkPrivate(key) {
if (key === '') {
return "不能为空"
}
if (key.length != 66 && key.length != 64) {
return false, "秘钥长度必须为66或者64"
}
if (!key.match(/^(0x)?([0-9A-fa-f]{64})+$/)) {
return "秘钥为16进制表示[0-9A-fa-f]"
}
return ""
}
// 随机私钥
function newRandomKey() {
let randomByte = ethers.utils.randomBytes(32)
let randomNumber = ethers.utils.bigNumberify(randomByte);
return randomNumber.toHexString()
}
// 通过私钥创建钱包
function newWalletFromPrivateKey(privateKey) {
let wallets = []
let wallet = new ethers.Wallet(privateKey)
wallets.push(wallet)
return wallets
}
3.3 助记词新建或导入
助记词导入执行bip44协议,定义路径,默认**“m/44’/60’/0’/0/0”**,也可以进行多账号导入,修改index值即可
import {Button, Loader,Form, Grid, Header, Image, Message, Segment} from 'semantic-ui-react'
import PubSub from 'pubsub-js'
import React, {Component} from 'react'
let service = require('../../service/service')
// disorder timber among submit tell early claw certain sadness embark neck salad
export default class MmicLogin extends Component {
state = {
privateKey: "",
mmic: "",
pwd: "",
path: "m/44'/60'/0'/0/0",
}
// 处理输入文本绑定
handleChange = (e, {name, value}) => {
this.setState({[name]: value})
}
// 生成助记词
handleGenMicc = () => {
let mmic = service.genMmic()
this.setState({mmic})
}
// 助记词导入
onMMICClick = () => {
let {mmic, path} = this.state
let wallets = service.newWalletFromMmic(mmic, path)
PubSub.publish("onLoginSucc", wallets)
}
render() {
return (
<Form size='large' onSubmit={this.onMMICClick}>
<Segment stacked>
<Form.TextArea
placeholder='12 words'
name="mmic"
value={this.state.mmic}
onChange={this.handleChange}/>
<Form.Input
fluid
icon='user'
iconPosition='left'
type='path'
value={this.state.path}
onChange={this.handleChange}
/>
<a onClick={this.handleGenMicc}>随机生成</a>
<br/>
<br/>
{/*<Form.Input*/}
{/*fluid*/}
{/*icon='lock'*/}
{/*iconPosition='left'*/}
{/*placeholder='Password'*/}
{/*type='password'*/}
{/*value={this.state.pwd}*/}
{/*onChange={this.handleChange}*/}
{/*/>*/}
<Form.Button color='teal' fluid size='large'>
助记词导入
</Form.Button>
</Segment>
</Form>
)
}
}
// 生成助记词
function genMmic() {
let words = ethers.utils.HDNode.entropyToMnemonic(ethers.utils.randomBytes(16));
return words
}
// 通过助记词创建钱包
function newWalletFromMmic(mmic, path) {
let wallets = []
for (let i = 0; i < 10; i++) {
path = PATH_PREFIX + i
let wallet = ethers.Wallet.fromMnemonic(mmic, path)
wallets.push(wallet)
console.log(i, wallets)
}
return wallets
}
3.4 keystore导入
wallet 需要连接provider 才可以使用
wallet balance为Object类型,金额需要手工转换(ethers.utils)
导出需要密码,
import {Button, Form, Grid, Header, Image, Loader, Message, Segment} from 'semantic-ui-react'
import PubSub from 'pubsub-js'
import _ethets2 from "ethers"
import React, {Component} from 'react'
let service = require('../../service/service')
export default class KeyStoreLogin extends Component {
state = {
keyStore: "",
pwd: '',
loading:false
}
handleChange = (e, {name, value}) => {
this.setState({[name]: value})
}
// 处理导入
handleKeyImport = () => {
let {keyStore, pwd} = this.state
if (keyStore==""){
return
}
console.log(service.checkJsonWallet(keyStore))
this.setState({loading:true})
service.newWalletFromJson(keyStore, pwd).then(wallets => {
PubSub.publish("onLoginSucc", wallets)
this.setState({loading:false})
}).catch(e => {
console.log(e)
alert("导入出错" + e)
this.setState({loading:false})
})
}
onFileChooseClick = ()=>{
}
render() {
return (
<Form size='large'>
<Loader active={this.state.loading} inline />
<Segment>
<Form.TextArea
placeholder='keystore为json格式'
name="keyStore"
value={this.state.keyStore}
onChange={this.handleChange}/>
<Form.Input
fluid
icon='lock'
iconPosition='left'
placeholder='Password'
type='password'
name = "pwd"
value={this.state.pwd}
onChange={this.handleChange}
/>
<Button
color='teal' fluid size='large'
onClick={this.handleKeyImport}>
导入
</Button>
</Segment>
</Form>
)
}
}
// 从keystore导入钱包,需要密码
function newWalletFromJson(json, pwd) {
return new Promise(async (resolve, reject) => {
try {
let wallets = []
let wallet = await ethers.Wallet.fromEncryptedJson(json, pwd, false)
wallets.push(wallet)
resolve(wallets)
} catch (e) {
reject(e)
}
})
}
通过keystorejson文件校验是否包含地址
// 校验地址
function checkJsonWallet(data) {
return ethers.utils.getJsonWalletAddress(data)
}
4、钱包页面
获取钱包信息需要连接以太环境,请提前确认开启端口
钱包字段信息获取为异步,注意不可以直接调用显示
转账对地址及金额进行校验
import React, {Component} from 'react'
import {Grid, Form, Header, Loader, Button, Loading, Segment, Image} from 'semantic-ui-react'
let ethers = require('ethers')
let service = require('../../service/service')
let fileSaver = require('file-saver');
export default class Wallet extends Component {
state = {
wallets: [],// 支持多账户,默认第0个
selectWallet: 0,
provider: "http://127.0.0.1:8545", //环境
walletInfo: [], // 钱包信息,获取为异步,单独存储下
activeWallet: {}, // 当前活跃钱包
txto: "", // 交易接收地址
txvalue: "", // 转账交易金额
pwd: "", // 导出keystore需要密码
// UI状态表示
txPositive: false, //
loading: false,
exportLoading: false,
}
constructor(props) {
super(props)
this.state.wallets = props.wallets
this.state.selectWallet = props.wallets.length == 0 ? -1 : 0
}
// 更新钱包信息
updateActiveWallet() {
if (this.state.wallets.length == 0) {
return null
}
let activeWallet = this.getActiveWallet()
this.setState({activeWallet})
this.loadActiveWalletInfo(activeWallet)
return activeWallet
}
// 获取当前的钱包
getActiveWallet() {
let wallet = this.state.wallets[this.state.selectWallet]
console.log("wallet", wallet)
// 激活钱包需要连接provider
return service.connectWallet(wallet, this.state.provider)
}
// 加载钱包信息
async loadActiveWalletInfo(wallet) {
let address = await wallet.getAddress()
let balance = await wallet.getBalance()
// 获取交易次数
let tx = await wallet.getTransactionCount()
this.setState({
walletInfo: [address, balance, tx]
})
}
// 发送交易
onSendClick = () => {
let {txto, txvalue, activeWallet} = this.state
// balance 为Object类型
console.log("balance", activeWallet)
// 地址校验
let address = service.checkAddress(txto)
if (address == "") {
alert("地址不正确")
return
}
console.log(txvalue, isNaN(txvalue))
if (isNaN(txvalue)) {
alert("转账金额不合法")
return
}
// 以太币转换,发送wei单位
txvalue = ethers.utils.parseEther(txvalue);
console.log("txvalue", txvalue)
// 设置加载loading,成功或者识别后取消loading
this.setState({loading: true})
service.sendTransaction(activeWallet, txto, txvalue)
.then(tx => {
console.log(tx)
alert("交易成功")
this.updateActiveWallet()
this.setState({loading: false, txto: "", txvalue: ""})
})
.catch(e => {
this.setState({loading: false})
console.log(e);
alert(e);
})
}
// 查看私钥
onExportPrivate = () => {
alert(this.getActiveWallet().privateKey)
}
// 导出keystore
onExportClick = () => {
let pwd = this.state.pwd;
if (pwd.length < 6) {
alert("密码长度不能小于6")
return
}
this.setState({exportLoading: true})
// 通过密码加密
this.getActiveWallet().encrypt(pwd, false).then(json => {
let blob = new Blob([json], {type: "text/plain;charset=utf-8"})
fileSaver.saveAs(blob, "keystore.json")
this.setState({exportLoading: false})
});
}
// 页面加载完毕,更新钱包信息
componentDidMount() {
this.updateActiveWallet()
}
handleChange = (e, {name, value}) => {
this.setState({[name]: value})
}
render() {
// 金额显示需要手工转换
let wallet = this.state.walletInfo
if (wallet.length == 0) {
return <Loader active inline/>
}
let balance = wallet[1]
let balanceShow = ethers.utils.formatEther(balance) + "(" + balance.toString() + ")"
return (
<Grid textAlign='center' verticalAlign='middle'>
<Grid.Column style={{maxWidth: 650, marginTop: 10}}>
<Header as='h2' color='teal' textAlign='center'>
<Image src='images/logo.png'/> EHT钱包
</Header>
<Segment stacked textAlign='left'>
<Header as='h1'>Account</Header>
<Form.Input
style={{width: "100%"}}
action={{
color: 'teal',
labelPosition: 'left',
icon: 'address card',
content: '地址'
}}
actionPosition='left'
value={wallet[0]}
/>
<br/>
<Form.Input
style={{width: "100%"}}
action={{
color: 'teal',
labelPosition: 'left',
icon: 'ethereum',
content: '余额'
}}
actionPosition='left'
value={balanceShow}
/>
<br/>
<Form.Input
actionPosition='left'
action={{
color: 'teal',
labelPosition: 'left',
icon: 'numbered list',
content: '交易'
}}
style={{width: "100%"}}
value={wallet[2]}
/>
</Segment>
<Segment stacked textAlign='left'>
<Header as='h1'>转账|提现</Header>
<Form.Input
style={{width: "100%"}}
action={{
color: 'teal',
labelPosition: 'left',
icon: 'address card',
content: '地址'
}}
actionPosition='left'
defaultValue='52.03'
type='text' name='txto' required value={this.state.txto}
placeholder='对方地址' onChange={this.handleChange}/>
<br/>
<Form.Input
style={{width: "100%"}}
action={{
color: 'teal',
labelPosition: 'left',
icon: 'ethereum',
content: '金额'
}}
actionPosition='left'
defaultValue='1.00'
type='text' name='txvalue' required value={this.state.txvalue}
placeholder='以太' onChange={this.handleChange}/>
<br/>
<Button
color='twitter'
style={{width: "100%"}}
size='large'
loading={this.state.loading}
onClick={this.onSendClick}>
确认
</Button>
</Segment>
<Segment stacked textAlign='left'>
<Header as='h1'>设置</Header>
<Form.Input
style={{width: "100%"}}
action={{
color: 'teal',
labelPosition: 'left',
icon: 'lock',
content: '密码'
}}
actionPosition='left'
defaultValue='1.00'
type='pwd' name='pwd' required value={this.state.pwd}
placeholder='密码'
onChange={this.handleChange}/>
<br/>
<Button
color='twitter'
style={{width: "48%"}}
onClick={this.onExportPrivate}>
查看私钥
</Button>
<Button
color='twitter'
style={{width: "48%"}}
onClick={this.onExportClick}
loading={this.state.exportLoading}>
导出keystore
</Button>
</Segment>
</Grid.Column>
</Grid>
)
}
}
5、service.js
import {BlockTag, Provider, TransactionRequest, TransactionResponse} from "ethers/providers";
import {Arrayish, BigNumber, ProgressCallback} from "ethers/utils";
let ethers = require('ethers')
// 默认路径
const PATH_PREFIX = "m/44'/60'/0'/0/"
// 私钥校验
function checkPrivate(key) {
if (key === '') {
return "不能为空"
}
if (key.length != 66 && key.length != 64) {
return false, "秘钥长度必须为66或者64"
}
if (!key.match(/^(0x)?([0-9A-fa-f]{64})+$/)) {
return "秘钥为16进制表示[0-9A-fa-f]"
}
return ""
}
// 校验地址
function checkJsonWallet(data) {
return ethers.utils.getJsonWalletAddress(data)
}
// 连接provider
function connectWallet(wallet, providerurl) {
let provider = new ethers.providers.JsonRpcProvider(providerurl);
return wallet.connect(provider);
}
// 发送交易
function sendTransaction(wallet,to,value) {
return wallet.sendTransaction({
to: to,
value: value,
})
}
6 、多账户钱包
TODO,页面中为wallet[] 数组,想实现账号的,增加 账号选择模块即可
8 问题
ganache provider是可以获取钱包信息,但转账会提示余额不足
大家使用 ganache-cli 开启eth测试环境即可
页面转账成功,余额减少后,可登陆转账的账号,查看真实余额,也都可以使用工具查看
【可选1】进入truffle develop 使用命令查看余额
【可选2】为了便于查看转账及余额信息,大家可以开启geth命令行,开启时添加端口
-
geth attach provider 开启geth命令行
-
eth.accounts 获取当前账号信息
-
eth.getBalance(eth.accounts[8]) 获取某一个账号余额
➜ 00-wallet git:(master) ✗ geth attach http://localhost:8545
Welcome to the Geth JavaScript console!
> eth.accounts
["0xfef3d415f66464c3b38e10fd5f31edbead7be44b", "0xfc26f518d2f7091667dbdd81ee04d1f17d122359", "0x5e7363aa3c0669083a554dde5ed548a8ec90ff12", "0x57060a8a16bff2615769282eb83d1b50891f04a9", "0xc1f109c747e70bbc85371bcb6fbdc8fe23219da9", "0x3d25841411dd7917c123d980f4dd33cad101cc31", "0x9b477be361d60597e24dd7838d29f706396a3fa1", "0x8adffcabe036474de3a6d6f513bfd6df19fbcc1f", "0x2394c966264c3794247136637e0dc9924dfad3d7", "0xbf513ae069d7a58eb4d0f8c6e17402dbe2cc1bee"]
> eth.getBalance(eth.accounts[0])
100000000000000000000
> eth.getBalance(eth.accounts[8])
120000000000000000000
9 源码地址
无业游民-唐朝
课件及源码 https://github.com/bigsui/eth-webwallet
10、相关链接
官方GitHub网址:https://github.com/ethereum
Geth的Github地址:https://github.com/ethereum/go-ethereum
官方不同系统安装Geth指南地址:https://github.com/ethereum/go-ethereum/wiki/Building-Ethereum
官方Geth命令行参数说明:https://github.com/ethereum/go-ethereum/wiki/Command-Line-Options