Fragments of verbose memory

冗長な記憶の断片 - Web技術のメモをほぼ毎日更新

Oct 31, 2026 - 日記

fork-wallet 0.3.0: dAppの異常系を検証するヘッドレスウォレット

fork-wallet 0.3.0: dAppの異常系を検証するヘッドレスウォレット

dAppテスト専用のヘッドレスウォレット、fork-wallet 0.3.0をMITライセンスで公開しました。 ブラウザ上のdAppに対して、承認待ちや拒否、送信後の応答消失をPlaywright から指定できます。 ウォレットの承認画面を操作せず、テストコードでその判断とタイミングを制御する仕組みです。

ウォレットがエラーを返しても、トランザクションが送られていないとは限りません。 このとき、dAppが何も確認せずに送り直すと、同じ操作を二重に実行する可能性があります。 承認したトランザクションはローカルのEthereum実行環境で動かすため、画面の表示とチェーン上の結果を突き合わせられます。 実ウォレットの接続確認に加えて、アプリ側の異常時の処理を繰り返し検証したい開発者に向けて作りました。

接続して送れるだけでは足りない理由

dAppの操作には、ユーザーが承認するまでの待ち時間があります。 その間に接続権限が取り消されたり、アカウントやチェーンが変わったりしたら、アプリは処理中の表示と送信内容を見直す必要があります。 接続と送信の成功だけを確かめても、こうした途中の変化を処理できるかは分かりません。

さらに、送信の成否と、アプリが結果を受け取れたかは別です。 例えば、ノードがトランザクションを受け付けた後、アプリへハッシュが届く前に応答が失われる状況があります。 画面にはエラーが出ていても、チェーンでは処理が進んでいるかもしれません。 再送を許す前に、送信履歴やコントラクトの状態を照合する設計が必要です。

この確認を、成功応答やエラー応答の差し替えだけで済ませると、実際の残高やコントラクトの状態変化まで検証したことにはなりません。 fork-walletでは、ウォレットの応答を制御しながら、ローカルチェーン上では実際に処理を実行する組み合わせを用意しました。 アプリがどう表示したかに加えて、送信が何件あったか、残高や状態がどう変わったかをテストで確かめます。

Synpress、dAppwright、web3-mockとの違い

既存のツールでも、ウォレット接続や送信のテストはできます。 fork-walletを選ぶ理由は「ウォレットを自動操作できるから」だけではありません。 テストで確かめたい対象を、実ウォレットの画面、RPCの応答、アプリとチェーンの整合性に分けると違いが見えます。

Synpress はPlaywrightとCypressを使ったWeb3のE2Eテストを提供し、MetaMaskの操作やAnvilとの連携を扱います。 dAppwright は、PlaywrightからMetaMaskやCoinbase Walletの拡張機能を操作するライブラリです。 どちらも、実ウォレットとの接続や承認画面を含めた確認に使えます。

ここでいうヘッドレスウォレットは、ブラウザを非表示で動かすこととは区別しています。 例えばdAppwrightにもヘッドレス実行がありますが、操作する対象は実ウォレットの拡張機能です。 fork-walletはその画面を介さず、注入したproviderへの判断をテストコードから指定します。 なお、Synpressにはウォレットのモックを使うAPI もあります。 ここでは実ウォレットを操作する使い方を中心に比較します。

一方、@depay/web3-mock は、ウォレットやRPCの応答をモックに置き換えるライブラリです。 アカウント、残高、コントラクト呼び出しなどに応答を用意し、画面の分岐を試す用途に向いています。

ツール 主に制御する対象 テストで確かめること fork-walletとの使い分け
Synpress MetaMask操作、ブラウザ設定、Anvilなど 実ウォレットを含むdAppの一連の操作 MetaMask固有の接続や承認UIを確認したいとき
dAppwright MetaMaskやCoinbase Walletの拡張機能 Playwrightによる実ウォレットとの連携 対象ウォレットの画面を実際に操作したいとき
web3-mock ウォレットやRPCが返す応答 指定した結果に対する画面や処理の分岐 チェーンで実行せず、応答を固定して試したいとき
fork-wallet 注入したproviderの承認状態と応答 異常時のアプリの挙動と、ローカルAnvil上の実行結果 承認待ちや応答消失を制御し、送信結果まで照合したいとき

