diff --git a/.github/workflows/receiving-icp.yml b/.github/workflows/receiving-icp.yml new file mode 100644 index 0000000000..08da6c207a --- /dev/null +++ b/.github/workflows/receiving-icp.yml @@ -0,0 +1,28 @@ +name: receiving_icp + +on: + push: + branches: [master] + pull_request: + paths: + - rust/receiving-icp/** + - .github/workflows/receiving-icp.yml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + rust-receiving_icp: + 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/receiving-icp + run: | + icp network start -d + icp deploy + bash test.sh diff --git a/rust/receiving-icp/Cargo.lock b/rust/receiving-icp/Cargo.lock index 9321920a1d..4f07043e61 100644 --- a/rust/receiving-icp/Cargo.lock +++ b/rust/receiving-icp/Cargo.lock @@ -20,6 +20,15 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "backend" +version = "0.1.0" +dependencies = [ + "candid", + "ic-cdk 0.20.0", + "ic-ledger-types", +] + [[package]] name = "binread" version = "2.2.0" @@ -60,9 +69,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "candid" -version = "0.10.19" +version = "0.10.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ea81e16df186fae1979175058f05dfbfac6e2fdf3b161edcbdc440ef09232cf" +checksum = "f8f781afa4a1303e3eab4ada0720a874942bcfa936ce01b816ac6378945c43a9" dependencies = [ "anyhow", "binread", @@ -83,9 +92,9 @@ dependencies = [ [[package]] name = "candid_derive" -version = "0.10.19" +version = "0.10.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e6d499625531c41f474e55160a40313b33d002262ddaae40cade71bcc3bc75a" +checksum = "ad6ae8e7944dd0035651bc0e7b3a3e4cb16f5fc43f8ae4fd76b36ff2cd52759f" dependencies = [ "lazy_static", "proc-macro2", @@ -143,8 +152,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -161,13 +180,37 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.106", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn 2.0.106", ] @@ -230,40 +273,71 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "ic-cdk" -version = "0.18.7" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4efb278f5d3ef033b3eed7f01f1096eaf67701896aa5ef69f5eddf5a84833dc0" +checksum = "818d6d5416a8f0212e1b132703b0da51e36c55f2b96677e96f2bbe7702e1bd85" dependencies = [ "candid", "ic-cdk-executor", - "ic-cdk-macros", + "ic-cdk-macros 0.19.0", "ic-error-types", "ic-management-canister-types", "ic0", + "pin-project-lite", "serde", "serde_bytes", "slotmap", "thiserror 2.0.17", ] +[[package]] +name = "ic-cdk" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057912339f889013f42b36cc0623585949ed278457efb32aef041bdc48acb111" +dependencies = [ + "candid", + "ic-cdk-executor", + "ic-cdk-macros 0.20.0", + "ic-error-types", + "ic0", + "pin-project-lite", + "serde", + "thiserror 2.0.17", +] + [[package]] name = "ic-cdk-executor" -version = "1.0.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99f4ee8930fd2e491177e2eb7fff53ee1c407c13b9582bdc7d6920cf83109a2d" +checksum = "33716b730ded33690b8a704bff3533fda87d229e58046823647d28816e9bcee7" dependencies = [ "ic0", "slotmap", + "smallvec", +] + +[[package]] +name = "ic-cdk-macros" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66dad91a214945cb3605bc9ef6901b87e2ac41e3624284c2cabba49d43aa4f43" +dependencies = [ + "candid", + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] name = "ic-cdk-macros" -version = "0.18.7" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb14c5d691cc9d72bb95459b4761e3a4b3444b85a63d17555d5ddd782969a1e" +checksum = "b140627c01710ac185fbc984ab1fda1781ffef4abbd952e07383350899b0952b" dependencies = [ "candid", - "darling", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.106", @@ -282,14 +356,14 @@ dependencies = [ [[package]] name = "ic-ledger-types" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb52826a353b583012628af6da762b52672350686c3275234febfadeca965ea" +checksum = "60ab0da348a638e01beb5bcc6c0b92e51efbe351950ff99c125d53fff77899d5" dependencies = [ "candid", "crc32fast", "hex", - "ic-cdk", + "ic-cdk 0.19.0", "serde", "serde_bytes", "sha2", @@ -297,9 +371,9 @@ dependencies = [ [[package]] name = "ic-management-canister-types" -version = "0.3.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea7e5b8a0f7c3b320d9450ac950547db4f24a31601b5d398f9680b64427455d2" +checksum = "3149217e24186df3f13dc45eee14cdb3e5cad07d0b2b67bd53555c1c55462957" dependencies = [ "candid", "serde", @@ -384,6 +458,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "pretty" version = "0.12.5" @@ -397,9 +477,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -422,15 +502,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "receiving-icp" -version = "0.1.0" -dependencies = [ - "candid", - "ic-cdk", - "ic-ledger-types", -] - [[package]] name = "rustversion" version = "1.0.22" @@ -503,6 +574,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + [[package]] name = "stacker" version = "0.1.22" diff --git a/rust/receiving-icp/Cargo.toml b/rust/receiving-icp/Cargo.toml index 4c558b8bf8..d1e49e317a 100644 --- a/rust/receiving-icp/Cargo.toml +++ b/rust/receiving-icp/Cargo.toml @@ -1,12 +1,3 @@ -[package] -name = "receiving-icp" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -candid = "0.10.19" -ic-cdk = "0.18.7" -ic-ledger-types = "0.15.0" +[workspace] +members = ["backend"] +resolver = "2" diff --git a/rust/receiving-icp/README.md b/rust/receiving-icp/README.md index 564fd9f464..0450cf8f08 100644 --- a/rust/receiving-icp/README.md +++ b/rust/receiving-icp/README.md @@ -2,4 +2,55 @@ A canister demonstrating how to receive ICP tokens by generating account identifiers and checking balances on the ICP ledger. -This canister can be deployed on [ICP Ninja](https://icp.ninja/projects/receive-icp) for quick testing and demonstration. +The canister exposes methods to compute account identifiers (including subaccounts based on arbitrary 128-bit upper/lower values) and to query balances from the ledger canister. This makes it easy to give each user or purpose a distinct deposit address while keeping all ICP under one canister's control. + +## Environment configuration + +The ICP ledger canister ID is configured via `icp.yaml` and read at runtime as a canister environment variable via `ic_cdk::api::env_var_value("ICP_LEDGER_CANISTER_ID")`: + +| Environment | Ledger | Canister ID | +|---|---|---| +| `local` | ICP ledger (pre-deployed by icp-cli) | `ryjl3-tyaaa-aaaaa-aaaba-cai` | +| `staging` | TESTICP ledger | `xafvr-biaaa-aaaai-aql5q-cai` | +| `production` | ICP ledger (mainnet) | `ryjl3-tyaaa-aaaaa-aaaba-cai` | + +The local environment uses the same principal as production because icp-cli's local network pre-deploys the ICP ledger at that well-known address. Staging uses the TESTICP ledger so you can test token flows without spending real ICP. + +## Build and deploy from the command line + +### Prerequisites + +- Node.js +- icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` + +### Install + +```bash +git clone https://github.com/dfinity/examples +cd examples/rust/receiving-icp +``` + +### Deploy and test locally + +```bash +icp network start -d +icp deploy +bash test.sh +icp network stop +``` + +`bash test.sh` runs 7 tests: account identifier format, subaccount uniqueness, funding the main account and a specific subaccount via account ID hex, balance queries, and subaccount independence. Tests are delta-based and idempotent across re-runs. + +### Deploy to staging or production + +```bash +# Staging — targets the TESTICP ledger +icp deploy --environment staging + +# Production — targets the mainnet ICP ledger +icp deploy --environment production +``` + +## Security considerations and best practices + +Refer to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for information on security and best practices for your ICP app. diff --git a/rust/receiving-icp/backend/Cargo.toml b/rust/receiving-icp/backend/Cargo.toml new file mode 100644 index 0000000000..2fb8a3b688 --- /dev/null +++ b/rust/receiving-icp/backend/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "backend" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] +path = "lib.rs" + +[dependencies] +candid = "0.10" +ic-cdk = "0.20" +ic-ledger-types = "0.16" diff --git a/rust/receiving-icp/src/lib.rs b/rust/receiving-icp/backend/lib.rs similarity index 51% rename from rust/receiving-icp/src/lib.rs rename to rust/receiving-icp/backend/lib.rs index 5835d2b3ac..fcb13220b6 100644 --- a/rust/receiving-icp/src/lib.rs +++ b/rust/receiving-icp/backend/lib.rs @@ -1,45 +1,52 @@ use candid::Principal; use ic_ledger_types::{AccountBalanceArgs, AccountIdentifier, Subaccount}; -// This is the ledger principal for TESTICP -// To use real ICP, use `ryjl3-tyaaa-aaaaa-aaaba-cai` instead. -const LEDGER_PRINCIPAL: &str = "xafvr-biaaa-aaaai-aql5q-cai"; +// Read the ledger principal at runtime from the canister environment variable +// set by icp-cli at deploy time. The value is configured per environment in icp.yaml: +// local / production: ryjl3-tyaaa-aaaaa-aaaba-cai (ICP ledger) +// staging: xafvr-biaaa-aaaai-aql5q-cai (TESTICP ledger) +// +// Deploy with `icp deploy --environment staging` to target TESTICP. +fn ledger_principal() -> Principal { + let id = ic_cdk::api::env_var_value("ICP_LEDGER_CANISTER_ID"); + Principal::from_text(&id).expect("invalid ICP_LEDGER_CANISTER_ID") +} fn get_account(upper: u128, lower: u128) -> AccountIdentifier { - // Create a 32-byte array by combining the little endian representation of upper and lower + // Create a 32-byte array by combining the little endian representation of upper and lower. let mut subaccount_bytes = [0u8; 32]; subaccount_bytes[0..16].copy_from_slice(&upper.to_le_bytes()); subaccount_bytes[16..32].copy_from_slice(&lower.to_le_bytes()); AccountIdentifier::new(&ic_cdk::api::canister_self(), &Subaccount(subaccount_bytes)) } -/// Retrieves the canister's main account. +/// Retrieves the canister's main account identifier. #[ic_cdk::query] async fn account() -> String { get_account(0, 0).to_string() } -/// Retrieves the canister's subaccount based on upper and lower values. +/// Retrieves an account identifier for a specific subaccount. #[ic_cdk::query] async fn subaccount(upper: u128, lower: u128) -> String { get_account(upper, lower).to_string() } -/// Retrieves own balance from the ledger. +/// Retrieves the canister's ICP balance from the ledger. #[ic_cdk::update] async fn get_balance() -> u64 { get_balance_of_subaccount(0, 0).await } -/// Retrieves own balance from the ledger from a specific subaccount +/// Retrieves the ICP balance of a specific subaccount from the ledger. #[ic_cdk::update] async fn get_balance_of_subaccount(upper: u128, lower: u128) -> u64 { - let ledger = Principal::from_text(LEDGER_PRINCIPAL).expect("invalid ledger principal"); let account = get_account(upper, lower); - // Retrieves the account's balance from the ledger. - let balance = ic_ledger_types::account_balance(ledger, &AccountBalanceArgs { account }) + let balance = ic_ledger_types::account_balance(ledger_principal(), &AccountBalanceArgs { account }) .await .expect("call to get balance failed"); balance.e8s() } + +ic_cdk::export_candid!(); diff --git a/rust/receiving-icp/dfx.json b/rust/receiving-icp/dfx.json deleted file mode 100644 index 0f2f33af6a..0000000000 --- a/rust/receiving-icp/dfx.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "canisters": { - "receiving-icp": { - "candid": "src/receiving-icp.did", - "package": "receiving-icp", - "type": "rust" - } - }, - "defaults": { - "build": { - "args": "", - "packtool": "" - } - }, - "output_env_file": ".env", - "version": 1 -} diff --git a/rust/receiving-icp/icp.yaml b/rust/receiving-icp/icp.yaml new file mode 100644 index 0000000000..5021e17794 --- /dev/null +++ b/rust/receiving-icp/icp.yaml @@ -0,0 +1,30 @@ +canisters: + - name: backend + recipe: + type: "@dfinity/rust@v3.3.0" + +# ICP_LEDGER_CANISTER_ID is injected at deploy time by the recipe and baked +# into the WASM. Deploy with `--environment staging` to target the TESTICP +# ledger, or omit the flag to use the production ICP ledger (default for +# both local and production). +environments: + - name: local + network: local + settings: + backend: + environment_variables: + ICP_LEDGER_CANISTER_ID: "ryjl3-tyaaa-aaaaa-aaaba-cai" + + - name: staging + network: ic + settings: + backend: + environment_variables: + ICP_LEDGER_CANISTER_ID: "xafvr-biaaa-aaaai-aql5q-cai" + + - name: production + network: ic + settings: + backend: + environment_variables: + ICP_LEDGER_CANISTER_ID: "ryjl3-tyaaa-aaaaa-aaaba-cai" diff --git a/rust/receiving-icp/rust-toolchain.toml b/rust/receiving-icp/rust-toolchain.toml new file mode 100644 index 0000000000..990104f055 --- /dev/null +++ b/rust/receiving-icp/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +targets = ["wasm32-unknown-unknown"] diff --git a/rust/receiving-icp/src/receiving-icp.did b/rust/receiving-icp/src/receiving-icp.did deleted file mode 100644 index 9eea15cd59..0000000000 --- a/rust/receiving-icp/src/receiving-icp.did +++ /dev/null @@ -1,6 +0,0 @@ -service : { - account: () -> (text) query; - subaccount: (nat, nat) -> (text) query; - get_balance: () -> (nat64); - get_balance_of_subaccount: (nat, nat) -> (nat64); -} diff --git a/rust/receiving-icp/test.sh b/rust/receiving-icp/test.sh new file mode 100755 index 0000000000..ee500754d9 --- /dev/null +++ b/rust/receiving-icp/test.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -e + +backend=$(icp canister status backend -i) +echo "Backend: $backend" + +# Extract the nat64 value from Candid output like "(100_000_000 : nat64)" +e8s_from_result() { + echo "$1" | grep -oE '[0-9_]+' | tr -d '_' | head -1 +} + +echo "=== Test 1: account returns a 64-char hex account identifier ===" +result=$(icp canister call --query backend account '()') +echo "$result" +echo "$result" | grep -qE '"[0-9a-f]{64}"' && echo "PASS" || (echo "FAIL" && exit 1) +main_hex=$(echo "$result" | grep -oE '[0-9a-f]{64}') + +echo "=== Test 2: subaccount(0, 0) returns same account as account() ===" +result_sub=$(icp canister call --query backend subaccount '(0, 0)') +echo "account: $result" +echo "subaccount(0,0): $result_sub" +[ "$result" = "$result_sub" ] && echo "PASS" || (echo "FAIL" && exit 1) + +echo "=== Test 3: subaccount(1, 0) differs from subaccount(0, 0) ===" +result_sub1=$(icp canister call --query backend subaccount '(1, 0)') +echo "subaccount(0,0): $result_sub" +echo "subaccount(1,0): $result_sub1" +sub1_hex=$(echo "$result_sub1" | grep -oE '[0-9a-f]{64}') +[ "$result_sub" != "$result_sub1" ] && echo "PASS" || (echo "FAIL" && exit 1) + +echo "=== Test 4: fund main account with 1 ICP — get_balance increases by 100_000_000 e8s ===" +before=$(e8s_from_result "$(icp canister call backend get_balance '()')") +icp token transfer 1 "$main_hex" +after=$(e8s_from_result "$(icp canister call backend get_balance '()')") +delta=$((after - before)) +echo " before=$before after=$after delta=$delta" +[ "$delta" -eq 100000000 ] && echo "PASS" || (echo "FAIL: expected delta +100000000 e8s" && exit 1) + +echo "=== Test 5: get_balance_of_subaccount(0, 0) agrees with get_balance() ===" +balance_main=$(e8s_from_result "$(icp canister call backend get_balance '()')") +balance_sub0=$(e8s_from_result "$(icp canister call backend get_balance_of_subaccount '(0, 0)')") +echo " get_balance(): $balance_main get_balance_of_subaccount(0,0): $balance_sub0" +[ "$balance_main" -eq "$balance_sub0" ] && echo "PASS" || (echo "FAIL: balances differ" && exit 1) + +echo "=== Test 6: fund subaccount(1, 0) via account ID hex — get_balance_of_subaccount increases by 100_000_000 e8s ===" +echo " Subaccount(1,0) account ID: $sub1_hex" +before=$(e8s_from_result "$(icp canister call backend get_balance_of_subaccount '(1, 0)')") +icp token transfer 1 "$sub1_hex" +after=$(e8s_from_result "$(icp canister call backend get_balance_of_subaccount '(1, 0)')") +delta=$((after - before)) +echo " before=$before after=$after delta=$delta" +[ "$delta" -eq 100000000 ] && echo "PASS" || (echo "FAIL: expected delta +100000000 e8s" && exit 1) + +echo "=== Test 7: subaccount(2, 0) is unfunded — proves subaccounts are independent ===" +balance_sub0=$(e8s_from_result "$(icp canister call backend get_balance_of_subaccount '(0, 0)')") +balance_sub2=$(e8s_from_result "$(icp canister call backend get_balance_of_subaccount '(2, 0)')") +echo " subaccount(0,0): $balance_sub0 e8s (funded)" +echo " subaccount(2,0): $balance_sub2 e8s (never funded)" +[ "$balance_sub0" -gt 0 ] || (echo "FAIL: subaccount(0,0) should have balance" && exit 1) +[ "$balance_sub2" -eq 0 ] && echo "PASS" || (echo "FAIL: unfunded subaccount(2,0) should have 0 balance" && exit 1)