帶你玩轉區塊鏈--基於Ethers.js實現一個虛擬貨幣錢包-第二章-第五節【ETH篇】

一、意義:

           在區塊鏈世界中,虛擬貨幣一般存放在三個地方(礦機用戶端、錢包、交易所)。礦機端轉賬繁瑣,沒有UI界面,對於無運維基礎的用戶來說具有很大的上手難度。交易端無法直接獲取礦機端的收益,且有項目方跑路的危險性。爲了解決以上問題,亟需一款既有安全性又能簡單完成轉賬的軟件。此時“錢包”橫空出世,以區塊鏈世界運輸者的身份完善了區塊鏈世界。

          錢包的作用也很直觀,兩個功能:1.存放錢。2.交易(轉賬,把錢付出去,收進來)。下面我們來刨根挖底、一步一步開發一款自己的錢包。

---------------------------------------------------------------------------------------------------------------------------------------

如果您改進了代碼或者想對照代碼學習,請訪問我的Github。

如果您有問題想要討論。請加我微信:laughing_jk(加我,請備註來源,謝謝)

錢包項目源碼:https://github.com/lsy-zhaoshuaiji/wallet

---------------------------------------------------------------------------------------------------------------------------------------

二、虛擬貨幣錢包技術剖析

1.HD錢包BIP44

相信各位小夥伴,在平時生活中都使用或聽說過區塊鏈錢包。那麼各位對於助記詞一定不會陌生,一個簡單的助記詞是如何創建和管理多幣種的地址呢?答案就在HD錢包裏。

HD 錢包是目前常用的確定性錢包 ,HD 是 Hierarchical Deterministic(分層確定性)的縮寫。所謂分層,就是一個大公司可以爲每個子部門分別生成不同的私鑰,子部門還可以再管理子子部門的私鑰,每個部門可以看到所有子部門裏的幣,也可以花這裏面的幣。也可以只給會計人員某個層級的公鑰,讓他可以看見這個部門及子部門的收支記錄,但不能花裏面的錢,使得財務管理更方便了。這個過程就是基於BIP44來實現的,具體實現的方法就不深究了,各位有一個概念即可。

2.Ethers.js

Ethers.js則是一個輕量級的web3.js替代品。具有Web3的全部功能。且Ethers.js具有創建錢包等相關功能,所以我們在此節將使用Ethers.js與合約交互。官方文檔:https://learnblockchain.cn/docs/ethers.js/

安裝:

npm install --save ethers

5種生產錢包的方法:

爲了快速測試,這裏我們默認連接ganache-cli(8545)

let ethers=require('ethers');
//1.通過私鑰創建錢包
let privateKey='0xec3702c3c89d352b95e0e0288fab1ca16c55977963804748e215cb9415c72b58';
let w1=new ethers.Wallet(privateKey);
console.log(w1.privateKey);
console.log(w1.address);
//2.隨機創建一個新的錢包
let w2=new ethers.Wallet.createRandom();
console.log(w2.privateKey);
console.log(w2.address);
console.log(w2.mnemonic);
//3.給定json文件,創建錢包
let data = {
    id: "fb1280c0-d646-4e40-9550-7026b1be504a",
    address: "88a5c2d9919e46f883eb62f7b8dd9d0cc45bc290",
    Crypto: {
        kdfparams: {
            dklen: 32,
            p: 1,
            salt: "bbfa53547e3e3bfcc9786a2cbef8504a5031d82734ecef02153e29daeed658fd",
            r: 8,
            n: 262144
        },
        kdf: "scrypt",
        ciphertext: "10adcc8bcaf49474c6710460e0dc974331f71ee4c7baa7314b4a23d25fd6c406",
        mac: "1cf53b5ae8d75f8c037b453e7c3c61b010225d916768a6b145adf5cf9cb3a703",
        cipher: "aes-128-ctr",
        cipherparams: {
            iv: "1dcdf13e49cea706994ed38804f6d171"
        }
    },
    "version" : 3
};

let json = JSON.stringify(data);
let password = "foo";

