メインコンテンツへスキップ

Web3アプリ用のサーバーコンポーネントとエージェント

エージェント
サーバー
オフチェーン
初級
Ori Pomerantz
2024年7月15日
15 分の読書

はじめに

ほとんどの場合、分散型アプリはソフトウェアを配布するためにサーバーを使用しますが、実際のインタラクションはすべてクライアント(通常はWebブラウザ)とブロックチェーンの間で行われます。

Webサーバー、クライアント、ブロックチェーン間の通常のインタラクション

ただし、アプリケーションが独立して実行されるサーバーコンポーネントを持つことでメリットが得られるケースもあります。 このようなサーバーは、トランザクションを発行することで、イベントやAPIなどの他のソースからのリクエストに応答できます。

サーバーを追加した場合のインタラクション

このようなサーバーが果たすことのできるタスクには、いくつか考えられます。

  • 秘密の状態の保持者。 ゲームでは、ゲームが知っているすべての情報をプレイヤーが利用できるようにしない方が便利なことがよくあります。 しかし、ブロックチェーン上に秘密はありません。ブロックチェーンにある情報は誰でも簡単に見つけ出すことができます。 したがって、ゲームの状態の一部を秘密にしておく必要がある場合は、それを別の場所に保存する必要があります(そして、おそらくその状態の効果をゼロ知識証明を使用して検証する必要があります)。

  • 集中型オラクル。 ステークが十分に低い場合、オンラインで情報を読み取ってからチェーンに投稿する外部サーバーは、オラクルとして使用するのに十分な場合があります。

  • エージェント。 ブロックチェーン上では、それをアクティブにするトランザクションがなければ何も起こりません。 サーバーは、ユーザーに代わって、機会が生じたときにアービトラージなどのアクションを実行できます。

サンプルプログラム

サンプルサーバーはgithub (opens in a new tab)でご覧いただけます。 このサーバーは、HardhatのGreeterの修正版であるこのコントラクト (opens in a new tab)からのイベントをリッスンします。 挨拶が変更されると、それを元に戻します。

実行方法:

  1. リポジトリをクローンします。

    1git clone https://github.com/qbzzt/20240715-server-component.git
    2cd 20240715-server-component
  2. 必要なパッケージをインストールします。 まだインストールしていない場合は、まずNodeをインストール (opens in a new tab)してください。

    1npm install
  3. .envを編集して、Holeskyテストネット上にETHを持つアカウントの秘密鍵を指定します。 Holesky上にETHをお持ちでない場合は、このフォーセット (opens in a new tab)を使用できます。

    1PRIVATE_KEY=0x <ここに秘密鍵を入力>
  4. サーバーを起動します。

    1npm start
  5. ブロックエクスプローラー (opens in a new tab)にアクセスし、秘密鍵を持つアドレスとは異なるアドレスを使用して挨拶を変更します。 挨拶が自動的に元に戻されることを確認してください。

仕組み

サーバーコンポーネントの書き方を理解する最も簡単な方法は、サンプルを一行ずつ見ていくことです。

src/app.ts

プログラムの大部分はsrc/app.ts (opens in a new tab)に含まれています。

前提条件となるオブジェクトの作成
1import {
2 createPublicClient,
3 createWalletClient,
4 getContract,
5 http,
6 Address,
7} from "viem"

これらは、私たちが必要とするViem (opens in a new tab)のエンティティ、関数、およびAddress (opens in a new tab)です。 このサーバーはTypeScript (opens in a new tab)で書かれています。これは、JavaScriptを強く型付け (opens in a new tab)されたものにする拡張機能です。

1import { privateKeyToAccount } from "viem/accounts"

この関数 (opens in a new tab)を使用すると、秘密鍵に対応するウォレット情報(アドレスを含む)を生成できます。

1import { holesky } from "viem/chains"

Viemでブロックチェーンを使用するには、その定義をインポートする必要があります。 このケースでは、Holesky (opens in a new tab)テストブロックチェーンに接続します。

1// これは.envの定義をprocess.envに追加する方法です。
2import * as dotenv from "dotenv"
3dotenv.config()

