在上一次开发中,我们在显示用户账号ERC20代币列表时,漏掉了一个功能,就是在显示列表的同时更新用户的代币余额。这一次开发我们先把这个功能补上,然后再完成ERC20代币的转账功能。
一、更新用户ERC20代币余额
在上一次的开发中,用户ERC20代币余额只在添加代币的时候获取了一次。在显示列表界面里仅监听了用户代币改变事件,但是如果这个事件不是在钱包打开的时候广播的,那么就无法监听,因此用户账号的代币余额不会更新。并且按照正常的逻辑,在显示用户代币列表时应该自动显示最新的代币余额,如下图:
在React函数组件中进行类似操作通常的做法是在组件加载完成之后来使用useEffect
来获取用户代币余额,然后再更新对应的代币信息。由于获取代币余额需要用到其它代币信息,比如代币地址等,所以这个更新代币信息的useEffect
的依赖项要用到代币信息本身。这样就会造成一个无限循环,具体流程如下:
组件加载完毕,显示代币余额 => 获取最新代币余额,更新代币信息 => 组件重新渲染 => 依赖项代币信息更新导致重新执行useEffect
中的内容 => 再次获取代币余额并更新 => 再次渲染并重新获取,无限循环。
所以我们必须用一个状态变量来控制这种重复获取用户代币余额的行为。首先该变量设置为false,如果获取了余额,更新余额之前将变量设置为true。更新余额重新渲染后检查该变量是否为false,如果不是,则代表已经更新过,就跳过不再更新。同时,用户账号或网络的改变要重置这个变量为false,这样可以获取别的账号或者网络对应的代币的最新余额。我们来看一下代码片断:
const [isRefresh,setIsRefresh] = useState(false)
//每次切换网络或者用户时,更新刷新状态
useEffect(()=> {
if(wallet && network){
setIsRefresh(false)
}
},[wallet,network])
//每次打开界面时只更新一次代币余额
if(!isRefresh) {
Promise.all(allPromise).then(results => {
//更新所有代币余额
for(let i=0;i<len;i++) {
tokens[i].balance = results[i]
}
if(!stale) {
setIsRefresh(true)
updateAllTokens(wallet.address,network,tokens)
}
}).catch( () =>{
})
}
这里第一行代码就是设置用来标记是否获取过代币余额的状态变量。第二段代码就是账号或者网络改变时重置该状态,第三段代码就是如果该状态为false,则更新所有代币余额。
也可以将该更新代码放置到后台,也就是在src\contexts\StorageProvider.js
中进行,更新的处理和用户ETH余额更新处理类似。但是用户代币余额只在显示代币列表和发送代币时用到,为了减少刷新次数,所以把它放在对应界面上进行处理。
二、本次开发简述
本次开发主要是实现用户代币转账的功能。这次的开发难度不大,主要是在前面已经实现的ETH转账的基础上进行一些修改。
点击用户ERC20代表里的任意代币,就会进入钱包主界面并且显示该代币:
上图中我们的主界面显示的是GEC代币,它的余额在显示时会自动更新一次。同时也会监听交易事件来实时更新,处理的方式和前面提到的更新用户代币余额的方式相似。
我们点击发送按钮,会出现和发送ETH类似的界面:
我们填入接收人的地址和发送的数量,数量也可以为小数。然后点击发送按钮,会出现一个确认弹框,点击确定,在短时间的loading动画后就会出现简要交易信息的界面。
注意:不管是发送ETH还是发送代币都需要手续费,都要消耗少量的ETH。如果发送失败,退回到钱包主界面检查一下自己是否有足够的ETH。
注意:在这个界面中是可以切换网络的。由于一个网络中的代币在另一个网络中是不存在的,而ETH是都存在的,所以切换网络后钱包会自动变成发送ETH,这里用户需要小心不要发错。
将网络切换到Kovan测试网,可以看到发送100个GEC变成了100个ETH,如上图。因为我的ETH数量没有那么多,第一发不出去,第二钱包会有提示。但是如果用户是发送1个GEC,这里就变成了1个ETH,还是有很大机会发送出去的。
让我们点击取消,退回到主界面。接上面的操作,此时网络已经变成了Kovan测试网,而我们也要在这个网进行测试,点击左上角的菜单按钮:
让我们点击FIXED代币,再次进入主界面来重启发送流程:
点击发送,会弹出确认按钮,可以让用户在发送前最后检查,防止发错。
点击确定,然后会显示一个loading动画,同时调用以太坊上的代币合约进行代币转移。交易提交后会进入简要交易信息界面:
相比发送ETH时的交易信息,我们增加了一个交易类型信息。目前这个界面有些文字位置对的不是很齐,需要再细心调整一下。
耐心等待交易完成,因为Kovan测试网的出块速度很快,所以交易应该很快就能确认。下面是完成后的界面,此时交易状态已经更新,pending动画也停止了。
点击返回按钮退回到钱包主界面,因为我们的代币余额是进入主界面之后再更新,所以还可以在这个界面看到代币余额更新的动作,也就是下图中的2090变成1990。
好了,这次开发的主要功能介绍完了,完整代码大家还是去看git仓库,这里只讲一下几个要点。
三、代码要点介绍
- 我们增加了一个全局变量
tokenSelectedIndex
代表当前选中的代币,比如是ETH还是GEC。在代币列表里当前选中的货币会有背景提示。
const {
network,wallet,ethPrice,tokenSelectedIndex} = useGlobal()
在开发功能介绍的最后,我们的钱包主界面显示的是FIXED代币,所以点击左上角的菜单按钮,代币列表中FIXED代币就会显示为选中状态。如果我们未选中任何代币,则默认选择ETH,如果我们隐藏了当前选中的代币,则也是改成选中ETH。
每次切换网络时,我们都设置成选择ETH,因为一个网络的代币在另一个网络中不存在。
const handleSelected = key => () => {
if(selectedIndex === key) {
return;
}
setSelectedIndex(key)
setOpen(false);
updateGlobal({
tokenSelectedIndex:0,
network:NET_WORKS[key]
})
};
这里使用了一个闭包,因为函数组件中没有this,所以它无法使用类组件中常用的this.handleSelected.bind(this,key)
这种方法在绑定函数的同时进行参数传递。在函数组件中绑定点击事件常用的方法就是用闭包来生成一个新的函数来进行参数传递。另外一个方法是将具体的点击对象设定一个value
属性,点击的时候绑定的函数取e.currentTarget.value
值。但是这个值不是总能取到,有的时候还是使用闭包方便一些,还是推荐大家在需要传递参数的情况下使用闭包来绑定点击事件。
- 调用合约发送代币,这里的代码简单解释一下:
if(isToken) {
//将钱包绑定合约直接调用方法进行
let contract = getErc20Token(token.address,network,wallet)
if(contract) {
let amount = convertFixedToBigNumber(eth_amount,token.decimals)
let args = [_address,amount]
contract.transfer(...args).then(tx => {
setCircleOpen(false)
if(sendCallback){
sendCallback(tx,eth_amount,SYMBOL)
}
}).catch(err =>{
setCircleOpen(false)
return showSnackbar(SYMBOL + "发送失败",'error')
})
}else{
setCircleOpen(false)
return showSnackbar("合约未初始化,请稍候",'info')
}
}
第三行代码是获取一个合约对象,它和对应的钱包绑定在一起,也就是绑定后的合约既能调用读取合约数据的view
或者pure
方法,也能调用改写合约数据的方法。其实contract.transfer(...args)
这个函数调用还可以有一个额外可选参数,也就是options。它和前面文章提到过的直接ETH转账时创建的交易对象类似,比如可以设定随交易发送的ETH数量,设定chainId等。
四、总结
这次开发首先补上了上一次开发的一个遗漏,然后在给ETH转账的基础上适当修改以用于ERC20代币转账。主要练习了在useEffect
中防止无限更新用户代币余额和怎么调用合约来转移代币。这个钱包还有一个不完善的地方一直没有修改,那就是eth价格采自etherscan。而etherscan的API没有梯子是访问不了的,计划下次开发时把它改成一个另一个数据源。
我们的钱包现在的功能已经有:账号创建、登录、导入与导出;添加、显示、隐藏和发送ERC20代币,实时显示并更新ERC20代币余额;发送ETH并且实时更新ETH余额;支持主网络和三大测试网络,其实也可以支持Goerli测试网,不过没必要弄这么多。目前并不支持多账号和历史记录保存等。
我们的钱包离开发计划只有一个功能没有实现了,那就是签名交易。因为这是一个简单的网页版钱包,所以交易数据只能通过URL发送过来然后进行签名,实现的方式有些丑陋,我们计划在下一次开发实现它。
另外由于时间有限,本地localhost节点的适用不打算做开发了。计划在下一次开发中把localhost网络取消掉,以后有空开发了再补上。
本钱包码云(gitee)仓库地址为: => https://gitee.com/TianCaoJiangLin/khwallet
欢迎读者留言指出错误或者提出改进意见。