邊學邊用--使用React下的Material UI框架開發一個簡單的仿MetaMask的網頁版以太坊錢包(四)

一、前言

       在上一章,我們完成了錢包的底層實現及新建賬號、錢包登錄和導入賬號這三個頁面之間的關聯。本章我們接着開發,完成錢包詳情界面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>&nbsp;</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的獲取方法,獲取的同時也一併對錢包進行測試。

  1. 將我們錢包中的網絡切換到Kovan測試網,如下圖:
    在這裏插入圖片描述
           可以看到,我的這個賬號在ropsten測試網上的測試ETH數量爲0.9995個,和主網是不同的。

  2. 打開Kovan測試網測試ETH獲取網站(也叫kovan水龍頭):https://gitter.im/kovan-testnet/faucet

  3. 點擊最下方的登錄按鈕來登錄,你可能需要一個github賬號。
    在這裏插入圖片描述

  4. 在我們的錢包中點擊我的賬號,將賬號地址複製到粘貼板中。

  5. 在剛纔那個水龍頭網站最下方輸入框裏粘貼你的地址,然後回車發送。用這種方式獲取一週只能獲取3個測試幣。 在這裏插入圖片描述

  6. 送完了會有提示,如下:送了3個ETH到我的賬號裏,並且提示不是真的幣,沒有價值,只用於測試。
    在這裏插入圖片描述

  7. 等待我們的錢包自動刷新ETH數量。
    在這裏插入圖片描述
           可以看到,我們錢包kovan測試網下ETH數量已經自動更新了,我們收到了3個ETH。-_-

八、總結

       這次開發我們主要實現了錢包詳情主界面UI的拼接和賬號ETH餘額及總價值的顯示。這其中ETH的價格來源於etherscan,在主網狀態下每一分鐘更新一次;而用戶餘額是不分網絡實時更新的。

       主界面上其它按鈕功能暫未實現,我們計劃在下一次開發中實現ETH的發送(轉賬)功能。

       本學習工程碼雲(gittee) 上的git倉庫地址爲: => https://gitee.com/TianCaoJiangLin/khwallet

       懇請大家留言指正或者提出寶貴意見、建議。

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