はじめに

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

今回は、アプリケーションがウォレットに複数のオンチェーン呼び出しをバッチで送信し、そのステータスを確認するための新しいJSON-RPCメソッドを定義しているEIP5792についてまとめていきます!

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

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

概要

EIP5792は、アプリケーションがウォレットに対して複数のオンチェーン呼び出し(call)をバッチとして送信し、そのステータスを確認するための新しいJSON-RPCメソッドを定義する提案です。

従来のeth_sendTransactionでは1回のリクエストで1つのトランザクションしか送信できませんでしたが、EIP5792では複数の呼び出しを1つのリクエストでまとめて送信できます。 スマートアカウントを使用している場合、これらの呼び出しを1つのトランザクションでアトミックに実行することも可能です。 また、ウォレットがどのような機能(ケーパビリティ)をサポートしているかをクエリしたり、送信したバッチの実行ステータスを確認したりするためのメソッドも提供されています。

EIP5792は、従来のeth_sendTransactionメソッドの制限を克服し、ペイマスター(Paymaster)やアトミックなバッチ実行など、現代的な機能をサポートするために設計されています。

動機

この提案が生まれた背景には以下のような課題がありました。

eth_sendTransactionの限界

従来のeth_sendTransactionメソッドは、単一のトランザクションを送信するために設計されており、現代的なニーズに対応できませんでした。

開発者は複数の呼び出しを1つのRPCリクエストでバッチとして送信したいというニーズを持っています。 多くのスマートアカウントは、これらの呼び出しを1つのトランザクションでアトミックに実行できますが、従来のメソッドではこの機能を活用できませんでした。 また、ペイマスターなどの新しいウォレット機能を活用するための標準的な方法も存在しませんでした。

スマートアカウントの普及

ERC4337(Account Abstraction)の普及により、スマートコントラクトウォレット(スマートアカウント)が増加しています。 スマートアカウントは、複数の呼び出しを単一のトランザクションで実行できるため、ガス効率が良く、ユーザー体験も向上します。

EIP5792は、これらの課題を解決するために、ウォレットとアプリケーション間の通信を更新し、現代の開発者ニーズに対応する新しいJSON-RPCメソッドを定義します。 これにより、バッチ送信、アトミック実行、ペイマスターなどの高度な機能を標準化された方法で利用できるようになります。

仕様

RPCメソッド一覧

EIP5792では、以下の4つの新しいJSON-RPCメソッドを定義しています。

メソッド名 説明
wallet_sendCalls 複数の呼び出しをバッチとしてウォレットに送信する
wallet_getCallsStatus 送信した呼び出しバッチのステータスを取得する
wallet_showCallsStatus ウォレットに呼び出しバッチの情報を表示するよう要求する
wallet_getCapabilities 特定のアドレスとチェーンに対するウォレットのケーパビリティをクエリする

wallet_sendCalls

複数の呼び出しをバッチとしてウォレットに送信するためのメソッドです。

パラメータ

javascript
type SendCallsParams = {
  version: string;           // 必須: 使用するプロトコルバージョン(例: "2.0.0")
  id?: string;               // オプション: 呼び出しバッチの識別子
  from?: `0x${string}`;      // オプション: 送信元アドレス
  chainId: `0x${string}`;    // 必須: 16進数でエンコードされたチェーンID
  atomicRequired: boolean;   // 必須: アトミック実行が必要かどうか
  calls: {
    to?: `0x${string}`;      // オプション: 送信先アドレス
    data?: `0x${string}`;    // オプション: 呼び出しデータ
    value?: `0x${string}`;   // オプション: 送信するネイティブトークンの量
    capabilities?: Record<string, Capability>;  // オプション: 呼び出し固有のケーパビリティ
  }[];
  capabilities?: Record<string, Capability>;    // オプション: バッチ全体のケーパビリティ
};

各パラメータの詳細を以下の表に示します。

