🥷 Try it live — no local setup. ICP Ninja is a web-based IDE that builds and deploys this project to the mainnet for free, right in your browser. Click the badge above, or hit Deploy if you're already in Ninja. To build and run it locally instead, follow the steps below.
This example demonstrates how to deploy a canister on the Internet Computer that can send and receive Ether (ETH) on the Ethereum network. The canister uses threshold ECDSA to sign Ethereum transactions and HTTPS outcalls to communicate with the Ethereum network via the EVM RPC canister.
This example internally leverages:
- Threshold ECDSA: Each user's Ethereum address is derived deterministically from the canister's master ECDSA key using a derivation path based on the user's IC principal. This means each user has a unique, stable Ethereum address controlled by the canister.
- HTTPS outcalls: The canister communicates with the Ethereum network via the EVM RPC canister (canister ID
7hfb6-caaaa-aaaar-qadga-caion ICP mainnet), which forwards requests to public Ethereum RPC providers such ashttps://ethereum-sepolia-rpc.publicnode.com.
For a deeper understanding of the ICP ↔ ETH integration, see the Ethereum integration.
- Node.js
- icp-cli:
npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm
git clone https://github.com/dfinity/examples
cd examples/rust/basic_ethereumThe local icp-cli network supports real HTTPS outcalls, so get_balance, transaction_count, and transaction_count_with_client work against live Ethereum Sepolia data without deploying to ICP mainnet. To query Ethereum mainnet data instead, pass --args '(opt record {ethereum_network = opt variant {Mainnet}})' to icp deploy.
icp network start -d
icp deploy
bash test.sh
icp network stopbash test.sh verifies address derivation (threshold ECDSA), queries a known funded Sepolia address's balance via the raw EVM RPC canister interface, and queries its transaction count (nonce) via the high-level evm_rpc_client — demonstrating both usage patterns side by side.
send_eth requires a funded Ethereum address and is not covered in the automated tests. See Sending ETH below.
icp deploy -e icThis deploys only the backend canister and points it to the shared EVM RPC canister (7hfb6-caaaa-aaaar-qadga-cai) already running on ICP mainnet. The default configuration uses Ethereum Sepolia testnet with test_key_1 — suitable for testing with free Sepolia ETH from a faucet.
To deploy for production use on Ethereum mainnet, update the init_args in icp.yaml to use variant {Mainnet} and "key_1" — see the comment in icp.yaml for the exact value.
Each IC principal gets a unique, stable Ethereum address controlled by this canister. The address is derived deterministically from the principal using the canister's threshold ECDSA key — the same principal always maps to the same address.
Passing null returns the address for your own IC principal (the identity you are calling with):
icp canister call backend ethereum_address '(null)'
# Returns your Ethereum address, e.g. ("0x378a452B20d1f06008C06c581b1656BdC5313c0C")You can also look up the address for any other IC principal:
icp canister call backend ethereum_address '(opt principal "hkroy-sm7vs-yyjs7-ekppe-qqnwx-hm4zf-n7ybs-titsi-k6e3k-ucuiu-uqe")'
# Returns e.g. ("0x8d68f7B3cdb40A2E77071077658b01A9EA4B040F")Query the ETH balance (in Wei) for any Ethereum address:
icp canister call backend get_balance '(opt "0x378a452B20d1f06008C06c581b1656BdC5313c0C")'Query the transaction count for any Ethereum address using the high-level EvmRpcClient. This calls eth_getTransactionCount, which returns the nonce — the number of transactions sent from the address (outgoing only, not received):
icp canister call backend transaction_count_with_client '(opt "0x378a452B20d1f06008C06c581b1656BdC5313c0C", null)'Passing null uses the derived Ethereum address of your calling IC principal:
icp canister call backend transaction_count_with_client '(null, null)'To send ETH, your derived Ethereum address must be funded first:
- Get your Ethereum address (see above) — this is the address managed by the canister for your IC principal.
- Get some Sepolia ETH from Alchemy's Sepolia faucet.
- Send Sepolia ETH to your address using any Ethereum wallet (e.g. MetaMask).
- Once the transaction has at least one confirmation, verify the balance:
icp canister call backend get_balance '(null)'Then send ETH (amount in Wei):
icp canister call backend send_eth '("0xdd2851Cdd40aE6536831558DD46db62fAc7A844d", 1)'Returns the transaction hash. Track it on Sepolia Etherscan.
Note: Due to the replicated nature of HTTPS outcalls, errors such as "transaction already known" or "nonce too low" may be reported even if the transaction was successfully broadcast. Verify by checking Etherscan or confirming that the transaction count for the address increased.
The example uses PublicNode by default — a free, no-registration provider that works out of the box locally and on mainnet. This is sufficient for getting started and automated testing.
For production deployments requiring premium providers (Alchemy, Ankr, BlockPi), refer to the EVM RPC canister documentation for how to configure API keys. Once configured, change evm_rpc_services() in backend/state.rs to pass None instead of an explicit provider list to use all configured providers for better consensus.
Refer to the security best practices for information on security and best practices for your ICP app. For this example the following aspects are particularly relevant:
- Certify query responses if they are relevant for security: since the app offers a method to read balances.
- Use a governance framework like SNS to make a canister have a decentralized controller: decentralized control may be essential for canisters holding ETH on behalf of users.