一、意義:
在區塊鏈世界中,虛擬貨幣一般存放在三個地方(礦機用戶端、錢包、交易所)。礦機端轉賬繁瑣,沒有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()