一、前言
在上一章,我們完成了錢包的底層實現及新建賬號、錢包登錄和導入賬號這三個頁面之間的關聯。本章我們接着開發,完成錢包詳情界面UI的拼接和錢包餘額的顯示。
今天的內容比較簡單,我們計劃實現一個這樣的界面:
我們對比一下MetaMask的頁面:
可以看到,我們的頁面還是很像的,除了LOGO,多賬號等其它要素都有,以前MetaMask是沒有存入按鈕的。好了,我們來開始吧!
三、拼接錢包詳情上部UI
從計劃圖中可以看出,錢包詳情頁面上部有三個按鈕,分別爲菜單、我的賬號和賬號選項。今天的主要任務是把UI拼接出來,菜單和賬號的具體功能暫不實現。
新建src/components/DetailHeader/index.js
,代碼如下:
import React, {useState} from 'react';
import {makeStyles} from '@material-ui/core/styles';
import {useGlobal} from 'contexts/GlobalProvider'
import DehazeIcon from '@material-ui/icons/Dehaze';
import MoreHorizIcon from '@material-ui/icons/MoreHoriz';
import Tooltip from '@material-ui/core/Tooltip';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import ListItemText from '@material-ui/core/ListItemText';
import copy from 'copy-to-clipboard';
import {useSimpleSnackbar} from 'contexts/SimpleSnackbar.jsx';
import { isMobile } from 'react-device-detect';
import {shortenAddress} from 'utils';
const COPY_TO_CLIPBOARD = '複製到剪貼板'
const COPIED = '已複製'
const useStyles = makeStyles(theme => ({
Container:{
display: 'flex',
justifyContent: 'space-between',
width:"100%"
},
accountBtn:{
marginTop:theme.spacing(-1)
}
}));
function DetailHeader() {
const classes = useStyles();
const {wallet} = useGlobal();
const {address} = wallet;
const [clickTip,setClickTip] = useState(COPY_TO_CLIPBOARD)
const showSnackbar = useSimpleSnackbar()
const copyAddress = (e) => {
e.preventDefault()
if(copy(address)){
if(isMobile) {
showSnackbar(COPIED,'info')
}else{
setClickTip(COPIED)
}
}
}
const closeAddressTip = (e) => {
e.preventDefault()
setTimeout(()=>{
setClickTip(COPY_TO_CLIPBOARD)
},500)
}
return (
<div className={classes.Container}>
{/* 未知原因:在容器佈局爲flex時必須再包裝一個div,否則IconButton的背景會失真 */}
<div>
<Tooltip title="菜單" >
<IconButton color="inherit" aria-label="Menu" >
<DehazeIcon />
</IconButton>
</Tooltip>
</div>
<div className={classes.accountBtn}>
<Tooltip title={clickTip} onClose={closeAddressTip}>
<Button onClick={copyAddress} style={{borderRadius:20}}>
<ListItemText primary="我的賬號" secondary={shortenAddress(address)}
/>
</Button>
</Tooltip>
</div>
<div>
<Tooltip title="賬號選項" >
<IconButton
color="inherit" aria-label="Menu"
aria-haspopup="true"
>
<MoreHorizIcon/>
</IconButton>
</Tooltip>
</div>
</div>
)
}
export default DetailHeader
上面的代碼中,我們點擊中間的賬號會將賬號地址複製到粘貼板中。它通過copy-to-clipboard
這個庫來實現,記住要先npm install copy-to-clipboard
安裝它。由於Tooltip
在移動端不顯示,所以在點擊賬號時做了一個判斷,如果是移動端就顯示一個消息條來提示用戶地址已經複製;如果是桌面端則直接改變Tooltip
的內容來提示。Tooltip
在關閉時將提示內容恢復成初始內容,但是這裏有一點要注意,見代碼:
const closeAddressTip = (e) => {
e.preventDefault()
setTimeout(()=>{
setClickTip(COPY_TO_CLIPBOARD)
},500)
}
這裏延時了500毫秒來更新tooltip的顯示,這個是故意爲之的。因爲時序問題,不延時的話關閉提示時會先顯示初始內容然後再關閉。
這裏還有一個小問題,我在代碼註釋裏有提到,就是圖標按鈕在上幾級容器是flex
佈局下背景會失真,我們通過再外包一個<div />
來解決。具體原因沒有仔細研究,有興趣的讀者下載源碼後可以把外包的<div>
拿掉來複現,看能不能找到問題的根源。
三、拼出錢包詳情下方界面
從計劃圖中可以看出,詳情下方界面主要是一個以太坊的LOGO,然後就是用戶餘額和對應的ETH總價值(以美元計算)。這個價格採自etherscan的數據,每分鐘更新一次,用戶餘額實時更新。
運行本代碼你需要事先在網絡上找一張以太坊的LOGO圖片,然後保存在src/components/assets/
目錄下,名字叫着ether.png
(當然你也可以改成別的名字)。
新建src/components/DetailBody/index.js
,代碼如下:
import React, {useState,useEffect} from 'react';
import {makeStyles} from '@material-ui/core/styles';
import {useGlobal} from 'contexts/GlobalProvider'
import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import ListItemText from '@material-ui/core/ListItemText';
import {ethers} from 'ethers'
import {convertToEth} from 'utils';
import etherIcon from 'components/assets/ether.png';
const useStyles = makeStyles(theme => ({
container: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width:"100%"
},
avatar: {
border: 1,
borderStyle: "solid",
borderColor: "#33333333",
marginTop: theme.spacing(3),
width: theme.spacing(7),
height: theme.spacing(7),
},
balanceText:{
marginTop: theme.spacing(3),
},
sendBtn:{
width:"40%",
margin:theme.spacing(3),
},
}));
//使用etherscan來查詢ETH價格
const etherscanProvider = new ethers.providers.EtherscanProvider();
//每分鐘定時查詢ETH價格
const INTERVAL = 60000;
//只有主網絡纔有查詢並顯示ETH價格的必要
const MAINNET = 'homestead'
function DetailBody() {
const classes = useStyles()
const {network,wallet} = useGlobal()
const {address} = wallet
const [balance,setBalance] = useState(0)
const [ethPrice,setEthPrice] = useState(0)
//更新ETH價格,每一分鐘更新一次
useEffect(()=>{
if(network === MAINNET){
let stale = false
function getPrice() {
etherscanProvider.getEtherPrice().then( price => {
if(!stale) {
setEthPrice(+price)
}
}).catch( e => {});
}
getPrice()
let interval = setInterval(getPrice,INTERVAL)
//進行相關清理
return () =>{
stale = true
clearInterval(interval)
}
}
},[network])
//更新ETH數量
useEffect(()=>{
setBalance(0)
let provider = ethers.getDefaultProvider(network)
let stale = false
//監聽ETH變化
provider.on(address, _balance => {
if(!stale){
setBalance(convertToEth(_balance))
}
});
return ()=>{
stale = true
provider.removeAllListeners(address)
}
},[network,address])
return (
<div className={classes.container}>
<Avatar alt="Ether Logo" src={etherIcon} className={classes.avatar} />
<ListItemText className={classes.balanceText}
primary={<Typography variant="h6" align='center' color='textPrimary'>
{`${balance.toFixed(4)} ETH`}
</Typography>}
secondary = {<Typography variant="body1" align='center' color='textSecondary'>
{network === MAINNET ? `${(balance * ethPrice).toFixed(2)} USD` : <span> </span>}
</Typography>}
/>
<Button className={classes.sendBtn} variant="contained" color='primary' >
發送
</Button>
</div>
)
}
export default DetailBody
從上面的代碼中可以看到,在切換網絡時我們首先將餘額清零setBalance(0)
,然後再獲取餘額信息。這個地方可以拓展一下,就是未獲取到餘額之前給出提示,比如顯示”正在獲取中…“。這裏我們就不實現了,歡迎有興趣的讀者自己去實現。
注意,我們獲取到的賬戶餘額都是以wei爲單位的BigNumber,需要進行轉換成我們常用的十進制浮點數(單位爲ETH),轉換代碼也比較簡單:
import {utils} from 'ethers'
export function convertToEth(_bigNumber) {
let eth_string = utils.formatEther(_bigNumber)
return + eth_string
}
四、完成錢包詳情頁面的拼接
我們把前面兩個元素組合起來,再稍微加上一點內容,就可以得到我們的計劃界面了。修改src\views\WalletDetail.jsx
,完整代碼如下:
import React from 'react';
import {makeStyles} from '@material-ui/core/styles';
import Divider from '@material-ui/core/Divider';
import DetailHeader from 'components/DetailHeader';
import DetailBody from 'components/DetailBody';
const useStyles = makeStyles(theme => ({
container: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
margin: theme.spacing(2),
},
divider:{
width:"100%",
marginTop: theme.spacing(-1),
}
}));
function WalletDetail() {
const classes = useStyles();
return (
<div className={classes.container}>
<DetailHeader />
<div className={classes.divider} >
<Divider />
</div>
<DetailBody />
<div className={classes.divider} >
歷史記錄
<Divider />
</div>
</div>
)
}
export default WalletDetail
代碼比較簡單,這其中<Divider />
是一條分隔線。
五、修改工具類文件
修改src\utils\index.js
,將本次使用的工具類方法添加進去,完整的代碼爲:
import crypto from 'crypto'
import {utils} from 'ethers'
export function aesEncrypt(data,key) {
let cipher = crypto.createCipher('aes192', key);
let crypted = cipher.update(data, 'utf8', 'hex');
crypted += cipher.final('hex');
return crypted;
}
export function aesDecrypt(encrypted, key) {
let decipher = crypto.createDecipher('aes192', key);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
export function getPasswordLength() {
let length = process.env.REACT_APP_PASSWORD_LENGTH;
return +length
}
export function shortenAddress(address, digits = 4) {
return `${address.substring(0, digits + 2)}...${address.substring(42 - digits)}`
}
export function convertToEth(_bigNumber) {
let eth_string = utils.formatEther(_bigNumber)
return + eth_string
}
六、運行錢包並測試
在運行之前你需要更新了最新的代碼(每一篇文章結束時都附有碼雲上的git倉庫地址),如果沒有更新的話先跳到文章結尾查看git倉庫地址來更新。
npm start
來運行我們的錢包,如果提示有模塊找不到,先安裝好。如果你第一次使用我們的錢包,會提示你創建或者導入賬號;如果已經使用過了,會出現一個如下的登錄界面:
輸入你的密碼後你就會進入錢包詳情頁面,它會顯示該賬號擁有的ETH餘額和對應的總價值(單位美元)。如果我們切換到測試網,也會顯示你在測試網絡的測試ETH餘額,但不會顯示總價值(因爲測試網ETH沒有價值)。由於Localhost 8545需要你事先在本地運行一個ganache節點,所以目前並未實現,請不要點擊。
下面是主網界面:
可以看到筆者目前主網有0.2633個ETH,這些ETH總價值43.86美金(以截圖時的價格計算的)。沒有ETH的小夥伴們也不要着急,有認識的朋友有ETH的,可以讓他轉一點過來,這樣錢包裏就可以看見了。沒有這樣的朋友或者朋友不發的也不要急,我們可以切換到測試網絡進行測試,下面我給出在Kovan測試網上進行測試的方法。
七、在Kovan測試網中進行測試
我們主要測試錢包顯示ETH數量是否正確,能否自動更新。要想測試,就必須先獲取測試ETH。在三大測試網中,在寫這篇文章時,Ropsten測試幣收不到,Rinkeby測試幣獲取比較麻煩,所以我先介紹Kovan測試網測試ETH的獲取方法,獲取的同時也一併對錢包進行測試。
-
將我們錢包中的網絡切換到Kovan測試網,如下圖:
可以看到,我的這個賬號在ropsten測試網上的測試ETH數量爲0.9995個,和主網是不同的。 -
打開Kovan測試網測試ETH獲取網站(也叫kovan水龍頭):https://gitter.im/kovan-testnet/faucet
-
點擊最下方的登錄按鈕來登錄,你可能需要一個github賬號。
-
在我們的錢包中點擊我的賬號,將賬號地址複製到粘貼板中。
-
在剛纔那個水龍頭網站最下方輸入框裏粘貼你的地址,然後回車發送。用這種方式獲取一週只能獲取3個測試幣。
-
送完了會有提示,如下:送了3個ETH到我的賬號裏,並且提示不是真的幣,沒有價值,只用於測試。
-
等待我們的錢包自動刷新ETH數量。
可以看到,我們錢包kovan測試網下ETH數量已經自動更新了,我們收到了3個ETH。-_-
八、總結
這次開發我們主要實現了錢包詳情主界面UI的拼接和賬號ETH餘額及總價值的顯示。這其中ETH的價格來源於etherscan,在主網狀態下每一分鐘更新一次;而用戶餘額是不分網絡實時更新的。
主界面上其它按鈕功能暫未實現,我們計劃在下一次開發中實現ETH的發送(轉賬)功能。
本學習工程碼雲(gittee) 上的git倉庫地址爲: => https://gitee.com/TianCaoJiangLin/khwallet
懇請大家留言指正或者提出寶貴意見、建議。