👾

Web3フロントエンドTips

2023/04/07に公開
2

この記事は昔に書いた以下の記事の和訳&アップデートになります。
https://dev.to/yujiym/web-30-frontend-stacks-in-2023-1i03

1.5年くらいDeFi(EVM)のフロントエンドを書いてきて、おすすめのスタック・ライブラリについてとWeb3ならではのTipsや工夫点についてまとめました。他に何かおすすめがあったり、質問があれば教えてください!

1. システム構成

Image description

2. スタック・ライブラリ

フォルダ構成 : Turborepo

monorepo (multiple apps in one repo) 構成を使うことが多いです。
コードのほとんどは packages 配下の共通フォルダに置き、各アプリにはルーティングのための最小限のページコンポーネント構成とします。

以下のようなケースでアプリの拡張が柔軟に対応できます

  • Chain毎に異なるページや関数を持つdApps
  • 機能を限定したLite版を別途用意するケース
  • ミドルウェア、APIやツールを切り出すケース
ファイル構成
.
├── apps
│   ├── dapp
│   │   └── pages (or app)  # each app has only pages dir
│   ├── sub-dapp
│   └── ...
├── packages
│   ├── assets
│   │   ├── img
│   │   └── styles
│   ├── config
│   ├── lib
│   │   ├── constants
│   │   │   └── abis  # abi json
│   │   ├── queries  # thegraph queries
│   │   ├── store  # state
│   │   ├── types
│   │   └── utils
│   ├── react-lib
│   │   ├── components  # components
│   │   └── hooks  # hooks
│   └── tsconfig
└── test

Frontend Framework & ホスティング : Next.js + TypeScript + Vercel

Web3向けのライブラリが充実しているので、Reactが無難な選択肢です。
Next.jsをTurborepoでデプロイする場合、Vercelが最も簡単なのですが、Vercelの便利な機能(Edge関数、ISR...)を使ってしまうと、ベンダーロックされてしまい、後々IPFSにデプロイしてdAppを完全分散化するケースで移行が難しくなってしまいます。
Viteesbuildも小規模なアプリケーションやツールには適していると思います。
最近はServerless frameworkを使ってもっと小さな構成でアプリを作れないか考えています。

ethereum接続 : wagmi + ethers.js

wagmiethers.jsをwrapしたReact Hooksですでした。類似ライブラリは多いですが、機能も多く、テストも充実しているのでおすすめです。 -> wagmiの他のライブラリとの比較
また、wagmiはEthereum ABIのTypeScript型を提供していて、これはzodスキーマとも連携して動作します。これにより、dAppsの型チェックがより厳密にできるようになります。-> ABIType
今はethers.jsの代替としてtypescript nativeで軽量で、ヒューマンリーダブルなエラーを返す(これ一番大事)、viemを作っています。次のversionでethers.js依存はなくなります。

書込み系処理の例

react-lib/hooks/useApprove.ts
import { usePrepareContractWrite, useContractWrite, useWaitForTransaction, erc20ABI } from 'wagmi'
....
export default function useApprove(targetAddress: AddressType, owner: AddressType, spender: AddressType) {
  ....
  const args: [AddressType, BigNumber] = useDebounce([spender, BigNumber.from(amount)])
  const enabled: boolean = useDebounce(!!targetAddress && !!owner && !!spender)

  const prepareFn = usePrepareContractWrite({
    address: targetAddress,
    abi: erc20ABI,
    functionName: 'approve',
    args,
    enabled
  })

  const writeFn = useContractWrite({
    ...prepare.config,
    onSuccess(data) {
      ....
    }
  })

  const waitFn = useWaitForTransaction({
    chainId,
    hash: write.data?.hash,
    wait: write.data?.wait
  })

  return {
    prepareFn,
    writeFn,
    waitFn
  }
}
SomeComponent.tsx
import useApprove from 'react-lib/hooks/contracts/useApprove'
....

