边学边用--使用React下的Material UI框架开发一个简单的仿MetaMask的网页版以太坊钱包(七)

       在前一篇文章里,我们完成了钱包的账号导出功能。 这次我们计划开发钱包显示ERC20代币列表的功能。因为我们是仿MetaMask做的,所以目前只能手动添加Erc20代币。

一、主要功能演示

       用户登录钱包后,点击左上角的菜单按钮,会出现我的账号界面:
在这里插入图片描述

       这个界面在MetaMask中是用一个抽屉实现的,也就是有抽屉动画。在Material UI中抽屉动画都是全屏的,再加上我们的钱包在Web页面中只占屏幕中心很小一部分,所以多次尝试之后由于个人能力问题无法使用抽屉实现。于是使用了常用的路由功能进行了跳转,简化这一部分的处理。有兴趣的读者可以自己研究一下Material UI的抽屉动画。

       界面里点击账号地址可以复制,点击详情按钮就会出现上一篇文章实现的用户详情界面。让我们点击添加代币(这里是ERC20代币),出现如下界面:
在这里插入图片描述
       这里我们和MetaMask界面相比也做了一些简化。在最上方的文本框里输入对应网络的代币合约地址并点击查询,查询结束后会显示该代币的符号、精度和你的代币余额。如果你输入的不是一个有效的代币地址,会提示地址无效。
在这里插入图片描述
       点击添加按钮,就可以将它添加到我们的代币列表里去了,注意这里的列表是可以上下滚动的。如果点击取消,则会回到钱包主界面,如果代币已经添加,会有提示。
在这里插入图片描述
       可以看到我们的列表里已经有GEC代币了,点击GEC代币右边的扩展按钮,会出现一个菜单,它包含隐藏代币和在EtherScan上查看两项内容:
在这里插入图片描述
       我们点击隐藏代币(隐藏就是不在列表里显示了,并不会弄丢你的代币),会出现一个确认画面:
在这里插入图片描述
       点击取消会回到列表,我们来点击隐藏,GEC代币就会从你的列表里删除并返回到代币列表,此时你会看到和未添加时一样的画面。如果想再次显示GEC代币,你只能再次添加它。

       让我们自己发行一个空气币KHC(钱包叫KHWallet,币当然叫KHCoins啦),把它和GEC都添加到列表里:
在这里插入图片描述
       你现在可以将你所有的代币都添加到列表里了。如果你想知道某个已经添加的代币的相关信息,点击右边扩展按钮,再点击在etherscan上查看按钮,就可以在浏览器里看到了,注意地址栏里包含了代币地址。
在这里插入图片描述
       这次开发只完成了列表里ERC20代币的添加、显示和隐藏功能。点击代币用来在钱包主界面显示并转移代币的功能并未开发。这个放到下一次开发,不过当前账号的代币余额监测是实现了,可以用来保持代币余额实时更新。

二、测试代币余额显示

       我们来测试一下代币余额是否能实时更新,让我们切换到Kovan测试网进行免费操作。Kovan测试网的测试ETH获取方法我在前面的文章已经提到过了,欢迎读者查看我整个系列的文章来获取对该钱包一个全面的认识。

       让我们把钱包和MetaMask都打开,并且准备两个账号,一个在钱包里使用,一个在MetaMask中使用,它们都添加相同的代币,如下图:
在这里插入图片描述
       可以看到,左边我的账号有988004400个测试代币,右边的Kovan账号2有200个测试代币,让我们从Kovan2里向我的账号里转移100个测试代币。

       在MetaMask代币列表界面里(打开方式和我们的钱包一样)点击测试币,进入如下界面(这个是我们下一步要实现的):
在这里插入图片描述
       可以看见MetaMask主界面显示了200个测试代币,在我们的钱包里点击账号地址进行复制,再点击MetaMask里的发送按钮(可能需要重新打开该页面):
在这里插入图片描述
       地址栏直接粘贴我的账号,数量那一栏输入100。点击下一步,在下一个画面点击确认,等待交易完成,同时盯着钱包界面:
在这里插入图片描述
       在MetaMask代币已发送确认出现以前,左边的钱包里的代币余额就已经更新了。这是因为代币余额是通过监测过滤的交易事件更新的,事件的接收会早于交易的接收确认。

       还可以进行反向测试,就是使用MetaMask登录我的账号,向Kovan账号2里发100个测试代币,在交易确认之前,左边钱包里的代币余额会更新。这里我们就不再演示了。

三、这次开发的要点

       这次开发除了UI拼接、刷新或者显示的工作量比较大外,也有一些设计或者代码编写上的要点:

  1. 本地存储的设计。账号的代币列表肯定保存在本地存储里,以什么样的格式保存、怎么更新、怎么读取都需要好好设计。具体实现在src\contexts\StorageProvider.js里,读者也可以有更好的设计思路。下面是其中一段注释:
