コントラクトのユーザーインターフェースを構築する
Ethereumエコシステムに必要な機能を見つけました。 それを実装するためにスマートコントラクトを作成し、オフチェーンで実行される関連コードもいくつか作成したかもしれません。 素晴らしいことです! 残念ながら、ユーザーインターフェースがなければユーザーはつきませんし、前回ウェブサイトを作成したときは、人々はダイヤルアップモデムを使い、JavaScriptはまだ新しいものでした。
この記事は、そんなあなたのためのものです。 プログラミングの知識があり、JavaScriptやHTMLについても少しは知っているけれど、ユーザーインターフェースのスキルは錆びついて時代遅れだと想定しています。 一緒にシンプルな最新のアプリケーションをレビューし、最近ではどのように行われているかを見ていきましょう。
なぜこれが重要なのか
Etherscan (opens in a new tab)やBlockscout (opens in a new tab)を使って、人々にコントラクトを操作してもらうことも理論上は可能です。 経験豊富なEthereanにとっては、それで十分でしょう。 しかし、私たちはさらに10億人の人々 (opens in a new tab)にサービスを提供しようとしています。 これは優れたユーザーエクスペリエンスなしには実現できません。そして、フレンドリーなユーザーインターフェースはその大きな部分を占めています。
Greeterアプリケーション
モダンなUIの仕組みの背景には多くの理論があり、それを説明する (opens in a new tab)優れたサイトもたくさんあります (opens in a new tab)。 これらのサイトで行われている素晴らしい作業を繰り返す代わりに、実際に触って学べるアプリケーションから始めることを好むと仮定します。 物事を進めるにはまだ理論が必要で、それにも触れます。ただ、ソースファイルごとに進め、それらに到達するたびに物事を議論していきます。
インストール
-
必要に応じて、Holeskyブロックチェーン (opens in a new tab)をウォレットに追加し、テストETHを入手 (opens in a new tab)してください。
-
GitHubリポジトリをクローンします。
git clone https://github.com/qbzzt/20230801-modern-ui.git -
必要なパッケージをインストールします。
cd 20230801-modern-ui pnpm install -
アプリケーションを起動します。
pnpm dev -
アプリケーションによって表示されたURLに移動します。 ほとんどの場合、それはhttp://localhost:5173/ (opens in a new tab)です。
-
HardhatのGreeterを少し修正したコントラクトのソースコードを、ブロックチェーンエクスプローラー (opens in a new tab)で確認できます。
ファイルのウォークスルー
index.html
このファイルは、スクリプトファイルをインポートするこの行を除いて、標準的なHTMLの定型文です。
<script type="module" src="/src/main.tsx"></script>
src/main.tsx
このファイル拡張子は、このファイルがTypeScript (opens in a new tab)で書かれたReactコンポーネント (opens in a new tab)であることを示しています。TypeScriptは、型チェック (opens in a new tab)をサポートするJavaScriptの拡張機能です。 TypeScriptはJavaScriptにコンパイルされるため、クライアントサイドでの実行に使用できます。
import '@rainbow-me/rainbowkit/styles.css'
import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
import * as React from 'react'
import * as ReactDOM from 'react-dom/client'
import { WagmiConfig } from 'wagmi'
import { chains, config } from './wagmi'
必要なライブラリコードをインポートします。
import { App } from './App'
アプリケーションを実装するReactコンポーネントをインポートします(下記参照)。
ReactDOM.createRoot(document.getElementById('root')!).render(
ルートのReactコンポーネントを作成します。 renderのパラメータはJSX (opens in a new tab)です。これはHTMLとJavaScript/TypeScriptの両方を使用する拡張言語です。 ここでの感嘆符は、TypeScriptコンポーネントに「document.getElementById('root')がReactDOM.createRootの有効なパラメータになるかどうかはわからないでしょうが、心配しないでください。私は開発者であり、そうなることを保証します」と伝えています。
<React.StrictMode>
アプリケーションはReact.StrictModeコンポーネント (opens in a new tab)の中に入ります。 このコンポーネントは、Reactライブラリに追加のデバッグチェックを挿入するように指示します。これは開発中に役立ちます。
<WagmiConfig config={config}>
アプリケーションはWagmiConfigコンポーネント (opens in a new tab)の中にもあります。 wagmi (we are going to make it) ライブラリ (opens in a new tab)は、ReactのUI定義をviemライブラリ (opens in a new tab)に接続し、イーサリアムの分散型アプリケーションを作成します。
<RainbowKitProvider chains={chains}>
そして最後に、RainbowKitProviderコンポーネント (opens in a new tab)です。 このコンポーネントは、ログインとウォレットとアプリケーション間の通信を処理します。
<App />
これで、実際にUIを実装するアプリケーション用のコンポーネントを持つことができます。 コンポーネントの最後にある/>は、XML標準に従い、このコンポーネント内に定義がないことをReactに伝えます。
</RainbowKitProvider>
</WagmiConfig>
</React.StrictMode>,
)
もちろん、他のコンポーネントも閉じる必要があります。
src/App.tsx
import { ConnectButton } from '@rainbow-me/rainbowkit'
import { useAccount } from 'wagmi'
import { Greeter } from './components/Greeter'
export function App() {
これはReactコンポーネントを作成する標準的な方法です。レンダリングが必要になるたびに呼び出される関数を定義します。 この関数は通常、上部にTypeScriptまたはJavaScriptコードがあり、その後にJSXコードを返すreturnステートメントが続きます。
const { isConnected } = useAccount()
ここでuseAccount (opens in a new tab)を使用して、ウォレットを介してブロックチェーンに接続されているかどうかを確認します。
慣例として、Reactではuse...という名前の関数は、何らかのデータを返すフック (opens in a new tab)です。 このようなフックを使用すると、コンポーネントがデータを取得するだけでなく、そのデータが変更されると、コンポーネントは更新された情報で再レンダリングされます。
return (
<>
ReactコンポーネントのJSXは、1つのコンポーネントを返す_必要_があります。 複数のコンポーネントがあり、「自然に」まとめるものがない場合は、空のコンポーネント(<> ... </>)を使用して、それらを1つのコンポーネントにまとめます。
<h1>Greeter</h1>
<ConnectButton />
ConnectButtonコンポーネント (opens in a new tab)はRainbowKitから取得します。 接続されていない場合は、Connect Walletボタンが表示され、ウォレットについて説明し、使用するウォレットを選択できるモーダルが開きます。 接続されると、使用しているブロックチェーン、アカウントアドレス、ETH残高が表示されます。 これらの表示を使用して、ネットワークを切り替えたり、切断したりできます。
{isConnected && (
実際のJavaScript(またはJavaScriptにコンパイルされるTypeScript)をJSXに挿入する必要がある場合は、括弧({})を使用します。
a && bという構文は、a ?の短縮形です。 b : a (opens in a new tab)。 つまり、aが真の場合、bに評価され、それ以外の場合はa(false、0など)に評価されます。 これは、特定の条件が満たされた場合にのみコンポーネントを表示するようにReactに指示する簡単な方法です。
この場合、ユーザーがブロックチェーンに接続されている場合にのみ、ユーザーにGreeterを表示したいと考えています。
<Greeter />
)}
</>
)
}
src/components/Greeter.tsx
このファイルには、UI機能のほとんどが含まれています。 これには通常、複数のファイルに含まれる定義が含まれていますが、これはチュートリアルであるため、プログラムはパフォーマンスやメンテナンスの容易さよりも、初回で理解しやすいように最適化されています。
import { useState, ChangeEventHandler } from 'react'
import { useNetwork,
useReadContract,
usePrepareContractWrite,
useContractWrite,
useContractEvent
} from 'wagmi'
これらのライブラリ関数を使用します。 繰り返しになりますが、これらは使用される場所で以下に説明されています。
import { AddressType } from 'abitype'
abitypeライブラリ (opens in a new tab)は、AddressType (opens in a new tab)など、さまざまなイーサリアムのデータ型に対するTypeScriptの定義を提供します。
let greeterABI = [
.
.
.
] as const // greeterABI
GreeterコントラクトのABIです。
コントラクトとUIを同時に開発している場合、通常はそれらを同じリポジトリに配置し、Solidityコンパイラによって生成されたABIをアプリケーション内のファイルとして使用します。 ただし、コントラクトはすでに開発済みで変更されないため、ここではその必要はありません。
type AddressPerBlockchainType = {
[key: number]: AddressType
}
TypeScriptは静的型付け言語です。 この定義を使用して、Greeterコントラクトが異なるチェーンにデプロイされているアドレスを指定します。 キーは数値(chainId)で、値はAddressType(アドレス)です。
const contractAddrs: AddressPerBlockchainType = {
// Holesky
17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8',
// Sepolia
11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0'
}
サポートされている2つのネットワーク、Holesky (opens in a new tab)とSepolia (opens in a new tab)上のコントラクトのアドレスです。
注意: 実際にはRedstone Holesky用に3番目の定義がありますが、これについては後ほど説明します。
type ShowObjectAttrsType = {
name: string,
object: any
}
この型は、ShowObjectコンポーネント(後述)へのパラメータとして使用されます。 これには、デバッグ目的で表示されるオブジェクトの名前とその値が含まれます。
type ShowGreetingAttrsType = {
greeting: string | undefined
}
どの時点でも、あいさつの内容を知っているか(ブロックチェーンから読み取ったため)、知らないか(まだ受信していないため)のどちらかです。 したがって、文字列または何もないかのどちらかになりうる型があると便利です。
Greeterコンポーネント
const Greeter = () => {
最後に、コンポーネントを定義します。
const { chain } = useNetwork()
使用しているチェーンに関する情報は、wagmi (opens in a new tab)から提供されます。
これはフック(use...)なので、この情報が変更されるたびにコンポーネントが再描画されます。
const greeterAddr = chain && contractAddrs[chain.id]
Greeterコントラクトのアドレスはチェーンによって異なり、チェーン情報がない場合や、そのコントラクトが存在しないチェーン上にある場合はundefinedになります。
const readResults = useReadContract({
address: greeterAddr,
abi: greeterABI,
functionName: "greet" , // No arguments
watch: true
})
useReadContractフック (opens in a new tab)は、コントラクトから情報を読み取ります。 UIでreadResultsを展開すると、それが返す情報を正確に確認できます。 この場合、あいさつが変更されたときに通知されるように、監視を続けたいと考えています。
注意: setGreetingイベント (opens in a new tab)をリッスンして、あいさつが変更されたことを知り、そのように更新することもできます。 ただし、より効率的かもしれませんが、すべての場合に適用できるわけではありません。 ユーザーが異なるチェーンに切り替えると、あいさつも変更されますが、その変更にはイベントが伴いません。 イベントをリッスンするコード部分と、チェーンの変更を識別する部分を分けることもできますが、それは単にwatchパラメータ (opens in a new tab)を設定するよりも複雑になります。
const [ newGreeting, setNewGreeting ] = useState("")
ReactのuseStateフック (opens in a new tab)を使用すると、コンポーネントのレンダリング間で値が持続する状態変数を指定できます。 初期値はパラメータであり、この場合は空の文字列です。
useStateフックは、2つの値を持つリストを返します。
- 状態変数の現在の値。
- 必要に応じて状態変数を変更する関数。 これはフックなので、呼び出されるたびにコンポーネントが再レンダリングされます。
この場合、ユーザーが設定したい新しいあいさつのために状態変数を使用しています。
const greetingChange : ChangeEventHandler<HTMLInputElement> = (evt) =>
setNewGreeting(evt.target.value)
これは、新しいあいさつ入力フィールドが変更されたときのイベントハンドラです。 型ChangeEventHandler<HTMLInputElement> (opens in a new tab)は、これがHTML入力要素の値の変更に対するハンドラであることを指定します。 この<HTMLInputElement>の部分は、これがジェネリック型 (opens in a new tab)であるために使用されます。
const preparedTx = usePrepareContractWrite({
address: greeterAddr,
abi: greeterABI,
functionName: 'setGreeting',
args: [ newGreeting ]
})
const workingTx = useContractWrite(preparedTx.config)
これは、クライアント側からブロックチェーントランザクションを送信するプロセスです。
eth_estimateGas(opens in a new tab)を使用して、ブロックチェーン上のノードにトランザクションを送信します。- ノードからの応答を待ちます。
- 応答が受信されたら、ウォレットを介してユーザーにトランザクションへの署名を求めます。 このステップは、ノードの応答が受信された後に行う_必要_があります。なぜなら、ユーザーは署名する前にトランザクションのガス代を表示されるからです。
- ユーザーが承認するのを待ちます。
- 今度は
eth_sendRawTransaction(opens in a new tab)を使用して、トランザクションを再度送信します。
ステップ2は、体感できるほどの時間がかかる可能性があり、その間、ユーザーは自分のコマンドがユーザーインターフェースで本当に受信されたのか、なぜまだトランザクションへの署名を求められないのか、疑問に思うでしょう。 それは、悪いユーザーエクスペリエンス(UX)につながります。
解決策は、prepareフック (opens in a new tab)を使用することです。 パラメータが変更されるたびに、すぐにノードにeth_estimateGasリクエストを送信します。 そして、ユーザーが実際にトランザクションを送信したいとき(この場合はあいさつの更新を押す)、ガス代が既知であるため、ユーザーはすぐにウォレットページを見ることができます。
return (
これで、ついに返す実際のHTMLを作成できます。
<>
<h2>Greeter</h2>
{
!readResults.isError && !readResults.isLoading &&
<ShowGreeting greeting={readResults.data} />
}
<hr />
ShowGreetingコンポーネント(後述)を作成しますが、あいさつがブロックチェーンから正常に読み取られた場合に限ります。
<input type="text"
value={newGreeting}
onChange={greetingChange}
/>
これは、ユーザーが新しいあいさつを設定できる入力テキストフィールドです。 ユーザーがキーを押すたびに、greetingChangeが呼び出され、それがsetNewGreetingを呼び出します。 setNewGreetingはuseStateフックから来ているので、これによりGreeterコンポーネントが再びレンダリングされます。 このことは、次のことを意味します。
- 新しいあいさつの値を保持するために
valueを指定する必要があります。そうしないと、デフォルトの空の文字列に戻ってしまいます。 usePrepareContractWriteはnewGreetingが変更されるたびに呼び出されるため、準備されたトランザクションには常に最新のnewGreetingが含まれます。
<button disabled={!workingTx.write}
onClick={workingTx.write}
>
あいさつの更新
</button>
workingTx.writeがない場合は、あいさつの更新を送信するために必要な情報をまだ待っているため、ボタンは無効になります。 workingTx.writeの値がある場合、それがトランザクションを送信するために呼び出す関数です。
<hr />
<ShowObject name="readResults" object={readResults} />
<ShowObject name="preparedTx" object={preparedTx} />
<ShowObject name="workingTx" object={workingTx} />
</>
)
}
最後に、何をしているかを確認しやすくするために、使用する3つのオブジェクトを表示します。
readResultspreparedTxworkingTx
ShowGreetingコンポーネント
このコンポーネントは、
const ShowGreeting = (attrs : ShowGreetingAttrsType) => {
コンポーネント関数は、コンポーネントのすべての属性を持つパラメータを受け取ります。
return <b>{attrs.greeting}</b>
}
ShowObjectコンポーネント
情報提供を目的として、ShowObjectコンポーネントを使用して、重要なオブジェクト(あいさつを読み取るためのreadResultsと、作成するトランザクションのためのpreparedTxおよびworkingTx)を表示します。
const ShowObject = (attrs: ShowObjectAttrsType ) => {
const keys = Object.keys(attrs.object)
const funs = keys.filter(k => typeof attrs.object[k] == "function")
return <>
<details>
すべての情報でUIが乱雑にならないように、表示したり閉じたりできるようにするために、details (opens in a new tab)タグを使用します。
<summary>{attrs.name}</summary>
<pre>
{JSON.stringify(attrs.object, null, 2)}
ほとんどのフィールドは、JSON.stringify (opens in a new tab)を使用して表示されます。
</pre>
{ funs.length > 0 &&
<>
Functions:
<ul>
例外は関数で、JSON標準 (opens in a new tab)の一部ではないため、別々に表示する必要があります。
{funs.map((f, i) =>
JSX内では、{中括弧}内のコードはJavaScriptとして解釈されます。 そして、(丸括弧)内のコードは、再びJSXとして解釈されます。
(<li key={i}>{f}</li>)
)}
Reactは、DOMツリー (opens in a new tab)内のタグに一意の識別子を必要とします。 これは、同じタグの子(この場合は、順序なしリスト (opens in a new tab))が、異なるkey属性を必要とすることを意味します。
</ul>
</>
}
</details>
</>
}
様々なHTMLタグを閉じます。
最後のexport
export { Greeter }
Greeterコンポーネントは、アプリケーションにエクスポートする必要があるものです。
src/wagmi.ts
最後に、WAGMIに関連するさまざまな定義がsrc/wagmi.tsにあります。 ほとんどが変更する必要のない定型文であるため、ここではすべてを説明するつもりはありません。
ここでのコードは、記事の後半で別のチェーン(Redstone Holesky (opens in a new tab))を追加するため、GitHub上のもの (opens in a new tab)と完全に同じではありません。
import { getDefaultWallets } from '@rainbow-me/rainbowkit'
import { configureChains, createConfig } from 'wagmi'
import { holesky, sepolia } from 'wagmi/chains'
アプリケーションがサポートするブロックチェーンをインポートします。 サポートされているチェーンのリストは、viemのGitHub (opens in a new tab)で確認できます。
import { publicProvider } from 'wagmi/providers/public'
const walletConnectProjectId = 'c96e690bb92b6311e8e9b2a6a22df575'
WalletConnect (opens in a new tab)を使用するには、アプリケーションのプロジェクトIDが必要です。 これはcloud.walletconnect.com (opens in a new tab)で取得できます。
const { chains, publicClient, webSocketPublicClient } = configureChains(
[ holesky, sepolia ],
[
publicProvider(),
],
)
const { connectors } = getDefaultWallets({
appName: 'My wagmi + RainbowKit App',
chains,
projectId: walletConnectProjectId,
})
export const config = createConfig({
autoConnect: true,
connectors,
publicClient,
webSocketPublicClient,
})
export { chains }
別のブロックチェーンの追加
最近では多くのL2スケーリングソリューションがあり、viemがまだサポートしていないものをサポートしたいと思うかもしれません。 そのためには、src/wagmi.tsを修正します。 これらの手順は、Redstone Holesky (opens in a new tab)を追加する方法を説明しています。
-
viemから
defineChain型をインポートします。import { defineChain } from 'viem' -
ネットワーク定義を追加します。
const redstoneHolesky = defineChain({ id: 17_001, name: 'Redstone Holesky', network: 'redstone-holesky', nativeCurrency: { decimals: 18, name: 'Ether', symbol: 'ETH', }, rpcUrls: { default: { http: ['https://rpc.holesky.redstone.xyz'], webSocket: ['wss://rpc.holesky.redstone.xyz/ws'], }, public: { http: ['https://rpc.holesky.redstone.xyz'], webSocket: ['wss://rpc.holesky.redstone.xyz/ws'], }, }, blockExplorers: { default: { name: 'Explorer', url: 'https://explorer.holesky.redstone.xyz' }, }, }) -
configureChains呼び出しに新しいチェーンを追加します。const { chains, publicClient, webSocketPublicClient } = configureChains( [ holesky, sepolia, redstoneHolesky ], [ publicProvider(), ], ) -
アプリケーションが新しいネットワーク上のコントラクトのアドレスを認識していることを確認します。 この場合、
src/components/Greeter.tsxを次のように変更します。const contractAddrs : AddressPerBlockchainType = { // Holesky 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8', // Redstone Holesky 17001: '0x4919517f82a1B89a32392E1BF72ec827ba9986D3', // Sepolia 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0' }
結論
もちろん、Greeterのユーザーインターフェースを提供することには、実際には関心がないでしょう。 独自のコントラクト用のユーザーインターフェースを作成したいはずです。 独自のアプリケーションを作成するには、次の手順を実行します。
-
wagmiアプリケーションを作成するよう指定します。
pnpm create wagmi -
アプリケーションに名前を付けます。
-
Reactフレームワークを選択します。
-
Viteバリアントを選択します。
さあ、あなたのコントラクトを世界中の人々が使えるようにしましょう。
私の他の作品はこちらでご覧いただけます (opens in a new tab).
ページの最終更新: 2026年3月3日