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

一、開發目標

       在本次開發之前,筆者對網絡選擇按鈕中各按鈕顏色的實現進行了修改,摒棄了自定義Theme這種實現,改成了直接給圖標設置顏色。這裏作一下說明,具體代碼不再列出。另外,前幾次開發中提到的Provider實質上是指Context,特此更正說明。

       在上一次開發中,我們拼接了錢包主界面並且能夠實時正確顯示用戶的餘額。這次,我們來完成錢包主界面的發送ETH功能。讓我們先登錄錢包並切換到Kovan測試網:
在這裏插入圖片描述
       你也可以先切換到kovan測試網再登錄,都是相同的。進入到主界面後會自動獲取用戶餘額,在獲取餘額之前,發送按鈕是置灰的,不可點擊。點擊發送按鈕,我們計劃顯示一個這樣的簡單頁面:
在這裏插入圖片描述
       在這裏仍然可以切換網絡,同樣,切換後會自動獲取用戶餘額,未獲取前發送不可點擊。點擊取消按鈕可以退回到錢包主界面。讓我們輸入一個賬號地址並且點擊發送,如果驗證通過,會出現一個確認框:
在這裏插入圖片描述
       用戶可再次檢查一下是否輸入有誤,點擊確定後,在短時間的loading之後會出現如下界面:
在這裏插入圖片描述
       該界面會顯示本次轉賬的簡要信息,也提供了在EtherScan上查看詳情的鏈接。注:測試網的交易也是可以在EtherScan查看的。經過短時間的等待後,交易狀態會自動變更爲已完成,任何時候點擊返回按鈕可以直接返回到主界面。注意:本錢包開發計劃中是沒有保存交易記錄這個功能的,所以這裏點擊返回後就看不到這次的交易信息了。

       提示

       在國內無法直接訪問EtherScan,你可能需要一個梯子。不過如果是主網交易,也可以在EtherScan.CN查看,這個不需要梯子。這裏給出EtherScan.CN鏈接:https://cn.etherscan.com/

二、React狀態提升

       在前一次的開發中,我們在錢包主界面實時獲取和更新用戶的賬戶餘額。可以看到,本次開發中,在發送表單界面也有同樣的需求,因爲用戶切換網絡之後必須獲取對應網絡的賬戶餘額並且更新。這樣一來,在兩個地方都需要共享餘額及對餘額的修改。按照React的設計原則,需要進行狀態提升,將原來在錢包主界面中實現的對餘額的實時更新提升到這兩個界面的公共父組件(元素)中去。這裏爲了減少複雜度,我們把用戶餘額從GlobalProvider.js中分離出來,把它和更新餘額的實現放在一個專門的Context中去。先刪除相應的代碼,然後再新建src\contexts\BalancesProvider.js,代碼如下:

/**
*  本文件用來實時更新用戶的餘額
*/
import React, { createContext, useEffect, useContext, useReducer, useMemo, useCallback } from 'react'
import { ethers } from 'ethers'
import { safeAccess } from '../utils'
import {useGlobal} from './GlobalProvider'

const UPDATE='UPDATE'
const BalancesProvider = createContext()

function useBalancesContext() {
  return useContext(BalancesProvider)
}

function reducer(state,{type,payload}) {
    switch (type) {
        case UPDATE:{
            const {address,network,value} = payload
            return {
                ...state,
                [address]:{
                    ...(safeAccess(state,[address]) || {}),
                    [network]:{
                        value
                    }
                }
            }
        }
        default:{
          throw Error(`Unexpected action type in BalancesContext reducer: '${type}'.`)
        }
    }
}

export default function Provider({children}) {
    const [state, dispatch] = useReducer(reducer, {})
    const {wallet,network} = useGlobal()

    const update = useCallback((address,network,value) => {
        dispatch({type:UPDATE, payload:{address,network,value}})
    },[])

    //刷新每個賬號在每個網絡下的餘額
    useEffect(()=>{
        if(wallet) {
            const {address} = wallet
            let stale = false
            const provider = ethers.getDefaultProvider(network)
            provider.on(address, value => {
                if(!stale ){
                   update(address,network,value)
                }
            });

            return ()=>{
                stale = true
                provider.removeAllListeners(address)
            }
        }
    },[wallet,network,update])

    return (
        <BalancesProvider.Provider value={useMemo(() => [state, { update }], [state, update])}>
            {children}
        </BalancesProvider.Provider>
    )
}

