以太坊HD钱包开发 三 —— 代码实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/bondsui/article/details/85780940

以太坊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

猜你喜欢

转载自blog.csdn.net/bondsui/article/details/85780940