/**  本地存储计划示例,
{

    "0x1234....":{
                    crypt:"ifajfay08",
                    erc20Tokens:{
                                    homestead:[
                                                {
                                                    address:'0x1234....'
                                                    balance:0x78,
                                                    symbol:'',
                                                    decimals:''

                                                },
                                                {
                                                    address:'0x1234....'
                                                    symbol:'',
                                                    decimals:''
                                                }
                                            ]
                                }
                  }
}
*/

       本地存储是Json格式,每个地址对应一个加密后密钥(用来登录用)及20代币列表。其中20代币列表又根据网络作了区分,某网络下的所有20代币是个数组,数组的每个元素是20代币对象,包含它的地址,账号余额,符号和精度等。

       这里面有一个小细节:因为在Js库中,以太坊返回的整数值为bigNumber格式,它保存在本地时转换成了16进制字符串形式,从本地读取使用的时候需要先转换成bigNumber

  1. ERC20代币合约。既然要和代币打交道,就必然涉及代币合约。代币合约主要元素有代币的地址、代币合约的ABI,当然还有属于哪个网络。这其中代币合约的ABI可以是普通的编译器产生的ABI,也可以是人类可读的ABI(ethers库两者都可使用,其它库笔者没使用过,如有需要请自己核实)。本钱包中使用的ERC20代币合约的ABI为可读ABI,代码如下:
[
  "function balanceOf(address owner) view returns (uint)",
  "function decimals() view returns (uint8)",
  "function symbol() view returns (string)",
  "function allowance(address tokenOwner, address spender) view returns (uint)",
  "function transfer(address to, uint amount)",
  "function approve(address spender, uint amount)",
  "event Transfer(address indexed from, address indexed to, uint amount)",
  "event Approval(address indexed tokenOwner, address indexed spender, uint amount)"
]

       获取合约的代码片断如下:

//获取ERC20代币合约
export function getErc20Token(tokenAddress,network,wallet) {
    
    
    if(!isAddress(tokenAddress) || !network) {
    
    
        return null;
    }
    try{
    
    
        let provider = ethers.getDefaultProvider(network);
        if(wallet) {
    
    
            provider = wallet.connect(provider)
        }
        return new ethers.Contract(tokenAddress,ERC20_ABI,provider)
    }catch{
    
    
        return null
    }
}

       这里再次推荐:读者如果对开发以太坊上的Dapp有兴趣,请先去阅读ethers库。

  1. 账号ERC20代币余额的获取和刷新,我们先看获取代码:
//获取某个地址在某个token余额
export async function getTokenBalance(tokenContract,address) {
    
    
    return tokenContract.balanceOf(address).catch(error => {
    
    
        error.code = ERROR_CODES.TOKEN_BALANCE
        throw error
    })
}

       这个代码很简单,直接调用合约的balanceOf方法,注意它返回的是一个promise。

       我们再看看刷新的实现,这个相对比较复杂,也许有更好的办法或者优化:

//监听用户代币变化
useEffect(()=>{
    
    
    if(tokens.length > 1) {
    
    
        let stale = false
        let allContracts = []
        for (let token of tokens) {
    
    
            if(token.symbol !== 'ETH'){
    
    
                allContracts.push(getErc20Token(token.address,network,wallet))
            }
        }
        for (let contract of allContracts) {
    
    
            let filter1 = contract.filters.Transfer(wallet.address,null)
            let filter2 = contract.filters.Transfer(null,wallet.address)
            // eslint-disable-next-line
            contract.on(filter1,(from,to,amount,event) => {
    
    
                getTokenBalance(contract,wallet.address).then(_balance => {
    
    
                    if(!stale) {
    
    
                        updateTokenBalance(wallet.address,network,contract.address,_balance)
                    }
                })
            })
            // eslint-disable-next-line
            contract.on(filter2,(from,to,amount,event) => {
    
    
                getTokenBalance(contract,wallet.address).then(_balance => {
    
    
                    if(!stale) {
    
    
                        updateTokenBalance(wallet.address,network,contract.address,_balance)
                    }
                })
            })
        }

        return () => {
    
    
            stale = true
            for (let contract of allContracts) {
    
    
                contract.removeAllListeners('Transfer')
            }
        }
    }
},[tokens,network,wallet,updateTokenBalance])

       这里我们把所有的代币合约都设置了监听器,监听它的Transfer事件。通过分别设置过滤器,过滤出那些发送者或者接收者为用户账号的事件。监听到事件后再获取用户最新余额来进行更新。当界面退出时,取消所有监听器。

       这里还有一点小提示,对于一个纯数组 arrays (没有额外属性)来讲,for (let key of arrays)for (let key in arrays)来讲是不同的,前者获取的是元素,后者获取的是下标,希望使用的时候小心不要用错了。

四、总结

       这次开发相比前几次而言,相对复杂些,时间也比较紧,没有详尽测试,有什么不完善的或者错误的地方欢迎读者在阅读或者使用的过程中给予指正,不胜感谢。

       随着开发的深入,你越来越会感觉到你需要详尽的去弄清Material UI框架的每一个组件的每一个属性,甚至每一个CSS规则。然而这是一个长期的过程,并且你也很难深入到底层的实现。所以,保持一直学习Material UI框架中每一个组件是非常必要的,温故而知新。不仅要知道组件的常用的属性的用法,甚至还要需要知道常用的CSS规则是什么样的,怎么修改。我想就算不是学习Material UI,就算你是学习Vue里面的一些框架,也是同样的道理。

       具体的UI代码比较繁琐复杂,这里不举例,大家可以下载了我的代码慢慢看。

       另外,推荐一篇文章:CSS,艺术、科学还是梦魇(你应该知道的一切)。我个人觉得这是每个前端开发者必看的一篇文章,虽然我不是前端开发者。

       下一次开发我们计划实现ERC20代币的转账功能

       本钱包码云(gitee)仓库地址为: => https://gitee.com/TianCaoJiangLin/khwallet

       欢迎读者留言指出错误或者提出改进意见。

猜你喜欢

转载自blog.csdn.net/weixin_39430411/article/details/104145456
今日推荐