export function useBalance(address,network) {
    const [state,] = useBalancesContext()
    const {value} = safeAccess(state,[address,network]) || {}
    return value
}

       這個代碼記錄了用戶賬號在各個網絡(主網和測試網)中的ETH餘額,並且實時更新。當然,只有切換到某網絡纔會實時更新該網絡的賬號餘額,不是當前選中的網絡並不會實時更新。

       Context的作用我在前面的開發中有提及,稍後會接着介紹,這裏先講這段代碼中三個關鍵點:

  1. useReducer的用法。 useState 的替代方案。它接收一個形如 (state, action) => newStatereducer,並返回當前的 state 以及與其配套的 dispatch 方法。一般在Context中 useReducer使用的比較多,詳情見:https://zh-hans.reactjs.org/docs/hooks-reference.html#usereducer
  2. 錢包的實時更新方法。在useEffect中利用ethers庫通過監聽用戶餘額變化事件實現,注意它的依賴項和返回函數。useEffect的詳細用法 :https://zh-hans.reactjs.org/docs/hooks-effect.html
  3. 最後導出的useBalance是一個獲取用戶餘額的自定義hook。它有兩個參數,分別是賬號地址和網絡,這就意味着它支持多賬號多網絡餘額的獲取和更新,雖然目前在我們的開發計劃中錢包是單賬號錢包。

三、React數據流向

       我們藉助發送按鈕點擊後的邏輯實現來介紹React的數據流向。React不同於Vue,它的數據是自上而下單向流動的,每個組件只能更改它自己的數據(狀態),而Vue是雙向流動的。React這樣設計是故意爲之的,一方面減少複雜度,另一方面可以快速定位問題所在,因爲狀態只能被擁有它的組件所更改。而雙向流動也有它的好處,這裏誰優誰劣不作比較,只是介紹一下React的設計。

       新建src\views\SendEther.jsx,代碼如下:

/**
*  本文件用來實現錢包主界面發送ETH功能
*/
import React,{useState} from 'react';
import { withRouter } from "react-router";
import SendEtherForm from './SendEtherForm'
import TransactionInfo from './TransactionInfo'

//交易狀態
const BEGIN = 'begin'
const PENDING = 'pending'

const values_init = {
    status:BEGIN,
    tx:null,            //交易HASH
    td:null             //交易結果
}

function SendEther({history}) {
    const [values,setValues] = useState(values_init)

    //交易發送之後轉到交易信息頁面
    const sendOver = tx => {
        setValues({
            status:PENDING,
            tx,
            td:null
        })
    }
    //返回主界面
    const resverseBack = () => {
        history.push('/detail')
    }

    const {status,tx} = values
    if(status === BEGIN){
        return (
            <SendEtherForm cancelCallback={resverseBack} sendCallback={sendOver} />
        )
    }else if(status === PENDING) {
        return (
            <TransactionInfo tx={tx} reverseCallback={resverseBack} />
        )
    }else {
        return null
    }
}

export default withRouter(SendEther)

       注意這段代碼:<TransactionInfo tx={tx} reverseCallback={resverseBack} />,它將組件的數據(狀態)tx通過TransactionInfo組件的屬性tx進行了傳遞,也就是數據從上向下傳遞是通過屬性進行的,而屬性是不可更改的。因此如果傳遞的數據發生了變化,肯定是原組件進行了修改。

       然而我們有時又希望子組件在某些操作後能修改父組件的數據(狀態),怎麼辦?讓我們來看這句代碼:<SendEtherForm cancelCallback={resverseBack} sendCallback={sendOver} />,它將一個修改父組件狀態的方法sendOver作來一個回調函數通過屬性sendCallback傳遞給了組件SendEtherForm。這樣組件SendEtherForm在ETH轉賬交易發出後執行sendCallback回調,便會調用父組件對應的sendOver方法來修改父組件的狀態。狀態修改後,父組件就會重新渲染從而顯示交易信息界面,而不再是顯示轉賬表單界面。

       在React中,不管是傳遞狀態還是反向修改狀態,必須將父組件的狀態或者修改方法通過屬性一級一級往下傳,這樣在組件樹層次比較多的時候,會造成一些中間組件擁有這些並無太大意義的屬性。爲了避免這種情況,我們可以使用Context。Context提供了一個無需爲每層組件手動添加 props,就能在組件樹間進行數據傳遞的方法。但是Context在使用前必須初始化,一般在組件樹的最高層進行初始化。

       Context的詳細教程:https://zh-hans.reactjs.org/docs/context.html

