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

       在上一次開發中,我們在顯示用戶賬號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倉庫,這裏只講一下幾個要點。

三、代碼要點介紹

  1. 我們增加了一個全局變量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值。但是這個值不是總能取到,有的時候還是使用閉包方便一些,還是推薦大家在需要傳遞參數的情況下使用閉包來綁定點擊事件。

  1. 調用合約發送代幣,這裏的代碼簡單解釋一下:
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

歡迎讀者留言指出錯誤或者提出改進意見。

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