これは速度や対応範囲の順位表ではありません。 競合を同じ条件で実行した性能比較は行っていません。 また、他のツールで同等の試験を組めないという主張でもありません。 比較は各プロジェクトの公式資料に記載された用途に基づきます。

なお、ローカルEthereumノードのAnvil はfork-walletが使う実行環境です。 Anvilはチェーンの状態やブロック生成を制御しますが、それだけでブラウザウォレットの承認待ちを制御するものではありません。 fork-walletがアプリとAnvilの間に入り、ウォレットへの要求をいつ承認するか、どの応答をアプリへ返すかをテストから指定します。

承認待ちと送信済みの状態を分けて制御

fork-walletは、ブラウザからウォレットを呼び出す共通インターフェースのEIP-1193 に沿ったproviderを注入します。 複数のウォレットを検出するためのEIP-6963 にも対応しています。 この共通インターフェースを通して、dApp側はウォレットに要求を送り、テスト側はその要求への判断を制御します。

トランザクション送信に対する主な指定は次の4つです。 approveとlostの行は、入力検証や送信前チェックを通り、ノードがハッシュを返した場合の動作です。 承認を指定しても、チェックに失敗すれば送信されません。

モード ローカルノードへの送信 アプリが受け取る結果
approve 送信する ノードが返したトランザクションハッシュ
reject 送信しない 拒否を表すエラー4001
hold 保留中は送信しない テストが承認や拒否を決めるまで待つ
lost 送信する ハッシュを渡さずエラー-32603を返す

holdはウォレットの承認待ちです。 送信後にブロックへ取り込まれるのを待つ状態とは区別します。 保留中にアプリが二重操作を防ぐか、アカウント変更後に古い要求を破棄するか、といった確認に使います。

接続権限の取り消し、切断と再接続、アカウント変更、事前登録したチェーンへの切り替えもテストから操作できます。 ただし、すでにノードへの送信が始まったトランザクションは、後から切断しても取り消せません。 テストでも、送信前に止めた要求と送信後の処理を区別して扱います。

初期設定では自動承認するため、人間が承認した証拠にはなりません。 拒否や保留を試すときは、dAppの操作を始める前にモードを指定します。

personal_signとeth_signTypedData_v4による署名も扱えますが、標準では無効です。 有効にするには署名内容を検証する専用のポリシーが必要で、チェーンIDは31337か31338に限られます。 署名を試す場合も使い捨てのアカウントだけを使い、署名データや署名結果をログやトレースに残さないでください。

npmから0.3.0を導入する

npmのfork-wallet 0.3.0 を導入します。 Node.js 22以上とnpmを用意し、テスト用プロジェクトのディレクトリで次を実行します。 fork-walletとPlaywrightを開発用依存として追加し、Chromiumをインストールします。 Anvil 1.8.3は別途インストールし、PATHから実行できる状態にしてください。 付属の起動ヘルパーにはlsofも必要です。 資格情報に関係する環境変数があると起動を拒否するため、テスト用の環境を分けて実行します。 常用するシェルの認証設定を書き換える必要はありません。

1
2
npm install --save-dev [email protected] [email protected]
npx playwright install chromium

npm ls fork-wallet playwrightで導入した版を確認できます。 設定と完全な実行例はREADME 、各メソッドとオプションはAPI資料 を参照してください。 0.1.1から更新する場合はAPIが異なるため、API資料の移行手順も確認してください。

lostで応答だけを失わせる例

0.3.0のlostを使い、dAppが受け取る応答とチェーン上の結果を照合します。 一時的なアカウントから1 weiを送り、ページがエラーを受け取っても実行が成功していることを確かめる例です。

以下はその制御と確認に絞った抜粋です。 walletはfork-walletをブラウザへ組み込んだ際に返る制御オブジェクト、pageは接続承認済みのページ、rpcはテスト側のローカルRPCクライアントです。 nodeは起動したAnvilの制御オブジェクト、recipientは使い捨ての送金先です。 assertとsleepにはNode.jsの標準ライブラリを使います。 ノード起動、送信内容の検証、終了処理は省略しているため、この断片だけでは実行できません。 送信後は実行結果の記録であるレシート、受取残高、送信元のnonce(送信順序を表す番号)を照合します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
wallet.nextRequest({ method: 'eth_sendTransaction', mode: 'lost' });

