Web3 全端工程師的技術養成之路 - Day 4: Web3 與前端:實作第一個 DApp

KryptoCamp 教練 Harry Chen 撰寫發表,為 2023 ITHOME 鐵人賽 Web3 組冠軍作品

Day 4 — Web3 與前端:實作第一個 DApp

今天我們會用 React 實作一個最簡單的去中心化應用,也就是 Decentralized App(簡稱 DApp)。許多區塊鏈應用之所以只需要前端的技術,是因為可以直接把區塊鏈本身當成後端來用,因為區塊鏈就支援讀寫的操作。

區塊鏈節點服務介紹

對於「讀取」操作可以透過區塊鏈的節點服務提供商,得到區塊鏈上的即時資料。對於「寫入」操作則可以透過發送一個交易請求給錢包,讓使用者在錢包內確認交易後,把交易送到區塊鏈節點,等待交易被寫入區塊鏈。由於請求的格式都是 JSON RPC,所以節點也被稱為 RPC Node。

上面提到不管是讀取或寫入操作,都會依賴「區塊鏈節點」服務提供商,因此他們是區塊鏈應用中非常重要的角色。回顧我們之前提過的概念:

區塊鏈的本質其實就是一個帳本,紀錄著每個帳戶(也就是地址)上持有多少資產的資訊。比較特別的是這些資訊會被公開並備份到大量的電腦上(我們把它稱為區塊鏈的節點),透過密碼學的機制確保這個帳本是無法竄改的。

當我們想開發一個 DApp 時,如果還要自己架設區塊鏈節點,並且把所有區塊鏈的歷史資料全部同步下來,那勢必會花很高的儲存空間與網路頻寬成本,例如截至今天比特幣的歷史資料已超過 500GB,以太坊則超過 1000GB,而且每個節點要能即時跟其他節點同步資料。因此最簡單的作法是使用別人已經建好的節點服務,而上次介紹的 Alchemy 則是市面上最有名的節點服務提供商之一(另外還有像 InfuraQuicknode 等等),接下來會假設大家已經註冊 Alchemy 服務。另外對自建節點這個主題有興趣的話也可以參考 Ethereum 的 Run a node 教學

今日目標

前一天我們操作了測試鏈的 Uniswap,可以看到一進入 Uniswap 介面會有連結錢包的功能,連結上了之後介面會顯示當下錢包地址、連接的鏈、這個地址的餘額,以及點擊鏈的圖示可以切換不同的鏈。今天我們的目標是能把這些功能的雛形完成。

準備工作

首先到 Alchemy 的 Apps dashboard 建立一個新的 App,這樣才能拿到 API Key 做後續的操作。由於我們會在測試鏈上開發,Chain 跟 Network 就選擇 Ethereum Sepolia,名字隨意填就好

建立後點擊 View Keys 就可以看到這個 App 的 API Key 跟串接的方式,先紀錄 API Key 即可

另外也需要創一個新的前端專案,我個人是使用 pnpm create next-app 指令建立,讀者也可以選擇自己熟悉的套件管理器或 bundler, css 設定等等。

WAGMI

我們會使用 wagmi 這個套件來實作今天需要的功能。wagmi 提供完整的 hooks 可以用來跟錢包、Ethereum 互動,我們就不用自己用更底層的 ethers.jsviem 甚至 JSON-RPC 開始寫。安裝方式也很簡單:

pnpm i wagmi viem

而因為 wagmi 套件還蠻常改版,有時會造成套件不相容的問題(v1 也是最近才推出),現在我安裝的版本是 viem v1.9.0 以及 wagmi v1.3.10,如果未來看到的介面不同可能是這個原因。

另外有趣的一個小知識是 wagmi 是 We All Gonna Make It 的簡寫,主要是因為 NFT 流行的早期一群早期使用者會很常在 Discord, Twitter 等地方刷 WAGMI 很期待 NFT 項目的前景,就成為了一個 web3 的迷因。

連接與查看錢包餘額

安裝好之後就可以先貼上官方的範例程式碼來使用:

"use client";

---