四、實現ETH轉賬

       在以太坊中,ETH轉賬也是一個交易,而交易的一般描述爲:外部賬號(用戶,非合約)發起一個交易(創建一個交易對象),然後用私鑰將該交易簽名並且發送,並等待以太坊上的礦工進行打包執行和發佈到所有節點。這中間如果包含改變以太坊狀態的操作則會消耗gas。同時你還需要設置一個gas價格,消耗的gas數量乘於gas價格就是你這次交易的手續費,會從你的餘額中扣除。gas價格的多少也決定了交易的速度,你出的價格高你就是VIP通道,你出的價格低就沒有礦工理你,你就只能慢慢等或者根本無法打包。我們也可以隨交易同時發送ETH,它也從你的餘額中扣除。如果執行的過程中出錯(或者gas不夠),你的交易就會失敗,所有造成的改變被重置,發送的ETH會被退回,但手續費會根據出錯的實際情況部分消耗或者完全消耗。如果礦工執行了交易並且發佈到所有節點,你的交易就成功了,就永久保存在以太坊上了。

       在具體的實現中,執行轉賬我們得先構建一個交易對象。在JavaScript中一個交易對象當然也就是一個普通的對象,比如{},它有以下幾個可選屬性:

  • to 代表調用的地址,轉賬時就是接收ETH的地址
  • gasLimit 本次交易消耗的最大gas,一般來說ETH轉賬設置成23000即可,最小不能小於21000
  • gasPrice 你願意爲你的gas出的價格,當然是你付給別人的價格
  • nonce 這次交易的編號,編號對每個地址來說都是自動增長的,代表已經完成的交易數量。你可以設定成一個正在pending的交易的nonce來加速或者覆蓋該交易,一般情況下缺省即可。
  • data 隨交易發送的數據, 注意發送數據會收手續費,數據越大手續費越多。轉賬時沒有特殊需要缺省即可。
  • value 隨交易發送的ETH數量。如果是轉賬,就是轉賬的ETH數量。
  • chainId 交易網絡的ID,防止使用了錯誤的網絡交易。一般缺省即可。

       上面七項屬性都是可選項,意即可以省略,當然不能全部省略。

       在src\views\SendEtherForm\index.js中,我們構建交易對象的代碼塊爲:

let transaction = {
    to:_address,
    value:utils.parseEther("" + eth_amount),
    gasLimit:GAS_LIMIT,
    gasPrice:utils.parseUnits("" + gas_price,'gwei'),
    chainId:getChainIdByNetwork(_network)
}

       可以看到和上面七個屬性相比,我們沒有nonce和data,我們只轉ETH,沒有交易數據,所以沒有data。nonce讓它自己決定就好。

       簽名並且發送交易的代碼塊爲:

//簽名併發送交易
let provider = ethers.getDefaultProvider(network)
let tx_wallet = wallet.connect(provider)
let sendPromise = tx_wallet.sendTransaction(transaction);
setOpen(false)
setCircleOpen(true)
sendPromise.then(tx => {
    setCircleOpen(false)
    if(sendCallback){
        sendCallback(tx)
    }
}).catch(err =>{
    setCircleOpen(false)
    return showSnackbar("ETH發送失敗,請檢查你的餘額")
})

       前兩行代碼將我們的錢包和要交易的網絡進行了鏈接,第三行代碼我們使用錢包對象的sendTransaction方法來簽名併發送交易,注意它返回一個promise。交易發送後我們會得到一個交易對象tx,它包含交易的哈希、nonce、接收者等信息。可以看到,我們將該交易對象tx通過回調函數傳遞給了父組件(數據流中的反向改變數據)。

       交易對象tx有一個wait()方法,它用來等待交易的執行結果(礦工打包執行併發布的結果),它也返回一個pormise,我們來看一下它的用法 ,在src\views\TransactionInfo\index.js中,代碼塊爲:

useEffect(()=>{
    if(tx){
        tx.wait().then(td => {
            setState({
                pendingOver:true,
                td
            })
        }).catch(err => {})
    }
},[tx])

       tx.wait()返回一個包含交易結果的td,我們可以用它來顯示一些執行信息,比如轉賬是否成功等。注意,tx.wait()的執行時長是未定的,視網絡擁堵情況和礦工的選擇。交易在簽名發送後而又未返回結果時,交易的狀態是pending,字面意思就是懸而未決的。交易結果返回後狀態要麼是成功的,要麼是失敗的。

       在我們的交易信息界面還使用了三個進度條來動態表示pending,有興趣的讀者可以看一下進度條的用法。最後放一個轉賬完成的界面:
在這裏插入圖片描述
       這是點擊界面上 EtherScan上查看 超鏈接後EtherScan上的結果:
在這裏插入圖片描述

五、總結

       本次開發我們主要完成了更新用戶賬戶餘額的狀態提升和用戶轉賬功能的實現。同時也介紹了以太坊上交易的一般流程和交易對象的構建方法。最後對交易簽名發送和執行的返回結果進行了簡要介紹。具體的UI代碼沒有介紹,大家可以直接查看或者下載碼雲git倉庫上的代碼。

       下次開發計劃實現賬號的導出功能。

       本錢包碼雲git倉庫地址: => https://gitee.com/TianCaoJiangLin/khwallet

       懇請大家留言指正或者提出寶貴意見、建議。

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