From 60c6a233713c6fe2730da4ea5b737bf6f39590a4 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 16 Jun 2026 19:47:01 +0200 Subject: [PATCH 01/12] chore: migrate rust/receiving-icp to icp-cli Replaces dfx.json with icp.yaml, moves Rust source to backend/, adds Makefile with tests for account identifiers and ledger balance queries, and adds a CI workflow using icp-dev-env-rust:1.0.0. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/receiving_icp.yml | 28 +++++++++++++++++ rust/receiving-icp/Cargo.lock | 2 +- rust/receiving-icp/Cargo.toml | 15 ++------- rust/receiving-icp/Makefile | 36 ++++++++++++++++++++++ rust/receiving-icp/README.md | 31 ++++++++++++++++++- rust/receiving-icp/backend/Cargo.toml | 12 ++++++++ rust/receiving-icp/{src => backend}/lib.rs | 0 rust/receiving-icp/dfx.json | 17 ---------- rust/receiving-icp/icp.yaml | 8 +++++ rust/receiving-icp/rust-toolchain.toml | 2 ++ rust/receiving-icp/src/receiving-icp.did | 6 ---- 11 files changed, 120 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/receiving_icp.yml create mode 100644 rust/receiving-icp/Makefile create mode 100644 rust/receiving-icp/backend/Cargo.toml rename rust/receiving-icp/{src => backend}/lib.rs (100%) delete mode 100644 rust/receiving-icp/dfx.json create mode 100644 rust/receiving-icp/icp.yaml create mode 100644 rust/receiving-icp/rust-toolchain.toml delete mode 100644 rust/receiving-icp/src/receiving-icp.did diff --git a/.github/workflows/receiving_icp.yml b/.github/workflows/receiving_icp.yml new file mode 100644 index 0000000000..458c78883f --- /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.0 + 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 + make test diff --git a/rust/receiving-icp/Cargo.lock b/rust/receiving-icp/Cargo.lock index 9321920a1d..2702dfd783 100644 --- a/rust/receiving-icp/Cargo.lock +++ b/rust/receiving-icp/Cargo.lock @@ -423,7 +423,7 @@ dependencies = [ ] [[package]] -name = "receiving-icp" +name = "backend" version = "0.1.0" dependencies = [ "candid", 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/Makefile b/rust/receiving-icp/Makefile new file mode 100644 index 0000000000..8634e7f3c7 --- /dev/null +++ b/rust/receiving-icp/Makefile @@ -0,0 +1,36 @@ +.PHONY: test + +test: + @echo "=== Test 1: account returns a non-empty account identifier ===" + @result=$$(icp canister call --query backend account '()') && \ + echo "$$result" && \ + echo "$$result" | grep -q '"' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 2: subaccount(0, 0) returns same account as account() ===" + @result_account=$$(icp canister call --query backend account '()') && \ + result_sub=$$(icp canister call --query backend subaccount '(0, 0)') && \ + echo "account: $$result_account" && \ + echo "subaccount(0,0): $$result_sub" && \ + [ "$$result_account" = "$$result_sub" ] && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 3: subaccount(1, 0) returns a different account than subaccount(0, 0) ===" + @result_sub0=$$(icp canister call --query backend subaccount '(0, 0)') && \ + result_sub1=$$(icp canister call --query backend subaccount '(1, 0)') && \ + echo "subaccount(0,0): $$result_sub0" && \ + echo "subaccount(1,0): $$result_sub1" && \ + [ "$$result_sub0" != "$$result_sub1" ] && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 4: get_balance returns a numeric balance (0 for fresh canister) ===" + @result=$$(icp canister call backend get_balance '()') && \ + echo "$$result" && \ + echo "$$result" | grep -q '0' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 5: get_balance_of_subaccount(0, 0) returns 0 for fresh canister ===" + @result=$$(icp canister call backend get_balance_of_subaccount '(0, 0)') && \ + echo "$$result" && \ + echo "$$result" | grep -q '0' && \ + echo "PASS" || (echo "FAIL" && exit 1) diff --git a/rust/receiving-icp/README.md b/rust/receiving-icp/README.md index 564fd9f464..dc513a8395 100644 --- a/rust/receiving-icp/README.md +++ b/rust/receiving-icp/README.md @@ -2,4 +2,33 @@ 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. + +> **Note:** By default, the canister connects to a test ICP ledger (`xafvr-biaaa-aaaai-aql5q-cai`). To receive real ICP, update `LEDGER_PRINCIPAL` in `backend/lib.rs` to the mainnet ledger principal `ryjl3-tyaaa-aaaaa-aaaba-cai`. + +## 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 + +```bash +icp network start -d +icp deploy +make test +icp network stop +``` + +## Security considerations and best practices + +For information about security best practices for ICP canisters, see the [security overview](https://docs.internetcomputer.org/guides/security/overview). diff --git a/rust/receiving-icp/backend/Cargo.toml b/rust/receiving-icp/backend/Cargo.toml new file mode 100644 index 0000000000..1677a5f6df --- /dev/null +++ b/rust/receiving-icp/backend/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "backend" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +candid = "0.10" +ic-cdk = "0.18" +ic-ledger-types = "0.15.0" diff --git a/rust/receiving-icp/src/lib.rs b/rust/receiving-icp/backend/lib.rs similarity index 100% rename from rust/receiving-icp/src/lib.rs rename to rust/receiving-icp/backend/lib.rs 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..df5c7edaaf --- /dev/null +++ b/rust/receiving-icp/icp.yaml @@ -0,0 +1,8 @@ +networks: + - name: local + mode: managed + +canisters: + - name: backend + recipe: + type: "@dfinity/rust@v3.3.0" 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); -} From 75f1e3ce790e090af48e75fe052724e834ab7266 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 17 Jun 2026 16:36:47 +0200 Subject: [PATCH 02/12] improve(rust/receiving-icp): per-environment ledger config via icp.yaml + crate bumps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace hardcoded TESTICP ledger principal with env!("ICP_LEDGER_CANISTER_ID") baked at compile time by the @dfinity/rust recipe: local / production → ryjl3-tyaaa-aaaaa-aaaba-cai (ICP ledger) staging → xafvr-biaaa-aaaai-aql5q-cai (TESTICP ledger) icp.yaml now uses the environments block with per-environment environment_variables under settings. Deploy with `icp deploy --environment staging` to target TESTICP. Also: ic-cdk 0.18 → 0.20, ic-ledger-types 0.15 → 0.16, CI image 1.0.0 → 1.0.1. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/receiving_icp.yml | 2 +- rust/receiving-icp/Cargo.lock | 139 ++++++++++++++++++++------ rust/receiving-icp/README.md | 28 ++++-- rust/receiving-icp/backend/Cargo.toml | 5 +- rust/receiving-icp/backend/lib.rs | 21 ++-- rust/receiving-icp/icp.yaml | 30 +++++- 6 files changed, 169 insertions(+), 56 deletions(-) diff --git a/.github/workflows/receiving_icp.yml b/.github/workflows/receiving_icp.yml index 458c78883f..fc9f1147fa 100644 --- a/.github/workflows/receiving_icp.yml +++ b/.github/workflows/receiving_icp.yml @@ -15,7 +15,7 @@ concurrency: jobs: rust-receiving_icp: runs-on: ubuntu-24.04 - container: ghcr.io/dfinity/icp-dev-env-rust:1.0.0 + container: ghcr.io/dfinity/icp-dev-env-rust:1.0.1 env: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: diff --git a/rust/receiving-icp/Cargo.lock b/rust/receiving-icp/Cargo.lock index 2702dfd783..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.18.7" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb14c5d691cc9d72bb95459b4761e3a4b3444b85a63d17555d5ddd782969a1e" +checksum = "66dad91a214945cb3605bc9ef6901b87e2ac41e3624284c2cabba49d43aa4f43" dependencies = [ "candid", - "darling", + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "ic-cdk-macros" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b140627c01710ac185fbc984ab1fda1781ffef4abbd952e07383350899b0952b" +dependencies = [ + "candid", + "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 = "backend" -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/README.md b/rust/receiving-icp/README.md index dc513a8395..40191e8b4f 100644 --- a/rust/receiving-icp/README.md +++ b/rust/receiving-icp/README.md @@ -4,7 +4,17 @@ A canister demonstrating how to receive ICP tokens by generating account identif 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. -> **Note:** By default, the canister connects to a test ICP ledger (`xafvr-biaaa-aaaai-aql5q-cai`). To receive real ICP, update `LEDGER_PRINCIPAL` in `backend/lib.rs` to the mainnet ledger principal `ryjl3-tyaaa-aaaaa-aaaba-cai`. +## Environment configuration + +The ICP ledger canister ID is configured via `icp.yaml` and baked into the WASM at compile time using `env!("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 @@ -13,22 +23,22 @@ The canister exposes methods to compute account identifiers (including subaccoun - 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 ```bash +# Local (default) icp network start -d icp deploy make test icp network stop + +# Staging (targets TESTICP ledger on mainnet) +icp deploy --environment staging + +# Production +icp deploy --environment production ``` ## Security considerations and best practices -For information about security best practices for ICP canisters, see the [security overview](https://docs.internetcomputer.org/guides/security/overview). +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 index 1677a5f6df..2fb8a3b688 100644 --- a/rust/receiving-icp/backend/Cargo.toml +++ b/rust/receiving-icp/backend/Cargo.toml @@ -5,8 +5,9 @@ edition = "2021" [lib] crate-type = ["cdylib"] +path = "lib.rs" [dependencies] candid = "0.10" -ic-cdk = "0.18" -ic-ledger-types = "0.15.0" +ic-cdk = "0.20" +ic-ledger-types = "0.16" diff --git a/rust/receiving-icp/backend/lib.rs b/rust/receiving-icp/backend/lib.rs index 5835d2b3ac..27ec68313a 100644 --- a/rust/receiving-icp/backend/lib.rs +++ b/rust/receiving-icp/backend/lib.rs @@ -1,42 +1,45 @@ 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"; +// The ledger principal is baked in at deploy time from the ICP_LEDGER_CANISTER_ID +// environment variable, which icp.yaml configures per environment: +// local / production: ryjl3-tyaaa-aaaaa-aaaba-cai (ICP ledger) +// staging: xafvr-biaaa-aaaai-aql5q-cai (TESTICP ledger) +// +// See icp.yaml for the full environment configuration. +const LEDGER_PRINCIPAL: &str = env!("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 }) .await .expect("call to get balance failed"); diff --git a/rust/receiving-icp/icp.yaml b/rust/receiving-icp/icp.yaml index df5c7edaaf..5021e17794 100644 --- a/rust/receiving-icp/icp.yaml +++ b/rust/receiving-icp/icp.yaml @@ -1,8 +1,30 @@ -networks: - - name: local - mode: managed - 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" From 26f273a67d0d4e15aa8279587477ef1e13260855 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 17 Jun 2026 16:42:13 +0200 Subject: [PATCH 03/12] fix(rust/receiving-icp): use option_env! with fallback for ledger principal env!() fails at compile time when ICP_LEDGER_CANISTER_ID is not injected by the recipe. The @dfinity/rust recipe does not yet inject environment_variables from the icp.yaml environments.settings block into the cargo build environment (only PUBLIC_CANISTER_ID:* vars are injected automatically). option_env! with a hardcoded fallback ensures the build always succeeds while still demonstrating the per-environment configuration concept in icp.yaml. When/if the recipe gains support for injecting custom env vars, the option_env! approach will pick them up automatically. Co-Authored-By: Claude Sonnet 4.6 --- rust/receiving-icp/backend/lib.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/rust/receiving-icp/backend/lib.rs b/rust/receiving-icp/backend/lib.rs index 27ec68313a..33757caa7f 100644 --- a/rust/receiving-icp/backend/lib.rs +++ b/rust/receiving-icp/backend/lib.rs @@ -1,13 +1,19 @@ use candid::Principal; use ic_ledger_types::{AccountBalanceArgs, AccountIdentifier, Subaccount}; -// The ledger principal is baked in at deploy time from the ICP_LEDGER_CANISTER_ID -// environment variable, which icp.yaml configures per environment: -// local / production: ryjl3-tyaaa-aaaaa-aaaba-cai (ICP ledger) -// staging: xafvr-biaaa-aaaai-aql5q-cai (TESTICP ledger) +// The ledger principal is configured per environment in icp.yaml via +// ICP_LEDGER_CANISTER_ID. If the recipe injects it at build time, that value +// is used; otherwise the default (ICP ledger, same for local and production) +// applies. Deploy with `icp deploy --environment staging` to target TESTICP. // -// See icp.yaml for the full environment configuration. -const LEDGER_PRINCIPAL: &str = env!("ICP_LEDGER_CANISTER_ID"); +// Note: environment_variables in icp.yaml `environments.settings` are intended +// to be injected at build time by the recipe. Until the recipe supports this +// fully, option_env! with a fallback ensures the build always succeeds. +const LEDGER_PRINCIPAL: &str = if let Some(id) = option_env!("ICP_LEDGER_CANISTER_ID") { + id +} else { + "ryjl3-tyaaa-aaaaa-aaaba-cai" // ICP ledger (local and production) +}; fn get_account(upper: u128, lower: u128) -> AccountIdentifier { // Create a 32-byte array by combining the little endian representation of upper and lower. From 91785ed9abb5a7524d0d71b068c4b31eb8af73d7 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 17 Jun 2026 17:33:17 +0200 Subject: [PATCH 04/12] fix(rust/receiving-icp): add export_candid!() + revert to env!() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ic_cdk::export_candid!() — required for candid-extractor to find get_candid_pointer and extract the Candid interface - Revert to env!("ICP_LEDGER_CANISTER_ID") — icp-cli injects environment variables from the icp.yaml environments.settings block at build time, so env!() works correctly Co-Authored-By: Claude Sonnet 4.6 --- rust/receiving-icp/backend/lib.rs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/rust/receiving-icp/backend/lib.rs b/rust/receiving-icp/backend/lib.rs index 33757caa7f..4afc61c69e 100644 --- a/rust/receiving-icp/backend/lib.rs +++ b/rust/receiving-icp/backend/lib.rs @@ -1,19 +1,14 @@ use candid::Principal; use ic_ledger_types::{AccountBalanceArgs, AccountIdentifier, Subaccount}; -// The ledger principal is configured per environment in icp.yaml via -// ICP_LEDGER_CANISTER_ID. If the recipe injects it at build time, that value -// is used; otherwise the default (ICP ledger, same for local and production) -// applies. Deploy with `icp deploy --environment staging` to target TESTICP. +// The ledger principal is injected at build time by icp-cli from the +// ICP_LEDGER_CANISTER_ID environment variable configured per environment +// in icp.yaml: +// local / production: ryjl3-tyaaa-aaaaa-aaaba-cai (ICP ledger) +// staging: xafvr-biaaa-aaaai-aql5q-cai (TESTICP ledger) // -// Note: environment_variables in icp.yaml `environments.settings` are intended -// to be injected at build time by the recipe. Until the recipe supports this -// fully, option_env! with a fallback ensures the build always succeeds. -const LEDGER_PRINCIPAL: &str = if let Some(id) = option_env!("ICP_LEDGER_CANISTER_ID") { - id -} else { - "ryjl3-tyaaa-aaaaa-aaaba-cai" // ICP ledger (local and production) -}; +// Deploy with `icp deploy --environment staging` to target TESTICP. +const LEDGER_PRINCIPAL: &str = env!("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. @@ -52,3 +47,5 @@ async fn get_balance_of_subaccount(upper: u128, lower: u128) -> u64 { balance.e8s() } + +ic_cdk::export_candid!(); From 14d642b5e6df5e32ffbb28f1fe9828541677b40f Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 17 Jun 2026 17:38:11 +0200 Subject: [PATCH 05/12] fix(rust/receiving-icp): use ic_cdk::api::env_var_value at runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Environment variables injected by icp-cli are read at RUNTIME via ic_cdk::api::env_var_value, not at compile time via env!(). They are stored as WASM metadata and retrieved when the canister executes — the same mechanism as Runtime.envVar in Motoko. Co-Authored-By: Claude Sonnet 4.6 --- rust/receiving-icp/backend/lib.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/rust/receiving-icp/backend/lib.rs b/rust/receiving-icp/backend/lib.rs index 4afc61c69e..dc9b0b31f0 100644 --- a/rust/receiving-icp/backend/lib.rs +++ b/rust/receiving-icp/backend/lib.rs @@ -1,14 +1,16 @@ use candid::Principal; use ic_ledger_types::{AccountBalanceArgs, AccountIdentifier, Subaccount}; -// The ledger principal is injected at build time by icp-cli from the -// ICP_LEDGER_CANISTER_ID environment variable configured per environment -// in icp.yaml: +// Read the ledger principal at runtime from the environment variable injected +// by icp-cli. 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. -const LEDGER_PRINCIPAL: &str = env!("ICP_LEDGER_CANISTER_ID"); +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. @@ -39,9 +41,8 @@ async fn get_balance() -> u64 { /// 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); - 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"); From ed0f044772814b55d9458c3461d81453346237e4 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 17 Jun 2026 17:38:46 +0200 Subject: [PATCH 06/12] fix(rust/receiving-icp): README: runtime not compile time --- rust/receiving-icp/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/receiving-icp/README.md b/rust/receiving-icp/README.md index 40191e8b4f..72f1b33944 100644 --- a/rust/receiving-icp/README.md +++ b/rust/receiving-icp/README.md @@ -6,7 +6,7 @@ The canister exposes methods to compute account identifiers (including subaccoun ## Environment configuration -The ICP ledger canister ID is configured via `icp.yaml` and baked into the WASM at compile time using `env!("ICP_LEDGER_CANISTER_ID")`: +The ICP ledger canister ID is configured via `icp.yaml` and read at runtime via `ic_cdk::api::env_var_value("ICP_LEDGER_CANISTER_ID")`: | Environment | Ledger | Canister ID | |---|---|---| From 4c867971bf7de6b51d3812fdb7697f0c9494e9f1 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 17 Jun 2026 17:40:10 +0200 Subject: [PATCH 07/12] docs(rust/receiving-icp): add Install section --- rust/receiving-icp/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rust/receiving-icp/README.md b/rust/receiving-icp/README.md index 72f1b33944..03facd1543 100644 --- a/rust/receiving-icp/README.md +++ b/rust/receiving-icp/README.md @@ -23,6 +23,13 @@ The local environment uses the same principal as production because icp-cli's lo - 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 ```bash From 7616acac5f9383e6083b4c35594cea05f9274fa6 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 17 Jun 2026 17:44:25 +0200 Subject: [PATCH 08/12] improve(rust/receiving-icp): strengthen tests to cover ledger interaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Test 1: verify 64-char hex format (not just presence of a quote) - Test 4: match exact Candid output (0 : nat64) instead of grepping '0' - Test 5 (new): fund canister with 1 ICP, verify get_balance returns 100_000_000 e8s — this actually exercises the ledger inter-canister call - Test 6 (new): verify get_balance_of_subaccount(0,0) also returns 100_000_000 e8s - Test 7 (new): verify get_balance_of_subaccount(1,0) returns 0 (unfunded subaccount) Co-Authored-By: Claude Sonnet 4.6 --- rust/receiving-icp/Makefile | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/rust/receiving-icp/Makefile b/rust/receiving-icp/Makefile index 8634e7f3c7..28c572770d 100644 --- a/rust/receiving-icp/Makefile +++ b/rust/receiving-icp/Makefile @@ -1,21 +1,21 @@ .PHONY: test test: - @echo "=== Test 1: account returns a non-empty account identifier ===" + @echo "=== Test 1: account returns a 64-char hex account identifier ===" @result=$$(icp canister call --query backend account '()') && \ echo "$$result" && \ - echo "$$result" | grep -q '"' && \ + echo "$$result" | grep -qE '"[0-9a-f]{64}"' && \ echo "PASS" || (echo "FAIL" && exit 1) @echo "=== Test 2: subaccount(0, 0) returns same account as account() ===" @result_account=$$(icp canister call --query backend account '()') && \ result_sub=$$(icp canister call --query backend subaccount '(0, 0)') && \ - echo "account: $$result_account" && \ + echo "account: $$result_account" && \ echo "subaccount(0,0): $$result_sub" && \ [ "$$result_account" = "$$result_sub" ] && \ echo "PASS" || (echo "FAIL" && exit 1) - @echo "=== Test 3: subaccount(1, 0) returns a different account than subaccount(0, 0) ===" + @echo "=== Test 3: subaccount(1, 0) differs from subaccount(0, 0) ===" @result_sub0=$$(icp canister call --query backend subaccount '(0, 0)') && \ result_sub1=$$(icp canister call --query backend subaccount '(1, 0)') && \ echo "subaccount(0,0): $$result_sub0" && \ @@ -23,14 +23,28 @@ test: [ "$$result_sub0" != "$$result_sub1" ] && \ echo "PASS" || (echo "FAIL" && exit 1) - @echo "=== Test 4: get_balance returns a numeric balance (0 for fresh canister) ===" + @echo "=== Test 4: get_balance returns 0 for unfunded canister ===" @result=$$(icp canister call backend get_balance '()') && \ echo "$$result" && \ - echo "$$result" | grep -q '0' && \ + echo "$$result" | grep -qF '(0 : nat64)' && \ echo "PASS" || (echo "FAIL" && exit 1) - @echo "=== Test 5: get_balance_of_subaccount(0, 0) returns 0 for fresh canister ===" + @echo "=== Test 5: fund canister with 1 ICP — get_balance returns 100_000_000 e8s ===" + @set -e; \ + backend=$$(icp canister status backend -i); \ + icp token transfer 1 "$$backend"; \ + result=$$(icp canister call backend get_balance '()'); \ + echo "$$result"; \ + echo "$$result" | grep -qF '(100000000 : nat64)' && echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 6: get_balance_of_subaccount(0, 0) also returns 100_000_000 e8s ===" @result=$$(icp canister call backend get_balance_of_subaccount '(0, 0)') && \ echo "$$result" && \ - echo "$$result" | grep -q '0' && \ + echo "$$result" | grep -qF '(100000000 : nat64)' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 7: get_balance_of_subaccount(1, 0) returns 0 (different subaccount) ===" + @result=$$(icp canister call backend get_balance_of_subaccount '(1, 0)') && \ + echo "$$result" && \ + echo "$$result" | grep -qF '(0 : nat64)' && \ echo "PASS" || (echo "FAIL" && exit 1) From a372d53bb666c1a01fc4bb42f703fc81b4c4d4cf Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 17 Jun 2026 17:55:23 +0200 Subject: [PATCH 09/12] improve(rust/receiving-icp): bash test script + subaccount funding tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace Makefile with test.sh (bash) for Git Bash compatibility. New tests: - Test 8: fund subaccount(1,0) via account ID hex using `icp token transfer 1 "$sub1_hex"` — demonstrates that icp token transfer accepts ICP ledger account IDs directly as receivers - Cross-check: verify balance via `icp token balance "$hex"` to show CLI and canister agree on the funded subaccount balance Improvements: - Test 1: grep for 64-char hex format, extract account ID for reuse - Tests 5-7: match exact Candid output (100000000 : nat64) - Tests use natural bash variables (no $$ / \ continuation overhead) Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/receiving_icp.yml | 2 +- rust/receiving-icp/Makefile | 50 ----------------------- rust/receiving-icp/README.md | 2 +- rust/receiving-icp/test.sh | 63 +++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 52 deletions(-) delete mode 100644 rust/receiving-icp/Makefile create mode 100755 rust/receiving-icp/test.sh diff --git a/.github/workflows/receiving_icp.yml b/.github/workflows/receiving_icp.yml index fc9f1147fa..fedc4425d3 100644 --- a/.github/workflows/receiving_icp.yml +++ b/.github/workflows/receiving_icp.yml @@ -25,4 +25,4 @@ jobs: run: | icp network start -d icp deploy - make test + bash test.sh diff --git a/rust/receiving-icp/Makefile b/rust/receiving-icp/Makefile deleted file mode 100644 index 28c572770d..0000000000 --- a/rust/receiving-icp/Makefile +++ /dev/null @@ -1,50 +0,0 @@ -.PHONY: test - -test: - @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) - - @echo "=== Test 2: subaccount(0, 0) returns same account as account() ===" - @result_account=$$(icp canister call --query backend account '()') && \ - result_sub=$$(icp canister call --query backend subaccount '(0, 0)') && \ - echo "account: $$result_account" && \ - echo "subaccount(0,0): $$result_sub" && \ - [ "$$result_account" = "$$result_sub" ] && \ - echo "PASS" || (echo "FAIL" && exit 1) - - @echo "=== Test 3: subaccount(1, 0) differs from subaccount(0, 0) ===" - @result_sub0=$$(icp canister call --query backend subaccount '(0, 0)') && \ - result_sub1=$$(icp canister call --query backend subaccount '(1, 0)') && \ - echo "subaccount(0,0): $$result_sub0" && \ - echo "subaccount(1,0): $$result_sub1" && \ - [ "$$result_sub0" != "$$result_sub1" ] && \ - echo "PASS" || (echo "FAIL" && exit 1) - - @echo "=== Test 4: get_balance returns 0 for unfunded canister ===" - @result=$$(icp canister call backend get_balance '()') && \ - echo "$$result" && \ - echo "$$result" | grep -qF '(0 : nat64)' && \ - echo "PASS" || (echo "FAIL" && exit 1) - - @echo "=== Test 5: fund canister with 1 ICP — get_balance returns 100_000_000 e8s ===" - @set -e; \ - backend=$$(icp canister status backend -i); \ - icp token transfer 1 "$$backend"; \ - result=$$(icp canister call backend get_balance '()'); \ - echo "$$result"; \ - echo "$$result" | grep -qF '(100000000 : nat64)' && echo "PASS" || (echo "FAIL" && exit 1) - - @echo "=== Test 6: get_balance_of_subaccount(0, 0) also returns 100_000_000 e8s ===" - @result=$$(icp canister call backend get_balance_of_subaccount '(0, 0)') && \ - echo "$$result" && \ - echo "$$result" | grep -qF '(100000000 : nat64)' && \ - echo "PASS" || (echo "FAIL" && exit 1) - - @echo "=== Test 7: get_balance_of_subaccount(1, 0) returns 0 (different subaccount) ===" - @result=$$(icp canister call backend get_balance_of_subaccount '(1, 0)') && \ - echo "$$result" && \ - echo "$$result" | grep -qF '(0 : nat64)' && \ - echo "PASS" || (echo "FAIL" && exit 1) diff --git a/rust/receiving-icp/README.md b/rust/receiving-icp/README.md index 03facd1543..bed3d67954 100644 --- a/rust/receiving-icp/README.md +++ b/rust/receiving-icp/README.md @@ -36,7 +36,7 @@ cd examples/rust/receiving-icp # Local (default) icp network start -d icp deploy -make test +bash test.sh icp network stop # Staging (targets TESTICP ledger on mainnet) diff --git a/rust/receiving-icp/test.sh b/rust/receiving-icp/test.sh new file mode 100755 index 0000000000..f769653e22 --- /dev/null +++ b/rust/receiving-icp/test.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -e + +backend=$(icp canister status backend -i) +echo "Backend: $backend" + +e8s_from_icp() { + echo "$1" | awk '{printf "%.0f", $1 * 100000000}' +} + +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: get_balance returns 0 before funding ===" +result=$(icp canister call backend get_balance '()') +echo "$result" +echo "$result" | grep -qF '(0 : nat64)' && echo "PASS" || (echo "FAIL" && exit 1) + +echo "=== Test 5: fund main account with 1 ICP — get_balance returns 100_000_000 e8s ===" +icp token transfer 1 "$main_hex" +result=$(icp canister call backend get_balance '()') +echo "$result" +echo "$result" | grep -qF '(100000000 : nat64)' && echo "PASS" || (echo "FAIL" && exit 1) + +echo "=== Test 6: get_balance_of_subaccount(0, 0) matches get_balance() ===" +result=$(icp canister call backend get_balance_of_subaccount '(0, 0)') +echo "$result" +echo "$result" | grep -qF '(100000000 : nat64)' && echo "PASS" || (echo "FAIL" && exit 1) + +echo "=== Test 7: get_balance_of_subaccount(1, 0) returns 0 before funding ===" +result=$(icp canister call backend get_balance_of_subaccount '(1, 0)') +echo "$result" +echo "$result" | grep -qF '(0 : nat64)' && echo "PASS" || (echo "FAIL" && exit 1) + +echo "=== Test 8: fund subaccount(1, 0) via account ID hex — get_balance_of_subaccount returns 100_000_000 e8s ===" +echo " Subaccount(1,0) account ID: $sub1_hex" +icp token transfer 1 "$sub1_hex" +result=$(icp canister call backend get_balance_of_subaccount '(1, 0)') +echo "$result" +echo "$result" | grep -qF '(100000000 : nat64)' && echo "PASS" || (echo "FAIL" && exit 1) + +# Cross-check: icp token balance with account ID hex should agree +echo "=== Cross-check: icp token balance with account ID hex ===" +balance_cli=$(icp token balance -q "$sub1_hex") +balance_e8s=$(e8s_from_icp "$balance_cli") +echo " icp token balance: $balance_cli ($balance_e8s e8s)" +[ "$balance_e8s" -eq 100000000 ] && echo "PASS" || (echo "FAIL: expected 100000000 e8s" && exit 1) From 3abb2bce5f91ebb261482063b06916267da7297b Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 17 Jun 2026 17:59:44 +0200 Subject: [PATCH 10/12] fix(rust/receiving-icp): match (100_000_000 : nat64) with underscores --- rust/receiving-icp/test.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/receiving-icp/test.sh b/rust/receiving-icp/test.sh index f769653e22..8bb6a36a61 100755 --- a/rust/receiving-icp/test.sh +++ b/rust/receiving-icp/test.sh @@ -36,12 +36,12 @@ echo "=== Test 5: fund main account with 1 ICP — get_balance returns 100_000_0 icp token transfer 1 "$main_hex" result=$(icp canister call backend get_balance '()') echo "$result" -echo "$result" | grep -qF '(100000000 : nat64)' && echo "PASS" || (echo "FAIL" && exit 1) +echo "$result" | grep -qF '(100_000_000 : nat64)' && echo "PASS" || (echo "FAIL" && exit 1) echo "=== Test 6: get_balance_of_subaccount(0, 0) matches get_balance() ===" result=$(icp canister call backend get_balance_of_subaccount '(0, 0)') echo "$result" -echo "$result" | grep -qF '(100000000 : nat64)' && echo "PASS" || (echo "FAIL" && exit 1) +echo "$result" | grep -qF '(100_000_000 : nat64)' && echo "PASS" || (echo "FAIL" && exit 1) echo "=== Test 7: get_balance_of_subaccount(1, 0) returns 0 before funding ===" result=$(icp canister call backend get_balance_of_subaccount '(1, 0)') @@ -53,7 +53,7 @@ echo " Subaccount(1,0) account ID: $sub1_hex" icp token transfer 1 "$sub1_hex" result=$(icp canister call backend get_balance_of_subaccount '(1, 0)') echo "$result" -echo "$result" | grep -qF '(100000000 : nat64)' && echo "PASS" || (echo "FAIL" && exit 1) +echo "$result" | grep -qF '(100_000_000 : nat64)' && echo "PASS" || (echo "FAIL" && exit 1) # Cross-check: icp token balance with account ID hex should agree echo "=== Cross-check: icp token balance with account ID hex ===" From 81056ac1ac70082723f4a2a44175370295205a16 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 17 Jun 2026 18:08:07 +0200 Subject: [PATCH 11/12] fix(rust/receiving-icp): idempotent delta-based tests + clean README/summary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test.sh: - All balance checks now delta-based (before/after funding) — idempotent across re-runs regardless of prior state - Test 7: check unfunded subaccount(2,0) has 0 balance to prove independence (previous approach assumed (0,0) and (1,0) had different balances which fails after equal funding runs) - Remove unused e8s_from_icp helper and cross-check block README: - Separate "Deploy and test locally" from "Deploy to staging or production" - Describe what test.sh covers (7 tests, delta-based, idempotent) Co-Authored-By: Claude Sonnet 4.6 --- rust/receiving-icp/README.md | 13 +++++--- rust/receiving-icp/test.sh | 63 +++++++++++++++++------------------- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/rust/receiving-icp/README.md b/rust/receiving-icp/README.md index bed3d67954..c65f8a3ccf 100644 --- a/rust/receiving-icp/README.md +++ b/rust/receiving-icp/README.md @@ -30,19 +30,24 @@ git clone https://github.com/dfinity/examples cd examples/rust/receiving-icp ``` -### Deploy and test +### Deploy and test locally ```bash -# Local (default) 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. -# Staging (targets TESTICP ledger on mainnet) +### Deploy to staging or production + +```bash +# Staging — targets the TESTICP ledger icp deploy --environment staging -# Production +# Production — targets the mainnet ICP ledger icp deploy --environment production ``` diff --git a/rust/receiving-icp/test.sh b/rust/receiving-icp/test.sh index 8bb6a36a61..ee500754d9 100755 --- a/rust/receiving-icp/test.sh +++ b/rust/receiving-icp/test.sh @@ -4,8 +4,9 @@ set -e backend=$(icp canister status backend -i) echo "Backend: $backend" -e8s_from_icp() { - echo "$1" | awk '{printf "%.0f", $1 * 100000000}' +# 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 ===" @@ -27,37 +28,33 @@ 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: get_balance returns 0 before funding ===" -result=$(icp canister call backend get_balance '()') -echo "$result" -echo "$result" | grep -qF '(0 : nat64)' && echo "PASS" || (echo "FAIL" && exit 1) - -echo "=== Test 5: fund main account with 1 ICP — get_balance returns 100_000_000 e8s ===" +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" -result=$(icp canister call backend get_balance '()') -echo "$result" -echo "$result" | grep -qF '(100_000_000 : nat64)' && echo "PASS" || (echo "FAIL" && exit 1) - -echo "=== Test 6: get_balance_of_subaccount(0, 0) matches get_balance() ===" -result=$(icp canister call backend get_balance_of_subaccount '(0, 0)') -echo "$result" -echo "$result" | grep -qF '(100_000_000 : nat64)' && echo "PASS" || (echo "FAIL" && exit 1) - -echo "=== Test 7: get_balance_of_subaccount(1, 0) returns 0 before funding ===" -result=$(icp canister call backend get_balance_of_subaccount '(1, 0)') -echo "$result" -echo "$result" | grep -qF '(0 : nat64)' && echo "PASS" || (echo "FAIL" && exit 1) - -echo "=== Test 8: fund subaccount(1, 0) via account ID hex — get_balance_of_subaccount returns 100_000_000 e8s ===" +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" -result=$(icp canister call backend get_balance_of_subaccount '(1, 0)') -echo "$result" -echo "$result" | grep -qF '(100_000_000 : nat64)' && echo "PASS" || (echo "FAIL" && exit 1) - -# Cross-check: icp token balance with account ID hex should agree -echo "=== Cross-check: icp token balance with account ID hex ===" -balance_cli=$(icp token balance -q "$sub1_hex") -balance_e8s=$(e8s_from_icp "$balance_cli") -echo " icp token balance: $balance_cli ($balance_e8s e8s)" -[ "$balance_e8s" -eq 100000000 ] && echo "PASS" || (echo "FAIL: expected 100000000 e8s" && exit 1) +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) From 40a070f9b0f3d4ea3c1598038b7222189542adce Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 18 Jun 2026 09:14:19 +0200 Subject: [PATCH 12/12] fix(rust/receiving-icp): address Mathias review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib.rs + README: describe ICP_LEDGER_CANISTER_ID as a canister environment variable (a canister setting applied at deploy time via icp.yaml), not 'WASM metadata' — per ic_cdk::api::env_var_value docs - Rename receiving_icp.yml → receiving-icp.yml to match example dir name convention (.yml) Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/{receiving_icp.yml => receiving-icp.yml} | 2 +- rust/receiving-icp/README.md | 2 +- rust/receiving-icp/backend/lib.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename .github/workflows/{receiving_icp.yml => receiving-icp.yml} (93%) diff --git a/.github/workflows/receiving_icp.yml b/.github/workflows/receiving-icp.yml similarity index 93% rename from .github/workflows/receiving_icp.yml rename to .github/workflows/receiving-icp.yml index fedc4425d3..08da6c207a 100644 --- a/.github/workflows/receiving_icp.yml +++ b/.github/workflows/receiving-icp.yml @@ -6,7 +6,7 @@ on: pull_request: paths: - rust/receiving-icp/** - - .github/workflows/receiving_icp.yml + - .github/workflows/receiving-icp.yml concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/rust/receiving-icp/README.md b/rust/receiving-icp/README.md index c65f8a3ccf..0450cf8f08 100644 --- a/rust/receiving-icp/README.md +++ b/rust/receiving-icp/README.md @@ -6,7 +6,7 @@ The canister exposes methods to compute account identifiers (including subaccoun ## Environment configuration -The ICP ledger canister ID is configured via `icp.yaml` and read at runtime via `ic_cdk::api::env_var_value("ICP_LEDGER_CANISTER_ID")`: +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 | |---|---|---| diff --git a/rust/receiving-icp/backend/lib.rs b/rust/receiving-icp/backend/lib.rs index dc9b0b31f0..fcb13220b6 100644 --- a/rust/receiving-icp/backend/lib.rs +++ b/rust/receiving-icp/backend/lib.rs @@ -1,8 +1,8 @@ use candid::Principal; use ic_ledger_types::{AccountBalanceArgs, AccountIdentifier, Subaccount}; -// Read the ledger principal at runtime from the environment variable injected -// by icp-cli. The value is configured per environment in icp.yaml: +// 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) //