パラメータ名 型 必須 説明
version string 必須 使用するプロトコルバージョン。現在は「2.0.0」
id string オプション 呼び出しバッチを一意に識別するID。アプリケーションごと、送信者ごとに一意である必要がある
from 0x${string} オプション 送信元アドレス。省略した場合はウォレットがユーザーに選択を促す
chainId 0x${string} 必須 呼び出しを実行するチェーンのID(16進数)
atomicRequired boolean 必須 trueの場合、全ての呼び出しがアトミックかつ連続的に実行される必要がある
calls array 必須 実行する呼び出しの配列
capabilities object オプション バッチ全体に適用するケーパビリティ

戻り値

戻り値の型定義を以下に示します。

javascript
type SendCallsResult = {
  id: string;                              // バッチの識別子
  capabilities?: Record<string, any>;      // ウォレットからのケーパビリティレスポンス
};

idフィールドにはバッチの識別子が含まれ、capabilitiesフィールドにはウォレットからのケーパビリティレスポンスがオプションで含まれます。

ウォレットの義務

wallet_sendCallsを実装するウォレットは、以下の義務を負います。

義務 説明
実行順序 呼び出しは指定された順序で、指定されたチェーン上で実行する必要がある
送信元アドレス fromアドレスから送信するか、fromが省略された場合はユーザーにアドレス選択を促す必要がある
アトミック実行 atomicRequiredがtrueの場合、全ての呼び出しをアトミックかつ連続的に実行する必要がある
ケーパビリティの処理 サポートされていない非オプションのケーパビリティはリジェクトする必要がある
IDの尊重 提供されたid値を尊重し、重複するIDはリジェクトする必要がある

使用例

DEXでトークンをスワップする際の使用例を以下に示します。

javascript
const params = {
  version: "2.0.0",
  chainId: "0x1", // Ethereum Mainnet
  atomicRequired: true,
  from: "0x1234567890abcdef1234567890abcdef12345678",
  calls: [
    {
      // 1. トークンのapprove
      to: "0xA0b86a33E6e7eF3d0b7e8e3d9e3e3e3e3e3e3e3e", // USDCコントラクト
      data: "0x095ea7b3000000000000000000000000def1c0ded9bec7f1a1670819833240f027b25eee0000000000000000000000000000000000000000000000000000000005f5e100", // approve(spender, amount)
    },
    {
      // 2. スワップの実行
      to: "0xDEF1C0ded9bec7F1a1670819833240f027b25EeE", // DEXルーターコントラクト
      data: "0x...", // swap関数のcalldata
      value: "0x0",
    }
  ]
};

const response = await provider.request({
  method: "wallet_sendCalls",
  params: [params]
});

console.log("Batch ID:", response.id);

このコードでは、ERC20トークンのapproveとDEXルーターのswapを1つのバッチにまとめて送信しています。 atomicRequired: trueを指定することで、両方の呼び出しがアトミックに実行されることを要求しています。

wallet_getCallsStatus

送信した呼び出しバッチのステータスを取得するためのメソッドです。

パラメータ

パラメータとしてバッチIDを渡します。

javascript
type GetCallsParams = [string];

戻り値

戻り値の型定義を以下に示します。

javascript
type GetCallsResult = {
  version: string;
  id: `0x${string}`;
  chainId: `0x${string}`;
  status: number;
  atomic: boolean;
  receipts?: {
    logs: {
      address: `0x${string}`;
      data: `0x${string}`;
      topics: `0x${string}`[];
    }[];
    status: `0x${string}`;
    blockHash: `0x${string}`;
    blockNumber: `0x${string}`;
    gasUsed: `0x${string}`;
    transactionHash: `0x${string}`;
  }[];
  capabilities?: Record<string, any>;
};

各フィールドの詳細を以下の表に示します。

フィールド名 型 説明
version string プロトコルバージョン
id 0x${string} バッチの識別子
chainId 0x${string} 実行されたチェーンのID
status number 現在のステータスを示すコード
atomic boolean 全ての呼び出しが単一のトランザクションで実行されたかどうか
receipts array トランザクションレシートの配列(バッチがオンチェーンに含まれた場合)

使用例

