diff --git a/.cargo/config.toml b/.cargo/config.toml index 25e71b13..4c902a0b 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,7 @@ [alias] bin = ["run", "--package", "cargo-bin", "--"] pkg = ["run", "--bin", "build_pkgs", "--"] +api = ["run", "--bin", "api_tools", "--"] # We need to enable dynamic linking for musl targets # See https://github.com/rust-lang/rust/issues/59302#issue-422994250 @@ -10,4 +11,3 @@ rustflags = ["-C", "target-feature=-crt-static"] [target.x86_64-unknown-linux-musl] rustflags = ["-C", "target-feature=-crt-static"] - diff --git a/.github/workflows/algod_client_tests.yml b/.github/workflows/algod_client_tests.yml new file mode 100644 index 00000000..240288ae --- /dev/null +++ b/.github/workflows/algod_client_tests.yml @@ -0,0 +1,63 @@ +name: Algod Client Tests + +on: + push: + branches: + - main + paths: + - "crates/algod_client/**" + - "crates/algod_client_tests/**" + - "crates/algokit_http_client/**" + - "crates/algokit_transact/**" + - ".github/workflows/algod_client_tests.yml" + pull_request: + branches: + - main + paths: + - "crates/algod_client/**" + - "crates/algod_client_tests/**" + - "crates/algokit_http_client/**" + - "crates/algokit_transact/**" + - ".github/workflows/algod_client_tests.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + algod_client_tests: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.85.0 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Install algokit CLI + run: uv tool install algokit + + - name: Add uv tools to PATH + run: echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Verify algokit installation + run: | + algokit --version + which algokit + + - name: Build algod_client + run: cargo build -p algod_client + + - name: Build algod_client_tests + run: cargo build -p algod_client_tests + + - name: Run algod_client integration tests + run: cargo test -p algod_client_tests --verbose diff --git a/.github/workflows/api_ci.yml b/.github/workflows/api_ci.yml new file mode 100644 index 00000000..9e0205b8 --- /dev/null +++ b/.github/workflows/api_ci.yml @@ -0,0 +1,96 @@ +name: API OAS Generator CI +# This workflow tests the OpenAPI Specification (OAS) generator in the api/ folder. +# It validates the Python-based Jinja2 generator, ensures generated Rust code compiles, +# and checks for output stability. For testing the generated algod_client crate itself, +# see the algod_client_tests.yml workflow. + +on: + push: + branches: + - main + paths: + - "api/**" + - "tools/api_tools/**" + - ".github/workflows/api_ci.yml" + pull_request: + branches: + - main + paths: + - "api/**" + - "tools/api_tools/**" + - ".github/workflows/api_ci.yml" + workflow_dispatch: + +permissions: + contents: read + +env: + API_DIR: api + OUTPUT_DIR: crates/algod_client + +jobs: + oas_generator_check: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.85.0 + components: rustfmt + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Install bun dependencies + working-directory: ${{ env.API_DIR }} + run: bun install + + - name: Install uv dependencies + working-directory: ${{ env.API_DIR }}/oas_generator + run: uv sync --extra dev + + - name: Run OAS generator linting + run: cargo api lint-oas + + - name: Run OAS generator tests + run: cargo api test-oas + + - name: Store original state for stability check + run: | + git diff --name-only HEAD~1..HEAD -- ${{ env.OUTPUT_DIR }} > /tmp/initial_changes.txt || true + git status --porcelain ${{ env.OUTPUT_DIR }} > /tmp/initial_status.txt || true + + - name: Generate algod API client + run: cargo api generate-algod + + - name: Check for output stability + run: | + git status --porcelain ${{ env.OUTPUT_DIR }} > /tmp/post_generation_status.txt + + if [ -s /tmp/post_generation_status.txt ]; then + echo "❌ Output instability detected! The following files were modified or created:" + cat /tmp/post_generation_status.txt + echo "" + echo "🔍 Detailed diff:" + git diff --no-pager ${{ env.OUTPUT_DIR }} + echo "" + echo "💡 This indicates that the generated code is not in sync with the current specification." + echo " Please run 'cargo api generate-algod' and commit the changes." + exit 1 + else + echo "✅ Output stability check passed - no unexpected changes detected" + fi + + - name: Verify generated code compiles + run: | + cargo check --manifest-path Cargo.toml -p algod_client diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index c7fce8bf..4a4bd007 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -35,13 +35,9 @@ jobs: outputs: # The packages that use Uniffi bindings ffi_packages: ${{ steps.set_ffi_packages.outputs.ffi_packages }} - # The API client packages - api_packages: ${{ steps.set_api_packages.outputs.api_packages }} steps: - id: set_ffi_packages run: echo 'ffi_packages=["algokit_transact"]' >> $GITHUB_OUTPUT - - id: set_api_packages - run: echo 'api_packages=["algod_api"]' >> $GITHUB_OUTPUT typescript_wasm_ci_cd: needs: setup @@ -71,7 +67,6 @@ jobs: secrets: BOT_ID: ${{ secrets.BOT_ID }} BOT_SK: ${{ secrets.BOT_SK }} - deploy_docs: # Only run on pushes to main (not on PRs) if: github.event_name == 'push' diff --git a/Cargo.lock b/Cargo.lock index 739df98d..0769f3bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,12 +38,47 @@ dependencies = [ ] [[package]] -name = "algod_api" +name = "algod_client" +version = "0.0.1" +dependencies = [ + "algokit_http_client", + "algokit_transact", + "base64 0.22.1", + "rmp-serde", + "serde", + "serde_bytes", + "serde_json", + "serde_repr", + "serde_with", + "thiserror 1.0.69", + "tokio", + "tokio-test", + "url", + "uuid", +] + +[[package]] +name = "algod_client_tests" version = "0.1.0" dependencies = [ + "algod_client", "algokit_http_client", + "algokit_transact", + "base64 0.22.1", + "criterion", + "ed25519-dalek", + "futures", + "hex", + "lazy_static", + "once_cell", + "rand", + "regex", + "reqwest", "serde", "serde_json", + "sha2", + "tokio", + "tokio-test", ] [[package]] @@ -53,7 +88,10 @@ dependencies = [ "async-trait", "js-sys", "reqwest", + "serde", + "serde-wasm-bindgen", "thiserror 2.0.7", + "tsify-next", "uniffi", "wasm-bindgen", "wasm-bindgen-futures", @@ -106,7 +144,7 @@ dependencies = [ name = "algokit_utils" version = "0.1.0" dependencies = [ - "algod_api", + "algod_client", "algokit_http_client", "algokit_transact", "async-trait", @@ -145,6 +183,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "0.6.18" @@ -200,6 +244,16 @@ version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +[[package]] +name = "api_tools" +version = "0.1.0" +dependencies = [ + "clap", + "color-eyre", + "duct", + "shlex", +] + [[package]] name = "arbitrary" version = "1.4.1" @@ -250,6 +304,28 @@ dependencies = [ "nom", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.88" @@ -486,6 +562,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.4" @@ -518,6 +600,33 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.4.4" @@ -703,6 +812,42 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -737,6 +882,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1052,6 +1203,7 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", + "rand_core", "serde", "sha2", "subtle", @@ -1269,6 +1421,21 @@ dependencies = [ "new_debug_unreachable", ] +[[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" @@ -1285,6 +1452,17 @@ 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" @@ -1320,6 +1498,7 @@ 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", @@ -1439,6 +1618,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "handlebars" version = "6.3.2" @@ -1497,6 +1686,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1982,6 +2177,17 @@ dependencies = [ "serde", ] +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "is_executable" version = "0.1.2" @@ -1997,6 +2203,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.14" @@ -2372,6 +2587,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "opener" version = "0.7.2" @@ -2662,6 +2883,34 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2759,6 +3008,26 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.8" @@ -3628,6 +3897,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tokio" version = "1.45.1" @@ -3677,6 +3956,30 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + [[package]] name = "tokio-tungstenite" version = "0.21.0" diff --git a/Cargo.toml b/Cargo.toml index 86ad49fd..e1d9e975 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,19 @@ [workspace] resolver = "2" -members = [ "crates/algod_api", "crates/algokit_http_client", +members = [ + "crates/algokit_http_client", "crates/algokit_transact", - "crates/algokit_transact_ffi", "crates/algokit_utils", - "crates/ffi_macros", "crates/ffi_mutex", + "crates/algokit_transact_ffi", + "crates/algokit_utils", + "crates/ffi_macros", + "crates/ffi_mutex", + "crates/algod_client", + "crates/algod_client_tests", "tools/build_pkgs", "crates/uniffi-bindgen", "docs", "tools/cargo-bin", + "tools/api_tools", ] [workspace.dependencies] diff --git a/api/README.md b/api/README.md index 100dfaf1..e1ce3f8c 100644 --- a/api/README.md +++ b/api/README.md @@ -1,66 +1,113 @@ # Algorand API Tools -This package contains tools for working with the Algorand API specifications and generating HTTP client libraries. +This package contains tools for working with the Algorand API specifications and generating Rust HTTP client libraries using a custom Jinja2-based generator. ## Prerequisites -- [Bun](https://bun.sh/) - JavaScript runtime and package manager -- [OpenJDK](https://adoptium.net/) - Java Development Kit +- [Python 3.12+](https://www.python.org/) - Required for the custom OAS generator +- [uv](https://docs.astral.sh/uv/) - Python package manager +- [Rust](https://rustup.rs/) - Required for compiling generated clients and running API tools +- [Bun](https://bun.sh/) - JavaScript runtime (only for convert-openapi script) ## Setup ```bash -# Install dependencies +# Install Python dependencies for the OAS generator +cd api/oas_generator +uv install + +# Install JavaScript dependencies (only needed for convert-openapi) +cd ../ bun install ``` ## Available Scripts -> NOTE: These scripts must be run inside the `./api` directory. +> NOTE: These scripts can be run from the repository root using `cargo api `. ### Convert OpenAPI 2.0 to OpenAPI 3.0 Converts the Algod OpenAPI 2.0 spec to OpenAPI 3.0: ```bash -bun run convert-openapi +cargo api convert-openapi ``` The converted spec will be available at `specs/algod.oas3.json`. -### Generate API Clients +### Generate Rust API Clients -Generates TypeScript and Python API clients based on the OpenAPI spec: +Generates Rust API clients using the custom Jinja2-based generator: ```bash -bun run generate:{algod_api}:{py|ts} +cargo api generate-algod ``` -The generated API clients will be available in the `./packages/` directory: +The generated Rust client will be available at `../crates/algod_client/`. + +### Development Scripts + +```bash +# Test the OAS generator +cargo api test-oas + +# Format the OAS generator code +cargo api format-oas + +# Lint and type-check the OAS generator +cargo api lint-oas + +# Format generated Rust code +cargo api format-algod +``` -- `./packages/typescript/algod_api/` - algod TypeScript client -- `./packages/python/algod_api/` - algod Python client +## Custom Rust OAS Generator -## OpenAPI Specs for algorand apis +The project uses a custom Jinja2-based generator located in `oas_generator/` that creates optimized Rust API clients from OpenAPI 3.x specifications. + +### Features + +- **Complete Rust Client Generation**: APIs, models, and configuration +- **Msgpack Support**: Automatic detection and handling of binary encoding +- **Signed Transactions**: Algorand-specific vendor extension support (`x-algokit-signed-txn`) +- **Type Safety**: Comprehensive OpenAPI to Rust type mapping +- **Template-based**: Customizable Jinja2 templates for code generation + +### Generated Structure + +The generator creates a complete Rust crate with the following structure: + +``` +crates/algod_client/ +├── Cargo.toml +├── README.md +└── src/ + ├── lib.rs + ├── apis/ + │ ├── mod.rs + │ ├── client.rs + │ └── {endpoint}.rs + └── models/ + ├── mod.rs + └── {model}.rs +``` -## Algod +## OpenAPI Specs for Algorand APIs -The `algod.oas2.json` is taken directly from [go-algorand](https://github.com/algorand/go-algorand/blob/master/daemon/algod/api/algod.oas2.json). The script under [scripts/convert-openapi.ts](scripts/convert-openapi.ts) is used to convert the spec to OpenAPI 3.0 via [swagger converter](https://converter.swagger.io/) endpoint. The current approach is to manually edit and tweak the algod.oas2.json fixing known issues on a spec from go-algorand, then use the openapi-generator-cli to generate clients on the v3 spec. OpenAPI v3 is preferred for client generation as it offers enhanced schema features, better component reusability, and improved type definitions compared to v2. Additionally, most modern code generators like openapi-generator-cli provide better support and more accurate code generation when working with v3 specifications. +### Algod -## OpenAPI Generator Configuration +The `algod.oas2.json` is taken directly from [go-algorand](https://github.com/algorand/go-algorand/blob/master/daemon/algod/api/algod.oas2.json). To convert the spec to OpenAPI 3.0, use `cargo api convert-openapi` which runs the TypeScript script [scripts/convert-openapi.ts](scripts/convert-openapi.ts) via [swagger converter](https://converter.swagger.io/) endpoint. -The client generation is configured with the following options: +The current approach is to manually edit and tweak the algod.oas2.json fixing known issues from the go-algorand spec, then use the custom Rust OAS generator to generate clients from the v3 spec. OpenAPI v3 is preferred for client generation as it offers enhanced schema features, better component reusability, and improved type definitions compared to v2. -### TypeScript Client +## Generator Configuration -- Package name: `@algorandfoundation/algokit-algod-api` -- ES6 support: true -- Manually refined tsconfig setup to build cjs, esm clients along with browser support. -- Custom tests defined in `oas_templates/typescript/custom-tests/` that implement tests for initial batch of transaction endpoints. More endpoint tests are to be added in the future. +The custom Rust generator is configured with: -### Python Client +- **Package name**: `algod_client` +- **Msgpack detection**: Automatic handling of binary-encoded fields +- **Algorand extensions**: Support for signed transaction via a vendor extension +- **Type safety**: Complete OpenAPI to Rust type mapping +- **Error handling**: Comprehensive error types and response handling -- Package name: `algokit_algod_api`. -- Ignoring various unneeded supporting files like tox.ini, git_push.sh, etc. -- Various improvements to make auto generated code compatible with poetry and more modern python conventions and practices. -- Custom tests defined in `oas_templates/python/custom-tests/` that implement tests for initial batch of transaction endpoints. More endpoint tests are to be added in the future. +For detailed information about the generator architecture and customization options, see [`oas_generator/ARCHITECTURE.md`](oas_generator/ARCHITECTURE.md). diff --git a/api/bun.lock b/api/bun.lock index 72b1ff40..a42257ae 100644 --- a/api/bun.lock +++ b/api/bun.lock @@ -5,7 +5,6 @@ "name": "api", "devDependencies": { "@apidevtools/swagger-parser": "^11.0.0", - "@openapitools/openapi-generator-cli": "2.19.1", "@types/bun": "latest", "@types/node": "^20.10.0", "prettier": "^3.5.3", @@ -24,360 +23,38 @@ "@apidevtools/swagger-parser": ["@apidevtools/swagger-parser@11.0.1", "", { "dependencies": { "@apidevtools/json-schema-ref-parser": "13.0.2", "@apidevtools/openapi-schemas": "^2.1.0", "@apidevtools/swagger-methods": "^3.0.2", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "call-me-maybe": "^1.0.2" }, "peerDependencies": { "openapi-types": ">=7" } }, "sha512-0OzWjKPUr7dvXOgQi6hsNLpwgQRtPgyQoYMuaIB+Zj50Qjbwxph/nu4BndwOA446FtQUTwkR3BxLnORpVYLHYw=="], - "@babel/runtime": ["@babel/runtime@7.27.6", "", {}, "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q=="], - - "@lukeed/csprng": ["@lukeed/csprng@1.1.0", "", {}, "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA=="], - - "@nestjs/axios": ["@nestjs/axios@4.0.0", "", { "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "axios": "^1.3.1", "rxjs": "^7.0.0" } }, "sha512-1cB+Jyltu/uUPNQrpUimRHEQHrnQrpLzVj6dU3dgn6iDDDdahr10TgHFGTmw5VuJ9GzKZsCLDL78VSwJAs/9JQ=="], - - "@nestjs/common": ["@nestjs/common@11.0.20", "", { "dependencies": { "file-type": "20.4.1", "iterare": "1.2.1", "load-esm": "1.0.2", "tslib": "2.8.1", "uid": "2.0.2" }, "peerDependencies": { "class-transformer": "*", "class-validator": "*", "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "optionalPeers": ["class-transformer", "class-validator"] }, "sha512-/GH8NDCczjn6+6RNEtSNAts/nq/wQE8L1qZ9TRjqjNqEsZNE1vpFuRIhmcO2isQZ0xY5rySnpaRdrOAul3gQ3A=="], - - "@nestjs/core": ["@nestjs/core@11.0.20", "", { "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", "path-to-regexp": "8.2.0", "tslib": "2.8.1", "uid": "2.0.2" }, "peerDependencies": { "@nestjs/common": "^11.0.0", "@nestjs/microservices": "^11.0.0", "@nestjs/platform-express": "^11.0.0", "@nestjs/websockets": "^11.0.0", "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "optionalPeers": ["@nestjs/microservices", "@nestjs/platform-express", "@nestjs/websockets"] }, "sha512-yUkEzBGiRNSEThVl6vMCXgoA9sDGWoRbJsTLdYdCC7lg7PE1iXBnna1FiBfQjT995pm0fjyM1e3WsXmyWeJXbw=="], - - "@nuxt/opencollective": ["@nuxt/opencollective@0.4.1", "", { "dependencies": { "consola": "^3.2.3" }, "bin": { "opencollective": "bin/opencollective.js" } }, "sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ=="], - - "@nuxtjs/opencollective": ["@nuxtjs/opencollective@0.3.2", "", { "dependencies": { "chalk": "^4.1.0", "consola": "^2.15.0", "node-fetch": "^2.6.1" }, "bin": { "opencollective": "bin/opencollective.js" } }, "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA=="], - - "@openapitools/openapi-generator-cli": ["@openapitools/openapi-generator-cli@2.19.1", "", { "dependencies": { "@nestjs/axios": "4.0.0", "@nestjs/common": "11.0.20", "@nestjs/core": "11.0.20", "@nuxtjs/opencollective": "0.3.2", "axios": "1.8.4", "chalk": "4.1.2", "commander": "8.3.0", "compare-versions": "4.1.4", "concurrently": "6.5.1", "console.table": "0.10.0", "fs-extra": "11.3.0", "glob": "9.3.5", "inquirer": "8.2.6", "lodash": "4.17.21", "proxy-agent": "6.5.0", "reflect-metadata": "0.2.2", "rxjs": "7.8.2", "tslib": "2.8.1" }, "bin": { "openapi-generator-cli": "main.js" } }, "sha512-APP3EPI/m7bg220qS+7UAFiyLJFbNCjlsEEjrP2sLmW4Za44U8e3Lb2zDy3sbvJvIUnpYWe+hu9RbrxrPP9djQ=="], - - "@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="], - - "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - - "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], - "@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/node": ["@types/node@20.19.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q=="], - "agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="], - "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], - "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], - - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], - - "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - - "axios": ["axios@1.8.4", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw=="], - - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - - "basic-ftp": ["basic-ftp@5.0.5", "", {}, "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg=="], - - "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - - "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="], - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - "call-me-maybe": ["call-me-maybe@1.0.2", "", {}, "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="], - - "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - - "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - - "cli-width": ["cli-width@3.0.0", "", {}, "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw=="], - - "cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], - - "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - - "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], - - "compare-versions": ["compare-versions@4.1.4", "", {}, "sha512-FemMreK9xNyL8gQevsdRMrvO4lFCkQP7qbuktn1q8ndcNk1+0mz7lgE7b/sNvbhVgY4w6tMN1FDp6aADjqw2rw=="], - - "concurrently": ["concurrently@6.5.1", "", { "dependencies": { "chalk": "^4.1.0", "date-fns": "^2.16.1", "lodash": "^4.17.21", "rxjs": "^6.6.3", "spawn-command": "^0.0.2-1", "supports-color": "^8.1.0", "tree-kill": "^1.2.2", "yargs": "^16.2.0" }, "bin": { "concurrently": "bin/concurrently.js" } }, "sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag=="], - - "consola": ["consola@2.15.3", "", {}, "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw=="], - - "console.table": ["console.table@0.10.0", "", { "dependencies": { "easy-table": "1.1.0" } }, "sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g=="], - - "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], - - "date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="], - - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - - "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], - - "degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="], - - "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "easy-table": ["easy-table@1.1.0", "", { "optionalDependencies": { "wcwidth": ">=1.0.1" } }, "sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA=="], - - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - - "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], - - "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - - "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], - "fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="], - "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], - - "figures": ["figures@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg=="], - - "file-type": ["file-type@20.4.1", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ=="], - - "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], - - "form-data": ["form-data@4.0.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA=="], - - "fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], - - "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "get-uri": ["get-uri@6.0.4", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ=="], - - "glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], - - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - - "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "inquirer": ["inquirer@8.2.6", "", { "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", "ora": "^5.4.1", "run-async": "^2.4.0", "rxjs": "^7.5.5", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", "wrap-ansi": "^6.0.1" } }, "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg=="], - - "ip-address": ["ip-address@9.0.5", "", { "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" } }, "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], - - "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], - - "iterare": ["iterare@1.2.1", "", {}, "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q=="], - "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - "jsbn": ["jsbn@1.1.0", "", {}, "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="], - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], - - "load-esm": ["load-esm@1.0.2", "", {}, "sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw=="], - - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - - "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - - "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - - "minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="], - - "minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "mute-stream": ["mute-stream@0.0.8", "", {}, "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="], - - "netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="], - - "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - - "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], - "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - - "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], - - "pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="], - - "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="], - - "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - - "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], - "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="], - "proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="], - - "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], - - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - - "reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="], - - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - - "run-async": ["run-async@2.4.1", "", {}, "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ=="], - - "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], - - "socks": ["socks@2.8.5", "", { "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" } }, "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww=="], - - "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], - - "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - - "spawn-command": ["spawn-command@0.0.2", "", {}, "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ=="], - - "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], - - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "strtok3": ["strtok3@10.3.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw=="], - - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], - - "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], - - "token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="], - - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - - "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - "uid": ["uid@2.0.2", "", { "dependencies": { "@lukeed/csprng": "^1.0.0" } }, "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g=="], - - "uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="], - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], - - "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - - "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - - "yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], - - "yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], - - "@nuxt/opencollective/consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - - "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "concurrently/rxjs": ["rxjs@6.6.7", "", { "dependencies": { "tslib": "^1.9.0" } }, "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ=="], - - "concurrently/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - - "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - - "concurrently/rxjs/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], } } diff --git a/api/oas_generator/.gitignore b/api/oas_generator/.gitignore new file mode 100644 index 00000000..a52899f8 --- /dev/null +++ b/api/oas_generator/.gitignore @@ -0,0 +1,151 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# uv +.uv/ + +# Generated rust client output +generated_rust_client/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db diff --git a/api/oas_generator/ARCHITECTURE.md b/api/oas_generator/ARCHITECTURE.md new file mode 100644 index 00000000..b9e6f30c --- /dev/null +++ b/api/oas_generator/ARCHITECTURE.md @@ -0,0 +1,113 @@ +# Architecture + +## Overview + +The Rust OAS Generator is a Jinja2-based code generator that converts OpenAPI 3.x specifications into Rust API clients. The architecture emphasizes separation of concerns between parsing, analysis, and generation phases. + +## Core Components + +### 1. CLI Interface (`cli.py`) + +Entry point providing command-line interface with argument parsing, file validation, and error handling. Orchestrates the parsing and generation pipeline. + +### 2. OpenAPI Parser (`parser/oas_parser.py`) + +Transforms OpenAPI JSON specifications into structured Python dataclasses: + +- **ParsedSpec**: Root container for parsed specification +- **Operation**: HTTP operations with Rust-specific metadata +- **Schema**: Data models with type information +- **Parameter/Response**: Request/response components + +#### Key Features + +- Recursive reference resolution (`$ref`) +- Msgpack operation detection +- Vendor extension processing (`x-algokit-signed-txn`) +- Dependency graph construction for schema relationships + +### 3. Template Engine (`generator/template_engine.py`) + +Jinja2-based code generation system with specialized analyzers: + +- **OperationAnalyzer**: Groups operations, analyzes parameters +- **ResponseAnalyzer**: Handles success/error response types +- **TypeAnalyzer**: Manages imports and type dependencies +- **RustTemplateEngine**: Configures Jinja2 environment with custom filters + +### 4. Template Filters (`generator/filters.py`) + +Custom Jinja2 filters for Rust code generation: + +- `rust_doc_comment`: Format documentation strings +- `ensure_semver`: Validate semantic versioning +- `is_signed_transaction_field`: Algorand transaction detection +- `get_dependencies_for_schema`: Dynamic import generation + +### 5. Templates (`templates/`) + +Jinja2 templates organized by component type: + +``` +templates/ +├── base/ # Core library files +├── apis/ # API endpoint implementations +├── models/ # Data model definitions +└── Cargo.toml.j2 # Project configuration +``` + +## Data Flow + +1. **Parse**: CLI loads OpenAPI spec → OASParser creates structured dataclasses +2. **Analyze**: Template engine analyzers extract metadata for code generation +3. **Generate**: Jinja2 templates render Rust code using parsed data and analysis +4. **Output**: Generated files written to target directory structure + +## Type System + +### OpenAPI → Rust Mapping + +| OpenAPI Type | Rust Type | +|--------------|-----------| +| `string` | `String` | +| `integer` | `u32` or `u64` | +| `number` | `f32`/`f64` | +| `boolean` | `bool` | +| `array` | `Vec` | +| `object` | `serde_json::Value` | + +### Special Cases + +- **References**: `#/components/schemas/Model` → `Model` +- **Msgpack Fields**: Base64-encoded properties → `Vec` +- **Keywords**: Rust reserved words escaped with `r#` prefix +- **Integers**: Type selection based on: + - `u64` for fields marked with `x-algokit-bigint: true` + - `u32` for fields with `format: "int32"` + - `u32` for fields with `maximum ≤ 4,294,967,295` + - `u32` for small bounded fields (e.g., `maximum ≤ 100`) + - `u32` for enum-like fields (descriptions containing "value `1`", "type.", etc.) + - `u64` as default for potentially large blockchain values +- **x-algokit-bigint**: Fields marked with this extension explicitly use `u64` for 64-bit precision + +## Msgpack Integration + +Supports binary encoding for Algorand blockchain integration: + +1. **Detection**: Content-Type `application/msgpack` or vendor extensions +2. **Propagation**: Dependency graph traversal marks related schemas +3. **Implementation**: Affected schemas implement `AlgorandMsgpack` trait + +## Error Handling + +- **File Operations**: Graceful handling with backup/restore +- **JSON Parsing**: Detailed error messages for malformed specs +- **Template Rendering**: Context validation and error reporting +- **Type Resolution**: Circular reference detection + +## Extension Points + +- **Custom Filters**: Add domain-specific template functions +- **Template Override**: Replace default templates with custom implementations +- **Schema Extensions**: Support additional vendor extensions +- **Type Mappings**: Extend OpenAPI to Rust type conversions diff --git a/api/oas_generator/README.md b/api/oas_generator/README.md new file mode 100644 index 00000000..e0da7626 --- /dev/null +++ b/api/oas_generator/README.md @@ -0,0 +1,81 @@ +# Rust OAS Generator + +A Jinja2-based generator that produces Rust API clients from OpenAPI 3.x specifications. + +## Overview + +This tool replaces traditional OpenAPI generators with a custom implementation optimized for Rust client generation. It supports msgpack encoding and Algorand-specific vendor extensions for signed transactions. + +## Installation + +```bash +cd api/oas_generator +uv sync +``` + +## Usage + +### Basic Generation + +```bash +rust_oas_generator spec.json +``` + +### Custom Output Directory + +```bash +rust_oas_generator spec.json --output ./my_client --package-name my_api_client +``` + +### Verbose Output + +```bash +rust_oas_generator spec.json --verbose +``` + +## Features + +- **Complete Rust Client Generation**: APIs, models, and configuration +- **Msgpack Support**: Automatic detection and handling of binary encoding +- **Signed Transactions**: Algorand-specific vendor extension support (`x-algokit-signed-txn`) +- **Type Safety**: Comprehensive OpenAPI to Rust type mapping +- **Template-based**: Customizable Jinja2 templates for code generation + +## Generated Structure + +``` +generated/ +├── Cargo.toml +├── README.md +└── src/ + ├── lib.rs + ├── apis/ + │ ├── mod.rs + │ ├── client.rs + │ ├── configuration.rs + │ └── {endpoint}.rs + └── models/ + ├── mod.rs + └── {model}.rs +``` + +## Requirements + +- Python 3.12+ +- OpenAPI 3.x specification (JSON format) + +## Development + +```bash +# Install dev dependencies +uv install --dev + +# Run tests +pytest + +# Lint code +ruff check + +# Type check +mypy rust_oas_generator +``` diff --git a/api/oas_generator/pyproject.toml b/api/oas_generator/pyproject.toml new file mode 100644 index 00000000..55403332 --- /dev/null +++ b/api/oas_generator/pyproject.toml @@ -0,0 +1,134 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "rust-oas-generator" +version = "1.0.0" +description = "A Jinja2-based generator that produces Rust API clients from OpenAPI specifications" +readme = "README.md" +authors = [{ name = "AlgoKit Core Team" }] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", +] +requires-python = ">=3.12" +dependencies = ["jinja2>=3.0.0"] + +[project.optional-dependencies] +dev = ["pytest>=8.0.0", "ruff>=0.1.0", "mypy>=1.0.0"] + +[project.scripts] +rust_oas_generator = "rust_oas_generator.cli:main" + +[tool.hatch.build.targets.wheel] +packages = ["src/rust_oas_generator"] + +[tool.hatch.build.targets.sdist] +include = ["src/", "README.md", "LICENSE"] + +[tool.ruff] +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", +] + +target-version = "py312" +line-length = 120 +lint.select = [ + "F", # pyflakes + "E", + "W", # pycodestyle + "C90", # mccabe + "I", # isort + "N", # PEP8 naming + "UP", # pyupgrade + "YTT", # flake8-2020 + "ANN", # flake8-annotations + "FBT", # flake8-boolean-trap + "B", # flake8-bugbear + "A", # flake8-builtins + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "T10", # flake8-debugger + "ISC", # flake8-implicit-str-concat + "ICN", # flake8-import-conventions + "PIE", # flake8-pie + "T20", # flake8-print + "PYI", # flake8-pyi + "PT", # flake8-pytest-style + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "SLF", # flake8-self + "SIM", # flake8-simplify + "TID", # flake8-tidy-imports + "ARG", # flake8-unused-arguments + "PTH", # flake8-use-pathlib + "ERA", # eradicate + "PGH", # pygrep-hooks + "PL", # pylint + "RUF", # Ruff-specific rules +] +lint.ignore = [ + "T201", # Allow print statements in CLI tools + "BLE001", # Allow catching general exceptions in CLI + "EXE001", # Allow non-executable scripts + "TID252", # Allow relative imports within package +] + +# Allow fix for all enabled rules (when `--fix`) is provided. +lint.fixable = ["ALL"] +lint.unfixable = [] + +[tool.ruff.format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +[tool.mypy] +python_version = "3.12" +warn_unused_ignores = true +warn_redundant_casts = true +warn_unused_configs = true +warn_unreachable = true +warn_return_any = true +strict = true +disallow_untyped_decorators = true +disallow_any_generics = false +implicit_reexport = false +show_error_codes = true + +[tool.uv] +dev-dependencies = ["mypy>=1.14.1", "ruff>=0.12.0"] diff --git a/api/oas_generator/rust_oas_generator/__init__.py b/api/oas_generator/rust_oas_generator/__init__.py new file mode 100644 index 00000000..8cba75dd --- /dev/null +++ b/api/oas_generator/rust_oas_generator/__init__.py @@ -0,0 +1,19 @@ +""" +Rust OpenAPI Client Generator + +A Jinja2-based generator that produces Rust API clients from OpenAPI specifications. +Designed for maintainability, LLM-friendliness, and architectural improvements. +""" + +from .generator import RustCodeGenerator, RustTemplateEngine +from .parser import OASParser, ParsedSpec + +__version__ = "1.0.0" +__author__ = "OpenAPI Rust Generator" + +__all__ = [ + "OASParser", + "ParsedSpec", + "RustCodeGenerator", + "RustTemplateEngine", +] diff --git a/api/oas_generator/rust_oas_generator/cli.py b/api/oas_generator/rust_oas_generator/cli.py new file mode 100644 index 00000000..d305d6f7 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/cli.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +"""Command-line interface for the Rust OAS Generator.""" + +import argparse +import contextlib +import json +import shutil +import sys +import tempfile +import traceback +from collections.abc import Generator +from pathlib import Path + +from rust_oas_generator.generator.template_engine import RustCodeGenerator +from rust_oas_generator.parser.oas_parser import OASParser +from rust_oas_generator.utils.file_utils import write_files_to_disk + +# Exit codes for better error reporting +EXIT_SUCCESS = 0 +EXIT_FILE_NOT_FOUND = 1 +EXIT_INVALID_JSON = 2 +EXIT_GENERATION_ERROR = 3 + + +def parse_command_line_args(args: list[str] | None = None) -> argparse.Namespace: + """Create and configure the command line argument parser.""" + parser = argparse.ArgumentParser( + description="Generate Rust client from OpenAPI specification", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + %(prog)s spec.json + %(prog)s spec.json --output ./client --package-name my_client + %(prog)s spec.json --verbose + """, + ) + parser.add_argument( + "spec_file", + type=Path, + help="Path to OpenAPI specification file (JSON or YAML)", + metavar="SPEC_FILE", + ) + parser.add_argument( + "--output", + "-o", + type=Path, + default=Path("./generated"), + help="Output directory for generated files (default: %(default)s)", + dest="output_dir", + ) + parser.add_argument( + "--package-name", + "-p", + default="api_client", + help="Name for the generated Rust package (default: %(default)s)", + dest="package_name", + ) + parser.add_argument( + "--template-dir", + "-t", + type=Path, + help="Custom template directory (optional)", + dest="template_dir", + ) + parser.add_argument( + "--verbose", + "-v", + action="store_true", + help="Enable verbose output", + ) + parser.add_argument( + "--description", + "-d", + help="Custom description for the generated package (overrides spec description)", + dest="custom_description", + ) + + parsed_args = parser.parse_args(args) + + # Validate spec file exists + if not parsed_args.spec_file.exists(): + parser.error(f"Specification file not found: {parsed_args.spec_file}") + + return parsed_args + + +def print_verbose_info(*, operation_count: int, schema_count: int) -> None: + """Print verbose information about parsed specification.""" + print(f"Parsed {operation_count} operations") + print(f"Found {schema_count} schemas") + + +def print_generation_summary(*, file_count: int, files: dict[Path, str], output_dir: Path) -> None: + """Print summary of generated files.""" + print(f"Generated {file_count} files:") + for file_path in sorted(files.keys()): + print(f" {file_path}") + print(f"\nRust client generated successfully in {output_dir}") + + +@contextlib.contextmanager +def backup_and_clean_output_dir(output_dir: Path) -> Generator[None, None, None]: + """A context manager to backup and clean the output directory.""" + backup_dir = None + if output_dir.exists() and any(output_dir.iterdir()): + backup_dir = Path(tempfile.mkdtemp()) + shutil.copytree(output_dir, backup_dir, dirs_exist_ok=True) + + # Clean output directory before generation + if output_dir.exists(): + shutil.rmtree(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + try: + yield + except Exception: + if backup_dir: + print( + "Error: Generation failed. Restoring original content.", + file=sys.stderr, + ) + if output_dir.exists(): + shutil.rmtree(output_dir) + shutil.copytree(backup_dir, output_dir, dirs_exist_ok=True) + raise + finally: + if backup_dir: + shutil.rmtree(backup_dir) + + +def generate_rust_client_from_spec( + *, + spec_file: Path, + output_dir: Path, + package_name: str, + verbose: bool, + custom_description: str | None = None, +) -> dict[Path, str]: + """Generate Rust client from OpenAPI specification file.""" + # Parse OpenAPI specification + parser = OASParser() + parsed_spec = parser.parse_file(spec_file) + + if verbose: + print_verbose_info( + operation_count=len(parsed_spec.operations), + schema_count=len(parsed_spec.schemas), + ) + + # Generate Rust client files + generator = RustCodeGenerator() + return generator.generate_client( + parsed_spec, + output_dir, + package_name, + custom_description=custom_description, + ) + + +def main(args: list[str] | None = None) -> int: + """Generate Rust client from OpenAPI specification.""" + parsed_args = parse_command_line_args(args) + + try: + with backup_and_clean_output_dir(parsed_args.output_dir): + generated_files = generate_rust_client_from_spec( + spec_file=parsed_args.spec_file, + output_dir=parsed_args.output_dir, + package_name=parsed_args.package_name, + verbose=parsed_args.verbose, + custom_description=parsed_args.custom_description, + ) + + # Write files to disk + write_files_to_disk(generated_files) + + if parsed_args.verbose: + print_generation_summary( + file_count=len(generated_files), + files=generated_files, + output_dir=parsed_args.output_dir, + ) + else: + print(f"Rust client generated successfully in {parsed_args.output_dir}") + + return EXIT_SUCCESS + + except FileNotFoundError: + print(f"Error: Specification file not found: {parsed_args.spec_file}", file=sys.stderr) + return EXIT_FILE_NOT_FOUND + except json.JSONDecodeError as e: + print(f"Error: Invalid JSON in specification file: {e}", file=sys.stderr) + return EXIT_INVALID_JSON + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + if parsed_args.verbose: + traceback.print_exc() + return EXIT_GENERATION_ERROR + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/api/oas_generator/rust_oas_generator/generator/__init__.py b/api/oas_generator/rust_oas_generator/generator/__init__.py new file mode 100644 index 00000000..7e8f439b --- /dev/null +++ b/api/oas_generator/rust_oas_generator/generator/__init__.py @@ -0,0 +1,13 @@ +""" +Rust Code Generator Module + +This module provides Jinja2-based code generation for Rust API clients +from OpenAPI specifications. +""" + +from .template_engine import RustCodeGenerator, RustTemplateEngine + +__all__ = [ + "RustCodeGenerator", + "RustTemplateEngine", +] diff --git a/api/oas_generator/rust_oas_generator/generator/filters.py b/api/oas_generator/rust_oas_generator/generator/filters.py new file mode 100644 index 00000000..c40e367e --- /dev/null +++ b/api/oas_generator/rust_oas_generator/generator/filters.py @@ -0,0 +1,270 @@ +""" +Enhanced Jinja2 filters for Rust code generation with msgpack support. + +This module provides custom Jinja2 filters specifically designed for +generating Rust code from OpenAPI specifications. +""" + +from __future__ import annotations + +from typing import Any + +# Semantic versioning constants +_MIN_SEMVER_PARTS = 2 +_MAX_SEMVER_PARTS = 3 +_DEFAULT_VERSION = "0.1.0" + +# Documentation patterns for Rust +_DOC_BULLET_PREFIXES = frozenset({"* ", "- ", "+ "}) +_DOC_INDENT_PREFIX = "/// " +_DOC_NORMAL_PREFIX = "/// " + + +def rust_doc_comment(text: str, indent: int = 0) -> str: + """Convert text to Rust doc comment format. + + This function handles single-line and multi-line documentation, + applying proper Rust doc comment formatting with intelligent + indentation for bullet points. + + Args: + text: The text to convert to doc comments. + indent: Number of spaces for base indentation. + + Returns: + Formatted Rust doc comment string. + + Example: + >>> rust_doc_comment("This is a function") + '/// This is a function' + >>> rust_doc_comment("* Item 1\\n* Item 2") + '/// * Item 1\\n/// * Item 2' + """ + if not text: + return "" + + lines = text.strip().split("\n") + indent_str = " " * indent + + if len(lines) == 1: + return f"{indent_str}{_DOC_NORMAL_PREFIX}{lines[0]}" + + result: list[str] = [] + for i, line in enumerate(lines): + stripped_line = line.strip() + + # Check if this line is a bullet point + is_bullet = any(stripped_line.startswith(p) for p in _DOC_BULLET_PREFIXES) + + # Check if we need a blank line before this line for proper formatting + if ( + i > 0 + and stripped_line + and not is_bullet + and not result[-1].strip().endswith("///") + and any(lines[j].strip().startswith(p) for p in _DOC_BULLET_PREFIXES for j in range(max(0, i - 3), i)) + ): + # Add blank doc comment line before starting new paragraph after bullet points + result.append(f"{indent_str}///") + + prefix = _DOC_INDENT_PREFIX if is_bullet else _DOC_NORMAL_PREFIX + result.append(f"{indent_str}{prefix}{stripped_line}") + + return "\n".join(result) + + +def is_signed_transaction_field(vendor_extensions: dict[str, Any]) -> bool: + """Detect if this schema represents a SignedTransaction. + + Args: + vendor_extensions: Dictionary of vendor-specific extensions. + + Returns: + True if this schema represents a signed transaction. + """ + return bool(vendor_extensions.get("x-algokit-signed-txn", False)) + + +def needs_msgpack_trait(schema: dict[str, Any]) -> bool: + """Determine if schema needs AlgorandMsgpack trait implementation. + + Args: + schema: The schema dictionary to check. + + Returns: + True if the schema should implement the AlgorandMsgpack trait. + """ + vendor_extensions = schema.get("vendor_extensions", {}) + return any("msgpack" in key.lower() for key in vendor_extensions) + + +def get_dependencies_for_schema(schema: dict[str, Any]) -> list[str]: + """Get list of dependencies needed for this schema. + + Args: + schema: The schema dictionary to analyze. + + Returns: + List of import statements needed for this schema. + """ + dependencies = ["use serde::{Deserialize, Serialize};"] + + if schema.get("has_msgpack_fields", False): + dependencies.append("use serde_with::serde_as;") + + vendor_extensions = schema.get("vendor_extensions", {}) + if vendor_extensions.get("x-algokit-signed-txn"): + dependencies.extend( + [ + "use algokit_transact::SignedTransaction as AlgokitSignedTransaction;", + "use algokit_transact::AlgorandMsgpack;", + ] + ) + + return dependencies + + +def _parse_version_parts(version_str: str) -> list[str]: + """Parse version string into numeric parts. + + Args: + version_str: Version string to parse. + + Returns: + List of numeric version parts as strings. + """ + if not version_str: + return [] + + # Remove 'v' prefix and split by dots + cleaned_version = version_str.lstrip("v") + parts = [part.strip() for part in cleaned_version.split(".") if part.strip()] + + # Ensure all parts are numeric, replace invalid parts with "0" + return [part if part.isdigit() else "0" for part in parts] + + +def ensure_semver(version_str: str) -> str: + """Ensure version string is valid semantic versioning format. + + Args: + version_str: Version string to validate and format. + + Returns: + Valid semantic version string (e.g., "1.2.3"). + + Examples: + >>> ensure_semver("1") + '1.0.0' + >>> ensure_semver("1.2") + '1.2.0' + >>> ensure_semver("v1.2.3") + '1.2.3' + """ + if not version_str: + return _DEFAULT_VERSION + + parts = _parse_version_parts(version_str) + + if not parts: + return _DEFAULT_VERSION + + # Ensure we have exactly 3 parts (major.minor.patch) + match len(parts): + case 1: + parts.extend(["0", "0"]) + case 2: + parts.append("0") + case n if n > _MAX_SEMVER_PARTS: + parts = parts[:_MAX_SEMVER_PARTS] + + return ".".join(parts) + + +def semver_string(version: str) -> str: + """Format version string for Cargo.toml semver compatibility. + + This is an alias for ensure_semver to maintain backward compatibility. + + Args: + version: Version string to format. + + Returns: + Valid semantic version string. + """ + return ensure_semver(version) + + +def is_valid_rust_identifier(name: str) -> bool: + """Check if a string is a valid Rust identifier. + + Args: + name: String to check. + + Returns: + True if the string is a valid Rust identifier. + """ + return bool(name) and (name[0].isalpha() or name[0] == "_") and all(char.isalnum() or char == "_" for char in name) + + +def sanitize_rust_string_literal(text: str) -> str: + """Sanitize text for use in Rust string literals. + + Args: + text: Text to sanitize. + + Returns: + Sanitized text safe for Rust string literals. + """ + if not text: + return "" + + # Escape special characters + escape_map = { + "\\": "\\\\", + '"': '\\"', + "\n": "\\n", + "\t": "\\t", + } + + result = text + for char, escaped in escape_map.items(): + result = result.replace(char, escaped) + + return result + + +def http_method_enum(method: str) -> str: + """Convert HTTP method string to HttpMethod enum variant. + + Args: + method: HTTP method string (e.g., "GET", "POST"). + + Returns: + HttpMethod enum variant (e.g., "HttpMethod::Get", "HttpMethod::Post"). + """ + method_mapping = { + "GET": "HttpMethod::Get", + "POST": "HttpMethod::Post", + "PUT": "HttpMethod::Put", + "DELETE": "HttpMethod::Delete", + "PATCH": "HttpMethod::Patch", + "HEAD": "HttpMethod::Head", + "OPTIONS": "HttpMethod::Options", + } + + return method_mapping.get(method.upper(), f"HttpMethod::{method.title()}") + + +# Register filters that will be available in Jinja templates +FILTERS = { + "rust_doc_comment": rust_doc_comment, + "is_signed_transaction_field": is_signed_transaction_field, + "needs_msgpack_trait": needs_msgpack_trait, + "get_dependencies_for_schema": get_dependencies_for_schema, + "ensure_semver": ensure_semver, + "semver_string": semver_string, + "is_valid_rust_identifier": is_valid_rust_identifier, + "sanitize_rust_string_literal": sanitize_rust_string_literal, + "http_method_enum": http_method_enum, +} diff --git a/api/oas_generator/rust_oas_generator/generator/template_engine.py b/api/oas_generator/rust_oas_generator/generator/template_engine.py new file mode 100644 index 00000000..18fa0ef6 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/generator/template_engine.py @@ -0,0 +1,509 @@ +""" +Rust Template Engine for OpenAPI Client Generation + +This module uses Jinja2 templates to generate Rust API client code +from parsed OpenAPI specifications. +""" + +from __future__ import annotations + +from collections.abc import Callable +from functools import partial +from pathlib import Path +from typing import Any + +from jinja2 import Environment, FileSystemLoader, select_autoescape + +from rust_oas_generator.generator.filters import FILTERS +from rust_oas_generator.parser.oas_parser import ( + Operation, + Parameter, + ParsedSpec, + Response, + Schema, + rust_type_from_openapi, +) +from rust_oas_generator.utils.string_case import ( + normalize_rust_identifier as normalize_name, +) +from rust_oas_generator.utils.string_case import ( + rust_pascal_case, + rust_snake_case, +) + +# Constants for type checking +PRIMITIVE_TYPES = frozenset( + { + "String", + "str", + "u32", + "u64", + "f32", + "f64", + "bool", + "Vec", + "Vec", + "Vec", + "Vec", + "serde_json::Value", + "std::path::PathBuf", + "()", + } +) + +# Types that conflict with std types and need qualification +STD_CONFLICTING_TYPES = frozenset({"Box"}) + + +def qualify_type_name(type_name: str | None) -> str | None: + """Qualify type names that conflict with std types.""" + if not type_name: + return type_name + + # Extract base type from generics + base_type = type_name + if "<" in type_name: + base_type = type_name.split("<")[0] + + # If the base type conflicts with std types, qualify it + if base_type in STD_CONFLICTING_TYPES: + if "<" in type_name: + # Handle generic types like Vec + generic_part = type_name[type_name.index("<") :] + return f"crate::models::{base_type}{generic_part}" + return f"crate::models::{type_name}" + + return type_name + + +RUST_KEYWORDS = frozenset( + { + "box", + "type", + "match", + "fn", + "let", + "use", + "mod", + "struct", + "enum", + "impl", + "trait", + "true", + "false", + "if", + "else", + "while", + "for", + "loop", + "break", + "continue", + "return", + } +) + + +class ParameterEnumAnalyzer: + """Analyzes parameters to collect enum definitions.""" + + @staticmethod + def collect_parameter_enums(operations: list[Operation]) -> dict[str, dict[str, Any]]: + """Collect all unique parameter enums from operations.""" + enums = {} + + for operation in operations: + for param in operation.parameters: + if param.is_enum_parameter: + enum_name = param.rust_enum_type + if enum_name and enum_name not in enums: + enums[enum_name] = { + "enum_values": param.enum_values, + "description": param.description, + "parameter_name": param.name, + } + + return enums + + +class OperationAnalyzer: + """Analyzes operations for parameters, types, and responses.""" + + @staticmethod + def get_unique_tags(operations: list[Operation]) -> list[str]: + """Get unique tags from operations.""" + tags = {tag for op in operations for tag in op.tags} + return sorted(tags) + + @staticmethod + def group_operations_by_tag(operations: list[Operation]) -> dict[str, list[Operation]]: + """Group operations by their first tag.""" + groups: dict[str, list[Operation]] = {} + for operation in operations: + tag = operation.tags[0] if operation.tags else "default" + groups.setdefault(tag, []).append(operation) + return groups + + @staticmethod + def get_parameters_by_type(operation: Operation, param_type: str) -> list[Parameter]: + """Get parameters of specific type for an operation.""" + return [p for p in operation.parameters if p.param_type == param_type] + + @staticmethod + def has_parameter_type(operation: Operation, param_type: str) -> bool: + """Check if operation has parameters of given type.""" + return any(p.param_type == param_type for p in operation.parameters) + + @staticmethod + def get_request_body_type(operation: Operation) -> str | None: + """Get the request body type for an operation.""" + if not operation.request_body: + return None + + content = operation.request_body.get("content", {}) + if not content: + return None + + first_content_type = next(iter(content.keys())) + schema = content[first_content_type].get("schema", {}) + + if "$ref" in schema: + ref_name = schema["$ref"].split("/")[-1] + type_name = rust_pascal_case(ref_name) + return qualify_type_name(type_name) + + return rust_type_from_openapi(schema, {}) + + +class ResponseAnalyzer: + """Analyzes responses for types and error handling.""" + + @staticmethod + def is_error_status(status_code: str) -> bool: + """Check if status code represents an error.""" + return status_code.startswith(("4", "5")) or status_code == "default" + + @staticmethod + def get_success_response_type(operation: Operation) -> str | None: + """Get the success response type for an operation.""" + for status_code, response in operation.responses.items(): + if status_code.startswith("2"): + return qualify_type_name(response.rust_type) + return None + + @staticmethod + def get_error_types(operation: Operation) -> list[str]: + """Get error response types for an operation.""" + error_types = [] + for status_code, response in operation.responses.items(): + if ResponseAnalyzer.is_error_status(status_code): + qualified_type = qualify_type_name(response.rust_type) + error_type = f"Status{status_code}({qualified_type})" if qualified_type else f"Status{status_code}()" + error_types.append(error_type) + + if not any("DefaultResponse" in t for t in error_types): + error_types.append("DefaultResponse()") + error_types.append("UnknownValue(serde_json::Value)") + + return error_types + + @staticmethod + def get_response_types_by_filter( + operations: list[Operation], + filter_func: Callable[[str, Response], bool], + ) -> list[str]: + """Get response types filtered by a condition.""" + response_types: set[str] = set() + for operation in operations: + for status_code, response in operation.responses.items(): + if response.rust_type and filter_func(status_code, response): + response_types.add(response.rust_type) + return sorted(response_types) + + @classmethod + def get_all_response_types(cls, operations: list[Operation]) -> list[str]: + """Get all unique response types used across operations.""" + + def is_success_response(status_code: str, response: Response) -> bool: + return ( + status_code.startswith("2") + and response.rust_type is not None + and response.rust_type.endswith("Response") + ) + + return cls.get_response_types_by_filter(operations, is_success_response) + + +class TypeAnalyzer: + """Analyzes types for imports and dependencies.""" + + @staticmethod + def extract_base_type(type_str: str) -> str: + """Extract base type from Vec or Option.""" + if type_str.startswith("Vec<") and type_str.endswith(">"): + return type_str[4:-1] + if type_str.startswith("Option<") and type_str.endswith(">"): + return type_str[7:-1] + return type_str + + @classmethod + def should_import_request_body_type(cls, request_body_type: str) -> bool: + """Check if a request body type is a custom model that needs to be imported.""" + if not request_body_type or request_body_type in PRIMITIVE_TYPES or "<" in request_body_type: + return False + return request_body_type[0].isupper() and request_body_type.isalnum() + + @classmethod + def collect_types_from_responses(cls, operation: Operation, used_types: set[str]) -> None: + """Collect types from operation responses.""" + for _status_code, response in operation.responses.items(): + if response.rust_type: + base_type = cls.extract_base_type(response.rust_type) + if base_type not in PRIMITIVE_TYPES: + used_types.add(base_type) + + @classmethod + def collect_types_from_parameters(cls, operation: Operation, used_types: set[str]) -> None: + """Collect types from operation parameters.""" + for param in operation.parameters: + base_type = cls.extract_base_type(param.rust_type) + if base_type not in PRIMITIVE_TYPES: + used_types.add(base_type) + + @classmethod + def get_all_used_types(cls, operations: list[Operation]) -> list[str]: + """Get all unique custom types used across operations for imports.""" + used_types: set[str] = set() + for operation in operations: + cls.collect_types_from_responses(operation, used_types) + cls.collect_types_from_parameters(operation, used_types) + return sorted(used_types) + + @classmethod + def get_operation_used_types(cls, operation: Operation) -> list[str]: + """Get all unique custom types used by a single operation for imports.""" + used_types: set[str] = set() + cls.collect_types_from_responses(operation, used_types) + cls.collect_types_from_parameters(operation, used_types) + return sorted(used_types) + + +class RustTemplateEngine: + """Template engine for generating Rust code.""" + + def __init__(self, template_dir: Path | None = None) -> None: + """Initialize the template engine.""" + if template_dir is None: + current_dir = Path(__file__).parent + template_dir = current_dir.parent / "templates" + + self.template_dir = Path(template_dir) + self.env = Environment( + loader=FileSystemLoader(str(self.template_dir)), + autoescape=select_autoescape(["html", "xml"]), + trim_blocks=True, + lstrip_blocks=True, + ) + + self._register_filters() + self._register_globals() + + def _register_filters(self) -> None: + """Register custom Jinja2 filters for Rust code generation.""" + # Built-in filters + builtin_filters = { + "snake_case": rust_snake_case, + "pascal_case": rust_pascal_case, + "normalize_name": normalize_name, + "rust_type": lambda schema, schemas: rust_type_from_openapi(schema, schemas), + "rust_doc_comment": self._rust_doc_comment, + "rust_string_literal": self._rust_string_literal, + "rust_optional": self._rust_optional, + "rust_vec": self._rust_vec, + } + + # Register all filters + self.env.filters.update(builtin_filters) + self.env.filters.update(FILTERS) + + def _register_globals(self) -> None: + """Register global functions available in templates.""" + # Create analyzers + param_enum_analyzer = ParameterEnumAnalyzer() + op_analyzer = OperationAnalyzer() + resp_analyzer = ResponseAnalyzer() + type_analyzer = TypeAnalyzer() + + globals_map: dict[str, Any] = { + # Parameter enum analysis + "collect_parameter_enums": param_enum_analyzer.collect_parameter_enums, + # Operation analysis + "get_unique_tags": op_analyzer.get_unique_tags, + "group_operations_by_tag": op_analyzer.group_operations_by_tag, + # Response analysis + "get_error_types": resp_analyzer.get_error_types, + "get_success_response_type": resp_analyzer.get_success_response_type, + "get_all_response_types": resp_analyzer.get_all_response_types, + "get_endpoint_response_types": lambda op: resp_analyzer.get_all_response_types([op]), + # Type analysis + "get_all_used_types": type_analyzer.get_all_used_types, + "get_operation_used_types": type_analyzer.get_operation_used_types, + # Parameter-related functions + "has_format_parameter": lambda op: any(param.name == "format" for param in op.parameters), + "has_path_parameters": partial(op_analyzer.has_parameter_type, param_type="path"), + "has_query_parameters": partial(op_analyzer.has_parameter_type, param_type="query"), + "has_header_parameters": partial(op_analyzer.has_parameter_type, param_type="header"), + "get_path_parameters": partial(op_analyzer.get_parameters_by_type, param_type="path"), + "get_query_parameters": partial(op_analyzer.get_parameters_by_type, param_type="query"), + "get_header_parameters": partial(op_analyzer.get_parameters_by_type, param_type="header"), + # Request body functions + "has_request_body": lambda op: op.request_body is not None, + "get_request_body_type": op_analyzer.get_request_body_type, + "get_request_body_name": lambda op: "request" if op.request_body else None, + "is_request_body_required": lambda op: bool(op.request_body and op.request_body.get("required", False)), + "should_import_request_body_type": type_analyzer.should_import_request_body_type, + } + + self.env.globals.update(globals_map) + + def render_template(self, template_name: str, context: dict[str, Any]) -> str: + """Render a template with the given context.""" + template = self.env.get_template(template_name) + return template.render(**context) + + @staticmethod + def _rust_doc_comment(text: str, indent: int = 0) -> str: + """Format text as Rust doc comment.""" + if not text: + return "" + + lines = text.strip().split("\n") + prefix = " " * indent + "/// " + return "\n".join(prefix + line.strip() for line in lines) + + @staticmethod + def _rust_string_literal(text: str) -> str: + """Format text as Rust string literal.""" + escaped = text.replace("\\", "\\\\").replace('"', '\\"') + return f'"{escaped}"' + + @staticmethod + def _rust_optional(rust_type: str) -> str: + """Wrap Rust type in Option if not already optional.""" + return rust_type if rust_type.startswith("Option<") else f"Option<{rust_type}>" + + @staticmethod + def _rust_vec(rust_type: str) -> str: + """Wrap Rust type in Vec.""" + return f"Vec<{rust_type}>" + + +class RustCodeGenerator: + """Main code generator for Rust clients.""" + + def __init__(self, template_engine: RustTemplateEngine | None = None) -> None: + """Initialize the code generator.""" + self.template_engine = template_engine or RustTemplateEngine() + + def generate_client( + self, + spec: ParsedSpec, + output_dir: Path, + package_name: str = "api_client", + custom_description: str | None = None, + ) -> dict[Path, str]: + """Generate complete Rust client from OpenAPI spec.""" + output_dir = Path(output_dir) + context = { + "spec": spec, + "package_name": package_name, + "operations": spec.operations, + "schemas": spec.schemas, + "content_types": spec.content_types, + "custom_description": custom_description, + } + + files = {} + files.update(self._generate_base_files(context, output_dir)) + files.update(self._generate_model_files(spec.schemas, context, output_dir)) + files.update(self._generate_parameter_enums(spec.operations, context, output_dir)) + files.update(self._generate_api_files(spec.operations, context, output_dir)) + files.update(self._generate_project_files(context, output_dir)) + + return files + + def _generate_base_files(self, context: dict[str, Any], output_dir: Path) -> dict[Path, str]: + """Generate base library files.""" + src_dir = output_dir / "src" + return { + src_dir / "lib.rs": self.template_engine.render_template("base/lib.rs.j2", context), + } + + def _generate_model_files( + self, + schemas: dict[str, Schema], + context: dict[str, Any], + output_dir: Path, + ) -> dict[Path, str]: + """Generate model files.""" + files = {} + models_dir = output_dir / "src" / "models" + + for _, schema in schemas.items(): + model_context = {**context, "schema": schema} + content = self.template_engine.render_template("models/model.rs.j2", model_context) + + filename = f"{schema.rust_file_name}.rs" + files[models_dir / filename] = content + + models_context = {**context, "schemas": schemas} + files[models_dir / "mod.rs"] = self.template_engine.render_template("models/mod.rs.j2", models_context) + + return files + + def _generate_parameter_enums( + self, + operations: list[Operation], + context: dict[str, Any], + output_dir: Path, + ) -> dict[Path, str]: + """Generate parameter enum files.""" + files = {} + param_enums = ParameterEnumAnalyzer.collect_parameter_enums(operations) + + if param_enums: + apis_dir = output_dir / "src" / "apis" + enum_context = {**context, "parameter_enums": param_enums} + content = self.template_engine.render_template("apis/parameter_enums.rs.j2", enum_context) + files[apis_dir / "parameter_enums.rs"] = content + + return files + + def _generate_api_files( + self, + operations: list[Operation], + context: dict[str, Any], + output_dir: Path, + ) -> dict[Path, str]: + """Generate individual API files per endpoint.""" + files = {} + apis_dir = output_dir / "src" / "apis" + + for operation in operations: + endpoint_context = {**context, "operation": operation} + content = self.template_engine.render_template("apis/endpoint.rs.j2", endpoint_context) + files[apis_dir / f"{operation.rust_function_name}.rs"] = content + + client_context = {**context, "operations": operations} + files[apis_dir / "client.rs"] = self.template_engine.render_template("apis/client.rs.j2", client_context) + + api_context = {**context, "operations": operations} + files[apis_dir / "mod.rs"] = self.template_engine.render_template("apis/mod.rs.j2", api_context) + + return files + + def _generate_project_files(self, context: dict[str, Any], output_dir: Path) -> dict[Path, str]: + """Generate project configuration files.""" + return { + output_dir / "Cargo.toml": self.template_engine.render_template("base/Cargo.toml.j2", context), + output_dir / "README.md": self.template_engine.render_template("base/README.md.j2", context), + } diff --git a/api/oas_generator/rust_oas_generator/parser/__init__.py b/api/oas_generator/rust_oas_generator/parser/__init__.py new file mode 100644 index 00000000..b8b01d07 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/parser/__init__.py @@ -0,0 +1,37 @@ +""" +OpenAPI Parser Module for Rust Client Generation + +This module provides parsing capabilities for OpenAPI specifications +to extract information needed for Rust client generation. +""" + +from rust_oas_generator.utils.string_case import ( + normalize_rust_identifier as normalize_name, +) +from rust_oas_generator.utils.string_case import rust_pascal_case as pascal_case +from rust_oas_generator.utils.string_case import rust_snake_case as snake_case + +from .oas_parser import ( + OASParser, + Operation, + Parameter, + ParsedSpec, + Property, + Response, + Schema, + rust_type_from_openapi, +) + +__all__ = [ + "OASParser", + "Operation", + "Parameter", + "ParsedSpec", + "Property", + "Response", + "Schema", + "normalize_name", + "pascal_case", + "rust_type_from_openapi", + "snake_case", +] diff --git a/api/oas_generator/rust_oas_generator/parser/oas_parser.py b/api/oas_generator/rust_oas_generator/parser/oas_parser.py new file mode 100644 index 00000000..1cc62781 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/parser/oas_parser.py @@ -0,0 +1,904 @@ +""" +OpenAPI Specification Parser for Rust Client Generation. + +This module parses OpenAPI 3.x specifications and extracts information +needed to generate Rust API clients with comprehensive type mapping +and msgpack support. +""" + +import json +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Final + +from rust_oas_generator.utils.string_case import ( + escape_rust_keyword, + rust_pascal_case, + rust_snake_case, +) + +# Type mapping constants for OpenAPI to Rust conversion +_OPENAPI_TYPE_MAPPING: Final = { + "string": { + None: "String", + "date": "String", + "date-time": "String", + "byte": "String", + "binary": "Vec", + }, + "integer": { + None: "u64", + "int32": "u32", + "int64": "u64", + "uint64": "u64", + }, + "number": { + None: "f64", + "float": "f32", + "double": "f64", + }, + "boolean": { + None: "bool", + }, + "object": { + None: "serde_json::Value", + }, +} + +# Constants for integer type selection +_U32_MAX_VALUE: Final = 4294967295 # Value of u32::MAX +_SMALL_INTEGER_MAX: Final = 100 # Threshold for small bounded integers +_ENUM_KEYWORDS: Final = frozenset( + ["value `1`", "value `2`", "value 1", "value 2", "refers to", "type.", "action.", "enum"] +) + +# HTTP methods supported by OpenAPI +_HTTP_METHODS: Final = frozenset({"get", "post", "put", "delete", "patch", "head", "options"}) + +# Content types that indicate msgpack support +_MSGPACK_CONTENT_TYPES: Final = frozenset({"application/msgpack", "application/x-binary"}) + + +def _select_integer_rust_type(schema: dict[str, Any]) -> str: + """Select appropriate Rust integer type based on schema constraints. + + Args: + schema: OpenAPI schema dictionary for an integer type. + + Returns: + Rust type string: either "u32" or "u64". + """ + # Check if explicit format is provided + schema_format = schema.get("format") + if schema_format: + return _get_openapi_type_mapping("integer", schema_format) + + # Auto-detect u32 vs u64 based on constraints + maximum = schema.get("maximum") + minimum = schema.get("minimum") + + # Use u32 if maximum is within u32 range + if maximum is not None and maximum <= _U32_MAX_VALUE: + return "u32" + + # Use u32 for small bounded integers (common patterns) + if minimum is not None and minimum >= 0 and maximum is not None and maximum <= _SMALL_INTEGER_MAX: + return "u32" + + # Use u32 for enum-like descriptions (type discriminators) + description = schema.get("description", "").lower() + if any(keyword in description for keyword in _ENUM_KEYWORDS): + return "u32" + + # Default to u64 for potentially large blockchain values + return "u64" + + +def _extract_ref_name(ref_string: str) -> str: + """Extract the reference name from an OpenAPI $ref string. + + Args: + ref_string: The $ref value (e.g., "#/components/schemas/Model"). + + Returns: + The extracted reference name (e.g., "Model"). + """ + return ref_string.split("/")[-1] + + +def _get_openapi_type_mapping(schema_type: str, schema_format: str | None) -> str: + """Get Rust type mapping for OpenAPI schema type and format. + + Args: + schema_type: The OpenAPI schema type. + schema_format: The OpenAPI schema format (optional). + + Returns: + The corresponding Rust type. + """ + type_formats = _OPENAPI_TYPE_MAPPING.get(schema_type, {}) + if isinstance(type_formats, dict): + result = type_formats.get(schema_format, "String") + return str(result) if result is not None else "String" + return "String" + + +def rust_type_from_openapi( + schema: dict[str, Any], + schemas: dict[str, Any], + visited: set[str] | None = None, +) -> str: + """Convert OpenAPI schema type to Rust type string. + + Args: + schema: The schema dictionary from OpenAPI spec. + schemas: All available schemas for reference resolution. + visited: Set of visited references to prevent cycles. + + Returns: + Rust type string. + """ + if visited is None: + visited = set() + + # Handle x-algokit-bigint extension for u64 mapping + if schema.get("x-algokit-bigint") is True: + return "u64" + + # Handle references + if "$ref" in schema: + ref_name = _extract_ref_name(schema["$ref"]) + + if ref_name in visited: + return rust_pascal_case(ref_name) + + visited.add(ref_name) + + # Return the original name without ModelBox renaming + return rust_pascal_case(ref_name) + + schema_type = schema.get("type", "string") + + # Handle array types + if schema_type == "array": + items_schema = schema.get("items", {}) + items_type = rust_type_from_openapi(items_schema, schemas, visited) + return f"Vec<{items_type}>" + + # Smart integer type selection for non-bigint fields + if schema_type == "integer" and not schema.get("x-algokit-bigint"): + return _select_integer_rust_type(schema) + + # Handle primitive types + schema_format = schema.get("format") + return _get_openapi_type_mapping(schema_type, schema_format) + + +def detect_msgpack_field(prop_data: dict[str, Any]) -> bool: + """Detect if a property should use msgpack/base64 encoding. + + Args: + prop_data: The property data dictionary. + + Returns: + True if the property should use msgpack encoding. + """ + # Check format + if prop_data.get("format") == "byte": + return True + + # Check description for base64 indicator + description = prop_data.get("description", "").lower() + if "base64" in description: + return True + + # Check vendor extensions + return bool(prop_data.get("x-msgpack-encoding")) + + +def detect_msgpack_support_for_operation(operation_data: dict[str, Any]) -> bool: + """Detect if an operation supports msgpack content type or binary data. + + Args: + operation_data: The operation data dictionary. + + Returns: + True if the operation supports msgpack. + """ + # Check request body content types + request_body = operation_data.get("requestBody", {}) + content = request_body.get("content", {}) + + if any(ct in content for ct in _MSGPACK_CONTENT_TYPES): + return True + + # Check for binary format in request body + if "application/x-binary" in content: + binary_content = content["application/x-binary"] + schema = binary_content.get("schema", {}) + if schema.get("format") == "binary": + return True + + # Check response content types + responses = operation_data.get("responses", {}) + for response_data in responses.values(): + response_content = response_data.get("content", {}) + if any(ct in response_content for ct in _MSGPACK_CONTENT_TYPES): + return True + + return False + + +def should_implement_algokit_msgpack( + schema_data: dict[str, Any], + *, + operation_msgpack_support: bool = False, +) -> bool: + """Determine if a schema should implement AlgorandMsgpack trait. + + Args: + schema_data: The schema data dictionary. + operation_msgpack_support: Whether operations support msgpack. + + Returns: + True if the schema should implement AlgorandMsgpack. + """ + # Check schema-level vendor extensions + if schema_data.get("x-algokit-signed-txn", False): + return True + + # Check property-level vendor extensions + properties = schema_data.get("properties", {}) + for prop_data in properties.values(): + if prop_data.get("x-algokit-signed-txn", False): + return True + + # Check array items for signed transaction markers + if prop_data.get("type") == "array": + items = prop_data.get("items", {}) + if items.get("x-algokit-signed-txn", False): + return True + + return operation_msgpack_support + + +def rust_type_with_msgpack( + schema: dict[str, Any], + schemas: dict[str, Any], + visited: set[str] | None = None, +) -> str: + """Convert OpenAPI schema type to Rust type with msgpack considerations. + + Args: + schema: The schema dictionary. + schemas: All available schemas. + visited: Set of visited references. + + Returns: + Rust type string, using Vec for msgpack fields. + """ + return "Vec" if detect_msgpack_field(schema) else rust_type_from_openapi(schema, schemas, visited) + + +@dataclass +class Parameter: + """Represents an OpenAPI parameter.""" + + name: str + param_type: str + rust_type: str + required: bool + description: str | None = None + enum_values: list[str] = field(default_factory=list) + rust_name: str = field(init=False) + rust_field_name: str = field(init=False) + + def __post_init__(self) -> None: + self.rust_name = rust_snake_case(self.name) + self.rust_field_name = escape_rust_keyword(self.rust_name) + + @property + def rust_enum_type(self) -> str | None: + """Generate Rust enum type name if this parameter has enum constraints.""" + if not self.enum_values: + return None + return rust_pascal_case(self.name) + + @property + def is_enum_parameter(self) -> bool: + """Check if this parameter should use an enum type.""" + return bool(self.enum_values) + + @property + def effective_rust_type(self) -> str: + """Get the effective Rust type, using enum if available, otherwise the original rust_type.""" + if self.is_enum_parameter and self.rust_enum_type: + return self.rust_enum_type + return self.rust_type + + +@dataclass +class Response: + """Represents an OpenAPI response.""" + + status_code: str + description: str + rust_type: str | None = None + content_types: list[str] = field(default_factory=list) + supports_msgpack: bool = False + + +@dataclass +class Operation: + """Represents an OpenAPI operation.""" + + operation_id: str + method: str + path: str + summary: str | None + description: str | None + parameters: list[Parameter] + request_body: dict[str, Any] | None + responses: dict[str, Response] + tags: list[str] + rust_function_name: str = field(init=False) + rust_error_enum: str = field(init=False) + supports_msgpack: bool = False + request_body_supports_msgpack: bool = False + + def __post_init__(self) -> None: + self.rust_function_name = rust_snake_case(self.operation_id) + self.rust_error_enum = f"{rust_pascal_case(self.operation_id)}Error" + + +@dataclass +class Property: + """Represents a schema property.""" + + name: str + rust_type: str + required: bool + description: str | None = None + is_base64_encoded: bool = False + vendor_extensions: list[tuple[str, Any]] = field(default_factory=list) + format: str | None = None + items: "Property | None" = None + rust_name: str = field(init=False) + rust_field_name: str = field(init=False) + rust_type_with_msgpack: str = field(init=False) + is_msgpack_field: bool = field(init=False) + is_signed_transaction: bool = field(init=False) + + def __post_init__(self) -> None: + self.rust_name = rust_snake_case(self.name) + self.rust_field_name = escape_rust_keyword(self.rust_name) + self.rust_type_with_msgpack = "Vec" if self.is_base64_encoded else self.rust_type + self.is_msgpack_field = self.is_base64_encoded + + self.is_signed_transaction = any( + "x-algokit-signed-txn" in ext_name and ext_value for ext_name, ext_value in self.vendor_extensions + ) + + if self.items and hasattr(self.items, "vendor_extensions"): + self.is_signed_transaction = self.is_signed_transaction or any( + "x-algokit-signed-txn" in ext_name and ext_value for ext_name, ext_value in self.items.vendor_extensions + ) + + +@dataclass +class Schema: + """Represents an OpenAPI schema.""" + + name: str + schema_type: str + description: str | None + properties: list[Property] + required_fields: list[str] + vendor_extensions: dict[str, Any] = field(default_factory=dict) + rust_struct_name: str = field(init=False) + rust_file_name: str = field(init=False) + has_msgpack_fields: bool = field(init=False) + has_required_fields: bool = field(init=False) + implements_algokit_msgpack: bool = field(init=False) + has_signed_transaction_fields: bool = field(init=False) + + def __post_init__(self) -> None: + # Keep the original struct name without renaming + self.rust_struct_name = rust_pascal_case(self.name) + # Use _model suffix for file name only when there's a conflict (like Box) + self.rust_file_name = f"{self.name.lower()}_model" if self.name == "Box" else rust_snake_case(self.name) + self.has_msgpack_fields = any(prop.is_base64_encoded for prop in self.properties) + self.has_required_fields = len(self.required_fields) > 0 + self.has_signed_transaction_fields = any(prop.is_signed_transaction for prop in self.properties) + + +@dataclass +class ParsedSpec: + """Represents a parsed OpenAPI specification.""" + + info: dict[str, Any] + servers: list[dict[str, Any]] + operations: list[Operation] + schemas: dict[str, Schema] + content_types: list[str] + has_msgpack_operations: bool = False + + +class OASParser: + """Parser for OpenAPI 3.x specifications.""" + + def __init__(self) -> None: + self.spec_data: dict[str, Any] | None = None + self.schemas: dict[str, Any] = {} + self.msgpack_operations: list[str] = [] + + def parse_file(self, file_path: str | Path) -> ParsedSpec: + """Parse OpenAPI specification from file.""" + path = Path(file_path) + with path.open(encoding="utf-8") as f: + self.spec_data = json.load(f) + return self._parse_spec() + + def parse_dict(self, spec_dict: dict[str, Any]) -> ParsedSpec: + """Parse OpenAPI specification from dictionary.""" + self.spec_data = spec_dict + return self._parse_spec() + + def _parse_spec(self) -> ParsedSpec: + """Parse the loaded specification.""" + if not self.spec_data: + msg = "No specification data loaded" + raise ValueError(msg) + + self.schemas = self.spec_data.get("components", {}).get("schemas", {}) + + info = self.spec_data.get("info", {}) + servers = self.spec_data.get("servers", []) + operations = self._parse_operations() + schemas = self._parse_schemas() + content_types = self._extract_content_types() + + has_msgpack_operations = len(self.msgpack_operations) > 0 + self._update_schemas_for_msgpack(schemas, has_msgpack_operations=has_msgpack_operations) + + return ParsedSpec( + info=info, + servers=servers, + operations=operations, + schemas=schemas, + content_types=content_types, + has_msgpack_operations=has_msgpack_operations, + ) + + def _update_schemas_for_msgpack( + self, + schemas: dict[str, Schema], + *, + has_msgpack_operations: bool, + ) -> None: + """Update schemas to implement AlgorandMsgpack trait when appropriate.""" + if not has_msgpack_operations: + for schema_name, schema in schemas.items(): + raw_schema = self.schemas.get(schema_name, {}) + schema.implements_algokit_msgpack = should_implement_algokit_msgpack(raw_schema) + return + + dependency_graph = self._build_schema_dependency_graph() + root_msgpack_schemas = self._get_msgpack_root_schemas() + + all_msgpack_schemas = set() + queue = list(root_msgpack_schemas) + visited = set() + + while queue: + schema_name = queue.pop(0) + if schema_name in visited: + continue + visited.add(schema_name) + all_msgpack_schemas.add(schema_name) + + if schema_name in dependency_graph: + for dep in dependency_graph[schema_name]: + if dep not in visited: + queue.append(dep) + + for schema_name, schema in schemas.items(): + raw_schema = self.schemas.get(schema_name, {}) + is_msgpack_related = schema_name in all_msgpack_schemas + schema.implements_algokit_msgpack = should_implement_algokit_msgpack( + raw_schema, operation_msgpack_support=is_msgpack_related + ) + + def _get_msgpack_root_schemas(self) -> set[str]: + """Get the root schemas that require msgpack support.""" + root_schemas = self._get_msgpack_schemas_from_operations() + root_schemas.update(self._get_msgpack_schemas_from_extensions()) + + # Also include response schemas from msgpack operations that were created during parsing + # This covers cases where inline response schemas get converted to named schemas + if self.msgpack_operations and self.spec_data: + for path_item in self.spec_data.get("paths", {}).values(): + for method, op_data in path_item.items(): + if ( + method not in _HTTP_METHODS + or not detect_msgpack_support_for_operation(op_data) + or op_data.get("operationId") not in self.msgpack_operations + ): + continue + + # Check response schemas for this msgpack operation + for resp_data in op_data.get("responses", {}).values(): + content = resp_data.get("content", {}) + for ct, _ in content.items(): + if ct in _MSGPACK_CONTENT_TYPES: + operation_id = op_data.get("operationId") + if operation_id and operation_id in self.schemas: + root_schemas.add(operation_id) + + return root_schemas + + def _get_msgpack_schemas_from_operations(self) -> set[str]: + """Get schemas used in request/response bodies of msgpack-enabled operations.""" + schemas: set[str] = set() + if not self.spec_data: + return schemas + + for path_item in self.spec_data.get("paths", {}).values(): + for method, op_data in path_item.items(): + if method not in _HTTP_METHODS or not detect_msgpack_support_for_operation(op_data): + continue + + # Request body + request_body = op_data.get("requestBody", {}) + content = request_body.get("content", {}) + for ct, cd in content.items(): + if ct in _MSGPACK_CONTENT_TYPES and "$ref" in cd.get("schema", {}): + schemas.add(_extract_ref_name(cd["schema"]["$ref"])) + + # Response bodies + for resp_data in op_data.get("responses", {}).values(): + content = resp_data.get("content", {}) + for ct, cd in content.items(): + if ct in _MSGPACK_CONTENT_TYPES and "$ref" in cd.get("schema", {}): + schemas.add(_extract_ref_name(cd["schema"]["$ref"])) + return schemas + + def _get_msgpack_schemas_from_extensions(self) -> set[str]: + """Get schemas with x-algokit-signed-txn vendor extension.""" + return { + schema_name + for schema_name, schema_data in self.schemas.items() + if should_implement_algokit_msgpack(schema_data) + } + + def _build_schema_dependency_graph(self) -> dict[str, set[str]]: + """Build a dependency graph for all schemas.""" + dependency_graph = {} + for schema_name, schema_data in self.schemas.items(): + dependency_graph[schema_name] = self._extract_refs_from_schema_part(schema_data) + return dependency_graph + + def _extract_refs_from_schema_part(self, schema_part: Any) -> set[str]: # noqa: ANN401 + """Extract all schema references from a part of a schema.""" + refs = set() + if isinstance(schema_part, dict): + refs.update(self._extract_refs_from_dict(schema_part)) + elif isinstance(schema_part, list): + for item in schema_part: + refs.update(self._extract_refs_from_schema_part(item)) + return refs + + def _extract_refs_from_dict(self, schema_dict: dict[str, Any]) -> set[str]: + """Helper to extract refs from a dictionary part of a schema.""" + refs = set() + if "$ref" in schema_dict: + refs.add(_extract_ref_name(schema_dict["$ref"])) + + if "properties" in schema_dict and isinstance( + schema_dict["properties"], + dict, + ): + for prop_schema in schema_dict["properties"].values(): + refs.update(self._extract_refs_from_schema_part(prop_schema)) + + if "items" in schema_dict: + refs.update(self._extract_refs_from_schema_part(schema_dict["items"])) + + for key in ("allOf", "oneOf", "anyOf"): + if key in schema_dict and isinstance(schema_dict[key], list): + for sub_schema in schema_dict[key]: + refs.update(self._extract_refs_from_schema_part(sub_schema)) + return refs + + def _parse_operations(self) -> list[Operation]: + """Parse all operations from paths.""" + operations: list[Operation] = [] + if not self.spec_data: + return operations + paths = self.spec_data.get("paths", {}) + + for path, path_item in paths.items(): + for method, operation_data in path_item.items(): + if method.lower() in _HTTP_METHODS: + operation = self._parse_operation( + path, + method.upper(), + operation_data, + ) + if operation: + operations.append(operation) + + if operation.supports_msgpack: + self.msgpack_operations.append(operation.operation_id) + + return operations + + def _parse_operation( + self, + path: str, + method: str, + operation_data: dict[str, Any], + ) -> Operation | None: + """Parse a single operation.""" + operation_id = operation_data.get("operationId") + if not operation_id: + return None + + supports_msgpack = detect_msgpack_support_for_operation(operation_data) + request_body_supports_msgpack = self._check_request_body_msgpack_support( + operation_data, + ) + + parameters = [] + for param_data in operation_data.get("parameters", []): + param = self._parse_parameter(param_data) + if param: + parameters.append(param) + + responses = {} + for status_code, response_data in operation_data.get("responses", {}).items(): + response = self._parse_response(status_code, response_data, operation_id) + responses[status_code] = response + + return Operation( + operation_id=operation_id, + method=method, + path=path, + summary=operation_data.get("summary"), + description=operation_data.get("description"), + parameters=parameters, + request_body=operation_data.get("requestBody"), + responses=responses, + tags=operation_data.get("tags", []), + supports_msgpack=supports_msgpack, + request_body_supports_msgpack=request_body_supports_msgpack, + ) + + def _check_request_body_msgpack_support( + self, + operation_data: dict[str, Any], + ) -> bool: + """Check if request body supports msgpack or binary transmission.""" + request_body = operation_data.get("requestBody", {}) + content = request_body.get("content", {}) + + if "application/msgpack" in content: + return True + + if "application/x-binary" in content: + binary_content = content["application/x-binary"] + schema = binary_content.get("schema", {}) + format_value: str | None = schema.get("format") + return format_value == "binary" + + return False + + def _parse_parameter(self, param_data: dict[str, Any]) -> Parameter | None: + """Parse a parameter.""" + if "$ref" in param_data: + param_data = self._resolve_reference(param_data["$ref"]) + + name = param_data.get("name") + if not name: + return None + + schema = param_data.get("schema", {}) + rust_type = rust_type_from_openapi(schema, self.schemas, set()) + enum_values = schema.get("enum", []) if schema.get("type") == "string" else [] + + return Parameter( + name=name, + param_type=param_data.get("in", "query"), + rust_type=rust_type, + required=param_data.get("required", False), + description=param_data.get("description"), + enum_values=enum_values, + ) + + def _resolve_reference(self, ref: str) -> dict[str, Any]: + """Resolve a JSON reference.""" + if not self.spec_data: + return {} + + ref_path = ref.split("/") + resolved: dict[str, Any] | None = self.spec_data + for part in ref_path[1:]: # Skip '#' + if resolved is None: + return {} + resolved = resolved.get(part) + return resolved or {} + + def _parse_response( + self, + status_code: str, + response_data: dict[str, Any], + operation_id: str, + ) -> Response: + """Parse a response.""" + content = response_data.get("content", {}) + content_types = list(content.keys()) + supports_msgpack = "application/msgpack" in content_types + + rust_type = self._determine_response_rust_type( + content_types, + content, + status_code, + operation_id, + response_data, + ) + + return Response( + status_code=status_code, + description=response_data.get("description", ""), + rust_type=rust_type, + content_types=content_types, + supports_msgpack=supports_msgpack, + ) + + def _determine_response_rust_type( + self, + content_types: list[str], + content: dict[str, Any], + status_code: str, + operation_id: str, + response_data: dict[str, Any], + ) -> str | None: + """Determine the Rust type for a response.""" + if not content_types: + return None + + first_content = content[content_types[0]] + schema = first_content.get("schema", {}) + + if self._should_create_response_model(schema, status_code): + response_model_name = operation_id + + self.schemas[response_model_name] = self._create_response_schema( + response_model_name, + schema, + response_data.get("description", ""), + ) + + return rust_pascal_case(response_model_name) + + return rust_type_from_openapi(schema, self.schemas, set()) + + def _should_create_response_model( + self, + schema: dict[str, Any], + status_code: str, + ) -> bool: + """Determine if we should create a response model for this schema.""" + if not status_code.startswith("2") or "$ref" in schema: + return False + + if schema.get("type") == "object" and "properties" in schema: + return True + + return bool("required" in schema or "allOf" in schema or "oneOf" in schema) + + def _create_response_schema( + self, + _name: str, + schema: dict[str, Any], + description: str, + ) -> dict[str, Any]: + """Create a response schema from an inline schema.""" + response_schema = schema.copy() + if description and "description" not in response_schema: + response_schema["description"] = description + return response_schema + + def _parse_schemas(self) -> dict[str, Schema]: + """Parse all schemas.""" + schemas = {} + + for schema_name, schema_data in self.schemas.items(): + schema = self._parse_schema(schema_name, schema_data) + if schema: + schemas[schema_name] = schema + + return schemas + + def _parse_schema(self, name: str, schema_data: dict[str, Any]) -> Schema | None: + """Parse a single schema.""" + schema_type = schema_data.get("type", "object") + properties_data = schema_data.get("properties", {}) + required_fields = schema_data.get("required", []) + + # Extract vendor extensions + vendor_extensions = {} + for key, value in schema_data.items(): + if key.startswith("x-"): + vendor_extensions[key] = value + + properties = [] + for prop_name, prop_data in properties_data.items(): + rust_type = rust_type_from_openapi(prop_data, self.schemas, set()) + is_base64_encoded = detect_msgpack_field(prop_data) + + # Extract vendor extensions for this property + prop_vendor_extensions = [] + for key, value in prop_data.items(): + if key.startswith("x-"): + prop_vendor_extensions.append((key, value)) + + # Handle array items with vendor extensions + items_property = None + if prop_data.get("type") == "array" and "items" in prop_data: + items_data = prop_data["items"] + items_vendor_extensions = [] + for key, value in items_data.items(): + if key.startswith("x-"): + items_vendor_extensions.append((key, value)) + + items_property = Property( + name=f"{prop_name}_item", + rust_type=rust_type_from_openapi(items_data, self.schemas, set()), + required=False, + description=items_data.get("description"), + is_base64_encoded=detect_msgpack_field(items_data), + vendor_extensions=items_vendor_extensions, + format=items_data.get("format"), + ) + + prop = Property( + name=prop_name, + rust_type=rust_type, + required=prop_name in required_fields, + description=prop_data.get("description"), + is_base64_encoded=is_base64_encoded, + vendor_extensions=prop_vendor_extensions, + format=prop_data.get("format"), + items=items_property, + ) + properties.append(prop) + + return Schema( + name=name, + schema_type=schema_type, + description=schema_data.get("description"), + properties=properties, + required_fields=required_fields, + vendor_extensions=vendor_extensions, + ) + + def _extract_content_types(self) -> list[str]: + """Extract all content types used in the API.""" + content_types = set() + + if not self.spec_data: + return [] + + for path_item in self.spec_data.get("paths", {}).values(): + for operation in path_item.values(): + if isinstance(operation, dict): + request_body = operation.get("requestBody", {}) + content = request_body.get("content", {}) + content_types.update(content.keys()) + + for response in operation.get("responses", {}).values(): + response_content = response.get("content", {}) + content_types.update(response_content.keys()) + + return sorted(content_types) diff --git a/api/oas_generator/rust_oas_generator/templates/apis/api.rs.j2 b/api/oas_generator/rust_oas_generator/templates/apis/api.rs.j2 new file mode 100644 index 00000000..3ecdfe7a --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/apis/api.rs.j2 @@ -0,0 +1,119 @@ +/* + * {{ spec.info.title }} + * + * {{ spec.info.description or "API client generated from OpenAPI specification" }} + * + * The version of the OpenAPI document: {{ spec.info.version }} + {% if spec.info.contact and spec.info.contact.email %} * Contact: {{ spec.info.contact.email }} + {% endif %} * Generated by: Rust OpenAPI Generator + */ + +use reqwest; +use serde::{Deserialize, Serialize, de::Error as _}; +use crate::{apis::ResponseContent, models}; +use super::{Error, configuration, ContentType}; + +// Import all response types +{% set used_types = get_all_used_types(operations) %} +{% if used_types %} +use crate::models::{ +{% for used_type in used_types %} + {{ used_type }}, +{% endfor %} +}; +{% endif %} + +{% for operation in operations %} +/// struct for typed errors of method [`{{ operation.rust_function_name }}`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum {{ operation.rust_error_enum }} { +{% for error_type in get_error_types(operation) %} + {{ error_type }}, +{% endfor %} +} + +{% endfor %} + +{% for operation in operations %} +{% if operation.description %} +{{ operation.description | rust_doc_comment }} +{% elif operation.summary %} +{{ operation.summary | rust_doc_comment }} +{% endif %} +pub async fn {{ operation.rust_function_name }}( + configuration: &configuration::Configuration, +{% for param in operation.parameters %} + {% if param.required %}{{ param.rust_name }}: {% if param.rust_type == "String" %}&str{% else %}{{ param.rust_type }}{% endif %}, + {% else %}{{ param.rust_name }}: Option<{% if param.rust_type == "String" %}&str{% else %}{{ param.rust_type }}{% endif %}>, + {% endif %} +{% endfor %} +) -> Result<{% if get_success_response_type(operation) %}{{ get_success_response_type(operation) }}{% else %}(){% endif %}, Error<{{ operation.rust_error_enum }}>> { + // add a prefix to parameters to efficiently prevent name collisions +{% for param in operation.parameters %} + let p_{{ param.rust_name }} = {{ param.rust_name }}; +{% endfor %} + + let uri_str = format!("{}{{ operation.path | replace("-", "_") }}", configuration.base_path{% if has_path_parameters(operation) %}, {% for param in get_path_parameters(operation) %}{{ param.name | replace("-", "_") }}={% if param.rust_type in ["i32", "i64", "u32", "u64", "f32", "f64"] %}p_{{ param.rust_name }}{% else %}crate::apis::urlencode(p_{{ param.rust_name }}){% endif %}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}); + let mut req_builder = configuration.client.request(reqwest::Method::{{ operation.method }}, &uri_str); + +{% if has_query_parameters(operation) %} +{% for param in get_query_parameters(operation) %} + {% if param.required %} + req_builder = req_builder.query(&[("{{ param.name }}", &p_{{ param.rust_name }}.to_string())]); + {% else %} + if let Some(ref param_value) = p_{{ param.rust_name }} { + req_builder = req_builder.query(&[("{{ param.name }}", ¶m_value.to_string())]); + } + {% endif %} +{% endfor %} +{% endif %} + + if let Some(ref user_agent) = configuration.user_agent { + req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); + } + if let Some(ref apikey) = configuration.api_key { + let key = apikey.key.clone(); + let value = match apikey.prefix { + Some(ref prefix) => format!("{} {}", prefix, key), + None => key, + }; + req_builder = req_builder.header("X-Algo-API-Token", value); + }; + + let req = req_builder.build()?; + let resp = configuration.client.execute(req).await?; + + let status = resp.status(); + {% if get_success_response_type(operation) %} + let content_type = resp + .headers() + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/octet-stream"); + let content_type = super::ContentType::from(content_type); + {% endif %} + + if !status.is_client_error() && !status.is_server_error() { + {% if get_success_response_type(operation) %} + match content_type { + ContentType::Json => { + let content = resp.text().await?; + serde_json::from_str(&content).map_err(Error::from) + }, + ContentType::Text => Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `{{ get_success_response_type(operation) }}`"))), + ContentType::Unsupported(unknown_type) => Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `{{ get_success_response_type(operation) }}`")))), + } + {% else %} + let _content = resp.text().await?; + Ok(()) + {% endif %} + } else { + let content = resp.text().await?; + let entity: Option<{{ operation.rust_error_enum }}> = serde_json::from_str(&content).ok(); + Err(Error::ResponseError(ResponseContent { status, content, entity })) + } +} + +{% endfor %} + diff --git a/api/oas_generator/rust_oas_generator/templates/apis/client.rs.j2 b/api/oas_generator/rust_oas_generator/templates/apis/client.rs.j2 new file mode 100644 index 00000000..395966e9 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/apis/client.rs.j2 @@ -0,0 +1,132 @@ +/* + * {{ spec.info.title }} + * + * {{ spec.info.description or "API client generated from OpenAPI specification" }} + * + * The version of the OpenAPI document: {{ spec.info.version }} + {% if spec.info.contact and spec.info.contact.email %} * Contact: {{ spec.info.contact.email }} + {% endif %} * Generated by: Rust OpenAPI Generator + */ + +use super::Error; +use algokit_http_client::{DefaultHttpClient, HttpClient}; +use std::sync::Arc; +{% if collect_parameter_enums(operations) %} +use super::parameter_enums::*; +{% endif %} +{% if operations %} +{% set used_types = [] %} +{% for operation in operations %} + {% set operation_types = get_operation_used_types(operation) %} + {% for type_name in operation_types %} + {% if type_name not in used_types %} + {% set _ = used_types.append(type_name) %} + {% endif %} + {% endfor %} + {# Also collect request body types #} + {% if has_request_body(operation) %} + {% set request_body_type = get_request_body_type(operation) %} + {% if request_body_type and should_import_request_body_type(request_body_type) and request_body_type not in used_types %} + {% set _ = used_types.append(request_body_type) %} + {% endif %} + {% endif %} +{% endfor %} +{% if used_types %} +use crate::models::{ +{% for used_type in used_types %} + {{ used_type }}, +{% endfor %} +}; +{% endif %} +{% endif %} + +/// The main Algod API client. +/// +/// This client provides convenient access to all Algod API endpoints. +/// It wraps the lower-level endpoint functions with a more ergonomic interface. +/// All methods return a unified `Error` type that can represent any endpoint error. +#[derive(Clone)] +pub struct AlgodClient { + http_client: Arc, +} + +impl AlgodClient { + /// Create a new AlgodClient with a custom http client. + pub fn new(http_client: Arc) -> Self { + Self { http_client } + } + + /// Create a new AlgodClient for Algorand TestNet. + #[cfg(feature = "default_client")] + pub fn testnet() -> Self { + let http_client = + Arc::new(DefaultHttpClient::new("https://testnet-api.4160.nodely.dev")); + Self::new(http_client) + } + + /// Create a new AlgodClient for Algorand MainNet. + #[cfg(feature = "default_client")] + pub fn mainnet() -> Self { + let http_client = + Arc::new(DefaultHttpClient::new("https://mainnet-api.4160.nodely.dev")); + Self::new(http_client) + } + + /// Create a new AlgodClient for a local localnet environment. + #[cfg(feature = "default_client")] + pub fn localnet() -> Self { + let http_client = Arc::new(DefaultHttpClient::with_header( + "http://localhost:4001", + "X-Algo-API-Token", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ).expect("Failed to create HTTP client with X-Algo-API-Token header")); + Self::new(http_client) + } + +{% for operation in operations %} + {% if operation.summary %} + /// {{ operation.summary }} + {% elif operation.description %} + /// {{ operation.description | replace('\n', '\n /// ') }} + {% endif %} + {% if operation.deprecated %} + #[deprecated] + {% endif %} + pub async fn {{ operation.rust_function_name }}( + &self, + {% if has_request_body(operation) %} + {% set request_body_name = get_request_body_name(operation) %} + {% set request_body_type = get_request_body_type(operation) %} + {% if is_request_body_required(operation) %}{{ request_body_name }}: {{ request_body_type }}, + {% else %}{{ request_body_name }}: Option<{{ request_body_type }}>, + {% endif %} + {% endif %} + {% for param in operation.parameters %} + {% if param.is_enum_parameter %} + {% if param.required %}{{ param.rust_name }}: {{ param.rust_enum_type }}, + {% else %}{{ param.rust_name }}: Option<{{ param.rust_enum_type }}>, + {% endif %} + {% else %} + {% if param.required %}{{ param.rust_name }}: {% if param.rust_type == "String" %}&str{% else %}{{ param.rust_type }}{% endif %}, + {% else %}{{ param.rust_name }}: Option<{% if param.rust_type == "String" %}&str{% else %}{{ param.rust_type }}{% endif %}>, + {% endif %} + {% endif %} + {% endfor %} + ) -> Result<{% if get_success_response_type(operation) %}{{ get_success_response_type(operation) }}{% else %}(){% endif %}, Error> { + super::{{ operation.rust_function_name }}::{{ operation.rust_function_name }}( + self.http_client.as_ref(), + {% if has_request_body(operation) %} + {{ get_request_body_name(operation) }}, + {% endif %} + {% for param in operation.parameters %} + {% if param.is_enum_parameter %} + {{ param.rust_name }}, + {% else %} + {{ param.rust_name }}, + {% endif %} + {% endfor %} + ).await + } + +{% endfor %} +} diff --git a/api/oas_generator/rust_oas_generator/templates/apis/endpoint.rs.j2 b/api/oas_generator/rust_oas_generator/templates/apis/endpoint.rs.j2 new file mode 100644 index 00000000..af6476de --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/apis/endpoint.rs.j2 @@ -0,0 +1,195 @@ +/* + * {{ spec.info.title }} + * + * {{ spec.info.description or "API client generated from OpenAPI specification" }} + * + * The version of the OpenAPI document: {{ spec.info.version }} + {% if spec.info.contact and spec.info.contact.email %} * Contact: {{ spec.info.contact.email }} + {% endif %} * Generated by: Rust OpenAPI Generator + */ + +use serde::{Deserialize, Serialize}; +use algokit_http_client::{HttpClient, HttpMethod}; +use std::collections::HashMap; + +use super::{Error, AlgodApiError, ContentType}; +{% if operation.supports_msgpack %} +use algokit_transact::AlgorandMsgpack; +{% endif %} +{% set param_enums = collect_parameter_enums([operation]) %} +{% if param_enums %} +use super::parameter_enums::*; +{% endif %} + +// Import all custom types used by this endpoint +{% set used_types = get_operation_used_types(operation) %} +{% if used_types %} +use crate::models::{ +{% for used_type in used_types %} + {{ used_type }}, +{% endfor %} +}; +{% endif %} + +// Import request body type if needed +{% if has_request_body(operation) %} +{% set request_body_type = get_request_body_type(operation) %} +{% if should_import_request_body_type(request_body_type) %} +use crate::models::{{ request_body_type }}; +{% endif %} +{% endif %} + +/// struct for typed errors of method [`{{ operation.rust_function_name }}`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum {{ operation.rust_error_enum }} { +{% set error_types = get_error_types(operation) %} +{% for error_type in error_types %} + {{ error_type }}, +{% endfor %} +} + +{% if operation.description %} +{{ operation.description | rust_doc_comment }} +{% elif operation.summary %} +{{ operation.summary | rust_doc_comment }} +{% endif %} +pub async fn {{ operation.rust_function_name }}( + http_client: &dyn HttpClient, +{% if has_request_body(operation) %} + {% set request_body_name = get_request_body_name(operation) %} + {% set request_body_type = get_request_body_type(operation) %} + {% if is_request_body_required(operation) %}{{ request_body_name }}: {{ request_body_type }}, + {% else %}{{ request_body_name }}: Option<{{ request_body_type }}>, + {% endif %} +{% endif %} +{% for param in operation.parameters %} + {% if param.is_enum_parameter %} + {% if param.required %}{{ param.rust_name }}: {{ param.rust_enum_type }}, + {% else %}{{ param.rust_name }}: Option<{{ param.rust_enum_type }}>, + {% endif %} + {% else %} + {% if param.required %}{{ param.rust_name }}: {% if param.rust_type == "String" %}&str{% else %}{{ param.rust_type }}{% endif %}, + {% else %}{{ param.rust_name }}: Option<{% if param.rust_type == "String" %}&str{% else %}{{ param.rust_type }}{% endif %}>, + {% endif %} + {% endif %} +{% endfor %} + +) -> Result<{% if get_success_response_type(operation) %}{{ get_success_response_type(operation) }}{% else %}(){% endif %}, Error> { + {% for param in operation.parameters %} + let p_{{ param.rust_name }} = {{ param.rust_name }}; + {% endfor %} + {% if has_request_body(operation) %} + let p_{{ get_request_body_name(operation) }} = {{ get_request_body_name(operation) }}; + {% endif %} + + {% set rust_path = operation.path.replace('-', '_') %} + {% set path_params = get_path_parameters(operation) %} + {% if path_params %} + let path = format!("{{ rust_path }}", {% for p in path_params %}{{p.rust_name}}={% if p.rust_type == "String" %}crate::apis::urlencode(p_{{p.rust_name}}){% else %}p_{{p.rust_name}}{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}); + {% else %} + let path = "{{ rust_path }}".to_string(); + {% endif %} + + let{% if get_query_parameters(operation) %} mut{% endif %} query_params: HashMap = HashMap::new(); + {% for param in get_query_parameters(operation) %} + {% if param.required %} + query_params.insert("{{ param.name }}".to_string(), p_{{ param.rust_name }}.to_string()); + {% else %} + if let Some(value) = p_{{ param.rust_name }} { + query_params.insert("{{ param.name }}".to_string(), value.to_string()); + } + {% endif %} + {% endfor %} + + {% if not operation.supports_msgpack %} + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = {% if has_request_body(operation) %} + Some(serde_json::to_vec(&p_{{ get_request_body_name(operation) }}).map_err(|e| Error::Serde(e.to_string()))?) + {% else %} + None + {% endif %}; + {% elif not has_format_parameter(operation) %} + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/msgpack".to_string()); + headers.insert("Accept".to_string(), "application/msgpack".to_string()); + + let body = {% if has_request_body(operation) %} + {% if operation.request_body_supports_msgpack %} + {% if request_body_type == "Vec" %} + Some(p_{{ get_request_body_name(operation) }}) + {% else %} + Some(rmp_serde::to_vec_named(&p_{{ get_request_body_name(operation) }}).map_err(|e| Error::Serde(e.to_string()))?) + {% endif %} + {% else %} + Some(serde_json::to_vec(&p_{{ get_request_body_name(operation) }}).map_err(|e| Error::Serde(e.to_string()))?) + {% endif %} + {% else %} + None + {% endif %}; + {% else %} + let use_msgpack = {% for param in operation.parameters if param.name == "format" %}{% if param.is_enum_parameter %}p_{{ param.rust_name }}.map(|f| f != Format::Json).unwrap_or(true){% else %}p_{{ param.rust_name }}.map(|f| f != "json").unwrap_or(true){% endif %}{% endfor %}; + + let mut headers: HashMap = HashMap::new(); + if use_msgpack { + headers.insert("Content-Type".to_string(), "application/msgpack".to_string()); + headers.insert("Accept".to_string(), "application/msgpack".to_string()); + } else { + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + } + + let body = {% if has_request_body(operation) %} + {% if operation.request_body_supports_msgpack %} + if use_msgpack { + {% if request_body_type == "Vec" %} + Some(p_{{ get_request_body_name(operation) }}) + {% else %} + Some(rmp_serde::to_vec_named(&p_{{ get_request_body_name(operation) }}).map_err(|e| Error::Serde(e.to_string()))?) + {% endif %} + } else { + Some(serde_json::to_vec(&p_{{ get_request_body_name(operation) }}).map_err(|e| Error::Serde(e.to_string()))?) + } + {% else %} + Some(serde_json::to_vec(&p_{{ get_request_body_name(operation) }}).map_err(|e| Error::Serde(e.to_string()))?) + {% endif %} + {% else %} + None + {% endif %}; + {% endif %} + + let response = http_client + .request( + {{ operation.method | http_method_enum }}, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + {% if get_success_response_type(operation) %} + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())), + ContentType::MsgPack => rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())), + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + }, + ContentType::Unsupported(ct) => Err(Error::Serde(format!("Unsupported content type: {}", ct))), + } + {% else %} + let _ = response; + Ok(()) + {% endif %} +} diff --git a/api/oas_generator/rust_oas_generator/templates/apis/mod.rs.j2 b/api/oas_generator/rust_oas_generator/templates/apis/mod.rs.j2 new file mode 100644 index 00000000..5883df06 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/apis/mod.rs.j2 @@ -0,0 +1,96 @@ +/* + * {{ spec.info.title }} + * + * {{ spec.info.description or "API client generated from OpenAPI specification" }} + * + * The version of the OpenAPI document: {{ spec.info.version }} + {% if spec.info.contact and spec.info.contact.email %} * Contact: {{ spec.info.contact.email }} + {% endif %} * Generated by: Rust OpenAPI Generator + */ + +// Consolidated client +pub mod client; + +{% set param_enums = collect_parameter_enums(operations) %} +{% if param_enums %} +// Parameter enums for type-safe API parameters +pub mod parameter_enums; +{% endif %} + +// Individual endpoint modules +{% for operation in operations %} +pub mod {{ operation.rust_function_name }}; +{% endfor %} + +/// Unified error type that can represent any API error from any endpoint +#[derive(Debug, thiserror::Error)] +pub enum AlgodApiError { +{% for operation in operations %} + #[error("{{ operation.rust_function_name | title }} error: {0:?}")] + {{ operation.rust_function_name | pascal_case }}({{ operation.rust_function_name }}::{{ operation.rust_error_enum }}), +{% endfor %} + #[error("Unknown API error: {0}")] + Unknown(String), +} + +{% for operation in operations %} +impl From<{{ operation.rust_function_name }}::{{ operation.rust_error_enum }}> for AlgodApiError { + fn from(err: {{ operation.rust_function_name }}::{{ operation.rust_error_enum }}) -> Self { + AlgodApiError::{{ operation.rust_function_name | pascal_case }}(err) + } +} + +{% endfor %} + +/// The main error type for all algod client operations +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("HTTP error: {0}")] + Http(#[from] algokit_http_client::HttpError), + #[error("Serialization error: {0}")] + Serde(String), + #[error("API error: {0}")] + Api(#[from] AlgodApiError), +} + +#[derive(Debug, PartialEq, Eq)] +pub enum ContentType { + Json, + MsgPack, + Text, + Unsupported(String), +} + +impl From<&str> for ContentType { + fn from(content_type: &str) -> Self { + if content_type.contains("application/json") { + ContentType::Json + } else if content_type.contains("application/msgpack") { + ContentType::MsgPack + } else if content_type.contains("text/plain") { + ContentType::Text + } else { + ContentType::Unsupported(content_type.to_string()) + } + } +} + +pub fn urlencode>(s: T) -> String { + ::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes()).collect() +} + +// Re-export the consolidated client +pub use client::AlgodClient; + +{% if param_enums %} +// Re-export parameter enums +pub use parameter_enums::*; +{% endif %} + +// Re-export all endpoint functions +{% for operation in operations %} +pub use {{ operation.rust_function_name }}::{ + {{ operation.rust_function_name }}, + {{ operation.rust_error_enum }}, +}; +{% endfor %} diff --git a/api/oas_generator/rust_oas_generator/templates/apis/parameter_enums.rs.j2 b/api/oas_generator/rust_oas_generator/templates/apis/parameter_enums.rs.j2 new file mode 100644 index 00000000..d37d98f0 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/apis/parameter_enums.rs.j2 @@ -0,0 +1,50 @@ +/* + * Parameter Enums for {{ spec.info.title }} + * + * Auto-generated enums for parameters with constrained string values. + * + * Generated by: Rust OpenAPI Generator + */ + +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::str::FromStr; + +{% for enum_name, enum_data in parameter_enums.items() %} +{% if enum_data.enum_values %} +{{ (enum_data.description or "Parameter enum for " + enum_name.lower()) | rust_doc_comment }} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum {{ enum_name }} { +{% for value in enum_data.enum_values %} + /// {{ value }} + {% if value == "json" %}Json{% elif value == "msgpack" %}Msgpack{% else %}{{ value | pascal_case }}{% endif %}, +{% endfor %} +} + +impl fmt::Display for {{ enum_name }} { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let value = match self { +{% for value in enum_data.enum_values %} + {{ enum_name }}::{% if value == "json" %}Json{% elif value == "msgpack" %}Msgpack{% else %}{{ value | pascal_case }}{% endif %} => "{{ value }}", +{% endfor %} + }; + write!(f, "{}", value) + } +} + +impl FromStr for {{ enum_name }} { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { +{% for value in enum_data.enum_values %} + "{{ value }}" => Ok({{ enum_name }}::{% if value == "json" %}Json{% elif value == "msgpack" %}Msgpack{% else %}{{ value | pascal_case }}{% endif %}), +{% endfor %} + _ => Err(format!("Invalid {{ enum_name }}: {}", s)), + } + } +} + +{% endif %} +{% endfor %} diff --git a/api/oas_generator/rust_oas_generator/templates/base/Cargo.toml.j2 b/api/oas_generator/rust_oas_generator/templates/base/Cargo.toml.j2 new file mode 100644 index 00000000..1df4249e --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/base/Cargo.toml.j2 @@ -0,0 +1,46 @@ +[package] +name = "{{ package_name | snake_case }}" +version = "{{ spec.info.version | ensure_semver }}" +authors = ["AlgoKit Core Team"] +description = "{{ custom_description or spec.info.description or spec.info.title }}" +license = "MIT" +edition = "2024" + +[features] +default = ["default_client"] +default_client = ["algokit_http_client/default_client"] + +[dependencies] +# Core serialization +serde = { version = "^1.0", features = ["derive"] } +serde_with = { version = "^3.8", default-features = false, features = [ + "base64", + "std", + "macros", +] } +serde_json = "^1.0" +serde_repr = "^0.1" +serde_bytes = "^0.11" + +# HTTP client +algokit_http_client = { path = "../algokit_http_client", features = ["ffi_uniffi"] } +url = "^2.5" + +{% if spec.has_msgpack_operations %} +# AlgoKit dependencies for msgpack and signed transactions +algokit_transact = { path = "../algokit_transact" } +# MessagePack serialization +rmp-serde = "^1.1" +{% endif %} + +# Error handling +thiserror = "^1.0" + +# Utilities +base64 = "^0.22" +uuid = { version = "^1.0", features = ["v4"] } + +[dev-dependencies] +tokio = { version = "1.0", features = ["full"] } +tokio-test = "^0.4" + diff --git a/api/oas_generator/rust_oas_generator/templates/base/README.md.j2 b/api/oas_generator/rust_oas_generator/templates/base/README.md.j2 new file mode 100644 index 00000000..6dfc8639 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/base/README.md.j2 @@ -0,0 +1,189 @@ +# {{ spec.info.title }} + +{{ spec.info.description or "Rust API client generated from OpenAPI specification" }} + +**Version:** {{ spec.info.version }} +{% if spec.info.contact and spec.info.contact.email %}**Contact:** {{ spec.info.contact.email }} +{% endif %} + +This Rust crate provides a client library for the {{ spec.info.title }} API. + +## Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +{{ package_name | snake_case }} = "{{ spec.info.version }}" +``` + +## Usage + +```rust +use {{ package_name | snake_case }}::{{ package_name | pascal_case }}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize client (choose one based on your network) + let client = {{ package_name | pascal_case }}::localnet(); // For local development + // let client = {{ package_name | pascal_case }}::testnet(); // For TestNet + // let client = {{ package_name | pascal_case }}::mainnet(); // For MainNet + + // Example: Get network status + let status = client.get_status().await?; + println!("Network status: {:?}", status); + + // Example: Get transaction parameters + let params = client.transaction_params().await?; + println!("Min fee: {}", params.min_fee); + println!("Last round: {}", params.last_round); + + // Example: Get account information + let account_address = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + let account_info = client.account_information( + None, // format + account_address, + None, // exclude + ).await?; + println!("Account balance: {}", account_info.amount); + + Ok(()) +} +``` + +## Configuration + +The client provides convenient constructors for different networks: + +```rust +use {{ package_name | snake_case }}::{{ package_name | pascal_case }}; + +// For local development (uses localhost:4001 with default API token) +let client = {{ package_name | pascal_case }}::localnet(); + +// For Algorand TestNet +let client = {{ package_name | pascal_case }}::testnet(); + +// For Algorand MainNet +let client = {{ package_name | pascal_case }}::mainnet(); +``` + +For custom configurations, you can use a custom HTTP client: + +```rust +use {{ package_name | snake_case }}::{{ package_name | pascal_case }}; +use algokit_http_client::DefaultHttpClient; +use std::sync::Arc; + +// Custom endpoint with API token +let http_client = Arc::new( + DefaultHttpClient::with_header( + "{{ spec.servers[0].url if spec.servers else 'https://api.example.com' }}", + "X-API-Key", + "your-api-key" + )? +); +let client = {{ package_name | pascal_case }}::new(http_client); +``` + +## Complete Example + +Here's a more comprehensive example showing how to check network status, get account information, and prepare for transactions: + +```rust +use {{ package_name | snake_case }}::{{ package_name | pascal_case }}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Connect to localnet + let client = {{ package_name | pascal_case }}::localnet(); + + // Check if the node is healthy and ready + client.health_check().await?; + client.get_ready().await?; + println!("✓ Node is healthy and ready"); + + // Get network information + let status = client.get_status().await?; + println!("✓ Connected to network"); + println!(" Last round: {}", status.last_round); + println!(" Catching up: {}", status.catchup_time.unwrap_or(0)); + + // Get transaction parameters needed for building transactions + let params = client.transaction_params().await?; + println!("✓ Retrieved transaction parameters"); + println!(" Genesis ID: {}", params.genesis_id); + println!(" Min fee: {}", params.min_fee); + + // Example: Get account information + let test_address = "7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q"; + match client.account_information(None, test_address, None).await { + Ok(account) => { + println!("✓ Account information retrieved"); + println!(" Address: {}", account.address); + println!(" Balance: {} microAlgos", account.amount); + println!(" Min balance: {} microAlgos", account.min_balance); + } + Err(e) => { + println!("⚠ Could not retrieve account info: {}", e); + } + } + + // Example: Get application information (if you have an app ID) + // let app_id = 123456; + // let app_info = client.get_application_by_id(app_id).await?; + // println!("App global state: {:?}", app_info.params.global_state); + + Ok(()) +} +``` + +## API Operations + +This client provides access to {{ operations | length }} API operations: + +{% for operation in operations %} +- `{{ operation.rust_function_name }}` - {{ operation.summary or operation.description or "No description" }} +{% endfor %} + +## Models + +The following data models are available: + +{% for schema_name, schema in schemas.items() %} +- `{{ schema.rust_struct_name }}` - {{ schema.description or "No description" }} +{% endfor %} + +## Error Handling + +All API operations return a `Result` type. Errors include: + +- Network errors (connection issues, timeouts) +- HTTP errors (4xx, 5xx status codes) +- Serialization errors (invalid JSON responses) + +```rust +// Example error handling +match client.get_status().await { + Ok(status) => { + println!("Node is running on round: {}", status.last_round); + } + Err(error) => { + eprintln!("Failed to get node status: {:?}", error); + // Handle specific error types if needed + } +} + +// Or use the ? operator for early returns +let params = client.transaction_params().await + .map_err(|e| format!("Failed to get transaction params: {}", e))?; +``` + +## Generated Code + +This client was generated from an OpenAPI specification using a custom Rust code generator. + +**Generated on:** Generated by Rust OpenAPI Generator +**OpenAPI Version:** {{ spec.openapi or "3.0.0" }} +**Generator:** Rust OpenAPI Generator + diff --git a/api/oas_generator/rust_oas_generator/templates/base/lib.rs.j2 b/api/oas_generator/rust_oas_generator/templates/base/lib.rs.j2 new file mode 100644 index 00000000..7fa495b6 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/base/lib.rs.j2 @@ -0,0 +1,9 @@ +#![allow(unused_imports)] +#![allow(clippy::too_many_arguments)] + +pub mod apis; +pub mod models; + +// Re-export the main client for convenience +pub use apis::AlgodClient; + diff --git a/api/oas_generator/rust_oas_generator/templates/base/mod.rs.j2 b/api/oas_generator/rust_oas_generator/templates/base/mod.rs.j2 new file mode 100644 index 00000000..fd3f4c27 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/base/mod.rs.j2 @@ -0,0 +1,96 @@ +/* + * {{ spec.info.title }} + * + * {{ spec.info.description or "API client generated from OpenAPI specification" }} + * + * The version of the OpenAPI document: {{ spec.info.version }} + {% if spec.info.contact and spec.info.contact.email %} * Contact: {{ spec.info.contact.email }} + {% endif %} * Generated by: Rust OpenAPI Generator + */ + +use reqwest; +use serde::{Deserialize, Serialize}; + +pub mod configuration; + +// Individual endpoint modules +{% for operation in operations %} +pub mod {{ operation.rust_function_name }}; +{% endfor %} + +#[derive(Debug, Clone)] +pub struct ResponseContent { + pub status: reqwest::StatusCode, + pub content: String, + pub entity: Option, +} + +#[derive(Debug)] +pub enum Error { + Reqwest(reqwest::Error), + Serde(serde_json::Error), + Io(std::io::Error), + ResponseError(ResponseContent), +} + +impl From for Error { + fn from(e: reqwest::Error) -> Self { + Error::Reqwest(e) + } +} + +impl From for Error { + fn from(e: serde_json::Error) -> Self { + Error::Serde(e) + } +} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error::Io(e) + } +} + +pub fn urlencode>(s: T) -> String { + ::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes()).collect() +} + +{% set content_types = spec.content_types %} +{% set has_text_plain = "text/plain" in content_types %} +{% set has_msgpack = spec.has_msgpack_operations %} +#[derive(Debug, PartialEq)] +pub enum ContentType { + Json, + {% if has_text_plain %}Text, + {% endif %} + {% if has_msgpack %}MsgPack, + {% endif %} + Unsupported(String), +} + +impl From<&str> for ContentType { + fn from(content_type: &str) -> Self { + if content_type.starts_with("application") && content_type.contains("json") { + Self::Json + {% if has_text_plain %} + } else if content_type.starts_with("text/plain") { + Self::Text + {% endif %} + {% if has_msgpack %} + } else if content_type.starts_with("application/msgpack") { + Self::MsgPack + {% endif %} + } else { + Self::Unsupported(content_type.to_string()) + } + } +} + +// Re-export all endpoint functions +{% for operation in operations %} +pub use {{ operation.rust_function_name }}::{ + {{ operation.rust_function_name }}, + {{ operation.rust_error_enum }}, +}; +{% endfor %} + diff --git a/api/oas_generator/rust_oas_generator/templates/models/mod.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/mod.rs.j2 new file mode 100644 index 00000000..1966c40d --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/models/mod.rs.j2 @@ -0,0 +1,15 @@ +/* + * {{ spec.info.title }} + * + * {{ spec.info.description or "API client generated from OpenAPI specification" }} + * + * The version of the OpenAPI document: {{ spec.info.version }} + {% if spec.info.contact and spec.info.contact.email %} * Contact: {{ spec.info.contact.email }} + {% endif %} * Generated by: Rust OpenAPI Generator + */ + +{% for schema_name, schema in schemas.items() %} +pub mod {{ schema.rust_file_name }}; +pub use self::{{ schema.rust_file_name }}::{{ schema.rust_struct_name }}; +{% endfor %} + diff --git a/api/oas_generator/rust_oas_generator/templates/models/model.rs.j2 b/api/oas_generator/rust_oas_generator/templates/models/model.rs.j2 new file mode 100644 index 00000000..5fe9f1e9 --- /dev/null +++ b/api/oas_generator/rust_oas_generator/templates/models/model.rs.j2 @@ -0,0 +1,190 @@ +/* + * {{ spec.info.title }} + * + * {{ spec.info.description or "API client generated from OpenAPI specification" }} + * + * The version of the OpenAPI document: {{ spec.info.version }} + {% if spec.info.contact and spec.info.contact.email %} * Contact: {{ spec.info.contact.email }} + {% endif %} * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; +{% if schema.has_msgpack_fields %} +use serde_with::serde_as; +{% endif %} +{% if schema.has_signed_transaction_fields or schema.implements_algokit_msgpack %} +use algokit_transact::{SignedTransaction as AlgokitSignedTransaction, AlgorandMsgpack}; +{% endif %} + +{# Generate imports for custom types used in this schema #} +{% set custom_types = [] %} +{% for property in schema.properties %} + {% set rust_type = property.rust_type_with_msgpack %} + {# Extract base type from Vec or Option #} + {% if rust_type.startswith('Vec<') %} + {% set inner_type = rust_type[4:-1] %} + {% elif rust_type.startswith('Option<') %} + {% set inner_type = rust_type[7:-1] %} + {% else %} + {% set inner_type = rust_type %} + {% endif %} + + {# Check if it's a custom type (starts with uppercase and doesn't contain :: or primitives) and it's not the current schema's own type #} + {% if inner_type and inner_type[0].isupper() and '::' not in inner_type and inner_type not in ['String', 'Vec', 'Option'] and not inner_type.startswith('i') and not inner_type.startswith('u') and inner_type != 'bool' and inner_type != schema.rust_struct_name %} + {% if inner_type not in custom_types %} + {% set _ = custom_types.append(inner_type) %} + {% endif %} + {% endif %} +{% endfor %} + +{# Generate the import statements #} +{% for custom_type in custom_types %} +use crate::models::{{ custom_type }}; +{% endfor %} + +{% if schema.description %} +{{ schema.description | rust_doc_comment }} +{% endif %} +{% if schema.has_msgpack_fields %} +#[serde_as] +{% endif %} +{% set has_signed_tx = schema.has_signed_transaction_fields %} +{% set complex_signed_tx_fields = [] %} +{% for property in schema.properties %} + {% if property.required and property.is_signed_transaction and not property.rust_type.startswith('Vec<') %} + {% set _ = complex_signed_tx_fields.append(property) %} + {% endif %} +{% endfor %} +{% set needs_manual_default = complex_signed_tx_fields|length > 0 %} +#[derive(Clone, {% if not needs_manual_default %}Default, {% endif %}Debug, PartialEq, Serialize, Deserialize)] +{% if schema.vendor_extensions.get('x-algokit-signed-txn') %} +// This struct represents a SignedTransaction and can be converted to/from AlgoKit's SignedTransaction +{% endif %} +pub struct {{ schema.rust_struct_name }} { +{% for property in schema.properties %} + {% if property.description %} + {{ property.description | rust_doc_comment(4) }} + {% endif %} + {% if property.is_base64_encoded %} + {% if property.required %} + #[serde_as(as = "serde_with::base64::Base64")] + {% else %} + #[serde_as(as = "Option")] + {% endif %} + {% endif %} + #[serde(rename = "{{ property.name }}"{% if not property.required %}, skip_serializing_if = "Option::is_none"{% endif %})] + {% if property.is_signed_transaction %} + pub {{ property.rust_field_name }}: {% if property.required %}{% if property.rust_type.startswith('Vec<') %}Vec{% else %}AlgokitSignedTransaction{% endif %}{% else %}Option<{% if property.rust_type.startswith('Vec<') %}Vec{% else %}AlgokitSignedTransaction{% endif %}>{% endif %}, + {% else %} + pub {{ property.rust_field_name }}: {% if property.required %}{{ property.rust_type_with_msgpack }}{% else %}Option<{{ property.rust_type_with_msgpack }}>{% endif %}, + {% endif %} +{% endfor %} +} + +{% if needs_manual_default %} +impl Default for {{ schema.rust_struct_name }} { + fn default() -> Self { + Self { +{% for property in schema.properties %} + {% if property.is_signed_transaction %} + {{ property.rust_field_name }}: {% if property.required %}{% if property.rust_type.startswith('Vec<') %}Vec::new(){% else %}AlgokitSignedTransaction { + transaction: algokit_transact::Transaction::Payment(algokit_transact::PaymentTransactionFields { + header: algokit_transact::TransactionHeader { + sender: Default::default(), + fee: None, + first_valid: 0, + last_valid: 0, + genesis_hash: None, + genesis_id: None, + note: None, + rekey_to: None, + lease: None, + group: None, + }, + receiver: Default::default(), + amount: 0, + close_remainder_to: None, + }), + signature: None, + auth_address: None, + }{% endif %}{% else %}None{% endif %}, + {% elif property.required %} + {{ property.rust_field_name }}: {% if property.rust_type == "String" %}"".to_string(){% elif property.rust_type.startswith('Vec<') %}Vec::new(){% elif property.rust_type.startswith('i') or property.rust_type.startswith('u') %}0{% elif property.rust_type == "bool" %}false{% elif property.rust_type == "serde_json::Value" %}serde_json::Value::Null{% else %}Default::default(){% endif %}, + {% else %} + {{ property.rust_field_name }}: None, + {% endif %} +{% endfor %} + } + } +} +{% endif %} + +{% if schema.vendor_extensions.get('x-algokit-signed-txn') %} +impl From for {{ schema.rust_struct_name }} { + fn from(signed_tx: AlgokitSignedTransaction) -> Self { + // Convert AlgoKit SignedTransaction to this struct + // This conversion should map the fields appropriately + Self { + // Map fields based on the actual schema structure + // You may need to customize this based on the specific fields in your schema + ..Default::default() + } + } +} + +impl TryFrom<{{ schema.rust_struct_name }}> for AlgokitSignedTransaction { + type Error = Box; + + fn try_from(value: {{ schema.rust_struct_name }}) -> Result { + // Convert this struct to AlgoKit SignedTransaction + // This conversion should map the fields appropriately + // You may need to customize this based on the specific fields in your schema + todo!("Implement conversion to AlgoKit SignedTransaction based on your schema fields") + } +} +{% endif %} + +{% if schema.implements_algokit_msgpack %} +impl AlgorandMsgpack for {{ schema.rust_struct_name }} { + {% if schema.vendor_extensions.get('x-algokit-signed-txn') %} + const PREFIX: &'static [u8] = b""; // No prefix for SignedTransaction + {% else %} + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type + {% endif %} +} +{% endif %} + +impl {{ schema.rust_struct_name }} { + {% if schema.has_required_fields %} + /// Constructor for {{ schema.rust_struct_name }} + pub fn new({% for property in schema.properties if property.required %}{{ property.rust_field_name }}: {% if property.is_signed_transaction %}{% if property.rust_type.startswith('Vec<') %}Vec{% else %}AlgokitSignedTransaction{% endif %}{% else %}{{ property.rust_type_with_msgpack }}{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}) -> {{ schema.rust_struct_name }} { + {{ schema.rust_struct_name }} { +{% for property in schema.properties if property.required %} + {{ property.rust_field_name }}, +{% endfor %} +{% for property in schema.properties if not property.required %} + {{ property.rust_field_name }}: None, +{% endfor %} + } + } + {% else %} + /// Default constructor for {{ schema.rust_struct_name }} + pub fn new() -> {{ schema.rust_struct_name }} { + {{ schema.rust_struct_name }}::default() + } + {% endif %} + + {% if schema.implements_algokit_msgpack %} + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } + {% endif %} +} + diff --git a/api/oas_generator/rust_oas_generator/utils/__init__.py b/api/oas_generator/rust_oas_generator/utils/__init__.py new file mode 100644 index 00000000..1b9efafc --- /dev/null +++ b/api/oas_generator/rust_oas_generator/utils/__init__.py @@ -0,0 +1,46 @@ +""" +Utilities Module for Rust Client Generation + +This module provides utility functions for file operations, string case conversions, +and other common tasks in the Rust client generation process. +""" + +from .file_utils import clean_output_directory, ensure_directory, write_files_to_disk +from .string_case import ( + alphanumcase, + camelcase, + constcase, + escape_rust_keyword, + lowercase, + normalize_rust_identifier, + pascalcase, + rust_const_case, + rust_pascal_case, + rust_snake_case, + snakecase, + spinalcase, + titlecase, + trimcase, + uppercase, +) + +__all__ = [ + "alphanumcase", + "camelcase", + "clean_output_directory", + "constcase", + "ensure_directory", + "escape_rust_keyword", + "lowercase", + "normalize_rust_identifier", + "pascalcase", + "rust_const_case", + "rust_pascal_case", + "rust_snake_case", + "snakecase", + "spinalcase", + "titlecase", + "trimcase", + "uppercase", + "write_files_to_disk", +] diff --git a/api/oas_generator/rust_oas_generator/utils/file_utils.py b/api/oas_generator/rust_oas_generator/utils/file_utils.py new file mode 100644 index 00000000..91aaad4b --- /dev/null +++ b/api/oas_generator/rust_oas_generator/utils/file_utils.py @@ -0,0 +1,81 @@ +""" +File utilities for the OAS generator. + +This module provides file and directory operations for the Rust OAS generator +with proper type annotations and documentation. +""" + +import shutil +from pathlib import Path + + +def write_files_to_disk(files: dict[Path, str]) -> None: + """Write generated files to disk. + + Args: + files: Dictionary mapping file paths to their content. + """ + for path, content in files.items(): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def clean_output_directory(output_dir: Path) -> None: + """Clean the output directory by removing all files and subdirectories. + + Args: + output_dir: Path to the output directory to clean. + """ + shutil.rmtree(output_dir, ignore_errors=True) + output_dir.mkdir(parents=True, exist_ok=True) + + +def copy_file(src: Path, dest: Path) -> None: + """Copy a file from source to destination. + + Args: + src: Source file path. + dest: Destination file path. + """ + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dest) + + +def ensure_directory(directory: Path) -> None: + """Ensure that a directory exists. + + Args: + directory: Path to the directory to create. + """ + directory.mkdir(parents=True, exist_ok=True) + + +def get_relative_path(file_path: Path, base_path: Path) -> Path: + """Get relative path from base_path to file_path. + + Args: + file_path: Target file path. + base_path: Base path to calculate relative path from. + + Returns: + Relative path from base_path to file_path, or the original + path if it cannot be made relative. + """ + try: + return file_path.relative_to(base_path) + except ValueError: + return file_path + + +def list_rust_files(directory: Path) -> list[Path]: + """List all .rs files in a directory recursively. + + Args: + directory: Directory to search for Rust files. + + Returns: + Sorted list of paths to .rs files. + """ + if not directory.is_dir(): + return [] + return sorted(directory.rglob("*.rs")) diff --git a/api/oas_generator/rust_oas_generator/utils/string_case.py b/api/oas_generator/rust_oas_generator/utils/string_case.py new file mode 100644 index 00000000..956aec2e --- /dev/null +++ b/api/oas_generator/rust_oas_generator/utils/string_case.py @@ -0,0 +1,365 @@ +""" +String case conversion utilities for Rust client generation. + +This module provides comprehensive string case conversion utilities with +specific support for Rust naming conventions and keyword handling. + +Based on https://github.com/okunishinishi/python-stringcase +with additional Rust-specific naming conventions. +""" + +import re +from collections.abc import Callable +from typing import Final + +# Regex patterns for case conversion +_SNAKE_CASE_DELIMITER_PATTERN: Final = re.compile(r"[\-\.\s]") +_ACRONYM_PATTERN: Final = re.compile(r"([A-Z])([A-Z][a-z])") +_LOWER_UPPER_PATTERN: Final = re.compile(r"([a-z0-9])([A-Z])") +_NON_ALPHANUMERIC_PATTERN: Final = re.compile(r"[^a-zA-Z0-9_]") +_NON_WORD_PATTERN: Final = re.compile(r"[^\w\-]") + +# Reserved Rust keywords that need to be escaped with r# +RUST_KEYWORDS: Final = frozenset( + { + # Strict keywords (cannot be used as identifiers) + "as", + "break", + "const", + "continue", + "crate", + "else", + "enum", + "extern", + "false", + "fn", + "for", + "if", + "impl", + "in", + "let", + "loop", + "match", + "mod", + "move", + "mut", + "pub", + "ref", + "return", + "self", + "Self", + "static", + "struct", + "super", + "trait", + "true", + "type", + "unsafe", + "use", + "where", + "while", + # Weak keywords (context-dependent, but safer to escape) + "async", + "await", + "dyn", + "union", + "try", + # Reserved keywords (not yet used but reserved for future use) + "abstract", + "become", + "box", + "do", + "final", + "macro", + "override", + "priv", + "typeof", + "unsized", + "virtual", + "yield", + # Special identifiers + "'static", + } +) + + +def _convert_if_not_empty(string: str | None, conversion_func: Callable[[str], str]) -> str: + """Safely convert a string, returning empty string if input is None or empty.""" + return conversion_func(string) if string else "" + + +def snakecase(string: str | None) -> str: + """Convert string into snake_case. + + Handles various formats including camelCase with acronyms. + + Args: + string: String to convert. + + Returns: + Snake case string. + + Examples: + >>> snakecase("HelloWorld") + 'hello_world' + >>> snakecase("hello-world") + 'hello_world' + >>> snakecase("getHTTPResponse") + 'get_http_response' + """ + + def _snakecase(s: str) -> str: + s = _SNAKE_CASE_DELIMITER_PATTERN.sub("_", s) + s = _ACRONYM_PATTERN.sub(r"\1_\2", s) + s = _LOWER_UPPER_PATTERN.sub(r"\1_\2", s) + return s.lower() + + return _convert_if_not_empty(string, _snakecase) + + +def camelcase(string: str | None) -> str: + """Convert string into camel case. + + Args: + string: String to convert. + + Returns: + Camel case string. + + Examples: + >>> camelcase("hello_world") + 'helloWorld' + >>> camelcase("hello-world") + 'helloWorld' + >>> camelcase("getHTTPResponse") + 'getHttpResponse' + """ + + def _camelcase(s: str) -> str: + words = snakecase(s).split("_") + if not words: + return "" + return words[0] + "".join(word.capitalize() for word in words[1:]) + + return _convert_if_not_empty(string, _camelcase) + + +def capitalcase(string: str | None) -> str: + """Convert string into capital case (first letter uppercase). + + Args: + string: String to convert. + + Returns: + Capital case string. + + Examples: + >>> capitalcase("hello world") + 'Hello world' + """ + + def _capitalcase(s: str) -> str: + return s[0].upper() + s[1:] + + return _convert_if_not_empty(string, _capitalcase) + + +def constcase(string: str | None) -> str: + """Convert string into CONSTANT_CASE (upper snake case). + + Args: + string: String to convert. + + Returns: + Constant case string. + + Examples: + >>> constcase("hello_world") + 'HELLO_WORLD' + >>> constcase("helloWorld") + 'HELLO_WORLD' + """ + return snakecase(string).upper() + + +def lowercase(string: str | None) -> str: + """Convert string into lowercase. + + Args: + string: String to convert. + + Returns: + Lowercase string. + """ + return string.lower() if string else "" + + +def pascalcase(string: str | None) -> str: + """Convert string into PascalCase. + + Args: + string: String to convert. + + Returns: + PascalCase string. + + Examples: + >>> pascalcase("hello_world") + 'HelloWorld' + >>> pascalcase("hello-world") + 'HelloWorld' + >>> pascalcase("getHTTPResponse") + 'GetHttpResponse' + """ + + def _pascalcase(s: str) -> str: + return "".join(word.capitalize() for word in snakecase(s).split("_")) + + return _convert_if_not_empty(string, _pascalcase) + + +def spinalcase(string: str | None) -> str: + """Convert string into spinal-case (kebab-case). + + Args: + string: String to convert. + + Returns: + Spinal case string. + + Examples: + >>> spinalcase("hello_world") + 'hello-world' + """ + return snakecase(string).replace("_", "-") + + +def titlecase(string: str | None) -> str: + """Convert string into Title Case. + + Args: + string: String to convert. + + Returns: + Title case string. + + Examples: + >>> titlecase("hello_world") + 'Hello World' + """ + return " ".join(capitalcase(word) for word in snakecase(string).split("_") if word) + + +def trimcase(string: str | None) -> str: + """Convert string into trimmed string. + + Args: + string: String to convert. + + Returns: + Trimmed string. + """ + return string.strip() if string else "" + + +def uppercase(string: str | None) -> str: + """Convert string into uppercase. + + Args: + string: String to convert. + + Returns: + Uppercase string. + """ + return string.upper() if string else "" + + +def alphanumcase(string: str | None) -> str: + """Remove all non-alphanumeric characters (keeps only 0-9, a-z, A-Z). + + Args: + string: String to convert. + + Returns: + String with only alphanumeric characters. + + Examples: + >>> alphanumcase("hello@world#123") + 'helloworld123' + """ + + def _alphanumcase(s: str) -> str: + return "".join(char for char in s if char.isalnum()) + + return _convert_if_not_empty(string, _alphanumcase) + + +# Rust-specific naming utilities + +rust_snake_case = snakecase +rust_const_case = constcase +rust_pascal_case = pascalcase + + +def normalize_rust_identifier(name: str | None) -> str: + """Normalize name to be a valid Rust identifier. + + This function ensures the resulting string is a valid Rust identifier: + - Replaces invalid characters with underscores + - Ensures it doesn't start with a digit + - Preserves valid alphanumeric characters and underscores + + Args: + name: The string to normalize. + + Returns: + A valid Rust identifier. + + Examples: + >>> normalize_rust_identifier("123invalid") + '_123invalid' + >>> normalize_rust_identifier("valid@name") + 'valid_name' + """ + + def _normalize(s: str) -> str: + # Replace invalid characters with underscores + normalized = _NON_ALPHANUMERIC_PATTERN.sub("_", s) + + # Ensure it doesn't start with a digit + if normalized and normalized[0].isdigit(): + normalized = f"_{normalized}" + + return normalized + + return _convert_if_not_empty(name, _normalize) + + +def escape_rust_keyword(name: str) -> str: + """Escape Rust keywords with r# prefix if necessary. + + Args: + name: The identifier name to check. + + Returns: + The name with r# prefix if it's a Rust keyword, otherwise unchanged. + + Examples: + >>> escape_rust_keyword("type") + 'r#type' + >>> escape_rust_keyword("name") + 'name' + """ + return f"r#{name}" if name in RUST_KEYWORDS else name + + +def is_rust_keyword(name: str) -> bool: + """Check if a name is a Rust keyword. + + Args: + name: The identifier name to check. + + Returns: + True if the name is a Rust keyword, False otherwise. + """ + return name in RUST_KEYWORDS diff --git a/api/oas_generator/tests/__init__.py b/api/oas_generator/tests/__init__.py new file mode 100644 index 00000000..5414e656 --- /dev/null +++ b/api/oas_generator/tests/__init__.py @@ -0,0 +1 @@ +# Test package for rust_oas_generator diff --git a/api/oas_generator/tests/test_msgpack_implementation.py b/api/oas_generator/tests/test_msgpack_implementation.py new file mode 100644 index 00000000..be994a45 --- /dev/null +++ b/api/oas_generator/tests/test_msgpack_implementation.py @@ -0,0 +1,170 @@ +""" +Test msgpack implementation logic in the OAS parser. + +This test validates that all schemas related to msgpack operations +(directly or indirectly through dependencies) properly implement the msgpack trait. +""" + +from pathlib import Path + +import pytest + +from rust_oas_generator.parser.oas_parser import OASParser, ParsedSpec + +# Constants for test thresholds +MIN_MSGPACK_COVERAGE = 50.0 +EXPECTED_ALGOD_COVERAGE = 80.0 + + +class TestMsgpackImplementation: + """Test class for msgpack implementation validation.""" + + @pytest.fixture + def algod_spec_path(self) -> Path: + """Get the path to the algod OAS spec.""" + spec_path = Path(__file__).parent.parent / "specs" / "algod.oas3.json" + if not spec_path.exists(): + spec_path = Path(__file__).parent.parent.parent / "specs" / "algod.oas3.json" + + if not spec_path.exists(): + pytest.skip("algod.oas3.json not found") + + return spec_path + + @pytest.fixture + def parsed_spec(self, algod_spec_path: Path) -> tuple[ParsedSpec, OASParser]: + """Parse the algod OAS spec.""" + parser = OASParser() + return parser.parse_file(algod_spec_path), parser + + def test_msgpack_operations_detected(self, parsed_spec: tuple[ParsedSpec, OASParser]) -> None: + """Test that msgpack operations are correctly detected.""" + spec, parser = parsed_spec + + # Should have msgpack operations + assert spec.has_msgpack_operations, "Should detect msgpack operations in algod spec" + + # Find msgpack operations + msgpack_operations = [op for op in spec.operations if op.supports_msgpack] + assert len(msgpack_operations) > 0, "Should find msgpack operations" + + def test_msgpack_request_body_detection(self, parsed_spec: tuple[ParsedSpec, OASParser]) -> None: + """Test that request bodies with msgpack support are detected.""" + spec, parser = parsed_spec + + # Find operations with msgpack request bodies + msgpack_request_ops = [op for op in spec.operations if op.request_body_supports_msgpack] + + assert len(msgpack_request_ops) > 0, "Should find operations with msgpack request bodies" + + def test_root_msgpack_schemas_identified(self, parsed_spec: tuple[ParsedSpec, OASParser]) -> None: + """Test that root msgpack schemas are correctly identified.""" + spec, parser = parsed_spec + + root_schemas = parser._get_msgpack_root_schemas() # noqa: SLF001 + assert len(root_schemas) > 0, "Should identify root msgpack schemas" + + def test_dependency_graph_built(self, parsed_spec: tuple[ParsedSpec, OASParser]) -> None: + """Test that schema dependency graph is built correctly.""" + spec, parser = parsed_spec + + dependency_graph = parser._build_schema_dependency_graph() # noqa: SLF001 + assert len(dependency_graph) > 0, "Should build dependency graph" + + # Check some known dependencies + if "Account" in dependency_graph: + account_deps = dependency_graph["Account"] + expected_deps = {"ApplicationLocalState", "AssetHolding", "AccountParticipation"} + for expected_dep in expected_deps: + assert expected_dep in account_deps, f"Account should depend on {expected_dep}" + + if "SimulateRequest" in dependency_graph: + simulate_deps = dependency_graph["SimulateRequest"] + assert "SimulateRequestTransactionGroup" in simulate_deps, ( + "SimulateRequest should depend on SimulateRequestTransactionGroup" + ) + + def test_all_msgpack_schemas_implement_trait(self, parsed_spec: tuple[ParsedSpec, OASParser]) -> None: + """Test that all schemas requiring msgpack implement the trait.""" + spec, parser = parsed_spec + + # Get root schemas and build dependency graph + root_msgpack_schemas = parser._get_msgpack_root_schemas() # noqa: SLF001 + dependency_graph = parser._build_schema_dependency_graph() # noqa: SLF001 + + # Find all schemas that should implement msgpack using BFS + msgpack_schemas = set() + queue = list(root_msgpack_schemas) + visited = set() + + while queue: + schema_name = queue.pop(0) + if schema_name in visited: + continue + visited.add(schema_name) + msgpack_schemas.add(schema_name) + + if schema_name in dependency_graph: + for dep in dependency_graph[schema_name]: + if dep not in visited: + queue.append(dep) + + assert len(msgpack_schemas) > 0, "Should find schemas requiring msgpack" + + # Check that all these schemas implement msgpack + missing_implementations = [] + for schema_name in msgpack_schemas: + schema = spec.schemas.get(schema_name) + if schema and not schema.implements_algokit_msgpack: + missing_implementations.append(schema_name) + + assert len(missing_implementations) == 0, f"Schemas missing msgpack implementation: {missing_implementations}" + + def test_response_models_implement_msgpack(self, parsed_spec: tuple[ParsedSpec, OASParser]) -> None: + """Test that response models for msgpack operations implement msgpack.""" + spec, parser = parsed_spec + + # Find msgpack operations + msgpack_operations = [op for op in spec.operations if op.supports_msgpack] + + for operation in msgpack_operations: + for _status_code, response in operation.responses.items(): + if response.rust_type and response.supports_msgpack: + # Find the schema for this response + response_schema = spec.schemas.get(response.rust_type) + if response_schema: + assert response_schema.implements_algokit_msgpack, ( + f"Response schema {response.rust_type} for {operation.operation_id} " + "should implement msgpack" + ) + + def test_no_false_positives(self, parsed_spec: tuple[ParsedSpec, OASParser]) -> None: + """Test that schemas not related to msgpack don't unnecessarily implement it.""" + spec, parser = parsed_spec + + # Get all schemas that should implement msgpack + root_msgpack_schemas = parser._get_msgpack_root_schemas() # noqa: SLF001 + dependency_graph = parser._build_schema_dependency_graph() # noqa: SLF001 + + msgpack_schemas = set() + queue = list(root_msgpack_schemas) + visited = set() + + while queue: + schema_name = queue.pop(0) + if schema_name in visited: + continue + visited.add(schema_name) + msgpack_schemas.add(schema_name) + + if schema_name in dependency_graph: + for dep in dependency_graph[schema_name]: + if dep not in visited: + queue.append(dep) + + # Check that schemas implementing msgpack are in the expected set + for schema_name, schema in spec.schemas.items(): + if schema.implements_algokit_msgpack: + assert schema_name in msgpack_schemas, ( + f"Schema {schema_name} implements msgpack but is not in expected msgpack schema set" + ) diff --git a/api/oas_generator/uv.lock b/api/oas_generator/uv.lock new file mode 100644 index 00000000..80a99125 --- /dev/null +++ b/api/oas_generator/uv.lock @@ -0,0 +1,186 @@ +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, +] + +[[package]] +name = "mypy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668 }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060 }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167 }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341 }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991 }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016 }, + { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097 }, + { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728 }, + { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965 }, + { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660 }, + { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198 }, + { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276 }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, +] + +[[package]] +name = "ruff" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/90/5255432602c0b196a0da6720f6f76b93eb50baef46d3c9b0025e2f9acbf3/ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c", size = 4376101 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/fd/b46bb20e14b11ff49dbc74c61de352e0dc07fb650189513631f6fb5fc69f/ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848", size = 10311554 }, + { url = "https://files.pythonhosted.org/packages/e7/d3/021dde5a988fa3e25d2468d1dadeea0ae89dc4bc67d0140c6e68818a12a1/ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6", size = 11118435 }, + { url = "https://files.pythonhosted.org/packages/07/a2/01a5acf495265c667686ec418f19fd5c32bcc326d4c79ac28824aecd6a32/ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0", size = 10466010 }, + { url = "https://files.pythonhosted.org/packages/4c/57/7caf31dd947d72e7aa06c60ecb19c135cad871a0a8a251723088132ce801/ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48", size = 10661366 }, + { url = "https://files.pythonhosted.org/packages/e9/ba/aa393b972a782b4bc9ea121e0e358a18981980856190d7d2b6187f63e03a/ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807", size = 10173492 }, + { url = "https://files.pythonhosted.org/packages/d7/50/9349ee777614bc3062fc6b038503a59b2034d09dd259daf8192f56c06720/ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82", size = 11761739 }, + { url = "https://files.pythonhosted.org/packages/04/8f/ad459de67c70ec112e2ba7206841c8f4eb340a03ee6a5cabc159fe558b8e/ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c", size = 12537098 }, + { url = "https://files.pythonhosted.org/packages/ed/50/15ad9c80ebd3c4819f5bd8883e57329f538704ed57bac680d95cb6627527/ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165", size = 12154122 }, + { url = "https://files.pythonhosted.org/packages/76/e6/79b91e41bc8cc3e78ee95c87093c6cacfa275c786e53c9b11b9358026b3d/ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2", size = 11363374 }, + { url = "https://files.pythonhosted.org/packages/db/c3/82b292ff8a561850934549aa9dc39e2c4e783ab3c21debe55a495ddf7827/ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4", size = 11587647 }, + { url = "https://files.pythonhosted.org/packages/2b/42/d5760d742669f285909de1bbf50289baccb647b53e99b8a3b4f7ce1b2001/ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514", size = 10527284 }, + { url = "https://files.pythonhosted.org/packages/19/f6/fcee9935f25a8a8bba4adbae62495c39ef281256693962c2159e8b284c5f/ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88", size = 10158609 }, + { url = "https://files.pythonhosted.org/packages/37/fb/057febf0eea07b9384787bfe197e8b3384aa05faa0d6bd844b94ceb29945/ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51", size = 11141462 }, + { url = "https://files.pythonhosted.org/packages/10/7c/1be8571011585914b9d23c95b15d07eec2d2303e94a03df58294bc9274d4/ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a", size = 11641616 }, + { url = "https://files.pythonhosted.org/packages/6a/ef/b960ab4818f90ff59e571d03c3f992828d4683561095e80f9ef31f3d58b7/ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb", size = 10525289 }, + { url = "https://files.pythonhosted.org/packages/34/93/8b16034d493ef958a500f17cda3496c63a537ce9d5a6479feec9558f1695/ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0", size = 11598311 }, + { url = "https://files.pythonhosted.org/packages/d0/33/4d3e79e4a84533d6cd526bfb42c020a23256ae5e4265d858bd1287831f7d/ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b", size = 10724946 }, +] + +[[package]] +name = "rust-oas-generator" +version = "1.0.0" +source = { editable = "." } +dependencies = [ + { name = "jinja2" }, +] + +[package.optional-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "jinja2", specifier = ">=3.0.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.14.1" }, + { name = "ruff", specifier = ">=0.12.0" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, +] diff --git a/api/package.json b/api/package.json index 0ca920c4..49821e96 100644 --- a/api/package.json +++ b/api/package.json @@ -3,17 +3,8 @@ "module": "index.ts", "type": "module", "private": true, - "scripts": { - "generate": "openapi-generator-cli generate", - "generate:algod_api": "bun generate:algod_api:all", - "generate:algod_api:all": "bun scripts/generate-clients.ts algod all", - "generate:algod_api:ts": "bun scripts/generate-clients.ts algod typescript", - "generate:algod_api:py": "bun scripts/generate-clients.ts algod python", - "convert-openapi": "bun scripts/convert-openapi.ts" - }, "devDependencies": { "@apidevtools/swagger-parser": "^11.0.0", - "@openapitools/openapi-generator-cli": "2.19.1", "@types/bun": "latest", "@types/node": "^20.10.0", "prettier": "^3.5.3" diff --git a/api/scripts/convert-openapi.ts b/api/scripts/convert-openapi.ts index d1187aaf..d8850c37 100644 --- a/api/scripts/convert-openapi.ts +++ b/api/scripts/convert-openapi.ts @@ -197,15 +197,30 @@ function fixBigInt(spec: OpenAPISpec): number { { fieldName: "last-round" }, { fieldName: "confirmed-round" }, { fieldName: "asset-id" }, + { fieldName: "application-index" }, + { fieldName: "asset-index" }, { fieldName: "current_round" }, { fieldName: "online-money" }, { fieldName: "total-money" }, { fieldName: "amount" }, + { fieldName: "asset-closing-amount" }, + { fieldName: "closing-amount" }, + { fieldName: "close_rewards" }, { fieldName: "id" }, { fieldName: "index", excludedModels: ["LightBlockHeaderProof"] }, { fieldName: "last-proposed" }, { fieldName: "last-heartbeat" }, { fieldName: "application-index" }, + { fieldName: "min-balance" }, + { fieldName: "amount-without-pending-rewards" }, + { fieldName: "pending-rewards" }, + { fieldName: "rewards" }, + { fieldName: "reward-base" }, + { fieldName: "vote-first-valid" }, + { fieldName: "vote-key-dilution" }, + { fieldName: "vote-last-valid" }, + { fieldName: "catchup-time" }, + { fieldName: "time-since-last-round" }, ]; const processObject = (obj: any, objName?: string): void => { diff --git a/api/scripts/generate-clients.ts b/api/scripts/generate-clients.ts deleted file mode 100644 index fef9b1fb..00000000 --- a/api/scripts/generate-clients.ts +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env bun - -import { execSync } from "child_process"; -import { join } from "path"; -import { parseArgs } from "util"; -import { mkdirSync, existsSync, copyFileSync, rmSync, readdirSync } from "fs"; - -const LANGUAGE_OPTIONS = ["typescript", "python"] as string[]; -type Language = (typeof LANGUAGE_OPTIONS)[number]; - -const API_OPTIONS = ["algod" /*, "indexer", "kmd"*/] as string[]; -type Api = (typeof API_OPTIONS)[number]; - -// Parse command line arguments -const { positionals } = parseArgs({ - allowPositionals: true, -}); - -if (positionals.length !== 2) { - throw new Error("Usage: bun scripts/generate-clients.ts "); -} - -const [apiArg, languageArg] = positionals; -const apis = API_OPTIONS.includes(apiArg) ? [apiArg] : API_OPTIONS; -const languages = LANGUAGE_OPTIONS.includes(languageArg) ? [languageArg] : LANGUAGE_OPTIONS; - -const SPEC_PATH = join(process.cwd(), "specs", "algod.oas3.json"); -const OUTPUT_DIR = join(process.cwd(), "..", "packages"); - -// Template directories -const TEMPLATES_DIR = join(process.cwd(), "oas_templates"); -const TYPESCRIPT_TEMPLATE = join(TEMPLATES_DIR, "typescript"); -const PYTHON_TEMPLATE = join(TEMPLATES_DIR, "python"); - -if (!existsSync(OUTPUT_DIR)) { - mkdirSync(OUTPUT_DIR, { recursive: true }); -} - -/** - * Ensures a clean output directory exists with proper .gitignore - */ -function prepareOutputDirectory(directory: string) { - // Remove the directory if it exists - const fileIgnoreList = ["release.config.cjs", "bun.lock", "poetry.lock"]; - if (existsSync(directory)) { - const files = readdirSync(directory); - for (const file of files.filter((f) => !fileIgnoreList.includes(f))) { - const filePath = join(directory, file); - rmSync(filePath, { recursive: true, force: true }); - } - } else { - // Create a fresh directory - mkdirSync(directory, { recursive: true }); - } - - console.log(`Prepared clean directory: ${directory}`); -} - -function copyIgnoreFile(templateDir: string, outputDir: string) { - const ignoreFilePath = join(templateDir, ".openapi-generator-ignore"); - if (existsSync(ignoreFilePath)) { - console.log("ensuring ignore rules are propagated before client generation"); - const destPath = join(outputDir, ".openapi-generator-ignore"); - copyFileSync(ignoreFilePath, destPath); - } -} - -function generateTypescriptClient(outputDir: string) { - copyIgnoreFile(TYPESCRIPT_TEMPLATE, outputDir); - - const cmd = [ - "bunx openapi-generator-cli generate", - `-i ${SPEC_PATH}`, - "-g typescript", - `-o ${outputDir}`, - `-t ${TYPESCRIPT_TEMPLATE}`, - `-c ${TYPESCRIPT_TEMPLATE}/openapi-config.yaml`, - ].join(" "); - - console.log(`Executing: ${cmd}`); - execSync(cmd, { stdio: "inherit" }); -} - -function generatePythonClient(outputDir: string) { - copyIgnoreFile(PYTHON_TEMPLATE, outputDir); - - const cmd = [ - "bunx openapi-generator-cli generate", - `-i ${SPEC_PATH}`, - "-g python", - `-o ${outputDir}`, - `-t ${PYTHON_TEMPLATE}`, - `-c ${PYTHON_TEMPLATE}/openapi-config.yaml`, - "--global-property=apis,models,apiTests=false,modelTests=false,supportingFiles", - ].join(" "); - - console.log(`Executing: ${cmd}`); - execSync(cmd, { stdio: "inherit" }); -} -function main() { - try { - for (const api of apis) { - const api_package = `${api}_api`; - - if (languages.includes("typescript")) { - const outputDir = join(OUTPUT_DIR, "typescript", api_package); - console.log(`Generating TypeScript ${apis} client...`); - prepareOutputDirectory(outputDir); - generateTypescriptClient(outputDir); - console.log(`TypeScript ${apis} client generated successfully!`); - } - - if (languages.includes("python")) { - const outputDir = join(OUTPUT_DIR, "python", api_package); - console.log(`Generating Python ${apis} client...`); - prepareOutputDirectory(outputDir); - generatePythonClient(outputDir); - console.log(`Python ${apis} client generated successfully!`); - } - } - - console.log("Client generation completed!"); - } catch (error) { - console.error("Error generating clients:", error); - process.exit(1); - } -} - -main(); diff --git a/api/specs/algod.oas3.json b/api/specs/algod.oas3.json index 54050dca..44fc7c38 100644 --- a/api/specs/algod.oas3.json +++ b/api/specs/algod.oas3.json @@ -2094,7 +2094,8 @@ "properties": { "catchup-time": { "type": "integer", - "description": "CatchupTime in nanoseconds" + "description": "CatchupTime in nanoseconds", + "x-algokit-bigint": true }, "last-round": { "type": "integer", @@ -2123,7 +2124,8 @@ }, "time-since-last-round": { "type": "integer", - "description": "TimeSinceLastRound in nanoseconds" + "description": "TimeSinceLastRound in nanoseconds", + "x-algokit-bigint": true }, "last-catchpoint": { "type": "string", @@ -2271,7 +2273,8 @@ "properties": { "catchup-time": { "type": "integer", - "description": "CatchupTime in nanoseconds" + "description": "CatchupTime in nanoseconds", + "x-algokit-bigint": true }, "last-round": { "type": "integer", @@ -2300,7 +2303,8 @@ }, "time-since-last-round": { "type": "integer", - "description": "TimeSinceLastRound in nanoseconds" + "description": "TimeSinceLastRound in nanoseconds", + "x-algokit-bigint": true }, "last-catchpoint": { "type": "string", @@ -5020,11 +5024,13 @@ }, "min-balance": { "type": "integer", - "description": "MicroAlgo balance required by the account.\n\nThe requirement grows based on asset and application usage." + "description": "MicroAlgo balance required by the account.\n\nThe requirement grows based on asset and application usage.", + "x-algokit-bigint": true }, "amount-without-pending-rewards": { "type": "integer", - "description": "specifies the amount of MicroAlgos in the account, without the pending rewards." + "description": "specifies the amount of MicroAlgos in the account, without the pending rewards.", + "x-algokit-bigint": true }, "apps-local-state": { "type": "array", @@ -5094,15 +5100,18 @@ }, "pending-rewards": { "type": "integer", - "description": "amount of MicroAlgos of pending rewards in this account." + "description": "amount of MicroAlgos of pending rewards in this account.", + "x-algokit-bigint": true }, "reward-base": { "type": "integer", - "description": "\\[ebase\\] used as part of the rewards computation. Only applicable to accounts which are participating." + "description": "\\[ebase\\] used as part of the rewards computation. Only applicable to accounts which are participating.", + "x-algokit-bigint": true }, "rewards": { "type": "integer", - "description": "\\[ern\\] total rewards of MicroAlgos the account has received, including pending rewards." + "description": "\\[ern\\] total rewards of MicroAlgos the account has received, including pending rewards.", + "x-algokit-bigint": true }, "round": { "type": "integer", @@ -5173,15 +5182,18 @@ }, "vote-first-valid": { "type": "integer", - "description": "\\[voteFst\\] First round for which this participation is valid." + "description": "\\[voteFst\\] First round for which this participation is valid.", + "x-algokit-bigint": true }, "vote-key-dilution": { "type": "integer", - "description": "\\[voteKD\\] Number of subkeys in each batch of participation keys." + "description": "\\[voteKD\\] Number of subkeys in each batch of participation keys.", + "x-algokit-bigint": true }, "vote-last-valid": { "type": "integer", - "description": "\\[voteLst\\] Last round for which this participation is valid." + "description": "\\[voteLst\\] Last round for which this participation is valid.", + "x-algokit-bigint": true }, "vote-participation-key": { "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", @@ -6124,7 +6136,8 @@ "properties": { "asset-index": { "type": "integer", - "description": "The asset index if the transaction was found and it created an asset." + "description": "The asset index if the transaction was found and it created an asset.", + "x-algokit-bigint": true }, "application-index": { "type": "integer", @@ -6137,11 +6150,13 @@ }, "closing-amount": { "type": "integer", - "description": "Closing amount for the transaction." + "description": "Closing amount for the transaction.", + "x-algokit-bigint": true }, "asset-closing-amount": { "type": "integer", - "description": "The number of the asset's unit that were transferred to the close-to address." + "description": "The number of the asset's unit that were transferred to the close-to address.", + "x-algokit-bigint": true }, "confirmed-round": { "type": "integer", @@ -7045,7 +7060,8 @@ "properties": { "catchup-time": { "type": "integer", - "description": "CatchupTime in nanoseconds" + "description": "CatchupTime in nanoseconds", + "x-algokit-bigint": true }, "last-round": { "type": "integer", @@ -7074,7 +7090,8 @@ }, "time-since-last-round": { "type": "integer", - "description": "TimeSinceLastRound in nanoseconds" + "description": "TimeSinceLastRound in nanoseconds", + "x-algokit-bigint": true }, "last-catchpoint": { "type": "string", diff --git a/crates/algod_api/Cargo.toml b/crates/algod_api/Cargo.toml deleted file mode 100644 index 138c48ad..00000000 --- a/crates/algod_api/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "algod_api" -version = "0.1.0" -edition = "2024" - -[features] -default = ["default_http_client"] -default_http_client = ["algokit_http_client/default_client"] - - -[dependencies] -algokit_http_client = { version = "0.1.0", path = "../algokit_http_client" } -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.140" diff --git a/crates/algod_api/src/lib.rs b/crates/algod_api/src/lib.rs deleted file mode 100644 index 10c10298..00000000 --- a/crates/algod_api/src/lib.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::sync::Arc; - -use algokit_http_client::{HttpClient, HttpError}; - -#[cfg(feature = "default_http_client")] -use algokit_http_client::DefaultHttpClient; - -use serde::{Deserialize, Serialize}; - -pub struct AlgodClient { - http_client: Arc, -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct TransactionParams { - pub consensus_version: String, - pub fee: u64, - pub last_round: u64, - pub genesis_id: String, - pub genesis_hash: String, - pub min_fee: u64, -} - -/// A temporary AlgodClient until the proper client is generated. -/// The exepectation is that this client will use a HttpClient to make requests to the Algorand API -impl AlgodClient { - pub fn new(http_client: Arc) -> Self { - AlgodClient { http_client } - } - - #[cfg(feature = "default_http_client")] - pub fn testnet() -> Self { - AlgodClient { - http_client: Arc::new(DefaultHttpClient::new( - "https://testnet-api.4160.nodely.dev", - )), - } - } - - pub async fn transaction_params(&self) -> Result { - let path = "/v2/transactions/params".to_string(); - let response = self.http_client.get(path).await?; - - serde_json::from_slice(&response).map_err(|e| HttpError::HttpError(e.to_string())) - } -} diff --git a/crates/algod_client/Cargo.toml b/crates/algod_client/Cargo.toml new file mode 100644 index 00000000..6631db55 --- /dev/null +++ b/crates/algod_client/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "algod_client" +version = "0.0.1" +authors = ["AlgoKit Core Team"] +description = "API client for algod interaction." +license = "MIT" +edition = "2024" + +[features] +default = ["default_client"] +default_client = ["algokit_http_client/default_client"] + +[dependencies] +# Core serialization +serde = { version = "^1.0", features = ["derive"] } +serde_with = { version = "^3.8", default-features = false, features = [ + "base64", + "std", + "macros", +] } +serde_json = "^1.0" +serde_repr = "^0.1" +serde_bytes = "^0.11" + +# HTTP client +algokit_http_client = { path = "../algokit_http_client", features = ["ffi_uniffi"] } +url = "^2.5" + +# AlgoKit dependencies for msgpack and signed transactions +algokit_transact = { path = "../algokit_transact" } +# MessagePack serialization +rmp-serde = "^1.1" + +# Error handling +thiserror = "^1.0" + +# Utilities +base64 = "^0.22" +uuid = { version = "^1.0", features = ["v4"] } + +[dev-dependencies] +tokio = { version = "1.0", features = ["full"] } +tokio-test = "^0.4" diff --git a/crates/algod_client/README.md b/crates/algod_client/README.md new file mode 100644 index 00000000..3e43c988 --- /dev/null +++ b/crates/algod_client/README.md @@ -0,0 +1,332 @@ +# Algod REST API. + +API endpoint for algod operations. + +**Version:** 0.0.1 +**Contact:** contact@algorand.com + +This Rust crate provides a client library for the Algod REST API. API. + +## Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +algod_client = "0.0.1" +``` + +## Usage + +```rust +use algod_client::AlgodClient; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize client (choose one based on your network) + let client = AlgodClient::localnet(); // For local development + // let client = AlgodClient::testnet(); // For TestNet + // let client = AlgodClient::mainnet(); // For MainNet + + // Example: Get network status + let status = client.get_status().await?; + println!("Network status: {:?}", status); + + // Example: Get transaction parameters + let params = client.transaction_params().await?; + println!("Min fee: {}", params.min_fee); + println!("Last round: {}", params.last_round); + + // Example: Get account information + let account_address = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + let account_info = client.account_information( + None, // format + account_address, + None, // exclude + ).await?; + println!("Account balance: {}", account_info.amount); + + Ok(()) +} +``` + +## Configuration + +The client provides convenient constructors for different networks: + +```rust +use algod_client::AlgodClient; + +// For local development (uses localhost:4001 with default API token) +let client = AlgodClient::localnet(); + +// For Algorand TestNet +let client = AlgodClient::testnet(); + +// For Algorand MainNet +let client = AlgodClient::mainnet(); +``` + +For custom configurations, you can use a custom HTTP client: + +```rust +use algod_client::AlgodClient; +use algokit_http_client::DefaultHttpClient; +use std::sync::Arc; + +// Custom endpoint with API token +let http_client = Arc::new( + DefaultHttpClient::with_header( + "http://localhost/", + "X-API-Key", + "your-api-key" + )? +); +let client = AlgodClient::new(http_client); +``` + +## Complete Example + +Here's a more comprehensive example showing how to check network status, get account information, and prepare for transactions: + +```rust +use algod_client::AlgodClient; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Connect to localnet + let client = AlgodClient::localnet(); + + // Check if the node is healthy and ready + client.health_check().await?; + client.get_ready().await?; + println!("✓ Node is healthy and ready"); + + // Get network information + let status = client.get_status().await?; + println!("✓ Connected to network"); + println!(" Last round: {}", status.last_round); + println!(" Catching up: {}", status.catchup_time.unwrap_or(0)); + + // Get transaction parameters needed for building transactions + let params = client.transaction_params().await?; + println!("✓ Retrieved transaction parameters"); + println!(" Genesis ID: {}", params.genesis_id); + println!(" Min fee: {}", params.min_fee); + + // Example: Get account information + let test_address = "7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q"; + match client.account_information(None, test_address, None).await { + Ok(account) => { + println!("✓ Account information retrieved"); + println!(" Address: {}", account.address); + println!(" Balance: {} microAlgos", account.amount); + println!(" Min balance: {} microAlgos", account.min_balance); + } + Err(e) => { + println!("⚠ Could not retrieve account info: {}", e); + } + } + + // Example: Get application information (if you have an app ID) + // let app_id = 123456; + // let app_info = client.get_application_by_id(app_id).await?; + // println!("App global state: {:?}", app_info.params.global_state); + + Ok(()) +} +``` + +## API Operations + +This client provides access to 55 API operations: + +- `health_check` - Returns OK if healthy. +- `get_ready` - Returns OK if healthy and fully caught up. +- `metrics` - Return metrics about algod functioning. +- `get_genesis` - Gets the genesis information. +- `swagger_json` - Gets the current swagger spec. +- `get_version` - Retrieves the supported API versions, binary build versions, and genesis information. +- `get_debug_settings_prof` - Retrieves the current settings for blocking and mutex profiles +- `put_debug_settings_prof` - Enables blocking and mutex profiles, and returns the old settings +- `get_config` - Gets the merged config file. +- `account_information` - Get account information. +- `account_asset_information` - Get account information about a given asset. +- `account_assets_information` - Get a list of assets held by an account, inclusive of asset params. +- `account_application_information` - Get account information about a given app. +- `get_pending_transactions_by_address` - Get a list of unconfirmed transactions currently in the transaction pool by address. +- `get_block` - Get the block for the given round. +- `get_block_txids` - Get the top level transaction IDs for the block on the given round. +- `get_block_hash` - Get the block hash for the block on the given round. +- `get_transaction_proof` - Get a proof for a transaction in a block. +- `get_block_logs` - Get all of the logs from outer and inner app calls in the given round +- `get_supply` - Get the current supply reported by the ledger. +- `get_participation_keys` - Return a list of participation keys +- `add_participation_key` - Add a participation key to the node +- `generate_participation_keys` - Generate and install participation keys to the node. +- `get_participation_key_by_id` - Get participation key info given a participation ID +- `append_keys` - Append state proof keys to a participation key +- `delete_participation_key_by_id` - Delete a given participation key by ID +- `shutdown_node` - Special management endpoint to shutdown the node. Optionally provide a timeout parameter to indicate that the node should begin shutting down after a number of seconds. +- `get_status` - Gets the current node status. +- `wait_for_block` - Gets the node status after waiting for a round after the given round. +- `raw_transaction` - Broadcasts a raw transaction or transaction group to the network. +- `raw_transaction_async` - Fast track for broadcasting a raw transaction or transaction group to the network through the tx handler without performing most of the checks and reporting detailed errors. Should be only used for development and performance testing. +- `simulate_transaction` - Simulates a raw transaction or transaction group as it would be evaluated on the network. The simulation will use blockchain state from the latest committed round. +- `transaction_params` - Get parameters for constructing a new transaction +- `get_pending_transactions` - Get a list of unconfirmed transactions currently in the transaction pool. +- `pending_transaction_information` - Get a specific pending transaction. +- `get_ledger_state_delta` - Get a LedgerStateDelta object for a given round +- `get_transaction_group_ledger_state_deltas_for_round` - Get LedgerStateDelta objects for all transaction groups in a given round +- `get_ledger_state_delta_for_transaction_group` - Get a LedgerStateDelta object for a given transaction group +- `get_state_proof` - Get a state proof that covers a given round +- `get_light_block_header_proof` - Gets a proof for a given light block header inside a state proof commitment +- `get_application_by_id` - Get application information. +- `get_application_boxes` - Get all box names for a given application. +- `get_application_box_by_name` - Get box information for a given application. +- `get_asset_by_id` - Get asset information. +- `get_sync_round` - Returns the minimum sync round the ledger is keeping in cache. +- `unset_sync_round` - Removes minimum sync round restriction from the ledger. +- `set_sync_round` - Given a round, tells the ledger to keep that round in its cache. +- `teal_compile` - Compile TEAL source code to binary, produce its hash +- `teal_disassemble` - Disassemble program bytes into the TEAL source code. +- `start_catchup` - Starts a catchpoint catchup. +- `abort_catchup` - Aborts a catchpoint catchup. +- `teal_dryrun` - Provide debugging information for a transaction (or group). +- `experimental_check` - Returns OK if experimental API is enabled. +- `get_block_time_stamp_offset` - Returns the timestamp offset. Timestamp offsets can only be set in dev mode. +- `set_block_time_stamp_offset` - Given a timestamp offset in seconds, adds the offset to every subsequent block header's timestamp. + +## Models + +The following data models are available: + +- `GenesisAllocation` - No description +- `Genesis` - No description +- `LedgerStateDelta` - Ledger StateDelta object +- `LedgerStateDeltaForTransactionGroup` - Contains a ledger delta for a single transaction group +- `Account` - Account information at a given round. + +Definition: +data/basics/userBalance.go : AccountData + +- `AccountAssetHolding` - AccountAssetHolding describes the account's asset holding and asset parameters (if either exist) for a specific asset ID. +- `AccountParticipation` - AccountParticipation describes the parameters used by this account in consensus protocol. +- `Asset` - Specifies both the unique identifier and the parameters for an asset +- `AssetHolding` - Describes an asset held by an account. + +Definition: +data/basics/userBalance.go : AssetHolding +- `AssetParams` - AssetParams specifies the parameters for an asset. + +\[apar\] when part of an AssetConfig transaction. + +Definition: +data/transactions/asset.go : AssetParams +- `AssetHoldingReference` - References an asset held by an account. +- `ApplicationLocalReference` - References an account's local state for an application. +- `ApplicationStateSchema` - Specifies maximums on the number of each type that may be stored. +- `ApplicationLocalState` - Stores local state associated with an application. +- `ParticipationKey` - Represents a participation key used by the node. +- `TealKeyValueStore` - Represents a key-value store for use in an application. +- `TealKeyValue` - Represents a key-value pair in an application store. +- `TealValue` - Represents a TEAL value. +- `AvmValue` - Represents an AVM value. +- `AvmKeyValue` - Represents an AVM key-value pair in an application store. +- `StateDelta` - Application state delta. +- `AccountStateDelta` - Application state delta. +- `EvalDeltaKeyValue` - Key-value pairs for StateDelta. +- `EvalDelta` - Represents a TEAL value delta. +- `Application` - Application index and its parameters +- `ApplicationParams` - Stores the global information associated with an application. +- `DryrunState` - Stores the TEAL eval step data +- `DryrunTxnResult` - DryrunTxnResult contains any LogicSig or ApplicationCall program debug information and state updates from a dryrun. +- `ErrorResponse` - An error response with optional data field. +- `DryrunRequest` - Request data type for dryrun endpoint. Given the Transactions and simulated ledger state upload, run TEAL scripts and return debugging information. +- `DryrunSource` - DryrunSource is TEAL source text that gets uploaded, compiled, and inserted into transactions or application state. +- `SimulateRequest` - Request type for simulation endpoint. +- `SimulateRequestTransactionGroup` - A transaction group to simulate. +- `SimulateTraceConfig` - An object that configures simulation execution trace. +- `Box` - Box name and its content. +- `BoxDescriptor` - Box descriptor describes a Box. +- `BoxReference` - References a box of an application. +- `KvDelta` - A single Delta containing the key, the previous value and the current value for a single round. +- `Version` - algod version information. +- `DebugSettingsProf` - algod mutex and blocking profiling state. +- `BuildVersion` - No description +- `PendingTransactionResponse` - Details about a pending transaction. If the transaction was recently confirmed, includes confirmation details like the round and reward details. +- `SimulateTransactionGroupResult` - Simulation result for an atomic transaction group +- `SimulateTransactionResult` - Simulation result for an individual transaction +- `StateProof` - Represents a state proof and its corresponding message +- `LightBlockHeaderProof` - Proof of membership and position of a light block header. +- `StateProofMessage` - Represents the message that the state proofs are attesting to. +- `SimulationEvalOverrides` - The set of parameters and limits override during simulation. If this set of parameters is present, then evaluation parameters may differ from standard evaluation in certain ways. +- `ScratchChange` - A write operation into a scratch slot. +- `ApplicationStateOperation` - An operation against an application's global/local/box state. +- `ApplicationKvStorage` - An application's global/local/box state. +- `ApplicationInitialStates` - An application's initial global/local/box states that were accessed during simulation. +- `SimulationOpcodeTraceUnit` - The set of trace information and effect from evaluating a single opcode. +- `SimulationTransactionExecTrace` - The execution trace of calling an app or a logic sig, containing the inner app call trace in a recursive way. +- `SimulateUnnamedResourcesAccessed` - These are resources that were accessed by this group that would normally have caused failure, but were allowed in simulation. Depending on where this object is in the response, the unnamed resources it contains may or may not qualify for group resource sharing. If this is a field in SimulateTransactionGroupResult, the resources do qualify, but if this is a field in SimulateTransactionResult, they do not qualify. In order to make this group valid for actual submission, resources that qualify for group sharing can be made available by any transaction of the group; otherwise, resources must be placed in the same transaction which accessed them. +- `SimulateInitialStates` - Initial states of resources that were accessed during simulation. +- `AppCallLogs` - The logged messages from an app call along with the app ID and outer transaction ID. Logs appear in the same order that they were emitted. +- `AccountAssetInformation` - AccountAssetResponse describes the account's asset holding and asset parameters (if either exist) for a specific asset ID. Asset parameters will only be returned if the provided address is the asset's creator. +- `AccountAssetsInformation` - AccountAssetsInformationResponse contains a list of assets held by an account. +- `AccountApplicationInformation` - AccountApplicationResponse describes the account's application local state and global state (AppLocalState and AppParams, if either exists) for a specific application ID. Global state will only be returned if the provided address is the application's creator. +- `GetPendingTransactionsByAddress` - PendingTransactions is an array of signed transactions exactly as they were submitted. +- `GetBlock` - Encoded block object. +- `GetBlockTxids` - Top level transaction IDs in a block. +- `GetBlockHash` - Hash of a block header. +- `GetTransactionProof` - Proof of transaction in a block. +- `GetBlockLogs` - All logs emitted in the given round. Each app call, whether top-level or inner, that contains logs results in a separate AppCallLogs object. Therefore there may be multiple AppCallLogs with the same application ID and outer transaction ID in the event of multiple inner app calls to the same app. App calls with no logs are not included in the response. AppCallLogs are returned in the same order that their corresponding app call appeared in the block (pre-order traversal of inner app calls) +- `GetSupply` - Supply represents the current supply of MicroAlgos in the system +- `AddParticipationKey` - Participation ID of the submission +- `GetStatus` - NodeStatus contains the information about a node status +- `WaitForBlock` - NodeStatus contains the information about a node status +- `RawTransaction` - Transaction ID of the submission. +- `SimulateTransaction` - Result of a transaction group simulation. +- `TransactionParams` - TransactionParams contains the parameters that help a client construct +a new transaction. +- `GetPendingTransactions` - PendingTransactions is an array of signed transactions exactly as they were submitted. +- `GetTransactionGroupLedgerStateDeltasForRound` - Response containing all ledger state deltas for transaction groups, with their associated Ids, in a single round. +- `GetApplicationBoxes` - Box names of an application +- `GetSyncRound` - Response containing the ledger's minimum sync round +- `TealCompile` - Teal compile Result +- `TealDisassemble` - Teal disassembly Result +- `StartCatchup` - An catchpoint start response. +- `AbortCatchup` - An catchpoint abort response. +- `TealDryrun` - DryrunResponse contains per-txn debug information from a dryrun. +- `GetBlockTimeStampOffset` - Response containing the timestamp offset in seconds + +## Error Handling + +All API operations return a `Result` type. Errors include: + +- Network errors (connection issues, timeouts) +- HTTP errors (4xx, 5xx status codes) +- Serialization errors (invalid JSON responses) + +```rust +// Example error handling +match client.get_status().await { + Ok(status) => { + println!("Node is running on round: {}", status.last_round); + } + Err(error) => { + eprintln!("Failed to get node status: {:?}", error); + // Handle specific error types if needed + } +} + +// Or use the ? operator for early returns +let params = client.transaction_params().await + .map_err(|e| format!("Failed to get transaction params: {}", e))?; +``` + +## Generated Code + +This client was generated from an OpenAPI specification using a custom Rust code generator. + +**Generated on:** Generated by Rust OpenAPI Generator +**OpenAPI Version:** 3.0.0 +**Generator:** Rust OpenAPI Generator diff --git a/crates/algod_client/src/apis/abort_catchup.rs b/crates/algod_client/src/apis/abort_catchup.rs new file mode 100644 index 00000000..d6409dfe --- /dev/null +++ b/crates/algod_client/src/apis/abort_catchup.rs @@ -0,0 +1,86 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{AbortCatchup, ErrorResponse}; + +// Import request body type if needed + +/// struct for typed errors of method [`abort_catchup`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum AbortCatchupError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Given a catchpoint, it aborts catching up to this catchpoint +pub async fn abort_catchup( + http_client: &dyn HttpClient, + catchpoint: &str, +) -> Result { + let p_catchpoint = catchpoint; + + let path = format!( + "/v2/catchup/{catchpoint}", + catchpoint = crate::apis::urlencode(p_catchpoint) + ); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Delete, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/account_application_information.rs b/crates/algod_client/src/apis/account_application_information.rs new file mode 100644 index 00000000..e9ffc2fc --- /dev/null +++ b/crates/algod_client/src/apis/account_application_information.rs @@ -0,0 +1,106 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::parameter_enums::*; +use super::{AlgodApiError, ContentType, Error}; +use algokit_transact::AlgorandMsgpack; + +// Import all custom types used by this endpoint +use crate::models::{AccountApplicationInformation, ErrorResponse}; + +// Import request body type if needed + +/// struct for typed errors of method [`account_application_information`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum AccountApplicationInformationError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Given a specific account public key and application ID, this call returns the account's application local state and global state (AppLocalState and AppParams, if either exists). Global state will only be returned if the provided address is the application's creator. +pub async fn account_application_information( + http_client: &dyn HttpClient, + format: Option, + address: &str, + application_id: u64, +) -> Result { + let p_format = format; + let p_address = address; + let p_application_id = application_id; + + let path = format!( + "/v2/accounts/{address}/applications/{application_id}", + address = crate::apis::urlencode(p_address), + application_id = p_application_id + ); + + let mut query_params: HashMap = HashMap::new(); + if let Some(value) = p_format { + query_params.insert("format".to_string(), value.to_string()); + } + + let use_msgpack = p_format.map(|f| f != Format::Json).unwrap_or(true); + + let mut headers: HashMap = HashMap::new(); + if use_msgpack { + headers.insert( + "Content-Type".to_string(), + "application/msgpack".to_string(), + ); + headers.insert("Accept".to_string(), "application/msgpack".to_string()); + } else { + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + } + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/account_asset_information.rs b/crates/algod_client/src/apis/account_asset_information.rs new file mode 100644 index 00000000..da4f45fa --- /dev/null +++ b/crates/algod_client/src/apis/account_asset_information.rs @@ -0,0 +1,106 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::parameter_enums::*; +use super::{AlgodApiError, ContentType, Error}; +use algokit_transact::AlgorandMsgpack; + +// Import all custom types used by this endpoint +use crate::models::{AccountAssetInformation, ErrorResponse}; + +// Import request body type if needed + +/// struct for typed errors of method [`account_asset_information`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum AccountAssetInformationError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Given a specific account public key and asset ID, this call returns the account's asset holding and asset parameters (if either exist). Asset parameters will only be returned if the provided address is the asset's creator. +pub async fn account_asset_information( + http_client: &dyn HttpClient, + format: Option, + address: &str, + asset_id: u64, +) -> Result { + let p_format = format; + let p_address = address; + let p_asset_id = asset_id; + + let path = format!( + "/v2/accounts/{address}/assets/{asset_id}", + address = crate::apis::urlencode(p_address), + asset_id = p_asset_id + ); + + let mut query_params: HashMap = HashMap::new(); + if let Some(value) = p_format { + query_params.insert("format".to_string(), value.to_string()); + } + + let use_msgpack = p_format.map(|f| f != Format::Json).unwrap_or(true); + + let mut headers: HashMap = HashMap::new(); + if use_msgpack { + headers.insert( + "Content-Type".to_string(), + "application/msgpack".to_string(), + ); + headers.insert("Accept".to_string(), "application/msgpack".to_string()); + } else { + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + } + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/account_assets_information.rs b/crates/algod_client/src/apis/account_assets_information.rs new file mode 100644 index 00000000..1951712b --- /dev/null +++ b/crates/algod_client/src/apis/account_assets_information.rs @@ -0,0 +1,96 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{AccountAssetsInformation, ErrorResponse}; + +// Import request body type if needed + +/// struct for typed errors of method [`account_assets_information`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum AccountAssetsInformationError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Lookup an account's asset holdings. +pub async fn account_assets_information( + http_client: &dyn HttpClient, + address: &str, + limit: Option, + next: Option<&str>, +) -> Result { + let p_address = address; + let p_limit = limit; + let p_next = next; + + let path = format!( + "/v2/accounts/{address}/assets", + address = crate::apis::urlencode(p_address) + ); + + let mut query_params: HashMap = HashMap::new(); + if let Some(value) = p_limit { + query_params.insert("limit".to_string(), value.to_string()); + } + if let Some(value) = p_next { + query_params.insert("next".to_string(), value.to_string()); + } + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/account_information.rs b/crates/algod_client/src/apis/account_information.rs new file mode 100644 index 00000000..f59ade88 --- /dev/null +++ b/crates/algod_client/src/apis/account_information.rs @@ -0,0 +1,108 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::parameter_enums::*; +use super::{AlgodApiError, ContentType, Error}; +use algokit_transact::AlgorandMsgpack; + +// Import all custom types used by this endpoint +use crate::models::{Account, ErrorResponse}; + +// Import request body type if needed + +/// struct for typed errors of method [`account_information`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum AccountInformationError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Given a specific account public key, this call returns the account's status, balance and spendable amounts +pub async fn account_information( + http_client: &dyn HttpClient, + format: Option, + address: &str, + exclude: Option, +) -> Result { + let p_format = format; + let p_address = address; + let p_exclude = exclude; + + let path = format!( + "/v2/accounts/{address}", + address = crate::apis::urlencode(p_address) + ); + + let mut query_params: HashMap = HashMap::new(); + if let Some(value) = p_format { + query_params.insert("format".to_string(), value.to_string()); + } + if let Some(value) = p_exclude { + query_params.insert("exclude".to_string(), value.to_string()); + } + + let use_msgpack = p_format.map(|f| f != Format::Json).unwrap_or(true); + + let mut headers: HashMap = HashMap::new(); + if use_msgpack { + headers.insert( + "Content-Type".to_string(), + "application/msgpack".to_string(), + ); + headers.insert("Accept".to_string(), "application/msgpack".to_string()); + } else { + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + } + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/add_participation_key.rs b/crates/algod_client/src/apis/add_participation_key.rs new file mode 100644 index 00000000..0af23ae7 --- /dev/null +++ b/crates/algod_client/src/apis/add_participation_key.rs @@ -0,0 +1,89 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; +use algokit_transact::AlgorandMsgpack; + +// Import all custom types used by this endpoint +use crate::models::{AddParticipationKey, ErrorResponse}; + +// Import request body type if needed + +/// struct for typed errors of method [`add_participation_key`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum AddParticipationKeyError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status404(ErrorResponse), + Status500(ErrorResponse), + Status503(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Add a participation key to the node +pub async fn add_participation_key( + http_client: &dyn HttpClient, + request: Vec, +) -> Result { + let p_request = request; + + let path = "/v2/participation".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert( + "Content-Type".to_string(), + "application/msgpack".to_string(), + ); + headers.insert("Accept".to_string(), "application/msgpack".to_string()); + + let body = Some(p_request); + + let response = http_client + .request( + HttpMethod::Post, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/append_keys.rs b/crates/algod_client/src/apis/append_keys.rs new file mode 100644 index 00000000..5c1759b1 --- /dev/null +++ b/crates/algod_client/src/apis/append_keys.rs @@ -0,0 +1,93 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; +use algokit_transact::AlgorandMsgpack; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, ParticipationKey}; + +// Import request body type if needed + +/// struct for typed errors of method [`append_keys`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum AppendKeysError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status404(ErrorResponse), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Given a participation ID, append state proof keys to a particular set of participation keys +pub async fn append_keys( + http_client: &dyn HttpClient, + request: Vec, + participation_id: &str, +) -> Result { + let p_participation_id = participation_id; + let p_request = request; + + let path = format!( + "/v2/participation/{participation_id}", + participation_id = crate::apis::urlencode(p_participation_id) + ); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert( + "Content-Type".to_string(), + "application/msgpack".to_string(), + ); + headers.insert("Accept".to_string(), "application/msgpack".to_string()); + + let body = Some(p_request); + + let response = http_client + .request( + HttpMethod::Post, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/client.rs b/crates/algod_client/src/apis/client.rs new file mode 100644 index 00000000..dce1ffb2 --- /dev/null +++ b/crates/algod_client/src/apis/client.rs @@ -0,0 +1,552 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use super::Error; +use super::parameter_enums::*; +use crate::models::{ + AbortCatchup, Account, AccountApplicationInformation, AccountAssetInformation, + AccountAssetsInformation, AddParticipationKey, Application, Asset, Box, DebugSettingsProf, + DryrunRequest, ErrorResponse, Genesis, GetApplicationBoxes, GetBlock, GetBlockHash, + GetBlockLogs, GetBlockTimeStampOffset, GetBlockTxids, GetPendingTransactions, + GetPendingTransactionsByAddress, GetStatus, GetSupply, GetSyncRound, + GetTransactionGroupLedgerStateDeltasForRound, GetTransactionProof, LedgerStateDelta, + LightBlockHeaderProof, ParticipationKey, PendingTransactionResponse, RawTransaction, + SimulateRequest, SimulateTransaction, StartCatchup, StateProof, TealCompile, TealDisassemble, + TealDryrun, TransactionParams, Version, WaitForBlock, +}; +use algokit_http_client::{DefaultHttpClient, HttpClient}; +use std::sync::Arc; + +/// The main Algod API client. +/// +/// This client provides convenient access to all Algod API endpoints. +/// It wraps the lower-level endpoint functions with a more ergonomic interface. +/// All methods return a unified `Error` type that can represent any endpoint error. +#[derive(Clone)] +pub struct AlgodClient { + http_client: Arc, +} + +impl AlgodClient { + /// Create a new AlgodClient with a custom http client. + pub fn new(http_client: Arc) -> Self { + Self { http_client } + } + + /// Create a new AlgodClient for Algorand TestNet. + #[cfg(feature = "default_client")] + pub fn testnet() -> Self { + let http_client = Arc::new(DefaultHttpClient::new( + "https://testnet-api.4160.nodely.dev", + )); + Self::new(http_client) + } + + /// Create a new AlgodClient for Algorand MainNet. + #[cfg(feature = "default_client")] + pub fn mainnet() -> Self { + let http_client = Arc::new(DefaultHttpClient::new( + "https://mainnet-api.4160.nodely.dev", + )); + Self::new(http_client) + } + + /// Create a new AlgodClient for a local localnet environment. + #[cfg(feature = "default_client")] + pub fn localnet() -> Self { + let http_client = Arc::new( + DefaultHttpClient::with_header( + "http://localhost:4001", + "X-Algo-API-Token", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ) + .expect("Failed to create HTTP client with X-Algo-API-Token header"), + ); + Self::new(http_client) + } + + /// Returns OK if healthy. + pub async fn health_check(&self) -> Result<(), Error> { + super::health_check::health_check(self.http_client.as_ref()).await + } + + /// Returns OK if healthy and fully caught up. + pub async fn get_ready(&self) -> Result<(), Error> { + super::get_ready::get_ready(self.http_client.as_ref()).await + } + + /// Return metrics about algod functioning. + pub async fn metrics(&self) -> Result<(), Error> { + super::metrics::metrics(self.http_client.as_ref()).await + } + + /// Gets the genesis information. + pub async fn get_genesis(&self) -> Result { + super::get_genesis::get_genesis(self.http_client.as_ref()).await + } + + /// Gets the current swagger spec. + pub async fn swagger_json(&self) -> Result { + super::swagger_json::swagger_json(self.http_client.as_ref()).await + } + + /// Retrieves the supported API versions, binary build versions, and genesis information. + pub async fn get_version(&self) -> Result { + super::get_version::get_version(self.http_client.as_ref()).await + } + + /// Retrieves the current settings for blocking and mutex profiles + pub async fn get_debug_settings_prof(&self) -> Result { + super::get_debug_settings_prof::get_debug_settings_prof(self.http_client.as_ref()).await + } + + /// Enables blocking and mutex profiles, and returns the old settings + pub async fn put_debug_settings_prof(&self) -> Result { + super::put_debug_settings_prof::put_debug_settings_prof(self.http_client.as_ref()).await + } + + /// Gets the merged config file. + pub async fn get_config(&self) -> Result { + super::get_config::get_config(self.http_client.as_ref()).await + } + + /// Get account information. + pub async fn account_information( + &self, + format: Option, + address: &str, + exclude: Option, + ) -> Result { + super::account_information::account_information( + self.http_client.as_ref(), + format, + address, + exclude, + ) + .await + } + + /// Get account information about a given asset. + pub async fn account_asset_information( + &self, + format: Option, + address: &str, + asset_id: u64, + ) -> Result { + super::account_asset_information::account_asset_information( + self.http_client.as_ref(), + format, + address, + asset_id, + ) + .await + } + + /// Get a list of assets held by an account, inclusive of asset params. + pub async fn account_assets_information( + &self, + address: &str, + limit: Option, + next: Option<&str>, + ) -> Result { + super::account_assets_information::account_assets_information( + self.http_client.as_ref(), + address, + limit, + next, + ) + .await + } + + /// Get account information about a given app. + pub async fn account_application_information( + &self, + format: Option, + address: &str, + application_id: u64, + ) -> Result { + super::account_application_information::account_application_information( + self.http_client.as_ref(), + format, + address, + application_id, + ) + .await + } + + /// Get a list of unconfirmed transactions currently in the transaction pool by address. + pub async fn get_pending_transactions_by_address( + &self, + address: &str, + max: Option, + format: Option, + ) -> Result { + super::get_pending_transactions_by_address::get_pending_transactions_by_address( + self.http_client.as_ref(), + address, + max, + format, + ) + .await + } + + /// Get the block for the given round. + pub async fn get_block( + &self, + format: Option, + round: u64, + header_only: Option, + ) -> Result { + super::get_block::get_block(self.http_client.as_ref(), format, round, header_only).await + } + + /// Get the top level transaction IDs for the block on the given round. + pub async fn get_block_txids(&self, round: u64) -> Result { + super::get_block_txids::get_block_txids(self.http_client.as_ref(), round).await + } + + /// Get the block hash for the block on the given round. + pub async fn get_block_hash(&self, round: u64) -> Result { + super::get_block_hash::get_block_hash(self.http_client.as_ref(), round).await + } + + /// Get a proof for a transaction in a block. + pub async fn get_transaction_proof( + &self, + round: u64, + txid: &str, + hashtype: Option, + format: Option, + ) -> Result { + super::get_transaction_proof::get_transaction_proof( + self.http_client.as_ref(), + round, + txid, + hashtype, + format, + ) + .await + } + + /// Get all of the logs from outer and inner app calls in the given round + pub async fn get_block_logs(&self, round: u64) -> Result { + super::get_block_logs::get_block_logs(self.http_client.as_ref(), round).await + } + + /// Get the current supply reported by the ledger. + pub async fn get_supply(&self) -> Result { + super::get_supply::get_supply(self.http_client.as_ref()).await + } + + /// Return a list of participation keys + pub async fn get_participation_keys(&self) -> Result, Error> { + super::get_participation_keys::get_participation_keys(self.http_client.as_ref()).await + } + + /// Add a participation key to the node + pub async fn add_participation_key( + &self, + request: Vec, + ) -> Result { + super::add_participation_key::add_participation_key(self.http_client.as_ref(), request) + .await + } + + /// Generate and install participation keys to the node. + pub async fn generate_participation_keys( + &self, + address: &str, + dilution: Option, + first: u64, + last: u64, + ) -> Result { + super::generate_participation_keys::generate_participation_keys( + self.http_client.as_ref(), + address, + dilution, + first, + last, + ) + .await + } + + /// Get participation key info given a participation ID + pub async fn get_participation_key_by_id( + &self, + participation_id: &str, + ) -> Result { + super::get_participation_key_by_id::get_participation_key_by_id( + self.http_client.as_ref(), + participation_id, + ) + .await + } + + /// Append state proof keys to a participation key + pub async fn append_keys( + &self, + request: Vec, + participation_id: &str, + ) -> Result { + super::append_keys::append_keys(self.http_client.as_ref(), request, participation_id).await + } + + /// Delete a given participation key by ID + pub async fn delete_participation_key_by_id( + &self, + participation_id: &str, + ) -> Result<(), Error> { + super::delete_participation_key_by_id::delete_participation_key_by_id( + self.http_client.as_ref(), + participation_id, + ) + .await + } + + /// Special management endpoint to shutdown the node. Optionally provide a timeout parameter to indicate that the node should begin shutting down after a number of seconds. + pub async fn shutdown_node(&self, timeout: Option) -> Result { + super::shutdown_node::shutdown_node(self.http_client.as_ref(), timeout).await + } + + /// Gets the current node status. + pub async fn get_status(&self) -> Result { + super::get_status::get_status(self.http_client.as_ref()).await + } + + /// Gets the node status after waiting for a round after the given round. + pub async fn wait_for_block(&self, round: u64) -> Result { + super::wait_for_block::wait_for_block(self.http_client.as_ref(), round).await + } + + /// Broadcasts a raw transaction or transaction group to the network. + pub async fn raw_transaction(&self, request: Vec) -> Result { + super::raw_transaction::raw_transaction(self.http_client.as_ref(), request).await + } + + /// Fast track for broadcasting a raw transaction or transaction group to the network through the tx handler without performing most of the checks and reporting detailed errors. Should be only used for development and performance testing. + pub async fn raw_transaction_async(&self, request: Vec) -> Result<(), Error> { + super::raw_transaction_async::raw_transaction_async(self.http_client.as_ref(), request) + .await + } + + /// Simulates a raw transaction or transaction group as it would be evaluated on the network. The simulation will use blockchain state from the latest committed round. + pub async fn simulate_transaction( + &self, + request: SimulateRequest, + format: Option, + ) -> Result { + super::simulate_transaction::simulate_transaction( + self.http_client.as_ref(), + request, + format, + ) + .await + } + + /// Get parameters for constructing a new transaction + pub async fn transaction_params(&self) -> Result { + super::transaction_params::transaction_params(self.http_client.as_ref()).await + } + + /// Get a list of unconfirmed transactions currently in the transaction pool. + pub async fn get_pending_transactions( + &self, + max: Option, + format: Option, + ) -> Result { + super::get_pending_transactions::get_pending_transactions( + self.http_client.as_ref(), + max, + format, + ) + .await + } + + /// Get a specific pending transaction. + pub async fn pending_transaction_information( + &self, + txid: &str, + format: Option, + ) -> Result { + super::pending_transaction_information::pending_transaction_information( + self.http_client.as_ref(), + txid, + format, + ) + .await + } + + /// Get a LedgerStateDelta object for a given round + pub async fn get_ledger_state_delta( + &self, + round: u64, + format: Option, + ) -> Result { + super::get_ledger_state_delta::get_ledger_state_delta( + self.http_client.as_ref(), + round, + format, + ) + .await + } + + /// Get LedgerStateDelta objects for all transaction groups in a given round + pub async fn get_transaction_group_ledger_state_deltas_for_round( + &self, + round: u64, + format: Option, + ) -> Result { + super::get_transaction_group_ledger_state_deltas_for_round::get_transaction_group_ledger_state_deltas_for_round( + self.http_client.as_ref(), + round, + format, + ).await + } + + /// Get a LedgerStateDelta object for a given transaction group + pub async fn get_ledger_state_delta_for_transaction_group( + &self, + id: &str, + format: Option, + ) -> Result { + super::get_ledger_state_delta_for_transaction_group::get_ledger_state_delta_for_transaction_group( + self.http_client.as_ref(), + id, + format, + ).await + } + + /// Get a state proof that covers a given round + pub async fn get_state_proof(&self, round: u64) -> Result { + super::get_state_proof::get_state_proof(self.http_client.as_ref(), round).await + } + + /// Gets a proof for a given light block header inside a state proof commitment + pub async fn get_light_block_header_proof( + &self, + round: u64, + ) -> Result { + super::get_light_block_header_proof::get_light_block_header_proof( + self.http_client.as_ref(), + round, + ) + .await + } + + /// Get application information. + pub async fn get_application_by_id(&self, application_id: u64) -> Result { + super::get_application_by_id::get_application_by_id( + self.http_client.as_ref(), + application_id, + ) + .await + } + + /// Get all box names for a given application. + pub async fn get_application_boxes( + &self, + application_id: u64, + max: Option, + ) -> Result { + super::get_application_boxes::get_application_boxes( + self.http_client.as_ref(), + application_id, + max, + ) + .await + } + + /// Get box information for a given application. + pub async fn get_application_box_by_name( + &self, + application_id: u64, + name: &str, + ) -> Result { + super::get_application_box_by_name::get_application_box_by_name( + self.http_client.as_ref(), + application_id, + name, + ) + .await + } + + /// Get asset information. + pub async fn get_asset_by_id(&self, asset_id: u64) -> Result { + super::get_asset_by_id::get_asset_by_id(self.http_client.as_ref(), asset_id).await + } + + /// Returns the minimum sync round the ledger is keeping in cache. + pub async fn get_sync_round(&self) -> Result { + super::get_sync_round::get_sync_round(self.http_client.as_ref()).await + } + + /// Removes minimum sync round restriction from the ledger. + pub async fn unset_sync_round(&self) -> Result<(), Error> { + super::unset_sync_round::unset_sync_round(self.http_client.as_ref()).await + } + + /// Given a round, tells the ledger to keep that round in its cache. + pub async fn set_sync_round(&self, round: u64) -> Result<(), Error> { + super::set_sync_round::set_sync_round(self.http_client.as_ref(), round).await + } + + /// Compile TEAL source code to binary, produce its hash + pub async fn teal_compile( + &self, + request: Vec, + sourcemap: Option, + ) -> Result { + super::teal_compile::teal_compile(self.http_client.as_ref(), request, sourcemap).await + } + + /// Disassemble program bytes into the TEAL source code. + pub async fn teal_disassemble(&self, request: String) -> Result { + super::teal_disassemble::teal_disassemble(self.http_client.as_ref(), request).await + } + + /// Starts a catchpoint catchup. + pub async fn start_catchup( + &self, + catchpoint: &str, + min: Option, + ) -> Result { + super::start_catchup::start_catchup(self.http_client.as_ref(), catchpoint, min).await + } + + /// Aborts a catchpoint catchup. + pub async fn abort_catchup(&self, catchpoint: &str) -> Result { + super::abort_catchup::abort_catchup(self.http_client.as_ref(), catchpoint).await + } + + /// Provide debugging information for a transaction (or group). + pub async fn teal_dryrun(&self, request: Option) -> Result { + super::teal_dryrun::teal_dryrun(self.http_client.as_ref(), request).await + } + + /// Returns OK if experimental API is enabled. + pub async fn experimental_check(&self) -> Result<(), Error> { + super::experimental_check::experimental_check(self.http_client.as_ref()).await + } + + /// Returns the timestamp offset. Timestamp offsets can only be set in dev mode. + pub async fn get_block_time_stamp_offset(&self) -> Result { + super::get_block_time_stamp_offset::get_block_time_stamp_offset(self.http_client.as_ref()) + .await + } + + /// Given a timestamp offset in seconds, adds the offset to every subsequent block header's timestamp. + pub async fn set_block_time_stamp_offset(&self, offset: u64) -> Result<(), Error> { + super::set_block_time_stamp_offset::set_block_time_stamp_offset( + self.http_client.as_ref(), + offset, + ) + .await + } +} diff --git a/crates/algod_client/src/apis/delete_participation_key_by_id.rs b/crates/algod_client/src/apis/delete_participation_key_by_id.rs new file mode 100644 index 00000000..8079b759 --- /dev/null +++ b/crates/algod_client/src/apis/delete_participation_key_by_id.rs @@ -0,0 +1,68 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::ErrorResponse; + +// Import request body type if needed + +/// struct for typed errors of method [`delete_participation_key_by_id`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum DeleteParticipationKeyByIdError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status404(ErrorResponse), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Delete a given participation key by ID +pub async fn delete_participation_key_by_id( + http_client: &dyn HttpClient, + participation_id: &str, +) -> Result<(), Error> { + let p_participation_id = participation_id; + + let path = format!( + "/v2/participation/{participation_id}", + participation_id = crate::apis::urlencode(p_participation_id) + ); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Delete, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let _ = response; + Ok(()) +} diff --git a/crates/algod_client/src/apis/experimental_check.rs b/crates/algod_client/src/apis/experimental_check.rs new file mode 100644 index 00000000..33f667c9 --- /dev/null +++ b/crates/algod_client/src/apis/experimental_check.rs @@ -0,0 +1,56 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint + +// Import request body type if needed + +/// struct for typed errors of method [`experimental_check`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ExperimentalCheckError { + Status404(), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Returns OK if experimental API is enabled. +pub async fn experimental_check(http_client: &dyn HttpClient) -> Result<(), Error> { + let path = "/v2/experimental".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let _ = response; + Ok(()) +} diff --git a/crates/algod_client/src/apis/generate_participation_keys.rs b/crates/algod_client/src/apis/generate_participation_keys.rs new file mode 100644 index 00000000..63b95c80 --- /dev/null +++ b/crates/algod_client/src/apis/generate_participation_keys.rs @@ -0,0 +1,98 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::ErrorResponse; + +// Import request body type if needed + +/// struct for typed errors of method [`generate_participation_keys`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GenerateParticipationKeysError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status500(ErrorResponse), + Status503(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Generate and install participation keys to the node. +pub async fn generate_participation_keys( + http_client: &dyn HttpClient, + address: &str, + dilution: Option, + first: u64, + last: u64, +) -> Result { + let p_address = address; + let p_dilution = dilution; + let p_first = first; + let p_last = last; + + let path = format!( + "/v2/participation/generate/{address}", + address = crate::apis::urlencode(p_address) + ); + + let mut query_params: HashMap = HashMap::new(); + if let Some(value) = p_dilution { + query_params.insert("dilution".to_string(), value.to_string()); + } + query_params.insert("first".to_string(), p_first.to_string()); + query_params.insert("last".to_string(), p_last.to_string()); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Post, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_application_box_by_name.rs b/crates/algod_client/src/apis/get_application_box_by_name.rs new file mode 100644 index 00000000..2989c077 --- /dev/null +++ b/crates/algod_client/src/apis/get_application_box_by_name.rs @@ -0,0 +1,90 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{Box, ErrorResponse}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_application_box_by_name`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetApplicationBoxByNameError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status404(ErrorResponse), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Given an application ID and box name, it returns the round, box name, and value (each base64 encoded). Box names must be in the goal app call arg encoding form 'encoding:value'. For ints, use the form 'int:1234'. For raw bytes, use the form 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use the form 'addr:XYZ...'. +pub async fn get_application_box_by_name( + http_client: &dyn HttpClient, + application_id: u64, + name: &str, +) -> Result { + let p_application_id = application_id; + let p_name = name; + + let path = format!( + "/v2/applications/{application_id}/box", + application_id = p_application_id + ); + + let mut query_params: HashMap = HashMap::new(); + query_params.insert("name".to_string(), p_name.to_string()); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_application_boxes.rs b/crates/algod_client/src/apis/get_application_boxes.rs new file mode 100644 index 00000000..bcaedfb1 --- /dev/null +++ b/crates/algod_client/src/apis/get_application_boxes.rs @@ -0,0 +1,91 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, GetApplicationBoxes}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_application_boxes`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetApplicationBoxesError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Given an application ID, return all Box names. No particular ordering is guaranteed. Request fails when client or server-side configured limits prevent returning all Box names. +pub async fn get_application_boxes( + http_client: &dyn HttpClient, + application_id: u64, + max: Option, +) -> Result { + let p_application_id = application_id; + let p_max = max; + + let path = format!( + "/v2/applications/{application_id}/boxes", + application_id = p_application_id + ); + + let mut query_params: HashMap = HashMap::new(); + if let Some(value) = p_max { + query_params.insert("max".to_string(), value.to_string()); + } + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_application_by_id.rs b/crates/algod_client/src/apis/get_application_by_id.rs new file mode 100644 index 00000000..6c135135 --- /dev/null +++ b/crates/algod_client/src/apis/get_application_by_id.rs @@ -0,0 +1,87 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{Application, ErrorResponse}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_application_by_id`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetApplicationByIdError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status404(ErrorResponse), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Given a application ID, it returns application information including creator, approval and clear programs, global and local schemas, and global state. +pub async fn get_application_by_id( + http_client: &dyn HttpClient, + application_id: u64, +) -> Result { + let p_application_id = application_id; + + let path = format!( + "/v2/applications/{application_id}", + application_id = p_application_id + ); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_asset_by_id.rs b/crates/algod_client/src/apis/get_asset_by_id.rs new file mode 100644 index 00000000..db6cad3d --- /dev/null +++ b/crates/algod_client/src/apis/get_asset_by_id.rs @@ -0,0 +1,81 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{Asset, ErrorResponse}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_asset_by_id`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetAssetByIdError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status404(ErrorResponse), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Given a asset ID, it returns asset information including creator, name, total supply and special addresses. +pub async fn get_asset_by_id(http_client: &dyn HttpClient, asset_id: u64) -> Result { + let p_asset_id = asset_id; + + let path = format!("/v2/assets/{asset_id}", asset_id = p_asset_id); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_block.rs b/crates/algod_client/src/apis/get_block.rs new file mode 100644 index 00000000..6b3a442f --- /dev/null +++ b/crates/algod_client/src/apis/get_block.rs @@ -0,0 +1,106 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::parameter_enums::*; +use super::{AlgodApiError, ContentType, Error}; +use algokit_transact::AlgorandMsgpack; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, GetBlock}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_block`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetBlockError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status404(ErrorResponse), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Get the block for the given round. +pub async fn get_block( + http_client: &dyn HttpClient, + format: Option, + round: u64, + header_only: Option, +) -> Result { + let p_format = format; + let p_round = round; + let p_header_only = header_only; + + let path = format!("/v2/blocks/{round}", round = p_round); + + let mut query_params: HashMap = HashMap::new(); + if let Some(value) = p_format { + query_params.insert("format".to_string(), value.to_string()); + } + if let Some(value) = p_header_only { + query_params.insert("header-only".to_string(), value.to_string()); + } + + let use_msgpack = p_format.map(|f| f != Format::Json).unwrap_or(true); + + let mut headers: HashMap = HashMap::new(); + if use_msgpack { + headers.insert( + "Content-Type".to_string(), + "application/msgpack".to_string(), + ); + headers.insert("Accept".to_string(), "application/msgpack".to_string()); + } else { + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + } + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_block_hash.rs b/crates/algod_client/src/apis/get_block_hash.rs new file mode 100644 index 00000000..d5ac3358 --- /dev/null +++ b/crates/algod_client/src/apis/get_block_hash.rs @@ -0,0 +1,84 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, GetBlockHash}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_block_hash`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetBlockHashError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status404(ErrorResponse), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Get the block hash for the block on the given round. +pub async fn get_block_hash( + http_client: &dyn HttpClient, + round: u64, +) -> Result { + let p_round = round; + + let path = format!("/v2/blocks/{round}/hash", round = p_round); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_block_logs.rs b/crates/algod_client/src/apis/get_block_logs.rs new file mode 100644 index 00000000..9259abfa --- /dev/null +++ b/crates/algod_client/src/apis/get_block_logs.rs @@ -0,0 +1,83 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, GetBlockLogs}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_block_logs`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetBlockLogsError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status404(ErrorResponse), + Status500(ErrorResponse), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Get all of the logs from outer and inner app calls in the given round +pub async fn get_block_logs( + http_client: &dyn HttpClient, + round: u64, +) -> Result { + let p_round = round; + + let path = format!("/v2/blocks/{round}/logs", round = p_round); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_block_time_stamp_offset.rs b/crates/algod_client/src/apis/get_block_time_stamp_offset.rs new file mode 100644 index 00000000..38f9ee6e --- /dev/null +++ b/crates/algod_client/src/apis/get_block_time_stamp_offset.rs @@ -0,0 +1,78 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, GetBlockTimeStampOffset}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_block_time_stamp_offset`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetBlockTimeStampOffsetError { + Status400(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Gets the current timestamp offset. +pub async fn get_block_time_stamp_offset( + http_client: &dyn HttpClient, +) -> Result { + let path = "/v2/devmode/blocks/offset".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_block_txids.rs b/crates/algod_client/src/apis/get_block_txids.rs new file mode 100644 index 00000000..b8e9e6fc --- /dev/null +++ b/crates/algod_client/src/apis/get_block_txids.rs @@ -0,0 +1,84 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, GetBlockTxids}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_block_txids`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetBlockTxidsError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status404(ErrorResponse), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Get the top level transaction IDs for the block on the given round. +pub async fn get_block_txids( + http_client: &dyn HttpClient, + round: u64, +) -> Result { + let p_round = round; + + let path = format!("/v2/blocks/{round}/txids", round = p_round); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_config.rs b/crates/algod_client/src/apis/get_config.rs new file mode 100644 index 00000000..c592be17 --- /dev/null +++ b/crates/algod_client/src/apis/get_config.rs @@ -0,0 +1,74 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint + +// Import request body type if needed + +/// struct for typed errors of method [`get_config`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetConfigError { + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Returns the merged (defaults + overrides) config file in json. +pub async fn get_config(http_client: &dyn HttpClient) -> Result { + let path = "/debug/settings/config".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_debug_settings_prof.rs b/crates/algod_client/src/apis/get_debug_settings_prof.rs new file mode 100644 index 00000000..6a680a37 --- /dev/null +++ b/crates/algod_client/src/apis/get_debug_settings_prof.rs @@ -0,0 +1,76 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::DebugSettingsProf; + +// Import request body type if needed + +/// struct for typed errors of method [`get_debug_settings_prof`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetDebugSettingsProfError { + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Retrieves the current settings for blocking and mutex profiles +pub async fn get_debug_settings_prof( + http_client: &dyn HttpClient, +) -> Result { + let path = "/debug/settings/pprof".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_genesis.rs b/crates/algod_client/src/apis/get_genesis.rs new file mode 100644 index 00000000..8f236987 --- /dev/null +++ b/crates/algod_client/src/apis/get_genesis.rs @@ -0,0 +1,75 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::Genesis; + +// Import request body type if needed + +/// struct for typed errors of method [`get_genesis`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetGenesisError { + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Returns the entire genesis file in json. +pub async fn get_genesis(http_client: &dyn HttpClient) -> Result { + let path = "/genesis".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_ledger_state_delta.rs b/crates/algod_client/src/apis/get_ledger_state_delta.rs new file mode 100644 index 00000000..6c4017bd --- /dev/null +++ b/crates/algod_client/src/apis/get_ledger_state_delta.rs @@ -0,0 +1,102 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::parameter_enums::*; +use super::{AlgodApiError, ContentType, Error}; +use algokit_transact::AlgorandMsgpack; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, LedgerStateDelta}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_ledger_state_delta`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetLedgerStateDeltaError { + Status401(ErrorResponse), + Status404(ErrorResponse), + Status408(ErrorResponse), + Status500(ErrorResponse), + Status503(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Get ledger deltas for a round. +pub async fn get_ledger_state_delta( + http_client: &dyn HttpClient, + round: u64, + format: Option, +) -> Result { + let p_round = round; + let p_format = format; + + let path = format!("/v2/deltas/{round}", round = p_round); + + let mut query_params: HashMap = HashMap::new(); + if let Some(value) = p_format { + query_params.insert("format".to_string(), value.to_string()); + } + + let use_msgpack = p_format.map(|f| f != Format::Json).unwrap_or(true); + + let mut headers: HashMap = HashMap::new(); + if use_msgpack { + headers.insert( + "Content-Type".to_string(), + "application/msgpack".to_string(), + ); + headers.insert("Accept".to_string(), "application/msgpack".to_string()); + } else { + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + } + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_ledger_state_delta_for_transaction_group.rs b/crates/algod_client/src/apis/get_ledger_state_delta_for_transaction_group.rs new file mode 100644 index 00000000..b447bc51 --- /dev/null +++ b/crates/algod_client/src/apis/get_ledger_state_delta_for_transaction_group.rs @@ -0,0 +1,105 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::parameter_enums::*; +use super::{AlgodApiError, ContentType, Error}; +use algokit_transact::AlgorandMsgpack; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, LedgerStateDelta}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_ledger_state_delta_for_transaction_group`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetLedgerStateDeltaForTransactionGroupError { + Status401(ErrorResponse), + Status404(ErrorResponse), + Status408(ErrorResponse), + Status500(ErrorResponse), + Status501(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Get a ledger delta for a given transaction group. +pub async fn get_ledger_state_delta_for_transaction_group( + http_client: &dyn HttpClient, + id: &str, + format: Option, +) -> Result { + let p_id = id; + let p_format = format; + + let path = format!( + "/v2/deltas/txn/group/{id}", + id = crate::apis::urlencode(p_id) + ); + + let mut query_params: HashMap = HashMap::new(); + if let Some(value) = p_format { + query_params.insert("format".to_string(), value.to_string()); + } + + let use_msgpack = p_format.map(|f| f != Format::Json).unwrap_or(true); + + let mut headers: HashMap = HashMap::new(); + if use_msgpack { + headers.insert( + "Content-Type".to_string(), + "application/msgpack".to_string(), + ); + headers.insert("Accept".to_string(), "application/msgpack".to_string()); + } else { + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + } + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_light_block_header_proof.rs b/crates/algod_client/src/apis/get_light_block_header_proof.rs new file mode 100644 index 00000000..edc3e930 --- /dev/null +++ b/crates/algod_client/src/apis/get_light_block_header_proof.rs @@ -0,0 +1,85 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, LightBlockHeaderProof}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_light_block_header_proof`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetLightBlockHeaderProofError { + Status401(ErrorResponse), + Status404(ErrorResponse), + Status408(ErrorResponse), + Status500(ErrorResponse), + Status503(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Gets a proof for a given light block header inside a state proof commitment +pub async fn get_light_block_header_proof( + http_client: &dyn HttpClient, + round: u64, +) -> Result { + let p_round = round; + + let path = format!("/v2/blocks/{round}/lightheader/proof", round = p_round); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_participation_key_by_id.rs b/crates/algod_client/src/apis/get_participation_key_by_id.rs new file mode 100644 index 00000000..9cf1a32e --- /dev/null +++ b/crates/algod_client/src/apis/get_participation_key_by_id.rs @@ -0,0 +1,87 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, ParticipationKey}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_participation_key_by_id`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetParticipationKeyByIdError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status404(ErrorResponse), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Given a participation ID, return information about that participation key +pub async fn get_participation_key_by_id( + http_client: &dyn HttpClient, + participation_id: &str, +) -> Result { + let p_participation_id = participation_id; + + let path = format!( + "/v2/participation/{participation_id}", + participation_id = crate::apis::urlencode(p_participation_id) + ); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_participation_keys.rs b/crates/algod_client/src/apis/get_participation_keys.rs new file mode 100644 index 00000000..11ef740d --- /dev/null +++ b/crates/algod_client/src/apis/get_participation_keys.rs @@ -0,0 +1,81 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, ParticipationKey}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_participation_keys`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetParticipationKeysError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status404(ErrorResponse), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Return a list of participation keys +pub async fn get_participation_keys( + http_client: &dyn HttpClient, +) -> Result, Error> { + let path = "/v2/participation".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_pending_transactions.rs b/crates/algod_client/src/apis/get_pending_transactions.rs new file mode 100644 index 00000000..8c75b513 --- /dev/null +++ b/crates/algod_client/src/apis/get_pending_transactions.rs @@ -0,0 +1,103 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::parameter_enums::*; +use super::{AlgodApiError, ContentType, Error}; +use algokit_transact::AlgorandMsgpack; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, GetPendingTransactions}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_pending_transactions`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetPendingTransactionsError { + Status401(ErrorResponse), + Status500(ErrorResponse), + Status503(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Get the list of pending transactions, sorted by priority, in decreasing order, truncated at the end at MAX. If MAX = 0, returns all pending transactions. +pub async fn get_pending_transactions( + http_client: &dyn HttpClient, + max: Option, + format: Option, +) -> Result { + let p_max = max; + let p_format = format; + + let path = "/v2/transactions/pending".to_string(); + + let mut query_params: HashMap = HashMap::new(); + if let Some(value) = p_max { + query_params.insert("max".to_string(), value.to_string()); + } + if let Some(value) = p_format { + query_params.insert("format".to_string(), value.to_string()); + } + + let use_msgpack = p_format.map(|f| f != Format::Json).unwrap_or(true); + + let mut headers: HashMap = HashMap::new(); + if use_msgpack { + headers.insert( + "Content-Type".to_string(), + "application/msgpack".to_string(), + ); + headers.insert("Accept".to_string(), "application/msgpack".to_string()); + } else { + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + } + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_pending_transactions_by_address.rs b/crates/algod_client/src/apis/get_pending_transactions_by_address.rs new file mode 100644 index 00000000..d22cf754 --- /dev/null +++ b/crates/algod_client/src/apis/get_pending_transactions_by_address.rs @@ -0,0 +1,109 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::parameter_enums::*; +use super::{AlgodApiError, ContentType, Error}; +use algokit_transact::AlgorandMsgpack; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, GetPendingTransactionsByAddress}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_pending_transactions_by_address`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetPendingTransactionsByAddressError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status500(ErrorResponse), + Status503(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Get the list of pending transactions by address, sorted by priority, in decreasing order, truncated at the end at MAX. If MAX = 0, returns all pending transactions. +pub async fn get_pending_transactions_by_address( + http_client: &dyn HttpClient, + address: &str, + max: Option, + format: Option, +) -> Result { + let p_address = address; + let p_max = max; + let p_format = format; + + let path = format!( + "/v2/accounts/{address}/transactions/pending", + address = crate::apis::urlencode(p_address) + ); + + let mut query_params: HashMap = HashMap::new(); + if let Some(value) = p_max { + query_params.insert("max".to_string(), value.to_string()); + } + if let Some(value) = p_format { + query_params.insert("format".to_string(), value.to_string()); + } + + let use_msgpack = p_format.map(|f| f != Format::Json).unwrap_or(true); + + let mut headers: HashMap = HashMap::new(); + if use_msgpack { + headers.insert( + "Content-Type".to_string(), + "application/msgpack".to_string(), + ); + headers.insert("Accept".to_string(), "application/msgpack".to_string()); + } else { + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + } + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_ready.rs b/crates/algod_client/src/apis/get_ready.rs new file mode 100644 index 00000000..1d837827 --- /dev/null +++ b/crates/algod_client/src/apis/get_ready.rs @@ -0,0 +1,57 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint + +// Import request body type if needed + +/// struct for typed errors of method [`get_ready`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetReadyError { + Status500(), + Status503(), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Returns OK if healthy and fully caught up. +pub async fn get_ready(http_client: &dyn HttpClient) -> Result<(), Error> { + let path = "/ready".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let _ = response; + Ok(()) +} diff --git a/crates/algod_client/src/apis/get_state_proof.rs b/crates/algod_client/src/apis/get_state_proof.rs new file mode 100644 index 00000000..bd0fb149 --- /dev/null +++ b/crates/algod_client/src/apis/get_state_proof.rs @@ -0,0 +1,85 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, StateProof}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_state_proof`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetStateProofError { + Status401(ErrorResponse), + Status404(ErrorResponse), + Status408(ErrorResponse), + Status500(ErrorResponse), + Status503(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Get a state proof that covers a given round +pub async fn get_state_proof( + http_client: &dyn HttpClient, + round: u64, +) -> Result { + let p_round = round; + + let path = format!("/v2/stateproofs/{round}", round = p_round); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_status.rs b/crates/algod_client/src/apis/get_status.rs new file mode 100644 index 00000000..2eca9254 --- /dev/null +++ b/crates/algod_client/src/apis/get_status.rs @@ -0,0 +1,77 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, GetStatus}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_status`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetStatusError { + Status401(ErrorResponse), + Status500(String), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Gets the current node status. +pub async fn get_status(http_client: &dyn HttpClient) -> Result { + let path = "/v2/status".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_supply.rs b/crates/algod_client/src/apis/get_supply.rs new file mode 100644 index 00000000..ec657135 --- /dev/null +++ b/crates/algod_client/src/apis/get_supply.rs @@ -0,0 +1,76 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, GetSupply}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_supply`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetSupplyError { + Status401(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Get the current supply reported by the ledger. +pub async fn get_supply(http_client: &dyn HttpClient) -> Result { + let path = "/v2/ledger/supply".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_sync_round.rs b/crates/algod_client/src/apis/get_sync_round.rs new file mode 100644 index 00000000..91105ed0 --- /dev/null +++ b/crates/algod_client/src/apis/get_sync_round.rs @@ -0,0 +1,79 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, GetSyncRound}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_sync_round`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetSyncRoundError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status500(ErrorResponse), + Status503(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Gets the minimum sync round for the ledger. +pub async fn get_sync_round(http_client: &dyn HttpClient) -> Result { + let path = "/v2/ledger/sync".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_transaction_group_ledger_state_deltas_for_round.rs b/crates/algod_client/src/apis/get_transaction_group_ledger_state_deltas_for_round.rs new file mode 100644 index 00000000..690e50ec --- /dev/null +++ b/crates/algod_client/src/apis/get_transaction_group_ledger_state_deltas_for_round.rs @@ -0,0 +1,102 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::parameter_enums::*; +use super::{AlgodApiError, ContentType, Error}; +use algokit_transact::AlgorandMsgpack; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, GetTransactionGroupLedgerStateDeltasForRound}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_transaction_group_ledger_state_deltas_for_round`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetTransactionGroupLedgerStateDeltasForRoundError { + Status401(ErrorResponse), + Status404(ErrorResponse), + Status408(ErrorResponse), + Status500(ErrorResponse), + Status501(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Get ledger deltas for transaction groups in a given round. +pub async fn get_transaction_group_ledger_state_deltas_for_round( + http_client: &dyn HttpClient, + round: u64, + format: Option, +) -> Result { + let p_round = round; + let p_format = format; + + let path = format!("/v2/deltas/{round}/txn/group", round = p_round); + + let mut query_params: HashMap = HashMap::new(); + if let Some(value) = p_format { + query_params.insert("format".to_string(), value.to_string()); + } + + let use_msgpack = p_format.map(|f| f != Format::Json).unwrap_or(true); + + let mut headers: HashMap = HashMap::new(); + if use_msgpack { + headers.insert( + "Content-Type".to_string(), + "application/msgpack".to_string(), + ); + headers.insert("Accept".to_string(), "application/msgpack".to_string()); + } else { + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + } + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_transaction_proof.rs b/crates/algod_client/src/apis/get_transaction_proof.rs new file mode 100644 index 00000000..ce5a0a1e --- /dev/null +++ b/crates/algod_client/src/apis/get_transaction_proof.rs @@ -0,0 +1,101 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::parameter_enums::*; +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, GetTransactionProof}; + +// Import request body type if needed + +/// struct for typed errors of method [`get_transaction_proof`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetTransactionProofError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status404(ErrorResponse), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Get a proof for a transaction in a block. +pub async fn get_transaction_proof( + http_client: &dyn HttpClient, + round: u64, + txid: &str, + hashtype: Option, + format: Option, +) -> Result { + let p_round = round; + let p_txid = txid; + let p_hashtype = hashtype; + let p_format = format; + + let path = format!( + "/v2/blocks/{round}/transactions/{txid}/proof", + round = p_round, + txid = crate::apis::urlencode(p_txid) + ); + + let mut query_params: HashMap = HashMap::new(); + if let Some(value) = p_hashtype { + query_params.insert("hashtype".to_string(), value.to_string()); + } + if let Some(value) = p_format { + query_params.insert("format".to_string(), value.to_string()); + } + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/get_version.rs b/crates/algod_client/src/apis/get_version.rs new file mode 100644 index 00000000..32ba6da1 --- /dev/null +++ b/crates/algod_client/src/apis/get_version.rs @@ -0,0 +1,74 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::Version; + +// Import request body type if needed + +/// struct for typed errors of method [`get_version`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetVersionError { + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Retrieves the supported API versions, binary build versions, and genesis information. +pub async fn get_version(http_client: &dyn HttpClient) -> Result { + let path = "/versions".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/health_check.rs b/crates/algod_client/src/apis/health_check.rs new file mode 100644 index 00000000..1cc348a4 --- /dev/null +++ b/crates/algod_client/src/apis/health_check.rs @@ -0,0 +1,55 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint + +// Import request body type if needed + +/// struct for typed errors of method [`health_check`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum HealthCheckError { + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Returns OK if healthy. +pub async fn health_check(http_client: &dyn HttpClient) -> Result<(), Error> { + let path = "/health".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let _ = response; + Ok(()) +} diff --git a/crates/algod_client/src/apis/metrics.rs b/crates/algod_client/src/apis/metrics.rs new file mode 100644 index 00000000..d0178f25 --- /dev/null +++ b/crates/algod_client/src/apis/metrics.rs @@ -0,0 +1,55 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint + +// Import request body type if needed + +/// struct for typed errors of method [`metrics`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum MetricsError { + Status404(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Return metrics about algod functioning. +pub async fn metrics(http_client: &dyn HttpClient) -> Result<(), Error> { + let path = "/metrics".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let _ = response; + Ok(()) +} diff --git a/crates/algod_client/src/apis/mod.rs b/crates/algod_client/src/apis/mod.rs new file mode 100644 index 00000000..5bb3cd62 --- /dev/null +++ b/crates/algod_client/src/apis/mod.rs @@ -0,0 +1,644 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +// Consolidated client +pub mod client; + +// Parameter enums for type-safe API parameters +pub mod parameter_enums; + +// Individual endpoint modules +pub mod abort_catchup; +pub mod account_application_information; +pub mod account_asset_information; +pub mod account_assets_information; +pub mod account_information; +pub mod add_participation_key; +pub mod append_keys; +pub mod delete_participation_key_by_id; +pub mod experimental_check; +pub mod generate_participation_keys; +pub mod get_application_box_by_name; +pub mod get_application_boxes; +pub mod get_application_by_id; +pub mod get_asset_by_id; +pub mod get_block; +pub mod get_block_hash; +pub mod get_block_logs; +pub mod get_block_time_stamp_offset; +pub mod get_block_txids; +pub mod get_config; +pub mod get_debug_settings_prof; +pub mod get_genesis; +pub mod get_ledger_state_delta; +pub mod get_ledger_state_delta_for_transaction_group; +pub mod get_light_block_header_proof; +pub mod get_participation_key_by_id; +pub mod get_participation_keys; +pub mod get_pending_transactions; +pub mod get_pending_transactions_by_address; +pub mod get_ready; +pub mod get_state_proof; +pub mod get_status; +pub mod get_supply; +pub mod get_sync_round; +pub mod get_transaction_group_ledger_state_deltas_for_round; +pub mod get_transaction_proof; +pub mod get_version; +pub mod health_check; +pub mod metrics; +pub mod pending_transaction_information; +pub mod put_debug_settings_prof; +pub mod raw_transaction; +pub mod raw_transaction_async; +pub mod set_block_time_stamp_offset; +pub mod set_sync_round; +pub mod shutdown_node; +pub mod simulate_transaction; +pub mod start_catchup; +pub mod swagger_json; +pub mod teal_compile; +pub mod teal_disassemble; +pub mod teal_dryrun; +pub mod transaction_params; +pub mod unset_sync_round; +pub mod wait_for_block; + +/// Unified error type that can represent any API error from any endpoint +#[derive(Debug, thiserror::Error)] +pub enum AlgodApiError { + #[error("Health_check error: {0:?}")] + HealthCheck(health_check::HealthCheckError), + #[error("Get_ready error: {0:?}")] + GetReady(get_ready::GetReadyError), + #[error("Metrics error: {0:?}")] + Metrics(metrics::MetricsError), + #[error("Get_genesis error: {0:?}")] + GetGenesis(get_genesis::GetGenesisError), + #[error("Swagger_json error: {0:?}")] + SwaggerJson(swagger_json::SwaggerJsonError), + #[error("Get_version error: {0:?}")] + GetVersion(get_version::GetVersionError), + #[error("Get_debug_settings_prof error: {0:?}")] + GetDebugSettingsProf(get_debug_settings_prof::GetDebugSettingsProfError), + #[error("Put_debug_settings_prof error: {0:?}")] + PutDebugSettingsProf(put_debug_settings_prof::PutDebugSettingsProfError), + #[error("Get_config error: {0:?}")] + GetConfig(get_config::GetConfigError), + #[error("Account_information error: {0:?}")] + AccountInformation(account_information::AccountInformationError), + #[error("Account_asset_information error: {0:?}")] + AccountAssetInformation(account_asset_information::AccountAssetInformationError), + #[error("Account_assets_information error: {0:?}")] + AccountAssetsInformation(account_assets_information::AccountAssetsInformationError), + #[error("Account_application_information error: {0:?}")] + AccountApplicationInformation(account_application_information::AccountApplicationInformationError), + #[error("Get_pending_transactions_by_address error: {0:?}")] + GetPendingTransactionsByAddress(get_pending_transactions_by_address::GetPendingTransactionsByAddressError), + #[error("Get_block error: {0:?}")] + GetBlock(get_block::GetBlockError), + #[error("Get_block_txids error: {0:?}")] + GetBlockTxids(get_block_txids::GetBlockTxidsError), + #[error("Get_block_hash error: {0:?}")] + GetBlockHash(get_block_hash::GetBlockHashError), + #[error("Get_transaction_proof error: {0:?}")] + GetTransactionProof(get_transaction_proof::GetTransactionProofError), + #[error("Get_block_logs error: {0:?}")] + GetBlockLogs(get_block_logs::GetBlockLogsError), + #[error("Get_supply error: {0:?}")] + GetSupply(get_supply::GetSupplyError), + #[error("Get_participation_keys error: {0:?}")] + GetParticipationKeys(get_participation_keys::GetParticipationKeysError), + #[error("Add_participation_key error: {0:?}")] + AddParticipationKey(add_participation_key::AddParticipationKeyError), + #[error("Generate_participation_keys error: {0:?}")] + GenerateParticipationKeys(generate_participation_keys::GenerateParticipationKeysError), + #[error("Get_participation_key_by_id error: {0:?}")] + GetParticipationKeyById(get_participation_key_by_id::GetParticipationKeyByIdError), + #[error("Append_keys error: {0:?}")] + AppendKeys(append_keys::AppendKeysError), + #[error("Delete_participation_key_by_id error: {0:?}")] + DeleteParticipationKeyById(delete_participation_key_by_id::DeleteParticipationKeyByIdError), + #[error("Shutdown_node error: {0:?}")] + ShutdownNode(shutdown_node::ShutdownNodeError), + #[error("Get_status error: {0:?}")] + GetStatus(get_status::GetStatusError), + #[error("Wait_for_block error: {0:?}")] + WaitForBlock(wait_for_block::WaitForBlockError), + #[error("Raw_transaction error: {0:?}")] + RawTransaction(raw_transaction::RawTransactionError), + #[error("Raw_transaction_async error: {0:?}")] + RawTransactionAsync(raw_transaction_async::RawTransactionAsyncError), + #[error("Simulate_transaction error: {0:?}")] + SimulateTransaction(simulate_transaction::SimulateTransactionError), + #[error("Transaction_params error: {0:?}")] + TransactionParams(transaction_params::TransactionParamsError), + #[error("Get_pending_transactions error: {0:?}")] + GetPendingTransactions(get_pending_transactions::GetPendingTransactionsError), + #[error("Pending_transaction_information error: {0:?}")] + PendingTransactionInformation(pending_transaction_information::PendingTransactionInformationError), + #[error("Get_ledger_state_delta error: {0:?}")] + GetLedgerStateDelta(get_ledger_state_delta::GetLedgerStateDeltaError), + #[error("Get_transaction_group_ledger_state_deltas_for_round error: {0:?}")] + GetTransactionGroupLedgerStateDeltasForRound(get_transaction_group_ledger_state_deltas_for_round::GetTransactionGroupLedgerStateDeltasForRoundError), + #[error("Get_ledger_state_delta_for_transaction_group error: {0:?}")] + GetLedgerStateDeltaForTransactionGroup(get_ledger_state_delta_for_transaction_group::GetLedgerStateDeltaForTransactionGroupError), + #[error("Get_state_proof error: {0:?}")] + GetStateProof(get_state_proof::GetStateProofError), + #[error("Get_light_block_header_proof error: {0:?}")] + GetLightBlockHeaderProof(get_light_block_header_proof::GetLightBlockHeaderProofError), + #[error("Get_application_by_id error: {0:?}")] + GetApplicationById(get_application_by_id::GetApplicationByIdError), + #[error("Get_application_boxes error: {0:?}")] + GetApplicationBoxes(get_application_boxes::GetApplicationBoxesError), + #[error("Get_application_box_by_name error: {0:?}")] + GetApplicationBoxByName(get_application_box_by_name::GetApplicationBoxByNameError), + #[error("Get_asset_by_id error: {0:?}")] + GetAssetById(get_asset_by_id::GetAssetByIdError), + #[error("Get_sync_round error: {0:?}")] + GetSyncRound(get_sync_round::GetSyncRoundError), + #[error("Unset_sync_round error: {0:?}")] + UnsetSyncRound(unset_sync_round::UnsetSyncRoundError), + #[error("Set_sync_round error: {0:?}")] + SetSyncRound(set_sync_round::SetSyncRoundError), + #[error("Teal_compile error: {0:?}")] + TealCompile(teal_compile::TealCompileError), + #[error("Teal_disassemble error: {0:?}")] + TealDisassemble(teal_disassemble::TealDisassembleError), + #[error("Start_catchup error: {0:?}")] + StartCatchup(start_catchup::StartCatchupError), + #[error("Abort_catchup error: {0:?}")] + AbortCatchup(abort_catchup::AbortCatchupError), + #[error("Teal_dryrun error: {0:?}")] + TealDryrun(teal_dryrun::TealDryrunError), + #[error("Experimental_check error: {0:?}")] + ExperimentalCheck(experimental_check::ExperimentalCheckError), + #[error("Get_block_time_stamp_offset error: {0:?}")] + GetBlockTimeStampOffset(get_block_time_stamp_offset::GetBlockTimeStampOffsetError), + #[error("Set_block_time_stamp_offset error: {0:?}")] + SetBlockTimeStampOffset(set_block_time_stamp_offset::SetBlockTimeStampOffsetError), + #[error("Unknown API error: {0}")] + Unknown(String), +} + +impl From for AlgodApiError { + fn from(err: health_check::HealthCheckError) -> Self { + AlgodApiError::HealthCheck(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_ready::GetReadyError) -> Self { + AlgodApiError::GetReady(err) + } +} + +impl From for AlgodApiError { + fn from(err: metrics::MetricsError) -> Self { + AlgodApiError::Metrics(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_genesis::GetGenesisError) -> Self { + AlgodApiError::GetGenesis(err) + } +} + +impl From for AlgodApiError { + fn from(err: swagger_json::SwaggerJsonError) -> Self { + AlgodApiError::SwaggerJson(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_version::GetVersionError) -> Self { + AlgodApiError::GetVersion(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_debug_settings_prof::GetDebugSettingsProfError) -> Self { + AlgodApiError::GetDebugSettingsProf(err) + } +} + +impl From for AlgodApiError { + fn from(err: put_debug_settings_prof::PutDebugSettingsProfError) -> Self { + AlgodApiError::PutDebugSettingsProf(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_config::GetConfigError) -> Self { + AlgodApiError::GetConfig(err) + } +} + +impl From for AlgodApiError { + fn from(err: account_information::AccountInformationError) -> Self { + AlgodApiError::AccountInformation(err) + } +} + +impl From for AlgodApiError { + fn from(err: account_asset_information::AccountAssetInformationError) -> Self { + AlgodApiError::AccountAssetInformation(err) + } +} + +impl From for AlgodApiError { + fn from(err: account_assets_information::AccountAssetsInformationError) -> Self { + AlgodApiError::AccountAssetsInformation(err) + } +} + +impl From for AlgodApiError { + fn from(err: account_application_information::AccountApplicationInformationError) -> Self { + AlgodApiError::AccountApplicationInformation(err) + } +} + +impl From + for AlgodApiError +{ + fn from( + err: get_pending_transactions_by_address::GetPendingTransactionsByAddressError, + ) -> Self { + AlgodApiError::GetPendingTransactionsByAddress(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_block::GetBlockError) -> Self { + AlgodApiError::GetBlock(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_block_txids::GetBlockTxidsError) -> Self { + AlgodApiError::GetBlockTxids(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_block_hash::GetBlockHashError) -> Self { + AlgodApiError::GetBlockHash(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_transaction_proof::GetTransactionProofError) -> Self { + AlgodApiError::GetTransactionProof(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_block_logs::GetBlockLogsError) -> Self { + AlgodApiError::GetBlockLogs(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_supply::GetSupplyError) -> Self { + AlgodApiError::GetSupply(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_participation_keys::GetParticipationKeysError) -> Self { + AlgodApiError::GetParticipationKeys(err) + } +} + +impl From for AlgodApiError { + fn from(err: add_participation_key::AddParticipationKeyError) -> Self { + AlgodApiError::AddParticipationKey(err) + } +} + +impl From for AlgodApiError { + fn from(err: generate_participation_keys::GenerateParticipationKeysError) -> Self { + AlgodApiError::GenerateParticipationKeys(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_participation_key_by_id::GetParticipationKeyByIdError) -> Self { + AlgodApiError::GetParticipationKeyById(err) + } +} + +impl From for AlgodApiError { + fn from(err: append_keys::AppendKeysError) -> Self { + AlgodApiError::AppendKeys(err) + } +} + +impl From for AlgodApiError { + fn from(err: delete_participation_key_by_id::DeleteParticipationKeyByIdError) -> Self { + AlgodApiError::DeleteParticipationKeyById(err) + } +} + +impl From for AlgodApiError { + fn from(err: shutdown_node::ShutdownNodeError) -> Self { + AlgodApiError::ShutdownNode(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_status::GetStatusError) -> Self { + AlgodApiError::GetStatus(err) + } +} + +impl From for AlgodApiError { + fn from(err: wait_for_block::WaitForBlockError) -> Self { + AlgodApiError::WaitForBlock(err) + } +} + +impl From for AlgodApiError { + fn from(err: raw_transaction::RawTransactionError) -> Self { + AlgodApiError::RawTransaction(err) + } +} + +impl From for AlgodApiError { + fn from(err: raw_transaction_async::RawTransactionAsyncError) -> Self { + AlgodApiError::RawTransactionAsync(err) + } +} + +impl From for AlgodApiError { + fn from(err: simulate_transaction::SimulateTransactionError) -> Self { + AlgodApiError::SimulateTransaction(err) + } +} + +impl From for AlgodApiError { + fn from(err: transaction_params::TransactionParamsError) -> Self { + AlgodApiError::TransactionParams(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_pending_transactions::GetPendingTransactionsError) -> Self { + AlgodApiError::GetPendingTransactions(err) + } +} + +impl From for AlgodApiError { + fn from(err: pending_transaction_information::PendingTransactionInformationError) -> Self { + AlgodApiError::PendingTransactionInformation(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_ledger_state_delta::GetLedgerStateDeltaError) -> Self { + AlgodApiError::GetLedgerStateDelta(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_transaction_group_ledger_state_deltas_for_round::GetTransactionGroupLedgerStateDeltasForRoundError) -> Self { + AlgodApiError::GetTransactionGroupLedgerStateDeltasForRound(err) + } +} + +impl From + for AlgodApiError +{ + fn from( + err: get_ledger_state_delta_for_transaction_group::GetLedgerStateDeltaForTransactionGroupError, + ) -> Self { + AlgodApiError::GetLedgerStateDeltaForTransactionGroup(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_state_proof::GetStateProofError) -> Self { + AlgodApiError::GetStateProof(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_light_block_header_proof::GetLightBlockHeaderProofError) -> Self { + AlgodApiError::GetLightBlockHeaderProof(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_application_by_id::GetApplicationByIdError) -> Self { + AlgodApiError::GetApplicationById(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_application_boxes::GetApplicationBoxesError) -> Self { + AlgodApiError::GetApplicationBoxes(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_application_box_by_name::GetApplicationBoxByNameError) -> Self { + AlgodApiError::GetApplicationBoxByName(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_asset_by_id::GetAssetByIdError) -> Self { + AlgodApiError::GetAssetById(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_sync_round::GetSyncRoundError) -> Self { + AlgodApiError::GetSyncRound(err) + } +} + +impl From for AlgodApiError { + fn from(err: unset_sync_round::UnsetSyncRoundError) -> Self { + AlgodApiError::UnsetSyncRound(err) + } +} + +impl From for AlgodApiError { + fn from(err: set_sync_round::SetSyncRoundError) -> Self { + AlgodApiError::SetSyncRound(err) + } +} + +impl From for AlgodApiError { + fn from(err: teal_compile::TealCompileError) -> Self { + AlgodApiError::TealCompile(err) + } +} + +impl From for AlgodApiError { + fn from(err: teal_disassemble::TealDisassembleError) -> Self { + AlgodApiError::TealDisassemble(err) + } +} + +impl From for AlgodApiError { + fn from(err: start_catchup::StartCatchupError) -> Self { + AlgodApiError::StartCatchup(err) + } +} + +impl From for AlgodApiError { + fn from(err: abort_catchup::AbortCatchupError) -> Self { + AlgodApiError::AbortCatchup(err) + } +} + +impl From for AlgodApiError { + fn from(err: teal_dryrun::TealDryrunError) -> Self { + AlgodApiError::TealDryrun(err) + } +} + +impl From for AlgodApiError { + fn from(err: experimental_check::ExperimentalCheckError) -> Self { + AlgodApiError::ExperimentalCheck(err) + } +} + +impl From for AlgodApiError { + fn from(err: get_block_time_stamp_offset::GetBlockTimeStampOffsetError) -> Self { + AlgodApiError::GetBlockTimeStampOffset(err) + } +} + +impl From for AlgodApiError { + fn from(err: set_block_time_stamp_offset::SetBlockTimeStampOffsetError) -> Self { + AlgodApiError::SetBlockTimeStampOffset(err) + } +} + +/// The main error type for all algod client operations +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("HTTP error: {0}")] + Http(#[from] algokit_http_client::HttpError), + #[error("Serialization error: {0}")] + Serde(String), + #[error("API error: {0}")] + Api(#[from] AlgodApiError), +} + +#[derive(Debug, PartialEq, Eq)] +pub enum ContentType { + Json, + MsgPack, + Text, + Unsupported(String), +} + +impl From<&str> for ContentType { + fn from(content_type: &str) -> Self { + if content_type.contains("application/json") { + ContentType::Json + } else if content_type.contains("application/msgpack") { + ContentType::MsgPack + } else if content_type.contains("text/plain") { + ContentType::Text + } else { + ContentType::Unsupported(content_type.to_string()) + } + } +} + +pub fn urlencode>(s: T) -> String { + ::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes()).collect() +} + +// Re-export the consolidated client +pub use client::AlgodClient; + +// Re-export parameter enums +pub use parameter_enums::*; + +// Re-export all endpoint functions +pub use abort_catchup::{AbortCatchupError, abort_catchup}; +pub use account_application_information::{ + AccountApplicationInformationError, account_application_information, +}; +pub use account_asset_information::{AccountAssetInformationError, account_asset_information}; +pub use account_assets_information::{AccountAssetsInformationError, account_assets_information}; +pub use account_information::{AccountInformationError, account_information}; +pub use add_participation_key::{AddParticipationKeyError, add_participation_key}; +pub use append_keys::{AppendKeysError, append_keys}; +pub use delete_participation_key_by_id::{ + DeleteParticipationKeyByIdError, delete_participation_key_by_id, +}; +pub use experimental_check::{ExperimentalCheckError, experimental_check}; +pub use generate_participation_keys::{ + GenerateParticipationKeysError, generate_participation_keys, +}; +pub use get_application_box_by_name::{GetApplicationBoxByNameError, get_application_box_by_name}; +pub use get_application_boxes::{GetApplicationBoxesError, get_application_boxes}; +pub use get_application_by_id::{GetApplicationByIdError, get_application_by_id}; +pub use get_asset_by_id::{GetAssetByIdError, get_asset_by_id}; +pub use get_block::{GetBlockError, get_block}; +pub use get_block_hash::{GetBlockHashError, get_block_hash}; +pub use get_block_logs::{GetBlockLogsError, get_block_logs}; +pub use get_block_time_stamp_offset::{GetBlockTimeStampOffsetError, get_block_time_stamp_offset}; +pub use get_block_txids::{GetBlockTxidsError, get_block_txids}; +pub use get_config::{GetConfigError, get_config}; +pub use get_debug_settings_prof::{GetDebugSettingsProfError, get_debug_settings_prof}; +pub use get_genesis::{GetGenesisError, get_genesis}; +pub use get_ledger_state_delta::{GetLedgerStateDeltaError, get_ledger_state_delta}; +pub use get_ledger_state_delta_for_transaction_group::{ + GetLedgerStateDeltaForTransactionGroupError, get_ledger_state_delta_for_transaction_group, +}; +pub use get_light_block_header_proof::{ + GetLightBlockHeaderProofError, get_light_block_header_proof, +}; +pub use get_participation_key_by_id::{GetParticipationKeyByIdError, get_participation_key_by_id}; +pub use get_participation_keys::{GetParticipationKeysError, get_participation_keys}; +pub use get_pending_transactions::{GetPendingTransactionsError, get_pending_transactions}; +pub use get_pending_transactions_by_address::{ + GetPendingTransactionsByAddressError, get_pending_transactions_by_address, +}; +pub use get_ready::{GetReadyError, get_ready}; +pub use get_state_proof::{GetStateProofError, get_state_proof}; +pub use get_status::{GetStatusError, get_status}; +pub use get_supply::{GetSupplyError, get_supply}; +pub use get_sync_round::{GetSyncRoundError, get_sync_round}; +pub use get_transaction_group_ledger_state_deltas_for_round::{ + GetTransactionGroupLedgerStateDeltasForRoundError, + get_transaction_group_ledger_state_deltas_for_round, +}; +pub use get_transaction_proof::{GetTransactionProofError, get_transaction_proof}; +pub use get_version::{GetVersionError, get_version}; +pub use health_check::{HealthCheckError, health_check}; +pub use metrics::{MetricsError, metrics}; +pub use pending_transaction_information::{ + PendingTransactionInformationError, pending_transaction_information, +}; +pub use put_debug_settings_prof::{PutDebugSettingsProfError, put_debug_settings_prof}; +pub use raw_transaction::{RawTransactionError, raw_transaction}; +pub use raw_transaction_async::{RawTransactionAsyncError, raw_transaction_async}; +pub use set_block_time_stamp_offset::{SetBlockTimeStampOffsetError, set_block_time_stamp_offset}; +pub use set_sync_round::{SetSyncRoundError, set_sync_round}; +pub use shutdown_node::{ShutdownNodeError, shutdown_node}; +pub use simulate_transaction::{SimulateTransactionError, simulate_transaction}; +pub use start_catchup::{StartCatchupError, start_catchup}; +pub use swagger_json::{SwaggerJsonError, swagger_json}; +pub use teal_compile::{TealCompileError, teal_compile}; +pub use teal_disassemble::{TealDisassembleError, teal_disassemble}; +pub use teal_dryrun::{TealDryrunError, teal_dryrun}; +pub use transaction_params::{TransactionParamsError, transaction_params}; +pub use unset_sync_round::{UnsetSyncRoundError, unset_sync_round}; +pub use wait_for_block::{WaitForBlockError, wait_for_block}; diff --git a/crates/algod_client/src/apis/parameter_enums.rs b/crates/algod_client/src/apis/parameter_enums.rs new file mode 100644 index 00000000..c6a72234 --- /dev/null +++ b/crates/algod_client/src/apis/parameter_enums.rs @@ -0,0 +1,109 @@ +/* + * Parameter Enums for Algod REST API. + * + * Auto-generated enums for parameters with constrained string values. + * + * Generated by: Rust OpenAPI Generator + */ + +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::str::FromStr; + +/// Configures whether the response object is JSON or MessagePack encoded. If not provided, defaults to JSON. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Format { + /// json + Json, + /// msgpack + Msgpack, +} + +impl fmt::Display for Format { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let value = match self { + Format::Json => "json", + Format::Msgpack => "msgpack", + }; + write!(f, "{}", value) + } +} + +impl FromStr for Format { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "json" => Ok(Format::Json), + "msgpack" => Ok(Format::Msgpack), + _ => Err(format!("Invalid Format: {}", s)), + } + } +} + +/// When set to `all` will exclude asset holdings, application local state, created asset parameters, any created application parameters. Defaults to `none`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Exclude { + /// all + All, + /// none + None, +} + +impl fmt::Display for Exclude { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let value = match self { + Exclude::All => "all", + Exclude::None => "none", + }; + write!(f, "{}", value) + } +} + +impl FromStr for Exclude { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "all" => Ok(Exclude::All), + "none" => Ok(Exclude::None), + _ => Err(format!("Invalid Exclude: {}", s)), + } + } +} + +/// The type of hash function used to create the proof, must be one of: +/// * sha512_256 +/// * sha256 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Hashtype { + /// sha512_256 + Sha512256, + /// sha256 + Sha256, +} + +impl fmt::Display for Hashtype { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let value = match self { + Hashtype::Sha512256 => "sha512_256", + Hashtype::Sha256 => "sha256", + }; + write!(f, "{}", value) + } +} + +impl FromStr for Hashtype { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "sha512_256" => Ok(Hashtype::Sha512256), + "sha256" => Ok(Hashtype::Sha256), + _ => Err(format!("Invalid Hashtype: {}", s)), + } + } +} diff --git a/crates/algod_client/src/apis/pending_transaction_information.rs b/crates/algod_client/src/apis/pending_transaction_information.rs new file mode 100644 index 00000000..275fdfdf --- /dev/null +++ b/crates/algod_client/src/apis/pending_transaction_information.rs @@ -0,0 +1,108 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::parameter_enums::*; +use super::{AlgodApiError, ContentType, Error}; +use algokit_transact::AlgorandMsgpack; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, PendingTransactionResponse}; + +// Import request body type if needed + +/// struct for typed errors of method [`pending_transaction_information`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum PendingTransactionInformationError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status404(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Given a transaction ID of a recently submitted transaction, it returns information about it. There are several cases when this might succeed: +/// - transaction committed (committed round > 0) +/// - transaction still in the pool (committed round = 0, pool error = "") +/// - transaction removed from pool due to error (committed round = 0, pool error != "") +/// +/// Or the transaction may have happened sufficiently long ago that the node no longer remembers it, and this will return an error. +pub async fn pending_transaction_information( + http_client: &dyn HttpClient, + txid: &str, + format: Option, +) -> Result { + let p_txid = txid; + let p_format = format; + + let path = format!( + "/v2/transactions/pending/{txid}", + txid = crate::apis::urlencode(p_txid) + ); + + let mut query_params: HashMap = HashMap::new(); + if let Some(value) = p_format { + query_params.insert("format".to_string(), value.to_string()); + } + + let use_msgpack = p_format.map(|f| f != Format::Json).unwrap_or(true); + + let mut headers: HashMap = HashMap::new(); + if use_msgpack { + headers.insert( + "Content-Type".to_string(), + "application/msgpack".to_string(), + ); + headers.insert("Accept".to_string(), "application/msgpack".to_string()); + } else { + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + } + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/put_debug_settings_prof.rs b/crates/algod_client/src/apis/put_debug_settings_prof.rs new file mode 100644 index 00000000..a34d512b --- /dev/null +++ b/crates/algod_client/src/apis/put_debug_settings_prof.rs @@ -0,0 +1,76 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::DebugSettingsProf; + +// Import request body type if needed + +/// struct for typed errors of method [`put_debug_settings_prof`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum PutDebugSettingsProfError { + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Enables blocking and mutex profiles, and returns the old settings +pub async fn put_debug_settings_prof( + http_client: &dyn HttpClient, +) -> Result { + let path = "/debug/settings/pprof".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Put, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/raw_transaction.rs b/crates/algod_client/src/apis/raw_transaction.rs new file mode 100644 index 00000000..00c7d82c --- /dev/null +++ b/crates/algod_client/src/apis/raw_transaction.rs @@ -0,0 +1,88 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; +use algokit_transact::AlgorandMsgpack; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, RawTransaction}; + +// Import request body type if needed + +/// struct for typed errors of method [`raw_transaction`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum RawTransactionError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status500(ErrorResponse), + Status503(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Broadcasts a raw transaction or transaction group to the network. +pub async fn raw_transaction( + http_client: &dyn HttpClient, + request: Vec, +) -> Result { + let p_request = request; + + let path = "/v2/transactions".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert( + "Content-Type".to_string(), + "application/msgpack".to_string(), + ); + headers.insert("Accept".to_string(), "application/msgpack".to_string()); + + let body = Some(p_request); + + let response = http_client + .request( + HttpMethod::Post, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/raw_transaction_async.rs b/crates/algod_client/src/apis/raw_transaction_async.rs new file mode 100644 index 00000000..3869f405 --- /dev/null +++ b/crates/algod_client/src/apis/raw_transaction_async.rs @@ -0,0 +1,70 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; +use algokit_transact::AlgorandMsgpack; + +// Import all custom types used by this endpoint +use crate::models::ErrorResponse; + +// Import request body type if needed + +/// struct for typed errors of method [`raw_transaction_async`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum RawTransactionAsyncError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status404(), + Status500(ErrorResponse), + Status503(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Fast track for broadcasting a raw transaction or transaction group to the network through the tx handler without performing most of the checks and reporting detailed errors. Should be only used for development and performance testing. +pub async fn raw_transaction_async( + http_client: &dyn HttpClient, + request: Vec, +) -> Result<(), Error> { + let p_request = request; + + let path = "/v2/transactions/async".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert( + "Content-Type".to_string(), + "application/msgpack".to_string(), + ); + headers.insert("Accept".to_string(), "application/msgpack".to_string()); + + let body = Some(p_request); + + let response = http_client + .request( + HttpMethod::Post, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let _ = response; + Ok(()) +} diff --git a/crates/algod_client/src/apis/set_block_time_stamp_offset.rs b/crates/algod_client/src/apis/set_block_time_stamp_offset.rs new file mode 100644 index 00000000..f568f1f8 --- /dev/null +++ b/crates/algod_client/src/apis/set_block_time_stamp_offset.rs @@ -0,0 +1,64 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::ErrorResponse; + +// Import request body type if needed + +/// struct for typed errors of method [`set_block_time_stamp_offset`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum SetBlockTimeStampOffsetError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Sets the timestamp offset (seconds) for blocks in dev mode. Providing an offset of 0 will unset this value and try to use the real clock for the timestamp. +pub async fn set_block_time_stamp_offset( + http_client: &dyn HttpClient, + offset: u64, +) -> Result<(), Error> { + let p_offset = offset; + + let path = format!("/v2/devmode/blocks/offset/{offset}", offset = p_offset); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Post, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let _ = response; + Ok(()) +} diff --git a/crates/algod_client/src/apis/set_sync_round.rs b/crates/algod_client/src/apis/set_sync_round.rs new file mode 100644 index 00000000..4d92c3b9 --- /dev/null +++ b/crates/algod_client/src/apis/set_sync_round.rs @@ -0,0 +1,62 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::ErrorResponse; + +// Import request body type if needed + +/// struct for typed errors of method [`set_sync_round`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum SetSyncRoundError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status500(ErrorResponse), + Status503(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Sets the minimum sync round on the ledger. +pub async fn set_sync_round(http_client: &dyn HttpClient, round: u64) -> Result<(), Error> { + let p_round = round; + + let path = format!("/v2/ledger/sync/{round}", round = p_round); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Post, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let _ = response; + Ok(()) +} diff --git a/crates/algod_client/src/apis/shutdown_node.rs b/crates/algod_client/src/apis/shutdown_node.rs new file mode 100644 index 00000000..f52ba665 --- /dev/null +++ b/crates/algod_client/src/apis/shutdown_node.rs @@ -0,0 +1,81 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint + +// Import request body type if needed + +/// struct for typed errors of method [`shutdown_node`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ShutdownNodeError { + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Special management endpoint to shutdown the node. Optionally provide a timeout parameter to indicate that the node should begin shutting down after a number of seconds. +pub async fn shutdown_node( + http_client: &dyn HttpClient, + timeout: Option, +) -> Result { + let p_timeout = timeout; + + let path = "/v2/shutdown".to_string(); + + let mut query_params: HashMap = HashMap::new(); + if let Some(value) = p_timeout { + query_params.insert("timeout".to_string(), value.to_string()); + } + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Post, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/simulate_transaction.rs b/crates/algod_client/src/apis/simulate_transaction.rs new file mode 100644 index 00000000..370bca78 --- /dev/null +++ b/crates/algod_client/src/apis/simulate_transaction.rs @@ -0,0 +1,106 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::parameter_enums::*; +use super::{AlgodApiError, ContentType, Error}; +use algokit_transact::AlgorandMsgpack; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, SimulateTransaction}; + +// Import request body type if needed +use crate::models::SimulateRequest; + +/// struct for typed errors of method [`simulate_transaction`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum SimulateTransactionError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status500(ErrorResponse), + Status503(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Simulates a raw transaction or transaction group as it would be evaluated on the network. The simulation will use blockchain state from the latest committed round. +pub async fn simulate_transaction( + http_client: &dyn HttpClient, + request: SimulateRequest, + format: Option, +) -> Result { + let p_format = format; + let p_request = request; + + let path = "/v2/transactions/simulate".to_string(); + + let mut query_params: HashMap = HashMap::new(); + if let Some(value) = p_format { + query_params.insert("format".to_string(), value.to_string()); + } + + let use_msgpack = p_format.map(|f| f != Format::Json).unwrap_or(true); + + let mut headers: HashMap = HashMap::new(); + if use_msgpack { + headers.insert( + "Content-Type".to_string(), + "application/msgpack".to_string(), + ); + headers.insert("Accept".to_string(), "application/msgpack".to_string()); + } else { + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + } + + let body = if use_msgpack { + Some(rmp_serde::to_vec_named(&p_request).map_err(|e| Error::Serde(e.to_string()))?) + } else { + Some(serde_json::to_vec(&p_request).map_err(|e| Error::Serde(e.to_string()))?) + }; + + let response = http_client + .request( + HttpMethod::Post, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/start_catchup.rs b/crates/algod_client/src/apis/start_catchup.rs new file mode 100644 index 00000000..19751d7b --- /dev/null +++ b/crates/algod_client/src/apis/start_catchup.rs @@ -0,0 +1,92 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, StartCatchup}; + +// Import request body type if needed + +/// struct for typed errors of method [`start_catchup`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum StartCatchupError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status408(ErrorResponse), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Given a catchpoint, it starts catching up to this catchpoint +pub async fn start_catchup( + http_client: &dyn HttpClient, + catchpoint: &str, + min: Option, +) -> Result { + let p_catchpoint = catchpoint; + let p_min = min; + + let path = format!( + "/v2/catchup/{catchpoint}", + catchpoint = crate::apis::urlencode(p_catchpoint) + ); + + let mut query_params: HashMap = HashMap::new(); + if let Some(value) = p_min { + query_params.insert("min".to_string(), value.to_string()); + } + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Post, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/swagger_json.rs b/crates/algod_client/src/apis/swagger_json.rs new file mode 100644 index 00000000..11332002 --- /dev/null +++ b/crates/algod_client/src/apis/swagger_json.rs @@ -0,0 +1,74 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint + +// Import request body type if needed + +/// struct for typed errors of method [`swagger_json`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum SwaggerJsonError { + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Returns the entire swagger spec in json. +pub async fn swagger_json(http_client: &dyn HttpClient) -> Result { + let path = "/swagger.json".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/teal_compile.rs b/crates/algod_client/src/apis/teal_compile.rs new file mode 100644 index 00000000..ed44061a --- /dev/null +++ b/crates/algod_client/src/apis/teal_compile.rs @@ -0,0 +1,89 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, TealCompile}; + +// Import request body type if needed + +/// struct for typed errors of method [`teal_compile`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum TealCompileError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status404(), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Given TEAL source code in plain text, return base64 encoded program bytes and base32 SHA512_256 hash of program bytes (Address style). This endpoint is only enabled when a node's configuration file sets EnableDeveloperAPI to true. +pub async fn teal_compile( + http_client: &dyn HttpClient, + request: Vec, + sourcemap: Option, +) -> Result { + let p_sourcemap = sourcemap; + let p_request = request; + + let path = "/v2/teal/compile".to_string(); + + let mut query_params: HashMap = HashMap::new(); + if let Some(value) = p_sourcemap { + query_params.insert("sourcemap".to_string(), value.to_string()); + } + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = Some(serde_json::to_vec(&p_request).map_err(|e| Error::Serde(e.to_string()))?); + + let response = http_client + .request( + HttpMethod::Post, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/teal_disassemble.rs b/crates/algod_client/src/apis/teal_disassemble.rs new file mode 100644 index 00000000..36eea64c --- /dev/null +++ b/crates/algod_client/src/apis/teal_disassemble.rs @@ -0,0 +1,88 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; +use algokit_transact::AlgorandMsgpack; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, TealDisassemble}; + +// Import request body type if needed + +/// struct for typed errors of method [`teal_disassemble`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum TealDisassembleError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status404(), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Given the program bytes, return the TEAL source code in plain text. This endpoint is only enabled when a node's configuration file sets EnableDeveloperAPI to true. +pub async fn teal_disassemble( + http_client: &dyn HttpClient, + request: String, +) -> Result { + let p_request = request; + + let path = "/v2/teal/disassemble".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert( + "Content-Type".to_string(), + "application/msgpack".to_string(), + ); + headers.insert("Accept".to_string(), "application/msgpack".to_string()); + + let body = Some(serde_json::to_vec(&p_request).map_err(|e| Error::Serde(e.to_string()))?); + + let response = http_client + .request( + HttpMethod::Post, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/teal_dryrun.rs b/crates/algod_client/src/apis/teal_dryrun.rs new file mode 100644 index 00000000..05d0822d --- /dev/null +++ b/crates/algod_client/src/apis/teal_dryrun.rs @@ -0,0 +1,89 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; +use algokit_transact::AlgorandMsgpack; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, TealDryrun}; + +// Import request body type if needed +use crate::models::DryrunRequest; + +/// struct for typed errors of method [`teal_dryrun`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum TealDryrunError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status404(), + Status500(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Executes TEAL program(s) in context and returns debugging information about the execution. This endpoint is only enabled when a node's configuration file sets EnableDeveloperAPI to true. +pub async fn teal_dryrun( + http_client: &dyn HttpClient, + request: Option, +) -> Result { + let p_request = request; + + let path = "/v2/teal/dryrun".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert( + "Content-Type".to_string(), + "application/msgpack".to_string(), + ); + headers.insert("Accept".to_string(), "application/msgpack".to_string()); + + let body = Some(rmp_serde::to_vec_named(&p_request).map_err(|e| Error::Serde(e.to_string()))?); + + let response = http_client + .request( + HttpMethod::Post, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/transaction_params.rs b/crates/algod_client/src/apis/transaction_params.rs new file mode 100644 index 00000000..71fe7401 --- /dev/null +++ b/crates/algod_client/src/apis/transaction_params.rs @@ -0,0 +1,78 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, TransactionParams}; + +// Import request body type if needed + +/// struct for typed errors of method [`transaction_params`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum TransactionParamsError { + Status401(ErrorResponse), + Status500(ErrorResponse), + Status503(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Get parameters for constructing a new transaction +pub async fn transaction_params(http_client: &dyn HttpClient) -> Result { + let path = "/v2/transactions/params".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/apis/unset_sync_round.rs b/crates/algod_client/src/apis/unset_sync_round.rs new file mode 100644 index 00000000..cfd64a36 --- /dev/null +++ b/crates/algod_client/src/apis/unset_sync_round.rs @@ -0,0 +1,60 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::ErrorResponse; + +// Import request body type if needed + +/// struct for typed errors of method [`unset_sync_round`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum UnsetSyncRoundError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status500(ErrorResponse), + Status503(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Unset the ledger sync round. +pub async fn unset_sync_round(http_client: &dyn HttpClient) -> Result<(), Error> { + let path = "/v2/ledger/sync".to_string(); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Delete, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let _ = response; + Ok(()) +} diff --git a/crates/algod_client/src/apis/wait_for_block.rs b/crates/algod_client/src/apis/wait_for_block.rs new file mode 100644 index 00000000..5076b3d2 --- /dev/null +++ b/crates/algod_client/src/apis/wait_for_block.rs @@ -0,0 +1,84 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use algokit_http_client::{HttpClient, HttpMethod}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use super::{AlgodApiError, ContentType, Error}; + +// Import all custom types used by this endpoint +use crate::models::{ErrorResponse, WaitForBlock}; + +// Import request body type if needed + +/// struct for typed errors of method [`wait_for_block`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum WaitForBlockError { + Status400(ErrorResponse), + Status401(ErrorResponse), + Status500(ErrorResponse), + Status503(ErrorResponse), + Statusdefault(), + DefaultResponse(), + UnknownValue(serde_json::Value), +} + +/// Waits for a block to appear after round {round} and returns the node's status at the time. There is a 1 minute timeout, when reached the current status is returned regardless of whether or not it is the round after the given round. +pub async fn wait_for_block( + http_client: &dyn HttpClient, + round: u64, +) -> Result { + let p_round = round; + + let path = format!("/v2/status/wait_for_block_after/{round}", round = p_round); + + let query_params: HashMap = HashMap::new(); + + let mut headers: HashMap = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("Accept".to_string(), "application/json".to_string()); + + let body = None; + + let response = http_client + .request( + HttpMethod::Get, + path, + Some(query_params), + body, + Some(headers), + ) + .await + .map_err(Error::Http)?; + + let content_type = response + .headers + .get("content-type") + .map(|s| s.as_str()) + .unwrap_or("application/json"); + + match ContentType::from(content_type) { + ContentType::Json => { + serde_json::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::MsgPack => { + rmp_serde::from_slice(&response.body).map_err(|e| Error::Serde(e.to_string())) + } + ContentType::Text => { + let text = String::from_utf8(response.body).map_err(|e| Error::Serde(e.to_string()))?; + Err(Error::Serde(format!("Unexpected text response: {}", text))) + } + ContentType::Unsupported(ct) => { + Err(Error::Serde(format!("Unsupported content type: {}", ct))) + } + } +} diff --git a/crates/algod_client/src/lib.rs b/crates/algod_client/src/lib.rs new file mode 100644 index 00000000..7430015d --- /dev/null +++ b/crates/algod_client/src/lib.rs @@ -0,0 +1,8 @@ +#![allow(unused_imports)] +#![allow(clippy::too_many_arguments)] + +pub mod apis; +pub mod models; + +// Re-export the main client for convenience +pub use apis::AlgodClient; diff --git a/crates/algod_client/src/models/abort_catchup.rs b/crates/algod_client/src/models/abort_catchup.rs new file mode 100644 index 00000000..d2bf9beb --- /dev/null +++ b/crates/algod_client/src/models/abort_catchup.rs @@ -0,0 +1,27 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// An catchpoint abort response. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct AbortCatchup { + /// Catchup abort response string + #[serde(rename = "catchup-message")] + pub catchup_message: String, +} + +impl AbortCatchup { + /// Constructor for AbortCatchup + pub fn new(catchup_message: String) -> AbortCatchup { + AbortCatchup { catchup_message } + } +} diff --git a/crates/algod_client/src/models/account.rs b/crates/algod_client/src/models/account.rs new file mode 100644 index 00000000..c7fc17fd --- /dev/null +++ b/crates/algod_client/src/models/account.rs @@ -0,0 +1,188 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::AccountParticipation; +use crate::models::Application; +use crate::models::ApplicationLocalState; +use crate::models::ApplicationStateSchema; +use crate::models::Asset; +use crate::models::AssetHolding; + +/// Account information at a given round. +/// +/// Definition: +/// data/basics/userBalance.go : AccountData +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct Account { + /// the account public key + #[serde(rename = "address")] + pub address: String, + /// \[algo\] total number of MicroAlgos in the account + #[serde(rename = "amount")] + pub amount: u64, + /// MicroAlgo balance required by the account. + /// + /// The requirement grows based on asset and application usage. + #[serde(rename = "min-balance")] + pub min_balance: u64, + /// specifies the amount of MicroAlgos in the account, without the pending rewards. + #[serde(rename = "amount-without-pending-rewards")] + pub amount_without_pending_rewards: u64, + /// \[appl\] applications local data stored in this account. + /// + /// Note the raw object uses `map[int] -> AppLocalState` for this type. + #[serde(rename = "apps-local-state", skip_serializing_if = "Option::is_none")] + pub apps_local_state: Option>, + /// The count of all applications that have been opted in, equivalent to the count of application local data (AppLocalState objects) stored in this account. + #[serde(rename = "total-apps-opted-in")] + pub total_apps_opted_in: u64, + #[serde(rename = "apps-total-schema", skip_serializing_if = "Option::is_none")] + pub apps_total_schema: Option, + /// \[teap\] the sum of all extra application program pages for this account. + #[serde( + rename = "apps-total-extra-pages", + skip_serializing_if = "Option::is_none" + )] + pub apps_total_extra_pages: Option, + /// \[asset\] assets held by this account. + /// + /// Note the raw object uses `map[int] -> AssetHolding` for this type. + #[serde(rename = "assets", skip_serializing_if = "Option::is_none")] + pub assets: Option>, + /// The count of all assets that have been opted in, equivalent to the count of AssetHolding objects held by this account. + #[serde(rename = "total-assets-opted-in")] + pub total_assets_opted_in: u64, + /// \[appp\] parameters of applications created by this account including app global data. + /// + /// Note: the raw account uses `map[int] -> AppParams` for this type. + #[serde(rename = "created-apps", skip_serializing_if = "Option::is_none")] + pub created_apps: Option>, + /// The count of all apps (AppParams objects) created by this account. + #[serde(rename = "total-created-apps")] + pub total_created_apps: u64, + /// \[apar\] parameters of assets created by this account. + /// + /// Note: the raw account uses `map[int] -> Asset` for this type. + #[serde(rename = "created-assets", skip_serializing_if = "Option::is_none")] + pub created_assets: Option>, + /// The count of all assets (AssetParams objects) created by this account. + #[serde(rename = "total-created-assets")] + pub total_created_assets: u64, + /// \[tbx\] The number of existing boxes created by this account's app. + #[serde(rename = "total-boxes", skip_serializing_if = "Option::is_none")] + pub total_boxes: Option, + /// \[tbxb\] The total number of bytes used by this account's app's box keys and values. + #[serde(rename = "total-box-bytes", skip_serializing_if = "Option::is_none")] + pub total_box_bytes: Option, + #[serde(rename = "participation", skip_serializing_if = "Option::is_none")] + pub participation: Option, + /// Whether or not the account can receive block incentives if its balance is in range at proposal time. + #[serde(rename = "incentive-eligible", skip_serializing_if = "Option::is_none")] + pub incentive_eligible: Option, + /// amount of MicroAlgos of pending rewards in this account. + #[serde(rename = "pending-rewards")] + pub pending_rewards: u64, + /// \[ebase\] used as part of the rewards computation. Only applicable to accounts which are participating. + #[serde(rename = "reward-base", skip_serializing_if = "Option::is_none")] + pub reward_base: Option, + /// \[ern\] total rewards of MicroAlgos the account has received, including pending rewards. + #[serde(rename = "rewards")] + pub rewards: u64, + /// The round for which this information is relevant. + #[serde(rename = "round")] + pub round: u64, + /// \[onl\] delegation status of the account's MicroAlgos + /// * Offline - indicates that the associated account is delegated. + /// * Online - indicates that the associated account used as part of the delegation pool. + /// * NotParticipating - indicates that the associated account is neither a delegator nor a delegate. + #[serde(rename = "status")] + pub status: String, + /// Indicates what type of signature is used by this account, must be one of: + /// * sig + /// * msig + /// * lsig + #[serde(rename = "sig-type", skip_serializing_if = "Option::is_none")] + pub sig_type: Option, + /// \[spend\] the address against which signing should be checked. If empty, the address of the current account is used. This field can be updated in any transaction by setting the RekeyTo field. + #[serde(rename = "auth-addr", skip_serializing_if = "Option::is_none")] + pub auth_addr: Option, + /// The round in which this account last proposed the block. + #[serde(rename = "last-proposed", skip_serializing_if = "Option::is_none")] + pub last_proposed: Option, + /// The round in which this account last went online, or explicitly renewed their online status. + #[serde(rename = "last-heartbeat", skip_serializing_if = "Option::is_none")] + pub last_heartbeat: Option, +} + +impl AlgorandMsgpack for Account { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl Account { + /// Constructor for Account + pub fn new( + address: String, + amount: u64, + min_balance: u64, + amount_without_pending_rewards: u64, + total_apps_opted_in: u64, + total_assets_opted_in: u64, + total_created_apps: u64, + total_created_assets: u64, + pending_rewards: u64, + rewards: u64, + round: u64, + status: String, + ) -> Account { + Account { + address, + amount, + min_balance, + amount_without_pending_rewards, + total_apps_opted_in, + total_assets_opted_in, + total_created_apps, + total_created_assets, + pending_rewards, + rewards, + round, + status, + apps_local_state: None, + apps_total_schema: None, + apps_total_extra_pages: None, + assets: None, + created_apps: None, + created_assets: None, + total_boxes: None, + total_box_bytes: None, + participation: None, + incentive_eligible: None, + reward_base: None, + sig_type: None, + auth_addr: None, + last_proposed: None, + last_heartbeat: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/account_application_information.rs b/crates/algod_client/src/models/account_application_information.rs new file mode 100644 index 00000000..28a9154e --- /dev/null +++ b/crates/algod_client/src/models/account_application_information.rs @@ -0,0 +1,53 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::ApplicationLocalState; +use crate::models::ApplicationParams; + +/// AccountApplicationResponse describes the account's application local state and global state (AppLocalState and AppParams, if either exists) for a specific application ID. Global state will only be returned if the provided address is the application's creator. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct AccountApplicationInformation { + /// The round for which this information is relevant. + #[serde(rename = "round")] + pub round: u64, + #[serde(rename = "app-local-state", skip_serializing_if = "Option::is_none")] + pub app_local_state: Option, + #[serde(rename = "created-app", skip_serializing_if = "Option::is_none")] + pub created_app: Option, +} + +impl AlgorandMsgpack for AccountApplicationInformation { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl AccountApplicationInformation { + /// Constructor for AccountApplicationInformation + pub fn new(round: u64) -> AccountApplicationInformation { + AccountApplicationInformation { + round, + app_local_state: None, + created_app: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/account_asset_holding.rs b/crates/algod_client/src/models/account_asset_holding.rs new file mode 100644 index 00000000..1bf1b643 --- /dev/null +++ b/crates/algod_client/src/models/account_asset_holding.rs @@ -0,0 +1,34 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +use crate::models::AssetHolding; +use crate::models::AssetParams; + +/// AccountAssetHolding describes the account's asset holding and asset parameters (if either exist) for a specific asset ID. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct AccountAssetHolding { + #[serde(rename = "asset-holding")] + pub asset_holding: AssetHolding, + #[serde(rename = "asset-params", skip_serializing_if = "Option::is_none")] + pub asset_params: Option, +} + +impl AccountAssetHolding { + /// Constructor for AccountAssetHolding + pub fn new(asset_holding: AssetHolding) -> AccountAssetHolding { + AccountAssetHolding { + asset_holding, + asset_params: None, + } + } +} diff --git a/crates/algod_client/src/models/account_asset_information.rs b/crates/algod_client/src/models/account_asset_information.rs new file mode 100644 index 00000000..f481a319 --- /dev/null +++ b/crates/algod_client/src/models/account_asset_information.rs @@ -0,0 +1,53 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::AssetHolding; +use crate::models::AssetParams; + +/// AccountAssetResponse describes the account's asset holding and asset parameters (if either exist) for a specific asset ID. Asset parameters will only be returned if the provided address is the asset's creator. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct AccountAssetInformation { + /// The round for which this information is relevant. + #[serde(rename = "round")] + pub round: u64, + #[serde(rename = "asset-holding", skip_serializing_if = "Option::is_none")] + pub asset_holding: Option, + #[serde(rename = "created-asset", skip_serializing_if = "Option::is_none")] + pub created_asset: Option, +} + +impl AlgorandMsgpack for AccountAssetInformation { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl AccountAssetInformation { + /// Constructor for AccountAssetInformation + pub fn new(round: u64) -> AccountAssetInformation { + AccountAssetInformation { + round, + asset_holding: None, + created_asset: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/account_assets_information.rs b/crates/algod_client/src/models/account_assets_information.rs new file mode 100644 index 00000000..c583c949 --- /dev/null +++ b/crates/algod_client/src/models/account_assets_information.rs @@ -0,0 +1,38 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +use crate::models::AccountAssetHolding; + +/// AccountAssetsInformationResponse contains a list of assets held by an account. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct AccountAssetsInformation { + /// The round for which this information is relevant. + #[serde(rename = "round")] + pub round: u64, + /// Used for pagination, when making another request provide this token with the next parameter. + #[serde(rename = "next-token", skip_serializing_if = "Option::is_none")] + pub next_token: Option, + #[serde(rename = "asset-holdings", skip_serializing_if = "Option::is_none")] + pub asset_holdings: Option>, +} + +impl AccountAssetsInformation { + /// Constructor for AccountAssetsInformation + pub fn new(round: u64) -> AccountAssetsInformation { + AccountAssetsInformation { + round, + next_token: None, + asset_holdings: None, + } + } +} diff --git a/crates/algod_client/src/models/account_participation.rs b/crates/algod_client/src/models/account_participation.rs new file mode 100644 index 00000000..aca20226 --- /dev/null +++ b/crates/algod_client/src/models/account_participation.rs @@ -0,0 +1,75 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +/// AccountParticipation describes the parameters used by this account in consensus protocol. +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct AccountParticipation { + /// \[sel\] Selection public key (if any) currently registered for this round. + #[serde_as(as = "serde_with::base64::Base64")] + #[serde(rename = "selection-participation-key")] + pub selection_participation_key: Vec, + /// \[voteFst\] First round for which this participation is valid. + #[serde(rename = "vote-first-valid")] + pub vote_first_valid: u64, + /// \[voteKD\] Number of subkeys in each batch of participation keys. + #[serde(rename = "vote-key-dilution")] + pub vote_key_dilution: u64, + /// \[voteLst\] Last round for which this participation is valid. + #[serde(rename = "vote-last-valid")] + pub vote_last_valid: u64, + /// \[vote\] root participation public key (if any) currently registered for this round. + #[serde_as(as = "serde_with::base64::Base64")] + #[serde(rename = "vote-participation-key")] + pub vote_participation_key: Vec, + /// \[stprf\] Root of the state proof key (if any) + #[serde_as(as = "Option")] + #[serde(rename = "state-proof-key", skip_serializing_if = "Option::is_none")] + pub state_proof_key: Option>, +} + +impl AlgorandMsgpack for AccountParticipation { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl AccountParticipation { + /// Constructor for AccountParticipation + pub fn new( + selection_participation_key: Vec, + vote_first_valid: u64, + vote_key_dilution: u64, + vote_last_valid: u64, + vote_participation_key: Vec, + ) -> AccountParticipation { + AccountParticipation { + selection_participation_key, + vote_first_valid, + vote_key_dilution, + vote_last_valid, + vote_participation_key, + state_proof_key: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/account_state_delta.rs b/crates/algod_client/src/models/account_state_delta.rs new file mode 100644 index 00000000..7cd51d5b --- /dev/null +++ b/crates/algod_client/src/models/account_state_delta.rs @@ -0,0 +1,45 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::StateDelta; + +/// Application state delta. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct AccountStateDelta { + #[serde(rename = "address")] + pub address: String, + #[serde(rename = "delta")] + pub delta: StateDelta, +} + +impl AlgorandMsgpack for AccountStateDelta { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl AccountStateDelta { + /// Constructor for AccountStateDelta + pub fn new(address: String, delta: StateDelta) -> AccountStateDelta { + AccountStateDelta { address, delta } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/add_participation_key.rs b/crates/algod_client/src/models/add_participation_key.rs new file mode 100644 index 00000000..8928abbd --- /dev/null +++ b/crates/algod_client/src/models/add_participation_key.rs @@ -0,0 +1,27 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// Participation ID of the submission +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct AddParticipationKey { + /// encoding of the participation ID. + #[serde(rename = "partId")] + pub part_id: String, +} + +impl AddParticipationKey { + /// Constructor for AddParticipationKey + pub fn new(part_id: String) -> AddParticipationKey { + AddParticipationKey { part_id } + } +} diff --git a/crates/algod_client/src/models/app_call_logs.rs b/crates/algod_client/src/models/app_call_logs.rs new file mode 100644 index 00000000..cf4ed5b1 --- /dev/null +++ b/crates/algod_client/src/models/app_call_logs.rs @@ -0,0 +1,37 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// The logged messages from an app call along with the app ID and outer transaction ID. Logs appear in the same order that they were emitted. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct AppCallLogs { + /// An array of logs + #[serde(rename = "logs")] + pub logs: Vec, + /// The application from which the logs were generated + #[serde(rename = "application-index")] + pub application_index: u64, + /// The transaction ID of the outer app call that lead to these logs + #[serde(rename = "txId")] + pub tx_id: String, +} + +impl AppCallLogs { + /// Constructor for AppCallLogs + pub fn new(logs: Vec, application_index: u64, tx_id: String) -> AppCallLogs { + AppCallLogs { + logs, + application_index, + tx_id, + } + } +} diff --git a/crates/algod_client/src/models/application.rs b/crates/algod_client/src/models/application.rs new file mode 100644 index 00000000..b0f2a7c2 --- /dev/null +++ b/crates/algod_client/src/models/application.rs @@ -0,0 +1,46 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::ApplicationParams; + +/// Application index and its parameters +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct Application { + /// \[appidx\] application index. + #[serde(rename = "id")] + pub id: u64, + #[serde(rename = "params")] + pub params: ApplicationParams, +} + +impl AlgorandMsgpack for Application { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl Application { + /// Constructor for Application + pub fn new(id: u64, params: ApplicationParams) -> Application { + Application { id, params } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/application_initial_states.rs b/crates/algod_client/src/models/application_initial_states.rs new file mode 100644 index 00000000..27eae92b --- /dev/null +++ b/crates/algod_client/src/models/application_initial_states.rs @@ -0,0 +1,56 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::ApplicationKvStorage; + +/// An application's initial global/local/box states that were accessed during simulation. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct ApplicationInitialStates { + /// Application index. + #[serde(rename = "id")] + pub id: u64, + /// An application's initial local states tied to different accounts. + #[serde(rename = "app-locals", skip_serializing_if = "Option::is_none")] + pub app_locals: Option>, + #[serde(rename = "app-globals", skip_serializing_if = "Option::is_none")] + pub app_globals: Option, + #[serde(rename = "app-boxes", skip_serializing_if = "Option::is_none")] + pub app_boxes: Option, +} + +impl AlgorandMsgpack for ApplicationInitialStates { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl ApplicationInitialStates { + /// Constructor for ApplicationInitialStates + pub fn new(id: u64) -> ApplicationInitialStates { + ApplicationInitialStates { + id, + app_locals: None, + app_globals: None, + app_boxes: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/application_kv_storage.rs b/crates/algod_client/src/models/application_kv_storage.rs new file mode 100644 index 00000000..f74850a6 --- /dev/null +++ b/crates/algod_client/src/models/application_kv_storage.rs @@ -0,0 +1,47 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::AvmKeyValue; + +/// An application's global/local/box state. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct ApplicationKvStorage { + /// Key-Value pairs representing application states. + #[serde(rename = "kvs")] + pub kvs: Vec, + /// The address of the account associated with the local state. + #[serde(rename = "account", skip_serializing_if = "Option::is_none")] + pub account: Option, +} + +impl AlgorandMsgpack for ApplicationKvStorage { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl ApplicationKvStorage { + /// Constructor for ApplicationKvStorage + pub fn new(kvs: Vec) -> ApplicationKvStorage { + ApplicationKvStorage { kvs, account: None } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/application_local_reference.rs b/crates/algod_client/src/models/application_local_reference.rs new file mode 100644 index 00000000..2ce64a25 --- /dev/null +++ b/crates/algod_client/src/models/application_local_reference.rs @@ -0,0 +1,45 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +/// References an account's local state for an application. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct ApplicationLocalReference { + /// Address of the account with the local state. + #[serde(rename = "account")] + pub account: String, + /// Application ID of the local state application. + #[serde(rename = "app")] + pub app: u64, +} + +impl AlgorandMsgpack for ApplicationLocalReference { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl ApplicationLocalReference { + /// Constructor for ApplicationLocalReference + pub fn new(account: String, app: u64) -> ApplicationLocalReference { + ApplicationLocalReference { account, app } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/application_local_state.rs b/crates/algod_client/src/models/application_local_state.rs new file mode 100644 index 00000000..dad0f6d6 --- /dev/null +++ b/crates/algod_client/src/models/application_local_state.rs @@ -0,0 +1,53 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::ApplicationStateSchema; +use crate::models::TealKeyValueStore; + +/// Stores local state associated with an application. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct ApplicationLocalState { + /// The application which this local state is for. + #[serde(rename = "id")] + pub id: u64, + #[serde(rename = "schema")] + pub schema: ApplicationStateSchema, + #[serde(rename = "key-value", skip_serializing_if = "Option::is_none")] + pub key_value: Option, +} + +impl AlgorandMsgpack for ApplicationLocalState { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl ApplicationLocalState { + /// Constructor for ApplicationLocalState + pub fn new(id: u64, schema: ApplicationStateSchema) -> ApplicationLocalState { + ApplicationLocalState { + id, + schema, + key_value: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/application_params.rs b/crates/algod_client/src/models/application_params.rs new file mode 100644 index 00000000..400ac761 --- /dev/null +++ b/crates/algod_client/src/models/application_params.rs @@ -0,0 +1,86 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +use crate::models::ApplicationStateSchema; +use crate::models::TealKeyValueStore; + +/// Stores the global information associated with an application. +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct ApplicationParams { + /// The address that created this application. This is the address where the parameters and global state for this application can be found. + #[serde(rename = "creator")] + pub creator: String, + /// \[approv\] approval program. + #[serde_as(as = "serde_with::base64::Base64")] + #[serde(rename = "approval-program")] + pub approval_program: Vec, + /// \[clearp\] approval program. + #[serde_as(as = "serde_with::base64::Base64")] + #[serde(rename = "clear-state-program")] + pub clear_state_program: Vec, + /// \[epp\] the amount of extra program pages available to this app. + #[serde( + rename = "extra-program-pages", + skip_serializing_if = "Option::is_none" + )] + pub extra_program_pages: Option, + #[serde(rename = "local-state-schema", skip_serializing_if = "Option::is_none")] + pub local_state_schema: Option, + #[serde( + rename = "global-state-schema", + skip_serializing_if = "Option::is_none" + )] + pub global_state_schema: Option, + #[serde(rename = "global-state", skip_serializing_if = "Option::is_none")] + pub global_state: Option, + /// \[v\] the number of updates to the application programs + #[serde(rename = "version", skip_serializing_if = "Option::is_none")] + pub version: Option, +} + +impl AlgorandMsgpack for ApplicationParams { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl ApplicationParams { + /// Constructor for ApplicationParams + pub fn new( + creator: String, + approval_program: Vec, + clear_state_program: Vec, + ) -> ApplicationParams { + ApplicationParams { + creator, + approval_program, + clear_state_program, + extra_program_pages: None, + local_state_schema: None, + global_state_schema: None, + global_state: None, + version: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/application_state_operation.rs b/crates/algod_client/src/models/application_state_operation.rs new file mode 100644 index 00000000..291d514c --- /dev/null +++ b/crates/algod_client/src/models/application_state_operation.rs @@ -0,0 +1,68 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +use crate::models::AvmValue; + +/// An operation against an application's global/local/box state. +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct ApplicationStateOperation { + /// Operation type. Value `w` is **write**, `d` is **delete**. + #[serde(rename = "operation")] + pub operation: String, + /// Type of application state. Value `g` is **global state**, `l` is **local state**, `b` is **boxes**. + #[serde(rename = "app-state-type")] + pub app_state_type: String, + /// The key (name) of the global/local/box state. + #[serde_as(as = "serde_with::base64::Base64")] + #[serde(rename = "key")] + pub key: Vec, + #[serde(rename = "new-value", skip_serializing_if = "Option::is_none")] + pub new_value: Option, + /// For local state changes, the address of the account associated with the local state. + #[serde(rename = "account", skip_serializing_if = "Option::is_none")] + pub account: Option, +} + +impl AlgorandMsgpack for ApplicationStateOperation { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl ApplicationStateOperation { + /// Constructor for ApplicationStateOperation + pub fn new( + operation: String, + app_state_type: String, + key: Vec, + ) -> ApplicationStateOperation { + ApplicationStateOperation { + operation, + app_state_type, + key, + new_value: None, + account: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/application_state_schema.rs b/crates/algod_client/src/models/application_state_schema.rs new file mode 100644 index 00000000..692ce06b --- /dev/null +++ b/crates/algod_client/src/models/application_state_schema.rs @@ -0,0 +1,48 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +/// Specifies maximums on the number of each type that may be stored. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct ApplicationStateSchema { + /// \[nui\] num of uints. + #[serde(rename = "num-uint")] + pub num_uint: u64, + /// \[nbs\] num of byte slices. + #[serde(rename = "num-byte-slice")] + pub num_byte_slice: u64, +} + +impl AlgorandMsgpack for ApplicationStateSchema { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl ApplicationStateSchema { + /// Constructor for ApplicationStateSchema + pub fn new(num_uint: u64, num_byte_slice: u64) -> ApplicationStateSchema { + ApplicationStateSchema { + num_uint, + num_byte_slice, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/asset.rs b/crates/algod_client/src/models/asset.rs new file mode 100644 index 00000000..b365435c --- /dev/null +++ b/crates/algod_client/src/models/asset.rs @@ -0,0 +1,46 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::AssetParams; + +/// Specifies both the unique identifier and the parameters for an asset +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct Asset { + /// unique asset identifier + #[serde(rename = "index")] + pub index: u64, + #[serde(rename = "params")] + pub params: AssetParams, +} + +impl AlgorandMsgpack for Asset { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl Asset { + /// Constructor for Asset + pub fn new(index: u64, params: AssetParams) -> Asset { + Asset { index, params } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/asset_holding.rs b/crates/algod_client/src/models/asset_holding.rs new file mode 100644 index 00000000..5032f630 --- /dev/null +++ b/crates/algod_client/src/models/asset_holding.rs @@ -0,0 +1,55 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +/// Describes an asset held by an account. +/// +/// Definition: +/// data/basics/userBalance.go : AssetHolding +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct AssetHolding { + /// \[a\] number of units held. + #[serde(rename = "amount")] + pub amount: u64, + /// Asset ID of the holding. + #[serde(rename = "asset-id")] + pub asset_id: u64, + /// \[f\] whether or not the holding is frozen. + #[serde(rename = "is-frozen")] + pub is_frozen: bool, +} + +impl AlgorandMsgpack for AssetHolding { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl AssetHolding { + /// Constructor for AssetHolding + pub fn new(amount: u64, asset_id: u64, is_frozen: bool) -> AssetHolding { + AssetHolding { + amount, + asset_id, + is_frozen, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/asset_holding_reference.rs b/crates/algod_client/src/models/asset_holding_reference.rs new file mode 100644 index 00000000..a6f43bf5 --- /dev/null +++ b/crates/algod_client/src/models/asset_holding_reference.rs @@ -0,0 +1,45 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +/// References an asset held by an account. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct AssetHoldingReference { + /// Address of the account holding the asset. + #[serde(rename = "account")] + pub account: String, + /// Asset ID of the holding. + #[serde(rename = "asset")] + pub asset: u64, +} + +impl AlgorandMsgpack for AssetHoldingReference { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl AssetHoldingReference { + /// Constructor for AssetHoldingReference + pub fn new(account: String, asset: u64) -> AssetHoldingReference { + AssetHoldingReference { account, asset } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/asset_params.rs b/crates/algod_client/src/models/asset_params.rs new file mode 100644 index 00000000..c8d10742 --- /dev/null +++ b/crates/algod_client/src/models/asset_params.rs @@ -0,0 +1,111 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +/// AssetParams specifies the parameters for an asset. +/// +/// \[apar\] when part of an AssetConfig transaction. +/// +/// Definition: +/// data/transactions/asset.go : AssetParams +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct AssetParams { + /// \[c\] Address of account used to clawback holdings of this asset. If empty, clawback is not permitted. + #[serde(rename = "clawback", skip_serializing_if = "Option::is_none")] + pub clawback: Option, + /// The address that created this asset. This is the address where the parameters for this asset can be found, and also the address where unwanted asset units can be sent in the worst case. + #[serde(rename = "creator")] + pub creator: String, + /// \[dc\] The number of digits to use after the decimal point when displaying this asset. If 0, the asset is not divisible. If 1, the base unit of the asset is in tenths. If 2, the base unit of the asset is in hundredths, and so on. This value must be between 0 and 19 (inclusive). + #[serde(rename = "decimals")] + pub decimals: u32, + /// \[df\] Whether holdings of this asset are frozen by default. + #[serde(rename = "default-frozen", skip_serializing_if = "Option::is_none")] + pub default_frozen: Option, + /// \[f\] Address of account used to freeze holdings of this asset. If empty, freezing is not permitted. + #[serde(rename = "freeze", skip_serializing_if = "Option::is_none")] + pub freeze: Option, + /// \[m\] Address of account used to manage the keys of this asset and to destroy it. + #[serde(rename = "manager", skip_serializing_if = "Option::is_none")] + pub manager: Option, + /// \[am\] A commitment to some unspecified asset metadata. The format of this metadata is up to the application. + #[serde_as(as = "Option")] + #[serde(rename = "metadata-hash", skip_serializing_if = "Option::is_none")] + pub metadata_hash: Option>, + /// \[an\] Name of this asset, as supplied by the creator. Included only when the asset name is composed of printable utf-8 characters. + #[serde(rename = "name", skip_serializing_if = "Option::is_none")] + pub name: Option, + /// Base64 encoded name of this asset, as supplied by the creator. + #[serde_as(as = "Option")] + #[serde(rename = "name-b64", skip_serializing_if = "Option::is_none")] + pub name_b64: Option>, + /// \[r\] Address of account holding reserve (non-minted) units of this asset. + #[serde(rename = "reserve", skip_serializing_if = "Option::is_none")] + pub reserve: Option, + /// \[t\] The total number of units of this asset. + #[serde(rename = "total")] + pub total: u64, + /// \[un\] Name of a unit of this asset, as supplied by the creator. Included only when the name of a unit of this asset is composed of printable utf-8 characters. + #[serde(rename = "unit-name", skip_serializing_if = "Option::is_none")] + pub unit_name: Option, + /// Base64 encoded name of a unit of this asset, as supplied by the creator. + #[serde_as(as = "Option")] + #[serde(rename = "unit-name-b64", skip_serializing_if = "Option::is_none")] + pub unit_name_b64: Option>, + /// \[au\] URL where more information about the asset can be retrieved. Included only when the URL is composed of printable utf-8 characters. + #[serde(rename = "url", skip_serializing_if = "Option::is_none")] + pub url: Option, + /// Base64 encoded URL where more information about the asset can be retrieved. + #[serde_as(as = "Option")] + #[serde(rename = "url-b64", skip_serializing_if = "Option::is_none")] + pub url_b64: Option>, +} + +impl AlgorandMsgpack for AssetParams { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl AssetParams { + /// Constructor for AssetParams + pub fn new(creator: String, decimals: u32, total: u64) -> AssetParams { + AssetParams { + creator, + decimals, + total, + clawback: None, + default_frozen: None, + freeze: None, + manager: None, + metadata_hash: None, + name: None, + name_b64: None, + reserve: None, + unit_name: None, + unit_name_b64: None, + url: None, + url_b64: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/avm_key_value.rs b/crates/algod_client/src/models/avm_key_value.rs new file mode 100644 index 00000000..042c8814 --- /dev/null +++ b/crates/algod_client/src/models/avm_key_value.rs @@ -0,0 +1,48 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +use crate::models::AvmValue; + +/// Represents an AVM key-value pair in an application store. +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct AvmKeyValue { + #[serde_as(as = "serde_with::base64::Base64")] + #[serde(rename = "key")] + pub key: Vec, + #[serde(rename = "value")] + pub value: AvmValue, +} + +impl AlgorandMsgpack for AvmKeyValue { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl AvmKeyValue { + /// Constructor for AvmKeyValue + pub fn new(key: Vec, value: AvmValue) -> AvmKeyValue { + AvmKeyValue { key, value } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/avm_value.rs b/crates/algod_client/src/models/avm_value.rs new file mode 100644 index 00000000..f06d7e6d --- /dev/null +++ b/crates/algod_client/src/models/avm_value.rs @@ -0,0 +1,52 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +/// Represents an AVM value. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct AvmValue { + /// value type. Value `1` refers to **bytes**, value `2` refers to **uint64** + #[serde(rename = "type")] + pub r#type: u32, + /// bytes value. + #[serde(rename = "bytes", skip_serializing_if = "Option::is_none")] + pub bytes: Option, + /// uint value. + #[serde(rename = "uint", skip_serializing_if = "Option::is_none")] + pub uint: Option, +} + +impl AlgorandMsgpack for AvmValue { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl AvmValue { + /// Constructor for AvmValue + pub fn new(r#type: u32) -> AvmValue { + AvmValue { + r#type, + bytes: None, + uint: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/box_descriptor.rs b/crates/algod_client/src/models/box_descriptor.rs new file mode 100644 index 00000000..3b3eb1b9 --- /dev/null +++ b/crates/algod_client/src/models/box_descriptor.rs @@ -0,0 +1,30 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +/// Box descriptor describes a Box. +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct BoxDescriptor { + /// Base64 encoded box name + #[serde_as(as = "serde_with::base64::Base64")] + #[serde(rename = "name")] + pub name: Vec, +} + +impl BoxDescriptor { + /// Constructor for BoxDescriptor + pub fn new(name: Vec) -> BoxDescriptor { + BoxDescriptor { name } + } +} diff --git a/crates/algod_client/src/models/box_model.rs b/crates/algod_client/src/models/box_model.rs new file mode 100644 index 00000000..12e07ee1 --- /dev/null +++ b/crates/algod_client/src/models/box_model.rs @@ -0,0 +1,37 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +/// Box name and its content. +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct Box { + /// The round for which this information is relevant + #[serde(rename = "round")] + pub round: u64, + /// \[name\] box name, base64 encoded + #[serde_as(as = "serde_with::base64::Base64")] + #[serde(rename = "name")] + pub name: Vec, + /// \[value\] box value, base64 encoded. + #[serde_as(as = "serde_with::base64::Base64")] + #[serde(rename = "value")] + pub value: Vec, +} + +impl Box { + /// Constructor for Box + pub fn new(round: u64, name: Vec, value: Vec) -> Box { + Box { round, name, value } + } +} diff --git a/crates/algod_client/src/models/box_reference.rs b/crates/algod_client/src/models/box_reference.rs new file mode 100644 index 00000000..f096a16e --- /dev/null +++ b/crates/algod_client/src/models/box_reference.rs @@ -0,0 +1,48 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +/// References a box of an application. +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct BoxReference { + /// Application ID which this box belongs to + #[serde(rename = "app")] + pub app: u64, + /// Base64 encoded box name + #[serde_as(as = "serde_with::base64::Base64")] + #[serde(rename = "name")] + pub name: Vec, +} + +impl AlgorandMsgpack for BoxReference { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl BoxReference { + /// Constructor for BoxReference + pub fn new(app: u64, name: Vec) -> BoxReference { + BoxReference { app, name } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/build_version.rs b/crates/algod_client/src/models/build_version.rs new file mode 100644 index 00000000..84c3eebd --- /dev/null +++ b/crates/algod_client/src/models/build_version.rs @@ -0,0 +1,49 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct BuildVersion { + #[serde(rename = "branch")] + pub branch: String, + #[serde(rename = "build_number")] + pub build_number: u64, + #[serde(rename = "channel")] + pub channel: String, + #[serde(rename = "commit_hash")] + pub commit_hash: String, + #[serde(rename = "major")] + pub major: u64, + #[serde(rename = "minor")] + pub minor: u64, +} + +impl BuildVersion { + /// Constructor for BuildVersion + pub fn new( + branch: String, + build_number: u64, + channel: String, + commit_hash: String, + major: u64, + minor: u64, + ) -> BuildVersion { + BuildVersion { + branch, + build_number, + channel, + commit_hash, + major, + minor, + } + } +} diff --git a/crates/algod_client/src/models/debug_settings_prof.rs b/crates/algod_client/src/models/debug_settings_prof.rs new file mode 100644 index 00000000..a2c387cf --- /dev/null +++ b/crates/algod_client/src/models/debug_settings_prof.rs @@ -0,0 +1,30 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// algod mutex and blocking profiling state. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct DebugSettingsProf { + /// The rate of blocking events. The profiler aims to sample an average of one blocking event per rate nanoseconds spent blocked. To turn off profiling entirely, pass rate 0. + #[serde(rename = "block-rate", skip_serializing_if = "Option::is_none")] + pub block_rate: Option, + /// The rate of mutex events. On average 1/rate events are reported. To turn off profiling entirely, pass rate 0 + #[serde(rename = "mutex-rate", skip_serializing_if = "Option::is_none")] + pub mutex_rate: Option, +} + +impl DebugSettingsProf { + /// Default constructor for DebugSettingsProf + pub fn new() -> DebugSettingsProf { + DebugSettingsProf::default() + } +} diff --git a/crates/algod_client/src/models/dryrun_request.rs b/crates/algod_client/src/models/dryrun_request.rs new file mode 100644 index 00000000..b47b6a99 --- /dev/null +++ b/crates/algod_client/src/models/dryrun_request.rs @@ -0,0 +1,76 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::Account; +use crate::models::Application; +use crate::models::DryrunSource; + +/// Request data type for dryrun endpoint. Given the Transactions and simulated ledger state upload, run TEAL scripts and return debugging information. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct DryrunRequest { + #[serde(rename = "txns")] + pub txns: Vec, + #[serde(rename = "accounts")] + pub accounts: Vec, + #[serde(rename = "apps")] + pub apps: Vec, + /// ProtocolVersion specifies a specific version string to operate under, otherwise whatever the current protocol of the network this algod is running in. + #[serde(rename = "protocol-version")] + pub protocol_version: String, + /// Round is available to some TEAL scripts. Defaults to the current round on the network this algod is attached to. + #[serde(rename = "round")] + pub round: u64, + /// LatestTimestamp is available to some TEAL scripts. Defaults to the latest confirmed timestamp this algod is attached to. + #[serde(rename = "latest-timestamp")] + pub latest_timestamp: u64, + #[serde(rename = "sources")] + pub sources: Vec, +} + +impl AlgorandMsgpack for DryrunRequest { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl DryrunRequest { + /// Constructor for DryrunRequest + pub fn new( + txns: Vec, + accounts: Vec, + apps: Vec, + protocol_version: String, + round: u64, + latest_timestamp: u64, + sources: Vec, + ) -> DryrunRequest { + DryrunRequest { + txns, + accounts, + apps, + protocol_version, + round, + latest_timestamp, + sources, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/dryrun_source.rs b/crates/algod_client/src/models/dryrun_source.rs new file mode 100644 index 00000000..fbb35584 --- /dev/null +++ b/crates/algod_client/src/models/dryrun_source.rs @@ -0,0 +1,53 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +/// DryrunSource is TEAL source text that gets uploaded, compiled, and inserted into transactions or application state. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct DryrunSource { + /// FieldName is what kind of sources this is. If lsig then it goes into the transactions[this.TxnIndex].LogicSig. If approv or clearp it goes into the Approval Program or Clear State Program of application[this.AppIndex]. + #[serde(rename = "field-name")] + pub field_name: String, + #[serde(rename = "source")] + pub source: String, + #[serde(rename = "txn-index")] + pub txn_index: u64, + #[serde(rename = "app-index")] + pub app_index: u64, +} + +impl AlgorandMsgpack for DryrunSource { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl DryrunSource { + /// Constructor for DryrunSource + pub fn new(field_name: String, source: String, txn_index: u64, app_index: u64) -> DryrunSource { + DryrunSource { + field_name, + source, + txn_index, + app_index, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/dryrun_state.rs b/crates/algod_client/src/models/dryrun_state.rs new file mode 100644 index 00000000..e8c7e0e6 --- /dev/null +++ b/crates/algod_client/src/models/dryrun_state.rs @@ -0,0 +1,45 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +use crate::models::TealValue; + +/// Stores the TEAL eval step data +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct DryrunState { + /// Line number + #[serde(rename = "line")] + pub line: u64, + /// Program counter + #[serde(rename = "pc")] + pub pc: u64, + #[serde(rename = "stack")] + pub stack: Vec, + #[serde(rename = "scratch", skip_serializing_if = "Option::is_none")] + pub scratch: Option>, + /// Evaluation error if any + #[serde(rename = "error", skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +impl DryrunState { + /// Constructor for DryrunState + pub fn new(line: u64, pc: u64, stack: Vec) -> DryrunState { + DryrunState { + line, + pc, + stack, + scratch: None, + error: None, + } + } +} diff --git a/crates/algod_client/src/models/dryrun_txn_result.rs b/crates/algod_client/src/models/dryrun_txn_result.rs new file mode 100644 index 00000000..97b5c9cc --- /dev/null +++ b/crates/algod_client/src/models/dryrun_txn_result.rs @@ -0,0 +1,69 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +use crate::models::AccountStateDelta; +use crate::models::DryrunState; +use crate::models::StateDelta; + +/// DryrunTxnResult contains any LogicSig or ApplicationCall program debug information and state updates from a dryrun. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct DryrunTxnResult { + /// Disassembled program line by line. + #[serde(rename = "disassembly")] + pub disassembly: Vec, + /// Disassembled lsig program line by line. + #[serde( + rename = "logic-sig-disassembly", + skip_serializing_if = "Option::is_none" + )] + pub logic_sig_disassembly: Option>, + #[serde(rename = "logic-sig-trace", skip_serializing_if = "Option::is_none")] + pub logic_sig_trace: Option>, + #[serde(rename = "logic-sig-messages", skip_serializing_if = "Option::is_none")] + pub logic_sig_messages: Option>, + #[serde(rename = "app-call-trace", skip_serializing_if = "Option::is_none")] + pub app_call_trace: Option>, + #[serde(rename = "app-call-messages", skip_serializing_if = "Option::is_none")] + pub app_call_messages: Option>, + #[serde(rename = "global-delta", skip_serializing_if = "Option::is_none")] + pub global_delta: Option, + #[serde(rename = "local-deltas", skip_serializing_if = "Option::is_none")] + pub local_deltas: Option>, + #[serde(rename = "logs", skip_serializing_if = "Option::is_none")] + pub logs: Option>, + /// Budget added during execution of app call transaction. + #[serde(rename = "budget-added", skip_serializing_if = "Option::is_none")] + pub budget_added: Option, + /// Budget consumed during execution of app call transaction. + #[serde(rename = "budget-consumed", skip_serializing_if = "Option::is_none")] + pub budget_consumed: Option, +} + +impl DryrunTxnResult { + /// Constructor for DryrunTxnResult + pub fn new(disassembly: Vec) -> DryrunTxnResult { + DryrunTxnResult { + disassembly, + logic_sig_disassembly: None, + logic_sig_trace: None, + logic_sig_messages: None, + app_call_trace: None, + app_call_messages: None, + global_delta: None, + local_deltas: None, + logs: None, + budget_added: None, + budget_consumed: None, + } + } +} diff --git a/crates/algod_client/src/models/error_response.rs b/crates/algod_client/src/models/error_response.rs new file mode 100644 index 00000000..654700ee --- /dev/null +++ b/crates/algod_client/src/models/error_response.rs @@ -0,0 +1,46 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +/// An error response with optional data field. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct ErrorResponse { + #[serde(rename = "data", skip_serializing_if = "Option::is_none")] + pub data: Option, + #[serde(rename = "message")] + pub message: String, +} + +impl AlgorandMsgpack for ErrorResponse { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl ErrorResponse { + /// Constructor for ErrorResponse + pub fn new(message: String) -> ErrorResponse { + ErrorResponse { + message, + data: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/eval_delta.rs b/crates/algod_client/src/models/eval_delta.rs new file mode 100644 index 00000000..1d0f21c6 --- /dev/null +++ b/crates/algod_client/src/models/eval_delta.rs @@ -0,0 +1,52 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +/// Represents a TEAL value delta. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct EvalDelta { + /// \[at\] delta action. + #[serde(rename = "action")] + pub action: u32, + /// \[bs\] bytes value. + #[serde(rename = "bytes", skip_serializing_if = "Option::is_none")] + pub bytes: Option, + /// \[ui\] uint value. + #[serde(rename = "uint", skip_serializing_if = "Option::is_none")] + pub uint: Option, +} + +impl AlgorandMsgpack for EvalDelta { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl EvalDelta { + /// Constructor for EvalDelta + pub fn new(action: u32) -> EvalDelta { + EvalDelta { + action, + bytes: None, + uint: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/eval_delta_key_value.rs b/crates/algod_client/src/models/eval_delta_key_value.rs new file mode 100644 index 00000000..9adf44f5 --- /dev/null +++ b/crates/algod_client/src/models/eval_delta_key_value.rs @@ -0,0 +1,45 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::EvalDelta; + +/// Key-value pairs for StateDelta. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct EvalDeltaKeyValue { + #[serde(rename = "key")] + pub key: String, + #[serde(rename = "value")] + pub value: EvalDelta, +} + +impl AlgorandMsgpack for EvalDeltaKeyValue { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl EvalDeltaKeyValue { + /// Constructor for EvalDeltaKeyValue + pub fn new(key: String, value: EvalDelta) -> EvalDeltaKeyValue { + EvalDeltaKeyValue { key, value } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/genesis.rs b/crates/algod_client/src/models/genesis.rs new file mode 100644 index 00000000..ba0be69b --- /dev/null +++ b/crates/algod_client/src/models/genesis.rs @@ -0,0 +1,61 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +use crate::models::GenesisAllocation; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct Genesis { + #[serde(rename = "alloc")] + pub alloc: Vec, + #[serde(rename = "comment", skip_serializing_if = "Option::is_none")] + pub comment: Option, + #[serde(rename = "devmode", skip_serializing_if = "Option::is_none")] + pub devmode: Option, + #[serde(rename = "fees")] + pub fees: String, + #[serde(rename = "id")] + pub id: String, + #[serde(rename = "network")] + pub network: String, + #[serde(rename = "proto")] + pub proto: String, + #[serde(rename = "rwd")] + pub rwd: String, + #[serde(rename = "timestamp")] + pub timestamp: u64, +} + +impl Genesis { + /// Constructor for Genesis + pub fn new( + alloc: Vec, + fees: String, + id: String, + network: String, + proto: String, + rwd: String, + timestamp: u64, + ) -> Genesis { + Genesis { + alloc, + fees, + id, + network, + proto, + rwd, + timestamp, + comment: None, + devmode: None, + } + } +} diff --git a/crates/algod_client/src/models/genesis_allocation.rs b/crates/algod_client/src/models/genesis_allocation.rs new file mode 100644 index 00000000..28dde43c --- /dev/null +++ b/crates/algod_client/src/models/genesis_allocation.rs @@ -0,0 +1,33 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GenesisAllocation { + #[serde(rename = "addr")] + pub addr: String, + #[serde(rename = "comment")] + pub comment: String, + #[serde(rename = "state")] + pub state: serde_json::Value, +} + +impl GenesisAllocation { + /// Constructor for GenesisAllocation + pub fn new(addr: String, comment: String, state: serde_json::Value) -> GenesisAllocation { + GenesisAllocation { + addr, + comment, + state, + } + } +} diff --git a/crates/algod_client/src/models/get_application_boxes.rs b/crates/algod_client/src/models/get_application_boxes.rs new file mode 100644 index 00000000..41663113 --- /dev/null +++ b/crates/algod_client/src/models/get_application_boxes.rs @@ -0,0 +1,28 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +use crate::models::BoxDescriptor; + +/// Box names of an application +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetApplicationBoxes { + #[serde(rename = "boxes")] + pub boxes: Vec, +} + +impl GetApplicationBoxes { + /// Constructor for GetApplicationBoxes + pub fn new(boxes: Vec) -> GetApplicationBoxes { + GetApplicationBoxes { boxes } + } +} diff --git a/crates/algod_client/src/models/get_block.rs b/crates/algod_client/src/models/get_block.rs new file mode 100644 index 00000000..e04bc116 --- /dev/null +++ b/crates/algod_client/src/models/get_block.rs @@ -0,0 +1,45 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +/// Encoded block object. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetBlock { + /// Block header data. + #[serde(rename = "block")] + pub block: serde_json::Value, + /// Optional certificate object. This is only included when the format is set to message pack. + #[serde(rename = "cert", skip_serializing_if = "Option::is_none")] + pub cert: Option, +} + +impl AlgorandMsgpack for GetBlock { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl GetBlock { + /// Constructor for GetBlock + pub fn new(block: serde_json::Value) -> GetBlock { + GetBlock { block, cert: None } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/get_block_hash.rs b/crates/algod_client/src/models/get_block_hash.rs new file mode 100644 index 00000000..2c42e65d --- /dev/null +++ b/crates/algod_client/src/models/get_block_hash.rs @@ -0,0 +1,27 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// Hash of a block header. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetBlockHash { + /// Block header hash. + #[serde(rename = "blockHash")] + pub block_hash: String, +} + +impl GetBlockHash { + /// Constructor for GetBlockHash + pub fn new(block_hash: String) -> GetBlockHash { + GetBlockHash { block_hash } + } +} diff --git a/crates/algod_client/src/models/get_block_logs.rs b/crates/algod_client/src/models/get_block_logs.rs new file mode 100644 index 00000000..68ec3ad5 --- /dev/null +++ b/crates/algod_client/src/models/get_block_logs.rs @@ -0,0 +1,28 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +use crate::models::AppCallLogs; + +/// All logs emitted in the given round. Each app call, whether top-level or inner, that contains logs results in a separate AppCallLogs object. Therefore there may be multiple AppCallLogs with the same application ID and outer transaction ID in the event of multiple inner app calls to the same app. App calls with no logs are not included in the response. AppCallLogs are returned in the same order that their corresponding app call appeared in the block (pre-order traversal of inner app calls) +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetBlockLogs { + #[serde(rename = "logs")] + pub logs: Vec, +} + +impl GetBlockLogs { + /// Constructor for GetBlockLogs + pub fn new(logs: Vec) -> GetBlockLogs { + GetBlockLogs { logs } + } +} diff --git a/crates/algod_client/src/models/get_block_time_stamp_offset.rs b/crates/algod_client/src/models/get_block_time_stamp_offset.rs new file mode 100644 index 00000000..46756300 --- /dev/null +++ b/crates/algod_client/src/models/get_block_time_stamp_offset.rs @@ -0,0 +1,27 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// Response containing the timestamp offset in seconds +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetBlockTimeStampOffset { + /// Timestamp offset in seconds. + #[serde(rename = "offset")] + pub offset: u64, +} + +impl GetBlockTimeStampOffset { + /// Constructor for GetBlockTimeStampOffset + pub fn new(offset: u64) -> GetBlockTimeStampOffset { + GetBlockTimeStampOffset { offset } + } +} diff --git a/crates/algod_client/src/models/get_block_txids.rs b/crates/algod_client/src/models/get_block_txids.rs new file mode 100644 index 00000000..efa1a66d --- /dev/null +++ b/crates/algod_client/src/models/get_block_txids.rs @@ -0,0 +1,27 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// Top level transaction IDs in a block. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetBlockTxids { + /// Block transaction IDs. + #[serde(rename = "blockTxids")] + pub block_txids: Vec, +} + +impl GetBlockTxids { + /// Constructor for GetBlockTxids + pub fn new(block_txids: Vec) -> GetBlockTxids { + GetBlockTxids { block_txids } + } +} diff --git a/crates/algod_client/src/models/get_pending_transactions.rs b/crates/algod_client/src/models/get_pending_transactions.rs new file mode 100644 index 00000000..cc406f5c --- /dev/null +++ b/crates/algod_client/src/models/get_pending_transactions.rs @@ -0,0 +1,51 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +/// PendingTransactions is an array of signed transactions exactly as they were submitted. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetPendingTransactions { + /// An array of signed transaction objects. + #[serde(rename = "top-transactions")] + pub top_transactions: Vec, + /// Total number of transactions in the pool. + #[serde(rename = "total-transactions")] + pub total_transactions: u64, +} + +impl AlgorandMsgpack for GetPendingTransactions { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl GetPendingTransactions { + /// Constructor for GetPendingTransactions + pub fn new( + top_transactions: Vec, + total_transactions: u64, + ) -> GetPendingTransactions { + GetPendingTransactions { + top_transactions, + total_transactions, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/get_pending_transactions_by_address.rs b/crates/algod_client/src/models/get_pending_transactions_by_address.rs new file mode 100644 index 00000000..b9b7bf60 --- /dev/null +++ b/crates/algod_client/src/models/get_pending_transactions_by_address.rs @@ -0,0 +1,51 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +/// PendingTransactions is an array of signed transactions exactly as they were submitted. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetPendingTransactionsByAddress { + /// An array of signed transaction objects. + #[serde(rename = "top-transactions")] + pub top_transactions: Vec, + /// Total number of transactions in the pool. + #[serde(rename = "total-transactions")] + pub total_transactions: u64, +} + +impl AlgorandMsgpack for GetPendingTransactionsByAddress { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl GetPendingTransactionsByAddress { + /// Constructor for GetPendingTransactionsByAddress + pub fn new( + top_transactions: Vec, + total_transactions: u64, + ) -> GetPendingTransactionsByAddress { + GetPendingTransactionsByAddress { + top_transactions, + total_transactions, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/get_status.rs b/crates/algod_client/src/models/get_status.rs new file mode 100644 index 00000000..81afcd35 --- /dev/null +++ b/crates/algod_client/src/models/get_status.rs @@ -0,0 +1,171 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// NodeStatus contains the information about a node status +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetStatus { + /// CatchupTime in nanoseconds + #[serde(rename = "catchup-time")] + pub catchup_time: u64, + /// LastRound indicates the last round seen + #[serde(rename = "last-round")] + pub last_round: u64, + /// LastVersion indicates the last consensus version supported + #[serde(rename = "last-version")] + pub last_version: String, + /// NextVersion of consensus protocol to use + #[serde(rename = "next-version")] + pub next_version: String, + /// NextVersionRound is the round at which the next consensus version will apply + #[serde(rename = "next-version-round")] + pub next_version_round: u64, + /// NextVersionSupported indicates whether the next consensus version is supported by this node + #[serde(rename = "next-version-supported")] + pub next_version_supported: bool, + /// StoppedAtUnsupportedRound indicates that the node does not support the new rounds and has stopped making progress + #[serde(rename = "stopped-at-unsupported-round")] + pub stopped_at_unsupported_round: bool, + /// TimeSinceLastRound in nanoseconds + #[serde(rename = "time-since-last-round")] + pub time_since_last_round: u64, + /// The last catchpoint seen by the node + #[serde(rename = "last-catchpoint", skip_serializing_if = "Option::is_none")] + pub last_catchpoint: Option, + /// The current catchpoint that is being caught up to + #[serde(rename = "catchpoint", skip_serializing_if = "Option::is_none")] + pub catchpoint: Option, + /// The total number of accounts included in the current catchpoint + #[serde( + rename = "catchpoint-total-accounts", + skip_serializing_if = "Option::is_none" + )] + pub catchpoint_total_accounts: Option, + /// The number of accounts from the current catchpoint that have been processed so far as part of the catchup + #[serde( + rename = "catchpoint-processed-accounts", + skip_serializing_if = "Option::is_none" + )] + pub catchpoint_processed_accounts: Option, + /// The number of accounts from the current catchpoint that have been verified so far as part of the catchup + #[serde( + rename = "catchpoint-verified-accounts", + skip_serializing_if = "Option::is_none" + )] + pub catchpoint_verified_accounts: Option, + /// The total number of key-values (KVs) included in the current catchpoint + #[serde( + rename = "catchpoint-total-kvs", + skip_serializing_if = "Option::is_none" + )] + pub catchpoint_total_kvs: Option, + /// The number of key-values (KVs) from the current catchpoint that have been processed so far as part of the catchup + #[serde( + rename = "catchpoint-processed-kvs", + skip_serializing_if = "Option::is_none" + )] + pub catchpoint_processed_kvs: Option, + /// The number of key-values (KVs) from the current catchpoint that have been verified so far as part of the catchup + #[serde( + rename = "catchpoint-verified-kvs", + skip_serializing_if = "Option::is_none" + )] + pub catchpoint_verified_kvs: Option, + /// The total number of blocks that are required to complete the current catchpoint catchup + #[serde( + rename = "catchpoint-total-blocks", + skip_serializing_if = "Option::is_none" + )] + pub catchpoint_total_blocks: Option, + /// The number of blocks that have already been obtained by the node as part of the catchup + #[serde( + rename = "catchpoint-acquired-blocks", + skip_serializing_if = "Option::is_none" + )] + pub catchpoint_acquired_blocks: Option, + /// Upgrade delay + #[serde(rename = "upgrade-delay", skip_serializing_if = "Option::is_none")] + pub upgrade_delay: Option, + /// This node's upgrade vote + #[serde(rename = "upgrade-node-vote", skip_serializing_if = "Option::is_none")] + pub upgrade_node_vote: Option, + /// Yes votes required for consensus upgrade + #[serde( + rename = "upgrade-votes-required", + skip_serializing_if = "Option::is_none" + )] + pub upgrade_votes_required: Option, + /// Total votes cast for consensus upgrade + #[serde(rename = "upgrade-votes", skip_serializing_if = "Option::is_none")] + pub upgrade_votes: Option, + /// Yes votes cast for consensus upgrade + #[serde(rename = "upgrade-yes-votes", skip_serializing_if = "Option::is_none")] + pub upgrade_yes_votes: Option, + /// No votes cast for consensus upgrade + #[serde(rename = "upgrade-no-votes", skip_serializing_if = "Option::is_none")] + pub upgrade_no_votes: Option, + /// Next protocol round + #[serde( + rename = "upgrade-next-protocol-vote-before", + skip_serializing_if = "Option::is_none" + )] + pub upgrade_next_protocol_vote_before: Option, + /// Total voting rounds for current upgrade + #[serde( + rename = "upgrade-vote-rounds", + skip_serializing_if = "Option::is_none" + )] + pub upgrade_vote_rounds: Option, +} + +impl GetStatus { + /// Constructor for GetStatus + pub fn new( + catchup_time: u64, + last_round: u64, + last_version: String, + next_version: String, + next_version_round: u64, + next_version_supported: bool, + stopped_at_unsupported_round: bool, + time_since_last_round: u64, + ) -> GetStatus { + GetStatus { + catchup_time, + last_round, + last_version, + next_version, + next_version_round, + next_version_supported, + stopped_at_unsupported_round, + time_since_last_round, + last_catchpoint: None, + catchpoint: None, + catchpoint_total_accounts: None, + catchpoint_processed_accounts: None, + catchpoint_verified_accounts: None, + catchpoint_total_kvs: None, + catchpoint_processed_kvs: None, + catchpoint_verified_kvs: None, + catchpoint_total_blocks: None, + catchpoint_acquired_blocks: None, + upgrade_delay: None, + upgrade_node_vote: None, + upgrade_votes_required: None, + upgrade_votes: None, + upgrade_yes_votes: None, + upgrade_no_votes: None, + upgrade_next_protocol_vote_before: None, + upgrade_vote_rounds: None, + } + } +} diff --git a/crates/algod_client/src/models/get_supply.rs b/crates/algod_client/src/models/get_supply.rs new file mode 100644 index 00000000..2547c9a4 --- /dev/null +++ b/crates/algod_client/src/models/get_supply.rs @@ -0,0 +1,37 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// Supply represents the current supply of MicroAlgos in the system +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetSupply { + /// Round + #[serde(rename = "current_round")] + pub current_round: u64, + /// OnlineMoney + #[serde(rename = "online-money")] + pub online_money: u64, + /// TotalMoney + #[serde(rename = "total-money")] + pub total_money: u64, +} + +impl GetSupply { + /// Constructor for GetSupply + pub fn new(current_round: u64, online_money: u64, total_money: u64) -> GetSupply { + GetSupply { + current_round, + online_money, + total_money, + } + } +} diff --git a/crates/algod_client/src/models/get_sync_round.rs b/crates/algod_client/src/models/get_sync_round.rs new file mode 100644 index 00000000..c77dfd46 --- /dev/null +++ b/crates/algod_client/src/models/get_sync_round.rs @@ -0,0 +1,27 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// Response containing the ledger's minimum sync round +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetSyncRound { + /// The minimum sync round for the ledger. + #[serde(rename = "round")] + pub round: u64, +} + +impl GetSyncRound { + /// Constructor for GetSyncRound + pub fn new(round: u64) -> GetSyncRound { + GetSyncRound { round } + } +} diff --git a/crates/algod_client/src/models/get_transaction_group_ledger_state_deltas_for_round.rs b/crates/algod_client/src/models/get_transaction_group_ledger_state_deltas_for_round.rs new file mode 100644 index 00000000..db3c3194 --- /dev/null +++ b/crates/algod_client/src/models/get_transaction_group_ledger_state_deltas_for_round.rs @@ -0,0 +1,45 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::LedgerStateDeltaForTransactionGroup; + +/// Response containing all ledger state deltas for transaction groups, with their associated Ids, in a single round. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetTransactionGroupLedgerStateDeltasForRound { + #[serde(rename = "Deltas")] + pub deltas: Vec, +} + +impl AlgorandMsgpack for GetTransactionGroupLedgerStateDeltasForRound { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl GetTransactionGroupLedgerStateDeltasForRound { + /// Constructor for GetTransactionGroupLedgerStateDeltasForRound + pub fn new( + deltas: Vec, + ) -> GetTransactionGroupLedgerStateDeltasForRound { + GetTransactionGroupLedgerStateDeltasForRound { deltas } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/get_transaction_proof.rs b/crates/algod_client/src/models/get_transaction_proof.rs new file mode 100644 index 00000000..d2323c23 --- /dev/null +++ b/crates/algod_client/src/models/get_transaction_proof.rs @@ -0,0 +1,57 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +/// Proof of transaction in a block. +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct GetTransactionProof { + /// Proof of transaction membership. + #[serde_as(as = "serde_with::base64::Base64")] + #[serde(rename = "proof")] + pub proof: Vec, + /// Hash of SignedTxnInBlock for verifying proof. + #[serde_as(as = "serde_with::base64::Base64")] + #[serde(rename = "stibhash")] + pub stibhash: Vec, + /// Represents the depth of the tree that is being proven, i.e. the number of edges from a leaf to the root. + #[serde(rename = "treedepth")] + pub treedepth: u64, + /// Index of the transaction in the block's payset. + #[serde(rename = "idx")] + pub idx: u64, + /// The type of hash function used to create the proof, must be one of: + /// * sha512_256 + /// * sha256 + #[serde(rename = "hashtype")] + pub hashtype: String, +} + +impl GetTransactionProof { + /// Constructor for GetTransactionProof + pub fn new( + proof: Vec, + stibhash: Vec, + treedepth: u64, + idx: u64, + hashtype: String, + ) -> GetTransactionProof { + GetTransactionProof { + proof, + stibhash, + treedepth, + idx, + hashtype, + } + } +} diff --git a/crates/algod_client/src/models/kv_delta.rs b/crates/algod_client/src/models/kv_delta.rs new file mode 100644 index 00000000..b962dcf2 --- /dev/null +++ b/crates/algod_client/src/models/kv_delta.rs @@ -0,0 +1,34 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +/// A single Delta containing the key, the previous value and the current value for a single round. +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct KvDelta { + /// The key, base64 encoded. + #[serde_as(as = "Option")] + #[serde(rename = "key", skip_serializing_if = "Option::is_none")] + pub key: Option>, + /// The new value of the KV store entry, base64 encoded. + #[serde_as(as = "Option")] + #[serde(rename = "value", skip_serializing_if = "Option::is_none")] + pub value: Option>, +} + +impl KvDelta { + /// Default constructor for KvDelta + pub fn new() -> KvDelta { + KvDelta::default() + } +} diff --git a/crates/algod_client/src/models/ledger_state_delta.rs b/crates/algod_client/src/models/ledger_state_delta.rs new file mode 100644 index 00000000..affdd8b4 --- /dev/null +++ b/crates/algod_client/src/models/ledger_state_delta.rs @@ -0,0 +1,38 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +/// Ledger StateDelta object +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct LedgerStateDelta {} + +impl AlgorandMsgpack for LedgerStateDelta { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl LedgerStateDelta { + /// Default constructor for LedgerStateDelta + pub fn new() -> LedgerStateDelta { + LedgerStateDelta::default() + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/ledger_state_delta_for_transaction_group.rs b/crates/algod_client/src/models/ledger_state_delta_for_transaction_group.rs new file mode 100644 index 00000000..42920a18 --- /dev/null +++ b/crates/algod_client/src/models/ledger_state_delta_for_transaction_group.rs @@ -0,0 +1,45 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::LedgerStateDelta; + +/// Contains a ledger delta for a single transaction group +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct LedgerStateDeltaForTransactionGroup { + #[serde(rename = "Delta")] + pub delta: LedgerStateDelta, + #[serde(rename = "Ids")] + pub ids: Vec, +} + +impl AlgorandMsgpack for LedgerStateDeltaForTransactionGroup { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl LedgerStateDeltaForTransactionGroup { + /// Constructor for LedgerStateDeltaForTransactionGroup + pub fn new(delta: LedgerStateDelta, ids: Vec) -> LedgerStateDeltaForTransactionGroup { + LedgerStateDeltaForTransactionGroup { delta, ids } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/light_block_header_proof.rs b/crates/algod_client/src/models/light_block_header_proof.rs new file mode 100644 index 00000000..3f76bc7f --- /dev/null +++ b/crates/algod_client/src/models/light_block_header_proof.rs @@ -0,0 +1,40 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +/// Proof of membership and position of a light block header. +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct LightBlockHeaderProof { + /// The index of the light block header in the vector commitment tree + #[serde(rename = "index")] + pub index: u64, + /// Represents the depth of the tree that is being proven, i.e. the number of edges from a leaf to the root. + #[serde(rename = "treedepth")] + pub treedepth: u64, + /// The encoded proof. + #[serde_as(as = "serde_with::base64::Base64")] + #[serde(rename = "proof")] + pub proof: Vec, +} + +impl LightBlockHeaderProof { + /// Constructor for LightBlockHeaderProof + pub fn new(index: u64, treedepth: u64, proof: Vec) -> LightBlockHeaderProof { + LightBlockHeaderProof { + index, + treedepth, + proof, + } + } +} diff --git a/crates/algod_client/src/models/mod.rs b/crates/algod_client/src/models/mod.rs new file mode 100644 index 00000000..001278d3 --- /dev/null +++ b/crates/algod_client/src/models/mod.rs @@ -0,0 +1,176 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +pub mod genesis_allocation; +pub use self::genesis_allocation::GenesisAllocation; +pub mod genesis; +pub use self::genesis::Genesis; +pub mod ledger_state_delta; +pub use self::ledger_state_delta::LedgerStateDelta; +pub mod ledger_state_delta_for_transaction_group; +pub use self::ledger_state_delta_for_transaction_group::LedgerStateDeltaForTransactionGroup; +pub mod account; +pub use self::account::Account; +pub mod account_asset_holding; +pub use self::account_asset_holding::AccountAssetHolding; +pub mod account_participation; +pub use self::account_participation::AccountParticipation; +pub mod asset; +pub use self::asset::Asset; +pub mod asset_holding; +pub use self::asset_holding::AssetHolding; +pub mod asset_params; +pub use self::asset_params::AssetParams; +pub mod asset_holding_reference; +pub use self::asset_holding_reference::AssetHoldingReference; +pub mod application_local_reference; +pub use self::application_local_reference::ApplicationLocalReference; +pub mod application_state_schema; +pub use self::application_state_schema::ApplicationStateSchema; +pub mod application_local_state; +pub use self::application_local_state::ApplicationLocalState; +pub mod participation_key; +pub use self::participation_key::ParticipationKey; +pub mod teal_key_value_store; +pub use self::teal_key_value_store::TealKeyValueStore; +pub mod teal_key_value; +pub use self::teal_key_value::TealKeyValue; +pub mod teal_value; +pub use self::teal_value::TealValue; +pub mod avm_value; +pub use self::avm_value::AvmValue; +pub mod avm_key_value; +pub use self::avm_key_value::AvmKeyValue; +pub mod state_delta; +pub use self::state_delta::StateDelta; +pub mod account_state_delta; +pub use self::account_state_delta::AccountStateDelta; +pub mod eval_delta_key_value; +pub use self::eval_delta_key_value::EvalDeltaKeyValue; +pub mod eval_delta; +pub use self::eval_delta::EvalDelta; +pub mod application; +pub use self::application::Application; +pub mod application_params; +pub use self::application_params::ApplicationParams; +pub mod dryrun_state; +pub use self::dryrun_state::DryrunState; +pub mod dryrun_txn_result; +pub use self::dryrun_txn_result::DryrunTxnResult; +pub mod error_response; +pub use self::error_response::ErrorResponse; +pub mod dryrun_request; +pub use self::dryrun_request::DryrunRequest; +pub mod dryrun_source; +pub use self::dryrun_source::DryrunSource; +pub mod simulate_request; +pub use self::simulate_request::SimulateRequest; +pub mod simulate_request_transaction_group; +pub use self::simulate_request_transaction_group::SimulateRequestTransactionGroup; +pub mod simulate_trace_config; +pub use self::simulate_trace_config::SimulateTraceConfig; +pub mod box_model; +pub use self::box_model::Box; +pub mod box_descriptor; +pub use self::box_descriptor::BoxDescriptor; +pub mod box_reference; +pub use self::box_reference::BoxReference; +pub mod kv_delta; +pub use self::kv_delta::KvDelta; +pub mod version; +pub use self::version::Version; +pub mod debug_settings_prof; +pub use self::debug_settings_prof::DebugSettingsProf; +pub mod build_version; +pub use self::build_version::BuildVersion; +pub mod pending_transaction_response; +pub use self::pending_transaction_response::PendingTransactionResponse; +pub mod simulate_transaction_group_result; +pub use self::simulate_transaction_group_result::SimulateTransactionGroupResult; +pub mod simulate_transaction_result; +pub use self::simulate_transaction_result::SimulateTransactionResult; +pub mod state_proof; +pub use self::state_proof::StateProof; +pub mod light_block_header_proof; +pub use self::light_block_header_proof::LightBlockHeaderProof; +pub mod state_proof_message; +pub use self::state_proof_message::StateProofMessage; +pub mod simulation_eval_overrides; +pub use self::simulation_eval_overrides::SimulationEvalOverrides; +pub mod scratch_change; +pub use self::scratch_change::ScratchChange; +pub mod application_state_operation; +pub use self::application_state_operation::ApplicationStateOperation; +pub mod application_kv_storage; +pub use self::application_kv_storage::ApplicationKvStorage; +pub mod application_initial_states; +pub use self::application_initial_states::ApplicationInitialStates; +pub mod simulation_opcode_trace_unit; +pub use self::simulation_opcode_trace_unit::SimulationOpcodeTraceUnit; +pub mod simulation_transaction_exec_trace; +pub use self::simulation_transaction_exec_trace::SimulationTransactionExecTrace; +pub mod simulate_unnamed_resources_accessed; +pub use self::simulate_unnamed_resources_accessed::SimulateUnnamedResourcesAccessed; +pub mod simulate_initial_states; +pub use self::simulate_initial_states::SimulateInitialStates; +pub mod app_call_logs; +pub use self::app_call_logs::AppCallLogs; +pub mod account_asset_information; +pub use self::account_asset_information::AccountAssetInformation; +pub mod account_assets_information; +pub use self::account_assets_information::AccountAssetsInformation; +pub mod account_application_information; +pub use self::account_application_information::AccountApplicationInformation; +pub mod get_pending_transactions_by_address; +pub use self::get_pending_transactions_by_address::GetPendingTransactionsByAddress; +pub mod get_block; +pub use self::get_block::GetBlock; +pub mod get_block_txids; +pub use self::get_block_txids::GetBlockTxids; +pub mod get_block_hash; +pub use self::get_block_hash::GetBlockHash; +pub mod get_transaction_proof; +pub use self::get_transaction_proof::GetTransactionProof; +pub mod get_block_logs; +pub use self::get_block_logs::GetBlockLogs; +pub mod get_supply; +pub use self::get_supply::GetSupply; +pub mod add_participation_key; +pub use self::add_participation_key::AddParticipationKey; +pub mod get_status; +pub use self::get_status::GetStatus; +pub mod wait_for_block; +pub use self::wait_for_block::WaitForBlock; +pub mod raw_transaction; +pub use self::raw_transaction::RawTransaction; +pub mod simulate_transaction; +pub use self::simulate_transaction::SimulateTransaction; +pub mod transaction_params; +pub use self::transaction_params::TransactionParams; +pub mod get_pending_transactions; +pub use self::get_pending_transactions::GetPendingTransactions; +pub mod get_transaction_group_ledger_state_deltas_for_round; +pub use self::get_transaction_group_ledger_state_deltas_for_round::GetTransactionGroupLedgerStateDeltasForRound; +pub mod get_application_boxes; +pub use self::get_application_boxes::GetApplicationBoxes; +pub mod get_sync_round; +pub use self::get_sync_round::GetSyncRound; +pub mod teal_compile; +pub use self::teal_compile::TealCompile; +pub mod teal_disassemble; +pub use self::teal_disassemble::TealDisassemble; +pub mod start_catchup; +pub use self::start_catchup::StartCatchup; +pub mod abort_catchup; +pub use self::abort_catchup::AbortCatchup; +pub mod teal_dryrun; +pub use self::teal_dryrun::TealDryrun; +pub mod get_block_time_stamp_offset; +pub use self::get_block_time_stamp_offset::GetBlockTimeStampOffset; diff --git a/crates/algod_client/src/models/participation_key.rs b/crates/algod_client/src/models/participation_key.rs new file mode 100644 index 00000000..ddafc246 --- /dev/null +++ b/crates/algod_client/src/models/participation_key.rs @@ -0,0 +1,67 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +use crate::models::AccountParticipation; + +/// Represents a participation key used by the node. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct ParticipationKey { + /// The key's ParticipationID. + #[serde(rename = "id")] + pub id: String, + /// Address the key was generated for. + #[serde(rename = "address")] + pub address: String, + /// When registered, this is the first round it may be used. + #[serde( + rename = "effective-first-valid", + skip_serializing_if = "Option::is_none" + )] + pub effective_first_valid: Option, + /// When registered, this is the last round it may be used. + #[serde( + rename = "effective-last-valid", + skip_serializing_if = "Option::is_none" + )] + pub effective_last_valid: Option, + /// Round when this key was last used to vote. + #[serde(rename = "last-vote", skip_serializing_if = "Option::is_none")] + pub last_vote: Option, + /// Round when this key was last used to propose a block. + #[serde( + rename = "last-block-proposal", + skip_serializing_if = "Option::is_none" + )] + pub last_block_proposal: Option, + /// Round when this key was last used to generate a state proof. + #[serde(rename = "last-state-proof", skip_serializing_if = "Option::is_none")] + pub last_state_proof: Option, + #[serde(rename = "key")] + pub key: AccountParticipation, +} + +impl ParticipationKey { + /// Constructor for ParticipationKey + pub fn new(id: String, address: String, key: AccountParticipation) -> ParticipationKey { + ParticipationKey { + id, + address, + key, + effective_first_valid: None, + effective_last_valid: None, + last_vote: None, + last_block_proposal: None, + last_state_proof: None, + } + } +} diff --git a/crates/algod_client/src/models/pending_transaction_response.rs b/crates/algod_client/src/models/pending_transaction_response.rs new file mode 100644 index 00000000..9206976b --- /dev/null +++ b/crates/algod_client/src/models/pending_transaction_response.rs @@ -0,0 +1,144 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::AccountStateDelta; +use crate::models::StateDelta; + +/// Details about a pending transaction. If the transaction was recently confirmed, includes confirmation details like the round and reward details. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct PendingTransactionResponse { + /// The asset index if the transaction was found and it created an asset. + #[serde(rename = "asset-index", skip_serializing_if = "Option::is_none")] + pub asset_index: Option, + /// The application index if the transaction was found and it created an application. + #[serde(rename = "application-index", skip_serializing_if = "Option::is_none")] + pub application_index: Option, + /// Rewards in microalgos applied to the close remainder to account. + #[serde(rename = "close-rewards", skip_serializing_if = "Option::is_none")] + pub close_rewards: Option, + /// Closing amount for the transaction. + #[serde(rename = "closing-amount", skip_serializing_if = "Option::is_none")] + pub closing_amount: Option, + /// The number of the asset's unit that were transferred to the close-to address. + #[serde( + rename = "asset-closing-amount", + skip_serializing_if = "Option::is_none" + )] + pub asset_closing_amount: Option, + /// The round where this transaction was confirmed, if present. + #[serde(rename = "confirmed-round", skip_serializing_if = "Option::is_none")] + pub confirmed_round: Option, + /// Indicates that the transaction was kicked out of this node's transaction pool (and specifies why that happened). An empty string indicates the transaction wasn't kicked out of this node's txpool due to an error. + #[serde(rename = "pool-error")] + pub pool_error: String, + /// Rewards in microalgos applied to the receiver account. + #[serde(rename = "receiver-rewards", skip_serializing_if = "Option::is_none")] + pub receiver_rewards: Option, + /// Rewards in microalgos applied to the sender account. + #[serde(rename = "sender-rewards", skip_serializing_if = "Option::is_none")] + pub sender_rewards: Option, + /// Local state key/value changes for the application being executed by this transaction. + #[serde(rename = "local-state-delta", skip_serializing_if = "Option::is_none")] + pub local_state_delta: Option>, + #[serde(rename = "global-state-delta", skip_serializing_if = "Option::is_none")] + pub global_state_delta: Option, + /// Logs for the application being executed by this transaction. + #[serde(rename = "logs", skip_serializing_if = "Option::is_none")] + pub logs: Option>, + /// Inner transactions produced by application execution. + #[serde(rename = "inner-txns", skip_serializing_if = "Option::is_none")] + pub inner_txns: Option>, + /// The raw signed transaction. + #[serde(rename = "txn")] + pub txn: AlgokitSignedTransaction, +} + +impl Default for PendingTransactionResponse { + fn default() -> Self { + Self { + asset_index: None, + application_index: None, + close_rewards: None, + closing_amount: None, + asset_closing_amount: None, + confirmed_round: None, + pool_error: "".to_string(), + receiver_rewards: None, + sender_rewards: None, + local_state_delta: None, + global_state_delta: None, + logs: None, + inner_txns: None, + txn: AlgokitSignedTransaction { + transaction: algokit_transact::Transaction::Payment( + algokit_transact::PaymentTransactionFields { + header: algokit_transact::TransactionHeader { + sender: Default::default(), + fee: None, + first_valid: 0, + last_valid: 0, + genesis_hash: None, + genesis_id: None, + note: None, + rekey_to: None, + lease: None, + group: None, + }, + receiver: Default::default(), + amount: 0, + close_remainder_to: None, + }, + ), + signature: None, + auth_address: None, + }, + } + } +} + +impl AlgorandMsgpack for PendingTransactionResponse { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl PendingTransactionResponse { + /// Constructor for PendingTransactionResponse + pub fn new(pool_error: String, txn: AlgokitSignedTransaction) -> PendingTransactionResponse { + PendingTransactionResponse { + pool_error, + txn, + asset_index: None, + application_index: None, + close_rewards: None, + closing_amount: None, + asset_closing_amount: None, + confirmed_round: None, + receiver_rewards: None, + sender_rewards: None, + local_state_delta: None, + global_state_delta: None, + logs: None, + inner_txns: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/raw_transaction.rs b/crates/algod_client/src/models/raw_transaction.rs new file mode 100644 index 00000000..9821ce62 --- /dev/null +++ b/crates/algod_client/src/models/raw_transaction.rs @@ -0,0 +1,27 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// Transaction ID of the submission. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct RawTransaction { + /// encoding of the transaction hash. + #[serde(rename = "txId")] + pub tx_id: String, +} + +impl RawTransaction { + /// Constructor for RawTransaction + pub fn new(tx_id: String) -> RawTransaction { + RawTransaction { tx_id } + } +} diff --git a/crates/algod_client/src/models/scratch_change.rs b/crates/algod_client/src/models/scratch_change.rs new file mode 100644 index 00000000..6fbc9d2c --- /dev/null +++ b/crates/algod_client/src/models/scratch_change.rs @@ -0,0 +1,46 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::AvmValue; + +/// A write operation into a scratch slot. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct ScratchChange { + /// The scratch slot written. + #[serde(rename = "slot")] + pub slot: u64, + #[serde(rename = "new-value")] + pub new_value: AvmValue, +} + +impl AlgorandMsgpack for ScratchChange { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl ScratchChange { + /// Constructor for ScratchChange + pub fn new(slot: u64, new_value: AvmValue) -> ScratchChange { + ScratchChange { slot, new_value } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/simulate_initial_states.rs b/crates/algod_client/src/models/simulate_initial_states.rs new file mode 100644 index 00000000..109219c9 --- /dev/null +++ b/crates/algod_client/src/models/simulate_initial_states.rs @@ -0,0 +1,44 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::ApplicationInitialStates; + +/// Initial states of resources that were accessed during simulation. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct SimulateInitialStates { + /// The initial states of accessed application before simulation. The order of this array is arbitrary. + #[serde(rename = "app-initial-states", skip_serializing_if = "Option::is_none")] + pub app_initial_states: Option>, +} + +impl AlgorandMsgpack for SimulateInitialStates { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl SimulateInitialStates { + /// Default constructor for SimulateInitialStates + pub fn new() -> SimulateInitialStates { + SimulateInitialStates::default() + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/simulate_request.rs b/crates/algod_client/src/models/simulate_request.rs new file mode 100644 index 00000000..b4069cf9 --- /dev/null +++ b/crates/algod_client/src/models/simulate_request.rs @@ -0,0 +1,83 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::SimulateRequestTransactionGroup; +use crate::models::SimulateTraceConfig; + +/// Request type for simulation endpoint. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct SimulateRequest { + /// The transaction groups to simulate. + #[serde(rename = "txn-groups")] + pub txn_groups: Vec, + /// If provided, specifies the round preceding the simulation. State changes through this round will be used to run this simulation. Usually only the 4 most recent rounds will be available (controlled by the node config value MaxAcctLookback). If not specified, defaults to the latest available round. + #[serde(rename = "round", skip_serializing_if = "Option::is_none")] + pub round: Option, + /// Allows transactions without signatures to be simulated as if they had correct signatures. + #[serde( + rename = "allow-empty-signatures", + skip_serializing_if = "Option::is_none" + )] + pub allow_empty_signatures: Option, + /// Lifts limits on log opcode usage during simulation. + #[serde(rename = "allow-more-logging", skip_serializing_if = "Option::is_none")] + pub allow_more_logging: Option, + /// Allows access to unnamed resources during simulation. + #[serde( + rename = "allow-unnamed-resources", + skip_serializing_if = "Option::is_none" + )] + pub allow_unnamed_resources: Option, + /// Applies extra opcode budget during simulation for each transaction group. + #[serde( + rename = "extra-opcode-budget", + skip_serializing_if = "Option::is_none" + )] + pub extra_opcode_budget: Option, + #[serde(rename = "exec-trace-config", skip_serializing_if = "Option::is_none")] + pub exec_trace_config: Option, + /// If true, signers for transactions that are missing signatures will be fixed during evaluation. + #[serde(rename = "fix-signers", skip_serializing_if = "Option::is_none")] + pub fix_signers: Option, +} + +impl AlgorandMsgpack for SimulateRequest { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl SimulateRequest { + /// Constructor for SimulateRequest + pub fn new(txn_groups: Vec) -> SimulateRequest { + SimulateRequest { + txn_groups, + round: None, + allow_empty_signatures: None, + allow_more_logging: None, + allow_unnamed_resources: None, + extra_opcode_budget: None, + exec_trace_config: None, + fix_signers: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/simulate_request_transaction_group.rs b/crates/algod_client/src/models/simulate_request_transaction_group.rs new file mode 100644 index 00000000..9a9c7ffa --- /dev/null +++ b/crates/algod_client/src/models/simulate_request_transaction_group.rs @@ -0,0 +1,42 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +/// A transaction group to simulate. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct SimulateRequestTransactionGroup { + /// An atomic transaction group. + #[serde(rename = "txns")] + pub txns: Vec, +} + +impl AlgorandMsgpack for SimulateRequestTransactionGroup { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl SimulateRequestTransactionGroup { + /// Constructor for SimulateRequestTransactionGroup + pub fn new(txns: Vec) -> SimulateRequestTransactionGroup { + SimulateRequestTransactionGroup { txns } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/simulate_trace_config.rs b/crates/algod_client/src/models/simulate_trace_config.rs new file mode 100644 index 00000000..cfe93191 --- /dev/null +++ b/crates/algod_client/src/models/simulate_trace_config.rs @@ -0,0 +1,51 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +/// An object that configures simulation execution trace. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct SimulateTraceConfig { + /// A boolean option for opting in execution trace features simulation endpoint. + #[serde(rename = "enable", skip_serializing_if = "Option::is_none")] + pub enable: Option, + /// A boolean option enabling returning stack changes together with execution trace during simulation. + #[serde(rename = "stack-change", skip_serializing_if = "Option::is_none")] + pub stack_change: Option, + /// A boolean option enabling returning scratch slot changes together with execution trace during simulation. + #[serde(rename = "scratch-change", skip_serializing_if = "Option::is_none")] + pub scratch_change: Option, + /// A boolean option enabling returning application state changes (global, local, and box changes) with the execution trace during simulation. + #[serde(rename = "state-change", skip_serializing_if = "Option::is_none")] + pub state_change: Option, +} + +impl AlgorandMsgpack for SimulateTraceConfig { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl SimulateTraceConfig { + /// Default constructor for SimulateTraceConfig + pub fn new() -> SimulateTraceConfig { + SimulateTraceConfig::default() + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/simulate_transaction.rs b/crates/algod_client/src/models/simulate_transaction.rs new file mode 100644 index 00000000..7da19693 --- /dev/null +++ b/crates/algod_client/src/models/simulate_transaction.rs @@ -0,0 +1,70 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::SimulateInitialStates; +use crate::models::SimulateTraceConfig; +use crate::models::SimulateTransactionGroupResult; +use crate::models::SimulationEvalOverrides; + +/// Result of a transaction group simulation. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct SimulateTransaction { + /// The version of this response object. + #[serde(rename = "version")] + pub version: u64, + /// The round immediately preceding this simulation. State changes through this round were used to run this simulation. + #[serde(rename = "last-round")] + pub last_round: u64, + /// A result object for each transaction group that was simulated. + #[serde(rename = "txn-groups")] + pub txn_groups: Vec, + #[serde(rename = "eval-overrides", skip_serializing_if = "Option::is_none")] + pub eval_overrides: Option, + #[serde(rename = "exec-trace-config", skip_serializing_if = "Option::is_none")] + pub exec_trace_config: Option, + #[serde(rename = "initial-states", skip_serializing_if = "Option::is_none")] + pub initial_states: Option, +} + +impl AlgorandMsgpack for SimulateTransaction { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl SimulateTransaction { + /// Constructor for SimulateTransaction + pub fn new( + version: u64, + last_round: u64, + txn_groups: Vec, + ) -> SimulateTransaction { + SimulateTransaction { + version, + last_round, + txn_groups, + eval_overrides: None, + exec_trace_config: None, + initial_states: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/simulate_transaction_group_result.rs b/crates/algod_client/src/models/simulate_transaction_group_result.rs new file mode 100644 index 00000000..8535b93f --- /dev/null +++ b/crates/algod_client/src/models/simulate_transaction_group_result.rs @@ -0,0 +1,72 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::SimulateTransactionResult; +use crate::models::SimulateUnnamedResourcesAccessed; + +/// Simulation result for an atomic transaction group +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct SimulateTransactionGroupResult { + /// Simulation result for individual transactions + #[serde(rename = "txn-results")] + pub txn_results: Vec, + /// If present, indicates that the transaction group failed and specifies why that happened + #[serde(rename = "failure-message", skip_serializing_if = "Option::is_none")] + pub failure_message: Option, + /// If present, indicates which transaction in this group caused the failure. This array represents the path to the failing transaction. Indexes are zero based, the first element indicates the top-level transaction, and successive elements indicate deeper inner transactions. + #[serde(rename = "failed-at", skip_serializing_if = "Option::is_none")] + pub failed_at: Option>, + /// Total budget added during execution of app calls in the transaction group. + #[serde(rename = "app-budget-added", skip_serializing_if = "Option::is_none")] + pub app_budget_added: Option, + /// Total budget consumed during execution of app calls in the transaction group. + #[serde( + rename = "app-budget-consumed", + skip_serializing_if = "Option::is_none" + )] + pub app_budget_consumed: Option, + #[serde( + rename = "unnamed-resources-accessed", + skip_serializing_if = "Option::is_none" + )] + pub unnamed_resources_accessed: Option, +} + +impl AlgorandMsgpack for SimulateTransactionGroupResult { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl SimulateTransactionGroupResult { + /// Constructor for SimulateTransactionGroupResult + pub fn new(txn_results: Vec) -> SimulateTransactionGroupResult { + SimulateTransactionGroupResult { + txn_results, + failure_message: None, + failed_at: None, + app_budget_added: None, + app_budget_consumed: None, + unnamed_resources_accessed: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/simulate_transaction_result.rs b/crates/algod_client/src/models/simulate_transaction_result.rs new file mode 100644 index 00000000..065dde10 --- /dev/null +++ b/crates/algod_client/src/models/simulate_transaction_result.rs @@ -0,0 +1,74 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::PendingTransactionResponse; +use crate::models::SimulateUnnamedResourcesAccessed; +use crate::models::SimulationTransactionExecTrace; + +/// Simulation result for an individual transaction +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct SimulateTransactionResult { + #[serde(rename = "txn-result")] + pub txn_result: PendingTransactionResponse, + /// Budget used during execution of an app call transaction. This value includes budged used by inner app calls spawned by this transaction. + #[serde( + rename = "app-budget-consumed", + skip_serializing_if = "Option::is_none" + )] + pub app_budget_consumed: Option, + /// Budget used during execution of a logic sig transaction. + #[serde( + rename = "logic-sig-budget-consumed", + skip_serializing_if = "Option::is_none" + )] + pub logic_sig_budget_consumed: Option, + #[serde(rename = "exec-trace", skip_serializing_if = "Option::is_none")] + pub exec_trace: Option, + #[serde( + rename = "unnamed-resources-accessed", + skip_serializing_if = "Option::is_none" + )] + pub unnamed_resources_accessed: Option, + /// The account that needed to sign this transaction when no signature was provided and the provided signer was incorrect. + #[serde(rename = "fixed-signer", skip_serializing_if = "Option::is_none")] + pub fixed_signer: Option, +} + +impl AlgorandMsgpack for SimulateTransactionResult { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl SimulateTransactionResult { + /// Constructor for SimulateTransactionResult + pub fn new(txn_result: PendingTransactionResponse) -> SimulateTransactionResult { + SimulateTransactionResult { + txn_result, + app_budget_consumed: None, + logic_sig_budget_consumed: None, + exec_trace: None, + unnamed_resources_accessed: None, + fixed_signer: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/simulate_unnamed_resources_accessed.rs b/crates/algod_client/src/models/simulate_unnamed_resources_accessed.rs new file mode 100644 index 00000000..ad52bc60 --- /dev/null +++ b/crates/algod_client/src/models/simulate_unnamed_resources_accessed.rs @@ -0,0 +1,64 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::ApplicationLocalReference; +use crate::models::AssetHoldingReference; +use crate::models::BoxReference; + +/// These are resources that were accessed by this group that would normally have caused failure, but were allowed in simulation. Depending on where this object is in the response, the unnamed resources it contains may or may not qualify for group resource sharing. If this is a field in SimulateTransactionGroupResult, the resources do qualify, but if this is a field in SimulateTransactionResult, they do not qualify. In order to make this group valid for actual submission, resources that qualify for group sharing can be made available by any transaction of the group; otherwise, resources must be placed in the same transaction which accessed them. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct SimulateUnnamedResourcesAccessed { + /// The unnamed accounts that were referenced. The order of this array is arbitrary. + #[serde(rename = "accounts", skip_serializing_if = "Option::is_none")] + pub accounts: Option>, + /// The unnamed assets that were referenced. The order of this array is arbitrary. + #[serde(rename = "assets", skip_serializing_if = "Option::is_none")] + pub assets: Option>, + /// The unnamed applications that were referenced. The order of this array is arbitrary. + #[serde(rename = "apps", skip_serializing_if = "Option::is_none")] + pub apps: Option>, + /// The unnamed boxes that were referenced. The order of this array is arbitrary. + #[serde(rename = "boxes", skip_serializing_if = "Option::is_none")] + pub boxes: Option>, + /// The number of extra box references used to increase the IO budget. This is in addition to the references defined in the input transaction group and any referenced to unnamed boxes. + #[serde(rename = "extra-box-refs", skip_serializing_if = "Option::is_none")] + pub extra_box_refs: Option, + /// The unnamed asset holdings that were referenced. The order of this array is arbitrary. + #[serde(rename = "asset-holdings", skip_serializing_if = "Option::is_none")] + pub asset_holdings: Option>, + /// The unnamed application local states that were referenced. The order of this array is arbitrary. + #[serde(rename = "app-locals", skip_serializing_if = "Option::is_none")] + pub app_locals: Option>, +} + +impl AlgorandMsgpack for SimulateUnnamedResourcesAccessed { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl SimulateUnnamedResourcesAccessed { + /// Default constructor for SimulateUnnamedResourcesAccessed + pub fn new() -> SimulateUnnamedResourcesAccessed { + SimulateUnnamedResourcesAccessed::default() + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/simulation_eval_overrides.rs b/crates/algod_client/src/models/simulation_eval_overrides.rs new file mode 100644 index 00000000..e19bb6f0 --- /dev/null +++ b/crates/algod_client/src/models/simulation_eval_overrides.rs @@ -0,0 +1,66 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +/// The set of parameters and limits override during simulation. If this set of parameters is present, then evaluation parameters may differ from standard evaluation in certain ways. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct SimulationEvalOverrides { + /// If true, transactions without signatures are allowed and simulated as if they were properly signed. + #[serde( + rename = "allow-empty-signatures", + skip_serializing_if = "Option::is_none" + )] + pub allow_empty_signatures: Option, + /// If true, allows access to unnamed resources during simulation. + #[serde( + rename = "allow-unnamed-resources", + skip_serializing_if = "Option::is_none" + )] + pub allow_unnamed_resources: Option, + /// The maximum log calls one can make during simulation + #[serde(rename = "max-log-calls", skip_serializing_if = "Option::is_none")] + pub max_log_calls: Option, + /// The maximum byte number to log during simulation + #[serde(rename = "max-log-size", skip_serializing_if = "Option::is_none")] + pub max_log_size: Option, + /// The extra opcode budget added to each transaction group during simulation + #[serde( + rename = "extra-opcode-budget", + skip_serializing_if = "Option::is_none" + )] + pub extra_opcode_budget: Option, + /// If true, signers for transactions that are missing signatures will be fixed during evaluation. + #[serde(rename = "fix-signers", skip_serializing_if = "Option::is_none")] + pub fix_signers: Option, +} + +impl AlgorandMsgpack for SimulationEvalOverrides { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl SimulationEvalOverrides { + /// Default constructor for SimulationEvalOverrides + pub fn new() -> SimulationEvalOverrides { + SimulationEvalOverrides::default() + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/simulation_opcode_trace_unit.rs b/crates/algod_client/src/models/simulation_opcode_trace_unit.rs new file mode 100644 index 00000000..65671279 --- /dev/null +++ b/crates/algod_client/src/models/simulation_opcode_trace_unit.rs @@ -0,0 +1,68 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::ApplicationStateOperation; +use crate::models::AvmValue; +use crate::models::ScratchChange; + +/// The set of trace information and effect from evaluating a single opcode. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct SimulationOpcodeTraceUnit { + /// The program counter of the current opcode being evaluated. + #[serde(rename = "pc")] + pub pc: u64, + /// The writes into scratch slots. + #[serde(rename = "scratch-changes", skip_serializing_if = "Option::is_none")] + pub scratch_changes: Option>, + /// The operations against the current application's states. + #[serde(rename = "state-changes", skip_serializing_if = "Option::is_none")] + pub state_changes: Option>, + /// The indexes of the traces for inner transactions spawned by this opcode, if any. + #[serde(rename = "spawned-inners", skip_serializing_if = "Option::is_none")] + pub spawned_inners: Option>, + /// The number of deleted stack values by this opcode. + #[serde(rename = "stack-pop-count", skip_serializing_if = "Option::is_none")] + pub stack_pop_count: Option, + /// The values added by this opcode to the stack. + #[serde(rename = "stack-additions", skip_serializing_if = "Option::is_none")] + pub stack_additions: Option>, +} + +impl AlgorandMsgpack for SimulationOpcodeTraceUnit { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl SimulationOpcodeTraceUnit { + /// Constructor for SimulationOpcodeTraceUnit + pub fn new(pc: u64) -> SimulationOpcodeTraceUnit { + SimulationOpcodeTraceUnit { + pc, + scratch_changes: None, + state_changes: None, + spawned_inners: None, + stack_pop_count: None, + stack_additions: None, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/simulation_transaction_exec_trace.rs b/crates/algod_client/src/models/simulation_transaction_exec_trace.rs new file mode 100644 index 00000000..2f3344c4 --- /dev/null +++ b/crates/algod_client/src/models/simulation_transaction_exec_trace.rs @@ -0,0 +1,91 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +use crate::models::SimulationOpcodeTraceUnit; + +/// The execution trace of calling an app or a logic sig, containing the inner app call trace in a recursive way. +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct SimulationTransactionExecTrace { + /// Program trace that contains a trace of opcode effects in an approval program. + #[serde( + rename = "approval-program-trace", + skip_serializing_if = "Option::is_none" + )] + pub approval_program_trace: Option>, + /// SHA512_256 hash digest of the approval program executed in transaction. + #[serde_as(as = "Option")] + #[serde( + rename = "approval-program-hash", + skip_serializing_if = "Option::is_none" + )] + pub approval_program_hash: Option>, + /// Program trace that contains a trace of opcode effects in a clear state program. + #[serde( + rename = "clear-state-program-trace", + skip_serializing_if = "Option::is_none" + )] + pub clear_state_program_trace: Option>, + /// SHA512_256 hash digest of the clear state program executed in transaction. + #[serde_as(as = "Option")] + #[serde( + rename = "clear-state-program-hash", + skip_serializing_if = "Option::is_none" + )] + pub clear_state_program_hash: Option>, + /// If true, indicates that the clear state program failed and any persistent state changes it produced should be reverted once the program exits. + #[serde( + rename = "clear-state-rollback", + skip_serializing_if = "Option::is_none" + )] + pub clear_state_rollback: Option, + /// The error message explaining why the clear state program failed. This field will only be populated if clear-state-rollback is true and the failure was due to an execution error. + #[serde( + rename = "clear-state-rollback-error", + skip_serializing_if = "Option::is_none" + )] + pub clear_state_rollback_error: Option, + /// Program trace that contains a trace of opcode effects in a logic sig. + #[serde(rename = "logic-sig-trace", skip_serializing_if = "Option::is_none")] + pub logic_sig_trace: Option>, + /// SHA512_256 hash digest of the logic sig executed in transaction. + #[serde_as(as = "Option")] + #[serde(rename = "logic-sig-hash", skip_serializing_if = "Option::is_none")] + pub logic_sig_hash: Option>, + /// An array of SimulationTransactionExecTrace representing the execution trace of any inner transactions executed. + #[serde(rename = "inner-trace", skip_serializing_if = "Option::is_none")] + pub inner_trace: Option>, +} + +impl AlgorandMsgpack for SimulationTransactionExecTrace { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl SimulationTransactionExecTrace { + /// Default constructor for SimulationTransactionExecTrace + pub fn new() -> SimulationTransactionExecTrace { + SimulationTransactionExecTrace::default() + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/start_catchup.rs b/crates/algod_client/src/models/start_catchup.rs new file mode 100644 index 00000000..4306dedd --- /dev/null +++ b/crates/algod_client/src/models/start_catchup.rs @@ -0,0 +1,27 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// An catchpoint start response. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct StartCatchup { + /// Catchup start response string + #[serde(rename = "catchup-message")] + pub catchup_message: String, +} + +impl StartCatchup { + /// Constructor for StartCatchup + pub fn new(catchup_message: String) -> StartCatchup { + StartCatchup { catchup_message } + } +} diff --git a/crates/algod_client/src/models/state_delta.rs b/crates/algod_client/src/models/state_delta.rs new file mode 100644 index 00000000..aa9042c8 --- /dev/null +++ b/crates/algod_client/src/models/state_delta.rs @@ -0,0 +1,38 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +/// Application state delta. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct StateDelta {} + +impl AlgorandMsgpack for StateDelta { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl StateDelta { + /// Default constructor for StateDelta + pub fn new() -> StateDelta { + StateDelta::default() + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/state_proof.rs b/crates/algod_client/src/models/state_proof.rs new file mode 100644 index 00000000..584c8ee5 --- /dev/null +++ b/crates/algod_client/src/models/state_proof.rs @@ -0,0 +1,37 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +use crate::models::StateProofMessage; + +/// Represents a state proof and its corresponding message +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct StateProof { + #[serde(rename = "Message")] + pub message: StateProofMessage, + /// The encoded StateProof for the message. + #[serde_as(as = "serde_with::base64::Base64")] + #[serde(rename = "StateProof")] + pub state_proof: Vec, +} + +impl StateProof { + /// Constructor for StateProof + pub fn new(message: StateProofMessage, state_proof: Vec) -> StateProof { + StateProof { + message, + state_proof, + } + } +} diff --git a/crates/algod_client/src/models/state_proof_message.rs b/crates/algod_client/src/models/state_proof_message.rs new file mode 100644 index 00000000..74954dc8 --- /dev/null +++ b/crates/algod_client/src/models/state_proof_message.rs @@ -0,0 +1,55 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +/// Represents the message that the state proofs are attesting to. +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct StateProofMessage { + /// The vector commitment root on all light block headers within a state proof interval. + #[serde_as(as = "serde_with::base64::Base64")] + #[serde(rename = "BlockHeadersCommitment")] + pub block_headers_commitment: Vec, + /// The vector commitment root of the top N accounts to sign the next StateProof. + #[serde_as(as = "serde_with::base64::Base64")] + #[serde(rename = "VotersCommitment")] + pub voters_commitment: Vec, + /// An integer value representing the natural log of the proven weight with 16 bits of precision. This value would be used to verify the next state proof. + #[serde(rename = "LnProvenWeight")] + pub ln_proven_weight: u64, + /// The first round the message attests to. + #[serde(rename = "FirstAttestedRound")] + pub first_attested_round: u64, + /// The last round the message attests to. + #[serde(rename = "LastAttestedRound")] + pub last_attested_round: u64, +} + +impl StateProofMessage { + /// Constructor for StateProofMessage + pub fn new( + block_headers_commitment: Vec, + voters_commitment: Vec, + ln_proven_weight: u64, + first_attested_round: u64, + last_attested_round: u64, + ) -> StateProofMessage { + StateProofMessage { + block_headers_commitment, + voters_commitment, + ln_proven_weight, + first_attested_round, + last_attested_round, + } + } +} diff --git a/crates/algod_client/src/models/teal_compile.rs b/crates/algod_client/src/models/teal_compile.rs new file mode 100644 index 00000000..d58599f8 --- /dev/null +++ b/crates/algod_client/src/models/teal_compile.rs @@ -0,0 +1,40 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +/// Teal compile Result +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct TealCompile { + /// base32 SHA512_256 of program bytes (Address style) + #[serde(rename = "hash")] + pub hash: String, + /// base64 encoded program bytes + #[serde_as(as = "serde_with::base64::Base64")] + #[serde(rename = "result")] + pub result: Vec, + /// JSON of the source map + #[serde(rename = "sourcemap", skip_serializing_if = "Option::is_none")] + pub sourcemap: Option, +} + +impl TealCompile { + /// Constructor for TealCompile + pub fn new(hash: String, result: Vec) -> TealCompile { + TealCompile { + hash, + result, + sourcemap: None, + } + } +} diff --git a/crates/algod_client/src/models/teal_disassemble.rs b/crates/algod_client/src/models/teal_disassemble.rs new file mode 100644 index 00000000..02223194 --- /dev/null +++ b/crates/algod_client/src/models/teal_disassemble.rs @@ -0,0 +1,27 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// Teal disassembly Result +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct TealDisassemble { + /// disassembled Teal code + #[serde(rename = "result")] + pub result: String, +} + +impl TealDisassemble { + /// Constructor for TealDisassemble + pub fn new(result: String) -> TealDisassemble { + TealDisassemble { result } + } +} diff --git a/crates/algod_client/src/models/teal_dryrun.rs b/crates/algod_client/src/models/teal_dryrun.rs new file mode 100644 index 00000000..8e2da060 --- /dev/null +++ b/crates/algod_client/src/models/teal_dryrun.rs @@ -0,0 +1,37 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +use crate::models::DryrunTxnResult; + +/// DryrunResponse contains per-txn debug information from a dryrun. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct TealDryrun { + #[serde(rename = "txns")] + pub txns: Vec, + #[serde(rename = "error")] + pub error: String, + /// Protocol version is the protocol version Dryrun was operated under. + #[serde(rename = "protocol-version")] + pub protocol_version: String, +} + +impl TealDryrun { + /// Constructor for TealDryrun + pub fn new(txns: Vec, error: String, protocol_version: String) -> TealDryrun { + TealDryrun { + txns, + error, + protocol_version, + } + } +} diff --git a/crates/algod_client/src/models/teal_key_value.rs b/crates/algod_client/src/models/teal_key_value.rs new file mode 100644 index 00000000..02f28c4b --- /dev/null +++ b/crates/algod_client/src/models/teal_key_value.rs @@ -0,0 +1,45 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +use crate::models::TealValue; + +/// Represents a key-value pair in an application store. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct TealKeyValue { + #[serde(rename = "key")] + pub key: String, + #[serde(rename = "value")] + pub value: TealValue, +} + +impl AlgorandMsgpack for TealKeyValue { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl TealKeyValue { + /// Constructor for TealKeyValue + pub fn new(key: String, value: TealValue) -> TealKeyValue { + TealKeyValue { key, value } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/teal_key_value_store.rs b/crates/algod_client/src/models/teal_key_value_store.rs new file mode 100644 index 00000000..a021e03e --- /dev/null +++ b/crates/algod_client/src/models/teal_key_value_store.rs @@ -0,0 +1,38 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +/// Represents a key-value store for use in an application. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct TealKeyValueStore {} + +impl AlgorandMsgpack for TealKeyValueStore { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl TealKeyValueStore { + /// Default constructor for TealKeyValueStore + pub fn new() -> TealKeyValueStore { + TealKeyValueStore::default() + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/teal_value.rs b/crates/algod_client/src/models/teal_value.rs new file mode 100644 index 00000000..462c0194 --- /dev/null +++ b/crates/algod_client/src/models/teal_value.rs @@ -0,0 +1,52 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use algokit_transact::{AlgorandMsgpack, SignedTransaction as AlgokitSignedTransaction}; +use serde::{Deserialize, Serialize}; + +/// Represents a TEAL value. +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct TealValue { + /// \[tt\] value type. Value `1` refers to **bytes**, value `2` refers to **uint** + #[serde(rename = "type")] + pub r#type: u32, + /// \[tb\] bytes value. + #[serde(rename = "bytes")] + pub bytes: String, + /// \[ui\] uint value. + #[serde(rename = "uint")] + pub uint: u64, +} + +impl AlgorandMsgpack for TealValue { + const PREFIX: &'static [u8] = b""; // Adjust prefix as needed for your specific type +} + +impl TealValue { + /// Constructor for TealValue + pub fn new(r#type: u32, bytes: String, uint: u64) -> TealValue { + TealValue { + r#type, + bytes, + uint, + } + } + + /// Encode this struct to msgpack bytes using AlgorandMsgpack trait + pub fn to_msgpack(&self) -> Result, Box> { + Ok(self.encode()?) + } + + /// Decode msgpack bytes to this struct using AlgorandMsgpack trait + pub fn from_msgpack(bytes: &[u8]) -> Result> { + Ok(Self::decode(bytes)?) + } +} diff --git a/crates/algod_client/src/models/transaction_params.rs b/crates/algod_client/src/models/transaction_params.rs new file mode 100644 index 00000000..df7c1457 --- /dev/null +++ b/crates/algod_client/src/models/transaction_params.rs @@ -0,0 +1,65 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +/// TransactionParams contains the parameters that help a client construct +/// a new transaction. +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct TransactionParams { + /// ConsensusVersion indicates the consensus protocol version + /// as of LastRound. + #[serde(rename = "consensus-version")] + pub consensus_version: String, + /// Fee is the suggested transaction fee + /// Fee is in units of micro-Algos per byte. + /// Fee may fall to zero but transactions must still have a fee of + /// at least MinTxnFee for the current network protocol. + #[serde(rename = "fee")] + pub fee: u64, + /// GenesisHash is the hash of the genesis block. + #[serde_as(as = "serde_with::base64::Base64")] + #[serde(rename = "genesis-hash")] + pub genesis_hash: Vec, + /// GenesisID is an ID listed in the genesis block. + #[serde(rename = "genesis-id")] + pub genesis_id: String, + /// LastRound indicates the last round seen + #[serde(rename = "last-round")] + pub last_round: u64, + /// The minimum transaction fee (not per byte) required for the + /// txn to validate for the current network protocol. + #[serde(rename = "min-fee")] + pub min_fee: u64, +} + +impl TransactionParams { + /// Constructor for TransactionParams + pub fn new( + consensus_version: String, + fee: u64, + genesis_hash: Vec, + genesis_id: String, + last_round: u64, + min_fee: u64, + ) -> TransactionParams { + TransactionParams { + consensus_version, + fee, + genesis_hash, + genesis_id, + last_round, + min_fee, + } + } +} diff --git a/crates/algod_client/src/models/version.rs b/crates/algod_client/src/models/version.rs new file mode 100644 index 00000000..2948709d --- /dev/null +++ b/crates/algod_client/src/models/version.rs @@ -0,0 +1,47 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +use crate::models::BuildVersion; + +/// algod version information. +#[serde_as] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct Version { + #[serde(rename = "build")] + pub build: BuildVersion, + #[serde_as(as = "serde_with::base64::Base64")] + #[serde(rename = "genesis_hash_b64")] + pub genesis_hash_b64: Vec, + #[serde(rename = "genesis_id")] + pub genesis_id: String, + #[serde(rename = "versions")] + pub versions: Vec, +} + +impl Version { + /// Constructor for Version + pub fn new( + build: BuildVersion, + genesis_hash_b64: Vec, + genesis_id: String, + versions: Vec, + ) -> Version { + Version { + build, + genesis_hash_b64, + genesis_id, + versions, + } + } +} diff --git a/crates/algod_client/src/models/wait_for_block.rs b/crates/algod_client/src/models/wait_for_block.rs new file mode 100644 index 00000000..281784da --- /dev/null +++ b/crates/algod_client/src/models/wait_for_block.rs @@ -0,0 +1,171 @@ +/* + * Algod REST API. + * + * API endpoint for algod operations. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: contact@algorand.com + * Generated by: Rust OpenAPI Generator + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// NodeStatus contains the information about a node status +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct WaitForBlock { + /// CatchupTime in nanoseconds + #[serde(rename = "catchup-time")] + pub catchup_time: u64, + /// LastRound indicates the last round seen + #[serde(rename = "last-round")] + pub last_round: u64, + /// LastVersion indicates the last consensus version supported + #[serde(rename = "last-version")] + pub last_version: String, + /// NextVersion of consensus protocol to use + #[serde(rename = "next-version")] + pub next_version: String, + /// NextVersionRound is the round at which the next consensus version will apply + #[serde(rename = "next-version-round")] + pub next_version_round: u64, + /// NextVersionSupported indicates whether the next consensus version is supported by this node + #[serde(rename = "next-version-supported")] + pub next_version_supported: bool, + /// StoppedAtUnsupportedRound indicates that the node does not support the new rounds and has stopped making progress + #[serde(rename = "stopped-at-unsupported-round")] + pub stopped_at_unsupported_round: bool, + /// TimeSinceLastRound in nanoseconds + #[serde(rename = "time-since-last-round")] + pub time_since_last_round: u64, + /// The last catchpoint seen by the node + #[serde(rename = "last-catchpoint", skip_serializing_if = "Option::is_none")] + pub last_catchpoint: Option, + /// The current catchpoint that is being caught up to + #[serde(rename = "catchpoint", skip_serializing_if = "Option::is_none")] + pub catchpoint: Option, + /// The total number of accounts included in the current catchpoint + #[serde( + rename = "catchpoint-total-accounts", + skip_serializing_if = "Option::is_none" + )] + pub catchpoint_total_accounts: Option, + /// The number of accounts from the current catchpoint that have been processed so far as part of the catchup + #[serde( + rename = "catchpoint-processed-accounts", + skip_serializing_if = "Option::is_none" + )] + pub catchpoint_processed_accounts: Option, + /// The number of accounts from the current catchpoint that have been verified so far as part of the catchup + #[serde( + rename = "catchpoint-verified-accounts", + skip_serializing_if = "Option::is_none" + )] + pub catchpoint_verified_accounts: Option, + /// The total number of key-values (KVs) included in the current catchpoint + #[serde( + rename = "catchpoint-total-kvs", + skip_serializing_if = "Option::is_none" + )] + pub catchpoint_total_kvs: Option, + /// The number of key-values (KVs) from the current catchpoint that have been processed so far as part of the catchup + #[serde( + rename = "catchpoint-processed-kvs", + skip_serializing_if = "Option::is_none" + )] + pub catchpoint_processed_kvs: Option, + /// The number of key-values (KVs) from the current catchpoint that have been verified so far as part of the catchup + #[serde( + rename = "catchpoint-verified-kvs", + skip_serializing_if = "Option::is_none" + )] + pub catchpoint_verified_kvs: Option, + /// The total number of blocks that are required to complete the current catchpoint catchup + #[serde( + rename = "catchpoint-total-blocks", + skip_serializing_if = "Option::is_none" + )] + pub catchpoint_total_blocks: Option, + /// The number of blocks that have already been obtained by the node as part of the catchup + #[serde( + rename = "catchpoint-acquired-blocks", + skip_serializing_if = "Option::is_none" + )] + pub catchpoint_acquired_blocks: Option, + /// Upgrade delay + #[serde(rename = "upgrade-delay", skip_serializing_if = "Option::is_none")] + pub upgrade_delay: Option, + /// This node's upgrade vote + #[serde(rename = "upgrade-node-vote", skip_serializing_if = "Option::is_none")] + pub upgrade_node_vote: Option, + /// Yes votes required for consensus upgrade + #[serde( + rename = "upgrade-votes-required", + skip_serializing_if = "Option::is_none" + )] + pub upgrade_votes_required: Option, + /// Total votes cast for consensus upgrade + #[serde(rename = "upgrade-votes", skip_serializing_if = "Option::is_none")] + pub upgrade_votes: Option, + /// Yes votes cast for consensus upgrade + #[serde(rename = "upgrade-yes-votes", skip_serializing_if = "Option::is_none")] + pub upgrade_yes_votes: Option, + /// No votes cast for consensus upgrade + #[serde(rename = "upgrade-no-votes", skip_serializing_if = "Option::is_none")] + pub upgrade_no_votes: Option, + /// Next protocol round + #[serde( + rename = "upgrade-next-protocol-vote-before", + skip_serializing_if = "Option::is_none" + )] + pub upgrade_next_protocol_vote_before: Option, + /// Total voting rounds for current upgrade + #[serde( + rename = "upgrade-vote-rounds", + skip_serializing_if = "Option::is_none" + )] + pub upgrade_vote_rounds: Option, +} + +impl WaitForBlock { + /// Constructor for WaitForBlock + pub fn new( + catchup_time: u64, + last_round: u64, + last_version: String, + next_version: String, + next_version_round: u64, + next_version_supported: bool, + stopped_at_unsupported_round: bool, + time_since_last_round: u64, + ) -> WaitForBlock { + WaitForBlock { + catchup_time, + last_round, + last_version, + next_version, + next_version_round, + next_version_supported, + stopped_at_unsupported_round, + time_since_last_round, + last_catchpoint: None, + catchpoint: None, + catchpoint_total_accounts: None, + catchpoint_processed_accounts: None, + catchpoint_verified_accounts: None, + catchpoint_total_kvs: None, + catchpoint_processed_kvs: None, + catchpoint_verified_kvs: None, + catchpoint_total_blocks: None, + catchpoint_acquired_blocks: None, + upgrade_delay: None, + upgrade_node_vote: None, + upgrade_votes_required: None, + upgrade_votes: None, + upgrade_yes_votes: None, + upgrade_no_votes: None, + upgrade_next_protocol_vote_before: None, + upgrade_vote_rounds: None, + } + } +} diff --git a/crates/algod_client_tests/Cargo.toml b/crates/algod_client_tests/Cargo.toml new file mode 100644 index 00000000..c57ad7d6 --- /dev/null +++ b/crates/algod_client_tests/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "algod_client_tests" +version = "0.1.0" +authors = ["AlgoKit Core Team"] +description = "Integration tests for algod_client crate" +license = "MIT" +edition = "2024" + +[dependencies] +# Test dependencies +tokio = { version = "1.39.2", features = ["full"] } +tokio-test = "^0.4" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.133" + +# Algod client under test +algod_client = { path = "../algod_client" } +algokit_transact = { path = "../algokit_transact", features = ["test_utils"] } +algokit_http_client = { path = "../algokit_http_client" } + +# Test fixtures and utilities +once_cell = "1.19" +base64 = "0.22.1" +reqwest = "^0.12" +futures = "0.3" + +# Algorand account generation replaced with algokit_transact and ed25519_dalek + +# CLI support and string processing +regex = "1.10.2" + +# Cryptographic operations +ed25519-dalek = { version = "2.1.1", features = ["rand_core"] } +hex = "0.4.3" +rand = "0.8" +sha2 = "0.10.8" +lazy_static = "1.4" + +[dev-dependencies] +criterion = "0.5" diff --git a/crates/algod_client_tests/README.md b/crates/algod_client_tests/README.md new file mode 100644 index 00000000..1f0eb0b6 --- /dev/null +++ b/crates/algod_client_tests/README.md @@ -0,0 +1,120 @@ +# Algod Client Tests + +Integration tests for the `algod_client` crate, demonstrating usage patterns and ensuring compatibility with the Algorand network. + +> TODO: Temporary crate! Tests are to be merged into the algokit_utils crate. + +## Overview + +This crate provides integration tests for the Algod API client, using a local Algorand network (localnet) for testing. The tests showcase: + +- **Transaction parameter retrieval** +- **Raw transaction broadcasting** +- **Pending transaction information** +- **Transaction simulation** + +## Test Architecture + +### Consolidated Client Pattern + +All tests now use a **global client fixture** for consistent, idiomatic Rust testing: + +```rust +use algod_client_tests::{get_algod_client, LocalnetManager}; + +#[tokio::test] +async fn test_example() { + // Ensure localnet is running + LocalnetManager::ensure_running().await.expect("Failed to start localnet"); + + // Use the global client - no need to create individual clients + let result = get_algod_client().transaction_params().await; + + assert!(result.is_ok()); + let params = result.unwrap(); + println!("Genesis ID: {}", params.genesis_id); +} +``` + +### Benefits of the New Approach + +✅ **DRY Principle**: No code duplication across test files +✅ **Consistent Configuration**: Single source of truth for client setup +✅ **Idiomatic Rust**: Uses `OnceLock` for thread-safe global state +✅ **Easy Maintenance**: Configuration changes apply to all tests automatically +✅ **Consolidated API**: Uses the ergonomic `AlgodClient` instead of individual endpoint functions + +### Before vs After + +**Before (❌ Old pattern)**: + +```rust +// Each test file had duplicate client setup +use algod_client::apis::{configuration::Configuration, transaction_params}; +use std::sync::OnceLock; + +static CONFIG: OnceLock = OnceLock::new(); + +fn get_config() -> &'static Configuration { + CONFIG.get_or_init(|| ALGOD_CONFIG.clone()) +} + +// Individual endpoint function calls +let result = transaction_params::transaction_params(get_config()).await; +``` + +**After (✅ New pattern)**: + +```rust +// Simple import and usage +use algod_client_tests::get_algod_client; + +// Consolidated client method calls +let result = get_algod_client().transaction_params().await; +``` + +## Running Tests + +Tests require a algokit-cli installed with docker available. Tests will programmatically invoke localnet via algokit-cli: + +```bash +# Run all integration tests +cargo test -p algod_client_tests + +# Run a specific test with output +cargo test -p algod_client_tests transaction_params -- --nocapture + +# Check test compilation without running +cargo check -p algod_client_tests +``` + +## Test Configuration + +The global client uses environment variables with sensible defaults: + +- `ALGORAND_HOST` (default: `http://localhost:4001`) +- `ALGORAND_API_TOKEN` (default: localnet token) + +## Test Structure + +``` +crates/algod_client_tests/ +├── src/ +│ ├── lib.rs # Module exports +│ ├── fixtures.rs # Global client fixture & test utilities +│ ├── localnet.rs # Localnet management +│ ├── account_helpers.rs # Test account creation & management +│ └── mnemonic.rs # Mnemonic utilities +└── tests/ + ├── transaction_params.rs # Parameter retrieval tests + ├── raw_transaction.rs # Transaction broadcasting tests + ├── pending_transaction_information.rs # Pending transaction tests + └── simulate_transactions.rs # Transaction simulation tests +``` + +## Key Components + +- **`get_algod_client()`**: Global client fixture for all tests +- **`LocalnetManager`**: Ensures test environment is available +- **`TestAccountManager`**: Creates funded test accounts +- **`LocalnetTransactionMother`**: Builds test transactions diff --git a/crates/algod_client_tests/src/account_helpers.rs b/crates/algod_client_tests/src/account_helpers.rs new file mode 100644 index 00000000..fbc13e27 --- /dev/null +++ b/crates/algod_client_tests/src/account_helpers.rs @@ -0,0 +1,432 @@ +use algod_client::AlgodClient; +use algokit_transact::{ + Address, AlgorandMsgpack, PaymentTransactionBuilder, SignedTransaction, Transaction, + TransactionHeaderBuilder, +}; +use ed25519_dalek::{Signer, SigningKey}; +use hex; +use rand::rngs::OsRng; +use regex::Regex; +use std::convert::TryInto; +use std::process::Command; +use std::str::FromStr; +use tokio::time::{Duration, sleep}; + +use crate::mnemonic::{from_key, to_key}; + +/// Test account configuration +#[derive(Debug, Clone)] +pub struct TestAccountConfig { + /// Initial funding amount in microALGOs (default: 10 ALGO = 10,000,000 microALGOs) + pub initial_funds: u64, + /// Whether to suppress log messages + pub suppress_log: bool, + /// Network type (LocalNet, TestNet, MainNet) + pub network_type: NetworkType, + /// Optional note for funding transaction + pub funding_note: Option, +} + +impl Default for TestAccountConfig { + fn default() -> Self { + Self { + initial_funds: 10_000_000, // 10 ALGO + suppress_log: false, + network_type: NetworkType::LocalNet, + funding_note: None, + } + } +} + +/// Network types for testing +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum NetworkType { + LocalNet, + TestNet, + MainNet, +} + +/// A test account using algokit_transact and ed25519_dalek with proper Algorand mnemonics +#[derive(Debug, Clone)] +pub struct TestAccount { + /// The ed25519 signing key + signing_key: SigningKey, + /// Cached balance in microALGOs + pub balance: Option, +} + +impl TestAccount { + /// Generate a new random test account using ed25519_dalek + pub fn generate() -> Result> { + // Generate a random signing key directly (like algonaut does) + let signing_key = SigningKey::generate(&mut OsRng); + + Ok(Self { + signing_key, + balance: None, + }) + } + + /// Create account from mnemonic using proper Algorand 25-word mnemonics + pub fn from_mnemonic( + mnemonic_str: &str, + ) -> Result> { + // Convert 25-word mnemonic to 32-byte key using our mnemonic module + let key_bytes = + to_key(mnemonic_str).map_err(|e| format!("Failed to parse mnemonic: {}", e))?; + + let signing_key = SigningKey::from_bytes(&key_bytes); + + Ok(Self { + signing_key, + balance: None, + }) + } + + /// Get the account's address using algokit_transact + pub fn address(&self) -> Result> { + let verifying_key = self.signing_key.verifying_key(); + let public_key_bytes = verifying_key.to_bytes(); + let address = Address::from_pubkey(&public_key_bytes); + Ok(address) + } + + /// Get the account's mnemonic (proper Algorand 25-word mnemonic) + pub fn mnemonic(&self) -> String { + let key_bytes = self.signing_key.to_bytes(); + from_key(&key_bytes).unwrap_or_else(|_| { + // Fallback to hex for debugging if mnemonic generation fails + hex::encode(key_bytes) + }) + } + + /// Sign a transaction using ed25519_dalek and algokit_transact + pub fn sign_transaction( + &self, + transaction: &Transaction, + ) -> Result, Box> { + // Encode the transaction to get the bytes to sign + let transaction_bytes = transaction.encode()?; + + // Sign the transaction bytes using ed25519_dalek + let signature = self.signing_key.sign(&transaction_bytes); + + // Create a SignedTransaction with the signature + let signed_transaction = SignedTransaction { + transaction: transaction.clone(), + signature: Some(signature.to_bytes()), + auth_address: None, + }; + + // Encode the signed transaction to msgpack bytes + let signed_bytes = signed_transaction.encode()?; + + Ok(signed_bytes) + } +} + +/// LocalNet dispenser for funding test accounts using AlgoKit CLI +pub struct LocalNetDispenser { + client: AlgodClient, + dispenser_account: Option, +} + +impl LocalNetDispenser { + /// Create a new LocalNet dispenser + pub fn new(client: AlgodClient) -> Self { + Self { + client, + dispenser_account: None, + } + } + + /// Get the LocalNet dispenser account from AlgoKit CLI + pub async fn get_dispenser_account( + &mut self, + ) -> Result<&TestAccount, Box> { + if self.dispenser_account.is_none() { + self.dispenser_account = Some(self.fetch_dispenser_from_algokit().await?); + } + + Ok(self.dispenser_account.as_ref().unwrap()) + } + + /// Fetch the dispenser account using AlgoKit CLI + async fn fetch_dispenser_from_algokit( + &self, + ) -> Result> { + // Get list of accounts to find the one with highest balance + let output = Command::new("algokit") + .args(["goal", "account", "list"]) + .output() + .map_err(|e| format!("Failed to run algokit goal account list: {}", e))?; + + if !output.status.success() { + return Err(format!( + "algokit goal account list failed: {}", + String::from_utf8_lossy(&output.stderr) + ) + .into()); + } + + let accounts_output = String::from_utf8_lossy(&output.stdout); + + // Parse output to find account with highest balance + let re = Regex::new(r"([A-Z0-9]{58})\s+(\d+)\s+microAlgos")?; + let mut highest_balance = 0u64; + let mut dispenser_address = String::new(); + + for cap in re.captures_iter(&accounts_output) { + let address = cap[1].to_string(); + let balance: u64 = cap[2].parse().unwrap_or(0); + + if balance > highest_balance { + highest_balance = balance; + dispenser_address = address; + } + } + + if dispenser_address.is_empty() { + return Err("No funded accounts found in LocalNet".into()); + } + + println!( + "Found LocalNet dispenser account: {} with {} microALGOs", + dispenser_address, highest_balance + ); + + // Export the account to get its mnemonic + let output = Command::new("algokit") + .args(["goal", "account", "export", "-a", &dispenser_address]) + .output() + .map_err(|e| format!("Failed to export account {}: {}", dispenser_address, e))?; + + if !output.status.success() { + return Err(format!( + "Failed to export account {}: {}", + dispenser_address, + String::from_utf8_lossy(&output.stderr) + ) + .into()); + } + + let export_output = String::from_utf8_lossy(&output.stdout); + + // Parse mnemonic from output + let mnemonic = export_output + .split('"') + .nth(1) + .ok_or("Could not extract mnemonic from algokit output")?; + + // Create account from mnemonic using proper Algorand mnemonic parsing + let mut test_account = TestAccount::from_mnemonic(mnemonic)?; + test_account.balance = Some(highest_balance); + + Ok(test_account) + } + + /// Fund an account with ALGOs using the dispenser + pub async fn fund_account( + &mut self, + recipient_address: &str, + amount: u64, + ) -> Result> { + // Get transaction parameters first (before borrowing self mutably) + let params = self + .client + .transaction_params() + .await + .map_err(|e| format!("Failed to get transaction params: {:?}", e))?; + + let dispenser = self.get_dispenser_account().await?; + + // Convert recipient address string to algokit_transact::Address + let recipient = Address::from_str(recipient_address)?; + + // Convert genesis hash Vec to 32-byte array (already decoded from base64) + let genesis_hash_bytes: [u8; 32] = + params.genesis_hash.try_into().map_err(|v: Vec| { + format!("Genesis hash must be 32 bytes, got {} bytes", v.len()) + })?; + + // Build funding transaction + let header = TransactionHeaderBuilder::default() + .sender(dispenser.address()?) + .fee(params.min_fee) + .first_valid(params.last_round) + .last_valid(params.last_round + 1000) + .genesis_id(params.genesis_id.clone()) + .genesis_hash(genesis_hash_bytes) + .note(b"LocalNet test funding".to_vec()) + .build()?; + + let payment_fields = PaymentTransactionBuilder::default() + .header(header) + .receiver(recipient) + .amount(amount) + .build_fields()?; + + let transaction = Transaction::Payment(payment_fields); + let signed_bytes = dispenser.sign_transaction(&transaction)?; + + // Submit transaction + let response = self + .client + .raw_transaction(signed_bytes) + .await + .map_err(|e| format!("Failed to submit transaction: {:?}", e))?; + + println!( + "✓ Funded account {} with {} microALGOs (txn: {})", + recipient_address, amount, response.tx_id + ); + + Ok(response.tx_id) + } +} + +/// Test account manager for generating and managing test accounts +pub struct TestAccountManager { + dispenser: LocalNetDispenser, +} + +impl TestAccountManager { + /// Create a new test account manager + pub fn new(client: AlgodClient) -> Self { + let dispenser = LocalNetDispenser::new(client); + Self { dispenser } + } + + /// Get a test account with optional configuration + pub async fn get_test_account( + &mut self, + config: Option, + ) -> Result> { + let config = config.unwrap_or_default(); + + // Generate new account using ed25519_dalek + let test_account = TestAccount::generate()?; + let address_str = test_account.address()?.to_string(); + + // Fund the account based on network type + match config.network_type { + NetworkType::LocalNet => { + self.dispenser + .fund_account(&address_str, config.initial_funds) + .await?; + } + NetworkType::TestNet => { + println!( + "⚠ TestNet funding not yet implemented. Please fund manually: {}", + address_str + ); + } + NetworkType::MainNet => { + println!( + "⚠ MainNet detected. Account generated but not funded: {}", + address_str + ); + } + } + + Ok(test_account) + } + + /// Create a funded account pair (sender, receiver) for testing + pub async fn create_account_pair( + &mut self, + ) -> Result<(TestAccount, TestAccount), Box> { + let sender_config = TestAccountConfig { + initial_funds: 10_000_000, // 10 ALGO + suppress_log: false, + network_type: NetworkType::LocalNet, + funding_note: Some("Test sender account".to_string()), + }; + + let receiver_config = TestAccountConfig { + initial_funds: 1_000_000, // 1 ALGO + suppress_log: false, + network_type: NetworkType::LocalNet, + funding_note: Some("Test receiver account".to_string()), + }; + + let sender = self.get_test_account(Some(sender_config)).await?; + let receiver = self.get_test_account(Some(receiver_config)).await?; + + Ok((sender, receiver)) + } + + /// Generate multiple test accounts at once + pub async fn get_test_accounts( + &mut self, + count: usize, + config: Option, + ) -> Result, Box> { + let mut accounts = Vec::with_capacity(count); + + for i in 0..count { + let account_config = config.clone().unwrap_or_default(); + if !account_config.suppress_log { + println!("Generating test account {} of {}", i + 1, count); + } + + let account = self.get_test_account(Some(account_config)).await?; + accounts.push(account); + + // Small delay to avoid overwhelming the network + sleep(Duration::from_millis(100)).await; + } + + Ok(accounts) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_account_generation_with_algokit_transact() { + // Test basic account generation using algokit_transact and ed25519_dalek with proper mnemonics + let account = TestAccount::generate().expect("Failed to generate test account"); + let address = account.address().expect("Failed to get address"); + + assert!(!address.to_string().is_empty()); + let mnemonic = account.mnemonic(); + assert!(!mnemonic.is_empty()); + + // Test that we get proper 25-word mnemonics (or hex fallback for debugging) + let word_count = mnemonic.split_whitespace().count(); + println!("Mnemonic word count: {}", word_count); + println!("Generated account address: {}", address); + println!("Generated account mnemonic: {}", mnemonic); + } + + #[tokio::test] + async fn test_account_from_mnemonic_with_algokit_transact() { + let original = TestAccount::generate().expect("Failed to generate test account"); + let mnemonic = original.mnemonic(); + + // Only test round-trip if we have a proper mnemonic (not hex fallback) + if mnemonic.split_whitespace().count() == 25 { + // Recover account from mnemonic using proper Algorand mnemonic parsing + let recovered = TestAccount::from_mnemonic(&mnemonic) + .expect("Failed to recover account from mnemonic"); + + // Both should have the same address + let original_addr = original.address().expect("Failed to get original address"); + let recovered_addr = recovered + .address() + .expect("Failed to get recovered address"); + + assert_eq!(original_addr.to_string(), recovered_addr.to_string()); + assert_eq!(original.mnemonic(), recovered.mnemonic()); + + println!("✓ Successfully recovered account from mnemonic"); + println!(" Original: {}", original_addr); + println!(" Recovered: {}", recovered_addr); + } else { + println!("⚠ Skipping mnemonic round-trip test (using hex fallback)"); + } + } +} diff --git a/crates/algod_client_tests/src/fixtures.rs b/crates/algod_client_tests/src/fixtures.rs new file mode 100644 index 00000000..80316a36 --- /dev/null +++ b/crates/algod_client_tests/src/fixtures.rs @@ -0,0 +1,64 @@ +use algod_client::AlgodClient; +use algokit_transact::{PaymentTransactionBuilder, TransactionHeaderBuilder}; +use base64::{Engine, prelude::BASE64_STANDARD}; + +/// Global algod client instance - shared across all tests +/// Note: We use a function that returns a reference to avoid circular imports +pub fn get_algod_client() -> AlgodClient { + AlgodClient::localnet() +} + +/// Re-export the test utilities from algokit_transact +pub use algokit_transact::test_utils::{ + AddressMother, TestDataMother, TransactionHeaderMother, TransactionMother, +}; + +/// Extended transaction builders for localnet testing +pub struct LocalnetTransactionMother; + +impl LocalnetTransactionMother { + /// LocalNet header configuration (using dockernet genesis) + pub fn localnet_header() -> TransactionHeaderBuilder { + TransactionHeaderBuilder::default() + .genesis_id(String::from("dockernet-v1")) + .genesis_hash( + BASE64_STANDARD + .decode("R4ZGJ8m36vYb6qo6HwKKqb4ZRjP8IZNOCgdp42uJ2So=") + .unwrap() + .try_into() + .unwrap(), + ) + .fee(1000) + .to_owned() + } + + /// Simple localnet transaction header with sender and validity window + pub fn simple_localnet_header() -> TransactionHeaderBuilder { + Self::localnet_header() + .sender(AddressMother::address()) + .first_valid(1000) + .last_valid(2000) + .to_owned() + } + + /// Simple payment transaction for localnet testing + pub fn simple_payment() -> PaymentTransactionBuilder { + PaymentTransactionBuilder::default() + .header(Self::simple_localnet_header().build().unwrap()) + .amount(1000000) // 1 ALGO + .receiver(AddressMother::neil()) + .to_owned() + } + + /// Payment transaction with note for localnet testing + pub fn payment_with_note() -> PaymentTransactionBuilder { + Self::simple_payment() + .header( + Self::simple_localnet_header() + .note(b"test payment".to_vec()) + .build() + .unwrap(), + ) + .to_owned() + } +} diff --git a/crates/algod_client_tests/src/lib.rs b/crates/algod_client_tests/src/lib.rs new file mode 100644 index 00000000..ce8eea17 --- /dev/null +++ b/crates/algod_client_tests/src/lib.rs @@ -0,0 +1,11 @@ +pub mod account_helpers; +pub mod fixtures; +pub mod localnet; +pub mod mnemonic; + +// Re-export commonly used items for convenience +pub use account_helpers::{ + LocalNetDispenser, NetworkType, TestAccount, TestAccountConfig, TestAccountManager, +}; +pub use fixtures::*; +pub use localnet::LocalnetManager; diff --git a/crates/algod_client_tests/src/localnet.rs b/crates/algod_client_tests/src/localnet.rs new file mode 100644 index 00000000..2ee5b008 --- /dev/null +++ b/crates/algod_client_tests/src/localnet.rs @@ -0,0 +1,127 @@ +use std::process::Command; +use std::time::Duration; +use tokio::time::sleep; + +/// Utility functions for managing localnet for tests +pub struct LocalnetManager; + +impl LocalnetManager { + /// Check if localnet is running by trying to connect to the algod endpoint + pub async fn is_running() -> bool { + let client = reqwest::Client::new(); + let url = + std::env::var("ALGORAND_HOST").unwrap_or_else(|_| "http://localhost:4001".to_string()); + + match client.get(format!("{}/health", url)).send().await { + Ok(response) => response.status().is_success(), + Err(_) => false, + } + } + + /// Start localnet using algokit + pub fn start() -> Result<(), String> { + println!("Starting localnet..."); + + let output = Command::new("algokit") + .args(["localnet", "start"]) + .output() + .map_err(|e| format!("Failed to execute algokit: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("Failed to start localnet: {}", stderr)); + } + + println!("Localnet started successfully"); + Ok(()) + } + + /// Stop localnet using algokit + pub fn stop() -> Result<(), String> { + println!("Stopping localnet..."); + + let output = Command::new("algokit") + .args(["localnet", "stop"]) + .output() + .map_err(|e| format!("Failed to execute algokit: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("Failed to stop localnet: {}", stderr)); + } + + println!("Localnet stopped successfully"); + Ok(()) + } + + /// Reset localnet to a clean state + pub fn reset() -> Result<(), String> { + println!("Resetting localnet..."); + + let output = Command::new("algokit") + .args(["localnet", "reset"]) + .output() + .map_err(|e| format!("Failed to execute algokit: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("Failed to reset localnet: {}", stderr)); + } + + println!("Localnet reset successfully"); + Ok(()) + } + + /// Wait for localnet to be ready + pub async fn wait_for_ready(timeout_seconds: u64) -> Result<(), String> { + let start = std::time::Instant::now(); + let timeout = Duration::from_secs(timeout_seconds); + + while start.elapsed() < timeout { + if Self::is_running().await { + println!("Localnet is ready!"); + return Ok(()); + } + + sleep(Duration::from_millis(500)).await; + } + + Err(format!( + "Timeout waiting for localnet to be ready after {} seconds", + timeout_seconds + )) + } + + /// Ensure localnet is running, start if needed + pub async fn ensure_running() -> Result<(), String> { + if !Self::is_running().await { + Self::start()?; + Self::wait_for_ready(30).await?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + #[ignore] // Ignore by default, run with --ignored for integration tests + async fn test_localnet_management() { + // Test that we can check if localnet is running + let is_running = LocalnetManager::is_running().await; + println!("Localnet running: {}", is_running); + + // If not running, try to start it + if !is_running { + LocalnetManager::ensure_running() + .await + .expect("Failed to start localnet"); + assert!( + LocalnetManager::is_running().await, + "Localnet should be running after start" + ); + } + } +} diff --git a/crates/algod_client_tests/src/mnemonic.rs b/crates/algod_client_tests/src/mnemonic.rs new file mode 100644 index 00000000..938dc813 --- /dev/null +++ b/crates/algod_client_tests/src/mnemonic.rs @@ -0,0 +1,366 @@ +use sha2::{Digest, Sha512_256}; +use std::collections::HashMap; + +const BITS_PER_WORD: usize = 11; +const KEY_LEN_BYTES: usize = 32; +const MNEM_LEN_WORDS: usize = 25; // includes checksum word +const MNEMONIC_DELIM: &str = " "; + +static WORDLIST: &[&str] = &[ + "abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", + "abuse", "access", "accident", "account", "accuse", "achieve", "acid", "acoustic", "acquire", + "across", "act", "action", "actor", "actress", "actual", "adapt", "add", "addict", "address", + "adjust", "admit", "adult", "advance", "advice", "aerobic", "affair", "afford", "afraid", + "again", "age", "agent", "agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album", + "alcohol", "alert", "alien", "all", "alley", "allow", "almost", "alone", "alpha", "already", + "also", "alter", "always", "amateur", "amazing", "among", "amount", "amused", "analyst", + "anchor", "ancient", "anger", "angle", "angry", "animal", "ankle", "announce", "annual", + "another", "answer", "antenna", "antique", "anxiety", "any", "apart", "apology", "appear", + "apple", "approve", "april", "arch", "arctic", "area", "arena", "argue", "arm", "armed", + "armor", "army", "around", "arrange", "arrest", "arrive", "arrow", "art", "artefact", "artist", + "artwork", "ask", "aspect", "assault", "asset", "assist", "assume", "asthma", "athlete", + "atom", "attack", "attend", "attitude", "attract", "auction", "audit", "august", "aunt", + "author", "auto", "autumn", "average", "avocado", "avoid", "awake", "aware", "away", "awesome", + "awful", "awkward", "axis", "baby", "bachelor", "bacon", "badge", "bag", "balance", "balcony", + "ball", "bamboo", "banana", "banner", "bar", "barely", "bargain", "barrel", "base", "basic", + "basket", "battle", "beach", "bean", "beauty", "because", "become", "beef", "before", "begin", + "behave", "behind", "believe", "below", "belt", "bench", "benefit", "best", "betray", "better", + "between", "beyond", "bicycle", "bid", "bike", "bind", "biology", "bird", "birth", "bitter", + "black", "blade", "blame", "blanket", "blast", "bleak", "bless", "blind", "blood", "blossom", + "blouse", "blue", "blur", "blush", "board", "boat", "body", "boil", "bomb", "bone", "bonus", + "book", "boost", "border", "boring", "borrow", "boss", "bottom", "bounce", "box", "boy", + "bracket", "brain", "brand", "brass", "brave", "bread", "breeze", "brick", "bridge", "brief", + "bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom", "brother", "brown", + "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb", "bulk", "bullet", "bundle", + "bunker", "burden", "burger", "burst", "bus", "business", "busy", "butter", "buyer", "buzz", + "cabbage", "cabin", "cable", "cactus", "cage", "cake", "call", "calm", "camera", "camp", "can", + "canal", "cancel", "candy", "cannon", "canoe", "canvas", "canyon", "capable", "capital", + "captain", "car", "carbon", "card", "cargo", "carpet", "carry", "cart", "case", "cash", + "casino", "castle", "casual", "cat", "catalog", "catch", "category", "cattle", "caught", + "cause", "caution", "cave", "ceiling", "celery", "cement", "census", "century", "cereal", + "certain", "chair", "chalk", "champion", "change", "chaos", "chapter", "charge", "chase", + "chat", "cheap", "check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child", + "chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar", "cinnamon", + "circle", "citizen", "city", "civil", "claim", "clap", "clarify", "claw", "clay", "clean", + "clerk", "clever", "click", "client", "cliff", "climb", "clinic", "clip", "clock", "clog", + "close", "cloth", "cloud", "clown", "club", "clump", "cluster", "clutch", "coach", "coast", + "coconut", "code", "coffee", "coil", "coin", "collect", "color", "column", "combine", "come", + "comfort", "comic", "common", "company", "concert", "conduct", "confirm", "congress", + "connect", "consider", "control", "convince", "cook", "cool", "copper", "copy", "coral", + "core", "corn", "correct", "cost", "cotton", "couch", "country", "couple", "course", "cousin", + "cover", "coyote", "crack", "cradle", "craft", "cram", "crane", "crash", "crater", "crawl", + "crazy", "cream", "credit", "creek", "crew", "cricket", "crime", "crisp", "critic", "crop", + "cross", "crouch", "crowd", "crucial", "cruel", "cruise", "crumble", "crunch", "crush", "cry", + "crystal", "cube", "culture", "cup", "cupboard", "curious", "current", "curtain", "curve", + "cushion", "custom", "cute", "cycle", "dad", "damage", "damp", "dance", "danger", "daring", + "dash", "daughter", "dawn", "day", "deal", "debate", "debris", "decade", "december", "decide", + "decline", "decorate", "decrease", "deer", "defense", "define", "defy", "degree", "delay", + "deliver", "demand", "demise", "denial", "dentist", "deny", "depart", "depend", "deposit", + "depth", "deputy", "derive", "describe", "desert", "design", "desk", "despair", "destroy", + "detail", "detect", "develop", "device", "devote", "diagram", "dial", "diamond", "diary", + "dice", "diesel", "diet", "differ", "digital", "dignity", "dilemma", "dinner", "dinosaur", + "direct", "dirt", "disagree", "discover", "disease", "dish", "dismiss", "disorder", "display", + "distance", "divert", "divide", "divorce", "dizzy", "doctor", "document", "dog", "doll", + "dolphin", "domain", "donate", "donkey", "donor", "door", "dose", "double", "dove", "draft", + "dragon", "drama", "drastic", "draw", "dream", "dress", "drift", "drill", "drink", "drip", + "drive", "drop", "drum", "dry", "duck", "dumb", "dune", "during", "dust", "dutch", "duty", + "dwarf", "dynamic", "eager", "eagle", "early", "earn", "earth", "easily", "east", "easy", + "echo", "ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight", "either", + "elbow", "elder", "electric", "elegant", "element", "elephant", "elevator", "elite", "else", + "embark", "embody", "embrace", "emerge", "emotion", "employ", "empower", "empty", "enable", + "enact", "end", "endless", "endorse", "enemy", "energy", "enforce", "engage", "engine", + "enhance", "enjoy", "enlist", "enough", "enrich", "enroll", "ensure", "enter", "entire", + "entry", "envelope", "episode", "equal", "equip", "era", "erase", "erode", "erosion", "error", + "erupt", "escape", "essay", "essence", "estate", "eternal", "ethics", "evidence", "evil", + "evoke", "evolve", "exact", "example", "excess", "exchange", "excite", "exclude", "excuse", + "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit", "exotic", "expand", + "expect", "expire", "explain", "expose", "express", "extend", "extra", "eye", "eyebrow", + "fabric", "face", "faculty", "fade", "faint", "faith", "fall", "false", "fame", "family", + "famous", "fan", "fancy", "fantasy", "farm", "fashion", "fat", "fatal", "father", "fatigue", + "fault", "favorite", "feature", "february", "federal", "fee", "feed", "feel", "female", + "fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field", "figure", "file", + "film", "filter", "final", "find", "fine", "finger", "finish", "fire", "firm", "first", + "fiscal", "fish", "fit", "fitness", "fix", "flag", "flame", "flash", "flat", "flavor", "flee", + "flight", "flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly", "foam", + "focus", "fog", "foil", "fold", "follow", "food", "foot", "force", "forest", "forget", "fork", + "fortune", "forum", "forward", "fossil", "foster", "found", "fox", "fragile", "frame", + "frequent", "fresh", "friend", "fringe", "frog", "front", "frost", "frown", "frozen", "fruit", + "fuel", "fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy", "gallery", + "game", "gap", "garage", "garbage", "garden", "garlic", "garment", "gas", "gasp", "gate", + "gather", "gauge", "gaze", "general", "genius", "genre", "gentle", "genuine", "gesture", + "ghost", "giant", "gift", "giggle", "ginger", "giraffe", "girl", "give", "glad", "glance", + "glare", "glass", "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue", + "goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip", "govern", "gown", + "grab", "grace", "grain", "grant", "grape", "grass", "gravity", "great", "green", "grid", + "grief", "grit", "grocery", "group", "grow", "grunt", "guard", "guess", "guide", "guilt", + "guitar", "gun", "gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy", + "harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard", "head", "health", + "heart", "heavy", "hedgehog", "height", "hello", "helmet", "help", "hen", "hero", "hidden", + "high", "hill", "hint", "hip", "hire", "history", "hobby", "hockey", "hold", "hole", "holiday", + "hollow", "home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital", "host", + "hotel", "hour", "hover", "hub", "huge", "human", "humble", "humor", "hundred", "hungry", + "hunt", "hurdle", "hurry", "hurt", "husband", "hybrid", "ice", "icon", "idea", "identify", + "idle", "ignore", "ill", "illegal", "illness", "image", "imitate", "immense", "immune", + "impact", "impose", "improve", "impulse", "inch", "include", "income", "increase", "index", + "indicate", "indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", + "initial", "inject", "injury", "inmate", "inner", "innocent", "input", "inquiry", "insane", + "insect", "inside", "inspire", "install", "intact", "interest", "into", "invest", "invite", + "involve", "iron", "island", "isolate", "issue", "item", "ivory", "jacket", "jaguar", "jar", + "jazz", "jealous", "jeans", "jelly", "jewel", "job", "join", "joke", "journey", "joy", "judge", + "juice", "jump", "jungle", "junior", "junk", "just", "kangaroo", "keen", "keep", "ketchup", + "key", "kick", "kid", "kidney", "kind", "kingdom", "kiss", "kit", "kitchen", "kite", "kitten", + "kiwi", "knee", "knife", "knock", "know", "lab", "label", "labor", "ladder", "lady", "lake", + "lamp", "language", "laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law", + "lawn", "lawsuit", "layer", "lazy", "leader", "leaf", "learn", "leave", "lecture", "left", + "leg", "legal", "legend", "leisure", "lemon", "lend", "length", "lens", "leopard", "lesson", + "letter", "level", "liar", "liberty", "library", "license", "life", "lift", "light", "like", + "limb", "limit", "link", "lion", "liquid", "list", "little", "live", "lizard", "load", "loan", + "lobster", "local", "lock", "logic", "lonely", "long", "loop", "lottery", "loud", "lounge", + "love", "loyal", "lucky", "luggage", "lumber", "lunar", "lunch", "luxury", "lyrics", "machine", + "mad", "magic", "magnet", "maid", "mail", "main", "major", "make", "mammal", "man", "manage", + "mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin", "marine", + "market", "marriage", "mask", "mass", "master", "match", "material", "math", "matrix", + "matter", "maximum", "maze", "meadow", "mean", "measure", "meat", "mechanic", "medal", "media", + "melody", "melt", "member", "memory", "mention", "menu", "mercy", "merge", "merit", "merry", + "mesh", "message", "metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind", + "minimum", "minor", "minute", "miracle", "mirror", "misery", "miss", "mistake", "mix", "mixed", + "mixture", "mobile", "model", "modify", "mom", "moment", "monitor", "monkey", "monster", + "month", "moon", "moral", "more", "morning", "mosquito", "mother", "motion", "motor", + "mountain", "mouse", "move", "movie", "much", "muffin", "mule", "multiply", "muscle", "museum", + "mushroom", "music", "must", "mutual", "myself", "mystery", "myth", "naive", "name", "napkin", + "narrow", "nasty", "nation", "nature", "near", "neck", "need", "negative", "neglect", + "neither", "nephew", "nerve", "nest", "net", "network", "neutral", "never", "news", "next", + "nice", "night", "noble", "noise", "nominee", "noodle", "normal", "north", "nose", "notable", + "note", "nothing", "notice", "novel", "now", "nuclear", "number", "nurse", "nut", "oak", + "obey", "object", "oblige", "obscure", "observe", "obtain", "obvious", "occur", "ocean", + "october", "odor", "off", "offer", "office", "often", "oil", "okay", "old", "olive", "olympic", + "omit", "once", "one", "onion", "online", "only", "open", "opera", "opinion", "oppose", + "option", "orange", "orbit", "orchard", "order", "ordinary", "organ", "orient", "original", + "orphan", "ostrich", "other", "outdoor", "outer", "output", "outside", "oval", "oven", "over", + "own", "owner", "oxygen", "oyster", "ozone", "pact", "paddle", "page", "pair", "palace", + "palm", "panda", "panel", "panic", "panther", "paper", "parade", "parent", "park", "parrot", + "party", "pass", "patch", "path", "patient", "patrol", "pattern", "pause", "pave", "payment", + "peace", "peanut", "pear", "peasant", "pelican", "pen", "penalty", "pencil", "people", + "pepper", "perfect", "permit", "person", "pet", "phone", "photo", "phrase", "physical", + "piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot", "pink", "pioneer", + "pipe", "pistol", "pitch", "pizza", "place", "planet", "plastic", "plate", "play", "please", + "pledge", "pluck", "plug", "plunge", "poem", "poet", "point", "polar", "pole", "police", + "pond", "pony", "pool", "popular", "portion", "position", "possible", "post", "potato", + "pottery", "poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare", + "present", "pretty", "prevent", "price", "pride", "primary", "print", "priority", "prison", + "private", "prize", "problem", "process", "produce", "profit", "program", "project", "promote", + "proof", "property", "prosper", "protect", "proud", "provide", "public", "pudding", "pull", + "pulp", "pulse", "pumpkin", "punch", "pupil", "puppy", "purchase", "purity", "purpose", + "purse", "push", "put", "puzzle", "pyramid", "quality", "quantum", "quarter", "question", + "quick", "quit", "quiz", "quote", "rabbit", "raccoon", "race", "rack", "radar", "radio", + "rail", "rain", "raise", "rally", "ramp", "ranch", "random", "range", "rapid", "rare", "rate", + "rather", "raven", "raw", "razor", "ready", "real", "reason", "rebel", "rebuild", "recall", + "receive", "recipe", "record", "recycle", "reduce", "reflect", "reform", "refuse", "region", + "regret", "regular", "reject", "relax", "release", "relief", "rely", "remain", "remember", + "remind", "remove", "render", "renew", "rent", "reopen", "repair", "repeat", "replace", + "report", "require", "rescue", "resemble", "resist", "resource", "response", "result", + "retire", "retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib", + "ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid", "ring", "riot", "ripple", + "risk", "ritual", "rival", "river", "road", "roast", "robot", "robust", "rocket", "romance", + "roof", "rookie", "room", "rose", "rotate", "rough", "round", "route", "royal", "rubber", + "rude", "rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness", "safe", "sail", + "salad", "salmon", "salon", "salt", "salute", "same", "sample", "sand", "satisfy", "satoshi", + "sauce", "sausage", "save", "say", "scale", "scan", "scare", "scatter", "scene", "scheme", + "school", "science", "scissors", "scorpion", "scout", "scrap", "screen", "script", "scrub", + "sea", "search", "season", "seat", "second", "secret", "section", "security", "seed", "seek", + "segment", "select", "sell", "seminar", "senior", "sense", "sentence", "series", "service", + "session", "settle", "setup", "seven", "shadow", "shaft", "shallow", "share", "shed", "shell", + "sheriff", "shield", "shift", "shine", "ship", "shiver", "shock", "shoe", "shoot", "shop", + "short", "shoulder", "shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side", + "siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar", "simple", "since", + "sing", "siren", "sister", "situate", "six", "size", "skate", "sketch", "ski", "skill", "skin", + "skirt", "skull", "slab", "slam", "sleep", "slender", "slice", "slide", "slight", "slim", + "slogan", "slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth", "snack", + "snake", "snap", "sniff", "snow", "soap", "soccer", "social", "sock", "soda", "soft", "solar", + "soldier", "solid", "solution", "solve", "someone", "song", "soon", "sorry", "sort", "soul", + "sound", "soup", "source", "south", "space", "spare", "spatial", "spawn", "speak", "special", + "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin", "spirit", "split", + "spoil", "sponsor", "spoon", "sport", "spot", "spray", "spread", "spring", "spy", "square", + "squeeze", "squirrel", "stable", "stadium", "staff", "stage", "stairs", "stamp", "stand", + "start", "state", "stay", "steak", "steel", "stem", "step", "stereo", "stick", "still", + "sting", "stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street", + "strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject", "submit", + "subway", "success", "such", "sudden", "suffer", "sugar", "suggest", "suit", "summer", "sun", + "sunny", "sunset", "super", "supply", "supreme", "sure", "surface", "surge", "surprise", + "surround", "survey", "suspect", "sustain", "swallow", "swamp", "swap", "swarm", "swear", + "sweet", "swift", "swim", "swing", "switch", "sword", "symbol", "symptom", "syrup", "system", + "table", "tackle", "tag", "tail", "talent", "talk", "tank", "tape", "target", "task", "taste", + "tattoo", "taxi", "teach", "team", "tell", "ten", "tenant", "tennis", "tent", "term", "test", + "text", "thank", "that", "theme", "then", "theory", "there", "they", "thing", "this", + "thought", "three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger", "tilt", + "timber", "time", "tiny", "tip", "tired", "tissue", "title", "toast", "tobacco", "today", + "toddler", "toe", "together", "toilet", "token", "tomato", "tomorrow", "tone", "tongue", + "tonight", "tool", "tooth", "top", "topic", "topple", "torch", "tornado", "tortoise", "toss", + "total", "tourist", "toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic", + "train", "transfer", "trap", "trash", "travel", "tray", "treat", "tree", "trend", "trial", + "tribe", "trick", "trigger", "trim", "trip", "trophy", "trouble", "truck", "true", "truly", + "trumpet", "trust", "truth", "try", "tube", "tuition", "tumble", "tuna", "tunnel", "turkey", + "turn", "turtle", "twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical", + "ugly", "umbrella", "unable", "unaware", "uncle", "uncover", "under", "undo", "unfair", + "unfold", "unhappy", "uniform", "unique", "unit", "universe", "unknown", "unlock", "until", + "unusual", "unveil", "update", "upgrade", "uphold", "upon", "upper", "upset", "urban", "urge", + "usage", "use", "used", "useful", "useless", "usual", "utility", "vacant", "vacuum", "vague", + "valid", "valley", "valve", "van", "vanish", "vapor", "various", "vast", "vault", "vehicle", + "velvet", "vendor", "venture", "venue", "verb", "verify", "version", "very", "vessel", + "veteran", "viable", "vibrant", "vicious", "victory", "video", "view", "village", "vintage", + "violin", "virtual", "virus", "visa", "visit", "visual", "vital", "vivid", "vocal", "voice", + "void", "volcano", "volume", "vote", "voyage", "wage", "wagon", "wait", "walk", "wall", + "walnut", "want", "warfare", "warm", "warrior", "wash", "wasp", "waste", "water", "wave", + "way", "wealth", "weapon", "wear", "weasel", "weather", "web", "wedding", "weekend", "weird", + "welcome", "west", "wet", "whale", "what", "wheat", "wheel", "when", "where", "whip", + "whisper", "wide", "width", "wife", "wild", "will", "win", "window", "wine", "wing", "wink", + "winner", "winter", "wire", "wisdom", "wise", "wish", "witness", "wolf", "woman", "wonder", + "wood", "wool", "word", "work", "world", "worry", "worth", "wrap", "wreck", "wrestle", "wrist", + "write", "wrong", "yard", "year", "yellow", "you", "young", "youth", "zebra", "zero", "zone", + "zoo", +]; + +lazy_static::lazy_static! { + static ref WORD_TO_INDEX: HashMap<&'static str, usize> = { + let mut map = HashMap::new(); + for (i, word) in WORDLIST.iter().enumerate() { + map.insert(*word, i); + } + map + }; +} + +/// Error type for mnemonic operations +#[derive(Debug)] +pub enum MnemonicError { + InvalidKeyLength, + InvalidMnemonicLength, + InvalidWordsInMnemonic, + InvalidChecksum, +} + +impl std::fmt::Display for MnemonicError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MnemonicError::InvalidKeyLength => write!(f, "Invalid key length"), + MnemonicError::InvalidMnemonicLength => write!(f, "Invalid mnemonic length"), + MnemonicError::InvalidWordsInMnemonic => write!(f, "Invalid words in mnemonic"), + MnemonicError::InvalidChecksum => write!(f, "Invalid checksum"), + } + } +} + +impl std::error::Error for MnemonicError {} + +/// Converts a 32-byte key into a 25 word mnemonic. The generated +/// mnemonic includes a checksum. Each word in the mnemonic represents 11 bits +/// of data, and the last 11 bits are reserved for the checksum. +pub fn from_key(key: &[u8]) -> Result { + if key.len() != KEY_LEN_BYTES { + return Err(MnemonicError::InvalidKeyLength); + } + let check_word = checksum(key)?; + let mut words: Vec<_> = to_u11_array(key) + .into_iter() + .map(get_word) + .collect::, _>>()?; + words.push(check_word); + Ok(words.join(MNEMONIC_DELIM)) +} + +/// Converts a mnemonic generated using the library into the source +/// key used to create it. It returns an error if the passed mnemonic has +/// an incorrect checksum, if the number of words is unexpected, or if one +/// of the passed words is not found in the words list. +pub fn to_key(string: &str) -> Result<[u8; KEY_LEN_BYTES], MnemonicError> { + let mut mnemonic: Vec<&str> = string.split(MNEMONIC_DELIM).collect(); + if mnemonic.len() != MNEM_LEN_WORDS { + return Err(MnemonicError::InvalidMnemonicLength); + } + let check_word = mnemonic.pop().unwrap(); + let mut nums = Vec::with_capacity(mnemonic.len()); + for word in mnemonic { + let n = WORD_TO_INDEX + .get(word) + .ok_or(MnemonicError::InvalidWordsInMnemonic)?; + nums.push(*n as u32); + } + let mut bytes = to_byte_array(&nums); + if bytes.len() != KEY_LEN_BYTES + 1 { + return Err(MnemonicError::InvalidKeyLength); + } + let _ = bytes.pop(); + if check_word != checksum(&bytes)? { + return Err(MnemonicError::InvalidChecksum); + } + let mut key = [0; KEY_LEN_BYTES]; + key.copy_from_slice(&bytes); + Ok(key) +} + +// Returns a word corresponding to the 11 bit checksum of the data +fn checksum(data: &[u8]) -> Result<&'static str, MnemonicError> { + let d = Sha512_256::digest(data); + get_word(to_u11_array(&d[0..2])[0]) +} + +// Assumes little-endian +fn to_u11_array(bytes: &[u8]) -> Vec { + let mut buf = 0u32; + let mut bit_count = 0; + let mut out = Vec::with_capacity((bytes.len() * 8).div_ceil(BITS_PER_WORD)); + for &b in bytes { + buf |= (u32::from(b)) << bit_count; + bit_count += 8; + if bit_count >= BITS_PER_WORD as u32 { + out.push(buf & 0x7ff); + buf >>= BITS_PER_WORD as u32; + bit_count -= BITS_PER_WORD as u32; + } + } + if bit_count != 0 { + out.push(buf & 0x7ff); + } + out +} + +// takes an array of 11 byte numbers and converts them to 8 bit numbers +fn to_byte_array(nums: &[u32]) -> Vec { + let mut buf = 0; + let mut bit_count = 0; + let mut out = Vec::with_capacity((nums.len() * BITS_PER_WORD).div_ceil(8)); + for &n in nums { + buf |= n << bit_count; + bit_count += BITS_PER_WORD as u32; + while bit_count >= 8 { + out.push((buf & 0xff) as u8); + buf >>= 8; + bit_count -= 8; + } + } + if bit_count != 0 { + out.push((buf & 0xff) as u8) + } + out +} + +// Gets the word corresponding to the 11 bit number from the word list +fn get_word(i: u32) -> Result<&'static str, MnemonicError> { + WORDLIST + .get(i as usize) + .copied() + .ok_or(MnemonicError::InvalidWordsInMnemonic) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_round_trip() { + let key = [1u8; 32]; + let mnemonic = from_key(&key).unwrap(); + let recovered_key = to_key(&mnemonic).unwrap(); + assert_eq!(key, recovered_key); + } +} diff --git a/crates/algod_client_tests/tests/pending_transaction_information.rs b/crates/algod_client_tests/tests/pending_transaction_information.rs new file mode 100644 index 00000000..0d747265 --- /dev/null +++ b/crates/algod_client_tests/tests/pending_transaction_information.rs @@ -0,0 +1,98 @@ +use algod_client::apis::Format; +use algod_client_tests::{ + LocalnetManager, NetworkType, TestAccountConfig, TestAccountManager, get_algod_client, +}; +use algokit_transact::{PaymentTransactionBuilder, Transaction, TransactionHeaderBuilder}; +use std::convert::TryInto; + +#[tokio::test] +async fn test_pending_transaction_broadcast() { + LocalnetManager::ensure_running() + .await + .expect("Failed to start localnet"); + + // Create account manager and generate test accounts + let mut account_manager = TestAccountManager::new(get_algod_client()); + + let sender_config = TestAccountConfig { + initial_funds: 10_000_000, // 10 ALGO + suppress_log: true, + network_type: NetworkType::LocalNet, + funding_note: Some("Test sender account".to_string()), + }; + + let receiver_config = TestAccountConfig { + initial_funds: 1_000_000, // 1 ALGO + suppress_log: true, + network_type: NetworkType::LocalNet, + funding_note: Some("Test receiver account".to_string()), + }; + + let sender = account_manager + .get_test_account(Some(sender_config)) + .await + .expect("Failed to create sender account"); + + let receiver = account_manager + .get_test_account(Some(receiver_config)) + .await + .expect("Failed to create receiver account"); + + let sender_addr = sender.address().expect("Failed to get sender address"); + let receiver_addr = receiver.address().expect("Failed to get receiver address"); + + // Get transaction parameters + let params = get_algod_client() + .transaction_params() + .await + .expect("Failed to get transaction params"); + + // Convert genesis hash to 32-byte array + let genesis_hash_bytes: [u8; 32] = params + .genesis_hash + .try_into() + .expect("Genesis hash must be 32 bytes"); + + // Build transaction header + let header = TransactionHeaderBuilder::default() + .sender(sender_addr.clone()) + .fee(params.min_fee) + .first_valid(params.last_round) + .last_valid(params.last_round + 1000) + .genesis_id(params.genesis_id.clone()) + .genesis_hash(genesis_hash_bytes) + .note(b"Test payment transaction".to_vec()) + .build() + .expect("Failed to build transaction header"); + + // Build payment transaction + let payment_fields = PaymentTransactionBuilder::default() + .header(header) + .receiver(receiver_addr) + .amount(500_000) // 0.5 ALGO + .build_fields() + .expect("Failed to build payment fields"); + + let transaction = Transaction::Payment(payment_fields); + let signed_bytes = sender + .sign_transaction(&transaction) + .expect("Failed to sign transaction"); + + let response = get_algod_client() + .raw_transaction(signed_bytes) + .await + .expect("Failed to broadcast transaction"); + + assert!( + !response.tx_id.is_empty(), + "Response should contain a transaction ID" + ); + + let pending_transaction = get_algod_client() + .pending_transaction_information(&response.tx_id, Some(Format::Msgpack)) + .await + .expect("Failed to get pending transaction information"); + + assert_eq!(pending_transaction.pool_error, ""); + assert!(pending_transaction.confirmed_round.is_some()); +} diff --git a/crates/algod_client_tests/tests/raw_transaction.rs b/crates/algod_client_tests/tests/raw_transaction.rs new file mode 100644 index 00000000..aef1bcc8 --- /dev/null +++ b/crates/algod_client_tests/tests/raw_transaction.rs @@ -0,0 +1,89 @@ +use algod_client_tests::{ + LocalnetManager, NetworkType, TestAccountConfig, TestAccountManager, get_algod_client, +}; +use algokit_transact::{PaymentTransactionBuilder, Transaction, TransactionHeaderBuilder}; +use std::convert::TryInto; + +#[tokio::test] +async fn test_raw_transaction_broadcast() { + LocalnetManager::ensure_running() + .await + .expect("Failed to start localnet"); + + // Create account manager and generate test accounts + let mut account_manager = TestAccountManager::new(get_algod_client()); + + let sender_config = TestAccountConfig { + initial_funds: 10_000_000, // 10 ALGO + suppress_log: true, + network_type: NetworkType::LocalNet, + funding_note: Some("Test sender account".to_string()), + }; + + let receiver_config = TestAccountConfig { + initial_funds: 1_000_000, // 1 ALGO + suppress_log: true, + network_type: NetworkType::LocalNet, + funding_note: Some("Test receiver account".to_string()), + }; + + let sender = account_manager + .get_test_account(Some(sender_config)) + .await + .expect("Failed to create sender account"); + + let receiver = account_manager + .get_test_account(Some(receiver_config)) + .await + .expect("Failed to create receiver account"); + + let sender_addr = sender.address().expect("Failed to get sender address"); + let receiver_addr = receiver.address().expect("Failed to get receiver address"); + + // Get transaction parameters + let params = get_algod_client() + .transaction_params() + .await + .expect("Failed to get transaction params"); + + // Convert genesis hash to 32-byte array + let genesis_hash_bytes: [u8; 32] = params + .genesis_hash + .try_into() + .expect("Genesis hash must be 32 bytes"); + + // Build transaction header + let header = TransactionHeaderBuilder::default() + .sender(sender_addr.clone()) + .fee(params.min_fee) + .first_valid(params.last_round) + .last_valid(params.last_round + 1000) + .genesis_id(params.genesis_id.clone()) + .genesis_hash(genesis_hash_bytes) + .note(b"Test payment transaction".to_vec()) + .build() + .expect("Failed to build transaction header"); + + // Build payment transaction + let payment_fields = PaymentTransactionBuilder::default() + .header(header) + .receiver(receiver_addr) + .amount(500_000) // 0.5 ALGO + .build_fields() + .expect("Failed to build payment fields"); + + let transaction = Transaction::Payment(payment_fields); + let signed_bytes = sender + .sign_transaction(&transaction) + .expect("Failed to sign transaction"); + + let response = get_algod_client() + .raw_transaction(signed_bytes) + .await + .expect("Failed to broadcast transaction"); + + assert!( + !response.tx_id.is_empty(), + "Response should contain a transaction ID" + ); +} diff --git a/crates/algod_client_tests/tests/simulate_transactions.rs b/crates/algod_client_tests/tests/simulate_transactions.rs new file mode 100644 index 00000000..76ad4df5 --- /dev/null +++ b/crates/algod_client_tests/tests/simulate_transactions.rs @@ -0,0 +1,80 @@ +// Simulate transaction tests +// These tests demonstrate the integration test structure and API communication + +use algod_client::{ + apis::Format, + models::{SimulateRequest, SimulateRequestTransactionGroup, SimulateTraceConfig}, +}; +use algod_client_tests::{LocalnetManager, LocalnetTransactionMother, get_algod_client}; +use algokit_transact::SignedTransaction; + +#[tokio::test] +async fn test_simulate_transactions() { + LocalnetManager::ensure_running() + .await + .expect("Failed to start localnet"); + + // Create multiple transactions for group simulation using LocalnetTransactionMother + let transaction1 = LocalnetTransactionMother::simple_payment().build().unwrap(); + let transaction2 = LocalnetTransactionMother::payment_with_note() + .build() + .unwrap(); + + let signed_transactions = vec![ + SignedTransaction { + transaction: transaction1, + signature: None, + auth_address: None, + }, + SignedTransaction { + transaction: transaction2, + signature: None, + auth_address: None, + }, + ]; + + let txn_group = SimulateRequestTransactionGroup { + txns: signed_transactions.clone(), + }; + + let exec_trace_config = SimulateTraceConfig { + enable: Some(true), + stack_change: Some(true), + scratch_change: Some(true), + state_change: Some(true), + }; + + let simulate_request = SimulateRequest { + txn_groups: vec![txn_group], + allow_empty_signatures: Some(true), + allow_more_logging: Some(true), + allow_unnamed_resources: Some(true), + round: None, + extra_opcode_budget: Some(1000), + exec_trace_config: Some(exec_trace_config), + fix_signers: Some(true), + }; + + // Call the simulate transaction endpoint + let result = get_algod_client() + .simulate_transaction(simulate_request, Some(Format::Msgpack)) + .await; + + assert!( + result.is_ok(), + "Multi-transaction simulation should succeed: {:?}", + result.err() + ); + + let response = result.unwrap(); + assert_eq!( + response.txn_groups.len(), + 1, + "Should have one transaction group" + ); + assert_eq!( + response.txn_groups[0].txn_results.len(), + 2, + "Should have two transaction results" + ); +} diff --git a/crates/algod_client_tests/tests/transaction_params.rs b/crates/algod_client_tests/tests/transaction_params.rs new file mode 100644 index 00000000..01c3c21a --- /dev/null +++ b/crates/algod_client_tests/tests/transaction_params.rs @@ -0,0 +1,44 @@ +use algod_client::AlgodClient; +use algod_client_tests::{LocalnetManager, get_algod_client}; +use algokit_http_client::DefaultHttpClient; +use std::sync::Arc; + +#[tokio::test] +async fn test_get_transaction_params() { + // Ensure localnet is running + LocalnetManager::ensure_running() + .await + .expect("Failed to start localnet"); + + // Call the transaction params endpoint + let result = get_algod_client().transaction_params().await; + + // Verify the call succeeded + assert!( + result.is_ok(), + "Get transaction params should succeed: {:?}", + result.err() + ); + + let response = result.unwrap(); + + // Basic response validation + assert!( + !response.genesis_id.is_empty(), + "Genesis ID should not be empty" + ); + assert!( + !response.genesis_hash.is_empty(), + "Genesis hash should not be empty" + ); +} + +#[tokio::test] +async fn test_transaction_params_error_handling() { + let http_client = Arc::new(DefaultHttpClient::new("http://invalid-host:9999")); + let invalid_client = AlgodClient::new(http_client); + let result = invalid_client.transaction_params().await; + + // This should fail due to connection error + assert!(result.is_err(), "Invalid host should result in error"); +} diff --git a/crates/algokit_http_client/Cargo.toml b/crates/algokit_http_client/Cargo.toml index 2325bc36..7fb8d26c 100644 --- a/crates/algokit_http_client/Cargo.toml +++ b/crates/algokit_http_client/Cargo.toml @@ -6,14 +6,22 @@ edition = "2024" [features] default = ["default_client"] ffi_uniffi = ["dep:uniffi"] -ffi_wasm = ["dep:wasm-bindgen", "dep:js-sys"] +ffi_wasm = [ + "dep:wasm-bindgen", + "dep:js-sys", + "dep:tsify-next", + "dep:serde-wasm-bindgen", +] default_client = ["dep:reqwest"] [dependencies] async-trait = "0.1.88" js-sys = { workspace = true, optional = true } reqwest = { version = "0.12.19", optional = true } +serde = { version = "1.0", features = ["derive"] } +serde-wasm-bindgen = { version = "0.6", optional = true } thiserror.workspace = true +tsify-next = { workspace = true, optional = true } uniffi = { workspace = true, optional = true } wasm-bindgen = { workspace = true, optional = true } wasm-bindgen-futures = "0.4.50" diff --git a/crates/algokit_http_client/src/lib.rs b/crates/algokit_http_client/src/lib.rs index 4395cba5..dadadffa 100644 --- a/crates/algokit_http_client/src/lib.rs +++ b/crates/algokit_http_client/src/lib.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use std::collections::HashMap; #[cfg(feature = "ffi_uniffi")] uniffi::setup_scaffolding!(); @@ -7,7 +8,46 @@ uniffi::setup_scaffolding!(); #[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Error))] pub enum HttpError { #[error("HttpError: {0}")] - HttpError(String), + RequestError(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Enum))] +#[cfg_attr(feature = "ffi_wasm", derive(tsify_next::Tsify))] +#[cfg_attr(feature = "ffi_wasm", tsify(into_wasm_abi, from_wasm_abi))] +#[cfg_attr(feature = "ffi_wasm", derive(serde::Serialize, serde::Deserialize))] +pub enum HttpMethod { + Get, + Post, + Put, + Delete, + Patch, + Head, + Options, +} + +impl HttpMethod { + pub fn as_str(&self) -> &'static str { + match self { + HttpMethod::Get => "GET", + HttpMethod::Post => "POST", + HttpMethod::Put => "PUT", + HttpMethod::Delete => "DELETE", + HttpMethod::Patch => "PATCH", + HttpMethod::Head => "HEAD", + HttpMethod::Options => "OPTIONS", + } + } +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "ffi_wasm", derive(tsify_next::Tsify))] +#[cfg_attr(feature = "ffi_wasm", tsify(into_wasm_abi, from_wasm_abi))] +#[cfg_attr(feature = "ffi_wasm", derive(serde::Serialize, serde::Deserialize))] +pub struct HttpResponse { + pub body: Vec, + pub headers: HashMap, } #[cfg(not(feature = "ffi_wasm"))] @@ -18,54 +58,146 @@ pub enum HttpError { /// /// By default, this trait requires the implementing type to be `Send + Sync`. /// For WASM targets, enable the `ffi_wasm` feature to use a different implementation that is compatible with WASM. -/// -/// With the `ffi_uniffi` feature enabled, this is exported as a foreign trait, meaning it is implemented natively in the foreign language. -/// pub trait HttpClient: Send + Sync { - async fn get(&self, path: String) -> Result, HttpError>; + async fn request( + &self, + method: HttpMethod, + path: String, + query: Option>, + body: Option>, + headers: Option>, + ) -> Result; } #[cfg(feature = "default_client")] pub struct DefaultHttpClient { - host: String, + client: reqwest::Client, + base_url: String, } #[cfg(feature = "default_client")] impl DefaultHttpClient { - pub fn new(host: &str) -> Self { + pub fn new(base_url: &str) -> Self { DefaultHttpClient { - host: host.to_string(), + client: reqwest::Client::new(), + base_url: base_url.to_string(), } } + + pub fn with_header( + base_url: &str, + header_name: &str, + header_value: &str, + ) -> Result { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::HeaderName::from_bytes(header_name.as_bytes()).map_err(|e| { + HttpError::RequestError(format!("Invalid header name '{}': {}", header_name, e)) + })?, + reqwest::header::HeaderValue::from_str(header_value).map_err(|e| { + HttpError::RequestError(format!("Invalid header value '{}': {}", header_value, e)) + })?, + ); + let client = reqwest::Client::builder() + .default_headers(headers) + .build() + .map_err(|e| HttpError::RequestError(format!("Failed to build HTTP client: {}", e)))?; + Ok(DefaultHttpClient { + client, + base_url: base_url.to_string(), + }) + } } #[cfg(feature = "default_client")] #[cfg_attr(feature = "ffi_wasm", async_trait(?Send))] #[cfg_attr(not(feature = "ffi_wasm"), async_trait)] impl HttpClient for DefaultHttpClient { - async fn get(&self, path: String) -> Result, HttpError> { - let response = reqwest::get(self.host.clone() + &path) + async fn request( + &self, + method: HttpMethod, + path: String, + query: Option>, + body: Option>, + headers: Option>, + ) -> Result { + let url = format!("{}{}", self.base_url, path); + let method = reqwest::Method::from_bytes(method.as_str().as_bytes()) + .map_err(|e| HttpError::RequestError(e.to_string()))?; + + let mut request_builder = self.client.request(method, &url); + + if let Some(query_params) = query { + request_builder = request_builder.query(&query_params); + } + + if let Some(header_params) = headers { + for (key, value) in header_params { + request_builder = request_builder.header(key, value); + } + } + + if let Some(body_data) = body { + request_builder = request_builder.body(body_data); + } + + let response = request_builder + .send() .await - .map_err(|e| HttpError::HttpError(e.to_string()))? + .map_err(|e| HttpError::RequestError(e.to_string()))?; + + if !response.status().is_success() { + let status = response.status(); + let text = response + .text() + .await + .unwrap_or_else(|_| "Failed to read error response text".to_string()); + return Err(HttpError::RequestError(format!( + "Request failed with status {}: {}", + status, text + ))); + } + + let response_headers = response + .headers() + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) + .collect(); + + let body = response .bytes() .await - .map_err(|e| HttpError::HttpError(e.to_string()))? + .map_err(|e| HttpError::RequestError(e.to_string()))? .to_vec(); - Ok(response) + Ok(HttpResponse { + body, + headers: response_headers, + }) } } +// WASM-specific implementations #[cfg(feature = "ffi_wasm")] use wasm_bindgen::prelude::*; #[cfg(feature = "ffi_wasm")] use js_sys::Uint8Array; +#[cfg(feature = "ffi_wasm")] +use tsify_next::Tsify; + #[cfg(feature = "ffi_wasm")] #[async_trait(?Send)] pub trait HttpClient { - async fn get(&self, path: String) -> Result, HttpError>; + async fn request( + &self, + method: HttpMethod, + path: String, + query: Option>, + body: Option>, + headers: Option>, + ) -> Result; } #[wasm_bindgen] @@ -77,22 +209,62 @@ extern "C" { pub type WasmHttpClient; #[wasm_bindgen(method, catch)] - async fn get(this: &WasmHttpClient, path: &str) -> Result; + async fn request( + this: &WasmHttpClient, + method: &str, + path: &str, + query: &JsValue, + body: &JsValue, + headers: &JsValue, + ) -> Result; } #[cfg(feature = "ffi_wasm")] #[async_trait(?Send)] impl HttpClient for WasmHttpClient { - async fn get(&self, path: String) -> Result, HttpError> { - let result = self.get(&path).await.map_err(|e| { - HttpError::HttpError( - e.as_string().unwrap_or( - "A HTTP error ocurred in JavaScript, but it cannot be converted to a string" - .to_string(), - ), - ) - })?; - - Ok(result.to_vec()) + async fn request( + &self, + method: HttpMethod, + path: String, + query: Option>, + body: Option>, + headers: Option>, + ) -> Result { + let query_js = match query { + Some(q) => serde_wasm_bindgen::to_value(&q).unwrap_or(JsValue::NULL), + None => JsValue::NULL, + }; + + let body_js = match body { + Some(b) => { + let array = Uint8Array::new_with_length(b.len() as u32); + array.copy_from(&b); + array.into() + } + None => JsValue::NULL, + }; + + let headers_js = match headers { + Some(h) => serde_wasm_bindgen::to_value(&h).unwrap_or(JsValue::NULL), + None => JsValue::NULL, + }; + + let result = self + .request(method.as_str(), &path, &query_js, &body_js, &headers_js) + .await + .map_err(|e| { + HttpError::RequestError( + e.as_string().unwrap_or( + "A HTTP error occurred in JavaScript, but it cannot be converted to a string" + .to_string(), + ), + ) + })?; + + // Parse the response from JavaScript + let response = HttpResponse::from_js(result) + .map_err(|e| HttpError::RequestError(format!("Failed to parse response: {:?}", e)))?; + + Ok(response) } } diff --git a/crates/algokit_transact/Cargo.toml b/crates/algokit_transact/Cargo.toml index 72b911d1..2d8e80d6 100644 --- a/crates/algokit_transact/Cargo.toml +++ b/crates/algokit_transact/Cargo.toml @@ -4,11 +4,11 @@ version = "0.1.0" edition = "2021" [features] -test_utils = ["dep:base64", "dep:ed25519-dalek", "dep:convert_case"] +test_utils = ["dep:ed25519-dalek", "dep:convert_case"] [dependencies] base32 = "0.5.1" -base64 = { version = "0.22.1", optional = true } +base64 = "0.22.1" convert_case = { version = "0.8.0", optional = true } derive_builder = { version = "0.20.2" } ed25519-dalek = { version = "2.1.1", optional = true } diff --git a/crates/algokit_transact/src/lib.rs b/crates/algokit_transact/src/lib.rs index 3a3f7e65..04fcb054 100644 --- a/crates/algokit_transact/src/lib.rs +++ b/crates/algokit_transact/src/lib.rs @@ -1,7 +1,6 @@ mod address; pub mod constants; mod error; -pub mod msgpack; mod traits; mod transactions; mod utils; @@ -19,13 +18,6 @@ pub use transactions::{ Transaction, TransactionHeader, TransactionHeaderBuilder, }; -// Re-export msgpack functionality -pub use msgpack::{ - decode_base64_msgpack_to_json, decode_msgpack_to_json, encode_json_to_base64_msgpack, - encode_json_to_msgpack, sort_and_filter_json, supported_models, AlgoKitMsgPackError, - ModelRegistry, ModelType, ToMsgPack, -}; - #[cfg(test)] mod tests; diff --git a/crates/algokit_transact/src/msgpack/mod.rs b/crates/algokit_transact/src/msgpack/mod.rs deleted file mode 100644 index d276d65e..00000000 --- a/crates/algokit_transact/src/msgpack/mod.rs +++ /dev/null @@ -1,394 +0,0 @@ -use base64::engine::general_purpose::STANDARD as BASE64; -use base64::Engine; -use rmp::encode::{self as rmp_encode, ValueWriteError}; -use serde::{de::DeserializeOwned, Serialize}; -use serde_json::Value; -use std::collections::{BTreeMap, HashMap}; -use thiserror::Error; - -mod models; -pub use models::*; - -#[derive(Debug, Error)] -pub enum AlgoKitMsgPackError { - #[error("Error occurred during serialization: {0}")] - SerializationError(#[from] serde_json::Error), - #[error("Error occurred during msgpack encoding: {0}")] - MsgpackEncodingError(#[from] rmp_serde::encode::Error), - #[error("Error occurred during msgpack decoding: {0}")] - MsgpackDecodingError(#[from] rmp_serde::decode::Error), - #[error("Error occurred during base64 decoding: {0}")] - Base64DecodingError(#[from] base64::DecodeError), - #[error("Error occurred during msgpack writing: {0}")] - MsgpackWriteError(String), - #[error("Unknown model type: {0}")] - UnknownModelError(String), - #[error("IO error: {0}")] - IoError(String), - #[error("Error occurred during value writing: {0}")] - ValueWriteError(String), -} - -impl From for AlgoKitMsgPackError { - fn from(err: std::io::Error) -> Self { - AlgoKitMsgPackError::IoError(err.to_string()) - } -} - -impl From for AlgoKitMsgPackError { - fn from(err: ValueWriteError) -> Self { - AlgoKitMsgPackError::ValueWriteError(format!("{:?}", err)) - } -} - -pub type Result = std::result::Result; - -pub trait ToMsgPack: Serialize { - fn to_msg_pack(&self) -> Result> { - let json_value = serde_json::to_value(self)?; - let processed_value = sort_and_filter_json(json_value)?; - let mut buf = Vec::new(); - encode_value_to_msgpack(&processed_value, &mut buf)?; - Ok(buf) - } - - fn to_msg_pack_base64(&self) -> Result { - let bytes = self.to_msg_pack()?; - Ok(BASE64.encode(&bytes)) - } -} - -pub fn sort_and_filter_json(value: Value) -> Result { - match value { - Value::Object(map) => { - let mut sorted_map = BTreeMap::new(); - for (key, val) in map { - let processed_val = sort_and_filter_json(val)?; - if !is_zero_value(&processed_val) { - sorted_map.insert(key, processed_val); - } - } - Ok(Value::Object(serde_json::Map::from_iter(sorted_map))) - } - Value::Array(arr) => { - let mut new_arr = Vec::with_capacity(arr.len()); - for item in arr { - new_arr.push(sort_and_filter_json(item)?); - } - Ok(Value::Array(new_arr)) - } - _ => Ok(value), - } -} - -fn is_zero_value(value: &Value) -> bool { - match value { - Value::Null => true, - Value::Bool(b) => !b, - Value::Number(n) => { - if n.is_i64() { - n.as_i64().unwrap() == 0 - } else if n.is_u64() { - n.as_u64().unwrap() == 0 - } else if n.is_f64() { - n.as_f64().unwrap() == 0.0 - } else { - false - } - } - Value::String(s) => s.is_empty(), - Value::Array(a) => a.is_empty(), - Value::Object(o) => o.is_empty(), - } -} - -pub(crate) fn encode_value_to_msgpack(value: &Value, buf: &mut Vec) -> Result<()> { - match value { - Value::Null => rmp_encode::write_nil(buf)?, - Value::Bool(b) => rmp_encode::write_bool(buf, *b)?, - Value::Number(n) => { - if let Some(i) = n.as_i64() { - rmp_encode::write_i64(buf, i)?; - } else if let Some(u) = n.as_u64() { - rmp_encode::write_u64(buf, u)?; - } else if let Some(f) = n.as_f64() { - rmp_encode::write_f64(buf, f)?; - } - } - Value::String(s) => rmp_encode::write_str(buf, s)?, - Value::Array(arr) => { - if arr.iter().all( - |item| matches!(item, Value::Number(n) if n.is_u64() && n.as_u64().unwrap() <= 255), - ) { - let bin_data: Vec = arr - .iter() - .filter_map(|v| v.as_u64().map(|n| n as u8)) - .collect(); - rmp_encode::write_bin(buf, &bin_data)?; - return Ok(()); - } - rmp_encode::write_array_len(buf, arr.len() as u32)?; - for item in arr { - encode_value_to_msgpack(item, buf)?; - } - } - Value::Object(obj) => { - rmp_encode::write_map_len(buf, obj.len() as u32)?; - for (key, value) in obj { - rmp_encode::write_str(buf, key)?; - encode_value_to_msgpack(value, buf)?; - } - } - } - Ok(()) -} - -pub struct ModelRegistry { - registry: HashMap>, -} - -impl ModelRegistry { - pub fn new() -> Self { - Self { - registry: HashMap::new(), - } - } - - pub fn register(&mut self, model_type: ModelType) - where - T: DeserializeOwned + Serialize + 'static, - { - self.registry - .insert(model_type, Box::new(TypedModelHandler::::new())); - } - - pub fn encode_json_to_msgpack(&self, model_type: ModelType, json_str: &str) -> Result> { - if let Some(handler) = self.registry.get(&model_type) { - handler.encode_json_to_msgpack(json_str) - } else { - Err(AlgoKitMsgPackError::UnknownModelError( - model_type.as_str().to_string(), - )) - } - } - - pub fn decode_msgpack_to_json( - &self, - model_type: ModelType, - msgpack_bytes: &[u8], - ) -> Result { - if let Some(handler) = self.registry.get(&model_type) { - handler.decode_msgpack_to_json(msgpack_bytes) - } else { - Err(AlgoKitMsgPackError::UnknownModelError( - model_type.as_str().to_string(), - )) - } - } - - pub fn encode_json_to_base64_msgpack( - &self, - model_type: ModelType, - json_str: &str, - ) -> Result { - let msgpack_bytes = self.encode_json_to_msgpack(model_type, json_str)?; - Ok(BASE64.encode(&msgpack_bytes)) - } - - pub fn decode_base64_msgpack_to_json( - &self, - model_type: ModelType, - base64_str: &str, - ) -> Result { - let msgpack_bytes = BASE64.decode(base64_str)?; - self.decode_msgpack_to_json(model_type, &msgpack_bytes) - } - - pub fn list_models(&self) -> Vec { - self.registry.keys().cloned().collect() - } -} - -impl Default for ModelRegistry { - fn default() -> Self { - let mut registry = Self::new(); - models::register_all_models(&mut registry); - registry - } -} - -trait ModelHandler { - fn encode_json_to_msgpack(&self, json_str: &str) -> Result>; - fn decode_msgpack_to_json(&self, msgpack_bytes: &[u8]) -> Result; -} - -struct TypedModelHandler -where - T: DeserializeOwned + Serialize, -{ - _phantom: std::marker::PhantomData, -} - -impl TypedModelHandler -where - T: DeserializeOwned + Serialize, -{ - fn new() -> Self { - Self { - _phantom: std::marker::PhantomData, - } - } -} - -impl ModelHandler for TypedModelHandler -where - T: DeserializeOwned + Serialize, -{ - fn encode_json_to_msgpack(&self, json_str: &str) -> Result> { - let model: T = serde_json::from_str(json_str)?; - Ok(rmp_serde::to_vec_named(&model)?) - } - - fn decode_msgpack_to_json(&self, msgpack_bytes: &[u8]) -> Result { - let model: T = rmp_serde::from_slice(msgpack_bytes)?; - Ok(serde_json::to_string(&model)?) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ModelType { - SimulateRequest, - SimulateTransaction200Response, -} - -impl ModelType { - /// Convert a ModelType to its string representation - pub fn as_str(&self) -> &'static str { - match self { - ModelType::SimulateRequest => "SimulateRequest", - ModelType::SimulateTransaction200Response => "SimulateTransaction200Response", - } - } - - /// Convert a string to a ModelType, returning None if the string doesn't match any model - #[allow(clippy::should_implement_trait)] - pub fn from_str(s: &str) -> Option { - match s { - "SimulateRequest" => Some(ModelType::SimulateRequest), - "SimulateTransaction200Response" => Some(ModelType::SimulateTransaction200Response), - _ => None, - } - } - - /// Get all available model types - pub fn all() -> Vec { - vec![ - ModelType::SimulateRequest, - ModelType::SimulateTransaction200Response, - ] - } -} - -// Public API functions -pub fn encode_json_to_msgpack(model_type: ModelType, json_str: &str) -> Result> { - let registry = ModelRegistry::default(); - registry.encode_json_to_msgpack(model_type, json_str) -} - -pub fn decode_msgpack_to_json(model_type: ModelType, msgpack_bytes: &[u8]) -> Result { - let registry = ModelRegistry::default(); - registry.decode_msgpack_to_json(model_type, msgpack_bytes) -} - -pub fn encode_json_to_base64_msgpack(model_type: ModelType, json_str: &str) -> Result { - let registry = ModelRegistry::default(); - registry.encode_json_to_base64_msgpack(model_type, json_str) -} - -pub fn decode_base64_msgpack_to_json(model_type: ModelType, base64_str: &str) -> Result { - let registry = ModelRegistry::default(); - registry.decode_base64_msgpack_to_json(model_type, base64_str) -} - -pub fn supported_models() -> Vec { - let registry = ModelRegistry::default(); - registry.list_models() -} - -// Allow users to use the standard `str::parse()` style API as well. -impl std::str::FromStr for ModelType { - type Err = (); - - fn from_str(s: &str) -> std::result::Result { - ModelType::from_str(s).ok_or(()) - } -} - -// Provide a blanket implementation so every `Serialize` type automatically -// gets the `ToMsgPack` methods. -impl ToMsgPack for T where T: Serialize {} - -// Implement `Display` for `ModelType` so it can be printed directly and used in -// other error types if needed. -impl std::fmt::Display for ModelType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.as_str()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn test_sort_and_filter_json() { - // Test object with unsorted keys and zero values - let input = json!({ - "z": "z-value", - "a": "a-value", - "empty_string": "", - "empty_array": [], - "zero_number": 0, - "non_zero": 42, - "false_value": false, - "true_value": true, - "nested": { - "y": "y-value", - "x": "x-value", - "empty_obj": {}, - "zero_num": 0 - } - }); - - let result = sort_and_filter_json(input).unwrap(); - - // Expected: keys sorted alphabetically, zero values removed - let expected = json!({ - "a": "a-value", - "nested": { - "x": "x-value", - "y": "y-value" - }, - "non_zero": 42, - "true_value": true, - "z": "z-value" - }); - - assert_eq!(result, expected); - - // Check JSON string representation to verify key order - let result_str = serde_json::to_string(&result).unwrap(); - assert!(result_str.find("\"a\"").unwrap() < result_str.find("\"nested\"").unwrap()); - assert!(result_str.find("\"nested\"").unwrap() < result_str.find("\"non_zero\"").unwrap()); - assert!( - result_str.find("\"non_zero\"").unwrap() < result_str.find("\"true_value\"").unwrap() - ); - assert!(result_str.find("\"true_value\"").unwrap() < result_str.find("\"z\"").unwrap()); - - // Check nested sorting - let nested_obj = result.as_object().unwrap().get("nested").unwrap(); - let nested_str = serde_json::to_string(nested_obj).unwrap(); - assert!(nested_str.find("\"x\"").unwrap() < nested_str.find("\"y\"").unwrap()); - } -} diff --git a/crates/algokit_transact/src/msgpack/models/mod.rs b/crates/algokit_transact/src/msgpack/models/mod.rs deleted file mode 100644 index 8a473d69..00000000 --- a/crates/algokit_transact/src/msgpack/models/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod simulate; - -pub use simulate::*; - -use crate::ModelRegistry; - -/// Register all models in the registry -pub fn register_all_models(registry: &mut ModelRegistry) { - // Register simulation models - simulate::register_simulation_models(registry); -} diff --git a/crates/algokit_transact/src/msgpack/models/simulate.rs b/crates/algokit_transact/src/msgpack/models/simulate.rs deleted file mode 100644 index 14b5ec37..00000000 --- a/crates/algokit_transact/src/msgpack/models/simulate.rs +++ /dev/null @@ -1,306 +0,0 @@ -use crate::msgpack::{ - encode_value_to_msgpack, AlgoKitMsgPackError, ModelHandler, ModelRegistry, ModelType, Result, -}; -use base64::engine::general_purpose::STANDARD as BASE64; -use base64::Engine; -use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; - -// ----------------------------- -// Simulation request structures -// ----------------------------- - -#[skip_serializing_none] -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] -pub struct SimulateRequest { - #[serde(rename = "allow-empty-signatures")] - pub allow_empty_signatures: Option, - #[serde(rename = "allow-more-logging")] - pub allow_more_logging: Option, - #[serde(rename = "allow-unnamed-resources")] - pub allow_unnamed_resources: Option, - #[serde(rename = "exec-trace-config")] - pub exec_trace_config: Option, - #[serde(rename = "extra-opcode-budget")] - pub extra_opcode_budget: Option, - #[serde(rename = "fix-signers")] - pub fix_signers: Option, - #[serde(rename = "round")] - pub round: Option, - #[serde(rename = "txn-groups")] - pub txn_groups: Vec, -} - -impl SimulateRequest { - pub fn new(txn_groups: Vec) -> Self { - Self { - txn_groups, - ..Default::default() - } - } -} - -#[skip_serializing_none] -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] -pub struct SimulateTraceConfig { - #[serde(rename = "enable")] - pub enable: Option, - #[serde(rename = "scratch-change")] - pub scratch_change: Option, - #[serde(rename = "stack-change")] - pub stack_change: Option, - #[serde(rename = "state-change")] - pub state_change: Option, -} - -impl SimulateTraceConfig { - pub fn new() -> Self { - Self::default() - } -} - -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] -pub struct SimulateRequestTransactionGroup { - #[serde(rename = "txns")] - pub txns: Vec, -} - -impl SimulateRequestTransactionGroup { - pub fn new(txns: Vec) -> Self { - Self { txns } - } -} - -// --------------------- -// Simulation responses -// --------------------- - -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] -pub struct SimulateUnnamedResourcesAccessed { - #[serde(flatten)] - pub other: serde_json::Value, -} - -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] -pub struct SimulationTransactionExecTrace { - #[serde(flatten)] - pub other: serde_json::Value, -} - -#[skip_serializing_none] -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] -pub struct SimulationEvalOverrides { - #[serde(rename = "allow-empty-signatures")] - pub allow_empty_signatures: Option, - #[serde(rename = "allow-unnamed-resources")] - pub allow_unnamed_resources: Option, - #[serde(rename = "max-log-calls")] - pub max_log_calls: Option, - #[serde(rename = "max-log-size")] - pub max_log_size: Option, - #[serde(rename = "extra-opcode-budget")] - pub extra_opcode_budget: Option, - #[serde(rename = "fix-signers")] - pub fix_signers: Option, -} - -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] -pub struct SimulateInitialStates { - #[serde(flatten)] - pub other: serde_json::Value, -} - -#[skip_serializing_none] -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] -pub struct SimulateTransactionResult { - #[serde(rename = "txn-result")] - pub txn_result: serde_json::Value, - #[serde(rename = "app-budget-consumed")] - pub app_budget_consumed: Option, - #[serde(rename = "logic-sig-budget-consumed")] - pub logic_sig_budget_consumed: Option, - #[serde(rename = "exec-trace")] - pub exec_trace: Option, - #[serde(rename = "unnamed-resources-accessed")] - pub unnamed_resources_accessed: Option, - #[serde(rename = "fixed-signer")] - pub fixed_signer: Option, -} - -#[skip_serializing_none] -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] -pub struct SimulateTransactionGroupResult { - #[serde(rename = "txn-results")] - pub txn_results: Vec, - #[serde(rename = "failure-message")] - pub failure_message: Option, - #[serde(rename = "failed-at")] - pub failed_at: Option>, // path indices - #[serde(rename = "app-budget-added")] - pub app_budget_added: Option, - #[serde(rename = "app-budget-consumed")] - pub app_budget_consumed: Option, - #[serde(rename = "unnamed-resources-accessed")] - pub unnamed_resources_accessed: Option, -} - -#[skip_serializing_none] -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] -pub struct SimulateTransaction200Response { - pub version: i64, - #[serde(rename = "last-round")] - pub last_round: i64, - #[serde(rename = "txn-groups")] - pub txn_groups: Vec, - #[serde(rename = "eval-overrides")] - pub eval_overrides: Option, - #[serde(rename = "exec-trace-config")] - pub exec_trace_config: Option, - #[serde(rename = "initial-states")] - pub initial_states: Option, -} - -// -------------------------------------- -// Custom encoding/decoding for simulation -// -------------------------------------- - -struct SimulateRequestHandler; - -impl ModelHandler for SimulateRequestHandler { - fn encode_json_to_msgpack(&self, json_str: &str) -> Result> { - let json_value: serde_json::Value = serde_json::from_str(json_str)?; - let mut buf = Vec::new(); - match &json_value { - serde_json::Value::Object(map) => { - // Sort keys for consistent output - let mut keys: Vec<&String> = map.keys().collect(); - keys.sort(); - rmp::encode::write_map_len(&mut buf, map.len() as u32)?; - for key in keys { - let value = map.get(key).expect("key just taken exists"); - rmp::encode::write_str(&mut buf, key)?; - match key.as_str() { - "txn-groups" => Self::encode_txn_groups(&mut buf, value)?, - _ => encode_value_to_msgpack(value, &mut buf)?, - } - } - } - _ => { - return Err(AlgoKitMsgPackError::MsgpackWriteError( - "Expected JSON object".into(), - )) - } - } - Ok(buf) - } - - fn decode_msgpack_to_json(&self, _msgpack_bytes: &[u8]) -> Result { - Err(AlgoKitMsgPackError::MsgpackWriteError( - "Simulate request decoding is not supported".into(), - )) - } -} - -impl SimulateRequestHandler { - fn encode_txn_groups(buf: &mut Vec, value: &serde_json::Value) -> Result<()> { - if let serde_json::Value::Array(groups) = value { - rmp::encode::write_array_len(buf, groups.len() as u32)?; - for group in groups { - match group { - serde_json::Value::Object(group_obj) => { - rmp::encode::write_map_len(buf, group_obj.len() as u32)?; - for (key, val) in group_obj { - rmp::encode::write_str(buf, key)?; - if key == "txns" { - if let serde_json::Value::Array(txns) = val { - rmp::encode::write_array_len(buf, txns.len() as u32)?; - for txn in txns { - match txn { - serde_json::Value::String(s) => { - buf.extend_from_slice(&BASE64.decode(s)?); - } - _ => encode_value_to_msgpack(txn, buf)?, - } - } - } else { - encode_value_to_msgpack(val, buf)?; - } - } else { - encode_value_to_msgpack(val, buf)?; - } - } - } - _ => encode_value_to_msgpack(group, buf)?, - } - } - } else { - encode_value_to_msgpack(value, buf)?; - } - Ok(()) - } - - fn rmpv_to_json(value: &rmpv::Value) -> serde_json::Value { - use rmpv::Value as V; - match value { - V::Nil => serde_json::Value::Null, - V::Boolean(b) => serde_json::Value::Bool(*b), - V::Integer(i) => { - if let Some(n) = i.as_i64() { - serde_json::Value::Number(n.into()) - } else if let Some(n) = i.as_u64() { - serde_json::Value::Number(serde_json::Number::from(n)) - } else { - serde_json::Value::String(i.to_string()) - } - } - V::F32(f) => serde_json::Number::from_f64(*f as f64).unwrap().into(), - V::F64(f) => serde_json::Number::from_f64(*f).unwrap().into(), - V::String(s) => serde_json::Value::String(s.as_str().unwrap_or_default().into()), - V::Binary(b) | V::Ext(_, b) => serde_json::Value::String(BASE64.encode(b)), - V::Array(arr) => serde_json::Value::Array(arr.iter().map(Self::rmpv_to_json).collect()), - V::Map(map) => { - let mut m = serde_json::Map::with_capacity(map.len()); - for (k, v) in map { - let key = match k { - V::String(s) => s.as_str().unwrap_or_default().to_string(), - _ => k.to_string(), - }; - m.insert(key, Self::rmpv_to_json(v)); - } - serde_json::Value::Object(m) - } - } - } -} - -struct SimulateResponseHandler; - -impl ModelHandler for SimulateResponseHandler { - fn encode_json_to_msgpack(&self, json_str: &str) -> Result> { - let v: serde_json::Value = serde_json::from_str(json_str)?; - Ok(rmp_serde::to_vec_named(&v)?) - } - - fn decode_msgpack_to_json(&self, msgpack_bytes: &[u8]) -> Result { - use std::io::Cursor; - let mut cursor = Cursor::new(msgpack_bytes); - let root: rmpv::Value = rmpv::decode::read_value(&mut cursor) - .map_err(|e| AlgoKitMsgPackError::IoError(e.to_string()))?; - let json_val = SimulateRequestHandler::rmpv_to_json(&root); - Ok(serde_json::to_string(&json_val)?) - } -} - -// ----------------------------- -// Registration helper -// ----------------------------- - -pub fn register_simulation_models(registry: &mut ModelRegistry) { - registry - .registry - .insert(ModelType::SimulateRequest, Box::new(SimulateRequestHandler)); - registry.registry.insert( - ModelType::SimulateTransaction200Response, - Box::new(SimulateResponseHandler), - ); -} diff --git a/crates/algokit_transact/tests_msgpack/account_tests.rs b/crates/algokit_transact/tests_msgpack/account_tests.rs deleted file mode 100644 index b96668a6..00000000 --- a/crates/algokit_transact/tests_msgpack/account_tests.rs +++ /dev/null @@ -1,85 +0,0 @@ -use algokit_msgpack::{Account, AlgoKitMsgPackError, ModelType}; -use base64::engine::general_purpose::STANDARD as BASE64; -use base64::Engine; - -#[test] -fn test_account_decoding_error() { - // Test that encoding returns the expected error - let mut account = Account::default(); - account.address = "ABCDEFG".to_string(); - account.amount = 100; - account.min_balance = 50; - account.amount_without_pending_rewards = 90; - account.pending_rewards = 10; - account.rewards = 20; - account.round = 1234; - account.status = "Online".to_string(); - account.total_apps_opted_in = 0; - account.total_assets_opted_in = 0; - account.total_created_apps = 0; - account.total_created_assets = 0; - - // Convert to JSON - let json = serde_json::to_string(&account).unwrap(); - - // Test error response for encoding - let result = algokit_msgpack::encode_json_to_msgpack(ModelType::Account, &json); - assert!(result.is_err()); - if let Err(AlgoKitMsgPackError::MsgpackWriteError(msg)) = result { - assert!(msg.contains("not supported")); - } else { - panic!("Expected MsgpackWriteError"); - } -} - -#[test] -fn test_decode_simplified_account_msgpack() { - // This is a complex MessagePack structure with an "algo" field that contains the account amount - // The structure includes multiple fields with both string and integer keys - let msgpack_base64 = "haRhbGdvzgADh4SkYXBhcoHNBC+IomFuplNUUC0jMaJhddlCaXBmczovL2JhZnliZWljZGR6N2tidXhhamo2Ym9iNWJqcXR3ZXE2d2Noa2RraXE0dnZod3J3cm5lN2l6NGYyNXhpoWPEIDB4md+EJXeeSDFN4bcfh84mIJ1V2fPQfw0osXuAyMvtomRmw6FtxCAweJnfhCV3nkgxTeG3H4fOJiCdVdnz0H8NKLF7gMjL7aFyxCAweJnfhCV3nkgxTeG3H4fOJiCdVdnz0H8NKLF7gMjL7aF0AaJ1bqNTVFClYXNzZXSBzQQvgaFhAaN0YngBpHRieGJI"; - let msgpack_bytes = BASE64 - .decode(msgpack_base64) - .expect("Failed to decode base64"); - - let json_str = algokit_msgpack::decode_msgpack_to_json(ModelType::Account, &msgpack_bytes) - .expect("Failed to decode simplified account MessagePack"); - - println!("Decoded JSON: {}", json_str); - - // The result should be a JSON object with the amount field set to 231300 (0x0387 84) - let account: serde_json::Value = - serde_json::from_str(&json_str).expect("Failed to parse decoded JSON"); - - assert_eq!(account["amount"], 231300); -} - -#[test] -fn test_decode_complex_account_msgpack() { - // This is a more complex MessagePack structure with multiple fields - let msgpack_base64 = "hKRhbGdvzgX3GYCkYXBhcorNS/CEomFuqFRlc3QgMjY5omF1s2h0dHBzOi8vZXhhbXBsZS5jb22hdM0PR6J1bqRURVNUzUvxhKJhbqhUZXN0IDExNKJhdbNodHRwczovL2V4YW1wbGUuY29toXTNFqWidW6kVEVTVM1L8oSiYW6oVGVzdCA3NDWiYXWzaHR0cHM6Ly9leGFtcGxlLmNvbaF0zRIWonVupFRFU1TNS/OEomFuqFRlc3QgMTk4omF1s2h0dHBzOi8vZXhhbXBsZS5jb22hdM0KvaJ1bqRURVNUzUv0hKJhbqhUZXN0IDEzMqJhdbNodHRwczovL2V4YW1wbGUuY29toXTNDQyidW6kVEVTVM1L9YSiYW6oVGVzdCA2NzeiYXWzaHR0cHM6Ly9leGFtcGxlLmNvbaF0zRRhonVupFRFU1TNS/aEomFuqFRlc3QgODM0omF1s2h0dHBzOi8vZXhhbXBsZS5jb22hdM0h8aJ1bqRURVNUzUv3hKJhbqhUZXN0IDQ4M6JhdbNodHRwczovL2V4YW1wbGUuY29toXTNCUuidW6kVEVTVM1L+ISiYW6oVGVzdCAxOTGiYXWzaHR0cHM6Ly9leGFtcGxlLmNvbaF0zSNKonVupFRFU1TNS/mEomFuqFRlc3QgNTU1omF1s2h0dHBzOi8vZXhhbXBsZS5jb22hdM0epqJ1bqRURVNUpGFwcHCKzUv6gqZhcHByb3bEVgoxG0EANIAEAr7OETYaAI4BAAOBAEMxGRREMRhENhoBVwIAiAAgSRUWVwYCTFCABBUffHVMULCBAUMxGUD/1DEYFESBAUOKAQGAB0hlbGxvLCCL/1CJpmNsZWFycMQECoEBQ81L+4KmYXBwcm92xFYKMRtBADSABAK+zhE2GgCOAQADgQBDMRkURDEYRDYaAVcCAIgAIEkVFlcGAkxQgAQVH3x1TFCwgQFDMRlA/9QxGBREgQFDigEBgAdIZWxsbywgi/9QiaZjbGVhcnDEBAqBAUPNS/yCpmFwcHJvdsRWCjEbQQA0gAQCvs4RNhoAjgEAA4EAQzEZFEQxGEQ2GgFXAgCIACBJFRZXBgJMUIAEFR98dUxQsIEBQzEZQP/UMRgURIEBQ4oBAYAHSGVsbG8sIIv/UImmY2xlYXJwxAQKgQFDzUv9gqZhcHByb3bEVgoxG0EANIAEAr7OETYaAI4BAAOBAEMxGRREMRhENhoBVwIAiAAgSRUWVwYCTFCABBUffHVMULCBAUMxGUD/1DEYFESBAUOKAQGAB0hlbGxvLCCL/1CJpmNsZWFycMQECoEBQ81L/oKmYXBwcm92xFYKMRtBADSABAK+zhE2GgCOAQADgQBDMRkURDEYRDYaAVcCAIgAIEkVFlcGAkxQgAQVH3x1TFCwgQFDMRlA/9QxGBREgQFDigEBgAdIZWxsbywgi/9QiaZjbGVhcnDEBAqBAUPNS/+CpmFwcHJvdsRWCjEbQQA0gAQCvs4RNhoAjgEAA4EAQzEZFEQxGEQ2GgFXAgCIACBJFRZXBgJMUIAEFR98dUxQsIEBQzEZQP/UMRgURIEBQ4oBAYAHSGVsbG8sIIv/UImmY2xlYXJwxAQKgQFDzUwAgqZhcHByb3bEVgoxG0EANIAEAr7OETYaAI4BAAOBAEMxGRREMRhENhoBVwIAiAAgSRUWVwYCTFCABBUffHVMULCBAUMxGUD/1DEYFESBAUOKAQGAB0hlbGxvLCCL/1CJpmNsZWFycMQECoEBQ81MAYKmYXBwcm92xFYKMRtBADSABAK+zhE2GgCOAQADgQBDMRkURDEYRDYaAVcCAIgAIEkVFlcGAkxQgAQVH3x1TFCwgQFDMRlA/9QxGBREgQFDigEBgAdIZWxsbywgi/9QiaZjbGVhcnDEBAqBAUPNTAKCpmFwcHJvdsRWCjEbQQA0gAQCvs4RNhoAjgEAA4EAQzEZFEQxGEQ2GgFXAgCIACBJFRZXBgJMUIAEFR98dUxQsIEBQzEZQP/UMRgURIEBQ4oBAYAHSGVsbG8sIIv/UImmY2xlYXJwxAQKgQFDzUwDgqZhcHByb3bEVgoxG0EANIAEAr7OETYaAI4BAAOBAEMxGRREMRhENhoBVwIAiAAgSRUWVwYCTFCABBUffHVMULCBAUMxGUD/1DEYFESBAUOKAQGAB0hlbGxvLCCL/1CJpmNsZWFycMQECoEBQ6Vhc3NldIrNS/CBoWHND0fNS/GBoWHNFqXNS/KBoWHNEhbNS/OBoWHNCr3NS/SBoWHNDQzNS/WBoWHNFGHNS/aBoWHNIfHNS/eBoWHNCUvNS/iBoWHNI0rNS/mBoWHNHqY="; - let msgpack_bytes = BASE64 - .decode(msgpack_base64) - .expect("Failed to decode base64"); - - let json_str = algokit_msgpack::decode_msgpack_to_json(ModelType::Account, &msgpack_bytes) - .expect("Failed to decode complex account MessagePack"); - - println!("Decoded JSON: {}", json_str); - - // Parse the resulting JSON - let account: serde_json::Value = - serde_json::from_str(&json_str).expect("Failed to parse decoded JSON"); - - // Verify the amount was extracted correctly - should be 100080000 (0x05F719 80) - assert_eq!(account["amount"], 100080000); - - // Verify created_assets, created_apps, and assets fields are populated using the actual key names - assert!(account["created-assets"].is_array()); - assert!(account["created-apps"].is_array()); - assert!(account["assets"].is_array()); - - // Check counts - assert_eq!(account["total-created-assets"], 10); - assert_eq!(account["total-created-apps"], 10); - assert_eq!(account["total-assets-opted-in"], 10); -} diff --git a/crates/algokit_transact/tests_msgpack/simulate_tests.rs b/crates/algokit_transact/tests_msgpack/simulate_tests.rs deleted file mode 100644 index 07dbedbc..00000000 --- a/crates/algokit_transact/tests_msgpack/simulate_tests.rs +++ /dev/null @@ -1,65 +0,0 @@ -use algokit_msgpack::{ - decode_base64_msgpack_to_json, encode_json_to_base64_msgpack, encode_json_to_msgpack, - ModelType, SimulateTransaction200Response, -}; -use base64::engine::general_purpose::STANDARD as BASE64; -use base64::Engine; - -#[test] -fn test_decode_simulate_response_200() { - let base64_msgpack = "hq5ldmFsLW92ZXJyaWRlc4S2YWxsb3ctZW1wdHktc2lnbmF0dXJlc8O3YWxsb3ctdW5uYW1lZC1yZXNvdXJjZXPDrW1heC1sb2ctY2FsbHPNCACsbWF4LWxvZy1zaXplzgABAACxZXhlYy10cmFjZS1jb25maWeEpmVuYWJsZcOuc2NyYXRjaC1jaGFuZ2XDrHN0YWNrLWNoYW5nZcOsc3RhdGUtY2hhbmdlw65pbml0aWFsLXN0YXRlc4CqbGFzdC1yb3VuZDWqdHhuLWdyb3Vwc5GBq3R4bi1yZXN1bHRzkYGqdHhuLXJlc3VsdIKqcG9vbC1lcnJvcqCjdHhugqNzaWfEQMRvOrLGLclzOfFppoyvhgTXsC+h/Qw59v5hc4k7CA9oVmEJZpcqjxweDlJg1C/vElTWwXL0zA/U59Ua/DjLhw+jdHhuiaNhbXTOAA9CQKNmZWXNA+iiZnY1o2dlbqxkb2NrZXJuZXQtdjGiZ2jEIEeJCm8ejvOqNCXVH+4GP95TdhioDiMH0wMRTIiwAmAUomx2zQQdo3JjdsQgOpJtq/2KwvdRn45on+Fhv0qXhguGb2ZMduXle8VCoPSjc25kxCA6km2r/YrC91Gfjmif4WG/SpeGC4ZvZkx25eV7xUKg9KR0eXBlo3Bhead2ZXJzaW9uAg=="; - - let json_str = - decode_base64_msgpack_to_json(ModelType::SimulateTransaction200Response, base64_msgpack) - .expect("Failed to decode MessagePack"); - - let resp: SimulateTransaction200Response = serde_json::from_str(&json_str) - .expect("Failed to parse JSON into SimulateTransaction200Response"); - - assert!(resp.version >= 1, "version should be positive"); - assert!( - !resp.txn_groups.is_empty(), - "should contain at least one transaction group" - ); -} - -#[test] -fn test_encode() { - let simulate_request_json = r#"{"txn-groups": [{"txns": ["gqNzaWfEQC0RQ1E6Y+/iS6luFP6Q9c6Veo838jRIABcV+jSzetx61nlrmasonRDbxN02mbCESJw98o7IfKgQvSMvk9kE0gqjdHhuiaNhbXTOAA9CQKNmZWXNA+iiZnYzo2dlbqxkb2NrZXJuZXQtdjGiZ2jEIEeJCm8ejvOqNCXVH+4GP95TdhioDiMH0wMRTIiwAmAUomx2zQQbo3JjdsQg/x0nrFM+VxALq2Buu1UscgDBy0OKIY2MGnDzg8xkNaOjc25kxCD/HSesUz5XEAurYG67VSxyAMHLQ4ohjYwacPODzGQ1o6R0eXBlo3BheQ=="]}], "allow-empty-signatures": true, "allow-more-logging": true, "allow-unnamed-resources": true, "exec-trace-config": {"enable": true, "stack-change": true, "scratch-change": true, "state-change": true}}"#; - - let expected_base64 = "hbZhbGxvdy1lbXB0eS1zaWduYXR1cmVzw7JhbGxvdy1tb3JlLWxvZ2dpbmfDt2FsbG93LXVubmFtZWQtcmVzb3VyY2Vzw7FleGVjLXRyYWNlLWNvbmZpZ4SmZW5hYmxlw6xzdGFjay1jaGFuZ2XDrnNjcmF0Y2gtY2hhbmdlw6xzdGF0ZS1jaGFuZ2XDqnR4bi1ncm91cHORgaR0eG5zkYKjc2lnxEAtEUNROmPv4kupbhT+kPXOlXqPN/I0SAAXFfo0s3rcetZ5a5mrKJ0Q28TdNpmwhEicPfKOyHyoEL0jL5PZBNIKo3R4bomjYW10zgAPQkCjZmVlzQPoomZ2M6NnZW6sZG9ja2VybmV0LXYxomdoxCBHiQpvHo7zqjQl1R/uBj/eU3YYqA4jB9MDEUyIsAJgFKJsds0EG6NyY3bEIP8dJ6xTPlcQC6tgbrtVLHIAwctDiiGNjBpw84PMZDWjo3NuZMQg/x0nrFM+VxALq2Buu1UscgDBy0OKIY2MGnDzg8xkNaOkdHlwZaNwYXk="; - - let msgpack_bytes = encode_json_to_msgpack(ModelType::SimulateRequest, simulate_request_json) - .expect("Failed to encode SimulateRequest"); - - let actual_base64 = BASE64.encode(&msgpack_bytes); - println!("Actual base64: {}", actual_base64); - - assert_eq!( - actual_base64, expected_base64, - "Base64 encoded MessagePack doesn't match expected value" - ); -} - -#[test] -fn test_decode() { - let simulate_request_json = r#"{"txn-groups": [{"txns": ["gqNzaWfEQC0RQ1E6Y+/iS6luFP6Q9c6Veo838jRIABcV+jSzetx61nlrmasonRDbxN02mbCESJw98o7IfKgQvSMvk9kE0gqjdHhuiaNhbXTOAA9CQKNmZWXNA+iiZnYzo2dlbqxkb2NrZXJuZXQtdjGiZ2jEIEeJCm8ejvOqNCXVH+4GP95TdhioDiMH0wMRTIiwAmAUomx2zQQbo3JjdsQg/x0nrFM+VxALq2Buu1UscgDBy0OKIY2MGnDzg8xkNaOjc25kxCD/HSesUz5XEAurYG67VSxyAMHLQ4ohjYwacPODzGQ1o6R0eXBlo3BheQ=="]}], "allow-empty-signatures": true, "allow-more-logging": true, "allow-unnamed-resources": true, "exec-trace-config": {"enable": true, "stack-change": true, "scratch-change": true, "state-change": true}}"#; - - let msgpack_bytes = - encode_json_to_base64_msgpack(ModelType::SimulateRequest, simulate_request_json) - .expect("Failed to encode SimulateRequest"); - - // SimulateRequest decoding is intentionally not supported - let result = decode_base64_msgpack_to_json(ModelType::SimulateRequest, &msgpack_bytes); - assert!( - result.is_err(), - "SimulateRequest decoding should return an error" - ); - - if let Err(e) = result { - assert!( - e.to_string().contains("not supported"), - "Error should mention 'not supported'" - ); - } -} diff --git a/crates/algokit_transact_ffi/src/lib.rs b/crates/algokit_transact_ffi/src/lib.rs index cee7efb9..24dc0606 100644 --- a/crates/algokit_transact_ffi/src/lib.rs +++ b/crates/algokit_transact_ffi/src/lib.rs @@ -1,13 +1,6 @@ mod transactions; use algokit_transact::constants::*; -use algokit_transact::msgpack::{ - decode_base64_msgpack_to_json as internal_decode_base64_msgpack_to_json, - decode_msgpack_to_json as internal_decode_msgpack_to_json, - encode_json_to_base64_msgpack as internal_encode_json_to_base64_msgpack, - encode_json_to_msgpack as internal_encode_json_to_msgpack, - AlgoKitMsgPackError as InternalMsgPackError, ModelType as InternalModelType, -}; use algokit_transact::{ AlgorandMsgpack, Byte32, EstimateTransactionSize, TransactionId, Transactions, Validate, }; @@ -72,30 +65,6 @@ impl From for AlgoKitTransactError { } } -// Convert msgpack errors to FFI errors -impl From for AlgoKitTransactError { - fn from(e: InternalMsgPackError) -> Self { - match e { - InternalMsgPackError::SerializationError(e) => { - AlgoKitTransactError::MsgPackError(e.to_string()) - } - InternalMsgPackError::MsgpackEncodingError(e) => { - AlgoKitTransactError::MsgPackError(e.to_string()) - } - InternalMsgPackError::MsgpackDecodingError(e) => { - AlgoKitTransactError::MsgPackError(e.to_string()) - } - InternalMsgPackError::Base64DecodingError(e) => { - AlgoKitTransactError::MsgPackError(e.to_string()) - } - InternalMsgPackError::MsgpackWriteError(s) => AlgoKitTransactError::MsgPackError(s), - InternalMsgPackError::UnknownModelError(s) => AlgoKitTransactError::MsgPackError(s), - InternalMsgPackError::IoError(s) => AlgoKitTransactError::MsgPackError(s), - InternalMsgPackError::ValueWriteError(s) => AlgoKitTransactError::MsgPackError(s), - } - } -} - #[cfg(feature = "ffi_uniffi")] use uniffi::{self}; @@ -967,89 +936,3 @@ mod tests { } } } - -// ========== MessagePack FFI Functions ========== - -#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] -#[cfg_attr(feature = "ffi_wasm", derive(tsify_next::Tsify))] -#[cfg_attr(feature = "ffi_wasm", tsify(into_wasm_abi, from_wasm_abi))] -#[cfg_attr(feature = "ffi_uniffi", derive(uniffi::Enum))] -pub enum ModelType { - SimulateRequest, - SimulateTransaction200Response, -} - -impl From for InternalModelType { - fn from(model_type: ModelType) -> Self { - match model_type { - ModelType::SimulateRequest => InternalModelType::SimulateRequest, - ModelType::SimulateTransaction200Response => { - InternalModelType::SimulateTransaction200Response - } - } - } -} - -impl From for ModelType { - fn from(model_type: InternalModelType) -> Self { - match model_type { - InternalModelType::SimulateRequest => ModelType::SimulateRequest, - InternalModelType::SimulateTransaction200Response => { - ModelType::SimulateTransaction200Response - } - } - } -} - -#[ffi_func] -pub fn encode_json_to_msgpack( - model_type: ModelType, - json_str: &str, -) -> Result, AlgoKitTransactError> { - let internal_type: InternalModelType = model_type.into(); - Ok(internal_encode_json_to_msgpack(internal_type, json_str)?) -} - -#[ffi_func] -pub fn decode_msgpack_to_json( - model_type: ModelType, - msgpack_bytes: &[u8], -) -> Result { - let internal_type: InternalModelType = model_type.into(); - Ok(internal_decode_msgpack_to_json( - internal_type, - msgpack_bytes, - )?) -} - -#[ffi_func] -pub fn encode_json_to_base64_msgpack( - model_type: ModelType, - json_str: &str, -) -> Result { - let internal_type: InternalModelType = model_type.into(); - Ok(internal_encode_json_to_base64_msgpack( - internal_type, - json_str, - )?) -} - -#[ffi_func] -pub fn decode_base64_msgpack_to_json( - model_type: ModelType, - base64_str: &str, -) -> Result { - let internal_type: InternalModelType = model_type.into(); - Ok(internal_decode_base64_msgpack_to_json( - internal_type, - base64_str, - )?) -} - -#[ffi_func] -pub fn supported_models() -> Vec { - algokit_transact::msgpack::supported_models() - .into_iter() - .map(Into::into) - .collect() -} diff --git a/crates/algokit_utils/Cargo.toml b/crates/algokit_utils/Cargo.toml index 1dd09d74..9e446e65 100644 --- a/crates/algokit_utils/Cargo.toml +++ b/crates/algokit_utils/Cargo.toml @@ -5,12 +5,17 @@ edition = "2024" [features] default = ["default_http_client"] -default_http_client = ["dep:reqwest", "algokit_http_client/default_client"] +default_http_client = [ + "algokit_http_client/default_client", + "algod_client/default_client", +] [dependencies] -algod_api = { version = "0.1.0", path = "../algod_api" } +algod_client = { path = "../algod_client" } algokit_http_client = { version = "0.1.0", path = "../algokit_http_client", default-features = false } -algokit_transact = { version = "0.1.0", path = "../algokit_transact", features = ["test_utils"] } +algokit_transact = { version = "0.1.0", path = "../algokit_transact", features = [ + "test_utils", +] } async-trait = { version = "0.1.88" } base64 = "0.22.1" derive_more = { version = "2.0.1", features = ["full"] } diff --git a/crates/algokit_utils/src/lib.rs b/crates/algokit_utils/src/lib.rs index f70207c2..cccc55c9 100644 --- a/crates/algokit_utils/src/lib.rs +++ b/crates/algokit_utils/src/lib.rs @@ -1,14 +1,8 @@ -use algod_api::AlgodClient; - -use algod_api::TransactionParams; -use algokit_transact::Address; -use algokit_transact::FeeParams; -use algokit_transact::PaymentTransactionFields; -use algokit_transact::SignedTransaction; -use algokit_transact::Transaction; -use algokit_transact::TransactionHeader; -use algokit_transact::Transactions; -use base64::{Engine as _, engine::general_purpose}; +use algod_client::{AlgodClient, apis::Error as AlgodError, models::TransactionParams}; +use algokit_transact::{ + Address, FeeParams, PaymentTransactionFields, SignedTransaction, Transaction, + TransactionHeader, Transactions, +}; use derive_more::Debug; use std::sync::Arc; @@ -16,8 +10,8 @@ use async_trait::async_trait; #[derive(Debug, thiserror::Error)] pub enum ComposerError { - #[error(transparent)] - HttpError(#[from] algokit_http_client::HttpError), + #[error("Algod client error: {0}")] + AlgodClientError(#[from] AlgodError), #[error("Decode Error: {0}")] DecodeError(String), #[error("Transaction Error: {0}")] @@ -52,78 +46,17 @@ pub struct PaymentParams { pub close_remainder_to: Option
, } -#[derive(Debug)] -pub struct AssetTransferParams { - /// Part of the "specialized" asset transaction types. - /// Based on the primitive asset transfer, this struct implements asset transfers - /// without additional side effects. - /// Only in the case where the receiver is equal to the sender and the amount is zero, - /// this is an asset opt-in transaction. - pub common_params: CommonParams, - pub asset_id: u64, - pub amount: u64, - pub receiver: Address, -} - -#[derive(Debug)] -pub struct AssetOptInParams { - /// Part of the "specialized" asset transaction types. - /// Based on the primitive asset transfer, this struct implements asset opt-in - /// without additional side effects. - pub common_params: CommonParams, - pub asset_id: u64, -} - -#[derive(Debug)] -pub struct AssetOptOutParams { - /// Part of the "specialized" asset transaction types. - /// Based on the primitive asset transfer, this struct implements asset opt-out - /// without additional side effects. - pub common_params: CommonParams, - pub asset_id: u64, - pub close_remainder_to: Option
, -} - -#[derive(Debug)] -pub struct AssetClawbackParams { - /// Part of the "specialized" asset transaction types. - /// Based on the primitive asset transfer, this struct implements asset clawback - /// without additional side effects. - pub common_params: CommonParams, - pub asset_id: u64, - pub amount: u64, - pub receiver: Address, - // The address from which ASAs are taken. - pub clawback_target: Address, -} - // TODO: TransactionWithSigner #[derive(Debug)] pub enum ComposerTxn { Transaction(Transaction), Payment(PaymentParams), - AssetTransfer(AssetTransferParams), - AssetOptIn(AssetOptInParams), - AssetOptOut(AssetOptOutParams), - AssetClawback(AssetClawbackParams), } impl ComposerTxn { pub fn common_params(&self) -> CommonParams { match self { ComposerTxn::Payment(payment_params) => payment_params.common_params.clone(), - ComposerTxn::AssetTransfer(asset_transfer_params) => { - asset_transfer_params.common_params.clone() - } - ComposerTxn::AssetOptIn(asset_opt_in_params) => { - asset_opt_in_params.common_params.clone() - } - ComposerTxn::AssetOptOut(asset_opt_out_params) => { - asset_opt_out_params.common_params.clone() - } - ComposerTxn::AssetClawback(asset_clawback_params) => { - asset_clawback_params.common_params.clone() - } _ => CommonParams::default(), } } @@ -227,34 +160,6 @@ impl Composer { self.push(ComposerTxn::Payment(payment_params)) } - pub fn add_asset_transfer( - &mut self, - asset_transfer_params: AssetTransferParams, - ) -> Result<(), String> { - self.push(ComposerTxn::AssetTransfer(asset_transfer_params)) - } - - pub fn add_asset_opt_in( - &mut self, - asset_opt_in_params: AssetOptInParams, - ) -> Result<(), String> { - self.push(ComposerTxn::AssetOptIn(asset_opt_in_params)) - } - - pub fn add_asset_opt_out( - &mut self, - asset_opt_out_params: AssetOptOutParams, - ) -> Result<(), String> { - self.push(ComposerTxn::AssetOptOut(asset_opt_out_params)) - } - - pub fn add_asset_clawback( - &mut self, - asset_clawback_params: AssetClawbackParams, - ) -> Result<(), String> { - self.push(ComposerTxn::AssetClawback(asset_clawback_params)) - } - pub fn add_transaction(&mut self, transaction: Transaction) -> Result<(), String> { self.push(ComposerTxn::Transaction(transaction)) } @@ -272,7 +177,7 @@ impl Composer { self.algod_client .transaction_params() .await - .map_err(ComposerError::HttpError) + .map_err(Into::into) } pub async fn build(&mut self) -> Result<&mut Self, ComposerError> { @@ -282,32 +187,22 @@ impl Composer { let suggested_params = self.get_suggested_params().await?; - let default_header = TransactionHeader { - fee: Some(suggested_params.fee), - genesis_id: Some(suggested_params.genesis_id), - genesis_hash: Some( - general_purpose::STANDARD - .decode(suggested_params.genesis_hash) - .map_err(|e| { - ComposerError::DecodeError(format!("Failed to decode genesis hash: {}", e)) - })? - .try_into() - .map_err(|e| { - ComposerError::DecodeError(format!( - "Failed to convert genesis hash: {:?}", - e - )) - })?, - ), - // The rest of these fields are set further down per txn - first_valid: 0, - last_valid: 0, - sender: Address::default(), - rekey_to: None, - note: None, - lease: None, - group: None, - }; + let default_header = + TransactionHeader { + fee: Some(suggested_params.fee), + genesis_id: Some(suggested_params.genesis_id), + genesis_hash: Some(suggested_params.genesis_hash.try_into().map_err(|_e| { + ComposerError::DecodeError("Invalid genesis hash".to_string()) + })?), + // The rest of these fields are set further down per txn + first_valid: 0, + last_valid: 0, + sender: Address::default(), + rekey_to: None, + note: None, + lease: None, + group: None, + }; let txs = self .transactions @@ -318,56 +213,14 @@ impl Composer { let mut transaction: algokit_transact::Transaction = match composer_txn { ComposerTxn::Transaction(txn) => txn.clone(), ComposerTxn::Payment(pay_params) => { - Transaction::Payment(PaymentTransactionFields { + let pay_params = PaymentTransactionFields { header: default_header.clone(), receiver: pay_params.receiver.clone(), amount: pay_params.amount, close_remainder_to: pay_params.close_remainder_to.clone(), - }) - } - ComposerTxn::AssetTransfer(asset_transfer_params) => { - Transaction::AssetTransfer( - algokit_transact::AssetTransferTransactionFields { - header: default_header.clone(), - asset_id: asset_transfer_params.asset_id, - amount: asset_transfer_params.amount, - receiver: asset_transfer_params.receiver.clone(), - asset_sender: None, - close_remainder_to: None, - }, - ) - } - ComposerTxn::AssetOptIn(asset_opt_in_params) => Transaction::AssetTransfer( - algokit_transact::AssetTransferTransactionFields { - header: default_header.clone(), - asset_id: asset_opt_in_params.asset_id, - amount: 0, - receiver: asset_opt_in_params.common_params.sender.clone(), - asset_sender: None, - close_remainder_to: None, - }, - ), - ComposerTxn::AssetOptOut(asset_opt_out_params) => Transaction::AssetTransfer( - algokit_transact::AssetTransferTransactionFields { - header: default_header.clone(), - asset_id: asset_opt_out_params.asset_id, - amount: 0, - receiver: asset_opt_out_params.common_params.sender.clone(), - asset_sender: None, - close_remainder_to: asset_opt_out_params.close_remainder_to.clone(), - }, - ), - ComposerTxn::AssetClawback(asset_clawback_params) => { - Transaction::AssetTransfer( - algokit_transact::AssetTransferTransactionFields { - header: default_header.clone(), - asset_id: asset_clawback_params.asset_id, - amount: asset_clawback_params.amount, - receiver: asset_clawback_params.receiver.clone(), - asset_sender: Some(asset_clawback_params.clawback_target.clone()), - close_remainder_to: None, - }, - ) + }; + + Transaction::Payment(pay_params) } }; @@ -428,6 +281,7 @@ impl Composer { mod tests { use super::*; use algokit_transact::test_utils::{AddressMother, TransactionMother}; + use base64::{Engine, prelude::BASE64_STANDARD}; #[test] fn test_add_transaction() { @@ -454,7 +308,9 @@ mod tests { assert_eq!( response.genesis_hash, - "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" + BASE64_STANDARD + .decode("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=") + .unwrap() ); } @@ -508,108 +364,6 @@ mod tests { assert!(composer.built_group().is_some()); } - #[tokio::test] - async fn test_build_asset_transfer() { - let mut composer = Composer::testnet(); - let asset_transfer_params = AssetTransferParams { - common_params: CommonParams { - sender: AddressMother::address(), - signer: None, - rekey_to: None, - note: None, - lease: None, - static_fee: None, - extra_fee: None, - max_fee: None, - validity_window: None, - first_valid_round: None, - last_valid_round: None, - }, - asset_id: 12345, - amount: 1000, - receiver: AddressMother::address(), - }; - assert!(composer.add_asset_transfer(asset_transfer_params).is_ok()); - assert!(composer.build().await.is_ok()); - assert!(composer.built_group().is_some()); - } - - #[tokio::test] - async fn test_build_asset_opt_in() { - let mut composer = Composer::testnet(); - let asset_opt_in_params = AssetOptInParams { - common_params: CommonParams { - sender: AddressMother::address(), - signer: None, - rekey_to: None, - note: None, - lease: None, - static_fee: None, - extra_fee: None, - max_fee: None, - validity_window: None, - first_valid_round: None, - last_valid_round: None, - }, - asset_id: 12345, - }; - assert!(composer.add_asset_opt_in(asset_opt_in_params).is_ok()); - assert!(composer.build().await.is_ok()); - assert!(composer.built_group().is_some()); - } - - #[tokio::test] - async fn test_build_asset_opt_out() { - let mut composer = Composer::testnet(); - let asset_opt_out_params = AssetOptOutParams { - common_params: CommonParams { - sender: AddressMother::address(), - signer: None, - rekey_to: None, - note: None, - lease: None, - static_fee: None, - extra_fee: None, - max_fee: None, - validity_window: None, - first_valid_round: None, - last_valid_round: None, - }, - asset_id: 12345, - close_remainder_to: Some(AddressMother::neil()), - }; - assert!(composer.add_asset_opt_out(asset_opt_out_params).is_ok()); - assert!(composer.build().await.is_ok()); - assert!(composer.built_group().is_some()); - } - - #[tokio::test] - async fn test_build_asset_clawback() { - let mut composer = Composer::testnet(); - let asset_clawback_params = AssetClawbackParams { - common_params: CommonParams { - sender: AddressMother::address(), - signer: None, - rekey_to: None, - note: None, - lease: None, - static_fee: None, - extra_fee: None, - max_fee: None, - validity_window: None, - first_valid_round: None, - last_valid_round: None, - }, - asset_id: 12345, - amount: 1000, - receiver: AddressMother::address(), - clawback_target: AddressMother::neil(), - }; - assert!(composer.add_asset_clawback(asset_clawback_params).is_ok()); - assert!(composer.build().await.is_ok()); - assert!(composer.built_group().is_some()); - } - #[tokio::test] async fn test_gather_signatures() { let mut composer = Composer { diff --git a/tools/api_tools/Cargo.toml b/tools/api_tools/Cargo.toml new file mode 100644 index 00000000..ccc383a4 --- /dev/null +++ b/tools/api_tools/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "api_tools" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.5.40", features = ["derive"] } +color-eyre = "0.6.5" +duct = "1.0.0" +shlex = "1.3.0" diff --git a/tools/api_tools/src/main.rs b/tools/api_tools/src/main.rs new file mode 100644 index 00000000..9ddb08fa --- /dev/null +++ b/tools/api_tools/src/main.rs @@ -0,0 +1,141 @@ +use std::collections::HashMap; +use std::env; +use std::path::{Path, PathBuf}; +use std::process::Output; + +use clap::{Parser, Subcommand}; +use color_eyre::eyre::Result; +use duct::cmd; + +#[derive(Parser, Debug)] +#[command(author, version, about = "API development tools", long_about = None)] +#[command(propagate_version = true)] +struct Args { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// Test the OAS generator + #[command(name = "test-oas")] + TestOas, + /// Format the OAS generator code + #[command(name = "format-oas")] + FormatOas, + /// Lint and type-check the OAS generator + #[command(name = "lint-oas")] + LintOas, + /// Format generated Rust code + #[command(name = "format-algod")] + FormatAlgod, + /// Generate algod API client + #[command(name = "generate-algod")] + GenerateAlgod, + /// Convert OpenAPI specification + #[command(name = "convert-openapi")] + ConvertOpenapi, +} + +fn get_repo_root() -> PathBuf { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let repo_root = Path::new(manifest_dir) + .parent() // tools/ + .unwrap() + .parent() // repo root + .unwrap(); + + PathBuf::from(repo_root) +} + +fn run( + command_str: &str, + dir: Option<&Path>, + env_vars: Option>, +) -> Result { + let parsed_command: Vec = shlex::Shlex::new(command_str).collect(); + + let working_dir = get_repo_root().join(dir.unwrap_or(Path::new(""))); + let mut command = cmd(&parsed_command[0], &parsed_command[1..]) + .dir(&working_dir) + .stderr_to_stdout(); + + if let Some(env_vars) = env_vars { + for (key, value) in &env_vars { + command = command.env(key, value); + } + } + + Ok(command.run()?) +} + +fn execute_command(command: &Commands) -> Result<()> { + match command { + Commands::TestOas => { + run("uv run pytest", Some(Path::new("api/oas_generator")), None)?; + } + Commands::FormatOas => { + run( + "uv run ruff format", + Some(Path::new("api/oas_generator")), + None, + )?; + } + Commands::LintOas => { + run( + "uv run ruff check", + Some(Path::new("api/oas_generator")), + None, + )?; + run( + "uv run mypy rust_oas_generator", + Some(Path::new("api/oas_generator")), + None, + )?; + } + Commands::FormatAlgod => { + run( + "cargo fmt --manifest-path Cargo.toml -p algod_client", + None, + None, + )?; + } + Commands::GenerateAlgod => { + // Generate the client + run( + "uv run python -m rust_oas_generator.cli ../specs/algod.oas3.json --output ../../crates/algod_client/ --package-name algod_client --description \"API client for algod interaction.\"", + Some(Path::new("api/oas_generator")), + None, + )?; + // Format the generated code + run( + "cargo fmt --manifest-path Cargo.toml -p algod_client", + None, + None, + )?; + } + Commands::ConvertOpenapi => { + run( + "bun scripts/convert-openapi.ts", + Some(Path::new("api")), + None, + )?; + } + } + Ok(()) +} + +fn main() -> Result<()> { + color_eyre::install()?; + + if std::env::var("RUST_BACKTRACE").is_err() { + unsafe { + std::env::set_var("RUST_BACKTRACE", "full"); + } + } + + let args = Args::parse(); + execute_command(&args.command)?; + + Ok(()) +}