ウォレットとの接続を簡単にしてくれるライブラリwagmiの使い方

2023/01/24に公開

2024/1/7追記

wagmi v2がリリースされ多くの破壊的変更が入ったため、記事内のコードは動作しなくなっています。
ただし、基本的な思想は変わっていません。
マイグレーション方法は以下にあります。
https://wagmi.sh/react/guides/migrate-from-v1-to-v2

また、v1のドキュメントは以下からまだ見ることが出来ます。
https://1.x.wagmi.sh/

前置き

ウォレットとの接続を簡単にしてくれるライブラリを色々試した結果、wagmiという以下のライブラリが使いやすかったので、簡単な使い方と詰まった点のメモです
https://wagmi.sh/

以下の公式ドキュメントをベースに記載します
https://wagmi.sh/react/getting-started

Get Started

インストール

npm i wagmi ethers // またはyarn add, pnpm i

チェーンの設定

import { configureChains, mainnet } from 'wagmi'
import { publicProvider } from 'wagmi/providers/public'

const { chains, provider, webSocketProvider } = configureChains(
 [mainnet],
 [publicProvider()],
)

ここではmainnetを指定していますが、goerliなどのテストネットももちろんあります。
また、複数指定することもできます。

wagmi clientの作成

import { WagmiConfig, createClient, configureChains, mainnet } from 'wagmi'
import { publicProvider } from 'wagmi/providers/public'

const { chains, provider, webSocketProvider } = configureChains(
 [mainnet],
 [publicProvider()],
)

const client = createClient({
 autoConnect: true,
 provider,
 webSocketProvider,
})

autoConnectはページを開いた際、前回接続していたウォレットに接続するかどうかという項目です。
defaultはfalseのようですが、便利なのでtrueが良さそうです。

ラップ

const client = createClient({
  autoConnect: true,
  provider,
  webSocketProvider,
})
 
function App() {
  return (
    <WagmiConfig client={client}>
      <YourRoutes />
    </WagmiConfig>
  )
}

基本的な使い方

import { useAccount, useConnect, useEnsName } from 'wagmi'
import { InjectedConnector } from 'wagmi/connectors/injected'

function Profile() {
 const { address, isConnected } = useAccount()
 const { data: ensName } = useEnsName({ address })
 const { connect } = useConnect({
 connector: new InjectedConnector(),
 })

 if (isConnected) return <div>Connected to {ensName ?? address}</div>
 return <button onClick={() => connect()}>Connect Wallet</button>
}

useAccountuseConnectのように様々なhookがあり、それらを使い分けていく感じです。

ウォレットとのやりとり

チェーン切り替え

import { useNetwork, useSwitchNetwork } from 'wagmi'

function App() {
 const { chain } = useNetwork()
 const { chains, error, isLoading, pendingChainId, switchNetwork } =
 useSwitchNetwork()

 return (
 <>
 {chain && <div>Connected to {chain.name}</div>}

 {chains.map((x) => (
 <button
 disabled={!switchNetwork || x.id === chain?.id}
 key={x.id}
 onClick={() => switchNetwork?.(x.id)}
 >
 {x.name}
 {isLoading && pendingChainId === x.id && ' (switching)'}
 </button>
 ))}

 <div>{error && error.message}</div>
 </>
 )
}

useSwitchNetworkを使用することで簡単に実装ができます。
useNetworkで現在接続しているチェーンを取得できるので、正しいチェーンに接続している時のみSwitchChainボタンを表示もできます。

コントラクトとのやりとり

読み取り

useContractReadを使用して、コントラクト内のview関数を呼び出せます

import { useContractRead } from 'wagmi'

function App() {
 const { data, isError, isLoading } = useContractRead({
 address: '0xecb504d39723b0be0e3a9aa33d646642d1051ee1',
 abi: wagmigotchiABI,
 functionName: 'getHunger',
 })
}

useContractReadsを使えば、一度に複数のview関数を叩くこともできます