export default function SomeComponent() {
  const { prepareFn, writeFn, waitFn } = useApprove(targetAddress, account, spenderAddress)

  // handle view with each hook's return value -> https://wagmi.sh/react/hooks/useContractWrite#return-value
  return (
    <div>
      {approve.prepare.isSuccess &&
        <button
          onClick={() => writeFn?.write}
          disabled={writeFn.isLoading || prepareFn.isError}
        >
          Approve
        </button>
      )}
      {writeFn.isLoading && <p>Loading...</p>}
      {writeFn.isSuccess && <p>Tx submitted!</p>}
      {waitFn.isSuccess && <p>Approved!</p>}
    </div>
  )
}

インデックス、集計処理 : The Graph + urql

The Graph is an indexing protocol for querying networks like Ethereum and IPFS. Anyone can build and publish open APIs, called subgraphs, making data easily accessible.

Contract Callは遅く、1度に1つしか呼び出せないため、複数のコントラクトコール結果をインデックスしたり、集計値を計算するためのレイヤーが必要になってきます。(例:liquidity一覧をtableで取得する)
集計とインデックス処理は、Contractのeventをトリガーとするsubgraphでyml形式で指定でき、その結果をthegraphのdbに保存します。
クライアント側では、graphqlでデータを問い合わせることができます。urqlは、軽量なgraphqlクライアントです。

ハマりポイントとしては、

  • 同block内で実行順序が保証されないようなケースへ対応しておかないと、エラーになる
  • localhost環境で開発・debugするには、Archive Nodeが必要(過去時点のstateに対してcontracl callを実行するため)、HardHat環境ではMock等をかかなければいけない

subsquidが最近EVM Chainのサポートを追加したので、次はこれを試してみたいと思っています。

// define query
import { gql } from 'urql'

export const GET_MY_LIQUIDITIES = gql`
  query getMyLiquidities($account: String) {
    ${FRAGMENT}
    user(id: $account) {
      id
      liquidities {
        id
        protocols {
          ...Fragment
        }
      }
    }
  }
`
SomeComponent.tsx
// call in component
import { useQuery } from 'urql'
....
export function SomeComponent() {
  ....
  const [{ data, fetching }] = useQuery({
    query: GET_MY_LIQUIDITIES,
    variables: {
      account: account.toLowerCase()
    },
    pause: !account
  })
  ....
}

UI, CSS : tailwindcss + PostCSS + Radix UI + UI components by shadcn

シンプルで&カスタマイズ可能なFrameworkが好みです。
Radix UIはHeadless UIよりも機能が豊富ですが、スタイルを追加するのは難しいです。なので、shadcnによるコンポーネントはほど良い塩梅だと思います。

私はこのCSS variables with Tailwind CSS setupを使っています。

State管理 : jotai

jotaiは、シンプルで軽量な状態管理ライブラリです。useState + ContextAPI のようなシンプルな構成ですが、余計な再レンダリングを防いでくれたり、多くのユーティリティがあります。
類似ライブラリとして、recoil, zustand, valtioがあるので、好きなものを選ぶのが良いと思います。

// packages/store/index.ts
import { atom } from "jotai";

type SessionType = {
  chainId?: number;
  account?: string;
};

export const sessionsAtom = atom<SessionType>({});
// in component or hook
import { sessionAtom } from 'lib/store'
import { useAtom } from 'jotai'

....
const [session, setSession] = useAtom(sessionAtom)

....
setSession({ chainId: res.chainId, account: res.address })

フォームライブラリ:react-hook-form & zod

前述したwagmi、typescriptと親和性の高いzodでスキーマ管理をして、react-hook-formを使うことが多いです。

3. Web3 frontend tips

トランザクションを非同期で処理する

BlockChainのレスポンス(特にMainnet)は、Web2のそれのレスポンスよりかなり遅いです。
ユーザーがトランザクションを送信した後、ローディング表示やトランザクションのステータスの表示などのハンドリングが必要です。

