レッスン2 — 本物の Provider トレイトを読む
問い
素朴な RPC から本物のトレイトの形まで Provider を組み立ててきた。今度はソースを開く — 各行は組み立てステップにどう対応するか + 戻り値型の機構(ProviderCall / RpcWithBlock / EthCall / PendingTransactionBuilder)の存在理由は?
原理(最小モデル)
Send + Syncスーパートレイト. 本番ユーザーはArc<P>をワーカータスクへクローン、これがないとコンパイル不可。auto_impl(&, &mut, Box, Rc, Arc)の 5 種.Providerは状態読み取りに&selfのみ → 5 種すべてが動く。Database(revm)の&mut self制約とは非対称。root()だけが必須. ほかは全部デフォルト実装、ラッパーはself.inner.root()の 1 行で済む。client()とweak_client()の 2 種. 短命 RPC は client(強参照、ライフタイム束縛)/ 長寿命タスク(サブスクリプション、tokio::spawn)は weak_client(Weak 参照、drop を妨げない)。- 3 戻り値型パターン.
ProviderCall(パラメータなし)/RpcWithBlock(ブロック選択)/EthCall(多次元オプション)。各々が対応 RPC のオプション構造に合うビルダー。 SendTransactionの複数ステップ. 1. 送信 → 2. 確認数設定 → 3. await でレシート。「送信 → 採掘 → 確定」ステートマシン。
具体例
トレイトヘッダー:
#[auto_impl(&, &mut, Box, Rc, Arc)]
pub trait Provider<N: Network = Ethereum>: Send + Sync {
fn root(&self) -> &RootProvider<N>;
fn client(&self) -> ClientRef<'_> {
self.root().client()
}
fn weak_client(&self) -> WeakClient {
self.root().weak_client()
}
// ...多数の RPC メソッド、すべてデフォルト実装
}
Database vs Provider の auto_impl 非対称:
| トレイト | auto_impl | メソッド signature |
|---|---|---|
Database(revm) | &mut, Box | &mut self — キャッシュその場書き換え |
Provider(alloy) | &, &mut, Box, Rc, Arc | &self — キャッシュは内部可変性で |
Database は &mut self → &/Rc/Arc 使えない(&mut T 取り出せない)。Provider は &self → 5 種全部 OK。
3 戻り値型パターン:
get_block_number — パラメータなし、シンプル結果:
fn get_block_number(&self) -> ProviderCall<NoParams, U64, BlockNumber> {
self.client().request_noparams("eth_blockNumber").into()
}
ProviderCall<P, R, F> = future ビルダー、P = パラメータ型、R = 生応答型、F = ユーザー型。impl Future ではなく builder にすることで全 RPC メソッドが同じ形 + カスタマイズの仕組み常備。
get_balance — ビルダー経由でブロック選択:
fn get_balance(&self, address: Address) -> RpcWithBlock<Address, U256> {
RpcWithBlock::new_provider(move |block| {
self.client().request("eth_getBalance", (address, block)).into()
})
}
RpcWithBlock がブロック選択ビルダー:
provider.get_balance(addr).await // latest(デフォルト)
provider.get_balance(addr).block_id(1_000_000.into()).await // 過去
provider.get_balance(addr).hash(some_hash).await // ブロックハッシュ
provider.get_balance(addr).pending().await // pending ブロック
call — カスタマイズの幅大:
fn call(&self, tx: N::TransactionRequest) -> EthCall<N> {
EthCall::new(self.client(), tx)
}
EthCall チェーン可能メソッド:
.block(BlockId)— 特定ブロックに対する eth_call.overrides(state_overrides)— 状態オーバーライド付き(アカウントなりすまし、残高 / コード上書き).gas(...)、.value(...)、.from(...)— tx 補正
3 戻り値型と RPC オプション構造の対応:
| RPC メソッド | オプションパラメータ | 戻り値型 |
|---|---|---|
eth_blockNumber | なし | ProviderCall(await するだけ) |
eth_getBalance | block | RpcWithBlock(オプション次元 1 つ) |
eth_call | block, from, gas, value, state | EthCall(オプション次元が多数) |
デフォルト実装パターン:
fn get_X(&self, args...) -> SomeReturnType {
self.client().request("rpc_methodName", args).into()
}
クライアント経由でリクエスト構築 → .into() で適切なビルダー / future 型へ変換 → ラッパーが変えたい部分以外をオーバーライドしなくて良い 理由。
send_transaction の複数ステップ:
fn send_transaction(&self, tx: N::TransactionRequest) -> SendTransaction<N>
ステートマシン:
provider.send_transaction(tx).await? // tx ハッシュのみ、待たない
provider.send_transaction(tx).with_required_confirmations(3).get_receipt().await? // 3 ブロック待つ
失敗例(誤解)
「Provider も Database も同じ auto_impl リスト」— 間違い。Database = &mut, Box(2 種)/ Provider = &, &mut, Box, Rc, Arc(5 種)。差異は &mut self vs &self のメソッド signature、auto_impl リストは契約の厳密宣言。
「get_balance は impl Future<Output = U256> を返せば十分」— 間違い。RpcWithBlock で await 前にブロック選択カスタマイズ可能、全 RPC が同じビルダー形 → カスタマイズ仕組みが常備。
「weak_client() は client() の劣化版」— 間違い。長寿命タスク(サブスクリプション、tokio::spawn)用、Weak 参照は drop を妨げない → プロバイダ drop でタスクが気づいてシャットダウン。
ステップで組み立てる
Step 1: トレイトヘッダーの 3 注目点
Send + Sync + auto_impl(5 種) + root() のみ必須。
Step 2: Database vs Provider 非対称の理由
&mut self vs &self のメソッド signature。
Step 3: client() と weak_client() の使い分け
短命 RPC = client / 長寿命タスク = weak_client。
Step 4: 3 戻り値型パターン
ProviderCall(オプション 0)/ RpcWithBlock(オプション 1 次元)/ EthCall(多次元)。
Step 5: デフォルト実装パターン
self.client().request(name, args).into() の 1 行、ラッパーは変える部分のみ override。
Step 6: send_transaction ステートマシン
送信 → 確認数設定 → await でレシート。
答え合わせ
ProviderとDatabaseの auto_impl 非対称の決め手: メソッド signature の違い。Database::basic(&mut self, addr)は&mut→&/Rc/Arcから取り出せない。Provider::get_balance(&self, addr)は&self→ 5 種すべて OK。キャッシュ戦略の選択(その場書き換え vs 内部可変性)が auto_impl 幅を決める。get_balanceがimpl FutureでなくRpcWithBlockを返す理由: ほとんどの呼び出しはlatestだが、ビルダーパターンで.block_id(N)/.hash(h)/.pending()で過去 / ハッシュ / pending を選択可能 → メソッド 1 つで問い合わせ方多数。関数引数でblock_idを渡す設計だと全メソッドが煩雑化。SignerProviderラッパーが書くメソッド数:send_transactionの 1 つだけ(tx 署名 + inner に転送)。残り 29 はトレイトのデフォルト実装に任せる → それらはself.root()経由でトランスポート取得。30 メソッド本体書く代わりに 1 個。
合格基準
auto_implの 5 種を即答できる。DatabasevsProviderの非対称を signature 差で説明できる。client()とweak_client()の使い分けを言える。- 3 戻り値型と RPC オプション構造の対応を言える。
send_transactionステートマシン 3 ステップを順に言える。
まとめ(3行)
Providerヘッダー =Send + Sync+auto_impl(5 種、Databaseの 2 種と非対称)+root()のみ必須 + デフォルト実装 30 メソッド。- 3 戻り値型(
ProviderCall/RpcWithBlock/EthCall)が各 RPC のオプション構造に合うビルダー、await 前カスタマイズで「1 メソッド + 問い合わせ方多数」を実現。 SignerProviderはsend_transaction1 つだけ override、残り 29 はデフォルト実装がself.root()経由で動く = ラッパー作者の 30 メソッド本体書きが 1 個に。