前端工程 區塊鏈 程式開發

React 整合 Ethereum 的新方法:試玩 useDApp

之前自己做 Ethereum 的專案時,常常是自己拿 Web3.js 或 Ethers.js 來做,包含前一次的幣安黑客松作品也是這樣來的。最近看到 Solidity 讀書會 上面有人分享 EthWorks 的 Bounty Program 而發現新玩具,也稍微把玩了一下。這篇就來記錄一下,順便看能不能做出什麼去投 Bounty。

TL;DR

在快速把玩下,可以簡單說這個工具有點過新,所以文件上有許多是直接沒寫內容的(說不定有些隱藏功能甚至沒有寫進文件)。但這個工具提供了許多 Hook,也幫忙處理許多如切換帳戶的瑣事。過去利用 Web3/Ethers 時需要自己去註冊監聽,透過 useDApp 就可以更好的運用 Hook 來完成這些事情。

此外,useDApp 會將多個 Function Call 透過 Multicall 的方式包裝,進而降低 RPC 的呼叫。一面對於使用者端降低網路開銷,一面也減輕 RPC 節點的消耗,特別是如 Infura 這種有限額的服務。

你可以在 https://github.com/flyinglimao/usedapp-demo 下載下面的程式碼,或到 https://flyinglimao.github.io/usedapp-demo 看看效果。

開始使用

你可能會需要一個 React 專案來開始:

npx create-react-app usedapp-demo --template typescript

後面的 --template typescript 是我習慣,同時也因為文件可能不夠完整,透過 TypeScript 來幫忙,不喜歡或不會 TypeScript 的可以自行移除。

備註:下面的圖片有用 Styled-Component 包過,看起來和實際程式碼會不同

接著安裝 useDApp:

yarn add @usedapp/core

同樣,喜歡 npm 的朋友可以改用 npm install @usedapp/core

安裝完後,我們需要一個 DAppProvider

// App.tsx
import { Config, ChainId, DAppProvider } from '@usedapp/core';

const config: Config = {
  readOnlyChainId: ChainId.Kovan,
  readOnlyUrls: {
    [ChainId.Kovan]: '', // 如果沒有的話可以到 Infura.io 上辦一個
    },
  // multicallAddresses: Mulitcall 用,如果不是原生支援的鏈可能需要設定
  // supportedChains: 如果使用者切錯鏈會提示,透過下面的 useEther 的 error 回報
  // pollingInterval: 輪詢間隔,如果 RPC Node 要付錢可以考慮降低,但資料更新就會比較慢的樣子
}

function App() {
  return (
    <DAppProvider>
      // ...
    </DappProvider>
  );
}

連接錢包

連接錢包的函數通過 useEthers() 提供,可以通過檢查 account 來判斷是否連接:

function Connect() {
  const { activateBrowserWallet, account, error } = useEthers();
  useEffect(() => {
    if (error) alert(error);
  }, [error]);

  return account ?
    <h1>{ account }</h1>:
    <button onClick={() => activateBrowserWallet()}>連接錢包</button>;
}

當使用者端沒有錢包卻點連接時,error 會設定,要注意的是 error 只會出現一瞬間,所以這邊用 useEffect 來抓,但感覺應該會有變動,參考本文時請留意這點。

可以試著在錢包中切換帳戶,會注意到他的地址跟著變了。
連接到錢包的 Demo

取得餘額

取得 ETH 餘額應該也是經常用到的功能,可以利用 useEtherBalance。如果要取得 ERC-20 的代幣可以用 useTokenBalance

import { useEtherBalance, useEthers, useTokenBalance } from "@usedapp/core";
import { formatEther, formatUnits } from '@ethersproject/units';

export default function Balance() {
  const { account } = useEthers();
  const ethBalance = useEtherBalance(account);
  const usdcBalance = useTokenBalance('0xe22da380ee6b445bb8273c81944adeb6e8450422', account); // Faucet: https://testnet-v1.aave.com/faucet/USDC-0xe22da380ee6b445bb8273c81944adeb6e84504220x506b0b2cf20faa8f38a4e2b524ee43e1f4458cc5

  return (
    account ?
      <div>
        <h1>Ethers: { ethBalance ? formatEther(ethBalance) : 0 }</h1>
        <h1>USDC: { usdcBalance ? formatUnits(usdcBalance, 6) : 0 }</h1>
      </div>:
    <h1>Not Connect Yet</h1>
  );
}

這邊用了 Ethers.js 的函數來顯示,需要的話可以用 yarn add ethers 安裝。另外可以試著在錢包裡面轉帳,過一段時間後就會看到數字變了。
顯示餘額(ETH 與 USDC)

呼叫合約

作為一個 DApp,沒有合約可說是太奇怪了。如果要取得合約的值(不必發起交易的那些),可以用 useContractCall,這裡會需要用 Ethers.js 的 Interface:

import { Interface } from '@ethersproject/abi';
import { useContractCall, useContractCalls } from '@usedapp/core';
import { ChangeEvent, useState } from 'react';

const abiJSON = [ /* ERC-20 ABI */ ];
const abi: Interface = new Interface(abiJSON);

export default function Contract() {
  const { account } = useEthers();
  const [ address, setAddress ] = useState('0xe22da380ee6b445bb8273c81944adeb6e8450422');
  const tokenName = useContractCall({
    abi,
    address,
    method: 'name',
    args: [],
  });
  // 可以用 useContractCalls 來打包
  const [ tokenDecimals, tokenBalance ] = useContractCalls([
    {
      abi,
      address,
      method: 'decimals',
      args: [],
    },
    {
      abi,
      address,
      method: 'balanceOf',
      args: [account],
    }
  ]);
  return (
    <div>
      <h1>
        代幣地址:
        <Input
          type="text"
          onChange={(event: ChangeEvent<HTMLInputElement>) => setAddress(event.currentTarget.value)}
          value={address}
        />
      </h1>
      <h1>代幣名稱:{ tokenName }</h1>
      { tokenBalance && <h1>代幣餘額:{ formatUnits(tokenBalance[0], tokenDecimals) }</h1> }
    </div>
  );
}

useContractCall 回傳的實際上是一個陣列,上面的有點小髒這點請注意。
合約靜態函數呼叫範例

發起交易的呼叫用的則是 useContractFunction,第一個參數要放的是從 Ethers.js 產生的 Contract 物件,Singer 部分不需要設置,useDApp 會自動完成,也可以在第三個參數中設定,第二個參數則是要呼叫的函數名稱:

import { Contract } from '@ethersproject/contracts';
import { ChainId, getExplorerTransactionLink, useContractFunction, useEthers } from '@usedapp/core';

const abiJSON = [
  {
    "constant": false,
    "inputs": [
      {
        "name": "_to",
        "type": "address"
      },
      {
        "name": "_value",
        "type": "uint256"
      }
    ],
    "name": "transfer",
    "outputs": [
      {
        "name": "",
        "type": "bool"
      }
    ],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
];
const usdcContract = new Contract('0xe22da380ee6b445bb8273c81944adeb6e8450422', abiJSON);

export default function Transaction() {
  const { send, state } = useContractFunction(usdcContract, 'transfer');
  function shortenTransactionHash(hash: string) {
    return hash.substr(0, 6) + '...' + hash.substr(-4)
  }
  return (
    <div>
      { state.status === 'None' && <></> }
      { state.status === 'Exception' && <h1>交易失敗,參數不正確</h1> }
      { state.status === 'Mining' && 
        <h1>交易進行中<br />
          <a href={getExplorerTransactionLink(state.transaction.hash, ChainId.Kovan)}>
            { shortenTransactionHash(state.transaction.hash) }
          </a>
        </h1>
      }
      { state.status === 'Success' &&
        <h1>交易成功<br />
          <a href={getExplorerTransactionLink(state.transaction.hash, ChainId.Kovan)}>
            { shortenTransactionHash(state.transaction.hash) }
          </a>
          位於 { state.receipt.blockNumber }
        </h1>
      }
      { state.status === 'Fail' &&
        <h1>交易失敗<br />
          <a href={getExplorerTransactionLink(state.transaction.hash, ChainId.Kovan)}>
            { shortenTransactionHash(state.transaction.hash) } 
          </a>
          位於 { state.receipt.blockNumber }
        </h1>
      }
      <button onClick={() => send('0x000000000000000000000000000000000000dead', '1000000')}>燒毀 1 USDC</button>
    </div>
  );
}

state 下必定會有 status,用來表示交易狀態,一開始會是 None。交易送出後會開始有 transaction,具體物件結構要參考 Ethers.js 的文件(TransactionTransactionRespones)。需要留意的是 transaction 不會在交易完成後更新,因此如區塊高度等出塊後才有的資料要到 receipt 取得,參考文件是 TransactionReceipt
發起交易 Demo
上面偷渡了 getExplorerTransactionLink 的用法,相似的還有 getExplorerAddressLink

小結

目前用下來覺得還滿實用的,他可以處理掉很多滿麻煩的事情,例如前面提到的切換帳戶的問題,同時也避免掉需要自行包裝成 Hook 的麻煩。美中不足的地方應該是對 ABI 的支援,目前看下來還是得回到 Ethers.js 上,如果能在 useDApp 中提供或許會更好。其次是合約相關的 Hook 沒辦法根據 ABI 去制定型別,因此無法發揮 TypeScript 的能力去避免錯誤的 Code。最後不確定是不是我的問題,BigNumber 直接用會提示 Overflow。
由於文件沒寫完,所以有些 Provider 和 Hook 我就沒去看了,有興趣的可以翻翻文件

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *