邊學邊用--使用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

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

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