├── .gitattributes ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── conventional-commits.yml │ ├── e2e.yml │ ├── fmt.yml │ ├── ic-ref.yml │ ├── lint.yml │ ├── netlify.yml │ ├── provision-darwin.sh │ ├── provision-linux.sh │ ├── release.yml │ ├── shellcheck.yml │ └── test.yml ├── .gitignore ├── .mergify.yml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── e2e └── bash │ ├── icx.bash │ └── util │ └── assertions.bash ├── ic-agent ├── Cargo.toml ├── README.md ├── http_mock_service_worker.js └── src │ ├── agent │ ├── agent_config.rs │ ├── agent_error.rs │ ├── agent_test.rs │ ├── agent_test │ │ ├── pruned_subnet.bin │ │ ├── req_with_delegated_cert_response.bin │ │ ├── subnet_keys.bin │ │ ├── with_subnet_key.bin │ │ └── wrong_subnet.bin │ ├── builder.rs │ ├── http_transport │ │ ├── mod.rs │ │ └── reqwest_transport.rs │ ├── mod.rs │ ├── nonce.rs │ ├── response_authentication.rs │ ├── route_provider.rs │ ├── route_provider │ │ └── dynamic_routing │ │ │ ├── dynamic_route_provider.rs │ │ │ ├── health_check.rs │ │ │ ├── messages.rs │ │ │ ├── mod.rs │ │ │ ├── node.rs │ │ │ ├── nodes_fetch.rs │ │ │ ├── snapshot │ │ │ ├── latency_based_routing.rs │ │ │ ├── mod.rs │ │ │ ├── round_robin_routing.rs │ │ │ └── routing_snapshot.rs │ │ │ ├── test_utils.rs │ │ │ └── type_aliases.rs │ └── status.rs │ ├── export.rs │ ├── identity │ ├── anonymous.rs │ ├── basic.rs │ ├── delegated.rs │ ├── error.rs │ ├── mod.rs │ ├── prime256v1.rs │ └── secp256k1.rs │ ├── lib.rs │ └── util.rs ├── ic-certification └── README.md ├── ic-identity-hsm ├── Cargo.toml ├── README.md └── src │ ├── hsm.rs │ └── lib.rs ├── ic-transport-types ├── Cargo.toml └── src │ ├── lib.rs │ ├── request_id.rs │ ├── request_id │ └── error.rs │ └── signed.rs ├── ic-utils ├── Cargo.toml ├── README.md └── src │ ├── call.rs │ ├── call │ └── expiry.rs │ ├── canister.rs │ ├── interfaces.rs │ ├── interfaces │ ├── bitcoin_canister.rs │ ├── http_request.rs │ ├── management_canister.rs │ ├── management_canister │ │ ├── attributes.rs │ │ └── builders.rs │ └── wallet.rs │ └── lib.rs ├── icx-cert ├── Cargo.toml ├── README.md └── src │ ├── main.rs │ └── pprint.rs ├── icx ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── ref-tests ├── Cargo.toml ├── src │ ├── lib.rs │ ├── universal_canister.rs │ └── utils.rs └── tests │ ├── ic-ref.rs │ └── integration.rs ├── rust-toolchain.toml └── scripts └── cargo_publish.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @dfinity/sdk 2 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | DFINITY. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | By participating in this project, you agree to abide by our [Code of Conduct](./CODE_OF_CONDUCT.md). 4 | 5 | As a member of the community, you are invited and encouraged to contribute by submitting issues, offering suggestions for improvements, adding review comments to existing pull requests, or creating new pull requests to fix issues. 6 | 7 | ## Contents of this repository 8 | 9 | This repository contains a collection of libraries and tools for building software around the Internet Computer, in Rust. 10 | 11 | ## Before you contribute 12 | 13 | Before contributing, please take a few minutes to review these contributor guidelines. 14 | The contributor guidelines are intended to make the contribution process easy and effective for everyone involved in addressing your issue, assessing changes, and finalizing your pull requests. 15 | 16 | Before contributing, consider the following: 17 | 18 | - If you want to report an issue, click **Issues**. 19 | 20 | - If you have more general questions related to Motoko and its use, post a message to the [community forum](https://forum.dfinity.org/) or submit a [support request](mailto://support@dfinity.org). 21 | 22 | - If you are reporting a bug, provide as much information about the problem as possible. 23 | 24 | - If you want to contribute directly to this repository, typical fixes might include any of the following: 25 | 26 | - Fixes to resolve bugs or documentation errors 27 | - Code improvements 28 | - Feature requests 29 | 30 | Note that any contribution to this repository must be submitted in the form of a **pull request**. 31 | 32 | - If you are creating a pull request, be sure that the pull request only implements one fix or suggestion. 33 | 34 | If you are new to working with GitHub repositories and creating pull requests, consider exploring [First Contributions](https://github.com/firstcontributions/first-contributions) or [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github). 35 | 36 | # How to make a contribution 37 | 38 | Depending on the type of contribution you want to make, you might follow different workflows. 39 | 40 | This section describes the most common workflow scenarios: 41 | 42 | - Reporting an issue 43 | - Submitting a pull request 44 | 45 | ### Reporting an issue 46 | 47 | To open a new issue: 48 | 49 | 1. Click **Issues**. 50 | 51 | 1. Click **New Issue**. 52 | 53 | 1. Click **Open a blank issue**. 54 | 55 | 1. Type a title and description, then click **Submit new issue**. 56 | 57 | Be as clear and descriptive as possible. 58 | 59 | For any problem, describe it in detail, including details about the library, the version of the code you are using, the result 60 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. 4 | 5 | Fixes # (issue) 6 | 7 | # How Has This Been Tested? 8 | 9 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. 10 | 11 | # Checklist: 12 | 13 | - [ ] The title of this PR complies with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 14 | - [ ] I have edited the CHANGELOG accordingly. 15 | - [ ] I have made corresponding changes to the documentation. 16 | -------------------------------------------------------------------------------- /.github/workflows/conventional-commits.yml: -------------------------------------------------------------------------------- 1 | name: Check PR title 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - reopened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | check: 12 | name: conventional-pr-title:required 13 | runs-on: ubuntu-latest 14 | steps: 15 | # Conventional commit patterns: 16 | # verb: description 17 | # verb!: description of breaking change 18 | # verb(scope): Description of change to $scope 19 | # verb(scope)!: Description of breaking change to $scope 20 | # verb: feat, fix, ... 21 | # scope: refers to the part of code being changed. E.g. " (accounts)" or " (accounts,canisters)" 22 | # !: Indicates that the PR contains a breaking change. 23 | - env: 24 | TITLE: ${{ github.event.pull_request.title }} 25 | run: | 26 | echo "PR title: $TITLE" 27 | if [[ "$TITLE" =~ ^(feat|fix|chore|build|ci|docs|style|refactor|perf|test)(\([-a-zA-Z0-9,]+\))?\!?\: ]]; then 28 | echo pass 29 | else 30 | echo "PR title does not match conventions" 31 | exit 1 32 | fi 33 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: test-e2e 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | test: 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | os: [ macos-13-large, ubuntu-22.04 ] 14 | dfx: [ '0.8.4', '0.9.2', '0.10.1', '0.11.1' ] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Cache Cargo 19 | uses: actions/cache@v4 20 | with: 21 | path: | 22 | ~/.cargo/registry 23 | ~/.cargo/git 24 | target 25 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-1 26 | 27 | - name: Provision Darwin 28 | if: contains(matrix.os, 'macos') 29 | run: bash .github/workflows/provision-darwin.sh 30 | - name: Provision Linux 31 | if: contains(matrix.os, 'ubuntu') 32 | run: bash .github/workflows/provision-linux.sh 33 | - name: Install DFX 34 | uses: dfinity/setup-dfx@main 35 | with: 36 | dfx-version: ${{ matrix.dfx }} 37 | - name: Setup for dfx version differences 38 | run: | 39 | if [[ "${{ matrix.dfx }}" == "0.8.4" ]]; then 40 | echo "DFX_NO_WALLET=--no-wallet" >> "$GITHUB_ENV" 41 | fi 42 | 43 | # - name: 'Run tests' 44 | # run: bats e2e/bash/icx.bash 45 | 46 | aggregate: 47 | name: e2e:required 48 | if: ${{ always() }} 49 | needs: [ test ] 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: check e2e test result 53 | if: ${{ needs.test.result != 'success' }} 54 | run: exit 1 55 | -------------------------------------------------------------------------------- /.github/workflows/fmt.yml: -------------------------------------------------------------------------------- 1 | name: Format 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | name: fmt 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Cache Cargo 17 | uses: actions/cache@v4 18 | with: 19 | path: | 20 | ~/.cargo/registry 21 | ~/.cargo/git 22 | target 23 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-1 24 | 25 | - name: Run Cargo Fmt 26 | run: cargo fmt --all -- --check 27 | env: 28 | RUST_BACKTRACE: 1 29 | aggregate: 30 | name: fmt:required 31 | runs-on: ubuntu-latest 32 | if: ${{ always() }} 33 | needs: test 34 | steps: 35 | - name: Check fmt result 36 | if: ${{ needs.test.result != 'success' }} 37 | run: exit 1 38 | -------------------------------------------------------------------------------- /.github/workflows/ic-ref.yml: -------------------------------------------------------------------------------- 1 | name: ic-ref 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | build: [linux-stable] 15 | pocket-ic: ["replica", "pocketic"] 16 | include: 17 | - build: linux-stable 18 | ic-hs-ref: "3d71032e" 19 | wallet-tag: "20230308" 20 | os: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: 20 26 | 27 | - uses: actions/checkout@v4 28 | 29 | - name: Install dfx 30 | uses: dfinity/setup-dfx@main 31 | with: 32 | dfx-version: "0.24.3" 33 | 34 | - name: Cargo cache 35 | uses: actions/cache@v4 36 | with: 37 | path: | 38 | ~/.cargo/registry 39 | ~/.cargo/git 40 | target 41 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 42 | 43 | - name: Download cycles-wallet canister 44 | run: | 45 | wget https://github.com/dfinity/cycles-wallet/releases/download/${{ matrix.wallet-tag }}/wallet.wasm 46 | mv wallet.wasm $HOME/wallet.wasm 47 | 48 | - name: Download universal-canister 49 | run: | 50 | wget https://download.dfinity.systems/ic-ref/ic-ref-test-0.0.1-${{ matrix.ic-hs-ref }}-x86_64-linux.tar.gz 51 | tar -xvf ic-ref-test-0.0.1-${{ matrix.ic-hs-ref }}-x86_64-linux.tar.gz test-data/universal-canister.wasm 52 | mv test-data/universal-canister.wasm $HOME/canister.wasm 53 | 54 | - name: Run Integration Tests 55 | run: | 56 | set -ex 57 | if [[ "${{ matrix.pocket-ic }}" == "replica" ]] 58 | then 59 | dfx start --background --clean 60 | else 61 | dfx start --background --clean --pocketic 62 | fi 63 | sleep 1 64 | export IC_REF_PORT=$(dfx info webserver-port) 65 | export IC_UNIVERSAL_CANISTER_PATH=$HOME/canister.wasm 66 | export IC_WALLET_CANISTER_PATH=$HOME/wallet.wasm 67 | if [[ "${{ matrix.pocket-ic }}" == "pocketic" ]] 68 | then 69 | export POCKET_IC="http://localhost:${IC_REF_PORT}" 70 | fi 71 | cargo test --all-features -- --ignored 72 | dfx stop 73 | env: 74 | RUST_BACKTRACE: 1 75 | 76 | - name: Install and Configure SoftHSM 77 | run: | 78 | set -ex 79 | sudo apt-get install -f libsofthsm2 opensc-pkcs11 opensc 80 | sudo usermod -a -G softhsm $USER 81 | echo "SOFTHSM2_CONF=$HOME/softhsm.conf" >>$GITHUB_ENV 82 | echo "directories.tokendir = $HOME/softhsm/tokens/" >$HOME/softhsm.conf 83 | mkdir -p $HOME/softhsm/tokens 84 | 85 | - name: Run Integration Tests with SoftHSM 86 | run: | 87 | set -ex 88 | softhsm2-util --init-token --slot $HSM_SLOT_INDEX --label "agent-rs-token" --so-pin $HSM_SO_PIN --pin $HSM_PIN 89 | # create key: 90 | pkcs11-tool -k --module $HSM_PKCS11_LIBRARY_PATH --login --slot-index $HSM_SLOT_INDEX -d $HSM_KEY_ID --key-type EC:prime256v1 --pin $HSM_PIN 91 | 92 | if [[ "${{ matrix.pocket-ic }}" == "replica" ]] 93 | then 94 | dfx start --background --clean 95 | else 96 | dfx start --background --clean --pocketic 97 | fi 98 | sleep 1 99 | export IC_REF_PORT=$(dfx info webserver-port) 100 | export IC_UNIVERSAL_CANISTER_PATH=$HOME/canister.wasm 101 | export IC_WALLET_CANISTER_PATH=$HOME/wallet.wasm 102 | if [[ "${{ matrix.pocket-ic }}" == "pocketic" ]] 103 | then 104 | export POCKET_IC="http://localhost:${IC_REF_PORT}" 105 | fi 106 | cd ref-tests 107 | cargo test --all-features -- --ignored --nocapture --test-threads=1 108 | dfx stop 109 | env: 110 | RUST_BACKTRACE: 1 111 | HSM_PKCS11_LIBRARY_PATH: /usr/lib/softhsm/libsofthsm2.so 112 | HSM_SO_PIN: 123456 113 | HSM_PIN: 1234 114 | HSM_SLOT_INDEX: 0 115 | HSM_KEY_ID: abcdef 116 | 117 | - name: Run Doc Tests 118 | run: | 119 | set -ex 120 | if [[ "${{ matrix.pocket-ic }}" == "replica" ]] 121 | then 122 | dfx start --background --clean 123 | else 124 | dfx start --background --clean --pocketic 125 | fi 126 | sleep 1 127 | export IC_REF_PORT=$(dfx info webserver-port) 128 | if [[ "${{ matrix.pocket-ic }}" == "pocketic" ]] 129 | then 130 | export POCKET_IC="http://localhost:${IC_REF_PORT}" 131 | fi 132 | cargo test --all-features --doc -- --ignored 133 | dfx stop 134 | env: 135 | RUST_BACKTRACE: 1 136 | 137 | aggregate: 138 | name: ic-ref:required 139 | runs-on: ubuntu-latest 140 | if: ${{ always() }} 141 | needs: test 142 | steps: 143 | - name: Check ic-ref result 144 | if: ${{ needs.test.result != 'success' }} 145 | run: exit 1 146 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | name: lint 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - uses: actions/cache@v4 17 | with: 18 | path: | 19 | ~/.cargo/registry 20 | ~/.cargo/git 21 | target 22 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 23 | 24 | - uses: taiki-e/install-action@v2 25 | with: 26 | tool: cargo-hack@0.6.21 27 | 28 | - name: Run Lint 29 | run: | 30 | cargo hack clippy --each-feature --exclude-features ic_ref_tests --no-dev-deps --verbose -- -D warnings 31 | cargo clippy --features ic_ref_tests --verbose --tests --benches -- -D warnings 32 | env: 33 | RUST_BACKTRACE: 1 34 | 35 | - name: Run Lint (WASM) 36 | run: CARGO_TARGET_DIR=target/wasm cargo clippy --target wasm32-unknown-unknown -p ic-agent --features wasm-bindgen -p ic-utils --verbose -- -D warnings 37 | aggregate: 38 | name: lint:required 39 | runs-on: ubuntu-latest 40 | if: ${{ always() }} 41 | needs: test 42 | steps: 43 | - name: Check lint result 44 | if: ${{ needs.test.result != 'success' }} 45 | run: exit 1 46 | -------------------------------------------------------------------------------- /.github/workflows/netlify.yml: -------------------------------------------------------------------------------- 1 | name: Publish Cargo docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request_target: 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - uses: actions/cache@v4 17 | with: 18 | path: | 19 | ~/.cargo/registry 20 | ~/.cargo/git 21 | target 22 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 23 | 24 | - name: Install Rust 25 | run: | 26 | rustup update ${{ matrix.rust }} --no-self-update 27 | rustup default ${{ matrix.rust }} 28 | - name: Build Docs 29 | run: | 30 | cargo doc --no-deps 31 | # Add an index.html for the root of the netlify docs. 32 | rustdoc README.md --output target/doc && mv target/doc/README.html target/doc/index.html 33 | 34 | - if: github.ref == 'refs/heads/main' 35 | name: Deploy to Netlify (main only) 36 | uses: South-Paw/action-netlify-deploy@v1.0.4 37 | with: 38 | github-token: ${{ secrets.GITHUB_TOKEN }} 39 | netlify-auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }} 40 | netlify-site-id: ${{ secrets.NETLIFY_SITE_ID }} 41 | build-dir: target/doc/ 42 | comment-on-commit: true 43 | 44 | - if: github.ref != 'refs/heads/main' 45 | name: Deploy to Netlify (PR only) 46 | id: deploy_docs 47 | uses: netlify/actions/cli@6c34c3fcafc69ac2e1d6dbf226560329c6dfc51b 48 | with: 49 | args: deploy --dir=target/doc/ --prod 50 | env: 51 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 52 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 53 | 54 | - if: github.ref != 'refs/heads/main' 55 | name: Commenting on PR 56 | uses: unsplash/comment-on-pr@v1.2.0 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | with: 60 | msg: | 61 | Netlify deployed agent-rust as draft 62 | 63 | Link: ${{ steps.deploy_docs.outputs.NETLIFY_URL }} 64 | -------------------------------------------------------------------------------- /.github/workflows/provision-darwin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | # Enter temporary directory. 6 | pushd /tmp 7 | 8 | # Install Node. 9 | version=14.15.4 10 | curl --location --output node.pkg "https://nodejs.org/dist/v$version/node-v$version.pkg" 11 | sudo installer -pkg node.pkg -store -target / 12 | rm node.pkg 13 | 14 | # Install Bats. 15 | if [ "$(uname -r)" = "19.6.0" ]; then 16 | brew unlink bats 17 | fi 18 | brew install bats-core 19 | 20 | # Install Bats support. 21 | version=0.3.0 22 | curl --location --output bats-support.tar.gz https://github.com/ztombol/bats-support/archive/v$version.tar.gz 23 | mkdir /usr/local/lib/bats-support 24 | tar --directory /usr/local/lib/bats-support --extract --file bats-support.tar.gz --strip-components 1 25 | rm bats-support.tar.gz 26 | 27 | # Set environment variables. 28 | BATS_SUPPORT="/usr/local/lib/bats-support" 29 | echo "BATS_SUPPORT=${BATS_SUPPORT}" >> "$GITHUB_ENV" 30 | 31 | # Exit temporary directory. 32 | popd 33 | 34 | # Build icx 35 | cargo build -p icx 36 | ICX="$(pwd)/target/debug/icx" 37 | echo "ICX=$ICX" >> "$GITHUB_ENV" 38 | -------------------------------------------------------------------------------- /.github/workflows/provision-linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | # Enter temporary directory. 6 | pushd /tmp 7 | 8 | # Install Node. 9 | wget --output-document install-node.sh "https://deb.nodesource.com/setup_14.x" 10 | sudo bash install-node.sh 11 | sudo apt-get install --yes nodejs 12 | rm install-node.sh 13 | 14 | # Install Bats. 15 | sudo apt-get install --yes bats 16 | 17 | # Install Bats support. 18 | version=0.3.0 19 | curl --location --output bats-support.tar.gz https://github.com/ztombol/bats-support/archive/v$version.tar.gz 20 | sudo mkdir /usr/local/lib/bats-support 21 | sudo tar --directory /usr/local/lib/bats-support --extract --file bats-support.tar.gz --strip-components 1 22 | rm bats-support.tar.gz 23 | 24 | # Set environment variables. 25 | BATS_SUPPORT="/usr/local/lib/bats-support" 26 | echo "BATS_SUPPORT=${BATS_SUPPORT}" >> "$GITHUB_ENV" 27 | echo "$HOME/bin" >> "$GITHUB_PATH" 28 | 29 | # Exit temporary directory. 30 | popd 31 | 32 | # Build icx 33 | cargo build -p icx 34 | ICX="$(pwd)/target/debug/icx" 35 | echo "ICX=$ICX" >> "$GITHUB_ENV" 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | name: ['linux', 'macos'] 16 | include: 17 | - os: ubuntu-latest 18 | name: linux 19 | target: x86_64-unknown-linux-musl 20 | binary_path: target/x86_64-unknown-linux-musl/release 21 | binary_files: icx 22 | - os: macos-13-large 23 | name: macos 24 | target: x86_64-apple-darwin 25 | binary_path: target/x86_64-apple-darwin/release 26 | binary_files: icx 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: taiki-e/install-action@v2 30 | with: 31 | tool: cross@0.2.5 32 | 33 | - name: Setup environment variables 34 | run: | 35 | echo "SHA_SHORT=${GITHUB_SHA::7}" >> $GITHUB_ENV 36 | echo "OPENSSL_STATIC=yes" >> $GITHUB_ENV 37 | binaries=$(echo ${{ matrix.binary_files }} | xargs -n1 echo "--bin"|xargs) 38 | echo "cargo_build_ARGS<> $GITHUB_ENV 39 | echo "--locked --release $binaries" >> $GITHUB_ENV 40 | echo "END" >> $GITHUB_ENV 41 | 42 | - name: Build 43 | run: | 44 | cross build --target ${{ matrix.target }} ${{ env.cargo_build_ARGS }} 45 | 46 | - name: Strip binaries 47 | run: | 48 | cd ${{ matrix.binary_path }} 49 | sudo chown -R $(whoami) . 50 | strip ${{ matrix.binary_files }} 51 | if: ${{ contains(matrix.os, 'ubuntu') }} 52 | 53 | - name: Check linkage 54 | run: | 55 | cd ${{ matrix.binary_path }} 56 | otool -L ${{ matrix.binary_files }} 57 | if: ${{ contains(matrix.os, 'macos') }} 58 | 59 | - name: Create tarball of binaries 60 | if: ${{ github.event_name == 'push' }} 61 | run: tar -zcC ${{ matrix.binary_path }} -f binaries.tar.gz ${{ matrix.binary_files }} 62 | 63 | - name: Upload tarball 64 | if: ${{ github.event_name == 'push' }} 65 | uses: svenstaro/upload-release-action@v2 66 | with: 67 | repo_token: ${{ secrets.GITHUB_TOKEN }} 68 | file: binaries.tar.gz 69 | asset_name: binaries-${{ matrix.name }}.tar.gz 70 | tag: ${{ env.SHA_SHORT }} 71 | -------------------------------------------------------------------------------- /.github/workflows/shellcheck.yml: -------------------------------------------------------------------------------- 1 | name: Check shell scripts 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - reopened 7 | - synchronize 8 | paths: 9 | - 'e2e/**' 10 | - '.github/**' 11 | push: 12 | branches: 13 | - main 14 | 15 | jobs: 16 | check_macos: 17 | # ubuntu-latest has shellcheck 0.4.6, while macos-12 has 0.7.1 18 | runs-on: macos-13-large 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Install shellcheck 22 | run: | 23 | mkdir $HOME/bin 24 | cd $HOME/bin 25 | curl -L https://github.com/koalaman/shellcheck/releases/download/v0.7.1/shellcheck-v0.7.1.darwin.x86_64.tar.xz \ 26 | | xz -d | tar x 27 | - name: Check e2e scripts 28 | run: $HOME/bin/shellcheck-v0.7.1/shellcheck e2e/bash/*.bash e2e/bash/util/*.bash 29 | - name: Check workflow scripts 30 | run: $HOME/bin/shellcheck-v0.7.1/shellcheck .github/workflows/*.sh 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, macos-13-large, windows-latest] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: actions/cache@v4 20 | with: 21 | path: | 22 | ~/.cargo/registry 23 | ~/.cargo/git 24 | target 25 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-1 26 | 27 | - name: Install wasm-pack and chromedriver 28 | if: ${{ matrix.os == 'ubuntu-latest' }} 29 | run: | 30 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 31 | sudo apt-get install -y chromium-chromedriver 32 | 33 | - name: Run Tests 34 | shell: bash 35 | run: | 36 | # Test all features and no features for each package. 37 | for p in $(cargo metadata --no-deps --format-version 1 | jq -r .packages[].manifest_path); do 38 | pushd $(dirname $p) 39 | cargo test --all-targets --all-features 40 | cargo test --all-targets --no-default-features 41 | popd 42 | done 43 | env: 44 | RUST_BACKTRACE: 1 45 | 46 | - name: Run Tests (WASM) 47 | if: ${{ matrix.os == 'ubuntu-latest' }} 48 | run: | 49 | CARGO_TARGET_DIR=target/wasm wasm-pack test --chrome --headless ic-agent --features wasm-bindgen 50 | 51 | aggregate: 52 | name: test:required 53 | runs-on: ubuntu-latest 54 | if: ${{ always() }} 55 | needs: test 56 | steps: 57 | - name: Check test result 58 | if: ${{ needs.test.result != 'success' }} 59 | run: exit 1 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | /.vscode/ 4 | .idea/ 5 | 6 | # will have compiled files and executables 7 | /target/ 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: Automatic merge 3 | conditions: 4 | - "#approved-reviews-by>=1" 5 | - "#changes-requested-reviews-by=0" 6 | - status-success=conventional-pr-title 7 | - label=automerge-squash 8 | actions: 9 | merge: 10 | method: squash 11 | strict: smart 12 | commit_message: title+body 13 | delete_head_branch: {} 14 | 15 | - name: Clean up automerge tags 16 | conditions: 17 | - closed 18 | actions: 19 | label: 20 | remove: 21 | - automerge-squash 22 | 23 | - name: Auto-approve auto-PRs 24 | conditions: 25 | - author=dfinity-bot 26 | actions: 27 | review: 28 | type: APPROVE 29 | message: This bot trusts that bot 30 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "ic-agent", 5 | "icx-cert", 6 | "ic-identity-hsm", 7 | "ic-utils", 8 | "ic-transport-types", 9 | "icx", 10 | "ref-tests", 11 | ] 12 | 13 | [workspace.package] 14 | version = "0.40.1" 15 | authors = ["DFINITY Stiftung "] 16 | edition = "2021" 17 | repository = "https://github.com/dfinity/agent-rs" 18 | # MSRV 19 | # Avoid updating this field unless we use new Rust features 20 | # Sync rust-version in rust-toolchain.toml 21 | rust-version = "1.78.0" 22 | license = "Apache-2.0" 23 | 24 | [workspace.lints.clippy] 25 | needless_lifetimes = "allow" 26 | 27 | [workspace.dependencies] 28 | ic-agent = { path = "ic-agent", version = "0.40.1", default-features = false } 29 | ic-utils = { path = "ic-utils", version = "0.40.1" } 30 | ic-transport-types = { path = "ic-transport-types", version = "0.40.1" } 31 | 32 | ic-certification = "3" 33 | candid = "0.10.10" 34 | candid_parser = "0.1.4" 35 | clap = "4.5.21" 36 | ed25519-consensus = "2.1.0" 37 | futures-util = "0.3.31" 38 | hex = "0.4.3" 39 | k256 = "0.13.4" 40 | leb128 = "0.2.5" 41 | pocket-ic = "6.0.0" 42 | p256 = "0.13.2" 43 | rand = "0.8.5" 44 | reqwest = { version = "0.12", default-features = false } 45 | serde = "1.0.215" 46 | serde_bytes = "0.11.15" 47 | serde_cbor = "0.11.2" 48 | serde_json = "1.0.133" 49 | serde_repr = "0.1.19" 50 | sha2 = "0.10.8" 51 | thiserror = "2.0.3" 52 | time = "0.3" 53 | tokio = { version = "1.41.1", default-features = false } 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 DFINITY Stiftung. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DFINITY's Rust Agent Repository 2 | ![GitHub Workflow Status](https://github.com/dfinity/agent-rs/workflows/Tests/badge.svg) 3 | 4 | 5 | 6 | ## Contributing 7 | Please follow the guidelines in the [CONTRIBUTING.md](.github/CONTRIBUTING.md) document. 8 | 9 | ## Building 10 | We use `cargo` to build this repo. Make sure you have rust stable installed. To build the repo: 11 | 12 | ```sh 13 | cargo build 14 | ``` 15 | 16 | ## Testing 17 | There are two suites of tests that can be executed from this repo; the regular cargo tests and 18 | the ic-ref tests. In order to run the ic-ref tests, you will need a running local reference 19 | server. If you do not have one, those tests will be ignored. 20 | 21 | ## Release 22 | To release: 23 | - increase the version number in Cargo.toml (`workspace.package` and `workspace.dependencies`) 24 | - add a header for the version under "## Unreleased" in CHANGELOG.md 25 | - run `cargo build` to update the lock file 26 | 27 | ## Packages 28 | This repo has multiple packages in its Cargo workspace. 29 | 30 | | Package Name | Links | Description | 31 | |---|---|---| 32 | | `ic-agent` | [![README](https://img.shields.io/badge/-README-green)](https://github.com/dfinity/agent-rs/tree/next/ic-agent) [![DOC](https://img.shields.io/badge/-DOC-blue)](https://docs.rs/ic_agent) | The `ic-agent` is a library to talk directly to the Replica. | 33 | | `ic-utils` | [![README](https://img.shields.io/badge/-README-green)](https://github.com/dfinity/agent-rs/tree/next/ic-utils) [![DOC](https://img.shields.io/badge/-DOC-blue)](https://docs.rs/ic_utils) | A library of utilities for managing calls and canisters. | 34 | | `icx` | [![README](https://img.shields.io/badge/-README-green)](https://github.com/dfinity/agent-rs/tree/next/icx) | A command line utility to use the agent. Not meant to be published, only available in this repo for tests. | 35 | | `ref-tests` | | A package that only exists to run the ic-ref tests with the ic-agent as the connection. | 36 | -------------------------------------------------------------------------------- /e2e/bash/icx.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # shellcheck disable=SC1090 4 | source "$BATS_SUPPORT"/load.bash 5 | 6 | load util/assertions 7 | 8 | setup() { 9 | cd "$(mktemp -d -t icx-e2e-XXXXXXXX)" || exit 1 10 | dfx new --no-frontend e2e_project 11 | cd e2e_project || exit 1 12 | dfx start --background 13 | dfx deploy 14 | } 15 | 16 | teardown() { 17 | echo teardown 18 | dfx stop 19 | } 20 | 21 | # this test does not work, and is not run in CI 22 | @test "sign update" { 23 | "$ICX" --pem "$HOME"/.config/dfx/identity/default/identity.pem update --serialize rwlgt-iiaaa-aaaaa-aaaaa-cai greet '("everyone")' > output.txt 24 | head -n 1 output.txt > update.json 25 | tail -n 1 output.txt > request_status.json 26 | "$ICX" send "$stderrf" >"$stdoutf"; echo -n "$?" > "$statusf" ) 17 | status="$(<"$statusf")"; rm "$statusf" 18 | 19 | stderr="$(cat "$stderrf")"; rm "$stderrf" 20 | stdout="$(cat "$stdoutf")"; rm "$stdoutf" 21 | # shellcheck disable=SC2015 22 | output="$( \ 23 | [ "$stderr" ] && echo "$stderr" || true; \ 24 | [ "$stdout" ] && echo "$stdout" || true; \ 25 | )" 26 | 27 | [[ $status == 0 ]] || \ 28 | ( (echo "$*"; echo "status: $status"; echo "$output" | batslib_decorate "Output") \ 29 | | batslib_decorate "Command failed" \ 30 | | fail) 31 | } 32 | 33 | # Asserts that a command line fails. Still sets $output to the stdout and stderr 34 | # of the command. 35 | # Arguments: 36 | # $@ - The command to run. 37 | # Returns: 38 | # none 39 | assert_command_fail() { 40 | x="$(mktemp)" 41 | local stderrf="$x" 42 | x="$(mktemp)" 43 | local stdoutf="$x" 44 | x="$(mktemp)" 45 | local statusf="$x" 46 | ( set +e; "$@" 2>"$stderrf" >"$stdoutf"; echo -n "$?" >"$statusf" ) 47 | status="$(<"$statusf")"; rm "$statusf" 48 | 49 | stderr="$(cat "$stderrf")"; rm "$stderrf" 50 | stdout="$(cat "$stdoutf")"; rm "$stdoutf" 51 | # shellcheck disable=SC2015 52 | output="$( 53 | [ "$stderr" ] && echo "$stderr" || true; 54 | [ "$stdout" ] && echo "$stdout" || true; 55 | )" 56 | 57 | [[ $status != 0 ]] || \ 58 | ( (echo "$*"; echo "$output" | batslib_decorate "Output") \ 59 | | batslib_decorate "Command succeeded (should have failed)" \ 60 | | fail) 61 | } 62 | 63 | # Asserts that a string contains another string, using regexp. 64 | # Arguments: 65 | # $1 - The regex to use to match. 66 | # $2 - The string to match against (output). By default it will use 67 | # $output. 68 | assert_match() { 69 | regex="$1" 70 | if [[ $# -lt 2 ]]; then 71 | text="$output" 72 | else 73 | text="$2" 74 | fi 75 | [[ "$text" =~ $regex ]] || \ 76 | (batslib_print_kv_single_or_multi 10 "regex" "$regex" "actual" "$text" \ 77 | | batslib_decorate "output does not match" \ 78 | | fail) 79 | } 80 | 81 | # Asserts that a string does not contain another string, using regexp. 82 | # Arguments: 83 | # $1 - The regex to use to match. 84 | # $2 - The string to match against (output). By default it will use 85 | # $output. 86 | assert_not_match() { 87 | regex="$1" 88 | if [[ $# -lt 2 ]]; then 89 | text="$output" 90 | else 91 | text="$2" 92 | fi 93 | if [[ "$text" =~ $regex ]]; then 94 | (batslib_print_kv_single_or_multi 10 "regex" "$regex" "actual" "$text" \ 95 | | batslib_decorate "output matches but is expected not to" \ 96 | | fail) 97 | fi 98 | } 99 | 100 | # Asserts a command will timeout. This assertion will fail if the command finishes before 101 | # the timeout period. If the command fails, it will also fail. 102 | # Arguments: 103 | # $1 - The amount of time (in seconds) to wait for. 104 | # $@ - The command to run. 105 | 106 | # Asserts that two values are equal. 107 | # Arguments: 108 | # $1 - The expected value. 109 | # $2 - The actual value. 110 | assert_eq() { 111 | expected="$1" 112 | if [[ $# -lt 2 ]]; then 113 | actual="$output" 114 | else 115 | actual="$2" 116 | fi 117 | 118 | [[ "$actual" == "$expected" ]] || \ 119 | (batslib_print_kv_single_or_multi 10 "expected" "$expected" "actual" "$actual" \ 120 | | batslib_decorate "output does not match" \ 121 | | fail) 122 | } 123 | 124 | # Asserts that two values are not equal. 125 | # Arguments: 126 | # $1 - The expected value. 127 | # $2 - The actual value. 128 | assert_neq() { 129 | expected="$1" 130 | if [[ $# -lt 2 ]]; then 131 | actual="$output" 132 | else 133 | actual="$2" 134 | fi 135 | 136 | [[ "$actual" != "$expected" ]] || \ 137 | (batslib_print_kv_single_or_multi 10 "expected" "$expected" "actual" "$actual" \ 138 | | batslib_decorate "output does not match" \ 139 | | fail) 140 | } 141 | 142 | 143 | # Asserts that a process exits within a timeframe 144 | # Arguments: 145 | # $1 - the PID 146 | # $2 - the timeout 147 | assert_process_exits() { 148 | pid="$1" 149 | timeout="$2" 150 | 151 | timeout "$timeout" sh -c \ 152 | "while kill -0 $pid; do echo waiting for process $pid to exit; sleep 1; done" \ 153 | || (echo "process $pid did not exit" && ps aux && exit 1) 154 | 155 | } 156 | 157 | assert_file_eventually_exists() { 158 | filename="$1" 159 | timeout="$2" 160 | 161 | timeout "$timeout" sh -c \ 162 | "until [ -f $filename ]; do echo waiting for $filename; sleep 1; done" \ 163 | || (echo "file $filename was never created" && ls && exit 1) 164 | } 165 | -------------------------------------------------------------------------------- /ic-agent/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-agent" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | rust-version.workspace = true 9 | description = "Agent library to communicate with the Internet Computer, following the Public Specification." 10 | homepage = "https://docs.rs/ic-agent" 11 | documentation = "https://docs.rs/ic-agent" 12 | readme = "README.md" 13 | categories = ["api-bindings", "data-structures", "no-std"] 14 | keywords = ["internet-computer", "agent", "icp", "dfinity"] 15 | include = ["src", "Cargo.toml", "../LICENSE", "README.md"] 16 | 17 | [lints] 18 | workspace = true 19 | 20 | [dependencies] 21 | arc-swap = "1.7" 22 | async-channel = "1.9" 23 | async-lock = "3.3" 24 | async-trait = "0.1" 25 | async-watch = "0.3" 26 | backoff = "0.4.0" 27 | cached = { version = "0.52", features = ["ahash"], default-features = false } 28 | candid = { workspace = true } 29 | der = "0.7" 30 | ecdsa = "0.16" 31 | ed25519-consensus = { workspace = true } 32 | elliptic-curve = "0.13" 33 | futures-util = { workspace = true } 34 | hex = { workspace = true } 35 | http = "1.0.0" 36 | http-body = "1.0.0" 37 | ic-certification = { workspace = true } 38 | ic-transport-types = { workspace = true } 39 | ic-verify-bls-signature = "0.5" 40 | k256 = { workspace = true, features = ["pem"] } 41 | p256 = { workspace = true, features = ["pem"] } 42 | leb128 = { workspace = true } 43 | pkcs8 = { version = "0.10.2", features = ["std"] } 44 | sec1 = { version = "0.7.2", features = ["pem"] } 45 | rand = { workspace = true } 46 | rangemap = "1.4" 47 | ring = { version = "0.17", optional = true } 48 | serde = { workspace = true, features = ["derive"] } 49 | serde_bytes = { workspace = true } 50 | serde_cbor = { workspace = true } 51 | serde_repr = { workspace = true } 52 | sha2 = { workspace = true } 53 | simple_asn1 = "0.6.1" 54 | stop-token = "0.7" 55 | thiserror = { workspace = true } 56 | time = { workspace = true } 57 | tower-service = "0.3" 58 | tracing = { version = "0.1", optional = true } 59 | url = "2.1.0" 60 | 61 | [dependencies.reqwest] 62 | workspace = true 63 | default-features = false 64 | features = ["blocking", "json", "rustls-tls-webpki-roots", "stream"] 65 | 66 | [dependencies.pem] 67 | version = "3" 68 | optional = true 69 | 70 | [target.'cfg(not(target_family = "wasm"))'.dependencies] 71 | tokio = { version = "1.24.2", features = ["time"] } 72 | 73 | [target.'cfg(target_family = "wasm")'.dependencies] 74 | getrandom = { version = "0.2", features = ["js"], optional = true } 75 | js-sys = { version = "0.3", optional = true } 76 | wasm-bindgen = { version = "0.2", optional = true } 77 | wasm-bindgen-futures = { version = "0.4", optional = true } 78 | web-sys = { version = "0.3", features = ["Window"], optional = true } 79 | 80 | [dev-dependencies] 81 | serde_json.workspace = true 82 | tracing-subscriber = "0.3" 83 | tracing = "0.1" 84 | 85 | [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] 86 | tokio = { workspace = true, features = ["full"] } 87 | mockito = "1.0.2" 88 | 89 | [target.'cfg(target_family = "wasm")'.dev-dependencies] 90 | wasm-bindgen-test = "0.3.34" 91 | web-sys = { version = "0.3", features = [ 92 | "Navigator", 93 | "ServiceWorkerContainer", 94 | "ServiceWorker", 95 | "ServiceWorkerRegistration", 96 | "ServiceWorkerState", 97 | ] } 98 | 99 | [features] 100 | default = ["pem"] 101 | pem = ["dep:pem"] 102 | ring = ["dep:ring"] 103 | ic_ref_tests = [ 104 | "default", 105 | ] # Used to separate integration tests for ic-ref which need a server running. 106 | wasm-bindgen = [ 107 | "dep:js-sys", 108 | "dep:wasm-bindgen", 109 | "dep:wasm-bindgen-futures", 110 | "dep:getrandom", 111 | "dep:web-sys", 112 | "time/wasm-bindgen", 113 | "backoff/wasm-bindgen", 114 | "cached/wasm", 115 | ] 116 | _internal_dynamic-routing = [] 117 | tracing = ["dep:tracing"] # Does very little right now. 118 | 119 | [package.metadata.docs.rs] 120 | targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] 121 | rustdoc-args = ["--cfg=docsrs"] 122 | features = [] 123 | -------------------------------------------------------------------------------- /ic-agent/README.md: -------------------------------------------------------------------------------- 1 | `ic-agent` is a simple-to-use library to interact with the [Internet Computer](https://internetcomputer.org) 2 | in Rust. It is the backend for [`dfx`](https://internetcomputer.org/docs/current/references/cli-reference/). 3 | 4 | ## Useful links 5 | 6 | - [Documentation (master)](https://agent-rust.netlify.app/ic_agent) 7 | - [Documentation (published)](https://docs.rs/ic_agent) 8 | -------------------------------------------------------------------------------- /ic-agent/http_mock_service_worker.js: -------------------------------------------------------------------------------- 1 | let db = null; 2 | 3 | async function getDb() { 4 | if (db) { 5 | return db; 6 | } else { 7 | return await new Promise((rs, rj) => { 8 | const req = indexedDB.open("http_mock", 1); 9 | req.onsuccess = (event) => rs(event.target.result); 10 | req.onerror = rj; 11 | req.onupgradeneeded = (event) => { 12 | db = event.target.result; 13 | db.createObjectStore("mocks", { keyPath: "nonce" }); 14 | }; 15 | }) 16 | } 17 | } 18 | 19 | async function setMock(mock) { 20 | const db = await getDb(); 21 | await new Promise((rs, rj) => { 22 | const transaction = db.transaction("mocks", "readwrite"); 23 | transaction.oncomplete = rs; 24 | transaction.onerror = rj; 25 | const store = transaction.objectStore("mocks"); 26 | store.put(mock); 27 | }) 28 | } 29 | 30 | async function getMock(nonce) { 31 | const db = await getDb(); 32 | return await new Promise((rs, rj) => { 33 | const req = db.transaction("mocks") 34 | .objectStore("mocks") 35 | .get(nonce); 36 | req.onsuccess = (event) => rs(event.target.result); 37 | req.onerror = rj; 38 | }); 39 | } 40 | 41 | // Status codes are chosen to avoid being picked up as successes by tests expecting a 404 or 500. 42 | 43 | self.addEventListener("fetch", (event) => { 44 | event.respondWith((async () => { 45 | try { 46 | const request = event.request; 47 | const url = new URL(request.url); 48 | if (url.host === "mock_configure") { 49 | const nonce = url.pathname.substring(1); 50 | const { method, path, status_code, body, headers } = await request.json(); 51 | const mock = await getMock(nonce) ?? { nonce, routes: [] }; 52 | mock.routes.push({ method, path, status_code, body, headers, hits: 0 }); 53 | await setMock(mock); 54 | return new Response(null, { status: 204 }); 55 | } else if (url.host === "mock_assert") { 56 | const nonce = url.pathname.substring(1); 57 | const mock = await getMock(nonce); 58 | if (mock === undefined) { 59 | return new Response(`no such mock id ${nonce}`, { status: 421 }); 60 | } 61 | const hitsMap = Object.fromEntries(mock.routes.map(route => [`${route.method} ${route.path}`, route.hits])); 62 | return new Response(JSON.stringify(hitsMap), { status: 200, headers: { 'Content-Type': 'application/json' } }); 63 | } else { 64 | const nonce = url.host.split('_')[1]; 65 | const mock = await getMock(nonce); 66 | if (mock === undefined) { 67 | return new Response(`no such mock id ${nonce}`, { status: 421 }); 68 | } 69 | for (const route of mock.routes) { 70 | if (request.method === route.method && url.pathname === route.path) { 71 | route.hits += 1; 72 | await setMock(mock); 73 | return new Response(Uint8Array.from(route.body), { status: route.status_code, headers: route.headers }); 74 | } 75 | } 76 | const possiblyMeant = mock.routes.find(route => route.path === url.pathname); 77 | if (possiblyMeant !== undefined) { 78 | return new Response(`expected ${possiblyMeant.method}, got ${request.method}`, { status: 405 }) 79 | } else { 80 | return new Response(`expected ${mock.routes.map(route => route.path).join(' | ')}, got ${url.pathname}`, { status: 410 }); 81 | } 82 | } 83 | } catch (e) { 84 | return new Response(e.toString(), { status: 503 }); 85 | } 86 | })()) 87 | }); 88 | 89 | self.addEventListener("activate", (event) => { 90 | skipWaiting(); 91 | event.waitUntil(clients.claim()); 92 | }); 93 | -------------------------------------------------------------------------------- /ic-agent/src/agent/agent_config.rs: -------------------------------------------------------------------------------- 1 | use reqwest::Client; 2 | use url::Url; 3 | 4 | use crate::{ 5 | agent::{NonceFactory, NonceGenerator}, 6 | identity::{anonymous::AnonymousIdentity, Identity}, 7 | }; 8 | use std::{sync::Arc, time::Duration}; 9 | 10 | use super::{route_provider::RouteProvider, HttpService}; 11 | 12 | /// A configuration for an agent. 13 | #[non_exhaustive] 14 | pub struct AgentConfig { 15 | /// See [`with_nonce_factory`](super::AgentBuilder::with_nonce_factory). 16 | pub nonce_factory: Arc, 17 | /// See [`with_identity`](super::AgentBuilder::with_identity). 18 | pub identity: Arc, 19 | /// See [`with_ingress_expiry`](super::AgentBuilder::with_ingress_expiry). 20 | pub ingress_expiry: Duration, 21 | /// See [`with_http_client`](super::AgentBuilder::with_http_client). 22 | pub client: Option, 23 | /// See [`with_route_provider`](super::AgentBuilder::with_route_provider). 24 | pub route_provider: Option>, 25 | /// See [`verify_query_signatures`](super::AgentBuilder::with_verify_query_signatures). 26 | pub verify_query_signatures: bool, 27 | /// See [`with_max_concurrent_requests`](super::AgentBuilder::with_max_concurrent_requests). 28 | pub max_concurrent_requests: usize, 29 | /// See [`with_max_response_body_size`](super::AgentBuilder::with_max_response_body_size). 30 | pub max_response_body_size: Option, 31 | /// See [`with_max_tcp_error_retries`](super::AgentBuilder::with_max_tcp_error_retries). 32 | pub max_tcp_error_retries: usize, 33 | /// See [`with_arc_http_middleware`](super::AgentBuilder::with_arc_http_middleware). 34 | pub http_service: Option>, 35 | /// See [`with_max_polling_time`](super::AgentBuilder::with_max_polling_time). 36 | pub max_polling_time: Duration, 37 | /// See [`with_background_dynamic_routing`](super::AgentBuilder::with_background_dynamic_routing). 38 | pub background_dynamic_routing: bool, 39 | /// See [`with_url`](super::AgentBuilder::with_url). 40 | pub url: Option, 41 | } 42 | 43 | impl Default for AgentConfig { 44 | fn default() -> Self { 45 | Self { 46 | nonce_factory: Arc::new(NonceFactory::random()), 47 | identity: Arc::new(AnonymousIdentity {}), 48 | ingress_expiry: Duration::from_secs(3 * 60), 49 | client: None, 50 | http_service: None, 51 | verify_query_signatures: true, 52 | max_concurrent_requests: 50, 53 | route_provider: None, 54 | max_response_body_size: None, 55 | max_tcp_error_retries: 0, 56 | max_polling_time: Duration::from_secs(60 * 5), 57 | background_dynamic_routing: false, 58 | url: None, 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ic-agent/src/agent/agent_test/pruned_subnet.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/agent-rs/23589cca4e455f069c203e42d3758c12f12b9f87/ic-agent/src/agent/agent_test/pruned_subnet.bin -------------------------------------------------------------------------------- /ic-agent/src/agent/agent_test/req_with_delegated_cert_response.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/agent-rs/23589cca4e455f069c203e42d3758c12f12b9f87/ic-agent/src/agent/agent_test/req_with_delegated_cert_response.bin -------------------------------------------------------------------------------- /ic-agent/src/agent/agent_test/subnet_keys.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/agent-rs/23589cca4e455f069c203e42d3758c12f12b9f87/ic-agent/src/agent/agent_test/subnet_keys.bin -------------------------------------------------------------------------------- /ic-agent/src/agent/agent_test/with_subnet_key.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/agent-rs/23589cca4e455f069c203e42d3758c12f12b9f87/ic-agent/src/agent/agent_test/with_subnet_key.bin -------------------------------------------------------------------------------- /ic-agent/src/agent/agent_test/wrong_subnet.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/agent-rs/23589cca4e455f069c203e42d3758c12f12b9f87/ic-agent/src/agent/agent_test/wrong_subnet.bin -------------------------------------------------------------------------------- /ic-agent/src/agent/builder.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | agent::{agent_config::AgentConfig, Agent}, 3 | AgentError, Identity, NonceFactory, NonceGenerator, 4 | }; 5 | use std::sync::Arc; 6 | 7 | use super::{route_provider::RouteProvider, HttpService}; 8 | 9 | /// A builder for an [`Agent`]. 10 | #[derive(Default)] 11 | pub struct AgentBuilder { 12 | config: AgentConfig, 13 | } 14 | 15 | impl AgentBuilder { 16 | /// Create an instance of [Agent] with the information from this builder. 17 | pub fn build(self) -> Result { 18 | Agent::new(self.config) 19 | } 20 | 21 | /// Set the dynamic transport layer for the [`Agent`], performing continuous discovery of the API boundary nodes 22 | /// and routing traffic via them based on latency. Cannot be set together with `with_route_provider`. 23 | /// 24 | /// See [`DynamicRouteProvider`](super::route_provider::DynamicRouteProvider) if more customization is needed such as polling intervals. 25 | pub fn with_background_dynamic_routing(mut self) -> Self { 26 | assert!( 27 | self.config.route_provider.is_none(), 28 | "with_background_dynamic_routing cannot be called with with_route_provider" 29 | ); 30 | self.config.background_dynamic_routing = true; 31 | self 32 | } 33 | 34 | /// Set the URL of the [`Agent`]. Either this or `with_route_provider` must be called (but not both). 35 | pub fn with_url>(mut self, url: S) -> Self { 36 | assert!( 37 | self.config.route_provider.is_none(), 38 | "with_url cannot be called with with_route_provider" 39 | ); 40 | self.config.url = Some(url.into().parse().unwrap()); 41 | self 42 | } 43 | 44 | /// Add a `NonceFactory` to this Agent. By default, no nonce is produced. 45 | pub fn with_nonce_factory(self, nonce_factory: NonceFactory) -> AgentBuilder { 46 | self.with_nonce_generator(nonce_factory) 47 | } 48 | 49 | /// Same as [`Self::with_nonce_factory`], but for any `NonceGenerator` type 50 | pub fn with_nonce_generator( 51 | self, 52 | nonce_factory: N, 53 | ) -> AgentBuilder { 54 | self.with_arc_nonce_generator(Arc::new(nonce_factory)) 55 | } 56 | 57 | /// Same as [`Self::with_nonce_generator`], but reuses an existing `Arc`. 58 | pub fn with_arc_nonce_generator( 59 | mut self, 60 | nonce_factory: Arc, 61 | ) -> AgentBuilder { 62 | self.config.nonce_factory = Arc::new(nonce_factory); 63 | self 64 | } 65 | 66 | /// Add an identity provider for signing messages. This is required. 67 | pub fn with_identity(self, identity: I) -> Self 68 | where 69 | I: 'static + Identity, 70 | { 71 | self.with_arc_identity(Arc::new(identity)) 72 | } 73 | 74 | /// Same as [`Self::with_identity`], but reuses an existing box 75 | pub fn with_boxed_identity(self, identity: Box) -> Self { 76 | self.with_arc_identity(Arc::from(identity)) 77 | } 78 | 79 | /// Same as [`Self::with_identity`], but reuses an existing `Arc` 80 | pub fn with_arc_identity(mut self, identity: Arc) -> Self { 81 | self.config.identity = identity; 82 | self 83 | } 84 | 85 | /// Provides a _default_ ingress expiry. This is the delta that will be applied 86 | /// at the time an update or query is made. The default expiry cannot be a 87 | /// fixed system time. This is also used when checking certificate timestamps. 88 | /// 89 | /// The timestamp corresponding to this duration may be rounded in order to reduce 90 | /// cache misses. The current implementation rounds to the nearest minute if the 91 | /// expiry is more than a minute, but this is not guaranteed. 92 | pub fn with_ingress_expiry(mut self, ingress_expiry: std::time::Duration) -> Self { 93 | self.config.ingress_expiry = ingress_expiry; 94 | self 95 | } 96 | 97 | /// Allows disabling query signature verification. Query signatures improve resilience but require 98 | /// a separate read-state call to fetch node keys. 99 | pub fn with_verify_query_signatures(mut self, verify_query_signatures: bool) -> Self { 100 | self.config.verify_query_signatures = verify_query_signatures; 101 | self 102 | } 103 | 104 | /// Sets the maximum number of requests that the agent will make concurrently. The replica is configured 105 | /// to only permit 50 concurrent requests per client. Set this value lower if you have multiple agents, 106 | /// to avoid the slowdown of retrying any 429 errors. 107 | pub fn with_max_concurrent_requests(mut self, max_concurrent_requests: usize) -> Self { 108 | self.config.max_concurrent_requests = max_concurrent_requests; 109 | self 110 | } 111 | 112 | /// Add a `RouteProvider` to this agent, to provide the URLs of boundary nodes. 113 | pub fn with_route_provider(self, provider: impl RouteProvider + 'static) -> Self { 114 | self.with_arc_route_provider(Arc::new(provider)) 115 | } 116 | 117 | /// Same as [`Self::with_route_provider`], but reuses an existing `Arc`. 118 | pub fn with_arc_route_provider(mut self, provider: Arc) -> Self { 119 | assert!( 120 | !self.config.background_dynamic_routing, 121 | "with_background_dynamic_routing cannot be called with with_route_provider" 122 | ); 123 | assert!( 124 | self.config.url.is_none(), 125 | "with_url cannot be called with with_route_provider" 126 | ); 127 | self.config.route_provider = Some(provider); 128 | self 129 | } 130 | 131 | /// Provide a pre-configured HTTP client to use. Use this to set e.g. HTTP timeouts or proxy configuration. 132 | pub fn with_http_client(mut self, client: reqwest::Client) -> Self { 133 | assert!( 134 | self.config.http_service.is_none(), 135 | "with_arc_http_middleware cannot be called with with_http_client" 136 | ); 137 | self.config.client = Some(client); 138 | self 139 | } 140 | 141 | /// Provide a custom `reqwest`-compatible HTTP service, e.g. to add per-request headers for custom boundary nodes. 142 | /// Most users will not need this and should use `with_http_client`. Cannot be called with `with_http_client`. 143 | /// 144 | /// The trait is automatically implemented for any `tower::Service` impl matching the one `reqwest::Client` uses, 145 | /// including `reqwest-middleware`. This is a low-level interface, and direct implementations must provide all automatic retry logic. 146 | pub fn with_arc_http_middleware(mut self, service: Arc) -> Self { 147 | assert!( 148 | self.config.client.is_none(), 149 | "with_arc_http_middleware cannot be called with with_http_client" 150 | ); 151 | self.config.http_service = Some(service); 152 | self 153 | } 154 | 155 | /// Retry up to the specified number of times upon encountering underlying TCP errors. 156 | pub fn with_max_tcp_error_retries(mut self, retries: usize) -> Self { 157 | self.config.max_tcp_error_retries = retries; 158 | self 159 | } 160 | 161 | /// Don't accept HTTP bodies any larger than `max_size` bytes. 162 | pub fn with_max_response_body_size(mut self, max_size: usize) -> Self { 163 | self.config.max_response_body_size = Some(max_size); 164 | self 165 | } 166 | /// Set the maximum time to wait for a response from the replica. 167 | pub fn with_max_polling_time(mut self, max_polling_time: std::time::Duration) -> Self { 168 | self.config.max_polling_time = max_polling_time; 169 | self 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /ic-agent/src/agent/http_transport/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module has been deprecated in favor of builder methods on `AgentBuilder`. 2 | 3 | #[deprecated(since = "0.38.0", note = "use the AgentBuilder methods")] 4 | #[doc(hidden)] 5 | pub mod reqwest_transport; 6 | #[doc(hidden)] 7 | #[allow(deprecated)] 8 | pub use reqwest_transport::ReqwestTransport; 9 | -------------------------------------------------------------------------------- /ic-agent/src/agent/http_transport/reqwest_transport.rs: -------------------------------------------------------------------------------- 1 | //! This module has been deprecated in favor of builder methods on `AgentBuilder`. 2 | #![allow(deprecated)] 3 | pub use reqwest; 4 | use std::sync::Arc; 5 | 6 | use reqwest::Client; 7 | 8 | use crate::{ 9 | agent::{ 10 | route_provider::{RoundRobinRouteProvider, RouteProvider}, 11 | AgentBuilder, 12 | }, 13 | AgentError, 14 | }; 15 | 16 | /// A legacy configuration object. `AgentBuilder::with_transport` will apply these settings to the builder. 17 | #[derive(Debug, Clone)] 18 | pub struct ReqwestTransport { 19 | route_provider: Arc, 20 | client: Client, 21 | max_response_body_size: Option, 22 | max_tcp_error_retries: usize, 23 | } 24 | 25 | impl ReqwestTransport { 26 | /// Equivalent to [`AgentBuilder::with_url`]. 27 | #[deprecated(since = "0.38.0", note = "Use AgentBuilder::with_url")] 28 | pub fn create>(url: U) -> Result { 29 | #[cfg(not(target_family = "wasm"))] 30 | { 31 | Self::create_with_client( 32 | url, 33 | Client::builder() 34 | .use_rustls_tls() 35 | .timeout(std::time::Duration::from_secs(360)) 36 | .build() 37 | .expect("Could not create HTTP client."), 38 | ) 39 | } 40 | #[cfg(all(target_family = "wasm", feature = "wasm-bindgen"))] 41 | { 42 | Self::create_with_client(url, Client::new()) 43 | } 44 | } 45 | 46 | /// Equivalent to [`AgentBuilder::with_url`] and [`AgentBuilder::with_http_client`]. 47 | #[deprecated( 48 | since = "0.38.0", 49 | note = "Use AgentBuilder::with_url and AgentBuilder::with_http_client" 50 | )] 51 | pub fn create_with_client>(url: U, client: Client) -> Result { 52 | let route_provider = Arc::new(RoundRobinRouteProvider::new(vec![url.into()])?); 53 | Self::create_with_client_route(route_provider, client) 54 | } 55 | 56 | /// Equivalent to [`AgentBuilder::with_http_client`] and [`AgentBuilder::with_route_provider`]. 57 | #[deprecated( 58 | since = "0.38.0", 59 | note = "Use AgentBuilder::with_http_client and AgentBuilder::with_arc_route_provider" 60 | )] 61 | pub fn create_with_client_route( 62 | route_provider: Arc, 63 | client: Client, 64 | ) -> Result { 65 | Ok(Self { 66 | route_provider, 67 | client, 68 | max_response_body_size: None, 69 | max_tcp_error_retries: 0, 70 | }) 71 | } 72 | 73 | /// Equivalent to [`AgentBuilder::with_max_response_body_size`]. 74 | #[deprecated( 75 | since = "0.38.0", 76 | note = "Use AgentBuilder::with_max_response_body_size" 77 | )] 78 | pub fn with_max_response_body_size(self, max_response_body_size: usize) -> Self { 79 | ReqwestTransport { 80 | max_response_body_size: Some(max_response_body_size), 81 | ..self 82 | } 83 | } 84 | 85 | /// Equivalent to [`AgentBuilder::with_max_tcp_error_retries`]. 86 | #[deprecated( 87 | since = "0.38.0", 88 | note = "Use AgentBuilder::with_max_tcp_error_retries" 89 | )] 90 | pub fn with_max_tcp_errors_retries(self, retries: usize) -> Self { 91 | ReqwestTransport { 92 | max_tcp_error_retries: retries, 93 | ..self 94 | } 95 | } 96 | } 97 | 98 | impl AgentBuilder { 99 | #[doc(hidden)] 100 | #[deprecated(since = "0.38.0", note = "Use the dedicated methods on AgentBuilder")] 101 | pub fn with_transport(self, transport: ReqwestTransport) -> Self { 102 | let mut builder = self 103 | .with_arc_route_provider(transport.route_provider) 104 | .with_http_client(transport.client) 105 | .with_max_tcp_error_retries(transport.max_tcp_error_retries); 106 | if let Some(max_size) = transport.max_response_body_size { 107 | builder = builder.with_max_response_body_size(max_size); 108 | } 109 | builder 110 | } 111 | #[doc(hidden)] 112 | #[deprecated(since = "0.38.0", note = "Use the dedicated methods on AgentBuilder")] 113 | pub fn with_arc_transport(self, transport: Arc) -> Self { 114 | self.with_transport((*transport).clone()) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /ic-agent/src/agent/nonce.rs: -------------------------------------------------------------------------------- 1 | use rand::{rngs::OsRng, Rng}; 2 | use std::sync::{ 3 | atomic::{AtomicU64, Ordering}, 4 | Arc, Mutex, 5 | }; 6 | 7 | /// A Factory for nonce blobs. 8 | #[derive(Clone)] 9 | pub struct NonceFactory { 10 | inner: Arc, 11 | } 12 | 13 | impl NonceFactory { 14 | /// Creates a nonce factory from an iterator over blobs. The iterator is not assumed to be fused. 15 | pub fn from_iterator(iter: Box> + Send>) -> Self { 16 | Self { 17 | inner: Arc::new(Iter::from(iter)), 18 | } 19 | } 20 | 21 | /// Creates a nonce factory that generates random blobs using `getrandom`. 22 | pub fn random() -> NonceFactory { 23 | Self { 24 | inner: Arc::new(RandomBlob {}), 25 | } 26 | } 27 | 28 | /// Creates a nonce factory that returns None every time. 29 | pub fn empty() -> NonceFactory { 30 | Self { 31 | inner: Arc::new(Empty), 32 | } 33 | } 34 | 35 | /// Creates a nonce factory that generates incrementing blobs. 36 | pub fn incrementing() -> NonceFactory { 37 | Self { 38 | inner: Arc::new(Incrementing::default()), 39 | } 40 | } 41 | 42 | /// Generates a nonce, if one is available. Otherwise, returns None. 43 | pub fn generate(&self) -> Option> { 44 | NonceGenerator::generate(self) 45 | } 46 | } 47 | 48 | impl NonceGenerator for NonceFactory { 49 | fn generate(&self) -> Option> { 50 | self.inner.generate() 51 | } 52 | } 53 | 54 | /// An interface for generating nonces. 55 | pub trait NonceGenerator: Send + Sync { 56 | /// Generates a nonce, if one is available. Otherwise, returns None. 57 | fn generate(&self) -> Option>; 58 | } 59 | 60 | pub struct Func(pub T); 61 | impl Option>> NonceGenerator for Func { 62 | fn generate(&self) -> Option> { 63 | (self.0)() 64 | } 65 | } 66 | 67 | pub struct Iter(Mutex); 68 | impl>> From for Iter { 69 | fn from(val: T) -> Iter { 70 | Iter(Mutex::new(val)) 71 | } 72 | } 73 | impl>> NonceGenerator for Iter { 74 | fn generate(&self) -> Option> { 75 | self.0.lock().unwrap().next() 76 | } 77 | } 78 | 79 | #[derive(Default)] 80 | pub struct RandomBlob {} 81 | impl NonceGenerator for RandomBlob { 82 | fn generate(&self) -> Option> { 83 | Some(OsRng.gen::<[u8; 16]>().to_vec()) 84 | } 85 | } 86 | 87 | #[derive(Default)] 88 | pub struct Empty; 89 | impl NonceGenerator for Empty { 90 | fn generate(&self) -> Option> { 91 | None 92 | } 93 | } 94 | 95 | #[derive(Default)] 96 | pub struct Incrementing { 97 | next: AtomicU64, 98 | } 99 | impl From for Incrementing { 100 | fn from(val: u64) -> Incrementing { 101 | Incrementing { 102 | next: AtomicU64::new(val), 103 | } 104 | } 105 | } 106 | impl NonceGenerator for Incrementing { 107 | fn generate(&self) -> Option> { 108 | let val = self.next.fetch_add(1, Ordering::Relaxed); 109 | Some(val.to_le_bytes().to_vec()) 110 | } 111 | } 112 | 113 | impl NonceGenerator for Box { 114 | fn generate(&self) -> Option> { 115 | (**self).generate() 116 | } 117 | } 118 | impl NonceGenerator for Arc { 119 | fn generate(&self) -> Option> { 120 | (**self).generate() 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ic-agent/src/agent/route_provider/dynamic_routing/messages.rs: -------------------------------------------------------------------------------- 1 | use crate::agent::route_provider::dynamic_routing::{health_check::HealthCheckStatus, node::Node}; 2 | 3 | /// Represents a message with fetched nodes. 4 | #[derive(Debug, Clone)] 5 | pub struct FetchedNodes { 6 | /// The fetched nodes. 7 | pub nodes: Vec, 8 | } 9 | 10 | /// Represents a message with the health state of a node. 11 | pub struct NodeHealthState { 12 | /// The node. 13 | pub node: Node, 14 | /// The health state of the node. 15 | pub health: HealthCheckStatus, 16 | } 17 | -------------------------------------------------------------------------------- /ic-agent/src/agent/route_provider/dynamic_routing/mod.rs: -------------------------------------------------------------------------------- 1 | //! A dynamic routing provider for the Internet Computer (IC) Agent that enables resilient, adaptive request routing through API boundary nodes. 2 | //! 3 | //! The `DynamicRouteProvider` is an implementation of the [`RouteProvider`](super::RouteProvider) trait. It dynamically discovers and monitors API boundary nodes, filters out unhealthy nodes, and routes API calls across healthy nodes using configurable strategies such as round-robin or latency-based routing. 4 | //! This ensures robust and performant interactions with the IC network by adapting to changes in node availability and topology. 5 | //! 6 | //! # Overview 7 | //! The IC Agent is capable of dispatching API calls to destination endpoints exposing an [HTTPS interface](https://internetcomputer.org/docs/references/ic-interface-spec#http-interface). These endpoints can be: 8 | //! 1. **Replica nodes**: part of the ICP. 9 | //! 2. **API boundary nodes**: part of the ICP. 10 | //! 3. **HTTP Gateways**: Third-party services that proxy requests to API boundary nodes, e.g., gateways hosted on the `ic0.app` domain. 11 | //! 12 | //! The Agent uses the [`RouteProvider`](super::RouteProvider) trait, namely its [`route()`](super::RouteProvider::route()) method to determine the destination endpoint for each call. 13 | //! For example this trait is implemented for [`Url`](https://docs.rs/url/latest/url/) and [`RoundRobinRouteProvider`](super::RoundRobinRouteProvider). 14 | //! The `DynamicRouteProvider` is a more complex implementation, which is intended to be used only for option (2), it provides: 15 | //! - **Automatic API Node Discovery**: periodically fetches the latest API boundary node topology. 16 | //! - **Health Monitoring**: Continuously checks health of all nodes in the topology. 17 | //! - **Flexible Routing**: Directs requests to healthy nodes using built-in or custom strategies: 18 | //! - [`RoundRobinRoutingSnapshot`](snapshot::round_robin_routing::RoundRobinRoutingSnapshot): Evenly distributes requests across healthy nodes. 19 | //! - [`LatencyRoutingSnapshot`](snapshot::latency_based_routing::LatencyRoutingSnapshot): Prioritizes low-latency nodes via weighted round-robin, with optional penalties if nodes are unavailable within a sliding time window. 20 | //! - **Customizability**: Supports custom node fetchers, health checkers, and routing logic. 21 | //! # Usage 22 | //! The `DynamicRouteProvider` can be used standalone or injected into the agent to enable dynamic routing. There are several ways to instantiate it: 23 | //! 1. **Via high-Level Agent API**: Initializes the agent with built-in dynamic routing. This method is user-friendly but provides limited customization options. 24 | //! 2. **Via [`DynamicRouteProviderBuilder`](dynamic_route_provider::DynamicRouteProviderBuilder)**: Creates a customized `DynamicRouteProvider` with a specific routing strategy and parameters. 25 | //! This instance can be used standalone or integrated into the agent via [`AgentBuilder::with_route_provider()`](super::super::AgentBuilder::with_route_provider). 26 | //! ## Example: High-Level Agent API 27 | //! ```rust 28 | //! use anyhow::Result; 29 | //! use ic_agent::Agent; 30 | //! use url::Url; 31 | //! 32 | //! #[tokio::main] 33 | //! async fn main() -> Result<()> { 34 | //! // Use the URL of an IC HTTP Gateway or even better - API boundary node as the initial seed 35 | //! let seed_url = Url::parse("https://ic0.app")?; 36 | //! 37 | //! // The agent starts with the seed node and discovers healthy API nodes dynamically 38 | //! // Until then, requests go through the seed, but only if it's healthy. 39 | //! let agent = Agent::builder() 40 | //! .with_url(seed_url) 41 | //! .with_background_dynamic_routing() 42 | //! .build()?; 43 | //! 44 | //! // ... use the agent for API calls 45 | //! 46 | //! Ok(()) 47 | //! } 48 | //! ``` 49 | //! **Note**: In the example above, `ic0.app` is used as a seed for initial topology discovery. However, it is not a true seed, as it is not an API boundary node in the ICP topology. 50 | //! It will be discarded after the first successful discovery. 51 | //! ## Example: Customized instantiation 52 | //! ```rust 53 | //! use std::{sync::Arc, time::Duration}; 54 | //! 55 | //! use anyhow::Result; 56 | //! use ic_agent::{ 57 | //! agent::route_provider::{ 58 | //! dynamic_routing::{ 59 | //! dynamic_route_provider::{DynamicRouteProvider, DynamicRouteProviderBuilder}, 60 | //! node::Node, 61 | //! snapshot::latency_based_routing::LatencyRoutingSnapshot, 62 | //! }, 63 | //! RouteProvider, 64 | //! }, 65 | //! Agent, 66 | //! }; 67 | //! use reqwest::Client; 68 | //! 69 | //! #[tokio::main] 70 | //! async fn main() -> Result<()> { 71 | //! // Choose a routing strategy: top 3 lowest-latency API boundary nodes selected via weighted round-robin 72 | //! let routing_strategy = LatencyRoutingSnapshot::new().set_k_top_nodes(3); 73 | //! 74 | //! // Alternatively, use a basic round-robin routing across all healthy API boundary nodes 75 | //! // let routing_strategy = RoundRobinRoutingSnapshot::new(); 76 | //! 77 | //! // Or implement and provide your own custom routing strategy 78 | //! 79 | //! // Seed nodes for initial topology discovery 80 | //! let seed_nodes = vec![ 81 | //! Node::new("ic0.app")?, 82 | //! // Optional: add known API boundary nodes to improve resilience 83 | //! // Node::new("")?, 84 | //! ]; 85 | //! 86 | //! // HTTP client for health checks and topology discovery 87 | //! let client = Client::builder().build()?; 88 | //! 89 | //! // Build dynamic route provider 90 | //! let route_provider: DynamicRouteProvider = 91 | //! DynamicRouteProviderBuilder::new(routing_strategy, seed_nodes, Arc::new(client)) 92 | //! // Set how often to fetch the latest API boundary node topology 93 | //! .with_fetch_period(Duration::from_secs(10)) 94 | //! // Set how often to perform health checks on the API boundary nodes 95 | //! .with_check_period(Duration::from_secs(2)) 96 | //! // Or optionally provide a custom node health checker implementation 97 | //! // .with_checker(custom_checker) 98 | //! // Or optionally provide a custom topology fetcher implementation 99 | //! // .with_fetcher(custom_fetcher) 100 | //! .build() 101 | //! .await; 102 | //! 103 | //! // Example: generate routing URLs 104 | //! let url_1 = route_provider.route().expect("failed to get routing URL"); 105 | //! eprintln!("Generated URL: {url_1}"); 106 | //! 107 | //! let url_2 = route_provider.route().expect("failed to get routing URL"); 108 | //! eprintln!("Generated URL: {url_2}"); 109 | //! 110 | //! // Or inject route_provider into the agent for dynamic routing 111 | //! let agent = Agent::builder() 112 | //! .with_route_provider(route_provider) 113 | //! .build()?; 114 | //! 115 | //! // ... use the agent for API calls 116 | //! 117 | //! Ok(()) 118 | //! } 119 | //! ``` 120 | //! # Implementation Details 121 | //! The `DynamicRouteProvider` spawns two background services: 122 | //! 1. `NodesFetchActor`: Periodically fetches the latest API boundary node topology and sends updates to the `HealthManagerActor`. 123 | //! 2. `HealthManagerActor`: Manages health checks for nodes, starts and stops `HealthCheckActor`s and updates the routing table (routing snapshot) with health information. 124 | //! 125 | //! These background services ensure the routing table remains up-to-date. 126 | //! # Configuration 127 | //! The [`DynamicRouteProviderBuilder`](dynamic_route_provider::DynamicRouteProviderBuilder) allows customized instantiation of `DynamicRouteProvider`: 128 | //! - **Fetch Period**: How often to fetch node topology (default: 5 seconds). 129 | //! - **Health Check Period**: How often to check node health (default: 1 second). 130 | //! - **Nodes Fetcher**: Custom implementation of the [`Fetch`](nodes_fetch::Fetch) trait for node discovery. 131 | //! - **Health Checker**: Custom implementation of the [`HealthCheck`](health_check::HealthCheck) trait for health monitoring. 132 | //! - **Routing Strategy**: Custom implementation of the [`RoutingSnapshot`](snapshot::routing_snapshot::RoutingSnapshot) trait for routing logic. 133 | //! Two built-in strategies are available: [`LatencyRoutingSnapshot`](snapshot::latency_based_routing::LatencyRoutingSnapshot) and [`RoundRobinRoutingSnapshot`](snapshot::round_robin_routing::RoundRobinRoutingSnapshot). 134 | //! 135 | //! # Error Handling 136 | //! Errors during node fetching or health checking are encapsulated in the [`DynamicRouteProviderError`](dynamic_route_provider::DynamicRouteProviderError) enum: 137 | //! - `NodesFetchError`: Occurs when fetching the topology fails. 138 | //! - `HealthCheckError`: Occurs when node health checks fail. 139 | //! These errors are not propagated to the caller. Instead, they are logged internally using the `tracing` crate. To capture these errors, configure a `tracing` subscriber in your application. 140 | //! If no healthy nodes are available, the [`route()`](super::RouteProvider::route()) method returns an [`AgentError::RouteProviderError`](super::super::agent_error::AgentError::RouteProviderError). 141 | //! # Testing 142 | //! The module includes comprehensive tests covering: 143 | //! - Mainnet integration with dynamic node discovery. 144 | //! - Routing behavior with topology and health updates. 145 | //! - Edge cases like initially unhealthy seeds, no healthy nodes, and empty topology fetches. 146 | //! 147 | //! These tests ensure the `DynamicRouteProvider` behaves correctly in various scenarios. 148 | pub mod dynamic_route_provider; 149 | /// Health check implementation. 150 | pub mod health_check; 151 | /// Messages used in dynamic routing. 152 | pub(super) mod messages; 153 | /// Node implementation. 154 | pub mod node; 155 | /// Nodes fetch implementation. 156 | pub mod nodes_fetch; 157 | /// Routing snapshot implementation. 158 | pub mod snapshot; 159 | #[cfg(test)] 160 | #[cfg_attr(target_family = "wasm", allow(unused))] 161 | pub(super) mod test_utils; 162 | /// Type aliases used in dynamic routing. 163 | pub(super) mod type_aliases; 164 | -------------------------------------------------------------------------------- /ic-agent/src/agent/route_provider/dynamic_routing/node.rs: -------------------------------------------------------------------------------- 1 | use url::Url; 2 | 3 | use crate::agent::ApiBoundaryNode; 4 | 5 | /// Represents a node in the dynamic routing. 6 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 7 | pub struct Node { 8 | domain: String, 9 | } 10 | 11 | impl Node { 12 | /// Creates a new `Node` instance from the domain name. 13 | pub fn new(domain: impl Into) -> Result { 14 | let domain = domain.into(); 15 | check_valid_domain(&domain)?; 16 | Ok(Self { domain }) 17 | } 18 | 19 | /// Returns the domain name of the node. 20 | pub fn domain(&self) -> &str { 21 | &self.domain 22 | } 23 | } 24 | 25 | impl Node { 26 | /// Converts the node to a routing URL. 27 | pub fn to_routing_url(&self) -> Url { 28 | Url::parse(&format!("https://{}", self.domain)).expect("failed to parse URL") 29 | } 30 | } 31 | 32 | impl From<&Node> for Url { 33 | fn from(node: &Node) -> Self { 34 | // Parsing can't fail, as the domain was checked at node instantiation. 35 | Url::parse(&format!("https://{}", node.domain)).expect("failed to parse URL") 36 | } 37 | } 38 | 39 | impl TryFrom for Node { 40 | type Error = url::ParseError; 41 | 42 | fn try_from(value: ApiBoundaryNode) -> Result { 43 | Node::new(value.domain) 44 | } 45 | } 46 | 47 | /// Checks if the given domain is a valid URL. 48 | fn check_valid_domain>(domain: S) -> Result<(), url::ParseError> { 49 | // Prepend scheme to make it a valid URL 50 | let url_string = format!("http://{}", domain.as_ref()); 51 | Url::parse(&url_string)?; 52 | Ok(()) 53 | } 54 | -------------------------------------------------------------------------------- /ic-agent/src/agent/route_provider/dynamic_routing/nodes_fetch.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use candid::Principal; 3 | use futures_util::FutureExt; 4 | use std::{fmt::Debug, sync::Arc, time::Duration}; 5 | use stop_token::StopToken; 6 | use url::Url; 7 | 8 | #[allow(unused)] 9 | use crate::agent::route_provider::dynamic_routing::health_check::HEALTH_MANAGER_ACTOR; 10 | use crate::agent::{ 11 | route_provider::dynamic_routing::{ 12 | dynamic_route_provider::DynamicRouteProviderError, 13 | messages::FetchedNodes, 14 | node::Node, 15 | snapshot::routing_snapshot::RoutingSnapshot, 16 | type_aliases::{AtomicSwap, SenderWatch}, 17 | }, 18 | Agent, HttpService, 19 | }; 20 | #[allow(unused)] 21 | const NODES_FETCH_ACTOR: &str = "NodesFetchActor"; 22 | 23 | /// Fetcher of nodes in the topology. 24 | #[cfg_attr(target_family = "wasm", async_trait(?Send))] 25 | #[cfg_attr(not(target_family = "wasm"), async_trait)] 26 | pub trait Fetch: Sync + Send + Debug { 27 | /// Fetches the nodes from the topology. 28 | async fn fetch(&self, url: Url) -> Result, DynamicRouteProviderError>; 29 | } 30 | 31 | /// A struct representing the fetcher of the nodes from the topology. 32 | #[derive(Debug)] 33 | pub struct NodesFetcher { 34 | http_client: Arc, 35 | subnet_id: Principal, 36 | // By default, the nodes fetcher is configured to talk to the mainnet of Internet Computer, and verifies responses using a hard-coded public key. 37 | // However, for testnets one can set up a custom public key. 38 | root_key: Option>, 39 | } 40 | 41 | impl NodesFetcher { 42 | /// Creates a new `NodesFetcher` instance. 43 | pub fn new( 44 | http_client: Arc, 45 | subnet_id: Principal, 46 | root_key: Option>, 47 | ) -> Self { 48 | Self { 49 | http_client, 50 | subnet_id, 51 | root_key, 52 | } 53 | } 54 | } 55 | 56 | #[cfg_attr(target_family = "wasm", async_trait(?Send))] 57 | #[cfg_attr(not(target_family = "wasm"), async_trait)] 58 | impl Fetch for NodesFetcher { 59 | async fn fetch(&self, url: Url) -> Result, DynamicRouteProviderError> { 60 | let agent = Agent::builder() 61 | .with_url(url) 62 | .with_arc_http_middleware(self.http_client.clone()) 63 | .build() 64 | .map_err(|err| { 65 | DynamicRouteProviderError::NodesFetchError(format!( 66 | "Failed to build the agent: {err}" 67 | )) 68 | })?; 69 | if let Some(key) = self.root_key.clone() { 70 | agent.set_root_key(key); 71 | } 72 | let api_bns = agent 73 | .fetch_api_boundary_nodes_by_subnet_id(self.subnet_id) 74 | .await 75 | .map_err(|err| { 76 | DynamicRouteProviderError::NodesFetchError(format!( 77 | "Failed to fetch API nodes: {err}" 78 | )) 79 | })?; 80 | // If some API BNs have invalid domain names, they are discarded. 81 | let nodes = api_bns 82 | .into_iter() 83 | .filter_map(|api_node| api_node.try_into().ok()) 84 | .collect(); 85 | return Ok(nodes); 86 | } 87 | } 88 | 89 | /// A struct representing the actor responsible for fetching existing nodes and communicating it with the listener. 90 | pub(super) struct NodesFetchActor { 91 | /// The fetcher object responsible for fetching the nodes. 92 | fetcher: Arc, 93 | /// Time period between fetches. 94 | period: Duration, 95 | /// The interval to wait before retrying to fetch the nodes in case of failures. 96 | fetch_retry_interval: Duration, 97 | /// Communication channel with the listener. 98 | fetch_sender: SenderWatch, 99 | /// The snapshot of the routing table. 100 | routing_snapshot: AtomicSwap, 101 | /// The token to cancel/stop the actor. 102 | token: StopToken, 103 | } 104 | 105 | impl NodesFetchActor 106 | where 107 | S: RoutingSnapshot, 108 | { 109 | /// Creates a new `NodesFetchActor` instance. 110 | pub fn new( 111 | fetcher: Arc, 112 | period: Duration, 113 | retry_interval: Duration, 114 | fetch_sender: SenderWatch, 115 | snapshot: AtomicSwap, 116 | token: StopToken, 117 | ) -> Self { 118 | Self { 119 | fetcher, 120 | period, 121 | fetch_retry_interval: retry_interval, 122 | fetch_sender, 123 | routing_snapshot: snapshot, 124 | token, 125 | } 126 | } 127 | 128 | /// Runs the actor. 129 | pub async fn run(self) { 130 | loop { 131 | // Retry until success: 132 | // - try to get a healthy node from the routing snapshot 133 | // - if snapshot is empty, break the cycle and wait for the next fetch cycle 134 | // - using the healthy node, try to fetch nodes from topology 135 | // - if failure, sleep and retry 136 | // - try send fetched nodes to the listener 137 | // - failure should never happen, but we trace it if it does 138 | loop { 139 | let snapshot = self.routing_snapshot.load(); 140 | if let Some(node) = snapshot.next_node() { 141 | match self.fetcher.fetch((&node).into()).await { 142 | Ok(nodes) => { 143 | let msg = Some(FetchedNodes { nodes }); 144 | match self.fetch_sender.send(msg) { 145 | Ok(()) => break, // message sent successfully, exist the loop 146 | Err(_err) => { 147 | log!(error, "{NODES_FETCH_ACTOR}: failed to send results to {HEALTH_MANAGER_ACTOR}: {_err:?}"); 148 | } 149 | } 150 | } 151 | Err(_err) => { 152 | log!( 153 | error, 154 | "{NODES_FETCH_ACTOR}: failed to fetch nodes: {_err:?}" 155 | ); 156 | } 157 | }; 158 | } else { 159 | // No healthy nodes in the snapshot, break the cycle and wait for the next fetch cycle 160 | log!(error, "{NODES_FETCH_ACTOR}: no nodes in the snapshot"); 161 | break; 162 | }; 163 | log!( 164 | warn, 165 | "Retrying to fetch the nodes in {:?}", 166 | self.fetch_retry_interval 167 | ); 168 | crate::util::sleep(self.fetch_retry_interval).await; 169 | } 170 | futures_util::select! { 171 | _ = crate::util::sleep(self.period).fuse() => { 172 | continue; 173 | } 174 | _ = self.token.clone().fuse() => { 175 | log!(warn, "{NODES_FETCH_ACTOR}: was gracefully cancelled"); 176 | break; 177 | } 178 | } 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /ic-agent/src/agent/route_provider/dynamic_routing/snapshot/mod.rs: -------------------------------------------------------------------------------- 1 | /// Snapshot of the routing table. 2 | pub mod latency_based_routing; 3 | /// Node implementation. 4 | pub mod round_robin_routing; 5 | /// Routing snapshot implementation. 6 | pub mod routing_snapshot; 7 | -------------------------------------------------------------------------------- /ic-agent/src/agent/route_provider/dynamic_routing/snapshot/routing_snapshot.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use crate::agent::route_provider::{ 4 | dynamic_routing::{health_check::HealthCheckStatus, node::Node}, 5 | RoutesStats, 6 | }; 7 | 8 | /// A trait for interacting with the snapshot of nodes (routing table). 9 | pub trait RoutingSnapshot: Send + Sync + Clone + Debug { 10 | /// Returns `true` if the snapshot has nodes. 11 | #[allow(unused)] 12 | fn has_nodes(&self) -> bool; 13 | /// Get next node from the snapshot. 14 | fn next_node(&self) -> Option; 15 | /// Get up to n different nodes from the snapshot. 16 | fn next_n_nodes(&self, n: usize) -> Vec; 17 | /// Syncs the nodes in the snapshot with the provided list of nodes, returning `true` if the snapshot was updated. 18 | fn sync_nodes(&mut self, nodes: &[Node]) -> bool; 19 | /// Updates the health status of a specific node, returning `true` if the node was found and updated. 20 | fn update_node(&mut self, node: &Node, health: HealthCheckStatus) -> bool; 21 | /// Returns statistics about the routes (nodes). 22 | fn routes_stats(&self) -> RoutesStats; 23 | } 24 | -------------------------------------------------------------------------------- /ic-agent/src/agent/route_provider/dynamic_routing/test_utils.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::time::Duration; 3 | use std::{fmt::Debug, hash::Hash, sync::Arc}; 4 | 5 | use arc_swap::ArcSwap; 6 | use async_trait::async_trait; 7 | use url::Url; 8 | 9 | use crate::agent::route_provider::{ 10 | dynamic_routing::{ 11 | dynamic_route_provider::DynamicRouteProviderError, 12 | health_check::{HealthCheck, HealthCheckStatus}, 13 | node::Node, 14 | nodes_fetch::Fetch, 15 | type_aliases::AtomicSwap, 16 | }, 17 | RouteProvider, 18 | }; 19 | 20 | pub(super) fn route_n_times(n: usize, f: Arc) -> Vec { 21 | (0..n) 22 | .map(|_| f.route().unwrap().domain().unwrap().to_string()) 23 | .collect() 24 | } 25 | 26 | pub(super) fn assert_routed_domains( 27 | actual: Vec, 28 | expected: Vec<&str>, 29 | expected_repetitions: usize, 30 | ) where 31 | T: AsRef + Eq + Hash + Debug + Ord, 32 | { 33 | fn build_count_map(items: &[T]) -> HashMap<&str, usize> 34 | where 35 | T: AsRef, 36 | { 37 | items.iter().fold(HashMap::new(), |mut map, item| { 38 | *map.entry(item.as_ref()).or_insert(0) += 1; 39 | map 40 | }) 41 | } 42 | let count_actual = build_count_map(&actual); 43 | let count_expected = build_count_map(&expected); 44 | 45 | let mut keys_actual = count_actual.keys().collect::>(); 46 | keys_actual.sort(); 47 | let mut keys_expected = count_expected.keys().collect::>(); 48 | keys_expected.sort(); 49 | // Assert all routed domains are present. 50 | assert_eq!(keys_actual, keys_expected); 51 | 52 | // Assert the expected repetition count of each routed domain. 53 | let actual_repetitions = count_actual.values().collect::>(); 54 | assert!(actual_repetitions 55 | .iter() 56 | .all(|&x| x == &expected_repetitions)); 57 | } 58 | 59 | #[derive(Debug)] 60 | pub(super) struct NodesFetcherMock { 61 | // A set of nodes, existing in the topology. 62 | pub nodes: AtomicSwap>, 63 | } 64 | 65 | #[cfg_attr(target_family = "wasm", async_trait(?Send))] 66 | #[cfg_attr(not(target_family = "wasm"), async_trait)] 67 | impl Fetch for NodesFetcherMock { 68 | async fn fetch(&self, _url: Url) -> Result, DynamicRouteProviderError> { 69 | let nodes = (*self.nodes.load_full()).clone(); 70 | Ok(nodes) 71 | } 72 | } 73 | 74 | impl Default for NodesFetcherMock { 75 | fn default() -> Self { 76 | Self::new() 77 | } 78 | } 79 | 80 | impl NodesFetcherMock { 81 | pub fn new() -> Self { 82 | Self { 83 | nodes: Arc::new(ArcSwap::from_pointee(vec![])), 84 | } 85 | } 86 | 87 | pub fn overwrite_nodes(&self, nodes: Vec) { 88 | self.nodes.store(Arc::new(nodes)); 89 | } 90 | } 91 | 92 | #[derive(Debug)] 93 | pub(super) struct NodeHealthCheckerMock { 94 | healthy_nodes: Arc>>, 95 | } 96 | 97 | impl Default for NodeHealthCheckerMock { 98 | fn default() -> Self { 99 | Self::new() 100 | } 101 | } 102 | 103 | #[cfg_attr(target_family = "wasm", async_trait(?Send))] 104 | #[cfg_attr(not(target_family = "wasm"), async_trait)] 105 | impl HealthCheck for NodeHealthCheckerMock { 106 | async fn check(&self, node: &Node) -> Result { 107 | let nodes = self.healthy_nodes.load_full(); 108 | let latency = match nodes.contains(node) { 109 | true => Some(Duration::from_secs(1)), 110 | false => None, 111 | }; 112 | Ok(HealthCheckStatus::new(latency)) 113 | } 114 | } 115 | 116 | impl NodeHealthCheckerMock { 117 | pub fn new() -> Self { 118 | Self { 119 | healthy_nodes: Arc::new(ArcSwap::from_pointee(HashSet::new())), 120 | } 121 | } 122 | 123 | pub fn overwrite_healthy_nodes(&self, healthy_nodes: Vec) { 124 | self.healthy_nodes 125 | .store(Arc::new(HashSet::from_iter(healthy_nodes))); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /ic-agent/src/agent/route_provider/dynamic_routing/type_aliases.rs: -------------------------------------------------------------------------------- 1 | use arc_swap::ArcSwap; 2 | use std::sync::Arc; 3 | 4 | /// A type alias for the sender end of a watch channel. 5 | pub(super) type SenderWatch = async_watch::Sender>; 6 | 7 | /// A type alias for the receiver end of a watch channel. 8 | pub(super) type ReceiverWatch = async_watch::Receiver>; 9 | 10 | /// A type alias for the sender end of a multi-producer, single-consumer channel. 11 | pub(super) type SenderMpsc = async_channel::Sender; 12 | 13 | /// A type alias for the receiver end of a multi-producer, single-consumer channel. 14 | pub(super) type ReceiverMpsc = async_channel::Receiver; 15 | 16 | /// A type alias for an atomic swap operation on a shared value. 17 | pub(super) type AtomicSwap = Arc>; 18 | -------------------------------------------------------------------------------- /ic-agent/src/agent/status.rs: -------------------------------------------------------------------------------- 1 | //! Types for interacting with the status endpoint of a replica. See [`Status`] for details. 2 | 3 | use std::{collections::BTreeMap, fmt::Debug}; 4 | 5 | /// Value returned by the status endpoint of a replica. This is a loose mapping to CBOR values. 6 | /// Because the agent should not return [`serde_cbor::Value`] directly across API boundaries, 7 | /// we reimplement it as [`Value`] here. 8 | #[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, Hash)] 9 | pub enum Value { 10 | /// See [`Null`](serde_cbor::Value::Null). 11 | Null, 12 | /// See [`String`](serde_cbor::Value::Text). 13 | String(String), 14 | /// See [`Integer`](serde_cbor::Value::Integer). 15 | Integer(i64), 16 | /// See [`Bool`](serde_cbor::Value::Bool). 17 | Bool(bool), 18 | /// See [`Bytes`](serde_cbor::Value::Bytes). 19 | Bytes(Vec), 20 | /// See [`Vec`](serde_cbor::Value::Array). 21 | Vec(Vec), 22 | /// See [`Map`](serde_cbor::Value::Map). 23 | Map(BTreeMap>), 24 | } 25 | 26 | impl std::fmt::Display for Value { 27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 28 | match self { 29 | Value::Null => f.write_str("null"), 30 | Value::String(s) => f.write_fmt(format_args!(r#""{}""#, s.escape_debug())), 31 | Value::Integer(i) => f.write_str(&i.to_string()), 32 | Value::Bool(true) => f.write_str("true"), 33 | Value::Bool(false) => f.write_str("false"), 34 | Value::Bytes(b) => f.debug_list().entries(b).finish(), 35 | Value::Vec(v) => f.debug_list().entries(v).finish(), 36 | Value::Map(m) => f.debug_map().entries(m).finish(), 37 | } 38 | } 39 | } 40 | 41 | /// The structure returned by [`super::Agent::status`], containing the information returned 42 | /// by the status endpoint of a replica. 43 | #[derive(Debug, Ord, PartialOrd, PartialEq, Eq)] 44 | pub struct Status { 45 | /// Optional. The precise git revision of the Internet Computer Protocol implementation. 46 | pub impl_version: Option, 47 | 48 | /// Optional. The health status of the replica. One hopes it's "healthy". 49 | pub replica_health_status: Option, 50 | 51 | /// Optional. The root (public) key used to verify certificates. 52 | pub root_key: Option>, 53 | 54 | /// Contains any additional values that the replica gave as status. 55 | pub values: BTreeMap>, 56 | } 57 | 58 | impl std::fmt::Display for Status { 59 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 60 | f.write_str("{\n")?; 61 | let mut first = true; 62 | for (key, value) in &self.values { 63 | if first { 64 | first = false; 65 | } else { 66 | f.write_str(",\n")?; 67 | } 68 | f.write_fmt(format_args!(r#" "{}": "#, key.escape_debug()))?; 69 | std::fmt::Display::fmt(&value, f)?; 70 | } 71 | f.write_str("\n}") 72 | } 73 | } 74 | 75 | fn cbor_value_to_value(value: &serde_cbor::Value) -> Result { 76 | match value { 77 | serde_cbor::Value::Null => Ok(Value::Null), 78 | serde_cbor::Value::Bool(b) => Ok(Value::Bool(*b)), 79 | serde_cbor::Value::Integer(i) => Ok(Value::Integer(*i as i64)), 80 | serde_cbor::Value::Bytes(b) => Ok(Value::Bytes(b.to_owned())), 81 | serde_cbor::Value::Text(s) => Ok(Value::String(s.to_owned())), 82 | serde_cbor::Value::Array(a) => Ok(Value::Vec( 83 | a.iter() 84 | .map(cbor_value_to_value) 85 | .collect::, ()>>() 86 | .map_err(|_| ())?, 87 | )), 88 | serde_cbor::Value::Map(m) => { 89 | let mut map = BTreeMap::new(); 90 | for (key, value) in m { 91 | let k = match key { 92 | serde_cbor::Value::Text(t) => t.to_owned(), 93 | serde_cbor::Value::Integer(i) => i.to_string(), 94 | _ => return Err(()), 95 | }; 96 | let v = Box::new(cbor_value_to_value(value)?); 97 | 98 | map.insert(k, v); 99 | } 100 | Ok(Value::Map(map)) 101 | } 102 | serde_cbor::Value::Tag(_, v) => cbor_value_to_value(v.as_ref()), 103 | _ => Err(()), 104 | } 105 | } 106 | 107 | impl std::convert::TryFrom<&serde_cbor::Value> for Status { 108 | type Error = (); 109 | 110 | fn try_from(value: &serde_cbor::Value) -> Result { 111 | let v = cbor_value_to_value(value)?; 112 | 113 | match v { 114 | Value::Map(map) => { 115 | let impl_version: Option = map.get("impl_version").and_then(|v| { 116 | if let Value::String(s) = v.as_ref() { 117 | Some(s.to_owned()) 118 | } else { 119 | None 120 | } 121 | }); 122 | let replica_health_status: Option = 123 | map.get("replica_health_status").and_then(|v| { 124 | if let Value::String(s) = v.as_ref() { 125 | Some(s.to_owned()) 126 | } else { 127 | None 128 | } 129 | }); 130 | let root_key: Option> = map.get("root_key").and_then(|v| { 131 | if let Value::Bytes(bytes) = v.as_ref() { 132 | Some(bytes.to_owned()) 133 | } else { 134 | None 135 | } 136 | }); 137 | 138 | Ok(Status { 139 | impl_version, 140 | replica_health_status, 141 | root_key, 142 | values: map, 143 | }) 144 | } 145 | _ => Err(()), 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /ic-agent/src/export.rs: -------------------------------------------------------------------------------- 1 | //! A module to re-export types that are visible through the ic-agent API. 2 | #[doc(inline)] 3 | pub use candid::types::principal::{Principal, PrincipalError}; 4 | pub use reqwest; 5 | -------------------------------------------------------------------------------- /ic-agent/src/identity/anonymous.rs: -------------------------------------------------------------------------------- 1 | use crate::{agent::EnvelopeContent, export::Principal, identity::Identity, Signature}; 2 | 3 | /// The anonymous identity. 4 | /// 5 | /// The caller will be represented as [`Principal::anonymous`], or `2vxsx-fae`. 6 | #[derive(Debug, Copy, Clone)] 7 | pub struct AnonymousIdentity; 8 | 9 | impl Identity for AnonymousIdentity { 10 | fn sender(&self) -> Result { 11 | Ok(Principal::anonymous()) 12 | } 13 | 14 | fn public_key(&self) -> Option> { 15 | None 16 | } 17 | 18 | fn sign(&self, _: &EnvelopeContent) -> Result { 19 | Ok(Signature { 20 | signature: None, 21 | public_key: None, 22 | delegations: None, 23 | }) 24 | } 25 | 26 | fn sign_arbitrary(&self, _: &[u8]) -> Result { 27 | Ok(Signature { 28 | public_key: None, 29 | signature: None, 30 | delegations: None, 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ic-agent/src/identity/basic.rs: -------------------------------------------------------------------------------- 1 | use crate::{agent::EnvelopeContent, export::Principal, Identity, Signature}; 2 | 3 | #[cfg(feature = "pem")] 4 | use crate::identity::error::PemError; 5 | 6 | use ed25519_consensus::SigningKey; 7 | use simple_asn1::{ 8 | oid, to_der, 9 | ASN1Block::{BitString, ObjectIdentifier, Sequence}, 10 | }; 11 | 12 | use std::fmt; 13 | 14 | use super::Delegation; 15 | 16 | /// A cryptographic identity which signs using an Ed25519 key pair. 17 | /// 18 | /// The caller will be represented via [`Principal::self_authenticating`], which contains the SHA-224 hash of the public key. 19 | pub struct BasicIdentity { 20 | private_key: KeyCompat, 21 | der_encoded_public_key: Vec, 22 | } 23 | 24 | impl fmt::Debug for BasicIdentity { 25 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 26 | f.debug_struct("BasicIdentity") 27 | .field("der_encoded_public_key", &self.der_encoded_public_key) 28 | .finish_non_exhaustive() 29 | } 30 | } 31 | 32 | impl BasicIdentity { 33 | /// Create a `BasicIdentity` from reading a PEM file at the path. 34 | #[cfg(feature = "pem")] 35 | pub fn from_pem_file>(file_path: P) -> Result { 36 | Self::from_pem(std::fs::File::open(file_path)?) 37 | } 38 | 39 | /// Create a `BasicIdentity` from reading a PEM File from a Reader. 40 | #[cfg(feature = "pem")] 41 | pub fn from_pem(pem_reader: R) -> Result { 42 | use der::{asn1::OctetString, Decode, ErrorKind, SliceReader, Tag, TagNumber}; 43 | use pkcs8::PrivateKeyInfo; 44 | 45 | let bytes: Vec = pem_reader.bytes().collect::>()?; 46 | let pem = pem::parse(bytes)?; 47 | let pki_res = PrivateKeyInfo::decode(&mut SliceReader::new(pem.contents())?); 48 | let mut truncated; 49 | let pki = match pki_res { 50 | Ok(pki) => pki, 51 | Err(e) => { 52 | if e.kind() 53 | == (ErrorKind::Noncanonical { 54 | tag: Tag::ContextSpecific { 55 | constructed: true, 56 | number: TagNumber::new(1), 57 | }, 58 | }) 59 | { 60 | // Very old versions of dfx generated nonconforming containers. They can only be imported if the extra data is removed. 61 | truncated = pem.into_contents(); 62 | if truncated[48..52] != *b"\xA1\x23\x03\x21" { 63 | return Err(e.into()); 64 | } 65 | // hatchet surgery 66 | truncated.truncate(48); 67 | truncated[1] = 46; 68 | truncated[4] = 0; 69 | PrivateKeyInfo::decode(&mut SliceReader::new(&truncated)?).map_err(|_| e)? 70 | } else { 71 | return Err(e.into()); 72 | } 73 | } 74 | }; 75 | let decoded_key = OctetString::from_der(pki.private_key)?; // ed25519 uses an octet string within another octet string 76 | let private_key = SigningKey::try_from(decoded_key.as_bytes())?; 77 | Ok(BasicIdentity::from_signing_key(private_key)) 78 | } 79 | 80 | /// Create a `BasicIdentity` from a `SigningKey` from `ed25519-consensus`. 81 | pub fn from_signing_key(key: SigningKey) -> Self { 82 | let public_key = key.verification_key(); 83 | let der_encoded_public_key = der_encode_public_key(public_key.as_bytes().to_vec()); 84 | 85 | Self { 86 | private_key: KeyCompat::Standard(key), 87 | der_encoded_public_key, 88 | } 89 | } 90 | 91 | /// Create a `BasicIdentity` from an `Ed25519KeyPair` from `ring`. 92 | #[cfg(feature = "ring")] 93 | pub fn from_key_pair(key_pair: ring::signature::Ed25519KeyPair) -> Self { 94 | use ring::signature::KeyPair; 95 | let der_encoded_public_key = der_encode_public_key(key_pair.public_key().as_ref().to_vec()); 96 | Self { 97 | private_key: KeyCompat::Ring(key_pair), 98 | der_encoded_public_key, 99 | } 100 | } 101 | } 102 | 103 | enum KeyCompat { 104 | Standard(SigningKey), 105 | #[cfg(feature = "ring")] 106 | Ring(ring::signature::Ed25519KeyPair), 107 | } 108 | 109 | impl KeyCompat { 110 | fn sign(&self, payload: &[u8]) -> Vec { 111 | match self { 112 | Self::Standard(k) => k.sign(payload).to_bytes().to_vec(), 113 | #[cfg(feature = "ring")] 114 | Self::Ring(k) => k.sign(payload).as_ref().to_vec(), 115 | } 116 | } 117 | } 118 | 119 | impl Identity for BasicIdentity { 120 | fn sender(&self) -> Result { 121 | Ok(Principal::self_authenticating(&self.der_encoded_public_key)) 122 | } 123 | 124 | fn public_key(&self) -> Option> { 125 | Some(self.der_encoded_public_key.clone()) 126 | } 127 | 128 | fn sign(&self, content: &EnvelopeContent) -> Result { 129 | self.sign_arbitrary(&content.to_request_id().signable()) 130 | } 131 | 132 | fn sign_delegation(&self, content: &Delegation) -> Result { 133 | self.sign_arbitrary(&content.signable()) 134 | } 135 | 136 | fn sign_arbitrary(&self, content: &[u8]) -> Result { 137 | let signature = self.private_key.sign(content); 138 | Ok(Signature { 139 | signature: Some(signature), 140 | public_key: self.public_key(), 141 | delegations: None, 142 | }) 143 | } 144 | } 145 | 146 | fn der_encode_public_key(public_key: Vec) -> Vec { 147 | // see Section 4 "SubjectPublicKeyInfo" in https://tools.ietf.org/html/rfc8410 148 | 149 | let id_ed25519 = oid!(1, 3, 101, 112); 150 | let algorithm = Sequence(0, vec![ObjectIdentifier(0, id_ed25519)]); 151 | let subject_public_key = BitString(0, public_key.len() * 8, public_key); 152 | let subject_public_key_info = Sequence(0, vec![algorithm, subject_public_key]); 153 | to_der(&subject_public_key_info).unwrap() 154 | } 155 | -------------------------------------------------------------------------------- /ic-agent/src/identity/delegated.rs: -------------------------------------------------------------------------------- 1 | use candid::Principal; 2 | use der::{Decode, SliceReader}; 3 | use ecdsa::signature::Verifier; 4 | use k256::Secp256k1; 5 | use p256::NistP256; 6 | use pkcs8::{spki::SubjectPublicKeyInfoRef, AssociatedOid, ObjectIdentifier}; 7 | use sec1::{EcParameters, EncodedPoint}; 8 | 9 | use crate::{agent::EnvelopeContent, Signature}; 10 | 11 | use super::{error::DelegationError, Delegation, Identity, SignedDelegation}; 12 | 13 | /// An identity that has been delegated the authority to authenticate as a different principal. 14 | pub struct DelegatedIdentity { 15 | to: Box, 16 | chain: Vec, 17 | from_key: Vec, 18 | } 19 | 20 | impl DelegatedIdentity { 21 | /// Creates a delegated identity that signs using `to`, for the principal corresponding to the public key `from_key`. 22 | /// 23 | /// `chain` must be a list of delegations connecting `from_key` to `to.public_key()`, and in that order; 24 | /// otherwise, this function will return an error. 25 | pub fn new( 26 | from_key: Vec, 27 | to: Box, 28 | chain: Vec, 29 | ) -> Result { 30 | let mut last_verified = &from_key; 31 | for delegation in &chain { 32 | let spki = SubjectPublicKeyInfoRef::decode( 33 | &mut SliceReader::new(&last_verified[..]).map_err(|_| DelegationError::Parse)?, 34 | ) 35 | .map_err(|_| DelegationError::Parse)?; 36 | if spki.algorithm.oid == elliptic_curve::ALGORITHM_OID { 37 | let Some(params) = spki.algorithm.parameters else { 38 | return Err(DelegationError::UnknownAlgorithm); 39 | }; 40 | let params = params 41 | .decode_as::() 42 | .map_err(|_| DelegationError::Parse)?; 43 | let curve = params 44 | .named_curve() 45 | .ok_or(DelegationError::UnknownAlgorithm)?; 46 | if curve == Secp256k1::OID { 47 | let pt = EncodedPoint::from_bytes(spki.subject_public_key.raw_bytes()) 48 | .map_err(|_| DelegationError::Parse)?; 49 | let vk = k256::ecdsa::VerifyingKey::from_encoded_point(&pt) 50 | .map_err(|_| DelegationError::Parse)?; 51 | let sig = k256::ecdsa::Signature::try_from(&delegation.signature[..]) 52 | .map_err(|_| DelegationError::Parse)?; 53 | vk.verify(&delegation.delegation.signable(), &sig) 54 | .map_err(|_| DelegationError::BrokenChain { 55 | from: last_verified.clone(), 56 | to: Some(delegation.delegation.clone()), 57 | })?; 58 | } else if curve == NistP256::OID { 59 | let pt = EncodedPoint::from_bytes(spki.subject_public_key.raw_bytes()) 60 | .map_err(|_| DelegationError::Parse)?; 61 | let vk = p256::ecdsa::VerifyingKey::from_encoded_point(&pt) 62 | .map_err(|_| DelegationError::Parse)?; 63 | let sig = p256::ecdsa::Signature::try_from(&delegation.signature[..]) 64 | .map_err(|_| DelegationError::Parse)?; 65 | vk.verify(&delegation.delegation.signable(), &sig) 66 | .map_err(|_| DelegationError::BrokenChain { 67 | from: last_verified.clone(), 68 | to: Some(delegation.delegation.clone()), 69 | })?; 70 | } else { 71 | return Err(DelegationError::UnknownAlgorithm); 72 | } 73 | } else if spki.algorithm.oid == ObjectIdentifier::new_unwrap("1.3.101.112") { 74 | let vk = ed25519_consensus::VerificationKey::try_from( 75 | spki.subject_public_key.raw_bytes(), 76 | ) 77 | .map_err(|_| DelegationError::Parse)?; 78 | let sig = ed25519_consensus::Signature::try_from(&delegation.signature[..]) 79 | .map_err(|_| DelegationError::Parse)?; 80 | vk.verify(&sig, &delegation.delegation.signable()) 81 | .map_err(|_| DelegationError::BrokenChain { 82 | from: last_verified.clone(), 83 | to: Some(delegation.delegation.clone()), 84 | })?; 85 | } else { 86 | return Err(DelegationError::UnknownAlgorithm); 87 | } 88 | last_verified = &delegation.delegation.pubkey; 89 | } 90 | let delegated_principal = Principal::self_authenticating(last_verified); 91 | if delegated_principal != to.sender().map_err(DelegationError::IdentityError)? { 92 | return Err(DelegationError::BrokenChain { 93 | from: last_verified.clone(), 94 | to: None, 95 | }); 96 | } 97 | 98 | Ok(Self::new_unchecked(from_key, to, chain)) 99 | } 100 | 101 | /// Creates a delegated identity that signs using `to`, for the principal corresponding to the public key `from_key`. 102 | /// 103 | /// `chain` must be a list of delegations connecting `from_key` to `to.public_key()`, and in that order; 104 | /// otherwise, the replica will reject this delegation when used as an identity. 105 | pub fn new_unchecked( 106 | from_key: Vec, 107 | to: Box, 108 | chain: Vec, 109 | ) -> Self { 110 | Self { 111 | to, 112 | chain, 113 | from_key, 114 | } 115 | } 116 | 117 | fn chain_signature(&self, mut sig: Signature) -> Signature { 118 | sig.public_key = self.public_key(); 119 | sig.delegations 120 | .get_or_insert(vec![]) 121 | .extend(self.chain.iter().cloned()); 122 | sig 123 | } 124 | } 125 | 126 | impl Identity for DelegatedIdentity { 127 | fn sender(&self) -> Result { 128 | Ok(Principal::self_authenticating(&self.from_key)) 129 | } 130 | fn public_key(&self) -> Option> { 131 | Some(self.from_key.clone()) 132 | } 133 | fn sign(&self, content: &EnvelopeContent) -> Result { 134 | self.to.sign(content).map(|sig| self.chain_signature(sig)) 135 | } 136 | fn sign_delegation(&self, content: &Delegation) -> Result { 137 | self.to 138 | .sign_delegation(content) 139 | .map(|sig| self.chain_signature(sig)) 140 | } 141 | fn sign_arbitrary(&self, content: &[u8]) -> Result { 142 | self.to 143 | .sign_arbitrary(content) 144 | .map(|sig| self.chain_signature(sig)) 145 | } 146 | fn delegation_chain(&self) -> Vec { 147 | let mut chain = self.to.delegation_chain(); 148 | chain.extend(self.chain.iter().cloned()); 149 | chain 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /ic-agent/src/identity/error.rs: -------------------------------------------------------------------------------- 1 | use ic_transport_types::Delegation; 2 | use thiserror::Error; 3 | 4 | /// An error happened while reading a PEM file. 5 | #[cfg(feature = "pem")] 6 | #[derive(Error, Debug)] 7 | pub enum PemError { 8 | /// An error occurred with disk I/O. 9 | #[error(transparent)] 10 | Io(#[from] std::io::Error), 11 | 12 | /// An unsupported curve was detected 13 | #[error("Only {0} curve is supported: {1:?}")] 14 | UnsupportedKeyCurve(String, Vec), 15 | 16 | /// An error occurred while reading the file in PEM format. 17 | #[cfg(feature = "pem")] 18 | #[error("An error occurred while reading the file: {0}")] 19 | PemError(#[from] pem::PemError), 20 | 21 | /// An error occurred while reading the file in DER format. 22 | #[cfg(feature = "pem")] 23 | #[error("An error occurred while reading the file: {0}")] 24 | DerError(#[from] der::Error), 25 | 26 | /// The key was rejected by ed25519-consensus. 27 | #[error("A key was rejected by ed25519-consensus: {0}")] 28 | KeyRejected(#[from] ed25519_consensus::Error), 29 | 30 | /// The key was rejected by k256. 31 | #[error("A key was rejected by k256: {0}")] 32 | ErrorStack(#[from] k256::pkcs8::Error), 33 | } 34 | 35 | /// An error occurred constructing a [`DelegatedIdentity`](super::delegated::DelegatedIdentity). 36 | #[derive(Error, Debug)] 37 | pub enum DelegationError { 38 | /// Parsing error in delegation bytes. 39 | #[error("A delegation could not be parsed")] 40 | Parse, 41 | /// A key in the chain did not match the signature of the next chain link. 42 | #[error("A link was missing in the delegation chain")] 43 | BrokenChain { 44 | /// The key that should have matched the next delegation 45 | from: Vec, 46 | /// The delegation that didn't match, or `None` if the `Identity` didn't match 47 | to: Option, 48 | }, 49 | /// A key with an unknown algorithm was used. The IC supports Ed25519, secp256k1, and prime256v1, and in ECDSA the curve must be specified. 50 | #[error("The delegation chain contained a key with an unknown algorithm")] 51 | UnknownAlgorithm, 52 | /// One of `Identity`'s functions returned an error. 53 | #[error("A delegated-to identity encountered an error: {0}")] 54 | IdentityError(String), 55 | } 56 | -------------------------------------------------------------------------------- /ic-agent/src/identity/mod.rs: -------------------------------------------------------------------------------- 1 | //! Types and traits dealing with identity across the Internet Computer. 2 | use std::sync::Arc; 3 | 4 | use crate::{agent::EnvelopeContent, export::Principal}; 5 | 6 | pub(crate) mod anonymous; 7 | pub(crate) mod basic; 8 | pub(crate) mod delegated; 9 | pub(crate) mod error; 10 | pub(crate) mod prime256v1; 11 | pub(crate) mod secp256k1; 12 | 13 | #[doc(inline)] 14 | pub use anonymous::AnonymousIdentity; 15 | #[doc(inline)] 16 | pub use basic::BasicIdentity; 17 | #[doc(inline)] 18 | pub use delegated::DelegatedIdentity; 19 | #[doc(inline)] 20 | pub use error::DelegationError; 21 | #[doc(inline)] 22 | pub use ic_transport_types::{Delegation, SignedDelegation}; 23 | #[doc(inline)] 24 | pub use prime256v1::Prime256v1Identity; 25 | #[doc(inline)] 26 | pub use secp256k1::Secp256k1Identity; 27 | 28 | #[cfg(feature = "pem")] 29 | #[doc(inline)] 30 | pub use error::PemError; 31 | 32 | /// A cryptographic signature, signed by an [Identity]. 33 | #[derive(Clone, Debug)] 34 | pub struct Signature { 35 | /// This is the DER-encoded public key. 36 | pub public_key: Option>, 37 | /// The signature bytes. 38 | pub signature: Option>, 39 | /// A list of delegations connecting `public_key` to the key that signed `signature`, and in that order. 40 | pub delegations: Option>, 41 | } 42 | 43 | /// An `Identity` produces [`Signatures`](Signature) for requests or delegations. It knows or 44 | /// represents the [`Principal`] of the sender. 45 | /// 46 | /// [`Agents`](crate::Agent) are assigned a single `Identity` object, but there can be multiple 47 | /// identities used. 48 | pub trait Identity: Send + Sync { 49 | /// Returns a sender, ie. the Principal ID that is used to sign a request. 50 | /// 51 | /// Only one sender can be used per request. 52 | fn sender(&self) -> Result; 53 | 54 | /// Produce the public key commonly returned in [`Signature`]. 55 | /// 56 | /// Should only return `None` if `sign` would do the same. 57 | fn public_key(&self) -> Option>; 58 | 59 | /// Sign a request ID derived from a content map. 60 | /// 61 | /// Implementors should call `content.to_request_id().signable()` for the actual bytes that need to be signed. 62 | fn sign(&self, content: &EnvelopeContent) -> Result; 63 | 64 | /// Sign a delegation to let another key be used to authenticate [`sender`](Identity::sender). 65 | /// 66 | /// Not all `Identity` implementations support this operation, though all `ic-agent` implementations other than `AnonymousIdentity` do. 67 | /// 68 | /// Implementors should call `content.signable()` for the actual bytes that need to be signed. 69 | fn sign_delegation(&self, content: &Delegation) -> Result { 70 | let _ = content; // silence unused warning 71 | Err(String::from("unsupported")) 72 | } 73 | 74 | /// Sign arbitrary bytes. 75 | /// 76 | /// Not all `Identity` implementations support this operation, though all `ic-agent` implementations do. 77 | fn sign_arbitrary(&self, content: &[u8]) -> Result { 78 | let _ = content; // silence unused warning 79 | Err(String::from("unsupported")) 80 | } 81 | 82 | /// A list of signed delegations connecting [`sender`](Identity::sender) 83 | /// to [`public_key`](Identity::public_key), and in that order. 84 | fn delegation_chain(&self) -> Vec { 85 | vec![] 86 | } 87 | } 88 | 89 | macro_rules! delegating_impl { 90 | ($implementor:ty, $name:ident => $self_expr:expr) => { 91 | impl Identity for $implementor { 92 | fn sender(&$name) -> Result { 93 | $self_expr.sender() 94 | } 95 | 96 | fn public_key(&$name) -> Option> { 97 | $self_expr.public_key() 98 | } 99 | 100 | fn sign(&$name, content: &EnvelopeContent) -> Result { 101 | $self_expr.sign(content) 102 | } 103 | 104 | fn sign_delegation(&$name, content: &Delegation) -> Result { 105 | $self_expr.sign_delegation(content) 106 | } 107 | 108 | fn sign_arbitrary(&$name, content: &[u8]) -> Result { 109 | $self_expr.sign_arbitrary(content) 110 | } 111 | 112 | fn delegation_chain(&$name) -> Vec { 113 | $self_expr.delegation_chain() 114 | } 115 | } 116 | }; 117 | } 118 | 119 | delegating_impl!(Box, self => **self); 120 | delegating_impl!(Arc, self => **self); 121 | delegating_impl!(&dyn Identity, self => *self); 122 | -------------------------------------------------------------------------------- /ic-agent/src/identity/prime256v1.rs: -------------------------------------------------------------------------------- 1 | use crate::{agent::EnvelopeContent, export::Principal, Identity, Signature}; 2 | 3 | #[cfg(feature = "pem")] 4 | use crate::identity::error::PemError; 5 | 6 | use p256::{ 7 | ecdsa::{self, signature::Signer, SigningKey, VerifyingKey}, 8 | pkcs8::{Document, EncodePublicKey}, 9 | SecretKey, 10 | }; 11 | #[cfg(feature = "pem")] 12 | use std::{fs::File, io, path::Path}; 13 | 14 | use super::Delegation; 15 | 16 | /// A cryptographic identity based on the Prime256v1 elliptic curve. 17 | /// 18 | /// The caller will be represented via [`Principal::self_authenticating`], which contains the SHA-224 hash of the public key. 19 | #[derive(Clone, Debug)] 20 | pub struct Prime256v1Identity { 21 | private_key: SigningKey, 22 | _public_key: VerifyingKey, 23 | der_encoded_public_key: Document, 24 | } 25 | 26 | impl Prime256v1Identity { 27 | /// Creates an identity from a PEM file. Shorthand for calling `from_pem` with `std::fs::read`. 28 | #[cfg(feature = "pem")] 29 | pub fn from_pem_file>(file_path: P) -> Result { 30 | Self::from_pem(File::open(file_path)?) 31 | } 32 | 33 | /// Creates an identity from a PEM certificate. 34 | #[cfg(feature = "pem")] 35 | pub fn from_pem(pem_reader: R) -> Result { 36 | use sec1::{pem::PemLabel, EcPrivateKey}; 37 | 38 | const EC_PARAMETERS: &str = "EC PARAMETERS"; 39 | const PRIME256V1: &[u8] = b"\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07"; 40 | 41 | let contents = pem_reader.bytes().collect::, io::Error>>()?; 42 | 43 | for pem in pem::parse_many(contents)? { 44 | if pem.tag() == EC_PARAMETERS && pem.contents() != PRIME256V1 { 45 | return Err(PemError::UnsupportedKeyCurve( 46 | "prime256v1".to_string(), 47 | pem.contents().to_vec(), 48 | )); 49 | } 50 | 51 | if pem.tag() != EcPrivateKey::PEM_LABEL { 52 | continue; 53 | } 54 | let private_key = 55 | SecretKey::from_sec1_der(pem.contents()).map_err(|_| pkcs8::Error::KeyMalformed)?; 56 | return Ok(Self::from_private_key(private_key)); 57 | } 58 | Err(pem::PemError::MissingData.into()) 59 | } 60 | 61 | /// Creates an identity from a private key. 62 | pub fn from_private_key(private_key: SecretKey) -> Self { 63 | let public_key = private_key.public_key(); 64 | let der_encoded_public_key = public_key 65 | .to_public_key_der() 66 | .expect("Cannot DER encode prime256v1 public key."); 67 | Self { 68 | private_key: private_key.into(), 69 | _public_key: public_key.into(), 70 | der_encoded_public_key, 71 | } 72 | } 73 | } 74 | 75 | impl Identity for Prime256v1Identity { 76 | fn sender(&self) -> Result { 77 | Ok(Principal::self_authenticating( 78 | self.der_encoded_public_key.as_ref(), 79 | )) 80 | } 81 | 82 | fn public_key(&self) -> Option> { 83 | Some(self.der_encoded_public_key.as_ref().to_vec()) 84 | } 85 | 86 | fn sign(&self, content: &EnvelopeContent) -> Result { 87 | self.sign_arbitrary(&content.to_request_id().signable()) 88 | } 89 | 90 | fn sign_delegation(&self, content: &Delegation) -> Result { 91 | self.sign_arbitrary(&content.signable()) 92 | } 93 | 94 | fn sign_arbitrary(&self, content: &[u8]) -> Result { 95 | let ecdsa_sig: ecdsa::Signature = self 96 | .private_key 97 | .try_sign(content) 98 | .map_err(|err| format!("Cannot create prime256v1 signature: {err}"))?; 99 | let r = ecdsa_sig.r().as_ref().to_bytes(); 100 | let s = ecdsa_sig.s().as_ref().to_bytes(); 101 | let mut bytes = [0u8; 64]; 102 | if r.len() > 32 || s.len() > 32 { 103 | return Err("Cannot create prime256v1 signature: malformed signature.".to_string()); 104 | } 105 | bytes[(32 - r.len())..32].clone_from_slice(&r); 106 | bytes[32 + (32 - s.len())..].clone_from_slice(&s); 107 | let signature = Some(bytes.to_vec()); 108 | let public_key = self.public_key(); 109 | Ok(Signature { 110 | public_key, 111 | signature, 112 | delegations: None, 113 | }) 114 | } 115 | } 116 | 117 | #[cfg(feature = "pem")] 118 | #[cfg(test)] 119 | mod test { 120 | use super::*; 121 | use candid::Encode; 122 | use p256::{ 123 | ecdsa::{signature::Verifier, Signature}, 124 | elliptic_curve::PrimeField, 125 | FieldBytes, Scalar, 126 | }; 127 | 128 | // WRONG_CURVE_IDENTITY_FILE is generated from the following command: 129 | // > openssl ecparam -name secp160r2 -genkey 130 | // it uses the secp160r2 curve instead of prime256v1 and should 131 | // therefore be rejected by Prime256v1Identity when loading an identity 132 | const WRONG_CURVE_IDENTITY_FILE: &str = "\ 133 | -----BEGIN EC PARAMETERS----- 134 | BgUrgQQAHg== 135 | -----END EC PARAMETERS----- 136 | -----BEGIN EC PRIVATE KEY----- 137 | MFACAQEEFI9cF6zXxMKhtjn1gBD7AHPbzehfoAcGBSuBBAAeoSwDKgAEh5NXszgR 138 | oGSXVWaGxcQhQWlFG4pbnOG+93xXzfRD7eKWOdmun2bKxQ== 139 | -----END EC PRIVATE KEY----- 140 | "; 141 | 142 | // WRONG_CURVE_IDENTITY_FILE_NO_PARAMS is generated from the following command: 143 | // > openssl ecparam -name secp160r2 -genkey -noout 144 | // it uses the secp160r2 curve instead of prime256v1 and should 145 | // therefore be rejected by Prime256v1Identity when loading an identity 146 | const WRONG_CURVE_IDENTITY_FILE_NO_PARAMS: &str = "\ 147 | -----BEGIN EC PRIVATE KEY----- 148 | MFACAQEEFI9cF6zXxMKhtjn1gBD7AHPbzehfoAcGBSuBBAAeoSwDKgAEh5NXszgR 149 | oGSXVWaGxcQhQWlFG4pbnOG+93xXzfRD7eKWOdmun2bKxQ== 150 | -----END EC PRIVATE KEY----- 151 | "; 152 | 153 | // IDENTITY_FILE was generated from the the following commands: 154 | // > openssl ecparam -name prime256v1 -genkey -noout -out identity.pem 155 | // > cat identity.pem 156 | const IDENTITY_FILE: &str = "\ 157 | -----BEGIN EC PRIVATE KEY----- 158 | MHcCAQEEIL1ybmbwx+uKYsscOZcv71MmKhrNqfPP0ke1unET5AY4oAoGCCqGSM49 159 | AwEHoUQDQgAEUbbZV4NerZTPWfbQ749/GNLu8TaH8BUS/I7/+ipsu+MPywfnBFIZ 160 | Sks4xGbA/ZbazsrMl4v446U5UIVxCGGaKw== 161 | -----END EC PRIVATE KEY----- 162 | "; 163 | 164 | // DER_ENCODED_PUBLIC_KEY was generated from the the following commands: 165 | // > openssl ec -in identity.pem -pubout -outform DER -out public.der 166 | // > hexdump -ve '1/1 "%.2x"' public.der 167 | const DER_ENCODED_PUBLIC_KEY: &str = "3059301306072a8648ce3d020106082a8648ce3d0301070342000451b6d957835ead94cf59f6d0ef8f7f18d2eef13687f01512fc8efffa2a6cbbe30fcb07e70452194a4b38c466c0fd96dacecacc978bf8e3a53950857108619a2b"; 168 | 169 | #[test] 170 | #[should_panic(expected = "UnsupportedKeyCurve")] 171 | fn test_prime256v1_reject_wrong_curve() { 172 | Prime256v1Identity::from_pem(WRONG_CURVE_IDENTITY_FILE.as_bytes()).unwrap(); 173 | } 174 | 175 | #[test] 176 | #[should_panic(expected = "KeyMalformed")] 177 | fn test_prime256v1_reject_wrong_curve_no_id() { 178 | Prime256v1Identity::from_pem(WRONG_CURVE_IDENTITY_FILE_NO_PARAMS.as_bytes()).unwrap(); 179 | } 180 | 181 | #[test] 182 | fn test_prime256v1_public_key() { 183 | // Create a prime256v1 identity from a PEM file. 184 | let identity = Prime256v1Identity::from_pem(IDENTITY_FILE.as_bytes()) 185 | .expect("Cannot create prime256v1 identity from PEM file."); 186 | 187 | // Assert the DER-encoded prime256v1 public key matches what we would expect. 188 | assert!(DER_ENCODED_PUBLIC_KEY == hex::encode(identity.der_encoded_public_key)); 189 | } 190 | 191 | #[test] 192 | fn test_prime256v1_signature() { 193 | // Create a prime256v1 identity from a PEM file. 194 | let identity = Prime256v1Identity::from_pem(IDENTITY_FILE.as_bytes()) 195 | .expect("Cannot create prime256v1 identity from PEM file."); 196 | 197 | // Create a prime256v1 signature for a hello-world canister. 198 | let message = EnvelopeContent::Call { 199 | nonce: None, 200 | ingress_expiry: 0, 201 | sender: identity.sender().unwrap(), 202 | canister_id: "bkyz2-fmaaa-aaaaa-qaaaq-cai".parse().unwrap(), 203 | method_name: "greet".to_string(), 204 | arg: Encode!(&"world").unwrap(), 205 | }; 206 | let signature = identity 207 | .sign(&message) 208 | .expect("Cannot create prime256v1 signature.") 209 | .signature 210 | .expect("Cannot find prime256v1 signature bytes."); 211 | 212 | // Import the prime256v1 signature. 213 | let r: Scalar = Option::from(Scalar::from_repr(*FieldBytes::from_slice( 214 | &signature[0..32], 215 | ))) 216 | .expect("Cannot extract r component from prime256v1 signature bytes."); 217 | let s: Scalar = Option::from(Scalar::from_repr(*FieldBytes::from_slice(&signature[32..]))) 218 | .expect("Cannot extract s component from prime256v1 signature bytes."); 219 | let ecdsa_sig = Signature::from_scalars(r, s) 220 | .expect("Cannot create prime256v1 signature from r and s components."); 221 | 222 | // Assert the prime256v1 signature is valid. 223 | identity 224 | ._public_key 225 | .verify(&message.to_request_id().signable(), &ecdsa_sig) 226 | .expect("Cannot verify prime256v1 signature."); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /ic-agent/src/identity/secp256k1.rs: -------------------------------------------------------------------------------- 1 | use crate::{agent::EnvelopeContent, export::Principal, Identity, Signature}; 2 | 3 | #[cfg(feature = "pem")] 4 | use crate::identity::error::PemError; 5 | 6 | use k256::{ 7 | ecdsa::{self, signature::Signer, SigningKey, VerifyingKey}, 8 | pkcs8::{Document, EncodePublicKey}, 9 | SecretKey, 10 | }; 11 | #[cfg(feature = "pem")] 12 | use std::{fs::File, io, path::Path}; 13 | 14 | use super::Delegation; 15 | 16 | /// A cryptographic identity based on the Secp256k1 elliptic curve. 17 | /// 18 | /// The caller will be represented via [`Principal::self_authenticating`], which contains the SHA-224 hash of the public key. 19 | #[derive(Clone, Debug)] 20 | pub struct Secp256k1Identity { 21 | private_key: SigningKey, 22 | _public_key: VerifyingKey, 23 | der_encoded_public_key: Document, 24 | } 25 | 26 | impl Secp256k1Identity { 27 | /// Creates an identity from a PEM file. Shorthand for calling `from_pem` with `std::fs::read`. 28 | #[cfg(feature = "pem")] 29 | pub fn from_pem_file>(file_path: P) -> Result { 30 | Self::from_pem(File::open(file_path)?) 31 | } 32 | 33 | /// Creates an identity from a PEM certificate. 34 | #[cfg(feature = "pem")] 35 | pub fn from_pem(pem_reader: R) -> Result { 36 | use sec1::{pem::PemLabel, EcPrivateKey}; 37 | 38 | const EC_PARAMETERS: &str = "EC PARAMETERS"; 39 | const SECP256K1: &[u8] = b"\x06\x05\x2b\x81\x04\x00\x0a"; 40 | 41 | let contents = pem_reader.bytes().collect::, io::Error>>()?; 42 | 43 | for pem in pem::parse_many(contents)? { 44 | if pem.tag() == EC_PARAMETERS && pem.contents() != SECP256K1 { 45 | return Err(PemError::UnsupportedKeyCurve( 46 | "secp256k1".to_string(), 47 | pem.contents().to_vec(), 48 | )); 49 | } 50 | 51 | if pem.tag() != EcPrivateKey::PEM_LABEL { 52 | continue; 53 | } 54 | let private_key = 55 | SecretKey::from_sec1_der(pem.contents()).map_err(|_| pkcs8::Error::KeyMalformed)?; 56 | return Ok(Self::from_private_key(private_key)); 57 | } 58 | Err(pem::PemError::MissingData.into()) 59 | } 60 | 61 | /// Creates an identity from a private key. 62 | pub fn from_private_key(private_key: SecretKey) -> Self { 63 | let public_key = private_key.public_key(); 64 | let der_encoded_public_key = public_key 65 | .to_public_key_der() 66 | .expect("Cannot DER encode secp256k1 public key."); 67 | Self { 68 | private_key: private_key.into(), 69 | _public_key: public_key.into(), 70 | der_encoded_public_key, 71 | } 72 | } 73 | } 74 | 75 | impl Identity for Secp256k1Identity { 76 | fn sender(&self) -> Result { 77 | Ok(Principal::self_authenticating( 78 | self.der_encoded_public_key.as_ref(), 79 | )) 80 | } 81 | 82 | fn public_key(&self) -> Option> { 83 | Some(self.der_encoded_public_key.as_ref().to_vec()) 84 | } 85 | 86 | fn sign(&self, content: &EnvelopeContent) -> Result { 87 | self.sign_arbitrary(&content.to_request_id().signable()) 88 | } 89 | 90 | fn sign_delegation(&self, content: &Delegation) -> Result { 91 | self.sign_arbitrary(&content.signable()) 92 | } 93 | 94 | fn sign_arbitrary(&self, content: &[u8]) -> Result { 95 | let ecdsa_sig: ecdsa::Signature = self 96 | .private_key 97 | .try_sign(content) 98 | .map_err(|err| format!("Cannot create secp256k1 signature: {err}"))?; 99 | let r = ecdsa_sig.r().as_ref().to_bytes(); 100 | let s = ecdsa_sig.s().as_ref().to_bytes(); 101 | let mut bytes = [0u8; 64]; 102 | if r.len() > 32 || s.len() > 32 { 103 | return Err("Cannot create secp256k1 signature: malformed signature.".to_string()); 104 | } 105 | bytes[(32 - r.len())..32].clone_from_slice(&r); 106 | bytes[32 + (32 - s.len())..].clone_from_slice(&s); 107 | let signature = Some(bytes.to_vec()); 108 | let public_key = self.public_key(); 109 | Ok(Signature { 110 | public_key, 111 | signature, 112 | delegations: None, 113 | }) 114 | } 115 | } 116 | 117 | #[cfg(feature = "pem")] 118 | #[cfg(test)] 119 | mod test { 120 | use super::*; 121 | use candid::Encode; 122 | use k256::{ 123 | ecdsa::{signature::Verifier, Signature}, 124 | elliptic_curve::PrimeField, 125 | FieldBytes, Scalar, 126 | }; 127 | 128 | // WRONG_CURVE_IDENTITY_FILE is generated from the following command: 129 | // > openssl ecparam -name secp160r2 -genkey 130 | // it uses hte secp160r2 curve instead of secp256k1 and should 131 | // therefore be rejected by Secp256k1Identity when loading an identity 132 | const WRONG_CURVE_IDENTITY_FILE: &str = "-----BEGIN EC PARAMETERS----- 133 | BgUrgQQAHg== 134 | -----END EC PARAMETERS----- 135 | -----BEGIN EC PRIVATE KEY----- 136 | MFACAQEEFI9cF6zXxMKhtjn1gBD7AHPbzehfoAcGBSuBBAAeoSwDKgAEh5NXszgR 137 | oGSXVWaGxcQhQWlFG4pbnOG+93xXzfRD7eKWOdmun2bKxQ== 138 | -----END EC PRIVATE KEY----- 139 | "; 140 | 141 | // WRONG_CURVE_IDENTITY_FILE_NO_PARAMS is generated from the following command: 142 | // > openssl ecparam -name secp160r2 -genkey -noout 143 | // it uses hte secp160r2 curve instead of secp256k1 and should 144 | // therefore be rejected by Secp256k1Identity when loading an identity 145 | const WRONG_CURVE_IDENTITY_FILE_NO_PARAMS: &str = "-----BEGIN EC PRIVATE KEY----- 146 | MFACAQEEFI9cF6zXxMKhtjn1gBD7AHPbzehfoAcGBSuBBAAeoSwDKgAEh5NXszgR 147 | oGSXVWaGxcQhQWlFG4pbnOG+93xXzfRD7eKWOdmun2bKxQ== 148 | -----END EC PRIVATE KEY----- 149 | "; 150 | 151 | // IDENTITY_FILE was generated from the the following commands: 152 | // > openssl ecparam -name secp256k1 -genkey -noout -out identity.pem 153 | // > cat identity.pem 154 | const IDENTITY_FILE: &str = "-----BEGIN EC PARAMETERS----- 155 | BgUrgQQACg== 156 | -----END EC PARAMETERS----- 157 | -----BEGIN EC PRIVATE KEY----- 158 | MHQCAQEEIAgy7nZEcVHkQ4Z1Kdqby8SwyAiyKDQmtbEHTIM+WNeBoAcGBSuBBAAK 159 | oUQDQgAEgO87rJ1ozzdMvJyZQ+GABDqUxGLvgnAnTlcInV3NuhuPv4O3VGzMGzeB 160 | N3d26cRxD99TPtm8uo2OuzKhSiq6EQ== 161 | -----END EC PRIVATE KEY----- 162 | "; 163 | 164 | // DER_ENCODED_PUBLIC_KEY was generated from the the following commands: 165 | // > openssl ec -in identity.pem -pubout -outform DER -out public.der 166 | // > hexdump -ve '1/1 "%.2x"' public.der 167 | const DER_ENCODED_PUBLIC_KEY: &str = "3056301006072a8648ce3d020106052b8104000a0342000480ef3bac9d68cf374cbc9c9943e180043a94c462ef8270274e57089d5dcdba1b8fbf83b7546ccc1b3781377776e9c4710fdf533ed9bcba8d8ebb32a14a2aba11"; 168 | 169 | #[test] 170 | #[should_panic(expected = "UnsupportedKeyCurve")] 171 | fn test_secp256k1_reject_wrong_curve() { 172 | Secp256k1Identity::from_pem(WRONG_CURVE_IDENTITY_FILE.as_bytes()).unwrap(); 173 | } 174 | 175 | #[test] 176 | #[should_panic(expected = "KeyMalformed")] 177 | fn test_secp256k1_reject_wrong_curve_no_id() { 178 | Secp256k1Identity::from_pem(WRONG_CURVE_IDENTITY_FILE_NO_PARAMS.as_bytes()).unwrap(); 179 | } 180 | 181 | #[test] 182 | fn test_secp256k1_public_key() { 183 | // Create a secp256k1 identity from a PEM file. 184 | let identity = Secp256k1Identity::from_pem(IDENTITY_FILE.as_bytes()) 185 | .expect("Cannot create secp256k1 identity from PEM file."); 186 | 187 | // Assert the DER-encoded secp256k1 public key matches what we would expect. 188 | assert!(DER_ENCODED_PUBLIC_KEY == hex::encode(identity.der_encoded_public_key)); 189 | } 190 | 191 | #[test] 192 | fn test_secp256k1_signature() { 193 | // Create a secp256k1 identity from a PEM file. 194 | let identity = Secp256k1Identity::from_pem(IDENTITY_FILE.as_bytes()) 195 | .expect("Cannot create secp256k1 identity from PEM file."); 196 | 197 | // Create a secp256k1 signature for a hello-world canister. 198 | let message = EnvelopeContent::Call { 199 | nonce: None, 200 | ingress_expiry: 0, 201 | sender: identity.sender().unwrap(), 202 | canister_id: "bkyz2-fmaaa-aaaaa-qaaaq-cai".parse().unwrap(), 203 | method_name: "greet".to_string(), 204 | arg: Encode!(&"world").unwrap(), 205 | }; 206 | let signature = identity 207 | .sign(&message) 208 | .expect("Cannot create secp256k1 signature.") 209 | .signature 210 | .expect("Cannot find secp256k1 signature bytes."); 211 | 212 | // Import the secp256k1 signature into OpenSSL. 213 | let r: Scalar = Option::from(Scalar::from_repr(*FieldBytes::from_slice( 214 | &signature[0..32], 215 | ))) 216 | .expect("Cannot extract r component from secp256k1 signature bytes."); 217 | let s: Scalar = Option::from(Scalar::from_repr(*FieldBytes::from_slice(&signature[32..]))) 218 | .expect("Cannot extract s component from secp256k1 signature bytes."); 219 | let ecdsa_sig = Signature::from_scalars(r, s) 220 | .expect("Cannot create secp256k1 signature from r and s components."); 221 | 222 | // Assert the secp256k1 signature is valid. 223 | identity 224 | ._public_key 225 | .verify(&message.to_request_id().signable(), &ecdsa_sig) 226 | .expect("Cannot verify secp256k1 signature."); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /ic-agent/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The `ic-agent` is a simple-to-use library that enables you to 2 | //! build applications and interact with the [Internet Computer](https://internetcomputer.org) 3 | //! in Rust. It serves as a Rust-based low-level backend for the 4 | //! DFINITY Canister Software Development Kit (SDK) and the command-line execution environment 5 | //! [`dfx`](https://internetcomputer.org/docs/current/developer-docs/setup/install). 6 | //! 7 | //! ## Overview 8 | //! The `ic-agent` is a Rust crate that can connect directly to the Internet 9 | //! Computer through the Internet Computer protocol (ICP). 10 | //! The key software components of the ICP are broadly referred to as the 11 | //! [replica](https://internetcomputer.org/docs/current/concepts/nodes-subnets). 12 | //! 13 | //! The agent is designed to be compatible with multiple versions of the 14 | //! replica API, and to expose both low-level APIs for communicating with 15 | //! Internet Computer protocol components like the replica and to provide 16 | //! higher-level APIs for communicating with software applications deployed 17 | //! as [canisters](https://internetcomputer.org/docs/current/concepts/canisters-code). 18 | //! 19 | //! ## Example 20 | //! The following example illustrates how to use the Agent interface to send 21 | //! a call to an Internet Computer's Ledger Canister to check the total ICP tokens supply. 22 | //! 23 | //! ```rust 24 | //!use anyhow::{Context, Result}; 25 | //!use candid::{Decode, Nat}; 26 | //!use ic_agent::{export::Principal, Agent}; 27 | //!use url::Url; 28 | //! 29 | //!pub async fn create_agent(url: Url, use_mainnet: bool) -> Result { 30 | //! let agent = Agent::builder().with_url(url).build()?; 31 | //! if !use_mainnet { 32 | //! agent.fetch_root_key().await?; 33 | //! } 34 | //! Ok(agent) 35 | //!} 36 | //! 37 | //!#[tokio::main] 38 | //!async fn main() -> Result<()> { 39 | //! // IC HTTP Gateway URL 40 | //! let url = Url::parse("https://ic0.app").unwrap(); 41 | //! let agent = create_agent(url, true).await?; 42 | //! 43 | //! // ICP Ledger Canister ID 44 | //! let canister_id = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai")?; 45 | //! 46 | //! // Method: icrc1_total_supply (takes no arguments, returns nat) 47 | //! let method_name = "icrc1_total_supply"; 48 | //! 49 | //! // Encode empty Candid arguments 50 | //! let args = candid::encode_args(())?; 51 | //! 52 | //! // Dispatch query call 53 | //! let response = agent 54 | //! .query(&canister_id, method_name) 55 | //! .with_arg(args) 56 | //! .call() 57 | //! .await 58 | //! .context("Failed to query icrc1_total_supply method.")?; 59 | //! 60 | //! // Decode the response as nat 61 | //! let total_supply_nat = 62 | //! Decode!(&response, Nat).context("Failed to decode total supply as nat.")?; 63 | //! 64 | //! println!("Total ICP Supply: {} ICP", total_supply_nat); 65 | //! 66 | //! Ok(()) 67 | //!} 68 | //! ``` 69 | //! For more information about the Agent interface used in this example, see the 70 | //! [Agent] documentation. 71 | //! 72 | //! ## References 73 | //! For an introduction to the Internet Computer and the DFINITY Canister SDK, 74 | //! see the following resources: 75 | //! 76 | //! - [How the IC Works](https://internetcomputer.org/docs/current/concepts/) 77 | //! - [DFINITY Canister SDK](https://internetcomputer.org/docs/current/references/cli-reference/) 78 | //! 79 | //! The Internet Computer protocol and interface specifications are not 80 | //! publicly available yet. When these specifications are made public and 81 | //! generally available, additional details about the versions supported will 82 | //! be available here. 83 | 84 | #![warn( 85 | missing_docs, 86 | rustdoc::broken_intra_doc_links, 87 | rustdoc::private_intra_doc_links 88 | )] 89 | #![cfg_attr(not(target_family = "wasm"), warn(clippy::future_not_send))] 90 | #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] 91 | 92 | #[macro_use] 93 | mod util; 94 | 95 | pub mod agent; 96 | pub mod export; 97 | pub mod identity; 98 | 99 | use agent::response_authentication::LookupPath; 100 | #[doc(inline)] 101 | pub use agent::{agent_error, agent_error::AgentError, Agent, NonceFactory, NonceGenerator}; 102 | #[doc(inline)] 103 | pub use ic_transport_types::{to_request_id, RequestId, RequestIdError, TransportCallResponse}; 104 | #[doc(inline)] 105 | pub use identity::{Identity, Signature}; 106 | 107 | // Re-export from ic_certification for backward compatibility. 108 | pub use ic_certification::{hash_tree, Certificate}; 109 | 110 | /// Looks up a value in the certificate's tree at the specified hash. 111 | /// 112 | /// Returns the value if it was found; otherwise, errors with `LookupPathAbsent`, `LookupPathUnknown`, or `LookupPathError`. 113 | pub fn lookup_value>( 114 | tree: &ic_certification::certificate::Certificate, 115 | path: P, 116 | ) -> Result<&[u8], AgentError> { 117 | agent::response_authentication::lookup_value(&tree.tree, path) 118 | } 119 | -------------------------------------------------------------------------------- /ic-agent/src/util.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::future::Future; 4 | use std::time::Duration; 5 | 6 | pub async fn sleep(d: Duration) { 7 | #[cfg(not(all(target_family = "wasm", feature = "wasm-bindgen")))] 8 | tokio::time::sleep(d).await; 9 | #[cfg(all(target_family = "wasm", feature = "wasm-bindgen"))] 10 | wasm_bindgen_futures::JsFuture::from(js_sys::Promise::new(&mut |rs, rj| { 11 | if let Err(e) = web_sys::window() 12 | .expect("global window unavailable") 13 | .set_timeout_with_callback_and_timeout_and_arguments_0(&rs, d.as_millis() as _) 14 | { 15 | use wasm_bindgen::UnwrapThrowExt; 16 | rj.call1(&rj, &e).unwrap_throw(); 17 | } 18 | })) 19 | .await 20 | .expect("unable to setTimeout"); 21 | #[cfg(all(target_family = "wasm", not(feature = "wasm-bindgen")))] 22 | const _: () = 23 | { panic!("Using ic-agent from WASM requires enabling the `wasm-bindgen` feature") }; 24 | } 25 | 26 | #[cfg(all(target_family = "wasm", feature = "wasm-bindgen"))] 27 | pub fn spawn(f: impl Future + 'static) { 28 | wasm_bindgen_futures::spawn_local(f); 29 | } 30 | 31 | #[cfg(not(all(target_family = "wasm", feature = "wasm-bindgen")))] 32 | pub fn spawn(f: impl Future + Send + 'static) { 33 | tokio::spawn(f); 34 | } 35 | 36 | macro_rules! log { 37 | ($name:ident, $($t:tt)*) => { #[cfg(feature = "tracing")] { tracing::$name!($($t)*) } }; 38 | } 39 | -------------------------------------------------------------------------------- /ic-certification/README.md: -------------------------------------------------------------------------------- 1 | # IC Certification 2 | 3 | This library has been moved to https://github.com/dfinity/response-verification. 4 | -------------------------------------------------------------------------------- /ic-identity-hsm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-identity-hsm" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | rust-version.workspace = true 9 | description = "Identity implementation for HSM for the ic-agent package." 10 | homepage = "https://docs.rs/ic-identity-hsm" 11 | documentation = "https://docs.rs/ic-identity-hsm" 12 | readme = "README.md" 13 | categories = ["api-bindings", "data-structures", "no-std"] 14 | keywords = ["internet-computer", "agent", "utility", "icp", "dfinity"] 15 | include = ["src", "Cargo.toml", "../LICENSE", "README.md"] 16 | 17 | [dependencies] 18 | hex = { workspace = true } 19 | ic-agent = { workspace = true, default-features = false } 20 | pkcs11 = "0.5.0" 21 | sha2 = { workspace = true } 22 | simple_asn1 = "0.6.0" 23 | thiserror = { workspace = true } 24 | 25 | [dev-dependencies] 26 | ic-agent = { workspace = true } 27 | -------------------------------------------------------------------------------- /ic-identity-hsm/README.md: -------------------------------------------------------------------------------- 1 | `ic-identity-hsm` is a crate to manage identities related to HSM (Hardware Security Module), allowing users to sign Internet Computer messages with their hardware key. Also supports SoftHSM. 2 | 3 | ## Useful links 4 | 5 | - [Documentation (master)](https://agent-rust.netlify.app/ic_identity_hsm) 6 | - [Documentation (published)](https://docs.rs/ic_identity_hsm) 7 | -------------------------------------------------------------------------------- /ic-identity-hsm/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A crate to manage identities related to HSM (Hardware Security Module), 2 | //! allowing users to sign Internet Computer messages with their hardware key. 3 | //! Also supports `SoftHSM`. 4 | //! 5 | //! # Example 6 | //! 7 | //! ```rust,no_run 8 | //! use ic_agent::agent::Agent; 9 | //! use ic_identity_hsm::HardwareIdentity; 10 | //! # fn main() -> Result<(), Box> { 11 | //! # let replica_url = ""; 12 | //! # let lib_path = ""; 13 | //! # let slot_index = 0; 14 | //! # let key_id = ""; 15 | //! let agent = Agent::builder() 16 | //! .with_url(replica_url) 17 | //! .with_identity(HardwareIdentity::new(lib_path, slot_index, key_id, || Ok("hunter2".to_string()))?) 18 | //! .build(); 19 | //! # Ok(()) 20 | //! # } 21 | 22 | #![deny( 23 | missing_docs, 24 | missing_debug_implementations, 25 | rustdoc::broken_intra_doc_links, 26 | rustdoc::private_intra_doc_links 27 | )] 28 | 29 | pub(crate) mod hsm; 30 | pub use hsm::{HardwareIdentity, HardwareIdentityError}; 31 | -------------------------------------------------------------------------------- /ic-transport-types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-transport-types" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | repository.workspace = true 7 | rust-version.workspace = true 8 | license.workspace = true 9 | description = "Types related to the HTTP transport for the Internet Computer" 10 | keywords = ["internet-computer", "agent", "types", "ic", "dfinity"] 11 | categories = ["cryptography::cryptocurrencies", "api-bindings"] 12 | 13 | [lints] 14 | workspace = true 15 | 16 | [dependencies] 17 | candid.workspace = true 18 | hex.workspace = true 19 | ic-certification.workspace = true 20 | leb128.workspace = true 21 | thiserror.workspace = true 22 | serde.workspace = true 23 | serde_bytes.workspace = true 24 | serde_cbor.workspace = true 25 | serde_repr.workspace = true 26 | sha2.workspace = true 27 | 28 | [dev-dependencies] 29 | serde_json.workspace = true 30 | -------------------------------------------------------------------------------- /ic-transport-types/src/request_id/error.rs: -------------------------------------------------------------------------------- 1 | //! Error type for the `RequestId` calculation. 2 | use thiserror::Error; 3 | 4 | /// Errors from reading a `RequestId` from a string. This is not the same as 5 | /// deserialization. 6 | #[derive(Error, Debug)] 7 | pub enum RequestIdFromStringError { 8 | /// The string was not of a valid length. 9 | #[error("Invalid string size: {0}. Must be even.")] 10 | InvalidSize(usize), 11 | 12 | /// The string was not in a valid hexadecimal format. 13 | #[error("Error while decoding hex: {0}")] 14 | FromHexError(hex::FromHexError), 15 | } 16 | 17 | /// An error during the calculation of the `RequestId`. 18 | /// 19 | /// Since we use serde for serializing a data type into a hash, this has to support traits that 20 | /// serde expects, such as Display 21 | #[derive(Error, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] 22 | pub enum RequestIdError { 23 | /// An unknown error occurred inside `serde`. 24 | #[error("A custom error happened inside Serde: {0}")] 25 | CustomSerdeError(String), 26 | /// The serializer was not given any data. 27 | #[error("Need to provide data to serialize")] 28 | EmptySerializer, 29 | /// A map was serialized with a key of `None`. 30 | #[error("Struct serializer received a key of None")] 31 | KeyWasNone, 32 | /// The serializer received a `bool`, which it does not support. 33 | #[error("Unsupported type: Bool")] 34 | UnsupportedTypeBool, 35 | /// The serializer received a `f32`, which it does not support. 36 | #[error("Unsupported type: f32")] 37 | UnsupportedTypeF32, 38 | /// The serializer received a `f64`, which it does not support. 39 | #[error("Unsupported type: f64")] 40 | UnsupportedTypeF64, 41 | /// The serializer received a `()`, which it does not support. 42 | #[error("Unsupported type: ()")] 43 | UnsupportedTypeUnit, 44 | // Variants and complex types. 45 | /// The serializer received an enum unit variant, which it does not support. 46 | #[error("Unsupported type: unit struct")] 47 | UnsupportedTypeUnitStruct, 48 | } 49 | 50 | impl serde::ser::Error for RequestIdError { 51 | fn custom(msg: T) -> Self 52 | where 53 | T: std::fmt::Display, 54 | { 55 | RequestIdError::CustomSerdeError(msg.to_string()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ic-transport-types/src/signed.rs: -------------------------------------------------------------------------------- 1 | //! Types representing signed messages. 2 | 3 | use crate::request_id::RequestId; 4 | use candid::Principal; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | /// A signed query request message. Produced by 8 | /// [`QueryBuilder::sign`](https://docs.rs/ic-agent/latest/ic_agent/agent/struct.QueryBuilder.html#method.sign). 9 | /// 10 | /// To submit this request, pass the `signed_query` field to [`Agent::query_signed`](https://docs.rs/ic-agent/latest/ic_agent/struct.Agent.html#method.query_signed). 11 | #[derive(Debug, Clone, Deserialize, Serialize)] 12 | pub struct SignedQuery { 13 | /// The Unix timestamp that the request will expire at. 14 | pub ingress_expiry: u64, 15 | /// The principal ID of the caller. 16 | pub sender: Principal, 17 | /// The principal ID of the canister being called. 18 | pub canister_id: Principal, 19 | /// The name of the canister method being called. 20 | pub method_name: String, 21 | /// The argument blob to be passed to the method. 22 | #[serde(with = "serde_bytes")] 23 | pub arg: Vec, 24 | /// The [effective canister ID](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-effective-canister-id) of the destination. 25 | pub effective_canister_id: Principal, 26 | /// The CBOR-encoded [authentication envelope](https://internetcomputer.org/docs/current/references/ic-interface-spec#authentication) for the request. 27 | /// This field can be passed to [`Agent::query_signed`](https://docs.rs/ic-agent/latest/ic_agent/struct.Agent.html#method.query_signed). 28 | #[serde(with = "serde_bytes")] 29 | pub signed_query: Vec, 30 | /// A nonce to uniquely identify this query call. 31 | #[serde(default)] 32 | #[serde(skip_serializing_if = "Option::is_none")] 33 | #[serde(with = "serde_bytes")] 34 | pub nonce: Option>, 35 | } 36 | 37 | /// A signed update request message. Produced by 38 | /// [`UpdateBuilder::sign`](https://docs.rs/ic-agent/latest/ic_agent/agent/struct.UpdateBuilder.html#method.sign). 39 | /// 40 | /// To submit this request, pass the `signed_update` field to [`Agent::update_signed`](https://docs.rs/ic-agent/latest/ic_agent/struct.Agent.html#method.update_signed). 41 | #[derive(Debug, Clone, Deserialize, Serialize)] 42 | pub struct SignedUpdate { 43 | /// A nonce to uniquely identify this update call. 44 | #[serde(default)] 45 | #[serde(skip_serializing_if = "Option::is_none")] 46 | #[serde(with = "serde_bytes")] 47 | pub nonce: Option>, 48 | /// The Unix timestamp that the request will expire at. 49 | pub ingress_expiry: u64, 50 | /// The principal ID of the caller. 51 | pub sender: Principal, 52 | /// The principal ID of the canister being called. 53 | pub canister_id: Principal, 54 | /// The name of the canister method being called. 55 | pub method_name: String, 56 | /// The argument blob to be passed to the method. 57 | #[serde(with = "serde_bytes")] 58 | pub arg: Vec, 59 | /// The [effective canister ID](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-effective-canister-id) of the destination. 60 | pub effective_canister_id: Principal, 61 | #[serde(with = "serde_bytes")] 62 | /// The CBOR-encoded [authentication envelope](https://internetcomputer.org/docs/current/references/ic-interface-spec#authentication) for the request. 63 | /// This field can be passed to [`Agent::update_signed`](https://docs.rs/ic-agent/latest/ic_agent/struct.Agent.html#method.update_signed). 64 | pub signed_update: Vec, 65 | /// The request ID. 66 | pub request_id: RequestId, 67 | } 68 | 69 | /// A signed request-status request message. Produced by 70 | /// [`Agent::sign_request_status`](https://docs.rs/ic-agent/latest/ic_agent/agent/struct.Agent.html#method.sign_request_status). 71 | /// 72 | /// To submit this request, pass the `signed_request_status` field to [`Agent::request_status_signed`](https://docs.rs/ic-agent/latest/ic_agent/struct.Agent.html#method.request_status_signed). 73 | #[derive(Debug, Clone, Deserialize, Serialize)] 74 | pub struct SignedRequestStatus { 75 | /// The Unix timestamp that the request will expire at. 76 | pub ingress_expiry: u64, 77 | /// The principal ID of the caller. 78 | pub sender: Principal, 79 | /// The [effective canister ID](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-effective-canister-id) of the destination. 80 | pub effective_canister_id: Principal, 81 | /// The request ID. 82 | pub request_id: RequestId, 83 | /// The CBOR-encoded [authentication envelope](https://internetcomputer.org/docs/current/references/ic-interface-spec#authentication) for the request. 84 | /// This field can be passed to [`Agent::request_status_signed`](https://docs.rs/ic-agent/latest/ic_agent/struct.Agent.html#method.request_status_signed). 85 | #[serde(with = "serde_bytes")] 86 | pub signed_request_status: Vec, 87 | } 88 | 89 | #[cfg(test)] 90 | mod tests { 91 | // Note this useful idiom: importing names from outer (for mod tests) scope. 92 | use super::*; 93 | 94 | #[test] 95 | fn test_query_serde() { 96 | let query = SignedQuery { 97 | ingress_expiry: 1, 98 | sender: Principal::management_canister(), 99 | canister_id: Principal::management_canister(), 100 | method_name: "greet".to_string(), 101 | arg: vec![0, 1], 102 | effective_canister_id: Principal::management_canister(), 103 | signed_query: vec![0, 1, 2, 3], 104 | nonce: None, 105 | }; 106 | let serialized = serde_json::to_string(&query).unwrap(); 107 | let deserialized = serde_json::from_str::(&serialized); 108 | assert!(deserialized.is_ok()); 109 | } 110 | 111 | #[test] 112 | fn test_update_serde() { 113 | let update = SignedUpdate { 114 | nonce: None, 115 | ingress_expiry: 1, 116 | sender: Principal::management_canister(), 117 | canister_id: Principal::management_canister(), 118 | method_name: "greet".to_string(), 119 | arg: vec![0, 1], 120 | effective_canister_id: Principal::management_canister(), 121 | signed_update: vec![0, 1, 2, 3], 122 | request_id: RequestId::new(&[0; 32]), 123 | }; 124 | let serialized = serde_json::to_string(&update).unwrap(); 125 | let deserialized = serde_json::from_str::(&serialized); 126 | assert!(deserialized.is_ok()); 127 | } 128 | 129 | #[test] 130 | fn test_request_status_serde() { 131 | let request_status = SignedRequestStatus { 132 | ingress_expiry: 1, 133 | sender: Principal::management_canister(), 134 | effective_canister_id: Principal::management_canister(), 135 | request_id: RequestId::new(&[0; 32]), 136 | signed_request_status: vec![0, 1, 2, 3], 137 | }; 138 | let serialized = serde_json::to_string(&request_status).unwrap(); 139 | let deserialized = serde_json::from_str::(&serialized); 140 | assert!(deserialized.is_ok()); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /ic-utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-utils" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | rust-version.workspace = true 9 | description = "Collection of utilities for Rust, on top of ic-agent, to communicate with the Internet Computer, following the Public Specification." 10 | homepage = "https://docs.rs/ic-utils" 11 | documentation = "https://docs.rs/ic-utils" 12 | readme = "README.md" 13 | categories = ["api-bindings", "data-structures", "no-std"] 14 | keywords = ["internet-computer", "agent", "utility", "icp", "dfinity"] 15 | include = ["src", "Cargo.toml", "../LICENSE", "README.md"] 16 | 17 | [lints] 18 | workspace = true 19 | 20 | [dependencies] 21 | async-trait = "0.1.68" 22 | candid = { workspace = true, features = ["value"] } 23 | futures-util = { workspace = true } 24 | ic-agent = { workspace = true, default-features = false } 25 | serde = { workspace = true } 26 | serde_bytes = { workspace = true } 27 | sha2 = { workspace = true } 28 | strum = "0.26" 29 | strum_macros = "0.26" 30 | thiserror = { workspace = true } 31 | time = { workspace = true } 32 | tokio = { workspace = true } 33 | semver = "1.0.7" 34 | once_cell = "1.10.0" 35 | 36 | [dev-dependencies] 37 | ed25519-consensus = { workspace = true } 38 | ic-agent = { workspace = true, default-features = true } 39 | pocket-ic = { workspace = true } 40 | rand = { workspace = true } 41 | tokio = { workspace = true, features = ["full"] } 42 | 43 | [features] 44 | raw = [] 45 | 46 | [package.metadata.docs.rs] 47 | targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] 48 | rustdoc-args = ["--cfg=docsrs"] 49 | features = ["raw"] 50 | -------------------------------------------------------------------------------- /ic-utils/README.md: -------------------------------------------------------------------------------- 1 | `ic-utils` is a collection of utilities to help communicating with the 2 | Internet Computer. It abstracts a few concepts over the lower level 3 | `ic-agent`. 4 | 5 | ## Useful links 6 | 7 | - [Documentation (published)](https://docs.rs/ic_utils) 8 | -------------------------------------------------------------------------------- /ic-utils/src/call/expiry.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, SystemTime}; 2 | 3 | use ic_agent::agent::{QueryBuilder, UpdateBuilder}; 4 | use time::OffsetDateTime; 5 | 6 | /// An expiry value. Either not specified (the default), a delay relative to the time the 7 | /// call is made, or a specific date time. 8 | #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Default)] 9 | pub enum Expiry { 10 | /// Unspecified. Will not try to override the Agent's value, which might itself have 11 | /// its own default value. 12 | #[default] 13 | Unspecified, 14 | 15 | /// A duration that will be added to the system time when the call is made. 16 | Delay(Duration), 17 | 18 | /// A specific date and time to use for the expiry of the request. 19 | DateTime(OffsetDateTime), 20 | } 21 | 22 | impl Expiry { 23 | /// Create an expiry that happens after a duration. 24 | #[inline] 25 | pub fn after(d: Duration) -> Self { 26 | Self::Delay(d) 27 | } 28 | 29 | /// Set the expiry field to a specific date and time. 30 | #[inline] 31 | pub fn at(dt: impl Into) -> Self { 32 | Self::DateTime(dt.into()) 33 | } 34 | 35 | pub(crate) fn apply_to_update(self, u: UpdateBuilder<'_>) -> UpdateBuilder<'_> { 36 | match self { 37 | Expiry::Unspecified => u, 38 | Expiry::Delay(d) => u.expire_after(d), 39 | Expiry::DateTime(dt) => u.expire_at(dt), 40 | } 41 | } 42 | 43 | pub(crate) fn apply_to_query(self, u: QueryBuilder<'_>) -> QueryBuilder<'_> { 44 | match self { 45 | Expiry::Unspecified => u, 46 | Expiry::Delay(d) => u.expire_after(d), 47 | Expiry::DateTime(dt) => u.expire_at(dt), 48 | } 49 | } 50 | } 51 | 52 | impl From for Expiry { 53 | fn from(d: Duration) -> Self { 54 | Self::Delay(d) 55 | } 56 | } 57 | 58 | impl From for Expiry { 59 | fn from(dt: SystemTime) -> Self { 60 | Self::DateTime(dt.into()) 61 | } 62 | } 63 | 64 | impl From for Expiry { 65 | fn from(dt: OffsetDateTime) -> Self { 66 | Self::DateTime(dt) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ic-utils/src/interfaces.rs: -------------------------------------------------------------------------------- 1 | pub mod bitcoin_canister; 2 | pub mod http_request; 3 | pub mod management_canister; 4 | pub mod wallet; 5 | 6 | pub use bitcoin_canister::BitcoinCanister; 7 | pub use http_request::HttpRequestCanister; 8 | pub use management_canister::ManagementCanister; 9 | pub use wallet::WalletCanister; 10 | -------------------------------------------------------------------------------- /ic-utils/src/interfaces/bitcoin_canister.rs: -------------------------------------------------------------------------------- 1 | //! The canister interface for the [Bitcoin canister](https://github.com/dfinity/bitcoin-canister). 2 | 3 | use std::ops::Deref; 4 | 5 | use candid::{CandidType, Principal}; 6 | use ic_agent::{Agent, AgentError}; 7 | use serde::Deserialize; 8 | 9 | use crate::{ 10 | call::{AsyncCall, SyncCall}, 11 | Canister, 12 | }; 13 | 14 | /// The canister interface for the IC [Bitcoin canister](https://github.com/dfinity/bitcoin-canister). 15 | #[derive(Debug)] 16 | pub struct BitcoinCanister<'agent> { 17 | canister: Canister<'agent>, 18 | network: BitcoinNetwork, 19 | } 20 | 21 | impl<'agent> Deref for BitcoinCanister<'agent> { 22 | type Target = Canister<'agent>; 23 | fn deref(&self) -> &Self::Target { 24 | &self.canister 25 | } 26 | } 27 | const MAINNET_ID: Principal = 28 | Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x01, 0xa0, 0x00, 0x04, 0x01, 0x01]); 29 | const TESTNET_ID: Principal = 30 | Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x01, 0xa0, 0x00, 0x01, 0x01, 0x01]); 31 | 32 | impl<'agent> BitcoinCanister<'agent> { 33 | /// Create a `BitcoinCanister` interface from an existing canister object. 34 | pub fn from_canister(canister: Canister<'agent>, network: BitcoinNetwork) -> Self { 35 | Self { canister, network } 36 | } 37 | /// Create a `BitcoinCanister` interface pointing to the specified canister ID. 38 | pub fn create(agent: &'agent Agent, canister_id: Principal, network: BitcoinNetwork) -> Self { 39 | Self::from_canister( 40 | Canister::builder() 41 | .with_agent(agent) 42 | .with_canister_id(canister_id) 43 | .build() 44 | .expect("all required fields should be set"), 45 | network, 46 | ) 47 | } 48 | /// Create a `BitcoinCanister` interface for the Bitcoin mainnet canister on the IC mainnet. 49 | pub fn mainnet(agent: &'agent Agent) -> Self { 50 | Self::for_network(agent, BitcoinNetwork::Mainnet).expect("valid network") 51 | } 52 | /// Create a `BitcoinCanister` interface for the Bitcoin testnet canister on the IC mainnet. 53 | pub fn testnet(agent: &'agent Agent) -> Self { 54 | Self::for_network(agent, BitcoinNetwork::Testnet).expect("valid network") 55 | } 56 | /// Create a `BitcoinCanister` interface for the specified Bitcoin network on the IC mainnet. Errors if `Regtest` is specified. 57 | pub fn for_network(agent: &'agent Agent, network: BitcoinNetwork) -> Result { 58 | let canister_id = match network { 59 | BitcoinNetwork::Mainnet => MAINNET_ID, 60 | BitcoinNetwork::Testnet => TESTNET_ID, 61 | BitcoinNetwork::Regtest => { 62 | return Err(AgentError::MessageError( 63 | "No applicable canister ID for regtest".to_string(), 64 | )) 65 | } 66 | }; 67 | Ok(Self::create(agent, canister_id, network)) 68 | } 69 | 70 | /// Gets the BTC balance (in satoshis) of a particular Bitcoin address, filtering by number of confirmations. 71 | /// Most applications should require 6 confirmations. 72 | pub fn get_balance( 73 | &self, 74 | address: &str, 75 | min_confirmations: Option, 76 | ) -> impl 'agent + AsyncCall { 77 | #[derive(CandidType)] 78 | struct In<'a> { 79 | address: &'a str, 80 | network: BitcoinNetwork, 81 | min_confirmations: Option, 82 | } 83 | self.update("bitcoin_get_balance") 84 | .with_arg(GetBalance { 85 | address, 86 | network: self.network, 87 | min_confirmations, 88 | }) 89 | .build() 90 | } 91 | 92 | /// Gets the BTC balance (in satoshis) of a particular Bitcoin address, filtering by number of confirmations. 93 | /// Most applications should require 6 confirmations. 94 | pub fn get_balance_query( 95 | &self, 96 | address: &str, 97 | min_confirmations: Option, 98 | ) -> impl 'agent + SyncCall { 99 | self.query("bitcoin_get_balance_query") 100 | .with_arg(GetBalance { 101 | address, 102 | network: self.network, 103 | min_confirmations, 104 | }) 105 | .build() 106 | } 107 | 108 | /// Fetch the list of [UTXOs](https://en.wikipedia.org/wiki/Unspent_transaction_output) for a Bitcoin address, 109 | /// filtering by number of confirmations. Most applications should require 6 confirmations. 110 | /// 111 | /// This method is paginated. If not all the results can be returned, then `next_page` will be set to `Some`, 112 | /// and its value can be passed to this method to get the next page. 113 | pub fn get_utxos( 114 | &self, 115 | address: &str, 116 | filter: Option, 117 | ) -> impl 'agent + AsyncCall { 118 | self.update("bitcoin_get_utxos") 119 | .with_arg(GetUtxos { 120 | address, 121 | network: self.network, 122 | filter, 123 | }) 124 | .build() 125 | } 126 | 127 | /// Fetch the list of [UTXOs](https://en.wikipedia.org/wiki/Unspent_transaction_output) for a Bitcoin address, 128 | /// filtering by number of confirmations. Most applications should require 6 confirmations. 129 | /// 130 | /// This method is paginated. If not all the results can be returned, then `next_page` will be set to `Some`, 131 | /// and its value can be passed to this method to get the next page. 132 | pub fn get_utxos_query( 133 | &self, 134 | address: &str, 135 | filter: Option, 136 | ) -> impl 'agent + SyncCall { 137 | self.query("bitcoin_get_utxos_query") 138 | .with_arg(GetUtxos { 139 | address, 140 | network: self.network, 141 | filter, 142 | }) 143 | .build() 144 | } 145 | 146 | /// Gets the transaction fee percentiles for the last 10,000 transactions. In the returned vector, `v[i]` is the `i`th percentile fee, 147 | /// measured in millisatoshis/vbyte, and `v[0]` is the smallest fee. 148 | pub fn get_current_fee_percentiles(&self) -> impl 'agent + AsyncCall,)> { 149 | #[derive(CandidType)] 150 | struct In { 151 | network: BitcoinNetwork, 152 | } 153 | self.update("bitcoin_get_current_fee_percentiles") 154 | .with_arg(In { 155 | network: self.network, 156 | }) 157 | .build() 158 | } 159 | /// Gets the block headers for the specified range of blocks. If `end_height` is `None`, the returned `tip_height` provides the tip at the moment 160 | /// the chain was queried. 161 | pub fn get_block_headers( 162 | &self, 163 | start_height: u32, 164 | end_height: Option, 165 | ) -> impl 'agent + AsyncCall { 166 | #[derive(CandidType)] 167 | struct In { 168 | start_height: u32, 169 | end_height: Option, 170 | } 171 | self.update("bitcoin_get_block_headers") 172 | .with_arg(In { 173 | start_height, 174 | end_height, 175 | }) 176 | .build() 177 | } 178 | /// Submits a new Bitcoin transaction. No guarantees are made about the outcome. 179 | pub fn send_transaction(&self, transaction: Vec) -> impl 'agent + AsyncCall { 180 | #[derive(CandidType, Deserialize)] 181 | struct In { 182 | network: BitcoinNetwork, 183 | #[serde(with = "serde_bytes")] 184 | transaction: Vec, 185 | } 186 | self.update("bitcoin_send_transaction") 187 | .with_arg(In { 188 | network: self.network, 189 | transaction, 190 | }) 191 | .build() 192 | } 193 | } 194 | 195 | #[derive(Debug, CandidType)] 196 | struct GetBalance<'a> { 197 | address: &'a str, 198 | network: BitcoinNetwork, 199 | min_confirmations: Option, 200 | } 201 | 202 | #[derive(Debug, CandidType)] 203 | struct GetUtxos<'a> { 204 | address: &'a str, 205 | network: BitcoinNetwork, 206 | filter: Option, 207 | } 208 | 209 | /// The Bitcoin network that a Bitcoin transaction is placed on. 210 | #[derive(Clone, Copy, Debug, CandidType, Deserialize, PartialEq, Eq)] 211 | pub enum BitcoinNetwork { 212 | /// The BTC network. 213 | #[serde(rename = "mainnet")] 214 | Mainnet, 215 | /// The TESTBTC network. 216 | #[serde(rename = "testnet")] 217 | Testnet, 218 | /// The REGTEST network. 219 | /// 220 | /// This is only available when developing with local replica. 221 | #[serde(rename = "regtest")] 222 | Regtest, 223 | } 224 | 225 | /// Defines how to filter results from [`BitcoinCanister::get_utxos_query`]. 226 | #[derive(Debug, Clone, CandidType, Deserialize)] 227 | pub enum UtxosFilter { 228 | /// Filter by the minimum number of UTXO confirmations. Most applications should set this to 6. 229 | #[serde(rename = "min_confirmations")] 230 | MinConfirmations(u32), 231 | /// When paginating results, use this page. Provided by [`GetUtxosResponse.next_page`](GetUtxosResponse). 232 | #[serde(rename = "page")] 233 | Page(#[serde(with = "serde_bytes")] Vec), 234 | } 235 | 236 | /// Unique output descriptor of a Bitcoin transaction. 237 | #[derive(Debug, Clone, CandidType, Deserialize)] 238 | pub struct UtxoOutpoint { 239 | /// The ID of the transaction. Not necessarily unique on its own. 240 | #[serde(with = "serde_bytes")] 241 | pub txid: Vec, 242 | /// The index of the outpoint within the transaction. 243 | pub vout: u32, 244 | } 245 | 246 | /// A Bitcoin [`UTXO`](https://en.wikipedia.org/wiki/Unspent_transaction_output), produced by a transaction. 247 | #[derive(Debug, Clone, CandidType, Deserialize)] 248 | pub struct Utxo { 249 | /// The transaction outpoint that produced this UTXO. 250 | pub outpoint: UtxoOutpoint, 251 | /// The BTC quantity, in satoshis. 252 | pub value: u64, 253 | /// The block index this transaction was placed at. 254 | pub height: u32, 255 | } 256 | 257 | /// Response type for the [`BitcoinCanister::get_utxos_query`] function. 258 | #[derive(Debug, Clone, CandidType, Deserialize)] 259 | pub struct GetUtxosResponse { 260 | /// A list of UTXOs available for the specified address. 261 | pub utxos: Vec, 262 | /// The hash of the tip. 263 | #[serde(with = "serde_bytes")] 264 | pub tip_block_hash: Vec, 265 | /// The block index of the tip of the chain known to the IC. 266 | pub tip_height: u32, 267 | /// If `Some`, then `utxos` does not contain the entire results of the query. 268 | /// Call `bitcoin_get_utxos_query` again using `UtxosFilter::Page` for the next page of results. 269 | pub next_page: Option>, 270 | } 271 | 272 | /// Response type for the [`BitcoinCanister::get_block_headers`] function. 273 | #[derive(Debug, Clone, CandidType, Deserialize)] 274 | pub struct GetBlockHeadersResponse { 275 | /// The tip of the chain, current to when the headers were fetched. 276 | pub tip_height: u32, 277 | /// The headers of the requested block range. 278 | pub block_headers: Vec>, 279 | } 280 | -------------------------------------------------------------------------------- /ic-utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! ic-utils is a collection of utilities to help build clients and canisters running 2 | //! on the Internet Computer. It is meant as a higher level tool. 3 | 4 | #![warn( 5 | missing_docs, 6 | missing_debug_implementations, 7 | elided_lifetimes_in_paths, 8 | rustdoc::broken_intra_doc_links, 9 | rustdoc::private_intra_doc_links 10 | )] 11 | #![cfg_attr(not(target_family = "wasm"), warn(clippy::future_not_send))] 12 | #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] 13 | 14 | /// Utilities to encapsulate calls to a canister. 15 | pub mod call; 16 | /// A higher-level canister type for managing various aspects of a canister. 17 | pub mod canister; 18 | /// A few known canister types for use with [`Canister`]. 19 | pub mod interfaces; 20 | 21 | pub use canister::{Argument, Canister}; 22 | -------------------------------------------------------------------------------- /icx-cert/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "icx-cert" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | rust-version.workspace = true 9 | description = "CLI tool to download a document from the Internet Computer and pretty-print the contents of its IC-Certificate header." 10 | homepage = "https://docs.rs/icx-cert" 11 | documentation = "https://docs.rs/icx-cert" 12 | readme = "README.md" 13 | categories = ["command-line-interface"] 14 | keywords = ["internet-computer", "agent", "icp", "dfinity", "certificate"] 15 | include = ["src", "Cargo.toml", "../LICENSE", "README.md"] 16 | 17 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 18 | 19 | [dependencies] 20 | anyhow = "1.0" 21 | base64 = "0.22" 22 | clap = { workspace = true, features = ["derive", "cargo", "color"] } 23 | hex = { workspace = true } 24 | ic-certification = { workspace = true } 25 | leb128 = "0.2.4" 26 | reqwest = { workspace = true, default-features = false, features = [ 27 | "blocking", 28 | "rustls-tls", 29 | ] } 30 | sha2 = { workspace = true } 31 | serde = { workspace = true, features = ["derive"] } 32 | serde_bytes = { workspace = true } 33 | serde_cbor = { workspace = true } 34 | time = { workspace = true, features = ["formatting"] } 35 | -------------------------------------------------------------------------------- /icx-cert/README.md: -------------------------------------------------------------------------------- 1 | # `ic-cert` 2 | This is a command-line tool for debugging certified HTTP responses. 3 | The tool downloads the document with the specified URL and pretty-prints the contents of the IC-Certificate header. 4 | -------------------------------------------------------------------------------- /icx-cert/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::{crate_authors, crate_version, Parser}; 3 | 4 | mod pprint; 5 | 6 | #[derive(Parser)] 7 | #[command( 8 | version = crate_version!(), 9 | author = crate_authors!(), 10 | )] 11 | enum Command { 12 | /// Fetches the specified URL and pretty-prints the certificate. 13 | #[clap(name = "print")] 14 | PPrint { 15 | url: String, 16 | 17 | /// Specifies one or more encodings to accept. 18 | #[arg(long)] 19 | accept_encoding: Option>, 20 | }, 21 | } 22 | 23 | fn main() -> Result<()> { 24 | match Command::parse() { 25 | Command::PPrint { 26 | url, 27 | accept_encoding, 28 | } => pprint::pprint(url, accept_encoding), 29 | } 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | use super::Command; 35 | use clap::CommandFactory; 36 | 37 | #[test] 38 | fn valid_command() { 39 | Command::command().debug_assert(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /icx-cert/src/pprint.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context, Result}; 2 | use base64::prelude::*; 3 | use ic_certification::{HashTree, LookupResult}; 4 | use reqwest::header; 5 | use serde::{de::DeserializeOwned, Deserialize}; 6 | use sha2::Digest; 7 | use time::{format_description::well_known::Rfc3339, OffsetDateTime}; 8 | 9 | /// Structured contents of the IC-Certificate header. 10 | struct StructuredCertHeader<'a> { 11 | certificate: &'a str, 12 | tree: &'a str, 13 | } 14 | 15 | /// A fully parsed replica certificate. 16 | #[derive(Deserialize)] 17 | struct ReplicaCertificate { 18 | tree: HashTree, 19 | signature: serde_bytes::ByteBuf, 20 | } 21 | 22 | /// Parses the value of IC-Certificate header. 23 | fn parse_structured_cert_header(value: &str) -> Result> { 24 | fn extract_field<'a>(value: &'a str, field_name: &'a str, prefix: &'a str) -> Result<&'a str> { 25 | let start = value.find(prefix).ok_or_else(|| { 26 | anyhow!( 27 | "Certificate header doesn't have '{}' field: {}", 28 | field_name, 29 | value, 30 | ) 31 | })? + prefix.len(); 32 | let len = value[start..].find(':').ok_or_else(|| { 33 | anyhow!( 34 | "malformed '{}' field: no ending colon found: {}", 35 | prefix, 36 | value 37 | ) 38 | })?; 39 | Ok(&value[start..(start + len)]) 40 | } 41 | 42 | Ok(StructuredCertHeader { 43 | certificate: extract_field(value, "certificate", "certificate=:")?, 44 | tree: extract_field(value, "tree", "tree=:")?, 45 | }) 46 | } 47 | 48 | /// Decodes base64-encoded CBOR value. 49 | fn parse_base64_cbor(s: &str) -> Result { 50 | let bytes = BASE64_STANDARD.decode(s).with_context(|| { 51 | format!( 52 | "failed to parse {}: invalid base64 {}", 53 | std::any::type_name::(), 54 | s 55 | ) 56 | })?; 57 | serde_cbor::from_slice(&bytes[..]).with_context(|| { 58 | format!( 59 | "failed to parse {}: malformed CBOR", 60 | std::any::type_name::() 61 | ) 62 | }) 63 | } 64 | 65 | /// Downloads the asset with the specified URL and pretty-print certificate contents. 66 | pub fn pprint(url: String, accept_encodings: Option>) -> Result<()> { 67 | let response = { 68 | let client = reqwest::blocking::Client::builder(); 69 | let client = if let Some(accept_encodings) = accept_encodings { 70 | let mut headers = header::HeaderMap::new(); 71 | let accept_encodings: String = accept_encodings.join(", "); 72 | headers.insert( 73 | "Accept-Encoding", 74 | header::HeaderValue::from_str(&accept_encodings).unwrap(), 75 | ); 76 | client.default_headers(headers) 77 | } else { 78 | client 79 | }; 80 | client 81 | .user_agent("icx-cert") 82 | .build()? 83 | .get(url) 84 | .send() 85 | .with_context(|| "failed to fetch the document")? 86 | }; 87 | 88 | let status = response.status().as_u16(); 89 | let certificate_header = response 90 | .headers() 91 | .get("IC-Certificate") 92 | .ok_or_else(|| anyhow!("IC-Certificate header not found: {:?}", response.headers()))? 93 | .to_owned(); 94 | let content_encoding = response 95 | .headers() 96 | .get("Content-Encoding") 97 | .map(|x| x.to_owned()); 98 | let data = response 99 | .bytes() 100 | .with_context(|| "failed to get response body")?; 101 | let certificate_str = certificate_header.to_str().with_context(|| { 102 | format!("failed to convert certificate header {certificate_header:?} to string") 103 | })?; 104 | let structured_header = parse_structured_cert_header(certificate_str)?; 105 | let tree: HashTree = parse_base64_cbor(structured_header.tree)?; 106 | let cert: ReplicaCertificate = parse_base64_cbor(structured_header.certificate)?; 107 | 108 | println!("STATUS: {status}"); 109 | println!("ROOT HASH: {}", hex::encode(cert.tree.digest())); 110 | if let Some(content_encoding) = content_encoding { 111 | println!("CONTENT-ENCODING: {}", content_encoding.to_str().unwrap()); 112 | } 113 | println!( 114 | "DATA HASH: {}", 115 | hex::encode(sha2::Sha256::digest(data.as_ref())) 116 | ); 117 | println!("TREE HASH: {}", hex::encode(tree.digest())); 118 | println!("SIGNATURE: {}", hex::encode(cert.signature.as_ref())); 119 | if let LookupResult::Found(mut date_bytes) = cert.tree.lookup_path(&["time"]) { 120 | let timestamp_nanos = leb128::read::unsigned(&mut date_bytes) 121 | .with_context(|| "failed to decode certificate time as LEB128")?; 122 | let dt = OffsetDateTime::from_unix_timestamp_nanos(timestamp_nanos.into()) 123 | .context("timestamp out of range")?; 124 | println!("CERTIFICATE TIME: {}", dt.format(&Rfc3339)?); 125 | } 126 | println!("CERTIFICATE TREE: {:#?}", cert.tree); 127 | println!("TREE: {tree:#?}"); 128 | Ok(()) 129 | } 130 | 131 | #[test] 132 | fn test_parse_structured_header() { 133 | let header = parse_structured_cert_header("certificate=:abcdef:, tree=:010203:").unwrap(); 134 | assert_eq!(header.certificate, "abcdef"); 135 | assert_eq!(header.tree, "010203"); 136 | } 137 | -------------------------------------------------------------------------------- /icx/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "icx" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | rust-version.workspace = true 9 | description = "CLI tool to call canisters on the Internet Computer." 10 | homepage = "https://docs.rs/icx" 11 | documentation = "https://docs.rs/icx" 12 | readme = "README.md" 13 | categories = ["command-line-interface", "web-programming::http-client"] 14 | keywords = ["internet-computer", "agent", "icp", "dfinity", "call"] 15 | include = ["src", "Cargo.toml", "../LICENSE", "README.md"] 16 | 17 | [lints] 18 | workspace = true 19 | 20 | [[bin]] 21 | name = "icx" 22 | path = "src/main.rs" 23 | 24 | [dependencies] 25 | anyhow = { version = "1.0", features = ["backtrace"] } 26 | candid = { workspace = true, features = ["value"] } 27 | candid_parser = { workspace = true } 28 | clap = { workspace = true, features = ["derive", "cargo", "color"] } 29 | ed25519-consensus = { workspace = true } 30 | hex = { workspace = true } 31 | humantime = "2.0.1" 32 | ic-agent = { workspace = true, default-features = true } 33 | ic-utils = { workspace = true } 34 | pocket-ic = { workspace = true } 35 | rand = { workspace = true } 36 | serde = { workspace = true } 37 | serde_json = { workspace = true } 38 | tokio = { workspace = true, features = ["full"] } 39 | -------------------------------------------------------------------------------- /icx/README.md: -------------------------------------------------------------------------------- 1 | # `icx` 2 | A command line tool to use the `ic-agent` crate directly. It allows simple communication with 3 | the Internet Computer. 4 | 5 | ## Installing `icx` 6 | To install `icx` you will have to build it locally, using `cargo`. Make a clone of this repository, 7 | then in it simply run `cargo build`: 8 | 9 | ```sh 10 | git clone https://github.com/dfinity/agent-rust.git 11 | cd agent-rust 12 | cargo build 13 | ``` 14 | 15 | The output executable will be in `target/debug/icx`. 16 | 17 | ## Using `icx` 18 | To get help, simply use `icx --help`. 19 | 20 | ### Identity 21 | To read a PEM file, you can pass it with the `--pem` argument. The PEM file must be a valid 22 | key that can be used for the Internet Computer signing and validation. 23 | 24 | ### Root Key 25 | For non-IC networks, pass `--fetch-root-key` to fetch the root key. When this argument is not present, 26 | icx uses the hard-coded public key. 27 | 28 | ### Examples 29 | To call the management canister's `create_canister` function, you can use the following: 30 | 31 | ```shell script 32 | icx update aaaaa-aa create_canister 33 | ``` 34 | 35 | If you have a candid file, you can use it to validate arguments. Pass it in with the 36 | `--candid=path/to/the/file.did` argument: 37 | 38 | ```shell script 39 | icx query 75hes-oqbaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-q greet --candid=~/path/greet.did '("World")' 40 | ``` 41 | 42 | #### Sign and Send Separation 43 | Pass `--serialize` when use `icx update` and `icx query` will serialize the signed canister call message as json. 44 | The output will print to stdout. When use `icx update`, a corresponding request_status message is also generated and printed to stderr. 45 | 46 | In the default IC project generated with `dfx new` and the local emulator has started with `dfx start --background`. 47 | 48 | ##### Sign 49 | ```shell script 50 | icx --fetch-root-key update --serialize rwlgt-iiaaa-aaaaa-aaaaa-cai greet '("everyone")' > output.txt 51 | head -n 1 output.txt > update.json 52 | tail -n 1 output.txt > request_status.json 53 | ``` 54 | > `rwlgt-iiaaa-aaaaa-aaaaa-cai` is the ID of hello canister in the default project. 55 | 56 | ##### Send 57 | ```shell script 58 | cat update.json | icx send 59 | ... 60 | RequestID: 0x1234.... 61 | ``` 62 | 63 | ##### Request status 64 | ```shell script 65 | cat request_status.json | icx --fetch-root-key send 66 | ... 67 | ("Hello, everyone!") 68 | ``` 69 | 70 | When sending message to the IC main net, all `--fech-root-key` are not required. So the sign step can be executed on an air-gapped machine. -------------------------------------------------------------------------------- /ref-tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ref-tests" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | candid = { workspace = true } 11 | ed25519-consensus = { workspace = true } 12 | ic-agent = { path = "../ic-agent" } 13 | ic-identity-hsm = { path = "../ic-identity-hsm" } 14 | ic-utils = { path = "../ic-utils", features = ["raw"] } 15 | k256 = { workspace = true } 16 | p256 = { workspace = true } 17 | pocket-ic = { workspace = true } 18 | rand = { workspace = true } 19 | serde = { workspace = true, features = ["derive"] } 20 | sha2 = { workspace = true } 21 | tokio = { workspace = true, features = ["full"] } 22 | 23 | [dev-dependencies] 24 | serde_cbor = { workspace = true } 25 | ic-certification = { workspace = true } 26 | -------------------------------------------------------------------------------- /ref-tests/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod universal_canister; 2 | pub mod utils; 3 | 4 | pub use utils::*; 5 | -------------------------------------------------------------------------------- /ref-tests/src/universal_canister.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | //! The Universal Canister (UC) is a canister built in Rust, compiled to Wasm, 3 | //! and serves as a canister that can be used for a multitude of tests. 4 | //! 5 | //! Payloads to UC can execute any arbitrary sequence of system methods, making 6 | //! it possible to test different canister behaviors without having to write up 7 | //! custom Wat files. 8 | use ic_agent::export::Principal; 9 | 10 | /// Operands used in encoding UC payloads. 11 | #[repr(u8)] 12 | enum Ops { 13 | Noop = 0, 14 | PushInt = 2, 15 | PushBytes = 3, 16 | ReplyDataAppend = 4, 17 | Reply = 5, 18 | Self_ = 6, 19 | Reject = 7, 20 | Caller = 8, 21 | CallSimple = 9, 22 | RejectMessage = 10, 23 | RejectCode = 11, 24 | IntToBlob = 12, 25 | MessagePayload = 13, 26 | StableSize = 15, 27 | StableGrow = 16, 28 | StableRead = 17, 29 | StableWrite = 18, 30 | DebugPrint = 19, 31 | Trap = 20, 32 | SetGlobal = 21, 33 | GetGlobal = 22, 34 | } 35 | 36 | /// A succinct shortcut for creating a `PayloadBuilder`, which is used to encode 37 | /// instructions to be executed by the UC. 38 | /// 39 | /// Example usage: 40 | /// ``` 41 | /// use ref_tests::universal_canister::payload; 42 | /// // Instruct the UC to reply with the bytes encoding "Hello" 43 | /// let bytes = payload().reply_data(b"Hello").build(); 44 | /// ``` 45 | pub fn payload() -> PayloadBuilder { 46 | PayloadBuilder::default() 47 | } 48 | 49 | /// A builder class for building payloads for the universal canister. 50 | /// 51 | /// Payloads for the UC encode `Ops` representing what instructions to 52 | /// execute. 53 | #[derive(Default)] 54 | pub struct PayloadBuilder(Vec); 55 | 56 | impl PayloadBuilder { 57 | fn op(self, b: Ops) -> Self { 58 | self.byte(b as u8) 59 | } 60 | 61 | fn byte(mut self, b: u8) -> Self { 62 | self.0.push(b); 63 | self 64 | } 65 | 66 | fn bytes(mut self, b: &[u8]) -> Self { 67 | self.0.extend_from_slice(b); 68 | self 69 | } 70 | 71 | pub fn push_int(self, int: u32) -> Self { 72 | self.op(Ops::PushInt).bytes(&int.to_le_bytes()) 73 | } 74 | 75 | pub fn reply_data(self, data: &[u8]) -> Self { 76 | self.push_bytes(data).reply_data_append().reply() 77 | } 78 | 79 | pub fn reply_int(self) -> Self { 80 | self.int_to_blob().reply_data_append().reply() 81 | } 82 | 83 | pub fn reply_data_append(self) -> Self { 84 | self.op(Ops::ReplyDataAppend) 85 | } 86 | 87 | pub fn append_and_reply(self) -> Self { 88 | self.reply_data_append().reply() 89 | } 90 | 91 | pub fn int_to_blob(self) -> Self { 92 | self.op(Ops::IntToBlob) 93 | } 94 | 95 | pub fn reply(self) -> Self { 96 | self.op(Ops::Reply) 97 | } 98 | 99 | pub fn stable_size(self) -> Self { 100 | self.op(Ops::StableSize) 101 | } 102 | 103 | pub fn push_bytes(self, data: &[u8]) -> Self { 104 | self.op(Ops::PushBytes) 105 | .bytes(&(data.len() as u32).to_le_bytes()) 106 | .bytes(data) 107 | } 108 | 109 | pub fn stable_grow(self, additional_pages: u32) -> Self { 110 | self.push_int(additional_pages).op(Ops::StableGrow) 111 | } 112 | 113 | pub fn stable_read(self, offset: u32, size: u32) -> Self { 114 | self.push_int(offset).push_int(size).op(Ops::StableRead) 115 | } 116 | 117 | pub fn stable_write(self, offset: u32, data: &[u8]) -> Self { 118 | self.push_int(offset).push_bytes(data).op(Ops::StableWrite) 119 | } 120 | 121 | /// A query from a UC to another UC. 122 | pub fn inter_query>(self, callee: P, call_args: CallArgs) -> Self { 123 | self.call_simple(callee, "query", call_args) 124 | } 125 | 126 | /// An update from a UC to another UC. 127 | pub fn inter_update>(self, callee: P, call_args: CallArgs) -> Self { 128 | self.call_simple(callee, "update", call_args) 129 | } 130 | 131 | pub fn call_simple>( 132 | self, 133 | callee: P, 134 | method: &str, 135 | call_args: CallArgs, 136 | ) -> Self { 137 | self.push_bytes(callee.into().as_slice()) 138 | .push_bytes(method.as_bytes()) 139 | .push_bytes(call_args.on_reply.as_slice()) 140 | .push_bytes(call_args.on_reject.as_slice()) 141 | .push_bytes(call_args.other_side.as_slice()) 142 | .op(Ops::CallSimple) 143 | } 144 | 145 | pub fn message_payload(self) -> Self { 146 | self.op(Ops::MessagePayload) 147 | } 148 | 149 | pub fn reject_message(self) -> Self { 150 | self.op(Ops::RejectMessage) 151 | } 152 | 153 | pub fn reject_code(self) -> Self { 154 | self.op(Ops::RejectCode) 155 | } 156 | 157 | pub fn reject(self) -> Self { 158 | self.op(Ops::Reject) 159 | } 160 | 161 | pub fn noop(self) -> Self { 162 | self.op(Ops::Noop) 163 | } 164 | 165 | pub fn caller(self) -> Self { 166 | self.op(Ops::Caller) 167 | } 168 | 169 | pub fn self_(self) -> Self { 170 | self.op(Ops::Self_) 171 | } 172 | 173 | /// Store data (in a global variable) on the heap. 174 | /// NOTE: This does _not_ correspond to a Wasm global. 175 | pub fn set_global_data(self, data: &[u8]) -> Self { 176 | self.push_bytes(data).op(Ops::SetGlobal) 177 | } 178 | 179 | /// Get data (stored in a global variable) from the heap. 180 | /// NOTE: This does _not_ correspond to a Wasm global. 181 | pub fn get_global_data(self) -> Self { 182 | self.op(Ops::GetGlobal) 183 | } 184 | 185 | pub fn debug_print(self, msg: &[u8]) -> Self { 186 | self.push_bytes(msg).op(Ops::DebugPrint) 187 | } 188 | 189 | pub fn trap_with_blob(self, data: &[u8]) -> Self { 190 | self.push_bytes(data).op(Ops::Trap) 191 | } 192 | 193 | pub fn trap(self) -> Self { 194 | self.trap_with_blob(&[]) // No data provided for trap. 195 | } 196 | 197 | pub fn build(self) -> Vec { 198 | self.0 199 | } 200 | } 201 | 202 | /// Arguments to be passed into `call_simple`. 203 | pub struct CallArgs { 204 | pub on_reply: Vec, 205 | pub on_reject: Vec, 206 | pub other_side: Vec, 207 | } 208 | 209 | impl Default for CallArgs { 210 | fn default() -> Self { 211 | Self { 212 | on_reply: Self::default_on_reply(), 213 | on_reject: Self::default_on_reject(), 214 | other_side: Self::default_other_side(), 215 | } 216 | } 217 | } 218 | 219 | impl CallArgs { 220 | pub fn on_reply>>(mut self, callback: C) -> Self { 221 | self.on_reply = callback.into(); 222 | self 223 | } 224 | 225 | pub fn on_reject>>(mut self, callback: C) -> Self { 226 | self.on_reject = callback.into(); 227 | self 228 | } 229 | 230 | pub fn other_side>>(mut self, callback: C) -> Self { 231 | self.other_side = callback.into(); 232 | self 233 | } 234 | 235 | // The default on_reply callback. 236 | // Replies to the caller with whatever arguments passed to it. 237 | fn default_on_reply() -> Vec { 238 | PayloadBuilder::default() 239 | .message_payload() 240 | .reply_data_append() 241 | .reply() 242 | .build() 243 | } 244 | 245 | // The default on_reject callback. 246 | // Replies to the caller with the reject code and message. 247 | fn default_on_reject() -> Vec { 248 | PayloadBuilder::default() 249 | .reject_code() 250 | .int_to_blob() 251 | .reply_data_append() 252 | .reject_message() 253 | .reply_data_append() 254 | .reply() 255 | .build() 256 | } 257 | 258 | // The default payload to be executed by the callee. 259 | // Replies with a message stating who the callee and the caller is. 260 | fn default_other_side() -> Vec { 261 | PayloadBuilder::default() 262 | .push_bytes(b"Hello ") 263 | .reply_data_append() 264 | .caller() 265 | .reply_data_append() 266 | .push_bytes(b" this is ") 267 | .reply_data_append() 268 | .self_() 269 | .reply_data_append() 270 | .reply() 271 | .build() 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /ref-tests/src/utils.rs: -------------------------------------------------------------------------------- 1 | use ed25519_consensus::SigningKey; 2 | use ic_agent::identity::{Prime256v1Identity, Secp256k1Identity}; 3 | use ic_agent::{export::Principal, identity::BasicIdentity, Agent, Identity}; 4 | use ic_identity_hsm::HardwareIdentity; 5 | use ic_utils::interfaces::{management_canister::builders::MemoryAllocation, ManagementCanister}; 6 | use rand::thread_rng; 7 | use std::{convert::TryFrom, error::Error, future::Future, path::Path}; 8 | 9 | const HSM_PKCS11_LIBRARY_PATH: &str = "HSM_PKCS11_LIBRARY_PATH"; 10 | const HSM_SLOT_INDEX: &str = "HSM_SLOT_INDEX"; 11 | const HSM_KEY_ID: &str = "HSM_KEY_ID"; 12 | const HSM_PIN: &str = "HSM_PIN"; 13 | const POCKET_IC: &str = "POCKET_IC"; 14 | 15 | pub async fn get_effective_canister_id() -> Principal { 16 | let default_effective_canister_id = 17 | Principal::from_text("rwlgt-iiaaa-aaaaa-aaaaa-cai").unwrap(); 18 | if let Ok(pocket_ic_url) = std::env::var(POCKET_IC) { 19 | pocket_ic::nonblocking::get_default_effective_canister_id(pocket_ic_url) 20 | .await 21 | .unwrap_or(default_effective_canister_id) 22 | } else { 23 | default_effective_canister_id 24 | } 25 | } 26 | 27 | pub fn create_identity() -> Result, String> { 28 | if std::env::var(HSM_PKCS11_LIBRARY_PATH).is_ok() { 29 | create_hsm_identity().map(|x| Box::new(x) as _) 30 | } else { 31 | create_basic_identity().map(|x| Box::new(x) as _) 32 | } 33 | } 34 | 35 | fn expect_env_var(name: &str) -> Result { 36 | std::env::var(name).map_err(|_| format!("Need to specify the {} environment variable", name)) 37 | } 38 | 39 | pub fn create_hsm_identity() -> Result { 40 | let path = expect_env_var(HSM_PKCS11_LIBRARY_PATH)?; 41 | let slot_index = expect_env_var(HSM_SLOT_INDEX)? 42 | .parse::() 43 | .map_err(|e| format!("Unable to parse {} value: {}", HSM_SLOT_INDEX, e))?; 44 | let key = expect_env_var(HSM_KEY_ID)?; 45 | let id = HardwareIdentity::new(path, slot_index, &key, get_hsm_pin) 46 | .map_err(|e| format!("Unable to create hw identity: {}", e))?; 47 | Ok(id) 48 | } 49 | 50 | fn get_hsm_pin() -> Result { 51 | expect_env_var(HSM_PIN) 52 | } 53 | 54 | // The SoftHSM library doesn't like to have two contexts created/initialized at once. 55 | // Trying to create two HardwareIdentity instances at the same time results in this error: 56 | // Unable to create hw identity: PKCS#11: CKR_CRYPTOKI_ALREADY_INITIALIZED (0x191) 57 | // 58 | // To avoid this, we use a basic identity for any second identity in tests. 59 | // 60 | // A shared container of Ctx objects might be possible instead, but my rust-fu is inadequate. 61 | pub fn create_basic_identity() -> Result { 62 | let sk = SigningKey::new(thread_rng()); 63 | 64 | Ok(BasicIdentity::from_signing_key(sk)) 65 | } 66 | 67 | /// Create a secp256k1identity, which unfortunately will always be the same one 68 | /// (So can only use one per test) 69 | pub fn create_secp256k1_identity() -> Result { 70 | // generated from the the following commands: 71 | // $ openssl ecparam -name secp256k1 -genkey -noout -out identity.pem 72 | // $ cat identity.pem 73 | let identity_file = " 74 | -----BEGIN EC PRIVATE KEY----- 75 | MHQCAQEEIJb2C89BvmJERgnT/vJLKpdHZb/hqTiC8EY2QtBRWZScoAcGBSuBBAAK 76 | oUQDQgAEDMl7g3vGKLsiLDA3fBRxDE9ZkM3GezZFa5HlKM/gYzNZfU3w8Tijjd73 77 | yeMC60IsMNxDjLqElV7+T7dkb5Ki7Q== 78 | -----END EC PRIVATE KEY-----"; 79 | 80 | let identity = Secp256k1Identity::from_pem(identity_file.as_bytes()) 81 | .expect("Cannot create secp256k1 identity from PEM file."); 82 | Ok(identity) 83 | } 84 | 85 | pub fn create_prime256v1_identity() -> Result { 86 | // generated from the following command: 87 | // $ openssl ecparam -name prime256v1 -genkey -noout -out identity.pem 88 | // $ cat identity.pem 89 | let identity_file = "\ 90 | -----BEGIN EC PRIVATE KEY----- 91 | MHcCAQEEIL1ybmbwx+uKYsscOZcv71MmKhrNqfPP0ke1unET5AY4oAoGCCqGSM49 92 | AwEHoUQDQgAEUbbZV4NerZTPWfbQ749/GNLu8TaH8BUS/I7/+ipsu+MPywfnBFIZ 93 | Sks4xGbA/ZbazsrMl4v446U5UIVxCGGaKw== 94 | -----END EC PRIVATE KEY-----"; 95 | 96 | let identity = Prime256v1Identity::from_pem(identity_file.as_bytes()) 97 | .expect("Cannot create prime256v1 identity from PEM file."); 98 | Ok(identity) 99 | } 100 | 101 | pub async fn create_agent(identity: impl Identity + 'static) -> Result { 102 | let port_env = std::env::var("IC_REF_PORT").unwrap_or_else(|_| "4943".into()); 103 | let port = port_env 104 | .parse::() 105 | .expect("Could not parse the IC_REF_PORT environment variable as an integer."); 106 | let builder = Agent::builder().with_url(format!("http://127.0.0.1:{port}")); 107 | builder 108 | .with_identity(identity) 109 | .build() 110 | .map_err(|e| format!("{:?}", e)) 111 | } 112 | 113 | pub fn with_agent(f: F) 114 | where 115 | R: Future>>, 116 | F: FnOnce(Agent) -> R, 117 | { 118 | let agent_identity = create_identity().expect("Could not create an identity."); 119 | with_agent_as(agent_identity, f) 120 | } 121 | 122 | pub fn with_agent_as(agent_identity: I, f: F) 123 | where 124 | I: Identity + 'static, 125 | R: Future>>, 126 | F: FnOnce(Agent) -> R, 127 | { 128 | let runtime = tokio::runtime::Runtime::new().expect("Could not create tokio runtime."); 129 | runtime.block_on(async { 130 | let agent = create_agent(agent_identity) 131 | .await 132 | .expect("Could not create an agent."); 133 | agent 134 | .fetch_root_key() 135 | .await 136 | .expect("could not fetch root key"); 137 | match f(agent).await { 138 | Ok(_) => {} 139 | Err(e) => panic!("{:?}", e), 140 | }; 141 | }) 142 | } 143 | 144 | pub async fn create_universal_canister(agent: &Agent) -> Result> { 145 | let canister_env = std::env::var("IC_UNIVERSAL_CANISTER_PATH") 146 | .expect("Need to specify the IC_UNIVERSAL_CANISTER_PATH environment variable."); 147 | 148 | let canister_path = Path::new(&canister_env); 149 | 150 | let canister_wasm = if !canister_path.exists() { 151 | panic!("Could not find the universal canister WASM file."); 152 | } else { 153 | std::fs::read(canister_path).expect("Could not read file.") 154 | }; 155 | 156 | let ic00 = ManagementCanister::create(agent); 157 | 158 | let (canister_id,) = ic00 159 | .create_canister() 160 | .as_provisional_create_with_amount(None) 161 | .with_effective_canister_id(get_effective_canister_id().await) 162 | .call_and_wait() 163 | .await?; 164 | 165 | ic00.install_code(&canister_id, &canister_wasm) 166 | .with_raw_arg(vec![]) 167 | .call_and_wait() 168 | .await?; 169 | 170 | Ok(canister_id) 171 | } 172 | 173 | pub fn get_wallet_wasm_from_env() -> Vec { 174 | let canister_env = std::env::var("IC_WALLET_CANISTER_PATH") 175 | .expect("Need to specify the IC_WALLET_CANISTER_PATH environment variable."); 176 | 177 | let canister_path = Path::new(&canister_env); 178 | 179 | if !canister_path.exists() { 180 | panic!("Could not find the wallet canister WASM file."); 181 | } else { 182 | std::fs::read(canister_path).expect("Could not read file.") 183 | } 184 | } 185 | 186 | pub async fn create_wallet_canister( 187 | agent: &Agent, 188 | cycles: Option, 189 | ) -> Result> { 190 | let canister_wasm = get_wallet_wasm_from_env(); 191 | 192 | let ic00 = ManagementCanister::create(agent); 193 | 194 | let (canister_id,) = ic00 195 | .create_canister() 196 | .as_provisional_create_with_amount(cycles) 197 | .with_effective_canister_id(get_effective_canister_id().await) 198 | .with_memory_allocation( 199 | MemoryAllocation::try_from(8000000000_u64) 200 | .expect("Memory allocation must be between 0 and 2^48 (i.e 256TB), inclusively."), 201 | ) 202 | .call_and_wait() 203 | .await?; 204 | 205 | ic00.install_code(&canister_id, &canister_wasm) 206 | .with_raw_arg(vec![]) 207 | .call_and_wait() 208 | .await?; 209 | 210 | Ok(canister_id) 211 | } 212 | 213 | pub fn with_universal_canister(f: F) 214 | where 215 | R: Future>>, 216 | F: FnOnce(Agent, Principal) -> R, 217 | { 218 | with_agent(|agent| async move { 219 | let canister_id = create_universal_canister(&agent).await?; 220 | f(agent, canister_id).await 221 | }) 222 | } 223 | 224 | pub fn with_universal_canister_as(identity: I, f: F) 225 | where 226 | I: Identity + 'static, 227 | R: Future>>, 228 | F: FnOnce(Agent, Principal) -> R, 229 | { 230 | with_agent_as(identity, |agent| async move { 231 | let canister_id = create_universal_canister(&agent).await?; 232 | f(agent, canister_id).await 233 | }) 234 | } 235 | 236 | pub fn with_wallet_canister(cycles: Option, f: F) 237 | where 238 | R: Future>>, 239 | F: FnOnce(Agent, Principal) -> R, 240 | { 241 | with_agent(|agent| async move { 242 | let canister_id = create_wallet_canister(&agent, cycles).await?; 243 | f(agent, canister_id).await 244 | }) 245 | } 246 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | # MSRV 3 | # Avoid updating this field unless we use new Rust features 4 | # Sync rust-version in workspace Cargo.toml 5 | channel = "1.78.0" 6 | components = ["rustfmt", "clippy"] 7 | targets = ["wasm32-unknown-unknown"] 8 | -------------------------------------------------------------------------------- /scripts/cargo_publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | cargo publish -p ic-transport-types 4 | cargo publish -p ic-agent 5 | cargo publish -p ic-identity-hsm 6 | cargo publish -p ic-utils 7 | cargo publish -p icx 8 | cargo publish -p icx-cert 9 | --------------------------------------------------------------------------------