mcpField
E
evm-rpc

evm-rpc

github.com/dfinity開発39281 回インストール今日更新
リポジトリ

evm-rpc について

Internet Computer の canister から EVM RPC canister を介して Ethereum や EVM 互換チェーンを呼び出すためのスキルです。ネットワーク選択、合意処理、Rust クライアントの利用、デプロイ上の注意点を扱います。

主な機能

  1. 01Ethereum と EVM チェーンの JSON-RPC 呼び出し
  2. 02複数プロバイダーの結果照合
  3. 03既定チェーンとカスタムチェーンに対応
  4. 04型付き Rust クライアントの利用
  5. 05cycles と応答サイズの扱いを説明

活用シーン

  1. 01残高などのオンチェーンデータ取得
  2. 02IC canister から EVM 互換ネットワークを照会
  3. 03RPC プロバイダー間の不一致への対応

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dfinity/icskills --skill evm-rpc

インストール後、Claude Code で/evm-rpcを入力して読み込まれたか確認します。

詳細

EVM RPC Canister — IC から Ethereum を呼び出す

概要

EVM RPC canister は、HTTPS アウトコールを介して Ethereum および EVM 互換チェーンに JSON-RPC 呼び出しをプロキシする IC システム canister です。お使いの canister が EVM RPC canister にリクエストを送信すると、EVM RPC canister は複数の RPC プロバイダーにリクエストを分散し、応答を比較して合意を確認したうえで、結果を返します。デフォルトのプロバイダーでは API キーは不要です。ブリッジやオラクルも必要ありません。

前提条件

  • evm_rpc_client crate(型付きクライアント API を提供し、evm_rpc_types を再エクスポートします)

Canister ID

CanisterIDサブネット
EVM RPC(メインネット)7hfb6-caaaa-aaaar-qadga-cai34 ノードのフィデューシャリー

Candid インターフェース:https://github.com/dfinity/evm-rpc-canister/releases/latest/download/evm_rpc.did — または canhelp スキルを使って、メインネットの canister から直接取得してください。

対応チェーン

チェーンChain IDCandidRust
Ethereum Mainnet1variant { EthMainnet }RpcServices::EthMainnet
Ethereum Sepolia11155111variant { EthSepolia }RpcServices::EthSepolia
Arbitrum One42161variant { ArbitrumOne }RpcServices::ArbitrumOne
Base Mainnet8453variant { BaseMainnet }RpcServices::BaseMainnet
Optimism Mainnet10variant { OptimismMainnet }RpcServices::OptimismMainnet
カスタム EVM チェーン任意variant { Custom }RpcServices::Custom

RPC プロバイダー

組み込みプロバイダー(デフォルトでは API キー不要):

プロバイダーEthereumSepoliaArbitrumBaseOptimism
Alchemyはいはいはいはいはい
Ankrはい-はいはいはい
BlockPiはいはいはいはいはい
Cloudflareはい----
LlamaNodesはい-はいはいはい
PublicNodeはいはいはいはいはい

Cycle コスト

計算式:

(5_912_000 + 60_000 * nodes + 2400 * request_bytes + 800 * max_response_bytes) * nodes * rpc_count

ここで、nodes = 34(フィデューシャリーサブネット)、rpc_count = クエリするプロバイダー数です。

実用上の目安: 初期予算として 10_000_000_000 cycles(10B)を送信してください。未使用の cycles は返金されます。通常の呼び出しでは 100M〜1B cycles(約 $0.0001〜$0.001 USD)がかかります。

呼び出しの前に requestCost を使うと、正確な見積もりを取得できます。

