边学边用--使用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

       恳请大家留言指正或者提出宝贵意见、建议。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Linux系统中安装 Yun(也称为YunOS或阿里云手机操作系统)通常涉及到的是安装Android应用,因为 YunOS 是基于 Android 的定制版本。以下是在基于 Debian 或 Ubuntu 系统(如Ubuntu Linux)上通过 AOSP (Android Open Source Project) 安装 Yun 应用的基本步骤: 1. **更新包列表并安装依赖**: ```sh sudo apt-get update sudo apt-get install build-essential git wget libncurses5-dev libudev-dev libssl-dev tk-dev libsqlite3-dev libbz2-dev liblzma-dev libarchive-dev ``` 2. **下载 Android SDK 和 NDK**: - 注册并下载 [Android Studio](https://developer.android.com/studio#downloads),它包含了 SDK 和 NDK。 - 选择 "SDK Manager" 并安装所需的组件。 3. **创建并激活一个新的 Android 模板项目**: - 使用 `android` 命令行工具初始化一个新的 AOSP项目,例如:`android create project --name YunApp --target <your-android-target> --path ~/yun_app --activity Main` - 替换 `<your-android-target>` 为适合 Yun 设备的 API 版本。 4. **配置 Gradle 和 Build**: - 打开 `build.gradle` 文件,确保适当的库和依赖项已添加,特别是对于运行 Yun 应用所必需的库。 5. **打包并推送到 Yun 设备**: - 使用 `./gradlew assembleDebug` 构建应用。 - 将 `.apk` 文件通过 USB 连接、ADB (adb push) 或者通过 Yun OS 的应用市场上传到设备。 6. **安装应用**: - 在 Yun 设备上通过文件管理器、设置 > 应用管理界面或者通过安装第三方工具(如快应用等)安装 apk 文件。 请注意,具体的 Yun 设备型号可能会有特定的要求或限制,所以在安装前最好查阅 YunOS 的官方文档或论坛以获取最新信息。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AiMateZero

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值