diff --git a/.github/workflows/basic_ethereum.yml b/.github/workflows/basic_ethereum.yml new file mode 100644 index 000000000..838291343 --- /dev/null +++ b/.github/workflows/basic_ethereum.yml @@ -0,0 +1,28 @@ +name: basic_ethereum + +on: + push: + branches: [master] + pull_request: + paths: + - rust/basic_ethereum/** + - .github/workflows/basic_ethereum.yml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + rust-basic_ethereum: + runs-on: ubuntu-24.04 + container: ghcr.io/dfinity/icp-dev-env-rust:1.0.1 + env: + ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Deploy and test + working-directory: rust/basic_ethereum + run: | + icp network start -d + icp deploy + bash test.sh diff --git a/rust/basic_ethereum/.devcontainer/devcontainer.json b/rust/basic_ethereum/.devcontainer/devcontainer.json deleted file mode 100644 index ebb0b8bcc..000000000 --- a/rust/basic_ethereum/.devcontainer/devcontainer.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "ICP Dev Environment", - "image": "ghcr.io/dfinity/icp-dev-env-slim:22", - "forwardPorts": [4943, 5173], - "portsAttributes": { - "4943": { - "label": "dfx", - "onAutoForward": "ignore" - }, - "5173": { - "label": "vite", - "onAutoForward": "openBrowser" - } - }, - "customizations": { - "vscode": { - "extensions": ["dfinity-foundation.vscode-motoko"] - } - } -} diff --git a/rust/basic_ethereum/BUILD.md b/rust/basic_ethereum/BUILD.md deleted file mode 100644 index 24cfcb754..000000000 --- a/rust/basic_ethereum/BUILD.md +++ /dev/null @@ -1,113 +0,0 @@ -# Continue building locally - -Projects deployed through ICP Ninja are temporary; they will only be live for 20 minutes before they are removed. The command-line tool `dfx` can be used to continue building your ICP Ninja project locally and deploy it to the mainnet. - -To migrate your ICP Ninja project off of the web browser and develop it locally, follow these steps. - -### 1. Install developer tools. - -You can install the developer tools natively or use Dev Containers. - -#### Option 1: Natively install developer tools - -> Installing `dfx` natively is currently only supported on macOS and Linux systems. On Windows, it is recommended to use the Dev Containers option. - -1. Install `dfx` with the following command: - -``` - -sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)" - -``` - -> On Apple Silicon (e.g., Apple M1 chip), make sure you have Rosetta installed (`softwareupdate --install-rosetta`). - -2. [Install NodeJS](https://nodejs.org/en/download/package-manager). - -3. For Rust projects, you will also need to: - -- Install [Rust](https://doc.rust-lang.org/cargo/getting-started/installation.html#install-rust-and-cargo): `curl https://sh.rustup.rs -sSf | sh` - -- Install [candid-extractor](https://crates.io/crates/candid-extractor): `cargo install candid-extractor` - -4. For Motoko projects, you will also need to: - -- Install the Motoko package manager [Mops](https://docs.mops.one/quick-start#2-install-mops-cli): `npm i -g ic-mops` - -Lastly, navigate into your project's directory that you downloaded from ICP Ninja. - -#### Option 2: Dev Containers - -Continue building your projects locally by installing the [Dev Container extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) for VS Code and [Docker](https://docs.docker.com/engine/install/). - -Make sure Docker is running, then navigate into your project's directory that you downloaded from ICP Ninja and start the Dev Container by selecting `Dev-Containers: Reopen in Container` in VS Code's command palette (F1 or Ctrl/Cmd+Shift+P). - -> Note that local development ports (e.g. the ports used by `dfx` or `vite`) are forwarded from the Dev Container to your local machine. In the VS code terminal, use Ctrl/Cmd+Click on the displayed local URLs to open them in your browser. To view the current port mappings, click the "Ports" tab in the VS Code terminal window. - -### 2. Start the local development environment. - -``` -dfx start --background -``` - -### 3. Create a local developer identity. - -To manage your project's canisters, it is recommended that you create a local [developer identity](https://internetcomputer.org/docs/building-apps/getting-started/identities) rather than use the `dfx` default identity that is not stored securely. - -To create a new identity, run the commands: - -``` - -dfx identity new IDENTITY_NAME - -dfx identity use IDENTITY_NAME - -``` - -Replace `IDENTITY_NAME` with your preferred identity name. The first command `dfx start --background` starts the local `dfx` processes, then `dfx identity new` will create a new identity and return your identity's seed phase. Be sure to save this in a safe, secure location. - -The third command `dfx identity use` will tell `dfx` to use your new identity as the active identity. Any canister smart contracts created after running `dfx identity use` will be owned and controlled by the active identity. - -Your identity will have a principal ID associated with it. Principal IDs are used to identify different entities on ICP, such as users and canisters. - -[Learn more about ICP developer identities](https://internetcomputer.org/docs/building-apps/getting-started/identities). - -### 4. Deploy the project locally. - -Deploy your project to your local developer environment with: - -``` -npm install -dfx deploy - -``` - -Your project will be hosted on your local machine. The local canister URLs for your project will be shown in the terminal window as output of the `dfx deploy` command. You can open these URLs in your web browser to view the local instance of your project. - -### 5. Obtain cycles. - -To deploy your project to the mainnet for long-term public accessibility, first you will need [cycles](https://internetcomputer.org/docs/building-apps/getting-started/tokens-and-cycles). Cycles are used to pay for the resources your project uses on the mainnet, such as storage and compute. - -> This cost model is known as ICP's [reverse gas model](https://internetcomputer.org/docs/building-apps/essentials/gas-cost), where developers pay for their project's gas fees rather than users pay for their own gas fees. This model provides an enhanced end user experience since they do not need to hold tokens or sign transactions when using a dapp deployed on ICP. - -> Learn how much a project may cost by using the [pricing calculator](https://internetcomputer.org/docs/building-apps/essentials/cost-estimations-and-examples). - -Cycles can be obtained through [converting ICP tokens into cycles using `dfx`](https://internetcomputer.org/docs/building-apps/developer-tools/dfx/dfx-cycles#dfx-cycles-convert). - -### 6. Deploy to the mainnet. - -Once you have cycles, run the command: - -``` - -dfx deploy --network ic - -``` - -After your project has been deployed to the mainnet, it will continuously require cycles to pay for the resources it uses. You will need to [top up](https://internetcomputer.org/docs/building-apps/canister-management/topping-up) your project's canisters or set up automatic cycles management through a service such as [CycleOps](https://cycleops.dev/). - -> If your project's canisters run out of cycles, they will be removed from the network. - -## Additional examples - -Additional code examples and sample applications can be found in the [DFINITY examples repo](https://github.com/dfinity/examples). diff --git a/rust/basic_ethereum/Cargo.lock b/rust/basic_ethereum/Cargo.lock index f33f3b585..34fcad71e 100644 --- a/rust/basic_ethereum/Cargo.lock +++ b/rust/basic_ethereum/Cargo.lock @@ -316,7 +316,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] -name = "basic_ethereum" +name = "backend" version = "0.1.0" dependencies = [ "alloy-consensus", diff --git a/rust/basic_ethereum/Cargo.toml b/rust/basic_ethereum/Cargo.toml index ad2551f2d..d1e49e317 100644 --- a/rust/basic_ethereum/Cargo.toml +++ b/rust/basic_ethereum/Cargo.toml @@ -1,26 +1,3 @@ -[package] -name = "basic_ethereum" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -alloy-consensus = "0.1.3" -alloy-eips = "0.1.3" -alloy-primitives = "0.7.6" -candid = "0.10" -evm-rpc-canister-types = "0.1.2" -# transitive dependency: ic-crypto-ecdsa-secp256k1 -> k256 -> ecdsa -> elliptic-curve -> crypto-bigint -> rand_core -> getrandom -# See https://forum.dfinity.org/t/module-imports-function-wbindgen-describe-from-wbindgen-placeholder-that-is-not-exported-by-the-runtime/11545/8 -getrandom = { version = "*", default-features = false, features = ["custom"] } -ic-cdk = "0.15" -ic-secp256k1 = { git = "https://github.com/dfinity/ic", tag = "release-2025-07-03_03-27-base", package = "ic-secp256k1" } -ic-sha3 = { git = "https://github.com/dfinity/ic", tag = "release-2025-07-03_03-27-base", package = "ic-sha3" } -ic-ethereum-types = { git = "https://github.com/dfinity/ic", tag = "release-2025-07-03_03-27-base", package = "ic-ethereum-types" } -serde = "1.0" -serde_json = "1.0" -serde_bytes = "0.11.15" -num-traits = "0.2.19" -num = "0.4.3" +[workspace] +members = ["backend"] +resolver = "2" diff --git a/rust/basic_ethereum/README.md b/rust/basic_ethereum/README.md index 1733e2d77..763f03dba 100644 --- a/rust/basic_ethereum/README.md +++ b/rust/basic_ethereum/README.md @@ -1,173 +1,115 @@ # Basic Ethereum -This tutorial will walk you through how to deploy a -sample [canister smart contract](https://internetcomputer.org/docs/current/developer-docs/multi-chain/ethereum/overview) -**that can send and receive Ether (ETH)** on the Internet Computer. +This example demonstrates how to deploy a canister smart contract 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. ## Architecture -This example internally leverages -the [threshold ECDSA](https://internetcomputer.org/docs/current/developer-docs/smart-contracts/encryption/t-ecdsa) -and [HTTPs outcalls](https://internetcomputer.org/docs/current/developer-docs/smart-contracts/advanced-features/https-outcalls/https-outcalls-overview) -features of the Internet Computer. +This example internally leverages: +- [Threshold ECDSA](https://internetcomputer.org/docs/current/developer-docs/smart-contracts/encryption/t-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](https://internetcomputer.org/docs/current/developer-docs/smart-contracts/advanced-features/https-outcalls/https-outcalls-overview): The canister communicates with the Ethereum network via the [EVM RPC canister](https://github.com/internet-computer-protocol/evm-rpc-canister/tree/main) (canister ID `7hfb6-caaaa-aaaar-qadga-cai` on ICP mainnet). -For a deeper understanding of the ICP < > ETH integration, see -the [Ethereum integration overview](https://internetcomputer.org/docs/current/developer-docs/multi-chain/ethereum/overview). +For a deeper understanding of the ICP ↔ ETH integration, see the [Ethereum integration overview](https://internetcomputer.org/docs/current/developer-docs/multi-chain/ethereum/overview). -## Deploying from ICP Ninja +> **Note:** `get_balance`, `transaction_count`, and `send_eth` require live HTTPS outcalls to the Ethereum network and work only when deployed on ICP mainnet. The `ethereum_address` function can be tested locally as it only uses threshold ECDSA, which is available in the local replica. -When viewing this project in ICP Ninja, you can deploy it directly to the mainnet for free by clicking "Run" in the upper right corner. Open this project in ICP Ninja: +## Build and deploy from the command line -[![](https://icp.ninja/assets/open.svg)](https://icp.ninja/i?g=https://github.com/dfinity/examples/rust/basic_ethereum) +### Prerequisites +- [Node.js](https://nodejs.org/) +- icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` -## Deploying with `dfx` - -- [x] Install the [IC SDK](https://internetcomputer.org/docs/current/developer-docs/getting-started/install). - -## Step 1: Building and deploying sample code - -### Clone the smart contract - -To clone and build the smart contract in **Rust**: +### Install ```bash git clone https://github.com/dfinity/examples cd examples/rust/basic_ethereum -git submodule update --init --recursive ``` -**If you are using MacOS, you'll need to install Homebrew and run `brew install llvm` to be able to compile the example. -** +### Deploy and test locally -### Acquire cycles to deploy +```bash +icp network start -d +icp deploy +bash test.sh +icp network stop +``` -Deploying to the Internet Computer requires [cycles](https://internetcomputer.org/docs/current/developer-docs/getting-started/tokens-and-cycles) (the equivalent of "gas" on other blockchains). +### Deploy to ICP mainnet (Sepolia testnet) -### Deploy the smart contract to the Internet Computer +To interact with the live Ethereum Sepolia testnet: ```bash -dfx deploy --ic basic_ethereum --argument '(opt record {ethereum_network = opt variant {Sepolia}; ecdsa_key_name = opt variant {TestKey1}})' +icp deploy --network ic --argument '(opt record {ethereum_network = opt variant {Sepolia}; ecdsa_key_name = opt variant {TestKey1}})' ``` -#### What this does - -- `dfx deploy` tells the command line interface to `deploy` the smart contract -- `--ic` tells the command line to deploy the smart contract to the mainnet ICP blockchain -- `--argument (opt record {ethereum_network = opt variant {Sepolia}; ecdsa_key_name = opt variant {TestKey1}})` - initializes the smart contract with the provided arguments: - - `ethereum_network = opt variant {Sepolia}`: the canister uses - the [Ethereum Testnet Sepolia](https://github.com/ethereum-lists/chains/blob/master/_data/chains/eip155-11155111.json) - network. - - `ecdsa_key_name = opt variant {TestKey1}`: the canister uses a test key for signing via threshold ECDSA that is - available on the ICP mainnet. - See [signing messages](https://internetcomputer.org/docs/current/developer-docs/smart-contracts/encryption/signing-messages#signing-messages-1) - for more details. +- `ethereum_network = opt variant {Sepolia}`: uses the [Ethereum Testnet Sepolia](https://github.com/ethereum-lists/chains/blob/master/_data/chains/eip155-11155111.json). +- `ecdsa_key_name = opt variant {TestKey1}`: uses a test key for threshold ECDSA signing available on ICP mainnet. See [signing messages](https://internetcomputer.org/docs/current/developer-docs/smart-contracts/encryption/t-ecdsa) for more details. -If successful, you should see an output that looks like this: +For production use with real ETH on Ethereum mainnet: ```bash -Deploying: basic_ethereum -Building canisters... -... -Deployed canisters. -URLs: -Candid: - basic_ethereum: https://a4gq6-oaaaa-aaaab-qaa4q-cai.raw.icp0.io/?id= +icp deploy --network ic --argument '(opt record {ethereum_network = opt variant {Mainnet}; ecdsa_key_name = opt variant {ProductionKey1}})' ``` -Your canister is live and ready to use! You can interact with it using either the command line or using the Candid UI, -which is the link you see in the output above. +## Interacting with the deployed canister -In the output above, to see the Candid Web UI for your ethereum canister, you would use the -URL `https://a4gq6-oaaaa-aaaab-qaa4q-cai.raw.icp0.io/?id=`. You should see the methods specified in the Candid file `basic_ethereum.did`. +### Step 1: Get your Ethereum address -## Step 2: Generating an Ethereum address +An Ethereum address is derived from the caller's IC principal via the canister's threshold ECDSA key. The same principal always maps to the same Ethereum address: -An Ethereum address can be derived from an ECDSA public key. To derive a user's specific address, identified on the IC -by a principal, the canister uses its own threshold ECDSA public key to derive a new public key deterministically for -each requested principal. To retrieve your Ethereum address, you can call the `ethereum_address` method on the -previously deployed canister: - -```shell -dfx canister --ic call basic_ethereum ethereum_address +```bash +icp canister call backend ethereum_address '(null)' +# Returns e.g. ("0x378a452B20d1f06008C06c581b1656BdC5313c0C") ``` -This will return an Ethereum address such as `("0x378a452B20d1f06008C06c581b1656BdC5313c0C")` that is tied to your -principal. Your address will be different. You can view such addresses on any Ethereum block explorer such as [Etherscan](https://etherscan.io/). +To get the Ethereum address for a different principal: -If you want to send some ETH to someone else, you can also use the above method to enquire about their Ethereum address -given their IC principal: - -```shell -dfx canister --ic call basic_ethereum ethereum_address '(opt principal "hkroy-sm7vs-yyjs7-ekppe-qqnwx-hm4zf-n7ybs-titsi-k6e3k-ucuiu-uqe")' +```bash +icp canister call backend ethereum_address '(opt principal "hkroy-sm7vs-yyjs7-ekppe-qqnwx-hm4zf-n7ybs-titsi-k6e3k-ucuiu-uqe")' +# Returns e.g. ("0x8d68f7B3cdb40A2E77071077658b01A9EA4B040F") ``` -This will return a different Ethereum address as the one above, such -as `("0x8d68f7B3cdb40A2E77071077658b01A9EA4B040F")`. - -## Step 3: Receiving ETH - -Now that you have your Ethereum address, let us send some (Sepolia) ETH to it: - -1. Get some Sepolia ETH if you don't have any. You can for example use [this faucet](https://www.alchemy.com/faucets/ethereum-sepolia). -2. Send some Sepolia ETH to the address you obtained in the previous step. You can use any Ethereum wallet (e.g., - Metamask) to do so. +You can view these addresses on any Ethereum block explorer such as [Etherscan](https://etherscan.io/). -Once the transaction has at least one confirmation, which can take a few seconds, -you'll be able to see it in your Ethereum address's balance, which should be visible in an Ethereum block explorer, -e.g., https://sepolia.etherscan.io/address/0x378a452b20d1f06008c06c581b1656bdc5313c0c. +### Step 2: Receive ETH (mainnet only) -## Step 4: Sending ETH +1. Get some Sepolia ETH from a faucet, for example [Alchemy's Sepolia faucet](https://www.alchemy.com/faucets/ethereum-sepolia). +2. Send Sepolia ETH to the address from Step 1 using any Ethereum wallet (e.g. MetaMask). +3. Once the transaction has at least one confirmation, verify the balance on [Sepolia Etherscan](https://sepolia.etherscan.io/). -You can send ETH using the `send_eth` endpoint on your canister, specifying an Ethereum destination address and an -amount in the smallest unit (Wei). For example, to send 1 Wei to `0xdd2851Cdd40aE6536831558DD46db62fAc7A844d`, run the following command: +### Step 3: Check your balance (mainnet only) -```shell -dfx canister --ic call basic_ethereum send_eth '("0xdd2851Cdd40aE6536831558DD46db62fAc7A844d", 1)' +```bash +icp canister call backend get_balance '(null)' ``` -The `send_eth` endpoint sends ETH by executing the following steps: +### Step 4: Send ETH (mainnet only) -1. Retrieving the transaction count for the sender's address at `Latest` block height. This is necessary because - Ethereum transactions for a given sender's address are ordered by a `nonce`, which is a monotonically incrementally - increasing non-negative counter. -2. Estimating the current transaction fees. For simplicity, the current gas fees are hard-coded with a generous limit. A - real world application would dynamically fetch the latest transaction fees, for example using - the [`eth_feeHistory`](https://github.com/internet-computer-protocol/evm-rpc-canister/blob/3cce151d4c1338d83e6741afa354ccf11dff41e8/candid/evm_rpc.did#L254) - method in the [EVM-RPC canister](https://github.com/internet-computer-protocol/evm-rpc-canister/tree/main). -3. Building an Ethereum transaction ([EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)) to send the specified amount - to the given receiver's address. -4. Signing the Ethereum transaction using - the [sign_with_ecdsa API](https://internetcomputer.org/docs/current/developer-docs/smart-contracts/encryption/signing-messages). -5. Sending the signed transaction to the Ethereum network using - the [`eth_sendRawTransaction`](https://github.com/internet-computer-protocol/evm-rpc-canister/blob/3cce151d4c1338d83e6741afa354ccf11dff41e8/candid/evm_rpc.did#L261) - method in the [EVM-RPC canister](https://github.com/internet-computer-protocol/evm-rpc-canister/tree/main). +Send 1 Wei to a destination address: -The `send_eth` endpoint returns the hash of the transaction sent to the Ethereum network, which can for example be used -to track the transaction on an Ethereum blockchain explorer. +```bash +icp canister call backend send_eth '("0xdd2851Cdd40aE6536831558DD46db62fAc7A844d", 1)' +``` -## Conclusion +The `send_eth` endpoint executes the following steps: -In this tutorial, you were able to: +1. Retrieves the transaction count (nonce) for the sender's address at `Latest` block height. Ethereum transactions are ordered by nonce, a monotonically increasing counter. +2. Estimates transaction fees. For simplicity, fees are hard-coded with a generous limit. A production application would dynamically fetch the latest fees via the [`eth_feeHistory`](https://github.com/internet-computer-protocol/evm-rpc-canister/blob/3cce151d4c1338d83e6741afa354ccf11dff41e8/candid/evm_rpc.did#L254) method. +3. Builds an [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) transaction. +4. Signs the transaction using the [sign_with_ecdsa API](https://internetcomputer.org/docs/current/developer-docs/smart-contracts/encryption/t-ecdsa). +5. Sends the signed transaction to the Ethereum network via [`eth_sendRawTransaction`](https://github.com/internet-computer-protocol/evm-rpc-canister/blob/3cce151d4c1338d83e6741afa354ccf11dff41e8/candid/evm_rpc.did#L261) in the EVM RPC canister. -* Deploy a canister smart contract on the ICP blockchain that can receive and send ETH. -* Acquire cycles to deploy the canister to the ICP mainnet. -* Connect the canister to the Ethereum Sepolia testnet. -* Send the canister some Sepolia ETH. -* Use the canister to send ETH to another Ethereum address. +Returns the transaction hash, which can be used to track the transaction on a block explorer. -Additional examples regarding the ICP < > ETH integration can be -found [here](https://internetcomputer.org/docs/current/developer-docs/multi-chain/examples#ethereum--evm-examples). +> **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. ## Security considerations and best practices -If you base your application on this example, we recommend you familiarize yourself with and adhere to -the [security best practices](https://internetcomputer.org/docs/current/references/security/) for developing on the -Internet Computer. This example may not implement all the best practices. +If you base your application on this example, we recommend you familiarize yourself with and adhere to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for developing on the Internet Computer. This example may not implement all the best practices. For example, the following aspects are particularly relevant for this app: -* [Certify query responses if they are relevant for security](https://internetcomputer.org/docs/current/references/security/general-security-best-practices#certify-query-responses-if-they-are-relevant-for-security), - since the app offers a method to read balances, for example. -* [Use a decentralized governance system like SNS to make a canister have a decentralized controller](https://internetcomputer.org/docs/current/references/security/rust-canister-development-security-best-practices#use-a-decentralized-governance-system-like-sns-to-make-a-canister-have-a-decentralized-controller), - since decentralized control may be essential for canisters holding ETH on behalf of users. +- [Certify query responses if they are relevant for security](https://docs.internetcomputer.org/guides/security/overview), since the app offers a method to read balances. +- [Use a decentralized governance system like SNS to make a canister have a decentralized controller](https://docs.internetcomputer.org/guides/security/overview), since decentralized control may be essential for canisters holding ETH on behalf of users. + +Additional examples for the ICP ↔ ETH integration can be found in the [ICP developer docs](https://internetcomputer.org/docs/current/developer-docs/multi-chain/ethereum/overview). diff --git a/rust/basic_ethereum/backend/Cargo.toml b/rust/basic_ethereum/backend/Cargo.toml new file mode 100644 index 000000000..0c5180067 --- /dev/null +++ b/rust/basic_ethereum/backend/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "backend" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +alloy-consensus = "0.1.3" +alloy-eips = "0.1.3" +alloy-primitives = "0.7.6" +candid = "0.10" +evm-rpc-canister-types = "0.1.2" +# transitive dependency: ic-crypto-ecdsa-secp256k1 -> k256 -> ecdsa -> elliptic-curve -> crypto-bigint -> rand_core -> getrandom +# See https://forum.dfinity.org/t/module-imports-function-wbindgen-describe-from-wbindgen-placeholder-that-is-not-exported-by-the-runtime/11545/8 +getrandom = { version = "*", default-features = false, features = ["custom"] } +ic-cdk = "0.15" +ic-secp256k1 = { git = "https://github.com/dfinity/ic", tag = "release-2025-07-03_03-27-base", package = "ic-secp256k1" } +ic-sha3 = { git = "https://github.com/dfinity/ic", tag = "release-2025-07-03_03-27-base", package = "ic-sha3" } +ic-ethereum-types = { git = "https://github.com/dfinity/ic", tag = "release-2025-07-03_03-27-base", package = "ic-ethereum-types" } +serde = "1.0" +serde_json = "1.0" +serde_bytes = "0.11.15" +num-traits = "0.2.19" +num = "0.4.3" diff --git a/rust/basic_ethereum/src/ecdsa.rs b/rust/basic_ethereum/backend/ecdsa.rs similarity index 100% rename from rust/basic_ethereum/src/ecdsa.rs rename to rust/basic_ethereum/backend/ecdsa.rs diff --git a/rust/basic_ethereum/src/ethereum_wallet.rs b/rust/basic_ethereum/backend/ethereum_wallet.rs similarity index 100% rename from rust/basic_ethereum/src/ethereum_wallet.rs rename to rust/basic_ethereum/backend/ethereum_wallet.rs diff --git a/rust/basic_ethereum/src/lib.rs b/rust/basic_ethereum/backend/lib.rs similarity index 100% rename from rust/basic_ethereum/src/lib.rs rename to rust/basic_ethereum/backend/lib.rs diff --git a/rust/basic_ethereum/src/state.rs b/rust/basic_ethereum/backend/state.rs similarity index 100% rename from rust/basic_ethereum/src/state.rs rename to rust/basic_ethereum/backend/state.rs diff --git a/rust/basic_ethereum/basic_ethereum.did b/rust/basic_ethereum/basic_ethereum.did deleted file mode 100644 index 36ff95e4e..000000000 --- a/rust/basic_ethereum/basic_ethereum.did +++ /dev/null @@ -1,57 +0,0 @@ -type InitArg = record { - // The canister will interact with this Ethereum network. - ethereum_network : opt EthereumNetwork; - - // The name of the ECDSA key to use. - ecdsa_key_name : opt EcdsaKeyName; -}; - -type EthereumNetwork = variant { - // The public Ethereum mainnet. - Mainnet; - // The public Ethereum Sepolia testnet. - Sepolia; -}; - -type EcdsaKeyName = variant { - // For local development with `dfx` - TestKeyLocalDevelopment; - // For testing with the Internet Computer's test key. - TestKey1; - // For running the canister in a production environment using the Internet Computer's production key. - ProductionKey1; -}; - -type BlockTag = variant { - Earliest; - Safe; - Finalized; - Latest; - Number : nat; - Pending; -}; - -// Base unit of ETH, i.e., 1 ETH = 10^18 Wei. -type Wei = nat; - -service : (opt InitArg) -> { - // Returns the Ethereum address to which the owner should send ETH - // before sending the amount to another address via the canister using the [send_eth] - // endpoint. - // - // If the owner is not set, it defaults to the caller's principal. - ethereum_address : (owner: opt principal) -> (text); - - // Returns the balance of the given Ethereum address. - // If no address is provided, the address derived from the caller's principal is used. - get_balance : (address: opt text) -> (Wei); - - // Returns the transaction count for the Ethereum address derived for the given principal. - // - // If the owner is not set, it defaults to the caller's principal. - transaction_count : (owner: opt principal, block_height: opt BlockTag) -> (nat); - - // Sends the given amount of ETH in base unit (Wei) to the given Ethereum address. - // Returns the transaction hash - send_eth : (to: text, amount: Wei) -> (text); -} diff --git a/rust/basic_ethereum/dfx.json b/rust/basic_ethereum/dfx.json deleted file mode 100644 index 75f38573a..000000000 --- a/rust/basic_ethereum/dfx.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "canisters": { - "basic_ethereum": { - "candid": "basic_ethereum.did", - "package": "basic_ethereum", - "type": "custom", - "build": [ - "cargo build --no-default-features --target wasm32-unknown-unknown --release -p basic_ethereum" - ], - "wasm": "target/wasm32-unknown-unknown/release/basic_ethereum.wasm", - "metadata": [ - { - "name": "candid:service" - } - ] - }, - "evm_rpc": { - "type": "custom", - "candid": "https://github.com/internet-computer-protocol/evm-rpc-canister/releases/download/release-2024-05-23/evm_rpc.did", - "wasm": "https://github.com/internet-computer-protocol/evm-rpc-canister/releases/download/release-2024-05-23/evm_rpc.wasm.gz", - "remote": { - "id": { - "ic": "7hfb6-caaaa-aaaar-qadga-cai" - } - }, - "specified_id": "7hfb6-caaaa-aaaar-qadga-cai", - "init_arg": "(record { nodesInSubnet = 28 })" - } - }, - "defaults": { - "build": { - "args": "", - "packtool": "" - } - }, - "version": 1 -} diff --git a/rust/basic_ethereum/icp.yaml b/rust/basic_ethereum/icp.yaml new file mode 100644 index 000000000..ae999e12f --- /dev/null +++ b/rust/basic_ethereum/icp.yaml @@ -0,0 +1,15 @@ +networks: + - name: local + mode: managed + +canisters: + - name: backend + recipe: + type: "@dfinity/rust@v3.3.0" + + - name: evm_rpc + build: + steps: + - type: pre-built + url: https://github.com/dfinity/evm-rpc-canister/releases/download/v2.2.0/evm_rpc.wasm.gz + init_args: "(record { nodesInSubnet = 28 })" diff --git a/rust/basic_ethereum/test.sh b/rust/basic_ethereum/test.sh new file mode 100755 index 000000000..92048f6c1 --- /dev/null +++ b/rust/basic_ethereum/test.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -e + +# Note: get_balance, transaction_count, and send_eth require live HTTPS outcalls +# to the Ethereum network and cannot be fully tested locally. The tests below +# verify canister deployment and the ethereum_address function, which derives +# an address using threshold ECDSA (available locally). +# For full end-to-end testing, deploy to ICP mainnet with Sepolia testnet. + +echo "=== Test 1: ethereum_address returns a valid 0x-prefixed Ethereum address ===" +result=$(icp canister call backend ethereum_address '(null)') && \ + echo "$result" && \ + echo "$result" | grep -qE '"0x[0-9a-fA-F]{40}"' && \ + echo "PASS" || (echo "FAIL" && exit 1) + +echo "=== Test 2: ethereum_address for a specific principal returns a valid address ===" +result=$(icp canister call backend ethereum_address '(opt principal "hkroy-sm7vs-yyjs7-ekppe-qqnwx-hm4zf-n7ybs-titsi-k6e3k-ucuiu-uqe")') && \ + echo "$result" && \ + echo "$result" | grep -qE '"0x[0-9a-fA-F]{40}"' && \ + echo "PASS" || (echo "FAIL" && exit 1) + +echo "=== Test 3: different principals derive different Ethereum addresses ===" +addr1=$(icp canister call backend ethereum_address '(null)' | grep -oE '0x[0-9a-fA-F]{40}') && \ + addr2=$(icp canister call backend ethereum_address '(opt principal "hkroy-sm7vs-yyjs7-ekppe-qqnwx-hm4zf-n7ybs-titsi-k6e3k-ucuiu-uqe")' | grep -oE '0x[0-9a-fA-F]{40}') && \ + echo "addr1=$addr1 addr2=$addr2" && \ + [ "$addr1" != "$addr2" ] && \ + echo "PASS" || (echo "FAIL" && exit 1)