これは、.envを環境に読み込む方法です。 これは後で説明する秘密鍵のために必要です。

1const greeterAddress : Address = "0xB8f6460Dc30c44401Be26B0d6eD250873d8a50A6"
2const greeterABI = [
3 {
4 "inputs": [
5 {
6 "internalType": "string",
7 "name": "_greeting",
8 "type": "string"
9 }
10 ],
11 "stateMutability": "nonpayable",
12 "type": "constructor"
13 },
14 .
15 .
16 .
17 {
18 "inputs": [
19 {
20 "internalType": "string",
21 "name": "_greeting",
22 "type": "string"
23 }
24 ],
25 "name": "setGreeting",
26 "outputs": [],
27 "stateMutability": "nonpayable",
28 "type": "function"
29 }
30] as const
すべて表示

コントラクトを使用するには、そのアドレスとが必要です。 ここでは両方を提供します。

JavaScript (したがってTypeScript) では、定数に新しい値を代入することはできませんが、その中に格納されているオブジェクトを_変更する_ことはできます。 as constという接尾辞を使用することで、リスト自体が定数であり、変更できないことをTypeScriptに伝えています。

1const publicClient = createPublicClient({
2 chain: holesky,
3 transport: http(),
4})

Viemのパブリッククライアント (opens in a new tab)を作成します。 パブリッククライアントには秘密鍵がアタッチされていないため、トランザクションを送信できません。 これらはview 関数 (opens in a new tab)を呼び出したり、アカウント残高を読み取ったりすることができます。

1const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)

環境変数はprocess.env (opens in a new tab)で利用できます。 しかし、TypeScriptは強く型付けされています。 環境変数は任意の文字列または空にすることができるため、環境変数の型はstring | undefinedです。 しかし、Viemではキーは0x${string}(文字列が続く0x)として定義されています。 ここで、PRIVATE_KEY環境変数がその型になることをTypeScriptに伝えます。 そうでない場合は、ランタイムエラーが発生します。

privateKeyToAccount (opens in a new tab)関数は、この秘密鍵を使用して完全なアカウントオブジェクトを作成します。

1const walletClient = createWalletClient({
2 account,
3 chain: holesky,
4 transport: http(),
5})

次に、アカウントオブジェクトを使用してウォレットクライアント (opens in a new tab)を作成します。 このクライアントは秘密鍵とアドレスを持っているため、トランザクションの送信に使用できます。

1const greeter = getContract({
2 address: greeterAddress,
3 abi: greeterABI,
4 client: { public: publicClient, wallet: walletClient },
5})

すべての前提条件が整ったので、ようやくコントラクトインスタンス (opens in a new tab)を作成できます。 このコントラクトインスタンスを使用して、オンチェーンコントラクトと通信します。

ブロックチェーンからの読み取り
1console.log(`Current greeting:`, await greeter.read.greet())

読み取り専用のコントラクト関数 (view (opens in a new tab)pure (opens in a new tab)) は read で利用できます。 この場合、これを使用してgreet (opens in a new tab)関数にアクセスし、挨拶を返します。

JavaScriptはシングルスレッドなので、時間のかかるプロセスを実行する場合は、非同期で実行することを指定する (opens in a new tab)必要があります。 ブロックチェーンを呼び出すには、読み取り専用の操作であっても、コンピュータとブロックチェーンノード間のラウンドトリップが必要です。 これが、ここでコードが結果をawaitする必要があると指定する理由です。

この仕組みに興味がある場合は、こちら (opens in a new tab)で読むことができますが、実際には、時間がかかる操作を開始する場合は結果をawaitすること、およびこれを行う関数はasyncとして宣言する必要があることだけを知っていれば十分です。