バッチのステータスを取得して結果を処理する例を以下に示します。

javascript
const status = await provider.request({
  method: "wallet_getCallsStatus",
  params: ["0xabc123..."]
});

console.log("Status:", status.status);
console.log("Atomic:", status.atomic);

if (status.receipts) {
  status.receipts.forEach((receipt, index) => {
    console.log(`Transaction ${index + 1}:`, receipt.transactionHash);
    console.log(`  Status: ${receipt.status === "0x1" ? "Success" : "Failed"}`);
    console.log(`  Gas Used: ${parseInt(receipt.gasUsed, 16)}`);
  });
}

このコードでは、バッチIDを渡してステータスを取得し、レシートが存在する場合は各トランザクションの情報を出力しています。

wallet_showCallsStatus

ウォレットに対して、特定の呼び出しバッチの情報をユーザーに表示するよう要求するメソッドです。

パラメータ

パラメータとしてバッチIDを渡します。

javascript
type ShowCallsStatusParams = [string];

戻り値

成功した場合は何も返しません。 バッチIDが不明な場合はエラーを返します。

使用例

ウォレットにバッチステータスを表示させる例を以下に示します。

javascript
try {
  await provider.request({
    method: "wallet_showCallsStatus",
    params: ["0xabc123..."]
  });
  console.log("Wallet is showing the batch status to the user.");
} catch (error) {
  if (error.code === 5730) {
    console.log("Unknown batch ID");
  }
}

このコードでは、バッチIDを渡してウォレットにステータス表示を要求しています。 エラーコード5730は不明なバッチIDを示します。

wallet_getCapabilities

特定のアドレスとチェーンに対するウォレットのケーパビリティをクエリするメソッドです。

パラメータ

パラメータとしてウォレットアドレスとチェーンIDの配列を渡します。

javascript
type GetCapabilitiesParams = [
  `0x${string}`,
  `0x${string}`[]
];

第1引数がウォレットアドレス、第2引数がクエリ対象のチェーンIDの配列(オプション)です。

戻り値

戻り値の型定義を以下に示します。

javascript
type GetCapabilitiesResult = Record<
  `0x${string}`,
  Record<string, any>
>;

チェーンIDをキー、そのチェーンでサポートされているケーパビリティのマップを値とするオブジェクトが返されます。

使用例

複数のチェーンに対するケーパビリティを取得する例を以下に示します。

javascript
const capabilities = await provider.request({
  method: "wallet_getCapabilities",
  params: [
    "0x1234567890abcdef1234567890abcdef12345678",
    ["0x1", "0x89"]
  ]
});

console.log("Ethereum capabilities:", capabilities["0x1"]);
console.log("Polygon capabilities:", capabilities["0x89"]);

if (capabilities["0x1"]?.atomic?.status === "supported") {
  console.log("Atomic batch execution is supported on Ethereum!");
}

このコードでは、Ethereum Mainnet(0x1)とPolygon(0x89)のケーパビリティを取得し、atomicケーパビリティがサポートされているかを確認しています。

ステータスコード一覧

wallet_getCallsStatusが返すステータスコードの一覧です。

コード 説明
100 バッチは実行保留中
200 バッチがオンチェーンで確認され、リバートなし
400 バッチは送信されなかった。ウォレットは再試行しない
500 バッチ全体がリバートした。ガス代のみが記録された
600 バッチの一部がリバートした。一部の変更がオンチェーンに含まれた

ステータスコードは、HTTPステータスコードに似た設計になっています。 100番台は「保留中」、200番台は「成功」、400番台は「クライアントエラー」、500-600番台は「実行エラー」を示します。

エラーコード一覧

EIP5792のメソッドが返すエラーコードの一覧です。

コード メッセージ 適用されるメソッド
-32602 Invalid params 全てのメソッド
4001 User Rejected Request wallet_sendCalls
4100 Unauthorized wallet_sendCalls, wallet_getCapabilities
5700 Unsupported non-optional capability wallet_sendCalls
5710 Unsupported chain id wallet_sendCalls
5720 Duplicate ID wallet_sendCalls
5730 Unknown bundle id wallet_getCallsStatus, wallet_showCallsStatus
5740 Bundle too large wallet_sendCalls
5750 Atomic-ready wallet rejected upgrade wallet_sendCalls
5760 Atomicity not supported wallet_sendCalls