注意点

  1. 送信する cycles が不足しています。 EVM RPC の呼び出しにはすべて cycles を添付する必要があります。evm_rpc_client は呼び出しごとにデフォルトで 10B cycles を使用しますが、.with_cycles() で上書きした場合、cycles が少なすぎると、失敗が通知されないまま処理が失敗したり、トラップが発生したりします。

  2. デフォルトの Equality 合意方式を使用しています。 デフォルトの合意方式では、すべてのプロバイダーが同一の応答を返す必要があります。プロバイダー間で 1〜2 ブロックのずれがよくある eth_getBlockByNumber(Latest) のようなクエリでは、この方式は失敗します。ほとんどの用途では、ConsensusStrategy::Threshold { total: Some(3), min: 2 }(3 件中 2 件の一致)を使用してください。

  3. Inconsistent 結果バリアントを無視しています。 閾値合意を使用しても、プロバイダー間の不一致が閾値を超える場合があります。複数プロバイダーへの呼び出しは、MultiRpcResult::Consistent(result) または MultiRpcResult::Inconsistent(results) を返します。プロバイダー間で不一致があると canister がトラップするため、両方の分岐を必ず処理してください。

  4. 誤ったチェーンバリアントを使用しています。 RpcServices::EthMainnet は Ethereum L1 用です。Arbitrum には RpcServices::ArbitrumOne、Base には RpcServices::BaseMainnet を使用してください。誤ったバリアントを使うと、別のチェーンにクエリします。

  5. 応答サイズの制限があります。 大きな応答(例:広範なフィルターを指定した eth_getLogs)は、最大応答サイズを超える可能性があります。クライアントビルダーで .with_response_size_estimate() を使用してください。使用しない場合、呼び出しは失敗します。

  6. 署名する前に eth_sendRawTransaction を呼び出しています。 EVM RPC canister はトランザクションに署名しません。ご自身でトランザクションに署名し(IC 管理 canister を介した閾値 ECDSA を使用)、署名済みの生バイト列を渡す必要があります。

  7. EVM RPC の Candid 型を手動で定義しています。 型付きクライアント API を提供し、evm_rpc_types のすべての Candid 型を再エクスポートする evm_rpc_client crate を使用してください。手動で定義した型は canister の実際のインターフェースとのずれが生じ、実行時に IC0503 デコードトラップを引き起こします。

実装

icp.yaml の設定

evm_rpc キャニスター定義が必要なのはローカル開発時のみです。ローカルレプリカには EVM RPC キャニスターがあらかじめインストールされていないため、ビルド済みの WASM から独自のコピーをデプロイします。メインネットでは、DFINITY が管理するキャニスターがすでに 7hfb6-caaaa-aaaar-qadga-cai にデプロイされているため、独自のインスタンスをデプロイしないでください。環境を使って、デプロイ先ごとにデプロイするキャニスターを制御します。

canisters:
  - name: backend
    recipe:
      type: "@dfinity/[email protected]"
  - name: evm_rpc
    build:
      steps:
        - type: pre-built
          url: https://github.com/dfinity/evm-rpc-canister/releases/latest/download/evm_rpc.wasm.gz
    init_args: "(record {})"

environments:
  - name: local
    network: local
    canisters: [backend, evm_rpc]
  - name: ic
    network: ic
    canisters: [backend]
    settings:
      backend:
        environment_variables:
          PUBLIC_CANISTER_ID:evm_rpc: "7hfb6-caaaa-aaaar-qadga-cai"
Rust

evm_rpc_client クレートは、EVM RPC キャニスターを呼び出すための型付きクライアント API を提供します。サイクルの付与、引数のエンコード、レスポンスのデコードを処理します。すべての Candid 型は evm_rpc_types から再エクスポートされています。Alloy エコシステムを使用するプロジェクトでは、alloy Cargo 機能を有効にすると、Alloy ネイティブのリクエスト型とレスポンス型を使用できます。

Cargo.toml
[package]
name = "evm_rpc_backend"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
evm_rpc_client = "0.4"
evm_rpc_types = "3"
ic-canister-runtime = "0.2"
ic-cdk = "0.20"
candid = "0.10"
serde_json = "1"
src/lib.rs
use candid::Principal;
use evm_rpc_client::{CandidResponseConverter, EvmRpcClient, NoRetry};
use evm_rpc_types::{
    Block, BlockTag, CallArgs, ConsensusStrategy,
    Hex, Hex20, Hex32, MultiRpcResult, RpcServices, SendRawTransactionStatus,
};
use ic_canister_runtime::IcRuntime;
use ic_cdk::update;
use std::str::FromStr;

// Resolve the EVM RPC canister ID from environment variable injected by icp-cli.
// Locally: auto-injected with the locally deployed evm_rpc canister ID.
// Mainnet: set explicitly in icp.yaml to the well-known canister ID.
fn client() -> EvmRpcClient<IcRuntime, CandidResponseConverter, NoRetry> {
    let canister_id = Principal::from_text(
        ic_cdk::api::env_var_value("PUBLIC_CANISTER_ID:evm_rpc"),
    )
    .expect("Invalid principal in PUBLIC_CANISTER_ID:evm_rpc");

    EvmRpcClient::builder(IcRuntime::new(), canister_id)
        .with_rpc_sources(RpcServices::EthMainnet(None))
        .with_consensus_strategy(ConsensusStrategy::Threshold {
            total: Some(3),
            min: 2,
        })
        .build()
}

// -- Get ETH balance via raw JSON-RPC --
// Returns hex-encoded wei (e.g., "0xde0b6b3a7640000" = 1 ETH).
// Parse hex to u128, divide by 10^18 to get ETH.