import {
  WagmiConfig,
  createConfig,
  useAccount,
  useConnect,
  useDisconnect,
  mainnet,
} from "wagmi";
import { createPublicClient, http } from "viem";
import { InjectedConnector } from "wagmi/connectors/injected";const config = createConfig({
  autoConnect: true,
  publicClient: createPublicClient({
    chain: mainnet,
    transport: http(),
  }),
});function Profile() {
  const { address, isConnected } = useAccount();
  const { connect } = useConnect({
    connector: new InjectedConnector(),
  });
  const { disconnect } = useDisconnect();  if (isConnected)
    return (
      <div>
        <div>Connected to {address}</div>
        <button onClick={() => disconnect()}>Disconnect</button>
      </div>
    );
  return <button onClick={() => connect()}>Connect Wallet</button>;
}export default function App() {
  return (
    <WagmiConfig config={config}>
      <main className="flex min-h-screen flex-col items-center justify-between p-24">
        <Profile />
      </main>
    </WagmiConfig>
  );
}

使用 pnpm run dev 跑起來後就可以看到畫面上出現 Connect Wallet 的字,點擊後就會跳出 Metamask 的連接錢包視窗,同意後畫面上就會顯示錢包地址跟 Disconnect 按鈕了。

用法很簡單,使用 createPublicClientcreateConfig 來建立 wagmi config 後,用 <WagmiConfig> 把整個 App 包起來,就可以使用它提供的各種 hooks 了,包含 useAccount, useConnectuseDisconnect,分別對應到拿連接的錢包地址、Connect、Disconnect 的操作。另外可以看到 createPublicClient 中傳入的是 mainnet 代表我們指定要連接以太坊的主網,以及 public client 的意思是使用公開、任何人都可以打的 ETH 節點服務網址,而這種公開服務就會有 rate limit,因此後面我們會把 public client 改成使用 Alchemy 的服務

接下來我們加上顯示餘額的功能,只要在 Profile() 中使用 useBalance 即可:

// ...const balance = useBalance({ address });

---

if (isConnected)
return (
// ...
<div>Balance: {balance.data?.formatted}</div>
// ...

顯示與切換鏈

完成上述步驟後會看到 Balance 顯示為 0,這是因為我們的地址在以太坊上還沒有 ETH,而是在 Sepolia 鏈上有 ETH,因此接下來我們需要顯示已經連上的鏈跟我們的 DApp 總共支援哪些鏈,並讓使用者可以方便地切換。在這之前順便把 public provider 換成 alchemy provider 來避免後續的 rate limit。把前面宣告 config 的部分改成以下程式碼即可:

import { alchemyProvider } from "wagmi/providers/alchemy";
import { publicProvider } from "wagmi/providers/public";

---

const { chains, publicClient, webSocketPublicClient } = configureChains(
  [mainnet, sepolia],
  [
    alchemyProvider({ apiKey: process.env.NEXT_PUBLIC_ALCHEMY_KEY }),
    publicProvider(),
  ]
);const config = createConfig({
  autoConnect: true,
  publicClient: publicClient,
});

並且在 .env.local 檔(會被 git ignore 掉)加上剛才在 Alchemy 拿到的 API Key

NEXT_PUBLIC_ALCHEMY_KEY=key

可以看到前面改成用 configureChains 先指定這個 DApp 支援的鏈,以及要用哪些節點服務即可,在 wagmi 套件中是把節點服務稱為 provider。

再來是顯示鏈,從 configureChains 拿到的 chains 就是我們 DApp 支援的鏈,並使用 useNetworkuseSwitchNetwork 拿到當下連接的鏈跟切換鏈的 function

import {
useSwitchNetwork,
useNetwork,
} from "wagmi";
import {useSwitchNetwork,useNetwork,} from "wagmi";

---

// ...
const { chain } = useNetwork();
const { switchNetwork } = useSwitchNetwork();// ...
{chain && <div>Connected to {chain.name}</div>}
{chains.map((x) => (
  <div key={x.id}>
    <button
      disabled={!switchNetwork || x.id === chain?.id}
      onClick={() => switchNetwork?.(x.id)}
    >
      {x.name} {x.id === chain?.id && "(current)"}
    </button>
  </div>
))}

這樣就能顯示所有 DApp 支援的鏈以及點擊觸發切換鏈的功能了!

另外如果使用者在跳出錢包切換鏈的請求時拒絕,在 useSwitchNetwork 裡也有 error 可以用來顯示拒絕的錯誤訊息,以及 isLoading 代表是否正在切換網路等等。

小結

今天我們實作了一些基本的 DApp 功能,包含連接錢包、顯示地址與餘額、切換鏈等功能。詳細的程式碼會放在這裡,在後續的內容如果有程式碼我也會盡量放到同個 repo 中。接下來我們會持續加入新的功能到 DApp 中。