ethers.Wallet.fromEncryptedJson(json, password).then(function(wallet) {
    console.log("Json Address: " + wallet.address);
    // "Address: 0x88a5C2d9919e46F883EB62F7b8Dd9d0CC45bc290"
});
//4.給定助記詞,生成錢包
let mnemonic = "pepper gift color reward collect timber pyramid rhythm avoid head city want";
let path = "m/44'/60'/0'/0/0";    //格式爲: m/44'/60'/0'/0/{account_index}
let secondMnemonicWallet = ethers.Wallet.fromMnemonic(mnemonic, path);
console.log("secondMnemonicWallet Address: " + secondMnemonicWallet.address);
//5.隨機創建一個助記詞錢包
let randValue=ethers.utils.randomBytes(16);
let w5_mnemonic=ethers.utils.HDNode.entropyToMnemonic(randValue);
console.log(`mnemonic: ${w5_mnemonic}`);
let path1 = "m/44'/60'/0'/0/0";
let thirdMnemonicWallet = ethers.Wallet.fromMnemonic(w5_mnemonic, path1);
console.log("thirdMnemonicWallet:",thirdMnemonicWallet.address);

處理加密的 JSON 錢包文件

加密錢包輸出 JSON wallet

let password = "password123";

function callback(progress) {
    console.log("Encrypting: " + parseInt(progress * 100) + "% complete");
}

let encryptPromise = wallet.encrypt(password, callback);

encryptPromise.then(function(json) {
    console.log(json);
});

3.React

一款前端框架,由於官方推出了Truffle-react框架,所以促進了大部分區塊鏈項目使用react開發。目前React和Vue擁有着區塊鏈項目很大的市場佔有率。

4.Pubsub.js消息的發佈訂閱

在前端框架中,無論是vue還是react。組件傳遞數據一般會使用props。props是一層一層傳遞數據的,且兄弟組件傳遞需要藉助父組件,比較麻煩。所以當傳遞的層數比較多時,可以用此工具庫。

安裝:

npm install pubsub-js --save

 使用:

// 導入
import PubSub from "pubsub-js"

// 在有數據的地方進行發佈
class Data extends React.Component{
  pubmsg = ()=>{
      PubSub.publish("頻道","頻道發佈的消息")
  }
  render() {
      return(
          <button onClick={this.pubmsg}>Data組件,發佈消息</button>
          )
      }
  }
  
  // 訂閱
  class App extends Component {
    // 組件將要被渲染的時候進行訂閱
    componentWillMount() {
      PubSub.subscribe("頻道", (msg,data)=> {
        console.log(msg,data)
      })
    }
  
    render() {
      return (
        <div className="App">
           <Data />
        </div>
      );
    }
  }

5.FileSaver.js

如果你需要保存較大的文件,不受 blob 的大小限制或內存限制,可以看一下更高級的 StreamSaver.js
它使用強大的 stream API,可以將數據直接異步地保存到硬盤。支持進度、取消操作以及完成事件回調。

npm install file-saver --save

 保存文字/json:

var FileSaver = require('file-saver');
var blob = new Blob(["Hello, world!"], {type: "text/plain;charset=utf-8"});
FileSaver.saveAs(blob, "hello world.txt");

 你可以保存一個文件結構,不需要指定文件名。文件自身已經包含了文件名,有一些方法來獲取文件實例(從 storage,file input,新的構造函數)
如果你想修改文件名,你可以在第二個參數設置文件名。

var file = new File(["Hello, world!"], "hello world.txt", {type: "text/plain;charset=utf-8"});
saveAs(file);

三、項目實現

1.準備工作

create-react-app Wallet-eth
npm install --save ethers

2.私鑰創建錢包代碼實現 

2.1在src下創建login.js,負責創建錢包的基礎前端頁面

import React from 'react'
import {Component} from 'react'
import { Tab,Grid,Header,Image } from 'semantic-ui-react'

const panes = [
    { menuItem: '私鑰', render: () => <Tab.Pane>Tab 1 Content</Tab.Pane> },
    { menuItem: '助記詞', render: () => <Tab.Pane>Tab 2 Content</Tab.Pane> },
    { menuItem: 'KeyStore', render: () => <Tab.Pane>Tab 3 Content</Tab.Pane> },
];
class LoginTab extends Component {
    componentWillMount(){

    }
    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" /> ETH錢包
                    </Header>
                    <Tab
                        menu={{ text: true }}
                        panes={panes}
                        style={{ maxWidth: 450 }}
                    />
                </Grid.Column>
            </Grid>
        )
    }
}

export default LoginTab

2.2創建privateKey.js,完善私鑰創建錢包的前端頁面

代碼如下:總結:1.<Form>嵌套<Segment>會在視覺上讓層次感更明顯 2.<br />標籤可以很好的控制兩個塊級元素保持距離

import React, { Component } from "react";
import { Form, Segment, Button } from "semantic-ui-react";
import  services from '../../src/view/service/service'
class PrivateKeyTab extends Component {
    state={
      privateKey:'',
      address:'',
      wallet: {}
    };
    CreateRanderHandler=()=>{
        let WalletObj=services.CreateRanderWallet();
        this.setState({
            privateKey:WalletObj.privateKey,
        });
    };
    handleChange = (e, { name, value }) => {
        this.setState({
            [name]: value
        });
        console.log("name :", name);
        console.log("value :", value);
    };
    onPrivateLoginClick = () => {
        //獲取私鑰(自動生成,用戶輸入)
        let privateKey = this.state.privateKey;
        let checkResult=services.checkPrivateKey(privateKey);
        if (checkResult){
             alert(checkResult);
             return
        }
        let WalletObj=services.CreateWalletByPrivateKey(privateKey);
        this.setState({
            wallet:WalletObj,
        });
        console.log("111 : ", this.state.wallet);
    };

    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}
                    />{" "}
                    <Button onClick={this.CreateRanderHandler}> 隨機生成 </Button>{" "}
                    <br />
                    <br />
                    <Button
                        color="teal"
                        fluid
                        size="large"
                        onClick={this.onPrivateLoginClick}
                    >
                        私鑰導入(下一步){" "}
                    </Button>{" "}
                </Segment>{" "}
            </Form>
        );
    }
}

export default PrivateKeyTab;

2.3完善privateKey.js,與ehters交互創建錢包,並利用Pubsub傳遞錢包對象,在APP.js中接收(省略...)

import React, { Component } from "react";
import { Form, Segment, Button } from "semantic-ui-react";
import  services from '../../src/view/service/service'
import Pubsub from 'pubsub-js'
class PrivateKeyTab extends Component {
    state={
      privateKey:'',
      address:'',
      wallet: {}
    };
    CreateRanderHandler=()=>{
        let WalletObj=services.CreateRanderWallet();
        this.setState({
            privateKey:WalletObj.privateKey,
        });
    };
    handleChange = (e, { name, value }) => {
        this.setState({
            [name]: value
        });
        console.log("name :", name);
        console.log("value :", value);
    };
    onPrivateLoginClick = () => {
        //獲取私鑰(自動生成,用戶輸入)
        let privateKey = this.state.privateKey;
        let checkResult=services.checkPrivateKey(privateKey);
        if (checkResult){
             alert(checkResult);
             return
        }
        let WalletObj=services.CreateWalletByPrivateKey(privateKey);
        this.setState({
            wallet:WalletObj,
        });
        Pubsub.publish('OnLoginSuccessfully',WalletObj);
        console.log("111 : ", this.state.wallet);
    };

    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}
                    />{" "}
                    <Button onClick={this.CreateRanderHandler}> 隨機生成 </Button>{" "}
                    <br />
                    <br />
                    <Button
                        color="teal"
                        fluid
                        size="large"
                        onClick={this.onPrivateLoginClick}
                    >
                        私鑰導入(下一步){" "}
                    </Button>{" "}
                </Segment>{" "}
            </Form>
        );
    }
}

export default PrivateKeyTab;

2.4創建wallet.js,連接provider,與以太坊網絡進行交互,得到錢包數據

import React from 'react'
import {Component} from 'react'
import AccountTab from './accountTab'
let ethers=require('ethers');
class Wallet extends Component {
    constructor(props){
        super();
        this.state=({
            wallet:props.wallet,
            address:'',
            balance:'',
            txCount:'',
        })
    }
    componentDidMount(){
        this.UpdateCurrentWallet()
    };
    async UpdateCurrentWallet(){
        let {wallet}=this.state;
        let provider=new ethers.providers.JsonRpcProvider('http://127.0.0.1:8545');
        let walletNew=wallet.connect(provider);
        let address=await walletNew.getAddress();
        let balance=await walletNew.getBalance();
        let txCount=await walletNew.getTransactionCount();
        console.log("address is : ",address)
        console.log("balance is : ",ethers.utils.formatEther(balance))
        console.log("txCount is : ",txCount)
        this.setState({
            address,
            balance,
            txCount,
        })

    };
    render(){
        return(
            <AccountTab allinfo={this.state}/>
        )
    }
}
export default Wallet

2.5創建account.js,接收wallet.js傳來的數據,顯示錢包數據

import React from "react";
import { Header, Image, Segment, Form } from "semantic-ui-react";

let AccountTab = (props) => {
    let { address, balance, txCount } = props.allinfo;
    return (
        <div>
            <Header as="h2" color="teal" textAlign="center">
                <Image src="images/logo.png" /> ETH錢包
            </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={address}
                />
                <br />
                <Form.Input
                    style={{ width: "100%" }}
                    action={{
                        color: "teal",
                        labelPosition: "left",
                        icon: "ethereum",
                        content: "餘額"
                    }}
                    actionPosition="left"
                    value={balance}
                />
                <br />
                <Form.Input
                    actionPosition="left"
                    action={{
                        color: "teal",
                        labelPosition: "left",
                        icon: "numbered list",
                        content: "交易"
                    }}
                    style={{ width: "100%" }}
                    value={txCount}
                />
            </Segment>
        </div>
    );
};

export default AccountTab;

2.6轉賬功能實現

async UpdateCurrentWallet(){
        let {wallet}=this.state;
        let provider=new ethers.providers.JsonRpcProvider('http://127.0.0.1:8545');
        let walletActive=wallet.connect(provider);
        let address=await walletActive.getAddress();
        let balance=await walletActive.getBalance();
        let txCount=await walletActive.getTransactionCount();
        console.log("address is : ",address)
        console.log("balance is : ",ethers.utils.formatEther(balance))
        console.log("txCount is : ",txCount)
        this.setState({
            address,
            balance,
            txCount,
            walletActive,
        })

    };
    onSendClick = async (txto, txvalue) => {
        console.log("txto: ", txto);
        console.log("txvalue: ", txvalue);

        //txto是否爲有效地址
        if (!services.checkAddress(txto)) {
            alert("轉賬地址無效!");
            return
        }

        //txvalue是否爲數字
        if (isNaN(txvalue)) {
            alert("轉賬數字無效!")
            return
        }

        //轉賬邏輯
        let walletActive = this.state.walletActive; //得到激活的錢包

        //這個轉換動作必須做,否則不滿足轉賬數據類型, 會出錯
        txvalue = ethers.utils.parseEther(txvalue);
        console.log("txvalue222 : ", txvalue);

        try {
            let res = await walletActive.sendTransaction({
                to: txto,
                value: txvalue
            });
            console.log("轉賬返回結果詳細信息 :", res);
            alert("轉賬成功!");

            //更新展示頁面
            this.UpdateCurrentWallet();
        } catch (error) {
            alert("轉賬失敗!");
            console.log(error);
        }
    };

2.7檢驗地址等信息代碼 services.js

let ethers=require('ethers');
let CreateRanderWallet=()=>{
    let w2=new ethers.Wallet.createRandom();
    return w2
};
let CreateWalletByPrivateKey=(PrivateKey)=>{
    console.log(PrivateKey,"wwwwwwwwwwwwwwwwwwwwwww");
    let w1=new ethers.Wallet(PrivateKey);
    return w1
};
let checkPrivateKey = (key) => {
    if (key === '') {
        return "不能爲空!"
    }

    if (key.length != 66 && key.length != 64) {
        return "密鑰長度爲66位或64位16進制數字"
    }
    //^ : 開頭
    //$ : 結尾
    //(0x)? : 可有可無
    //[0-9A-Fa-f]: 限定取值數據
    //{64}: 限定64個

    if (!key.match(/^(0x)?([0-9A-Fa-f]{64})$/)) {
        return "私鑰爲16進製表示,限定字符[0-9A-Fa-f]"
    }

    return ""
};
let checkAddress = (address) => {
    try {
        let addressNew = ethers.utils.getAddress(address);
        return addressNew
    } catch (error) {
        return ""
    }
}
let services={
    CreateRanderWallet,
    CreateWalletByPrivateKey,
    checkPrivateKey,
    checkAddress,
};


module.exports=services;

3.助記詞生成錢包(略)

由於代碼大致相同,這裏就省略了,如有需要請在github上下載 。

4.KeyStore生成錢包(略)

由於代碼大致相同,這裏就省略了,如有需要請在github上下載 。Json文件可以在以太坊官方上註冊一個,方便測試

官網地址:https://www.myetherwallet.com/

5.獲取山寨幣餘額

const ethers = require('ethers');


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


// 地址來自上面部署的合約
let contractAddress = "0x144ca56ad934413a985cd0b956621dd414cddd38";

// 使用Provider 連接合約,將只有對合約的可讀權限
let contract = new ethers.Contract(contractAddress, abi, defaultProvider);

let currentValue =async ()=>{
    let res=await contract.balanceOf("0xc394628866a34f5Fc6355350700EE8c90dcD1b94");
    console.log(res.toString())
} ;
currentValue()

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章