From 0d99e8faec19d714f9bc7109b4a4da126bf4d042 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 16 Jun 2026 20:03:41 +0200 Subject: [PATCH 01/12] chore: migrate rust/stake_neuron_from_cli to icp-cli Converts the dfx-based CLI binary into an icp-cli canister example. Replaces ic-agent/tokio/clap with ic-cdk inter-canister calls, adds icp.yaml (rust@v3.3.0), Makefile with local compute_subaccount tests, CI workflow, and updated README preserving the educational NNS staking domain knowledge. Co-Authored-By: Claude Sonnet 4.6 --- .../rust-stake-neuron-from-cli-example.yaml | 37 - .github/workflows/stake_neuron_from_cli.yml | 28 + rust/stake_neuron_from_cli/Cargo.lock | 2859 ----------------- rust/stake_neuron_from_cli/Cargo.toml | 22 +- rust/stake_neuron_from_cli/Makefile | 24 + rust/stake_neuron_from_cli/README.md | 133 +- rust/stake_neuron_from_cli/backend/Cargo.toml | 14 + rust/stake_neuron_from_cli/backend/lib.rs | 257 ++ rust/stake_neuron_from_cli/dfx.json | 17 - rust/stake_neuron_from_cli/icp.yaml | 8 + .../stake_neuron_from_cli/rust-toolchain.toml | 2 + rust/stake_neuron_from_cli/setup_and_run.sh | 121 - rust/stake_neuron_from_cli/src/lib.rs | 219 -- rust/stake_neuron_from_cli/src/main.rs | 40 - 14 files changed, 393 insertions(+), 3388 deletions(-) delete mode 100644 .github/workflows/rust-stake-neuron-from-cli-example.yaml create mode 100644 .github/workflows/stake_neuron_from_cli.yml delete mode 100644 rust/stake_neuron_from_cli/Cargo.lock create mode 100644 rust/stake_neuron_from_cli/Makefile create mode 100644 rust/stake_neuron_from_cli/backend/Cargo.toml create mode 100644 rust/stake_neuron_from_cli/backend/lib.rs delete mode 100644 rust/stake_neuron_from_cli/dfx.json create mode 100644 rust/stake_neuron_from_cli/icp.yaml create mode 100644 rust/stake_neuron_from_cli/rust-toolchain.toml delete mode 100755 rust/stake_neuron_from_cli/setup_and_run.sh delete mode 100644 rust/stake_neuron_from_cli/src/lib.rs delete mode 100644 rust/stake_neuron_from_cli/src/main.rs diff --git a/.github/workflows/rust-stake-neuron-from-cli-example.yaml b/.github/workflows/rust-stake-neuron-from-cli-example.yaml deleted file mode 100644 index 8f88e6ff44..0000000000 --- a/.github/workflows/rust-stake-neuron-from-cli-example.yaml +++ /dev/null @@ -1,37 +0,0 @@ -name: rust-stake-neuron-from-cli -on: - push: - branches: - - master - pull_request: - paths: - - rust/stake_neuron_from_cli/** - - .github/workflows/provision-darwin.sh - - .github/workflows/provision-linux.sh - - .github/workflows/rust-stake-neuron-from-cli-example.yaml -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true -jobs: - rust-stake-neuron-from-cli-example-darwin: - runs-on: macos-15 - steps: - - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1.2.0 - - name: Provision Darwin - run: bash .github/workflows/provision-darwin.sh - - name: Rust Stake Neuron from CLI Darwin - run: | - pushd rust/stake_neuron_from_cli - ./setup_and_run.sh - popd - rust-stake-neuron-from-cli-example-linux: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1.2.0 - - name: Provision Linux - run: bash .github/workflows/provision-linux.sh - - name: Rust Stake Neuron from CLI Linux - run: | - pushd rust/stake_neuron_from_cli - ./setup_and_run.sh - popd diff --git a/.github/workflows/stake_neuron_from_cli.yml b/.github/workflows/stake_neuron_from_cli.yml new file mode 100644 index 0000000000..4ba1791753 --- /dev/null +++ b/.github/workflows/stake_neuron_from_cli.yml @@ -0,0 +1,28 @@ +name: stake_neuron_from_cli + +on: + push: + branches: [master] + pull_request: + paths: + - rust/stake_neuron_from_cli/** + - .github/workflows/stake_neuron_from_cli.yml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + rust-stake_neuron_from_cli: + 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/stake_neuron_from_cli + run: | + icp network start -d + icp deploy + make test diff --git a/rust/stake_neuron_from_cli/Cargo.lock b/rust/stake_neuron_from_cli/Cargo.lock deleted file mode 100644 index b2f1dccca2..0000000000 --- a/rust/stake_neuron_from_cli/Cargo.lock +++ /dev/null @@ -1,2859 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "anstream" -version = "0.6.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" -dependencies = [ - "windows-sys 0.60.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.60.2", -] - -[[package]] -name = "anyhow" -version = "1.0.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - -[[package]] -name = "async-lock" -version = "3.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "backoff" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" -dependencies = [ - "getrandom 0.2.16", - "instant", - "rand 0.8.5", -] - -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" - -[[package]] -name = "binread" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16598dfc8e6578e9b597d9910ba2e73618385dc9f4b1d43dd92c349d6be6418f" -dependencies = [ - "binread_derive", - "lazy_static", - "rustversion", -] - -[[package]] -name = "binread_derive" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9672209df1714ee804b1f4d4f68c8eb2a90b1f7a07acf472f88ce198ef1fed" -dependencies = [ - "either", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "bitflags" -version = "2.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" - -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bls12_381" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3c196a77437e7cc2fb515ce413a6401291578b5afc8ecb29a3c7ab957f05941" -dependencies = [ - "digest 0.9.0", - "ff 0.12.1", - "group 0.12.1", - "pairing", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" - -[[package]] -name = "cached" -version = "0.46.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c8c50262271cdf5abc979a5f76515c234e764fa025d1ba4862c0f0bcda0e95" -dependencies = [ - "ahash", - "hashbrown", - "instant", - "once_cell", - "thiserror 1.0.69", -] - -[[package]] -name = "candid" -version = "0.10.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaac522d18020d5fbc8320ecb12a9b13b2137ae31133da2d42fa256a825507c4" -dependencies = [ - "anyhow", - "binread", - "byteorder", - "candid_derive", - "hex", - "ic_principal", - "leb128", - "num-bigint", - "num-traits", - "paste", - "pretty", - "serde", - "serde_bytes", - "stacker", - "thiserror 1.0.69", -] - -[[package]] -name = "candid_derive" -version = "0.10.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a1b4fddbd462182050989068d53604a91a3d0f117c3c8316c6818023df00add" -dependencies = [ - "lazy_static", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "cc" -version = "1.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "clap" -version = "4.5.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "clap_lex" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "curve25519-dalek-ng" -version = "4.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.6.4", - "subtle-ng", - "zeroize", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "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", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "const-oid", - "crypto-common", - "subtle", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "ed25519-consensus" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" -dependencies = [ - "curve25519-dalek-ng", - "hex", - "rand_core 0.6.4", - "serde", - "sha2 0.9.9", - "thiserror 1.0.69", - "zeroize", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest 0.10.7", - "ff 0.13.1", - "generic-array", - "group 0.13.0", - "pem-rfc7468", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "ff" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", - "zeroize", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasi 0.14.3+wasi-0.2.4", - "wasm-bindgen", -] - -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "group" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" -dependencies = [ - "ff 0.12.1", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff 0.13.1", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "half" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] - -[[package]] -name = "hyper-util" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "ic-agent" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f08d1b54f1834fb9637a57855f1d87bb9dbec7a9dec09499737580692829cb0" -dependencies = [ - "async-lock", - "backoff", - "cached", - "candid", - "ed25519-consensus", - "futures-util", - "hex", - "http", - "http-body", - "ic-certification", - "ic-transport-types", - "ic-verify-bls-signature", - "k256", - "leb128", - "p256", - "pem", - "pkcs8", - "rand 0.8.5", - "rangemap", - "reqwest", - "ring", - "rustls-webpki 0.101.7", - "sec1", - "serde", - "serde_bytes", - "serde_cbor", - "serde_repr", - "sha2 0.10.9", - "simple_asn1", - "thiserror 1.0.69", - "time", - "tokio", - "url", -] - -[[package]] -name = "ic-cdk" -version = "0.18.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4efb278f5d3ef033b3eed7f01f1096eaf67701896aa5ef69f5eddf5a84833dc0" -dependencies = [ - "candid", - "ic-cdk-executor", - "ic-cdk-macros", - "ic-error-types", - "ic-management-canister-types", - "ic0", - "serde", - "serde_bytes", - "slotmap", - "thiserror 2.0.16", -] - -[[package]] -name = "ic-cdk-executor" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99f4ee8930fd2e491177e2eb7fff53ee1c407c13b9582bdc7d6920cf83109a2d" -dependencies = [ - "ic0", - "slotmap", -] - -[[package]] -name = "ic-cdk-macros" -version = "0.18.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb14c5d691cc9d72bb95459b4761e3a4b3444b85a63d17555d5ddd782969a1e" -dependencies = [ - "candid", - "darling", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "ic-certification" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64ee3d8b6e81b51f245716d3e0badb63c283c00f3c9fb5d5219afc30b5bf821" -dependencies = [ - "hex", - "serde", - "serde_bytes", - "sha2 0.10.9", -] - -[[package]] -name = "ic-error-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbeeb3d91aa179d6496d7293becdacedfc413c825cac79fd54ea1906f003ee55" -dependencies = [ - "serde", - "strum", - "strum_macros", -] - -[[package]] -name = "ic-ledger-types" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb52826a353b583012628af6da762b52672350686c3275234febfadeca965ea" -dependencies = [ - "candid", - "crc32fast", - "hex", - "ic-cdk", - "serde", - "serde_bytes", - "sha2 0.10.9", -] - -[[package]] -name = "ic-management-canister-types" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea7e5b8a0f7c3b320d9450ac950547db4f24a31601b5d398f9680b64427455d2" -dependencies = [ - "candid", - "serde", - "serde_bytes", -] - -[[package]] -name = "ic-transport-types" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1c68959f501cfcb79319b6720fd1903a661d77b503e6128bfce31f91ad87aca" -dependencies = [ - "candid", - "hex", - "ic-certification", - "leb128", - "serde", - "serde_bytes", - "serde_repr", - "sha2 0.10.9", - "thiserror 1.0.69", -] - -[[package]] -name = "ic-verify-bls-signature" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583b1c03380cf86059160cc6c91dcbf56c7b5f141bf3a4f06bc79762d775fac4" -dependencies = [ - "bls12_381", - "lazy_static", - "pairing", - "sha2 0.9.9", -] - -[[package]] -name = "ic0" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8877193e1921b5fd16accb0305eb46016868cd1935b05c05eca0ec007b943272" - -[[package]] -name = "ic_principal" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1762deb6f7c8d8c2bdee4b6c5a47b60195b74e9b5280faa5ba29692f8e17429c" -dependencies = [ - "crc32fast", - "data-encoding", - "serde", - "sha2 0.10.9", - "thiserror 1.0.69", -] - -[[package]] -name = "icu_collections" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" - -[[package]] -name = "icu_properties" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "potential_utf", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" - -[[package]] -name = "icu_provider" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" -dependencies = [ - "displaydoc", - "icu_locale_core", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags", - "cfg-if", - "libc", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "k256" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" -dependencies = [ - "cfg-if", - "ecdsa", - "elliptic-curve", - "once_cell", - "sha2 0.10.9", - "signature", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leb128" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" - -[[package]] -name = "libc" -version = "0.2.175" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" - -[[package]] -name = "litemap" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" - -[[package]] -name = "lock_api" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "memchr" -version = "2.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" -dependencies = [ - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", - "serde", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2 0.10.9", -] - -[[package]] -name = "pairing" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135590d8bdba2b31346f9cd1fb2a912329f5135e832a4f422942eb6ead8b6b3b" -dependencies = [ - "group 0.12.1", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.52.6", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pem" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b13fe415cdf3c8e44518e18a7c95a13431d9bdf6d15367d82b23c377fdd441a" -dependencies = [ - "base64 0.21.7", - "serde", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "potential_utf" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "pretty" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac98773b7109bc75f475ab5a134c9b64b87e59d776d31098d8f346922396a477" -dependencies = [ - "arrayvec", - "typed-arena", - "unicode-width", -] - -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[package]] -name = "proc-macro2" -version = "1.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "psm" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" -dependencies = [ - "cc", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.16", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" -dependencies = [ - "bytes", - "getrandom 0.3.3", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.16", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.3", -] - -[[package]] -name = "rangemap" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223" - -[[package]] -name = "redox_syscall" -version = "0.5.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" -dependencies = [ - "bitflags", -] - -[[package]] -name = "reqwest" -version = "0.12.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "webpki-roots", -] - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustls" -version = "0.23.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki 0.103.4", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_bytes" -version = "0.11.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_cbor" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" -dependencies = [ - "half", - "serde", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "serde_json" -version = "1.0.143" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" -dependencies = [ - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - -[[package]] -name = "simple_asn1" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror 2.0.16", - "time", -] - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "slotmap" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "stacker" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys 0.59.0", -] - -[[package]] -name = "stake_neuron_from_cli" -version = "0.1.0" -dependencies = [ - "candid", - "clap", - "crc32fast", - "hex", - "ic-agent", - "ic-ledger-types", - "serde", - "sha2 0.10.9", - "tokio", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.106", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "subtle-ng" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" -dependencies = [ - "thiserror-impl 2.0.16", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "time" -version = "0.3.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" - -[[package]] -name = "time-macros" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.47.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" -dependencies = [ - "backtrace", - "bytes", - "io-uring", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "slab", - "socket2", - "tokio-macros", - "windows-sys 0.59.0", -] - -[[package]] -name = "tokio-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" -dependencies = [ - "once_cell", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typed-arena" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" - -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasi" -version = "0.14.3+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "web-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.3", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - -[[package]] -name = "wit-bindgen" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" - -[[package]] -name = "writeable" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" - -[[package]] -name = "yoke" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" - -[[package]] -name = "zerotrie" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] diff --git a/rust/stake_neuron_from_cli/Cargo.toml b/rust/stake_neuron_from_cli/Cargo.toml index 352e09a524..d1e49e317a 100644 --- a/rust/stake_neuron_from_cli/Cargo.toml +++ b/rust/stake_neuron_from_cli/Cargo.toml @@ -1,19 +1,3 @@ -[package] -name = "stake_neuron_from_cli" -version = "0.1.0" -edition = "2021" - -[[bin]] -name = "stake_neuron_from_cli" -path = "src/main.rs" - -[dependencies] -ic-agent = "0.35" -ic-ledger-types = "0.15.0" -tokio = { version = "1", features = ["full"] } -clap = { version = "4", features = ["derive"] } -candid = "0.10" -sha2 = "0.10" -hex = "0.4" -serde = { version = "1.0", features = ["derive"] } -crc32fast = "1.3" +[workspace] +members = ["backend"] +resolver = "2" diff --git a/rust/stake_neuron_from_cli/Makefile b/rust/stake_neuron_from_cli/Makefile new file mode 100644 index 0000000000..04637b7a13 --- /dev/null +++ b/rust/stake_neuron_from_cli/Makefile @@ -0,0 +1,24 @@ +.PHONY: test + +test: + @echo "=== Test 1/2: compute_subaccount returns a 64-char hex string ===" + @result=$$(icp canister call --query backend compute_subaccount \ + '(principal "aaaaa-aa", 0 : nat64)') && \ + echo "$$result" && \ + echo "$$result" | grep -qE '"[0-9a-f]{64}"' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "=== Test 2/2: compute_subaccount is deterministic (same inputs → same output) ===" + @r1=$$(icp canister call --query backend compute_subaccount \ + '(principal "aaaaa-aa", 42 : nat64)') && \ + r2=$$(icp canister call --query backend compute_subaccount \ + '(principal "aaaaa-aa", 42 : nat64)') && \ + echo "Run 1: $$r1" && \ + echo "Run 2: $$r2" && \ + [ "$$r1" = "$$r2" ] && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "" + @echo "NOTE: stake_neuron requires the canister to hold ICP and NNS Governance" + @echo "to be reachable. Test against mainnet by calling:" + @echo " icp canister call --network ic backend stake_neuron '( : nat64, : nat64)'" diff --git a/rust/stake_neuron_from_cli/README.md b/rust/stake_neuron_from_cli/README.md index 815a87f4f4..8390fc39dc 100644 --- a/rust/stake_neuron_from_cli/README.md +++ b/rust/stake_neuron_from_cli/README.md @@ -1,121 +1,102 @@ -# Neuron Staking from CLI +# Neuron Staking -This example demonstrates how to stake ICP to create an NNS Governance neuron. +This example demonstrates how to stake ICP to create an NNS Governance neuron from a canister. It shows the subaccount computation and two-step staking process required to correctly create a neuron. + +> **Mainnet required for full staking**: The `stake_neuron` endpoint makes inter-canister calls to the ICP Ledger (`ryjl3-tyaaa-aaaaa-aaaba-cai`) and NNS Governance (`rrkah-fqaaa-aaaaa-aaaaq-cai`) canisters, which only exist on mainnet. The `compute_subaccount` query can be tested locally. ## What you can learn -### 1. **Neuron Staking Subaccount Calculation** +### 1. Neuron Staking Subaccount Calculation + +The neuron staking subaccount is the most critical thing to get right when staking a neuron. +If you send your ICP to the wrong subaccount, the ICP will be permanently lost, as there will +not be a way to ask the Governance canister to retrieve it. -The neuron staking subaccount is the most critical thing to get right when staking a neuron. -If you send your ICP to the wrong subaccount, the ICP will be permanently lost, as there will not be a way -to ask the Governance canister to retrieve it. +Therefore, test your implementation (like in this example project) against a known reference +before sending real ICP. -Therefore, test your implementation (like in this example project) against a test version of Governance to ensure -that it is able to send the ICP to the right destination. +The subaccount is a domain-separated SHA-256 hash over the controller principal and nonce: ```rust -/// Compute the subaccount for neuron staking fn compute_neuron_staking_subaccount(controller: Principal, nonce: u64) -> [u8; 32] { - let domain_length: [u8; 1] = [b"neuron-stake".len() as u8]; + let domain = b"neuron-stake"; + let domain_length: [u8; 1] = [domain.len() as u8]; let mut hasher = Sha256::new(); - hasher.update(&domain_length); - hasher.update(b"neuron-stake"); + hasher.update(domain_length); + hasher.update(domain); hasher.update(controller.as_slice()); - hasher.update(&nonce.to_be_bytes()); + hasher.update(nonce.to_be_bytes()); hasher.finalize().into() } ``` -### 2. **Two-Step Neuron Creation Process** +### 2. Two-Step Neuron Creation Process The neuron creation flow requires first making a ledger transfer, and then sending a message to tell NNS Governance about the transfer you made so that it can create a neuron. -1. **Transfer ICP** → Transfer tokens to NNS Governance at the computed subaccount -2. **Claim Neuron** → Call `claim_or_refresh_neuron_from_account` to create the neuron +1. **Transfer ICP** — Transfer tokens to NNS Governance at the computed subaccount +2. **Claim Neuron** — Call `claim_or_refresh_neuron_from_account` to register the transfer as a neuron + +The nonce used in step 1 (as the transfer memo) must match the nonce used in step 2. +The minimum staking amount enforced by NNS Governance is 1 ICP (100,000,000 e8s). +The standard ICP transfer fee of 0.0001 ICP (10,000 e8s) is deducted on top of the staked amount. ```rust // Step 1: Transfer ICP to the computed subaccount -let to_account = AccountIdentifier::new( & governance_principal, & subaccount); let transfer_args = TransferArgs { -memo: Memo(args.nonce), -amount: Tokens::from_e8s(args.amount), -fee: DEFAULT_FEE, // Standard ICP transfer fee -from_subaccount: None, -to: to_account, -created_at_time: None, + memo: Memo(nonce), + amount: Tokens { e8s: amount }, + fee: Tokens { e8s: 10_000 }, + to: AccountIdentifier { hash: build_account_identifier(governance_principal, &subaccount) }, + .. }; -let transfer_result_bytes = agent -.update( & Principal::from_text(ICP_LEDGER_CANISTER_ID) ?, "transfer") -.with_arg(Encode!(&transfer_args)?) -.call_and_wait() -.await?; - // Step 2: Claim the neuron using the same nonce let claim_request = ClaimOrRefreshNeuronFromAccount { -controller: Some(controller), -memo: args.nonce, + controller: Some(canister_id), + memo: nonce, }; - -let claim_result_bytes = agent -.update( -& governance_principal, -"claim_or_refresh_neuron_from_account", -) -.with_arg(Encode!(&claim_request)?) -.call_and_wait() -.await?; ``` -### 3. **Identity Management with ic-agent** +## Build and deploy from the command line -Additionally, you can see how you can use ic-agent to use different key formats to send messages. - -```rust -async fn load_identity(identity_path: &PathBuf) -> Result, Box> { - let pem_content = std::fs::read_to_string(identity_path)?; +### Prerequisites - // Try different identity formats - if let Ok(identity) = BasicIdentity::from_pem(pem_content.as_bytes()) { - return Ok(Box::new(identity)); - } +- Node.js +- icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` - if let Ok(identity) = Secp256k1Identity::from_pem(pem_content.as_bytes()) { - return Ok(Box::new(identity)); - } +### Install - Err("Could not parse identity file".into()) -} +```bash +git clone https://github.com/dfinity/examples +cd examples/rust/stake_neuron_from_cli ``` -## Running the Example - -### Prerequisites - -1. **Install dfx**: - Follow [DFINITY SDK installation](https://internetcomputer.org/docs/current/developer-docs/setup/install/) -2. **Rust toolchain**: `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs/ | sh` +### Deploy and test -### Run the script ```bash -# Set up local NNS and run example -chmod +x setup_and_run.sh -./setup_and_run.sh +icp network start -d +icp deploy +make test +icp network stop ``` -This will: +The `make test` target verifies the `compute_subaccount` query locally. -1. Start a local IC replica -2. Deploy ICP Ledger and NNS Governance canisters with basic configuration -3. Run the staking example -4. Leave the environment running for inspection +### Testing stake_neuron on mainnet -### Inspecting Results - -After running locally, you can verify the neuron was created: +To test the full staking flow you need a canister deployed to mainnet that holds ICP: ```bash -# Query the governance canister -dfx canister call nns_governance list_neurons "(record {neuron_ids=vec{}; include_neurons_readable_by_caller=true})" +# Deploy to mainnet +icp deploy --network ic + +# Fund the canister with ICP (from your wallet), then call stake_neuron. +# amount: e8s to stake (minimum 100_000_000 = 1 ICP), nonce: unique u64 per neuron +icp canister call --network ic backend stake_neuron '(100010000 : nat64, 0 : nat64)' ``` + +## 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/stake_neuron_from_cli/backend/Cargo.toml b/rust/stake_neuron_from_cli/backend/Cargo.toml new file mode 100644 index 0000000000..aa976d4106 --- /dev/null +++ b/rust/stake_neuron_from_cli/backend/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "backend" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] +path = "lib.rs" + +[dependencies] +candid = "0.10" +ic-cdk = "0.20" +serde = { version = "1.0", features = ["derive"] } +sha2 = "0.10" diff --git a/rust/stake_neuron_from_cli/backend/lib.rs b/rust/stake_neuron_from_cli/backend/lib.rs new file mode 100644 index 0000000000..f11ee48fe7 --- /dev/null +++ b/rust/stake_neuron_from_cli/backend/lib.rs @@ -0,0 +1,257 @@ +use candid::{CandidType, Deserialize, Principal}; +use ic_cdk::call::Call; +use ic_cdk::{query, update}; +use sha2::{Digest, Sha256}; + +// Canonical mainnet canister IDs for ICP Ledger and NNS Governance +const ICP_LEDGER_CANISTER_ID: &str = "ryjl3-tyaaa-aaaaa-aaaba-cai"; +const NNS_GOVERNANCE_CANISTER_ID: &str = "rrkah-fqaaa-aaaaa-aaaaq-cai"; + +// Standard ICP transfer fee: 0.0001 ICP = 10,000 e8s +const DEFAULT_FEE_E8S: u64 = 10_000; + +// ── Candid types for ICP Ledger ───────────────────────────────────────────── + +#[derive(CandidType, Deserialize, Clone, Debug)] +struct AccountIdentifier { + hash: Vec, +} + +#[derive(CandidType, Deserialize, Clone, Debug)] +struct Tokens { + e8s: u64, +} + +#[derive(CandidType, Deserialize, Clone, Debug)] +struct Memo(u64); + +#[derive(CandidType, Deserialize, Clone, Debug)] +struct TransferArgs { + memo: Memo, + amount: Tokens, + fee: Tokens, + from_subaccount: Option>, + to: AccountIdentifier, + created_at_time: Option, +} + +#[derive(CandidType, Deserialize, Debug)] +enum TransferError { + BadFee { expected_fee: Tokens }, + InsufficientFunds { balance: Tokens }, + TxTooOld { allowed_window_nanos: u64 }, + TxCreatedInFuture, + TxDuplicate { duplicate_of: u64 }, +} + +#[derive(CandidType, Deserialize, Debug)] +enum TransferResult { + Ok(u64), + Err(TransferError), +} + +// ── Candid types for NNS Governance ───────────────────────────────────────── + +#[derive(CandidType, Deserialize, Debug)] +struct ClaimOrRefreshNeuronFromAccount { + controller: Option, + memo: u64, +} + +#[derive(CandidType, Deserialize, Debug)] +struct NeuronId { + id: u64, +} + +#[derive(CandidType, Deserialize, Debug)] +struct GovernanceError { + error_type: i32, + error_message: String, +} + +#[derive(CandidType, Deserialize, Debug)] +enum ClaimOrRefreshResult { + Error(GovernanceError), + NeuronId(NeuronId), +} + +#[derive(CandidType, Deserialize, Debug)] +struct ClaimOrRefreshNeuronFromAccountResponse { + result: Option, +} + +// ── Public return type ─────────────────────────────────────────────────────── + +#[derive(CandidType, Deserialize, Debug)] +pub struct StakeNeuronResult { + pub neuron_id: u64, + pub controller: Principal, + pub staked_amount_e8s: u64, + pub block_index: u64, + pub subaccount_hex: String, +} + +// ── Core logic ─────────────────────────────────────────────────────────────── + +/// Compute the subaccount for neuron staking. +/// +/// The neuron staking subaccount is the most critical thing to get right. +/// If ICP is sent to the wrong subaccount it is permanently lost — the +/// Governance canister cannot retrieve it. +/// +/// Domain-separated SHA-256 hash over: +/// \x0c (length of "neuron-stake" = 12) +/// "neuron-stake" +/// controller bytes +/// nonce as big-endian u64 +fn compute_neuron_staking_subaccount(controller: Principal, nonce: u64) -> [u8; 32] { + let domain = b"neuron-stake"; + let domain_length: [u8; 1] = [domain.len() as u8]; + let mut hasher = Sha256::new(); + hasher.update(domain_length); + hasher.update(domain); + hasher.update(controller.as_slice()); + hasher.update(nonce.to_be_bytes()); + hasher.finalize().into() +} + +/// Build an ICP AccountIdentifier from a principal + subaccount. +/// +/// Format: CRC32(0x0a || "account-id" || principal || subaccount) +/// || 0x0a || "account-id" || principal || subaccount +/// The final identifier is the last 28 bytes of the 32-byte SHA-256 hash, +/// prepended with the 4-byte CRC32 checksum. +fn build_account_identifier(principal: Principal, subaccount: &[u8; 32]) -> Vec { + let mut hasher = Sha256::new(); + hasher.update(b"\x0aaccount-id"); + hasher.update(principal.as_slice()); + hasher.update(subaccount); + let hash: [u8; 32] = hasher.finalize().into(); + + // CRC32 over the 28-byte account body (bytes 4..32 of hash) + let crc = crc32(&hash[4..]); + let mut result = Vec::with_capacity(32); + result.extend_from_slice(&crc.to_be_bytes()); + result.extend_from_slice(&hash[4..]); + result +} + +fn crc32(data: &[u8]) -> u32 { + let mut crc: u32 = 0xFFFF_FFFF; + for &byte in data { + crc ^= u32::from(byte); + for _ in 0..8 { + if crc & 1 != 0 { + crc = (crc >> 1) ^ 0xEDB8_8320; + } else { + crc >>= 1; + } + } + } + !crc +} + +fn hex_encode(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{:02x}", b)).collect() +} + +// ── Canister endpoints ─────────────────────────────────────────────────────── + +/// Return the hex-encoded neuron staking subaccount for the given +/// (controller, nonce) pair. Call this before sending any ICP to verify +/// the destination subaccount matches your expectation. +#[query] +fn compute_subaccount(controller: Principal, nonce: u64) -> String { + let sub = compute_neuron_staking_subaccount(controller, nonce); + hex_encode(&sub) +} + +/// Perform the two-step neuron staking process on behalf of this canister: +/// +/// 1. Transfer `amount` e8s of ICP from this canister's default account to +/// the NNS Governance canister at the computed staking subaccount. +/// 2. Call `claim_or_refresh_neuron_from_account` on the Governance canister +/// to complete the neuron creation. +/// +/// **Prerequisites (mainnet):** +/// - This canister must hold at least `amount + 10_000` e8s of ICP +/// (the extra 10_000 e8s cover the standard transfer fee). +/// - The minimum staking amount enforced by NNS Governance is 100_000_000 e8s +/// (1 ICP). +/// +/// **Local testing:** The NNS canisters are not deployed on a default local +/// network, so this method can only be exercised against mainnet or a local +/// NNS replica. See the README for details. +#[update] +async fn stake_neuron(amount: u64, nonce: u64) -> Result { + let controller = ic_cdk::api::msg_caller(); + let canister_id = ic_cdk::api::canister_self(); + + let subaccount = compute_neuron_staking_subaccount(controller, nonce); + let governance_principal = + Principal::from_text(NNS_GOVERNANCE_CANISTER_ID).map_err(|e| e.to_string())?; + + let to_account = AccountIdentifier { + hash: build_account_identifier(governance_principal, &subaccount), + }; + + // Step 1: Transfer ICP to the governance staking subaccount + let transfer_args = TransferArgs { + memo: Memo(nonce), + amount: Tokens { e8s: amount }, + fee: Tokens { + e8s: DEFAULT_FEE_E8S, + }, + from_subaccount: None, + to: to_account, + created_at_time: None, + }; + + let ledger = + Principal::from_text(ICP_LEDGER_CANISTER_ID).map_err(|e| e.to_string())?; + + let transfer_result: TransferResult = Call::bounded_wait(ledger, "transfer") + .with_arg(transfer_args) + .await + .map_err(|e| format!("Transfer call failed: {:?}", e))? + .candid() + .map_err(|e| format!("Transfer decode failed: {:?}", e))?; + + let block_index = match transfer_result { + TransferResult::Ok(idx) => idx, + TransferResult::Err(e) => return Err(format!("Transfer failed: {:?}", e)), + }; + + // Step 2: Claim the neuron using the same nonce + let claim_request = ClaimOrRefreshNeuronFromAccount { + controller: Some(canister_id), + memo: nonce, + }; + + let claim_response: ClaimOrRefreshNeuronFromAccountResponse = + Call::bounded_wait(governance_principal, "claim_or_refresh_neuron_from_account") + .with_arg(claim_request) + .await + .map_err(|e| format!("Governance call failed: {:?}", e))? + .candid() + .map_err(|e| format!("Governance decode failed: {:?}", e))?; + + let neuron_id = match claim_response.result { + Some(ClaimOrRefreshResult::NeuronId(n)) => n.id, + Some(ClaimOrRefreshResult::Error(e)) => { + return Err(format!("Governance error ({}): {}", e.error_type, e.error_message)) + } + None => return Err("No result returned from governance canister".to_string()), + }; + + Ok(StakeNeuronResult { + neuron_id, + controller, + staked_amount_e8s: amount, + block_index, + subaccount_hex: hex_encode(&subaccount), + }) +} + +// Export Candid interface +ic_cdk::export_candid!(); diff --git a/rust/stake_neuron_from_cli/dfx.json b/rust/stake_neuron_from_cli/dfx.json deleted file mode 100644 index d6192020e9..0000000000 --- a/rust/stake_neuron_from_cli/dfx.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": 1, - "canisters": { - "icp_ledger": { - "type": "custom", - "candid": "https://raw.githubusercontent.com/dfinity/ic/69b755062f5ef0a7d6efc9a127172b46121420c8/rs/ledger_suite/icp/ledger.did", - "wasm": "https://download.dfinity.systems/ic/69b755062f5ef0a7d6efc9a127172b46121420c8/canisters/ledger-canister_notify-method.wasm.gz", - "specified_id": "ryjl3-tyaaa-aaaaa-aaaba-cai" - }, - "nns_governance": { - "type": "custom", - "candid": "https://raw.githubusercontent.com/dfinity/ic/69b755062f5ef0a7d6efc9a127172b46121420c8/rs/nns/governance/canister/governance.did", - "wasm": "https://download.dfinity.systems/ic/69b755062f5ef0a7d6efc9a127172b46121420c8/canisters/governance-canister.wasm.gz", - "specified_id": "rrkah-fqaaa-aaaaa-aaaaq-cai" - } - } -} \ No newline at end of file diff --git a/rust/stake_neuron_from_cli/icp.yaml b/rust/stake_neuron_from_cli/icp.yaml new file mode 100644 index 0000000000..df5c7edaaf --- /dev/null +++ b/rust/stake_neuron_from_cli/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/stake_neuron_from_cli/rust-toolchain.toml b/rust/stake_neuron_from_cli/rust-toolchain.toml new file mode 100644 index 0000000000..990104f055 --- /dev/null +++ b/rust/stake_neuron_from_cli/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +targets = ["wasm32-unknown-unknown"] diff --git a/rust/stake_neuron_from_cli/setup_and_run.sh b/rust/stake_neuron_from_cli/setup_and_run.sh deleted file mode 100755 index 228e94fe38..0000000000 --- a/rust/stake_neuron_from_cli/setup_and_run.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env bash -set -e - -echo "===========STARTING LOCAL IC=========" -# Stop any existing dfx instance -dfx stop 2>/dev/null || true - -# Start clean local IC -dfx start --background --clean --host "127.0.0.1:4943" - -# Wait a moment for the replica to be ready -sleep 3 - -echo "===========CREATING TEST IDENTITY=========" -# Create a test identity for our CLI -dfx identity new test_user_stake_neuron_from_cli --storage-mode plaintext || true -dfx identity use test_user_stake_neuron_from_cli - -echo "===========SETTING UP ACCOUNTS=========" -# Get account identifiers for initialization -export MINTER_ACCOUNT_ID=$(dfx --identity anonymous ledger account-id) -export DEFAULT_ACCOUNT_ID=$(dfx ledger account-id) - -echo "Minter account: $MINTER_ACCOUNT_ID" -echo "Default account: $DEFAULT_ACCOUNT_ID" - -echo "===========DEPLOYING LEDGER=========" -# Deploy ICP ledger with initial balance -dfx deploy icp_ledger --argument " - (variant { - Init = record { - minting_account = \"$MINTER_ACCOUNT_ID\"; - initial_values = vec { - record { - \"$DEFAULT_ACCOUNT_ID\"; - record { - e8s = 1_000_000_000_000 : nat64; - }; - }; - }; - send_whitelist = vec {}; - transfer_fee = opt record { - e8s = 10_000 : nat64; - }; - token_symbol = opt \"LICP\"; - token_name = opt \"Local ICP\"; - } - }) -" - -echo "===========CHECKING LEDGER BALANCE=========" -# Verify ledger is working -dfx canister call icp_ledger account_balance "(record { account = $(python3 -c 'print("vec{" + ";".join([str(b) for b in bytes.fromhex("'$DEFAULT_ACCOUNT_ID'")]) + "}")') })" - -echo "===========DEPLOYING GOVERNANCE=========" -# Deploy NNS governance with all required fields -dfx deploy nns_governance --argument " - (record { - default_followees = vec {}; - making_sns_proposal = null; - most_recent_monthly_node_provider_rewards = null; - maturity_modulation_last_updated_at_timestamp_seconds = null; - wait_for_quiet_threshold_seconds = 86400 : nat64; - metrics = null; - neuron_management_voting_period_seconds = null; - node_providers = vec {}; - cached_daily_maturity_modulation_basis_points = null; - economics = opt record { - neuron_minimum_stake_e8s = 100_000_000 : nat64; - max_proposals_to_keep_per_topic = 100 : nat32; - neuron_management_fee_per_proposal_e8s = 1_000_000 : nat64; - reject_cost_e8s = 1_000_000 : nat64; - transaction_fee_e8s = 10_000 : nat64; - neuron_spawn_dissolve_delay_seconds = 604_800 : nat64; - minimum_icp_xdr_rate = 2_590 : nat64; - maximum_node_provider_rewards_e8s = 1_000_000_000 : nat64; - neurons_fund_economics = null; - voting_power_economics = null; - }; - restore_aging_summary = null; - spawning_neurons = null; - latest_reward_event = null; - to_claim_transfers = vec {}; - short_voting_period_seconds = 345600 : nat64; - proposals = vec {}; - xdr_conversion_rate = null; - in_flight_commands = vec {}; - neurons = vec {}; - genesis_timestamp_seconds = $(date +%s) : nat64; - }) -" - -echo "===========SETUP COMPLETE=========" -echo "Local IC is running with:" -echo "- ICP Ledger: $(dfx canister id icp_ledger)" -echo "- NNS Governance: $(dfx canister id nns_governance)" - -echo "" -echo "===========RUNNING CLI TOOL=========" -# Find the identity file for the test_user_stake_neuron_from_cli -IDENTITY_FILE="$HOME/.config/dfx/identity/test_user_stake_neuron_from_cli/identity.pem" - -if [ ! -f "$IDENTITY_FILE" ]; then - echo "ERROR: Identity file not found at $IDENTITY_FILE" - echo "Available identities:" - dfx identity list - exit 1 -fi - -echo "Using identity file: $IDENTITY_FILE" -cargo run -- --identity "$IDENTITY_FILE" --url "http://127.0.0.1:4943" - -echo "" -echo "===========SETUP REMAINS ACTIVE=========" -echo "The local IC is still running. You can now:" -echo "- Query canisters: dfx canister call ''" -echo "- Check status: dfx canister status " -echo "- View canister IDs: dfx canister id " -echo "" -echo "To stop the local IC later, run: dfx stop" -echo "Current replica URL: http://127.0.0.1:4943" \ No newline at end of file diff --git a/rust/stake_neuron_from_cli/src/lib.rs b/rust/stake_neuron_from_cli/src/lib.rs deleted file mode 100644 index 97ef0dd777..0000000000 --- a/rust/stake_neuron_from_cli/src/lib.rs +++ /dev/null @@ -1,219 +0,0 @@ -use candid::{CandidType, Decode, Deserialize, Encode, Principal}; -use ic_agent::identity::{BasicIdentity, Secp256k1Identity}; -use ic_agent::{Agent, Identity}; -use ic_ledger_types::{ - AccountIdentifier, Memo, Subaccount, Tokens, TransferArgs, TransferResult, DEFAULT_FEE, -}; -use sha2::{Digest, Sha256}; -use std::path::PathBuf; - -// Canonical canister IDs -const ICP_LEDGER_CANISTER_ID: &str = "ryjl3-tyaaa-aaaaa-aaaba-cai"; -const NNS_GOVERNANCE_CANISTER_ID: &str = "rrkah-fqaaa-aaaaa-aaaaq-cai"; - -pub struct StakeNeuronArgs { - pub identity_path: PathBuf, - pub ic_url: String, - pub amount: u64, - pub nonce: u64, -} - -pub async fn stake_neuron( - args: StakeNeuronArgs, -) -> Result> { - println!("Loading identity from: {}", args.identity_path.display()); - println!("Connecting to IC at: {}", args.ic_url); - println!( - "Staking amount: {} e8s ({} ICP)", - args.amount, - args.amount as f64 / 100_000_000.0 - ); - println!("Using nonce: {}", args.nonce); - - let identity = load_identity(&args.identity_path).await?; - - let controller = identity.sender()?; - println!("Identity principal: {}", controller); - - let agent = Agent::builder() - .with_url(&args.ic_url) - .with_identity(identity) - .build() - .expect("Failed to create IC agent"); - - agent.fetch_root_key().await?; - - println!("Successfully created IC agent!"); - - // Step 1: Compute neuron staking subaccount - let subaccount = Subaccount(compute_neuron_staking_subaccount(controller, args.nonce)); - println!("Calculated subaccount: \"{}\"", hex::encode(subaccount.0)); - - // Step 2: Transfer ICP to governance canister subaccount - println!("\n===========TRANSFERRING ICP TO GOVERNANCE========="); - let governance_principal = Principal::from_text(NNS_GOVERNANCE_CANISTER_ID)?; - - let to_account = AccountIdentifier::new(&governance_principal, &subaccount); - let transfer_args = TransferArgs { - memo: Memo(args.nonce), - amount: Tokens::from_e8s(args.amount), - fee: DEFAULT_FEE, // Standard ICP transfer fee - from_subaccount: None, - to: to_account, - created_at_time: None, - }; - - let transfer_result_bytes = agent - .update(&Principal::from_text(ICP_LEDGER_CANISTER_ID)?, "transfer") - .with_arg(Encode!(&transfer_args)?) - .call_and_wait() - .await?; - - let transfer_result = Decode!(&transfer_result_bytes, TransferResult)?; - - let block_index = match transfer_result { - TransferResult::Ok(block_index) => { - println!("Transfer successful! Block index: {}", block_index); - block_index - } - TransferResult::Err(transfer_error) => { - println!("Transfer failed: {:?}", transfer_error); - return Err(format!("Transfer failed: {:?}", transfer_error).into()); - } - }; - - // Step 3: Claim or refresh neuron from account - println!("\n===========CLAIMING NEURON FROM ACCOUNT========="); - - let claim_request = ClaimOrRefreshNeuronFromAccount { - controller: Some(controller), - memo: args.nonce, - }; - - let claim_result_bytes = agent - .update( - &governance_principal, - "claim_or_refresh_neuron_from_account", - ) - .with_arg(Encode!(&claim_request)?) - .call_and_wait() - .await?; - - println!("Raw response bytes length: {}", claim_result_bytes.len()); - println!( - "Raw response bytes (hex): {}", - hex::encode(&claim_result_bytes) - ); - - let claim_result = Decode!(&claim_result_bytes, ClaimOrRefreshNeuronFromAccountResponse)?; - - println!("Full governance response: {:?}", claim_result); - - let neuron_id = if let Some(result) = claim_result.result { - match result { - ClaimOrRefreshResult::Error(error) => { - println!( - "Governance error: {} (type: {})", - error.error_message, error.error_type - ); - return Err(format!("Governance error: {}", error.error_message).into()); - } - ClaimOrRefreshResult::NeuronId(neuron_id) => { - println!("Neuron successfully claimed/refreshed!"); - println!("Neuron ID: {}", neuron_id.id); - println!("Controller: {}", controller); - println!( - "Staked amount: {} e8s ({} ICP)", - args.amount, - args.amount as f64 / 100_000_000.0 - ); - neuron_id.id - } - } - } else { - println!("No result received - this might indicate a decoding issue"); - return Err("No result received from governance canister".into()); - }; - - println!("\nNeuron staking completed successfully!"); - - Ok(NeuronStakingResult { - neuron_id, - controller, - staked_amount_e8s: args.amount, - block_index, - subaccount, - }) -} - -pub struct NeuronStakingResult { - pub neuron_id: u64, - pub controller: Principal, - pub staked_amount_e8s: u64, - pub block_index: u64, - pub subaccount: Subaccount, -} - -async fn load_identity( - identity_path: &PathBuf, -) -> Result, Box> { - println!( - "Attempting to load identity from: {}", - identity_path.display() - ); - - // Read the PEM file - let pem_content = std::fs::read_to_string(identity_path)?; - - // Try to parse as different identity types - if let Ok(identity) = BasicIdentity::from_pem(pem_content.as_bytes()) { - println!("Loaded basic identity"); - return Ok(Box::new(identity)); - } - - if let Ok(identity) = Secp256k1Identity::from_pem(pem_content.as_bytes()) { - println!("Loaded secp256k1 identity"); - return Ok(Box::new(identity)); - } - - Err("Could not parse identity file as either BasicIdentity or Secp256k1Identity".into()) -} - -/// Compute the subaccount for neuron staking -fn compute_neuron_staking_subaccount(controller: Principal, nonce: u64) -> [u8; 32] { - let domain_length: [u8; 1] = [b"neuron-stake".len() as u8]; - let mut hasher = Sha256::new(); - hasher.update(&domain_length); - hasher.update(b"neuron-stake"); - hasher.update(controller.as_slice()); - hasher.update(&nonce.to_be_bytes()); - hasher.finalize().into() -} - -#[derive(CandidType, Deserialize, Debug)] -struct NeuronId { - id: u64, -} - -#[derive(CandidType, Deserialize, Debug)] -struct ClaimOrRefreshNeuronFromAccount { - controller: Option, - memo: u64, -} - -#[derive(CandidType, Deserialize, Debug)] -enum ClaimOrRefreshResult { - Error(GovernanceError), - NeuronId(NeuronId), -} - -#[derive(CandidType, Deserialize, Debug)] -struct ClaimOrRefreshNeuronFromAccountResponse { - result: Option, -} - -#[derive(CandidType, Deserialize, Debug)] -struct GovernanceError { - error_type: i32, - error_message: String, -} diff --git a/rust/stake_neuron_from_cli/src/main.rs b/rust/stake_neuron_from_cli/src/main.rs deleted file mode 100644 index d8fc4b0733..0000000000 --- a/rust/stake_neuron_from_cli/src/main.rs +++ /dev/null @@ -1,40 +0,0 @@ -use clap::Parser; -use std::path::PathBuf; -use stake_neuron_from_cli::{stake_neuron, StakeNeuronArgs}; - -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct Args { - /// Path to the private key file (PEM format) - #[arg(short, long)] - identity: PathBuf, - - /// URL of the IC replica - #[arg(short, long, default_value = "http://127.0.0.1:4943")] - url: String, - - /// Amount of ICP to stake (in e8s, default: 1 ICP + fees = 100_010_000 e8s) - #[arg(short, long, default_value = "100010000")] - amount: u64, - - /// Nonce for the neuron staking subaccount - #[arg(short, long, default_value = "0")] - nonce: u64, -} - -#[tokio::main] -async fn main() -> Result<(), Box> { - let args = Args::parse(); - - let Args { identity, url, amount, nonce }= args; - - let stake_args = StakeNeuronArgs { - identity_path: identity, - ic_url: url, - amount, - nonce, - }; - - let _result = stake_neuron(stake_args).await?; - Ok(()) -} \ No newline at end of file From 232a3c7d173a06370705bd9a777c5e2d36093d7a Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 17 Jun 2026 19:02:38 +0200 Subject: [PATCH 02/12] chore(rust/stake_neuron_from_cli): test.sh over Makefile, bump CI image to 1.0.1 Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/stake_neuron_from_cli.yml | 4 ++-- rust/stake_neuron_from_cli/Makefile | 24 --------------------- rust/stake_neuron_from_cli/README.md | 4 ++-- rust/stake_neuron_from_cli/test.sh | 24 +++++++++++++++++++++ 4 files changed, 28 insertions(+), 28 deletions(-) delete mode 100644 rust/stake_neuron_from_cli/Makefile create mode 100755 rust/stake_neuron_from_cli/test.sh diff --git a/.github/workflows/stake_neuron_from_cli.yml b/.github/workflows/stake_neuron_from_cli.yml index 4ba1791753..046ca23fba 100644 --- a/.github/workflows/stake_neuron_from_cli.yml +++ b/.github/workflows/stake_neuron_from_cli.yml @@ -15,7 +15,7 @@ concurrency: jobs: rust-stake_neuron_from_cli: 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: @@ -25,4 +25,4 @@ jobs: run: | icp network start -d icp deploy - make test + bash test.sh diff --git a/rust/stake_neuron_from_cli/Makefile b/rust/stake_neuron_from_cli/Makefile deleted file mode 100644 index 04637b7a13..0000000000 --- a/rust/stake_neuron_from_cli/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -.PHONY: test - -test: - @echo "=== Test 1/2: compute_subaccount returns a 64-char hex string ===" - @result=$$(icp canister call --query backend compute_subaccount \ - '(principal "aaaaa-aa", 0 : nat64)') && \ - echo "$$result" && \ - echo "$$result" | grep -qE '"[0-9a-f]{64}"' && \ - echo "PASS" || (echo "FAIL" && exit 1) - - @echo "=== Test 2/2: compute_subaccount is deterministic (same inputs → same output) ===" - @r1=$$(icp canister call --query backend compute_subaccount \ - '(principal "aaaaa-aa", 42 : nat64)') && \ - r2=$$(icp canister call --query backend compute_subaccount \ - '(principal "aaaaa-aa", 42 : nat64)') && \ - echo "Run 1: $$r1" && \ - echo "Run 2: $$r2" && \ - [ "$$r1" = "$$r2" ] && \ - echo "PASS" || (echo "FAIL" && exit 1) - - @echo "" - @echo "NOTE: stake_neuron requires the canister to hold ICP and NNS Governance" - @echo "to be reachable. Test against mainnet by calling:" - @echo " icp canister call --network ic backend stake_neuron '( : nat64, : nat64)'" diff --git a/rust/stake_neuron_from_cli/README.md b/rust/stake_neuron_from_cli/README.md index 8390fc39dc..1d98bc9062 100644 --- a/rust/stake_neuron_from_cli/README.md +++ b/rust/stake_neuron_from_cli/README.md @@ -78,11 +78,11 @@ cd examples/rust/stake_neuron_from_cli ```bash icp network start -d icp deploy -make test +bash test.sh icp network stop ``` -The `make test` target verifies the `compute_subaccount` query locally. +The `bash test.sh` script verifies the `compute_subaccount` query locally. ### Testing stake_neuron on mainnet diff --git a/rust/stake_neuron_from_cli/test.sh b/rust/stake_neuron_from_cli/test.sh new file mode 100755 index 0000000000..340386bcaa --- /dev/null +++ b/rust/stake_neuron_from_cli/test.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -e + +echo "=== Test 1/2: compute_subaccount returns a 64-char hex string ===" +result=$(icp canister call --query backend compute_subaccount \ + '(principal "aaaaa-aa", 0 : nat64)') && \ + echo "$result" && \ + echo "$result" | grep -qE '"[0-9a-f]{64}"' && \ + echo "PASS" || (echo "FAIL" && exit 1) + +echo "=== Test 2/2: compute_subaccount is deterministic (same inputs → same output) ===" +r1=$(icp canister call --query backend compute_subaccount \ + '(principal "aaaaa-aa", 42 : nat64)') && \ + r2=$(icp canister call --query backend compute_subaccount \ + '(principal "aaaaa-aa", 42 : nat64)') && \ + echo "Run 1: $r1" && \ + echo "Run 2: $r2" && \ + [ "$r1" = "$r2" ] && \ + echo "PASS" || (echo "FAIL" && exit 1) + +echo "" +echo "NOTE: stake_neuron requires the canister to hold ICP and NNS Governance" +echo "to be reachable. Test against mainnet by calling:" +echo " icp canister call --network ic backend stake_neuron '( : nat64, : nat64)'" From 88466d19fbb22c31c724c27a8b753c4522fbdd47 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 2 Jul 2026 16:57:45 +0200 Subject: [PATCH 03/12] fix(rust/stake_neuron_from_cli): enable local NNS testing, fix controller/subaccount bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add nns: true to icp.yaml so NNS canisters deploy at mainnet IDs locally - Fix bug in stake_neuron: subaccount was computed from msg_caller() but Governance was told controller = canister_self(); Governance recomputes the expected subaccount from the controller field, so these must match — switched both to canister_self() since the canister holds the ICP and owns the neuron - Replace hand-rolled ledger types (AccountIdentifier, TransferArgs, Tokens, Memo, crc32, build_account_identifier) with ic-ledger-types = "0.16"; the transfer() helper handles correct Candid serialization of AccountIdentifier as a blob - Update test.sh: fund backend with 2 ICP via icp token transfer, add Test 3 that calls stake_neuron and asserts a neuron ID is returned from local NNS Governance - Update edition to 2024 Co-Authored-By: Claude Sonnet 4.6 --- rust/stake_neuron_from_cli/backend/Cargo.toml | 3 +- rust/stake_neuron_from_cli/backend/lib.rs | 156 ++++-------------- rust/stake_neuron_from_cli/icp.yaml | 1 + rust/stake_neuron_from_cli/test.sh | 36 ++-- 4 files changed, 54 insertions(+), 142 deletions(-) diff --git a/rust/stake_neuron_from_cli/backend/Cargo.toml b/rust/stake_neuron_from_cli/backend/Cargo.toml index aa976d4106..dc439f24d8 100644 --- a/rust/stake_neuron_from_cli/backend/Cargo.toml +++ b/rust/stake_neuron_from_cli/backend/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "backend" version = "0.1.0" -edition = "2021" +edition = "2024" [lib] crate-type = ["cdylib"] @@ -10,5 +10,6 @@ path = "lib.rs" [dependencies] candid = "0.10" ic-cdk = "0.20" +ic-ledger-types = "0.16" serde = { version = "1.0", features = ["derive"] } sha2 = "0.10" diff --git a/rust/stake_neuron_from_cli/backend/lib.rs b/rust/stake_neuron_from_cli/backend/lib.rs index f11ee48fe7..f7eb41384b 100644 --- a/rust/stake_neuron_from_cli/backend/lib.rs +++ b/rust/stake_neuron_from_cli/backend/lib.rs @@ -1,55 +1,13 @@ use candid::{CandidType, Deserialize, Principal}; use ic_cdk::call::Call; use ic_cdk::{query, update}; +use ic_ledger_types::{ + AccountIdentifier, Memo, Subaccount, Tokens, MAINNET_LEDGER_CANISTER_ID, +}; use sha2::{Digest, Sha256}; -// Canonical mainnet canister IDs for ICP Ledger and NNS Governance -const ICP_LEDGER_CANISTER_ID: &str = "ryjl3-tyaaa-aaaaa-aaaba-cai"; const NNS_GOVERNANCE_CANISTER_ID: &str = "rrkah-fqaaa-aaaaa-aaaaq-cai"; -// Standard ICP transfer fee: 0.0001 ICP = 10,000 e8s -const DEFAULT_FEE_E8S: u64 = 10_000; - -// ── Candid types for ICP Ledger ───────────────────────────────────────────── - -#[derive(CandidType, Deserialize, Clone, Debug)] -struct AccountIdentifier { - hash: Vec, -} - -#[derive(CandidType, Deserialize, Clone, Debug)] -struct Tokens { - e8s: u64, -} - -#[derive(CandidType, Deserialize, Clone, Debug)] -struct Memo(u64); - -#[derive(CandidType, Deserialize, Clone, Debug)] -struct TransferArgs { - memo: Memo, - amount: Tokens, - fee: Tokens, - from_subaccount: Option>, - to: AccountIdentifier, - created_at_time: Option, -} - -#[derive(CandidType, Deserialize, Debug)] -enum TransferError { - BadFee { expected_fee: Tokens }, - InsufficientFunds { balance: Tokens }, - TxTooOld { allowed_window_nanos: u64 }, - TxCreatedInFuture, - TxDuplicate { duplicate_of: u64 }, -} - -#[derive(CandidType, Deserialize, Debug)] -enum TransferResult { - Ok(u64), - Err(TransferError), -} - // ── Candid types for NNS Governance ───────────────────────────────────────── #[derive(CandidType, Deserialize, Debug)] @@ -93,7 +51,7 @@ pub struct StakeNeuronResult { // ── Core logic ─────────────────────────────────────────────────────────────── -/// Compute the subaccount for neuron staking. +/// Compute the staking subaccount for a (controller, nonce) pair. /// /// The neuron staking subaccount is the most critical thing to get right. /// If ICP is sent to the wrong subaccount it is permanently lost — the @@ -104,51 +62,14 @@ pub struct StakeNeuronResult { /// "neuron-stake" /// controller bytes /// nonce as big-endian u64 -fn compute_neuron_staking_subaccount(controller: Principal, nonce: u64) -> [u8; 32] { +fn compute_neuron_staking_subaccount(controller: Principal, nonce: u64) -> Subaccount { let domain = b"neuron-stake"; - let domain_length: [u8; 1] = [domain.len() as u8]; let mut hasher = Sha256::new(); - hasher.update(domain_length); + hasher.update([domain.len() as u8]); hasher.update(domain); hasher.update(controller.as_slice()); hasher.update(nonce.to_be_bytes()); - hasher.finalize().into() -} - -/// Build an ICP AccountIdentifier from a principal + subaccount. -/// -/// Format: CRC32(0x0a || "account-id" || principal || subaccount) -/// || 0x0a || "account-id" || principal || subaccount -/// The final identifier is the last 28 bytes of the 32-byte SHA-256 hash, -/// prepended with the 4-byte CRC32 checksum. -fn build_account_identifier(principal: Principal, subaccount: &[u8; 32]) -> Vec { - let mut hasher = Sha256::new(); - hasher.update(b"\x0aaccount-id"); - hasher.update(principal.as_slice()); - hasher.update(subaccount); - let hash: [u8; 32] = hasher.finalize().into(); - - // CRC32 over the 28-byte account body (bytes 4..32 of hash) - let crc = crc32(&hash[4..]); - let mut result = Vec::with_capacity(32); - result.extend_from_slice(&crc.to_be_bytes()); - result.extend_from_slice(&hash[4..]); - result -} - -fn crc32(data: &[u8]) -> u32 { - let mut crc: u32 = 0xFFFF_FFFF; - for &byte in data { - crc ^= u32::from(byte); - for _ in 0..8 { - if crc & 1 != 0 { - crc = (crc >> 1) ^ 0xEDB8_8320; - } else { - crc >>= 1; - } - } - } - !crc + Subaccount(hasher.finalize().into()) } fn hex_encode(bytes: &[u8]) -> String { @@ -158,12 +79,11 @@ fn hex_encode(bytes: &[u8]) -> String { // ── Canister endpoints ─────────────────────────────────────────────────────── /// Return the hex-encoded neuron staking subaccount for the given -/// (controller, nonce) pair. Call this before sending any ICP to verify +/// (controller, nonce) pair. Call this before sending any ICP to verify /// the destination subaccount matches your expectation. #[query] fn compute_subaccount(controller: Principal, nonce: u64) -> String { - let sub = compute_neuron_staking_subaccount(controller, nonce); - hex_encode(&sub) + hex_encode(&compute_neuron_staking_subaccount(controller, nonce).0) } /// Perform the two-step neuron staking process on behalf of this canister: @@ -173,56 +93,38 @@ fn compute_subaccount(controller: Principal, nonce: u64) -> String { /// 2. Call `claim_or_refresh_neuron_from_account` on the Governance canister /// to complete the neuron creation. /// -/// **Prerequisites (mainnet):** +/// **Prerequisites:** /// - This canister must hold at least `amount + 10_000` e8s of ICP /// (the extra 10_000 e8s cover the standard transfer fee). /// - The minimum staking amount enforced by NNS Governance is 100_000_000 e8s /// (1 ICP). -/// -/// **Local testing:** The NNS canisters are not deployed on a default local -/// network, so this method can only be exercised against mainnet or a local -/// NNS replica. See the README for details. #[update] async fn stake_neuron(amount: u64, nonce: u64) -> Result { - let controller = ic_cdk::api::msg_caller(); let canister_id = ic_cdk::api::canister_self(); - let subaccount = compute_neuron_staking_subaccount(controller, nonce); + // The canister holds the ICP and becomes the neuron controller, so the + // staking subaccount is derived from the canister's own principal. + let subaccount = compute_neuron_staking_subaccount(canister_id, nonce); let governance_principal = Principal::from_text(NNS_GOVERNANCE_CANISTER_ID).map_err(|e| e.to_string())?; - let to_account = AccountIdentifier { - hash: build_account_identifier(governance_principal, &subaccount), - }; - - // Step 1: Transfer ICP to the governance staking subaccount - let transfer_args = TransferArgs { + // Step 1: Transfer ICP to the Governance canister's staking subaccount. + let transfer_args = ic_ledger_types::TransferArgs { memo: Memo(nonce), - amount: Tokens { e8s: amount }, - fee: Tokens { - e8s: DEFAULT_FEE_E8S, - }, + amount: Tokens::from_e8s(amount), + fee: Tokens::from_e8s(10_000), from_subaccount: None, - to: to_account, + to: AccountIdentifier::new(&governance_principal, &subaccount), created_at_time: None, }; - let ledger = - Principal::from_text(ICP_LEDGER_CANISTER_ID).map_err(|e| e.to_string())?; - - let transfer_result: TransferResult = Call::bounded_wait(ledger, "transfer") - .with_arg(transfer_args) + let block_index = ic_ledger_types::transfer(MAINNET_LEDGER_CANISTER_ID, &transfer_args) .await - .map_err(|e| format!("Transfer call failed: {:?}", e))? - .candid() - .map_err(|e| format!("Transfer decode failed: {:?}", e))?; - - let block_index = match transfer_result { - TransferResult::Ok(idx) => idx, - TransferResult::Err(e) => return Err(format!("Transfer failed: {:?}", e)), - }; + .map_err(|e| format!("ledger call failed: {:?}", e))? + .map_err(|e| format!("ledger transfer error: {:?}", e))?; - // Step 2: Claim the neuron using the same nonce + // Step 2: Claim the neuron — Governance checks that it received ICP at + // hash(controller, memo) and mints the neuron. let claim_request = ClaimOrRefreshNeuronFromAccount { controller: Some(canister_id), memo: nonce, @@ -232,24 +134,24 @@ async fn stake_neuron(amount: u64, nonce: u64) -> Result n.id, Some(ClaimOrRefreshResult::Error(e)) => { - return Err(format!("Governance error ({}): {}", e.error_type, e.error_message)) + return Err(format!("governance error ({}): {}", e.error_type, e.error_message)) } - None => return Err("No result returned from governance canister".to_string()), + None => return Err("no result returned from governance canister".to_string()), }; Ok(StakeNeuronResult { neuron_id, - controller, + controller: canister_id, staked_amount_e8s: amount, block_index, - subaccount_hex: hex_encode(&subaccount), + subaccount_hex: hex_encode(&subaccount.0), }) } diff --git a/rust/stake_neuron_from_cli/icp.yaml b/rust/stake_neuron_from_cli/icp.yaml index df5c7edaaf..170e59790f 100644 --- a/rust/stake_neuron_from_cli/icp.yaml +++ b/rust/stake_neuron_from_cli/icp.yaml @@ -1,6 +1,7 @@ networks: - name: local mode: managed + nns: true # deploys NNS canisters at their mainnet IDs on the local network canisters: - name: backend diff --git a/rust/stake_neuron_from_cli/test.sh b/rust/stake_neuron_from_cli/test.sh index 340386bcaa..6661607365 100755 --- a/rust/stake_neuron_from_cli/test.sh +++ b/rust/stake_neuron_from_cli/test.sh @@ -1,24 +1,32 @@ #!/usr/bin/env bash set -e -echo "=== Test 1/2: compute_subaccount returns a 64-char hex string ===" +backend=$(icp canister status backend -i) +echo "Backend canister: $backend" + +echo "=== Test 1: compute_subaccount returns a 64-char hex string ===" result=$(icp canister call --query backend compute_subaccount \ - '(principal "aaaaa-aa", 0 : nat64)') && \ + "(principal \"$backend\", 0 : nat64)") && \ echo "$result" && \ echo "$result" | grep -qE '"[0-9a-f]{64}"' && \ echo "PASS" || (echo "FAIL" && exit 1) -echo "=== Test 2/2: compute_subaccount is deterministic (same inputs → same output) ===" +echo "=== Test 2: compute_subaccount is deterministic (same inputs → same output) ===" r1=$(icp canister call --query backend compute_subaccount \ - '(principal "aaaaa-aa", 42 : nat64)') && \ - r2=$(icp canister call --query backend compute_subaccount \ - '(principal "aaaaa-aa", 42 : nat64)') && \ - echo "Run 1: $r1" && \ - echo "Run 2: $r2" && \ - [ "$r1" = "$r2" ] && \ - echo "PASS" || (echo "FAIL" && exit 1) + "(principal \"$backend\", 42 : nat64)") +r2=$(icp canister call --query backend compute_subaccount \ + "(principal \"$backend\", 42 : nat64)") +echo "Run 1: $r1" +echo "Run 2: $r2" +[ "$r1" = "$r2" ] && echo "PASS" || (echo "FAIL" && exit 1) + +echo "=== Setup: fund backend with 2 ICP ===" +icp token transfer 2 "$backend" -echo "" -echo "NOTE: stake_neuron requires the canister to hold ICP and NNS Governance" -echo "to be reachable. Test against mainnet by calling:" -echo " icp canister call --network ic backend stake_neuron '( : nat64, : nat64)'" +echo "=== Test 3: stake_neuron — stakes 1 ICP and returns a neuron ID ===" +# Minimum stake enforced by NNS Governance is 100_000_000 e8s (1 ICP). +# The transfer also deducts 10_000 e8s in fees, so we need at least 100_010_000 e8s. +result=$(icp canister call backend stake_neuron '(100_000_000 : nat64, 0 : nat64)') && \ + echo "$result" && \ + echo "$result" | grep -q 'Ok' && \ + echo "PASS" || (echo "FAIL" && exit 1) From e7fd5152dd84cb1a4ca931600696ccdb2812a572 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 2 Jul 2026 17:05:52 +0200 Subject: [PATCH 04/12] fix(rust/stake_neuron_from_cli): update README and docstring for local NNS testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove stale "mainnet required" callout — nns: true enables full local testing - Update code snippet to match actual ic-ledger-types-based implementation - Add Node.js v18+ and Rust to prerequisites - Fix deploy/test description to reflect that test.sh exercises stake_neuron locally - Fix mainnet canister call flag: --network ic → -e ic - Update stake_neuron docstring: remove "NNS not available locally" note, add nns: true requirement Co-Authored-By: Claude Sonnet 4.6 --- rust/stake_neuron_from_cli/README.md | 44 +++++++---------------- rust/stake_neuron_from_cli/backend/lib.rs | 9 ++--- 2 files changed, 17 insertions(+), 36 deletions(-) diff --git a/rust/stake_neuron_from_cli/README.md b/rust/stake_neuron_from_cli/README.md index 1d98bc9062..c7d18fa01c 100644 --- a/rust/stake_neuron_from_cli/README.md +++ b/rust/stake_neuron_from_cli/README.md @@ -2,8 +2,6 @@ This example demonstrates how to stake ICP to create an NNS Governance neuron from a canister. It shows the subaccount computation and two-step staking process required to correctly create a neuron. -> **Mainnet required for full staking**: The `stake_neuron` endpoint makes inter-canister calls to the ICP Ledger (`ryjl3-tyaaa-aaaaa-aaaba-cai`) and NNS Governance (`rrkah-fqaaa-aaaaa-aaaaq-cai`) canisters, which only exist on mainnet. The `compute_subaccount` query can be tested locally. - ## What you can learn ### 1. Neuron Staking Subaccount Calculation @@ -18,15 +16,14 @@ before sending real ICP. The subaccount is a domain-separated SHA-256 hash over the controller principal and nonce: ```rust -fn compute_neuron_staking_subaccount(controller: Principal, nonce: u64) -> [u8; 32] { +fn compute_neuron_staking_subaccount(controller: Principal, nonce: u64) -> Subaccount { let domain = b"neuron-stake"; - let domain_length: [u8; 1] = [domain.len() as u8]; let mut hasher = Sha256::new(); - hasher.update(domain_length); + hasher.update([domain.len() as u8]); hasher.update(domain); hasher.update(controller.as_slice()); hasher.update(nonce.to_be_bytes()); - hasher.finalize().into() + Subaccount(hasher.finalize().into()) } ``` @@ -42,29 +39,13 @@ The nonce used in step 1 (as the transfer memo) must match the nonce used in ste The minimum staking amount enforced by NNS Governance is 1 ICP (100,000,000 e8s). The standard ICP transfer fee of 0.0001 ICP (10,000 e8s) is deducted on top of the staked amount. -```rust -// Step 1: Transfer ICP to the computed subaccount -let transfer_args = TransferArgs { - memo: Memo(nonce), - amount: Tokens { e8s: amount }, - fee: Tokens { e8s: 10_000 }, - to: AccountIdentifier { hash: build_account_identifier(governance_principal, &subaccount) }, - .. -}; - -// Step 2: Claim the neuron using the same nonce -let claim_request = ClaimOrRefreshNeuronFromAccount { - controller: Some(canister_id), - memo: nonce, -}; -``` - ## Build and deploy from the command line ### Prerequisites -- Node.js -- icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` +- [Node.js](https://nodejs.org/) v18+ +- [icp-cli](https://cli.internetcomputer.org/): `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` +- [Rust](https://www.rust-lang.org/tools/install) with `wasm32-unknown-unknown` target: `rustup target add wasm32-unknown-unknown` ### Install @@ -82,19 +63,18 @@ bash test.sh icp network stop ``` -The `bash test.sh` script verifies the `compute_subaccount` query locally. +`icp.yaml` configures the local network with `nns: true`, which deploys the NNS canisters (ICP Ledger and Governance) at their mainnet IDs. `test.sh` funds the canister with ICP and exercises the full staking flow — both `compute_subaccount` and `stake_neuron` are tested locally. -### Testing stake_neuron on mainnet +### Testing on mainnet -To test the full staking flow you need a canister deployed to mainnet that holds ICP: +To test against mainnet, deploy and fund the canister with ICP, then call `stake_neuron`: ```bash -# Deploy to mainnet -icp deploy --network ic +icp deploy -e ic -# Fund the canister with ICP (from your wallet), then call stake_neuron. +# Fund the canister with ICP from your wallet, then call stake_neuron. # amount: e8s to stake (minimum 100_000_000 = 1 ICP), nonce: unique u64 per neuron -icp canister call --network ic backend stake_neuron '(100010000 : nat64, 0 : nat64)' +icp canister call -e ic backend stake_neuron '(100_000_000 : nat64, 0 : nat64)' ``` ## Security considerations and best practices diff --git a/rust/stake_neuron_from_cli/backend/lib.rs b/rust/stake_neuron_from_cli/backend/lib.rs index f7eb41384b..636c1339b3 100644 --- a/rust/stake_neuron_from_cli/backend/lib.rs +++ b/rust/stake_neuron_from_cli/backend/lib.rs @@ -94,10 +94,11 @@ fn compute_subaccount(controller: Principal, nonce: u64) -> String { /// to complete the neuron creation. /// /// **Prerequisites:** -/// - This canister must hold at least `amount + 10_000` e8s of ICP -/// (the extra 10_000 e8s cover the standard transfer fee). -/// - The minimum staking amount enforced by NNS Governance is 100_000_000 e8s -/// (1 ICP). +/// - The canister must hold at least `amount + 10_000` e8s of ICP +/// (10_000 e8s covers the standard transfer fee). +/// - The minimum staking amount enforced by NNS Governance is 100_000_000 e8s (1 ICP). +/// - The local network must be started with `nns: true` in `icp.yaml` so that the +/// ICP Ledger and NNS Governance canisters are available at their mainnet IDs. #[update] async fn stake_neuron(amount: u64, nonce: u64) -> Result { let canister_id = ic_cdk::api::canister_self(); From f958688a8a510d2a8c8187410ad7ed9315478cbd Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 2 Jul 2026 17:15:02 +0200 Subject: [PATCH 05/12] fix(rust/stake_neuron_from_cli): add icp token transfer command for mainnet funding Co-Authored-By: Claude Sonnet 4.6 --- rust/stake_neuron_from_cli/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rust/stake_neuron_from_cli/README.md b/rust/stake_neuron_from_cli/README.md index c7d18fa01c..7529c5f2c9 100644 --- a/rust/stake_neuron_from_cli/README.md +++ b/rust/stake_neuron_from_cli/README.md @@ -72,8 +72,11 @@ To test against mainnet, deploy and fund the canister with ICP, then call `stake ```bash icp deploy -e ic -# Fund the canister with ICP from your wallet, then call stake_neuron. -# amount: e8s to stake (minimum 100_000_000 = 1 ICP), nonce: unique u64 per neuron +# Fund the canister with ICP (minimum 1 ICP to stake + 0.0001 ICP transfer fee) +backend=$(icp canister status -e ic backend -i) +icp token transfer -e ic 1.0001 "$backend" + +# Stake a neuron — amount: e8s to stake, nonce: unique u64 per neuron icp canister call -e ic backend stake_neuron '(100_000_000 : nat64, 0 : nat64)' ``` From 3061b70f1e02bd8cb11abb834612796487fe2cac Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 2 Jul 2026 17:20:51 +0200 Subject: [PATCH 06/12] fix(rust/stake_neuron_from_cli): add minimum amount guard and authorization note - Reject amounts below 100_000_000 e8s early so no ICP is transferred before Governance rejects the claim and strands funds in the staking subaccount - Add code comment noting stake_neuron is unguarded; production deployments should restrict to an authorized principal Co-Authored-By: Claude Sonnet 4.6 --- rust/stake_neuron_from_cli/backend/lib.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rust/stake_neuron_from_cli/backend/lib.rs b/rust/stake_neuron_from_cli/backend/lib.rs index 636c1339b3..4de67709d2 100644 --- a/rust/stake_neuron_from_cli/backend/lib.rs +++ b/rust/stake_neuron_from_cli/backend/lib.rs @@ -99,8 +99,17 @@ fn compute_subaccount(controller: Principal, nonce: u64) -> String { /// - The minimum staking amount enforced by NNS Governance is 100_000_000 e8s (1 ICP). /// - The local network must be started with `nns: true` in `icp.yaml` so that the /// ICP Ledger and NNS Governance canisters are available at their mainnet IDs. +// NOTE: stake_neuron is unguarded — any caller can trigger a transfer from the +// canister's ICP balance. Production deployments should restrict this to an +// authorized principal (e.g. the canister controller). #[update] async fn stake_neuron(amount: u64, nonce: u64) -> Result { + // Reject below-minimum amounts early so no ICP is transferred before Governance + // would reject the claim and strand funds in the staking subaccount. + if amount < 100_000_000 { + return Err("amount must be at least 100_000_000 e8s (1 ICP)".to_string()); + } + let canister_id = ic_cdk::api::canister_self(); // The canister holds the ICP and becomes the neuron controller, so the From 5b161c8b67d7d7ce0bb6032748b92de691f32d85 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 2 Jul 2026 17:23:26 +0200 Subject: [PATCH 07/12] fix(rust/stake_neuron_from_cli): strengthen test.sh with known-value, guard, and cross-validation tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Test 1: known-value assertion for compute_subaccount against a fixed reference principal (aaaaa-aa, nonce 0) — catches any regression in the SHA-256 domain separation without depending on a deployment-specific canister ID - Test 2: verify different nonces produce different subaccounts (replaces trivial determinism check) - Test 3: verify stake_neuron rejects amounts below 100_000_000 e8s with a clear error - Test 4: cross-validate that subaccount_hex in the stake_neuron result matches compute_subaccount(backend, nonce) — the key safety invariant Co-Authored-By: Claude Sonnet 4.6 --- rust/stake_neuron_from_cli/test.sh | 49 ++++++++++++++++++------------ 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/rust/stake_neuron_from_cli/test.sh b/rust/stake_neuron_from_cli/test.sh index 6661607365..527ab4f00a 100755 --- a/rust/stake_neuron_from_cli/test.sh +++ b/rust/stake_neuron_from_cli/test.sh @@ -4,29 +4,38 @@ set -e backend=$(icp canister status backend -i) echo "Backend canister: $backend" -echo "=== Test 1: compute_subaccount returns a 64-char hex string ===" +echo "=== Test 1: compute_subaccount matches known reference value ===" +# Fixed reference: principal "aaaaa-aa" (management canister), nonce 0. +# The expected value is a pre-computed SHA-256 domain-separated hash. +# If this changes, the subaccount computation is broken and real ICP could be sent +# to the wrong address. result=$(icp canister call --query backend compute_subaccount \ - "(principal \"$backend\", 0 : nat64)") && \ - echo "$result" && \ - echo "$result" | grep -qE '"[0-9a-f]{64}"' && \ - echo "PASS" || (echo "FAIL" && exit 1) + '(principal "aaaaa-aa", 0 : nat64)') +echo "$result" +echo "$result" | grep -q '"b8c5a0fbf187460e550de4c606ab9ba102f7826c43ee644b80f275eb952c0aa8"' && \ + echo "PASS" || (echo "FAIL: subaccount hash does not match expected reference value" && exit 1) -echo "=== Test 2: compute_subaccount is deterministic (same inputs → same output) ===" -r1=$(icp canister call --query backend compute_subaccount \ - "(principal \"$backend\", 42 : nat64)") -r2=$(icp canister call --query backend compute_subaccount \ - "(principal \"$backend\", 42 : nat64)") -echo "Run 1: $r1" -echo "Run 2: $r2" -[ "$r1" = "$r2" ] && echo "PASS" || (echo "FAIL" && exit 1) +echo "=== Test 2: compute_subaccount changes with nonce ===" +s0=$(icp canister call --query backend compute_subaccount "(principal \"$backend\", 0 : nat64)") +s1=$(icp canister call --query backend compute_subaccount "(principal \"$backend\", 1 : nat64)") +echo "Nonce 0: $s0" +echo "Nonce 1: $s1" +[ "$s0" != "$s1" ] && echo "PASS" || (echo "FAIL: different nonces produced the same subaccount" && exit 1) + +echo "=== Test 3: stake_neuron rejects amount below 1 ICP minimum ===" +result=$(icp canister call backend stake_neuron '(99_999_999 : nat64, 0 : nat64)') +echo "$result" +echo "$result" | grep -q 'Err' && echo "PASS" || (echo "FAIL: expected rejection for sub-minimum amount" && exit 1) echo "=== Setup: fund backend with 2 ICP ===" icp token transfer 2 "$backend" -echo "=== Test 3: stake_neuron — stakes 1 ICP and returns a neuron ID ===" -# Minimum stake enforced by NNS Governance is 100_000_000 e8s (1 ICP). -# The transfer also deducts 10_000 e8s in fees, so we need at least 100_010_000 e8s. -result=$(icp canister call backend stake_neuron '(100_000_000 : nat64, 0 : nat64)') && \ - echo "$result" && \ - echo "$result" | grep -q 'Ok' && \ - echo "PASS" || (echo "FAIL" && exit 1) +echo "=== Test 4: stake_neuron stakes 1 ICP and subaccount_hex matches compute_subaccount ===" +expected_subaccount=$(icp canister call --query backend compute_subaccount \ + "(principal \"$backend\", 0 : nat64)" | grep -oE '[0-9a-f]{64}') +result=$(icp canister call backend stake_neuron '(100_000_000 : nat64, 0 : nat64)') +echo "$result" +echo "$result" | grep -q 'Ok' || (echo "FAIL: stake_neuron did not return Ok" && exit 1) +echo "$result" | grep -q "neuron_id" || (echo "FAIL: missing neuron_id in result" && exit 1) +echo "$result" | grep -q "$expected_subaccount" && \ + echo "PASS" || (echo "FAIL: subaccount_hex in result does not match compute_subaccount" && exit 1) From 1f4349ce68587295a993db861e13c3cd2f6b889a Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 2 Jul 2026 17:26:12 +0200 Subject: [PATCH 08/12] docs(rust/stake_neuron_from_cli): explain claim_or_refresh re-run behavior in test.sh Co-Authored-By: Claude Sonnet 4.6 --- rust/stake_neuron_from_cli/test.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rust/stake_neuron_from_cli/test.sh b/rust/stake_neuron_from_cli/test.sh index 527ab4f00a..b2c81ed100 100755 --- a/rust/stake_neuron_from_cli/test.sh +++ b/rust/stake_neuron_from_cli/test.sh @@ -31,6 +31,9 @@ echo "=== Setup: fund backend with 2 ICP ===" icp token transfer 2 "$backend" echo "=== Test 4: stake_neuron stakes 1 ICP and subaccount_hex matches compute_subaccount ===" +# Re-running this test tops up the existing neuron rather than creating a new one — +# claim_or_refresh reuses the same neuron_id for the same (controller, nonce) pair. +# Each re-run adds 1 ICP to the neuron's stake; the neuron_id stays the same. expected_subaccount=$(icp canister call --query backend compute_subaccount \ "(principal \"$backend\", 0 : nat64)" | grep -oE '[0-9a-f]{64}') result=$(icp canister call backend stake_neuron '(100_000_000 : nat64, 0 : nat64)') From 2e95c390c5b9c08f67a7dace193da981eeb9356e Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 2 Jul 2026 17:32:14 +0200 Subject: [PATCH 09/12] docs(rust/stake_neuron_from_cli): clarify staked_amount_e8s is per-call, not total neuron balance Co-Authored-By: Claude Sonnet 4.6 --- rust/stake_neuron_from_cli/backend/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rust/stake_neuron_from_cli/backend/lib.rs b/rust/stake_neuron_from_cli/backend/lib.rs index 4de67709d2..a790dff9cb 100644 --- a/rust/stake_neuron_from_cli/backend/lib.rs +++ b/rust/stake_neuron_from_cli/backend/lib.rs @@ -44,6 +44,9 @@ struct ClaimOrRefreshNeuronFromAccountResponse { pub struct StakeNeuronResult { pub neuron_id: u64, pub controller: Principal, + /// Amount staked in this call — not the neuron's total accumulated stake. + /// Re-running stake_neuron with the same nonce tops up the existing neuron; + /// query NNS Governance to retrieve the actual total balance. pub staked_amount_e8s: u64, pub block_index: u64, pub subaccount_hex: String, From cc1289718985dcf5c0adc93ed29f413ddc4bce15 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 2 Jul 2026 18:15:11 +0200 Subject: [PATCH 10/12] style(rust/stake_neuron_from_cli): pass &claim_request by reference to with_arg Co-Authored-By: Claude Sonnet 4.6 --- rust/stake_neuron_from_cli/backend/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/stake_neuron_from_cli/backend/lib.rs b/rust/stake_neuron_from_cli/backend/lib.rs index a790dff9cb..3d000e9a6f 100644 --- a/rust/stake_neuron_from_cli/backend/lib.rs +++ b/rust/stake_neuron_from_cli/backend/lib.rs @@ -145,7 +145,7 @@ async fn stake_neuron(amount: u64, nonce: u64) -> Result Date: Fri, 3 Jul 2026 12:01:12 +0200 Subject: [PATCH 11/12] fix(rust/stake_neuron_from_cli): revert to CLI binary, update deps, apply review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original example was a CLI binary using ic-agent — converting it to an icp-cli canister changed its fundamental purpose. This reverts to the original CLI approach while updating everything to current standards: - Revert from canister to CLI binary (src/main.rs + src/lib.rs, no backend/) - icp.yaml: network-only config with nns: true; no canister recipe - Upgrade ic-agent 0.35 → 0.47 (add pem + ring features for secp256k1 support — icp-cli generates secp256k1 keys by default) - Upgrade ic-ledger-types 0.15 → 0.16; drop hex and crc32fast (covered by ic-ledger-types) - edition = "2024" - Add --compute-only flag: prints the staking subaccount without any IC call or transfer — lets users verify the destination before committing funds - Rename amount → amount_e8s throughout; .unwrap() for constant principal - Clarify memo: the ledger transfer memo is cosmetic and does not need to match the governance nonce; what matters is that the nonce used to derive the staking subaccount matches the memo in claim_or_refresh_neuron_from_account - Document that anyone can complete step 2 (neuron controller is determined by the subaccount derived in step 1) - README: remove "domain-separated" jargon, list the 4 SHA-256 inputs plainly; add nns.internetcomputer.org link; note ICRC-2 as a safer alternative with link to dfinity/icp-cli#637 Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/stake_neuron_from_cli.yml | 3 +- rust/stake_neuron_from_cli/Cargo.toml | 22 ++- rust/stake_neuron_from_cli/README.md | 62 +++---- rust/stake_neuron_from_cli/backend/Cargo.toml | 15 -- rust/stake_neuron_from_cli/backend/lib.rs | 172 ------------------ rust/stake_neuron_from_cli/icp.yaml | 5 - rust/stake_neuron_from_cli/src/lib.rs | 164 +++++++++++++++++ rust/stake_neuron_from_cli/src/main.rs | 58 ++++++ rust/stake_neuron_from_cli/test.sh | 76 ++++---- 9 files changed, 310 insertions(+), 267 deletions(-) delete mode 100644 rust/stake_neuron_from_cli/backend/Cargo.toml delete mode 100644 rust/stake_neuron_from_cli/backend/lib.rs create mode 100644 rust/stake_neuron_from_cli/src/lib.rs create mode 100644 rust/stake_neuron_from_cli/src/main.rs diff --git a/.github/workflows/stake_neuron_from_cli.yml b/.github/workflows/stake_neuron_from_cli.yml index 046ca23fba..241013a14e 100644 --- a/.github/workflows/stake_neuron_from_cli.yml +++ b/.github/workflows/stake_neuron_from_cli.yml @@ -20,9 +20,8 @@ jobs: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - name: Deploy and test + - name: Build and test working-directory: rust/stake_neuron_from_cli run: | icp network start -d - icp deploy bash test.sh diff --git a/rust/stake_neuron_from_cli/Cargo.toml b/rust/stake_neuron_from_cli/Cargo.toml index d1e49e317a..79bd30d0e9 100644 --- a/rust/stake_neuron_from_cli/Cargo.toml +++ b/rust/stake_neuron_from_cli/Cargo.toml @@ -1,3 +1,19 @@ -[workspace] -members = ["backend"] -resolver = "2" +[package] +name = "stake_neuron_from_cli" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "stake_neuron_from_cli" +path = "src/main.rs" + +[dependencies] +candid = "0.10" +# pem: enables from_pem on identity types. +# ring: enables Secp256k1Identity (needed for keys generated by icp-cli). +ic-agent = { version = "0.47", features = ["pem", "ring"] } +ic-ledger-types = "0.16" +clap = { version = "4", features = ["derive"] } +sha2 = "0.10" +serde = { version = "1.0", features = ["derive"] } +tokio = { version = "1", features = ["full"] } diff --git a/rust/stake_neuron_from_cli/README.md b/rust/stake_neuron_from_cli/README.md index 7529c5f2c9..03d0f1bc77 100644 --- a/rust/stake_neuron_from_cli/README.md +++ b/rust/stake_neuron_from_cli/README.md @@ -1,22 +1,25 @@ -# Neuron Staking +# Neuron Staking from CLI -This example demonstrates how to stake ICP to create an NNS Governance neuron from a canister. It shows the subaccount computation and two-step staking process required to correctly create a neuron. +This example demonstrates how to stake ICP to create an NNS Governance neuron from a Rust CLI using `ic-agent`. It shows the two-step process that the ledger and governance canisters expect. + +Before diving in, you may want to try [nns.internetcomputer.org](https://nns.internetcomputer.org/) first — it offers an interactive introduction to staking neurons and voting without writing any code. ## What you can learn ### 1. Neuron Staking Subaccount Calculation -The neuron staking subaccount is the most critical thing to get right when staking a neuron. -If you send your ICP to the wrong subaccount, the ICP will be permanently lost, as there will -not be a way to ask the Governance canister to retrieve it. +The neuron staking subaccount is the most critical thing to get right. +If ICP is sent to the wrong subaccount it is permanently lost — the Governance canister cannot retrieve it. -Therefore, test your implementation (like in this example project) against a known reference -before sending real ICP. +The subaccount is a SHA-256 hash over these four inputs in order: -The subaccount is a domain-separated SHA-256 hash over the controller principal and nonce: +1. `12` — the length of the domain separator (one byte) +2. `"neuron-stake"` — the domain separator +3. controller principal as raw bytes (not the text form) +4. nonce as big-endian u64 ```rust -fn compute_neuron_staking_subaccount(controller: Principal, nonce: u64) -> Subaccount { +pub fn compute_neuron_staking_subaccount(controller: Principal, nonce: u64) -> Subaccount { let domain = b"neuron-stake"; let mut hasher = Sha256::new(); hasher.update([domain.len() as u8]); @@ -29,23 +32,24 @@ fn compute_neuron_staking_subaccount(controller: Principal, nonce: u64) -> Subac ### 2. Two-Step Neuron Creation Process -The neuron creation flow requires first making a ledger transfer, and then sending a message to tell -NNS Governance about the transfer you made so that it can create a neuron. +1. **Transfer ICP** to the Governance canister at the computed staking subaccount. +2. **Claim the neuron** by calling `claim_or_refresh_neuron_from_account` on Governance. + +The `memo` in the ledger transfer is just a label on the transaction and does not need to match anything in step 2. What matters is that the `nonce` (used to derive the staking subaccount in step 1) matches the `memo` field passed to `claim_or_refresh_neuron_from_account` in step 2 — that is how Governance recomputes the expected subaccount and verifies the funds. + +Note: **anyone can complete step 2**. The neuron's controller is determined entirely by the subaccount derived in step 1, regardless of who calls `claim_or_refresh_neuron_from_account`. -1. **Transfer ICP** — Transfer tokens to NNS Governance at the computed subaccount -2. **Claim Neuron** — Call `claim_or_refresh_neuron_from_account` to register the transfer as a neuron +The minimum staking amount enforced by NNS Governance is 100,000,000 e8s (1 ICP). The ICP transfer fee of 10,000 e8s is charged on top. -The nonce used in step 1 (as the transfer memo) must match the nonce used in step 2. -The minimum staking amount enforced by NNS Governance is 1 ICP (100,000,000 e8s). -The standard ICP transfer fee of 0.0001 ICP (10,000 e8s) is deducted on top of the staked amount. +> **A simpler alternative**: NNS Governance now supports a `create_neuron` method that accepts an ICRC-2 approval, handling the transfer atomically. This is safer because the ICP stays in the caller's account until Governance successfully claims it — there is no window where funds can be stranded at the staking subaccount. icp-cli support for this flow is being tracked at [dfinity/icp-cli#637](https://github.com/dfinity/icp-cli/pull/637). -## Build and deploy from the command line +## Build and run from the command line ### Prerequisites - [Node.js](https://nodejs.org/) v18+ - [icp-cli](https://cli.internetcomputer.org/): `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` -- [Rust](https://www.rust-lang.org/tools/install) with `wasm32-unknown-unknown` target: `rustup target add wasm32-unknown-unknown` +- [Rust](https://www.rust-lang.org/tools/install) v1.85+ with `wasm32-unknown-unknown` target: `rustup target add wasm32-unknown-unknown` ### Install @@ -54,30 +58,26 @@ git clone https://github.com/dfinity/examples cd examples/rust/stake_neuron_from_cli ``` -### Deploy and test +### Run locally ```bash icp network start -d -icp deploy bash test.sh icp network stop ``` -`icp.yaml` configures the local network with `nns: true`, which deploys the NNS canisters (ICP Ledger and Governance) at their mainnet IDs. `test.sh` funds the canister with ICP and exercises the full staking flow — both `compute_subaccount` and `stake_neuron` are tested locally. +`test.sh` creates a test identity, funds it with ICP from the local NNS, builds the CLI binary, and runs the full staking flow. -### Testing on mainnet - -To test against mainnet, deploy and fund the canister with ICP, then call `stake_neuron`: +### Run on mainnet ```bash -icp deploy -e ic - -# Fund the canister with ICP (minimum 1 ICP to stake + 0.0001 ICP transfer fee) -backend=$(icp canister status -e ic backend -i) -icp token transfer -e ic 1.0001 "$backend" +cargo build --release -# Stake a neuron — amount: e8s to stake, nonce: unique u64 per neuron -icp canister call -e ic backend stake_neuron '(100_000_000 : nat64, 0 : nat64)' +./target/release/stake_neuron_from_cli \ + --identity /path/to/identity.pem \ + --url https://icp-api.io \ + --amount-e8s 100000000 \ + --nonce 0 ``` ## Security considerations and best practices diff --git a/rust/stake_neuron_from_cli/backend/Cargo.toml b/rust/stake_neuron_from_cli/backend/Cargo.toml deleted file mode 100644 index dc439f24d8..0000000000 --- a/rust/stake_neuron_from_cli/backend/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "backend" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] -path = "lib.rs" - -[dependencies] -candid = "0.10" -ic-cdk = "0.20" -ic-ledger-types = "0.16" -serde = { version = "1.0", features = ["derive"] } -sha2 = "0.10" diff --git a/rust/stake_neuron_from_cli/backend/lib.rs b/rust/stake_neuron_from_cli/backend/lib.rs deleted file mode 100644 index 3d000e9a6f..0000000000 --- a/rust/stake_neuron_from_cli/backend/lib.rs +++ /dev/null @@ -1,172 +0,0 @@ -use candid::{CandidType, Deserialize, Principal}; -use ic_cdk::call::Call; -use ic_cdk::{query, update}; -use ic_ledger_types::{ - AccountIdentifier, Memo, Subaccount, Tokens, MAINNET_LEDGER_CANISTER_ID, -}; -use sha2::{Digest, Sha256}; - -const NNS_GOVERNANCE_CANISTER_ID: &str = "rrkah-fqaaa-aaaaa-aaaaq-cai"; - -// ── Candid types for NNS Governance ───────────────────────────────────────── - -#[derive(CandidType, Deserialize, Debug)] -struct ClaimOrRefreshNeuronFromAccount { - controller: Option, - memo: u64, -} - -#[derive(CandidType, Deserialize, Debug)] -struct NeuronId { - id: u64, -} - -#[derive(CandidType, Deserialize, Debug)] -struct GovernanceError { - error_type: i32, - error_message: String, -} - -#[derive(CandidType, Deserialize, Debug)] -enum ClaimOrRefreshResult { - Error(GovernanceError), - NeuronId(NeuronId), -} - -#[derive(CandidType, Deserialize, Debug)] -struct ClaimOrRefreshNeuronFromAccountResponse { - result: Option, -} - -// ── Public return type ─────────────────────────────────────────────────────── - -#[derive(CandidType, Deserialize, Debug)] -pub struct StakeNeuronResult { - pub neuron_id: u64, - pub controller: Principal, - /// Amount staked in this call — not the neuron's total accumulated stake. - /// Re-running stake_neuron with the same nonce tops up the existing neuron; - /// query NNS Governance to retrieve the actual total balance. - pub staked_amount_e8s: u64, - pub block_index: u64, - pub subaccount_hex: String, -} - -// ── Core logic ─────────────────────────────────────────────────────────────── - -/// Compute the staking subaccount for a (controller, nonce) pair. -/// -/// The neuron staking subaccount is the most critical thing to get right. -/// If ICP is sent to the wrong subaccount it is permanently lost — the -/// Governance canister cannot retrieve it. -/// -/// Domain-separated SHA-256 hash over: -/// \x0c (length of "neuron-stake" = 12) -/// "neuron-stake" -/// controller bytes -/// nonce as big-endian u64 -fn compute_neuron_staking_subaccount(controller: Principal, nonce: u64) -> Subaccount { - let domain = b"neuron-stake"; - let mut hasher = Sha256::new(); - hasher.update([domain.len() as u8]); - hasher.update(domain); - hasher.update(controller.as_slice()); - hasher.update(nonce.to_be_bytes()); - Subaccount(hasher.finalize().into()) -} - -fn hex_encode(bytes: &[u8]) -> String { - bytes.iter().map(|b| format!("{:02x}", b)).collect() -} - -// ── Canister endpoints ─────────────────────────────────────────────────────── - -/// Return the hex-encoded neuron staking subaccount for the given -/// (controller, nonce) pair. Call this before sending any ICP to verify -/// the destination subaccount matches your expectation. -#[query] -fn compute_subaccount(controller: Principal, nonce: u64) -> String { - hex_encode(&compute_neuron_staking_subaccount(controller, nonce).0) -} - -/// Perform the two-step neuron staking process on behalf of this canister: -/// -/// 1. Transfer `amount` e8s of ICP from this canister's default account to -/// the NNS Governance canister at the computed staking subaccount. -/// 2. Call `claim_or_refresh_neuron_from_account` on the Governance canister -/// to complete the neuron creation. -/// -/// **Prerequisites:** -/// - The canister must hold at least `amount + 10_000` e8s of ICP -/// (10_000 e8s covers the standard transfer fee). -/// - The minimum staking amount enforced by NNS Governance is 100_000_000 e8s (1 ICP). -/// - The local network must be started with `nns: true` in `icp.yaml` so that the -/// ICP Ledger and NNS Governance canisters are available at their mainnet IDs. -// NOTE: stake_neuron is unguarded — any caller can trigger a transfer from the -// canister's ICP balance. Production deployments should restrict this to an -// authorized principal (e.g. the canister controller). -#[update] -async fn stake_neuron(amount: u64, nonce: u64) -> Result { - // Reject below-minimum amounts early so no ICP is transferred before Governance - // would reject the claim and strand funds in the staking subaccount. - if amount < 100_000_000 { - return Err("amount must be at least 100_000_000 e8s (1 ICP)".to_string()); - } - - let canister_id = ic_cdk::api::canister_self(); - - // The canister holds the ICP and becomes the neuron controller, so the - // staking subaccount is derived from the canister's own principal. - let subaccount = compute_neuron_staking_subaccount(canister_id, nonce); - let governance_principal = - Principal::from_text(NNS_GOVERNANCE_CANISTER_ID).map_err(|e| e.to_string())?; - - // Step 1: Transfer ICP to the Governance canister's staking subaccount. - let transfer_args = ic_ledger_types::TransferArgs { - memo: Memo(nonce), - amount: Tokens::from_e8s(amount), - fee: Tokens::from_e8s(10_000), - from_subaccount: None, - to: AccountIdentifier::new(&governance_principal, &subaccount), - created_at_time: None, - }; - - let block_index = ic_ledger_types::transfer(MAINNET_LEDGER_CANISTER_ID, &transfer_args) - .await - .map_err(|e| format!("ledger call failed: {:?}", e))? - .map_err(|e| format!("ledger transfer error: {:?}", e))?; - - // Step 2: Claim the neuron — Governance checks that it received ICP at - // hash(controller, memo) and mints the neuron. - let claim_request = ClaimOrRefreshNeuronFromAccount { - controller: Some(canister_id), - memo: nonce, - }; - - let claim_response: ClaimOrRefreshNeuronFromAccountResponse = - Call::bounded_wait(governance_principal, "claim_or_refresh_neuron_from_account") - .with_arg(&claim_request) - .await - .map_err(|e| format!("governance call failed: {:?}", e))? - .candid() - .map_err(|e| format!("governance decode failed: {:?}", e))?; - - let neuron_id = match claim_response.result { - Some(ClaimOrRefreshResult::NeuronId(n)) => n.id, - Some(ClaimOrRefreshResult::Error(e)) => { - return Err(format!("governance error ({}): {}", e.error_type, e.error_message)) - } - None => return Err("no result returned from governance canister".to_string()), - }; - - Ok(StakeNeuronResult { - neuron_id, - controller: canister_id, - staked_amount_e8s: amount, - block_index, - subaccount_hex: hex_encode(&subaccount.0), - }) -} - -// Export Candid interface -ic_cdk::export_candid!(); diff --git a/rust/stake_neuron_from_cli/icp.yaml b/rust/stake_neuron_from_cli/icp.yaml index 170e59790f..e2d4a87406 100644 --- a/rust/stake_neuron_from_cli/icp.yaml +++ b/rust/stake_neuron_from_cli/icp.yaml @@ -2,8 +2,3 @@ networks: - name: local mode: managed nns: true # deploys NNS canisters at their mainnet IDs on the local network - -canisters: - - name: backend - recipe: - type: "@dfinity/rust@v3.3.0" diff --git a/rust/stake_neuron_from_cli/src/lib.rs b/rust/stake_neuron_from_cli/src/lib.rs new file mode 100644 index 0000000000..cf7e02fe5d --- /dev/null +++ b/rust/stake_neuron_from_cli/src/lib.rs @@ -0,0 +1,164 @@ +use candid::{CandidType, Decode, Deserialize, Encode, Principal}; +use ic_agent::{ + identity::{BasicIdentity, Secp256k1Identity}, + Agent, Identity, +}; +use ic_ledger_types::{ + AccountIdentifier, Memo, Subaccount, Tokens, TransferArgs, TransferResult, + MAINNET_LEDGER_CANISTER_ID, +}; +use sha2::{Digest, Sha256}; +use std::path::PathBuf; + +const NNS_GOVERNANCE_CANISTER_ID: &str = "rrkah-fqaaa-aaaaa-aaaaq-cai"; + +pub struct StakeNeuronArgs { + pub identity_path: PathBuf, + pub ic_url: String, + pub amount_e8s: u64, + pub nonce: u64, +} + +pub async fn stake_neuron(args: StakeNeuronArgs) -> Result<(), Box> { + if args.amount_e8s < 100_000_000 { + return Err("amount_e8s must be at least 100_000_000 (1 ICP)".into()); + } + + let pem = std::fs::read(&args.identity_path)?; + // Try Ed25519 first, then secp256k1 (default key type created by icp-cli). + let identity: Box = if let Ok(id) = BasicIdentity::from_pem(&pem) { + Box::new(id) + } else { + Box::new(Secp256k1Identity::from_pem(&pem)?) + }; + let controller = identity.sender()?; + + let agent = Agent::builder() + .with_url(&args.ic_url) + .with_identity(identity) + .build()?; + // fetch_root_key is only needed for local networks; on mainnet the root key is + // compiled into ic-agent and this call is a no-op. + agent.fetch_root_key().await?; + + println!("Controller : {controller}"); + println!("Amount : {} e8s ({} ICP)", args.amount_e8s, args.amount_e8s as f64 / 1e8); + println!("Nonce : {}", args.nonce); + + // ── Step 1: transfer ICP to governance's staking subaccount ───────────── + + let new_neuron_subaccount = compute_neuron_staking_subaccount(controller, args.nonce); + println!("Subaccount : {}", hex_encode(&new_neuron_subaccount.0)); + + let governance = Principal::from_text(NNS_GOVERNANCE_CANISTER_ID).unwrap(); + let transfer_args = TransferArgs { + // The memo here is only a label on the ledger transaction; it does not need + // to match the nonce passed to claim_or_refresh_neuron_from_account below. + memo: Memo(args.nonce), + amount: Tokens::from_e8s(args.amount_e8s), + fee: Tokens::from_e8s(10_000), + from_subaccount: None, + to: AccountIdentifier::new(&governance, &new_neuron_subaccount), + created_at_time: None, + }; + + let transfer_bytes = agent + .update(&MAINNET_LEDGER_CANISTER_ID, "transfer") + .with_arg(Encode!(&transfer_args)?) + .call_and_wait() + .await?; + let block_index = match Decode!(&transfer_bytes, TransferResult)? { + TransferResult::Ok(idx) => { + println!("Transfer OK (block {idx})"); + idx + } + TransferResult::Err(e) => return Err(format!("Transfer failed: {e:?}").into()), + }; + + // ── Step 2: claim the neuron ───────────────────────────────────────────── + // Anyone can call claim_or_refresh_neuron_from_account to complete this step + // because the neuron's controller is already determined by the subaccount + // derived in step 1 — regardless of who the caller is here. + + let claim_request = ClaimOrRefreshNeuronFromAccount { + // The controller that will own the neuron; Governance recomputes + // the expected subaccount as hash(controller, memo) and checks funds. + controller: Some(controller), + // This memo IS the nonce used when deriving the staking subaccount above + // and must match it exactly. + memo: args.nonce, + }; + + let claim_bytes = agent + .update(&governance, "claim_or_refresh_neuron_from_account") + .with_arg(Encode!(&claim_request)?) + .call_and_wait() + .await?; + let response = Decode!(&claim_bytes, ClaimOrRefreshNeuronFromAccountResponse)?; + + let neuron_id = match response.result { + Some(ClaimOrRefreshResult::NeuronId(n)) => n.id, + Some(ClaimOrRefreshResult::Error(e)) => { + return Err(format!("Governance error ({}): {}", e.error_type, e.error_message).into()) + } + None => return Err("No result from governance canister".into()), + }; + + println!("Neuron ID : {neuron_id}"); + println!("Block index: {block_index}"); + Ok(()) +} + +/// Compute the NNS neuron staking subaccount for a (controller, nonce) pair. +/// +/// The neuron staking subaccount is the most critical thing to get right. +/// If ICP is sent to the wrong subaccount it is permanently lost. +/// +/// SHA-256 over these inputs in order: +/// 1. 12 (the length of the domain separator "neuron-stake") +/// 2. "neuron-stake" +/// 3. controller principal as raw bytes (not the text form) +/// 4. nonce as big-endian u64 +pub fn compute_neuron_staking_subaccount(controller: Principal, nonce: u64) -> Subaccount { + let domain = b"neuron-stake"; + let mut hasher = Sha256::new(); + hasher.update([domain.len() as u8]); + hasher.update(domain); + hasher.update(controller.as_slice()); + hasher.update(nonce.to_be_bytes()); + Subaccount(hasher.finalize().into()) +} + +fn hex_encode(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +// ── Candid types for NNS Governance ───────────────────────────────────────── + +#[derive(CandidType, Deserialize, Debug)] +struct ClaimOrRefreshNeuronFromAccount { + controller: Option, + memo: u64, +} + +#[derive(CandidType, Deserialize, Debug)] +struct NeuronId { + id: u64, +} + +#[derive(CandidType, Deserialize, Debug)] +struct GovernanceError { + error_type: i32, + error_message: String, +} + +#[derive(CandidType, Deserialize, Debug)] +enum ClaimOrRefreshResult { + Error(GovernanceError), + NeuronId(NeuronId), +} + +#[derive(CandidType, Deserialize, Debug)] +struct ClaimOrRefreshNeuronFromAccountResponse { + result: Option, +} diff --git a/rust/stake_neuron_from_cli/src/main.rs b/rust/stake_neuron_from_cli/src/main.rs new file mode 100644 index 0000000000..33bf07bcfe --- /dev/null +++ b/rust/stake_neuron_from_cli/src/main.rs @@ -0,0 +1,58 @@ +use clap::Parser; +use stake_neuron_from_cli::{stake_neuron, StakeNeuronArgs}; +use std::path::PathBuf; + +#[derive(Parser, Debug)] +#[command(about = "Stake ICP to create an NNS Governance neuron via the two-step manual flow")] +struct Args { + /// Path to the identity PEM file (Ed25519; use ic-agent's "ring" feature for Secp256k1) + #[arg(short, long)] + identity: PathBuf, + + /// URL of the IC replica (default: icp-cli local network) + #[arg(short, long, default_value = "http://localhost:8000")] + url: String, + + /// Amount to stake in e8s — minimum 100_000_000 (1 ICP); the 10_000 e8s transfer fee is charged on top + #[arg(short, long, default_value = "100000000")] + amount_e8s: u64, + + /// Nonce — a unique u64 per neuron; controls the staking subaccount + #[arg(short, long, default_value = "0")] + nonce: u64, + + /// Print the staking subaccount and exit without transferring any ICP. + /// Use this to verify the destination before committing funds. + #[arg(long)] + compute_only: bool, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let args = Args::parse(); + if args.compute_only { + use stake_neuron_from_cli::compute_neuron_staking_subaccount; + let pem = std::fs::read(&args.identity)?; + let identity: Box = + if let Ok(id) = ic_agent::identity::BasicIdentity::from_pem(&pem) { + Box::new(id) + } else { + Box::new(ic_agent::identity::Secp256k1Identity::from_pem(&pem)?) + }; + let controller = identity.sender()?; + let sub = compute_neuron_staking_subaccount(controller, args.nonce); + let hex: String = sub.0.iter().map(|b| format!("{b:02x}")).collect(); + println!("Controller : {controller}"); + println!("Nonce : {}", args.nonce); + println!("Subaccount : {hex}"); + return Ok(()); + } + stake_neuron(StakeNeuronArgs { + identity_path: args.identity, + ic_url: args.url, + amount_e8s: args.amount_e8s, + nonce: args.nonce, + }) + .await?; + Ok(()) +} diff --git a/rust/stake_neuron_from_cli/test.sh b/rust/stake_neuron_from_cli/test.sh index b2c81ed100..19606a9a0b 100755 --- a/rust/stake_neuron_from_cli/test.sh +++ b/rust/stake_neuron_from_cli/test.sh @@ -1,44 +1,42 @@ #!/usr/bin/env bash set -e -backend=$(icp canister status backend -i) -echo "Backend canister: $backend" - -echo "=== Test 1: compute_subaccount matches known reference value ===" -# Fixed reference: principal "aaaaa-aa" (management canister), nonce 0. -# The expected value is a pre-computed SHA-256 domain-separated hash. -# If this changes, the subaccount computation is broken and real ICP could be sent -# to the wrong address. -result=$(icp canister call --query backend compute_subaccount \ - '(principal "aaaaa-aa", 0 : nat64)') -echo "$result" -echo "$result" | grep -q '"b8c5a0fbf187460e550de4c606ab9ba102f7826c43ee644b80f275eb952c0aa8"' && \ - echo "PASS" || (echo "FAIL: subaccount hash does not match expected reference value" && exit 1) - -echo "=== Test 2: compute_subaccount changes with nonce ===" -s0=$(icp canister call --query backend compute_subaccount "(principal \"$backend\", 0 : nat64)") -s1=$(icp canister call --query backend compute_subaccount "(principal \"$backend\", 1 : nat64)") -echo "Nonce 0: $s0" -echo "Nonce 1: $s1" -[ "$s0" != "$s1" ] && echo "PASS" || (echo "FAIL: different nonces produced the same subaccount" && exit 1) +trap 'rm -f /tmp/stake-neuron-test.pem; icp identity delete stake-neuron-test 2>/dev/null || true' EXIT + +cargo build --release -q + +BINARY=./target/release/stake_neuron_from_cli +URL=http://localhost:8000 + +# Create a plaintext test identity and export its PEM. +# nns: true in icp.yaml seeds the default icp-cli identity with ICP at startup. +icp identity new --storage plaintext stake-neuron-test 2>/dev/null || true +icp identity export stake-neuron-test > /tmp/stake-neuron-test.pem +PRINCIPAL=$(icp identity principal --identity stake-neuron-test) +echo "Test identity: $PRINCIPAL" + +echo "=== Test 1: subaccount is deterministic for the same (identity, nonce) pair ===" +# --compute-only derives the subaccount without making any IC calls or transfers. +sub1=$("$BINARY" --compute-only --identity /tmp/stake-neuron-test.pem --nonce 0 | grep "Subaccount" | awk '{print $3}') +sub2=$("$BINARY" --compute-only --identity /tmp/stake-neuron-test.pem --nonce 0 | grep "Subaccount" | awk '{print $3}') +echo "Subaccount: $sub1" +[ "$sub1" = "$sub2" ] && echo "PASS" || (echo "FAIL: subaccount is not deterministic" && exit 1) + +echo "=== Test 2: different nonces produce different subaccounts ===" +sub_n0=$("$BINARY" --compute-only --identity /tmp/stake-neuron-test.pem --nonce 0 | grep "Subaccount" | awk '{print $3}') +sub_n1=$("$BINARY" --compute-only --identity /tmp/stake-neuron-test.pem --nonce 1 | grep "Subaccount" | awk '{print $3}') +echo "Nonce 0: $sub_n0" +echo "Nonce 1: $sub_n1" +[ "$sub_n0" != "$sub_n1" ] && echo "PASS" || (echo "FAIL: different nonces produced the same subaccount" && exit 1) echo "=== Test 3: stake_neuron rejects amount below 1 ICP minimum ===" -result=$(icp canister call backend stake_neuron '(99_999_999 : nat64, 0 : nat64)') -echo "$result" -echo "$result" | grep -q 'Err' && echo "PASS" || (echo "FAIL: expected rejection for sub-minimum amount" && exit 1) - -echo "=== Setup: fund backend with 2 ICP ===" -icp token transfer 2 "$backend" - -echo "=== Test 4: stake_neuron stakes 1 ICP and subaccount_hex matches compute_subaccount ===" -# Re-running this test tops up the existing neuron rather than creating a new one — -# claim_or_refresh reuses the same neuron_id for the same (controller, nonce) pair. -# Each re-run adds 1 ICP to the neuron's stake; the neuron_id stays the same. -expected_subaccount=$(icp canister call --query backend compute_subaccount \ - "(principal \"$backend\", 0 : nat64)" | grep -oE '[0-9a-f]{64}') -result=$(icp canister call backend stake_neuron '(100_000_000 : nat64, 0 : nat64)') -echo "$result" -echo "$result" | grep -q 'Ok' || (echo "FAIL: stake_neuron did not return Ok" && exit 1) -echo "$result" | grep -q "neuron_id" || (echo "FAIL: missing neuron_id in result" && exit 1) -echo "$result" | grep -q "$expected_subaccount" && \ - echo "PASS" || (echo "FAIL: subaccount_hex in result does not match compute_subaccount" && exit 1) +"$BINARY" --url "$URL" --identity /tmp/stake-neuron-test.pem --amount-e8s 99999999 --nonce 0 2>&1 | \ + grep -q "100_000_000" && echo "PASS" || (echo "FAIL" && exit 1) + +echo "=== Setup: fund test identity with 2 ICP ===" +icp token transfer 2 "$PRINCIPAL" + +echo "=== Test 4: stake 1 ICP and receive a neuron ID ===" +output=$("$BINARY" --url "$URL" --identity /tmp/stake-neuron-test.pem --amount-e8s 100000000 --nonce 0) +echo "$output" +echo "$output" | grep -q "Neuron ID" && echo "PASS" || (echo "FAIL" && exit 1) From 1647761487a8be991174cb7661616b4d36110540 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 3 Jul 2026 12:13:10 +0200 Subject: [PATCH 12/12] docs(rust/stake_neuron_from_cli): clarify Rust vs icp-cli roles in README - State upfront that ic-agent/Rust is the primary artifact; icp-cli is only used for local network management and not required on mainnet - Explain why Node.js is needed (icp-cli installs via npm) and link to alternative installation methods - Add --compute-only usage example to the subaccount section Co-Authored-By: Claude Sonnet 4.6 --- rust/stake_neuron_from_cli/README.md | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/rust/stake_neuron_from_cli/README.md b/rust/stake_neuron_from_cli/README.md index 03d0f1bc77..1da89e9b00 100644 --- a/rust/stake_neuron_from_cli/README.md +++ b/rust/stake_neuron_from_cli/README.md @@ -1,6 +1,8 @@ # Neuron Staking from CLI -This example demonstrates how to stake ICP to create an NNS Governance neuron from a Rust CLI using `ic-agent`. It shows the two-step process that the ledger and governance canisters expect. +This example demonstrates how to stake ICP to create an NNS Governance neuron from a **Rust CLI binary** using [`ic-agent`](https://crates.io/crates/ic-agent). The core logic lives in `src/lib.rs` and can be adapted for any Rust application that needs to stake ICP programmatically. + +[icp-cli](https://cli.internetcomputer.org/) is used **only for local development** — to start a local IC replica with NNS canisters pre-deployed. The Rust binary connects directly to the IC via `ic-agent` and does not depend on icp-cli at runtime. Before diving in, you may want to try [nns.internetcomputer.org](https://nns.internetcomputer.org/) first — it offers an interactive introduction to staking neurons and voting without writing any code. @@ -30,6 +32,12 @@ pub fn compute_neuron_staking_subaccount(controller: Principal, nonce: u64) -> S } ``` +Use `--compute-only` to verify the subaccount for a given identity and nonce before sending any ICP: + +```bash +./target/release/stake_neuron_from_cli --compute-only --identity identity.pem --nonce 0 +``` + ### 2. Two-Step Neuron Creation Process 1. **Transfer ICP** to the Governance canister at the computed staking subaccount. @@ -47,9 +55,12 @@ The minimum staking amount enforced by NNS Governance is 100,000,000 e8s (1 ICP) ### Prerequisites -- [Node.js](https://nodejs.org/) v18+ -- [icp-cli](https://cli.internetcomputer.org/): `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` -- [Rust](https://www.rust-lang.org/tools/install) v1.85+ with `wasm32-unknown-unknown` target: `rustup target add wasm32-unknown-unknown` +- [Rust](https://www.rust-lang.org/tools/install) v1.85+ +- [icp-cli](https://cli.internetcomputer.org/) — only needed for local testing to start a local IC replica with NNS canisters. Install via npm (requires [Node.js](https://nodejs.org/) v18+): + ```bash + npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm + ``` + Or use one of the [alternative installation methods](https://cli.internetcomputer.org/1.0/guides/installation/#alternative-installation-methods) if you prefer to avoid Node.js. ### Install @@ -61,15 +72,15 @@ cd examples/rust/stake_neuron_from_cli ### Run locally ```bash -icp network start -d -bash test.sh +icp network start -d # starts a local IC replica with NNS canisters +bash test.sh # builds the binary, creates a funded test identity, runs the staking flow icp network stop ``` -`test.sh` creates a test identity, funds it with ICP from the local NNS, builds the CLI binary, and runs the full staking flow. - ### Run on mainnet +icp-cli is not required for mainnet. Build the binary and run it directly: + ```bash cargo build --release