#[update]
async fn get_eth_balance(address: String) -> String {
    let json = serde_json::json!({
        "jsonrpc": "2.0",
        "method": "eth_getBalance",
        "params": [address, "latest"],
        "id": 1
    });

    client()
        .multi_request(json)
        .send()
        .await
        .expect_consistent()
        .unwrap_or_else(|err| ic_cdk::trap(&format!("RPC error: {:?}", err)))
}

// -- Get latest block via typed API --

#[update]
async fn get_latest_block() -> Block {
    let result = client()
        .get_block_by_number(BlockTag::Latest)
        .send()
        .await;

    match result {
        MultiRpcResult::Consistent(Ok(block)) => block,
        MultiRpcResult::Consistent(Err(err)) => {
            ic_cdk::trap(&format!("RPC error: {:?}", err))
        }
        MultiRpcResult::Inconsistent(_) => {
            ic_cdk::trap("Providers returned inconsistent results")
        }
    }
}

// -- Read ERC-20 balance via eth_call --
// Returns hex-encoded uint256. Divide by 10^decimals (e.g., 6 for USDC, 18 for DAI).
// Function selector for balanceOf(address): 0x70a08231
// Pad address to 32 bytes (remove 0x prefix, left-pad with zeros)

#[update]
async fn get_erc20_balance(token_contract: String, wallet_address: String) -> String {
    let addr = wallet_address.trim_start_matches("0x");
    let calldata = format!("0x70a08231{:0>64}", addr);

    let args = CallArgs {
        transaction: evm_rpc_types::TransactionRequest {
            to: Some(Hex20::from_str(&token_contract).unwrap()),
            input: Some(Hex::from_str(&calldata).unwrap()),
            ..Default::default()
        },
        block: None,
    };

    let result = client()
        .call(args)
        .send()
        .await
        .expect_consistent()
        .unwrap_or_else(|err| ic_cdk::trap(&format!("eth_call error: {:?}", err)));

    result.to_string()
}

// -- Send a signed raw transaction --

#[update]
async fn send_raw_transaction(signed_tx_hex: String) -> SendRawTransactionStatus {
    client()
        .send_raw_transaction(Hex::from_str(&signed_tx_hex).unwrap())
        .send()
        .await
        .expect_consistent()
        .unwrap_or_else(|err| ic_cdk::trap(&format!("RPC error: {:?}", err)))
}

// -- Get transaction receipt --

#[update]
async fn get_transaction_receipt(tx_hash: String) -> evm_rpc_types::TransactionReceipt {
    client()
        .get_transaction_receipt(Hex32::from_str(&tx_hash).unwrap())
        .send()
        .await
        .expect_consistent()
        .unwrap_or_else(|err| ic_cdk::trap(&format!("RPC error: {:?}", err)))
        .unwrap_or_else(|| ic_cdk::trap("Transaction receipt not found"))
}

// -- Override chain per request (e.g., Arbitrum instead of the client default) --

#[update]
async fn get_arbitrum_block() -> Block {
    client()
        .get_block_by_number(BlockTag::Latest)
        .with_rpc_sources(RpcServices::ArbitrumOne(None))
        .send()
        .await
        .expect_consistent()
        .unwrap_or_else(|err| ic_cdk::trap(&format!("RPC error: {:?}", err)))
}

ic_cdk::export_candid!();

デプロイとテスト

# Local: deploys both backend and evm_rpc (as defined in the local environment)
icp network start -d
icp deploy -e local

# Mainnet: deploys only backend (evm_rpc is excluded from the ic environment)
icp deploy -e ic
icp CLI でテストする
# Set up variables
export CYCLES=10000000000

# Get ETH balance (raw JSON-RPC via single provider)
icp canister call evm_rpc request '(
  variant { EthMainnet = variant { PublicNode } },
  "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getBalance\",\"params\":[\"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045\",\"latest\"],\"id\":1}",
  1000
)' --with-cycles=$CYCLES

# Get latest block (typed API, multi-provider)
icp canister call evm_rpc eth_getBlockByNumber '(
  variant { EthMainnet = null },
  null,
  variant { Latest }
)' --with-cycles=$CYCLES

# Get transaction receipt
icp canister call evm_rpc eth_getTransactionReceipt '(
  variant { EthMainnet = null },
  null,
  "0xdd5d4b18923d7aae953c7996d791118102e889bea37b48a651157a4890e4746f"
)' --with-cycles=$CYCLES

# Check available providers
icp canister call evm_rpc getProviders

# Estimate cost before calling
icp canister call evm_rpc requestCost '(
  variant { EthMainnet = variant { PublicNode } },
  "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getBalance\",\"params\":[\"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045\",\"latest\"],\"id\":1}",
  1000
)'

コメント