function App() {
  const { data, isError, isLoading } = useContractReads({
    contracts: [
      {
        ...wagmigotchiContract,
        functionName: 'getAlive',
      },
      {
        ...wagmigotchiContract,
        functionName: 'getBoredom',
      },
      {
        ...mlootContract,
        functionName: 'getChest',
        args: [69],
      },
      {
        ...mlootContract,
        functionName: 'getWaist',
        args: [69],
      },
    ],
  })
}

書き込み

読み込みと異なり、usePrepareContractWriteuseContractWriteという2つのhookを使う必要があります。

import { useContractWrite, usePrepareContractWrite } from 'wagmi'

function App() {
 const { config } = usePrepareContractWrite({
 address: '0xecb504d39723b0be0e3a9aa33d646642d1051ee1',
 abi: wagmigotchiABI,
 functionName: 'feed',
 })
 const { data, isLoading, isSuccess, write } = useContractWrite(config)

 return (
 <div>
 <button disabled={!write} onClick={() => write?.()}>
 Feed
 </button>
 {isLoading && <div>Check Wallet</div>}
 {isSuccess && <div>Transaction: {JSON.stringify(data)}</div>}
 </div>
 )
}

読み取り&書き込みtips

useContractReadusePrepareContractWrite内の値は三項演算子で書くこともできます。

function App() {
 const { config } = usePrepareContractWrite({
 address: '0xecb504d39723b0be0e3a9aa33d646642d1051ee1',
 abi: wagmigotchiABI,
 functionName: flag ? 'add' : 'remove',
 args: flag ? [amount, duration] : [amount]  
 })
 const { data, isLoading, isSuccess, write } = useContractWrite(config)
}

自分は最初にusePrepareContractWriteを見た時、「関数分configを生成しなきゃいけないの?」と思いましたが、これならいい感じに減らせそうです。
また、mintConfig, transferConfigのようにどんどんconfigが増えていくケースもありそうですが、その場合はcomponentを分けるべきなのかなと感じました。

トランザクションの完了を待つ

const { isLoading } = useWaitForTransaction()

イベントの受け取り

import { useContractEvent } from 'wagmi'

function App() {
 useContractEvent({
 address: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
 abi: ensRegistryABI,
 eventName: 'NewOwner',
 listener(node, label, owner) {
 console.log(node, label, owner)
 },
 })
}

使用されているプロダクトの実例

有名どころだとFoundation, ENS、SushiSwapで使われているとのことです。
今回はSushiSwapを見ていきます。
https://github.com/sushiswap/sushiswap

全体

接続しているウォレットアカウントを取得できるuseAccountはかなり多用されています(当然ですが)

SushiBarSectionDesktop.tsx

stakeをする関数とやめる関数の部分にusePrepareContractWrite, useContractWriteが使われていました。三項演算子で書けてスッキリしています。

const { config } = usePrepareContractWrite({
    ...getSushiBarContractConfig(ChainId.ETHEREUM),
    functionName: stake ? 'enter' : 'leave',
    args: amount ? [BigNumber.from(amount.quotient.toString())] : undefined,
    enabled: !!amount?.quotient,
  })

  const { write, isLoading: isWritePending } = useContractWrite({
    ...config,
    onSettled,
  })

ただし、全体を見るとwagmiを使ってコントラクトコールしているのは数カ所だけでした。
わざわざ統一はしてないだけなのか、何か意図があるのかは不明です。。

useERC20Allowance.ts、usePairs.ts等

ERC20のallowanceを取得する処理などにuseContractReadが使われていました。
wagmiではERC20のABIが提供されているので実装しやすいです。

全体的にread系はuseXXX.tsを作成し、その中でwagmiをラップしてありました。
wagmiの仕様変更があった際に備えているのかなと思います。

export function useERC20Allowance(
  watch: boolean,
  token?: Token,
  owner?: string,
  spender?: string
): UseERC20AllowanceReturn {
  const args = useMemo(() => [owner, spender] as [Address, Address], [owner, spender])
  const data = useContractRead({
    address: token?.address,
    abi: erc20ABI,
    functionName: 'allowance',
    args,
    watch,
    enabled: !!token,
  })

  const amount = data?.data && token ? Amount.fromRawAmount(token, data.data.toString()) : undefined
  return useMemo(
    () => ({
      ...data,
      data: amount,
    }),
    [amount, data]
  )
}

Discussion