mcpField
E
evm-rpc

evm-rpc

github.com/dfinity开发39281 次安装今天更新
代码仓库

关于 evm-rpc

用于指导 Internet Computer canister 通过 EVM RPC canister 调用 Ethereum 及兼容链的技能,涵盖网络选择、共识处理、Rust 客户端和部署要点。

主要功能

  1. 01调用 Ethereum 和 EVM 链的 JSON-RPC
  2. 02查询多个提供商并比较结果
  3. 03支持指定链和自定义链
  4. 04使用类型化 Rust 客户端
  5. 05说明 cycles 与响应大小处理

适用场景

  1. 01读取余额等链上数据
  2. 02从 IC canister 查询 EVM 兼容网络
  3. 03处理 RPC 提供商结果不一致

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/evm-rpc确认已加载。

详细介绍

EVM RPC Canister — 从 IC 调用 Ethereum

这是什么

EVM RPC canister 是一个 IC 系统 canister,可通过 HTTPS 外部调用将 JSON-RPC 请求代理到 Ethereum 和兼容 EVM 的链。你的 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 主网1variant { EthMainnet }RpcServices::EthMainnet
Ethereum Sepolia11155111variant { EthSepolia }RpcServices::EthSepolia
Arbitrum One42161variant { ArbitrumOne }RpcServices::ArbitrumOne
Base 主网8453variant { BaseMainnet }RpcServices::BaseMainnet
Optimism 主网10variant { 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 过少会导致静默失败或触发 trap。

  2. 使用默认的 Equality 共识策略。 默认共识策略要求所有提供商返回完全相同的响应。对于 eth_getBlockByNumber(Latest) 这类查询,这通常会失败,因为各提供商的区块高度往往相差 1-2 个区块。大多数情况下应使用 ConsensusStrategy::Threshold { total: Some(3), min: 2 }(3 个中有 2 个达成一致)。

  3. 忽略 Inconsistent 结果变体。 即使使用阈值共识,提供商仍可能出现超出阈值的分歧。多提供商调用会返回 MultiRpcResult::Consistent(result) 或 MultiRpcResult::Inconsistent(results)。务必处理这两种情况,否则提供商发生分歧时,你的 canister 会触发 trap。

  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 类型。 使用 evm_rpc_client crate,它提供类型化客户端 API,并重新导出 evm_rpc_types 中的所有 Candid 类型。手动定义的类型会与 canister 的实际接口不一致,导致运行时出现 IC0503 解码 trap。

实现

icp.yaml 配置

evm_rpc canister 定义仅用于本地开发——本地 replica 未预装 EVM RPC canister,因此你需要使用预构建的 WASM 部署自己的副本。在主网上,DFINITY 维护的 canister 已部署在 7hfb6-caaaa-aaaar-qadga-cai——请勿部署自己的实例。使用环境控制各个 canister 的部署位置:

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 crate 提供了用于调用 EVM RPC canister 的类型化客户端 API。它负责处理 cycles 附加、参数编码和响应解码。所有 Candid 类型都从 evm_rpc_types 重新导出。对于使用 Alloy 生态系统的项目,可启用 alloy Cargo feature,以使用 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
)'

评论

支持 Markdown