ユーザーのトランザクションがconfirmされたにもかかわらず、thegraphのデータ更新が遅れるため、ユーザーの操作がブラウザに反映されないケースが発生します。このケースは、最初のレンダリングにはthegraphのデータを使用し、後でcontract callの結果でその値を上書きするようにして対応しています。

また、トランザクションがconfirmする前にユーザーがページやサイトを離れてしまった場合に対応するため、ユーザーの未完了のトランザクションは一旦localStorageで永続化して、確定するまで保持するようにしています。

// watching uncompleted transaction
import { useEffect } from 'react'
import { useBlockNumber, useProvider } from 'wagmi'
....

export default function useTxHandler() {
  const { chainId, account } = useWallet()
  const transactions = useTransactions() // get user's transactions from state
  const blockNumber = useBlockNumber({
    chainId,
    scopeKey: 'useTxHandler',
    watch: true
  })
  const provider = useProvider<any>()

  useEffect(() => {
    transactions.map(async (tx: Transaction) => {
      await provider
        .getTransactionReceipt(tx.hash)
        .then(async (transactionReceipt: any) => {
          if (transactionReceipt) {
            .... // update user's transaction status here
            })
          }
        })
        .catch((e: any) => {
          log(`failed to check transaction hash: ${tx.hash}`, e)
        })
    })
  }, [account, chainId, PROVIDER_URL, transactions, blockNumber.data])
}

BigNumberの取り扱いについて

ERC20には decimals フィールドがあり桁を意識して扱わなければなりません。また、基本Int型の扱える桁数を超えるため、BigNumberを扱うためのライブラリで処理する必要があり、いくつかライブラリが林立していました。(しかもサイズ意外と大きい)
ethers.js v6から、ES2020ビルトインのBigIntが採用されました。(wagmiのviemもBigInt)これを使っていきましょう。 -> Migrating from v5

Uniswap tokenlist format

This package includes a JSON schema for token lists, and TypeScript utilities for working with token lists.

OnChainにあるTokenの情報は限られており、OffChainのどこかにリスト形式で保持する必要があります。Uniswapが公開しているこの形式に従っておくと、取り扱いが便利です。

4. 気になっていること

Permit2

Permit2 introduces a low-overhead, next generation token approval/meta-tx system to make token approvals easier, more secure, and more consistent across applications.

各dAppの各コントラクトを承認する代わりに、一度Permit2を承認すれば、ERC20 ContractへのApproveをコントロールできるようになります。これにより、UX(walletの毎回のapproveがなくなる!)とセキュリティ(各Dappのコントラクトに対して大量のallowanceが残っていることによる問題)を改善することが期待できます。早くデファクトになってほしい。

Lit Protocol

Lit is distributed key management for encryption, signing, and compute.

Mina

Mina is building the privacy and security layer for web3 with zero knowledge proofs.

5. 所感

  • 1.5年前は雑なフロントエンド/ライブラリが多かったけど、ものすごい勢いでクオリティ高くなってきてる。動きも早い(2ヶ月前に書いた記事の和訳に伴う修正ですら結構あった)
  • 最近だとERC-4337によるAA実装や、ZKP関係で追えないくらいの新情報が流れてくる
  • UXをつきつめていくと、キモの台帳部分のセキュリティ以外については別の手段で代替するのが良いと思うようになった。OffChainで処理したり、Modular Blockchain (Lit Protocol, Mina Protocol, Ceramic Network)と組み合わせたり。そうなると、Web5やのNostrのやろうとしていることも筋は遠っているように思う。
  • ようやくDeFi以外のキラーユースケースが出てきそうな雰囲気があり楽しみ
GitHubで編集を提案

Discussion

HarukiHaruki

めちゃめちゃ参考になりました!モノレポ生成用のフレームワークとかあったんですね・・。yarn init から頑張って作ってました!