diff --git a/.github/workflows/ninja_pr_checks.yml b/.github/workflows/ninja_pr_checks.yml index 7d892deec..8d62de2c5 100644 --- a/.github/workflows/ninja_pr_checks.yml +++ b/.github/workflows/ninja_pr_checks.yml @@ -77,6 +77,7 @@ jobs: ["Photo Gallery (Rust)"]="rust/photo_gallery" ["Inter-canister calls (Rust)"]="rust/inter-canister-calls" ["X.509 (Rust)"]="rust/x509" + ["SNS Kongswap Adaptor (Rust)"]="rust/sns-adaptor" ) # Check if we should run all examples (workflow file changed) or just changed ones diff --git a/rust/sns-adaptor/README.md b/rust/sns-adaptor/README.md new file mode 100644 index 000000000..226af66b5 --- /dev/null +++ b/rust/sns-adaptor/README.md @@ -0,0 +1,172 @@ +# SNS Kongswap Adaptor + +[SNS Kongswap Adaptor](https://github.com/ShahriarJavidi/sns-kongswap-adaptor/tree/ICP-Ninja) is a Rust-based canister designed to act as an adaptor between the Service Nervous System (SNS) treasury and the KongSwap decentralized exchange on the Internet Computer. Its primary function is to facilitate and automate the management of token assets (such as SNS and ICP tokens) held by a DAO treasury, enabling operations like deposits, withdrawals, balance tracking, and token swaps through KongSwap. The adaptor interacts with multiple canisters, including ledger canisters for different tokens and the KongSwap backend, to execute and audit these operations securely and transparently. + +The codebase is structured to ensure robust state management, transaction auditing, and error handling. It provides mechanisms to refresh ledger metadata, manage asset balances, and emit transactions with detailed logging and access control. + +Please note that this forked code is slightly modified to ease the interactions. Most drastically: + +1. when initializing the adaptor, it doesn't expect any transfers/approvals +2. transfer flow for deposits has changed from `ICRC-2` to `ICRC-1` + +## What You Can Learn + +This is example teaches you +1. how to build a wrapper around the kongswap adaptor +2. how to interact with [SNS Kongswap Adaptor](https://github.com/ShahriarJavidi/sns-kongswap-adaptor/tree/ICP-Ninja). + +## Deploying from ICP Ninja + +When viewing this project in ICP Ninja, you can deploy it directly to the mainnet for free by clicking "Run" in the upper right corner. Open this project in ICP Ninja: + +[![](https://icp.ninja/assets/open.svg)](https://icp.ninja/i?g=https://github.com/ShahriarJavidi/sns-kongswap-adaptor/tree/ICP-Ninja) + +## Local Testing with demo.sh + +The `demo.sh` script inside the adaptor's repository provides a complete local testing environment: + +## Running the Example + +```bash +cd sns-kongswap-adaptor +./demo.sh +``` + +This will: +1. Start a local IC replica +2. Deploy an SNS and an ICP Ledger +3. Deploy the in-production wasm of Kongswap (as of 9th Septemeber 2025) +4. Deploy the adaptor +5. Deposit to the adaptor + +### Prerequisites + +1. **Install dfx**: Follow [DFINITY SDK installation](https://internetcomputer.org/docs/current/developer-docs/setup/install/) +2. **Rust toolchain**: `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs/ | sh` + + + +### Inspecting Results + +After running locally, you can verify the balances: + +```bash +( + variant { + Ok = record { + timestamp_ns = 1_757_497_646_192_427_578 : nat64; + asset_to_balances = opt vec { + record { + variant { + Token = record { + ledger_fee_decimals = 10_000 : nat; + ledger_canister_id = principal "ryjl3-tyaaa-aaaaa-aaaba-cai"; + symbol = "ICP"; + } + }; + record { + treasury_owner = opt record { + name = opt "DAO Treasury"; + amount_decimals = 0 : nat; + account = opt record { + owner = principal "2vxsx-fae"; + subaccount = null; + }; + }; + suspense = opt record { + name = null; + amount_decimals = 0 : nat; + account = null; + }; + fee_collector = opt record { + name = null; + amount_decimals = 20_000 : nat; + account = null; + }; + treasury_manager = opt record { + name = opt "KongSwapAdaptor(u6s2n-gx777-77774-qaaba-cai)"; + amount_decimals = 0 : nat; + account = opt record { + owner = principal "u6s2n-gx777-77774-qaaba-cai"; + subaccount = null; + }; + }; + external_custodian = opt record { + name = null; + amount_decimals = 80_000 : nat; + account = null; + }; + payees = opt record { + name = null; + amount_decimals = 0 : nat; + account = null; + }; + payers = opt record { + name = null; + amount_decimals = 0 : nat; + account = null; + }; + }; + }; + record { + variant { + Token = record { + ledger_fee_decimals = 10_000 : nat; + ledger_canister_id = principal "lvfsa-2aaaa-aaaaq-aaeyq-cai"; + symbol = "LSNS"; + } + }; + record { + treasury_owner = opt record { + name = opt "DAO Treasury"; + amount_decimals = 0 : nat; + account = opt record { + owner = principal "2vxsx-fae"; + subaccount = opt blob "\4d\a0\fd\dd\fd\fb\57\0a\e4\72\d5\e4\07\1c\f5\10\4d\a6\c0\be\71\ca\66\e9\b7\e1\db\6f\6e\ad\1d\c3"; + }; + }; + suspense = opt record { + name = null; + amount_decimals = 0 : nat; + account = null; + }; + fee_collector = opt record { + name = null; + amount_decimals = 20_000 : nat; + account = null; + }; + treasury_manager = opt record { + name = opt "KongSwapAdaptor(u6s2n-gx777-77774-qaaba-cai)"; + amount_decimals = 0 : nat; + account = opt record { + owner = principal "u6s2n-gx777-77774-qaaba-cai"; + subaccount = null; + }; + }; + external_custodian = opt record { + name = null; + amount_decimals = 80_000 : nat; + account = null; + }; + payees = opt record { + name = null; + amount_decimals = 0 : nat; + account = null; + }; + payers = opt record { + name = null; + amount_decimals = 0 : nat; + account = null; + }; + }; + }; + }; + } + }, +) +``` + +This output shows the result of a balance query for two tokens, "ICP" and "LSNS", displaying how each token is distributed among various roles in the treasury system. For both tokens, the balances are split into categories such as `treasury_owner`, `suspense`, `fee_collector`, `treasury_manager`, `external_custodian`, payees, and payers. Most balances are 0, except for `fee_collector` (20_000) and `external_custodian` (80_000). + +The values reflect that, when transferring funds to the DEX, two actions each incur a fee of 10_000: +first, giving approval to the DEX, and second, the transfer initiated by the DEX. These fees are accouned in the `fee_collector` (totaling 20_000 per token). The `external_custodian` balance (80_000) is the amount that actually reaches the DEX after fees are deducted. Thus, the output shows the net result of a typical DEX transfer flow, with fees accounted for and the final amount available to the DEX shown under `external_custodian`. \ No newline at end of file diff --git a/rust/sns-adaptor/demo.sh b/rust/sns-adaptor/demo.sh new file mode 100755 index 000000000..4eac524b6 --- /dev/null +++ b/rust/sns-adaptor/demo.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +dfx stop +set -e +trap 'dfx stop' EXIT + +echo "Deploying ICP Ledger canister..." +dfx start --background --clean + +export MINTER_ACCOUNT_ID=$(dfx --identity anonymous ledger account-id) +export DEFAULT_ACCOUNT_ID=$(dfx ledger account-id) + +dfx deploy icp_ledger_canister --argument " + (variant { + Init = record { + minting_account = \"$MINTER_ACCOUNT_ID\"; + initial_values = vec { + record { + \"$DEFAULT_ACCOUNT_ID\"; + record { + e8s = 10_000_000_000 : nat64; + }; + }; + }; + send_whitelist = vec {}; + transfer_fee = opt record { + e8s = 10_000 : nat64; + }; + token_symbol = opt \"LICP\"; + token_name = opt \"Local ICP\"; + } + }) +" +dfx canister call icp_ledger_canister account_balance '(record { account = '$(python3 -c 'print("vec{" + ";".join([str(b) for b in bytes.fromhex("'$DEFAULT_ACCOUNT_ID'")]) + "}")')'})' + +echo "Deploying SNS Ledger canister..." +dfx deploy sns_ledger_canister --argument " + (variant { + Init = record { + minting_account = \"$MINTER_ACCOUNT_ID\"; + initial_values = vec { + record { + \"$DEFAULT_ACCOUNT_ID\"; + record { + e8s = 10_000_000_000 : nat64; + }; + }; + }; + send_whitelist = vec {}; + transfer_fee = opt record { + e8s = 10_000 : nat64; + }; + token_symbol = opt \"LSNS\"; + token_name = opt \"Local SNS\"; + } + }) +" +dfx canister call sns_ledger_canister account_balance '(record { account = '$(python3 -c 'print("vec{" + ";".join([str(b) for b in bytes.fromhex("'$DEFAULT_ACCOUNT_ID'")]) + "}")')'})' + +echo "Deploying Kong Backend canister..." + +dfx deploy kong_backend + +export ICP_LEDGER_CANISTER=$(dfx canister id icp_ledger_canister) +export SNS_LEDGER_CANISTER=$(dfx canister id sns_ledger_canister) +export MINTER_PRINCIPAL=$(dfx identity --identity anonymous get-principal) + +# We calculate the expected subaccount for the treasury by +# using the function "utils::compute_treasury_subaccount_bytes" +# in kongswap_adaptor/tests/common/utils.rs. +# As principal "lvfsa-2aaaa-aaaaq-aaeyq-cai" is hardcoded in the function, +# we can just run the test to get the expected subaccount bytes. +export TREASURY_SUBACCOUNT="vec{77;160;253;221;253;251;87;10;228;114;213;228;7;28;245;16;77;166;192;190;113;202;102;233;183;225;219;111;110;173;29;195}"; + +echo "Deploying SNS Kongswap Adaptor canister..." +dfx deploy sns_kongswap_adaptor + +TOKENS_TRANSFER_ACCOUNT_ID="$(dfx ledger account-id --of-canister sns_kongswap_adaptor)" +TOKENS_TRANSFER_ACCOUNT_ID_BYTES="$(python3 -c 'print("vec{" + ";".join([str(b) for b in bytes.fromhex("'$TOKENS_TRANSFER_ACCOUNT_ID'")]) + "}")')" +dfx canister call icp_ledger_canister transfer "(record { to=${TOKENS_TRANSFER_ACCOUNT_ID_BYTES}; amount=record { e8s=100_000 }; fee=record { e8s=10_000 }; memo=0:nat64; }, )" +dfx canister call sns_ledger_canister transfer "(record { to=${TOKENS_TRANSFER_ACCOUNT_ID_BYTES}; amount=record { e8s=100_000 }; fee=record { e8s=10_000 }; memo=0:nat64; }, )" + +echo "Balances of the Kongswap Adaptor canister:" +dfx canister call icp_ledger_canister account_balance '(record { account = '$TOKENS_TRANSFER_ACCOUNT_ID_BYTES'})' +dfx canister call sns_ledger_canister account_balance '(record { account = '$TOKENS_TRANSFER_ACCOUNT_ID_BYTES'})' + +dfx canister call sns_kongswap_adaptor deposit \ +'(record { + allowances = vec { + record { + asset = variant { Token = record { + ledger_fee_decimals = 10000 : nat; + ledger_canister_id = principal "'$SNS_LEDGER_CANISTER'"; + symbol = "LSNS"; + }}; + amount_decimals = 100000 : nat; + owner_account = record { + owner = principal "'$MINTER_PRINCIPAL'"; + subaccount = opt '"$TREASURY_SUBACCOUNT"'; + }; + }; + record { + asset = variant { Token = record { + ledger_fee_decimals = 10000 : nat; + ledger_canister_id = principal "'$ICP_LEDGER_CANISTER'"; + symbol = "ICP"; + }}; + amount_decimals = 100000 : nat; + owner_account = record { + owner = principal "'$MINTER_PRINCIPAL'"; + subaccount = null; + }; + }; + }; +})' + +echo "DONE" \ No newline at end of file diff --git a/rust/sns-adaptor/dfx.json b/rust/sns-adaptor/dfx.json new file mode 100644 index 000000000..9fa8ae694 --- /dev/null +++ b/rust/sns-adaptor/dfx.json @@ -0,0 +1,63 @@ +{ + "canisters": { + "sns_kongswap_adaptor": { + "candid": "sns-kongswap-adaptor/kongswap_adaptor/kongswap-adaptor.did", + "type": "custom", + "shrink": true, + "gzip": true, + "wasm": "sns-kongswap-adaptor/target/wasm32-unknown-unknown/release/kongswap-adaptor-canister.wasm", + "build": [ + "cd sns-kongswap-adaptor && cargo build --target wasm32-unknown-unknown --release -p kongswap_adaptor && cd -" + ], + "metadata": [ + { + "name": "candid:service" + } + ], + "init_arg": "(variant { Init = record { assets = vec { variant { Token = record { ledger_fee_decimals = 10000 : nat; ledger_canister_id = principal \"lvfsa-2aaaa-aaaaq-aaeyq-cai\"; symbol = \"LSNS\"; } }; variant { Token = record { ledger_fee_decimals = 10000 : nat; ledger_canister_id = principal \"ryjl3-tyaaa-aaaaa-aaaba-cai\"; symbol = \"ICP\"; } } } }})" + }, + "kong_backend": { + "type": "custom", + "candid": "https://raw.githubusercontent.com/KongSwap/kong/4bf8f99df53dbd34bef0e55ab6364d85bb31c71a/src/kong_backend/kong_backend.did", + "wasm": "https://github.com/KongSwap/kong/raw/4bf8f99df53dbd34bef0e55ab6364d85bb31c71a/wasm/kong_backend.wasm.gz", + "remote": { + "id": { + "ic": "2ipq2-uqaaa-aaaar-qailq-cai" + } + }, + "specified_id": "2ipq2-uqaaa-aaaar-qailq-cai" + }, + "icp_ledger_canister": { + "type": "custom", + "candid": "https://raw.githubusercontent.com/dfinity/ic/69b755062f5ef0a7d6efc9a127172b46121420c8/rs/ledger_suite/icp/ledger.did", + "wasm": "https://download.dfinity.systems/ic/69b755062f5ef0a7d6efc9a127172b46121420c8/canisters/ledger-canister.wasm.gz", + "remote": { + "id": { + "ic": "ryjl3-tyaaa-aaaaa-aaaba-cai" + } + }, + "specified_id": "ryjl3-tyaaa-aaaaa-aaaba-cai", + "init_arg": "(variant { Init = record { minting_account = \"1c7a48ba6a562aa9eaa2481a9049cdf0433b9738c992d698c31d8abf89cadc79\"; initial_values = vec {}; send_whitelist = vec {}; transfer_fee = opt record { e8s = 10_000 : nat64; }; token_symbol = opt \"LICP\"; token_name = opt \"Local ICP\"; } })" + }, + "sns_ledger_canister": { + "type": "custom", + "candid": "https://raw.githubusercontent.com/dfinity/ic/83923a194d39835e8a7d9549f9f0831b962a60c2/rs/ledger_suite/icp/ledger.did", + "wasm": "https://download.dfinity.systems/ic/83923a194d39835e8a7d9549f9f0831b962a60c2/canisters/ledger-canister.wasm.gz", + "remote": { + "id": { + "ic": "lvfsa-2aaaa-aaaaq-aaeyq-cai" + } + }, + "specified_id": "lvfsa-2aaaa-aaaaq-aaeyq-cai", + "init_arg": "(variant { Init = record { minting_account = \"1c7a48ba6a562aa9eaa2481a9049cdf0433b9738c992d698c31d8abf89cadc79\"; initial_values = vec {}; send_whitelist = vec {}; transfer_fee = opt record { e8s = 10_000 : nat64; }; token_symbol = opt \"LSNS\"; token_name = opt \"Local SNS\"; } })" + } + }, + "defaults": { + "build": { + "args": "", + "packtool": "" + } + }, + "output_env_file": ".env", + "version": 1 +} \ No newline at end of file diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/.github/workflows/ci.yml b/rust/sns-adaptor/sns-kongswap-adaptor/.github/workflows/ci.yml new file mode 100644 index 000000000..4e1f1eefe --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/.github/workflows/ci.yml @@ -0,0 +1,59 @@ +name: CI +on: + pull_request: + push: + branches: [ main ] + +jobs: + test-release-candidate: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@1.85.0 + with: + targets: wasm32-unknown-unknown + + # Test dependencies + - uses: actions/setup-python@v4 + with: + python-version: '3.12' + + # Build dependencies + - run: git clone --depth 1 https://github.com/dfinity/ic.git ../ic + - run: wget -q https://github.com/dfinity/ic-wasm/releases/download/0.8.0/ic-wasm-linux64 -O /usr/local/bin/ic-wasm && chmod +x /usr/local/bin/ic-wasm + + # Run all tests + - run: ./scripts/test.py --verbose + timeout-minutes: 10 + + # Build release artifacts for main branch + - name: Build release artifacts + if: github.ref == 'refs/heads/main' + run: ./scripts/build.py + + - name: Generate version + id: version + run: | + COMMIT_SHORT=$(git rev-parse --short HEAD) + TIMESTAMP=$(date +%Y%m%d-%H%M%S) + RC_VERSION="rc-${TIMESTAMP}-${COMMIT_SHORT}" + echo "version=${RC_VERSION}" >> $GITHUB_OUTPUT + echo "tag=v${RC_VERSION}" >> $GITHUB_OUTPUT + + - name: Create Release with gh CLI + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create ${{ steps.version.outputs.tag }} \ + --title "Release Candidate ${{ steps.version.outputs.version }}" \ + --notes "🚀 **Release Candidate ${{ steps.version.outputs.version }}** + + **Commit**: ${{ github.sha }} + **Branch**: ${{ github.ref_name }} + **Triggered by**: ${{ github.actor }} + + This is an automated release candidate created after all tests passed on the main branch." \ + --prerelease \ + target/wasm32-unknown-unknown/release/kongswap-adaptor-canister.wasm.gz \ + kongswap_adaptor/kongswap-adaptor.did diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/.gitignore b/rust/sns-adaptor/sns-kongswap-adaptor/.gitignore new file mode 100644 index 000000000..81cf8b3a9 --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/.gitignore @@ -0,0 +1,31 @@ +# Rust +/target/ + +# Python +__pycache__/ +*.pyc +*.pyo + +# IDE/Editor +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# Environment +.env +.env.local + +# Test dependencies +ic-artifacts + +# Internet Computer (dfx) build artifacts +.dfx diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/Cargo.lock b/rust/sns-adaptor/sns-kongswap-adaptor/Cargo.lock new file mode 100644 index 000000000..5c7c7e42c --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/Cargo.lock @@ -0,0 +1,3399 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "getrandom 0.2.16", + "instant", + "rand 0.8.5", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + +[[package]] +name = "binread" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16598dfc8e6578e9b597d9910ba2e73618385dc9f4b1d43dd92c349d6be6418f" +dependencies = [ + "binread_derive", + "lazy_static", + "rustversion", +] + +[[package]] +name = "binread_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9672209df1714ee804b1f4d4f68c8eb2a90b1f7a07acf472f88ce198ef1fed" +dependencies = [ + "either", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "candid" +version = "0.10.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaac522d18020d5fbc8320ecb12a9b13b2137ae31133da2d42fa256a825507c4" +dependencies = [ + "anyhow", + "binread", + "byteorder", + "candid_derive", + "hex", + "ic_principal", + "leb128", + "num-bigint", + "num-traits", + "paste", + "pretty", + "serde", + "serde_bytes", + "stacker", + "thiserror 1.0.69", +] + +[[package]] +name = "candid_derive" +version = "0.10.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a1b4fddbd462182050989068d53604a91a3d0f117c3c8316c6818023df00add" +dependencies = [ + "lazy_static", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "candid_parser" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48a3da76f989cd350b7342c64c6c6008341bb6186f6832ef04e56dc50ba0fd76" +dependencies = [ + "anyhow", + "candid", + "codespan-reporting", + "convert_case", + "hex", + "lalrpop", + "lalrpop-util", + "logos", + "num-bigint", + "pretty", + "thiserror 1.0.69", +] + +[[package]] +name = "cc" +version = "1.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.106", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "ena" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +dependencies = [ + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" +dependencies = [ + "serde", +] + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fragile" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.0", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "ic-canister-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb82c4f617ecff6e452fe65af0489626ec7330ffe3eedd9ea14e6178eea48d1a" +dependencies = [ + "serde", +] + +[[package]] +name = "ic-cdk" +version = "0.18.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4efb278f5d3ef033b3eed7f01f1096eaf67701896aa5ef69f5eddf5a84833dc0" +dependencies = [ + "candid", + "ic-cdk-executor", + "ic-cdk-macros 0.18.7", + "ic-error-types", + "ic-management-canister-types", + "ic0", + "serde", + "serde_bytes", + "slotmap", + "thiserror 2.0.16", +] + +[[package]] +name = "ic-cdk-executor" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99f4ee8930fd2e491177e2eb7fff53ee1c407c13b9582bdc7d6920cf83109a2d" +dependencies = [ + "ic0", + "slotmap", +] + +[[package]] +name = "ic-cdk-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a618e4020cea88e933d8d2f8c7f86d570ec06213506a80d4f2c520a9bba512" +dependencies = [ + "candid", + "proc-macro2", + "quote", + "serde", + "serde_tokenstream", + "syn 1.0.109", +] + +[[package]] +name = "ic-cdk-macros" +version = "0.18.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb14c5d691cc9d72bb95459b4761e3a4b3444b85a63d17555d5ddd782969a1e" +dependencies = [ + "candid", + "darling", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "ic-cdk-timers" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea87cf31444de833db85bbd15e97bc135ee14529b13158ffdaf6530bf6d7e85" +dependencies = [ + "candid", + "futures", + "ic-cdk", + "ic0", + "serde", + "serde_bytes", + "slotmap", +] + +[[package]] +name = "ic-certification" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb40d73f9f8273dc6569a68859003bbd467c9dc6d53c6fd7d174742f857209d" +dependencies = [ + "hex", + "serde", + "serde_bytes", + "sha2", +] + +[[package]] +name = "ic-error-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbeeb3d91aa179d6496d7293becdacedfc413c825cac79fd54ea1906f003ee55" +dependencies = [ + "serde", + "strum", + "strum_macros", +] + +[[package]] +name = "ic-management-canister-types" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea7e5b8a0f7c3b320d9450ac950547db4f24a31601b5d398f9680b64427455d2" +dependencies = [ + "candid", + "serde", + "serde_bytes", +] + +[[package]] +name = "ic-stable-structures" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d30d4cf17aff1024e13133897048bcba580e063c9000571ab766ca37e2996f4" +dependencies = [ + "ic_principal", +] + +[[package]] +name = "ic-transport-types" +version = "0.40.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2e7706e55836e8104c98149ec0796d20d5213fef972ac01b544657d410f1883" +dependencies = [ + "candid", + "hex", + "ic-certification", + "leb128", + "serde", + "serde_bytes", + "serde_cbor", + "serde_repr", + "sha2", + "thiserror 2.0.16", +] + +[[package]] +name = "ic0" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8877193e1921b5fd16accb0305eb46016868cd1935b05c05eca0ec007b943272" + +[[package]] +name = "ic_principal" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1762deb6f7c8d8c2bdee4b6c5a47b60195b74e9b5280faa5ba29692f8e17429c" +dependencies = [ + "arbitrary", + "crc32fast", + "data-encoding", + "serde", + "sha2", + "thiserror 1.0.69", +] + +[[package]] +name = "icrc-cbor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90569d2894d9536c5416943556ac6339df249f06611b3c41029196b39e0dd119" +dependencies = [ + "candid", + "minicbor", + "num-bigint", + "num-traits", +] + +[[package]] +name = "icrc-ledger-types" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87c31beeee0e5ab964861a3d5ea2b5ed7b688b2b22400367a832b1fcf0db1fa4" +dependencies = [ + "base32", + "candid", + "crc32fast", + "hex", + "ic-stable-structures", + "icrc-cbor", + "itertools 0.12.1", + "minicbor", + "num-bigint", + "num-traits", + "serde", + "serde_bytes", + "sha2", + "strum", + "strum_macros", + "time", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kongswap_adaptor" +version = "0.1.0" +dependencies = [ + "candid", + "candid_parser", + "derivative", + "ic-canister-log", + "ic-cdk", + "ic-cdk-macros 0.8.4", + "ic-cdk-timers", + "ic-management-canister-types", + "ic-stable-structures", + "icrc-ledger-types", + "itertools 0.14.0", + "lazy_static", + "maplit", + "mockall", + "pocket-ic", + "pretty_assertions", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.16", + "tokio", +] + +[[package]] +name = "lalrpop" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools 0.11.0", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax 0.8.5", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" +dependencies = [ + "regex-automata 0.4.9", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "logos" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c000ca4d908ff18ac99b93a062cb8958d331c3220719c52e77cb19cc6ac5d2c1" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc487311295e0002e452025d6b580b77bb17286de87b57138f3b5db711cded68" +dependencies = [ + "beef", + "fnv", + "proc-macro2", + "quote", + "regex-syntax 0.6.29", + "syn 2.0.106", +] + +[[package]] +name = "logos-derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfc0d229f1f42d790440136d941afd806bc9e949e2bcb8faa813b0f00d1267e" +dependencies = [ + "logos-codegen", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minicbor" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7005aaf257a59ff4de471a9d5538ec868a21586534fff7f85dd97d4043a6139" +dependencies = [ + "minicbor-derive", +] + +[[package]] +name = "minicbor-derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1154809406efdb7982841adb6311b3d095b46f78342dd646736122fe6b19e267" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "mockall" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pocket-ic" +version = "9.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e523c23bda9dc26ae989aab647b8bd805b54c72a3f2f00d668830d8b490c9c8" +dependencies = [ + "backoff", + "base64 0.13.1", + "candid", + "flate2", + "hex", + "ic-certification", + "ic-management-canister-types", + "ic-transport-types", + "reqwest", + "schemars", + "serde", + "serde_bytes", + "serde_cbor", + "serde_json", + "sha2", + "slog", + "strum", + "strum_macros", + "tempfile", + "thiserror 2.0.16", + "tokio", + "tracing", + "tracing-appender", + "tracing-subscriber", + "wslpath", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "predicates" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +dependencies = [ + "difflib", + "float-cmp", + "itertools 0.10.5", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "pretty" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac98773b7109bc75f475ab5a134c9b64b87e59d776d31098d8f346922396a477" +dependencies = [ + "arrayvec", + "typed-arena", + "unicode-width", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psm" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" +dependencies = [ + "cc", +] + +[[package]] +name = "quinn" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.5.10", + "thiserror 2.0.16", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.16", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.5.10", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.106", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "serde_tokenstream" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "797ba1d80299b264f3aac68ab5d12e5825a561749db4df7cd7c8083900c5d4e9" +dependencies = [ + "proc-macro2", + "serde", + "syn 1.0.109", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "slog" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" +dependencies = [ + "erased-serde", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stacker" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.106", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tempfile" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.60.2", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl 2.0.16", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.6.0", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "time", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec961601b32b6f5d14ae8dabd35ff2ff2e2c6cb4c0e6641845ff105abe96d958" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "wslpath" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04a2ecdf2cc4d33a6a93d71bcfbc00bb1f635cdb8029a2cc0709204a045ec7a3" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/Cargo.toml b/rust/sns-adaptor/sns-kongswap-adaptor/Cargo.toml new file mode 100644 index 000000000..f6dfda105 --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/Cargo.toml @@ -0,0 +1,25 @@ +[workspace] +resolver = "2" +members = [ + "kongswap_adaptor", +] + +[workspace.package] +authors = ["DFINITY Stiftung"] +edition = "2021" +repository = "https://github.com/dfinity/sns-kongswap-adaptor" +homepage = "https://github.com/dfinity/sns-kongswap-adaptor#readme" +license = "Apache-2.0" +version = "0.1.0" + +[workspace.dependencies] +candid = "0.10.3" +ic-cdk = "0.18.6" +ic-cdk-timers = "0.12.2" +ic-cdk-macros = "0.8" +ic-ledger-types = "0.14.0" +ic-management-canister-types = "0.3.0" +ic-stable-structures = "0.6.8" +icrc-ledger-types = "0.1.9" +serde = "1.0.188" +pocket-ic = "9.0.2" diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/LICENSE b/rust/sns-adaptor/sns-kongswap-adaptor/LICENSE new file mode 100644 index 000000000..1a0a80e99 --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 DFINITY Foundation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/README.md b/rust/sns-adaptor/sns-kongswap-adaptor/README.md new file mode 100644 index 000000000..226af66b5 --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/README.md @@ -0,0 +1,172 @@ +# SNS Kongswap Adaptor + +[SNS Kongswap Adaptor](https://github.com/ShahriarJavidi/sns-kongswap-adaptor/tree/ICP-Ninja) is a Rust-based canister designed to act as an adaptor between the Service Nervous System (SNS) treasury and the KongSwap decentralized exchange on the Internet Computer. Its primary function is to facilitate and automate the management of token assets (such as SNS and ICP tokens) held by a DAO treasury, enabling operations like deposits, withdrawals, balance tracking, and token swaps through KongSwap. The adaptor interacts with multiple canisters, including ledger canisters for different tokens and the KongSwap backend, to execute and audit these operations securely and transparently. + +The codebase is structured to ensure robust state management, transaction auditing, and error handling. It provides mechanisms to refresh ledger metadata, manage asset balances, and emit transactions with detailed logging and access control. + +Please note that this forked code is slightly modified to ease the interactions. Most drastically: + +1. when initializing the adaptor, it doesn't expect any transfers/approvals +2. transfer flow for deposits has changed from `ICRC-2` to `ICRC-1` + +## What You Can Learn + +This is example teaches you +1. how to build a wrapper around the kongswap adaptor +2. how to interact with [SNS Kongswap Adaptor](https://github.com/ShahriarJavidi/sns-kongswap-adaptor/tree/ICP-Ninja). + +## Deploying from ICP Ninja + +When viewing this project in ICP Ninja, you can deploy it directly to the mainnet for free by clicking "Run" in the upper right corner. Open this project in ICP Ninja: + +[![](https://icp.ninja/assets/open.svg)](https://icp.ninja/i?g=https://github.com/ShahriarJavidi/sns-kongswap-adaptor/tree/ICP-Ninja) + +## Local Testing with demo.sh + +The `demo.sh` script inside the adaptor's repository provides a complete local testing environment: + +## Running the Example + +```bash +cd sns-kongswap-adaptor +./demo.sh +``` + +This will: +1. Start a local IC replica +2. Deploy an SNS and an ICP Ledger +3. Deploy the in-production wasm of Kongswap (as of 9th Septemeber 2025) +4. Deploy the adaptor +5. Deposit to the adaptor + +### Prerequisites + +1. **Install dfx**: Follow [DFINITY SDK installation](https://internetcomputer.org/docs/current/developer-docs/setup/install/) +2. **Rust toolchain**: `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs/ | sh` + + + +### Inspecting Results + +After running locally, you can verify the balances: + +```bash +( + variant { + Ok = record { + timestamp_ns = 1_757_497_646_192_427_578 : nat64; + asset_to_balances = opt vec { + record { + variant { + Token = record { + ledger_fee_decimals = 10_000 : nat; + ledger_canister_id = principal "ryjl3-tyaaa-aaaaa-aaaba-cai"; + symbol = "ICP"; + } + }; + record { + treasury_owner = opt record { + name = opt "DAO Treasury"; + amount_decimals = 0 : nat; + account = opt record { + owner = principal "2vxsx-fae"; + subaccount = null; + }; + }; + suspense = opt record { + name = null; + amount_decimals = 0 : nat; + account = null; + }; + fee_collector = opt record { + name = null; + amount_decimals = 20_000 : nat; + account = null; + }; + treasury_manager = opt record { + name = opt "KongSwapAdaptor(u6s2n-gx777-77774-qaaba-cai)"; + amount_decimals = 0 : nat; + account = opt record { + owner = principal "u6s2n-gx777-77774-qaaba-cai"; + subaccount = null; + }; + }; + external_custodian = opt record { + name = null; + amount_decimals = 80_000 : nat; + account = null; + }; + payees = opt record { + name = null; + amount_decimals = 0 : nat; + account = null; + }; + payers = opt record { + name = null; + amount_decimals = 0 : nat; + account = null; + }; + }; + }; + record { + variant { + Token = record { + ledger_fee_decimals = 10_000 : nat; + ledger_canister_id = principal "lvfsa-2aaaa-aaaaq-aaeyq-cai"; + symbol = "LSNS"; + } + }; + record { + treasury_owner = opt record { + name = opt "DAO Treasury"; + amount_decimals = 0 : nat; + account = opt record { + owner = principal "2vxsx-fae"; + subaccount = opt blob "\4d\a0\fd\dd\fd\fb\57\0a\e4\72\d5\e4\07\1c\f5\10\4d\a6\c0\be\71\ca\66\e9\b7\e1\db\6f\6e\ad\1d\c3"; + }; + }; + suspense = opt record { + name = null; + amount_decimals = 0 : nat; + account = null; + }; + fee_collector = opt record { + name = null; + amount_decimals = 20_000 : nat; + account = null; + }; + treasury_manager = opt record { + name = opt "KongSwapAdaptor(u6s2n-gx777-77774-qaaba-cai)"; + amount_decimals = 0 : nat; + account = opt record { + owner = principal "u6s2n-gx777-77774-qaaba-cai"; + subaccount = null; + }; + }; + external_custodian = opt record { + name = null; + amount_decimals = 80_000 : nat; + account = null; + }; + payees = opt record { + name = null; + amount_decimals = 0 : nat; + account = null; + }; + payers = opt record { + name = null; + amount_decimals = 0 : nat; + account = null; + }; + }; + }; + }; + } + }, +) +``` + +This output shows the result of a balance query for two tokens, "ICP" and "LSNS", displaying how each token is distributed among various roles in the treasury system. For both tokens, the balances are split into categories such as `treasury_owner`, `suspense`, `fee_collector`, `treasury_manager`, `external_custodian`, payees, and payers. Most balances are 0, except for `fee_collector` (20_000) and `external_custodian` (80_000). + +The values reflect that, when transferring funds to the DEX, two actions each incur a fee of 10_000: +first, giving approval to the DEX, and second, the transfer initiated by the DEX. These fees are accouned in the `fee_collector` (totaling 20_000 per token). The `external_custodian` balance (80_000) is the amount that actually reaches the DEX after fees are deducted. Thus, the output shows the net result of a typical DEX transfer flow, with fees accounted for and the final amount available to the DEX shown under `external_custodian`. \ No newline at end of file diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/config.json b/rust/sns-adaptor/sns-kongswap-adaptor/config.json new file mode 100644 index 000000000..d7fefd5b8 --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/config.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "kong_backend": { + "version": "4bf8f99df53dbd34bef0e55ab6364d85bb31c71a", + "url_template": "https://github.com/KongSwap/kong/raw/{version}/wasm/kong_backend.wasm.gz" + } + } +} \ No newline at end of file diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/Cargo.toml b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/Cargo.toml new file mode 100644 index 000000000..4a8d4779b --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "kongswap_adaptor" +version.workspace = true +authors.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +homepage.workspace = true + +[lib] +name = "kongswap_adaptor" +path = "src/lib.rs" + +[[bin]] +name = "kongswap-adaptor-canister" +path = "src/canister.rs" + +[dependencies] +candid = { workspace = true } +ic-cdk = { workspace = true } +ic-cdk-macros = { workspace = true } +ic-cdk-timers = { workspace = true } +ic-stable-structures = { workspace = true } +lazy_static = "1.5.0" +serde = { workspace = true } +maplit = "1.0.2" +itertools = "0.14.0" +thiserror = "2.0.12" +ic-canister-log = "0.2.0" +icrc-ledger-types.workspace = true +serde_json = "1.0.140" +pretty_assertions = "1.4.1" +derivative = "2.2" + +[dev-dependencies] +pocket-ic.workspace = true +ic-management-canister-types.workspace = true +candid_parser = "0.1.2" +sha2 = "0.10.9" +tokio = { version = "1.45.1", features = ["test-util", "macros"] } +mockall = "0.11" diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/kongswap-adaptor.did b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/kongswap-adaptor.did new file mode 100644 index 000000000..2d4cf527f --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/kongswap-adaptor.did @@ -0,0 +1,294 @@ +// NOTE TO POTENTIAL CLIENTS +// ========================= +// This API is a draft, it may change in the next few weeks. Please stay tuned, and await the final +// version before implementing your canister code or tooling that relies on this API. Thank you for +// your interest! +// +// Instructions for Treasury Manager implementers +// ============================================== +// 1. An implementation of this API can be integrated into the SNS framework only if it is blessed +// by the NNS community. +// 2. Before blessing a particular implementation, the NNS community will review the implementation. +// The following requirements will be taken into account: +// - The implementation must be open source, version controlled, and publically available +// at a known location. +// - The purpose of the implementation must be clearly stated, and the implementation must +// be designed to achieve exactly that purpose. +// - Implementations that rely on external trusted components (e.g., DEXs) must attest to those +// components being reputable and trustworthy. At the very least, the external components +// should be controlled by a DAO. +// - The implementation must be well documented at least on the system design level. +// - The implementation must be well tested: there should be unit tests for all used functions, +// and there should be integration tests for both the happy scenario as well as scenarios +// that demonstrate resilience of the implementation to external compotent failures. +// - The implementation must have a public audit report / code review by at least one reputable +// third party that is not affiliated with the implementation team. It is up to the NNS +// community to decide whether a particular audit report is sufficient. These discussions +// should be driven by the developers of a Treasury Manager implementation, e.g., +// using https://forum.dfinity.org. +// 3. This API assumes that the underlying ledgers are operated in a reasonable way. In particular, +// chnaging the total supply of tokens is not taken into account. This assumption makes sense +// for ledgers that are operated by DAOs, e.g., the ICP ledger and the native ledger of the SNS +// that registers a Treasury Manager extension. However, anyone can deploy a ledger canister +// and remain its controller; as a rule of thumb, such ledgers should not be trusted by DAOs. + +// Part O. Common types +// ==================== + +// This might be different from, e.g., ICRC-2 allowances; it's just a way to specify how much +// assets are expected to be available for the manager. Whether to use ICRC-1, ICRC-2, or something +// else is an implementation detail. +type Allowance = record { + asset : Asset; + amount_decimals : nat; + + // Needed to refund excess assets that cannot be managed at this time. + owner_account : Account; +}; + +type Asset = variant { + Token : record { + ledger_fee_decimals : nat; + ledger_canister_id : principal; + symbol : text; + }; +}; + +type Account = record { + owner : principal; + subaccount : opt blob; +}; + +// Part A. Canister init / upgrade arguments +// ========================================= + +type TreasuryManagerArg = variant { + Upgrade : record {}; + Init : TreasuryManagerInit; +}; + +type TreasuryManagerInit = record { + assets : vec Asset; +}; + +// Part B. Update function requests +// ================================ + +type DepositRequest = record { + allowances : vec Allowance; +}; + +type WithdrawRequest = record { + // Maps Ledger canister IDs of assets to be withdrawn to the respective withdraw accounts. + // + // If not set, accounts specified at the time of deposit will be used for the withdrawal. + withdraw_accounts : opt vec record { principal; Account }; +}; + +// Part C. Result of a Treasury Manager's operations +// ================================================= + +type Result = variant { + // Represents current balances of all parties known to the Treasury Manager from its perspective. + // Refer to the comment above `service : (TreasuryManagerArg)` for more details. + Ok : Balances; + + // Represents all errors potentially observed during a composite operation. + Err : vec Error; +}; + +type Error = record { + code : nat64; + message : text; + kind : ErrorKind; +}; + +type ErrorKind = variant { + // Prevents the call from being attempted. + Precondition : record {}; + + // Prevents the response from being interpreted. + Postcondition : record {}; + + // An error that occurred while calling a canister. + Call : record { + method : text; + canister_id : principal; + }; + + // Backend refers to, e.g., the DEX canister that this asset manager talks to. + Backend : record {}; + + // The service is currently not available; please call back later. + TemporarilyUnavailable : record {}; + + // An exotic error that cannot be categorized using the tags above. + Generic : record { + generic_error_name : text; + }; +}; + +type Balances = record { + timestamp_ns : nat64; + asset_to_balances : opt vec record { Asset; BalanceBook }; +}; + +/// Let `k` denote a particular state, `party[k]` denote the account balance of `party` +/// in state `k`, and `managed_assets` be the sum of all assets managed on behalf of +/// the treasury owner in state `k`. +/// +/// Initial managed assets +/// ---------------------- +/// managed_assets[0] == treasury_manager[0] +/// +/// (treasury_owner[0] == external_custodian[0] == fee_collector[0] +/// == payees[0] == payers[0] == suspense[0] == 0) +/// +/// Current managed assets +/// ---------------------- +/// managed_assets[k] == treasury_manager[k] + treasury_owner[k] + external_custodian[k] +/// +/// Under "normal operations", the following invariants hold for all k > 0: +/// 1) suspense[k] == 0 +/// 2) managed_assets[k] == managed_assets[k-1] + payers[k] - payees[k] - fee_collector[k] +type BalanceBook = record { + treasury_owner : opt Balance; + treasury_manager : opt Balance; + external_custodian : opt Balance; + fee_collector : opt Balance; + payees : opt Balance; + payers : opt Balance; + + // An account in which items are entered temporarily before allocation to the correct + // or final account, e.g., due to transient errors. + suspense : opt Balance; +}; + +type Balance = record { + amount_decimals : nat; + account : opt Account; + + // A human-readable name of the party that holds this balance. + name : opt text; +}; + +// Part D. Audit trail +// =================== + +type AuditTrail = record { + transactions : vec Transaction; +}; + +// Most operations that a Treasury Manager performs are (direct or indirect) ledger transactions. +// However, for generality, any call from the Treasury Manager can be recorded in the audit trail, +// even if it is not related to any literal ledger transaction, e.g., adding a token to a DEX +// for the first time, or checking the latest ledger metadata. +type Transaction = record { + result : TransactionResult; + timestamp_ns : nat64; + purpose : text; + canister_id : principal; + treasury_manager_operation : TreasuryManagerOperation; +}; + +type TransactionResult = variant { + Ok : TransactionWitness; + Err : Error; +}; + +// Most of the time, this just points to the Ledger block index. But for generality, one can +// also use this structure for representing witnesses of non-ledger transactions, e.g., from adding +// a token to a DEX for the first time. +type TransactionWitness = variant { + // A placeholder for a transaction witness used to record a transaction attempt before + // it is completed. + Pending; + + // For financial audits. + Ledger : vec Transfer; + + // For low-level debugging. + NonLedger : text; +}; + +type Transfer = record { + block_index : nat; + amount_decimals : nat; + ledger_canister_id : text; + sender : opt Account; + receiver : opt Account; +}; + +// Example use case in the audit trail: +// +// ```candid +// transactions = vec { +// record { +// treasury_manager_operation = { +// operation = Deposit; +// step = record { +// index = 0; +// is_final = false; +// }; +// }; +// ... +// }; +// record { +// treasury_manager_operation = { +// operation = Deposit; +// step = record { +// index = 1; +// is_final = true; +// }; +// }; +// ... +// }; +// }; +type TreasuryManagerOperation = record { + operation : Operation; + step : Step; +}; + +type Operation = variant { + Withdraw; + Deposit; + IssueReward; + Balances; +}; + +type Step = record { + index : nat64; + is_final : bool; +}; + +// Parties involved in the treasury asset management process: +// 1. treasury_owner - e.g., the SNS Governance canister. +// 2. treasury_manager - this canister. +// 3. external_custodian - e.g., the DEX in which assets are held temporarily. +// 4. fee_collector - takes into account all the fees incurred due to treasury_manager's work. +// 5. payees - e.g., developer salary payments. +// 6. payers - e.g., liquidity provider rewards. +// +// Expects flow of assets: +// +// (A) Initialization / Deposit +// ============================ +// ,--------------> payees +// / +// treasury_owner ---> treasury_manager ---> external_custodian +// \ \ \ +// `----------------------`-----------------------`--------> fee_collector +// +// (B) Withdrawal +// ============== +// payers --->. +// \ +// external_custodian ---> treasury_manager ---> treasury_owner +// \ \ +// `---------------------`---------------------------> fee_collector +service : (TreasuryManagerArg) -> { + deposit : (DepositRequest) -> (Result); + withdraw : (WithdrawRequest) -> (Result); + balances : (record {}) -> (Result) query; + audit_trail : (record {}) -> (AuditTrail) query; +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/agent.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/agent.rs new file mode 100644 index 000000000..9cd15d209 --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/agent.rs @@ -0,0 +1,46 @@ +use crate::treasury_manager::TransactionWitness; +use candid::{CandidType, Principal}; +use serde::de::DeserializeOwned; +use std::{error::Error, fmt::Display, future::Future}; + +pub mod ic_cdk_agent; +pub mod icrc_requests; +pub mod mock_agent; + +use std::fmt::Debug; + +pub(crate) const EXTERNAL_CALL_TIMEOUT_SECONDS: u32 = 15 * 60; // A time out of 15 minutes for requests. + +/// This trait represents a request that can be sent to a canister. +/// It defines the method name, payload, response type, and how to extract +/// a transaction witness from the response. +pub trait Request: Send { + fn method(&self) -> &'static str; + fn payload(&self) -> Result, candid::Error>; + + type Response: CandidType + DeserializeOwned + Send; + + /// The type representing the successful response from the canister. + /// + /// Either the same, or a sub-structure of `Response`. + type Ok: CandidType + DeserializeOwned + Send; + + fn transaction_witness( + &self, + canister_id: Principal, + response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String>; +} + +/// An abstract agent that can make calls to canisters. +/// Defining this trait allows dependency injection of different agent implementations, +/// such as a mock agent for testing purposes. +pub trait AbstractAgent: Send + Sync { + type Error: Display + Send + Error + 'static; + + fn call( + &mut self, + canister_id: impl Into + Send, + request: R, + ) -> impl Future> + Send; +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/agent/ic_cdk_agent.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/agent/ic_cdk_agent.rs new file mode 100644 index 000000000..71aafe542 --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/agent/ic_cdk_agent.rs @@ -0,0 +1,45 @@ +use crate::agent::EXTERNAL_CALL_TIMEOUT_SECONDS; + +use super::{AbstractAgent, Request}; +use candid::Principal; +use ic_cdk::call::{CallFailed, CandidDecodeFailed}; +use thiserror::Error; + +pub struct CdkAgent {} + +impl CdkAgent { + pub fn new() -> Self { + CdkAgent {} + } +} + +#[derive(Error, Debug)] +pub enum CdkAgentError { + #[error(transparent)] + CallFailed(#[from] CallFailed), + #[error("canister request could not be encoded: {0}")] + CandidEncode(candid::Error), + #[error(transparent)] + CandidDecode(#[from] CandidDecodeFailed), +} + +impl AbstractAgent for CdkAgent { + type Error = CdkAgentError; + + async fn call( + &mut self, + canister_id: impl Into + Send, + request: R, + ) -> Result { + let raw_args = request.payload().map_err(CdkAgentError::CandidEncode)?; + + let call_response = ic_cdk::call::Call::bounded_wait(canister_id.into(), request.method()) + .take_raw_args(raw_args) + .change_timeout(EXTERNAL_CALL_TIMEOUT_SECONDS) + .await?; + + let result = call_response.candid::<::Response>()?; + + Ok(result) + } +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/agent/icrc_requests.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/agent/icrc_requests.rs new file mode 100644 index 000000000..17ea5435c --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/agent/icrc_requests.rs @@ -0,0 +1,169 @@ +//! This module contains implementations of the `Request` trait for some ICRC-1 and ICRC-2 +//! functions used in the KongSwapAdaptor canister. See https://github.com/dfinity/ICRC-1 + +use crate::audit::serialize_reply; + +use super::Request; +use crate::treasury_manager::{TransactionWitness, Transfer}; +use candid::{CandidType, Error, Nat, Principal}; +use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue; +use icrc_ledger_types::icrc1::account::Account; +use icrc_ledger_types::icrc1::transfer::{TransferArg, TransferError}; +use icrc_ledger_types::icrc2::approve::{ApproveArgs, ApproveError}; +use icrc_ledger_types::icrc2::transfer_from::{TransferFromArgs, TransferFromError}; +use serde::Serialize; + +impl Request for Account { + fn method(&self) -> &'static str { + "icrc1_balance_of" + } + + fn payload(&self) -> Result, Error> { + candid::encode_one(self) + } + + type Response = Nat; + + type Ok = Self::Response; + + fn transaction_witness( + &self, + _canister_id: Principal, + response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + let human_readable = format!("Balance of account {}: {}", self, response); + + Ok((TransactionWitness::NonLedger(human_readable), response)) + } +} + +impl Request for TransferArg { + fn method(&self) -> &'static str { + "icrc1_transfer" + } + + fn payload(&self) -> Result, Error> { + candid::encode_one(self) + } + + type Response = Result; + + type Ok = Nat; + + fn transaction_witness( + &self, + canister_id: Principal, + response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + let block_index = response.map_err(|err| err.to_string())?; + + let ledger_canister_id = canister_id.to_string(); + let amount_decimals = self.amount.clone(); + + let witness = TransactionWitness::Ledger(vec![Transfer { + ledger_canister_id, + amount_decimals, + block_index: block_index.clone(), + sender: None, + receiver: None, + }]); + + Ok((witness, block_index)) + } +} + +impl Request for ApproveArgs { + fn method(&self) -> &'static str { + "icrc2_approve" + } + + fn payload(&self) -> Result, Error> { + candid::encode_one(self) + } + + type Response = Result; + + type Ok = Nat; + + fn transaction_witness( + &self, + canister_id: Principal, + response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + let block_index = response.map_err(|err| err.to_string())?; + + let ledger_canister_id = canister_id.to_string(); + let amount_decimals = self.amount.clone(); + + let witness = TransactionWitness::Ledger(vec![Transfer { + ledger_canister_id, + amount_decimals, + block_index: block_index.clone(), + sender: None, + receiver: None, + }]); + + Ok((witness, block_index)) + } +} + +#[derive(CandidType, Serialize, Clone, Debug, PartialEq, Eq)] +pub struct Icrc1MetadataRequest {} + +impl Request for Icrc1MetadataRequest { + fn method(&self) -> &'static str { + "icrc1_metadata" + } + + fn payload(&self) -> Result, Error> { + candid::encode_one(()) + } + + type Response = Vec<(String, MetadataValue)>; + + type Ok = Self::Response; + + fn transaction_witness( + &self, + _canister_id: Principal, + response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + let response_str = serialize_reply(&response); + Ok((TransactionWitness::NonLedger(response_str), response)) + } +} + +impl Request for TransferFromArgs { + fn method(&self) -> &'static str { + "icrc2_transfer_from" + } + + fn payload(&self) -> Result, Error> { + candid::encode_one(self) + } + + type Response = Result; + + type Ok = Nat; + + fn transaction_witness( + &self, + canister_id: Principal, + response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + let block_index = response.map_err(|err| err.to_string())?; + + let ledger_canister_id = canister_id.to_string(); + let amount_decimals = self.amount.clone(); + + let witness = TransactionWitness::Ledger(vec![Transfer { + ledger_canister_id, + amount_decimals, + block_index: block_index.clone(), + sender: None, + receiver: None, + }]); + + Ok((witness, block_index)) + } +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/agent/mock_agent.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/agent/mock_agent.rs new file mode 100644 index 000000000..849071dd8 --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/agent/mock_agent.rs @@ -0,0 +1,130 @@ +use crate::agent::AbstractAgent; +use crate::{agent::Request, requests::CommitStateRequest}; +use candid::{CandidType, Principal}; +use std::fmt::Debug; +use std::{collections::VecDeque, error::Error, fmt::Display}; + +#[derive(Clone, Debug)] +pub struct MockError { + pub message: String, +} + +impl From for MockError { + fn from(message: String) -> Self { + MockError { message } + } +} + +impl From<&str> for MockError { + fn from(message: &str) -> Self { + MockError { + message: message.to_string(), + } + } +} + +impl Display for MockError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +impl Error for MockError {} + +struct CallSpec { + raw_request: Vec, + raw_response: Vec, + canister_id: Principal, +} + +impl CallSpec { + fn new(canister_id: Principal, request: Req, response: Req::Response) -> Result + where + Req: Request, + { + let raw_request = request.payload().expect("Request is not encodable"); + let raw_response = candid::encode_one(response).expect("Response is not encodable"); + + Ok(Self { + raw_request, + raw_response, + canister_id, + }) + } +} + +pub struct MockAgent { + expected_calls: VecDeque, + self_canister_id: Principal, +} + +impl MockAgent { + pub fn new(self_canister_id: Principal) -> Self { + Self { + self_canister_id, + expected_calls: VecDeque::default(), + } + } + + pub fn add_call( + mut self, + canister_id: Principal, + request: Req, + response: Req::Response, + ) -> Self + where + Req: Request, + { + let call = CallSpec::new(canister_id, request, response) + .expect("Creating a new call specification failed"); + self.expected_calls.push_back(call); + + let commit_state = CallSpec::new(self.self_canister_id, CommitStateRequest {}, ()) + .expect("CommittState call creation failed"); + self.expected_calls.push_back(commit_state); + self + } + + pub fn finished_calls(&self) -> bool { + self.expected_calls.is_empty() + } +} + +impl AbstractAgent for MockAgent { + type Error = MockError; + // Infallable ! + async fn call( + &mut self, + canister_id: impl Into + Send, + request: R, + ) -> Result { + println!("started call..."); + let Ok(raw_request) = request.payload() else { + panic!("Cannot encode the request"); + }; + + let expected_call = self + .expected_calls + .pop_front() + .expect("Consumed all expected requests"); + + if raw_request != expected_call.raw_request { + println!("request: {:#?}", request); + println!("{:?}\n{:?}", raw_request, expected_call.raw_request); + panic!("Request doesn't match"); + } + let canister_id: Principal = canister_id.into(); + + assert_eq!( + canister_id, expected_call.canister_id, + "observed {canister_id}, expected {}", + expected_call.canister_id + ); + + let reply = candid::decode_one::(&expected_call.raw_response) + .expect("Unable to decode the response"); + + println!("successfully called canister ID: {}", canister_id); + return Ok(reply); + } +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/audit.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/audit.rs new file mode 100644 index 000000000..9204a0980 --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/audit.rs @@ -0,0 +1,108 @@ +use crate::treasury_manager::{AuditTrail, Operation, Step, TreasuryManagerOperation}; + +pub const MAX_REPLY_SIZE_BYTES: usize = 1_024; + +pub fn serialize_audit_trail( + audit_trail: &AuditTrail, + make_pretty: bool, +) -> Result { + let result = if make_pretty { + serde_json::to_string_pretty(&audit_trail.transactions) + } else { + serde_json::to_string(&audit_trail.transactions) + }; + result.map_err(|err| format!("{err:?}")) +} + +#[must_use] +#[derive(Debug)] +pub struct OperationContext { + pub operation: Operation, + + /// None indicates that there were no calls yet. + index: Option, +} + +impl OperationContext { + pub fn new(operation: Operation) -> Self { + Self { + operation, + index: None, + } + } + + /// Should be used for operations that are definitely not the final operation + /// of the current operation. + pub fn next_operation(&mut self) -> TreasuryManagerOperation { + let operation = self.operation; + + let index = self + .index + // If index is available, increment it by 1. + .map(|index| index.saturating_add(1)) + // Otherwise, start from 0. + .unwrap_or_default(); + + self.index = Some(index); + + let step = Step { + index, + is_final: false, + }; + + TreasuryManagerOperation { operation, step } + } +} + +/// TAKEN FROM: ic/rs/nervous_system/string/src/lib.rs +/// +/// Returns a possibly modified version of `s` that fits within the specified bounds (in terms of +/// the number of UTF-8 characters). +/// +/// More precisely, middle characters are removed such that the return value has at most `max_len` +/// characters. +/// +/// This is analogous clamp method on numeric types in that this makes the value bounded. +pub fn clamp_string_len(s: &str, max_len: usize) -> String { + // Collect into a vector so that we can safely index the input. + let chars: Vec<_> = s.chars().collect(); + if max_len <= 3 { + return chars.into_iter().take(max_len).collect(); + } + + if chars.len() <= max_len { + return s.to_string(); + } + + let ellipsis = "..."; + let content_len = max_len - ellipsis.len(); + let tail_len = content_len / 2; + let head_len = content_len - tail_len; + let tail_begin = chars.len() - tail_len; + + format!( + "{}{}{}", + chars[..head_len].iter().collect::(), + ellipsis, + chars[tail_begin..].iter().collect::(), + ) +} + +fn utf8_to_ascii_lossy(s: &str) -> String { + s.chars() + .map(|c| if c.is_ascii() { c } else { '?' }) + .collect() +} + +pub fn serialize_reply(reply: &R) -> String +where + R: serde::Serialize, +{ + let Ok(json_str) = serde_json::to_string(reply) else { + return "".to_string(); + }; + + let json_str = utf8_to_ascii_lossy(&json_str); + + clamp_string_len(&json_str, MAX_REPLY_SIZE_BYTES) +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/balances.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/balances.rs new file mode 100644 index 000000000..6f8c450ef --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/balances.rs @@ -0,0 +1,537 @@ +use std::fmt::Display; + +use crate::{ + kong_types::{RemoveLiquidityAmountsArgs, RemoveLiquidityAmountsReply, UpdateTokenArgs}, + log, log_err, + logged_arithmetics::{logged_saturating_add, logged_saturating_sub}, + tx_error_codes::TransactionErrorCodes, + validation::{decode_nat_to_u64, ValidatedAsset, ValidatedBalance, ValidatedSymbol}, + KongSwapAdaptor, KONG_BACKEND_CANISTER_ID, +}; +use candid::CandidType; +use icrc_ledger_types::{icrc::generic_metadata_value::MetadataValue, icrc1::account::Account}; +use kongswap_adaptor::treasury_manager::{Error, ErrorKind}; +use kongswap_adaptor::{ + agent::{icrc_requests::Icrc1MetadataRequest, AbstractAgent}, + audit::OperationContext, +}; +use serde::Deserialize; + +#[allow(dead_code)] +/// This enumeration indicates which entity in our eco-system, +/// we are talking about. The naming Party is used to avoid confusion +/// with the term `Account`. +pub(crate) enum Party { + TreasuryOwner, + TreasuryManager, + External, + FeeCollector, + Spendings, + Earnings, +} + +impl Display for Party { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Party::TreasuryOwner => write!(f, "TreasuryOwner"), + Party::TreasuryManager => write!(f, "TreasuryManager"), + Party::External => write!(f, "External"), + Party::FeeCollector => write!(f, "FeeCollector"), + Party::Earnings => write!(f, "Earning"), + Party::Spendings => write!(f, "Spendings"), + } + } +} + +#[derive(CandidType, Deserialize, Clone)] +pub(crate) struct ValidatedBalanceBook { + pub treasury_owner: ValidatedBalance, + pub treasury_manager: ValidatedBalance, + pub external: u64, + pub fee_collector: u64, + pub spendings: u64, + pub earnings: u64, + pub suspense: u64, +} + +#[derive(CandidType, Deserialize, Clone)] +pub(crate) struct ValidatedBalances { + pub timestamp_ns: u64, + pub asset_0: ValidatedAsset, + pub asset_1: ValidatedAsset, + pub asset_0_balance: ValidatedBalanceBook, + pub asset_1_balance: ValidatedBalanceBook, +} + +impl ValidatedBalances { + pub(crate) fn new( + timestamp_ns: u64, + asset_0: ValidatedAsset, + asset_1: ValidatedAsset, + owner_account: Account, + manager_account: Account, + ) -> Self { + let amount_decimals = 0; + let external = 0; + let fee_collector = 0; + let spendings = 0; + let earnings = 0; + let suspense = 0; + + let asset_0_balance = ValidatedBalanceBook { + treasury_owner: ValidatedBalance { + amount_decimals, + account: owner_account, + }, + treasury_manager: ValidatedBalance { + amount_decimals, + account: manager_account, + }, + external, + fee_collector, + spendings, + earnings, + suspense, + }; + let asset_1_balance = ValidatedBalanceBook { + treasury_owner: ValidatedBalance { + amount_decimals, + account: owner_account, + }, + treasury_manager: ValidatedBalance { + amount_decimals, + account: manager_account, + }, + external, + fee_collector, + spendings, + earnings, + suspense, + }; + + Self { + timestamp_ns, + asset_0, + asset_1, + asset_0_balance, + asset_1_balance, + } + } + + // As the metadata of an asset, e.g., its symbol and fee, might change over time, + // calling this function would update them. + pub(crate) fn refresh_asset(&mut self, asset_id: usize, asset_new: ValidatedAsset) { + let asset = if asset_id == 0 { + &mut self.asset_0 + } else if asset_id == 1 { + &mut self.asset_1 + } else { + log_err(&format!("Invalid asset_id {}: must be 0 or 1.", asset_id)); + return; + }; + + let asset_old_info = asset.clone(); + + let ValidatedAsset::Token { + symbol: new_symbol, + ledger_canister_id: _, + ledger_fee_decimals: new_ledger_fee_decimals, + } = asset_new; + + if asset.set_symbol(new_symbol) { + log(&format!( + "Changed asset_{} symbol from `{}` to `{}`.", + asset_id, + asset_old_info.symbol(), + new_symbol, + )); + return; + } + + if asset.set_ledger_fee_decimals(new_ledger_fee_decimals) { + log(&format!( + "Changed asset_{} ledger_fee_decimals from `{}` to `{}`.", + asset_id, + asset_old_info.ledger_fee_decimals(), + new_ledger_fee_decimals, + )); + } + } + + // This function updates the distribution of balances for + // a given asset held by the external protocol. + pub(crate) fn set_external_custodian_balance(&mut self, asset: ValidatedAsset, balance: u64) { + let balance_book = if asset == self.asset_0 { + &mut self.asset_0_balance + } else if asset == self.asset_1 { + &mut self.asset_1_balance + } else { + log_err(&format!( + "Invalid asset: must be {} or {}.", + self.asset_0.symbol(), + self.asset_1.symbol() + )); + return; + }; + + balance_book.external = balance; + } + + pub(crate) fn add_manager_balance(&mut self, asset: ValidatedAsset, amount: u64) { + let balance_book = if asset == self.asset_0 { + &mut self.asset_0_balance + } else if asset == self.asset_1 { + &mut self.asset_1_balance + } else { + log_err(&format!( + "Invalid asset: must be {} or {}.", + self.asset_0.symbol(), + self.asset_1.symbol() + )); + return; + }; + + balance_book.treasury_manager.amount_decimals = + logged_saturating_add(balance_book.treasury_manager.amount_decimals, amount); + } + + pub(crate) fn move_asset( + &mut self, + asset: ValidatedAsset, + from: Party, + to: Party, + amount: u64, + ) { + let balance_book = if asset == self.asset_0 { + &mut self.asset_0_balance + } else if asset == self.asset_1 { + &mut self.asset_1_balance + } else { + log_err(&format!( + "Invalid asset: must be {} or {}.", + self.asset_0.symbol(), + self.asset_1.symbol() + )); + return; + }; + + match (&from, &to) { + (Party::External, Party::TreasuryManager) => { + balance_book.external = logged_saturating_sub(balance_book.external, amount); + balance_book.treasury_manager.amount_decimals = logged_saturating_add( + balance_book.treasury_manager.amount_decimals, + logged_saturating_sub(amount, asset.ledger_fee_decimals()), + ); + } + (Party::TreasuryManager, Party::TreasuryOwner) => { + balance_book.treasury_manager.amount_decimals = + logged_saturating_sub(balance_book.treasury_manager.amount_decimals, amount); + balance_book.treasury_owner.amount_decimals = logged_saturating_add( + balance_book.treasury_owner.amount_decimals, + logged_saturating_sub(amount, asset.ledger_fee_decimals()), + ); + } + (Party::TreasuryManager, Party::External) => { + balance_book.treasury_manager.amount_decimals = + logged_saturating_sub(balance_book.treasury_manager.amount_decimals, amount); + balance_book.external = logged_saturating_add( + balance_book.external, + logged_saturating_sub(amount, asset.ledger_fee_decimals()), + ); + } + _ => { + log_err(&format!("Invalid asset movement from {} to {}", from, to)); + } + } + + balance_book.fee_collector = + logged_saturating_add(balance_book.fee_collector, asset.ledger_fee_decimals()); + } + + pub(crate) fn charge_approval_fee(&mut self, asset: ValidatedAsset) { + let balance_book = if asset == self.asset_0 { + &mut self.asset_0_balance + } else if asset == self.asset_1 { + &mut self.asset_1_balance + } else { + log_err(&format!( + "Invalid asset: must be {} or {}.", + self.asset_0.symbol(), + self.asset_1.symbol() + )); + return; + }; + + let fee = asset.ledger_fee_decimals(); + balance_book.fee_collector = logged_saturating_add(balance_book.fee_collector, fee); + balance_book.treasury_manager.amount_decimals = + logged_saturating_sub(balance_book.treasury_manager.amount_decimals, fee); + } + + pub(crate) fn find_deposit_discrepency( + &mut self, + asset: ValidatedAsset, + balance_before: u64, + balance_after: u64, + transferred_amount: u64, + ) { + let balance_book = if asset == self.asset_0 { + &mut self.asset_0_balance + } else if asset == self.asset_1 { + &mut self.asset_1_balance + } else { + log_err(&format!( + "Invalid asset: must be {} or {}.", + self.asset_0.symbol(), + self.asset_1.symbol() + )); + return; + }; + + // On a happy deposit, the balance of the trasury manager + // should not change more than the expected amount. Otherwise, + // it means that by mistake more tokens than expected are + // transferred to the external. + let manager_balance_delta = logged_saturating_sub(balance_before, balance_after); + if manager_balance_delta > transferred_amount { + balance_book.suspense = logged_saturating_add( + balance_book.suspense, + logged_saturating_sub(manager_balance_delta, transferred_amount), + ); + } + } + + // transferred_amount is the amount withdrawn from the external. + // Which means the amount received by the manager should be: + // transferred_amount - ledger fee + pub(crate) fn find_withdraw_discrepency( + &mut self, + asset: ValidatedAsset, + balance_before: u64, + balance_after: u64, + transferred_amount: u64, + ) { + let balance_book = if asset == self.asset_0 { + &mut self.asset_0_balance + } else if asset == self.asset_1 { + &mut self.asset_1_balance + } else { + log_err(&format!( + "Invalid asset: must be {} or {}.", + self.asset_0.symbol(), + self.asset_1.symbol() + )); + return; + }; + + let manager_balance_delta = logged_saturating_sub(balance_after, balance_before); + let expected_received_amount = + logged_saturating_sub(transferred_amount, asset.ledger_fee_decimals()); + if manager_balance_delta < expected_received_amount { + balance_book.suspense = logged_saturating_add( + balance_book.suspense, + logged_saturating_sub(expected_received_amount, manager_balance_delta), + ); + } + } +} + +impl KongSwapAdaptor { + async fn refresh_ledger_metadata_impl( + &mut self, + context: &mut OperationContext, + asset_id: usize, + mut asset: ValidatedAsset, + ) -> Result { + let ledger_canister_id = asset.ledger_canister_id(); + let old_asset = asset.clone(); + + // Phase I. Tell KongSwap to refresh. + { + let human_readable = format!( + "Calling KongSwapBackend.update_token for ledger #{} ({}).", + asset_id, ledger_canister_id, + ); + + let token = format!("IC.{}", ledger_canister_id); + + let result = self + .emit_transaction( + context.next_operation(), + *KONG_BACKEND_CANISTER_ID, + UpdateTokenArgs { token }, + human_readable, + ) + .await; + + if let Err(err) = result { + log_err(&format!( + "Error while updating KongSwap token for ledger #{} ({}): {:?}", + asset_id, ledger_canister_id, err, + )); + }; + } + + // Phase II. Refresh the localy stored metadata. + let human_readable = format!( + "Refreshing metadata for ledger #{} ({}).", + asset_id, ledger_canister_id, + ); + + let reply = self + .emit_transaction( + context.next_operation(), + ledger_canister_id, + Icrc1MetadataRequest {}, + human_readable, + ) + .await?; + + // II.A. Extract and potentially update the symbol. + let new_symbol = reply.iter().find_map(|(key, value)| { + if key == "icrc1:symbol" { + Some(value.clone()) + } else { + None + } + }); + + let Some(MetadataValue::Text(new_symbol)) = new_symbol else { + return Err(Error { + code: u64::from(TransactionErrorCodes::PostConditionCode), + message: format!( + "Ledger {} icrc1_metadata response does not have an `icrc1:symbol`.", + ledger_canister_id + ), + kind: ErrorKind::Postcondition {}, + }); + }; + + match ValidatedSymbol::try_from(new_symbol) { + Ok(new_symbol) => { + asset.set_symbol(new_symbol); + } + Err(err) => { + log_err(&format!( + "Failed to validate `icrc1:symbol` ({}). Keeping the old symbol `{}`.", + err, + old_asset.symbol() + )); + } + } + + // II.B. Refresh the ledger fee. + let new_fee = reply.into_iter().find_map(|(key, value)| { + if key == "icrc1:fee" { + Some(value) + } else { + None + } + }); + + let Some(MetadataValue::Nat(new_fee)) = new_fee else { + return Err(Error { + message: format!( + "Ledger {} icrc1_metadata response does not have an `icrc1:fee`.", + ledger_canister_id + ), + kind: ErrorKind::Postcondition {}, + code: u64::from(TransactionErrorCodes::PostConditionCode), + }); + }; + + match decode_nat_to_u64(new_fee) { + Ok(new_fee_decimals) => { + asset.set_ledger_fee_decimals(new_fee_decimals); + } + Err(err) => { + log_err(&format!( + "Failed to decode `icrc1:fee` as Nat ({}). Keeping the old fee {}.", + err, + old_asset.ledger_fee_decimals() + )); + } + } + + Ok(asset) + } + + /// Refreshes the latest metadata for the managed assets, e.g., to update the symbols. + pub async fn refresh_ledger_metadata( + &mut self, + context: &mut OperationContext, + ) -> Result<(), Error> { + let (asset_0, asset_1) = self.assets(); + + let asset_0 = self + .refresh_ledger_metadata_impl(context, 0, asset_0) + .await?; + + let asset_1 = self + .refresh_ledger_metadata_impl(context, 1, asset_1) + .await?; + + self.with_balances_mut(|validated_balances| { + validated_balances.refresh_asset(0, asset_0); + validated_balances.refresh_asset(1, asset_1); + }); + + Ok(()) + } + + /// Attempts to refresh the external custodian balances for both managed assets. + pub async fn refresh_balances_impl( + &mut self, + context: &mut OperationContext, + ) -> Result<(), Error> { + let remove_lp_token_amount = self.lp_balance(context).await; + + let human_readable = format!( + "Calling KongSwapBackend.remove_liquidity_amounts to estimate how much liquidity can be removed for LP token amount {}.", + remove_lp_token_amount + ); + + let (asset_0, asset_1) = self.assets(); + + let request = RemoveLiquidityAmountsArgs { + token_0: asset_0.symbol(), + token_1: asset_1.symbol(), + remove_lp_token_amount, + }; + + let reply = self + .emit_transaction( + context.next_operation(), + *KONG_BACKEND_CANISTER_ID, + request, + human_readable, + ) + .await?; + + let RemoveLiquidityAmountsReply { + amount_0, + amount_1, + lp_fee_0, + lp_fee_1, + .. + } = reply; + + let balance_0_decimals = decode_nat_to_u64(amount_0 + lp_fee_0).map_err(|err| Error { + code: u64::from(TransactionErrorCodes::PostConditionCode), + message: err.clone(), + kind: ErrorKind::Postcondition {}, + })?; + let balance_1_decimals = decode_nat_to_u64(amount_1 + lp_fee_1).map_err(|err| Error { + code: u64::from(TransactionErrorCodes::PostConditionCode), + message: err.clone(), + kind: ErrorKind::Postcondition {}, + })?; + + self.with_balances_mut(|validated_balances| { + validated_balances.set_external_custodian_balance(asset_0, balance_0_decimals); + validated_balances.set_external_custodian_balance(asset_1, balance_1_decimals); + }); + + Ok(()) + } +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/canister.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/canister.rs new file mode 100644 index 000000000..2a5b1df9a --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/canister.rs @@ -0,0 +1,385 @@ +use crate::state::storage::{ConfigState, StableTransaction}; +use crate::validation::{ + ValidatedDepositRequest, ValidatedTreasuryManagerInit, ValidatedWithdrawRequest, +}; +use candid::Principal; +use ic_canister_log::{declare_log_buffer, log}; +use ic_cdk::{init, inspect_message, post_upgrade, pre_upgrade, query, update}; +use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory}; +use ic_stable_structures::{Cell as StableCell, DefaultMemoryImpl, Vec as StableVec}; +use kongswap_adaptor::agent::ic_cdk_agent::CdkAgent; +use kongswap_adaptor::agent::AbstractAgent; +use kongswap_adaptor::audit::OperationContext; +use kongswap_adaptor::treasury_manager::{ + AuditTrail, AuditTrailRequest, Balances, BalancesRequest, DepositRequest, Error, Operation, + TreasuryManager, TreasuryManagerArg, TreasuryManagerResult, WithdrawRequest, +}; +use lazy_static::lazy_static; +use state::KongSwapAdaptor; +use std::{cell::RefCell, time::Duration}; + +mod balances; +mod deposit; +mod emit_transaction; +mod kong_api; +mod kong_types; +mod ledger_api; +mod logged_arithmetics; +mod rewards; +mod state; +mod tx_error_codes; +mod validation; +mod withdraw; + +const RUN_PERIODIC_TASKS_INTERVAL: Duration = Duration::from_secs(60 * 60); // one hour + +pub(crate) type Memory = VirtualMemory; +pub(crate) type StableAuditTrail = StableVec; +pub(crate) type StableBalances = StableCell; + +// Canister ID from the mainnet. +// See https://dashboard.internetcomputer.org/canister/2ipq2-uqaaa-aaaar-qailq-cai +lazy_static! { + static ref KONG_BACKEND_CANISTER_ID: Principal = + Principal::from_text("2ipq2-uqaaa-aaaar-qailq-cai").unwrap(); + static ref ICP_LEDGER_CANISTER_ID: Principal = + Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap(); +} + +const BALANCES_MEMORY_ID: MemoryId = MemoryId::new(0); +const AUDIT_TRAIL_MEMORY_ID: MemoryId = MemoryId::new(1); + +thread_local! { + static MEMORY_MANAGER: RefCell> = + RefCell::new(MemoryManager::init(DefaultMemoryImpl::default())); + + static BALANCES: RefCell = + MEMORY_MANAGER.with(|memory_manager| + RefCell::new( + StableCell::init( + memory_manager.borrow().get(BALANCES_MEMORY_ID), + ConfigState::default() + ) + .expect("BALANCES init should not cause errors") + ) + ); + + static AUDIT_TRAIL: RefCell = + MEMORY_MANAGER.with(|memory_manager| + RefCell::new( + StableVec::init( + memory_manager.borrow().get(AUDIT_TRAIL_MEMORY_ID) + ) + .expect("AUDIT_TRAIL init should not cause errors") + ) + ); + +} + +fn time_ns() -> u64 { + ic_cdk::api::time() +} + +fn canister_state() -> KongSwapAdaptor { + KongSwapAdaptor::new( + time_ns, + CdkAgent::new(), + ic_cdk::api::canister_self(), + &BALANCES, + &AUDIT_TRAIL, + ) +} + +/// Ensures that only the canister itself or its controllers can call the method. +/// The canister itself is allowed to call the method, as it can be used +/// in order to commit the canister state. +fn check_access() { + let caller = ic_cdk::api::msg_caller(); + + if caller == ic_cdk::api::canister_self() { + return; + } + + if ic_cdk::api::is_controller(&caller) { + return; + } + + ic_cdk::trap("Only a controller can call this method."); +} + +// Inspect the ingress messages in the pre-consensus phase and reject unauthorized access early. +#[inspect_message] +fn inspect_message() { + let method_name = ic_cdk::api::msg_method_name(); + + // Queries can be called by anyone, even if they are called as updates. + if !["balances", "audit_trail"].contains(&method_name.as_str()) { + check_access(); + } + + ic_cdk::api::accept_message(); +} + +declare_log_buffer!(name = LOG, capacity = 100); + +fn log_err(msg: &str) { + log(&format!("Error: {}", msg)); +} + +fn log(msg: &str) { + let msg = format!("[KongSwapAdaptor] {}", msg); + + if cfg!(target_arch = "wasm32") { + ic_cdk::api::debug_print(&msg); + } else { + println!("{}", msg); + } + + log!(LOG, "{}", msg); +} + +impl TreasuryManager for KongSwapAdaptor { + /// Withdraws the specified amounts of the managed assets to the specified accounts. + /// The state lock must be held upon calling this method. + async fn withdraw(&mut self, request: WithdrawRequest) -> TreasuryManagerResult { + self.check_state_lock()?; + + // We refresh the external custodian balances, as it could + // be unknown to the treasury manager, whether an external + // trader has swapped their tokens on the pool and consequently + // changes the balances or not. + self.refresh_balances().await; + + let (ledger_0, ledger_1) = self.ledgers(); + + let (default_owner_0, default_owner_1) = self.owner_accounts(); + + let ValidatedWithdrawRequest { + withdraw_account_0, + withdraw_account_1, + } = ( + ledger_0, + ledger_1, + default_owner_0, + default_owner_1, + request, + ) + .try_into() + .map_err(|err: String| vec![Error::new_precondition(err)])?; + + let mut context = OperationContext::new(Operation::Withdraw); + + let returned_amounts = self + .withdraw_impl(&mut context, withdraw_account_0, withdraw_account_1) + .await + .map(Balances::from)?; + + self.finalize_audit_trail_transaction(context); + + Ok(returned_amounts) + } + + /// Deposits the specified amounts of the managed assets from the specified accounts. + /// The state lock must be held upon calling this method. + async fn deposit(&mut self, request: DepositRequest) -> TreasuryManagerResult { + self.check_state_lock()?; + + let ValidatedDepositRequest { + allowance_0, + allowance_1, + } = request + .try_into() + .map_err(|err: String| vec![Error::new_precondition(err)])?; + + self.validate_deposit_args(allowance_0, allowance_1) + .map_err(|err| vec![err])?; + + let mut context = OperationContext::new(Operation::Deposit); + + let deposited_amounts = self + .deposit_impl(&mut context, allowance_0, allowance_1) + .await + .map(Balances::from)?; + + self.finalize_audit_trail_transaction(context); + + Ok(deposited_amounts) + } + + fn audit_trail(&self, _request: AuditTrailRequest) -> AuditTrail { + self.get_audit_trail() + } + + fn balances(&self, _request: BalancesRequest) -> TreasuryManagerResult { + Ok(Balances::from(self.get_cached_balances())) + } + + async fn refresh_balances(&mut self) { + // This operation _can_ and _should_ be lock-free. + // + // I. It should be lock free, as periodic tasks should not block deposits and withdrawals. + // II. It can be lock free, as it does not interfere with any other operations in a bad way. + // + // This is how this operation modifies the state: + // 0. It appends to the audit_trail. + // 1. It is the sole operation that modifies the external_custodian balances. + // 2. It does not modify any other part of the state. + + let mut context = OperationContext::new(Operation::Balances); + + let result = self.refresh_balances_impl(&mut context).await; + + if let Err(err) = result { + log_err(&format!("refresh_balances failed: {:?}", err)); + } + + self.finalize_audit_trail_transaction(context); + } + + async fn issue_rewards(&mut self) { + // This operation _can_ and _should_ be lock-free. + // + // I. It should be lock free, as periodic tasks should not block deposits and withdrawals. + // (If in the future issuing rewards would change the balances, this operation should + // probably be made blocking). + // II. It can be lock free, as it does not interfere with any other operations in a bad way. + // + // This is how this operation modifies the state: + // 0. It appends to the audit_trail. + // 1. It is the sole operation that modifies the metadata of managed assets. + // 2. It also modifies the balances of the following parties: + // - (Incrementing) Treasury owner balance. + // - (Decrementing) Treasury manager balance. + // - (Increment) fee collector balance. + // The order in which these increments and decrements happen is not very important, + // and thus it is okay that this order is not enforced by the code. + + let mut context = OperationContext::new(Operation::IssueReward); + + if let Err(err) = self.refresh_ledger_metadata(&mut context).await { + log_err(&format!("Failed to refresh ledger metadata: {:?}", err)); + } + + if let Err(err) = self.issue_rewards_impl(&mut context).await { + log_err(&format!("issue_rewards failed: {:?}", err)); + } + + self.finalize_audit_trail_transaction(context); + } +} + +/// Deposits the specified amounts of the managed assets from the specified accounts. +#[update] +async fn deposit(request: DepositRequest) -> TreasuryManagerResult { + check_access(); + + log("deposit."); + + let result = canister_state().deposit(request).await?; + + Ok(result) +} + +/// Withdraws the specified amounts of the managed assets to the specified accounts. +#[update] +async fn withdraw(request: WithdrawRequest) -> TreasuryManagerResult { + check_access(); + + log("withdraw."); + + let result = canister_state().withdraw(request).await?; + + Ok(result) +} + +/// Returns the cached balances of the managed assets. +#[query] +fn balances(request: BalancesRequest) -> TreasuryManagerResult { + canister_state().balances(request) +} + +/// Returns the audit trail of operations. +#[query] +fn audit_trail(request: AuditTrailRequest) -> AuditTrail { + canister_state().audit_trail(request) +} + +async fn run_periodic_tasks() { + log("run_periodic_tasks."); + + let mut kong_adaptor = canister_state(); + + // Now + // 1. Refresh ledger metadata + // 2. Issue rewards + // 3. Refresh balances + + // Before + // 1. Refresh balances + // - Refresh ledger metadata + // 2. Issue rewards + // - Refresh balances + + kong_adaptor.issue_rewards().await; + + kong_adaptor.refresh_balances().await; +} + +// @todo init_prodic_tasks is currently disabled. Enable it once +// we make the first deposit. +fn init_periodic_tasks() { + let _new_timer_id = ic_cdk_timers::set_timer_interval(RUN_PERIODIC_TASKS_INTERVAL, || { + ic_cdk::futures::spawn(run_periodic_tasks()) + }); +} + +/// When initializing the canister, we don't expect any token transfers to have happened. +/// We soelely set up the canister state by setting the managed assets and their owners. +#[init] +async fn canister_init(arg: TreasuryManagerArg) { + log("init..."); + + let TreasuryManagerArg::Init(init) = arg else { + ic_cdk::trap("Expected TreasuryManagerArg::Init on canister install."); + }; + + let ValidatedTreasuryManagerInit { asset_0, asset_1 } = init + .try_into() + .expect("Failed to validate TreasuryManagerInit."); + + canister_state().initialize(asset_0, asset_1, ic_cdk::api::msg_caller()); +} + +#[pre_upgrade] +fn canister_pre_upgrade() { + log("pre_upgrade."); +} + +#[post_upgrade] +fn canister_post_upgrade(arg: TreasuryManagerArg) { + log("post_upgrade."); + + let TreasuryManagerArg::Upgrade(_upgrade) = arg else { + ic_cdk::trap("Expected TreasuryManagerArg::Upgrade on canister upgrade."); + }; + + init_periodic_tasks(); +} + +/// Used in order to commit the canister state, which requires an inter-canister call. +/// Otherwise, a trap could discard the state mutations, complicating recovery. +/// See: https://internetcomputer.org/docs/building-apps/security/inter-canister-calls#journaling +#[update(hidden = true)] +fn commit_state() { + check_access(); +} + +fn candid_service() -> String { + candid::export_service!(); + __export_service() +} + +fn main() { + candid::export_service!(); + println!("{}", candid_service()); +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/deposit/mod.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/deposit/mod.rs new file mode 100644 index 000000000..a12a04585 --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/deposit/mod.rs @@ -0,0 +1,399 @@ +use crate::{ + balances::{Party, ValidatedBalances}, + kong_types::{ + AddLiquidityAmountsArgs, AddLiquidityAmountsReply, AddLiquidityArgs, AddPoolArgs, + }, + log_err, + logged_arithmetics::logged_saturating_sub, + validation::{decode_nat_to_u64, ValidatedAllowance}, + KongSwapAdaptor, KONG_BACKEND_CANISTER_ID, +}; +use candid::Nat; +use icrc_ledger_types::{icrc1::account::Account, icrc2::approve::ApproveArgs}; +use kongswap_adaptor::treasury_manager::{Error, ErrorKind}; +use kongswap_adaptor::{agent::AbstractAgent, audit::OperationContext}; + +const NS_IN_SECOND: u64 = 1_000_000_000; +pub(crate) const ONE_HOUR: u64 = 60 * 60 * NS_IN_SECOND; + +impl KongSwapAdaptor { + /// Enforces that each KongSwapAdaptor instance manages a single token pair. + pub(crate) fn validate_deposit_args( + &mut self, + allowance_0: ValidatedAllowance, + allowance_1: ValidatedAllowance, + ) -> Result<(), Error> { + let new_ledger_0 = allowance_0.asset.ledger_canister_id(); + let new_ledger_1 = allowance_1.asset.ledger_canister_id(); + + let (old_asset_0, old_asset_1) = self.assets(); + + if new_ledger_0 != old_asset_0.ledger_canister_id() + || new_ledger_1 != old_asset_1.ledger_canister_id() + { + return Err(Error::new_precondition(format!( + "This KongSwapAdaptor only supports {}:{} as token_{{0,1}} (got ledger_0 {}, ledger_1 {}).", + old_asset_0.symbol(), + old_asset_1.symbol(), + new_ledger_0, + new_ledger_1, + ))); + } + + Ok(()) + } + + async fn set_dex_allowances_impl( + &mut self, + context: &mut OperationContext, + allowance: ValidatedAllowance, + ) -> Result { + let ValidatedAllowance { + asset, + amount_decimals, + owner_account: _, + } = allowance; + + let human_readable = format!( + "Calling ICRC2 approve to set KongSwapBackend as spender for {}.", + asset.symbol() + ); + + let canister_id = asset.ledger_canister_id(); + let fee_decimals = asset.ledger_fee_decimals(); + + let approved_amount_decimals = amount_decimals.saturating_sub(fee_decimals); + let amount = Nat::from(approved_amount_decimals); + + let fee_decimals = Nat::from(fee_decimals); + let fee = Some(fee_decimals.clone()); + + let request = ApproveArgs { + from_subaccount: None, + spender: Account { + owner: *KONG_BACKEND_CANISTER_ID, + subaccount: None, + }, + // All approved tokens should be fully used up before the next deposit. + amount, + expected_allowance: Some(Nat::from(0u8)), + expires_at: Some(self.time_ns().saturating_add(ONE_HOUR)), + memo: None, + created_at_time: None, + fee, + }; + + // Fail early if at least one of the allowances fails. + self.emit_transaction( + context.next_operation(), + canister_id, + request, + human_readable, + ) + .await?; + + // Charge approval fees + self.charge_fee(asset); + + Ok(approved_amount_decimals) + } + + /// Set up the allowances for the KongSwapBackend canister. + async fn set_dex_allowances( + &mut self, + context: &mut OperationContext, + allowance_0: ValidatedAllowance, + allowance_1: ValidatedAllowance, + ) -> Result<(u64, u64), Error> { + let approved_amount_decimals_0 = self.set_dex_allowances_impl(context, allowance_0).await?; + let approved_amount_decimals_1 = self.set_dex_allowances_impl(context, allowance_1).await?; + + Ok((approved_amount_decimals_0, approved_amount_decimals_1)) + } + + /// When a pool already exists, we try to add liquidity to it. + /// In case of a success, this function returns how much + /// of each asset (including the transfer fee) is moved out. + async fn topup_pool( + &mut self, + context: &mut OperationContext, + allowance_0: ValidatedAllowance, + allowance_1: ValidatedAllowance, + ) -> Result<(u64, u64), Error> { + let ledger_0 = allowance_0.asset.ledger_canister_id(); + let ledger_1 = allowance_1.asset.ledger_canister_id(); + + let amount_0 = logged_saturating_sub( + allowance_0.amount_decimals, + allowance_0.asset.ledger_fee_decimals(), + ); + + let token_0 = format!("IC.{}", ledger_0); + let token_1 = format!("IC.{}", ledger_1); + + // This is a top-up operation for a pre-existing pool. + // A top-up requires computing amount_1 as a function of amount_0. + let AddLiquidityAmountsReply { amount_1, .. } = { + let human_readable = format!( + "Calling KongSwapBackend.add_liquidity_amounts to estimate how much liquidity can \ + be added for token_1 ={} when adding token_0 = {}, amount_0 = {}.", + token_1, token_0, amount_0, + ); + let request = AddLiquidityAmountsArgs { + token_0: token_0.clone(), + amount: Nat::from(amount_0), + token_1: token_1.clone(), + }; + + self.emit_transaction( + context.next_operation(), + *KONG_BACKEND_CANISTER_ID, + request, + human_readable, + ) + .await? + }; + + let human_readable = format!( + "Calling KongSwapBackend.add_liquidity to top up liquidity for \ + token_0 = {}, amount_0 = {}, token_1 = {}, amount_1 = {}.", + token_0, amount_0, token_1, amount_1 + ); + + let request = AddLiquidityArgs { + token_0, + amount_0: Nat::from(amount_0), + token_1, + amount_1: amount_1.clone(), + + // Not needed for the ICRC2 flow. + tx_id_0: None, + tx_id_1: None, + }; + + self.emit_transaction( + context.next_operation(), + *KONG_BACKEND_CANISTER_ID, + request, + human_readable, + ) + .await?; + + let amount_1 = decode_nat_to_u64(amount_1).map_err(Error::new_postcondition)?; + + // We return the whole amount that was paid by the treasury manager: + // the transferred amount to the external + the transfer fee paid for it. + Ok(( + amount_0.saturating_add(allowance_0.asset.ledger_fee_decimals()), + amount_1.saturating_add(allowance_1.asset.ledger_fee_decimals()), + )) + } + + /// When a pool already exists, the kongswap backend + /// returns an error. The returned error can be one of + /// the following two: + /// - "LP token {lp_token} already exists" + /// - "Pool {lp_token} already exists" + fn is_pool_already_deployed_error(&self, message: &String) -> bool { + let lp_toke_symbol = self.lp_token(); + + let tolerated_errors = [ + format!("LP token {} already exists", lp_toke_symbol), + format!("Pool {} already exists", lp_toke_symbol), + ]; + + tolerated_errors.contains(message) + } + + /// Depositing into the DEX involves several steps: + /// 1. Setting the allowances for the DEX canister to spend the tokens. + /// 2. As there are no unified "add or top-up" method, we first try to add the pool. + /// 3. If the pool already exists, we top-up the pool instead. + async fn deposit_into_dex( + &mut self, + context: &mut OperationContext, + mut allowance_0: ValidatedAllowance, + mut allowance_1: ValidatedAllowance, + ) -> Result<(), Vec> { + let (approved_amount_decimals_0, approved_amount_decimals_1) = self + .set_dex_allowances(context, allowance_0, allowance_1) + .await + .map_err(|err| vec![err])?; + + // Update the allowances with the approved amounts (taking icrc2_approve fees into account). + allowance_0.amount_decimals = approved_amount_decimals_0; + allowance_1.amount_decimals = approved_amount_decimals_1; + + let balances_before = self.get_ledger_balances(context).await?; + + let result = self.add_pool(context, allowance_0, allowance_1).await; + + if let Err(Error { + kind: _, + message, + code, + }) = result + { + if self.is_pool_already_deployed_error(&message) { + // If the pool already exists, we can proceed with a top-up. The allowances + // need to be updated with the amounts that were actually used. + (allowance_0.amount_decimals, allowance_1.amount_decimals) = self + .topup_pool(context, allowance_0, allowance_1) + .await + .map_err(|err| vec![err])?; + } else { + // It corresponds to a failed transfer from call. + let balances_after = self.get_ledger_balances(context).await?; + + self.find_discrepency( + allowance_0.asset, + balances_before.0, + balances_after.0, + 0, + true, + ); + self.find_discrepency( + allowance_1.asset, + balances_before.1, + balances_after.1, + 0, + true, + ); + + log_err(&format!( + "Deposting into DEX failed with the message: {}", + message + )); + + return Err(vec![Error { + kind: ErrorKind::Backend {}, + message, + code, + }]); + } + } + + self.move_asset( + allowance_0.asset, + allowance_0.amount_decimals, + Party::TreasuryManager, + Party::External, + ); + self.move_asset( + allowance_1.asset, + allowance_1.amount_decimals, + Party::TreasuryManager, + Party::External, + ); + + let balances_after = self.get_ledger_balances(context).await?; + + self.find_discrepency( + allowance_0.asset, + balances_before.0, + balances_after.0, + allowance_0.amount_decimals, + true, + ); + self.find_discrepency( + allowance_1.asset, + balances_before.1, + balances_after.1, + allowance_1.amount_decimals, + true, + ); + + Ok(()) + } + + /// Adding a pool involves first ensuring that both tokens + /// are registered with the DEX, and then calling the add_pool + /// method on the kongswap backend canister. + /// This function requires that the allowances have already been + /// given to the DEX canister to spend the tokens. + async fn add_pool( + &mut self, + context: &mut OperationContext, + allowance_0: ValidatedAllowance, + allowance_1: ValidatedAllowance, + ) -> Result<(), Error> { + let ledger_0 = allowance_0.asset.ledger_canister_id(); + let ledger_1 = allowance_1.asset.ledger_canister_id(); + + // Adjust the amounts to take the fees into account. + let amount_0 = Nat::from(logged_saturating_sub( + allowance_0.amount_decimals, + allowance_0.asset.ledger_fee_decimals(), + )); + let amount_1 = Nat::from(logged_saturating_sub( + allowance_1.amount_decimals, + allowance_1.asset.ledger_fee_decimals(), + )); + // Step 1. Ensure the tokens are registered with the DEX. + // Notes on why we first add SNS and then ICP: + // - KongSwap starts indexing tokens from 1. + // - The ICP token is assumed to have index 2. + // https://github.com/KongSwap/kong/blob/fe-predictions-update/src/kong_lib/src/ic/icp.rs#L1 + self.maybe_add_token(context, ledger_0).await?; + self.maybe_add_token(context, ledger_1).await?; + + // Step 3. Ensure the pool exists. + + let token_0 = format!("IC.{}", ledger_0); + let token_1 = format!("IC.{}", ledger_1); + + self.emit_transaction( + context.next_operation(), + *KONG_BACKEND_CANISTER_ID, + AddPoolArgs { + token_0: token_0.clone(), + amount_0: amount_0.clone(), + token_1: token_1.clone(), + amount_1, + // Liquidity provider fee in basis points 30=0.3%. + lp_fee_bps: Some(30), + // Not needed for the ICRC2 flow. + tx_id_0: None, + tx_id_1: None, + }, + "Calling KongSwapBackend.add_pool to add a new pool.".to_string(), + ) + .await?; + + Ok(()) + } + + // For the ICP-Ninja example, we change the flow + // from icrc2_approve + icrc2_transfer_from to icrc1_transfer. + pub async fn deposit_impl( + &mut self, + context: &mut OperationContext, + allowance_0: ValidatedAllowance, + allowance_1: ValidatedAllowance, + ) -> Result> { + self.add_manager_balance(allowance_0.asset, allowance_0.amount_decimals); + self.add_manager_balance(allowance_1.asset, allowance_1.amount_decimals); + + let deposit_into_dex_result = self + .deposit_into_dex(context, allowance_0, allowance_1) + .await; + + let returned_amounts_result = self + .return_remaining_assets_to_owner( + context, + allowance_0.owner_account, + allowance_1.owner_account, + ) + .await; + + match (deposit_into_dex_result, returned_amounts_result) { + (Ok(_), Ok(_)) => Ok(self.get_cached_balances()), + (Ok(_), Err(errs)) => Err(errs), + (Err(errs), Ok(_)) => Err(errs), + (Err(mut errs), Err(errs_1)) => { + errs.extend(errs_1.into_iter()); + Err(errs) + } + } + } +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/emit_transaction.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/emit_transaction.rs new file mode 100644 index 000000000..7034f767d --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/emit_transaction.rs @@ -0,0 +1,76 @@ +use crate::{ + log_err, + state::{storage::StableTransaction, KongSwapAdaptor}, +}; +use candid::{CandidType, Principal}; +use kongswap_adaptor::agent::{AbstractAgent, Request}; +use kongswap_adaptor::requests::CommitStateRequest; +use kongswap_adaptor::treasury_manager::{Error, TransactionWitness, TreasuryManagerOperation}; +use std::fmt::Debug; + +impl KongSwapAdaptor { + /// Performs the request call and records the transaction in the audit trail. + pub(crate) async fn emit_transaction( + &mut self, + operation: TreasuryManagerOperation, + canister_id: Principal, + request: R, + human_readable: String, + ) -> Result + where + R: Request + Clone + CandidType + Debug, + { + let pending_transaction = StableTransaction { + timestamp_ns: self.time_ns(), + result: Ok(TransactionWitness::Pending), + canister_id, + human_readable, + operation, + }; + let index = self.push_audit_trail_transaction(pending_transaction.clone()); + + let call_result = self + .agent + .call(canister_id, request.clone()) + .await + .map_err(|error| { + Error::new_call(request.method().to_string(), canister_id, error.to_string()) + }); + + let (result, function_output) = match call_result { + Err(err) => (Err(err.clone()), Err(err)), + Ok(response) => { + let res = request + .transaction_witness(canister_id, response) + .map_err(|err| Error::new_backend(err.to_string())); + + match res { + Err(err) => (Err(err.clone()), Err(err)), + Ok((witness, response)) => (Ok(witness), Ok(response)), + } + } + }; + + if let Some(index) = index { + self.set_audit_trail_transaction_result( + index, + StableTransaction { + result, + ..pending_transaction + }, + ); + } + + // Self-call to ensure that the state has been committed, to prevent state roll back in case + // of a panic that occurs before the next (meaningful) async operation. This is recommended: + // https://internetcomputer.org/docs/building-apps/security/inter-canister-calls#journaling + if let Err(err) = self.agent.call(self.id, CommitStateRequest {}).await { + log_err(&format!( + "Failed to commit state after emitting transaction: {}", + err + )); + } + + function_output + } +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/kong_api.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/kong_api.rs new file mode 100644 index 000000000..ef54661df --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/kong_api.rs @@ -0,0 +1,111 @@ +use crate::{ + kong_types::{ + kong_lp_balance_to_decimals, AddTokenArgs, UserBalanceLPReply, UserBalancesArgs, + UserBalancesReply, + }, + log_err, KongSwapAdaptor, KONG_BACKEND_CANISTER_ID, +}; +use candid::{Nat, Principal}; +use kongswap_adaptor::treasury_manager::Error; +use kongswap_adaptor::{agent::AbstractAgent, audit::OperationContext}; + +impl KongSwapAdaptor { + pub fn lp_token(&self) -> String { + let (asset_0, asset_1) = self.assets(); + format!("{}_{}", asset_0.symbol(), asset_1.symbol()) + } + + /// When adding a new token to KongSwap, if the token already exists, + /// the canister returns an error. This function calls the canister + /// to add the token, and ignores the error if the token already exists. + /// For any other error, it returns the error. + pub async fn maybe_add_token( + &mut self, + context: &mut OperationContext, + ledger_canister_id: Principal, + ) -> Result<(), Error> { + let token = format!("IC.{}", ledger_canister_id); + + let human_readable = format!( + "Calling KongSwapBackend.add_token to attempt to add {}.", + token + ); + + let request = AddTokenArgs { + token: token.clone(), + }; + + let response = self + .emit_transaction( + context.next_operation(), + *KONG_BACKEND_CANISTER_ID, + request, + human_readable, + ) + .await; + + match response { + Ok(_) => Ok(()), + Err(Error { message, .. }) if message == format!("Token {} already exists", token) => { + Ok(()) + } + Err(err) => Err(err), + } + } + + /// When spinning up a pool with token pair (A, B), the LP token is named "A_B". + /// This function queries the KongSwap backend canister to get the LP token balance + /// for the LP token named after the two assets managed by this adaptor. + pub async fn lp_balance(&mut self, context: &mut OperationContext) -> Nat { + let request = UserBalancesArgs { + principal_id: self.id.to_string(), + }; + + let human_readable = + "Calling KongSwapBackend.user_balances to get LP balances.".to_string(); + + let result = self + .emit_transaction( + context.next_operation(), + *KONG_BACKEND_CANISTER_ID, + request, + human_readable, + ) + .await; + + let replies = match result { + Ok(replies) => replies, + Err(err) => { + log_err(&format!( + "Failed to call KongSwapBackend.user_balances to get LP balance for {}: {}. \ + Defaulting to 0.", + self.lp_token(), + err.message + )); + return Nat::from(0_u8); + } + }; + + let lp_balance = replies.into_iter().find_map( + |UserBalancesReply::LP(UserBalanceLPReply { + symbol, balance, .. + })| { + if symbol == self.lp_token() { + Some(kong_lp_balance_to_decimals(balance)) + } else { + None + } + }, + ); + + if let Some(lp_balance) = lp_balance { + lp_balance + } else { + log_err(&format!( + "Failed to get LP balance for {}. Defaulting to 0.", + self.lp_token(), + )); + Nat::from(0_u8) + } + } +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/kong_types.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/kong_types.rs new file mode 100644 index 000000000..cbb960528 --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/kong_types.rs @@ -0,0 +1,644 @@ +/// This module defines the request and response types for interacting with the KongSwapAdaptor canister. +use candid::{CandidType, Nat}; +use kongswap_adaptor::treasury_manager::{TransactionWitness, Transfer}; +use kongswap_adaptor::{agent::Request, audit::serialize_reply}; +use serde::{Deserialize, Serialize}; + +const E8: u64 = 100_000_000; // 10^8, used for converting LP balances to decimals + +// ----------------- begin:add_liquidity_amounts ----------------- +pub fn kong_lp_balance_to_decimals(lp_balance: f64) -> Nat { + let result_u64 = if lp_balance.is_nan() { + u64::MAX // Handle NaN by returning the maximum value, so we attempt to withdraw all. + } else { + let e8_value = E8 as f64; + let result_f64 = lp_balance * e8_value; + result_f64.clamp(0.0, u64::MAX as f64).round() as u64 + }; + + Nat::from(result_u64) +} + +#[derive(CandidType, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AddLiquidityAmountsArgs { + pub token_0: String, + pub amount: Nat, + pub token_1: String, +} + +#[derive(CandidType, Debug, Clone, Serialize, Deserialize, Default)] +pub struct AddLiquidityAmountsReply { + pub symbol: String, + pub chain_0: String, + pub address_0: String, + pub symbol_0: String, + pub amount_0: Nat, + pub fee_0: Nat, + pub chain_1: String, + pub address_1: String, + pub symbol_1: String, + pub amount_1: Nat, + pub fee_1: Nat, + pub add_lp_token_amount: Nat, +} + +impl Request for AddLiquidityAmountsArgs { + fn method(&self) -> &'static str { + "add_liquidity_amounts" + } + + fn payload(&self) -> Result, candid::Error> { + let Self { + token_0, + amount, + token_1, + } = self.clone(); + + candid::encode_args((token_0, amount, token_1)) + } + + type Response = Result; + + type Ok = AddLiquidityAmountsReply; + + fn transaction_witness( + &self, + _canister_id: candid::Principal, + response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + let reply = response?; + + let witness = TransactionWitness::NonLedger(serialize_reply(&reply)); + + Ok((witness, reply)) + } +} +// ----------------- end:add_liquidity_amounts ----------------- + +// ----------------- begin:add_liquidity ----------------- +impl Request for AddLiquidityArgs { + fn method(&self) -> &'static str { + "add_liquidity" + } + + fn payload(&self) -> Result, candid::Error> { + candid::encode_one(self) + } + + type Response = Result; + + type Ok = AddLiquidityReply; + + fn transaction_witness( + &self, + _canister_id: candid::Principal, + response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + let reply = response?; + + if reply.status != "Success" { + return Err(format!( + "Failed to add liquidity: {}", + serialize_reply(&reply) + )); + } + + let transfers = reply.transfer_ids.iter().map(Transfer::from).collect(); + + let witness = TransactionWitness::Ledger(transfers); + + Ok((witness, reply)) + } +} + +#[derive(CandidType, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum TxId { + BlockIndex(Nat), + TransactionHash(String), +} + +#[derive(CandidType, Debug, Clone, Serialize, Deserialize)] +pub struct AddLiquidityArgs { + pub token_0: String, + pub amount_0: Nat, + pub tx_id_0: Option, + pub token_1: String, + pub amount_1: Nat, + pub tx_id_1: Option, +} + +#[derive(CandidType, Debug, Clone, Serialize, Deserialize, Default)] +pub struct AddLiquidityReply { + pub tx_id: u64, + pub request_id: u64, + pub status: String, + pub symbol: String, + pub chain_0: String, + pub address_0: String, + pub symbol_0: String, + pub amount_0: Nat, + pub chain_1: String, + pub address_1: String, + pub symbol_1: String, + pub amount_1: Nat, + pub add_lp_token_amount: Nat, + pub transfer_ids: Vec, + pub claim_ids: Vec, + pub ts: u64, +} + +#[derive(CandidType, Debug, Clone, Serialize, Deserialize)] +pub struct TransferIdReply { + pub transfer_id: u64, + pub transfer: TransferReply, +} + +#[derive(CandidType, Debug, Clone, Serialize, Deserialize)] +pub enum TransferReply { + IC(ICTransferReply), +} + +#[derive(CandidType, Debug, Clone, Serialize, Deserialize)] +pub struct ICTransferReply { + pub chain: String, + pub symbol: String, + pub is_send: bool, // from user's perspective. so if is_send is true, it means the user is sending the token + pub amount: Nat, + pub canister_id: String, + pub block_index: Nat, +} +// ----------------- end:add_liquidity ----------------- + +// ----------------- begin:add_token ----------------- +impl Request for AddTokenArgs { + fn method(&self) -> &'static str { + "add_token" + } + + fn payload(&self) -> Result, candid::Error> { + candid::encode_one(self) + } + + type Response = Result; + + type Ok = AddTokenReply; + + fn transaction_witness( + &self, + _canister_id: candid::Principal, + response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + let reply = response?; + + let witness = TransactionWitness::NonLedger(serialize_reply(&reply)); + + Ok((witness, reply)) + } +} + +// Arguments for adding a token. +#[derive(CandidType, Debug, Clone, Serialize, Deserialize)] +pub struct AddTokenArgs { + pub token: String, +} + +#[derive(CandidType, Clone, Debug, Serialize, Deserialize)] +pub enum AddTokenReply { + IC(ICReply), +} + +#[derive(CandidType, Clone, Debug, Serialize, Deserialize)] +pub struct ICReply { + pub token_id: u32, + pub chain: String, + pub canister_id: String, + pub name: String, + pub symbol: String, + pub decimals: u8, + pub fee: Nat, + pub icrc1: bool, + pub icrc2: bool, + pub icrc3: bool, + pub is_removed: bool, +} +// ----------------- end:add_token ----------------- + +// ----------------- begin:update_token ----------------- +#[derive(CandidType, Debug, Clone, Serialize, Deserialize)] +pub struct UpdateTokenArgs { + pub token: String, +} + +#[derive(CandidType, Clone, Debug, Serialize, Deserialize)] +pub enum UpdateTokenReply { + IC(ICReply), +} + +impl Request for UpdateTokenArgs { + fn method(&self) -> &'static str { + "update_token" + } + + fn payload(&self) -> Result, candid::Error> { + candid::encode_one(self) + } + + type Response = Result; + + type Ok = UpdateTokenReply; + + fn transaction_witness( + &self, + _canister_id: candid::Principal, + response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + let reply = response?; + + let witness = TransactionWitness::NonLedger(serialize_reply(&reply)); + + Ok((witness, reply)) + } +} +// ----------------- end:update_token ----------------- + +// ----------------- begin:add_pool ----------------- +impl Request for AddPoolArgs { + fn method(&self) -> &'static str { + "add_pool" + } + + fn payload(&self) -> Result, candid::Error> { + candid::encode_one(self) + } + + type Response = Result; + + type Ok = AddPoolReply; + + fn transaction_witness( + &self, + _canister_id: candid::Principal, + response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + let reply = response?; + + if reply.status != "Success" { + return Err(format!("Failed to add pool: {}", serialize_reply(&reply))); + } + + let transfers = reply.transfer_ids.iter().map(Transfer::from).collect(); + + let witness = TransactionWitness::Ledger(transfers); + + Ok((witness, reply)) + } +} + +#[derive(CandidType, Debug, Clone, Serialize, Deserialize, Default)] +pub struct AddPoolReply { + pub tx_id: u64, + pub pool_id: u32, + pub request_id: u64, + pub status: String, + pub name: String, + pub symbol: String, + pub chain_0: String, + pub address_0: String, + pub symbol_0: String, + pub amount_0: Nat, // Theoretically, upon adding a new pools, amount_i and balance_i should have the exact same value + pub balance_0: Nat, + pub chain_1: String, + pub address_1: String, + pub symbol_1: String, + pub amount_1: Nat, + pub balance_1: Nat, + pub lp_fee_bps: u8, + pub lp_token_symbol: String, + pub add_lp_token_amount: Nat, + pub transfer_ids: Vec, + pub claim_ids: Vec, + pub is_removed: bool, + pub ts: u64, +} + +impl From<&TransferIdReply> for Transfer { + fn from(transfer_id_reply: &TransferIdReply) -> Self { + let TransferIdReply { + transfer_id: _, + transfer: + TransferReply::IC(ICTransferReply { + amount, + canister_id, + block_index, + .. + }), + } = transfer_id_reply; + + let ledger_canister_id = canister_id.clone(); + let amount_deimals = amount.clone(); + let block_index = block_index.clone(); + + Self { + ledger_canister_id, + amount_decimals: amount_deimals, + block_index, + sender: None, + receiver: None, + } + } +} + +#[derive(CandidType, Debug, Clone, Serialize, Deserialize)] +pub struct AddPoolArgs { + pub token_0: String, + pub amount_0: Nat, + pub tx_id_0: Option, + pub token_1: String, + pub amount_1: Nat, + pub tx_id_1: Option, + pub lp_fee_bps: Option, +} +// ----------------- end:add_pool ----------------- + +// ----------------- begin:remove_liquidity_amounts ----------------- +impl Request for RemoveLiquidityAmountsArgs { + fn method(&self) -> &'static str { + "remove_liquidity_amounts" + } + + fn payload(&self) -> Result, candid::Error> { + let Self { + token_0, + token_1, + remove_lp_token_amount, + } = self; + + candid::encode_args((token_0, token_1, remove_lp_token_amount)) + } + + type Response = Result; + + type Ok = RemoveLiquidityAmountsReply; + + fn transaction_witness( + &self, + _canister_id: candid::Principal, + response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + let reply = response?; + + let witness = TransactionWitness::NonLedger(serialize_reply(&reply)); + + Ok((witness, reply)) + } +} + +#[derive(CandidType, Clone, Debug, Serialize, Deserialize)] +pub struct RemoveLiquidityAmountsArgs { + pub token_0: String, + pub token_1: String, + pub remove_lp_token_amount: Nat, +} + +#[derive(CandidType, Clone, Debug, Serialize, Deserialize, Default)] +pub struct RemoveLiquidityAmountsReply { + pub symbol: String, + pub chain_0: String, + pub address_0: String, + pub symbol_0: String, + pub amount_0: Nat, + pub lp_fee_0: Nat, + pub chain_1: String, + pub address_1: String, + pub symbol_1: String, + pub amount_1: Nat, + pub lp_fee_1: Nat, + pub remove_lp_token_amount: Nat, +} +// ----------------- end:remove_liquidity_amounts ----------------- + +// ----------------- begin:liquidity_amounts ----------------- +impl Request for RemoveLiquidityArgs { + fn method(&self) -> &'static str { + "remove_liquidity" + } + + fn payload(&self) -> Result, candid::Error> { + candid::encode_one(self) + } + + type Response = Result; + + type Ok = RemoveLiquidityReply; + + fn transaction_witness( + &self, + _canister_id: candid::Principal, + response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + let reply = response?; + + if reply.status != "Success" { + return Err(format!( + "Failed to remove liquidity: {}", + serialize_reply(&reply) + )); + } + + let transfers = reply.transfer_ids.iter().map(Transfer::from).collect(); + + let witness = TransactionWitness::Ledger(transfers); + + Ok((witness, reply)) + } +} + +#[derive(CandidType, Debug, Clone, Serialize, Deserialize, Default)] +pub struct RemoveLiquidityReply { + pub tx_id: u64, + pub request_id: u64, + pub status: String, + pub symbol: String, + pub chain_0: String, + pub address_0: String, + pub symbol_0: String, + pub amount_0: Nat, + pub lp_fee_0: Nat, + pub chain_1: String, + pub address_1: String, + pub symbol_1: String, + pub amount_1: Nat, + pub lp_fee_1: Nat, + pub remove_lp_token_amount: Nat, + pub transfer_ids: Vec, + pub claim_ids: Vec, + pub ts: u64, +} + +#[derive(CandidType, Debug, Clone, Serialize, Deserialize)] +pub struct RemoveLiquidityArgs { + pub token_0: String, + pub token_1: String, + pub remove_lp_token_amount: Nat, +} +// ----------------- end:liquidity_amounts ----------------- + +// ----------------- begin:user_balances ----------------- +impl Request for UserBalancesArgs { + fn method(&self) -> &'static str { + "user_balances" + } + + fn payload(&self) -> Result, candid::Error> { + candid::encode_one(self.principal_id.clone()) + } + + type Response = Result, String>; + + type Ok = Vec; + + fn transaction_witness( + &self, + _canister_id: candid::Principal, + response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + let replies = response?; + + let witness = TransactionWitness::NonLedger(serialize_reply(&replies)); + + Ok((witness, replies)) + } +} + +#[derive(CandidType, Clone, Debug, Serialize, Deserialize)] +pub struct UserBalancesArgs { + pub principal_id: String, +} + +#[derive(CandidType, Clone, Debug, Serialize, Deserialize)] +pub enum UserBalancesReply { + LP(UserBalanceLPReply), +} + +#[derive(CandidType, Clone, Debug, Serialize, Deserialize, Default)] +pub struct UserBalanceLPReply { + pub symbol: String, + pub name: String, + pub lp_token_id: u64, + pub balance: f64, + pub usd_balance: f64, + pub chain_0: String, + pub symbol_0: String, + pub address_0: String, + pub amount_0: f64, + pub usd_amount_0: f64, + pub chain_1: String, + pub symbol_1: String, + pub address_1: String, + pub amount_1: f64, + pub usd_amount_1: f64, + pub ts: u64, +} + +// ----------------- end:user_balances ----------------- + +// ----------------- begin:claims ----------------- +#[derive(CandidType, Debug, Clone, Serialize, Deserialize)] +pub struct ClaimsArgs { + pub principal_id: String, +} + +#[derive(CandidType, Debug, Clone, Serialize, Deserialize, Default)] +pub struct ClaimsReply { + pub claim_id: u64, + pub status: String, + pub chain: String, + pub symbol: String, + pub canister_id: Option, + pub amount: Nat, + pub fee: Nat, + pub to_address: String, + pub desc: String, + pub ts: u64, +} + +impl Request for ClaimsArgs { + fn method(&self) -> &'static str { + "claims" + } + + fn payload(&self) -> Result, candid::Error> { + candid::encode_one(&self.principal_id) + } + + type Response = Result, String>; + + type Ok = Vec; + + fn transaction_witness( + &self, + _canister_id: candid::Principal, + response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + let replies = response?; + + let witness = TransactionWitness::NonLedger(serialize_reply(&replies)); + + Ok((witness, replies)) + } +} +// ----------------- end:claims ----------------- + +// ----------------- begin:claim ----------------- +#[derive(CandidType, Debug, Clone, Serialize, Deserialize)] +pub struct ClaimArgs { + pub claim_id: u64, +} + +#[derive(CandidType, Debug, Clone, Serialize, Deserialize, Default)] +pub struct ClaimReply { + pub claim_id: u64, + pub status: String, + pub chain: String, + pub symbol: String, + pub canister_id: Option, + pub amount: Nat, + pub fee: Nat, + pub to_address: String, + pub desc: String, + pub transfer_ids: Vec, + pub ts: u64, +} + +impl Request for ClaimArgs { + fn method(&self) -> &'static str { + "claim" + } + + fn payload(&self) -> Result, candid::Error> { + candid::encode_one(&self.claim_id) + } + + type Response = Result; + + type Ok = ClaimReply; + + fn transaction_witness( + &self, + _canister_id: candid::Principal, + response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + let reply = response?; + + if reply.status != "Success" { + return Err(format!("Failed to claim: {}", serialize_reply(&reply))); + } + + let transfers = reply.transfer_ids.iter().map(Transfer::from).collect(); + + let witness = TransactionWitness::Ledger(transfers); + + Ok((witness, reply)) + } +} +// ----------------- end:claim ----------------- diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/ledger_api.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/ledger_api.rs new file mode 100644 index 000000000..a60f153bc --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/ledger_api.rs @@ -0,0 +1,154 @@ +/// Implements functions to interact with the ledger canisters to get balances +/// and return remaining assets to the owner accounts. +use crate::{ + balances::Party, + state::KongSwapAdaptor, + tx_error_codes::TransactionErrorCodes, + validation::{decode_nat_to_u64, ValidatedAsset}, +}; +use candid::Nat; +use icrc_ledger_types::icrc1::{ + account::Account, + transfer::{Memo, TransferArg}, +}; +use kongswap_adaptor::treasury_manager::{Error, ErrorKind}; +use kongswap_adaptor::{agent::AbstractAgent, audit::OperationContext}; + +impl KongSwapAdaptor { + async fn get_ledger_balance_decimals( + &mut self, + context: &mut OperationContext, + asset: ValidatedAsset, + ) -> Result { + let ledger_canister_id = asset.ledger_canister_id(); + + let request = Account { + owner: self.id, + subaccount: None, + }; + + let human_readable = format!( + "Calling {}.icrc1_balance_of to get the remaining balance of {}.", + ledger_canister_id, + asset.symbol(), + ); + + let balance_decimals = self + .emit_transaction( + context.next_operation(), + ledger_canister_id, + request, + human_readable, + ) + .await?; + + let balance_decimals = decode_nat_to_u64(balance_decimals).map_err(|error| Error { + code: u64::from(TransactionErrorCodes::PostConditionCode), + message: error.clone(), + kind: ErrorKind::Postcondition {}, + })?; + + Ok(balance_decimals) + } + + pub(crate) async fn get_ledger_balances( + &mut self, + context: &mut OperationContext, + ) -> Result<(u64, u64), Vec> { + let (asset_0, asset_1) = self.assets(); + + // TODO: These calls could be parallelized. + let balance_0_decimals = self.get_ledger_balance_decimals(context, asset_0).await; + let balance_1_decimals = self.get_ledger_balance_decimals(context, asset_1).await; + + match (balance_0_decimals, balance_1_decimals) { + (Ok(balance_0), Ok(balance_1)) => Ok((balance_0, balance_1)), + (Err(err), Ok(_)) | (Ok(_), Err(err)) => Err(vec![err]), + (Err(err_1), Err(err_2)) => Err(vec![err_1, err_2]), + } + } + + async fn return_remaining_assets_to_owner_impl( + &mut self, + context: &mut OperationContext, + asset: ValidatedAsset, + amount_decimals: u64, + withdraw_account: Account, + ) -> Result<(), Error> { + if amount_decimals == 0 { + return Ok(()); + } + + let ledger_canister_id = asset.ledger_canister_id(); + + let human_readable = format!( + "Calling {}.icrc1_transfer to return {} {} from KongSwapAdaptor to {}.", + ledger_canister_id, + amount_decimals, + asset.symbol(), + withdraw_account, + ); + + let operation = context.next_operation(); + + let request = TransferArg { + from_subaccount: None, + to: withdraw_account, + fee: Some(Nat::from(asset.ledger_fee_decimals())), + created_at_time: Some(self.time_ns()), + memo: Some(Memo::from(Vec::::from(operation))), + amount: Nat::from(amount_decimals - asset.ledger_fee_decimals()), + }; + + self.emit_transaction(operation, ledger_canister_id, request, human_readable) + .await?; + + self.move_asset( + asset, + amount_decimals, + Party::TreasuryManager, + Party::TreasuryOwner, + ); + + Ok(()) + } + + pub(crate) async fn return_remaining_assets_to_owner( + &mut self, + context: &mut OperationContext, + withdraw_account_0: Account, + withdraw_account_1: Account, + ) -> Result<(), Vec> { + let (asset_0, asset_1) = self.assets(); + + // Take into account that the ledger fee required for returning the assets. + let (return_amount_0_decimals, return_amount_1_decimals) = + self.get_ledger_balances(context).await?; + + let mut withdraw_errors = vec![]; + + for (asset, amount_decimals, withdraw_account) in [ + (asset_0, return_amount_0_decimals, withdraw_account_0), + (asset_1, return_amount_1_decimals, withdraw_account_1), + ] { + let result = self + .return_remaining_assets_to_owner_impl( + context, + asset, + amount_decimals, + withdraw_account, + ) + .await; + + if let Err(err) = result { + withdraw_errors.push(err); + } + } + + if !withdraw_errors.is_empty() { + return Err(withdraw_errors); + } + + Ok(()) + } +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/lib.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/lib.rs new file mode 100644 index 000000000..548949e10 --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/lib.rs @@ -0,0 +1,6 @@ +pub mod agent; +pub mod audit; +pub mod requests; +pub mod treasury_manager; + +ic_cdk::export_candid!(); diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/logged_arithmetics.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/logged_arithmetics.rs new file mode 100644 index 000000000..4d5a8899f --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/logged_arithmetics.rs @@ -0,0 +1,23 @@ +/// Logged versions of saturating arithmetic operations. +/// These functions perform the operations and log an error message if an overflow or underflow occurs. +use crate::log_err; + +pub(crate) fn logged_saturating_add(a: u64, b: u64) -> u64 { + match a.checked_add(b) { + Some(sum) => sum, + None => { + log_err(&format!("saturating_add({} + {}) overflowed", a, b)); + u64::MAX + } + } +} + +pub(super) fn logged_saturating_sub(a: u64, b: u64) -> u64 { + match a.checked_sub(b) { + Some(sub) => sub, + None => { + log_err(&format!("saturating_sub({} - {}) underflowed", a, b)); + 0 + } + } +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/requests.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/requests.rs new file mode 100644 index 000000000..883523236 --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/requests.rs @@ -0,0 +1,120 @@ +/// Implementations of the Request trait for various request types used in the KongSwapAdaptor. +use crate::agent::Request; +use crate::treasury_manager::{ + AuditTrail, AuditTrailRequest, BalancesRequest, DepositRequest, TransactionWitness, + TreasuryManagerResult, WithdrawRequest, +}; +use candid::CandidType; + +impl Request for DepositRequest { + fn method(&self) -> &'static str { + "deposit" + } + + fn payload(&self) -> Result, candid::Error> { + candid::encode_one(self) + } + + type Response = TreasuryManagerResult; + + type Ok = Self::Response; + + fn transaction_witness( + &self, + _canister_id: candid::Principal, + _response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + unimplemented!() + } +} + +impl Request for WithdrawRequest { + fn method(&self) -> &'static str { + "withdraw" + } + + fn payload(&self) -> Result, candid::Error> { + candid::encode_one(self) + } + + type Response = TreasuryManagerResult; + + type Ok = Self::Response; + + fn transaction_witness( + &self, + _canister_id: candid::Principal, + _response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + unimplemented!() + } +} + +impl Request for BalancesRequest { + fn method(&self) -> &'static str { + "balances" + } + + fn payload(&self) -> Result, candid::Error> { + candid::encode_one(self) + } + + type Response = TreasuryManagerResult; + + type Ok = Self::Response; + + fn transaction_witness( + &self, + _canister_id: candid::Principal, + _response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + unimplemented!() + } +} + +impl Request for AuditTrailRequest { + fn method(&self) -> &'static str { + "audit_trail" + } + + fn payload(&self) -> Result, candid::Error> { + candid::encode_one(self) + } + + type Response = AuditTrail; + + type Ok = Self::Response; + + fn transaction_witness( + &self, + _canister_id: candid::Principal, + _response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + unimplemented!() + } +} + +#[derive(CandidType, Debug)] +pub struct CommitStateRequest {} + +impl Request for CommitStateRequest { + fn method(&self) -> &'static str { + "commit_state" + } + + fn payload(&self) -> Result, candid::Error> { + Ok(candid::encode_one(&()).unwrap()) + } + + type Response = (); + + type Ok = (); + + fn transaction_witness( + &self, + _canister_id: candid::Principal, + _response: Self::Response, + ) -> Result<(TransactionWitness, Self::Ok), String> { + Err("CommitStateRequest does not have a transaction witness".to_string()) + } +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/rewards.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/rewards.rs new file mode 100644 index 000000000..6d5760c03 --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/rewards.rs @@ -0,0 +1,20 @@ +use crate::state::KongSwapAdaptor; +use kongswap_adaptor::{agent::AbstractAgent, audit::OperationContext, treasury_manager::Error}; + +/// The current version of KongSwap (as of 19.Sep.2025) does not support +/// any dedicagted rewards mechanism. +/// Therefore, this function is a no-op that simply returns any remaining +/// assets to the owner accounts. +impl KongSwapAdaptor { + pub async fn issue_rewards_impl( + &mut self, + context: &mut OperationContext, + ) -> Result<(), Vec> { + // TODO: Ask DEX to send our rewards back. + + let (withdraw_account_0, withdraw_account_1) = self.owner_accounts(); + + self.return_remaining_assets_to_owner(context, withdraw_account_0, withdraw_account_1) + .await + } +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/state.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/state.rs new file mode 100644 index 000000000..98fb20f21 --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/state.rs @@ -0,0 +1,375 @@ +use crate::{ + balances::{Party, ValidatedBalances}, + log_err, + logged_arithmetics::{logged_saturating_add, logged_saturating_sub}, + state::storage::{ConfigState, StableTransaction}, + validation::ValidatedAsset, + StableAuditTrail, StableBalances, +}; +use candid::Principal; +use icrc_ledger_types::icrc1::account::Account; +use kongswap_adaptor::{ + agent::AbstractAgent, + audit::OperationContext, + treasury_manager::{AuditTrail, Error, Operation, Transaction, TreasuryManagerOperation}, +}; +// use sns_treasury_manager::{Error, Operation, TreasuryManagerOperation}; +use std::{cell::RefCell, thread::LocalKey}; +// use treasury_manager::{AuditTrail, Transaction}; + +pub(crate) mod storage; + +const NS_IN_SECOND: u64 = 1_000_000_000; + +pub const MAX_LOCK_DURATION_NS: u64 = 45 * 60 * NS_IN_SECOND; // 45 minutes + +pub(crate) struct KongSwapAdaptor { + time_ns: fn() -> u64, + pub agent: A, + pub id: Principal, + balances: &'static LocalKey>, + audit_trail: &'static LocalKey>, +} + +impl KongSwapAdaptor { + pub fn new( + time_ns: fn() -> u64, + agent: A, + id: Principal, + balances: &'static LocalKey>, + audit_trail: &'static LocalKey>, + ) -> Self { + KongSwapAdaptor { + time_ns, + agent, + id, + balances, + audit_trail, + } + } + + pub fn time_ns(&self) -> u64 { + (self.time_ns)() + } + + /// Initializes the canister state with the given assets and their owner accounts. + /// This should only be called once, at canister initialization. + /// If called multiple times, it will log an error and do nothing. + pub fn initialize( + &self, + asset_0: ValidatedAsset, + asset_1: ValidatedAsset, + owner_account: Principal, + ) { + self.balances.with_borrow_mut(|cell| { + if let ConfigState::Initialized(balances) = cell.get() { + log_err(&format!( + "Cannot initialize balances: already initialized at timestamp {}", + balances.timestamp_ns + )); + } + + // On each ledger, use the main account and no subaccount for managing the assets. + let manager_account = Account { + owner: self.id, + subaccount: None, + }; + + let timestamp_ns = self.time_ns(); + + let validated_balances = ValidatedBalances::new( + timestamp_ns, + asset_0, + asset_1, + owner_account.into(), + manager_account, + ); + + if let Err(err) = cell.set(ConfigState::Initialized(validated_balances)) { + log_err(&format!("Failed to initialize balances: {:?}", err)); + } + }); + } + + /// Applies a function to the mutable reference of the balances, + /// if the canister has been initialized. + pub fn with_balances_mut(&self, f: F) + where + F: FnOnce(&mut ValidatedBalances), + { + self.balances.with_borrow_mut(|cell| { + let ConfigState::Initialized(balances) = cell.get() else { + return; + }; + + let mut mutable_balances = balances.clone(); + f(&mut mutable_balances); + + if let Err(err) = cell.set(ConfigState::Initialized(mutable_balances)) { + log_err(&format!("Failed to update balances: {:?}", err)); + } + }) + } + + /// Returns a copy of the balances. + /// + /// Only safe to call after the canister has been initialized. + pub fn get_cached_balances(&self) -> ValidatedBalances { + self.balances.with_borrow(|cell| { + let ConfigState::Initialized(balances) = cell.get() else { + ic_cdk::trap("BUG: Balances should be initialized"); + }; + + balances.clone() + }) + } + + /// Returns the two managed assets. + /// The first asset is always the SNS token, and the second asset is always ICP. + /// This ordering is important for DEX operations. + pub fn assets(&self) -> (ValidatedAsset, ValidatedAsset) { + let validated_balances = self.get_cached_balances(); + (validated_balances.asset_0, validated_balances.asset_1) + } + + /// Returns the owner accounts for the two managed assets, + /// set at canister initialization. + pub fn owner_accounts(&self) -> (Account, Account) { + let validated_balances = self.get_cached_balances(); + ( + validated_balances.asset_0_balance.treasury_owner.account, + validated_balances.asset_1_balance.treasury_owner.account, + ) + } + + /// Returns the ledger canister IDs for the two managed assets. + pub fn ledgers(&self) -> (Principal, Principal) { + let balances = self.get_cached_balances(); + ( + balances.asset_0.ledger_canister_id(), + balances.asset_1.ledger_canister_id(), + ) + } + + /// Charges the approval fee for the given asset from the manager balance. + pub fn charge_fee(&mut self, asset: ValidatedAsset) { + self.with_balances_mut(|validated_balances| validated_balances.charge_approval_fee(asset)); + } + + /// Returns the asset corresponding to the given ledger canister ID, + /// or None if the canister ID does not match either managed asset. + /// As this is called for valid canister IDs only, this should never return None. + pub fn get_asset_for_ledger(&self, canister_id: &String) -> Option { + let (asset_0, asset_1) = self.assets(); + if asset_0.ledger_canister_id().to_string() == *canister_id { + Some(asset_0) + } else if asset_1.ledger_canister_id().to_string() == *canister_id { + Some(asset_1) + } else { + None + } + } + + /// It moves the given amount of the given asset from `from` to `to` in the + /// corresponding balance book. + pub fn move_asset(&mut self, asset: ValidatedAsset, amount: u64, from: Party, to: Party) { + self.with_balances_mut(|validated_balances| { + validated_balances.move_asset(asset, from, to, amount) + }); + } + + /// Adds the given amount to the manager balance of the given asset. + /// This is called when a deposit is initiated, to reflect the incoming + /// transfer to the manager account. + pub fn add_manager_balance(&mut self, asset: ValidatedAsset, amount: u64) { + self.with_balances_mut(|validated_balances| { + validated_balances.add_manager_balance(asset, amount) + }); + } + + /// Finds discrepancies in the balances after a transfer. + /// If a discrepancy is found, it is recorded in the suspense. + /// This is used to detect unexpected fees or abnormal behavior of the DEX. + pub fn find_discrepency( + &mut self, + asset: ValidatedAsset, + balance_before: u64, + balance_after: u64, + transferred_amount: u64, + is_deposit: bool, + ) { + self.with_balances_mut(|validated_balances| { + if is_deposit { + validated_balances.find_deposit_discrepency( + asset, + balance_before, + balance_after, + transferred_amount, + ); + } else { + validated_balances.find_withdraw_discrepency( + asset, + balance_before, + balance_after, + transferred_amount, + ); + } + }); + } + + /// Returns the audit trail. + fn with_audit_trail(&self, f: F) -> R + where + F: FnOnce(&StableAuditTrail) -> R, + { + self.audit_trail.with_borrow(|audit_trail| f(audit_trail)) + } + + fn with_audit_trail_mut(&self, f: F) -> R + where + F: FnOnce(&mut StableAuditTrail) -> R, + { + self.audit_trail + .with_borrow_mut(|audit_trail| f(audit_trail)) + } + + /// Returns the index of the pushed transaction in the audit trail, or None if the transaction + /// could not be pushed. + pub fn push_audit_trail_transaction(&self, transaction: StableTransaction) -> Option { + self.with_audit_trail_mut(|audit_trail| { + let index = audit_trail.len(); + if let Err(err) = audit_trail.push(&transaction) { + log_err(&format!( + "Cannot push transaction to audit trail: {}\ntransaction: {:?}", + err, transaction + )); + None + } else { + Some(index) + } + }) + } + + /// Updates the transaction at the given index in the audit trail. + pub fn set_audit_trail_transaction_result(&self, index: u64, transaction: StableTransaction) { + self.with_audit_trail_mut(|audit_trail| { + if index < audit_trail.len() { + audit_trail.set(index, &transaction); + } else { + log_err(&format!( + "BUG: Invalid index {} for audit trail. Audit trail length: {}", + index, + audit_trail.len(), + )); + } + }); + } + + /// This function finalizes the audit trail by marking the last transaction with the same + /// operation as finalized. If no such transaction exists, it logs an error. + /// If the last transaction is already finalized, it does nothing. + pub fn finalize_audit_trail_transaction(&self, context: OperationContext) { + let index_transaction = self.with_audit_trail(|audit_trail| { + let num_transactions = audit_trail.len(); + audit_trail + .iter() + .rev() + .enumerate() + .find_map(|(rev_index, transaction)| { + let transaction_operation = transaction.operation; + + if transaction_operation.operation == context.operation + && !transaction_operation.step.is_final + { + let rev_index: u64 = match rev_index.try_into() { + Ok(index) => index, + Err(err) => { + log_err(&format!( + "BUG: cannot convert usize {} to u64: {}", + rev_index, err + )); + return None; + } + }; + let index = logged_saturating_sub( + num_transactions, + logged_saturating_add(rev_index, 1), + ); + + Some((index, transaction.clone())) + } else { + None + } + }) + }); + + let Some((index, mut transaction)) = index_transaction else { + log_err(&format!( + "Audit trail does not have an {} operation that could be finalized. \ + Operation context: {:?}", + context.operation.name(), + context, + )); + return; + }; + + transaction.operation.step.is_final = true; + + self.set_audit_trail_transaction_result(index, transaction); + } + + /// It checks the balances before and after a withdrawal from the DEX, + /// and updates the internal state accordingly. + /// If there are any claim IDs returned by the DEX, it returns an error. + fn get_remaining_lock_duration_ns(&self) -> Option { + let now_ns = self.time_ns(); + + fn is_locking_transaction(treasury_manager_operation: &TreasuryManagerOperation) -> bool { + [Operation::Deposit, Operation::Withdraw] + .contains(&treasury_manager_operation.operation) + } + + let AuditTrail { transactions } = self.get_audit_trail(); + let Some(transaction) = transactions + .iter() + .rev() + .find(|transaction| is_locking_transaction(&transaction.treasury_manager_operation)) + else { + return None; + }; + + if transaction.treasury_manager_operation.step.is_final { + return None; + } + + let acquired_timestamp_ns = transaction.timestamp_ns; + let expiry_timestamp_ns = + logged_saturating_add(acquired_timestamp_ns, MAX_LOCK_DURATION_NS); + + if now_ns > expiry_timestamp_ns { + log_err(&format!("Transaction lock expired: {:?}", transaction)); + return None; + } + + Some(logged_saturating_sub(expiry_timestamp_ns, now_ns)) + } + + /// Checks if the last transaction has been finalized, or if its lock has expired. + pub fn check_state_lock(&self) -> Result<(), Vec> { + if let Some(remaining_lock_duration_ns) = self.get_remaining_lock_duration_ns() { + return Err(vec![Error::new_temporarily_unavailable(format!( + "Canister state is locked. Please try again in {} seconds.", + remaining_lock_duration_ns / NS_IN_SECOND + ))]); + } + Ok(()) + } + + pub fn get_audit_trail(&self) -> AuditTrail { + let transactions = self + .audit_trail + .with_borrow(|audit_trail| audit_trail.iter().map(Transaction::from).collect()); + + AuditTrail { transactions } + } +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/state/storage.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/state/storage.rs new file mode 100644 index 000000000..c5ab7f3c4 --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/state/storage.rs @@ -0,0 +1,84 @@ +use candid::{CandidType, Principal}; +use ic_stable_structures::{storable::Bound, Storable}; +use kongswap_adaptor::treasury_manager::{ + Error, Transaction, TransactionWitness, TreasuryManagerOperation, +}; +use serde::Deserialize; +use std::borrow::Cow; + +use crate::balances::ValidatedBalances; + +/// Configuration state of the KongSwapAdaptor canister (exclusing the `audit_trail`). +#[derive(CandidType, Default, Deserialize)] +pub(crate) enum ConfigState { + /// This state is only used between wasm module initialization and canister_init(). + #[default] + Uninitialized, + + /// Includes only `balances` from `KongSwapAdaptor`, since `audit_trail` is stored separately. + Initialized(ValidatedBalances), +} + +impl Storable for ConfigState { + fn to_bytes(&self) -> Cow<[u8]> { + Cow::Owned(candid::encode_one(self).expect("Cannot encode ConfigState")) + } + + fn from_bytes(bytes: Cow<[u8]>) -> Self { + candid::decode_one(&bytes).expect("Cannot decode ConfigState") + } + + const BOUND: Bound = Bound::Bounded { + max_size: 1024, + is_fixed_size: true, + }; +} + +#[derive(CandidType, candid::Deserialize, Clone, Debug)] +pub(crate) struct StableTransaction { + pub timestamp_ns: u64, + pub canister_id: Principal, + pub result: Result, + pub human_readable: String, + pub operation: TreasuryManagerOperation, +} + +impl Storable for StableTransaction { + fn to_bytes(&self) -> Cow<[u8]> { + Cow::Owned(candid::encode_one(self).expect("Cannot encode StableTransaction")) + } + + fn from_bytes(bytes: Cow<[u8]>) -> Self { + candid::decode_one(&bytes).expect("Cannot decode StableTransaction") + } + + const BOUND: Bound = Bound::Bounded { + // TODO: Enforce this bound. + max_size: 2048, // Increased size to accommodate all fields + is_fixed_size: false, + }; +} + +impl From for Transaction { + fn from(item: StableTransaction) -> Self { + Self { + timestamp_ns: item.timestamp_ns, + canister_id: item.canister_id, + result: item.result, + purpose: item.human_readable, + treasury_manager_operation: item.operation, + } + } +} + +impl From for StableTransaction { + fn from(item: Transaction) -> Self { + Self { + timestamp_ns: item.timestamp_ns, + canister_id: item.canister_id, + result: item.result, + human_readable: item.purpose, + operation: item.treasury_manager_operation, + } + } +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/treasury_manager.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/treasury_manager.rs new file mode 100644 index 000000000..83bfde934 --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/treasury_manager.rs @@ -0,0 +1,507 @@ +// Prevents warnings from the Derivative macro. +#![allow(clippy::needless_lifetimes)] + +use candid::{CandidType, Nat, Principal}; +use derivative::Derivative; +use serde::{Deserialize, Serialize, Serializer}; +use std::{ + collections::BTreeMap, + fmt::{self, Display}, +}; + +#[derive(CandidType, Clone, Debug, Deserialize)] +pub struct TreasuryManagerInit { + pub assets: Vec, +} + +#[derive(CandidType, Clone, Debug, Deserialize)] +pub struct TreasuryManagerUpgrade {} + +#[derive(CandidType, Clone, Debug, Deserialize)] +pub enum TreasuryManagerArg { + Init(TreasuryManagerInit), + Upgrade(TreasuryManagerUpgrade), +} + +#[derive(CandidType, Clone, Debug, Deserialize, PartialEq)] +pub struct Balance { + #[serde(serialize_with = "serialize_nat_as_u64")] + pub amount_decimals: Nat, + pub account: Option, + pub name: Option, +} + +impl Balance { + pub fn new(amount_decimals: u64, account: Option, name: Option) -> Self { + Self { + amount_decimals: Nat::from(amount_decimals), + account, + name, + } + } + + fn zero(account: Option, name: Option) -> Self { + Self::new(0, account, name) + } +} + +#[derive(CandidType, Clone, Debug, Default, Deserialize, PartialEq)] +pub struct BalanceBook { + pub treasury_owner: Option, + pub treasury_manager: Option, + pub external_custodian: Option, + pub fee_collector: Option, + pub payees: Option, + pub payers: Option, + /// An account in which items are entered temporarily before allocation to the correct + /// or final account, e.g., due to transient errors. + pub suspense: Option, +} + +impl BalanceBook { + pub fn empty() -> Self { + Self { + treasury_owner: None, + treasury_manager: None, + external_custodian: None, + fee_collector: None, + payees: None, + payers: None, + suspense: None, + } + } + + pub fn with_treasury_owner(mut self, account: Account, name: String) -> Self { + self.treasury_owner = Some(Balance::zero(Some(account), Some(name))); + self + } + + pub fn with_treasury_manager(mut self, account: Account, name: String) -> Self { + self.treasury_manager = Some(Balance::zero(Some(account), Some(name))); + self + } + + pub fn with_external_custodian( + mut self, + account: Option, + name: Option, + ) -> Self { + self.external_custodian = Some(Balance::zero(account, name)); + self + } + + pub fn with_fee_collector(mut self, account: Option, name: Option) -> Self { + self.fee_collector = Some(Balance::zero(account, name)); + self + } + + pub fn with_payees(mut self, account: Option, name: Option) -> Self { + self.payees = Some(Balance::zero(account, name)); + self + } + + pub fn with_payers(mut self, account: Option, name: Option) -> Self { + self.payers = Some(Balance::zero(account, name)); + self + } + + pub fn with_suspense(mut self, name: Option) -> Self { + self.suspense = Some(Balance::zero(None, name)); + self + } + + pub fn treasury_owner(mut self, amount_decimals: u64) -> Self { + if let Some(treasury_owner) = self.treasury_owner.as_mut() { + treasury_owner.amount_decimals = Nat::from(amount_decimals) + } + self + } + + pub fn treasury_manager(mut self, amount_decimals: u64) -> Self { + if let Some(treasury_manager) = self.treasury_manager.as_mut() { + treasury_manager.amount_decimals = Nat::from(amount_decimals) + } + self + } + + pub fn external_custodian(mut self, amount_decimals: u64) -> Self { + if let Some(external_custodian) = self.external_custodian.as_mut() { + external_custodian.amount_decimals = Nat::from(amount_decimals) + } + self + } + + pub fn fee_collector(mut self, amount_decimals: u64) -> Self { + if let Some(fee_collector) = self.fee_collector.as_mut() { + fee_collector.amount_decimals = Nat::from(amount_decimals) + } + self + } + + pub fn suspense(mut self, amount_decimals: u64) -> Self { + if let Some(suspense) = self.suspense.as_mut() { + suspense.amount_decimals = Nat::from(amount_decimals) + } + self + } +} + +#[derive(CandidType, Clone, Debug, Default, Deserialize, PartialEq)] +pub struct Balances { + pub timestamp_ns: u64, + pub asset_to_balances: Option>, +} + +pub type TreasuryManagerResult = Result>; + +#[derive(CandidType, Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct Error { + pub code: u64, + pub message: String, + pub kind: ErrorKind, +} + +fn fmt_principal_as_string( + principal: &Principal, + f: &mut std::fmt::Formatter, +) -> Result<(), std::fmt::Error> { + write!(f, "{}", principal) +} + +#[derive(CandidType, Clone, Derivative, Deserialize, PartialEq, Serialize)] +#[derivative(Debug)] +pub enum ErrorKind { + /// Prevents the call from being attempted. + Precondition {}, + + /// Prevents the response from being interpreted. + Postcondition {}, + + /// An error that occurred while calling a canister. + Call { + method: String, + #[derivative(Debug(format_with = "fmt_principal_as_string"))] + canister_id: Principal, + }, + + /// Backend refers to, e.g., the DEX canister that this asset manager talks to. + Backend {}, + + /// The service is currently not available; please call back later. + TemporarilyUnavailable {}, + + /// An exotic error that cannot be categorized using the tags above. + Generic { generic_error_name: String }, +} + +impl Error { + pub fn new_precondition(message: impl ToString) -> Self { + Self { + code: 1, + message: message.to_string(), + kind: ErrorKind::Precondition {}, + } + } + + pub fn new_postcondition(message: String) -> Self { + Self { + code: 2, + message, + kind: ErrorKind::Postcondition {}, + } + } + + pub fn new_call(method: String, canister_id: Principal, message: String) -> Self { + Self { + code: 3, + message, + kind: ErrorKind::Call { + method, + canister_id, + }, + } + } + + pub fn new_backend(message: String) -> Self { + Self { + code: 4, + message, + kind: ErrorKind::Backend {}, + } + } + + pub fn new_temporarily_unavailable(message: String) -> Self { + Self { + code: 5, + message, + kind: ErrorKind::TemporarilyUnavailable {}, + } + } + + pub fn new_generic(code: u64, generic_error_name: String, message: String) -> Self { + Self { + code, + message, + kind: ErrorKind::Generic { generic_error_name }, + } + } +} + +pub trait TreasuryManager { + /// Implements the `deposit` API function. + fn deposit( + &mut self, + request: DepositRequest, + ) -> impl std::future::Future + Send; + + /// Implements the `withdraw` API function. + fn withdraw( + &mut self, + request: WithdrawRequest, + ) -> impl std::future::Future + Send; + + /// Implements the `audit_trail` API query function. + fn audit_trail(&self, request: AuditTrailRequest) -> AuditTrail; + + /// Implements the `balances` API query function. + fn balances(&self, request: BalancesRequest) -> TreasuryManagerResult; + + // While the following methods go beyond just the Treasury Manager API agreement, they guide + // the implementers to organize the code in a reasonable and predictable way. + + /// Context: the source of truth for balances are some remote canisters (e.g., the ledgers). + /// The Treasury Manager needs to have a local cache of these balances to be able to make + /// important decisions, e.g., how much can be refunded / withdrawn. That cache should be + /// regularly updated, and this is the function that should do that. + /// + /// Should not be exposed as an API function, but rather called periodically by the canister. + fn refresh_balances(&mut self) -> impl std::future::Future + Send; + + /// Should not be exposed as an API function, but rather called periodically by the canister. + fn issue_rewards(&mut self) -> impl std::future::Future + Send; +} + +#[derive(CandidType, Clone, Debug, Deserialize, PartialEq)] +pub struct DepositRequest { + pub allowances: Vec, +} + +#[derive(CandidType, Clone, Debug, Deserialize, PartialEq)] +pub struct BalancesRequest {} + +pub type Subaccount = [u8; 32]; + +#[derive(CandidType, Clone, Copy, Derivative, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[derivative(Debug)] +pub struct Account { + #[derivative(Debug(format_with = "fmt_principal_as_string"))] + pub owner: Principal, + pub subaccount: Option, +} + +#[derive(CandidType, Clone, Debug, Deserialize, PartialEq)] +pub struct WithdrawRequest { + /// If not set, accounts specified at the time of deposit will be used for the withdrawal. + pub withdraw_accounts: Option>, +} + +#[derive(CandidType, Clone, Debug, Deserialize, PartialEq)] +pub struct AuditTrailRequest {} + +#[derive(CandidType, Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] +pub struct Step { + pub index: usize, + pub is_final: bool, +} + +impl Display for Step { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.is_final { + write!(f, "{}-fin", self.index) + } else { + write!(f, "{}", self.index) + } + } +} + +#[derive(CandidType, Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] +pub enum Operation { + Deposit, + Balances, + IssueReward, + Withdraw, +} + +impl Operation { + pub fn name(&self) -> &'static str { + match self { + Self::Deposit => "Deposit", + Self::Balances => "Balances", + Self::IssueReward => "IssueReward", + Self::Withdraw => "Withdraw", + } + } +} + +#[derive(CandidType, Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] +pub struct TreasuryManagerOperation { + pub operation: Operation, + pub step: Step, +} + +impl TreasuryManagerOperation { + pub fn new(operation: Operation) -> Self { + Self { + operation, + step: Step { + index: 0, + is_final: false, + }, + } + } + + pub fn new_final(operation: Operation) -> Self { + Self { + operation, + step: Step { + index: 0, + is_final: false, + }, + } + } + + pub fn next(&self) -> Self { + let index = self.step.index.saturating_add(1); + Self { + operation: self.operation, + step: Step { + index, + is_final: false, + }, + } + } + + pub fn next_final(&self) -> Self { + let index = self.step.index.saturating_add(1); + Self { + operation: self.operation, + step: Step { + index, + is_final: true, + }, + } + } +} + +impl Display for TreasuryManagerOperation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "TreasuryManager.{}-{}", self.operation.name(), self.step) + } +} + +/// To be used for ledger transaction memos. +impl From for Vec { + fn from(operation: TreasuryManagerOperation) -> Self { + operation.to_string().as_bytes().to_vec() + } +} + +/// Most operations that a Treasury Manager performs are (direct or indirect) ledger transactions. +/// However, for generality, any call from the Treasury Manager can be recorded in the audit trail, +/// even if it is not related to any literal ledger transaction, e.g., adding a token to a DEX +/// for the first time, or checking the latest ledger metadata. +#[derive(CandidType, Clone, Derivative, Deserialize, PartialEq, Serialize)] +#[derivative(Debug)] +pub struct Transaction { + pub timestamp_ns: u64, + + #[derivative(Debug(format_with = "fmt_principal_as_string"))] + pub canister_id: Principal, + + pub result: Result, + pub purpose: String, + + pub treasury_manager_operation: TreasuryManagerOperation, +} + +impl Display for Transaction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}-{} {} {}", + self.treasury_manager_operation, + match &self.result { + Ok(_) => "✓", + Err(_) => "✗", + }, + self.canister_id, + self.purpose, + ) + } +} + +#[derive(CandidType, Clone, Debug, Deserialize, PartialEq)] +pub struct AuditTrail { + pub transactions: Vec, +} + +#[derive(CandidType, Clone, Derivative, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derivative(Debug)] +pub enum Asset { + Token { + symbol: String, + + #[derivative(Debug(format_with = "fmt_principal_as_string"))] + ledger_canister_id: Principal, + + #[serde(serialize_with = "serialize_nat_as_u64")] + ledger_fee_decimals: Nat, + }, +} + +#[derive(CandidType, Clone, Debug, PartialEq, Eq, Hash, Deserialize)] +pub struct Allowance { + pub asset: Asset, + + /// Total amount that may be consumed, including the fees. + #[serde(serialize_with = "serialize_nat_as_u64")] + pub amount_decimals: Nat, + + /// The owner account is used to return the leftover assets and issue rewards. + pub owner_account: Account, +} + +#[derive(CandidType, Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct Transfer { + pub ledger_canister_id: String, + #[serde(serialize_with = "serialize_nat_as_u64")] + pub amount_decimals: Nat, + #[serde(serialize_with = "serialize_nat_as_u64")] + pub block_index: Nat, + + pub sender: Option, + pub receiver: Option, +} + +/// Most of the time, this just points to the Ledger block index. But for generality, one can +/// also use this structure for representing witnesses of non-ledger transactions, e.g., from adding +/// a token to a DEX for the first time. +#[derive(CandidType, Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum TransactionWitness { + Pending, + + Ledger(Vec), + + /// Represents a transaction that is not related to the ledger, e.g., DEX operations. + /// The argument is a (best-effort) JSON encoding of the response (for human inspection). + NonLedger(String), +} + +fn serialize_nat_as_u64(nat: &Nat, serializer: S) -> Result +where + S: Serializer, +{ + // Convert Nat to u64 for JSON serialization + let value: String = nat.to_string(); + serializer.serialize_str(&value) +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/tx_error_codes.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/tx_error_codes.rs new file mode 100644 index 000000000..b13346f6d --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/tx_error_codes.rs @@ -0,0 +1,22 @@ +#[allow(dead_code)] +pub(crate) enum TransactionErrorCodes { + PreConditionCode, + PostConditionCode, + CallFailedCode, + BackendCode, + TemporaryUnavailableCode, + GenericErrorCode, +} + +impl From for u64 { + fn from(value: TransactionErrorCodes) -> Self { + match value { + TransactionErrorCodes::PreConditionCode => 0, + TransactionErrorCodes::PostConditionCode => 1, + TransactionErrorCodes::CallFailedCode => 2, + TransactionErrorCodes::BackendCode => 3, + TransactionErrorCodes::TemporaryUnavailableCode => 4, + TransactionErrorCodes::GenericErrorCode => 5, + } + } +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/validation.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/validation.rs new file mode 100644 index 000000000..9e8c0373b --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/validation.rs @@ -0,0 +1,662 @@ +use crate::{ + balances::{ValidatedBalanceBook, ValidatedBalances}, + ICP_LEDGER_CANISTER_ID, +}; +use candid::{CandidType, Nat, Principal}; +use icrc_ledger_types::icrc1::account::Account; +use kongswap_adaptor::treasury_manager::{ + Allowance, Asset, Balance, BalanceBook, Balances, DepositRequest, TreasuryManagerInit, + WithdrawRequest, +}; +use maplit::btreemap; +use serde::Deserialize; +use std::str::FromStr; + +pub const MAX_SYMBOL_BYTES: usize = 10; + +pub(crate) struct ValidatedTreasuryManagerInit { + pub asset_0: ValidatedAsset, + pub asset_1: ValidatedAsset, +} + +/// This function validates that the provided assets are suitable for use with the KongSwapAdaptor. +/// This is done by checking that: +/// 1. Exactly two assets are provided. +/// 2. One of the assets represents ICP tokens (asset_1). +/// 3. The other asset dis our SNS token (asset_0). +/// 4. The ledger_canister_id of the ICP asset is the ICP ledger canister ID. +/// 5. The ledger_canister_id of the SNS token asset is NOT the ICP ledger canister ID. +pub(crate) fn validate_assets( + mut assets: Vec, +) -> Result<(ValidatedAsset, ValidatedAsset), String> { + let mut problems = vec![]; + + let form_error = |err: &str| -> Result<(ValidatedAsset, ValidatedAsset), String> { + Err(format!("Invalid assets: {}", err)) + }; + + let Some(Asset::Token { + symbol: symbol_1, + ledger_canister_id: ledger_canister_id_1, + ledger_fee_decimals: ledger_fee_decimals_1, + }) = assets.pop() + else { + return form_error("KongSwapAdaptor requires some assets."); + }; + + let Some(Asset::Token { + symbol: symbol_0, + ledger_canister_id: ledger_canister_id_0, + ledger_fee_decimals: ledger_fee_decimals_0, + }) = assets.pop() + else { + return form_error(&format!( + "KongSwapAdaptor requires two assets (got {}).", + assets.len() + )); + }; + + if !assets.is_empty() { + problems.push(format!( + "KongSwapAdaptor requires exactly two assets (got {}).", + assets.len() + )); + } + + if symbol_0 == "ICP" { + problems.push("asset_0 must NOT represent ICP tokens.".to_string()); + } + + if symbol_1 != "ICP" { + problems.push("asset_1 must represent ICP tokens.".to_string()); + } + + if ledger_canister_id_0 == *ICP_LEDGER_CANISTER_ID { + problems.push("asset_0 ledger must NOT be the ICP ledger.".to_string()); + } + + if ledger_canister_id_1 != *ICP_LEDGER_CANISTER_ID { + problems.push("asset_1 ledger must be the ICP ledger.".to_string()); + } + + if !problems.is_empty() { + return form_error(&format!("\n - {}", problems.join(" - \n"))); + } + + let asset_0 = ValidatedAsset::try_from(Asset::Token { + symbol: symbol_0, + ledger_canister_id: ledger_canister_id_0, + ledger_fee_decimals: ledger_fee_decimals_0, + }) + .map_err(|err| format!("Failed to validate asset_0: {}", err))?; + + let asset_1 = ValidatedAsset::try_from(Asset::Token { + symbol: symbol_1, + ledger_canister_id: ledger_canister_id_1, + ledger_fee_decimals: ledger_fee_decimals_1, + }) + .map_err(|err| format!("Failed to validate asset_1: {}", err))?; + + Ok((asset_0, asset_1)) +} + +/// Validates that exactly two allowances are provided, and that their assets are valid. +/// Returns the validated allowances if successful, or an error message if validation fails. +/// An allowance is valid if: +/// 1. Its asset is valid (see `ValidatedAsset`). +/// 2. Its amount_decimals can be converted to u64. +/// 3. Its owner_account is valid (see `account_into_icrc1_account`). +pub(crate) fn validate_allowances( + mut allowances: Vec, +) -> Result<(ValidatedAllowance, ValidatedAllowance), String> { + let Some(allowance_1) = allowances.pop() else { + return Err("KongSwapAdaptor requires some allowances.".to_string()); + }; + + let Some(allowance_0) = allowances.pop() else { + return Err(format!( + "KongSwapAdaptor requires two allowances (got {}).", + allowances.len() + )); + }; + + let mut problems = vec![]; + + if !allowances.is_empty() { + problems.push(format!( + "KongSwapAdaptor requires exactly two allowances (got {}).", + allowances.len() + )); + } + + let allowance_0 = ValidatedAllowance::try_from(allowance_0) + .map_err(|err| format!("Failed to validate allowance_0: {}", err))?; + + let allowance_1 = ValidatedAllowance::try_from(allowance_1) + .map_err(|err| format!("Failed to validate allowance_1: {}", err))?; + + if !problems.is_empty() { + let problems = problems.join(" - \n"); + return Err(format!("Invalid allowances:\n - {}", problems)); + } + + Ok((allowance_0, allowance_1)) +} + +impl TryFrom for ValidatedAllowance { + type Error = String; + + fn try_from(allowance: Allowance) -> Result { + let Allowance { + asset, + amount_decimals, + owner_account, + } = allowance; + + let mut problems = vec![]; + + let asset = match ValidatedAsset::try_from(asset) { + Ok(asset) => Some(asset), + Err(err) => { + problems.push(err); + None + } + }; + + let amount_decimals = match decode_nat_to_u64(amount_decimals) { + Ok(amount_decimals) => Some(amount_decimals), + Err(err) => { + problems.push(err); + None + } + }; + + if !problems.is_empty() { + let problems = problems.join(" - \n"); + return Err(format!("Invalid allowance:\n - {}", problems)); + } + + let asset = asset.unwrap(); + let amount_decimals = amount_decimals.unwrap(); + let owner_account = account_into_icrc1_account(&owner_account); + + Ok(Self { + asset, + amount_decimals, + owner_account, + }) + } +} + +impl TryFrom for ValidatedAsset { + type Error = String; + + fn try_from(value: Asset) -> Result { + let Asset::Token { + symbol, + ledger_canister_id, + ledger_fee_decimals, + } = value; + + let symbol = ValidatedSymbol::try_from(symbol.as_str()) + .map_err(|err| format!("Failed to validate asset symbol: {}", err))?; + + let ledger_fee_decimals = decode_nat_to_u64(ledger_fee_decimals) + .map_err(|err| format!("Failed to validate asset ledger fee_decimals: {}", err))?; + + Ok(Self::Token { + symbol, + ledger_canister_id, + ledger_fee_decimals, + }) + } +} + +impl TryFrom for ValidatedTreasuryManagerInit { + type Error = String; + + fn try_from(init: TreasuryManagerInit) -> Result { + let TreasuryManagerInit { assets } = init; + + let (asset_0, asset_1) = validate_assets(assets) + .map_err(|err| format!("Failed to validate TreasuryManagerInit: {}", err))?; + + Ok(Self { asset_0, asset_1 }) + } +} + +#[derive(CandidType, Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub(crate) enum ValidatedAsset { + Token { + symbol: ValidatedSymbol, + ledger_canister_id: Principal, + ledger_fee_decimals: u64, + }, +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub(crate) struct ValidatedAllowance { + pub asset: ValidatedAsset, + pub amount_decimals: u64, + pub owner_account: Account, +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub(crate) struct ValidatedDepositRequest { + pub allowance_0: ValidatedAllowance, + pub allowance_1: ValidatedAllowance, +} + +impl TryFrom for ValidatedDepositRequest { + type Error = String; + + fn try_from(value: DepositRequest) -> Result { + let DepositRequest { allowances } = value; + + let (allowance_0, allowance_1) = validate_allowances(allowances) + .map_err(|err| format!("Failed to validate DepositRequest: {}", err))?; + + Ok(Self { + allowance_0, + allowance_1, + }) + } +} + +pub(crate) struct ValidatedWithdrawRequest { + pub withdraw_account_0: Account, + pub withdraw_account_1: Account, +} + +pub(crate) fn account_into_icrc1_account( + account: &kongswap_adaptor::treasury_manager::Account, +) -> Account { + Account { + owner: account.owner, + subaccount: account.subaccount, + } +} + +pub(crate) fn icrc1_account_into_account( + account: &Account, +) -> kongswap_adaptor::treasury_manager::Account { + kongswap_adaptor::treasury_manager::Account { + owner: account.owner, + subaccount: account.subaccount, + } +} + +impl TryFrom<(Principal, Principal, Account, Account, WithdrawRequest)> + for ValidatedWithdrawRequest +{ + type Error = String; + + fn try_from( + value: (Principal, Principal, Account, Account, WithdrawRequest), + ) -> Result { + let mut errors = vec![]; + + let ( + ledger_0, + ledger_1, + default_withdraw_account_0, + default_withdraw_account_1, + WithdrawRequest { withdraw_accounts }, + ) = value; + + let (withdraw_account_0, withdraw_account_1) = + if let Some(ledger_to_account) = withdraw_accounts { + if ledger_to_account.get(&ledger_0).is_none() { + errors.push(format!( + "Withdraw account for ledger {} not found.", + ledger_0 + )); + } + + if ledger_to_account.get(&ledger_1).is_none() { + errors.push(format!( + "Withdraw account for ledger {} not found.", + ledger_1 + )); + } + + if !errors.is_empty() { + return Err(errors.join(", ")); + } + + let withdraw_account_0 = ledger_to_account.get(&ledger_0).unwrap(); + let withdraw_account_1 = ledger_to_account.get(&ledger_1).unwrap(); + + ( + account_into_icrc1_account(withdraw_account_0), + account_into_icrc1_account(withdraw_account_1), + ) + } else { + (default_withdraw_account_0, default_withdraw_account_1) + }; + + Ok(Self { + withdraw_account_0, + withdraw_account_1, + }) + } +} + +// (symbol, ledger_canister_id, ledger_fee_decimals) +impl TryFrom<(String, String, u64)> for ValidatedAsset { + type Error = String; + + fn try_from(value: (String, String, u64)) -> Result { + let (symbol, ledger_canister_id, ledger_fee_decimals) = value; + + let symbol = ValidatedSymbol::try_from(symbol)?; + + let ledger_canister_id = Principal::from_str(&ledger_canister_id).map_err(|_| { + format!( + "Cannot interpret second component as a principal: {}", + ledger_canister_id + ) + })?; + + Ok(Self::Token { + symbol, + ledger_canister_id, + ledger_fee_decimals, + }) + } +} + +fn take_bytes(input: &str) -> [u8; MAX_SYMBOL_BYTES] { + let mut result = [0u8; MAX_SYMBOL_BYTES]; + let bytes = input.as_bytes(); + + let copy_len = std::cmp::min(bytes.len(), MAX_SYMBOL_BYTES); + result[..copy_len].copy_from_slice(&bytes[..copy_len]); + + result +} + +fn is_valid_symbol_character(b: &u8) -> bool { + *b == 0 || b.is_ascii_graphic() +} + +#[derive(CandidType, Clone, Copy, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct ValidatedSymbol { + /// An Ascii string of up to MAX_SYMBOL_BYTES, e.g., "CHAT" or "ICP". + /// Stored as a fixed-size byte array, so the whole `Asset` type can derive `Copy`. + /// Can be created from + repr: [u8; MAX_SYMBOL_BYTES], +} + +impl TryFrom<[u8; 10]> for ValidatedSymbol { + type Error = String; + + fn try_from(value: [u8; 10]) -> Result { + // Check that the symbol is valid ASCII. + if !value.iter().all(is_valid_symbol_character) { + return Err(format!("Symbol must be ASCII and graphic; got {:?}", value)); + } + + Ok(ValidatedSymbol { repr: value }) + } +} + +impl TryFrom<&str> for ValidatedSymbol { + type Error = String; + + fn try_from(value: &str) -> Result { + if value.len() > MAX_SYMBOL_BYTES { + return Err(format!( + "Symbol must not exceed {} bytes or characters, got {} bytes.", + MAX_SYMBOL_BYTES, + value.len() + )); + } + + let bytes = take_bytes(&value); + + let symbol = Self::try_from(bytes)?; + + Ok(symbol) + } +} + +impl TryFrom for ValidatedSymbol { + type Error = String; + + fn try_from(value: String) -> Result { + Self::try_from(value.as_str()) + } +} + +fn bytes_to_string(bytes: &[u8]) -> String { + // Find the first null byte (if any) + let null_pos = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len()); + + // Convert only ASCII characters + bytes[..null_pos].iter().map(|&c| c as char).collect() +} + +impl std::fmt::Display for ValidatedSymbol { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let symbol_str = bytes_to_string(&self.repr); + write!(f, "{}", symbol_str) + } +} + +impl std::fmt::Debug for ValidatedSymbol { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let symbol_str = bytes_to_string(&self.repr); + write!(f, "{}", symbol_str) + } +} + +impl ValidatedAsset { + pub fn symbol(&self) -> String { + match self { + Self::Token { symbol, .. } => symbol.to_string(), + } + } + + pub fn set_symbol(&mut self, new_symbol: ValidatedSymbol) -> bool { + match self { + Self::Token { ref mut symbol, .. } => { + if symbol == &new_symbol { + false + } else { + *symbol = new_symbol; + true + } + } + } + } + + pub fn ledger_fee_decimals(&self) -> u64 { + match self { + Self::Token { + ledger_fee_decimals, + .. + } => *ledger_fee_decimals, + } + } + + pub fn set_ledger_fee_decimals(&mut self, new_fee_decimals: u64) -> bool { + match self { + Self::Token { + ref mut ledger_fee_decimals, + .. + } => { + if ledger_fee_decimals == &new_fee_decimals { + false + } else { + *ledger_fee_decimals = new_fee_decimals; + true + } + } + } + } + + pub fn ledger_canister_id(&self) -> Principal { + match self { + Self::Token { + ledger_canister_id, .. + } => *ledger_canister_id, + } + } +} + +pub(crate) fn decode_nat_to_u64(value: Nat) -> Result { + let u64_digit_components = value.0.to_u64_digits(); + + match &u64_digit_components[..] { + [] => Ok(0), + [val] => Ok(*val), + vals => Err(format!( + "Error parsing a Nat value `{:?}` to u64: expected a unique u64 value, got {:?}.", + &value, + vals.len(), + )), + } +} + +impl From for Asset { + fn from(value: ValidatedAsset) -> Self { + let ValidatedAsset::Token { + symbol, + ledger_canister_id, + ledger_fee_decimals, + } = value; + + let symbol = symbol.to_string(); + let ledger_fee_decimals = Nat::from(ledger_fee_decimals); + + Self::Token { + symbol, + ledger_canister_id, + ledger_fee_decimals, + } + } +} + +impl From for Allowance { + fn from(value: ValidatedAllowance) -> Self { + let ValidatedAllowance { + asset, + amount_decimals, + owner_account, + } = value; + + let asset = Asset::from(asset); + let amount_decimals = Nat::from(amount_decimals); + let owner_account = icrc1_account_into_account(&owner_account); + + Allowance { + asset, + amount_decimals, + owner_account, + } + } +} + +#[derive(CandidType, Clone, Debug, Deserialize, PartialEq)] +pub struct ValidatedBalance { + pub amount_decimals: u64, + pub account: Account, +} + +impl From for Balance { + fn from(value: ValidatedBalance) -> Self { + Self { + amount_decimals: Nat::from(value.amount_decimals), + account: Some(kongswap_adaptor::treasury_manager::Account { + owner: value.account.owner, + subaccount: value.account.subaccount, + }), + name: None, + } + } +} + +impl TryFrom for ValidatedBalance { + type Error = String; + fn try_from(value: Balance) -> Result { + let mut errors = vec![]; + + let amount_decimals_result = decode_nat_to_u64(value.amount_decimals.clone()); + if amount_decimals_result.is_err() { + errors.push(format!( + "Failed to convert amount {} to u64", + value.amount_decimals + )); + }; + + let icrc1_account = value + .account + .map(|account| account_into_icrc1_account(&account)); + + if icrc1_account.is_none() { + errors.push(format!("Owner account of the balance is not set")); + }; + + if value.name.is_none() { + errors.push(format!("Name is not set")); + }; + + if !errors.is_empty() { + return Err(errors.join(", ")); + } + + Ok(Self { + amount_decimals: amount_decimals_result.unwrap(), + account: icrc1_account.unwrap(), + }) + } +} + +impl From for BalanceBook { + fn from(value: ValidatedBalanceBook) -> Self { + Self { + treasury_owner: Some(value.treasury_owner.clone().into()), + treasury_manager: Some(value.treasury_manager.clone().into()), + external_custodian: Some(Balance { + amount_decimals: Nat::from(value.external), + account: None, + name: None, + }), + fee_collector: Some(Balance { + amount_decimals: Nat::from(value.fee_collector), + account: None, + name: None, + }), + payees: Some(Balance { + amount_decimals: Nat::from(value.spendings), + account: None, + name: None, + }), + payers: Some(Balance { + amount_decimals: Nat::from(value.earnings), + account: None, + name: None, + }), + suspense: Some(Balance { + amount_decimals: Nat::from(value.suspense), + account: None, + name: None, + }), + } + } +} + +impl From for Balances { + fn from(value: ValidatedBalances) -> Self { + let asset_to_balances = Some(btreemap! { + Asset::from(value.asset_0) => BalanceBook::from(value.asset_0_balance), + Asset::from(value.asset_1) => BalanceBook::from(value.asset_1_balance), + }); + + Self { + timestamp_ns: value.timestamp_ns, + asset_to_balances, + } + } +} diff --git a/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/withdraw/mod.rs b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/withdraw/mod.rs new file mode 100644 index 000000000..862a59cf7 --- /dev/null +++ b/rust/sns-adaptor/sns-kongswap-adaptor/kongswap_adaptor/src/withdraw/mod.rs @@ -0,0 +1,265 @@ +use crate::{ + balances::{Party, ValidatedBalances}, + kong_types::{ + ClaimArgs, ClaimReply, ClaimsArgs, ClaimsReply, RemoveLiquidityArgs, RemoveLiquidityReply, + }, + logged_arithmetics::logged_saturating_add, + tx_error_codes::TransactionErrorCodes, + validation::decode_nat_to_u64, + KongSwapAdaptor, KONG_BACKEND_CANISTER_ID, +}; +use candid::Nat; +use icrc_ledger_types::icrc1::account::Account; +use kongswap_adaptor::treasury_manager::{Error, ErrorKind}; +use kongswap_adaptor::{agent::AbstractAgent, audit::OperationContext}; + +impl KongSwapAdaptor { + /// This functions first checks the current LP token balance, + /// and if it is greater than zero, it calls the kongswap backend + /// canister to withdraw all allocated tokens. + async fn withdraw_from_dex( + &mut self, + context: &mut OperationContext, + ) -> Result<(), Vec> { + let remove_lp_token_amount = self.lp_balance(context).await; + + if remove_lp_token_amount == Nat::from(0u8) { + // Nothing to withdraw. + return Ok(()); + } + + let human_readable = + "Calling KongSwapBackend.remove_liquidity to withdraw all allocated tokens." + .to_string(); + + let (asset_0, asset_1) = self.assets(); + + let request = RemoveLiquidityArgs { + token_0: asset_0.symbol(), + token_1: asset_1.symbol(), + remove_lp_token_amount, + }; + + let balances_before = self.get_ledger_balances(context).await?; + + let RemoveLiquidityReply { + claim_ids, + amount_0, + lp_fee_0, + amount_1, + lp_fee_1, + .. + } = self + .emit_transaction( + context.next_operation(), + *KONG_BACKEND_CANISTER_ID, + request, + human_readable, + ) + .await + .map_err(|err| vec![err])?; + + let balances_after = self.get_ledger_balances(context).await?; + + // When withdrawing from the DEX, transferring tokens could fail. + // In this case, kongswap backend reutrns a non-empty `claim_ids`. + // Here, we try to find out which token has been successfully + // withdrawn and update the balanaces accordingly. + if balances_after.0 > balances_before.0 { + let amount_0 = logged_saturating_add( + decode_nat_to_u64(amount_0).unwrap(), + decode_nat_to_u64(lp_fee_0).unwrap(), + ); + + self.find_discrepency( + asset_0, + balances_before.0, + balances_after.0, + amount_0, + false, + ); + self.move_asset(asset_0, amount_0, Party::External, Party::TreasuryManager); + } + + if balances_after.1 > balances_before.1 { + let amount_1 = logged_saturating_add( + decode_nat_to_u64(amount_1).unwrap(), + decode_nat_to_u64(lp_fee_1).unwrap(), + ); + self.find_discrepency( + asset_1, + balances_before.1, + balances_after.1, + amount_1, + false, + ); + self.move_asset(asset_1, amount_1, Party::External, Party::TreasuryManager); + } + + // If we have a non-empty `claim_ids`, we are going to return + // an Error, indicating the the withdrawal from the DEX, was + // incomplete or unsuccessful. + // It wouldn't break our accounting, as we have already updated + // the balances if any transfer has been successful. + if !claim_ids.is_empty() { + let claim_ids = claim_ids + .iter() + .map(|claim_id| claim_id.to_string()) + .collect::>() + .join(", "); + return Err(vec![Error { + code: u64::from(TransactionErrorCodes::BackendCode), + message: format!( + "Withdrawal from DEX might not be complete, returned claims: {}.", + claim_ids + ), + kind: ErrorKind::Backend {}, + }]); + } + + Ok(()) + } + + /// When a withdrawal from the DEX fails, the kongswap backend + /// creates a claim that can be retried later. This function + /// checks if there are any pending claims, and if so, tries to + /// withdraw the tokens again. + pub async fn retry_withdraw_from_dex( + &mut self, + context: &mut OperationContext, + ) -> Result<(), Vec> { + let human_readable = + "Calling KongSwapBackend.claims to check if a retry withdrawal is needed.".to_string(); + + let balances_before = self.get_ledger_balances(context).await?; + + // Check if there are any pending claims. + let claims = self + .emit_transaction( + context.next_operation(), + *KONG_BACKEND_CANISTER_ID, + ClaimsArgs { + principal_id: self.id.to_string(), + }, + human_readable, + ) + .await + .map_err(|err| vec![err])?; + + let mut errors = vec![]; + + // Try to withdraw each claim. + for ClaimsReply { + claim_id, symbol, .. + } in claims + { + let human_readable = format!( + "Calling KongSwapBackend.claim to claim the liquidity for {}, claim ID {}.", + symbol, claim_id, + ); + + let response = self + .emit_transaction( + context.next_operation(), + *KONG_BACKEND_CANISTER_ID, + ClaimArgs { claim_id }, + human_readable, + ) + .await; + + let balances_after = self.get_ledger_balances(context).await?; + // If withdrawal has previously failed and before retrying it, + // the symbol of the asset changes, hence, we need to check the + // ID of its corresponding ledger canister. + match response { + Ok(ClaimReply { + canister_id: Some(canister_id), + amount, + .. + }) => { + if let Some(asset) = self.get_asset_for_ledger(&canister_id) { + let (balances_before, balances_after) = if asset == self.assets().0 { + (balances_before.0, balances_after.0) + } else { + (balances_before.1, balances_after.1) + }; + + match decode_nat_to_u64(amount) { + Ok(amount) => { + self.move_asset( + asset, + amount, + Party::External, + Party::TreasuryManager, + ); + self.find_discrepency( + asset, + balances_before, + balances_after, + amount, + false, + ); + } + Err(err) => { + errors.push(Error::new_postcondition(format!( + "Failed to decode amount for claim ID {}: {}", + claim_id, err + ))); + } + } + } else { + errors.push(Error::new_postcondition(format!( + "Cannot identify asset for ledger `{}` for claim ID {}", + canister_id, claim_id + ))); + } + } + Ok(_) => { + errors.push(Error::new_postcondition(format!( + "Claim for claim ID {} returned no ledger canister ID.", + claim_id + ))); + } + Err(err) => { + errors.push(err); + } + } + } + + if !errors.is_empty() { + return Err(errors); + } + + Ok(()) + } + + pub async fn withdraw_impl( + &mut self, + context: &mut OperationContext, + withdraw_account_0: Account, + withdraw_account_1: Account, + ) -> Result> { + let mut errors = vec![]; + + if let Err(err) = self.withdraw_from_dex(context).await { + errors.extend(err.into_iter()); + } + + if let Err(err) = self.retry_withdraw_from_dex(context).await { + errors.extend(err.into_iter()); + } + + match self + .return_remaining_assets_to_owner(context, withdraw_account_0, withdraw_account_1) + .await + { + Ok(_) => {} + Err(err) => { + errors.extend(err.clone()); + return Err(err); + } + }; + + Ok(self.get_cached_balances()) + } +}