はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、Web3アプリとEthereumウォレットが一貫した方法でやり取りできるようにするための、共通インターフェース(Provider API)を標準化する提案しているERC1193についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIPについてまとめています。

概要

Ethereum Provider APIとは何か?

Ethereumの分散型アプリケーション(dapp)では、ユーザーの秘密鍵やアカウントを管理するための「ウォレット」と呼ばれるソフトウェアがよく使われています。 これらのウォレットは、JavaScriptオブジェクトを通じてアプリケーションとやり取りします。 このオブジェクトのことを「Provider(プロバイダー)」と呼びます。

このProviderは、Webページに挿入される形で提供され、多くの場合 window.ethereum としてアクセスされます。 ただし、この命名方法や提供方法はあくまで慣例であり、正式な仕様ではありません。

なぜEthereum Provider APIが必要なのか?

これまで、各ウォレットが独自にProviderを実装してきたため、インターフェースや挙動にばらつきがありました。 あるウォレットでは動作するコードが、別のウォレットでは動作しないこともあるなど、互換性の問題が発生していたのです。

このような課題を解決するために、Ethereum Provider APIが提案されました。 このAPIは、ウォレット同士の互換性を確保し、dapp開発者がどのウォレットを対象にしていても共通の方法でアクセスできるようにすることを目的としています。

どのようなAPIなのか?

Ethereum Provider APIは、以下の特徴を持っています。

  • ミニマルな設計
    • 必要最低限の機能に絞られています。
  • イベント駆動型
    • 状態の変化などを通知するイベントの仕組みがあります。
  • 通信手段に依存しない
    • HTTPやWebSocketなど、特定のプロトコルに依存せず抽象化されたインターフェースです。
  • 拡張可能
    • 必要に応じて新しいRPCメソッドやイベントを追加できます。

つまり、基本的な枠組みは決まっていますが、その上に機能を自由に追加できる柔軟性がある設計です。

window.ethereum がアクセスできる仕組み

Google拡張(Chrome Extension)としてEthereum Provider(例:MetaMaskなど)をブラウザにインストールすると、その拡張機能が自動的に window.ethereum をWebページのグローバルスコープに挿入します。

これは以下のような流れで動作しています。

Providerの挿入プロセス

  1. 拡張機能が「content script」ではなく「injected script」を用意する Chrome拡張は、Webページと直接通信できるスクリプト(content script)を持ちますが、window オブジェクトを直接書き換えるには、Webページと同じコンテキストで実行される「injected script」を挿入する必要があります。

  2. injected script が DOM に <script> タグとして埋め込まれ、実行される このスクリプトが window.ethereum にProviderオブジェクトを設定します。

    javascript
    window.ethereum = {
      request: async function(args) { ... },
      on: function(eventName, handler) { ... },
      // その他のプロパティ
    };
  3. WebページのJavaScriptから window.ethereum でアクセス可能になる dAppはこれを使ってEthereum RPCを実行します。

なぜ window.ethereum を使うのか?

window オブジェクトはブラウザにおけるグローバルオブジェクトであり、任意のJavaScriptコード(dAppなど)からアクセス可能です。 そのため、ウォレット拡張はここに ethereum プロパティを追加することで、dAppとEthereumネットワークをつなぐ標準的な入口を提供しているのです。

検知の仕組み

Webページ内のスクリプトは、以下のようにしてProviderの存在を検知することができます。

javascript
if (typeof window.ethereum !== 'undefined') {
  console.log('Ethereum provider detected');
} else {
  console.log('No Ethereum provider found');
}

これは、ユーザーがMetaMaskや他のウォレット拡張をインストールしているかを判断する際によく使われる方法です。

仕様

Ethereum Provider APIは、Webブラウザ上でEthereumにアクセスするための共通インターフェースを定義する仕様です。 このAPIは、ウォレットやクライアント間の不整合を解消し、dAppとウォレットとの統一的な接続方法を提供することを目的としています。

