├── .dockerignore ├── .github ├── CODEOWNERS └── workflows │ ├── ci.yml │ ├── release-with-github.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── INTERFACE_SPECIFICATION.md ├── LICENSE ├── README.md ├── assets ├── Cycles_Icon.png └── Cycles_Icon.svg ├── canister_ids.json ├── cycles-ledger ├── Cargo.toml ├── cycles-ledger.did ├── src │ ├── compact_account.rs │ ├── config.rs │ ├── endpoints.rs │ ├── lib.rs │ ├── logs.rs │ ├── main.rs │ ├── memo.rs │ └── storage.rs └── tests │ ├── client.rs │ ├── gen.rs │ └── tests.rs ├── depositor ├── Cargo.toml ├── depositor.did └── src │ ├── endpoints.rs │ ├── lib.rs │ └── main.rs ├── dfx.json ├── download-state-machine.sh ├── fake-cmc ├── Cargo.toml ├── fake-cmc.did └── src │ ├── lib.rs │ └── main.rs ├── rust-toolchain.toml ├── rustfmt.toml └── scripts └── docker-build /.dockerignore: -------------------------------------------------------------------------------- 1 | # git 2 | .git/ 3 | .github/ 4 | 5 | # Various IDEs and Editors 6 | .vscode/ 7 | .idea/ 8 | **/*~ 9 | 10 | # Mac OSX temporary files 11 | .DS_Store 12 | **/.DS_Store 13 | 14 | # dfx temporary files 15 | .dfx/ 16 | 17 | # rust 18 | target/ 19 | 20 | # General files to ignore 21 | cycles-ledger.wasm -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @dfinity/sdk 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Check and Test 2 | on: [pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | ic-commit: [ 072b2a6586c409efa88f2244d658307ff3a645d8 ] 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | # This is needed for building state-machine-tests 15 | - name: Install proto 16 | run: | 17 | sudo apt update 18 | sudo apt install -y protobuf-compiler libprotobuf-dev 19 | 20 | - name: Cache Cargo 21 | uses: actions/cache@v4 22 | with: 23 | path: | 24 | ~/.cargo/registry 25 | ~/.cargo/git 26 | target 27 | key: ${{ matrix.build }}-cargo-${{ hashFiles('**/Cargo.lock') }} 28 | restore-keys: | 29 | ${{ matrix.build }}-cargo- 30 | 31 | - name: Install Rust 32 | run: | 33 | rustup show 34 | 35 | - name: Cache StateMachine 36 | uses: actions/cache@v4 37 | with: 38 | path: ic-test-state-machine 39 | key: ${{ matrix.ic-commit }}-statemachine-binary 40 | 41 | - name: Download StateMachine binary 42 | run: ./download-state-machine.sh ${{ matrix.ic-commit }} linux 43 | 44 | - name: Check Format 45 | run: cargo fmt --all -- --check 46 | 47 | - name: Clippy 48 | run: cargo clippy --tests --benches -- -D clippy::all 49 | 50 | - name: Test 51 | run: cargo test 52 | env: 53 | RUST_BACKTRACE: 1 54 | -------------------------------------------------------------------------------- /.github/workflows/release-with-github.yml: -------------------------------------------------------------------------------- 1 | name: Release with GitHub Action 2 | 3 | permissions: 4 | contents: write 5 | pull-requests: write 6 | 7 | on: 8 | workflow_dispatch: 9 | inputs: 10 | semverBump: 11 | description: 'Specify SemVer version you wish to bump (see: https://github.com/crate-ci/cargo-release/blob/master/docs/reference.md#bump-level)' 12 | required: true 13 | type: choice 14 | options: 15 | - custom 16 | - release 17 | - patch 18 | - minor 19 | - major 20 | - alpha 21 | - beta 22 | - rc 23 | semverVersion: 24 | description: 'Specify exact SemVer version (corresponds to [version] listed here: https://github.com/crate-ci/cargo-release/blob/master/docs/reference.md#bump-level). Works only when you have selected [custom] in previous dropdox.' 25 | default: '' 26 | required: false 27 | type: string 28 | 29 | jobs: 30 | create-release: 31 | runs-on: ubuntu-latest 32 | if: ${{ !(inputs.semverBump == 'custom' && inputs.semverVersion == '') }} 33 | outputs: 34 | nev_version: ${{ steps.determine_version.outputs.NEW_VERSION }} 35 | steps: 36 | - uses: actions/checkout@v3 37 | with: 38 | fetch-depth: 0 39 | - name: Use Rust version from rust-toolchain.toml 40 | run: rustup show 41 | - uses: cargo-bins/cargo-binstall@main 42 | - name: Install dependencies 43 | run: cargo binstall cargo-release ripgrep -y 44 | - name: Determine new version number by dry-running `cargo-release` 45 | id: determine_version 46 | continue-on-error: true 47 | run: | 48 | if [[ "${{ inputs.semverBump }}" == "custom" ]] 49 | then 50 | cargo release version -p cycles-ledger ${{ inputs.semverVersion }} &> cargo-release-output.txt 51 | else 52 | cargo release version -p cycles-ledger ${{ inputs.semverBump }} &> cargo-release-output.txt 53 | fi 54 | cat cargo-release-output.txt 55 | NEW_VERSION=$(cat cargo-release-output.txt | rg "Upgrading .* from .* to (.*)" -r '$1' | tr -d ' ') 56 | echo $NEW_VERSION 57 | echo "NEW_VERSION=$NEW_VERSION" >> "$GITHUB_ENV" 58 | echo "NEW_VERSION=$NEW_VERSION" >> "$GITHUB_OUTPUT" 59 | rm cargo-release-output.txt 60 | - name: Switch to the release branch, and push it 61 | run: | 62 | BRANCH_NAME="release/cycles-ledger-v${{ env.NEW_VERSION }}" 63 | git checkout -b "$BRANCH_NAME" 64 | git push --set-upstream origin "$BRANCH_NAME" 65 | - name: Set up git config 66 | run: | 67 | git config author.email "${{ github.event.sender.id }}+${{ github.event.sender.login }}@users.noreply.github.com" 68 | git config author.name "${{ github.event.sender.login }}" 69 | git config committer.email "41898282+github-actions[bot]@users.noreply.github.com" 70 | git config committer.name "GitHub Actions Bot" 71 | git config user.email "${{ github.event.sender.id }}+${{ github.event.sender.login }}@users.noreply.github.com" 72 | git config user.name "${{ github.event.sender.login }}" 73 | - name: Execute `cargo-release` 74 | if: ${{ inputs.semverBump != 'custom' }} 75 | run: cargo release -p cycles-ledger --execute --no-confirm ${{ inputs.semverBump }} 76 | - name: Execute `cargo-release` 77 | if: ${{ inputs.semverBump == 'custom' }} 78 | run: cargo release -p cycles-ledger --execute --no-confirm ${{ inputs.semverVersion }} 79 | 80 | 81 | call-release-binaries-workflow: 82 | needs: create-release 83 | uses: ./.github/workflows/release.yml 84 | with: 85 | release_tag: cycles-ledger-v${{ needs.create-release.outputs.nev_version }} 86 | 87 | create-release-pr: 88 | needs: [create-release, call-release-binaries-workflow] 89 | runs-on: ubuntu-latest 90 | steps: 91 | - uses: actions/checkout@v3 92 | - name: Open the release PR 93 | env: 94 | GH_TOKEN: ${{ github.token }} 95 | run: | 96 | TAG="cycles-ledger-v${{ needs.create-release.outputs.nev_version }}" 97 | HEAD="release/$TAG" 98 | TITLE="chore(release): cycles-ledger-v${{ needs.create-release.outputs.nev_version }}" 99 | echo "PR created by this workflow: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> BODY.md 100 | echo "Link to release: https://github.com/dfinity/cycles-ledger/releases/tag/$TAG" >> BODY.md 101 | gh pr create --base main --head "$HEAD" --title "$TITLE" --body-file BODY.md 102 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # CI that: 2 | # 3 | # * checks for a Git Tag that looks like a release 4 | # * creates a Github Release™ and fills in its text 5 | # * builds artifacts with cargo-dist (executable-zips, installers) 6 | # * uploads those artifacts to the Github Release™ 7 | # 8 | # Note that the Github Release™ will be created before the artifacts, 9 | # so there will be a few minutes where the release has no artifacts 10 | # and then they will slowly trickle in, possibly failing. To make 11 | # this more pleasant we mark the release as a "draft" until all 12 | # artifacts have been successfully uploaded. This allows you to 13 | # choose what to do with partial successes and avoids spamming 14 | # anyone with notifications before the release is actually ready. 15 | name: Release binaries 16 | 17 | permissions: 18 | contents: write 19 | 20 | # This task will run whenever you push a git tag that looks like a version 21 | # like "v1", "v1.2.0", "v0.1.0-prerelease01", "my-app-v1.0.0", etc. 22 | # The version will be roughly parsed as ({PACKAGE_NAME}-)?v{VERSION}, where 23 | # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION 24 | # must be a Cargo-style SemVer Version. 25 | # 26 | # If PACKAGE_NAME is specified, then we will create a Github Release™ for that 27 | # package (erroring out if it doesn't have the given version or isn't cargo-dist-able). 28 | # 29 | # If PACKAGE_NAME isn't specified, then we will create a Github Release™ for all 30 | # (cargo-dist-able) packages in the workspace with that version (this is mode is 31 | # intended for workspaces with only one dist-able package, or with all dist-able 32 | # packages versioned/released in lockstep). 33 | # 34 | # If you push multiple tags at once, separate instances of this workflow will 35 | # spin up, creating an independent Github Release™ for each one. 36 | # 37 | # If there's a prerelease-style suffix to the version then the Github Release™ 38 | # will be marked as a prerelease. 39 | on: 40 | workflow_call: 41 | inputs: 42 | release_tag: 43 | required: true 44 | type: string 45 | push: 46 | tags: 47 | - '*-?v[0-9]+*' 48 | 49 | jobs: 50 | # Create the Github Release™ so the packages have something to be uploaded to 51 | create-release: 52 | runs-on: ubuntu-latest 53 | outputs: 54 | release-tag: ${{ steps.determine-release-tag.outputs.TAG }} 55 | env: 56 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | steps: 58 | - name: Determine correct tag 59 | id: determine-release-tag 60 | run: | 61 | echo 'INFO: Release tag received from input: ${{ inputs.release_tag }}' 62 | echo 'INFO: GitHub context ref_name: ${{ github.ref_name }}' 63 | if [[ "${{ github.workflow }}" == "Release with GitHub Action" ]] 64 | then 65 | echo "INFO: This run has been triggered from other workflow" 66 | TAG=${{ inputs.release_tag }} 67 | else 68 | echo "INFO: This run has been triggered from pushing the tag" 69 | TAG=${{ github.ref_name }} 70 | fi 71 | echo "INFO: The release tag is: $TAG" 72 | echo "TAG=$TAG" >> "$GITHUB_ENV" 73 | echo "TAG=$TAG" >> "$GITHUB_OUTPUT" 74 | - uses: actions/checkout@v3 75 | with: 76 | ref: ${{ env.TAG }} 77 | - name: Install Rust 78 | run: rustup show 79 | - id: create-release 80 | run: gh release create ${{ env.TAG }} --draft --prerelease="true" --title="${{ env.TAG }}" --notes="TBD" 81 | - name: Install dfx 82 | uses: dfinity/setup-dfx@main 83 | - name: Build WASM 84 | run: | 85 | dfx build --check 86 | echo "Cycles ledger (dfx build):" 87 | ls .dfx/local/canisters/cycles-ledger 88 | echo "Depositor (dfx build):" 89 | ls .dfx/local/canisters/depositor 90 | echo "Fake CMC (dfx build):" 91 | ls .dfx/local/canisters/fake-cmc 92 | ./scripts/docker-build 93 | echo "Cycles ledger (docker build):" 94 | ls cycles-ledger.wasm.gz 95 | (shasum -a 256 cycles-ledger.wasm.gz > cycles-ledger.wasm.gz.sha256) 96 | (cd .dfx/local/canisters/depositor && shasum -a 256 depositor.wasm.gz > depositor.wasm.gz.sha256) 97 | (cd .dfx/local/canisters/fake-cmc && shasum -a 256 fake-cmc.wasm.gz > fake-cmc.wasm.gz.sha256) 98 | gh release upload ${{ env.TAG }} .dfx/local/canisters/depositor/depositor.wasm.gz 99 | gh release upload ${{ env.TAG }} .dfx/local/canisters/depositor/depositor.wasm.gz.sha256 100 | gh release upload ${{ env.TAG }} depositor/depositor.did 101 | gh release upload ${{ env.TAG }} cycles-ledger.wasm.gz 102 | gh release upload ${{ env.TAG }} cycles-ledger.wasm.gz.sha256 103 | gh release upload ${{ env.TAG }} cycles-ledger/cycles-ledger.did 104 | gh release upload ${{ env.TAG }} .dfx/local/canisters/fake-cmc/fake-cmc.wasm.gz 105 | gh release upload ${{ env.TAG }} .dfx/local/canisters/fake-cmc/fake-cmc.wasm.gz.sha256 106 | gh release upload ${{ env.TAG }} fake-cmc/fake-cmc.did 107 | echo "uploaded!" 108 | - name: mark release as non-draft 109 | run: | 110 | gh release edit ${{ env.TAG }} --draft=false 111 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | # Ignore the dfx build artifacts 13 | .dfx 14 | 15 | # Ignore state machine binary 16 | ic-test-state-machine 17 | 18 | .vscode 19 | 20 | # cycles-ledger wasm file created by docker 21 | cycles-ledger.wasm.gz 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # CHANGELOG 4 | 5 | ## [Unreleased] - ReleaseDate 6 | 7 | ## [1.0.4] - 2025-04-10 8 | * Fixed a bug where refund blocks after an unsuccessful `withdraw` or `create_canister` had the timestamp of the initial burn block instead of the time when the error was processed. Approvals that have expired in the meantime will not be refunded. 9 | 10 | ## [1.0.3] - 2024-11-18 11 | * Adapted `icrc3_get_tip_certificate` to be compliant with the ICRC-3 specification by changing the encoding of `last_block_index` to `leb128`. 12 | 13 | ## [1.0.2] - 2024-10-28 14 | * Added `get_icp_xdr_conversion_rate` to mock CMC 15 | 16 | No changes to the cycles ledger. Released because a community project relies on the mock CMC and would like to have this feature available. 17 | 18 | ## [1.0.1] - 2024-08-22 19 | * Update `ic-cdk` dependency to patch a security issue. 20 | 21 | ## [1.0.0] - 2024-06-18 22 | * Added the logo to the metadata value `icrc1:logo`. 23 | * Fixed a bug where the cycles ledger took control over a newly created canister if `creation_args` is `Some` and `canister_settings` is `None`. 24 | 25 | ## [0.6.0] - 2024-03-25 26 | 27 | ## [0.5.0] - 2024-03-21 28 | 29 | ## [0.4.0] - 2024-03-20 30 | 31 | ## [0.3.0] - 2024-02-09 32 | 33 | ## [0.2.8] - 2024-01-19 34 | 35 | ## [0.2.1] - 2023-09-20 36 | 37 | ## [0.2.0] - 2023-09-18 38 | 39 | ## [0.1.0] - 2023-07-12 40 | 41 | 42 | [Unreleased]: https://github.com/dfinity/cycles-ledger/compare/cycles-ledger-v1.0.4...HEAD 43 | [1.0.4]: https://github.com/dfinity/cycles-ledger/compare/cycles-ledger-v1.0.3...cycles-ledger-v1.0.4 44 | [1.0.3]: https://github.com/dfinity/cycles-ledger/compare/cycles-ledger-v1.0.2...cycles-ledger-v1.0.3 45 | [1.0.2]: https://github.com/dfinity/cycles-ledger/compare/cycles-ledger-v1.0.1...cycles-ledger-v1.0.2 46 | [1.0.1]: https://github.com/dfinity/cycles-ledger/compare/cycles-ledger-v1.0.0...cycles-ledger-v1.0.1 47 | [1.0.0]: https://github.com/dfinity/cycles-ledger/compare/cycles-ledger-v0.6.0...cycles-ledger-v1.0.0 48 | [0.6.0]: https://github.com/dfinity/cycles-ledger/compare/cycles-ledger-v0.5.0...cycles-ledger-v0.6.0 49 | [0.5.0]: https://github.com/dfinity/cycles-ledger/compare/cycles-ledger-v0.4.0...cycles-ledger-v0.5.0 50 | [0.4.0]: https://github.com/dfinity/cycles-ledger/compare/cycles-ledger-v0.3.0...cycles-ledger-v0.4.0 51 | [0.3.0]: https://github.com/dfinity/cycles-ledger/compare/cycles-ledger-v0.2.8...cycles-ledger-v0.3.0 52 | [0.2.8]: https://github.com/dfinity/cycles-ledger/compare/cycles-ledger-v0.2.1...cycles-ledger-v0.2.8 53 | [0.2.1]: https://github.com/dfinity/cycles-ledger/compare/cycles-ledger-v0.2.0...cycles-ledger-v0.2.1 54 | [0.2.0]: https://github.com/dfinity/cycles-ledger/compare/cycles-ledger-v0.2.0...cycles-ledger-v0.2.0 55 | 56 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.22.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "ahash" 22 | version = "0.8.11" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 25 | dependencies = [ 26 | "cfg-if", 27 | "once_cell", 28 | "version_check", 29 | "zerocopy", 30 | ] 31 | 32 | [[package]] 33 | name = "aho-corasick" 34 | version = "1.1.3" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 37 | dependencies = [ 38 | "memchr", 39 | ] 40 | 41 | [[package]] 42 | name = "allocator-api2" 43 | version = "0.2.18" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 46 | 47 | [[package]] 48 | name = "anyhow" 49 | version = "1.0.86" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" 52 | 53 | [[package]] 54 | name = "arbitrary" 55 | version = "1.3.2" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" 58 | 59 | [[package]] 60 | name = "arrayvec" 61 | version = "0.5.2" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" 64 | 65 | [[package]] 66 | name = "ascii-canvas" 67 | version = "3.0.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" 70 | dependencies = [ 71 | "term", 72 | ] 73 | 74 | [[package]] 75 | name = "assert_matches" 76 | version = "1.5.0" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" 79 | 80 | [[package]] 81 | name = "async-trait" 82 | version = "0.1.81" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" 85 | dependencies = [ 86 | "proc-macro2", 87 | "quote", 88 | "syn 2.0.71", 89 | ] 90 | 91 | [[package]] 92 | name = "autocfg" 93 | version = "1.3.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 96 | 97 | [[package]] 98 | name = "backtrace" 99 | version = "0.3.73" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" 102 | dependencies = [ 103 | "addr2line", 104 | "cc", 105 | "cfg-if", 106 | "libc", 107 | "miniz_oxide", 108 | "object", 109 | "rustc-demangle", 110 | ] 111 | 112 | [[package]] 113 | name = "base32" 114 | version = "0.4.0" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" 117 | 118 | [[package]] 119 | name = "beef" 120 | version = "0.5.2" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" 123 | 124 | [[package]] 125 | name = "binread" 126 | version = "2.2.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "16598dfc8e6578e9b597d9910ba2e73618385dc9f4b1d43dd92c349d6be6418f" 129 | dependencies = [ 130 | "binread_derive", 131 | "lazy_static", 132 | "rustversion", 133 | ] 134 | 135 | [[package]] 136 | name = "binread_derive" 137 | version = "2.1.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "1d9672209df1714ee804b1f4d4f68c8eb2a90b1f7a07acf472f88ce198ef1fed" 140 | dependencies = [ 141 | "either", 142 | "proc-macro2", 143 | "quote", 144 | "syn 1.0.109", 145 | ] 146 | 147 | [[package]] 148 | name = "bit-set" 149 | version = "0.5.3" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" 152 | dependencies = [ 153 | "bit-vec", 154 | ] 155 | 156 | [[package]] 157 | name = "bit-vec" 158 | version = "0.6.3" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" 161 | 162 | [[package]] 163 | name = "bitflags" 164 | version = "2.6.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 167 | 168 | [[package]] 169 | name = "block-buffer" 170 | version = "0.10.4" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 173 | dependencies = [ 174 | "generic-array", 175 | ] 176 | 177 | [[package]] 178 | name = "byteorder" 179 | version = "1.5.0" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 182 | 183 | [[package]] 184 | name = "cached" 185 | version = "0.47.0" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "69b0116662497bc24e4b177c90eaf8870e39e2714c3fcfa296327a93f593fc21" 188 | dependencies = [ 189 | "ahash", 190 | "cached_proc_macro", 191 | "cached_proc_macro_types", 192 | "hashbrown", 193 | "instant", 194 | "once_cell", 195 | "thiserror", 196 | ] 197 | 198 | [[package]] 199 | name = "cached_proc_macro" 200 | version = "0.18.1" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "c878c71c2821aa2058722038a59a67583a4240524687c6028571c9b395ded61f" 203 | dependencies = [ 204 | "darling", 205 | "proc-macro2", 206 | "quote", 207 | "syn 1.0.109", 208 | ] 209 | 210 | [[package]] 211 | name = "cached_proc_macro_types" 212 | version = "0.1.1" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" 215 | 216 | [[package]] 217 | name = "candid" 218 | version = "0.10.9" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "7df77a80c72fcd356cf37ff59c812f37ff06dc9a81232b3aff0a308cb5996904" 221 | dependencies = [ 222 | "anyhow", 223 | "binread", 224 | "byteorder", 225 | "candid_derive", 226 | "hex", 227 | "ic_principal", 228 | "leb128", 229 | "num-bigint", 230 | "num-traits", 231 | "paste", 232 | "pretty", 233 | "serde", 234 | "serde_bytes", 235 | "stacker", 236 | "thiserror", 237 | ] 238 | 239 | [[package]] 240 | name = "candid_derive" 241 | version = "0.6.6" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "3de398570c386726e7a59d9887b68763c481477f9a043fb998a2e09d428df1a9" 244 | dependencies = [ 245 | "lazy_static", 246 | "proc-macro2", 247 | "quote", 248 | "syn 2.0.71", 249 | ] 250 | 251 | [[package]] 252 | name = "candid_parser" 253 | version = "0.1.4" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "48a3da76f989cd350b7342c64c6c6008341bb6186f6832ef04e56dc50ba0fd76" 256 | dependencies = [ 257 | "anyhow", 258 | "candid", 259 | "codespan-reporting", 260 | "convert_case", 261 | "hex", 262 | "lalrpop", 263 | "lalrpop-util", 264 | "logos", 265 | "num-bigint", 266 | "pretty", 267 | "thiserror", 268 | ] 269 | 270 | [[package]] 271 | name = "cc" 272 | version = "1.1.5" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "324c74f2155653c90b04f25b2a47a8a631360cb908f92a772695f430c7e31052" 275 | 276 | [[package]] 277 | name = "cfg-if" 278 | version = "1.0.0" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 281 | 282 | [[package]] 283 | name = "ciborium" 284 | version = "0.2.2" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" 287 | dependencies = [ 288 | "ciborium-io", 289 | "ciborium-ll", 290 | "serde", 291 | ] 292 | 293 | [[package]] 294 | name = "ciborium-io" 295 | version = "0.2.2" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" 298 | 299 | [[package]] 300 | name = "ciborium-ll" 301 | version = "0.2.2" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" 304 | dependencies = [ 305 | "ciborium-io", 306 | "half", 307 | ] 308 | 309 | [[package]] 310 | name = "codespan-reporting" 311 | version = "0.11.1" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" 314 | dependencies = [ 315 | "termcolor", 316 | "unicode-width", 317 | ] 318 | 319 | [[package]] 320 | name = "convert_case" 321 | version = "0.6.0" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" 324 | dependencies = [ 325 | "unicode-segmentation", 326 | ] 327 | 328 | [[package]] 329 | name = "cpufeatures" 330 | version = "0.2.12" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" 333 | dependencies = [ 334 | "libc", 335 | ] 336 | 337 | [[package]] 338 | name = "crc32fast" 339 | version = "1.4.2" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 342 | dependencies = [ 343 | "cfg-if", 344 | ] 345 | 346 | [[package]] 347 | name = "crunchy" 348 | version = "0.2.2" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" 351 | 352 | [[package]] 353 | name = "crypto-common" 354 | version = "0.1.6" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 357 | dependencies = [ 358 | "generic-array", 359 | "typenum", 360 | ] 361 | 362 | [[package]] 363 | name = "cycles-ledger" 364 | version = "1.0.0" 365 | dependencies = [ 366 | "anyhow", 367 | "assert_matches", 368 | "candid", 369 | "candid_parser", 370 | "ciborium", 371 | "depositor", 372 | "escargot", 373 | "hex", 374 | "ic-canister-log", 375 | "ic-canisters-http-types", 376 | "ic-cbor", 377 | "ic-cdk", 378 | "ic-cdk-macros", 379 | "ic-certificate-verification", 380 | "ic-certification", 381 | "ic-certified-map", 382 | "ic-metrics-encoder", 383 | "ic-stable-structures", 384 | "ic-test-state-machine-client", 385 | "icrc-ledger-types", 386 | "icrc1-test-env-state-machine", 387 | "icrc1-test-suite", 388 | "lazy_static", 389 | "leb128", 390 | "minicbor", 391 | "num-bigint", 392 | "num-traits", 393 | "proptest", 394 | "serde", 395 | "serde_bytes", 396 | "serde_json", 397 | "tempfile", 398 | "thiserror", 399 | "tokio", 400 | ] 401 | 402 | [[package]] 403 | name = "darling" 404 | version = "0.14.4" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" 407 | dependencies = [ 408 | "darling_core", 409 | "darling_macro", 410 | ] 411 | 412 | [[package]] 413 | name = "darling_core" 414 | version = "0.14.4" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" 417 | dependencies = [ 418 | "fnv", 419 | "ident_case", 420 | "proc-macro2", 421 | "quote", 422 | "strsim", 423 | "syn 1.0.109", 424 | ] 425 | 426 | [[package]] 427 | name = "darling_macro" 428 | version = "0.14.4" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" 431 | dependencies = [ 432 | "darling_core", 433 | "quote", 434 | "syn 1.0.109", 435 | ] 436 | 437 | [[package]] 438 | name = "data-encoding" 439 | version = "2.6.0" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" 442 | 443 | [[package]] 444 | name = "depositor" 445 | version = "1.0.0" 446 | dependencies = [ 447 | "candid", 448 | "candid_parser", 449 | "cycles-ledger", 450 | "ic-cdk", 451 | "ic-cdk-macros", 452 | "icrc-ledger-types", 453 | "serde", 454 | ] 455 | 456 | [[package]] 457 | name = "digest" 458 | version = "0.10.7" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 461 | dependencies = [ 462 | "block-buffer", 463 | "crypto-common", 464 | ] 465 | 466 | [[package]] 467 | name = "dirs-next" 468 | version = "2.0.0" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" 471 | dependencies = [ 472 | "cfg-if", 473 | "dirs-sys-next", 474 | ] 475 | 476 | [[package]] 477 | name = "dirs-sys-next" 478 | version = "0.1.2" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" 481 | dependencies = [ 482 | "libc", 483 | "redox_users", 484 | "winapi", 485 | ] 486 | 487 | [[package]] 488 | name = "either" 489 | version = "1.13.0" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 492 | 493 | [[package]] 494 | name = "ena" 495 | version = "0.14.3" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" 498 | dependencies = [ 499 | "log", 500 | ] 501 | 502 | [[package]] 503 | name = "equivalent" 504 | version = "1.0.1" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 507 | 508 | [[package]] 509 | name = "errno" 510 | version = "0.3.9" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 513 | dependencies = [ 514 | "libc", 515 | "windows-sys", 516 | ] 517 | 518 | [[package]] 519 | name = "escargot" 520 | version = "0.5.11" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "650eb5f6eeda986377996e9ed570cbc20cc16d30440696f82f129c863e4e3e83" 523 | dependencies = [ 524 | "log", 525 | "once_cell", 526 | "serde", 527 | "serde_json", 528 | ] 529 | 530 | [[package]] 531 | name = "fake-cmc" 532 | version = "1.0.0" 533 | dependencies = [ 534 | "candid", 535 | "candid_parser", 536 | "cycles-ledger", 537 | "ic-cdk", 538 | "ic-cdk-macros", 539 | "icrc-ledger-types", 540 | "serde", 541 | ] 542 | 543 | [[package]] 544 | name = "fastrand" 545 | version = "2.1.0" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" 548 | 549 | [[package]] 550 | name = "fixedbitset" 551 | version = "0.4.2" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" 554 | 555 | [[package]] 556 | name = "fnv" 557 | version = "1.0.7" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 560 | 561 | [[package]] 562 | name = "futures" 563 | version = "0.3.30" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" 566 | dependencies = [ 567 | "futures-channel", 568 | "futures-core", 569 | "futures-executor", 570 | "futures-io", 571 | "futures-sink", 572 | "futures-task", 573 | "futures-util", 574 | ] 575 | 576 | [[package]] 577 | name = "futures-channel" 578 | version = "0.3.30" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 581 | dependencies = [ 582 | "futures-core", 583 | "futures-sink", 584 | ] 585 | 586 | [[package]] 587 | name = "futures-core" 588 | version = "0.3.30" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 591 | 592 | [[package]] 593 | name = "futures-executor" 594 | version = "0.3.30" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 597 | dependencies = [ 598 | "futures-core", 599 | "futures-task", 600 | "futures-util", 601 | ] 602 | 603 | [[package]] 604 | name = "futures-io" 605 | version = "0.3.30" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 608 | 609 | [[package]] 610 | name = "futures-macro" 611 | version = "0.3.30" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 614 | dependencies = [ 615 | "proc-macro2", 616 | "quote", 617 | "syn 2.0.71", 618 | ] 619 | 620 | [[package]] 621 | name = "futures-sink" 622 | version = "0.3.30" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 625 | 626 | [[package]] 627 | name = "futures-task" 628 | version = "0.3.30" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 631 | 632 | [[package]] 633 | name = "futures-util" 634 | version = "0.3.30" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 637 | dependencies = [ 638 | "futures-channel", 639 | "futures-core", 640 | "futures-io", 641 | "futures-macro", 642 | "futures-sink", 643 | "futures-task", 644 | "memchr", 645 | "pin-project-lite", 646 | "pin-utils", 647 | "slab", 648 | ] 649 | 650 | [[package]] 651 | name = "generic-array" 652 | version = "0.14.7" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 655 | dependencies = [ 656 | "typenum", 657 | "version_check", 658 | ] 659 | 660 | [[package]] 661 | name = "getrandom" 662 | version = "0.2.15" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 665 | dependencies = [ 666 | "cfg-if", 667 | "libc", 668 | "wasi", 669 | ] 670 | 671 | [[package]] 672 | name = "gimli" 673 | version = "0.29.0" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" 676 | 677 | [[package]] 678 | name = "half" 679 | version = "2.4.1" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" 682 | dependencies = [ 683 | "cfg-if", 684 | "crunchy", 685 | ] 686 | 687 | [[package]] 688 | name = "hashbrown" 689 | version = "0.14.5" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 692 | dependencies = [ 693 | "ahash", 694 | "allocator-api2", 695 | ] 696 | 697 | [[package]] 698 | name = "hex" 699 | version = "0.4.3" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 702 | 703 | [[package]] 704 | name = "ic-canister-log" 705 | version = "0.2.0" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "cb82c4f617ecff6e452fe65af0489626ec7330ffe3eedd9ea14e6178eea48d1a" 708 | dependencies = [ 709 | "serde", 710 | ] 711 | 712 | [[package]] 713 | name = "ic-canisters-http-types" 714 | version = "0.9.0" 715 | source = "git+https://github.com/dfinity/ic?rev=b2f18ac0794d2225b53d9c3190b60dbadb4ac9b9#b2f18ac0794d2225b53d9c3190b60dbadb4ac9b9" 716 | dependencies = [ 717 | "candid", 718 | "serde", 719 | "serde_bytes", 720 | ] 721 | 722 | [[package]] 723 | name = "ic-cbor" 724 | version = "2.5.0" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "07bc917070b8fc4bd88e3199746372e44d507f54c93a9b191787e1caefca1eba" 727 | dependencies = [ 728 | "candid", 729 | "ic-certification", 730 | "leb128", 731 | "nom", 732 | "thiserror", 733 | ] 734 | 735 | [[package]] 736 | name = "ic-cdk" 737 | version = "0.12.2" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "0e908da565d9e304e83732500069ebb959e3d2cad80f894889ea37207112c7a0" 740 | dependencies = [ 741 | "candid", 742 | "ic-cdk-macros", 743 | "ic0", 744 | "serde", 745 | "serde_bytes", 746 | ] 747 | 748 | [[package]] 749 | name = "ic-cdk-macros" 750 | version = "0.8.4" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "a5a618e4020cea88e933d8d2f8c7f86d570ec06213506a80d4f2c520a9bba512" 753 | dependencies = [ 754 | "candid", 755 | "proc-macro2", 756 | "quote", 757 | "serde", 758 | "serde_tokenstream", 759 | "syn 1.0.109", 760 | ] 761 | 762 | [[package]] 763 | name = "ic-certificate-verification" 764 | version = "2.4.0" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "769142849e241e6cf7f5611f9b04983e958729495ea67d2de95e5d9a9c687d9b" 767 | dependencies = [ 768 | "cached", 769 | "candid", 770 | "ic-cbor", 771 | "ic-certification", 772 | "lazy_static", 773 | "leb128", 774 | "miracl_core_bls12381", 775 | "nom", 776 | "parking_lot", 777 | "sha2", 778 | "thiserror", 779 | ] 780 | 781 | [[package]] 782 | name = "ic-certification" 783 | version = "2.5.0" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "20052ce9255fbe2de7041a4f6996fddd095ba1f31ae83b6c0ccdee5be6e7bbcf" 786 | dependencies = [ 787 | "hex", 788 | "serde", 789 | "serde_bytes", 790 | "sha2", 791 | ] 792 | 793 | [[package]] 794 | name = "ic-certified-map" 795 | version = "0.4.0" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "197524aecec47db0b6c0c9f8821aad47272c2bd762c7a0ffe9715eaca0364061" 798 | dependencies = [ 799 | "serde", 800 | "serde_bytes", 801 | "sha2", 802 | ] 803 | 804 | [[package]] 805 | name = "ic-metrics-encoder" 806 | version = "1.1.1" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "8b5c7628eac357aecda461130f8074468be5aa4d258a002032d82d817f79f1f8" 809 | 810 | [[package]] 811 | name = "ic-stable-structures" 812 | version = "0.6.5" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "03f3044466a69802de74e710dc0300b706a05696a0531c942ca856751a13b0db" 815 | dependencies = [ 816 | "ic_principal", 817 | ] 818 | 819 | [[package]] 820 | name = "ic-test-state-machine-client" 821 | version = "3.0.1" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "b8e05a81e0cbdf178228d72ace06c60ac7fa99927b49a238f9ccf5ef82eaced6" 824 | dependencies = [ 825 | "candid", 826 | "ciborium", 827 | "serde", 828 | "serde_bytes", 829 | ] 830 | 831 | [[package]] 832 | name = "ic0" 833 | version = "0.21.1" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "a54b5297861c651551676e8c43df805dad175cc33bc97dbd992edbbb85dcbcdf" 836 | 837 | [[package]] 838 | name = "ic_principal" 839 | version = "0.1.1" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "1762deb6f7c8d8c2bdee4b6c5a47b60195b74e9b5280faa5ba29692f8e17429c" 842 | dependencies = [ 843 | "arbitrary", 844 | "crc32fast", 845 | "data-encoding", 846 | "serde", 847 | "sha2", 848 | "thiserror", 849 | ] 850 | 851 | [[package]] 852 | name = "icrc-ledger-types" 853 | version = "0.1.5" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "804c892bf95652101660a25cea10f059f73eb8973f6b04e0349758fda1190447" 856 | dependencies = [ 857 | "base32", 858 | "candid", 859 | "crc32fast", 860 | "hex", 861 | "num-bigint", 862 | "num-traits", 863 | "serde", 864 | "serde_bytes", 865 | "sha2", 866 | ] 867 | 868 | [[package]] 869 | name = "icrc1-test-env" 870 | version = "0.1.11" 871 | source = "registry+https://github.com/rust-lang/crates.io-index" 872 | checksum = "3884128d9da160c7ae50d2943ecc95cc8ddf176370c400ca3d07eca599583350" 873 | dependencies = [ 874 | "anyhow", 875 | "async-trait", 876 | "candid", 877 | "serde", 878 | "thiserror", 879 | ] 880 | 881 | [[package]] 882 | name = "icrc1-test-env-state-machine" 883 | version = "0.1.2" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "eecdf5fcc3b71ea33ac25d79d94e94cc3f4742d103e51b744bab78cfc6bb4749" 886 | dependencies = [ 887 | "anyhow", 888 | "async-trait", 889 | "candid", 890 | "hex", 891 | "ic-test-state-machine-client", 892 | "icrc1-test-env", 893 | ] 894 | 895 | [[package]] 896 | name = "icrc1-test-suite" 897 | version = "0.1.2" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "bed637d6e7c840d35216c8004d22106f12fade9a6750f50033f8c8922474c4b5" 900 | dependencies = [ 901 | "anyhow", 902 | "candid", 903 | "futures", 904 | "icrc1-test-env", 905 | ] 906 | 907 | [[package]] 908 | name = "ident_case" 909 | version = "1.0.1" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 912 | 913 | [[package]] 914 | name = "indexmap" 915 | version = "2.2.6" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 918 | dependencies = [ 919 | "equivalent", 920 | "hashbrown", 921 | ] 922 | 923 | [[package]] 924 | name = "instant" 925 | version = "0.1.13" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" 928 | dependencies = [ 929 | "cfg-if", 930 | ] 931 | 932 | [[package]] 933 | name = "itertools" 934 | version = "0.11.0" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" 937 | dependencies = [ 938 | "either", 939 | ] 940 | 941 | [[package]] 942 | name = "itoa" 943 | version = "1.0.11" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 946 | 947 | [[package]] 948 | name = "lalrpop" 949 | version = "0.20.2" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" 952 | dependencies = [ 953 | "ascii-canvas", 954 | "bit-set", 955 | "ena", 956 | "itertools", 957 | "lalrpop-util", 958 | "petgraph", 959 | "pico-args", 960 | "regex", 961 | "regex-syntax 0.8.4", 962 | "string_cache", 963 | "term", 964 | "tiny-keccak", 965 | "unicode-xid", 966 | "walkdir", 967 | ] 968 | 969 | [[package]] 970 | name = "lalrpop-util" 971 | version = "0.20.2" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" 974 | dependencies = [ 975 | "regex-automata", 976 | ] 977 | 978 | [[package]] 979 | name = "lazy_static" 980 | version = "1.5.0" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 983 | 984 | [[package]] 985 | name = "leb128" 986 | version = "0.2.5" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" 989 | 990 | [[package]] 991 | name = "libc" 992 | version = "0.2.155" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 995 | 996 | [[package]] 997 | name = "libm" 998 | version = "0.2.8" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" 1001 | 1002 | [[package]] 1003 | name = "libredox" 1004 | version = "0.1.3" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 1007 | dependencies = [ 1008 | "bitflags", 1009 | "libc", 1010 | ] 1011 | 1012 | [[package]] 1013 | name = "linux-raw-sys" 1014 | version = "0.4.14" 1015 | source = "registry+https://github.com/rust-lang/crates.io-index" 1016 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 1017 | 1018 | [[package]] 1019 | name = "lock_api" 1020 | version = "0.4.12" 1021 | source = "registry+https://github.com/rust-lang/crates.io-index" 1022 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 1023 | dependencies = [ 1024 | "autocfg", 1025 | "scopeguard", 1026 | ] 1027 | 1028 | [[package]] 1029 | name = "log" 1030 | version = "0.4.22" 1031 | source = "registry+https://github.com/rust-lang/crates.io-index" 1032 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 1033 | 1034 | [[package]] 1035 | name = "logos" 1036 | version = "0.13.0" 1037 | source = "registry+https://github.com/rust-lang/crates.io-index" 1038 | checksum = "c000ca4d908ff18ac99b93a062cb8958d331c3220719c52e77cb19cc6ac5d2c1" 1039 | dependencies = [ 1040 | "logos-derive", 1041 | ] 1042 | 1043 | [[package]] 1044 | name = "logos-codegen" 1045 | version = "0.13.0" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "dc487311295e0002e452025d6b580b77bb17286de87b57138f3b5db711cded68" 1048 | dependencies = [ 1049 | "beef", 1050 | "fnv", 1051 | "proc-macro2", 1052 | "quote", 1053 | "regex-syntax 0.6.29", 1054 | "syn 2.0.71", 1055 | ] 1056 | 1057 | [[package]] 1058 | name = "logos-derive" 1059 | version = "0.13.0" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "dbfc0d229f1f42d790440136d941afd806bc9e949e2bcb8faa813b0f00d1267e" 1062 | dependencies = [ 1063 | "logos-codegen", 1064 | ] 1065 | 1066 | [[package]] 1067 | name = "memchr" 1068 | version = "2.7.4" 1069 | source = "registry+https://github.com/rust-lang/crates.io-index" 1070 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 1071 | 1072 | [[package]] 1073 | name = "minicbor" 1074 | version = "0.19.1" 1075 | source = "registry+https://github.com/rust-lang/crates.io-index" 1076 | checksum = "d7005aaf257a59ff4de471a9d5538ec868a21586534fff7f85dd97d4043a6139" 1077 | dependencies = [ 1078 | "minicbor-derive", 1079 | ] 1080 | 1081 | [[package]] 1082 | name = "minicbor-derive" 1083 | version = "0.13.0" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "1154809406efdb7982841adb6311b3d095b46f78342dd646736122fe6b19e267" 1086 | dependencies = [ 1087 | "proc-macro2", 1088 | "quote", 1089 | "syn 1.0.109", 1090 | ] 1091 | 1092 | [[package]] 1093 | name = "minimal-lexical" 1094 | version = "0.2.1" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 1097 | 1098 | [[package]] 1099 | name = "miniz_oxide" 1100 | version = "0.7.4" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 1103 | dependencies = [ 1104 | "adler", 1105 | ] 1106 | 1107 | [[package]] 1108 | name = "miracl_core_bls12381" 1109 | version = "4.2.2" 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" 1111 | checksum = "d07cbe42e2a8dd41df582fb8e00fc24d920b5561cc301fcb6d14e2e0434b500f" 1112 | 1113 | [[package]] 1114 | name = "new_debug_unreachable" 1115 | version = "1.0.6" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 1118 | 1119 | [[package]] 1120 | name = "nom" 1121 | version = "7.1.3" 1122 | source = "registry+https://github.com/rust-lang/crates.io-index" 1123 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 1124 | dependencies = [ 1125 | "memchr", 1126 | "minimal-lexical", 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "num-bigint" 1131 | version = "0.4.6" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 1134 | dependencies = [ 1135 | "num-integer", 1136 | "num-traits", 1137 | "serde", 1138 | ] 1139 | 1140 | [[package]] 1141 | name = "num-integer" 1142 | version = "0.1.46" 1143 | source = "registry+https://github.com/rust-lang/crates.io-index" 1144 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 1145 | dependencies = [ 1146 | "num-traits", 1147 | ] 1148 | 1149 | [[package]] 1150 | name = "num-traits" 1151 | version = "0.2.19" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1154 | dependencies = [ 1155 | "autocfg", 1156 | "libm", 1157 | ] 1158 | 1159 | [[package]] 1160 | name = "object" 1161 | version = "0.36.1" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" 1164 | dependencies = [ 1165 | "memchr", 1166 | ] 1167 | 1168 | [[package]] 1169 | name = "once_cell" 1170 | version = "1.19.0" 1171 | source = "registry+https://github.com/rust-lang/crates.io-index" 1172 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 1173 | 1174 | [[package]] 1175 | name = "parking_lot" 1176 | version = "0.12.3" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 1179 | dependencies = [ 1180 | "lock_api", 1181 | "parking_lot_core", 1182 | ] 1183 | 1184 | [[package]] 1185 | name = "parking_lot_core" 1186 | version = "0.9.10" 1187 | source = "registry+https://github.com/rust-lang/crates.io-index" 1188 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 1189 | dependencies = [ 1190 | "cfg-if", 1191 | "libc", 1192 | "redox_syscall", 1193 | "smallvec", 1194 | "windows-targets", 1195 | ] 1196 | 1197 | [[package]] 1198 | name = "paste" 1199 | version = "1.0.15" 1200 | source = "registry+https://github.com/rust-lang/crates.io-index" 1201 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 1202 | 1203 | [[package]] 1204 | name = "petgraph" 1205 | version = "0.6.5" 1206 | source = "registry+https://github.com/rust-lang/crates.io-index" 1207 | checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" 1208 | dependencies = [ 1209 | "fixedbitset", 1210 | "indexmap", 1211 | ] 1212 | 1213 | [[package]] 1214 | name = "phf_shared" 1215 | version = "0.10.0" 1216 | source = "registry+https://github.com/rust-lang/crates.io-index" 1217 | checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" 1218 | dependencies = [ 1219 | "siphasher", 1220 | ] 1221 | 1222 | [[package]] 1223 | name = "pico-args" 1224 | version = "0.5.0" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" 1227 | 1228 | [[package]] 1229 | name = "pin-project-lite" 1230 | version = "0.2.14" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 1233 | 1234 | [[package]] 1235 | name = "pin-utils" 1236 | version = "0.1.0" 1237 | source = "registry+https://github.com/rust-lang/crates.io-index" 1238 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1239 | 1240 | [[package]] 1241 | name = "ppv-lite86" 1242 | version = "0.2.17" 1243 | source = "registry+https://github.com/rust-lang/crates.io-index" 1244 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 1245 | 1246 | [[package]] 1247 | name = "precomputed-hash" 1248 | version = "0.1.1" 1249 | source = "registry+https://github.com/rust-lang/crates.io-index" 1250 | checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 1251 | 1252 | [[package]] 1253 | name = "pretty" 1254 | version = "0.12.3" 1255 | source = "registry+https://github.com/rust-lang/crates.io-index" 1256 | checksum = "b55c4d17d994b637e2f4daf6e5dc5d660d209d5642377d675d7a1c3ab69fa579" 1257 | dependencies = [ 1258 | "arrayvec", 1259 | "typed-arena", 1260 | "unicode-width", 1261 | ] 1262 | 1263 | [[package]] 1264 | name = "proc-macro2" 1265 | version = "1.0.86" 1266 | source = "registry+https://github.com/rust-lang/crates.io-index" 1267 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 1268 | dependencies = [ 1269 | "unicode-ident", 1270 | ] 1271 | 1272 | [[package]] 1273 | name = "proptest" 1274 | version = "1.5.0" 1275 | source = "registry+https://github.com/rust-lang/crates.io-index" 1276 | checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" 1277 | dependencies = [ 1278 | "bit-set", 1279 | "bit-vec", 1280 | "bitflags", 1281 | "lazy_static", 1282 | "num-traits", 1283 | "rand", 1284 | "rand_chacha", 1285 | "rand_xorshift", 1286 | "regex-syntax 0.8.4", 1287 | "rusty-fork", 1288 | "tempfile", 1289 | "unarray", 1290 | ] 1291 | 1292 | [[package]] 1293 | name = "psm" 1294 | version = "0.1.21" 1295 | source = "registry+https://github.com/rust-lang/crates.io-index" 1296 | checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" 1297 | dependencies = [ 1298 | "cc", 1299 | ] 1300 | 1301 | [[package]] 1302 | name = "quick-error" 1303 | version = "1.2.3" 1304 | source = "registry+https://github.com/rust-lang/crates.io-index" 1305 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 1306 | 1307 | [[package]] 1308 | name = "quote" 1309 | version = "1.0.36" 1310 | source = "registry+https://github.com/rust-lang/crates.io-index" 1311 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 1312 | dependencies = [ 1313 | "proc-macro2", 1314 | ] 1315 | 1316 | [[package]] 1317 | name = "rand" 1318 | version = "0.8.5" 1319 | source = "registry+https://github.com/rust-lang/crates.io-index" 1320 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1321 | dependencies = [ 1322 | "libc", 1323 | "rand_chacha", 1324 | "rand_core", 1325 | ] 1326 | 1327 | [[package]] 1328 | name = "rand_chacha" 1329 | version = "0.3.1" 1330 | source = "registry+https://github.com/rust-lang/crates.io-index" 1331 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1332 | dependencies = [ 1333 | "ppv-lite86", 1334 | "rand_core", 1335 | ] 1336 | 1337 | [[package]] 1338 | name = "rand_core" 1339 | version = "0.6.4" 1340 | source = "registry+https://github.com/rust-lang/crates.io-index" 1341 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1342 | dependencies = [ 1343 | "getrandom", 1344 | ] 1345 | 1346 | [[package]] 1347 | name = "rand_xorshift" 1348 | version = "0.3.0" 1349 | source = "registry+https://github.com/rust-lang/crates.io-index" 1350 | checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" 1351 | dependencies = [ 1352 | "rand_core", 1353 | ] 1354 | 1355 | [[package]] 1356 | name = "redox_syscall" 1357 | version = "0.5.3" 1358 | source = "registry+https://github.com/rust-lang/crates.io-index" 1359 | checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" 1360 | dependencies = [ 1361 | "bitflags", 1362 | ] 1363 | 1364 | [[package]] 1365 | name = "redox_users" 1366 | version = "0.4.5" 1367 | source = "registry+https://github.com/rust-lang/crates.io-index" 1368 | checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" 1369 | dependencies = [ 1370 | "getrandom", 1371 | "libredox", 1372 | "thiserror", 1373 | ] 1374 | 1375 | [[package]] 1376 | name = "regex" 1377 | version = "1.10.5" 1378 | source = "registry+https://github.com/rust-lang/crates.io-index" 1379 | checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" 1380 | dependencies = [ 1381 | "aho-corasick", 1382 | "memchr", 1383 | "regex-automata", 1384 | "regex-syntax 0.8.4", 1385 | ] 1386 | 1387 | [[package]] 1388 | name = "regex-automata" 1389 | version = "0.4.7" 1390 | source = "registry+https://github.com/rust-lang/crates.io-index" 1391 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 1392 | dependencies = [ 1393 | "aho-corasick", 1394 | "memchr", 1395 | "regex-syntax 0.8.4", 1396 | ] 1397 | 1398 | [[package]] 1399 | name = "regex-syntax" 1400 | version = "0.6.29" 1401 | source = "registry+https://github.com/rust-lang/crates.io-index" 1402 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 1403 | 1404 | [[package]] 1405 | name = "regex-syntax" 1406 | version = "0.8.4" 1407 | source = "registry+https://github.com/rust-lang/crates.io-index" 1408 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 1409 | 1410 | [[package]] 1411 | name = "rustc-demangle" 1412 | version = "0.1.24" 1413 | source = "registry+https://github.com/rust-lang/crates.io-index" 1414 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1415 | 1416 | [[package]] 1417 | name = "rustix" 1418 | version = "0.38.34" 1419 | source = "registry+https://github.com/rust-lang/crates.io-index" 1420 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 1421 | dependencies = [ 1422 | "bitflags", 1423 | "errno", 1424 | "libc", 1425 | "linux-raw-sys", 1426 | "windows-sys", 1427 | ] 1428 | 1429 | [[package]] 1430 | name = "rustversion" 1431 | version = "1.0.17" 1432 | source = "registry+https://github.com/rust-lang/crates.io-index" 1433 | checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" 1434 | 1435 | [[package]] 1436 | name = "rusty-fork" 1437 | version = "0.3.0" 1438 | source = "registry+https://github.com/rust-lang/crates.io-index" 1439 | checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" 1440 | dependencies = [ 1441 | "fnv", 1442 | "quick-error", 1443 | "tempfile", 1444 | "wait-timeout", 1445 | ] 1446 | 1447 | [[package]] 1448 | name = "ryu" 1449 | version = "1.0.18" 1450 | source = "registry+https://github.com/rust-lang/crates.io-index" 1451 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 1452 | 1453 | [[package]] 1454 | name = "same-file" 1455 | version = "1.0.6" 1456 | source = "registry+https://github.com/rust-lang/crates.io-index" 1457 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1458 | dependencies = [ 1459 | "winapi-util", 1460 | ] 1461 | 1462 | [[package]] 1463 | name = "scopeguard" 1464 | version = "1.2.0" 1465 | source = "registry+https://github.com/rust-lang/crates.io-index" 1466 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1467 | 1468 | [[package]] 1469 | name = "serde" 1470 | version = "1.0.204" 1471 | source = "registry+https://github.com/rust-lang/crates.io-index" 1472 | checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" 1473 | dependencies = [ 1474 | "serde_derive", 1475 | ] 1476 | 1477 | [[package]] 1478 | name = "serde_bytes" 1479 | version = "0.11.15" 1480 | source = "registry+https://github.com/rust-lang/crates.io-index" 1481 | checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" 1482 | dependencies = [ 1483 | "serde", 1484 | ] 1485 | 1486 | [[package]] 1487 | name = "serde_derive" 1488 | version = "1.0.204" 1489 | source = "registry+https://github.com/rust-lang/crates.io-index" 1490 | checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" 1491 | dependencies = [ 1492 | "proc-macro2", 1493 | "quote", 1494 | "syn 2.0.71", 1495 | ] 1496 | 1497 | [[package]] 1498 | name = "serde_json" 1499 | version = "1.0.120" 1500 | source = "registry+https://github.com/rust-lang/crates.io-index" 1501 | checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" 1502 | dependencies = [ 1503 | "itoa", 1504 | "ryu", 1505 | "serde", 1506 | ] 1507 | 1508 | [[package]] 1509 | name = "serde_tokenstream" 1510 | version = "0.1.7" 1511 | source = "registry+https://github.com/rust-lang/crates.io-index" 1512 | checksum = "797ba1d80299b264f3aac68ab5d12e5825a561749db4df7cd7c8083900c5d4e9" 1513 | dependencies = [ 1514 | "proc-macro2", 1515 | "serde", 1516 | "syn 1.0.109", 1517 | ] 1518 | 1519 | [[package]] 1520 | name = "sha2" 1521 | version = "0.10.8" 1522 | source = "registry+https://github.com/rust-lang/crates.io-index" 1523 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 1524 | dependencies = [ 1525 | "cfg-if", 1526 | "cpufeatures", 1527 | "digest", 1528 | ] 1529 | 1530 | [[package]] 1531 | name = "siphasher" 1532 | version = "0.3.11" 1533 | source = "registry+https://github.com/rust-lang/crates.io-index" 1534 | checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 1535 | 1536 | [[package]] 1537 | name = "slab" 1538 | version = "0.4.9" 1539 | source = "registry+https://github.com/rust-lang/crates.io-index" 1540 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1541 | dependencies = [ 1542 | "autocfg", 1543 | ] 1544 | 1545 | [[package]] 1546 | name = "smallvec" 1547 | version = "1.13.2" 1548 | source = "registry+https://github.com/rust-lang/crates.io-index" 1549 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1550 | 1551 | [[package]] 1552 | name = "stacker" 1553 | version = "0.1.15" 1554 | source = "registry+https://github.com/rust-lang/crates.io-index" 1555 | checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" 1556 | dependencies = [ 1557 | "cc", 1558 | "cfg-if", 1559 | "libc", 1560 | "psm", 1561 | "winapi", 1562 | ] 1563 | 1564 | [[package]] 1565 | name = "string_cache" 1566 | version = "0.8.7" 1567 | source = "registry+https://github.com/rust-lang/crates.io-index" 1568 | checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" 1569 | dependencies = [ 1570 | "new_debug_unreachable", 1571 | "once_cell", 1572 | "parking_lot", 1573 | "phf_shared", 1574 | "precomputed-hash", 1575 | ] 1576 | 1577 | [[package]] 1578 | name = "strsim" 1579 | version = "0.10.0" 1580 | source = "registry+https://github.com/rust-lang/crates.io-index" 1581 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 1582 | 1583 | [[package]] 1584 | name = "syn" 1585 | version = "1.0.109" 1586 | source = "registry+https://github.com/rust-lang/crates.io-index" 1587 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1588 | dependencies = [ 1589 | "proc-macro2", 1590 | "quote", 1591 | "unicode-ident", 1592 | ] 1593 | 1594 | [[package]] 1595 | name = "syn" 1596 | version = "2.0.71" 1597 | source = "registry+https://github.com/rust-lang/crates.io-index" 1598 | checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" 1599 | dependencies = [ 1600 | "proc-macro2", 1601 | "quote", 1602 | "unicode-ident", 1603 | ] 1604 | 1605 | [[package]] 1606 | name = "tempfile" 1607 | version = "3.10.1" 1608 | source = "registry+https://github.com/rust-lang/crates.io-index" 1609 | checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" 1610 | dependencies = [ 1611 | "cfg-if", 1612 | "fastrand", 1613 | "rustix", 1614 | "windows-sys", 1615 | ] 1616 | 1617 | [[package]] 1618 | name = "term" 1619 | version = "0.7.0" 1620 | source = "registry+https://github.com/rust-lang/crates.io-index" 1621 | checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" 1622 | dependencies = [ 1623 | "dirs-next", 1624 | "rustversion", 1625 | "winapi", 1626 | ] 1627 | 1628 | [[package]] 1629 | name = "termcolor" 1630 | version = "1.4.1" 1631 | source = "registry+https://github.com/rust-lang/crates.io-index" 1632 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 1633 | dependencies = [ 1634 | "winapi-util", 1635 | ] 1636 | 1637 | [[package]] 1638 | name = "thiserror" 1639 | version = "1.0.63" 1640 | source = "registry+https://github.com/rust-lang/crates.io-index" 1641 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 1642 | dependencies = [ 1643 | "thiserror-impl", 1644 | ] 1645 | 1646 | [[package]] 1647 | name = "thiserror-impl" 1648 | version = "1.0.63" 1649 | source = "registry+https://github.com/rust-lang/crates.io-index" 1650 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 1651 | dependencies = [ 1652 | "proc-macro2", 1653 | "quote", 1654 | "syn 2.0.71", 1655 | ] 1656 | 1657 | [[package]] 1658 | name = "tiny-keccak" 1659 | version = "2.0.2" 1660 | source = "registry+https://github.com/rust-lang/crates.io-index" 1661 | checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" 1662 | dependencies = [ 1663 | "crunchy", 1664 | ] 1665 | 1666 | [[package]] 1667 | name = "tokio" 1668 | version = "1.38.1" 1669 | source = "registry+https://github.com/rust-lang/crates.io-index" 1670 | checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" 1671 | dependencies = [ 1672 | "backtrace", 1673 | "pin-project-lite", 1674 | "tokio-macros", 1675 | ] 1676 | 1677 | [[package]] 1678 | name = "tokio-macros" 1679 | version = "2.3.0" 1680 | source = "registry+https://github.com/rust-lang/crates.io-index" 1681 | checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" 1682 | dependencies = [ 1683 | "proc-macro2", 1684 | "quote", 1685 | "syn 2.0.71", 1686 | ] 1687 | 1688 | [[package]] 1689 | name = "typed-arena" 1690 | version = "2.0.2" 1691 | source = "registry+https://github.com/rust-lang/crates.io-index" 1692 | checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" 1693 | 1694 | [[package]] 1695 | name = "typenum" 1696 | version = "1.17.0" 1697 | source = "registry+https://github.com/rust-lang/crates.io-index" 1698 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 1699 | 1700 | [[package]] 1701 | name = "unarray" 1702 | version = "0.1.4" 1703 | source = "registry+https://github.com/rust-lang/crates.io-index" 1704 | checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" 1705 | 1706 | [[package]] 1707 | name = "unicode-ident" 1708 | version = "1.0.12" 1709 | source = "registry+https://github.com/rust-lang/crates.io-index" 1710 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1711 | 1712 | [[package]] 1713 | name = "unicode-segmentation" 1714 | version = "1.11.0" 1715 | source = "registry+https://github.com/rust-lang/crates.io-index" 1716 | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 1717 | 1718 | [[package]] 1719 | name = "unicode-width" 1720 | version = "0.1.13" 1721 | source = "registry+https://github.com/rust-lang/crates.io-index" 1722 | checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" 1723 | 1724 | [[package]] 1725 | name = "unicode-xid" 1726 | version = "0.2.4" 1727 | source = "registry+https://github.com/rust-lang/crates.io-index" 1728 | checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" 1729 | 1730 | [[package]] 1731 | name = "version_check" 1732 | version = "0.9.4" 1733 | source = "registry+https://github.com/rust-lang/crates.io-index" 1734 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1735 | 1736 | [[package]] 1737 | name = "wait-timeout" 1738 | version = "0.2.0" 1739 | source = "registry+https://github.com/rust-lang/crates.io-index" 1740 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 1741 | dependencies = [ 1742 | "libc", 1743 | ] 1744 | 1745 | [[package]] 1746 | name = "walkdir" 1747 | version = "2.5.0" 1748 | source = "registry+https://github.com/rust-lang/crates.io-index" 1749 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1750 | dependencies = [ 1751 | "same-file", 1752 | "winapi-util", 1753 | ] 1754 | 1755 | [[package]] 1756 | name = "wasi" 1757 | version = "0.11.0+wasi-snapshot-preview1" 1758 | source = "registry+https://github.com/rust-lang/crates.io-index" 1759 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1760 | 1761 | [[package]] 1762 | name = "winapi" 1763 | version = "0.3.9" 1764 | source = "registry+https://github.com/rust-lang/crates.io-index" 1765 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1766 | dependencies = [ 1767 | "winapi-i686-pc-windows-gnu", 1768 | "winapi-x86_64-pc-windows-gnu", 1769 | ] 1770 | 1771 | [[package]] 1772 | name = "winapi-i686-pc-windows-gnu" 1773 | version = "0.4.0" 1774 | source = "registry+https://github.com/rust-lang/crates.io-index" 1775 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1776 | 1777 | [[package]] 1778 | name = "winapi-util" 1779 | version = "0.1.8" 1780 | source = "registry+https://github.com/rust-lang/crates.io-index" 1781 | checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" 1782 | dependencies = [ 1783 | "windows-sys", 1784 | ] 1785 | 1786 | [[package]] 1787 | name = "winapi-x86_64-pc-windows-gnu" 1788 | version = "0.4.0" 1789 | source = "registry+https://github.com/rust-lang/crates.io-index" 1790 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1791 | 1792 | [[package]] 1793 | name = "windows-sys" 1794 | version = "0.52.0" 1795 | source = "registry+https://github.com/rust-lang/crates.io-index" 1796 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1797 | dependencies = [ 1798 | "windows-targets", 1799 | ] 1800 | 1801 | [[package]] 1802 | name = "windows-targets" 1803 | version = "0.52.6" 1804 | source = "registry+https://github.com/rust-lang/crates.io-index" 1805 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1806 | dependencies = [ 1807 | "windows_aarch64_gnullvm", 1808 | "windows_aarch64_msvc", 1809 | "windows_i686_gnu", 1810 | "windows_i686_gnullvm", 1811 | "windows_i686_msvc", 1812 | "windows_x86_64_gnu", 1813 | "windows_x86_64_gnullvm", 1814 | "windows_x86_64_msvc", 1815 | ] 1816 | 1817 | [[package]] 1818 | name = "windows_aarch64_gnullvm" 1819 | version = "0.52.6" 1820 | source = "registry+https://github.com/rust-lang/crates.io-index" 1821 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1822 | 1823 | [[package]] 1824 | name = "windows_aarch64_msvc" 1825 | version = "0.52.6" 1826 | source = "registry+https://github.com/rust-lang/crates.io-index" 1827 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1828 | 1829 | [[package]] 1830 | name = "windows_i686_gnu" 1831 | version = "0.52.6" 1832 | source = "registry+https://github.com/rust-lang/crates.io-index" 1833 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1834 | 1835 | [[package]] 1836 | name = "windows_i686_gnullvm" 1837 | version = "0.52.6" 1838 | source = "registry+https://github.com/rust-lang/crates.io-index" 1839 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1840 | 1841 | [[package]] 1842 | name = "windows_i686_msvc" 1843 | version = "0.52.6" 1844 | source = "registry+https://github.com/rust-lang/crates.io-index" 1845 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1846 | 1847 | [[package]] 1848 | name = "windows_x86_64_gnu" 1849 | version = "0.52.6" 1850 | source = "registry+https://github.com/rust-lang/crates.io-index" 1851 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1852 | 1853 | [[package]] 1854 | name = "windows_x86_64_gnullvm" 1855 | version = "0.52.6" 1856 | source = "registry+https://github.com/rust-lang/crates.io-index" 1857 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1858 | 1859 | [[package]] 1860 | name = "windows_x86_64_msvc" 1861 | version = "0.52.6" 1862 | source = "registry+https://github.com/rust-lang/crates.io-index" 1863 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1864 | 1865 | [[package]] 1866 | name = "zerocopy" 1867 | version = "0.7.35" 1868 | source = "registry+https://github.com/rust-lang/crates.io-index" 1869 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1870 | dependencies = [ 1871 | "zerocopy-derive", 1872 | ] 1873 | 1874 | [[package]] 1875 | name = "zerocopy-derive" 1876 | version = "0.7.35" 1877 | source = "registry+https://github.com/rust-lang/crates.io-index" 1878 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1879 | dependencies = [ 1880 | "proc-macro2", 1881 | "quote", 1882 | "syn 2.0.71", 1883 | ] 1884 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | # Production crates 5 | "cycles-ledger", 6 | 7 | # Crates used for testing only 8 | "depositor", 9 | "fake-cmc", 10 | ] 11 | 12 | [workspace.package] 13 | version = "1.0.0" 14 | edition = "2021" 15 | authors = ["DFINITY Stiftung "] 16 | license = "Apache-2.0" 17 | repository = "https://github.com/dfinity/cycles-ledger" 18 | publish = false # don't publish to crates.io 19 | 20 | [workspace.dependencies] 21 | candid = "0.10" 22 | candid_parser = "0.1" 23 | ic-cdk = { version = "0.12.2" } 24 | ic-cdk-macros = { version = "0.8.4" } 25 | icrc-ledger-types = "0.1.5" 26 | serde = "1" 27 | 28 | [workspace.metadata.release] # cargo-release 29 | # list of replacements to be made after issuing `cargo release SEMVER` 30 | pre-release-replacements = [ 31 | # cargo release expects to find CHANGELOG.md file inside the package that's supposed to be released (e.g. cycles-ledger), 32 | # therefore we need to go one directory level up, so it will search for the change log in the root of the repository 33 | { file = "../CHANGELOG.md", search = "Unreleased", replace = "{{version}}" }, 34 | { file = "../CHANGELOG.md", search = "\\.\\.\\.HEAD", replace = "...{{tag_name}}", exactly = 1 }, 35 | { file = "../CHANGELOG.md", search = "ReleaseDate", replace = "{{date}}" }, 36 | { file = "../CHANGELOG.md", search = "", replace = "\n\n## [Unreleased] - ReleaseDate", exactly = 1 }, 37 | { file = "../CHANGELOG.md", search = "", replace = "\n[Unreleased]: https://github.com/dfinity/cycles-ledger/compare/{{tag_name}}...HEAD", exactly = 1 }, 38 | ] 39 | 40 | [profile.release] 41 | lto = true 42 | opt-level = 'z' 43 | panic = 'abort' 44 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # This is copied and adapted from https://github.com/dfinity/exchange-rate-canister/blob/458adc36f9c3f6b7232a8868528262c0c3eb52ab/Dockerfile 2 | 3 | # Use this with 4 | # 5 | # docker build . -t cycles-ledger 6 | # container_id=$(docker create cycles-ledger no-op) 7 | # docker cp $container_id:cycles-ledger.wasm.gz cycles-ledger.wasm.gz 8 | # docker rm --volumes $container_id 9 | 10 | # This is the "builder", i.e. the base image used later to build the final 11 | # code. 12 | FROM --platform=linux/amd64 ubuntu:20.04 as builder 13 | SHELL ["bash", "-c"] 14 | 15 | ARG rust_version=1.85.0 16 | 17 | ENV TZ=UTC 18 | 19 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone && \ 20 | apt -yq update && \ 21 | apt -yqq install --no-install-recommends curl ca-certificates \ 22 | build-essential pkg-config libssl-dev llvm-dev liblmdb-dev clang cmake \ 23 | git jq 24 | 25 | # Install Rust and Cargo in /opt 26 | ENV RUSTUP_HOME=/opt/rustup \ 27 | CARGO_HOME=/opt/cargo \ 28 | PATH=/opt/cargo/bin:$PATH 29 | 30 | RUN curl --fail https://sh.rustup.rs -sSf \ 31 | | sh -s -- -y --default-toolchain ${rust_version}-x86_64-unknown-linux-gnu --no-modify-path && \ 32 | rustup default ${rust_version}-x86_64-unknown-linux-gnu && \ 33 | rustup target add wasm32-unknown-unknown && \ 34 | rustup show active-toolchain || rustup toolchain install 35 | 36 | ENV PATH=/cargo/bin:$PATH 37 | 38 | # Pre-build all cargo dependencies. Because cargo doesn't have a build option 39 | # to build only the dependencies, we pretend that our project is a simple, empty 40 | # `lib.rs`. Then we remove the dummy source files to make sure cargo rebuild 41 | # everything once the actual source code is COPYed (and e.g. doesn't trip on 42 | # timestamps being older) 43 | COPY Cargo.lock . 44 | COPY Cargo.toml . 45 | COPY cycles-ledger/Cargo.toml cycles-ledger/Cargo.toml 46 | COPY depositor/Cargo.toml depositor/Cargo.toml 47 | COPY fake-cmc/Cargo.toml fake-cmc/Cargo.toml 48 | RUN mkdir -p cycles-ledger/src && \ 49 | touch cycles-ledger/src/lib.rs && \ 50 | mkdir -p depositor/src && \ 51 | touch depositor/src/lib.rs && \ 52 | mkdir -p fake-cmc/src && \ 53 | touch fake-cmc/src/lib.rs && \ 54 | cargo build --target wasm32-unknown-unknown --release --package cycles-ledger && \ 55 | rm -rf cycles-ledger/ 56 | 57 | # Install dfx 58 | COPY dfx.json dfx.json 59 | 60 | ENV PATH="/root/.local/share/dfx/bin:${PATH}" 61 | RUN DFXVM_INIT_YES=true DFX_VERSION="$(jq -cr .dfx dfx.json)" \ 62 | sh -c "$(curl -fsSL https://internetcomputer.org/install.sh)" && \ 63 | dfx --version 64 | 65 | # Start the second container 66 | FROM --platform=linux/amd64 builder AS build 67 | SHELL ["bash", "-c"] 68 | 69 | # Build 70 | # ... put only git-tracked files in the build directory 71 | COPY . /build 72 | WORKDIR /build 73 | # Creates the wasm without creating the canister 74 | RUN dfx build --check cycles-ledger 75 | 76 | RUN ls -sh /build 77 | RUN ls -sh /build/.dfx/local/canisters/cycles-ledger/cycles-ledger.wasm.gz 78 | RUN sha256sum /build/.dfx/local/canisters/cycles-ledger/cycles-ledger.wasm.gz 79 | 80 | FROM --platform=linux/amd64 scratch AS scratch 81 | COPY --from=build /build/.dfx/local/canisters/cycles-ledger/cycles-ledger.wasm.gz / 82 | -------------------------------------------------------------------------------- /INTERFACE_SPECIFICATION.md: -------------------------------------------------------------------------------- 1 | ## Cycles Ledger API 2 | 3 | The canister ID of the cycles ledger is [`um5iw-rqaaa-aaaaq-qaaba-cai`](https://dashboard.internetcomputer.org/canister/um5iw-rqaaa-aaaaq-qaaba-cai). 4 | 5 | The cycles ledger complies with the [ICRC-1](https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-1/README.md), 6 | [ICRC-2](https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-2/README.md), and [ICRC-3](https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-3/README.md) standards and therefore offers all the corresponding endpoints. 7 | 8 | The additional endpoints are presented in the following. 9 | 10 | ### `deposit` 11 | ``` 12 | type Account = record { owner : principal; subaccount : opt vec nat8 }; 13 | 14 | type BlockIndex = nat; 15 | 16 | type DepositArgs = record { 17 | to : Account; 18 | memo : opt vec nat8; 19 | }; 20 | 21 | type DepositResult = record { balance : nat; block_index : BlockIndex }; 22 | 23 | deposit : (DepositArgs) -> (DepositResult); 24 | ``` 25 | 26 | This endpoint increments the balance of the provided account by the number of attached cycles. There is no fee when depositing cycles but at least 100M cycles must be attached, otherwise the call is rejected. 27 | The sender can optionally provide a memo as well. The memo may be at most 32 bytes long. 28 | 29 | ### `withdraw` 30 | ``` 31 | type WithdrawArgs = record { 32 | amount : nat; 33 | from_subaccount : opt vec nat8; 34 | to : principal; 35 | created_at_time : opt nat64; 36 | }; 37 | 38 | type RejectionCode = variant { 39 | NoError; 40 | CanisterError; 41 | SysTransient; 42 | DestinationInvalid; 43 | Unknown; 44 | SysFatal; 45 | CanisterReject; 46 | }; 47 | 48 | type WithdrawError = variant { 49 | GenericError : record { message : text; error_code : nat }; 50 | TemporarilyUnavailable; 51 | FailedToWithdraw : record { 52 | fee_block : opt nat; 53 | rejection_code : RejectionCode; 54 | rejection_reason : text; 55 | }; 56 | 57 | withdraw : (WithdrawArgs) -> (variant { Ok : BlockIndex; Err : WithdrawError }); 58 | ``` 59 | 60 | This endpoint withdraws the given amount from the caller's account and transfers the cycles to the provided canister ID. 61 | In addition to the amount and the canister ID (parameter `to`), the caller can provide a subaccount and a `created_at_time` timestamp. The timestamp is used to deduplicate calls, i.e., funds are withdrawn (at most) once when calling the endpoint multiple times with the same parameters. 62 | 63 | A fee of 100M cycles is deducted when withdrawing cycles. 64 | 65 | ### `withdraw_from` 66 | ``` 67 | type WithdrawFromArgs = record { 68 | spender_subaccount : opt vec nat8; 69 | from : Account; 70 | to : principal; 71 | amount : nat; 72 | created_at_time : opt nat64; 73 | }; 74 | 75 | withdraw_from : (WithdrawFromArgs) -> (variant { Ok : BlockIndex; Err : WithdrawFromError }); 76 | ``` 77 | This endpoint is similar to `withdraw` in that cycles are sent to the target canister (parameter `to`) if the call is successful. The difference is that the caller can specify any account (parameter `from`) from which the cycles are deducted. 78 | The owner of this account must have issued an `icrc2_approve` call beforehand, authorizing access to the funds for the caller's account, composed of the caller's principal and, optionally, a specific subaccount (parameter `spender_subaccount`). 79 | 80 | A fee of 100M cycles is deducted when withdrawing cycles. 81 | 82 | ### `create_canister` 83 | ``` 84 | type CanisterSettings = record { 85 | controllers : opt vec principal; 86 | compute_allocation : opt nat; 87 | memory_allocation : opt nat; 88 | freezing_threshold : opt nat; 89 | reserved_cycles_limit : opt nat; 90 | }; 91 | 92 | type SubnetFilter = record { 93 | subnet_type : opt text; 94 | }; 95 | 96 | type SubnetSelection = variant { 97 | Subnet : record { 98 | subnet : principal; 99 | }; 100 | Filter : SubnetFilter; 101 | }; 102 | 103 | type CmcCreateCanisterArgs = record { 104 | settings : opt CanisterSettings; 105 | subnet_selection : opt SubnetSelection; 106 | }; 107 | 108 | type CreateCanisterArgs = record { 109 | from_subaccount : opt vec nat8; 110 | created_at_time : opt nat64; 111 | amount : nat; 112 | creation_args : opt CmcCreateCanisterArgs; 113 | }; 114 | 115 | type CreateCanisterSuccess = record { 116 | block_id : BlockIndex; 117 | canister_id : principal; 118 | }; 119 | 120 | type CreateCanisterError = variant { 121 | InsufficientFunds : record { balance : nat }; 122 | TooOld; 123 | CreatedInFuture : record { ledger_time : nat64 }; 124 | TemporarilyUnavailable; 125 | Duplicate : record { 126 | duplicate_of : nat; 127 | canister_id : opt principal; 128 | }; 129 | FailedToCreate : record { 130 | fee_block : opt BlockIndex; 131 | refund_block : opt BlockIndex; 132 | error : text; 133 | }; 134 | GenericError : record { message : text; error_code : nat }; 135 | }; 136 | 137 | create_canister : (CreateCanisterArgs) -> (variant { Ok : CreateCanisterSuccess; Err : CreateCanisterError }); 138 | ``` 139 | 140 | This endpoint instructs the cycles ledger to create a canister, deducting the required cycles from the caller's account. 141 | The caller must specify the number of cycles to be used for the creation of the canister. In addition to the specified amount, a fee of 100M cycles is deducted. 142 | A `created_at_time` timestamp can be provided to ensure that (at most) one canister is created when calling the endpoint multiple times with the same parameters. 143 | The caller can specify the desired [canister settings](https://internetcomputer.org/docs/current/developer-docs/smart-contracts/maintain/settings). If no settings are provided, the standard settings are used. 144 | Moreover, the caller can influence where the canister will be deployed by selecting a specific subnet or by providing a [subnet type](https://internetcomputer.org/docs/current/references/subnets/subnet-types/). 145 | 146 | ### `create_canister_from` 147 | ``` 148 | type CreateCanisterFromArgs = record { 149 | from : Account; 150 | spender_subaccount : opt vec nat8; 151 | created_at_time : opt nat64; 152 | amount : nat; 153 | creation_args : opt CmcCreateCanisterArgs; 154 | }; 155 | 156 | create_canister_from : (CreateCanisterFromArgs) -> (variant { Ok : CreateCanisterSuccess; Err : CreateCanisterFromError }); 157 | ``` 158 | 159 | This endpoint creates a new canister the same way as the `create_canister` endpoint, the difference being the source of the cycles spent to create the canister. 160 | The call specifies the account (parameter `from`) from which the cycles are deducted. Note that there is again a fee of 100M added to the specified amount. 161 | The owner of this account must have issued an `icrc2_approve` call beforehand, authorizing access to the funds for the caller's account, composed of the caller's principal and, optionally, a specific subaccount (parameter `spender_subaccount`). 162 | 163 | -------------------------------------------------------------------------------- /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 2023 DFINITY Foundation. 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 | # Cycles Ledger 2 | 3 | The cycles ledger is a global ledger canister that enables principal IDs to hold cycles. 4 | 5 | The cycles ledger complies with the [ICRC-2](https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-2/README.md) and [ICRC-1](https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-1/README.md) token standards. 6 | Additionally, it implements the endpoints defined in the proposed [ICRC-3](https://github.com/dfinity/ICRC-1/pull/128) standard. 7 | 8 | The cycles ledger further provides endpoints to deposit and withdraw cycles, and also 9 | to create canisters using cycles. The full specification can be found [here](INTERFACE_SPECIFICATION.md). 10 | 11 | ## Cycles Ledger Canister 12 | 13 | ### Principal 14 | ```text 15 | um5iw-rqaaa-aaaaq-qaaba-cai 16 | ``` 17 | 18 | [View on Internet Computer Dashboard](https://dashboard.internetcomputer.org/canister/um5iw-rqaaa-aaaaq-qaaba-cai) 19 | 20 | ## Depositing Cycles 21 | 22 | The cycles ledger has the following endpoint for other canisters to deposit cycles. 23 | 24 | ``` 25 | type DepositArgs = record { 26 | to : Account; 27 | memo : opt vec nat8; 28 | }; 29 | 30 | type DepositResult = record { balance : nat; block_index : nat }; 31 | 32 | deposit : (DepositArgs) -> (DepositResult); 33 | ``` 34 | 35 | When invoked with a particular account (and, optionally, a memo), the balance of the account is incremented by the number of cycles attached to the call. There is no fee when depositing cycles; however, the number of cycles must be at least the transfer fee of **100M cycles**. 36 | 37 | > NOTE: The deposit is rejected if fewer than 100M cycles are attached to the call. 38 | 39 | ## Withdrawing Cycles 40 | 41 | The cycles ledger has the following endpoint to withdraw cycles to other canisters. 42 | 43 | ``` 44 | type BlockIndex = nat; 45 | 46 | type RejectionCode = variant { 47 | NoError; 48 | CanisterError; 49 | SysTransient; 50 | DestinationInvalid; 51 | Unknown; 52 | SysFatal; 53 | CanisterReject; 54 | }; 55 | 56 | type WithdrawArgs = record { 57 | amount : nat; 58 | from_subaccount : opt vec nat8; 59 | to : principal; 60 | created_at_time : opt nat64; 61 | }; 62 | 63 | type WithdrawError = variant { 64 | GenericError : record { message : text; error_code : nat }; 65 | TemporarilyUnavailable; 66 | FailedToWithdraw : record { 67 | fee_block : opt nat; 68 | rejection_code : RejectionCode; 69 | rejection_reason : text; 70 | }; 71 | Duplicate : record { duplicate_of : nat }; 72 | BadFee : record { expected_fee : nat }; 73 | InvalidReceiver : record { receiver : principal }; 74 | CreatedInFuture : record { ledger_time : nat64 }; 75 | TooOld; 76 | InsufficientFunds : record { balance : nat }; 77 | }; 78 | 79 | withdraw : (WithdrawArgs) -> (variant { Ok : BlockIndex; Err : WithdrawError }); 80 | ``` 81 | 82 | The two required parameters are the amount to be sent and the principal ID of 83 | the targeted canister ID. Optionally, the subaccount from which cycles are 84 | deducted and the time at which the transaction is created can be set as well. 85 | 86 | There is a fee of **100M cycles** for withdrawing cycles to another canister. 87 | 88 | > NOTE: The function returns an error when the parameter `to` is not a valid canister ID. 89 | 90 | ## Creating Canisters Using Cycles 91 | 92 | The canister creation process via cycles can be triggered from the cycles ledger 93 | using the endpoint `create_canister`. 94 | 95 | ``` 96 | type CreateCanisterArgs = record { 97 | from_subaccount : opt vec nat8; 98 | created_at_time : opt nat64; 99 | amount : nat; 100 | creation_args : opt CmcCreateCanisterArgs; 101 | }; 102 | 103 | type CmcCreateCanisterArgs = record { 104 | settings : opt CanisterSettings; 105 | subnet_selection : opt SubnetSelection; 106 | }; 107 | 108 | type CanisterSettings = record { 109 | controllers : opt vec principal; 110 | compute_allocation : opt nat; 111 | memory_allocation : opt nat; 112 | freezing_threshold : opt nat; 113 | }; 114 | 115 | type SubnetFilter = record { 116 | subnet_type : opt text; 117 | }; 118 | 119 | type SubnetSelection = variant { 120 | /// Choose a specific subnet 121 | Subnet : record { 122 | subnet : principal; 123 | }; 124 | Filter : SubnetFilter; 125 | }; 126 | 127 | type CreateCanisterError = variant { 128 | InsufficientFunds : record { balance : nat }; 129 | TooOld; 130 | CreatedInFuture : record { ledger_time : nat64 }; 131 | TemporarilyUnavailable; 132 | Duplicate : record { duplicate_of : nat }; 133 | FailedToCreate : record { 134 | fee_block : opt BlockIndex; 135 | refund_block : opt BlockIndex; 136 | error : text; 137 | }; 138 | GenericError : record { message : text; error_code : nat }; 139 | }; 140 | 141 | create_canister : (CreateCanisterArgs) -> (variant { Ok : CreateCanisterSuccess; Err : CreateCanisterError }); 142 | ``` 143 | 144 | The only parameter that must be provided is the number of cycles that should 145 | be used for the canister creation. 146 | The cycles ledger fee of **100M** cycles is deducted from the user's account 147 | together with the specified `amount`. The cycles ledger then sends the request to create a canister 148 | to the cycles minting canister, attaching `amount` cycles to the call. 149 | The cost for the canister creation itself can be found 150 | [here](https://internetcomputer.org/docs/current/developer-docs/gas-cost). 151 | 152 | > NOTE: The canister is created on a **random subnet** unless specified otherwise. `SubnetSelection` 153 | can be used to specify a particular subnet or subnet type. 154 | 155 | ## Build the cycles-ledger 156 | 157 | The cycles-ledger should be built using the script [./scripts/docker-build](./scripts/docker-build). The script will put the `cycles-ledger.wasm.gz` in the directory where it was run. 158 | 159 | ## Make a new Release 160 | 161 | The CI job [release-with-github.yml](https://github.com/dfinity/cycles-ledger/actions/workflows/release-with-github.yml) is responsible to create a new release. The release job uses [cargo-release](https://github.com/crate-ci/cargo-release/blob/master/docs/reference.md). This project follows [Semantic Versioning 2.0.0](https://semver.org/) (aka semver). 162 | 163 | The release job can be triggered by using [`gh`](https://cli.github.com/) or [directly from github](https://github.com/dfinity/cycles-ledger/actions/workflows/release-with-github.yml): 164 | 165 | ``` 166 | gh workflow run --repo dfinity/cycles-ledger "release-with-github.yml" -f semverBump=(major|minor|patch) 167 | ``` 168 | 169 | The job will then bump the version based on the strategy passed via `semverBump`, make the release and make a PR with the version changes and the release linked to the PR. See [this](https://github.com/crate-ci/cargo-release/blob/master/docs/reference.md#bump-level) for valid `semverBump` values and their effect. 170 | -------------------------------------------------------------------------------- /assets/Cycles_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/cycles-ledger/01236e4d60738fc2277d47d16b95f28cff564370/assets/Cycles_Icon.png -------------------------------------------------------------------------------- /assets/Cycles_Icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /canister_ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "cycles-ledger": { 3 | "ic": "um5iw-rqaaa-aaaaq-qaaba-cai" 4 | }, 5 | "index": { 6 | "ic": "ul4oc-4iaaa-aaaaq-qaabq-cai" 7 | } 8 | } -------------------------------------------------------------------------------- /cycles-ledger/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cycles-ledger" 3 | description = "The cycles ledger is a global ledger canister that enables principal IDs to hold cycles." 4 | version.workspace = true 5 | edition.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | publish.workspace = true 10 | 11 | [dependencies] 12 | anyhow = "1.0.75" 13 | candid.workspace = true 14 | ciborium = "0.2" 15 | ic-cdk.workspace = true 16 | ic-cdk-macros.workspace = true 17 | ic-certified-map = "0.4.0" 18 | ic-stable-structures = "0.6.4" 19 | icrc-ledger-types.workspace = true 20 | hex = "0.4.3" 21 | minicbor = { version = "0.19.1", features = ["alloc", "derive"] } 22 | num-bigint = "0.4" 23 | num-traits = "0.2" 24 | serde.workspace = true 25 | serde_bytes = "0.11" 26 | thiserror = "1.0" 27 | ic-canister-log = "0.2.0" 28 | ic-canisters-http-types = { git = "https://github.com/dfinity/ic", rev = "b2f18ac0794d2225b53d9c3190b60dbadb4ac9b9" } 29 | ic-metrics-encoder = "1.1" 30 | serde_json = "1.0.107" 31 | leb128 = "0.2.5" 32 | 33 | [dev-dependencies] 34 | assert_matches = "1.5.0" 35 | candid_parser.workspace = true 36 | depositor = { path = "../depositor" } 37 | escargot = { version = "0.5.7", features = ["print"] } 38 | ic-cbor = "2.3.0" 39 | ic-certification = "2.3.0" 40 | ic-certificate-verification = "2.3.0" 41 | ic-test-state-machine-client = "3.0.0" 42 | icrc1-test-env-state-machine = "0.1.2" 43 | icrc1-test-suite = "0.1.2" 44 | lazy_static = "1.4.0" 45 | proptest = "1.2.0" 46 | tempfile = "3.10.1" 47 | tokio = { version = "1.36.0", features = ["rt", "macros"] } 48 | 49 | [features] 50 | testing = [] 51 | -------------------------------------------------------------------------------- /cycles-ledger/cycles-ledger.did: -------------------------------------------------------------------------------- 1 | type Account = record { owner : principal; subaccount : opt vec nat8 }; 2 | type Allowance = record { allowance : nat; expires_at : opt nat64 }; 3 | type AllowanceArgs = record { account : Account; spender : Account }; 4 | type BlockIndex = nat; 5 | type ApproveArgs = record { 6 | from_subaccount : opt vec nat8; 7 | spender : Account; 8 | amount : nat; 9 | expected_allowance : opt nat; 10 | expires_at : opt nat64; 11 | fee : opt nat; 12 | memo : opt vec nat8; 13 | created_at_time : opt nat64; 14 | }; 15 | type ApproveError = variant { 16 | BadFee : record { expected_fee : nat }; 17 | InsufficientFunds : record { balance : nat }; 18 | AllowanceChanged : record { current_allowance : nat }; 19 | Expired : record { ledger_time : nat64 }; 20 | TooOld; 21 | CreatedInFuture : record { ledger_time : nat64 }; 22 | Duplicate : record { duplicate_of : nat }; 23 | TemporarilyUnavailable; 24 | GenericError : record { message : text; error_code : nat }; 25 | }; 26 | type DepositArgs = record { 27 | to : Account; 28 | memo : opt vec nat8; 29 | }; 30 | type DepositResult = record { balance : nat; block_index : BlockIndex }; 31 | type RejectionCode = variant { 32 | NoError; 33 | CanisterError; 34 | SysTransient; 35 | DestinationInvalid; 36 | Unknown; 37 | SysFatal; 38 | CanisterReject; 39 | }; 40 | type WithdrawArgs = record { 41 | amount : nat; 42 | from_subaccount : opt vec nat8; 43 | to : principal; 44 | created_at_time : opt nat64; 45 | }; 46 | type WithdrawError = variant { 47 | GenericError : record { message : text; error_code : nat }; 48 | TemporarilyUnavailable; 49 | FailedToWithdraw : record { 50 | fee_block : opt nat; 51 | rejection_code : RejectionCode; 52 | rejection_reason : text; 53 | }; 54 | Duplicate : record { duplicate_of : nat }; 55 | BadFee : record { expected_fee : nat }; 56 | InvalidReceiver : record { receiver : principal }; 57 | CreatedInFuture : record { ledger_time : nat64 }; 58 | TooOld; 59 | InsufficientFunds : record { balance : nat }; 60 | }; 61 | type WithdrawFromArgs = record { 62 | spender_subaccount : opt vec nat8; 63 | from : Account; 64 | to : principal; 65 | amount : nat; 66 | created_at_time : opt nat64; 67 | }; 68 | type WithdrawFromError = variant { 69 | GenericError : record { message : text; error_code : nat }; 70 | TemporarilyUnavailable; 71 | FailedToWithdrawFrom : record { 72 | withdraw_from_block : opt nat; 73 | refund_block : opt nat; 74 | approval_refund_block : opt nat; 75 | rejection_code : RejectionCode; 76 | rejection_reason : text; 77 | }; 78 | Duplicate : record { duplicate_of : BlockIndex }; 79 | InvalidReceiver : record { receiver : principal }; 80 | CreatedInFuture : record { ledger_time : nat64 }; 81 | TooOld; 82 | InsufficientFunds : record { balance : nat }; 83 | InsufficientAllowance : record { allowance : nat }; 84 | }; 85 | type SupportedStandard = record { url : text; name : text }; 86 | type SupportedBlockType = record { block_type : text; url : text }; 87 | type TransferArgs = record { 88 | from_subaccount : opt vec nat8; 89 | to : Account; 90 | amount : nat; 91 | fee : opt nat; 92 | memo : opt vec nat8; 93 | created_at_time : opt nat64; 94 | }; 95 | type TransferError = variant { 96 | BadFee : record { expected_fee : nat }; 97 | BadBurn : record { min_burn_amount : nat }; 98 | InsufficientFunds : record { balance : nat }; 99 | TooOld; 100 | CreatedInFuture : record { ledger_time : nat64 }; 101 | Duplicate : record { duplicate_of : nat }; 102 | TemporarilyUnavailable; 103 | GenericError : record { message : text; error_code : nat }; 104 | }; 105 | type TransferFromArgs = record { 106 | spender_subaccount : opt vec nat8; 107 | from : Account; 108 | to : Account; 109 | amount : nat; 110 | fee : opt nat; 111 | memo : opt vec nat8; 112 | created_at_time : opt nat64; 113 | }; 114 | type TransferFromError = variant { 115 | BadFee : record { expected_fee : nat }; 116 | BadBurn : record { min_burn_amount : nat }; 117 | InsufficientFunds : record { balance : nat }; 118 | InsufficientAllowance : record { allowance : nat }; 119 | TooOld; 120 | CreatedInFuture : record { ledger_time : nat64 }; 121 | Duplicate : record { duplicate_of : nat }; 122 | TemporarilyUnavailable; 123 | GenericError : record { message : text; error_code : nat }; 124 | }; 125 | type Value = variant { 126 | Int : int; 127 | Map : vec record { text; Value }; 128 | Nat : nat; 129 | Nat64 : nat64; 130 | Blob : vec nat8; 131 | Text : text; 132 | Array : vec Value; 133 | }; 134 | 135 | type GetArchivesArgs = record { 136 | // The last archive seen by the client. 137 | // The ledger will return archives coming 138 | // after this one if set, otherwise it 139 | // will return the first archives. 140 | from : opt principal; 141 | }; 142 | 143 | type GetArchivesResult = vec record { 144 | // The id of the archive 145 | canister_id : principal; 146 | 147 | // The first block in the archive 148 | start : nat; 149 | 150 | // The last block in the archive 151 | end : nat; 152 | }; 153 | 154 | type GetBlocksArgs = vec record { start : nat; length : nat }; 155 | 156 | type GetBlocksResult = record { 157 | // Total number of blocks in the 158 | // block log. 159 | log_length : nat; 160 | 161 | blocks : vec record { id : nat; block : Value }; 162 | 163 | // The archived_blocks vector is always going to be empty 164 | // for this ledger because there is no archive node. 165 | archived_blocks : vec record { 166 | args : GetBlocksArgs; 167 | callback : func(GetBlocksArgs) -> (GetBlocksResult) query; 168 | }; 169 | }; 170 | 171 | type DataCertificate = record { 172 | // See https://internetcomputer.org/docs/current/references/ic-interface-spec#certification 173 | certificate : blob; 174 | 175 | // CBOR encoded hash_tree 176 | hash_tree : blob; 177 | }; 178 | 179 | type InitArgs = record { 180 | max_blocks_per_request : nat64; 181 | index_id : opt principal; 182 | }; 183 | 184 | type ChangeIndexId = variant { 185 | Unset; 186 | SetTo : principal; 187 | }; 188 | 189 | type UpgradeArgs = record { 190 | max_blocks_per_request : opt nat64; 191 | change_index_id : opt ChangeIndexId; 192 | }; 193 | 194 | type LedgerArgs = variant { 195 | Init : InitArgs; 196 | Upgrade : opt UpgradeArgs; 197 | }; 198 | 199 | type CreateCanisterArgs = record { 200 | from_subaccount : opt vec nat8; 201 | created_at_time : opt nat64; 202 | amount : nat; 203 | creation_args : opt CmcCreateCanisterArgs; 204 | }; 205 | 206 | type CreateCanisterFromArgs = record { 207 | from : Account; 208 | spender_subaccount : opt vec nat8; 209 | created_at_time : opt nat64; 210 | amount : nat; 211 | creation_args : opt CmcCreateCanisterArgs; 212 | }; 213 | 214 | type CmcCreateCanisterArgs = record { 215 | // Optional canister settings that, if set, are applied to the newly created canister. 216 | // If not specified, the caller is the controller of the canister and the other settings are set to default values. 217 | settings : opt CanisterSettings; 218 | 219 | // Optional instructions to select on which subnet the new canister will be created on. 220 | subnet_selection : opt SubnetSelection; 221 | }; 222 | 223 | type CanisterSettings = record { 224 | controllers : opt vec principal; 225 | compute_allocation : opt nat; 226 | memory_allocation : opt nat; 227 | freezing_threshold : opt nat; 228 | reserved_cycles_limit : opt nat; 229 | }; 230 | 231 | type SubnetSelection = variant { 232 | /// Choose a specific subnet 233 | Subnet : record { 234 | subnet : principal; 235 | }; 236 | // Choose a random subnet that satisfies the specified properties. 237 | Filter : SubnetFilter; 238 | }; 239 | 240 | type SubnetFilter = record { 241 | subnet_type : opt text; 242 | }; 243 | 244 | type CreateCanisterSuccess = record { 245 | block_id : BlockIndex; 246 | canister_id : principal; 247 | }; 248 | 249 | type CreateCanisterError = variant { 250 | InsufficientFunds : record { balance : nat }; 251 | TooOld; 252 | CreatedInFuture : record { ledger_time : nat64 }; 253 | TemporarilyUnavailable; 254 | Duplicate : record { 255 | duplicate_of : nat; 256 | // If the original transaction created a canister then this field will contain the canister id. 257 | canister_id : opt principal; 258 | }; 259 | FailedToCreate : record { 260 | fee_block : opt BlockIndex; 261 | refund_block : opt BlockIndex; 262 | error : text; 263 | }; 264 | GenericError : record { message : text; error_code : nat }; 265 | }; 266 | 267 | type CreateCanisterFromError = variant { 268 | InsufficientFunds : record { balance : nat }; 269 | InsufficientAllowance : record { allowance : nat }; 270 | TooOld; 271 | CreatedInFuture : record { ledger_time : nat64 }; 272 | TemporarilyUnavailable; 273 | Duplicate : record { 274 | duplicate_of : nat; 275 | // If the original transaction created a canister then this field will contain the canister id. 276 | canister_id : opt principal; 277 | }; 278 | FailedToCreateFrom : record { 279 | create_from_block : opt BlockIndex; 280 | refund_block : opt BlockIndex; 281 | approval_refund_block : opt BlockIndex; 282 | rejection_code : RejectionCode; 283 | rejection_reason : text; 284 | }; 285 | GenericError : record { message : text; error_code : nat }; 286 | }; 287 | 288 | type MetadataValue = variant { 289 | Nat : nat; 290 | Int : int; 291 | Text : text; 292 | Blob : blob; 293 | }; 294 | 295 | type HttpRequest = record { 296 | url : text; 297 | method : text; 298 | body : vec nat8; 299 | headers : vec record { text; text }; 300 | }; 301 | type HttpResponse = record { 302 | body : vec nat8; 303 | headers : vec record { text; text }; 304 | status_code : nat16; 305 | }; 306 | 307 | service : (ledger_args : LedgerArgs) -> { 308 | deposit : (DepositArgs) -> (DepositResult); 309 | http_request : (HttpRequest) -> (HttpResponse) query; 310 | icrc1_balance_of : (Account) -> (nat) query; 311 | icrc1_decimals : () -> (nat8) query; 312 | icrc1_fee : () -> (nat) query; 313 | icrc1_metadata : () -> (vec record { text; MetadataValue }) query; 314 | icrc1_minting_account : () -> (opt Account) query; 315 | icrc1_name : () -> (text) query; 316 | icrc1_supported_standards : () -> (vec SupportedStandard) query; 317 | icrc1_symbol : () -> (text) query; 318 | icrc1_total_supply : () -> (nat) query; 319 | icrc1_transfer : (TransferArgs) -> (variant { Ok : BlockIndex; Err : TransferError }); 320 | icrc2_allowance : (AllowanceArgs) -> (Allowance) query; 321 | icrc2_approve : (ApproveArgs) -> (variant { Ok : nat; Err : ApproveError }); 322 | icrc2_transfer_from : (TransferFromArgs) -> (variant { Ok : nat; Err : TransferFromError }); 323 | icrc3_get_archives : (GetArchivesArgs) -> (GetArchivesResult) query; 324 | icrc3_get_tip_certificate : () -> (opt DataCertificate) query; 325 | icrc3_get_blocks : (GetBlocksArgs) -> (GetBlocksResult) query; 326 | icrc3_supported_block_types : () -> (vec SupportedBlockType) query; 327 | withdraw : (WithdrawArgs) -> (variant { Ok : BlockIndex; Err : WithdrawError }); 328 | withdraw_from : (WithdrawFromArgs) -> (variant { Ok : BlockIndex; Err : WithdrawFromError }); 329 | create_canister : (CreateCanisterArgs) -> (variant { Ok : CreateCanisterSuccess; Err : CreateCanisterError }); 330 | create_canister_from : (CreateCanisterFromArgs) -> (variant { Ok : CreateCanisterSuccess; Err : CreateCanisterFromError }); 331 | }; 332 | -------------------------------------------------------------------------------- /cycles-ledger/src/compact_account.rs: -------------------------------------------------------------------------------- 1 | use candid::Principal; 2 | use icrc_ledger_types::icrc1::account::{Account, Subaccount}; 3 | use serde::de::Error; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_bytes::ByteBuf; 6 | 7 | /// A compact representation of an Account. 8 | /// 9 | /// Instead of encoding accounts as structs with named fields, 10 | /// we encode them as tuples with variables number of elements. 11 | /// ```text 12 | /// [bytes] <=> Account { owner: bytes, subaccount : None } 13 | /// [x: bytes, y: bytes] <=> Account { owner: x, subaccount: Some(y) } 14 | /// ``` 15 | #[derive(Serialize, Deserialize)] 16 | #[serde(transparent)] 17 | pub struct CompactAccount(Vec); 18 | 19 | impl From for CompactAccount { 20 | fn from(acc: Account) -> Self { 21 | let mut components = vec![ByteBuf::from(acc.owner.as_slice().to_vec())]; 22 | if let Some(sub) = acc.subaccount { 23 | components.push(ByteBuf::from(sub.to_vec())) 24 | } 25 | CompactAccount(components) 26 | } 27 | } 28 | 29 | impl TryFrom for Account { 30 | type Error = String; 31 | fn try_from(compact: CompactAccount) -> Result { 32 | let elems = compact.0; 33 | if elems.is_empty() { 34 | return Err("account tuple must have at least one element".to_string()); 35 | } 36 | if elems.len() > 2 { 37 | return Err(format!( 38 | "account tuple must have at most two elements, got {}", 39 | elems.len() 40 | )); 41 | } 42 | 43 | let principal = 44 | Principal::try_from(&elems[0][..]).map_err(|e| format!("invalid principal: {}", e))?; 45 | let subaccount = if elems.len() > 1 { 46 | Some(Subaccount::try_from(&elems[1][..]).map_err(|_| { 47 | format!( 48 | "invalid subaccount: expected 32 bytes, got {}", 49 | elems[1].len() 50 | ) 51 | })?) 52 | } else { 53 | None 54 | }; 55 | 56 | Ok(Account { 57 | owner: principal, 58 | subaccount, 59 | }) 60 | } 61 | } 62 | 63 | pub fn serialize(acc: &Account, s: S) -> Result 64 | where 65 | S: serde::ser::Serializer, 66 | { 67 | CompactAccount::from(*acc).serialize(s) 68 | } 69 | 70 | pub fn deserialize<'de, D>(d: D) -> Result 71 | where 72 | D: serde::de::Deserializer<'de>, 73 | { 74 | let compact_account = CompactAccount::deserialize(d)?; 75 | Account::try_from(compact_account).map_err(D::Error::custom) 76 | } 77 | 78 | pub mod opt { 79 | use super::*; 80 | 81 | pub fn serialize(acc: &Option, s: S) -> Result 82 | where 83 | S: serde::ser::Serializer, 84 | { 85 | acc.map(CompactAccount::from).serialize(s) 86 | } 87 | 88 | pub fn deserialize<'de, D>(d: D) -> Result, D::Error> 89 | where 90 | D: serde::de::Deserializer<'de>, 91 | { 92 | type OptionalCompactAccount = Option; 93 | match OptionalCompactAccount::deserialize(d)? { 94 | Some(compact_account) => Account::try_from(compact_account) 95 | .map(Some) 96 | .map_err(D::Error::custom), 97 | None => Ok(None), 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /cycles-ledger/src/config.rs: -------------------------------------------------------------------------------- 1 | use candid::{CandidType, Deserialize, Principal}; 2 | use ic_stable_structures::Storable; 3 | use serde::Serialize; 4 | use std::{borrow::Cow, time::Duration}; 5 | 6 | pub const FEE: u128 = 100_000_000; 7 | pub const DECIMALS: u8 = 12; 8 | pub const TOKEN_NAME: &str = "Trillion Cycles"; 9 | pub const TOKEN_SYMBOL: &str = "TCYCLES"; 10 | // URI encoding of the file `/assets/Cycles_Icon.svg` 11 | pub const TOKEN_LOGO: &str = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjgzIiBoZWlnaHQ9IjI4NCIgdmlld0JveD0iMCAwIDI4MyAyODQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yODAuNDM2IDE0Mi4wNDRDMjgwLjQzNiA2NS4zMTMxIDIxOC4yMzMgMy4xMTAzNSAxNDEuNTAyIDMuMTEwMzVDNjQuNzcwNyAzLjExMDM1IDIuNTY3ODcgNjUuMzEzMSAyLjU2Nzg3IDE0Mi4wNDRDMi41Njc4NyAyMTguNzc1IDY0Ljc3MDcgMjgwLjk3OCAxNDEuNTAyIDI4MC45NzhDMjE4LjIzMyAyODAuOTc4IDI4MC40MzYgMjE4Ljc3NSAyODAuNDM2IDE0Mi4wNDRaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBvcGFjaXR5PSIwLjIiIGQ9Ik0xNDEuNTAxIDI1NC45NTVDMjAzLjg2IDI1NC45NTUgMjU0LjQxMiAyMDQuNDAzIDI1NC40MTIgMTQyLjA0NEMyNTQuNDEyIDc5LjY4NDYgMjAzLjg2IDI5LjEzMjQgMTQxLjUwMSAyOS4xMzI0Qzc5LjE0MTUgMjkuMTMyNCAyOC41ODk0IDc5LjY4NDYgMjguNTg5NCAxNDIuMDQ0QzI4LjU4OTQgMjA0LjQwMyA3OS4xNDE1IDI1NC45NTUgMTQxLjUwMSAyNTQuOTU1WiIgZmlsbD0id2hpdGUiIHN0cm9rZT0idXJsKCNwYWludDBfbGluZWFyXzIwMV8xNTQpIiBzdHJva2Utd2lkdGg9IjE0LjExMzkiLz4KPHBhdGggZD0iTTE1NC4yMDQgMTIzLjY5NlY1MC4zMDM2TDk5LjE1OTcgMTYwLjM5MkgxMzUuODU2VjIzMy43ODRMMTkwLjkgMTIzLjY5NkgxNTQuMjA0WiIgZmlsbD0idXJsKCNwYWludDFfbGluZWFyXzIwMV8xNTQpIi8+CjxwYXRoIGQ9Ik0yODAuNDM2IDE0Mi4wNDRDMjgwLjQzNiA2NS4zMTMxIDIxOC4yMzMgMy4xMTAzNSAxNDEuNTAyIDMuMTEwMzVDNjQuNzcwNyAzLjExMDM1IDIuNTY3ODcgNjUuMzEzMSAyLjU2Nzg3IDE0Mi4wNDRDMi41Njc4NyAyMTguNzc1IDY0Ljc3MDcgMjgwLjk3OCAxNDEuNTAyIDI4MC45NzhDMjE4LjIzMyAyODAuOTc4IDI4MC40MzYgMjE4Ljc3NSAyODAuNDM2IDE0Mi4wNDRaIiBzdHJva2U9InVybCgjcGFpbnQyX2xpbmVhcl8yMDFfMTU0KSIgc3Ryb2tlLXdpZHRoPSI0LjQxMDYiLz4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhcl8yMDFfMTU0IiB4MT0iMjEuNTMyNCIgeTE9IjIyLjA3NTYiIHgyPSIyNjYuODk2IiB5Mj0iNzcuMDcwOCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjM0IwMEI5Ii8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzI1ODZCNiIvPgo8L2xpbmVhckdyYWRpZW50Pgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MV9saW5lYXJfMjAxXzE1NCIgeDE9Ijk5LjE1OTciIHkxPSI1MC4zMDM2IiB4Mj0iMTk2LjQ2NiIgeTI9IjYxLjIwODQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iIzNCMDBCOSIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyNTg2QjYiLz4KPC9saW5lYXJHcmFkaWVudD4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDJfbGluZWFyXzIwMV8xNTQiIHgxPSIwLjM2MjU3MyIgeTE9IjAuOTA1MDUxIiB4Mj0iMjg5LjAyNSIgeTI9IjY1LjYwNSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjM0IwMEI5Ii8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzI1ODZCNiIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo="; 12 | pub const MAX_MEMO_LENGTH: u64 = 32; 13 | pub const PERMITTED_DRIFT: Duration = Duration::from_secs(60); 14 | pub const TRANSACTION_WINDOW: Duration = Duration::from_secs(24 * 60 * 60); 15 | // The maximum number of transactions in the transaction-to-hash and block-timestamp-to-block-index mappings to be pruned in a single prune process 16 | pub const TRANSACTION_PRUNE_LIMIT: usize = 100_000; 17 | // The maximum number of entries in the approval list and expiration queue to be pruned in a single prune process 18 | pub const APPROVE_PRUNE_LIMIT: usize = 100; 19 | pub const REMOTE_FUTURE: u64 = u64::MAX; 20 | 21 | #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq, Serialize)] 22 | pub struct Config { 23 | /// The maximum number of blocks 24 | /// returned by the [icrc3_get_blocks] 25 | /// endpoint 26 | pub max_blocks_per_request: u64, 27 | 28 | /// The principal of the index canister 29 | /// for this ledger 30 | pub index_id: Option, 31 | } 32 | 33 | impl Default for Config { 34 | fn default() -> Self { 35 | Self { 36 | max_blocks_per_request: 100, 37 | index_id: None, 38 | } 39 | } 40 | } 41 | 42 | impl Storable for Config { 43 | fn to_bytes(&self) -> Cow<[u8]> { 44 | let mut bytes = vec![]; 45 | ciborium::into_writer(self, &mut bytes).expect("Unable to serialize the config as CBOR"); 46 | Cow::Owned(bytes) 47 | } 48 | 49 | fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { 50 | ciborium::from_reader::(bytes.as_ref()) 51 | .expect("Unable to deserialize the config from its CBOR form") 52 | } 53 | 54 | const BOUND: ic_stable_structures::storable::Bound = 55 | ic_stable_structures::storable::Bound::Unbounded; 56 | } 57 | 58 | #[test] 59 | fn test_config_ser_de() { 60 | // do not use default 61 | let config = Config { 62 | max_blocks_per_request: 10, 63 | index_id: None, 64 | }; 65 | assert_eq!(Config::from_bytes(config.to_bytes()), config); 66 | } 67 | -------------------------------------------------------------------------------- /cycles-ledger/src/endpoints.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, marker::PhantomData}; 2 | 3 | use candid::{CandidType, Deserialize, Nat, Principal}; 4 | use ic_cdk::api::{call::RejectionCode, management_canister::provisional::CanisterSettings}; 5 | use icrc_ledger_types::{ 6 | icrc::generic_value::Value, 7 | icrc1::{ 8 | account::{Account, Subaccount}, 9 | transfer::{BlockIndex, Memo}, 10 | }, 11 | }; 12 | use serde::Serialize; 13 | 14 | use crate::{config::Config, storage::Block}; 15 | 16 | #[derive(Deserialize, CandidType, Clone, Debug, PartialEq, Eq)] 17 | pub enum ChangeIndexId { 18 | Unset, 19 | SetTo(Principal), 20 | } 21 | 22 | impl From for Option { 23 | fn from(value: ChangeIndexId) -> Self { 24 | match value { 25 | ChangeIndexId::Unset => None, 26 | ChangeIndexId::SetTo(index_id) => Some(index_id), 27 | } 28 | } 29 | } 30 | 31 | #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] 32 | pub struct UpgradeArgs { 33 | pub max_blocks_per_request: Option, 34 | pub change_index_id: Option, 35 | } 36 | 37 | #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] 38 | pub enum LedgerArgs { 39 | Init(Config), 40 | Upgrade(Option), 41 | } 42 | 43 | pub type NumCycles = Nat; 44 | 45 | #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] 46 | pub struct DepositArg { 47 | pub to: Account, 48 | pub memo: Option, 49 | } 50 | 51 | #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] 52 | pub struct DepositResult { 53 | pub block_index: Nat, 54 | pub balance: Nat, 55 | } 56 | 57 | #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] 58 | pub struct SupportedStandard { 59 | pub name: String, 60 | pub url: String, 61 | } 62 | 63 | #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] 64 | pub struct WithdrawArgs { 65 | #[serde(default)] 66 | pub from_subaccount: Option, 67 | pub to: Principal, 68 | #[serde(default)] 69 | pub created_at_time: Option, 70 | pub amount: NumCycles, 71 | } 72 | 73 | #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] 74 | pub struct WithdrawFromArgs { 75 | #[serde(default)] 76 | pub spender_subaccount: Option, 77 | pub from: Account, 78 | pub to: Principal, 79 | #[serde(default)] 80 | pub created_at_time: Option, 81 | pub amount: NumCycles, 82 | } 83 | 84 | #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] 85 | pub enum WithdrawError { 86 | BadFee { 87 | expected_fee: NumCycles, 88 | }, 89 | InsufficientFunds { 90 | balance: NumCycles, 91 | }, 92 | TooOld, 93 | CreatedInFuture { 94 | ledger_time: u64, 95 | }, 96 | TemporarilyUnavailable, 97 | Duplicate { 98 | duplicate_of: BlockIndex, 99 | }, 100 | FailedToWithdraw { 101 | fee_block: Option, 102 | rejection_code: RejectionCode, 103 | rejection_reason: String, 104 | }, 105 | GenericError { 106 | error_code: Nat, 107 | message: String, 108 | }, 109 | InvalidReceiver { 110 | receiver: Principal, 111 | }, 112 | } 113 | 114 | #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] 115 | pub enum WithdrawFromError { 116 | InsufficientFunds { 117 | balance: NumCycles, 118 | }, 119 | InsufficientAllowance { 120 | allowance: NumCycles, 121 | }, 122 | TooOld, 123 | CreatedInFuture { 124 | ledger_time: u64, 125 | }, 126 | TemporarilyUnavailable, 127 | Duplicate { 128 | duplicate_of: BlockIndex, 129 | }, 130 | FailedToWithdrawFrom { 131 | withdraw_from_block: Option, 132 | refund_block: Option, 133 | approval_refund_block: Option, 134 | rejection_code: RejectionCode, 135 | rejection_reason: String, 136 | }, 137 | GenericError { 138 | error_code: Nat, 139 | message: String, 140 | }, 141 | InvalidReceiver { 142 | receiver: Principal, 143 | }, 144 | } 145 | 146 | #[derive(Clone, Debug, Deserialize, PartialEq, Eq)] 147 | #[serde(try_from = "candid::types::reference::Func")] 148 | pub struct GetBlocksFn { 149 | pub canister_id: Principal, 150 | pub method: String, 151 | pub _marker: PhantomData<(GetBlocksArgs, GetBlocksResult)>, 152 | } 153 | 154 | impl GetBlocksFn { 155 | pub fn new(canister_id: Principal, method: impl Into) -> Self { 156 | Self { 157 | canister_id, 158 | method: method.into(), 159 | _marker: PhantomData, 160 | } 161 | } 162 | } 163 | 164 | impl From for candid::Func { 165 | fn from(archive_fn: GetBlocksFn) -> Self { 166 | let principal = Principal::try_from(archive_fn.canister_id.as_ref()) 167 | .expect("could not deserialize principal"); 168 | Self { 169 | principal, 170 | method: archive_fn.method, 171 | } 172 | } 173 | } 174 | 175 | impl TryFrom for GetBlocksFn { 176 | type Error = String; 177 | fn try_from(func: candid::types::reference::Func) -> Result { 178 | let canister_id = Principal::try_from(func.principal.as_slice()) 179 | .map_err(|e| format!("principal is not a canister id: {}", e))?; 180 | Ok(GetBlocksFn { 181 | canister_id, 182 | method: func.method, 183 | _marker: PhantomData, 184 | }) 185 | } 186 | } 187 | 188 | impl CandidType for GetBlocksFn { 189 | fn _ty() -> candid::types::Type { 190 | candid::func!((GetBlocksArgs) -> (GetBlocksResult) query) 191 | } 192 | 193 | fn idl_serialize(&self, serializer: S) -> Result<(), S::Error> 194 | where 195 | S: candid::types::Serializer, 196 | { 197 | candid::types::reference::Func::from(self.clone()).idl_serialize(serializer) 198 | } 199 | } 200 | 201 | #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] 202 | pub struct GetBlocksArg { 203 | pub start: Nat, 204 | pub length: Nat, 205 | } 206 | 207 | pub type GetBlocksArgs = Vec; 208 | 209 | #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] 210 | pub struct BlockWithId { 211 | pub id: Nat, 212 | pub block: Value, 213 | } 214 | 215 | impl Display for BlockWithId { 216 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 217 | let block = Block::from_value(self.block.to_owned()).unwrap(); 218 | write!(f, "BlockWithId {{")?; 219 | write!(f, " id: {}", self.id)?; 220 | write!(f, ", block: {}", block)?; 221 | write!(f, "}}") 222 | } 223 | } 224 | 225 | #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] 226 | pub struct ArchivedBlocks { 227 | pub args: GetBlocksArgs, 228 | pub callback: GetBlocksFn, 229 | } 230 | 231 | #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] 232 | pub struct GetBlocksResult { 233 | // Total number of blocks in the 234 | // block log. 235 | pub log_length: Nat, 236 | 237 | pub blocks: Vec, 238 | 239 | pub archived_blocks: Vec, 240 | } 241 | 242 | #[derive(CandidType, Deserialize, Debug)] 243 | pub struct DataCertificate { 244 | pub certificate: serde_bytes::ByteBuf, 245 | 246 | // CBOR encoded hash_tree 247 | pub hash_tree: serde_bytes::ByteBuf, 248 | } 249 | 250 | #[derive(Default, Debug, Clone, CandidType, Serialize, Deserialize, PartialEq, Eq)] 251 | pub struct CmcCreateCanisterArgs { 252 | pub subnet_selection: Option, 253 | pub settings: Option, 254 | } 255 | 256 | #[derive(Serialize, Deserialize, CandidType, Clone, Debug, PartialEq, Eq)] 257 | pub struct SubnetFilter { 258 | pub subnet_type: Option, 259 | } 260 | 261 | #[derive(Serialize, Deserialize, CandidType, Clone, Debug, PartialEq, Eq)] 262 | pub enum SubnetSelection { 263 | /// Choose a random subnet that satisfies the specified properties 264 | Filter(SubnetFilter), 265 | /// Choose a specific subnet 266 | Subnet { subnet: Principal }, 267 | } 268 | 269 | #[derive(Default, Debug, Clone, CandidType, Deserialize, PartialEq, Eq)] 270 | pub struct CreateCanisterArgs { 271 | #[serde(default)] 272 | pub from_subaccount: Option, 273 | #[serde(default)] 274 | pub created_at_time: Option, 275 | /// Amount of cycles used to create the canister. 276 | /// The new canister will have `amount - canister creation fee` cycles when created. 277 | pub amount: NumCycles, 278 | #[serde(default)] 279 | pub creation_args: Option, 280 | } 281 | 282 | #[derive(Debug, Clone, CandidType, Deserialize, PartialEq, Eq)] 283 | pub struct CreateCanisterFromArgs { 284 | pub from: Account, 285 | #[serde(default)] 286 | pub spender_subaccount: Option, 287 | #[serde(default)] 288 | pub created_at_time: Option, 289 | /// Amount of cycles used to create the canister. 290 | /// The new canister will have `amount - canister creation fee` cycles when created. 291 | pub amount: NumCycles, 292 | #[serde(default)] 293 | pub creation_args: Option, 294 | } 295 | 296 | /// Error for create_canister endpoint 297 | #[derive(Serialize, Deserialize, CandidType, Clone, Debug, PartialEq, Eq)] 298 | pub enum CmcCreateCanisterError { 299 | Refunded { 300 | refund_amount: u128, 301 | create_error: String, 302 | }, 303 | RefundFailed { 304 | create_error: String, 305 | refund_error: String, 306 | }, 307 | } 308 | 309 | /// Error for create_canister endpoint 310 | #[derive(Deserialize, CandidType, Clone, Debug, PartialEq, Eq)] 311 | pub enum CreateCanisterError { 312 | InsufficientFunds { 313 | balance: NumCycles, 314 | }, 315 | TooOld, 316 | CreatedInFuture { 317 | ledger_time: u64, 318 | }, 319 | TemporarilyUnavailable, 320 | Duplicate { 321 | duplicate_of: BlockIndex, 322 | canister_id: Option, 323 | }, 324 | FailedToCreate { 325 | fee_block: Option, 326 | refund_block: Option, 327 | error: String, 328 | }, 329 | GenericError { 330 | error_code: Nat, 331 | message: String, 332 | }, 333 | } 334 | 335 | /// Error for create_canister endpoint 336 | #[derive(Deserialize, CandidType, Clone, Debug, PartialEq, Eq)] 337 | pub enum CreateCanisterFromError { 338 | InsufficientFunds { 339 | balance: NumCycles, 340 | }, 341 | InsufficientAllowance { 342 | allowance: NumCycles, 343 | }, 344 | TooOld, 345 | CreatedInFuture { 346 | ledger_time: u64, 347 | }, 348 | TemporarilyUnavailable, 349 | Duplicate { 350 | duplicate_of: BlockIndex, 351 | canister_id: Option, 352 | }, 353 | FailedToCreateFrom { 354 | create_from_block: Option, 355 | refund_block: Option, 356 | approval_refund_block: Option, 357 | rejection_code: RejectionCode, 358 | rejection_reason: String, 359 | }, 360 | GenericError { 361 | error_code: Nat, 362 | message: String, 363 | }, 364 | } 365 | 366 | impl CreateCanisterError { 367 | pub const BAD_FEE_ERROR: u64 = 100_001; 368 | } 369 | 370 | #[derive(Deserialize, CandidType, Clone, Debug, PartialEq, Eq)] 371 | pub struct CreateCanisterSuccess { 372 | pub block_id: Nat, 373 | pub canister_id: Principal, 374 | } 375 | 376 | #[derive(Deserialize, CandidType, Clone, Debug, PartialEq, Eq)] 377 | pub struct SupportedBlockType { 378 | pub block_type: String, 379 | pub url: String, 380 | } 381 | 382 | #[derive(Deserialize, CandidType, Clone, Debug, PartialEq, Eq)] 383 | pub struct GetArchivesArgs { 384 | pub from: Option, 385 | } 386 | 387 | #[derive(Deserialize, CandidType, Clone, Debug, PartialEq, Eq)] 388 | pub struct GetArchivesResult { 389 | pub canister_id: Principal, 390 | pub start: Nat, 391 | pub end: Nat, 392 | } 393 | -------------------------------------------------------------------------------- /cycles-ledger/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use anyhow::{bail, Context}; 4 | use ciborium::Value as CiboriumValue; 5 | use endpoints::{CreateCanisterError, CreateCanisterFromError, WithdrawError, WithdrawFromError}; 6 | use icrc_ledger_types::{ 7 | icrc::generic_value::Value, icrc1::transfer::TransferError, 8 | icrc2::transfer_from::TransferFromError, 9 | }; 10 | use num_traits::ToPrimitive; 11 | use serde_bytes::ByteBuf; 12 | use thiserror::Error; 13 | 14 | pub mod compact_account; 15 | pub mod config; 16 | pub mod endpoints; 17 | pub mod logs; 18 | pub mod memo; 19 | pub mod storage; 20 | 21 | /// The maximum allowed value nesting within a CBOR value. 22 | const VALUE_DEPTH_LIMIT: usize = 64; 23 | 24 | #[derive(Debug, Error)] 25 | pub enum ValueDecodingError { 26 | #[error("CBOR value depth must not exceed {max_depth}")] 27 | DepthLimitExceeded { max_depth: usize }, 28 | #[error("unsupported CBOR map key value {0:?} (only text keys are allowed)")] 29 | UnsupportedKeyType(String), 30 | #[error("unsupported CBOR tag {0} (value = {1:?})")] 31 | UnsupportedTag(u64, CiboriumValue), 32 | #[error("unsupported CBOR value value {0}")] 33 | UnsupportedValueType(&'static str), 34 | #[error("cannot decode CBOR value {0:?}")] 35 | UnsupportedValue(CiboriumValue), 36 | } 37 | 38 | pub fn ciborium_to_generic_value( 39 | value: &CiboriumValue, 40 | depth: usize, 41 | ) -> Result { 42 | if depth >= VALUE_DEPTH_LIMIT { 43 | return Err(ValueDecodingError::DepthLimitExceeded { 44 | max_depth: VALUE_DEPTH_LIMIT, 45 | }); 46 | } 47 | 48 | match value { 49 | CiboriumValue::Integer(int) => { 50 | let v: i128 = (*int).into(); 51 | let uv: u128 = v 52 | .try_into() 53 | .map_err(|_| ValueDecodingError::UnsupportedValueType("negative integers"))?; 54 | Ok(Value::Int(uv.into())) 55 | } 56 | CiboriumValue::Bytes(bytes) => Ok(Value::Blob(ByteBuf::from(bytes.to_owned()))), 57 | CiboriumValue::Text(text) => Ok(Value::Text(text.to_owned())), 58 | CiboriumValue::Array(values) => Ok(Value::Array( 59 | values 60 | .iter() 61 | .map(|v| ciborium_to_generic_value(v, depth + 1)) 62 | .collect::, _>>()?, 63 | )), 64 | CiboriumValue::Map(map) => Ok(Value::Map( 65 | map.iter() 66 | .map(|(k, v)| { 67 | let key = k 68 | .to_owned() 69 | .into_text() 70 | .map_err(|k| ValueDecodingError::UnsupportedKeyType(format!("{:?}", k)))?; 71 | Ok((key, ciborium_to_generic_value(v, depth + 1)?)) 72 | }) 73 | .collect::, _>>()?, 74 | )), 75 | CiboriumValue::Bool(_) => Err(ValueDecodingError::UnsupportedValueType("bool")), 76 | CiboriumValue::Null => Err(ValueDecodingError::UnsupportedValueType("null")), 77 | CiboriumValue::Float(_) => Err(ValueDecodingError::UnsupportedValueType("float")), 78 | CiboriumValue::Tag(known_tags::SELF_DESCRIBED, value) => { 79 | ciborium_to_generic_value(value, depth + 1) 80 | } 81 | CiboriumValue::Tag(known_tags::BIGNUM, value) => { 82 | let value_bytes = value 83 | .to_owned() 84 | .into_bytes() 85 | .map_err(|_| ValueDecodingError::UnsupportedValueType("non-bytes bignums"))?; 86 | Ok(Value::Nat(candid::Nat(num_bigint::BigUint::from_bytes_be( 87 | &value_bytes, 88 | )))) 89 | } 90 | CiboriumValue::Tag(tag, value) => { 91 | Err(ValueDecodingError::UnsupportedTag(*tag, *value.to_owned())) 92 | } 93 | // NB. ciborium::value::Value is marked as #[non_exhaustive] 94 | other => Err(ValueDecodingError::UnsupportedValue(other.to_owned())), 95 | } 96 | } 97 | 98 | pub fn generic_to_ciborium_value(value: &Value, depth: usize) -> anyhow::Result { 99 | if depth >= VALUE_DEPTH_LIMIT { 100 | bail!("Depth limit exceeded (max_depth: {})", VALUE_DEPTH_LIMIT); 101 | } 102 | 103 | match value { 104 | Value::Int(int) => { 105 | let uv = int.0.to_u128().context("Unable to convert int to u128")?; 106 | let v = i128::try_from(uv).context("Unable to convert u128 to i128")?; 107 | let i = ciborium::value::Integer::try_from(v) 108 | .context("Unable to create ciborium Integer from i128")?; 109 | Ok(CiboriumValue::Integer(i)) 110 | } 111 | Value::Blob(bytes) => Ok(CiboriumValue::Bytes(bytes.to_vec())), 112 | Value::Text(text) => Ok(CiboriumValue::Text(text.to_owned())), 113 | Value::Array(values) => Ok(CiboriumValue::Array( 114 | values 115 | .iter() 116 | .enumerate() 117 | .map(|(i, v)| { 118 | generic_to_ciborium_value(v, depth + 1) 119 | .with_context(|| format!("Unable to convert element {}", i)) 120 | }) 121 | .collect::, _>>()?, 122 | )), 123 | Value::Map(map) => Ok(CiboriumValue::Map( 124 | map.iter() 125 | .map(|(k, v)| { 126 | let key = CiboriumValue::Text(k.to_owned()); 127 | let value = generic_to_ciborium_value(v, depth + 1) 128 | .with_context(|| format!("Unable to convert field {}", k))?; 129 | Ok::<(CiboriumValue, CiboriumValue), anyhow::Error>((key, value)) 130 | }) 131 | .collect::, _>>()?, 132 | )), 133 | Value::Nat(nat) => { 134 | let value_bytes = nat.0.to_bytes_be(); 135 | let value = CiboriumValue::from(value_bytes); 136 | Ok(CiboriumValue::Tag(known_tags::BIGNUM, Box::new(value))) 137 | } 138 | v => bail!("Unknown value: {:?}", v), 139 | } 140 | } 141 | 142 | mod known_tags { 143 | //! This module defines well-known CBOR tags used for block decoding. 144 | 145 | /// Tag for Self-described CBOR; see https://www.rfc-editor.org/rfc/rfc8949.html#name-self-described-cbor. 146 | pub const SELF_DESCRIBED: u64 = 55799; 147 | 148 | /// Tag for CBOR bignums; see https://www.rfc-editor.org/rfc/rfc8949.html#name-bignums. 149 | pub const BIGNUM: u64 = 2; 150 | } 151 | 152 | // Traps if the error is InsufficientAllowance 153 | pub fn transfer_from_error_to_transfer_error(e: TransferFromError) -> TransferError { 154 | match e { 155 | TransferFromError::BadFee { expected_fee } => TransferError::BadFee { expected_fee }, 156 | TransferFromError::BadBurn { min_burn_amount } => { 157 | TransferError::BadBurn { min_burn_amount } 158 | } 159 | TransferFromError::InsufficientFunds { balance } => { 160 | TransferError::InsufficientFunds { balance } 161 | } 162 | TransferFromError::InsufficientAllowance { .. } => { 163 | ic_cdk::trap("InsufficientAllowance error should not happen for transfer") 164 | } 165 | TransferFromError::TooOld => TransferError::TooOld, 166 | TransferFromError::CreatedInFuture { ledger_time } => { 167 | TransferError::CreatedInFuture { ledger_time } 168 | } 169 | TransferFromError::Duplicate { duplicate_of } => TransferError::Duplicate { duplicate_of }, 170 | TransferFromError::TemporarilyUnavailable => TransferError::TemporarilyUnavailable, 171 | TransferFromError::GenericError { 172 | error_code, 173 | message, 174 | } => TransferError::GenericError { 175 | error_code, 176 | message, 177 | }, 178 | } 179 | } 180 | 181 | // Traps if the error is InsufficientAllowance 182 | pub fn withdraw_from_error_to_withdraw_error(e: WithdrawFromError) -> WithdrawError { 183 | match e { 184 | WithdrawFromError::InsufficientFunds { balance } => { 185 | WithdrawError::InsufficientFunds { balance } 186 | } 187 | WithdrawFromError::InsufficientAllowance { .. } => { 188 | ic_cdk::trap("InsufficientAllowance error should not happen for withdraw") 189 | } 190 | WithdrawFromError::TooOld => WithdrawError::TooOld, 191 | WithdrawFromError::CreatedInFuture { ledger_time } => { 192 | WithdrawError::CreatedInFuture { ledger_time } 193 | } 194 | WithdrawFromError::TemporarilyUnavailable => WithdrawError::TemporarilyUnavailable, 195 | WithdrawFromError::Duplicate { duplicate_of } => WithdrawError::Duplicate { duplicate_of }, 196 | WithdrawFromError::GenericError { 197 | error_code, 198 | message, 199 | } => WithdrawError::GenericError { 200 | error_code, 201 | message, 202 | }, 203 | WithdrawFromError::InvalidReceiver { receiver } => { 204 | WithdrawError::InvalidReceiver { receiver } 205 | } 206 | WithdrawFromError::FailedToWithdrawFrom { 207 | refund_block: fee_block, 208 | rejection_code, 209 | rejection_reason, 210 | .. 211 | } => WithdrawError::FailedToWithdraw { 212 | fee_block, 213 | rejection_code, 214 | rejection_reason, 215 | }, 216 | } 217 | } 218 | 219 | // Traps if the error is InsufficientAllowance 220 | pub fn create_canister_from_error_to_create_canister_error( 221 | e: CreateCanisterFromError, 222 | ) -> CreateCanisterError { 223 | match e { 224 | CreateCanisterFromError::InsufficientFunds { balance } => { 225 | CreateCanisterError::InsufficientFunds { balance } 226 | } 227 | CreateCanisterFromError::InsufficientAllowance { .. } => { 228 | ic_cdk::trap("InsufficientAllowance error should not happen for create_canister") 229 | } 230 | CreateCanisterFromError::TooOld => CreateCanisterError::TooOld, 231 | CreateCanisterFromError::CreatedInFuture { ledger_time } => { 232 | CreateCanisterError::CreatedInFuture { ledger_time } 233 | } 234 | CreateCanisterFromError::TemporarilyUnavailable => { 235 | CreateCanisterError::TemporarilyUnavailable 236 | } 237 | CreateCanisterFromError::Duplicate { 238 | duplicate_of, 239 | canister_id, 240 | } => CreateCanisterError::Duplicate { 241 | duplicate_of, 242 | canister_id, 243 | }, 244 | CreateCanisterFromError::FailedToCreateFrom { 245 | create_from_block, 246 | refund_block, 247 | rejection_reason, 248 | .. 249 | } => CreateCanisterError::FailedToCreate { 250 | fee_block: create_from_block, 251 | refund_block, 252 | error: rejection_reason, 253 | }, 254 | CreateCanisterFromError::GenericError { 255 | error_code, 256 | message, 257 | } => CreateCanisterError::GenericError { 258 | error_code, 259 | message, 260 | }, 261 | } 262 | } 263 | 264 | #[cfg(test)] 265 | mod tests { 266 | use ciborium::{value::Integer, Value}; 267 | use num_bigint::BigUint; 268 | use proptest::{arbitrary::any, prelude::prop, prop_oneof, proptest, strategy::Strategy}; 269 | 270 | use crate::{ciborium_to_generic_value, generic_to_ciborium_value, known_tags}; 271 | 272 | fn ciborium_value_strategy() -> impl Strategy { 273 | let integer_strategy = any::().prop_map(|i| Value::Integer(Integer::from(i))); 274 | let bytes_strategy = any::>().prop_map(Value::Bytes); 275 | let text_strategy = any::().prop_map(Value::Text); 276 | let bignum_strategy = any::>().prop_map(|digits| { 277 | let value_bytes = BigUint::new(digits).to_bytes_be(); 278 | let value = Value::Bytes(value_bytes); 279 | Value::Tag(known_tags::BIGNUM, Box::new(value)) 280 | }); 281 | 282 | let leaf = prop_oneof![ 283 | integer_strategy, 284 | bytes_strategy, 285 | text_strategy.clone(), 286 | bignum_strategy, 287 | ]; 288 | 289 | leaf.prop_recursive( 290 | 8, // 8 levels deep 291 | 256, // Shoot for maximum size of 256 nodes 292 | 10, // We put up to 10 items per collection 293 | |inner| { 294 | prop_oneof![ 295 | // Take the inner strategy and make the two recursive cases. 296 | prop::collection::vec(inner.clone(), 0..10).prop_map(Value::Array), 297 | // Note that we force the key to be of type text because 298 | // it's the only key type supported. 299 | // We also use btree_map because we need the keys sorted for when 300 | // we check equality. 301 | prop::collection::btree_map(any::(), inner, 0..10).prop_map(|kvs| { 302 | let kvs = kvs.into_iter().map(|(k, v)| (Value::Text(k), v)).collect(); 303 | Value::Map(kvs) 304 | }), 305 | ] 306 | }, 307 | ) 308 | } 309 | 310 | proptest! { 311 | #[test] 312 | fn test_ciborium_to_generic_value(value in ciborium_value_strategy()) { 313 | let ciborium_value = ciborium_to_generic_value(&value, 0) 314 | .expect("Unable to convert ciborium value to generic value"); 315 | let actual_value = generic_to_ciborium_value(&ciborium_value, 0) 316 | .expect("Unable to convert generic value to ciborium value"); 317 | assert_eq!(value, actual_value); 318 | } 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /cycles-ledger/src/logs.rs: -------------------------------------------------------------------------------- 1 | use candid::Deserialize; 2 | use ic_canister_log::declare_log_buffer; 3 | use serde; 4 | 5 | // High-priority messages. 6 | declare_log_buffer!(name = P0, capacity = 1000); 7 | 8 | // Low-priority info messages. 9 | declare_log_buffer!(name = P1, capacity = 1000); 10 | 11 | #[derive(Clone, serde::Serialize, Deserialize, Debug)] 12 | pub enum Priority { 13 | P0, 14 | P1, 15 | } 16 | 17 | #[derive(Clone, serde::Serialize, Deserialize, Debug)] 18 | pub struct LogEntry { 19 | pub timestamp: u64, 20 | pub priority: Priority, 21 | pub file: String, 22 | pub line: u32, 23 | pub message: String, 24 | } 25 | 26 | #[derive(Clone, Default, serde::Serialize, Deserialize, Debug)] 27 | pub struct Log { 28 | pub entries: Vec, 29 | } 30 | -------------------------------------------------------------------------------- /cycles-ledger/src/main.rs: -------------------------------------------------------------------------------- 1 | use candid::{candid_method, Nat, Principal}; 2 | use cycles_ledger::endpoints::{ 3 | CmcCreateCanisterArgs, DataCertificate, GetArchivesArgs, GetArchivesResult, GetBlocksArgs, 4 | GetBlocksResult, LedgerArgs, SupportedBlockType, WithdrawError, WithdrawFromError, 5 | }; 6 | use cycles_ledger::logs::{Log, LogEntry, Priority}; 7 | use cycles_ledger::logs::{P0, P1}; 8 | use cycles_ledger::storage::{ 9 | balance_of, mutate_config, mutate_state, prune, read_config, read_state, State, 10 | }; 11 | use cycles_ledger::{ 12 | config, create_canister_from_error_to_create_canister_error, endpoints, storage, 13 | transfer_from_error_to_transfer_error, withdraw_from_error_to_withdraw_error, 14 | }; 15 | use ic_canister_log::export as export_logs; 16 | use ic_canisters_http_types::{HttpRequest, HttpResponse, HttpResponseBuilder}; 17 | use ic_cdk::api::call::{msg_cycles_accept128, msg_cycles_available128}; 18 | use ic_cdk_macros::{init, post_upgrade, query, update}; 19 | use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue; 20 | use icrc_ledger_types::icrc1::account::Account; 21 | use icrc_ledger_types::icrc1::transfer::TransferArg as TransferArgs; 22 | use icrc_ledger_types::icrc1::transfer::{Memo, TransferError}; 23 | use icrc_ledger_types::icrc2::allowance::{Allowance, AllowanceArgs}; 24 | use icrc_ledger_types::icrc2::approve::{ApproveArgs, ApproveError}; 25 | use icrc_ledger_types::icrc2::transfer_from::{TransferFromArgs, TransferFromError}; 26 | use num_traits::ToPrimitive; 27 | 28 | #[init] 29 | #[candid_method(init)] 30 | fn init(ledger_args: LedgerArgs) { 31 | match ledger_args { 32 | LedgerArgs::Init(config) => { 33 | mutate_state(|state| { 34 | state.config.set(config) 35 | .expect("Failed to change configuration"); 36 | }) 37 | } 38 | LedgerArgs::Upgrade(_) => 39 | ic_cdk::trap("Cannot initialize the canister with an Upgrade argument. Please provide an Init argument."), 40 | } 41 | } 42 | 43 | #[post_upgrade] 44 | fn post_upgrade(ledger_args: Option) { 45 | match ledger_args { 46 | Some(LedgerArgs::Upgrade(Some(upgrade_args))) => { 47 | if let Some(max_blocks_per_request) = upgrade_args.max_blocks_per_request { 48 | mutate_config(|config| { 49 | config.max_blocks_per_request = max_blocks_per_request; 50 | }) 51 | } 52 | if let Some(change_index_id) = upgrade_args.change_index_id { 53 | mutate_config(|config| { 54 | config.index_id = change_index_id.into(); 55 | }) 56 | } 57 | } 58 | None | Some(LedgerArgs::Upgrade(None)) => {}, 59 | _ => 60 | ic_cdk::trap("Cannot upgrade the canister with an Init argument. Please provide an Upgrade argument."), 61 | } 62 | } 63 | 64 | #[query] 65 | #[candid_method(query)] 66 | fn icrc1_name() -> String { 67 | config::TOKEN_NAME.to_string() 68 | } 69 | 70 | #[query] 71 | #[candid_method(query)] 72 | fn icrc1_symbol() -> String { 73 | config::TOKEN_SYMBOL.to_string() 74 | } 75 | 76 | #[query] 77 | #[candid_method(query)] 78 | fn icrc1_decimals() -> u8 { 79 | config::DECIMALS 80 | } 81 | 82 | #[query] 83 | #[candid_method(query)] 84 | fn icrc1_fee() -> Nat { 85 | Nat::from(config::FEE) 86 | } 87 | 88 | #[query] 89 | #[candid_method(query)] 90 | fn icrc1_minting_account() -> Option { 91 | None 92 | } 93 | 94 | #[query] 95 | #[candid_method(query)] 96 | fn icrc1_supported_standards() -> Vec { 97 | vec![ 98 | endpoints::SupportedStandard { 99 | name: "ICRC-1".to_string(), 100 | url: "https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-1/README.md" 101 | .to_string(), 102 | }, 103 | endpoints::SupportedStandard { 104 | name: "ICRC-2".to_string(), 105 | url: "https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-2/README.md" 106 | .to_string(), 107 | }, 108 | endpoints::SupportedStandard { 109 | name: "ICRC-3".to_string(), 110 | url: "https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-3/README.md" 111 | .to_string(), 112 | }, 113 | ] 114 | } 115 | 116 | #[query] 117 | #[candid_method(query)] 118 | fn icrc3_supported_block_types() -> Vec { 119 | vec![ 120 | SupportedBlockType { 121 | block_type: "1burn".to_string(), 122 | url: "https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-1/README.md" 123 | .to_string(), 124 | }, 125 | SupportedBlockType { 126 | block_type: "1mint".to_string(), 127 | url: "https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-1/README.md" 128 | .to_string(), 129 | }, 130 | SupportedBlockType { 131 | block_type: "2xfer".to_string(), 132 | url: "https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-2/README.md" 133 | .to_string(), 134 | }, 135 | SupportedBlockType { 136 | block_type: "2approve".to_string(), 137 | url: "https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-2/README.md" 138 | .to_string(), 139 | }, 140 | ] 141 | } 142 | 143 | #[query] 144 | #[candid_method(query)] 145 | fn icrc1_total_supply() -> Nat { 146 | Nat::from(read_state(|state| state.total_supply())) 147 | } 148 | 149 | #[query] 150 | #[candid_method(query)] 151 | fn icrc1_metadata() -> Vec<(String, MetadataValue)> { 152 | use MetadataValue as MV; 153 | 154 | let mut metadata = vec![ 155 | MV::entry("icrc1:decimals", config::DECIMALS as u64), 156 | MV::entry("icrc1:name", config::TOKEN_NAME), 157 | MV::entry("icrc1:symbol", config::TOKEN_SYMBOL), 158 | MV::entry("icrc1:fee", config::FEE), 159 | MV::entry("icrc1:max_memo_length", config::MAX_MEMO_LENGTH), 160 | MV::entry( 161 | "dfn:max_blocks_per_request", 162 | read_config(|config| config.max_blocks_per_request), 163 | ), 164 | MV::entry("icrc1:logo", config::TOKEN_LOGO), 165 | ]; 166 | if let Some(index_id) = read_config(|config| config.index_id) { 167 | metadata.push(MV::entry("dfn:index_id", index_id.as_slice())) 168 | } 169 | metadata 170 | } 171 | 172 | #[query] 173 | #[candid_method(query)] 174 | fn icrc1_balance_of(account: Account) -> Nat { 175 | Nat::from(storage::balance_of(&account)) 176 | } 177 | 178 | #[update] 179 | #[candid_method] 180 | fn deposit(arg: endpoints::DepositArg) -> endpoints::DepositResult { 181 | let cycles_available = msg_cycles_available128(); 182 | let amount = msg_cycles_accept128(cycles_available); 183 | 184 | match storage::deposit(arg.to, amount, arg.memo, ic_cdk::api::time()) { 185 | Ok(res) => res, 186 | Err(err) => ic_cdk::trap(&err.to_string()), 187 | } 188 | } 189 | 190 | fn execute_transfer( 191 | from: Account, 192 | to: Account, 193 | spender: Option, 194 | amount: Nat, 195 | fee: Option, 196 | memo: Option, 197 | created_at_time: Option, 198 | ) -> Result { 199 | let Some(amount) = amount.0.to_u128() else { 200 | return Err(TransferFromError::InsufficientFunds { 201 | balance: Nat::from(storage::balance_of(&from)), 202 | }); 203 | }; 204 | 205 | let suggested_fee = match fee { 206 | Some(fee) => match fee.0.to_u128() { 207 | None => { 208 | return Err(TransferFromError::BadFee { 209 | expected_fee: Nat::from(config::FEE), 210 | }) 211 | } 212 | Some(fee) => Some(fee), 213 | }, 214 | None => None, 215 | }; 216 | 217 | let now = ic_cdk::api::time(); 218 | let block_index = storage::transfer( 219 | from, 220 | to, 221 | spender, 222 | amount, 223 | memo, 224 | now, 225 | created_at_time, 226 | suggested_fee, 227 | )?; 228 | 229 | prune(now); 230 | 231 | Ok(block_index) 232 | } 233 | 234 | #[update] 235 | #[candid_method] 236 | fn icrc1_transfer(args: TransferArgs) -> Result { 237 | let from = Account { 238 | owner: ic_cdk::caller(), 239 | subaccount: args.from_subaccount, 240 | }; 241 | 242 | execute_transfer( 243 | from, 244 | args.to, 245 | None, 246 | args.amount, 247 | args.fee, 248 | args.memo, 249 | args.created_at_time, 250 | ) 251 | .map_err(transfer_from_error_to_transfer_error) 252 | } 253 | 254 | #[update] 255 | #[candid_method] 256 | fn icrc2_transfer_from(args: TransferFromArgs) -> Result { 257 | let spender = Account { 258 | owner: ic_cdk::caller(), 259 | subaccount: args.spender_subaccount, 260 | }; 261 | execute_transfer( 262 | args.from, 263 | args.to, 264 | Some(spender), 265 | args.amount, 266 | args.fee, 267 | args.memo, 268 | args.created_at_time, 269 | ) 270 | } 271 | 272 | #[query] 273 | #[candid_method(query)] 274 | fn icrc3_get_blocks(args: GetBlocksArgs) -> GetBlocksResult { 275 | storage::get_blocks(args) 276 | } 277 | 278 | async fn execute_withdraw( 279 | from: Account, 280 | to: Principal, 281 | spender: Option, 282 | amount: Nat, 283 | created_at_time: Option, 284 | ) -> Result { 285 | let Some(amount) = amount.0.to_u128() else { 286 | return Err(WithdrawFromError::InsufficientFunds { 287 | balance: Nat::from(balance_of(&from)), 288 | }); 289 | }; 290 | 291 | let now = ic_cdk::api::time(); 292 | storage::withdraw(from, to, spender, amount, now, created_at_time).await 293 | } 294 | 295 | #[update] 296 | #[candid_method] 297 | async fn withdraw(args: endpoints::WithdrawArgs) -> Result { 298 | let from = Account { 299 | owner: ic_cdk::caller(), 300 | subaccount: args.from_subaccount, 301 | }; 302 | execute_withdraw(from, args.to, None, args.amount, args.created_at_time) 303 | .await 304 | .map_err(withdraw_from_error_to_withdraw_error) 305 | } 306 | 307 | #[update] 308 | #[candid_method] 309 | async fn withdraw_from(args: endpoints::WithdrawFromArgs) -> Result { 310 | let spender = Account { 311 | owner: ic_cdk::caller(), 312 | subaccount: args.spender_subaccount, 313 | }; 314 | execute_withdraw( 315 | args.from, 316 | args.to, 317 | Some(spender), 318 | args.amount, 319 | args.created_at_time, 320 | ) 321 | .await 322 | } 323 | 324 | async fn execute_create_canister( 325 | from: Account, 326 | spender: Option, 327 | amount: Nat, 328 | created_at_time: Option, 329 | creation_args: Option, 330 | ) -> Result { 331 | let Some(amount) = amount.0.to_u128() else { 332 | return Err(endpoints::CreateCanisterFromError::InsufficientFunds { 333 | balance: Nat::from(balance_of(&from)), 334 | }); 335 | }; 336 | let now = ic_cdk::api::time(); 337 | storage::create_canister(from, spender, amount, now, created_at_time, creation_args).await 338 | } 339 | 340 | #[update] 341 | #[candid_method] 342 | async fn create_canister( 343 | args: endpoints::CreateCanisterArgs, 344 | ) -> Result { 345 | let from = Account { 346 | owner: ic_cdk::caller(), 347 | subaccount: args.from_subaccount, 348 | }; 349 | 350 | execute_create_canister( 351 | from, 352 | None, 353 | args.amount, 354 | args.created_at_time, 355 | args.creation_args, 356 | ) 357 | .await 358 | .map_err(create_canister_from_error_to_create_canister_error) 359 | } 360 | 361 | #[update] 362 | #[candid_method] 363 | async fn create_canister_from( 364 | args: endpoints::CreateCanisterFromArgs, 365 | ) -> Result { 366 | let spender = Account { 367 | owner: ic_cdk::caller(), 368 | subaccount: args.spender_subaccount, 369 | }; 370 | execute_create_canister( 371 | args.from, 372 | Some(spender), 373 | args.amount, 374 | args.created_at_time, 375 | args.creation_args, 376 | ) 377 | .await 378 | } 379 | 380 | #[query] 381 | #[candid_method(query)] 382 | fn icrc2_allowance(args: AllowanceArgs) -> Allowance { 383 | let allowance = storage::allowance(&args.account, &args.spender, ic_cdk::api::time()); 384 | let expires_at = if allowance.1 > 0 { 385 | Some(allowance.1) 386 | } else { 387 | None 388 | }; 389 | Allowance { 390 | allowance: Nat::from(allowance.0), 391 | expires_at, 392 | } 393 | } 394 | 395 | #[update] 396 | #[candid_method] 397 | fn icrc2_approve(args: ApproveArgs) -> Result { 398 | let from = Account { 399 | owner: ic_cdk::api::caller(), 400 | subaccount: args.from_subaccount, 401 | }; 402 | 403 | let now = ic_cdk::api::time(); 404 | 405 | let expected_allowance = match args.expected_allowance { 406 | Some(n) => match n.0.to_u128() { 407 | None => { 408 | return Err(ApproveError::AllowanceChanged { 409 | current_allowance: Nat::from(storage::allowance(&from, &args.spender, now).0), 410 | }) 411 | } 412 | Some(n) => Some(n), 413 | }, 414 | None => None, 415 | }; 416 | let suggested_fee = match args.fee { 417 | Some(fee) => match fee.0.to_u128() { 418 | None => { 419 | return Err(ApproveError::BadFee { 420 | expected_fee: Nat::from(config::FEE), 421 | }) 422 | } 423 | Some(fee) => Some(fee), 424 | }, 425 | None => None, 426 | }; 427 | 428 | let block_index = storage::approve( 429 | from, 430 | args.spender, 431 | args.amount.0.to_u128().unwrap_or(u128::MAX), 432 | args.memo, 433 | now, 434 | args.created_at_time, 435 | suggested_fee, 436 | expected_allowance, 437 | args.expires_at, 438 | )?; 439 | 440 | prune(now); 441 | 442 | Ok(block_index) 443 | } 444 | 445 | #[query] 446 | #[candid_method(query)] 447 | fn http_request(req: HttpRequest) -> HttpResponse { 448 | if req.path() == "/metrics" { 449 | let mut writer = 450 | ic_metrics_encoder::MetricsEncoder::new(vec![], ic_cdk::api::time() as i64 / 1_000_000); 451 | 452 | match encode_metrics(&mut writer) { 453 | Ok(()) => HttpResponseBuilder::ok() 454 | .header("Content-Type", "text/plain; version=0.0.4") 455 | .with_body_and_content_length(writer.into_inner()) 456 | .build(), 457 | Err(err) => { 458 | HttpResponseBuilder::server_error(format!("Failed to encode metrics: {}", err)) 459 | .build() 460 | } 461 | } 462 | } else if req.path() == "/logs" { 463 | use serde_json; 464 | let mut entries: Log = Default::default(); 465 | for entry in export_logs(&P0) { 466 | entries.entries.push(LogEntry { 467 | timestamp: entry.timestamp, 468 | priority: Priority::P0, 469 | file: entry.file.to_string(), 470 | line: entry.line, 471 | message: entry.message, 472 | }); 473 | } 474 | for entry in export_logs(&P1) { 475 | entries.entries.push(LogEntry { 476 | timestamp: entry.timestamp, 477 | priority: Priority::P1, 478 | file: entry.file.to_string(), 479 | line: entry.line, 480 | message: entry.message, 481 | }); 482 | } 483 | HttpResponseBuilder::ok() 484 | .header("Content-Type", "application/json; charset=utf-8") 485 | .with_body_and_content_length(serde_json::to_string(&entries).unwrap_or_default()) 486 | .build() 487 | } else { 488 | HttpResponseBuilder::not_found().build() 489 | } 490 | } 491 | 492 | pub fn encode_state_metrics( 493 | w: &mut ic_metrics_encoder::MetricsEncoder>, 494 | // State is destructored so that if new fields 495 | // are added then the dev must include them in 496 | // the metrics or ignore them (like with config) 497 | State { 498 | blocks, 499 | balances, 500 | approvals, 501 | expiration_queue, 502 | transaction_hashes, 503 | transaction_timestamps, 504 | config: _, 505 | cache, 506 | }: &State, 507 | ) -> std::io::Result<()> { 508 | w.encode_gauge( 509 | "cycles_ledger_number_of_blocks", 510 | blocks.len() as f64, 511 | "Total number of blocks stored in the stable memory.", 512 | )?; 513 | w.encode_gauge( 514 | "cycles_ledger_number_of_balances", 515 | balances.len() as f64, 516 | "Total number of balances stored in the stable memory.", 517 | )?; 518 | w.encode_gauge( 519 | "cycles_ledger_number_of_transaction_hashes", 520 | transaction_hashes.len() as f64, 521 | "Total number of transaction hashes stored in the stable memory.", 522 | )?; 523 | w.encode_gauge( 524 | "cycles_ledger_number_of_transaction_timestamps", 525 | transaction_timestamps.len() as f64, 526 | "Total number of transaction timestamps stored in the stable memory.", 527 | )?; 528 | w.encode_gauge( 529 | "cycles_ledger_number_of_approvals", 530 | approvals.len() as f64, 531 | "Total number of approvals stored in the stable memory.", 532 | )?; 533 | w.encode_gauge( 534 | "cycles_ledger_number_of_approval_expiration_entries", 535 | expiration_queue.len() as f64, 536 | "Total number of approval expiration entries in the stable memory.", 537 | )?; 538 | w.encode_gauge( 539 | "cycles_ledger_total_supply", 540 | cache.total_supply as f64, 541 | "Total cycles supply.", 542 | )?; 543 | Ok(()) 544 | } 545 | 546 | pub fn encode_metrics(w: &mut ic_metrics_encoder::MetricsEncoder>) -> std::io::Result<()> { 547 | w.encode_gauge( 548 | "cycles_ledger_stable_memory_pages", 549 | ic_cdk::api::stable::stable_size() as f64, 550 | "Size of the stable memory allocated by this canister measured in 64K Wasm pages.", 551 | )?; 552 | w.encode_gauge( 553 | "cycles_ledger_stable_memory_bytes", 554 | (ic_cdk::api::stable::stable_size() * 64 * 1024) as f64, 555 | "Size of the stable memory allocated by this canister.", 556 | )?; 557 | 558 | let cycle_balance = ic_cdk::api::canister_balance128() as f64; 559 | w.encode_gauge( 560 | "cycles_ledger_cycle_balance", 561 | cycle_balance, 562 | "Cycle balance on this canister.", 563 | )?; 564 | w.gauge_vec("cycle_balance", "Cycle balance on this canister.")? 565 | .value(&[("canister", "cycles-ledger")], cycle_balance)?; 566 | 567 | read_state(|state| encode_state_metrics(w, state))?; 568 | 569 | Ok(()) 570 | } 571 | 572 | #[query] 573 | #[candid_method(query)] 574 | fn icrc3_get_tip_certificate() -> Option { 575 | read_state(|state| state.get_tip_certificate()) 576 | } 577 | 578 | #[query] 579 | #[candid_method(query)] 580 | fn icrc3_get_archives(_args: GetArchivesArgs) -> Vec { 581 | vec![] 582 | } 583 | 584 | fn main() {} 585 | 586 | #[cfg(feature = "testing")] 587 | #[query] 588 | #[candid_method(query)] 589 | fn get_transaction_hashes() -> std::collections::BTreeMap<[u8; 32], u64> { 590 | let mut res = std::collections::BTreeMap::new(); 591 | read_state(|state| { 592 | for (key, (value, _maybe_canister)) in state.transaction_hashes.iter() { 593 | res.insert(key, value); 594 | } 595 | }); 596 | res 597 | } 598 | 599 | #[cfg(feature = "testing")] 600 | #[query] 601 | #[candid_method(query)] 602 | fn get_transaction_timestamps() -> std::collections::BTreeMap<(u64, u64), ()> { 603 | let mut res = std::collections::BTreeMap::new(); 604 | read_state(|state| { 605 | for (key, value) in state.transaction_timestamps.iter() { 606 | res.insert(key, value); 607 | } 608 | }); 609 | res 610 | } 611 | 612 | #[test] 613 | fn test_candid_interface_compatibility() { 614 | use candid_parser::utils::{service_equal, CandidSource}; 615 | use std::path::PathBuf; 616 | 617 | candid::export_service!(); 618 | let exported_interface = __export_service(); 619 | 620 | let expected_interface = 621 | PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("cycles-ledger.did"); 622 | 623 | println!( 624 | "Expected interface: {}\n\n", 625 | CandidSource::File(expected_interface.as_path()) 626 | .load() 627 | .unwrap() 628 | .1 629 | .unwrap() 630 | ); 631 | println!("Exported interface: {}\n\n", exported_interface); 632 | 633 | service_equal( 634 | CandidSource::Text(&exported_interface), 635 | CandidSource::File(expected_interface.as_path()), 636 | ) 637 | .expect("The assets canister interface is not compatible with the cycles-ledger.did file"); 638 | } 639 | -------------------------------------------------------------------------------- /cycles-ledger/src/memo.rs: -------------------------------------------------------------------------------- 1 | use crate::config::MAX_MEMO_LENGTH; 2 | use candid::Principal; 3 | use ic_cdk::api::management_canister::provisional::CanisterId; 4 | use icrc_ledger_types::icrc1::transfer::Memo; 5 | use minicbor::{Decode, Encode, Encoder}; 6 | 7 | #[derive(Decode, Encode, Debug, Clone, Copy, PartialEq, Eq)] 8 | pub struct WithdrawMemo<'a> { 9 | #[cbor(n(0), with = "minicbor::bytes")] 10 | pub receiver: &'a [u8], 11 | } 12 | 13 | impl<'a> From<&'a CanisterId> for WithdrawMemo<'a> { 14 | fn from(canister: &'a CanisterId) -> Self { 15 | Self { 16 | receiver: canister.as_slice(), 17 | } 18 | } 19 | } 20 | 21 | pub fn encode_withdraw_memo(target_canister: &Principal) -> Memo { 22 | let memo = WithdrawMemo::from(target_canister); 23 | let mut encoder = Encoder::new(Vec::new()); 24 | encoder.encode(memo).expect("Encoding of memo failed"); 25 | encoder.into_writer().into() 26 | } 27 | 28 | pub fn validate_memo(memo: &Option) -> Result<(), String> { 29 | if let Some(memo) = memo { 30 | if memo.0.len() as u64 > MAX_MEMO_LENGTH { 31 | return Err(format!( 32 | "memo length exceeds the maximum of {} bytes", 33 | MAX_MEMO_LENGTH 34 | )); 35 | } 36 | } 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /cycles-ledger/tests/client.rs: -------------------------------------------------------------------------------- 1 | use core::panic; 2 | use std::collections::BTreeMap; 3 | 4 | use candid::{CandidType, Decode, Encode, Nat, Principal}; 5 | use cycles_ledger::{ 6 | endpoints::{ 7 | self, CmcCreateCanisterError, CreateCanisterArgs, CreateCanisterFromArgs, 8 | CreateCanisterSuccess, DataCertificate, DepositResult, GetBlocksArg, GetBlocksArgs, 9 | GetBlocksResult, WithdrawArgs, WithdrawFromArgs, 10 | }, 11 | storage::{Block, CMC_PRINCIPAL}, 12 | }; 13 | use depositor::endpoints::DepositArg; 14 | use ic_cdk::api::management_canister::{ 15 | main::CanisterStatusResponse, provisional::CanisterIdRecord, 16 | }; 17 | use ic_test_state_machine_client::{StateMachine, WasmResult}; 18 | use icrc_ledger_types::{ 19 | icrc::generic_metadata_value::MetadataValue, 20 | icrc1::{ 21 | account::Account, 22 | transfer::{Memo, TransferArg as TransferArgs, TransferError}, 23 | }, 24 | icrc2::{ 25 | allowance::{Allowance, AllowanceArgs}, 26 | approve::{ApproveArgs, ApproveError}, 27 | transfer_from::{TransferFromArgs, TransferFromError}, 28 | }, 29 | }; 30 | use num_traits::ToPrimitive; 31 | use serde::Deserialize; 32 | 33 | // Panics if the canister is unreachable or it has rejected the query. 34 | pub fn query_or_panic( 35 | env: &StateMachine, 36 | canister_id: Principal, 37 | caller: Principal, 38 | method: &str, 39 | arg: I, 40 | ) -> O 41 | where 42 | I: CandidType, 43 | O: CandidType + for<'a> Deserialize<'a>, 44 | { 45 | let arg = Encode!(&arg).unwrap(); 46 | match env.query_call(canister_id, caller, method, arg) { 47 | Err(err) => { 48 | panic!("{canister_id}.{method} query failed with error {err} (caller: {caller})"); 49 | } 50 | Ok(WasmResult::Reject(err)) => { 51 | panic!("{canister_id}.{method} query rejected with error {err} (caller: {caller})"); 52 | } 53 | Ok(WasmResult::Reply(res)) => Decode!(&res, O).unwrap(), 54 | } 55 | } 56 | 57 | // Panics if the canister is unreachable or it has rejected the update. 58 | pub fn update_or_panic( 59 | env: &StateMachine, 60 | canister_id: Principal, 61 | caller: Principal, 62 | method: &str, 63 | arg: I, 64 | ) -> O 65 | where 66 | I: CandidType, 67 | O: CandidType + for<'a> Deserialize<'a>, 68 | { 69 | let arg = Encode!(&arg).unwrap(); 70 | match env.update_call(canister_id, caller, method, arg) { 71 | Err(err) => { 72 | panic!("{canister_id}.{method} failed with error {err} (caller: {caller})"); 73 | } 74 | Ok(WasmResult::Reject(err)) => { 75 | panic!("{canister_id}.{method} rejected with error {err} (caller: {caller})"); 76 | } 77 | Ok(WasmResult::Reply(res)) => Decode!(&res, O).unwrap(), 78 | } 79 | } 80 | 81 | pub fn deposit( 82 | env: &StateMachine, 83 | depositor_id: Principal, 84 | to: Account, 85 | cycles: u128, 86 | memo: Option, 87 | ) -> DepositResult { 88 | update_or_panic( 89 | env, 90 | depositor_id, 91 | to.owner, 92 | "deposit", 93 | DepositArg { cycles, to, memo }, 94 | ) 95 | } 96 | 97 | pub fn icrc1_balance_of(env: &StateMachine, ledger_id: Principal, account: Account) -> u128 { 98 | let res: Nat = query_or_panic( 99 | env, 100 | ledger_id, 101 | Principal::anonymous(), 102 | "icrc1_balance_of", 103 | account, 104 | ); 105 | res.0.to_u128().unwrap() 106 | } 107 | 108 | pub fn icrc1_fee(env: &StateMachine, ledger_id: Principal) -> u128 { 109 | let res: Nat = query_or_panic(env, ledger_id, Principal::anonymous(), "icrc1_fee", ()); 110 | res.0.to_u128().unwrap() 111 | } 112 | 113 | pub fn icrc1_total_supply(env: &StateMachine, ledger_id: Principal) -> u128 { 114 | let res: Nat = query_or_panic( 115 | env, 116 | ledger_id, 117 | Principal::anonymous(), 118 | "icrc1_total_supply", 119 | (), 120 | ); 121 | res.0.to_u128().unwrap() 122 | } 123 | 124 | pub fn get_block(env: &StateMachine, ledger_id: Principal, block_index: Nat) -> Block { 125 | let value = icrc3_get_blocks(env, ledger_id, vec![(block_index, Nat::from(1u64))]) 126 | .blocks 127 | .remove(0) 128 | .block; 129 | Block::from_value(value).unwrap() 130 | } 131 | 132 | pub fn withdraw( 133 | env: &StateMachine, 134 | ledger_id: Principal, 135 | caller: Principal, 136 | args: WithdrawArgs, 137 | ) -> Result { 138 | update_or_panic(env, ledger_id, caller, "withdraw", args) 139 | } 140 | 141 | pub fn withdraw_from( 142 | env: &StateMachine, 143 | ledger_id: Principal, 144 | caller: Principal, 145 | args: WithdrawFromArgs, 146 | ) -> Result { 147 | update_or_panic(env, ledger_id, caller, "withdraw_from", args) 148 | } 149 | 150 | pub fn create_canister( 151 | env: &StateMachine, 152 | ledger_id: Principal, 153 | caller: Principal, 154 | args: CreateCanisterArgs, 155 | ) -> Result { 156 | update_or_panic(env, ledger_id, caller, "create_canister", args) 157 | } 158 | 159 | pub fn create_canister_from( 160 | env: &StateMachine, 161 | ledger_id: Principal, 162 | caller: Principal, 163 | args: CreateCanisterFromArgs, 164 | ) -> Result { 165 | update_or_panic(env, ledger_id, caller, "create_canister_from", args) 166 | } 167 | 168 | pub fn canister_status( 169 | env: &StateMachine, 170 | canister_id: Principal, 171 | caller: Principal, 172 | ) -> CanisterStatusResponse { 173 | update_or_panic( 174 | env, 175 | Principal::management_canister(), 176 | caller, 177 | "canister_status", 178 | CanisterIdRecord { canister_id }, 179 | ) 180 | } 181 | 182 | pub fn fail_next_create_canister_with(env: &StateMachine, error: CmcCreateCanisterError) { 183 | let arg = Encode!(&error).unwrap(); 184 | if !matches!( 185 | env.update_call( 186 | CMC_PRINCIPAL, 187 | Principal::anonymous(), 188 | "fail_next_create_canister_with", 189 | arg, 190 | ) 191 | .unwrap(), 192 | WasmResult::Reply(_) 193 | ) { 194 | panic!("canister_status rejected") 195 | } 196 | } 197 | 198 | pub fn icrc2_allowance( 199 | env: &StateMachine, 200 | ledger_id: Principal, 201 | from: Account, 202 | spender: Account, 203 | ) -> Allowance { 204 | query_or_panic( 205 | env, 206 | ledger_id, 207 | Principal::anonymous(), 208 | "icrc2_allowance", 209 | AllowanceArgs { 210 | account: from, 211 | spender, 212 | }, 213 | ) 214 | } 215 | 216 | pub fn icrc2_approve( 217 | env: &StateMachine, 218 | ledger_id: Principal, 219 | caller: Principal, 220 | args: ApproveArgs, 221 | ) -> Result { 222 | update_or_panic(env, ledger_id, caller, "icrc2_approve", args) 223 | } 224 | 225 | pub fn icrc1_transfer( 226 | env: &StateMachine, 227 | ledger_id: Principal, 228 | from_owner: Principal, 229 | args: TransferArgs, 230 | ) -> Result { 231 | update_or_panic(env, ledger_id, from_owner, "icrc1_transfer", args) 232 | } 233 | 234 | pub fn icrc2_transfer_from( 235 | env: &StateMachine, 236 | ledger_id: Principal, 237 | caller: Principal, 238 | args: TransferFromArgs, 239 | ) -> Result { 240 | update_or_panic(env, ledger_id, caller, "icrc2_transfer_from", args) 241 | } 242 | 243 | pub fn icrc1_metadata(env: &StateMachine, ledger_id: Principal) -> Vec<(String, MetadataValue)> { 244 | query_or_panic(env, ledger_id, Principal::anonymous(), "icrc1_metadata", ()) 245 | } 246 | 247 | pub fn icrc3_get_blocks>( 248 | env: &StateMachine, 249 | ledger_id: Principal, 250 | start_lengths: Vec<(N, N)>, 251 | ) -> GetBlocksResult { 252 | let args: GetBlocksArgs = start_lengths 253 | .into_iter() 254 | .map(|(start, length)| GetBlocksArg { 255 | start: start.into(), 256 | length: length.into(), 257 | }) 258 | .collect(); 259 | query_or_panic( 260 | env, 261 | ledger_id, 262 | Principal::anonymous(), 263 | "icrc3_get_blocks", 264 | args, 265 | ) 266 | } 267 | 268 | pub fn transaction_hashes(env: &StateMachine, ledger_id: Principal) -> BTreeMap<[u8; 32], u64> { 269 | query_or_panic( 270 | env, 271 | ledger_id, 272 | Principal::anonymous(), 273 | "get_transaction_hashes", 274 | (), 275 | ) 276 | } 277 | 278 | pub fn transaction_timestamps( 279 | env: &StateMachine, 280 | ledger_id: Principal, 281 | ) -> BTreeMap<(u64, u64), ()> { 282 | query_or_panic( 283 | env, 284 | ledger_id, 285 | Principal::anonymous(), 286 | "get_transaction_timestamps", 287 | (), 288 | ) 289 | } 290 | 291 | pub fn get_tip_certificate(env: &StateMachine, ledger_id: Principal) -> DataCertificate { 292 | let res: Option = query_or_panic( 293 | env, 294 | ledger_id, 295 | Principal::anonymous(), 296 | "icrc3_get_tip_certificate", 297 | (), 298 | ); 299 | res.expect("icrc3_get_tip_certificate should return a non-null result for query calls") 300 | } 301 | -------------------------------------------------------------------------------- /cycles-ledger/tests/gen.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use candid::{Nat, Principal}; 4 | use cycles_ledger::{ 5 | config::FEE, 6 | endpoints::{DepositArg, WithdrawArgs}, 7 | }; 8 | use icrc_ledger_types::{ 9 | icrc1::{ 10 | account::Account, 11 | transfer::{Memo, TransferArg}, 12 | }, 13 | icrc2::{approve::ApproveArgs, transfer_from::TransferFromArgs}, 14 | }; 15 | use num_traits::ToPrimitive; 16 | use proptest::{ 17 | collection, option, 18 | prelude::any, 19 | prop_assert_eq, prop_compose, proptest, 20 | sample::select, 21 | strategy::{Just, Strategy, Union}, 22 | }; 23 | use serde_bytes::ByteBuf; 24 | 25 | // The arguments passed to an update call to the cycles ledger. 26 | #[derive(Clone, Debug)] 27 | pub enum CyclesLedgerCallArg { 28 | Approve(ApproveArgs), 29 | Deposit { amount: Nat, arg: DepositArg }, 30 | Withdraw(WithdrawArgs), 31 | Transfer(TransferArg), 32 | TransferFrom(TransferFromArgs), 33 | } 34 | 35 | impl From for CyclesLedgerCallArg { 36 | fn from(value: ApproveArgs) -> Self { 37 | Self::Approve(value) 38 | } 39 | } 40 | 41 | impl From<(Nat, DepositArg)> for CyclesLedgerCallArg { 42 | fn from((amount, arg): (Nat, DepositArg)) -> Self { 43 | Self::Deposit { amount, arg } 44 | } 45 | } 46 | 47 | impl From for CyclesLedgerCallArg { 48 | fn from(value: WithdrawArgs) -> Self { 49 | Self::Withdraw(value) 50 | } 51 | } 52 | 53 | impl From for CyclesLedgerCallArg { 54 | fn from(value: TransferArg) -> Self { 55 | Self::Transfer(value) 56 | } 57 | } 58 | 59 | impl From for CyclesLedgerCallArg { 60 | fn from(value: TransferFromArgs) -> Self { 61 | Self::TransferFrom(value) 62 | } 63 | } 64 | 65 | // An update call to the cycles ledger. 66 | #[derive(Clone, Debug)] 67 | pub struct CyclesLedgerCall { 68 | pub caller: Principal, 69 | pub arg: CyclesLedgerCallArg, 70 | } 71 | 72 | impl std::fmt::Display for CyclesLedgerCall { 73 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 74 | fn encode_memo(memo: &Option) -> Option { 75 | memo.as_ref().map(|bs| hex::encode(bs.0.as_slice())) 76 | } 77 | 78 | fn encode_account(owner: Principal, subaccount: Option<[u8; 32]>) -> String { 79 | Account { owner, subaccount }.to_string() 80 | } 81 | 82 | match &self.arg { 83 | CyclesLedgerCallArg::Approve(arg) => { 84 | write!(f, "Approve {{ ")?; 85 | write!( 86 | f, 87 | "from: {}, ", 88 | encode_account(self.caller, arg.from_subaccount) 89 | )?; 90 | write!(f, "spender: {}, ", arg.spender)?; 91 | write!(f, "amount: {}, ", arg.amount)?; 92 | write!(f, "expected_allowance: {:?}, ", arg.expected_allowance)?; 93 | write!(f, "expires_at: {:?}, ", arg.expires_at)?; 94 | write!(f, "fee: {:?}, ", arg.fee)?; 95 | write!(f, "memo: {:?}, ", encode_memo(&arg.memo))?; 96 | write!(f, "created_at_time: {:?} ", arg.created_at_time)?; 97 | write!(f, "}}") 98 | } 99 | CyclesLedgerCallArg::Deposit { amount, arg } => { 100 | write!(f, "Deposit {{ ")?; 101 | write!(f, "from: {}, ", self.caller)?; 102 | write!(f, "to: {}, ", arg.to)?; 103 | write!(f, "amount: {}, ", amount)?; 104 | write!(f, "memo:: {:?} ", encode_memo(&arg.memo))?; 105 | write!(f, "}}") 106 | } 107 | CyclesLedgerCallArg::Withdraw(arg) => { 108 | write!(f, "Withdraw {{ ")?; 109 | write!( 110 | f, 111 | "from, {}, ", 112 | encode_account(self.caller, arg.from_subaccount) 113 | )?; 114 | write!(f, "to: {}, ", arg.to)?; 115 | write!(f, "amount: {}, ", arg.amount)?; 116 | write!(f, "created_at_time: {:?} ", arg.created_at_time)?; 117 | write!(f, "}}") 118 | } 119 | CyclesLedgerCallArg::Transfer(arg) => { 120 | write!(f, "Transfer {{ ")?; 121 | write!( 122 | f, 123 | "from, {}, ", 124 | encode_account(self.caller, arg.from_subaccount) 125 | )?; 126 | write!(f, "to: {}, ", arg.to)?; 127 | write!(f, "amount: {}, ", arg.amount)?; 128 | write!(f, "created_at_time: {:?} ", arg.created_at_time)?; 129 | write!(f, "fee: {:?}, ", arg.fee)?; 130 | write!(f, "memo: {:?} ", encode_memo(&arg.memo))?; 131 | write!(f, "}}") 132 | } 133 | CyclesLedgerCallArg::TransferFrom(arg) => { 134 | write!(f, "TransferFrom {{ ")?; 135 | write!(f, "from, {}, ", arg.from)?; 136 | write!(f, "to: {}, ", arg.to)?; 137 | write!( 138 | f, 139 | "spender: {}, ", 140 | encode_account(self.caller, arg.spender_subaccount) 141 | )?; 142 | write!(f, "amount: {}, ", arg.amount)?; 143 | write!(f, "created_at_time: {:?} ", arg.created_at_time)?; 144 | write!(f, "fee: {:?}, ", arg.fee)?; 145 | write!(f, "memo: {:?} ", encode_memo(&arg.memo))?; 146 | write!(f, "}}") 147 | } 148 | } 149 | } 150 | } 151 | 152 | pub trait IsCyclesLedger { 153 | fn execute(&mut self, call: &CyclesLedgerCall) -> Result<(), String>; 154 | } 155 | 156 | // An in-memory cycles ledger state. 157 | #[derive(Clone, Debug, Default)] 158 | pub struct CyclesLedgerInMemory { 159 | pub balances: HashMap, 160 | pub allowances: HashMap<(Account, Account), u128>, 161 | pub total_supply: u128, 162 | pub depositor_cycles: u128, 163 | } 164 | 165 | impl CyclesLedgerInMemory { 166 | pub fn new(depositor_cycles: u128) -> Self { 167 | Self { 168 | depositor_cycles, 169 | ..Default::default() 170 | } 171 | } 172 | 173 | pub fn token_pool(&self) -> u128 { 174 | u128::MAX - self.total_supply 175 | } 176 | } 177 | 178 | impl IsCyclesLedger for CyclesLedgerInMemory { 179 | fn execute(&mut self, arg: &CyclesLedgerCall) -> Result<(), String> { 180 | match &arg.arg { 181 | CyclesLedgerCallArg::Approve(ApproveArgs { 182 | from_subaccount, 183 | spender, 184 | amount, 185 | .. 186 | }) => { 187 | let from = Account { 188 | owner: arg.caller, 189 | subaccount: *from_subaccount, 190 | }; 191 | let old_balance = self 192 | .balances 193 | .get(&from) 194 | .ok_or_else(|| format!("Account {} has 0 balance", from))?; 195 | self.balances.insert( 196 | from, 197 | old_balance 198 | .checked_sub(FEE) 199 | .ok_or("unable to subtract the fee")?, 200 | ); 201 | self.allowances.insert( 202 | (from, *spender), 203 | amount.0.to_u128().ok_or("amount is not a u128")?, 204 | ); 205 | self.total_supply = self 206 | .total_supply 207 | .checked_sub(FEE) 208 | .ok_or("total supply underflow")?; 209 | } 210 | CyclesLedgerCallArg::Deposit { 211 | amount, 212 | arg: DepositArg { to, .. }, 213 | } => { 214 | let amount = amount.0.to_u128().ok_or("amount is not a u128")? - FEE; 215 | // The precise cost of calling the deposit endpoint is unknown. 216 | // depositor_cycles is decreased by an arbitrary number plus 217 | // the amount. 218 | self.depositor_cycles = self 219 | .depositor_cycles 220 | .saturating_sub(10_000_000_000_000u128.saturating_add(amount)); 221 | 222 | let old_balance = self.balances.get(to).copied().unwrap_or_default(); 223 | self.balances 224 | .insert(*to, old_balance.checked_add(amount).ok_or("overflow")?); 225 | self.total_supply = self 226 | .total_supply 227 | .checked_add(amount) 228 | .ok_or("total supply overflow")?; 229 | } 230 | CyclesLedgerCallArg::Withdraw(WithdrawArgs { 231 | from_subaccount, 232 | amount, 233 | .. 234 | }) => { 235 | let from = Account { 236 | owner: arg.caller, 237 | subaccount: *from_subaccount, 238 | }; 239 | let old_balance = self 240 | .balances 241 | .get(&from) 242 | .ok_or_else(|| format!("Account {} has 0 balance", from))?; 243 | let amount = amount.0.to_u128().ok_or("amount is not a u128")?; 244 | let amount_plus_fee = amount.checked_add(FEE).ok_or("amount + FEE overflow")?; 245 | self.balances.insert( 246 | from, 247 | old_balance 248 | .checked_sub(amount_plus_fee) 249 | .ok_or("balance underflow")?, 250 | ); 251 | self.total_supply = self 252 | .total_supply 253 | .checked_sub(amount_plus_fee) 254 | .ok_or("total supply undeflow")?; 255 | } 256 | CyclesLedgerCallArg::Transfer(TransferArg { 257 | from_subaccount, 258 | to, 259 | amount, 260 | .. 261 | }) => { 262 | let from = Account { 263 | owner: arg.caller, 264 | subaccount: *from_subaccount, 265 | }; 266 | let old_balance = self 267 | .balances 268 | .get(&from) 269 | .ok_or_else(|| format!("Account {} has 0 balance", from))?; 270 | let amount = amount.0.to_u128().ok_or("amount is not a u128")?; 271 | let new_balance = old_balance 272 | .checked_sub(amount) 273 | .and_then(|b| b.checked_sub(FEE)) 274 | .ok_or("balance underflow")?; 275 | self.balances.insert(from, new_balance); 276 | let old_balance = self.balances.get(to).copied().unwrap_or_default(); 277 | self.balances.insert( 278 | *to, 279 | old_balance.checked_add(amount).ok_or("balance overflow")?, 280 | ); 281 | self.total_supply = self 282 | .total_supply 283 | .checked_sub(FEE) 284 | .ok_or("total supply underflow")?; 285 | } 286 | CyclesLedgerCallArg::TransferFrom(TransferFromArgs { 287 | spender_subaccount, 288 | from, 289 | to, 290 | amount, 291 | fee, 292 | memo, 293 | created_at_time, 294 | }) => { 295 | self.execute(&CyclesLedgerCall { 296 | caller: from.owner, 297 | arg: CyclesLedgerCallArg::Transfer(TransferArg { 298 | from_subaccount: from.subaccount, 299 | to: *to, 300 | fee: fee.to_owned(), 301 | created_at_time: *created_at_time, 302 | memo: memo.to_owned(), 303 | amount: amount.clone(), 304 | }), 305 | })?; 306 | let spender = Account { 307 | owner: arg.caller, 308 | subaccount: *spender_subaccount, 309 | }; 310 | let old_allowance = self 311 | .allowances 312 | .get(&(*from, spender)) 313 | .unwrap_or_else(|| panic!("Allowance of {:?} is 0", (from, spender))); 314 | let amount = amount.0.to_u128().ok_or("amount is not a u128")?; 315 | let new_allowance = old_allowance 316 | .checked_sub(amount) 317 | .and_then(|b| b.checked_sub(FEE)) 318 | .ok_or("allowance underflow")?; 319 | self.allowances.insert((*from, spender), new_allowance); 320 | } 321 | } 322 | Ok(()) 323 | } 324 | } 325 | 326 | // Represents a set of valid calls and the 327 | // in-memory state that results when performing those 328 | // calls. 329 | #[derive(Clone, Debug, Default)] 330 | pub struct CyclesLedgerCallsState { 331 | pub calls: Vec, 332 | pub state: CyclesLedgerInMemory, 333 | } 334 | 335 | impl CyclesLedgerCallsState { 336 | fn new(depositor_cycles: u128) -> Self { 337 | Self { 338 | calls: vec![], 339 | state: CyclesLedgerInMemory::new(depositor_cycles), 340 | } 341 | } 342 | 343 | // Return the number of tokens available for minting. 344 | fn token_pool(&self) -> u128 { 345 | self.state.token_pool() 346 | } 347 | 348 | fn accounts_with_at_least_fee(&self) -> Vec<(Account, u128)> { 349 | self.state 350 | .balances 351 | .iter() 352 | .filter_map(|(account, balance)| { 353 | if balance >= &FEE { 354 | Some((*account, *balance)) 355 | } else { 356 | None 357 | } 358 | }) 359 | .collect() 360 | } 361 | } 362 | 363 | impl IsCyclesLedger for CyclesLedgerCallsState { 364 | fn execute(&mut self, arg: &CyclesLedgerCall) -> Result<(), String> { 365 | self.state.execute(arg)?; 366 | self.calls.push(arg.clone()); 367 | Ok(()) 368 | } 369 | } 370 | 371 | fn arb_allowed_principal() -> impl Strategy { 372 | collection::vec(any::(), 0..30).prop_filter_map( 373 | "Management and anonymous principals are disabled", 374 | |bytes| { 375 | let principal = Principal::from_slice(&bytes); 376 | if principal == Principal::management_canister() || principal == Principal::anonymous() 377 | { 378 | None 379 | } else { 380 | Some(principal) 381 | } 382 | }, 383 | ) 384 | } 385 | 386 | fn arb_account() -> impl Strategy { 387 | (arb_allowed_principal(), option::of(any::<[u8; 32]>())) 388 | .prop_map(|(owner, subaccount)| Account { owner, subaccount }) 389 | } 390 | 391 | fn arb_amount(max: u128) -> impl Strategy { 392 | (0..=max).prop_map(Nat::from) 393 | } 394 | 395 | fn arb_memo() -> impl Strategy { 396 | collection::vec(any::(), 0..32).prop_map(|bytes| Memo(ByteBuf::from(bytes))) 397 | } 398 | 399 | fn arb_approve( 400 | token_pool: u128, 401 | allowances: Arc>, 402 | arb_approver: impl Strategy, 403 | arb_expires_at: impl Strategy>, 404 | arb_created_at_time: impl Strategy>, 405 | ) -> impl Strategy { 406 | ( 407 | arb_approver, 408 | arb_account(), 409 | arb_amount(token_pool), 410 | arb_expires_at, 411 | arb_created_at_time, 412 | ) 413 | .prop_filter("self-approve disabled", |(approver, spender, _, _, _)| { 414 | approver.owner != spender.owner 415 | }) 416 | .prop_flat_map( 417 | move |(approver, spender, amount, expires_at, created_at_time)| { 418 | let allowance = allowances 419 | .get(&(approver, spender)) 420 | .copied() 421 | .unwrap_or_default(); 422 | let arb_expected_allowance = option::of(Just(allowance.into())); 423 | let arb_suggested_fee = option::of(Just(FEE.into())); 424 | ( 425 | option::of(arb_memo()), 426 | arb_expected_allowance, 427 | arb_suggested_fee, 428 | ) 429 | .prop_map(move |(memo, expected_allowance, fee)| { 430 | CyclesLedgerCall { 431 | caller: approver.owner, 432 | arg: ApproveArgs { 433 | from_subaccount: approver.subaccount, 434 | spender, 435 | amount: amount.clone(), 436 | expected_allowance, 437 | expires_at, 438 | fee, 439 | memo, 440 | created_at_time, 441 | } 442 | .into(), 443 | } 444 | }) 445 | }, 446 | ) 447 | } 448 | 449 | prop_compose! { 450 | fn arb_deposit(depositor: Principal, depositor_cycles: u128) 451 | (to in arb_account(), 452 | // deposit requires that the amount is >= FEE 453 | amount in arb_amount(depositor_cycles - FEE).prop_map(|c| c + Nat::from(FEE)), 454 | memo in option::of(arb_memo()), 455 | ) 456 | -> CyclesLedgerCall { 457 | CyclesLedgerCall { 458 | caller: depositor, 459 | arg: (amount, DepositArg { to, memo, }).into() 460 | } 461 | } 462 | } 463 | 464 | prop_compose! { 465 | fn arb_withdraw( 466 | arb_from: impl Strategy, 467 | depositor: Principal, 468 | arb_created_at_time: impl Strategy>, 469 | ) 470 | ( 471 | (from, from_balance) in arb_from, 472 | created_at_time in arb_created_at_time 473 | ) 474 | ( 475 | from in Just(from), 476 | created_at_time in Just(created_at_time), 477 | amount in (0..=(from_balance - FEE)).prop_map(Nat::from), 478 | ) 479 | -> CyclesLedgerCall { 480 | CyclesLedgerCall { 481 | caller: from.owner, 482 | arg: WithdrawArgs { 483 | from_subaccount: from.subaccount, 484 | // Destination must exist so we pass the only 485 | // canister that we know exists except for the cycles ledger. 486 | to: depositor, 487 | created_at_time, 488 | amount 489 | }.into(), 490 | } 491 | } 492 | } 493 | 494 | prop_compose! { 495 | fn arb_transfer( 496 | arb_from: impl Strategy, 497 | arb_created_at_time: impl Strategy>, 498 | ) 499 | ( 500 | (from, from_balance) in arb_from, 501 | created_at_time in arb_created_at_time, 502 | ) 503 | ( 504 | from in Just(from), 505 | created_at_time in Just(created_at_time), 506 | to in arb_account().prop_filter("cannot self tranasfer", move |to| &from != to), 507 | fee in option::of(Just(FEE.into())), 508 | amount in (0..=(from_balance-FEE)).prop_map(Nat::from), 509 | memo in option::of(arb_memo()), 510 | ) 511 | -> CyclesLedgerCall { 512 | CyclesLedgerCall { 513 | caller: from.owner, 514 | arg: TransferArg { 515 | from_subaccount: from.subaccount, 516 | to, 517 | fee, 518 | created_at_time, 519 | memo, 520 | amount 521 | }.into(), 522 | } 523 | } 524 | } 525 | 526 | prop_compose! { 527 | fn arb_transfer_from( 528 | arb_from_spender: impl Strategy, 529 | arb_created_at_time: impl Strategy>, 530 | ) 531 | ( 532 | (from, spender, from_balance) in arb_from_spender, 533 | created_at_time in arb_created_at_time, 534 | ) 535 | (from in Just(from), 536 | spender in Just(spender), 537 | created_at_time in Just(created_at_time), 538 | to in arb_account().prop_filter("cannot transfer to self", move |to| &from != to), 539 | fee in option::of(Just(FEE.into())), 540 | amount in (0..=(from_balance-FEE)).prop_map(Nat::from), 541 | memo in option::of(arb_memo()), 542 | ) 543 | -> CyclesLedgerCall { 544 | CyclesLedgerCall { 545 | caller: spender.owner, 546 | arg: TransferFromArgs { 547 | from, 548 | to, 549 | spender_subaccount: spender.subaccount, 550 | fee, 551 | created_at_time, 552 | memo, 553 | amount, 554 | }.into(), 555 | } 556 | } 557 | } 558 | 559 | pub fn arb_cycles_ledger_call_state( 560 | depositor: Principal, 561 | depositor_cycles: u128, 562 | len: u8, 563 | now_in_nanos_since_epoch: u64, 564 | ) -> impl Strategy { 565 | if depositor_cycles < FEE { 566 | panic!( 567 | "Cannot run the test if the depositor doesn't have enough cycles for the first deposit" 568 | ); 569 | } 570 | arb_cycles_ledger_call_state_from( 571 | CyclesLedgerCallsState::new(depositor_cycles), 572 | depositor, 573 | len, 574 | now_in_nanos_since_epoch, 575 | ) 576 | } 577 | 578 | fn arb_created_at_time(now_in_nanos_since_epoch: u64) -> impl Strategy> { 579 | option::of(Just( 580 | now_in_nanos_since_epoch.saturating_sub(10_000_000_000), 581 | )) 582 | } 583 | 584 | // Note: this generator will blow up the stack for high `len` 585 | // because it will call itself recursively `len` times. If you need a bigger 586 | // state then do multiple sequential calls to 587 | // [arb_cycles_ledger_call_state_from]. 588 | pub fn arb_cycles_ledger_call_state_from( 589 | state: CyclesLedgerCallsState, 590 | depositor_id: Principal, 591 | len: u8, 592 | now_in_nanos_since_epoch: u64, 593 | ) -> impl Strategy { 594 | fn step( 595 | state: CyclesLedgerCallsState, 596 | depositor: Principal, 597 | now_in_nanos_since_epoch: u64, 598 | n: u8, 599 | ) -> impl Strategy { 600 | if n == 0 { 601 | return Just(state).boxed(); 602 | } 603 | 604 | let accounts = state.accounts_with_at_least_fee(); 605 | let allowances = Arc::new(state.state.allowances.clone()); 606 | let depositor_cycles = state.state.depositor_cycles; 607 | let token_pool = state.token_pool(); 608 | 609 | let mut arb_calls = vec![]; 610 | if depositor_cycles > 0 { 611 | let arb_deposit = arb_deposit(depositor, depositor_cycles); 612 | arb_calls.push(arb_deposit.boxed()); 613 | } 614 | if !accounts.is_empty() { 615 | let select_account_and_balance = Arc::new(select(accounts.clone())); 616 | 617 | // approve 618 | let select_account = select_account_and_balance.clone().prop_map(|(a, _)| a); 619 | let arb_expires_at = option::of(Just( 620 | now_in_nanos_since_epoch.saturating_add(3_600_000_000_000), 621 | )); 622 | let arb_approve = arb_approve( 623 | token_pool, 624 | allowances.clone(), 625 | select_account, 626 | arb_expires_at, 627 | arb_created_at_time(now_in_nanos_since_epoch), 628 | ); 629 | arb_calls.push(arb_approve.boxed()); 630 | 631 | // withdraw 632 | let arb_withdraw = arb_withdraw( 633 | select_account_and_balance.clone(), 634 | depositor, 635 | arb_created_at_time(now_in_nanos_since_epoch), 636 | ); 637 | arb_calls.push(arb_withdraw.boxed()); 638 | 639 | // transfer 640 | let arb_transfer = arb_transfer( 641 | select_account_and_balance, 642 | arb_created_at_time(now_in_nanos_since_epoch), 643 | ); 644 | arb_calls.push(arb_transfer.boxed()); 645 | 646 | // transfer_from 647 | let accounts: HashMap<_, _> = accounts.iter().copied().collect(); 648 | let mut from_spender_amount = vec![]; 649 | for ((from, spender), allowance) in allowances.as_ref() { 650 | let Some(balance) = accounts.get(from) else { 651 | continue; 652 | }; 653 | from_spender_amount.push((*from, *spender, *allowance.min(balance))); 654 | } 655 | if !from_spender_amount.is_empty() { 656 | let arb_transfer_from = arb_transfer_from( 657 | select(from_spender_amount), 658 | arb_created_at_time(now_in_nanos_since_epoch), 659 | ); 660 | arb_calls.push(arb_transfer_from.boxed()); 661 | } 662 | } 663 | 664 | if arb_calls.is_empty() { 665 | panic!("BUG: no valid call can be performed on the current state"); 666 | } 667 | 668 | // Union panics if arb_calls is empty but this shouldn't happen 669 | // as either the depositor has cycles or an account has funds. 670 | ( 671 | Union::new(arb_calls), 672 | Just(state), 673 | Just(now_in_nanos_since_epoch), 674 | ) 675 | .prop_flat_map(move |(call, mut state, now_in_nanos_since_epoch)| { 676 | state.execute(&call).unwrap(); 677 | step(state, depositor, now_in_nanos_since_epoch, n - 1) 678 | }) 679 | .boxed() 680 | } 681 | 682 | step(state, depositor_id, now_in_nanos_since_epoch, len) 683 | } 684 | 685 | #[test] 686 | fn test() { 687 | // check that [arb_cycles_ledger_call_state] doesn't panic 688 | proptest!(|(state in arb_cycles_ledger_call_state(Principal::anonymous(), u128::MAX, 10, 0))| { 689 | prop_assert_eq!(10, state.calls.len()) 690 | }) 691 | } 692 | -------------------------------------------------------------------------------- /depositor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "depositor" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | publish.workspace = true 9 | 10 | [dependencies] 11 | cycles-ledger = { path = "../cycles-ledger" } 12 | candid.workspace = true 13 | candid_parser.workspace = true 14 | ic-cdk.workspace = true 15 | ic-cdk-macros.workspace = true 16 | icrc-ledger-types.workspace = true 17 | serde.workspace = true 18 | 19 | [package.metadata.release] # cargo-release 20 | pre-release-replacements = [] 21 | -------------------------------------------------------------------------------- /depositor/depositor.did: -------------------------------------------------------------------------------- 1 | type Account = record { owner : principal; subaccount : opt vec nat8 }; 2 | type DepositArg = record { to : Account; memo : opt vec nat8; cycles : nat }; 3 | type DepositResult = record { balance : nat; block_index : nat }; 4 | type InitArg = record { ledger_id : principal }; 5 | service : (InitArg) -> { deposit : (DepositArg) -> (DepositResult) }; 6 | -------------------------------------------------------------------------------- /depositor/src/endpoints.rs: -------------------------------------------------------------------------------- 1 | use candid::{CandidType, Principal}; 2 | use icrc_ledger_types::icrc1::{account::Account, transfer::Memo}; 3 | use serde::Deserialize; 4 | 5 | #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] 6 | pub struct InitArg { 7 | pub ledger_id: Principal, 8 | } 9 | 10 | #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] 11 | pub struct DepositArg { 12 | pub cycles: u128, 13 | pub to: Account, 14 | pub memo: Option, 15 | } 16 | -------------------------------------------------------------------------------- /depositor/src/lib.rs: -------------------------------------------------------------------------------- 1 | use candid::Principal; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | pub mod endpoints; 5 | 6 | #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] 7 | pub struct Config { 8 | pub ledger_id: Principal, 9 | } 10 | 11 | // This isn't really used but it makes it easier to deal 12 | // with state because we don't have to use Option for when 13 | // the state is not initialized. 14 | impl Default for Config { 15 | fn default() -> Self { 16 | Self { 17 | ledger_id: Principal::management_canister(), 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /depositor/src/main.rs: -------------------------------------------------------------------------------- 1 | use candid::candid_method; 2 | use cycles_ledger::endpoints::DepositResult; 3 | use depositor::{ 4 | endpoints::{DepositArg, InitArg}, 5 | Config, 6 | }; 7 | use ic_cdk::api::call::call_with_payment128; 8 | use ic_cdk_macros::{init, update}; 9 | use std::cell::RefCell; 10 | 11 | thread_local! { 12 | static CONFIG: RefCell = RefCell::new(Config::default()); 13 | } 14 | 15 | fn with_config(f: impl FnOnce(&Config) -> R) -> R { 16 | CONFIG.with(|cell| f(&cell.borrow())) 17 | } 18 | 19 | fn main() {} 20 | 21 | #[init] 22 | #[candid_method(init)] 23 | fn init(arg: InitArg) { 24 | CONFIG.with(|cell| { 25 | *cell.borrow_mut() = Config { 26 | ledger_id: arg.ledger_id, 27 | }; 28 | }); 29 | } 30 | 31 | #[update] 32 | #[candid_method] 33 | async fn deposit(arg: DepositArg) -> DepositResult { 34 | let ledger_id = with_config(|config| config.ledger_id); 35 | let cycles = arg.cycles; 36 | let arg = cycles_ledger::endpoints::DepositArg { 37 | to: arg.to, 38 | memo: arg.memo, 39 | }; 40 | let (result,): (DepositResult,) = call_with_payment128(ledger_id, "deposit", (arg,), cycles) 41 | .await 42 | .expect("Unable to call deposit"); 43 | result 44 | } 45 | 46 | #[test] 47 | fn test_candid_interface_compatibility() { 48 | use candid_parser::utils::{service_equal, CandidSource}; 49 | use std::path::PathBuf; 50 | 51 | candid::export_service!(); 52 | let exported_interface = __export_service(); 53 | 54 | let expected_interface = 55 | PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("depositor.did"); 56 | 57 | println!( 58 | "Expected interface: {}\n\n", 59 | CandidSource::File(expected_interface.as_path()) 60 | .load() 61 | .unwrap() 62 | .1 63 | .unwrap() 64 | ); 65 | println!("Exported interface: {}\n\n", exported_interface); 66 | 67 | service_equal( 68 | CandidSource::Text(&exported_interface), 69 | CandidSource::File(expected_interface.as_path()), 70 | ) 71 | .expect("The despositor interface is not compatible with the depositor.did file"); 72 | } 73 | -------------------------------------------------------------------------------- /dfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "dfx": "0.25.0", 3 | "canisters": { 4 | "cycles-ledger": { 5 | "type": "rust", 6 | "candid": "./cycles-ledger/cycles-ledger.did", 7 | "package": "cycles-ledger", 8 | "optimize": "cycles", 9 | "gzip": true, 10 | "pullable": { 11 | "dependencies": [], 12 | "wasm_url": "https://github.com/dfinity/cycles-ledger/releases/latest/download/cycles-ledger.wasm.gz", 13 | "wasm_hash_url": "https://github.com/dfinity/cycles-ledger/releases/latest/download/cycles-ledger.wasm.gz.sha256", 14 | "init_guide": "Set max_blocks_per_request in Init record", 15 | "init_arg": "(variant{Init=record{max_blocks_per_request=1000}})" 16 | } 17 | }, 18 | "depositor": { 19 | "type": "rust", 20 | "candid": "./depositor/depositor.did", 21 | "package": "depositor", 22 | "optimize": "cycles", 23 | "gzip": true 24 | }, 25 | "fake-cmc": { 26 | "type": "rust", 27 | "candid": "./fake-cmc/fake-cmc.did", 28 | "package": "fake-cmc", 29 | "optimize": "cycles", 30 | "gzip": true 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /download-state-machine.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if test "$#" -ne 2; then 4 | echo "usage: download-state-machine.sh " 5 | exit 1 6 | fi 7 | 8 | IC_RELEASE_HASH="$1" 9 | PLATFORM="$2" 10 | 11 | DOWNLOAD_URL="https://download.dfinity.systems/ic/${IC_RELEASE_HASH}/binaries/x86_64-${PLATFORM}/ic-test-state-machine.gz" 12 | echo "downloading binary from ${DOWNLOAD_URL}" 13 | 14 | wget "${DOWNLOAD_URL}" 15 | gunzip ic-test-state-machine.gz 16 | chmod +x ic-test-state-machine -------------------------------------------------------------------------------- /fake-cmc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fake-cmc" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | publish.workspace = true 9 | 10 | [dependencies] 11 | cycles-ledger = { path = "../cycles-ledger" } 12 | candid.workspace = true 13 | candid_parser.workspace = true 14 | ic-cdk.workspace = true 15 | ic-cdk-macros.workspace = true 16 | icrc-ledger-types.workspace = true 17 | serde.workspace = true 18 | 19 | [package.metadata.release] # cargo-release 20 | pre-release-replacements = [] 21 | -------------------------------------------------------------------------------- /fake-cmc/fake-cmc.did: -------------------------------------------------------------------------------- 1 | type CanisterSettings = record { 2 | controllers : opt vec principal; 3 | freezing_threshold : opt nat; 4 | reserved_cycles_limit : opt nat; 5 | memory_allocation : opt nat; 6 | compute_allocation : opt nat; 7 | }; 8 | type CmcCreateCanisterArgs = record { 9 | subnet_selection : opt SubnetSelection; 10 | settings : opt CanisterSettings; 11 | }; 12 | type CmcCreateCanisterError = variant { 13 | Refunded : record { create_error : text; refund_amount : nat }; 14 | RefundFailed : record { create_error : text; refund_error : text }; 15 | }; 16 | type CmcCreateCanisterResult = variant { 17 | Ok : principal; 18 | Err : CmcCreateCanisterError; 19 | }; 20 | type SubnetFilter = record { subnet_type : opt text }; 21 | type SubnetSelection = variant { 22 | Filter : SubnetFilter; 23 | Subnet : record { subnet : principal }; 24 | }; 25 | 26 | type IcpXdrConversionRate = record { 27 | timestamp_seconds : nat64; 28 | xdr_permyriad_per_icp : nat64; 29 | }; 30 | 31 | type IcpXdrConversionRateResponse = record { 32 | data : IcpXdrConversionRate; 33 | hash_tree : blob; 34 | certificate : blob; 35 | }; 36 | 37 | service : { 38 | create_canister : (CmcCreateCanisterArgs) -> (CmcCreateCanisterResult); 39 | fail_next_create_canister_with : (CmcCreateCanisterError) -> (); 40 | last_create_canister_args : () -> (CmcCreateCanisterArgs) query; 41 | get_icp_xdr_conversion_rate : () -> (IcpXdrConversionRateResponse) query; 42 | }; 43 | -------------------------------------------------------------------------------- /fake-cmc/src/lib.rs: -------------------------------------------------------------------------------- 1 | use candid::CandidType; 2 | use cycles_ledger::endpoints::{CmcCreateCanisterArgs, CmcCreateCanisterError}; 3 | use ic_cdk::api::time; 4 | use serde::Deserialize; 5 | 6 | const NANOSECONDS_PER_SECOND: u64 = 1_000_000_000; 7 | 8 | #[derive(Debug, Deserialize, Eq, PartialEq, Default)] 9 | pub struct State { 10 | pub last_create_canister_args: Option, 11 | pub fail_next_create_canister_with: Option, 12 | } 13 | 14 | #[derive(CandidType, Deserialize, Default)] 15 | pub struct IcpXdrConversionRateResponse { 16 | pub certificate: Vec, 17 | pub data: IcpXdrConversionRate, 18 | pub hash_tree: Vec, 19 | } 20 | 21 | #[derive(CandidType, Deserialize)] 22 | pub struct IcpXdrConversionRate { 23 | pub xdr_permyriad_per_icp: u64, 24 | pub timestamp_seconds: u64, 25 | } 26 | 27 | impl Default for IcpXdrConversionRate { 28 | fn default() -> Self { 29 | Self { 30 | // mocked value 31 | xdr_permyriad_per_icp: 50_000, 32 | timestamp_seconds: time() / NANOSECONDS_PER_SECOND, 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /fake-cmc/src/main.rs: -------------------------------------------------------------------------------- 1 | use candid::{candid_method, Principal}; 2 | use core::panic; 3 | use cycles_ledger::endpoints::{CmcCreateCanisterArgs, CmcCreateCanisterError}; 4 | use fake_cmc::{IcpXdrConversionRateResponse, State}; 5 | use ic_cdk::{ 6 | api::{ 7 | call::{msg_cycles_accept128, msg_cycles_available128}, 8 | management_canister::main::CreateCanisterArgument, 9 | }, 10 | query, 11 | }; 12 | use ic_cdk_macros::update; 13 | use std::cell::RefCell; 14 | 15 | thread_local! { 16 | static STATE: RefCell = RefCell::new(State::default()); 17 | } 18 | 19 | fn main() {} 20 | 21 | #[candid_method] 22 | #[update] 23 | async fn create_canister(arg: CmcCreateCanisterArgs) -> Result { 24 | let cycles = msg_cycles_available128(); 25 | if cycles < 100_000_000_000 { 26 | return Err(CmcCreateCanisterError::Refunded { 27 | refund_amount: cycles, 28 | create_error: "Insufficient cycles attached.".to_string(), 29 | }); 30 | } 31 | 32 | let next_error = STATE.with(|s| { 33 | let mut state = s.borrow_mut(); 34 | state.last_create_canister_args = Some(arg.clone()); 35 | state.fail_next_create_canister_with.take() 36 | }); 37 | 38 | if let Some(error) = next_error { 39 | match error { 40 | CmcCreateCanisterError::Refunded { refund_amount, .. } => { 41 | msg_cycles_accept128(cycles - refund_amount); 42 | } 43 | CmcCreateCanisterError::RefundFailed { .. } => { 44 | let _ = msg_cycles_accept128(cycles); 45 | } 46 | }; 47 | return Err(error); 48 | }; 49 | ic_cdk::api::call::msg_cycles_accept128(cycles); 50 | 51 | // "Canister is already installed" happens because the canister id counter doesn't take into account that a canister with that id 52 | // was already created using `provisional_create_canister_with_id`. Simply loop to try the next canister id. 53 | loop { 54 | match ic_cdk::api::management_canister::main::create_canister( 55 | CreateCanisterArgument { 56 | settings: arg.settings.clone(), 57 | }, 58 | cycles, 59 | ) 60 | .await 61 | { 62 | Ok((record,)) => return Ok(record.canister_id), 63 | Err(error) => { 64 | if !error.1.contains("canister id already exists") { 65 | panic!("create_canister failed: {:?}", error) 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | #[candid_method] 73 | #[update] 74 | fn fail_next_create_canister_with(error: CmcCreateCanisterError) { 75 | STATE.with(|s| s.borrow_mut().fail_next_create_canister_with = Some(error)) 76 | } 77 | 78 | #[candid_method] 79 | #[query] 80 | fn get_icp_xdr_conversion_rate() -> IcpXdrConversionRateResponse { 81 | Default::default() 82 | } 83 | 84 | #[candid_method] 85 | #[query] 86 | fn last_create_canister_args() -> CmcCreateCanisterArgs { 87 | STATE.with(|s| { 88 | s.borrow() 89 | .last_create_canister_args 90 | .clone() 91 | .expect("No create_canister call recorded") 92 | }) 93 | } 94 | 95 | #[test] 96 | fn test_candid_interface_compatibility() { 97 | use candid_parser::utils::{service_equal, CandidSource}; 98 | use std::path::PathBuf; 99 | 100 | candid::export_service!(); 101 | let exported_interface = __export_service(); 102 | 103 | let expected_interface = 104 | PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("fake-cmc.did"); 105 | 106 | println!( 107 | "Expected interface: {}\n\n", 108 | CandidSource::File(expected_interface.as_path()) 109 | .load() 110 | .unwrap() 111 | .1 112 | .unwrap() 113 | ); 114 | println!("Exported interface: {}\n\n", exported_interface); 115 | 116 | service_equal( 117 | CandidSource::Text(&exported_interface), 118 | CandidSource::File(expected_interface.as_path()), 119 | ) 120 | .expect("The fake-cmc interface is not compatible with the fake-cmc.did file"); 121 | } 122 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.85.0" 3 | targets = ["wasm32-unknown-unknown"] 4 | components = ["rustfmt", "clippy"] 5 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | -------------------------------------------------------------------------------- /scripts/docker-build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # vim: ft=bash 3 | # Build cycles-ledger.wasm.gz inside docker. This outputs a single 4 | # file, cycles-ledger.wasm.gz, in the top-level directory. 5 | 6 | set -euo pipefail 7 | 8 | # Make sure we always run from the root 9 | SCRIPTS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 10 | cd "$SCRIPTS_DIR/.." 11 | 12 | function title() { 13 | echo "Build cycles-ledger Ledger inside Docker" 14 | } 15 | 16 | function usage() { 17 | cat << EOF 18 | 19 | Usage: 20 | $0 [] 21 | 22 | : (optional) the directory where cycles-ledger.wasm.gz will be put. If unset then the root of the project is used. 23 | EOF 24 | } 25 | 26 | function help() { 27 | cat << EOF 28 | 29 | This will create (and override) "/cycles-ledger.wasm.gz". 30 | EOF 31 | } 32 | 33 | ## Building 34 | 35 | function build() { 36 | outdir="$1" 37 | image_name="cycles-ledger" 38 | docker_build_args+=(--tag "$image_name" .) 39 | 40 | echo "The following image name will be used: $image_name" 41 | 42 | tmp_outdir=$(mktemp -d) 43 | 44 | set -x 45 | DOCKER_BUILDKIT=1 docker build "${docker_build_args[@]}" --output "$tmp_outdir" --progress plain 46 | set +x 47 | 48 | echo "Copying build output from $tmp_outdir to $PWD" 49 | mkdir -p "$(dirname ${outdir})" 50 | cp "$tmp_outdir/cycles-ledger.wasm.gz" "$outdir" 51 | 52 | echo "Removing $tmp_outdir" 53 | rm -rf "$tmp_outdir" 54 | } 55 | 56 | # ARGUMENT PARSING 57 | 58 | if [[ $# -gt 1 ]]; then 59 | >&2 echo "Too many arguments" 60 | usage 61 | echo 62 | exit 1 63 | fi 64 | 65 | OUTDIR="." 66 | while [[ $# -gt 0 ]]; do 67 | case $1 in 68 | -h|--help) 69 | title 70 | usage 71 | help 72 | exit 0 73 | ;; 74 | *) 75 | OUTDIR="$1" 76 | shift 77 | ;; 78 | esac 79 | done 80 | 81 | build "$OUTDIR" --------------------------------------------------------------------------------