-32602はパラメータの形式が不正な場合に返されます。 4001はユーザーがリクエストを拒否した場合、4100はアプリケーションがウォレットへのアクセス権限を持っていない場合に返されます。

5700番台のエラーはEIP5792固有のエラーです。 5700はサポートされていない必須ケーパビリティが要求された場合、5710はウォレットがサポートしていないチェーンIDが指定された場合に返されます。 5720は同じIDが既に使用されている場合、5730は存在しないバッチIDが指定された場合に返されます。 5740はバッチに含まれる呼び出しの数が多すぎる場合に返されます。 5750はアトミック対応ウォレットへのアップグレードをユーザーが拒否した場合、5760はatomicRequiredがtrueだがウォレットがアトミック実行をサポートしていない場合に返されます。

atomicケーパビリティ

atomicケーパビリティは、ウォレットがチェーンごとにバッチ実行をどのように保証するかを指定します。

ケーパビリティの構造

atomicケーパビリティの型定義を以下に示します。

javascript
type AtomicCapability = {
  status: 'supported' | 'ready' | 'unsupported';
};

statusフィールドには、アトミック実行のサポート状況を示す3つの値のいずれかが設定されます。

ステータスの意味

ステータス 説明
supported ウォレットは呼び出しをアトミックかつ連続的に実行できる
ready ウォレットはユーザーの承認待ちでアップグレード可能
unsupported アトミック実行の保証は提供されない

supportedの場合、ウォレットはatomicRequired: trueのリクエストに対して、全ての呼び出しを単一のトランザクションでアトミックかつ連続的に実行することを保証します。

readyの場合、ウォレットは現在アトミック実行をサポートしていませんが、ユーザーの承認を得ることでアップグレードできます。 例えば、EOAからスマートアカウントへの移行がこれに該当します。

unsupportedの場合、ウォレットはアトミック実行をサポートしていません。 atomicRequired: trueのリクエストはエラー5760でリジェクトされます。

atomicケーパビリティの確認例

アプリケーションでatomicケーパビリティを確認し、適切な処理を行う例を以下に示します。

javascript
const capabilities = await provider.request({
  method: "wallet_getCapabilities",
  params: [walletAddress, ["0x1"]]
});

const atomicStatus = capabilities["0x1"]?.atomic?.status;

switch (atomicStatus) {
  case "supported":
    console.log("アトミック実行がサポートされています");
    break;
  case "ready":
    console.log("アップグレードでアトミック実行が可能になります");
    break;
  case "unsupported":
    console.log("アトミック実行はサポートされていません");
    break;
  default:
    console.log("atomicケーパビリティが不明です");
}

このコードでは、ステータスに応じて適切なメッセージを表示しています。 supportedの場合はatomicRequired: trueでバッチを送信でき、readyの場合はユーザーにアップグレードを提案し、unsupportedの場合はフォールバック処理を実行します。

補足

DEXスワップの完全なフロー

DEXでトークンをスワップする際の完全なフローを以下に示します。 この例では、ケーパビリティの確認からバッチ送信、ステータスのポーリング、結果の処理までを行います。

