EVM RPC Canister

Hello devs! I am pleased to announce that the EVM RPC Canister is now live. Thank you all for your feedback and comments while we developed this service.

The canister is still in beta, and while we put it through some real-world battle testing, it will remain managed by DFINITY. Once we are confident it can serve the needs of all of your dapps safely and performantly, we will publish a GA release and put it under NNS control.

You can find the latest beta release here on github: Release 2024-01-31 (Beta) · internet-computer-protocol/ic-eth-rpc · GitHub

Big thank you to @THLO @gregory-demay @Manu and most of all @rvanasa for all of the hard work in getting this out. I don’t think I can give enough credit to @rvanasa — he really put his heart and soul into this to give you all a fast and easy way to bridge your canisters to EVM smart contracts. Incredible work, Ryan!

As you begin your integrations, please let us know of any issues you come across. We will address feedback as quickly as possible, but will give priority to bugs and security issues.

Thank you all and hear from you soon!

10 Likes

Hey I have a major question, does the canister rely on making on request to one Ethereum provider? Or does it make multiple requests and compare them? I don’t see any way to control that so far in the Candid file.

@THLO @gregory-demay @Manu @rvanasa

If there is no capability to use multiple providers and have the responses compared, I would like to understand the reasoning and suggest that this be implemented so that applications can choose to increase the security of their Ethereum integration until we have a direct integration.

This is the case specifically for the candid RPC methods. The general RPC endpoint accepts a provider id as a param and will leave the normalization / canonicalization up to the developer.

Is there something that isn’t obvious when you run one of the candid RPC methods? Perhaps @rvanasa can help out

By default, the canister makes requests to 3 different JSON-RPC providers (Cloudflare Web3, Ankr, and Public Node). We also support Alchemy and BlockPI out of the box.

It’s possible to select any number or combination of these RPC providers. Here is an example dfx command for making an RPC request with all 5 built-in Ethereum providers:

dfx canister call evm_rpc eth_getTransactionCount '(
  variant {EthMainnet = opt vec {
    variant {Cloudflare};
    variant {Ankr};
    variant {PublicNode};
    variant {Alchemy};
    variant {BlockPi};
  }},
  null,
  record {
    address = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
    block = variant {Latest};
  }
)' --with-cycles 10000000000 --wallet=$(dfx identity get-wallet --network ic) --network ic

Note that using more RPC providers increases the chance of encountering HTTP outcall consensus errors, but this may be worth it for the added security depending on the situation.

We are in the process of rolling out documentation and examples for how to use the canister; hopefully this is useful as a reference point for the time being.

3 Likes

Okay I understand now, I didn’t see the vec for providing the providers.

I am trying to get a basic example working, and eth_getBlockByNumber so far no matter what provider I try is returning "missing field baseFeePerGas:

{"Consistent":{"Err":{"HttpOutcallError":{"InvalidHttpJsonRpcResponse":{"status":200,"body":"{\"jsonrpc\":\"2.0\",\"id\":13,\"result\":{\"difficulty\":\"0x400000000\",\"extraData\":\"0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa\",\"gasLimit\":\"0x1388\",\"gasUsed\":\"0x0\",\"hash\":\"0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3\",\"logsBloom\":\"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"miner\":\"0x0000000000000000000000000000000000000000\",\"mixHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"nonce\":\"0x0000000000000042\",\"number\":\"0x0\",\"parentHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"receiptsRoot\":\"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421\",\"sha3Uncles\":\"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347\",\"size\":\"0x21c\",\"stateRoot\":\"0xd7f8974fb5ac78d9ac099b9ad5018bedc2ce0a72dad1827a1709da30580f0544\",\"timestamp\":\"0x0\",\"totalDifficulty\":\"0x400000000\",\"transactions\":[],\"transactionsRoot\":\"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421\",\"uncles\":[]}}\n","parsingError":["missing field `baseFeePerGas` at line 1 column 1473"]}}}}}

Is this in your local replica? I’m using the following dfx command to try and reproduce the issue:

dfx canister call evm_rpc eth_getBlockByNumber '(variant {EthMainnet}, null, variant {Latest})' --with-cycles 10000000000 --wallet=$(dfx identity get-wallet)

Does this command work for you? Feel free to send me a DM and we can try to narrow down what’s happening.

1 Like

Hello everyone,

Be sure to check out @dfx-json’s announcement blog post which gives some great background for the EVM RPC canister along with detailed installation instructions.

Documentation is now available for those interested in the full capabilities of the EVM RPC canister.

I also set up a full-stack starter project as an example of how to call RPC methods from a Motoko dapp. Here is a browser-based development environment so you can get started without downloading anything on your local machine.

Let us know if you run into any issues or have any questions; your feedback is highly important during this beta testing phase. Feel free to open an issue or even contribute a PR on the project’s GitHub repository.

Cheers!

7 Likes

Hello again! We just updated the EVM RPC canister to make it significantly easier to use custom EVM chains based directly on community feedback.

Here is an example command to get the latest block information on the Arbitrum L2 network:

dfx canister call evm_rpc eth_getBlockByNumber '(variant {Custom = record {chainId = 42161; services = vec {record {url = "https://1rpc.io/arb"}}}}, null, variant {Latest})' --with-cycles 10000000000 --wallet=$(dfx identity get-wallet)

Equivalent in Motoko:

let source = #Custom {
  chainId = 42161;
  services = [{ url = "https://1rpc.io/arb"; headers = null }];
};
Cycles.add(1000000000);
let result = await EvmRpc.eth_getBlockByNumber(source, null, #Latest);

A full list of chain ids and RPC services can be found on ChainList.org.

We appreciate your involvement during this beta testing phase and encourage you to reach out with any questions or requests here or on the GitHub repository.

4 Likes

Hello @rvanasa, are all the chains in the Chainlist website supported already?

I don’t think so, as it depends on which chains the RPC providers support.

1 Like

+1. It’s possible to use most networks listed on ChainList by specifying a custom provider (relevant documentation), although support varies depending on the specific chain and RPC method.

2 Likes

Exciting developments. When will it be fully rolled out in production?

We don’t have a clear timeline for when it will be out of beta. Basically, we want to gather enough data to make sure the canister is a) safe for production use and b) polished enough from a DX standpoint for production use. The more feedback devs like you can give us, the sooner we can get it out of beta, so I encourage everyone to run it through the wringer.

1 Like

Okay I am confused again here. Why is JsonRpcSource different than RpcSource for Request? If I want to do a raw Json RPC Request, not one of the pre-provided methods like eth_feeHistory, why is the Source different? Does JsonRpcSource also do consensus among various providers?

I was trying to do eth_getBalance and realized there wasn’t a candid method provided, so I switched to making a raw Request, put the Source is very different, that’s where this confusion is coming from.

Also what is the third parameter in the request args? request : (JsonRpcSource, text, nat64) -> (RequestResult);?

The Text is just JSON Text specifying what you want to do. The nat64 is the expected return size that the function likely uses in the HTTP outcall to allocate space and charge for gas.

If you are getting a balance you can set it pretty low because the response is likely a just at 256bit Number. If you are calling a function that might have a bigger size you may need to make it bigger…I this setup to try to get the recommended gas fee but it turned out being pretty useless as most L2 just have this hard coded:

/// Retrieve the latest block on the Ethereum blockchain.
  public func getEthGas() : async Nat {

    nonce_api_id += 1;

    // Select RPC services
    let services = #EthMainnet(#Alchemy);

    // Call `eth_getBlockByNumber` RPC method (unused cycles will be refunded)
    Cycles.add(1000000000);
    let result = await EvmRpc.request(services, "{\"id\": " # Nat.toText(nonce_api_id) # ", \"jsonrpc\": \"2.0\", \"method\": \"eth_maxPriorityFeePerGas\"}", 1000);

    switch result {
      // Consistent, successful results
      case (#Ok(val)){
        let myJSON = JSON.parse(val);
        Debug.print(debug_show(myJSON));
        let ?(#Object(array)) = myJSON;
         Debug.print(debug_show(array));
        let ?myNode = Array.find<(Text, JSON.JSON)>(array, func (x) {
          switch x {
            case ("result", _){
              true;
            };
            case (_, _){
              false;
            };
          };
        });
         Debug.print(debug_show(myNode));

        let #String(myHex) = myNode.1;
        Debug.print(debug_show(myHex));
        let ?justbytes = Text.stripStart(myHex, #text("0x"));
         Debug.print(debug_show(justbytes));
        let #ok(myBytes) = HEX.decode(justbytes);
        Debug.print(debug_show(myBytes));
        let myNat = Conversion.bytesToNat(myBytes);
        Debug.print(debug_show("ethgas" # debug_show(myNat)));
        myNat;
      };
      // Consistent error message
      case (#Err(err)) 15;
    };
  };

Thanks for the info on the last parameter, I’m mostly concerned with the first parameter, the RpcSource or JsonRpcSource, I don’t understand why they’re different and it’s confusing.

I’m more and more coming at this from a teaching angle, these small things cause confusion when introducing developers new to the IC.

1 Like

Hmm…must be an error in the docs. The did file has RpcSource as the first parameter. They are the same thing.

This indeed sounds like outdated documentation. In the latest EVM RPC version, JsonRpcSource is removed in favor of names which more clearly explain the distinction (RpcService and RpcServices).

Since everything appears up-to-date in the developer docs and GitHub repository, definitely let me know if any other website still uses the original Candid interface so we can fix that.

The nat64 arg is the maximum number of bytes in the response, which is used to determine the cost of the RPC request. I opened a PR to clarify this in the Candid interface (#181).

Hopefully this helps, and please feel free to mention any other usability issues. The goal is to smooth out these details before we lock the canister into the NNS.

1 Like

Hello everyone!

We released a new version of the EVM RPC canister. Here are the most notable changes:

  • eth_sendRawTransaction now returns the transaction hash on success.
  • The Candid interface now includes more input / output parameter names for clarity.
5 Likes