const code = await page.evaluate(async ({ from, to }) => {
  try {
    await window.ethereum.request({
      method: 'eth_sendTransaction',
      params: [{ from, to, value: '0x1', data: '0x' }],
    });
    return null;
  } catch (error) {
    return error.code;
  }
}, { from: node.account, to: recipient });

assert.equal(code, -32603);
assert.equal(wallet.submitted.length, 1);
let receipt;
for (let i = 0; i < 50; i++) {
  receipt = await rpc('eth_getTransactionReceipt', [wallet.submitted[0]]);
  if (receipt) break;
  await sleep(100);
}
assert.equal(receipt?.status, '0x1');

assert.equal(await rpc('eth_getBalance', [recipient, 'latest']), '0x1');
assert.equal(await rpc('eth_getTransactionCount', [node.account, 'latest']), '0x1');

この例の全体コードをmacOS環境で実行し、ページがエラーを受け取り、ローカルチェーンで1件の実行が成功することを確認しました。 受取残高は1 wei、送信元のnonceは1です。

ただし、wallet.submittedを参照できるのはテスト側です。 アプリへこの値を渡して復旧させたら、実際のアプリが自力で状態を照合できるかという確認を省いてしまいます。 自分のdAppのテストでは、直接providerを呼ぶ部分を画面操作に置き換え、アプリ自身の復旧処理を通した後でチェーンの結果と比較してください。

また、lostはテスト側がハッシュを取得できた状況を意図的に作る機能です。 実際のタイムアウトでは、テスト側にもハッシュが返らず、送信結果が不明な場合があります。 fork-walletはそうした結果をunknownとして区別しますが、再送や重複防止は実装しません。 アプリ側で、何を照合すれば再実行してよいと判断できるかを決める必要があります。

実ウォレットの検証は別に残す

fork-walletはMetaMaskの実装を再現するものではありません。 このテストが通っても、MetaMaskやWalletConnect、ハードウェアウォレットで同じ動作になることは保証しません。 fork-walletによるテストと併せて、実ウォレット固有の接続と画面操作をSynpressやdAppwrightなどで確認します。

SDKについても、すべての組み合わせへの互換性は主張していません。 0.3.0のREADMEで動作確認を記録しているSDK連携は、macOS上の次の組み合わせです。

  • ethers 6.17.0
  • viem 2.57.12
  • @wagmi/core 2.22.1
  • Playwright 1.56.1のChromium
  • Anvil 1.8.3

この確認は、Reactのコネクタを含む、あらゆるdAppへの対応を意味するものではありません。

使い捨てのローカル環境で動かす

安全のため、接続先はhttp://127.0.0.1:<port>に限定し、ページのオリジンも完全一致で指定します。 テスト側では、許可する宛先やcalldata、送金額をvalidateTransactionで検証し、beforeSendで自分が管理するノードかを確認します。 dAppには読み取り用のnode.publicEndpointを渡し、管理用のnode.endpointへアクセスさせないようにします。 beforeSendや検証コールバックは信頼するテストコードであり、暗号学的な所有証明ではありません。 これらはOSのサンドボックスの代わりにはならないため、実資産や本物の秘密鍵を持ち込まず、使い捨てのローカル環境で動かしてください。

名前に「fork」とありますが、付属のstartAnvilヘルパーが起動するのは通常のローカルチェーンです。 公開ネットワークのforkは独自のテスト環境で管理するもので、任意のDeFiアプリをそのまま動かせるという意味ではありません。

最初に増やすなら、送信後のエラーテスト

すでに接続と送信の正常系をテストしているなら、次は「送信済みなのに画面はエラー」のケースを加えてみてください。 確認するのは、エラーメッセージだけでなく、再操作を許す条件とチェーン上の実行件数です。

実ウォレットとの連携は実ウォレットで確かめつつ、承認待ちや応答消失のタイミングはテストから制御する。 fork-walletは、この異常時の確認をアプリ開発に加えるための道具です。 0.3.0のREADMEを参考に、既存の正常系テストへ1ケースずつ追加できます。 まずは使い捨てのローカル環境で、自分のdAppが送り直す前に何を確認しているかをテストしてみてください。