用語の定義

  • Provider(プロバイダー)
    • JavaScriptオブジェクトとしてWebアプリに提供され、Ethereumとのやり取りを可能にする窓口です。
  • **Client(クライアント
    • プロバイダーが送るRPC(リモート手続き呼び出し)を受け取り、結果を返すノードのようなエンドポイントです。
  • Wallet(ウォレット)
    • 秘密鍵を管理し、署名やユーザー認証の仲介を行うソフトウェアです。
    • プロバイダーとクライアントの間のミドルウェアでもあります。
  • RPC(Remote Procedure Call)
    • プロバイダーに送信される処理要求です。
    • これにより、ブロックチェーン上の情報取得やトランザクションの送信が行えます。

接続状態の定義

Providerが「接続済み」とは、少なくとも1つのチェーンに対してRPCリクエストを正常に処理できる状態を意味します。 逆に、全てのチェーンに接続できずリクエストを処理できない場合は「切断」とみなされます。

Provider APIの基本構造

APIの記述にはTypeScriptが使用されており、実装者はこれを基に独自の型定義を行うことが推奨されています。

APIの中核となるのが request メソッドです。 これはEthereumへのあらゆるリクエストを行う共通の関数で、特定のプロトコルや通信手段に依存しません。

typescript
interface RequestArguments {
  readonly method: string;
  readonly params?: readonly unknown[] | object;
}

Provider.request(args: RequestArguments): Promise<unknown>;

この request 関数にメソッド名とパラメータを渡すことで、Ethereumノードへ命令を送ります。 戻り値は Promise オブジェクトで、成功すれば期待される結果が返され、失敗すればエラーが返ります。

エラー処理のルール

リクエスト処理中に何らかの問題が発生した場合は、ProviderRpcError という形式のエラーで拒否されます。 これには数値のエラーコード、エラーメッセージ、必要に応じて詳細情報が含まれます。

例えば、以下のようなエラーコードが定義されています。

  • 4001
    • ユーザーがリクエストを拒否した。
  • 4100
    • 認証されていないリクエスト。
  • 4200
    • 未対応のメソッド。
  • 4900
    • すべてのチェーンから切断されている。
  • 4901
    • 特定のチェーンにのみ接続できない。

イベントの仕様

Providerはイベント処理機能を実装する必要があります。 具体的には、Node.jsの EventEmitter に準拠した on や removeListener 関数が提供されるべきです。

発生するイベントは以下です。

message

任意の通知を受け取る時に発行されるイベント。 例えば eth_subscribe のようなサブスクリプション通知を受け取る場合に使われます。

typescript
interface ProviderMessage {
  readonly type: string;
  readonly data: unknown;
}

サブスクリプションメッセージの場合、以下のような形式となります。

typescript
interface EthSubscription extends ProviderMessage {
  readonly type: 'eth_subscription';
  readonly data: {
    readonly subscription: string;
    readonly result: unknown;
  };
}

connect

Providerがチェーンへの接続に成功した時に発行されるイベント。

typescript
interface ProviderConnectInfo {
  readonly chainId: string;
}

chainId には接続先チェーンのIDが16進数文字列として含まれます。

disconnect

全チェーンから切断された場合に発行され、ProviderRpcError が渡されます。 このときの code はCloseEvent規格のステータスコードに従います。

chainChanged

接続先のチェーンが変更された時に発行されるイベント。 新しい chainId が渡されます。

accountsChanged

Providerが利用可能なアカウント情報に変更があった時に発行されるイベント。 新しいアカウントアドレスの配列が渡されます。

対応すべきRPCメソッド

ProviderはどのRPCメソッドをサポートするか自由に決定できますが、標準化されたメソッド(例:EIPで定義されたもの)に対応していない場合は、4200エラーで拒否することが推奨されています。

補足

Providerの目的と役割

Ethereum Providerは、Webアプリケーション(dApp)に対してEthereumネットワークへのアクセス手段を提供するための仕組みです。 Providerの基本的な役割は2つに集約されます。

1つ目は、Ethereum RPCリクエストを発行できるようにすることです。 これにより、アプリはEthereumノードに対して情報取得やトランザクション送信などの操作が可能になります。

2つ目は、接続先のEthereumチェーンやウォレットの状態変化をリアルタイムで検知・反映できるようにすることです。 例えば、ユーザーがアカウントを切り替えた場合や、別のチェーンに接続された場合に、それをアプリ側で適切に処理できる必要があります。

最小構成としてのAPI設計

Provider APIの仕様は非常にシンプルに設計されており、以下の要素で構成されています。

  • 1つの関数
    • request
  • 5つのイベント
    • message
    • connect
    • disconnect
    • chainChanged
    • accountsChanged

この中でも特に重要なのが request 関数と message イベントです。 これら2つだけでも、任意のRPCリクエストや通知の送受信が可能であり、Providerとして必要最低限の機能が実装できるようになっています。

残りの4つのイベントは、アプリケーションにとって「よくある状態変化」を効率的に検知するために用意されています。 例えば chainChanged や accountsChanged は、ユーザー体験を損なわないためには欠かせない通知機構です。

なぜこれらのイベントが含まれているのか

connect と disconnect イベントは、ProviderがRPCリクエストを処理できる状態かどうかの変化を示します。 これにより、アプリ側は「いつEthereumにアクセス可能なのか」を正確に把握できます。

chainChanged と accountsChanged は、接続チェーンの切り替えやアカウントの変更といった、ユーザー操作に伴う重要なイベントです。 これらの検知ができなければ、アプリは不正確な状態のまま動作し続けるおそれがあります。

こうしたイベントは、既に多くのdAppで実運用されていた設計パターンをもとに取り入れられており、実用上のニーズにしっかりと応える形になっています。

互換性

この仕様は、以前に非公式に広く使用されていた「ドラフト版Provider API」と互換性があります。 つまり、今回の正式仕様は旧バージョンの厳密な上位互換として設計されており、既存の多くのウォレット実装との整合性を保っています。

ただし、今回の仕様だけを実装したProviderは、旧APIに依存している古いdAppとは互換性がなくなります。 これは、旧仕様に特有のメソッドやイベントが廃止されている可能性があるためです。

つまり、開発者は新しいAPIを使用すれば今後の標準に沿った開発が可能になりますが、古いアプリとの互換性が必要な場合は、旧APIの挙動も考慮する必要があります。

セキュリティ

Providerの役割とセキュリティ境界

Ethereum Providerは、WebアプリケーションとEthereumクライアント(ノード)との間でRPC(リモートプロシージャコール)メッセージを送受信する役割を担います。ただし、Provider自身は秘密鍵やアカウントの管理を行いません。 これらの管理は、ProviderとEthereumクライアントの間に位置する「ウォレット(Wallet)」によって行われます。

つまり、Providerはユーザーの秘密鍵や個人情報を扱わず、単にリクエストを仲介する中継的なコンポーネントです。 そして、ウォレットがユーザーの資産やアカウントを保護する役割を担っています。

重要なのは、ProviderはWebページ内のJavaScriptオブジェクトとして公開されるため、信頼できない環境にさらされているという点です。 例えば、悪意のあるWebサイトはこのProviderオブジェクトを自由に操作できてしまいます。

悪意ある操作への備え

Providerは第三者のJavaScript環境下で動作するため、開発者は常に「Providerは攻撃者の支配下にある可能性がある」という前提で設計しなければなりません。 これを踏まえた上で、以下のような対策が推奨されます。

まず、Provider自身には秘密鍵や個人データを一切含めないことが大前提です。 あくまで中継役に徹し、センシティブな情報の保持や処理は避けるべきです。

次に、ウォレットとProviderのプロセスを明確に分離し、相互の影響を最小限にすることが重要です。 ウォレットがProvider経由で受け取るすべてのリクエストやデータは、信用せずに必ず検証する必要があります。

また、ウォレットやクライアントは、Providerからのリクエストに対してレート制限(リクエスト回数制限)を設けることで、DoS攻撃や悪質な連投からの保護を強化することが推奨されます。

チェーンの状態と整合性

Ethereumにおけるすべての操作は、ある特定のチェーン(ネットワーク)に向けて行われます。 そのため、Providerは常に正しいチェーンIDを示す必要があります。

このチェーンIDは、eth_chainId RPCメソッドによって取得されます。 ユーザーがネットワークを切り替えた場合、Providerはただちにその変化を検知し、chainChanged イベントを送出することで、アプリケーション側に通知しなければなりません。

これが適切に実装されていない場合、dAppは誤ったチェーンに向けてトランザクションを送るなどの深刻な問題を引き起こすおそれがあります。

アカウントの公開と変更の取り扱い

多くのEthereum操作(例:eth_sendTransaction)には、送信元となるユーザーのアカウント指定が必要です。 Providerはこの情報を eth_accounts メソッドを通じてアプリに提供します。 また、アカウントが変更された場合には accountsChanged イベントを送出します。

重要なのは、これらのアカウント情報はデフォルトで勝手に開示してはならないという点です。 ユーザーのプライバシー保護のため、Providerは初期状態ではアカウントを提供せず、アプリケーションが明示的にアクセス許可をリクエストした場合にのみ開示する設計が推奨されています。

そのためには、eth_requestAccounts(ERC1102)や wallet_requestPermissions(ERC2255)などのRPCメソッドを使って、ユーザーに確認を促す形でアカウントへのアクセスを申請します。

Ethereum Provider APIの使い方と実装例

Ethereum Provider APIは、WebアプリケーションがEthereumネットワークとやり取りするための共通インターフェースです。 このAppendixでは、APIの使い方、主要イベント、エラー処理、サブスクリプション、過去の非推奨APIとの違いについて、開発者向けに実践的な情報がまとめられています。

requestメソッドの使い方

Ethereum Providerで最も重要な機能が request メソッドです。 これはEthereumのRPCメソッドを呼び出すための標準的なインターフェースです。

typescript
interface RequestArguments {
  readonly method: string;
  readonly params?: readonly unknown[] | object;
}

Provider.request(args: RequestArguments): Promise<unknown>;

このメソッドはPromiseを返し、成功すればその結果が、失敗すれば ProviderRpcError という形式のエラーが返されます。 例えば、現在のアカウント一覧を取得するには以下のように記述します。

javascript
Provider.request({ method: 'eth_accounts' })
  .then((accounts) => console.log(accounts))
  .catch((error) => console.error(error));

使用可能なメソッドと引数については、それぞれのRPC仕様(たとえばEIP-1474)を参照します。

利用可能なRPCプロトコル

Ethereumとの通信には複数のRPCプロトコルが存在し、主なものは以下の2つです。

  • ERC1474: Ethereum JSON-RPC API
  • ERC1767: Ethereum GraphQL スキーマ

ERC1474については以下の記事を参考にしてください。

ERC1767については以下の記事を参考にしてください。

Providerはこれらのプロトコルに依存しない抽象的なインターフェースを提供します。

イベントの取り扱い

Ethereum ProviderはNode.jsの EventEmitter に準拠したイベントシステムを持ちます。 以下のようなイベントを利用して、状態変化を検知できます。

connect

Providerがチェーンへの接続に成功した時に発行されるイベントです。

typescript
Provider.on('connect', (connectInfo) => {
  console.log(connectInfo.chainId); // 例: "0x1"
});

disconnect

すべてのチェーンから切断された時に発行されるイベントです。

typescript
Provider.on('disconnect', (error) => {
  console.error(`Disconnected: ${error.message} (code: ${error.code})`);
});

chainChanged

接続先のチェーンIDが変わった時に発行されるイベントです。

typescript
Provider.on('chainChanged', (chainId) => {
  console.log(`New chain ID: ${chainId}`);
});

accountsChanged

アカウントが変更された時に発行されるイベントです。

typescript
Provider.on('accountsChanged', (accounts) => {
  console.log(`Current accounts: ${accounts.join(', ')}`);
});

message

任意の通知を受け取るイベントです。 eth_subscribe などの通知もここに含まれます。

typescript
Provider.on('message', (message) => {
  console.log(`Message type: ${message.type}`);
  console.log(message.data);
});

サブスクリプション(購読)

eth_subscribe などのRPCメソッドでイベントの購読が可能です。 購読した通知は message イベントで受け取ります。

javascript
Provider.request({ method: 'eth_subscribe', params: ['newHeads'] })
  .then((subscriptionId) => {
    Provider.on('message', (message) => {
      if (message.type === 'eth_subscription' && message.data.subscription === subscriptionId) {
        console.log(`New block:`, message.data.result);
      }
    });
  });

エラーの扱い

Providerが返すエラーは ProviderRpcError という形式で統一されています。

typescript
interface ProviderRpcError extends Error {
  message: string;
  code: number;
  data?: unknown;
}

例えば、ユーザーが操作を拒否したときには 4001 番のエラーコードが返されます。

よくある使用例

Webブラウザ上で window.ethereum を使う実装が一般的です。 以下はいくつかの代表的な使用例です。

チェーンIDを取得

javascript
window.ethereum.request({ method: 'eth_chainId' })
  .then((chainId) => {
    console.log(`Chain ID: ${chainId}`);
  });

最新ブロックの情報を取得

javascript
window.ethereum.request({ method: 'eth_getBlockByNumber', params: ['latest', true] })
  .then((block) => {
    console.log(`Block ${block.number}:`, block);
  });

アカウントの変更を監視

javascript
const logAccounts = (accounts) => {
  console.log(`Accounts:\n${accounts.join('\n')}`);
};
window.ethereum.on('accountsChanged', logAccounts);

接続終了の通知をログに出力

javascript
window.ethereum.on('disconnect', (error) => {
  console.log(`Connection closed: ${error.message} (code: ${error.code})`);
});

レガシーAPIとの違い

現在のProvider APIは、過去に非公式に広まっていた「レガシーAPI」の上位互換として設計されています。 レガシーAPIには次のようなメソッドやイベントが存在していましたが、現在は非推奨です。

  • sendAsync
    • コールバック形式でRPCを送信(request に置き換え)。
  • send
    • 形式が統一されておらず、非推奨。
  • close、networkChanged、notification
    • それぞれ disconnect、chainChanged、message に置き換え済み。

開発者は、これらの旧仕様を新規実装に取り入れるべきではなく、request やイベントAPIを使用することが推奨されています。

引用

Fabian Vogelsteller (@frozeman), Ryan Ghods (@ryanio), Victor Maia (@MaiaVictor), Marc Garreau (@wolovim), Erik Marks (@rekmarks), "EIP-1193: Ethereum Provider JavaScript API," Ethereum Improvement Proposals, no. 1193, June 2018. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1193.

最後に

今回は「Web3アプリとEthereumウォレットが一貫した方法でやり取りできるようにするための、共通インターフェース(Provider API)を標準化する提案しているERC1193」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!