トランザクションの発行
1const setGreeting = async (greeting: string): Promise<any> => {

これは、挨拶を変更するトランザクションを発行するために呼び出す関数です。 これは時間のかかる操作なので、関数はasyncとして宣言されています。 内部実装のため、async関数はPromiseオブジェクトを返す必要があります。 この場合、Promise<any>は、Promiseで何が返されるかを正確に指定しないことを意味します。

1const txHash = await greeter.write.setGreeting([greeting])

コントラクトインスタンスのwriteフィールドには、setGreeting (opens in a new tab)など、ブロックチェーンの状態に書き込む(トランザクションの送信を必要とする)すべての関数が含まれています。 パラメータがある場合はリストとして提供され、関数はトランザクションのハッシュを返します。

1 console.log(`Working on a fix, see https://eth-holesky.blockscout.com/tx/${txHash}`)
2
3 return txHash
4}

トランザクションのハッシュを報告し(表示するためのブロックエクスプローラーへのURLの一部として)、それを返します。

イベントへの応答
1greeter.watchEvent.SetGreeting({

watchEvent 関数 (opens in a new tab)を使用すると、イベントが発行されたときに実行する関数を指定できます。 1種類のイベント(この場合は SetGreeting)のみに関心がある場合は、この構文を使用してそのイベントタイプに限定できます。

1 onLogs: logs => {

onLogs関数は、ログエントリがある場合に呼び出されます。 イーサリアムでは、「ログ」と「イベント」は通常、同じ意味で使われます。

1console.log(
2 `Address ${logs[0].args.sender} changed the greeting to ${logs[0].args.greeting}`
3)

複数のイベントが発生する可能性がありますが、簡単にするために最初のイベントのみを扱います。 logs[0].argsはイベントの引数で、この場合はsendergreetingです。

1 if (logs[0].args.sender != account.address)
2 setGreeting(`${account.address} insists on it being Hello!`)
3 }
4})

送信者がこのサーバーで_ない_場合は、setGreetingを使用して挨拶を変更します。

package.json

このファイル (opens in a new tab)Node.js (opens in a new tab)の設定を制御します。 この記事では、重要な定義のみを説明します。

1{
2 "main": "dist/index.js",

この定義は、実行するJavaScriptファイルを指定します。

1 "scripts": {
2 "start": "tsc && node dist/app.js",
3 },

スクリプトは、さまざまなアプリケーションのアクションです。 この場合、私たちが持っているのはstartだけで、これはサーバーをコンパイルしてから実行します。 tscコマンドはtypescriptパッケージの一部で、TypeScriptをJavaScriptにコンパイルします。 手動で実行したい場合は、node_modules/.binにあります。 2番目のコマンドはサーバーを実行します。

1 "type": "module",

JavaScriptノードアプリケーションには複数のタイプがあります。 moduleタイプを使用すると、トップレベルのコードでawaitを使用できます。これは、遅い(そして非同期の)操作を行う場合に重要です。

1 "devDependencies": {
2 "@types/node": "^20.14.2",
3 "typescript": "^5.4.5"
4 },

これらは開発にのみ必要なパッケージです。 ここではtypescriptが必要で、Node.jsでそれを使用しているため、processなどのノード変数やオブジェクトの型も取得しています。 ^<version>表記 (opens in a new tab)は、そのバージョンまたは破壊的変更のないそれ以降のバージョンを意味します。 バージョン番号の意味の詳細については、こちら (opens in a new tab)を参照してください。

1 "dependencies": {
2 "dotenv": "^16.4.5",
3 "viem": "2.14.1"
4 }
5}

これらは、dist/app.jsを実行する際に、ランタイムで必要なパッケージです。

結論

ここで作成した集中型サーバーは、ユーザーのエージェントとして機能するという役割を果たします。 dappの機能を継続させたい、ガスを消費しても構わないという人なら誰でも、自分のアドレスでサーバーの新しいインスタンスを実行できます。

ただし、これは集中型サーバーのアクションが簡単に検証できる場合にのみ機能します。 集中型サーバーに秘密の状態情報があったり、難しい計算を実行したりする場合、それはアプリケーションを使用するために信頼する必要がある集中型エンティティであり、これはまさにブロックチェーンが避けようとしていることです。 今後の記事では、この問題を回避するためにゼロ知識証明を使用する方法を紹介する予定です。

私の他の作品はこちらでご覧いただけます (opens in a new tab).

最終更新: 2026年2月25日

このチュートリアルは役に立ちましたか?