/icp-evm-rust-bridge

A seamless Rust-based starter template for integrating ICP canisters with EVM-based smart contracts.

Primary LanguageJavaScript

ICP-EVM Integration Starter Template

This template provides a seamless integration between Internet Computer Protocol (ICP) canisters and Ethereum Virtual Machine (EVM) based smart contracts. It uses Rust for the backend canister and includes a simple Solidity smart contract for demonstration.

It has been built on top of the evm rpc rust template by Elias Datler.

Link to canister URLs:

  1. Frontend
  2. Backend

Quick Start

To get started with this template, run the following command:

bash -c "$(curl -fsSL https://raw.githubusercontent.com/Stephen-Kimoi/icp-evm-rust-bridge/main/install_and_deploy.sh)" -- project-name

This script will:

  1. Create a new project
  2. Clone this template into the new project
  3. Install dependencies
  4. Start a local replica
  5. Deploy the canister

After running the script, your project will be set up and ready to use!

Once the canister is deployed, this is what you'll see: alt text

You can click on the frontend link and this is what you'll see: alt text

Usage

When you click on Get Canister ETH Address Button it will display the ETH Address of the backend canister

You need to take the ETH Address generated and send some Sepolia ETH to it for your canister to perform write functionality (increase count & decrease count)

Performing read functionality Get Count doesn't require you to have Sepolia ETH in your canister.

Project Structure

/
├── backend/
│   └── src/
│       ├── eth_call.rs
│       └── lib.rs
├── smart_contract/
│   └── contracts/
│       └── Counter.sol
└── src/
    └── (frontend files)

Note: dfx test keys

On line 232 of eth_call.rs file:

    name: "test_key_1".to_string(), // use EcdsaKeyId::default() for mainnet use test_key_1 for testnet and dfx_test_key for local deployment

Ensure you use the correct key for your deployment i.e

  • test_key_1 for testnet
  • dfx_test_key for local deployment
  • EcdsaKeyId::default() for mainnet deployment

Backend (Rust Canister)

eth_call.rs

This file contains the core functionality for interacting with EVM-based smart contracts.

Key components:

  • call_smart_contract: Handles both read and write operations to the smart contract. It encodes the function call, sends it to the Ethereum network, and decodes the result.
  • sign_transaction: Signs transactions for write operations using ECDSA.
  • pubkey_and_signature: Retrieves the ECDSA public key and generates a signature for a given transaction hash.
  • get_ecdsa_public_key: Fetches the ECDSA public key associated with the canister.
  • next_id: Retrieves the next transaction nonce for the canister's Ethereum address.
  • Helper functions for data conversion and transaction signing.

lib.rs

This is the main entry point for the canister, containing the public functions that can be called.

Key functions:

  • get_latest_ethereum_block: Retrieves the latest block from the Ethereum network.
  • get_canister_eth_address: Derives the Ethereum address for the canister.
  • call_increase_count: Increases the counter in the smart contract.
  • get_count: Retrieves the current count from the smart contract.
  • call_decrease_count: Decreases the counter in the smart contract.

Smart Contract (Solidity)

Counter.sol

A simple smart contract that implements a counter with functions to increase, decrease, and get the current count. You can edit and add your smart contract code over here.

Usage

Read Functionalities

Clicking on the Get Canister ETH Address button will generate the Ethereum address of the canister.

Clicking on Get Count will return the count value.

Write Functionalities

The buttons Increase Count and Decrease Count perform write operations to the smart contract deployed on Sepolia.

Note: Write functions require the canister to have ETH for gas fees. For local deployments, you may need to send test ETH to the canister's address, which changes regularly.

Editing Your Code

To get started with editing your code, ensure you've changed the CONTRACT_ADDRESS and ABI_JSON in the lib.rs file inside the backend/src directory.

const CONTRACT_ADDRESS: &str = "0xAed5d7b083ad30ad6B50f698427aD4907845AAc3";

const ABI_JSON: &str = r#"
   [
        {
            "inputs": [],
            "stateMutability": "nonpayable",
            "type": "constructor"
        },
        {
            "inputs": [],
            "name": "decreaseCount",
            "outputs": [],
            "stateMutability": "nonpayable",
            "type": "function"
        },
        {
            "inputs": [],
            "name": "getCount",
            "outputs": [
                {
                "internalType": "uint256",
                "name": "",
                "type": "uint256"
                }
            ],
            "stateMutability": "view",
            "type": "function"
        },
        {
            "inputs": [],
            "name": "increaseCount",
            "outputs": [],
            "stateMutability": "nonpayable",
            "type": "function"
        }
    ]
"#;

Creating Read Functionalities

To create a read functionality, you can use the following example as a template:

#[ic_cdk::update]
async fn get_count() -> Result<u64, String> {
    let abi = get_abi();
    
    let result = call_smart_contract(
        CONTRACT_ADDRESS.to_string(), 
        &abi, 
        "getCount", 
        &[], 
        false, // This is a read operation
        None // Chain ID
    ).await?;

    let count_value = result
        .get(0)
        .ok_or("Expected a single value in the return value")?
        .clone()
        .into_uint()
        .ok_or("Expected a uint256 value")?;

    Ok(count_value.low_u64())
}

Creating Write Functionalities

For write operations, you can use the following example:

#[ic_cdk::update]
async fn call_increase_count() -> Result<String, String> {
    let abi = get_abi();

    let result = call_smart_contract(
        CONTRACT_ADDRESS.to_string(),
        &abi,
        "increaseCount",
        &[],
        true, // This is a write operation
        Some(U256::from(11155111)), // Sepolia chain ID
    )
    .await;

    match result {
        Ok(tx_hash) => Ok(format!("Increased count. Transaction hash: {:?}", tx_hash)),
        Err(e) => Err(format!("Failed to send transaction: {:?}", e))
    }
}

Important Notes

  • Ensure you've updated the CONTRACT_ADDRESS and ABI_JSON in the lib.rs file to match your deployed smart contract.
  • Write operations require the canister to have ETH for gas fees. For local deployments, you may need to send test ETH to the canister's address.
  • The canister's Ethereum address may change in local deployments, so be aware of this when testing write functionalities.