ウォレットとの接続を簡単にしてくれるライブラリwagmiの使い方
2024/1/7追記
wagmi v2がリリースされ多くの破壊的変更が入ったため、記事内のコードは動作しなくなっています。
ただし、基本的な思想は変わっていません。
マイグレーション方法は以下にあります。
また、v1のドキュメントは以下からまだ見ることが出来ます。
前置き
ウォレットとの接続を簡単にしてくれるライブラリを色々試した結果、wagmiという以下のライブラリが使いやすかったので、簡単な使い方と詰まった点のメモです
以下の公式ドキュメントをベースに記載します
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>
}
useAccount
やuseConnect
のように様々な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],
},
],
})
}
書き込み
読み込みと異なり、usePrepareContractWrite
とuseContractWrite
という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
useContractRead
やusePrepareContractWrite
内の値は三項演算子で書くこともできます。
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を見ていきます。
全体
接続しているウォレットアカウントを取得できる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