javascript
async function swapTokens(provider, userAddress, tokenAddress, spenderAddress, amount, swapData) {
  const capabilities = await provider.request({
    method: "wallet_getCapabilities",
    params: [userAddress, ["0x1"]]
  });

  const supportsAtomic = capabilities["0x1"]?.atomic?.status === "supported";
  console.log("Atomic support:", supportsAtomic);

  // 2. バッチリクエストを構築
  const sendCallsParams = {
    version: "2.0.0",
    chainId: "0x1",
    atomicRequired: supportsAtomic, // アトミックがサポートされている場合のみtrue
    from: userAddress,
    calls: [
      {
        // approve呼び出し
        to: tokenAddress,
        data: encodeApprove(spenderAddress, amount),
      },
      {
        // swap呼び出し
        to: spenderAddress,
        data: swapData,
      }
    ]
  };

  // 3. バッチを送信
  const { id: batchId } = await provider.request({
    method: "wallet_sendCalls",
    params: [sendCallsParams]
  });

  console.log("Batch submitted:", batchId);

  // 4. ステータスをポーリング
  let status;
  do {
    await sleep(2000); // 2秒待機
    status = await provider.request({
      method: "wallet_getCallsStatus",
      params: [batchId]
    });
    console.log("Current status:", status.status);
  } while (status.status === 100); // 100 = pending

  // 5. 結果を処理
  if (status.status === 200) {
    console.log("Swap completed successfully!");
    console.log("Atomic execution:", status.atomic);
    if (status.receipts) {
      console.log("Transaction hashes:", status.receipts.map(r => r.transactionHash));
    }
    return { success: true, receipts: status.receipts };
  } else if (status.status === 400) {
    console.log("Batch was not submitted");
    return { success: false, error: "Not submitted" };
  } else if (status.status === 500) {
    console.log("Batch completely reverted");
    return { success: false, error: "Reverted" };
  } else if (status.status === 600) {
    console.log("Partial revert occurred");
    return { success: false, error: "Partial revert", receipts: status.receipts };
  }
}

// ヘルパー関数
function encodeApprove(spender, amount) {
  // ERC20 approve(address,uint256) のエンコード
  const functionSelector = "0x095ea7b3";
  const encodedSpender = spender.slice(2).padStart(64, "0");
  const encodedAmount = amount.toString(16).padStart(64, "0");
  return functionSelector + encodedSpender + encodedAmount;
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

このコードでは、まずウォレットのケーパビリティを確認し、アトミック実行がサポートされている場合のみatomicRequired: trueを設定しています。 バッチ送信後はステータスコード100(保留中)の間ポーリングを続け、最終的なステータスに応じて結果を返します。 encodeApprove関数はERC20のapprove関数のcalldataをエンコードするヘルパー関数です。

フォールバック処理の例

アトミック実行がサポートされていない場合のフォールバック処理の例を以下に示します。 この例では、ケーパビリティに応じてアトミック実行と順次実行を切り替えます。

javascript
async function executeWithFallback(provider, userAddress, calls) {
  const capabilities = await provider.request({
    method: "wallet_getCapabilities",
    params: [userAddress, ["0x1"]]
  });

  const atomicStatus = capabilities["0x1"]?.atomic?.status;

  if (atomicStatus === "supported") {
    // アトミック実行を使用
    return await executeAtomic(provider, userAddress, calls);
  } else if (atomicStatus === "ready") {
    // ユーザーにアップグレードを提案
    const userWantsUpgrade = await promptUserForUpgrade();
    if (userWantsUpgrade) {
      try {
        return await executeAtomic(provider, userAddress, calls);
      } catch (error) {
        if (error.code === 5750) {
          // ユーザーがアップグレードを拒否した場合
          console.log("User rejected upgrade, falling back to sequential");
          return await executeSequential(provider, userAddress, calls);
        }
        throw error;
      }
    } else {
      return await executeSequential(provider, userAddress, calls);
    }
  } else {
    // アトミック実行非対応 - 順次実行
    return await executeSequential(provider, userAddress, calls);
  }
}

async function executeAtomic(provider, userAddress, calls) {
  const { id } = await provider.request({
    method: "wallet_sendCalls",
    params: [{
      version: "2.0.0",
      chainId: "0x1",
      atomicRequired: true,
      from: userAddress,
      calls
    }]
  });
  return await waitForCompletion(provider, id);
}

async function executeSequential(provider, userAddress, calls) {
  const { id } = await provider.request({
    method: "wallet_sendCalls",
    params: [{
      version: "2.0.0",
      chainId: "0x1",
      atomicRequired: false,
      from: userAddress,
      calls
    }]
  });
  return await waitForCompletion(provider, id);
}

async function waitForCompletion(provider, batchId) {
  let status;
  do {
    await sleep(2000);
    status = await provider.request({
      method: "wallet_getCallsStatus",
      params: [batchId]
    });
  } while (status.status === 100);
  return status;
}

async function promptUserForUpgrade() {
  return confirm("スマートアカウントにアップグレードしてアトミック実行を有効にしますか?");
}

このコードでは、executeWithFallback関数がケーパビリティを確認し、supportedの場合はアトミック実行、readyの場合はユーザーにアップグレードを提案、unsupportedの場合は順次実行を行います。 executeAtomicとexecuteSequentialはそれぞれatomicRequiredの値を変えてバッチを送信し、waitForCompletionで完了を待ちます。

命名の理由

メソッド名がwallet_sendCallsとなっている理由について説明します。

「transaction」ではなく「calls」という用語を使用しているのは、ウォレットの種類によって結果が異なるためです。 EOAウォレットでは、複数の呼び出しが複数のトランザクションになる可能性があります。 一方、スマートアカウントでは、複数の呼び出しが単一のトランザクションになる可能性があります。

「calls」という用語を使用することで、実装の詳細(トランザクション数)に依存しない抽象化が可能になります。

ガス制限について

EIP5792では、呼び出しごとのガス制限は定義されていません。 これは、ERC4337のUserOperationセマンティクスとの互換性を確保するためです。

ERC4337では、全ての呼び出しに対して単一のガス制限が使用されるため、呼び出しごとのガス制限は意味をなしません。 ウォレットがガス制限を自動的に見積もることが期待されています。

後方互換性

EIP5792は新しいJSON-RPCメソッドを定義するものであり、既存のメソッドを変更するものではありません。 そのため、後方互換性の問題はありません。

既存のアプリケーションは引き続きeth_sendTransactionを使用でき、新しい機能を利用したいアプリケーションはEIP5792のメソッドを採用できます。 既存のコードとの共存が可能であり、段階的な移行もできます。 ウォレットのサポート状況に応じてフォールバック処理を実装することで、両方のメソッドを適切に使い分けることができます。

セキュリティ

バッチIDの予測不可能性

バッチIDが予測可能な場合、悪意のある第三者がユーザーのトランザクション情報を推測できる可能性があります。 この問題に対処するため、バッチ識別子は予測不可能である必要があります。 ウォレットは暗号学的に安全な乱数を使用してバッチIDを生成する必要があります。

情報漏洩

wallet_getCallsStatusのケーパビリティレスポンスに機密データが含まれる可能性があります。 この問題に対処するため、ウォレットはケーパビリティレスポンスに機密情報を含めないようにする必要があります。

アトミック性の仮定

開発者がアトミック実行を仮定してコードを書くと、実際にはアトミックでない実行が行われた場合に問題が発生する可能性があります。 この問題に対処するため、開発者はatomicステータスの主張に関係なく、アトミック性を仮定すべきではありません。 実装によっては、複数のトランザクションにまたがってバッチが実行される可能性があります。

atomicRequired: trueを指定しても、実際にアトミック実行されたかどうかはwallet_getCallsStatusのatomicフィールドで確認する必要があります。

プライバシー考慮事項

EIP5792では、ユーザーの匿名性を保護するために以下の点が強調されています。

ウォレットは、機能がサポートされていないことと、権限がないことを区別する必要があります。 また、信頼されていない呼び出し元にケーパビリティを公開しないようにする必要があります。 過度なクエリやケーパビリティの過剰共有は、ユーザーのフィンガープリンティングにつながる可能性があるため、避ける必要があります。

引用

最後に

今回は「EIP5792による複数のオンチェーン呼び出しをバッチ送信するWallet Call API」についてまとめてきました! いかがだったでしょうか?

EIP5792は、現代のウォレットとアプリケーション間の通信を大幅に改善する提案です。 特にスマートアカウントを使用している場合、複数のトランザクションを1つにまとめてアトミックに実行できるため、ユーザー体験とガス効率が向上します。

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

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