From ae6ebeef1965a65b7d24d44dd82385af3b2d6269 Mon Sep 17 00:00:00 2001 From: David Rojas Date: Mon, 24 Nov 2025 17:19:33 -0500 Subject: [PATCH] chore: remove algokit_utils and algokit_utils_ffi crates --- Cargo.lock | 150 +- Cargo.toml | 2 - crates/algokit_utils/Cargo.toml | 51 - .../applications/app_client/compilation.rs | 93 - .../src/applications/app_client/error.rs | 52 - .../app_client/error_transformation.rs | 434 --- .../src/applications/app_client/mod.rs | 380 --- .../applications/app_client/params_builder.rs | 549 ---- .../applications/app_client/state_accessor.rs | 417 --- .../app_client/transaction_builder.rs | 232 -- .../app_client/transaction_sender.rs | 305 -- .../src/applications/app_client/types.rs | 181 -- .../src/applications/app_client/utils.rs | 21 - .../src/applications/app_deployer.rs | 1380 -------- .../applications/app_factory/compilation.rs | 90 - .../src/applications/app_factory/error.rs | 30 - .../src/applications/app_factory/mod.rs | 417 --- .../app_factory/params_builder.rs | 330 -- .../app_factory/transaction_builder.rs | 105 - .../app_factory/transaction_sender.rs | 170 - .../src/applications/app_factory/types.rs | 150 - .../src/applications/app_factory/utils.rs | 247 -- crates/algokit_utils/src/applications/mod.rs | 11 - .../src/clients/account_manager.rs | 59 - .../src/clients/algorand_client.rs | 181 -- .../algokit_utils/src/clients/app_manager.rs | 722 ----- .../src/clients/asset_manager.rs | 470 --- .../src/clients/client_manager.rs | 656 ---- crates/algokit_utils/src/clients/mod.rs | 19 - .../src/clients/network_client.rs | 147 - crates/algokit_utils/src/config.rs | 85 - crates/algokit_utils/src/lib.rs | 27 - .../src/transactions/app_call.rs | 1218 ------- .../src/transactions/asset_config.rs | 254 -- .../src/transactions/asset_freeze.rs | 45 - .../src/transactions/asset_transfer.rs | 173 - .../algokit_utils/src/transactions/common.rs | 127 - .../src/transactions/composer.rs | 2786 ----------------- .../algokit_utils/src/transactions/creator.rs | 414 --- .../src/transactions/key_registration.rs | 85 - crates/algokit_utils/src/transactions/mod.rs | 38 - .../algokit_utils/src/transactions/payment.rs | 45 - .../algokit_utils/src/transactions/sender.rs | 769 ----- crates/algokit_utils/tests/algod/block.rs | 41 - crates/algokit_utils/tests/algod/mod.rs | 5 - .../algod/pending_transaction_information.rs | 91 - .../tests/algod/raw_transaction.rs | 85 - .../tests/algod/simulate_transactions.rs | 84 - .../tests/algod/transaction_params.rs | 48 - crates/algokit_utils/tests/algod_tests.rs | 2 - .../app_client/client_management.rs | 147 - .../applications/app_client/compilation.rs | 52 - .../applications/app_client/default_values.rs | 300 -- .../applications/app_client/error_handling.rs | 36 - .../tests/applications/app_client/mod.rs | 8 - .../tests/applications/app_client/params.rs | 220 -- .../tests/applications/app_client/send.rs | 426 --- .../tests/applications/app_client/state.rs | 609 ---- .../tests/applications/app_client/structs.rs | 211 -- .../tests/applications/app_deployer.rs | 1394 --------- .../tests/applications/app_factory.rs | 1207 ------- .../algokit_utils/tests/applications/mod.rs | 3 - .../algokit_utils/tests/applications_tests.rs | 2 - .../tests/clients/algorand_client.rs | 66 - .../tests/clients/app_manager.rs | 504 --- .../tests/clients/asset_manager.rs | 422 --- .../tests/clients/client_manager.rs | 113 - crates/algokit_utils/tests/clients/mod.rs | 4 - crates/algokit_utils/tests/clients_tests.rs | 2 - .../algokit_utils/tests/common/app_fixture.rs | 214 -- crates/algokit_utils/tests/common/fixture.rs | 156 - .../tests/common/indexer_helpers.rs | 206 -- .../tests/common/local_net_dispenser.rs | 241 -- crates/algokit_utils/tests/common/logging.rs | 16 - crates/algokit_utils/tests/common/mnemonic.rs | 368 --- crates/algokit_utils/tests/common/mod.rs | 98 - .../tests/common/test_account.rs | 206 -- crates/algokit_utils/tests/indexer/mod.rs | 2 - .../tests/indexer/search_applications.rs | 114 - .../tests/indexer/search_transactions.rs | 106 - crates/algokit_utils/tests/indexer_tests.rs | 2 - .../algokit_utils/tests/kmd/key_management.rs | 101 - crates/algokit_utils/tests/kmd/mod.rs | 2 - .../tests/kmd/wallet_lifecycle.rs | 53 - crates/algokit_utils/tests/kmd_tests.rs | 1 - .../tests/transactions/composer/app_call.rs | 1434 --------- .../transactions/composer/asset_config.rs | 270 -- .../transactions/composer/asset_freeze.rs | 270 -- .../transactions/composer/asset_transfer.rs | 97 - .../composer/inner_fee_coverage.rs | 1895 ----------- .../transactions/composer/key_registration.rs | 370 --- .../tests/transactions/composer/mod.rs | 9 - .../tests/transactions/composer/payment.rs | 137 - .../composer/resource_population.rs | 1199 ------- .../composer/transaction_group.rs | 215 -- .../tests/transactions/creator.rs | 532 ---- .../algokit_utils/tests/transactions/mod.rs | 3 - .../tests/transactions/sender.rs | 415 --- .../algokit_utils/tests/transactions_tests.rs | 2 - crates/algokit_utils_ffi/Cargo.toml | 30 - crates/algokit_utils_ffi/src/abi/abi_type.rs | 256 -- crates/algokit_utils_ffi/src/abi/abi_value.rs | 200 -- crates/algokit_utils_ffi/src/abi/mod.rs | 2 - .../src/clients/algod_client.rs | 37 - crates/algokit_utils_ffi/src/clients/mod.rs | 1 - crates/algokit_utils_ffi/src/lib.rs | 7 - .../src/tests/asset_freeze_tests.rs | 306 -- .../src/tests/fixtures/localnet.rs | 159 - .../src/tests/fixtures/mnemonic.rs | 368 --- .../src/tests/fixtures/mod.rs | 7 - .../src/tests/fixtures/test_account.rs | 71 - .../src/tests/fixtures/test_fixture.rs | 177 -- crates/algokit_utils_ffi/src/tests/mod.rs | 4 - .../src/transactions/app_call.rs | 1557 --------- .../src/transactions/asset_config.rs | 321 -- .../src/transactions/asset_freeze.rs | 160 - .../src/transactions/asset_transfer.rs | 347 -- .../src/transactions/common.rs | 289 -- .../src/transactions/composer.rs | 512 --- .../src/transactions/key_registration.rs | 252 -- .../algokit_utils_ffi/src/transactions/mod.rs | 8 - .../src/transactions/payment.rs | 161 - crates/algokit_utils_ffi/uniffi.toml | 5 - .../algokit_utils.code-workspace | 44 - .../algokit_utils/algokit_utils/__init__.py | 11 - .../algokit_utils/ffi_algod_client.py | 139 - .../algokit_utils/ffi_composer.py | 129 - .../algokit_utils/algokit_utils/py.typed | 0 packages/python/algokit_utils/poetry.lock | 712 ----- packages/python/algokit_utils/poetry.toml | 2 - packages/python/algokit_utils/pyproject.toml | 43 - .../algokit_utils/tests/test_ffi_async.py | 67 - .../python/algokit_utils/tests/test_utils.py | 280 -- tools/build_pkgs/src/main.rs | 4 - 134 files changed, 3 insertions(+), 35598 deletions(-) delete mode 100644 crates/algokit_utils/Cargo.toml delete mode 100644 crates/algokit_utils/src/applications/app_client/compilation.rs delete mode 100644 crates/algokit_utils/src/applications/app_client/error.rs delete mode 100644 crates/algokit_utils/src/applications/app_client/error_transformation.rs delete mode 100644 crates/algokit_utils/src/applications/app_client/mod.rs delete mode 100644 crates/algokit_utils/src/applications/app_client/params_builder.rs delete mode 100644 crates/algokit_utils/src/applications/app_client/state_accessor.rs delete mode 100644 crates/algokit_utils/src/applications/app_client/transaction_builder.rs delete mode 100644 crates/algokit_utils/src/applications/app_client/transaction_sender.rs delete mode 100644 crates/algokit_utils/src/applications/app_client/types.rs delete mode 100644 crates/algokit_utils/src/applications/app_client/utils.rs delete mode 100644 crates/algokit_utils/src/applications/app_deployer.rs delete mode 100644 crates/algokit_utils/src/applications/app_factory/compilation.rs delete mode 100644 crates/algokit_utils/src/applications/app_factory/error.rs delete mode 100644 crates/algokit_utils/src/applications/app_factory/mod.rs delete mode 100644 crates/algokit_utils/src/applications/app_factory/params_builder.rs delete mode 100644 crates/algokit_utils/src/applications/app_factory/transaction_builder.rs delete mode 100644 crates/algokit_utils/src/applications/app_factory/transaction_sender.rs delete mode 100644 crates/algokit_utils/src/applications/app_factory/types.rs delete mode 100644 crates/algokit_utils/src/applications/app_factory/utils.rs delete mode 100644 crates/algokit_utils/src/applications/mod.rs delete mode 100644 crates/algokit_utils/src/clients/account_manager.rs delete mode 100644 crates/algokit_utils/src/clients/algorand_client.rs delete mode 100644 crates/algokit_utils/src/clients/app_manager.rs delete mode 100644 crates/algokit_utils/src/clients/asset_manager.rs delete mode 100644 crates/algokit_utils/src/clients/client_manager.rs delete mode 100644 crates/algokit_utils/src/clients/mod.rs delete mode 100644 crates/algokit_utils/src/clients/network_client.rs delete mode 100644 crates/algokit_utils/src/config.rs delete mode 100644 crates/algokit_utils/src/lib.rs delete mode 100644 crates/algokit_utils/src/transactions/app_call.rs delete mode 100644 crates/algokit_utils/src/transactions/asset_config.rs delete mode 100644 crates/algokit_utils/src/transactions/asset_freeze.rs delete mode 100644 crates/algokit_utils/src/transactions/asset_transfer.rs delete mode 100644 crates/algokit_utils/src/transactions/common.rs delete mode 100644 crates/algokit_utils/src/transactions/composer.rs delete mode 100644 crates/algokit_utils/src/transactions/creator.rs delete mode 100644 crates/algokit_utils/src/transactions/key_registration.rs delete mode 100644 crates/algokit_utils/src/transactions/mod.rs delete mode 100644 crates/algokit_utils/src/transactions/payment.rs delete mode 100644 crates/algokit_utils/src/transactions/sender.rs delete mode 100644 crates/algokit_utils/tests/algod/block.rs delete mode 100644 crates/algokit_utils/tests/algod/mod.rs delete mode 100644 crates/algokit_utils/tests/algod/pending_transaction_information.rs delete mode 100644 crates/algokit_utils/tests/algod/raw_transaction.rs delete mode 100644 crates/algokit_utils/tests/algod/simulate_transactions.rs delete mode 100644 crates/algokit_utils/tests/algod/transaction_params.rs delete mode 100644 crates/algokit_utils/tests/algod_tests.rs delete mode 100644 crates/algokit_utils/tests/applications/app_client/client_management.rs delete mode 100644 crates/algokit_utils/tests/applications/app_client/compilation.rs delete mode 100644 crates/algokit_utils/tests/applications/app_client/default_values.rs delete mode 100644 crates/algokit_utils/tests/applications/app_client/error_handling.rs delete mode 100644 crates/algokit_utils/tests/applications/app_client/mod.rs delete mode 100644 crates/algokit_utils/tests/applications/app_client/params.rs delete mode 100644 crates/algokit_utils/tests/applications/app_client/send.rs delete mode 100644 crates/algokit_utils/tests/applications/app_client/state.rs delete mode 100644 crates/algokit_utils/tests/applications/app_client/structs.rs delete mode 100644 crates/algokit_utils/tests/applications/app_deployer.rs delete mode 100644 crates/algokit_utils/tests/applications/app_factory.rs delete mode 100644 crates/algokit_utils/tests/applications/mod.rs delete mode 100644 crates/algokit_utils/tests/applications_tests.rs delete mode 100644 crates/algokit_utils/tests/clients/algorand_client.rs delete mode 100644 crates/algokit_utils/tests/clients/app_manager.rs delete mode 100644 crates/algokit_utils/tests/clients/asset_manager.rs delete mode 100644 crates/algokit_utils/tests/clients/client_manager.rs delete mode 100644 crates/algokit_utils/tests/clients/mod.rs delete mode 100644 crates/algokit_utils/tests/clients_tests.rs delete mode 100644 crates/algokit_utils/tests/common/app_fixture.rs delete mode 100644 crates/algokit_utils/tests/common/fixture.rs delete mode 100644 crates/algokit_utils/tests/common/indexer_helpers.rs delete mode 100644 crates/algokit_utils/tests/common/local_net_dispenser.rs delete mode 100644 crates/algokit_utils/tests/common/logging.rs delete mode 100644 crates/algokit_utils/tests/common/mnemonic.rs delete mode 100644 crates/algokit_utils/tests/common/mod.rs delete mode 100644 crates/algokit_utils/tests/common/test_account.rs delete mode 100644 crates/algokit_utils/tests/indexer/mod.rs delete mode 100644 crates/algokit_utils/tests/indexer/search_applications.rs delete mode 100644 crates/algokit_utils/tests/indexer/search_transactions.rs delete mode 100644 crates/algokit_utils/tests/indexer_tests.rs delete mode 100644 crates/algokit_utils/tests/kmd/key_management.rs delete mode 100644 crates/algokit_utils/tests/kmd/mod.rs delete mode 100644 crates/algokit_utils/tests/kmd/wallet_lifecycle.rs delete mode 100644 crates/algokit_utils/tests/kmd_tests.rs delete mode 100644 crates/algokit_utils/tests/transactions/composer/app_call.rs delete mode 100644 crates/algokit_utils/tests/transactions/composer/asset_config.rs delete mode 100644 crates/algokit_utils/tests/transactions/composer/asset_freeze.rs delete mode 100644 crates/algokit_utils/tests/transactions/composer/asset_transfer.rs delete mode 100644 crates/algokit_utils/tests/transactions/composer/inner_fee_coverage.rs delete mode 100644 crates/algokit_utils/tests/transactions/composer/key_registration.rs delete mode 100644 crates/algokit_utils/tests/transactions/composer/mod.rs delete mode 100644 crates/algokit_utils/tests/transactions/composer/payment.rs delete mode 100644 crates/algokit_utils/tests/transactions/composer/resource_population.rs delete mode 100644 crates/algokit_utils/tests/transactions/composer/transaction_group.rs delete mode 100644 crates/algokit_utils/tests/transactions/creator.rs delete mode 100644 crates/algokit_utils/tests/transactions/mod.rs delete mode 100644 crates/algokit_utils/tests/transactions/sender.rs delete mode 100644 crates/algokit_utils/tests/transactions_tests.rs delete mode 100644 crates/algokit_utils_ffi/Cargo.toml delete mode 100644 crates/algokit_utils_ffi/src/abi/abi_type.rs delete mode 100644 crates/algokit_utils_ffi/src/abi/abi_value.rs delete mode 100644 crates/algokit_utils_ffi/src/abi/mod.rs delete mode 100644 crates/algokit_utils_ffi/src/clients/algod_client.rs delete mode 100644 crates/algokit_utils_ffi/src/clients/mod.rs delete mode 100644 crates/algokit_utils_ffi/src/lib.rs delete mode 100644 crates/algokit_utils_ffi/src/tests/asset_freeze_tests.rs delete mode 100644 crates/algokit_utils_ffi/src/tests/fixtures/localnet.rs delete mode 100644 crates/algokit_utils_ffi/src/tests/fixtures/mnemonic.rs delete mode 100644 crates/algokit_utils_ffi/src/tests/fixtures/mod.rs delete mode 100644 crates/algokit_utils_ffi/src/tests/fixtures/test_account.rs delete mode 100644 crates/algokit_utils_ffi/src/tests/fixtures/test_fixture.rs delete mode 100644 crates/algokit_utils_ffi/src/tests/mod.rs delete mode 100644 crates/algokit_utils_ffi/src/transactions/app_call.rs delete mode 100644 crates/algokit_utils_ffi/src/transactions/asset_config.rs delete mode 100644 crates/algokit_utils_ffi/src/transactions/asset_freeze.rs delete mode 100644 crates/algokit_utils_ffi/src/transactions/asset_transfer.rs delete mode 100644 crates/algokit_utils_ffi/src/transactions/common.rs delete mode 100644 crates/algokit_utils_ffi/src/transactions/composer.rs delete mode 100644 crates/algokit_utils_ffi/src/transactions/key_registration.rs delete mode 100644 crates/algokit_utils_ffi/src/transactions/mod.rs delete mode 100644 crates/algokit_utils_ffi/src/transactions/payment.rs delete mode 100644 crates/algokit_utils_ffi/uniffi.toml delete mode 100644 packages/python/algokit_utils/algokit_utils.code-workspace delete mode 100644 packages/python/algokit_utils/algokit_utils/__init__.py delete mode 100644 packages/python/algokit_utils/algokit_utils/ffi_algod_client.py delete mode 100644 packages/python/algokit_utils/algokit_utils/ffi_composer.py delete mode 100644 packages/python/algokit_utils/algokit_utils/py.typed delete mode 100644 packages/python/algokit_utils/poetry.lock delete mode 100644 packages/python/algokit_utils/poetry.toml delete mode 100644 packages/python/algokit_utils/pyproject.toml delete mode 100644 packages/python/algokit_utils/tests/test_ffi_async.py delete mode 100644 packages/python/algokit_utils/tests/test_utils.py diff --git a/Cargo.lock b/Cargo.lock index a9db36c50..d96490772 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,7 +90,7 @@ dependencies = [ "algokit_transact", "base32", "base64 0.22.1", - "convert_case 0.8.0", + "convert_case", "derive_builder", "ed25519-dalek", "pretty_assertions", @@ -122,67 +122,6 @@ dependencies = [ "uniffi", ] -[[package]] -name = "algokit_utils" -version = "0.1.0" -dependencies = [ - "algod_client", - "algokit_abi", - "algokit_http_client", - "algokit_test_artifacts", - "algokit_transact", - "async-trait", - "base64 0.22.1", - "derive_more", - "dotenvy", - "ed25519-dalek", - "env_logger", - "futures", - "hex", - "indexer_client", - "insta", - "kmd_client", - "lazy_static", - "log", - "num-bigint", - "once_cell", - "rand", - "regex", - "reqwest", - "rstest", - "serde", - "serde_json", - "sha2", - "snafu", - "tokio", - "tokio-test", -] - -[[package]] -name = "algokit_utils_ffi" -version = "0.1.0" -dependencies = [ - "algod_client", - "algokit_abi", - "algokit_http_client", - "algokit_transact", - "algokit_transact_ffi", - "algokit_utils", - "async-trait", - "base32", - "base64 0.22.1", - "derive_more", - "ed25519-dalek", - "lazy_static", - "num-bigint", - "rand", - "regex", - "sha2", - "snafu", - "tokio", - "uniffi", -] - [[package]] name = "ammonia" version = "4.1.0" @@ -450,7 +389,7 @@ version = "0.1.0" dependencies = [ "clap", "color-eyre", - "convert_case 0.8.0", + "convert_case", "duct", "shlex", ] @@ -658,15 +597,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "convert_case" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "convert_case" version = "0.8.0" @@ -898,28 +828,6 @@ dependencies = [ "syn", ] -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "convert_case 0.7.1", - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] - [[package]] name = "diff" version = "0.1.13" @@ -954,12 +862,6 @@ dependencies = [ "mdbook", ] -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - [[package]] name = "dtoa" version = "1.0.10" @@ -1005,7 +907,6 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core", "serde", "sha2", "subtle", @@ -1104,7 +1005,7 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" name = "ffi_macros" version = "0.1.0" dependencies = [ - "convert_case 0.8.0", + "convert_case", "quote", "syn", ] @@ -1185,21 +1086,6 @@ 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" @@ -1216,23 +1102,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - [[package]] name = "futures-macro" version = "0.3.31" @@ -1268,13 +1137,10 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures-channel", "futures-core", - "futures-io", "futures-macro", "futures-sink", "futures-task", - "memchr", "pin-project-lite", "pin-utils", "slab", @@ -1896,8 +1762,6 @@ checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" dependencies = [ "console", "once_cell", - "pest", - "pest_derive", "serde", "similar", ] @@ -2746,9 +2610,7 @@ dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", - "futures-channel", "futures-core", - "futures-util", "h2 0.4.10", "http 1.3.1", "http-body 1.0.1", @@ -3826,12 +3688,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "uniffi" version = "0.29.4" diff --git a/Cargo.toml b/Cargo.toml index 6dc87803c..bd1504432 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ members = [ "crates/algokit_http_client", "crates/algokit_transact", "crates/algokit_transact_ffi", - "crates/algokit_utils", "crates/algokit_abi", "crates/algokit_test_artifacts", "crates/ffi_macros", @@ -16,7 +15,6 @@ members = [ "docs", "tools/cargo-bin", "tools/api_tools", - "crates/algokit_utils_ffi", ] [workspace.dependencies] diff --git a/crates/algokit_utils/Cargo.toml b/crates/algokit_utils/Cargo.toml deleted file mode 100644 index c114137d5..000000000 --- a/crates/algokit_utils/Cargo.toml +++ /dev/null @@ -1,51 +0,0 @@ -[package] -name = "algokit_utils" -version = "0.1.0" -edition = "2024" - -[features] -default = ["default_http_client"] -default_http_client = [ - "algokit_http_client/default_client", - "algod_client/default_client", - "indexer_client/default_client", -] - -[dependencies] -algod_client = { path = "../algod_client" } -indexer_client = { path = "../indexer_client" } -algokit_abi = { path = "../algokit_abi" } -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", -] } -kmd_client = { path = "../kmd_client" } -async-trait = { version = "0.1.88" } -base64 = "0.22.1" -derive_more = { version = "2.0.1", features = ["full"] } -dotenvy = "0.15" -log = "0.4.27" -reqwest = { version = "0.12.19", features = ["blocking"] } -snafu = { workspace = true } -tokio = { version = "1.45.1", features = ["time", "sync"] } - -# Dependencies used in algod client integrations tests -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0.133" -once_cell = "1.19" -futures = "0.3" -regex = "1.10.2" -ed25519-dalek = { version = "2.1.1", features = ["rand_core"] } -hex = "0.4.3" -rand = "0.8" -sha2 = { workspace = true } -lazy_static = "1.4" -num-bigint = "0.4" - -[dev-dependencies] -tokio = { version = "1.45.1", features = ["full", "test-util", "time"] } -tokio-test = "^0.4" -algokit_test_artifacts = { path = "../algokit_test_artifacts" } -env_logger = "0.11" -rstest = { workspace = true } -insta = { version = "1.43", features = ["json", "redactions"] } diff --git a/crates/algokit_utils/src/applications/app_client/compilation.rs b/crates/algokit_utils/src/applications/app_client/compilation.rs deleted file mode 100644 index 42a8f9296..000000000 --- a/crates/algokit_utils/src/applications/app_client/compilation.rs +++ /dev/null @@ -1,93 +0,0 @@ -use super::{AppClient, AppClientError}; -use crate::{ - Config, EventType, - applications::app_client::types::CompilationParams, - clients::app_manager::DeploymentMetadata, - config::{AppCompiledEventData, EventData}, -}; - -use crate::clients::app_manager::{CompiledPrograms, CompiledTeal}; - -impl AppClient { - /// Compile the application's approval and clear programs with optional template parameters. - pub async fn compile( - &self, - compilation_params: &CompilationParams, - ) -> Result { - let approval = self.compile_approval(compilation_params).await?; - let clear = self.compile_clear(compilation_params).await?; - - // Emit AppCompiled event when debug flag is enabled - if Config::debug() { - let app_name = self.app_name.clone(); - let approval_map = approval.source_map.clone(); - let clear_map = clear.source_map.clone(); - - let event = AppCompiledEventData { - app_name, - approval_source_map: approval_map, - clear_source_map: clear_map, - }; - Config::events() - .emit(EventType::AppCompiled, EventData::AppCompiled(event)) - .await; - } - - Ok(CompiledPrograms { approval, clear }) - } - - async fn compile_approval( - &self, - compilation_params: &CompilationParams, - ) -> Result { - // 1) Decode TEAL from ARC-56 source - let (teal, _) = - self.app_spec - .decoded_teal() - .map_err(|e| AppClientError::CompilationError { - message: e.to_string(), - })?; - - // 2-4) Compile via AppManager helper with template params and deploy-time controls - let metadata = DeploymentMetadata { - updatable: compilation_params.updatable, - deletable: compilation_params.deletable, - }; - - let compiled = self - .algorand() - .app() - .compile_teal_template( - &teal, - compilation_params.deploy_time_params.as_ref(), - Some(&metadata), - ) - .await - .map_err(|e| AppClientError::AppManagerError { source: e })?; - - Ok(compiled) - } - - async fn compile_clear( - &self, - compilation_params: &CompilationParams, - ) -> Result { - // 1) Decode TEAL from ARC-56 source - let (_, teal) = - self.app_spec - .decoded_teal() - .map_err(|e| AppClientError::CompilationError { - message: e.to_string(), - })?; - - // 2-4) Compile via AppManager helper with template params; no deploy-time controls for clear - let compiled = self - .algorand() - .app() - .compile_teal_template(&teal, compilation_params.deploy_time_params.as_ref(), None) - .await - .map_err(|e| AppClientError::AppManagerError { source: e })?; - - Ok(compiled) - } -} diff --git a/crates/algokit_utils/src/applications/app_client/error.rs b/crates/algokit_utils/src/applications/app_client/error.rs deleted file mode 100644 index 46ba51265..000000000 --- a/crates/algokit_utils/src/applications/app_client/error.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::ComposerError; -use crate::applications::app_client::types::LogicError; -use crate::clients::app_manager::AppManagerError; -use crate::clients::client_manager::ClientManagerError; -use crate::transactions::TransactionSenderError; -use algokit_abi::error::ABIError; -use algokit_transact::AlgoKitTransactError; -use snafu::Snafu; - -#[derive(Debug, Snafu)] -pub enum AppClientError { - #[snafu(display( - "No app ID found for network {network_names:?}. Available keys in spec: {available:?}" - ))] - AppIdNotFound { - network_names: Vec, - available: Vec, - }, - #[snafu(display("Network error: {message}"))] - Network { message: String }, - #[snafu(display("Lookup error: {message}"))] - Lookup { message: String }, - #[snafu(display("Method not found: {message}"))] - MethodNotFound { message: String }, - #[snafu(display("ABI error: {source}"))] - ABIError { source: ABIError }, - #[snafu(display("Transaction sender error: {source}"))] - TransactionSenderError { source: TransactionSenderError }, - #[snafu(display("App manager error: {source}"))] - AppManagerError { source: AppManagerError }, - #[snafu(display("Compilation error: {message}"))] - CompilationError { message: String }, - #[snafu(display("Validation error: {message}"))] - ValidationError { message: String }, - #[snafu(display("{message}"))] - LogicError { - message: String, - logic: Box, - }, - #[snafu(display("Transact error: {source}"))] - TransactError { source: AlgoKitTransactError }, - #[snafu(display("Params builder error: {message}"))] - ParamsBuilderError { message: String }, - #[snafu(display("Composer error: {source}"))] - ComposerError { source: ComposerError }, - #[snafu(display("App state error: {message}"))] - AppStateError { message: String }, - #[snafu(display("Decode error: {message}"))] - DecodeError { message: String }, - #[snafu(display("Client manager error: {source}"))] - ClientManagerError { source: ClientManagerError }, -} diff --git a/crates/algokit_utils/src/applications/app_client/error_transformation.rs b/crates/algokit_utils/src/applications/app_client/error_transformation.rs deleted file mode 100644 index ca8280abe..000000000 --- a/crates/algokit_utils/src/applications/app_client/error_transformation.rs +++ /dev/null @@ -1,434 +0,0 @@ -use super::types::LogicError; -use super::{AppClient, AppSourceMaps}; -use crate::AlgorandClient; -use crate::{AppClientError, TransactionSenderError}; -use algokit_abi::{Arc56Contract, arc56_contract::PcOffsetMethod}; -use lazy_static::lazy_static; -use regex::Regex; -use serde_json::Value as JsonValue; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct LogicErrorData { - pub transaction_id: String, - pub message: String, - pub pc: u64, -} - -lazy_static! { - static ref LOGIC_ERROR_RE: Regex = Regex::new( - r".*transaction (?P[A-Z2-7]{52}): logic eval error: (?P.*)\. Details: .*pc=(?P[0-9]+).*" - ) - .unwrap(); - static ref INNER_LOGIC_ERROR_RE: Regex = - Regex::new(r"inner tx (\d+) failed:.*?pc=([0-9]+)").unwrap(); -} - -pub(crate) fn extract_logic_error_data(error_str: &str) -> Option { - let caps = LOGIC_ERROR_RE.captures(error_str)?; - let pc = if let Some(inner) = INNER_LOGIC_ERROR_RE.captures(error_str) { - inner - .get(2) - .and_then(|m| m.as_str().parse::().ok()) - .unwrap_or_else(|| caps["pc"].parse::().unwrap_or(0)) - } else { - caps["pc"].parse::().unwrap_or(0) - }; - Some(LogicErrorData { - transaction_id: caps["transaction_id"].to_string(), - message: caps["message"].to_string(), - pc, - }) -} - -pub(crate) struct LogicErrorContext<'logic_error_ctx> { - pub app_id: u64, - pub app_spec: &'logic_error_ctx Arc56Contract, - pub algorand: &'logic_error_ctx AlgorandClient, - pub source_maps: Option<&'logic_error_ctx AppSourceMaps>, -} - -impl AppClient { - /// Import compiled source maps for approval and clear programs. - pub fn import_source_maps(&mut self, source_maps: AppSourceMaps) { - self.source_maps = Some(source_maps); - } - - /// Export compiled source maps if available. - pub fn export_source_maps(&self) -> Option { - self.source_maps.clone() - } -} - -impl LogicErrorContext<'_> { - /// Create an enhanced LogicError from a transaction error, applying source maps if available. - pub(crate) fn expose_logic_error( - &self, - error_message: &str, - is_clear_state_program: bool, - ) -> LogicError { - let parsed_logic_error_data = extract_logic_error_data(error_message); - let (mut line_no_opt, mut listing) = - self.apply_source_map_for_message(error_message, is_clear_state_program); - let source_map = self.get_source_map(is_clear_state_program).cloned(); - let transaction_id = Self::extract_transaction_id(error_message); - let pc_opt = Self::extract_pc(error_message); - - let mut logic = LogicError { - message: error_message.to_string(), - program: None, - source_map, - transaction_id, - pc: pc_opt, - line_no: line_no_opt, - lines: if listing.is_empty() { - None - } else { - Some(listing.clone()) - }, - traces: None, - logic_error_str: Some(error_message.to_string()), - }; - - let (tx_id, parsed_pc, msg_msg) = if let Some(p) = parsed_logic_error_data { - ( - Some(p.transaction_id.clone()), - Some(p.pc), - Some(p.message.clone()), - ) - } else { - (logic.transaction_id.clone(), logic.pc, None) - }; - - let mut arc56_error_message: Option = None; - let mut arc56_line_no: Option = None; - let mut arc56_listing: Vec = Vec::new(); - - if let Some(si_model) = self.app_spec.source_info.as_ref() { - let program_source_info = if is_clear_state_program { - &si_model.clear - } else { - &si_model.approval - }; - - let mut arc56_pc = parsed_pc.unwrap_or(0); - - if matches!( - program_source_info.pc_offset_method, - PcOffsetMethod::Cblocks - ) { - // Apply CBLOCKS offset only if compiled program bytes are available via cache - if let Some(bytes) = self.get_program_bytes(is_clear_state_program) { - let offset = Self::get_constant_block_offset(&bytes); - arc56_pc = arc56_pc.saturating_sub(offset as u64); - } - } - - if arc56_pc > 0 { - if let Some(source_info) = program_source_info - .source_info - .iter() - .find(|s| s.pc.iter().any(|v| *v as u64 == arc56_pc)) - { - if let Some(em) = &source_info.error_message { - arc56_error_message = Some(em.clone()); - } - if arc56_line_no.is_none() { - if let Some(teal_line) = source_info.teal { - arc56_line_no = Some(teal_line as u64); - } - } - } - } - - if arc56_line_no.is_some() - && self.app_spec.source.is_some() - && self.get_source_map(is_clear_state_program).is_none() - { - if let Some(teal_src) = self.decode_teal(is_clear_state_program) { - let center = arc56_line_no.unwrap(); - arc56_listing = Self::annotated_teal_snippet(&teal_src, center, 3); - } - } - } - - if line_no_opt.is_none() && arc56_line_no.is_some() { - line_no_opt = arc56_line_no; - logic.line_no = line_no_opt; - } - if listing.is_empty() && !arc56_listing.is_empty() { - listing = arc56_listing; - logic.lines = Some(listing.clone()); - } - - if let Some(emsg) = arc56_error_message.or(msg_msg) { - let app_id_from_msg = Self::extract_app_id(error_message); - let app_id = app_id_from_msg - .or_else(|| Some(self.app_id.to_string())) - .unwrap_or_else(|| "N/A".to_string()); - let txid_str = tx_id.unwrap_or_else(|| "N/A".to_string()); - let runtime_msg = format!( - "Runtime error when executing {} (appId: {}) in transaction {}: {}", - self.app_spec.name, app_id, txid_str, emsg - ); - logic.message = runtime_msg.clone(); - } - - logic - } - - /// Extract transaction id from an error string. - fn extract_transaction_id(error_str: &str) -> Option { - let re = regex::Regex::new(r"transaction ([A-Z2-7]{52})").unwrap(); - re.captures(error_str) - .and_then(|caps| caps.get(1)) - .map(|m| m.as_str().to_string()) - } - - /// Compute line and listing using a source map when available. - fn apply_source_map_for_message( - &self, - error_str: &str, - is_clear_state_program: bool, - ) -> (Option, Vec) { - let pc_opt = Self::extract_pc(error_str); - if let Some(pc) = pc_opt { - if let Some((line_no, listing)) = self.apply_source_map(pc, is_clear_state_program) { - return (Some(line_no), listing); - } - } - (None, Vec::new()) - } - - /// Extract program counter from an error string. - fn extract_pc(s: &str) -> Option { - for token in s.split(|c: char| c.is_whitespace() || c == ',' || c == ';') { - if let Some(idx) = token.find('=') { - let (k, v) = token.split_at(idx); - if k.ends_with("pc") { - if let Ok(parsed_logic_error_data) = v.trim_start_matches('=').parse::() { - return Some(parsed_logic_error_data); - } - } - } - } - None - } - - /// Map pc to TEAL line and extract a short snippet. - fn apply_source_map( - &self, - pc: u64, - is_clear_state_program: bool, - ) -> Option<(u64, Vec)> { - let map = self.get_source_map(is_clear_state_program)?; - let line_no = Self::map_pc_to_line(map, pc)?; - let listing = Self::truncate_listing(map, line_no, 3); - Some((line_no, listing)) - } - - /// Get the selected program's source map. - fn get_source_map(&self, is_clear_state_program: bool) -> Option<&JsonValue> { - let maps = self.source_maps?; - if is_clear_state_program { - maps.clear_source_map.as_ref() - } else { - maps.approval_source_map.as_ref() - } - } - - /// Map a program counter to a source line using the pc array. - fn map_pc_to_line(map: &JsonValue, pc: u64) -> Option { - let pcs = map.get("pc")?.as_array()?; - let mut best_line: Option = None; - for (i, entry) in pcs.iter().enumerate() { - if let Some(pc_val) = entry.as_u64() { - if pc_val == pc { - return Some(i as u64 + 1); - } - if pc_val < pc { - best_line = Some(i as u64 + 1); - } - } - } - best_line - } - - /// Format a numbered snippet around a source line from a source map. - fn truncate_listing(map: &JsonValue, center_line: u64, context: usize) -> Vec { - let mut lines: Vec = Vec::new(); - if let Some(source) = map.get("source").and_then(|s| s.as_str()) { - let src_lines: Vec<&str> = source.lines().collect(); - let total = src_lines.len(); - let center = center_line.saturating_sub(1) as usize; - let start = center.saturating_sub(context); - let end = (center + context + 1).min(total); - for (i, line) in src_lines.iter().enumerate().take(end).skip(start) { - lines.push(format!("{:>4} | {}", i + 1, line)); - } - } - lines - } - - /// Format a numbered snippet around a source line from raw TEAL. - fn truncate_teal_source(source: &str, center_line: u64, context: usize) -> Vec { - let mut lines: Vec = Vec::new(); - let src_lines: Vec<&str> = source.lines().collect(); - let total = src_lines.len(); - if total == 0 { - return lines; - } - let center = center_line.saturating_sub(1) as usize; - let start = center.saturating_sub(context); - let end = (center + context + 1).min(total); - for (i, line) in src_lines.iter().enumerate().take(end).skip(start) { - lines.push(format!("{:>4} | {}", i + 1, line)); - } - lines - } - - /// Like truncate_teal_source but adds a subtle error marker on the center line. - fn annotated_teal_snippet(source: &str, center_line: u64, context: usize) -> Vec { - let mut lines = Self::truncate_teal_source(source, center_line, context); - // Try to mark the line that equals center_line if present - let needle = format!("{:>4} |", center_line); - let mut marked = false; - for entry in &mut lines { - if entry.starts_with(&needle) { - *entry = format!("{}\t<-- Error", entry); - marked = true; - break; - } - } - // Fallback: mark middle line if exact match not found - if !marked && !lines.is_empty() { - let mid = lines.len() / 2; - lines[mid] = format!("{}\t<-- Error", lines[mid]); - } - lines - } - - /// Calculate the offset after initial constant blocks in a TEAL program (CBLOCKS). - fn get_constant_block_offset(program: &[u8]) -> usize { - const BYTE_CBLOCK: u8 = 38; // bytecblock - const INT_CBLOCK: u8 = 32; // intcblock - if program.is_empty() { - return 0; - } - let mut i = 1; // skip version byte - let len = program.len(); - let mut bytec_off: Option = None; - let mut intc_off: Option = None; - - while i < len { - let op = program[i]; - i += 1; - if op != BYTE_CBLOCK && op != INT_CBLOCK { - break; - } - if i >= len { - break; - } - let values_remaining = program[i] as usize; - i += 1; - for _ in 0..values_remaining { - if op == BYTE_CBLOCK { - if i >= len { - break; - } - let elem_len = program[i] as usize; - i += 1 + elem_len.min(len.saturating_sub(i)); - } else { - while i < len { - let b = program[i]; - i += 1; - if (b & 0x80) == 0 { - break; - } - } - } - } - let off = i; - if op == BYTE_CBLOCK { - bytec_off = Some(off) - } else { - intc_off = Some(off) - } - - if i >= len { - break; - } - let next = program[i]; - if next != BYTE_CBLOCK && next != INT_CBLOCK { - break; - } - } - bytec_off.or(intc_off).unwrap_or(0) - } - - /// Try to get compiled program bytes for the app from the compilation cache. - /// This avoids async calls; returns None if not available. - fn get_program_bytes(&self, is_clear_state_program: bool) -> Option> { - let teal_src = self.decode_teal(is_clear_state_program)?; - self.algorand - .app() - .get_compilation_result(&teal_src) - .map(|c| c.compiled_base64_to_bytes) - } - - /// Decode base64 TEAL source from the app spec. - fn decode_teal(&self, is_clear_state_program: bool) -> Option { - let src = self.app_spec.source.as_ref()?; - if is_clear_state_program { - src.get_decoded_clear().ok() - } else { - src.get_decoded_approval().ok() - } - } - - /// Extract app id from an error string. - fn extract_app_id(error_str: &str) -> Option { - let re = regex::Regex::new(r"app=(\d+)").ok()?; - re.captures(error_str) - .and_then(|caps| caps.get(1)) - .map(|m| m.as_str().to_string()) - } -} - -impl AppClient { - pub(crate) fn transform_transaction_error( - &self, - err: TransactionSenderError, - is_clear_state_program: bool, - ) -> AppClientError { - let error_message = err.to_string(); - - // Only transform errors that are for this app (when app_id is known) - if self.app_id() != 0 { - let app_tag = format!("app={}", self.app_id()); - if !error_message.contains(&app_tag) { - return AppClientError::TransactionSenderError { source: err }; - } - } - - let parsed_logic_error_data = extract_logic_error_data(&error_message); - - match parsed_logic_error_data { - Some(_) => { - let context = LogicErrorContext { - app_id: self.app_id(), - app_spec: self.app_spec(), - algorand: self.algorand(), - source_maps: self.source_maps.as_ref(), - }; - - let logic_error = - context.expose_logic_error(&error_message, is_clear_state_program); - AppClientError::LogicError { - message: logic_error.message.clone(), - logic: Box::new(logic_error), - } - } - None => AppClientError::TransactionSenderError { source: err }, - } - } -} diff --git a/crates/algokit_utils/src/applications/app_client/mod.rs b/crates/algokit_utils/src/applications/app_client/mod.rs deleted file mode 100644 index 509f761af..000000000 --- a/crates/algokit_utils/src/applications/app_client/mod.rs +++ /dev/null @@ -1,380 +0,0 @@ -use crate::applications::AppDeployer; -use crate::clients::app_manager::{AppState, BoxName}; -use crate::clients::network_client::NetworkDetails; -use crate::transactions::{TransactionComposerConfig, TransactionSigner}; -use crate::{AlgorandClient, clients::app_manager::BoxIdentifier}; -use crate::{SendParams, SendResult}; -use algokit_abi::{ABIType, ABIValue, Arc56Contract}; -use algokit_transact::Address; -use std::collections::HashMap; -use std::str::FromStr; -use std::sync::Arc; - -/// A box value decoded according to an ABI type -#[derive(Debug, Clone)] -pub struct BoxABIValue { - pub name: BoxName, - pub value: ABIValue, -} - -/// A box name and its raw value -#[derive(Debug, Clone)] -pub struct BoxValue { - pub name: BoxName, - pub value: Vec, -} -mod compilation; -mod error; -pub(crate) mod error_transformation; -mod params_builder; -mod state_accessor; -mod transaction_builder; -mod transaction_sender; -mod types; -mod utils; -pub use error::AppClientError; -use params_builder::ParamsBuilder; -pub use state_accessor::StateAccessor; -pub use transaction_builder::TransactionBuilder; -pub use transaction_sender::TransactionSender; -pub use types::{ - AppClientBareCallParams, AppClientMethodCallParams, AppClientParams, AppSourceMaps, - CompilationParams, FundAppAccountParams, LogicError, -}; - -type BoxNameFilter = Box bool>; - -/// A client for interacting with an Algorand smart contract application (ARC-56 focused). -pub struct AppClient { - app_id: u64, - app_spec: Arc56Contract, - algorand: Arc, - default_sender: Option, - default_signer: Option>, - source_maps: Option, - app_name: Option, - transaction_composer_config: Option, -} - -impl AppClient { - /// Create a new client from parameters. - pub fn new(params: AppClientParams) -> Self { - Self { - app_id: params.app_id, - app_spec: params.app_spec, - algorand: params.algorand, - default_sender: params.default_sender, - default_signer: params.default_signer, - source_maps: params.source_maps, - app_name: params.app_name, - transaction_composer_config: params.transaction_composer_config, - } - } - - /// Construct from the current network using app_spec.networks mapping. - /// - /// Matches on either the network alias ("localnet", "testnet", "mainnet") - /// or the network's genesis hash present in the node's suggested params. - pub async fn from_network( - app_spec: Arc56Contract, - algorand: Arc, - app_name: Option, - default_sender: Option, - default_signer: Option>, - source_maps: Option, - transaction_composer_config: Option, - ) -> Result { - let network = algorand - .client() - .network() - .await - .map_err(|e| AppClientError::Network { - message: e.to_string(), - })?; - - let candidate_keys = Self::candidate_network_keys(&network); - let (app_id, available_keys) = match &app_spec.networks { - Some(nets) => ( - Self::find_app_id_in_networks(&candidate_keys, nets), - nets.keys().cloned().collect(), - ), - None => (None, Vec::new()), - }; - - let app_id = app_id.ok_or_else(|| AppClientError::AppIdNotFound { - network_names: candidate_keys.clone(), - available: available_keys, - })?; - - Ok(Self::new(AppClientParams { - app_id, - app_spec, - algorand, - app_name, - default_sender, - default_signer, - source_maps, - transaction_composer_config, - })) - } - - /// Construct from creator address and application name via indexer lookup. - #[allow(clippy::too_many_arguments)] - pub async fn from_creator_and_name( - creator_address: &str, - app_name: &str, - app_spec: Arc56Contract, - algorand: Arc, - default_sender: Option, - default_signer: Option>, - source_maps: Option, - ignore_cache: Option, - transaction_composer_config: Option, - ) -> Result { - let address = Address::from_str(creator_address).map_err(|e| AppClientError::Lookup { - message: format!("Invalid creator address: {}", e), - })?; - - let indexer_client = algorand - .client() - .indexer() - .map_err(|e| AppClientError::ClientManagerError { source: e })?; - let mut app_deployer = AppDeployer::new( - algorand.app().clone(), - algorand.send().clone(), - Some(indexer_client), - ); - - let lookup = app_deployer - .get_creator_apps_by_name(&address, ignore_cache) - .await - .map_err(|e| AppClientError::Lookup { - message: e.to_string(), - })?; - - let app_metadata = lookup - .apps - .get(app_name) - .ok_or_else(|| AppClientError::Lookup { - message: format!( - "App not found for creator {} and name {}", - creator_address, app_name - ), - })?; - - Ok(Self::new(AppClientParams { - app_id: app_metadata.app_id, - app_spec, - algorand, - app_name: Some(app_name.to_string()), - default_sender, - default_signer, - source_maps, - transaction_composer_config, - })) - } - - fn candidate_network_keys(network: &NetworkDetails) -> Vec { - let mut names = vec![network.genesis_hash.clone()]; - if network.is_localnet { - names.push("localnet".to_string()); - } - if network.is_mainnet { - names.push("mainnet".to_string()); - } - if network.is_testnet { - names.push("testnet".to_string()); - } - names - } - - fn find_app_id_in_networks( - candidate_keys: &[String], - networks: &HashMap, - ) -> Option { - for key in candidate_keys { - if let Some(net) = networks.get(key) { - return Some(net.app_id); - } - } - None - } - - /// Get the application ID. - pub fn app_id(&self) -> u64 { - self.app_id - } - /// Get the ARC-56 application specification. - pub fn app_spec(&self) -> &Arc56Contract { - &self.app_spec - } - /// Get the Algorand client instance. - pub fn algorand(&self) -> &AlgorandClient { - &self.algorand - } - /// Get the application name if configured. - pub fn app_name(&self) -> Option<&String> { - self.app_name.as_ref() - } - /// Get the default sender address if configured. - pub fn default_sender(&self) -> Option<&String> { - self.default_sender.as_ref() - } - - /// Get the application's account address. - pub fn app_address(&self) -> Address { - Address::from_app_id(&self.app_id) - } - - fn get_sender_address(&self, sender: &Option) -> Result { - let sender_str = sender - .as_ref() - .or(self.default_sender.as_ref()) - .ok_or_else(|| AppClientError::ValidationError { - message: format!( - "No sender provided and no default sender configured for app {}", - self.app_name.as_deref().unwrap_or("") - ), - })?; - Address::from_str(sender_str).map_err(|e| AppClientError::ValidationError { - message: format!("Invalid sender address: {}", e), - }) - } - - /// Resolve the signer for a transaction based on the sender and default configuration. - /// Returns the provided signer, or the default_signer if sender matches default_sender. - pub(crate) fn resolve_signer( - &self, - sender: Option, - signer: Option>, - ) -> Option> { - signer.or_else(|| { - let should_use_default = sender.is_none() || sender == self.default_sender; - - should_use_default - .then(|| self.default_signer.clone()) - .flatten() - }) - } - - /// Fund the application's account with Algos. - pub async fn fund_app_account( - &self, - params: FundAppAccountParams, - send_params: Option, - ) -> Result { - self.send().fund_app_account(params, send_params).await - } - - /// Get the application's global state. - pub async fn get_global_state(&self) -> Result, AppState>, AppClientError> { - self.algorand - .app() - .get_global_state(self.app_id) - .await - .map_err(|e| AppClientError::AppManagerError { source: e }) - } - - /// Get the application's local state for a specific account. - pub async fn get_local_state( - &self, - address: &str, - ) -> Result, AppState>, AppClientError> { - self.algorand - .app() - .get_local_state(self.app_id, address) - .await - .map_err(|e| AppClientError::AppManagerError { source: e }) - } - - /// Get all box names for the application. - pub async fn get_box_names(&self) -> Result, AppClientError> { - self.algorand - .app() - .get_box_names(self.app_id) - .await - .map_err(|e| AppClientError::AppManagerError { source: e }) - } - - /// Get all box values (names and contents) for the application. - pub async fn get_box_values(&self) -> Result, AppClientError> { - let names = self.get_box_names().await?; - let mut values = Vec::new(); - for name in names { - let value = self.get_box_value(&name.name_raw).await?; - values.push(BoxValue { name, value }); - } - Ok(values) - } - - /// Get the raw value of a specific box. - pub async fn get_box_value(&self, name: &BoxIdentifier) -> Result, AppClientError> { - self.algorand - .app() - .get_box_value(self.app_id, name) - .await - .map_err(|e| AppClientError::AppManagerError { source: e }) - } - - /// Get a single box value decoded according to an ABI type. - pub async fn get_box_value_from_abi_type( - &self, - name: &BoxIdentifier, - abi_type: &ABIType, - ) -> Result { - self.algorand - .app() - .get_box_value_from_abi_type(self.app_id, name, abi_type) - .await - .map_err(|e| AppClientError::AppManagerError { source: e }) - } - - /// Get multiple box values decoded according to an ABI type. - pub async fn get_box_values_from_abi_type( - &self, - abi_type: &ABIType, - filter_func: Option, - ) -> Result, AppClientError> { - let names = self.get_box_names().await?; - let filtered_names = if let Some(filter) = filter_func { - names.into_iter().filter(|name| filter(name)).collect() - } else { - names - }; - - let box_names: Vec = filtered_names - .iter() - .map(|name| name.name_raw.clone()) - .collect(); - - let values = self - .algorand - .app() - .get_box_values_from_abi_type(self.app_id, &box_names, abi_type) - .await - .map_err(|e| AppClientError::AppManagerError { source: e })?; - - Ok(filtered_names - .into_iter() - .zip(values.into_iter()) - .map(|(name, value)| BoxABIValue { name, value }) - .collect()) - } - - /// Get a parameter builder for creating transaction parameters. - pub fn params(&self) -> ParamsBuilder<'_> { - ParamsBuilder { client: self } - } - /// Get a transaction builder for creating unsigned transactions. - pub fn create_transaction(&self) -> TransactionBuilder<'_> { - TransactionBuilder { client: self } - } - /// Get a transaction sender for executing transactions. - pub fn send(&self) -> TransactionSender<'_> { - TransactionSender { client: self } - } - /// Get a state accessor for reading application state with ABI decoding. - pub fn state(&self) -> StateAccessor<'_> { - StateAccessor::new(self) - } -} diff --git a/crates/algokit_utils/src/applications/app_client/params_builder.rs b/crates/algokit_utils/src/applications/app_client/params_builder.rs deleted file mode 100644 index dcd2340cf..000000000 --- a/crates/algokit_utils/src/applications/app_client/params_builder.rs +++ /dev/null @@ -1,549 +0,0 @@ -use super::AppClient; -use super::types::{ - AppClientBareCallParams, AppClientMethodCallParams, CompilationParams, FundAppAccountParams, -}; -use crate::AppClientError; -use crate::applications::app_client::utils::parse_account_refs_to_addresses; -use crate::clients::app_manager::AppState; -use crate::clients::app_manager::CompiledPrograms; -use crate::transactions::{ - AppCallMethodCallParams, AppCallParams, AppDeleteMethodCallParams, AppDeleteParams, - AppMethodCallArg, AppUpdateMethodCallParams, AppUpdateParams, PaymentParams, -}; -use algokit_abi::abi_method::ABIDefaultValue; -use algokit_abi::{ABIMethod, ABIMethodArgType, ABIType, ABIValue, DefaultValueSource}; -use algokit_transact::{Address, OnApplicationComplete}; -use base64::Engine; -use std::str::FromStr; - -enum StateSource<'app_client> { - Global, - Local(&'app_client str), -} - -pub struct ParamsBuilder<'app_client> { - pub(crate) client: &'app_client AppClient, -} - -pub struct BareParamsBuilder<'app_client> { - pub(crate) client: &'app_client AppClient, -} - -impl<'app_client> ParamsBuilder<'app_client> { - /// Get the bare call params builder. - pub fn bare(&self) -> BareParamsBuilder<'app_client> { - BareParamsBuilder { - client: self.client, - } - } - - /// Build parameters for an ABI method call with the specified on-complete action. - pub async fn call( - &self, - params: AppClientMethodCallParams, - on_complete: Option, - ) -> Result { - self.get_method_call_params(¶ms, on_complete.unwrap_or(OnApplicationComplete::NoOp)) - .await - } - - /// Build parameters for an ABI method call with OptIn on-complete action. - pub async fn opt_in( - &self, - params: AppClientMethodCallParams, - ) -> Result { - self.get_method_call_params(¶ms, OnApplicationComplete::OptIn) - .await - } - - /// Build parameters for an ABI method call with CloseOut on-complete action. - pub async fn close_out( - &self, - params: AppClientMethodCallParams, - ) -> Result { - self.get_method_call_params(¶ms, OnApplicationComplete::CloseOut) - .await - } - - /// Build parameters for an ABI method call with ClearState on-complete action. - pub async fn clear_state( - &self, - params: AppClientMethodCallParams, - ) -> Result { - self.get_method_call_params(¶ms, OnApplicationComplete::ClearState) - .await - } - - /// Build parameters for an ABI method call with Delete on-complete action. - pub async fn delete( - &self, - params: AppClientMethodCallParams, - ) -> Result { - let abi_method = self.get_abi_method(¶ms.method)?; - let sender = self.client.get_sender_address(¶ms.sender)?.as_str(); - let resolved_args = self - .resolve_args(&abi_method, ¶ms.args, &sender) - .await?; - - Ok(AppDeleteMethodCallParams { - sender: self.client.get_sender_address(¶ms.sender)?, - signer: self - .client - .resolve_signer(params.sender.clone(), params.signer.clone()), - rekey_to: get_optional_address(¶ms.rekey_to)?, - note: params.note.clone(), - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - app_id: self.client.app_id, - method: abi_method, - args: resolved_args, - account_references: parse_account_refs_to_addresses(¶ms.account_references)?, - app_references: params.app_references.clone(), - asset_references: params.asset_references.clone(), - box_references: params.box_references.clone(), - }) - } - - /// Build parameters for updating the application using an ABI method call. - pub async fn update( - &self, - params: AppClientMethodCallParams, - compilation_params: Option, - ) -> Result<(AppUpdateMethodCallParams, CompiledPrograms), AppClientError> { - // Compile programs (and populate AppManager cache/source maps) - let compilation_params = compilation_params.unwrap_or_default(); - let compiled_programs = self.client.compile(&compilation_params).await?; - - let abi_method = self.get_abi_method(¶ms.method)?; - let sender = self.client.get_sender_address(¶ms.sender)?.as_str(); - let resolved_args = self - .resolve_args(&abi_method, ¶ms.args, &sender) - .await?; - - let update_params = AppUpdateMethodCallParams { - sender: self.client.get_sender_address(¶ms.sender)?, - signer: self - .client - .resolve_signer(params.sender.clone(), params.signer.clone()), - rekey_to: get_optional_address(¶ms.rekey_to)?, - note: params.note.clone(), - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - app_id: self.client.app_id, - method: abi_method, - args: resolved_args, - account_references: parse_account_refs_to_addresses(¶ms.account_references)?, - app_references: params.app_references.clone(), - asset_references: params.asset_references.clone(), - box_references: params.box_references.clone(), - approval_program: compiled_programs.approval.compiled_base64_to_bytes.clone(), - clear_state_program: compiled_programs.clear.compiled_base64_to_bytes.clone(), - }; - - Ok((update_params, compiled_programs)) - } - - /// Build parameters for funding the application's account. - pub fn fund_app_account( - &self, - params: &FundAppAccountParams, - ) -> Result { - let sender = self.client.get_sender_address(¶ms.sender)?; - let receiver = self.client.app_address(); - let rekey_to = get_optional_address(¶ms.rekey_to)?; - - Ok(PaymentParams { - sender, - signer: self - .client - .resolve_signer(params.sender.clone(), params.signer.clone()), - rekey_to, - note: params.note.clone(), - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - receiver, - amount: params.amount, - }) - } - - async fn get_method_call_params( - &self, - params: &AppClientMethodCallParams, - on_complete: OnApplicationComplete, - ) -> Result { - let abi_method = self.get_abi_method(¶ms.method)?; - let sender = self.client.get_sender_address(¶ms.sender)?.as_str(); - let resolved_args = self - .resolve_args(&abi_method, ¶ms.args, &sender) - .await?; - - Ok(AppCallMethodCallParams { - sender: self.client.get_sender_address(¶ms.sender)?, - signer: self - .client - .resolve_signer(params.sender.clone(), params.signer.clone()), - rekey_to: get_optional_address(¶ms.rekey_to)?, - note: params.note.clone(), - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - app_id: self.client.app_id, - method: abi_method, - args: resolved_args, - account_references: parse_account_refs_to_addresses(¶ms.account_references)?, - app_references: params.app_references.clone(), - asset_references: params.asset_references.clone(), - box_references: params.box_references.clone(), - on_complete, - }) - } - - fn get_abi_method(&self, method_name_or_signature: &str) -> Result { - self.client - .app_spec - .find_abi_method(method_name_or_signature) - .map_err(|e| AppClientError::ABIError { source: e }) - } - - async fn resolve_args( - &self, - method: &ABIMethod, - provided: &Vec, - sender: &str, - ) -> Result, AppClientError> { - let mut resolved: Vec = Vec::with_capacity(method.args.len()); - - if method.args.len() != provided.len() { - return Err(AppClientError::ValidationError { - message: format!( - "The number of provided arguments is {} while the method expects {} arguments", - provided.len(), - method.args.len() - ), - }); - } - - for (index, (method_arg, provided_arg)) in method.args.iter().zip(provided).enumerate() { - let method_arg_name = method_arg - .name - .clone() - .unwrap_or_else(|| format!("arg{}", index + 1)); - match (&method_arg.arg_type, provided_arg) { - (ABIMethodArgType::Value(value_type), AppMethodCallArg::DefaultValue) => { - let default_value = method_arg.default_value.as_ref().ok_or_else(|| { - AppClientError::ParamsBuilderError { - message: format!( - "No default value defined for argument {} in call to method {}", - method_arg_name, method.name - ), - } - })?; - - let value = self - .resolve_default_value(default_value, value_type, sender) - .await - .map_err(|e| AppClientError::ParamsBuilderError { - message: format!( - "Failed to resolve default value for arg {}: {:?}", - method_arg_name, e - ), - })?; - resolved.push(AppMethodCallArg::ABIValue(value)); - } - (_, AppMethodCallArg::DefaultValue) => { - return Err(AppClientError::ParamsBuilderError { - message: format!( - "Default value is not supported by argument {} in call to method {}", - method_arg_name, method.name - ), - }); - } - // Intentionally defer type compatibility and structural validation to ABI - // encoding/composer (consistent with TS/Py). Here we only enforce arg count and - // default value handling; encoding will surface any mismatches. - (_, value) => { - resolved.push(value.clone()); - } - } - } - - Ok(resolved) - } - - async fn resolve_state_value( - &self, - default: &ABIDefaultValue, - value_type: &ABIType, - source: StateSource<'_>, - ) -> Result { - let key = base64::engine::general_purpose::STANDARD - .decode(&default.data) - .map_err(|e| AppClientError::ParamsBuilderError { - message: format!( - "Failed to decode {} key: {}", - match source { - StateSource::Global => "global", - StateSource::Local(_) => "local", - }, - e - ), - })?; - - let state = match source { - StateSource::Global => self.client.get_global_state().await?, - StateSource::Local(sender) => self.client.get_local_state(sender).await?, - }; - - let app_state = state - .values() - .find(|value| match value { - AppState::Uint(uint_value) => uint_value.key_raw == key, - AppState::Bytes(bytes_value) => bytes_value.key_raw == key, - }) - .ok_or_else(|| AppClientError::ParamsBuilderError { - message: format!( - "The key {} could not be found in {} storage", - default.data, - match source { - StateSource::Global => "global", - StateSource::Local(_) => "local", - } - ), - })?; - - match app_state { - AppState::Uint(uint_value) => Ok(ABIValue::from(uint_value.value)), - AppState::Bytes(bytes_value) => Ok(value_type - .decode(&bytes_value.value_raw) - .map_err(|e| AppClientError::ABIError { source: e })?), - } - } - - /// Resolve a default value from various sources (method call, literal, state, or box). - pub async fn resolve_default_value( - &self, - default: &ABIDefaultValue, - value_type: &ABIType, - sender: &str, - ) -> Result { - let value_type = default.value_type.clone().unwrap_or(value_type.clone()); - - match default.source { - DefaultValueSource::Method => { - let method_signature = default.data.clone(); - let arc56_method = self - .client - .app_spec - .get_method(&method_signature) - .map_err(|e| AppClientError::ABIError { source: e })?; - - let method_call_params = AppClientMethodCallParams { - method: method_signature.clone(), - args: vec![AppMethodCallArg::DefaultValue; arc56_method.args.len()], - sender: Some(sender.to_string()), - ..Default::default() - }; - - let app_call_result = - Box::pin(self.client.send().call(method_call_params, None, None)).await?; - - let abi_return = app_call_result.result.abi_return.ok_or_else(|| { - AppClientError::ParamsBuilderError { - message: "Method call did not return a value".to_string(), - } - })?; - match abi_return.return_value { - None => Err(AppClientError::ParamsBuilderError { - message: "Method call did not return a value".to_string(), - }), - Some(return_value) => Ok(return_value), - } - } - DefaultValueSource::Literal => { - let value_bytes = base64::engine::general_purpose::STANDARD - .decode(&default.data) - .map_err(|e| AppClientError::ParamsBuilderError { - message: format!("Failed to decode base64 literal: {}", e), - })?; - Ok(value_type - .decode(&value_bytes) - .map_err(|e| AppClientError::ABIError { source: e })?) - } - DefaultValueSource::Global => { - self.resolve_state_value(default, &value_type, StateSource::Global) - .await - } - DefaultValueSource::Local => { - self.resolve_state_value(default, &value_type, StateSource::Local(sender)) - .await - } - DefaultValueSource::Box => { - let box_key = base64::engine::general_purpose::STANDARD - .decode(&default.data) - .map_err(|e| AppClientError::ParamsBuilderError { - message: format!("Failed to decode box key: {}", e), - })?; - let box_value = self.client.get_box_value(&box_key).await?; - Ok(value_type - .decode(&box_value) - .map_err(|e| AppClientError::ABIError { source: e })?) - } - } - } -} - -impl BareParamsBuilder<'_> { - /// Build parameters for a bare application call with the specified on-complete action. - pub fn call( - &self, - params: AppClientBareCallParams, - on_complete: Option, - ) -> Result { - self.build_bare_app_call_params(params, on_complete.unwrap_or(OnApplicationComplete::NoOp)) - } - - /// Build parameters for a bare application call with OptIn on-complete action. - pub fn opt_in(&self, params: AppClientBareCallParams) -> Result { - self.build_bare_app_call_params(params, OnApplicationComplete::OptIn) - } - - /// Build parameters for a bare application call with CloseOut on-complete action. - pub fn close_out( - &self, - params: AppClientBareCallParams, - ) -> Result { - self.build_bare_app_call_params(params, OnApplicationComplete::CloseOut) - } - - /// Build parameters for a bare application call with Delete on-complete action. - pub fn delete( - &self, - params: AppClientBareCallParams, - ) -> Result { - Ok(AppDeleteParams { - sender: self.client.get_sender_address(¶ms.sender)?, - signer: self - .client - .resolve_signer(params.sender.clone(), params.signer.clone()), - rekey_to: get_optional_address(¶ms.rekey_to)?, - note: params.note.clone(), - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - app_id: self.client.app_id, - args: params.args, - account_references: parse_account_refs_to_addresses(¶ms.account_references)?, - app_references: params.app_references, - asset_references: params.asset_references, - box_references: params.box_references, - }) - } - - /// Build parameters for a bare application call with ClearState on-complete action. - pub fn clear_state( - &self, - params: AppClientBareCallParams, - ) -> Result { - self.build_bare_app_call_params(params, OnApplicationComplete::ClearState) - } - - /// Build parameters for updating the application using a bare application call. - pub async fn update( - &self, - params: AppClientBareCallParams, - compilation_params: Option, - ) -> Result<(AppUpdateParams, CompiledPrograms), AppClientError> { - // Compile programs (and populate AppManager cache/source maps) - let compilation_params = compilation_params.unwrap_or_default(); - let compiled_programs = self.client.compile(&compilation_params).await?; - - let update_params = AppUpdateParams { - sender: self.client.get_sender_address(¶ms.sender)?, - signer: self - .client - .resolve_signer(params.sender.clone(), params.signer.clone()), - rekey_to: get_optional_address(¶ms.rekey_to)?, - note: params.note.clone(), - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - app_id: self.client.app_id, - args: params.args, - account_references: parse_account_refs_to_addresses(¶ms.account_references)?, - app_references: params.app_references, - asset_references: params.asset_references, - box_references: params.box_references, - approval_program: compiled_programs.approval.compiled_base64_to_bytes.clone(), - clear_state_program: compiled_programs.clear.compiled_base64_to_bytes.clone(), - }; - - Ok((update_params, compiled_programs)) - } - - fn build_bare_app_call_params( - &self, - params: AppClientBareCallParams, - on_complete: OnApplicationComplete, - ) -> Result { - Ok(AppCallParams { - sender: self.client.get_sender_address(¶ms.sender)?, - signer: self - .client - .resolve_signer(params.sender.clone(), params.signer.clone()), - rekey_to: get_optional_address(¶ms.rekey_to)?, - note: params.note.clone(), - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - app_id: self.client.app_id, - on_complete, - args: params.args, - account_references: parse_account_refs_to_addresses(¶ms.account_references)?, - app_references: params.app_references, - asset_references: params.asset_references, - box_references: params.box_references, - }) - } -} - -fn get_optional_address(value: &Option) -> Result, AppClientError> { - match value { - Some(s) => { - Ok(Some(Address::from_str(s).map_err(|e| { - AppClientError::TransactError { source: e } - })?)) - } - None => Ok(None), - } -} diff --git a/crates/algokit_utils/src/applications/app_client/state_accessor.rs b/crates/algokit_utils/src/applications/app_client/state_accessor.rs deleted file mode 100644 index 1dabbbcbe..000000000 --- a/crates/algokit_utils/src/applications/app_client/state_accessor.rs +++ /dev/null @@ -1,417 +0,0 @@ -use super::{AppClient, AppClientError}; -use crate::clients::app_manager::AppState; -use algokit_abi::arc56_contract::{ABIStorageKey, ABIStorageMap}; -use algokit_abi::{ABIType, ABIValue}; -use async_trait::async_trait; -use base64::Engine; -use num_bigint::BigUint; -use std::collections::HashMap; - -pub struct BoxStateAccessor<'app_client> { - client: &'app_client AppClient, -} - -pub struct StateAccessor<'app_client> { - pub(crate) client: &'app_client AppClient, -} - -impl<'app_client> StateAccessor<'app_client> { - /// Create a new state accessor for the given app client. - pub fn new(client: &'app_client AppClient) -> Self { - Self { client } - } - - /// Get an accessor for the application's global state. - pub fn global_state(&self) -> AppStateAccessor<'_> { - let provider = GlobalStateProvider { - client: self.client, - }; - AppStateAccessor::new("global".to_string(), Box::new(provider)) - } - - /// Get an accessor for an account's local state with this application. - pub fn local_state(&self, address: &str) -> AppStateAccessor<'_> { - let provider = LocalStateProvider { - client: self.client, - address: address.to_string(), - }; - AppStateAccessor::new("local".to_string(), Box::new(provider)) - } - - /// Get an accessor for the application's box storage. - pub fn box_storage(&self) -> BoxStateAccessor<'app_client> { - BoxStateAccessor { - client: self.client, - } - } -} - -type GetStateResult = Result, AppState>, AppClientError>; - -#[async_trait(?Send)] -pub trait StateProvider { - async fn get_app_state(&self) -> GetStateResult; - fn get_storage_keys(&self) -> Result, AppClientError>; - fn get_storage_maps(&self) -> Result, AppClientError>; -} - -struct GlobalStateProvider<'app_client> { - client: &'app_client AppClient, -} - -#[async_trait(?Send)] -impl StateProvider for GlobalStateProvider<'_> { - async fn get_app_state(&self) -> GetStateResult { - self.client.get_global_state().await - } - - fn get_storage_keys(&self) -> Result, AppClientError> { - self.client - .app_spec - .get_global_abi_storage_keys() - .map_err(|e| AppClientError::ABIError { source: e }) - } - - fn get_storage_maps(&self) -> Result, AppClientError> { - self.client - .app_spec - .get_global_abi_storage_maps() - .map_err(|e| AppClientError::ABIError { source: e }) - } -} - -struct LocalStateProvider<'app_client> { - client: &'app_client AppClient, - address: String, -} - -#[async_trait(?Send)] -impl StateProvider for LocalStateProvider<'_> { - async fn get_app_state(&self) -> GetStateResult { - self.client.get_local_state(&self.address).await - } - - fn get_storage_keys(&self) -> Result, AppClientError> { - self.client - .app_spec - .get_local_abi_storage_keys() - .map_err(|e| AppClientError::ABIError { source: e }) - } - - fn get_storage_maps(&self) -> Result, AppClientError> { - self.client - .app_spec - .get_local_abi_storage_maps() - .map_err(|e| AppClientError::ABIError { source: e }) - } -} - -pub struct AppStateAccessor<'provider> { - name: String, - provider: Box, -} - -impl<'provider> AppStateAccessor<'provider> { - /// Create a new app state accessor with the given name and provider. - pub fn new(name: String, provider: Box) -> Self { - Self { name, provider } - } - - /// Get all ABI-decoded state values for this storage type. - pub async fn get_all(&self) -> Result>, AppClientError> { - let state = self.provider.get_app_state().await?; - let storage_key_map = self.provider.get_storage_keys()?; - - let mut result = HashMap::new(); - for (key_name, storage_key) in storage_key_map { - let abi_value = self.decode_storage_key(&key_name, &storage_key, &state)?; - result.insert(key_name, abi_value); - } - Ok(result) - } - - /// Get a specific ABI-decoded state value by key name. - pub async fn get_value(&self, key_name: &str) -> Result, AppClientError> { - let state = self.provider.get_app_state().await?; - let storage_key_map = self.provider.get_storage_keys()?; - - let storage_key = - storage_key_map - .get(key_name) - .ok_or_else(|| AppClientError::AppStateError { - message: format!("{} state key '{}' not found", self.name, key_name), - })?; - - self.decode_storage_key(key_name, storage_key, &state) - } - - fn decode_storage_key( - &self, - key_name: &str, - storage_key: &ABIStorageKey, - state: &HashMap, AppState>, - ) -> Result, AppClientError> { - let key_bytes = base64::engine::general_purpose::STANDARD - .decode(&storage_key.key) - .map_err(|e| AppClientError::AppStateError { - message: format!("Failed to decode {} key '{}': {}", self.name, key_name, e), - })?; - - let value = state.get(&key_bytes); - - match value { - None => Ok(None), - Some(app_state) => Ok(Some(decode_app_state(&storage_key.value_type, app_state)?)), - } - } - - /// Get all key-value pairs from an ABI-defined state map. - pub async fn get_map( - &self, - map_name: &str, - ) -> Result, AppClientError> { - let state = self.provider.get_app_state().await?; - let storage_map_map = self.provider.get_storage_maps()?; - let storage_map = - storage_map_map - .get(map_name) - .ok_or_else(|| AppClientError::AppStateError { - message: format!("{} state map '{}' not found", self.name, map_name), - })?; - let prefix_bytes = if let Some(prefix_b64) = &storage_map.prefix { - base64::engine::general_purpose::STANDARD - .decode(prefix_b64) - .map_err(|e| AppClientError::AppStateError { - message: format!("Failed to decode map prefix: {}", e), - })? - } else { - Vec::new() - }; - - let mut result = HashMap::new(); - for (key, app_state) in state.iter() { - if !key.starts_with(&prefix_bytes) { - continue; - } - - let tail = &key[prefix_bytes.len()..]; - let decoded_key = storage_map - .key_type - .decode(tail) - .map_err(|e| AppClientError::ABIError { source: e })?; - - let decoded_value = decode_app_state(&storage_map.value_type, app_state)?; - result.insert(decoded_key, decoded_value); - } - - Ok(result) - } - - /// Get a specific value from an ABI-defined state map by key. - pub async fn get_map_value( - &self, - map_name: &str, - key: ABIValue, - ) -> Result, AppClientError> { - let state = self.provider.get_app_state().await?; - let storage_map_map = self.provider.get_storage_maps()?; - let storage_map = - storage_map_map - .get(map_name) - .ok_or_else(|| AppClientError::AppStateError { - message: format!("{} state map '{}' not found", self.name, map_name), - })?; - - let prefix_bytes = if let Some(prefix_b64) = &storage_map.prefix { - base64::engine::general_purpose::STANDARD - .decode(prefix_b64) - .map_err(|e| AppClientError::AppStateError { - message: format!("Failed to decode map prefix: {}", e), - })? - } else { - Vec::new() - }; - let encoded_key = storage_map - .key_type - .encode(&key) - .map_err(|e| AppClientError::ABIError { source: e })?; - let full_key = [prefix_bytes, encoded_key].concat(); - - let value = state.get(&full_key); - - match value { - None => Ok(None), - Some(app_state) => Ok(Some(decode_app_state(&storage_map.value_type, app_state)?)), - } - } -} - -impl BoxStateAccessor<'_> { - /// Get all ABI-decoded box values for this application. - pub async fn get_all(&self) -> Result, AppClientError> { - let box_storage_keys = self - .client - .app_spec - .get_box_abi_storage_keys() - .map_err(|e| AppClientError::ABIError { source: e })?; - let mut results: HashMap = HashMap::new(); - - for (box_name, storage_key) in box_storage_keys { - let box_name_bytes = base64::engine::general_purpose::STANDARD - .decode(&storage_key.key) - .map_err(|e| AppClientError::AppStateError { - message: format!("Failed to decode box key '{}': {}", box_name, e), - })?; - - // TODO: what to do when it failed to fetch the box? - let box_value = self.client.get_box_value(&box_name_bytes).await?; - let abi_value = storage_key - .value_type - .decode(&box_value) - .map_err(|e| AppClientError::ABIError { source: e })?; - results.insert(box_name, abi_value); - } - - Ok(results) - } - - /// Get a specific ABI-decoded box value by name. - pub async fn get_value(&self, name: &str) -> Result { - let box_storage_keys = self - .client - .app_spec - .get_box_abi_storage_keys() - .map_err(|e| AppClientError::ABIError { source: e })?; - - let storage_key = - box_storage_keys - .get(name) - .ok_or_else(|| AppClientError::AppStateError { - message: format!("Box key '{}' not found", name), - })?; - - let box_name_bytes = base64::engine::general_purpose::STANDARD - .decode(&storage_key.key) - .map_err(|e| AppClientError::AppStateError { - message: format!("Failed to decode box key '{}': {}", name, e), - })?; - - // TODO: what to do when it failed to fetch the box? - let box_value = self.client.get_box_value(&box_name_bytes).await?; - storage_key - .value_type - .decode(&box_value) - .map_err(|e| AppClientError::ABIError { source: e }) - } - - /// Get all key-value pairs from an ABI-defined box map. - pub async fn get_map( - &self, - map_name: &str, - ) -> Result, AppClientError> { - let storage_map_map = self - .client - .app_spec - .get_box_abi_storage_maps() - .map_err(|e| AppClientError::ABIError { source: e })?; - let storage_map = - storage_map_map - .get(map_name) - .ok_or_else(|| AppClientError::AppStateError { - message: format!("Box map '{}' not found", map_name), - })?; - - let prefix_bytes = if let Some(prefix_b64) = &storage_map.prefix { - base64::engine::general_purpose::STANDARD - .decode(prefix_b64) - .map_err(|e| AppClientError::AppStateError { - message: format!("Failed to decode map prefix: {}", e), - })? - } else { - Vec::new() - }; - - let box_names = self.client.get_box_names().await?; - let box_names = box_names - .iter() - .filter(|box_name| box_name.name_raw.starts_with(&prefix_bytes)) - .collect::>(); - - let mut results: HashMap = HashMap::new(); - for box_name in box_names { - let tail = &box_name.name_raw[prefix_bytes.len()..]; - let decoded_key = storage_map - .key_type - .decode(tail) - .map_err(|e| AppClientError::ABIError { source: e })?; - - let box_value = self.client.get_box_value(&box_name.name_raw).await?; - let decoded_value = storage_map - .value_type - .decode(&box_value) - .map_err(|e| AppClientError::ABIError { source: e })?; - results.insert(decoded_key, decoded_value); - } - - Ok(results) - } - - /// Get a specific value from an ABI-defined box map by key. - pub async fn get_map_value( - &self, - map_name: &str, - key: &ABIValue, - ) -> Result, AppClientError> { - let storage_map_map = self - .client - .app_spec - .get_box_abi_storage_maps() - .map_err(|e| AppClientError::ABIError { source: e })?; - let storage_map = - storage_map_map - .get(map_name) - .ok_or_else(|| AppClientError::AppStateError { - message: format!("Box map '{}' not found", map_name), - })?; - - let prefix_bytes = if let Some(prefix_b64) = &storage_map.prefix { - base64::engine::general_purpose::STANDARD - .decode(prefix_b64) - .map_err(|e| AppClientError::AppStateError { - message: format!("Failed to decode map prefix: {}", e), - })? - } else { - Vec::new() - }; - - let encoded_key = storage_map - .key_type - .encode(key) - .map_err(|e| AppClientError::ABIError { source: e })?; - let full_key = [prefix_bytes, encoded_key].concat(); - - let box_value = match self.client.get_box_value(&full_key).await { - Ok(val) => val, - Err(AppClientError::AppStateError { .. }) => return Ok(None), - Err(e) => return Err(e), - }; - - let decoded = storage_map - .value_type - .decode(&box_value) - .map_err(|e| AppClientError::ABIError { source: e })?; - Ok(Some(decoded)) - } -} - -fn decode_app_state( - value_type: &ABIType, - app_state: &AppState, -) -> Result { - match &app_state { - AppState::Uint(uint_app_state) => Ok(ABIValue::Uint(BigUint::from(uint_app_state.value))), - AppState::Bytes(bytes_app_state) => Ok(value_type - .decode(&bytes_app_state.value_raw) - .map_err(|e| AppClientError::ABIError { source: e })?), - } -} diff --git a/crates/algokit_utils/src/applications/app_client/transaction_builder.rs b/crates/algokit_utils/src/applications/app_client/transaction_builder.rs deleted file mode 100644 index 0e99a3186..000000000 --- a/crates/algokit_utils/src/applications/app_client/transaction_builder.rs +++ /dev/null @@ -1,232 +0,0 @@ -use crate::AppClientError; -use algokit_transact::OnApplicationComplete; -use futures::TryFutureExt; - -use super::types::{AppClientBareCallParams, AppClientMethodCallParams, CompilationParams}; -use super::{AppClient, FundAppAccountParams}; - -pub struct TransactionBuilder<'app_client> { - pub(crate) client: &'app_client AppClient, -} - -pub struct BareTransactionBuilder<'app_client> { - pub(crate) client: &'app_client AppClient, -} - -impl TransactionBuilder<'_> { - /// Get the bare transaction builder. - pub fn bare(&self) -> BareTransactionBuilder<'_> { - BareTransactionBuilder { - client: self.client, - } - } - - /// Create an unsigned ABI method call transaction with the specified on-complete action. - pub async fn call( - &self, - params: AppClientMethodCallParams, - on_complete: Option, - ) -> Result { - let params = self.client.params().call(params, on_complete).await?; - let trasactions = self - .client - .algorand - .create() - .app_call_method_call(params) - .map_err(|e| AppClientError::ComposerError { source: e }) - .await?; - Ok(trasactions[0].clone()) - } - - /// Create an unsigned ABI method call transaction with OptIn on-complete action. - pub async fn opt_in( - &self, - params: AppClientMethodCallParams, - ) -> Result { - let params = self.client.params().opt_in(params).await?; - let trasactions = self - .client - .algorand - .create() - .app_call_method_call(params) - .map_err(|e| AppClientError::ComposerError { source: e }) - .await?; - Ok(trasactions[0].clone()) - } - - /// Create an unsigned ABI method call transaction with CloseOut on-complete action. - pub async fn close_out( - &self, - params: AppClientMethodCallParams, - ) -> Result { - let params = self.client.params().close_out(params).await?; - let trasactions = self - .client - .algorand - .create() - .app_call_method_call(params) - .map_err(|e| AppClientError::ComposerError { source: e }) - .await?; - Ok(trasactions[0].clone()) - } - - /// Create an unsigned ABI method call transaction with ClearState on-complete action. - pub async fn clear_state( - &self, - params: AppClientMethodCallParams, - ) -> Result { - let params = self.client.params().clear_state(params).await?; - let trasactions = self - .client - .algorand - .create() - .app_call_method_call(params) - .map_err(|e| AppClientError::ComposerError { source: e }) - .await?; - Ok(trasactions[0].clone()) - } - - /// Create an unsigned ABI method call transaction with Delete on-complete action. - pub async fn delete( - &self, - params: AppClientMethodCallParams, - ) -> Result { - let params = self.client.params().delete(params).await?; - let trasactions = self - .client - .algorand - .create() - .app_delete_method_call(params) - .map_err(|e| AppClientError::ComposerError { source: e }) - .await?; - Ok(trasactions[0].clone()) - } - - /// Create an unsigned application update transaction using an ABI method call. - pub async fn update( - &self, - params: AppClientMethodCallParams, - compilation_params: Option, - ) -> Result { - let (params, _compiled) = self - .client - .params() - .update(params, compilation_params) - .await?; - let trasactions = self - .client - .algorand() - .create() - .app_update_method_call(params) - .map_err(|e| AppClientError::ComposerError { source: e }) - .await?; - Ok(trasactions[0].clone()) - } - - /// Create an unsigned payment transaction to fund the application's account. - pub async fn fund_app_account( - &self, - params: FundAppAccountParams, - ) -> Result { - let params = self.client.params().fund_app_account(¶ms)?; - self.client - .algorand - .create() - .payment(params) - .map_err(|e| AppClientError::ComposerError { source: e }) - .await - } -} - -impl BareTransactionBuilder<'_> { - /// Create an unsigned bare application call transaction with the specified on-complete action. - pub async fn call( - &self, - params: AppClientBareCallParams, - on_complete: Option, - ) -> Result { - let params = self.client.params().bare().call(params, on_complete)?; - self.client - .algorand - .create() - .app_call(params) - .map_err(|e| AppClientError::ComposerError { source: e }) - .await - } - - /// Create an unsigned bare application call transaction with OptIn on-complete action. - pub async fn opt_in( - &self, - params: AppClientBareCallParams, - ) -> Result { - let params = self.client.params().bare().opt_in(params)?; - self.client - .algorand - .create() - .app_call(params) - .map_err(|e| AppClientError::ComposerError { source: e }) - .await - } - - /// Create an unsigned bare application call transaction with CloseOut on-complete action. - pub async fn close_out( - &self, - params: AppClientBareCallParams, - ) -> Result { - let params = self.client.params().bare().close_out(params)?; - self.client - .algorand - .create() - .app_call(params) - .map_err(|e| AppClientError::ComposerError { source: e }) - .await - } - - /// Create an unsigned bare application call transaction with Delete on-complete action. - pub async fn delete( - &self, - params: AppClientBareCallParams, - ) -> Result { - let params = self.client.params().bare().delete(params)?; - self.client - .algorand - .create() - .app_delete(params) - .map_err(|e| AppClientError::ComposerError { source: e }) - .await - } - - /// Create an unsigned bare application call transaction with ClearState on-complete action. - pub async fn clear_state( - &self, - params: AppClientBareCallParams, - ) -> Result { - let params = self.client.params().bare().clear_state(params)?; - self.client - .algorand - .create() - .app_call(params) - .map_err(|e| AppClientError::ComposerError { source: e }) - .await - } - - /// Create an unsigned application update transaction using a bare application call. - pub async fn update( - &self, - params: AppClientBareCallParams, - compilation_params: Option, - ) -> Result { - let (params, _compiled) = self - .client - .params() - .bare() - .update(params, compilation_params) - .await?; - self.client - .algorand() - .create() - .app_update(params) - .map_err(|e| AppClientError::ComposerError { source: e }) - .await - } -} diff --git a/crates/algokit_utils/src/applications/app_client/transaction_sender.rs b/crates/algokit_utils/src/applications/app_client/transaction_sender.rs deleted file mode 100644 index d84cdff4b..000000000 --- a/crates/algokit_utils/src/applications/app_client/transaction_sender.rs +++ /dev/null @@ -1,305 +0,0 @@ -use crate::applications::app_client::types::{ - AppClientUpdateMethodCallResult, AppClientUpdateResult, -}; -use crate::transactions::SendResult; -use crate::transactions::composer::SimulateParams; -use crate::{AppClientError, SendAppMethodCallResult, SendParams}; -use algokit_transact::{MAX_SIMULATE_OPCODE_BUDGET, OnApplicationComplete}; - -use super::types::{AppClientBareCallParams, AppClientMethodCallParams, CompilationParams}; -use super::{AppClient, FundAppAccountParams}; - -pub struct TransactionSender<'app_client> { - pub(crate) client: &'app_client AppClient, -} - -pub struct BareTransactionSender<'app_client> { - pub(crate) client: &'app_client AppClient, -} - -impl<'app_client> TransactionSender<'app_client> { - /// Get the bare transaction sender. - pub fn bare(&self) -> BareTransactionSender<'app_client> { - BareTransactionSender { - client: self.client, - } - } - - /// Execute an ABI method call with the specified on-complete action. - pub async fn call( - &self, - params: AppClientMethodCallParams, - on_complete: Option, - send_params: Option, - ) -> Result { - let arc56_method = self - .client - .app_spec - .get_method(¶ms.method) - .map_err(|e| AppClientError::ABIError { source: e })?; - - let mut method_params = self.client.params().call(params, on_complete).await?; - - if method_params.on_complete == OnApplicationComplete::NoOp - && arc56_method.readonly == Some(true) - { - let transaction_composer_config = self.client.transaction_composer_config.clone(); - - let mut composer = self - .client - .algorand() - .new_composer(transaction_composer_config.clone()); - - if transaction_composer_config - .clone() - .is_some_and(|c| c.cover_app_call_inner_transaction_fees) - && method_params.max_fee.is_some() - { - method_params.static_fee = method_params.max_fee; - method_params.extra_fee = None; - } - - let _ = composer - .add_app_call_method_call(method_params) - .map_err(|e| AppClientError::ComposerError { source: e }); - - let simulate_params = SimulateParams { - allow_unnamed_resources: Some( - transaction_composer_config - .map(|c| c.populate_app_call_resources.is_enabled()) - .unwrap_or(true), - ), - skip_signatures: true, - extra_opcode_budget: Some(MAX_SIMULATE_OPCODE_BUDGET), - ..Default::default() - }; - - let simulate_results = composer - .simulate(Some(simulate_params)) - .await - .map_err(|e| AppClientError::ComposerError { source: e })?; - - let last_result = simulate_results - .results - .last() - .ok_or(AppClientError::ValidationError { - message: "No transaction returned".to_string(), - })? - .clone(); - - Ok(SendAppMethodCallResult { - result: last_result, - group_results: simulate_results.results, - group: simulate_results.group, - }) - } else { - self.client - .algorand - .send() - .app_call_method_call(method_params, send_params) - .await - .map_err(|e| self.client.transform_transaction_error(e, false)) - } - } - - /// Execute an ABI method call with OptIn on-complete action. - pub async fn opt_in( - &self, - params: AppClientMethodCallParams, - send_params: Option, - ) -> Result { - let method_params = self.client.params().opt_in(params).await?; - - self.client - .algorand - .send() - .app_call_method_call(method_params, send_params) - .await - .map_err(|e| self.client.transform_transaction_error(e, false)) - } - - /// Execute an ABI method call with CloseOut on-complete action. - pub async fn close_out( - &self, - params: AppClientMethodCallParams, - send_params: Option, - ) -> Result { - let method_params = self.client.params().close_out(params).await?; - - self.client - .algorand - .send() - .app_call_method_call(method_params, send_params) - .await - .map_err(|e| self.client.transform_transaction_error(e, false)) - } - - /// Execute an ABI method call with Delete on-complete action. - pub async fn delete( - &self, - params: AppClientMethodCallParams, - send_params: Option, - ) -> Result { - let delete_params = self.client.params().delete(params).await?; - - self.client - .algorand - .send() - .app_delete_method_call(delete_params, send_params) - .await - .map_err(|e| self.client.transform_transaction_error(e, false)) - } - - /// Update the application using an ABI method call. - pub async fn update( - &self, - params: AppClientMethodCallParams, - compilation_params: Option, - send_params: Option, - ) -> Result { - let (update_params, compiled_programs) = self - .client - .params() - .update(params, compilation_params) - .await?; - - let result = self - .client - .algorand() - .send() - .app_update_method_call(update_params, send_params) - .await - .map_err(|e| self.client.transform_transaction_error(e, false))?; - - Ok(AppClientUpdateMethodCallResult { - result: result.result, - group_results: result.group_results, - group: result.group, - compiled_programs, - }) - } - - /// Send payment to fund the application's account. - pub async fn fund_app_account( - &self, - params: FundAppAccountParams, - send_params: Option, - ) -> Result { - let payment = self.client.params().fund_app_account(¶ms)?; - - self.client - .algorand - .send() - .payment(payment, send_params) - .await - .map_err(|e| self.client.transform_transaction_error(e, false)) - } -} - -impl BareTransactionSender<'_> { - /// Execute a bare application call with the specified on-complete action. - pub async fn call( - &self, - params: AppClientBareCallParams, - on_complete: Option, - send_params: Option, - ) -> Result { - let params = self.client.params().bare().call(params, on_complete)?; - self.client - .algorand - .send() - .app_call(params, send_params) - .await - .map_err(|e| self.client.transform_transaction_error(e, false)) - } - - /// Execute a bare application call with OptIn on-complete action. - pub async fn opt_in( - &self, - params: AppClientBareCallParams, - send_params: Option, - ) -> Result { - let app_call = self.client.params().bare().opt_in(params)?; - self.client - .algorand - .send() - .app_call(app_call, send_params) - .await - .map_err(|e| self.client.transform_transaction_error(e, false)) - } - - /// Execute a bare application call with CloseOut on-complete action. - pub async fn close_out( - &self, - params: AppClientBareCallParams, - send_params: Option, - ) -> Result { - let app_call = self.client.params().bare().close_out(params)?; - self.client - .algorand - .send() - .app_call(app_call, send_params) - .await - .map_err(|e| self.client.transform_transaction_error(e, false)) - } - - /// Execute a bare application call with Delete on-complete action. - pub async fn delete( - &self, - params: AppClientBareCallParams, - send_params: Option, - ) -> Result { - let delete_params = self.client.params().bare().delete(params)?; - self.client - .algorand - .send() - .app_delete(delete_params, send_params) - .await - .map_err(|e| self.client.transform_transaction_error(e, false)) - } - - /// Execute a bare application call with ClearState on-complete action. - pub async fn clear_state( - &self, - params: AppClientBareCallParams, - send_params: Option, - ) -> Result { - let app_call = self.client.params().bare().clear_state(params)?; - self.client - .algorand - .send() - .app_call(app_call, send_params) - .await - .map_err(|e| self.client.transform_transaction_error(e, true)) - } - - /// Update the application using a bare application call. - pub async fn update( - &self, - params: AppClientBareCallParams, - compilation_params: Option, - send_params: Option, - ) -> Result { - let (update_params, compiled_programs) = self - .client - .params() - .bare() - .update(params, compilation_params) - .await?; - - let result = self - .client - .algorand() - .send() - .app_update(update_params, send_params) - .await - .map_err(|e| self.client.transform_transaction_error(e, false))?; - - Ok(AppClientUpdateResult { - transaction: result.transaction, - confirmation: result.confirmation, - transaction_id: result.transaction_id, - compiled_programs, - }) - } -} diff --git a/crates/algokit_utils/src/applications/app_client/types.rs b/crates/algokit_utils/src/applications/app_client/types.rs deleted file mode 100644 index 308b3b024..000000000 --- a/crates/algokit_utils/src/applications/app_client/types.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::AlgorandClient; -use crate::clients::app_manager::{CompiledPrograms, TealTemplateValue}; -use crate::transactions::TransactionComposerConfig; -use crate::transactions::TransactionResult; -use crate::transactions::TransactionSigner; -use crate::transactions::app_call::AppMethodCallArg; -use algod_client::models::PendingTransactionResponse; -use algokit_abi::Arc56Contract; -use algokit_transact::BoxReference; -use algokit_transact::Byte32; -use algokit_transact::Transaction; -use derive_more::Debug; -use std::collections::HashMap; -use std::sync::Arc; - -/// Container for source maps captured during compilation/simulation. -#[derive(Debug, Clone, Default)] -pub struct AppSourceMaps { - pub approval_source_map: Option, - pub clear_source_map: Option, -} - -/// Parameters required to construct an AppClient instance. -#[derive(Clone)] -pub struct AppClientParams { - pub app_id: u64, - pub app_spec: Arc56Contract, - pub algorand: Arc, - pub app_name: Option, - pub default_sender: Option, - pub default_signer: Option>, - pub source_maps: Option, - pub transaction_composer_config: Option, -} - -/// Parameters for funding an application's account. -#[derive(Debug, Clone, Default)] -pub struct FundAppAccountParams { - pub amount: u64, - pub sender: Option, - #[debug(skip)] - pub signer: Option>, - pub rekey_to: Option, - pub note: Option>, - pub lease: Option<[u8; 32]>, - pub static_fee: Option, - pub extra_fee: Option, - pub max_fee: Option, - pub validity_window: Option, - pub first_valid_round: Option, - pub last_valid_round: Option, - pub close_remainder_to: Option, -} - -/// Parameters for ABI method call operations -#[derive(Debug, Clone, Default)] -pub struct AppClientMethodCallParams { - pub method: String, - pub args: Vec, - pub sender: Option, - #[debug(skip)] - pub signer: Option>, - pub rekey_to: Option, - pub note: Option>, - pub lease: Option<[u8; 32]>, - pub static_fee: Option, - pub extra_fee: Option, - pub max_fee: Option, - pub validity_window: Option, - pub first_valid_round: Option, - pub last_valid_round: Option, - pub account_references: Option>, - pub app_references: Option>, - pub asset_references: Option>, - pub box_references: Option>, -} - -/// Parameters for bare (non-ABI) app call operations -#[derive(Debug, Clone, Default)] -pub struct AppClientBareCallParams { - pub args: Option>>, - pub sender: Option, - #[debug(skip)] - pub signer: Option>, - pub rekey_to: Option, - pub note: Option>, - pub lease: Option<[u8; 32]>, - pub static_fee: Option, - pub extra_fee: Option, - pub max_fee: Option, - pub validity_window: Option, - pub first_valid_round: Option, - pub last_valid_round: Option, - pub account_references: Option>, - pub app_references: Option>, - pub asset_references: Option>, - pub box_references: Option>, -} - -/// Enriched logic error details with source map information. -#[derive(Debug, Clone, Default)] -pub struct LogicError { - pub message: String, - pub program: Option>, - pub source_map: Option, - pub transaction_id: Option, - pub pc: Option, - pub line_no: Option, - pub lines: Option>, - pub traces: Option>, - /// Original logic error string if parsed - pub logic_error_str: Option, -} - -impl std::fmt::Display for LogicError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let tx = self.transaction_id.as_deref().unwrap_or("N/A"); - let pc = self - .pc - .map(|p| p.to_string()) - .unwrap_or_else(|| "N/A".to_string()); - let mut base = format!("Txn {} had error '{}' at PC {}", tx, self.message, pc); - if let Some(line) = self.line_no { - base.push_str(&format!(" and Source Line {}", line)); - } - writeln!(f, "{}", base)?; - if let Some(trace) = self.annotated_trace() { - write!(f, "{}", trace)?; - } - Ok(()) - } -} - -impl LogicError { - /// Build a simple annotated snippet string from stored lines and line number. - pub fn annotated_trace(&self) -> Option { - let lines = self.lines.as_ref()?; - let line_no = self.line_no? as usize; - let mut out = String::new(); - for entry in lines { - out.push_str(entry); - if entry.starts_with(&format!("{:>4} |", line_no)) { - out.push_str("\t<--- Error"); - } - out.push('\n'); - } - if out.is_empty() { None } else { Some(out) } - } -} - -/// Compilation configuration for update/compile flows -#[derive(Debug, Clone, Default)] -pub struct CompilationParams { - pub deploy_time_params: Option>, - pub updatable: Option, - pub deletable: Option, -} - -#[derive(Clone, Debug)] -pub struct AppClientUpdateMethodCallResult { - /// The result of the primary (last) transaction - pub result: TransactionResult, - /// All transaction results - pub group_results: Vec, - /// The group ID (optional) - pub group: Option, - /// The compiled programs (approval and clear state) - pub compiled_programs: CompiledPrograms, -} - -#[derive(Clone, Debug)] -pub struct AppClientUpdateResult { - /// The primary transaction that has been sent - pub transaction: Transaction, - /// The response from sending and waiting for the primary transaction - pub confirmation: PendingTransactionResponse, - /// The transaction ID of the primary transaction that has been sent - pub transaction_id: String, - /// The compiled programs (approval and clear state) - pub compiled_programs: CompiledPrograms, -} diff --git a/crates/algokit_utils/src/applications/app_client/utils.rs b/crates/algokit_utils/src/applications/app_client/utils.rs deleted file mode 100644 index 9a60fd9bf..000000000 --- a/crates/algokit_utils/src/applications/app_client/utils.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::AppClientError; -use std::str::FromStr; - -/// Parse optional account reference strings into Address objects. -pub fn parse_account_refs_to_addresses( - account_refs: &Option>, -) -> Result>, AppClientError> { - match account_refs { - None => Ok(None), - Some(refs) => { - let mut result = Vec::with_capacity(refs.len()); - for s in refs { - result.push( - algokit_transact::Address::from_str(s) - .map_err(|e| AppClientError::TransactError { source: e })?, - ); - } - Ok(Some(result)) - } - } -} diff --git a/crates/algokit_utils/src/applications/app_deployer.rs b/crates/algokit_utils/src/applications/app_deployer.rs deleted file mode 100644 index 8e24e8887..000000000 --- a/crates/algokit_utils/src/applications/app_deployer.rs +++ /dev/null @@ -1,1380 +0,0 @@ -use crate::clients::app_manager::{ - AppInformation, AppManager, AppManagerError, CompiledPrograms, CompiledTeal, - DeploymentMetadata, TealTemplateParams, -}; -use crate::transactions::{TransactionResult, TransactionSender, TransactionSenderError}; -use crate::{ - AppCreateMethodCallParams, AppCreateParams, AppDeleteMethodCallParams, AppDeleteParams, - AppMethodCallArg, AppUpdateMethodCallParams, AppUpdateParams, ComposerError, SendParams, - create_transaction_params, -}; -use algokit_transact::{Address, Byte32, OnApplicationComplete}; -use base64::{Engine as _, engine::general_purpose}; -use indexer_client::{IndexerClient, apis::Error as IndexerError}; -use log::{debug, info, warn}; -use serde::{Deserialize, Serialize}; -use snafu::Snafu; -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; - -const APP_DEPLOY_NOTE_PREFIX: &str = "ALGOKIT_DEPLOYER"; - -/// Enum for app program variants - either TEAL source code or compiled bytecode -#[derive(Debug, Clone, PartialEq)] -pub enum AppProgram { - /// TEAL source code, which will be compiled with template parameters at deploy time - Teal(String), - /// Pre-compiled bytecode - CompiledBytes(Vec), -} - -impl Default for AppProgram { - fn default() -> Self { - Self::CompiledBytes(Vec::new()) - } -} - -/// What action to perform if a schema break (storage schema or extra pages change) is detected -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum OnSchemaBreak { - /// Fail the deployment (throw an error, default) - Fail, - /// Delete the old app and create a new one - Replace, - /// Deploy a new app and leave the old one as is - Append, -} - -impl Default for OnSchemaBreak { - fn default() -> Self { - Self::Fail - } -} - -/// What action to perform if a TEAL code update is detected -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum OnUpdate { - /// Fail the deployment (throw an error, default) - Fail, - /// Update the app with the new TEAL code - Update, - /// Delete the old app and create a new one - Replace, - /// Deploy a new app and leave the old one as is - Append, -} - -impl Default for OnUpdate { - fn default() -> Self { - Self::Fail - } -} - -/// The deployment metadata for an application -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct AppDeployMetadata { - /// The name of the application - pub name: String, - /// The version of the application - pub version: String, - /// Whether the application is updatable - pub updatable: Option, - /// Whether the application is deletable - pub deletable: Option, -} - -/// The metadata that can be collected about a deployed app -#[derive(Debug, Clone)] -pub struct AppMetadata { - /// The id of the app - pub app_id: u64, - /// The Algorand address of the account associated with the app - pub app_address: Address, - /// The round the app was created - pub created_round: u64, - /// The last round that the app was updated - pub updated_round: u64, - /// The metadata when the app was created - pub created_metadata: AppDeployMetadata, - /// Whether or not the app is deleted - pub deleted: bool, - /// The deployment metadata - pub name: String, - pub version: String, - pub updatable: Option, - pub deletable: Option, -} - -/// A lookup of name -> Algorand app for a creator -#[derive(Debug, Clone)] -pub struct AppLookup { - /// The address of the creator associated with this lookup - pub creator: Address, - /// A hash map of app name to app metadata - pub apps: HashMap, -} - -create_transaction_params! { - /// Parameters for the create transaction with program variants - #[derive(Default, Clone)] - pub struct DeployAppCreateParams { - pub on_complete: OnApplicationComplete, - pub approval_program: AppProgram, - pub clear_state_program: AppProgram, - pub args: Option>>, - pub account_references: Option>, - pub app_references: Option>, - pub asset_references: Option>, - pub box_references: Option>, - pub global_state_schema: Option, - pub local_state_schema: Option, - pub extra_program_pages: Option, - } -} - -create_transaction_params! { - /// Parameters for the create method call with program variants - #[derive(Default, Clone)] - pub struct DeployAppCreateMethodCallParams { - pub on_complete: OnApplicationComplete, - pub approval_program: AppProgram, - pub clear_state_program: AppProgram, - pub method: algokit_abi::ABIMethod, - pub args: Vec, - pub account_references: Option>, - pub app_references: Option>, - pub asset_references: Option>, - pub box_references: Option>, - pub global_state_schema: Option, - pub local_state_schema: Option, - pub extra_program_pages: Option, - } -} - -create_transaction_params! { - #[derive(Default, Clone)] - pub struct DeployAppUpdateParams { - pub args: Option>>, - pub account_references: Option>, - pub app_references: Option>, - pub asset_references: Option>, - pub box_references: Option>, - } -} - -create_transaction_params! { - /// Parameters for the update method call - #[derive(Default, Clone)] - pub struct DeployAppUpdateMethodCallParams { - pub method: algokit_abi::ABIMethod, - pub args: Vec, - pub account_references: Option>, - pub app_references: Option>, - pub asset_references: Option>, - pub box_references: Option>, - } -} - -create_transaction_params! { - /// Parameters for the delete transaction - #[derive(Clone, Default)] - pub struct DeployAppDeleteParams { - pub args: Option>>, - pub account_references: Option>, - pub app_references: Option>, - pub asset_references: Option>, - pub box_references: Option>, - } -} - -create_transaction_params! { - /// Parameters for the delete method call - #[derive(Default, Clone)] - pub struct DeployAppDeleteMethodCallParams { - pub method: algokit_abi::ABIMethod, - pub args: Vec, - pub account_references: Option>, - pub app_references: Option>, - pub asset_references: Option>, - pub box_references: Option>, - } -} - -#[derive(Debug, Clone)] -#[allow(clippy::large_enum_variant)] -pub enum CreateParams { - AppCreateCall(DeployAppCreateParams), - AppCreateMethodCall(DeployAppCreateMethodCallParams), -} - -#[derive(Debug, Clone)] -#[allow(clippy::large_enum_variant)] -pub enum UpdateParams { - AppUpdateCall(DeployAppUpdateParams), - AppUpdateMethodCall(DeployAppUpdateMethodCallParams), -} - -#[derive(Debug, Clone)] -#[allow(clippy::large_enum_variant)] -pub enum DeleteParams { - AppDeleteCall(DeployAppDeleteParams), - AppDeleteMethodCall(DeployAppDeleteMethodCallParams), -} - -/// The parameters to idempotently deploy an app -#[derive(Debug, Clone)] -pub struct AppDeployParams { - /// The deployment metadata - pub metadata: AppDeployMetadata, - /// Any deploy-time parameters to replace in the TEAL code before compiling - pub deploy_time_params: Option, - /// What action to perform if a schema break is detected - pub on_schema_break: Option, - /// What action to perform if a TEAL code update is detected - pub on_update: Option, - /// Create transaction parameters to use if a create needs to be issued as part of deployment - pub create_params: CreateParams, - /// Update transaction parameters to use if an update needs to be issued as part of deployment - pub update_params: UpdateParams, - /// Delete transaction parameters to use if a delete needs to be issued as part of deployment - pub delete_params: DeleteParams, - /// Optional cached value of the existing apps for the given creator - pub existing_deployments: Option, - /// Whether or not to ignore the app metadata cache and force a lookup - pub ignore_cache: Option, - /// Send transaction parameters - pub send_params: SendParams, -} - -/// The result of an app deployment operation -#[derive(Debug)] -pub enum AppDeployResult { - /// Application was created - Create { - app: AppMetadata, - /// The result of create transaction - create_result: TransactionResult, - /// All transaction results - group_results: Vec, - /// The group ID for the transaction group (if any) - group: Option, - /// The compiled approval and clear programs - compiled_programs: CompiledPrograms, - }, - /// Application was updated - Update { - app: AppMetadata, - /// The result of the update transaction - update_result: TransactionResult, - /// All transaction results - group_results: Vec, - /// The group ID for the transaction group (if any) - group: Option, - /// The compiled approval and clear programs - compiled_programs: CompiledPrograms, - }, - /// Application was replaced (deleted and recreated) - Replace { - app: AppMetadata, - /// The result of the delete transaction - delete_result: TransactionResult, - /// The result of the create transaction - create_result: TransactionResult, - /// All transaction results - group_results: Vec, - /// The group ID for the transaction group (if any) - group: Option, - /// The compiled approval and clear programs - compiled_programs: CompiledPrograms, - }, - /// No operation was performed - Nothing { app: AppMetadata }, -} - -/// Errors that can occur during app deployment -#[derive(Debug, Snafu)] -pub enum AppDeployError { - #[snafu(display("Composer error: {source}"))] - ComposerError { source: ComposerError }, - #[snafu(display("Indexer client error: {source}"))] - IndexerError { source: IndexerError }, - #[snafu(display("App manager error: {source}"))] - AppManagerError { source: AppManagerError }, - #[snafu(display("Transaction sender error: {source}"))] - TransactionSenderError { source: TransactionSenderError }, - #[snafu(display("Deployment failed: {message}"))] - DeploymentFailed { message: String }, - #[snafu(display("Deployment lookup failed: {message}"))] - DeploymentLookupFailed { message: String }, -} - -/// Allows management of deployment and deployment metadata of applications. -#[derive(Clone)] -pub struct AppDeployer { - indexer_client: Option>, - app_manager: AppManager, - transaction_sender: TransactionSender, - app_lookups: Arc>>, -} - -impl AppDeployer { - /// Create a new AppDeployer - /// - /// # Arguments - /// * `app_manager` - An `AppManager` instance - /// * `transaction_sender` - A `TransactionSender` instance - /// * `indexer_client` - An optional `IndexerClient` for app metadata lookup - pub fn new( - app_manager: AppManager, - transaction_sender: TransactionSender, - indexer_client: Option>, - ) -> Self { - Self { - indexer_client, - app_manager, - transaction_sender, - app_lookups: Arc::new(Mutex::new(HashMap::new())), - } - } - - pub async fn deploy( - &mut self, - deployment: AppDeployParams, - ) -> Result { - let AppDeployParams { - metadata, - deploy_time_params, - on_schema_break, - on_update, - mut create_params, - mut update_params, - delete_params, - existing_deployments, - ignore_cache, - send_params, - } = deployment; - - // Build deployment note using ARC2 format - let arc2_note = Self::build_deployment_note(&metadata)?; - match &mut create_params { - CreateParams::AppCreateCall(params) => { - params.note = Some(arc2_note.clone()); - } - CreateParams::AppCreateMethodCall(params) => { - params.note = Some(arc2_note.clone()); - } - } - match &mut update_params { - UpdateParams::AppUpdateCall(params) => { - params.note = Some(arc2_note); - } - UpdateParams::AppUpdateMethodCall(params) => { - params.note = Some(arc2_note); - } - } - - let sender = match &create_params { - CreateParams::AppCreateCall(params) => ¶ms.sender, - CreateParams::AppCreateMethodCall(params) => ¶ms.sender, - }; - - if let Some(ref existing_deployments) = existing_deployments { - if existing_deployments.creator != *sender { - return Err(AppDeployError::DeploymentFailed { - message: format!( - "Invalid existing deployments: received invalid existingDeployments value for creator {} when attempting to deploy for creator {}", - existing_deployments.creator, sender - ), - }); - } - } - - if existing_deployments.is_none() && self.indexer_client.is_none() { - return Err(AppDeployError::DeploymentFailed { - message: String::from( - "Either indexer client or existing deployments must be provided", - ), - }); - } - // Compile TEAL code if needed and handle template replacement - let compiled_programs = self - .compile_app_programs( - match &create_params { - CreateParams::AppCreateCall(params) => ¶ms.approval_program, - CreateParams::AppCreateMethodCall(params) => ¶ms.approval_program, - }, - match &create_params { - CreateParams::AppCreateCall(params) => ¶ms.clear_state_program, - CreateParams::AppCreateMethodCall(params) => ¶ms.clear_state_program, - }, - &metadata, - deploy_time_params.as_ref(), - ) - .await?; - - info!( - "Idempotently deploying app \"{}\" from creator {} using {} bytes of approval program and {} bytes of clear state program", - metadata.name, - sender, - compiled_programs.approval.compiled_base64_to_bytes.len(), - compiled_programs.clear.compiled_base64_to_bytes.len() - ); - - // Get existing apps - let app_lookup = match existing_deployments { - Some(apps) => apps, - None => self.get_creator_apps_by_name(sender, ignore_cache).await?, - }; - let existing_app_metadata = app_lookup.apps.get(&metadata.name); - - // If app doesn't exist or is deleted, create it - if existing_app_metadata.is_none() || existing_app_metadata.is_some_and(|app| app.deleted) { - info!( - "App {} not found in apps created by {}; deploying app with version {}.", - metadata.name, sender, metadata.version - ); - return self - .create_app(&metadata, &create_params, compiled_programs, &send_params) - .await; - } - - let existing_app_metadata = existing_app_metadata.unwrap(); - info!( - "Existing app {} found by creator {}, with app id {} and version {}.", - metadata.name, sender, existing_app_metadata.app_id, existing_app_metadata.version - ); - - let existing_app = self - .app_manager - .get_by_id(existing_app_metadata.app_id) - .await - .map_err(|e| AppDeployError::AppManagerError { source: e })?; - - // Check for changes - let is_update = self.is_program_different( - &compiled_programs.approval.compiled_base64_to_bytes, - &compiled_programs.clear.compiled_base64_to_bytes, - &existing_app, - )?; - let is_schema_break = self.is_schema_break( - &create_params, - &existing_app, - &compiled_programs.approval.compiled_base64_to_bytes, - &compiled_programs.clear.compiled_base64_to_bytes, - )?; - - if is_schema_break { - self.handle_schema_break( - on_schema_break.unwrap_or_default(), - existing_app_metadata, - &metadata, - &create_params, - compiled_programs, - &delete_params, - &send_params, - ) - .await - } else if is_update { - self.handle_update( - on_update.unwrap_or_default(), - existing_app_metadata, - &metadata, - &create_params, - &update_params, - compiled_programs, - &delete_params, - &send_params, - ) - .await - } else { - debug!("No detected changes in app, nothing to do."); - Ok(AppDeployResult::Nothing { - app: existing_app_metadata.clone(), - }) - } - } - - /// Get apps created by a specific creator address - pub async fn get_creator_apps_by_name( - &mut self, - creator_address: &Address, - ignore_cache: Option, - ) -> Result { - let creator_address_str = creator_address.to_string(); - let ignore_cache = ignore_cache.unwrap_or(false); - - if !ignore_cache { - { - let app_lookups = self.app_lookups.lock().unwrap(); - if let Some(cached_lookup) = app_lookups.get(&creator_address_str) { - return Ok(cached_lookup.clone()); - } - } - } - - let indexer = - self.indexer_client - .as_ref() - .ok_or(AppDeployError::DeploymentLookupFailed { - message: String::from( - "No indexer client or existing deployments cache provided", - ), - })?; - - // Query indexer for apps created by this address; localnet-only retry to allow catch-up - let created_apps_response = indexer - .lookup_account_created_applications(&creator_address_str, None, Some(true), None, None) - .await - .map_err(|e| AppDeployError::IndexerError { source: e })?; - - let mut app_lookup = HashMap::new(); - - // Sort applications by created_at_round to match TypeScript behavior - let mut sorted_apps = created_apps_response.applications; - sorted_apps.sort_by(|a, b| { - a.created_at_round - .unwrap_or(0) - .cmp(&b.created_at_round.unwrap_or(0)) - }); - - for app in &sorted_apps { - if let Some(created_at_round) = app.created_at_round { - let app_id = app.id; - // Search for ALL app transactions for this app to find both creation and latest update - let transactions_response = indexer - .search_for_transactions( - None, - None, - Some(&general_purpose::STANDARD.encode(APP_DEPLOY_NOTE_PREFIX)), - Some(indexer_client::apis::parameter_enums::TxType::Appl), - None, - None, - None, - None, - Some(created_at_round), - None, - None, - None, - None, - None, - None, - Some(&creator_address_str), - Some(indexer_client::apis::parameter_enums::AddressRole::Sender), - None, - None, - Some(app_id), - ) - .await - .map_err(|e| AppDeployError::IndexerError { source: e })?; - - let mut app_creation_transaction = None; - let mut latest_app_update_transaction = None; - - // Filter transactions to find creation and latest update - let mut app_transactions: Vec<_> = transactions_response.transactions; - // Sort by confirmed round (desc) and intra-round offset (desc) to get latest first - // In theory this is the order that indexer returns them, but this ensures it - app_transactions.sort_by(|a, b| { - match b - .confirmed_round - .unwrap_or(0) - .cmp(&a.confirmed_round.unwrap_or(0)) - { - std::cmp::Ordering::Equal => b - .intra_round_offset - .unwrap_or(0) - .cmp(&a.intra_round_offset.unwrap_or(0)), - other => other, - } - }); - - // Find creation transaction and latest update transaction - for transaction in &app_transactions { - if transaction.sender != creator_address_str { - continue; // Skip transactions not from the creator - } - - if let Some(app_transaction) = &transaction.application_transaction { - if app_transaction.application_id == 0 { - // App creation transaction - app_creation_transaction = Some(transaction); - } else if latest_app_update_transaction.is_none() { - // Latest app update transaction (first non-creation we encounter due to sorting) - latest_app_update_transaction = Some(transaction); - } - } - } - - if let Some(creation_txn) = app_creation_transaction { - if let Some(note) = &creation_txn.note { - let creation_note = Self::parse_deploy_note(note); - let update_note = latest_app_update_transaction - .and_then(|t| t.note.as_ref()) - .and_then(|note| Self::parse_deploy_note(note.as_slice())); - - if let Some(creation_metadata) = creation_note { - // Use update metadata if available, otherwise fall back to creation metadata - let current_metadata = - update_note.as_ref().unwrap_or(&creation_metadata); - - let app_metadata = AppMetadata { - app_id, - app_address: Address::from_app_id(&app_id), - created_round: created_at_round, - updated_round: latest_app_update_transaction - .and_then(|t| t.confirmed_round) - .unwrap_or( - creation_txn.confirmed_round.unwrap_or(created_at_round), - ), - created_metadata: creation_metadata.clone(), - deleted: app.deleted.unwrap_or(false), - name: current_metadata.name.clone(), - version: current_metadata.version.clone(), - updatable: current_metadata.updatable, - deletable: current_metadata.deletable, - }; - app_lookup.insert(creation_metadata.name, app_metadata); - } - } - } - } - } - - let lookup = AppLookup { - creator: creator_address.clone(), - apps: app_lookup, - }; - - { - let mut app_lookups = self.app_lookups.lock().unwrap(); - app_lookups.insert(creator_address_str, lookup.clone()); - } - Ok(lookup) - } - - fn build_deployment_note(metadata: &AppDeployMetadata) -> Result, AppDeployError> { - let metadata_json = - serde_json::to_string(&metadata).map_err(|e| AppDeployError::DeploymentFailed { - message: format!("Failed to serialize metadata: {}", e), - })?; - Ok(format!("{}:j{}", APP_DEPLOY_NOTE_PREFIX, metadata_json).into_bytes()) - } - - /// Compile app programs, applying template replacement only for TEAL variant - async fn compile_app_programs( - &self, - approval_program: &AppProgram, - clear_state_program: &AppProgram, - deployment_metadata: &AppDeployMetadata, - deploy_time_params: Option<&TealTemplateParams>, - ) -> Result { - let approval = match approval_program { - AppProgram::Teal(code) => { - let metadata = DeploymentMetadata { - updatable: deployment_metadata.updatable, - deletable: deployment_metadata.deletable, - }; - let metadata_opt = if metadata.updatable.is_some() || metadata.deletable.is_some() { - Some(&metadata) - } else { - None - }; - self.app_manager - .compile_teal_template(code, deploy_time_params, metadata_opt) - .await - .map_err(|e| AppDeployError::AppManagerError { source: e })? - } - AppProgram::CompiledBytes(bytes) => CompiledTeal { - teal: String::new(), // Not available for pre-compiled bytes - compiled: base64::engine::general_purpose::STANDARD.encode(bytes), - compiled_hash: String::new(), // Not available for pre-compiled bytes - compiled_base64_to_bytes: bytes.clone(), - source_map: None, - }, - }; - - let clear = match clear_state_program { - AppProgram::Teal(code) => self - .app_manager - .compile_teal_template(code, deploy_time_params, None) - .await - .map_err(|e| AppDeployError::AppManagerError { source: e })?, - AppProgram::CompiledBytes(bytes) => CompiledTeal { - teal: String::new(), // Not available for pre-compiled bytes - compiled: base64::engine::general_purpose::STANDARD.encode(bytes), - compiled_hash: String::new(), // Not available for pre-compiled bytes - compiled_base64_to_bytes: bytes.clone(), - source_map: None, - }, - }; - - Ok(CompiledPrograms { approval, clear }) - } - - fn parse_deploy_note(note: &[u8]) -> Option { - if let Ok(utf8_note) = std::str::from_utf8(note) { - if utf8_note.starts_with(&format!("{}:j", APP_DEPLOY_NOTE_PREFIX)) { - let json_part = &utf8_note[APP_DEPLOY_NOTE_PREFIX.len() + 2..]; - return serde_json::from_str::(json_part).ok(); - } - } - None - } - - fn is_program_different( - &self, - approval_program: &[u8], - clear_state_program: &[u8], - existing_app: &AppInformation, - ) -> Result { - let existing_approval_program = &existing_app.approval_program; - let existing_clear_state_program = &existing_app.clear_state_program; - - Ok(approval_program != existing_approval_program - || clear_state_program != existing_clear_state_program) - } - - fn is_schema_break( - &self, - create_params: &CreateParams, - existing_app: &AppInformation, - approval_program: &[u8], - clear_state_program: &[u8], - ) -> Result { - let (new_global_schema, new_local_schema) = match create_params { - CreateParams::AppCreateCall(params) => ( - params.global_state_schema.as_ref(), - params.local_state_schema.as_ref(), - ), - CreateParams::AppCreateMethodCall(params) => ( - params.global_state_schema.as_ref(), - params.local_state_schema.as_ref(), - ), - }; - - let new_extra_pages = - Self::calculate_extra_program_pages(approval_program, clear_state_program); - let global_ints_break = - new_global_schema.is_some_and(|schema| schema.num_uints > existing_app.global_ints); - let global_bytes_break = new_global_schema - .is_some_and(|schema| schema.num_byte_slices > existing_app.global_byte_slices); - let local_ints_break = - new_local_schema.is_some_and(|schema| schema.num_uints > existing_app.local_ints); - let local_bytes_break = new_local_schema - .is_some_and(|schema| schema.num_byte_slices > existing_app.local_byte_slices); - let extra_pages_break = new_extra_pages > existing_app.extra_program_pages.unwrap_or(0); - - Ok(global_ints_break - || global_bytes_break - || local_ints_break - || local_bytes_break - || extra_pages_break) - } - - #[allow(clippy::too_many_arguments)] - async fn handle_schema_break( - &mut self, - on_schema_break: OnSchemaBreak, - existing_app_metadata: &AppMetadata, - metadata: &AppDeployMetadata, - create_params: &CreateParams, - compiled_programs: CompiledPrograms, - delete_params: &DeleteParams, - send_params: &SendParams, - ) -> Result { - warn!( - "Detected a breaking app schema change in app {}", - existing_app_metadata.app_id - ); - - match on_schema_break { - OnSchemaBreak::Fail => Err(AppDeployError::DeploymentFailed { - message: String::from( - "Executing the fail on schema break strategy, stopping deployment. If you want to try deleting and recreating the app then re-run using the replace on schema break strategy", - ), - }), - OnSchemaBreak::Append => { - info!( - "Executing the append on schema break strategy, will attempt to create a new app" - ); - self.create_app(metadata, create_params, compiled_programs, send_params) - .await - } - OnSchemaBreak::Replace => { - if existing_app_metadata.deletable.unwrap_or(false) { - info!( - "Executing the replace on schema break strategy on deletable app, will attempt to create new app and delete old app" - ); - } else { - info!( - "Executing the replace on schema break strategy on non deletable app, will attempt to delete app, delete will most likely fail" - ); - } - self.replace_app( - existing_app_metadata, - metadata, - create_params, - delete_params, - compiled_programs, - send_params, - ) - .await - } - } - } - - #[allow(clippy::too_many_arguments)] - async fn handle_update( - &mut self, - on_update: OnUpdate, - existing_app_metadata: &AppMetadata, - metadata: &AppDeployMetadata, - create_params: &CreateParams, - update_params: &UpdateParams, - compiled_programs: CompiledPrograms, - delete_params: &DeleteParams, - send_params: &SendParams, - ) -> Result { - info!( - "Detected an update in app {} for creator", - existing_app_metadata.app_id - ); - - match on_update { - OnUpdate::Fail => Err(AppDeployError::DeploymentFailed { - message: String::from( - "Executing the fail on update strategy, stopping deployment. Try a different on update strategy to not fail", - ), - }), - OnUpdate::Append => { - info!("Executing the append on update strategy, will attempt to create a new app"); - self.create_app(metadata, create_params, compiled_programs, send_params) - .await - } - OnUpdate::Update => { - if existing_app_metadata.updatable.unwrap_or(false) { - info!("Executing the update on update strategy on updatable app"); - } else { - warn!( - "Executing the update on update strategy on non updatable app, will attempt to update app, update will most likely fail" - ); - } - self.update_app( - existing_app_metadata, - metadata, - update_params, - compiled_programs, - send_params, - ) - .await - } - OnUpdate::Replace => { - if existing_app_metadata.deletable.unwrap_or(false) { - warn!( - "Executing the replace on update strategy on deletable app, creating new app and deleting old app..." - ); - } else { - warn!( - "Executing the replace on update strategy on non deletable app, will attempt to create new app and delete old app, delete will most likely fail" - ); - } - self.replace_app( - existing_app_metadata, - metadata, - create_params, - delete_params, - compiled_programs, - send_params, - ) - .await - } - } - } - - async fn create_app( - &mut self, - metadata: &AppDeployMetadata, - create_params: &CreateParams, - compiled_programs: CompiledPrograms, - send_params: &SendParams, - ) -> Result { - let mut composer = self.transaction_sender.new_composer(None); - - match create_params { - CreateParams::AppCreateCall(params) => { - let computed_extra_pages = Self::calculate_extra_program_pages( - &compiled_programs.approval.compiled_base64_to_bytes, - &compiled_programs.clear.compiled_base64_to_bytes, - ); - let app_create_params = AppCreateParams { - sender: params.sender.clone(), - signer: params.signer.clone(), - rekey_to: params.rekey_to.clone(), - note: params.note.clone(), - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - on_complete: params.on_complete, - approval_program: compiled_programs.approval.compiled_base64_to_bytes.clone(), - clear_state_program: compiled_programs.clear.compiled_base64_to_bytes.clone(), - global_state_schema: params.global_state_schema.clone(), - local_state_schema: params.local_state_schema.clone(), - extra_program_pages: params.extra_program_pages.or(Some(computed_extra_pages)), - args: params.args.clone(), - account_references: params.account_references.clone(), - app_references: params.app_references.clone(), - asset_references: params.asset_references.clone(), - box_references: params.box_references.clone(), - }; - composer - .add_app_create(app_create_params) - .map_err(|e| AppDeployError::ComposerError { source: e })?; - } - CreateParams::AppCreateMethodCall(params) => { - let computed_extra_pages = Self::calculate_extra_program_pages( - &compiled_programs.approval.compiled_base64_to_bytes, - &compiled_programs.clear.compiled_base64_to_bytes, - ); - let app_create_method_params = AppCreateMethodCallParams { - sender: params.sender.clone(), - signer: params.signer.clone(), - rekey_to: params.rekey_to.clone(), - note: params.note.clone(), - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - on_complete: params.on_complete, - approval_program: compiled_programs.approval.compiled_base64_to_bytes.clone(), - clear_state_program: compiled_programs.clear.compiled_base64_to_bytes.clone(), - global_state_schema: params.global_state_schema.clone(), - local_state_schema: params.local_state_schema.clone(), - extra_program_pages: params.extra_program_pages.or(Some(computed_extra_pages)), - method: params.method.clone(), - args: params.args.clone(), - account_references: params.account_references.clone(), - app_references: params.app_references.clone(), - asset_references: params.asset_references.clone(), - box_references: params.box_references.clone(), - }; - composer - .add_app_create_method_call(app_create_method_params) - .map_err(|e| AppDeployError::ComposerError { source: e })?; - } - }; - - let composer_result = composer - .send(Some(send_params.clone())) - .await - .map_err(|e| AppDeployError::ComposerError { source: e })?; - - let create_transaction_index = composer_result.results.len() - 1; - - // Extract results from the create transaction - let create_result = composer_result.results[create_transaction_index].clone(); - - let confirmation = create_result.confirmation.clone(); - let app_id = confirmation - .app_id - .ok_or_else(|| AppDeployError::DeploymentFailed { - message: "App creation confirmation missing application-index".to_string(), - })?; - - let app_address = Address::from_app_id(&app_id); - let confirmed_round = - confirmation - .confirmed_round - .ok_or_else(|| AppDeployError::DeploymentFailed { - message: "App creation confirmation missing confirmed-round".to_string(), - })?; - - let app_metadata = AppMetadata { - app_id, - app_address, - created_round: confirmed_round, - updated_round: confirmed_round, - created_metadata: metadata.clone(), - deleted: false, - name: metadata.name.clone(), - version: metadata.version.clone(), - updatable: metadata.updatable, - deletable: metadata.deletable, - }; - - let sender = match create_params { - CreateParams::AppCreateCall(params) => ¶ms.sender, - CreateParams::AppCreateMethodCall(params) => ¶ms.sender, - }; - - self.update_app_lookup(sender, &app_metadata); - - Ok(AppDeployResult::Create { - app: app_metadata, - create_result, - group_results: composer_result.results, - group: composer_result.group, - compiled_programs, - }) - } - - async fn update_app( - &mut self, - existing_app_metadata: &AppMetadata, - metadata: &AppDeployMetadata, - update_params: &UpdateParams, - compiled_programs: CompiledPrograms, - send_params: &SendParams, - ) -> Result { - info!( - "Updating existing {} app to version {}.", - metadata.name, metadata.version - ); - let mut composer = self.transaction_sender.new_composer(None); - - match update_params { - UpdateParams::AppUpdateCall(params) => { - let app_update_params = AppUpdateParams { - sender: params.sender.clone(), - signer: params.signer.clone(), - rekey_to: params.rekey_to.clone(), - note: params.note.clone(), - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - app_id: existing_app_metadata.app_id, - approval_program: compiled_programs.approval.compiled_base64_to_bytes.to_vec(), - clear_state_program: compiled_programs.clear.compiled_base64_to_bytes.to_vec(), - args: params.args.clone(), - account_references: params.account_references.clone(), - app_references: params.app_references.clone(), - asset_references: params.asset_references.clone(), - box_references: params.box_references.clone(), - }; - composer - .add_app_update(app_update_params) - .map_err(|e| AppDeployError::ComposerError { source: e })?; - } - UpdateParams::AppUpdateMethodCall(params) => { - let app_update_method_params = AppUpdateMethodCallParams { - sender: params.sender.clone(), - signer: params.signer.clone(), - rekey_to: params.rekey_to.clone(), - note: params.note.clone(), - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - app_id: existing_app_metadata.app_id, - approval_program: compiled_programs.approval.compiled_base64_to_bytes.to_vec(), - clear_state_program: compiled_programs.clear.compiled_base64_to_bytes.to_vec(), - method: params.method.clone(), - args: params.args.clone(), - account_references: params.account_references.clone(), - app_references: params.app_references.clone(), - asset_references: params.asset_references.clone(), - box_references: params.box_references.clone(), - }; - composer - .add_app_update_method_call(app_update_method_params) - .map_err(|e| AppDeployError::ComposerError { source: e })?; - } - }; - - let composer_result = composer - .send(Some(send_params.clone())) - .await - .map_err(|e| AppDeployError::ComposerError { source: e })?; - - let update_transaction_index = composer_result.results.len() - 1; - - // Extract results from the update transaction - let update_result = composer_result.results[update_transaction_index].clone(); - - let confirmed_round = update_result.confirmation.confirmed_round.ok_or_else(|| { - AppDeployError::DeploymentFailed { - message: "App update confirmation missing confirmed-round".to_string(), - } - })?; - - let app_metadata = AppMetadata { - app_id: existing_app_metadata.app_id, - app_address: existing_app_metadata.app_address.clone(), - created_round: existing_app_metadata.created_round, - updated_round: confirmed_round, - created_metadata: existing_app_metadata.created_metadata.clone(), - deleted: false, - name: metadata.name.clone(), - version: metadata.version.clone(), - updatable: metadata.updatable, - deletable: metadata.deletable, - }; - - let sender = match update_params { - UpdateParams::AppUpdateCall(params) => ¶ms.sender, - UpdateParams::AppUpdateMethodCall(params) => ¶ms.sender, - }; - - self.update_app_lookup(sender, &app_metadata); - - Ok(AppDeployResult::Update { - app: app_metadata, - update_result, - group_results: composer_result.results, - group: composer_result.group, - compiled_programs, - }) - } - - /// Updates the app lookup cache with the given app metadata - fn update_app_lookup(&mut self, sender: &Address, app_metadata: &AppMetadata) { - let sender_str = sender.to_string(); - - { - let mut app_lookups = self.app_lookups.lock().unwrap(); - match app_lookups.get_mut(&sender_str) { - Some(lookup) => { - lookup - .apps - .insert(app_metadata.name.clone(), app_metadata.clone()); - } - None => { - app_lookups.insert( - sender_str, - AppLookup { - creator: sender.clone(), - apps: HashMap::from([( - app_metadata.name.clone(), - app_metadata.clone(), - )]), - }, - ); - } - } - } - } - - #[allow(clippy::too_many_arguments)] - async fn replace_app( - &mut self, - existing_app_metadata: &AppMetadata, - metadata: &AppDeployMetadata, - create_params: &CreateParams, - delete_params: &DeleteParams, - compiled_programs: CompiledPrograms, - send_params: &SendParams, - ) -> Result { - info!( - "Deploying a new {} app; deploying app with version {}.", - metadata.name, metadata.version - ); - warn!( - "Deleting existing {} app with id {} from account.", - metadata.name, existing_app_metadata.app_id - ); - - let mut composer = self.transaction_sender.new_composer(None); - - // Add create transaction and track its index - match create_params { - CreateParams::AppCreateCall(params) => { - let computed_extra_pages = Self::calculate_extra_program_pages( - &compiled_programs.approval.compiled_base64_to_bytes, - &compiled_programs.clear.compiled_base64_to_bytes, - ); - let app_create_params = AppCreateParams { - sender: params.sender.clone(), - signer: params.signer.clone(), - rekey_to: params.rekey_to.clone(), - note: params.note.clone(), - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - on_complete: params.on_complete, - approval_program: compiled_programs.approval.compiled_base64_to_bytes.to_vec(), - clear_state_program: compiled_programs.clear.compiled_base64_to_bytes.to_vec(), - global_state_schema: params.global_state_schema.clone(), - local_state_schema: params.local_state_schema.clone(), - extra_program_pages: params.extra_program_pages.or(Some(computed_extra_pages)), - args: params.args.clone(), - account_references: params.account_references.clone(), - app_references: params.app_references.clone(), - asset_references: params.asset_references.clone(), - box_references: params.box_references.clone(), - }; - composer - .add_app_create(app_create_params) - .map_err(|e| AppDeployError::ComposerError { source: e })?; - } - CreateParams::AppCreateMethodCall(params) => { - let computed_extra_pages = Self::calculate_extra_program_pages( - &compiled_programs.approval.compiled_base64_to_bytes, - &compiled_programs.clear.compiled_base64_to_bytes, - ); - let app_create_method_params = AppCreateMethodCallParams { - sender: params.sender.clone(), - signer: params.signer.clone(), - rekey_to: params.rekey_to.clone(), - note: params.note.clone(), - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - on_complete: params.on_complete, - approval_program: compiled_programs.approval.compiled_base64_to_bytes.to_vec(), - clear_state_program: compiled_programs.clear.compiled_base64_to_bytes.to_vec(), - global_state_schema: params.global_state_schema.clone(), - local_state_schema: params.local_state_schema.clone(), - extra_program_pages: params.extra_program_pages.or(Some(computed_extra_pages)), - method: params.method.clone(), - args: params.args.clone(), - account_references: params.account_references.clone(), - app_references: params.app_references.clone(), - asset_references: params.asset_references.clone(), - box_references: params.box_references.clone(), - }; - composer - .add_app_create_method_call(app_create_method_params) - .map_err(|e| AppDeployError::ComposerError { source: e })?; - } - }; - - let create_transaction_index = composer.count() - 1; - - // Add delete transaction - match delete_params { - DeleteParams::AppDeleteCall(params) => { - composer - .add_app_delete(AppDeleteParams { - sender: params.sender.clone(), - signer: params.signer.clone(), - rekey_to: params.rekey_to.clone(), - note: params.note.clone(), - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - app_id: existing_app_metadata.app_id, - args: params.args.clone(), - account_references: params.account_references.clone(), - app_references: params.app_references.clone(), - asset_references: params.asset_references.clone(), - box_references: params.box_references.clone(), - }) - .map_err(|e| AppDeployError::ComposerError { source: e })?; - } - DeleteParams::AppDeleteMethodCall(params) => { - composer - .add_app_delete_method_call(AppDeleteMethodCallParams { - sender: params.sender.clone(), - signer: params.signer.clone(), - rekey_to: params.rekey_to.clone(), - note: params.note.clone(), - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - app_id: existing_app_metadata.app_id, - method: params.method.clone(), - args: params.args.clone(), - account_references: params.account_references.clone(), - app_references: params.app_references.clone(), - asset_references: params.asset_references.clone(), - box_references: params.box_references.clone(), - }) - .map_err(|e| AppDeployError::ComposerError { source: e })?; - } - } - - let result = composer - .send(Some(send_params.clone())) - .await - .map_err(|e| AppDeployError::ComposerError { source: e })?; - - // Extract create and delete results directly - let delete_transaction_index = result.results.len() - 1; - let create_result = result.results[create_transaction_index].clone(); - let delete_result = result.results[delete_transaction_index].clone(); - - // Get create confirmation from the tracked index - let create_confirmation = create_result.confirmation.clone(); - let app_id = - create_confirmation - .app_id - .ok_or_else(|| AppDeployError::DeploymentFailed { - message: "App creation confirmation missing application-index".to_string(), - })?; - let confirmed_round = create_confirmation.confirmed_round.ok_or_else(|| { - AppDeployError::DeploymentFailed { - message: "App creation confirmation missing confirmed-round".to_string(), - } - })?; - let app_address = Address::from_app_id(&app_id); - - let app_metadata = AppMetadata { - app_id, - app_address, - created_round: confirmed_round, - updated_round: confirmed_round, - created_metadata: metadata.clone(), - deleted: false, - name: metadata.name.clone(), - version: metadata.version.clone(), - updatable: metadata.updatable, - deletable: metadata.deletable, - }; - - let sender = match create_params { - CreateParams::AppCreateCall(params) => ¶ms.sender, - CreateParams::AppCreateMethodCall(params) => ¶ms.sender, - }; - - self.update_app_lookup(sender, &app_metadata); - - Ok(AppDeployResult::Replace { - app: app_metadata, - delete_result, - create_result, - group_results: result.results, - group: result.group, - compiled_programs, - }) - } - - /// Calculate minimum number of extra program pages required to fit the programs. - fn calculate_extra_program_pages(approval: &[u8], clear: &[u8]) -> u32 { - let total = approval.len().saturating_add(clear.len()); - if total == 0 { - return 0; - } - let page_size = algokit_transact::PROGRAM_PAGE_SIZE; - let pages = ((total - 1) / page_size) as u32; - std::cmp::min(pages, algokit_transact::MAX_EXTRA_PROGRAM_PAGES) - } -} diff --git a/crates/algokit_utils/src/applications/app_factory/compilation.rs b/crates/algokit_utils/src/applications/app_factory/compilation.rs deleted file mode 100644 index ec43e7590..000000000 --- a/crates/algokit_utils/src/applications/app_factory/compilation.rs +++ /dev/null @@ -1,90 +0,0 @@ -use algokit_abi::CallOnApplicationComplete; - -use super::{AppFactory, AppFactoryError}; -use crate::applications::app_client::CompilationParams; -use crate::clients::app_manager::{ - CompiledPrograms, DELETABLE_TEMPLATE_NAME, DeploymentMetadata, UPDATABLE_TEMPLATE_NAME, -}; - -impl AppFactory { - pub(crate) fn resolve_compilation_params( - &self, - compilation_params: Option, - ) -> CompilationParams { - let mut resolved = compilation_params.unwrap_or_default(); - - // Merge factory params if available - if let Some(factory_params) = &self.compilation_params { - resolved.deploy_time_params = resolved - .deploy_time_params - .or_else(|| factory_params.deploy_time_params.clone()); - resolved.updatable = resolved.updatable.or(factory_params.updatable); - resolved.deletable = resolved.deletable.or(factory_params.deletable); - } - - // Auto-detect flags from spec if still unset - resolved.updatable = resolved.updatable.or_else(|| { - self.detect_deploy_time_control_flag( - UPDATABLE_TEMPLATE_NAME, - CallOnApplicationComplete::UpdateApplication, - ) - }); - resolved.deletable = resolved.deletable.or_else(|| { - self.detect_deploy_time_control_flag( - DELETABLE_TEMPLATE_NAME, - CallOnApplicationComplete::DeleteApplication, - ) - }); - - resolved - } - - pub(crate) async fn compile( - &self, - compilation_params: Option, - ) -> Result { - let compilation_params = self.resolve_compilation_params(compilation_params); - - let (approval_teal, clear_teal) = - self.app_spec() - .decoded_teal() - .map_err(|e| AppFactoryError::CompilationError { - message: e.to_string(), - })?; - - let metadata = DeploymentMetadata { - updatable: compilation_params.updatable, - deletable: compilation_params.deletable, - }; - - let approval = self - .algorand() - .app() - .compile_teal_template( - &approval_teal, - compilation_params.deploy_time_params.as_ref(), - Some(&metadata), - ) - .await - .map_err(|e| AppFactoryError::CompilationError { - message: e.to_string(), - })?; - - let clear = self - .algorand() - .app() - .compile_teal_template( - &clear_teal, - compilation_params.deploy_time_params.as_ref(), - None, - ) - .await - .map_err(|e| AppFactoryError::CompilationError { - message: e.to_string(), - })?; - - self.update_source_maps(approval.source_map.clone(), clear.source_map.clone()); - - Ok(CompiledPrograms { approval, clear }) - } -} diff --git a/crates/algokit_utils/src/applications/app_factory/error.rs b/crates/algokit_utils/src/applications/app_factory/error.rs deleted file mode 100644 index 0d178cbc1..000000000 --- a/crates/algokit_utils/src/applications/app_factory/error.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::applications::app_client::LogicError; -use crate::applications::app_deployer::AppDeployError; -use crate::{AppClientError, ComposerError, TransactionSenderError}; -use algokit_abi::ABIError; -use snafu::Snafu; - -#[derive(Debug, Snafu)] -pub enum AppFactoryError { - #[snafu(display("Method not found: {message}"))] - CompilationError { message: String }, - #[snafu(display("Validation error: {message}"))] - ValidationError { message: String }, - #[snafu(display("Params builder error: {message}"))] - ParamsBuilderError { message: String }, - #[snafu(display("ABI error: {source}"))] - ABIError { source: ABIError }, - #[snafu(display("App client error: {source}"))] - AppClientError { source: AppClientError }, - #[snafu(display("Transaction sender error: {source}"))] - AppDeployerError { source: AppDeployError }, - #[snafu(display("Composer error: {source}"))] - ComposerError { source: ComposerError }, - #[snafu(display("{message}"))] - LogicError { - message: String, - logic: Box, - }, - #[snafu(display("Transaction sender error: {source}"))] - TransactionSenderError { source: TransactionSenderError }, -} diff --git a/crates/algokit_utils/src/applications/app_factory/mod.rs b/crates/algokit_utils/src/applications/app_factory/mod.rs deleted file mode 100644 index 0e693f7e7..000000000 --- a/crates/algokit_utils/src/applications/app_factory/mod.rs +++ /dev/null @@ -1,417 +0,0 @@ -use crate::applications::app_client::error_transformation::LogicErrorContext; -use crate::applications::app_client::{AppClientMethodCallParams, CompilationParams}; -use crate::applications::app_deployer::{AppLookup, OnSchemaBreak, OnUpdate}; -use crate::applications::{ - AppDeployMetadata, AppDeployParams, AppDeployResult, CreateParams, DeleteParams, UpdateParams, -}; -use crate::transactions::{ - TransactionComposerConfig, TransactionSigner, composer::SendParams as ComposerSendParams, -}; -use crate::{AlgorandClient, AppClient, AppClientParams, AppSourceMaps, TransactionSenderError}; -use algokit_abi::Arc56Contract; -use algokit_abi::arc56_contract::CallOnApplicationComplete; -use std::str::FromStr; -use std::sync::{Arc, Mutex}; - -mod compilation; -mod error; -mod params_builder; -mod transaction_builder; -mod transaction_sender; -mod types; -mod utils; - -pub use error::AppFactoryError; -pub use params_builder::ParamsBuilder; -pub use transaction_builder::TransactionBuilder; -pub use transaction_sender::TransactionSender; -pub use types::*; - -/// ARC-56 factory that compiles an application spec, deploys app instances, -/// and builds [`AppClient`]s for interacting with them. -/// -/// Constructed from [`AppFactoryParams`], the factory centralises shared context such -/// as the Algorand client, default sender and signer, and any deploy-time template -/// substitutions. -pub struct AppFactory { - app_spec: Arc56Contract, - algorand: Arc, - app_name: String, - version: String, - default_sender: Option, - default_signer: Option>, - approval_source_map: Mutex>, - clear_source_map: Mutex>, - compilation_params: Option, - transaction_composer_config: Option, -} - -#[derive(Default)] -pub struct DeployArgs { - pub on_update: Option, - pub on_schema_break: Option, - pub create_params: Option, - pub update_params: Option, - pub delete_params: Option, - pub existing_deployments: Option, - pub ignore_cache: Option, - pub app_name: Option, - pub send_params: Option, -} - -impl AppFactory { - pub fn new(params: AppFactoryParams) -> Self { - let AppFactoryParams { - algorand, - app_spec, - app_name, - default_sender, - default_signer, - version, - compilation_params, - source_maps, - transaction_composer_config, - } = params; - - let (initial_approval_source_map, initial_clear_source_map) = match source_maps { - Some(maps) => (maps.approval_source_map, maps.clear_source_map), - None => (None, None), - }; - - Self { - app_spec, - algorand, - app_name: app_name.unwrap_or_else(|| "".to_string()), - version: version.unwrap_or_else(|| "1.0".to_string()), - default_sender, - default_signer, - approval_source_map: Mutex::new(initial_approval_source_map), - clear_source_map: Mutex::new(initial_clear_source_map), - compilation_params, - transaction_composer_config, - } - } - - /// Returns the application name derived from the app spec or provided override. - pub fn app_name(&self) -> &str { - &self.app_name - } - /// Returns the normalised ARC-56 contract backing this factory. - pub fn app_spec(&self) -> &Arc56Contract { - &self.app_spec - } - - /// Returns the shared [`AlgorandClient`] configured for the factory. - pub fn algorand(&self) -> Arc { - self.algorand.clone() - } - - pub fn version(&self) -> &str { - &self.version - } - - /// Returns a [`ParamsBuilder`] that defers transaction construction for create, - /// update, and delete operations while reusing factory defaults. - pub fn params(&self) -> ParamsBuilder<'_> { - ParamsBuilder { factory: self } - } - /// Returns a [`TransactionBuilder`] for constructing transactions without submitting - /// them yet. - pub fn create_transaction(&self) -> TransactionBuilder<'_> { - TransactionBuilder { factory: self } - } - /// Returns a [`TransactionSender`] that sends transactions immediately and surfaces - /// their results. - pub fn send(&self) -> TransactionSender<'_> { - TransactionSender { factory: self } - } - - /// Imports compiled source maps so subsequent calls can surface logic errors with - /// meaningful context. - pub fn import_source_maps(&self, source_maps: AppSourceMaps) { - *self.approval_source_map.lock().unwrap() = source_maps.approval_source_map; - *self.clear_source_map.lock().unwrap() = source_maps.clear_source_map; - } - - /// Exports the cached source maps, returning an error if they have not been loaded yet. - pub fn export_source_maps(&self) -> Result { - let approval = self - .approval_source_map - .lock() - .unwrap() - .clone() - .ok_or_else(|| AppFactoryError::ValidationError { - message: "Approval source map not loaded".to_string(), - })?; - let clear = self - .clear_source_map - .lock() - .unwrap() - .clone() - .ok_or_else(|| AppFactoryError::ValidationError { - message: "Clear source map not loaded".to_string(), - })?; - Ok(AppSourceMaps { - approval_source_map: Some(approval), - clear_source_map: Some(clear), - }) - } - - /// Creates a new [`AppClient`] configured for the provided application ID, with - /// optional overrides for name, sender, signer, and source maps. - pub fn get_app_client_by_id( - &self, - app_id: u64, - app_name: Option, - default_sender: Option, - default_signer: Option>, - source_maps: Option, - ) -> AppClient { - let resolved_source_maps = source_maps.or_else(|| self.current_source_maps()); - AppClient::new(AppClientParams { - app_id, - app_spec: self.app_spec.clone(), - algorand: self.algorand.clone(), - app_name: Some(app_name.unwrap_or_else(|| self.app_name.clone())), - default_sender: default_sender.or_else(|| self.default_sender.clone()), - default_signer: default_signer.or_else(|| self.default_signer.clone()), - source_maps: resolved_source_maps, - transaction_composer_config: self.transaction_composer_config.clone(), - }) - } - - /// Resolves an application by creator address and name using AlgoKit deployment - /// semantics and returns a configured [`AppClient`]. Optional overrides control the - /// resolved name, sender, signer, and whether the lookup cache is bypassed. - /// - /// # Errors - /// Returns [`AppFactoryError`] if the application cannot be resolved or the - /// resulting client cannot be created. - pub async fn get_app_client_by_creator_and_name( - &self, - creator_address: &str, - app_name: Option, - default_sender: Option, - default_signer: Option>, - ignore_cache: Option, - ) -> Result { - let resolved_app_name = app_name.unwrap_or_else(|| self.app_name.clone()); - let resolved_sender = default_sender.or_else(|| self.default_sender.clone()); - let resolved_signer = default_signer.or_else(|| self.default_signer.clone()); - - let client = AppClient::from_creator_and_name( - creator_address, - &resolved_app_name, - self.app_spec.clone(), - self.algorand.clone(), - resolved_sender, - resolved_signer, - self.current_source_maps(), - ignore_cache, - self.transaction_composer_config.clone(), - ) - .await - .map_err(|e| AppFactoryError::AppClientError { source: e })?; - - Ok(client) - } - - pub(crate) fn get_sender_address( - &self, - sender: &Option, - ) -> Result { - let sender_str = sender - .as_ref() - .or(self.default_sender.as_ref()) - .ok_or_else(|| { - format!( - "No sender provided and no default sender configured for app {}", - self.app_name - ) - })?; - algokit_transact::Address::from_str(sender_str) - .map_err(|e| format!("Invalid sender address: {}", e)) - } - - pub(crate) fn update_source_maps( - &self, - approval: Option, - clear: Option, - ) { - *self.approval_source_map.lock().unwrap() = approval; - *self.clear_source_map.lock().unwrap() = clear; - } - - pub(crate) fn current_source_maps(&self) -> Option { - let approval = self.approval_source_map.lock().unwrap().clone(); - let clear = self.clear_source_map.lock().unwrap().clone(); - - if approval.is_none() && clear.is_none() { - None - } else { - Some(AppSourceMaps { - approval_source_map: approval, - clear_source_map: clear, - }) - } - } - - pub(crate) fn detect_deploy_time_control_flag( - &self, - template_name: &str, - on_complete: CallOnApplicationComplete, - ) -> Option { - let source = self.app_spec().source.as_ref()?; - let approval = source.get_decoded_approval().ok()?; - if !approval.contains(template_name) { - return None; - } - - let bare_allows = self - .app_spec() - .bare_actions - .call - .iter() - .any(|action| *action == on_complete); - let method_allows = self.app_spec().methods.iter().any(|method| { - method - .actions - .call - .iter() - .any(|action| *action == on_complete) - }); - - Some(bare_allows || method_allows) - } - - /// Transform a transaction error using AppClient logic error exposure for factory flows. - pub(crate) fn handle_transaction_error( - &self, - error: TransactionSenderError, - is_clear_state_program: bool, - ) -> AppFactoryError { - let error_str = error.to_string(); - - if !(error_str.contains("logic eval error") || error_str.contains("logic error")) { - return AppFactoryError::TransactionSenderError { source: error }; - } - - let source_maps = self.current_source_maps(); - let context = LogicErrorContext { - app_id: 0, - app_spec: &self.app_spec, - algorand: self.algorand.as_ref(), - source_maps: source_maps.as_ref(), - }; - - let logic_error = context.expose_logic_error(&error_str, is_clear_state_program); - AppFactoryError::LogicError { - message: logic_error.message.clone(), - logic: Box::new(logic_error), - } - } - - /// Idempotently deploys (create, update, or replace) an application using - /// `AppDeployer` semantics. - /// - /// The factory applies deploy-time template substitutions and reuses default - /// sender/signer settings while coordinating create, update, and optional delete - /// transactions. - /// - /// # Notes - /// * Inspect `operation_performed` on the returned [`AppFactoryDeployResult`] to - /// understand which operation was executed and to access related metadata. - /// * When `on_schema_break` is `OnSchemaBreak::Replace`, a breaking schema change - /// deletes and recreates the application. - /// * When `on_update` is `OnUpdate::Replace`, differing TEAL sources trigger a - /// delete-and-recreate cycle. - /// - /// # Errors - /// Returns [`AppFactoryError`] if parameter construction fails, compilation fails, - /// or the deployment encounters an error on chain. - pub async fn deploy( - &self, - args: DeployArgs, - compilation_params: Option, - ) -> Result<(AppClient, AppDeployResult), AppFactoryError> { - let compilation_params = self.resolve_compilation_params(compilation_params); - - let create_deploy_params = match args.create_params { - Some(cp) => CreateParams::AppCreateMethodCall(self.params().create(cp)?), - None => CreateParams::AppCreateCall(self.params().bare().create(None)?), - }; - - let update_deploy_params = match args.update_params { - Some(up) => UpdateParams::AppUpdateMethodCall(self.params().deploy_update(up)?), - None => UpdateParams::AppUpdateCall(self.params().bare().deploy_update(None)?), - }; - - let delete_deploy_params = match args.delete_params { - Some(dp) => DeleteParams::AppDeleteMethodCall(self.params().deploy_delete(dp)?), - None => DeleteParams::AppDeleteCall(self.params().bare().deploy_delete(None)?), - }; - - let metadata = AppDeployMetadata { - name: args.app_name.unwrap_or_else(|| self.app_name.clone()), - version: self.version.clone(), - updatable: compilation_params.updatable, - deletable: compilation_params.deletable, - }; - - let deploy_params = AppDeployParams { - metadata, - deploy_time_params: compilation_params.deploy_time_params, - on_schema_break: args.on_schema_break, - on_update: args.on_update, - create_params: create_deploy_params, - update_params: update_deploy_params, - delete_params: delete_deploy_params, - existing_deployments: args.existing_deployments, - ignore_cache: args.ignore_cache, - send_params: args.send_params.unwrap_or_default(), - }; - - let mut app_deployer = self.algorand.as_ref().app_deployer(); - - let deploy_result = app_deployer - .deploy(deploy_params) - .await - .map_err(|e| AppFactoryError::AppDeployerError { source: e })?; - - let app_id = match &deploy_result { - AppDeployResult::Create { app, .. } - | AppDeployResult::Update { app, .. } - | AppDeployResult::Replace { app, .. } - | AppDeployResult::Nothing { app } => app.app_id, - }; - - let app_client = self.get_app_client_by_id(app_id, None, None, None, None); - - // Extract and update source maps from the deploy result - let (approval_source_map, clear_source_map) = match &deploy_result { - AppDeployResult::Create { - compiled_programs, .. - } => ( - compiled_programs.approval.source_map.clone(), - compiled_programs.clear.source_map.clone(), - ), - AppDeployResult::Update { - compiled_programs, .. - } => ( - compiled_programs.approval.source_map.clone(), - compiled_programs.clear.source_map.clone(), - ), - AppDeployResult::Replace { - compiled_programs, .. - } => ( - compiled_programs.approval.source_map.clone(), - compiled_programs.clear.source_map.clone(), - ), - AppDeployResult::Nothing { .. } => (None, None), - }; - - self.update_source_maps(approval_source_map, clear_source_map); - - Ok((app_client, deploy_result)) - } -} diff --git a/crates/algokit_utils/src/applications/app_factory/params_builder.rs b/crates/algokit_utils/src/applications/app_factory/params_builder.rs deleted file mode 100644 index 94d193f55..000000000 --- a/crates/algokit_utils/src/applications/app_factory/params_builder.rs +++ /dev/null @@ -1,330 +0,0 @@ -use super::{AppFactory, AppFactoryError}; -use crate::applications::app_client::{AppClientBareCallParams, AppClientMethodCallParams}; -use crate::applications::app_deployer::{ - AppProgram, DeployAppCreateMethodCallParams, DeployAppCreateParams, - DeployAppDeleteMethodCallParams, DeployAppDeleteParams, DeployAppUpdateMethodCallParams, - DeployAppUpdateParams, -}; -use crate::applications::app_factory::utils::merge_args_with_defaults; -use crate::applications::app_factory::{AppFactoryCreateMethodCallParams, AppFactoryCreateParams}; -use algokit_transact::OnApplicationComplete; -use algokit_transact::StateSchema as TxStateSchema; -use std::str::FromStr; - -use super::utils::resolve_signer; - -/// Builds method-call deploy parameters using the factory's configuration. -pub struct ParamsBuilder<'a> { - pub(crate) factory: &'a AppFactory, -} - -/// Builds bare (non-ABI) deploy parameters backed by factory defaults. -pub struct BareParamsBuilder<'a> { - pub(crate) factory: &'a AppFactory, -} - -impl<'a> ParamsBuilder<'a> { - /// Returns the bare parameter builder for constructing non-ABI transactions. - pub fn bare(&self) -> BareParamsBuilder<'a> { - BareParamsBuilder { - factory: self.factory, - } - } - - /// Builds [`DeployAppCreateMethodCallParams`] using the supplied inputs and the - /// factory's compiled programs. - /// - /// # Errors - /// Returns [`AppFactoryError`] if the spec cannot be compiled, the method cannot be - /// located, or the sender address is invalid. - pub fn create( - &self, - params: AppFactoryCreateMethodCallParams, - ) -> Result { - let (approval_teal, clear_teal) = self.factory.app_spec().decoded_teal().map_err(|e| { - AppFactoryError::CompilationError { - message: e.to_string(), - } - })?; - let method = self - .factory - .app_spec() - .find_abi_method(¶ms.method) - .map_err(|e| AppFactoryError::ABIError { source: e })?; - let sender = self - .factory - .get_sender_address(¶ms.sender) - .map_err(|message| AppFactoryError::ValidationError { message })?; - - // Merge user args with ARC-56 literal defaults for create-time ABI - let merged_args = merge_args_with_defaults(self.factory, ¶ms.method, ¶ms.args)?; - - Ok(DeployAppCreateMethodCallParams { - sender, - signer: resolve_signer(self.factory, ¶ms.sender, params.signer), - rekey_to: params.rekey_to, - note: params.note, - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - on_complete: params.on_complete.unwrap_or(OnApplicationComplete::NoOp), - approval_program: AppProgram::Teal(approval_teal), - clear_state_program: AppProgram::Teal(clear_teal), - method, - args: merged_args, - account_references: None, - app_references: params.app_references, - asset_references: params.asset_references, - box_references: params.box_references, - global_state_schema: params - .global_state_schema - .or_else(|| Some(default_global_schema(self.factory))), - local_state_schema: params - .local_state_schema - .or_else(|| Some(default_local_schema(self.factory))), - extra_program_pages: params.extra_program_pages, - }) - } - - /// Builds [`DeployAppUpdateMethodCallParams`] for an update call, merging default - /// arguments defined in the ARC-56 contract. - /// - /// # Errors - /// Returns [`AppFactoryError`] if the method cannot be resolved, default arguments - /// cannot be merged, or the sender address is invalid. - pub fn deploy_update( - &self, - params: AppClientMethodCallParams, - ) -> Result { - let method = self - .factory - .app_spec() - .find_abi_method(¶ms.method) - .map_err(|e| AppFactoryError::ABIError { source: e })?; - let sender = self - .factory - .get_sender_address(¶ms.sender) - .map_err(|message| AppFactoryError::ValidationError { message })?; - - let merged_args = - merge_args_with_defaults(self.factory, ¶ms.method, &Some(params.args.clone()))?; - - Ok(DeployAppUpdateMethodCallParams { - sender, - signer: resolve_signer(self.factory, ¶ms.sender, params.signer), - rekey_to: params - .rekey_to - .as_ref() - .and_then(|s| algokit_transact::Address::from_str(s).ok()), - note: params.note, - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - method, - args: merged_args, - account_references: None, - app_references: params.app_references, - asset_references: params.asset_references, - box_references: params.box_references, - }) - } - - /// Builds [`DeployAppDeleteMethodCallParams`] for a delete call, merging default - /// arguments defined in the ARC-56 contract. - /// - /// # Errors - /// Returns [`AppFactoryError`] if the method cannot be resolved, default arguments - /// cannot be merged, or the sender address is invalid. - pub fn deploy_delete( - &self, - params: AppClientMethodCallParams, - ) -> Result { - let method = self - .factory - .app_spec() - .find_abi_method(¶ms.method) - .map_err(|e| AppFactoryError::ABIError { source: e })?; - let sender = self - .factory - .get_sender_address(¶ms.sender) - .map_err(|message| AppFactoryError::ValidationError { message })?; - - let merged_args = - merge_args_with_defaults(self.factory, ¶ms.method, &Some(params.args.clone()))?; - - Ok(DeployAppDeleteMethodCallParams { - sender, - signer: resolve_signer(self.factory, ¶ms.sender, params.signer), - rekey_to: params - .rekey_to - .as_ref() - .and_then(|s| algokit_transact::Address::from_str(s).ok()), - note: params.note, - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - method, - args: merged_args, - account_references: None, - app_references: params.app_references, - asset_references: params.asset_references, - box_references: params.box_references, - }) - } -} - -impl BareParamsBuilder<'_> { - /// Builds [`DeployAppCreateParams`] using factory defaults and compiled programs. - /// - /// # Errors - /// Returns [`AppFactoryError`] if the spec cannot be compiled or the sender address - /// is invalid. - pub fn create( - &self, - params: Option, - ) -> Result { - let params = params.unwrap_or_default(); - let (approval_teal, clear_teal) = self.factory.app_spec().decoded_teal().map_err(|e| { - AppFactoryError::CompilationError { - message: e.to_string(), - } - })?; - let sender = self - .factory - .get_sender_address(¶ms.sender) - .map_err(|message| AppFactoryError::ValidationError { message })?; - - Ok(DeployAppCreateParams { - sender, - signer: resolve_signer(self.factory, ¶ms.sender, params.signer), - rekey_to: params.rekey_to, - note: params.note, - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - on_complete: params.on_complete.unwrap_or(OnApplicationComplete::NoOp), - approval_program: AppProgram::Teal(approval_teal), - clear_state_program: AppProgram::Teal(clear_teal), - args: params.args, - account_references: None, - app_references: params.app_references, - asset_references: params.asset_references, - box_references: params.box_references, - global_state_schema: params - .global_state_schema - .or_else(|| Some(default_global_schema(self.factory))), - local_state_schema: params - .local_state_schema - .or_else(|| Some(default_local_schema(self.factory))), - extra_program_pages: params.extra_program_pages, - }) - } - - /// Builds [`DeployAppUpdateParams`] for a bare update transaction using factory - /// defaults. - /// - /// # Errors - /// Returns [`AppFactoryError`] if the sender address is invalid. - pub fn deploy_update( - &self, - params: Option, - ) -> Result { - let params = params.unwrap_or_default(); - let sender = self - .factory - .get_sender_address(¶ms.sender) - .map_err(|message| AppFactoryError::ValidationError { message })?; - - Ok(DeployAppUpdateParams { - sender, - signer: resolve_signer(self.factory, ¶ms.sender, params.signer), - rekey_to: params - .rekey_to - .as_ref() - .and_then(|s| algokit_transact::Address::from_str(s).ok()), - note: params.note, - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - args: params.args, - account_references: None, - app_references: params.app_references, - asset_references: params.asset_references, - box_references: params.box_references, - }) - } - - /// Builds [`DeployAppDeleteParams`] for a bare delete transaction using factory - /// defaults. - /// - /// # Errors - /// Returns [`AppFactoryError`] if the sender address is invalid. - pub fn deploy_delete( - &self, - params: Option, - ) -> Result { - let params = params.unwrap_or_default(); - let sender = self - .factory - .get_sender_address(¶ms.sender) - .map_err(|message| AppFactoryError::ValidationError { message })?; - - Ok(DeployAppDeleteParams { - sender, - signer: resolve_signer(self.factory, ¶ms.sender, params.signer), - rekey_to: params - .rekey_to - .as_ref() - .and_then(|s| algokit_transact::Address::from_str(s).ok()), - note: params.note, - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - args: params.args, - account_references: None, - app_references: params.app_references, - asset_references: params.asset_references, - box_references: params.box_references, - }) - } -} - -fn default_global_schema(factory: &AppFactory) -> TxStateSchema { - let s = &factory.app_spec().state.schema.global_state; - TxStateSchema { - num_uints: s.ints, - num_byte_slices: s.bytes, - } -} - -fn default_local_schema(factory: &AppFactory) -> TxStateSchema { - let s = &factory.app_spec().state.schema.local_state; - TxStateSchema { - num_uints: s.ints, - num_byte_slices: s.bytes, - } -} diff --git a/crates/algokit_utils/src/applications/app_factory/transaction_builder.rs b/crates/algokit_utils/src/applications/app_factory/transaction_builder.rs deleted file mode 100644 index f00423e98..000000000 --- a/crates/algokit_utils/src/applications/app_factory/transaction_builder.rs +++ /dev/null @@ -1,105 +0,0 @@ -use super::AppFactory; -use super::utils::{ - build_bare_create_params, build_create_method_call_params, merge_args_with_defaults, -}; -use crate::applications::app_client::CompilationParams; -use crate::applications::app_factory::{ - AppFactoryCreateMethodCallParams, AppFactoryCreateParams, AppFactoryError, -}; -use algokit_transact::Transaction; -use futures::TryFutureExt; - -/// Builds transactions for AppFactory create flows without immediately submitting them. -pub struct TransactionBuilder<'app_factory> { - pub(crate) factory: &'app_factory AppFactory, -} - -/// Builds bare create transactions ready for manual submission. -pub struct BareTransactionBuilder<'app_factory> { - pub(crate) factory: &'app_factory AppFactory, -} - -impl<'app_factory> TransactionBuilder<'app_factory> { - /// Returns helpers for building bare (non-ABI) transactions. - pub fn bare(&self) -> BareTransactionBuilder<'app_factory> { - BareTransactionBuilder { - factory: self.factory, - } - } - - /// Builds transactions for an app creation method call without sending them. - /// - /// # Errors - /// Returns [`AppFactoryError`] if compilation fails, method lookup fails, or - /// transaction construction encounters invalid inputs. - pub async fn create( - &self, - params: AppFactoryCreateMethodCallParams, - compilation_params: Option, - ) -> Result, AppFactoryError> { - let compiled = self.factory.compile(compilation_params).await?; - let method = self - .factory - .app_spec() - .find_abi_method(¶ms.method) - .map_err(|e| AppFactoryError::ABIError { source: e })?; - let sender = self - .factory - .get_sender_address(¶ms.sender) - .map_err(|message| AppFactoryError::ValidationError { message })?; - - let merged_args = merge_args_with_defaults(self.factory, ¶ms.method, ¶ms.args)?; - - let create_params = build_create_method_call_params( - self.factory, - sender, - ¶ms, - method, - merged_args, - compiled.approval.compiled_base64_to_bytes, - compiled.clear.compiled_base64_to_bytes, - ); - - self.factory - .algorand() - .create() - .app_create_method_call(create_params) - .map_err(|e| AppFactoryError::ComposerError { source: e }) - .await - } -} - -impl BareTransactionBuilder<'_> { - /// Builds a bare app creation transaction without sending it. - /// - /// # Errors - /// Returns [`ComposerError`] if compilation fails or the sender address is invalid. - pub async fn create( - &self, - params: Option, - compilation_params: Option, - ) -> Result { - let params = params.unwrap_or_default(); - - let compiled = self.factory.compile(compilation_params).await?; - let sender = self - .factory - .get_sender_address(¶ms.sender) - .map_err(|message| AppFactoryError::ValidationError { message })?; - - let create_params = build_bare_create_params( - self.factory, - sender, - ¶ms, - compiled.approval.compiled_base64_to_bytes, - compiled.clear.compiled_base64_to_bytes, - ); - - self.factory - .algorand() - .create() - .app_create(create_params) - .map_err(|e| AppFactoryError::ComposerError { source: e }) - .await - } -} diff --git a/crates/algokit_utils/src/applications/app_factory/transaction_sender.rs b/crates/algokit_utils/src/applications/app_factory/transaction_sender.rs deleted file mode 100644 index c7a7a16f9..000000000 --- a/crates/algokit_utils/src/applications/app_factory/transaction_sender.rs +++ /dev/null @@ -1,170 +0,0 @@ -use super::AppFactory; -use super::utils::{ - build_bare_create_params, build_create_method_call_params, merge_args_with_defaults, -}; -use crate::applications::app_client::AppClient; -use crate::applications::app_client::CompilationParams; -use crate::applications::app_factory::{ - AppFactoryCreateMethodCallParams, AppFactoryCreateMethodCallResult, AppFactoryCreateParams, - AppFactoryCreateResult, AppFactoryError, -}; -use crate::transactions::SendParams; -use algokit_transact::Address; - -/// Sends factory-backed create transactions and returns both the client and send results. -pub struct TransactionSender<'app_factory> { - pub(crate) factory: &'app_factory AppFactory, -} - -/// Bare transaction helpers for AppFactory create flows. -pub struct BareTransactionSender<'app_factory> { - pub(crate) factory: &'app_factory AppFactory, -} - -impl<'app_factory> TransactionSender<'app_factory> { - /// Returns helpers for bare (non-ABI) create transactions. - pub fn bare(&self) -> BareTransactionSender<'app_factory> { - BareTransactionSender { - factory: self.factory, - } - } - - /// Sends an app creation method call and returns the new client with the factory - /// flavoured result wrapper. - /// - /// # Errors - /// Returns [`AppFactoryError`] if argument merging, compilation, or the - /// underlying transaction submission fails. - pub async fn create( - &self, - params: AppFactoryCreateMethodCallParams, - send_params: Option, - compilation_params: Option, - ) -> Result<(AppClient, AppFactoryCreateMethodCallResult), AppFactoryError> { - let compiled = self.factory.compile(compilation_params).await?; - - let method = self - .factory - .app_spec() - .find_abi_method(¶ms.method) - .map_err(|e| AppFactoryError::ABIError { source: e })?; - let sender = self - .factory - .get_sender_address(¶ms.sender) - .map_err(|message| AppFactoryError::ValidationError { message })?; - - let merged_args = merge_args_with_defaults(self.factory, ¶ms.method, ¶ms.args)?; - - let approval_bytes = compiled.approval.compiled_base64_to_bytes.clone(); - let clear_bytes = compiled.clear.compiled_base64_to_bytes.clone(); - - let create_params = build_create_method_call_params( - self.factory, - sender, - ¶ms, - method, - merged_args, - approval_bytes.clone(), - clear_bytes.clone(), - ); - - let result = self - .factory - .algorand() - .send() - .app_create_method_call(create_params, send_params) - .await - .map_err(|e| self.factory.handle_transaction_error(e, false))?; - - let app_client = self.factory.get_app_client_by_id( - result.app_id, - Some(self.factory.app_name().to_string()), - None, - None, - None, - ); - - // Extract app ID and construct the factory result - let app_id = result.app_id; - let app_address = Address::from_app_id(&app_id); - - let factory_result = AppFactoryCreateMethodCallResult { - result: result.result, - group_results: result.group_results, - group: result.group, - app_id, - app_address, - compiled_programs: compiled, - }; - - Ok((app_client, factory_result)) - } -} - -impl BareTransactionSender<'_> { - /// Sends a bare app creation and returns the new client with the send result. - /// - /// # Errors - /// Returns [`AppFactoryError`] if compilation fails, the sender address is - /// invalid, or the underlying transaction submission fails. - pub async fn create( - &self, - params: Option, - send_params: Option, - compilation_params: Option, - ) -> Result<(AppClient, AppFactoryCreateResult), AppFactoryError> { - let params = params.unwrap_or_default(); - - let compiled = self - .factory - .compile(compilation_params) - .await - .map_err(|e| AppFactoryError::ValidationError { - message: e.to_string(), - })?; - - let sender = self - .factory - .get_sender_address(¶ms.sender) - .map_err(|e| AppFactoryError::ValidationError { message: e })?; - - let create_params = build_bare_create_params( - self.factory, - sender, - ¶ms, - compiled.approval.compiled_base64_to_bytes.clone(), - compiled.clear.compiled_base64_to_bytes.clone(), - ); - - let result = self - .factory - .algorand() - .send() - .app_create(create_params, send_params) - .await - .map_err(|e| self.factory.handle_transaction_error(e, false))?; - - let app_id = result.app_id; - let app_address = Address::from_app_id(&app_id); - - let app_client = self.factory.get_app_client_by_id( - app_id, - Some(self.factory.app_name().to_string()), - None, - None, - None, - ); - - // Convert to factory result with flattened fields - let factory_result = AppFactoryCreateResult { - transaction: result.transaction, - confirmation: result.confirmation, - transaction_id: result.transaction_id, - app_id, - app_address, - compiled_programs: compiled, - }; - - Ok((app_client, factory_result)) - } -} diff --git a/crates/algokit_utils/src/applications/app_factory/types.rs b/crates/algokit_utils/src/applications/app_factory/types.rs deleted file mode 100644 index 94d5725c0..000000000 --- a/crates/algokit_utils/src/applications/app_factory/types.rs +++ /dev/null @@ -1,150 +0,0 @@ -use crate::AlgorandClient; -use crate::AppSourceMaps; -use crate::applications::app_client::CompilationParams; -use crate::clients::app_manager::CompiledPrograms; -use crate::transactions::{ - AppMethodCallArg, TransactionComposerConfig, TransactionResult, TransactionSigner, -}; -use algod_client::models::PendingTransactionResponse; -use algokit_abi::Arc56Contract; -use algokit_transact::Byte32; -use algokit_transact::{Address, Transaction}; -use std::sync::Arc; - -/// Result from sending an app create call via AppFactory. -#[derive(Clone, Debug)] -pub struct AppFactoryCreateResult { - /// The create transaction - pub transaction: Transaction, - /// The response from sending and waiting for the create transaction - pub confirmation: PendingTransactionResponse, - /// The create transaction ID - pub transaction_id: String, - /// The ID of the created app - pub app_id: u64, - /// The address of the created app - pub app_address: Address, - /// The compiled approval and clear programs - pub compiled_programs: CompiledPrograms, -} - -/// Result from sending an app create method call via AppFactory. -#[derive(Clone, Debug)] -pub struct AppFactoryCreateMethodCallResult { - /// The result of the primary (last) transaction - pub result: TransactionResult, - /// All transaction results - pub group_results: Vec, - /// The group ID for the transaction group (if any) - pub group: Option, - /// The ID of the created app - pub app_id: u64, - /// The address of the created app - pub app_address: Address, - /// The compiled approval and clear programs - pub compiled_programs: CompiledPrograms, -} - -pub struct AppFactoryParams { - pub algorand: Arc, - pub app_spec: Arc56Contract, - pub app_name: Option, - pub default_sender: Option, - pub default_signer: Option>, - pub version: Option, - pub compilation_params: Option, - pub source_maps: Option, - pub transaction_composer_config: Option, -} -#[derive(Clone, Default)] -pub struct AppFactoryCreateParams { - pub on_complete: Option, - pub args: Option>>, - pub account_references: Option>, - pub app_references: Option>, - pub asset_references: Option>, - pub box_references: Option>, - pub global_state_schema: Option, - pub local_state_schema: Option, - pub extra_program_pages: Option, - pub sender: Option, - pub signer: Option>, - pub rekey_to: Option, - pub note: Option>, - pub lease: Option<[u8; 32]>, - pub static_fee: Option, - pub extra_fee: Option, - pub max_fee: Option, - pub validity_window: Option, - pub first_valid_round: Option, - pub last_valid_round: Option, -} - -#[derive(Clone, Default)] -pub struct AppFactoryCreateMethodCallParams { - pub method: String, - pub args: Option>, - pub on_complete: Option, - pub account_references: Option>, - pub app_references: Option>, - pub asset_references: Option>, - pub box_references: Option>, - pub global_state_schema: Option, - pub local_state_schema: Option, - pub extra_program_pages: Option, - pub sender: Option, - pub signer: Option>, - pub rekey_to: Option, - pub note: Option>, - pub lease: Option<[u8; 32]>, - pub static_fee: Option, - pub extra_fee: Option, - pub max_fee: Option, - pub validity_window: Option, - pub first_valid_round: Option, - pub last_valid_round: Option, -} - -#[derive(Clone, Default)] -pub struct AppFactoryUpdateMethodCallParams { - pub app_id: u64, - pub method: String, - pub args: Option>, - pub sender: Option, - pub account_references: Option>, - pub app_references: Option>, - pub asset_references: Option>, - pub box_references: Option>, - pub signer: Option>, - pub rekey_to: Option, - pub note: Option>, - pub lease: Option<[u8; 32]>, - pub static_fee: Option, - pub extra_fee: Option, - pub max_fee: Option, - pub validity_window: Option, - pub first_valid_round: Option, - pub last_valid_round: Option, -} - -#[derive(Clone, Default)] -pub struct AppFactoryDeleteMethodCallParams { - pub app_id: u64, - pub method: String, - pub args: Option>, - pub sender: Option, - pub account_references: Option>, - pub app_references: Option>, - pub asset_references: Option>, - pub box_references: Option>, - pub signer: Option>, - pub rekey_to: Option, - pub note: Option>, - pub lease: Option<[u8; 32]>, - pub static_fee: Option, - pub extra_fee: Option, - pub max_fee: Option, - pub validity_window: Option, - pub first_valid_round: Option, - pub last_valid_round: Option, -} diff --git a/crates/algokit_utils/src/applications/app_factory/utils.rs b/crates/algokit_utils/src/applications/app_factory/utils.rs deleted file mode 100644 index 928b1104d..000000000 --- a/crates/algokit_utils/src/applications/app_factory/utils.rs +++ /dev/null @@ -1,247 +0,0 @@ -use super::{AppFactory, AppFactoryError}; -use crate::applications::app_factory::{AppFactoryCreateMethodCallParams, AppFactoryCreateParams}; -use crate::transactions::{ - AppCreateMethodCallParams, AppCreateParams, AppMethodCallArg, TransactionSigner, -}; -use algokit_abi::ABIMethod; -use algokit_abi::abi_type::ABIType; -use algokit_abi::arc56_contract::{DefaultValue, DefaultValueSource, MethodArg}; -use algokit_transact::{Address, OnApplicationComplete, StateSchema}; -use base64::Engine; -use base64::engine::general_purpose::STANDARD as Base64; -use std::str::FromStr; -use std::sync::Arc; - -/// Merge user-provided ABI method arguments with ARC-56 literal defaults. -/// Only 'literal' default values are supported; others will be ignored and treated as missing. -pub(crate) fn merge_args_with_defaults( - factory: &AppFactory, - method_name_or_signature: &str, - user_args: &Option>, -) -> Result, AppFactoryError> { - let contract = factory.app_spec(); - let method = contract.get_method(method_name_or_signature).map_err(|e| { - AppFactoryError::ValidationError { - message: e.to_string(), - } - })?; - - let mut result: Vec = Vec::with_capacity(method.args.len()); - let provided = user_args.as_ref().map(|v| v.as_slice()).unwrap_or(&[]); - - for (i, arg_def) in method.args.iter().enumerate() { - let method_arg_name = arg_def - .name - .as_ref() - .cloned() - .unwrap_or_else(|| format!("arg{}", i + 1)); - - if i < provided.len() { - let provided_arg = &provided[i]; - - if matches!(provided_arg, AppMethodCallArg::DefaultValue) { - let default = arg_def.default_value.as_ref().ok_or_else(|| { - AppFactoryError::ParamsBuilderError { - message: format!( - "No default value defined for argument {} in call to method {}", - method_arg_name, method.name - ), - } - })?; - - if default.source != DefaultValueSource::Literal { - return Err(AppFactoryError::ParamsBuilderError { - message: format!( - "Default value is not supported by argument {} in call to method {}", - method_arg_name, method.name - ), - }); - } - - let literal = - decode_literal_default_value(default, arg_def, &method.name, &method_arg_name)?; - result.push(AppMethodCallArg::ABIValue(literal)); - } else { - result.push(provided_arg.clone()); - } - - continue; - } - - if let Some(default) = &arg_def.default_value { - if matches!(default.source, DefaultValueSource::Literal) { - let literal = - decode_literal_default_value(default, arg_def, &method.name, &method_arg_name)?; - - result.push(AppMethodCallArg::ABIValue(literal)); - continue; - } - } - - return Err(AppFactoryError::ParamsBuilderError { - message: format!( - "No value provided for required argument {} in call to method {}", - method_arg_name, method.name - ), - }); - } - - Ok(result) -} - -fn decode_literal_default_value( - default: &DefaultValue, - arg_def: &MethodArg, - method_name: &str, - arg_name: &str, -) -> Result { - if !matches!(default.source, DefaultValueSource::Literal) { - return Err(AppFactoryError::ParamsBuilderError { - message: format!( - "Default value for argument {} in call to method {} must be a literal", - arg_name, method_name - ), - }); - } - - let abi_type_str = default.value_type.as_deref().unwrap_or(&arg_def.arg_type); - let abi_type = - ABIType::from_str(abi_type_str).map_err(|e| AppFactoryError::ParamsBuilderError { - message: e.to_string(), - })?; - - let bytes = Base64 - .decode(&default.data) - .map_err(|e| AppFactoryError::ParamsBuilderError { - message: format!( - "Failed to base64-decode default literal for argument {} in call to method {}: {}", - arg_name, method_name, e - ), - })?; - - let abi_value = abi_type - .decode(&bytes) - .map_err(|e| AppFactoryError::ABIError { source: e })?; - - Ok(abi_value) -} - -/// Resolve signer: prefer explicit signer; otherwise use factory default signer when -/// sender is unspecified or equals the factory default sender. -pub(crate) fn resolve_signer( - factory: &AppFactory, - sender: &Option, - signer: Option>, -) -> Option> { - signer.or_else( - || match (sender.as_deref(), factory.default_sender.as_deref()) { - (None, _) => factory.default_signer.clone(), - (Some(s), Some(d)) if s == d => factory.default_signer.clone(), - _ => None, - }, - ) -} - -/// Returns the provided schemas or falls back to those declared in the contract spec. -pub(crate) fn default_schemas( - factory: &AppFactory, - global: Option, - local: Option, -) -> (Option, Option) { - let g = global.or_else(|| { - let s = &factory.app_spec().state.schema.global_state; - Some(StateSchema { - num_uints: s.ints, - num_byte_slices: s.bytes, - }) - }); - let l = local.or_else(|| { - let s = &factory.app_spec().state.schema.local_state; - Some(StateSchema { - num_uints: s.ints, - num_byte_slices: s.bytes, - }) - }); - (g, l) -} - -pub(crate) fn build_create_method_call_params( - factory: &AppFactory, - sender: Address, - base: &AppFactoryCreateMethodCallParams, - method: ABIMethod, - args: Vec, - approval_program: Vec, - clear_state_program: Vec, -) -> AppCreateMethodCallParams { - let (global_state_schema, local_state_schema) = default_schemas( - factory, - base.global_state_schema.clone(), - base.local_state_schema.clone(), - ); - - AppCreateMethodCallParams { - sender, - signer: resolve_signer(factory, &base.sender, base.signer.clone()), - rekey_to: base.rekey_to.clone(), - note: base.note.clone(), - lease: base.lease, - static_fee: base.static_fee, - extra_fee: base.extra_fee, - max_fee: base.max_fee, - validity_window: base.validity_window, - first_valid_round: base.first_valid_round, - last_valid_round: base.last_valid_round, - on_complete: base.on_complete.unwrap_or(OnApplicationComplete::NoOp), - approval_program, - clear_state_program, - method, - args, - account_references: base.account_references.clone(), - app_references: base.app_references.clone(), - asset_references: base.asset_references.clone(), - box_references: base.box_references.clone(), - global_state_schema, - local_state_schema, - extra_program_pages: base.extra_program_pages, - } -} - -pub(crate) fn build_bare_create_params( - factory: &AppFactory, - sender: Address, - base: &AppFactoryCreateParams, - approval_program: Vec, - clear_state_program: Vec, -) -> AppCreateParams { - let (global_state_schema, local_state_schema) = default_schemas( - factory, - base.global_state_schema.clone(), - base.local_state_schema.clone(), - ); - - AppCreateParams { - sender, - signer: resolve_signer(factory, &base.sender, base.signer.clone()), - rekey_to: base.rekey_to.clone(), - note: base.note.clone(), - lease: base.lease, - static_fee: base.static_fee, - extra_fee: base.extra_fee, - max_fee: base.max_fee, - validity_window: base.validity_window, - first_valid_round: base.first_valid_round, - last_valid_round: base.last_valid_round, - on_complete: base.on_complete.unwrap_or(OnApplicationComplete::NoOp), - approval_program, - clear_state_program, - args: base.args.clone(), - account_references: base.account_references.clone(), - app_references: base.app_references.clone(), - asset_references: base.asset_references.clone(), - box_references: base.box_references.clone(), - global_state_schema, - local_state_schema, - extra_program_pages: base.extra_program_pages, - } -} diff --git a/crates/algokit_utils/src/applications/mod.rs b/crates/algokit_utils/src/applications/mod.rs deleted file mode 100644 index fd8cd5d0d..000000000 --- a/crates/algokit_utils/src/applications/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod app_client; -pub mod app_deployer; -pub mod app_factory; - -// Re-export commonly used client types -pub use app_deployer::{ - AppDeployError, AppDeployMetadata, AppDeployParams, AppDeployResult, AppDeployer, AppLookup, - AppMetadata, AppProgram, CreateParams, DeleteParams, DeployAppCreateMethodCallParams, - DeployAppCreateParams, DeployAppDeleteMethodCallParams, DeployAppDeleteParams, - DeployAppUpdateMethodCallParams, DeployAppUpdateParams, OnSchemaBreak, OnUpdate, UpdateParams, -}; diff --git a/crates/algokit_utils/src/clients/account_manager.rs b/crates/algokit_utils/src/clients/account_manager.rs deleted file mode 100644 index b3c99ac25..000000000 --- a/crates/algokit_utils/src/clients/account_manager.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::{collections::HashMap, sync::Arc}; - -use algokit_transact::Address; -use snafu::Snafu; - -use crate::{TransactionSigner, transactions::common::TransactionSignerGetter}; - -pub struct AccountManager { - default_signer: Option>, - accounts: HashMap>, -} - -impl Default for AccountManager { - fn default() -> Self { - Self::new() - } -} - -impl AccountManager { - pub fn new() -> Self { - Self { - default_signer: None, - accounts: HashMap::new(), - } - } - - pub fn set_default_signer(&mut self, default_signer: Arc) { - self.default_signer = Some(default_signer); - } - - pub fn set_signer(&mut self, sender: Address, signer: Arc) { - self.accounts.insert(sender, signer); - } - - pub fn get_signer( - &self, - sender: Address, - ) -> Result, AccountManagerError> { - self.accounts - .get(&sender) - .cloned() - .or(self.default_signer.clone()) - .ok_or_else(|| AccountManagerError::SignerNotFound { - address: sender.to_string(), - }) - } -} - -#[derive(Debug, Snafu)] -pub enum AccountManagerError { - #[snafu(display("No signer found for address: {address}"))] - SignerNotFound { address: String }, -} - -impl TransactionSignerGetter for AccountManager { - fn get_signer(&self, address: Address) -> Result, String> { - self.get_signer(address).map_err(|e| e.to_string()) - } -} diff --git a/crates/algokit_utils/src/clients/algorand_client.rs b/crates/algokit_utils/src/clients/algorand_client.rs deleted file mode 100644 index 305271046..000000000 --- a/crates/algokit_utils/src/clients/algorand_client.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::applications::AppDeployer; -use crate::clients::app_manager::AppManager; -use crate::clients::asset_manager::AssetManager; -use crate::clients::client_manager::ClientManager; -use crate::clients::network_client::{AlgoConfig, AlgorandService}; -use crate::transactions::{ - TransactionComposer, TransactionComposerConfig, TransactionComposerParams, TransactionCreator, - TransactionSender, -}; -use crate::{AccountManager, TransactionSigner}; -use algod_client::models::TransactionParams; -use algokit_transact::Address; -use std::sync::{Arc, Mutex}; - -pub struct AlgorandClient { - client_manager: ClientManager, - asset_manager: AssetManager, - app_manager: AppManager, - app_deployer: AppDeployer, - transaction_sender: TransactionSender, - transaction_creator: TransactionCreator, - account_manager: Arc>, - default_composer_config: Option, -} - -/// A client that brokers easy access to Algorand functionality. -pub struct AlgorandClientParams { - pub client_config: AlgoConfig, - pub composer_config: Option, -} - -impl AlgorandClient { - pub fn new(params: &AlgorandClientParams) -> Self { - let client_manager = ClientManager::new(¶ms.client_config).unwrap(); - let algod_client = client_manager.algod(); - - let account_manager = Arc::new(Mutex::new(AccountManager::new())); - - let new_composer = { - let algod_client = algod_client.clone(); - let account_manager = account_manager.clone(); - let default_composer_config = params.composer_config.clone(); - move |composer_config: Option| { - TransactionComposer::new(TransactionComposerParams { - algod_client: algod_client.clone(), - signer_getter: account_manager.clone(), - composer_config: composer_config.or_else(|| default_composer_config.clone()), - }) - } - }; - - let algod_client_for_asset = algod_client.clone(); - let asset_manager = AssetManager::new(algod_client_for_asset.clone(), new_composer.clone()); - let app_manager = AppManager::new(algod_client.clone()); - - // Create closure for new_composer function - let transaction_sender = - TransactionSender::new(new_composer.clone(), asset_manager.clone()); - - // Create closure for TransactionCreator - let transaction_creator = TransactionCreator::new(new_composer.clone()); - - let app_deployer = AppDeployer::new( - app_manager.clone(), - transaction_sender.clone(), - Some(client_manager.indexer().unwrap()), - ); - - Self { - client_manager, - account_manager: account_manager.clone(), - asset_manager, - app_manager, - app_deployer, - transaction_sender, - transaction_creator, - default_composer_config: params.composer_config.clone(), - } - } - - pub async fn get_suggested_params( - &self, - ) -> Result> { - Ok(self.client_manager.algod().transaction_params().await?) - } - - pub fn client(&self) -> &ClientManager { - &self.client_manager - } - - /// Get access to the AssetManager for asset operations - pub fn asset(&self) -> &AssetManager { - &self.asset_manager - } - - /// Get access to the AppManager for app operations - pub fn app(&self) -> &AppManager { - &self.app_manager - } - - /// Get access to the TransactionSender for sending transactions - pub fn send(&self) -> &TransactionSender { - &self.transaction_sender - } - - /// Get access to the TransactionCreator for building transactions - pub fn create(&self) -> &TransactionCreator { - &self.transaction_creator - } - - /// Create a new transaction composer for building transaction groups - pub fn new_composer(&self, params: Option) -> TransactionComposer { - TransactionComposer::new(TransactionComposerParams { - algod_client: self.client_manager.algod().clone(), - signer_getter: self.account_manager.clone(), - composer_config: params.or_else(|| self.default_composer_config.clone()), - }) - } - - pub fn default_localnet(params: Option) -> Self { - Self::new(&AlgorandClientParams { - client_config: AlgoConfig { - algod_config: ClientManager::get_default_localnet_config(AlgorandService::Algod), - indexer_config: Some(ClientManager::get_default_localnet_config( - AlgorandService::Indexer, - )), - kmd_config: Some(ClientManager::get_default_localnet_config( - AlgorandService::Kmd, - )), - }, - composer_config: params, - }) - } - - pub fn testnet(params: Option) -> Self { - Self::new(&AlgorandClientParams { - client_config: AlgoConfig { - algod_config: ClientManager::get_algonode_config("testnet", AlgorandService::Algod), - indexer_config: Some(ClientManager::get_algonode_config( - "testnet", - AlgorandService::Indexer, - )), - kmd_config: None, - }, - composer_config: params, - }) - } - - pub fn mainnet(params: Option) -> Self { - Self::new(&AlgorandClientParams { - client_config: AlgoConfig { - algod_config: ClientManager::get_algonode_config("mainnet", AlgorandService::Algod), - indexer_config: Some(ClientManager::get_algonode_config( - "mainnet", - AlgorandService::Indexer, - )), - kmd_config: None, - }, - composer_config: params, - }) - } - - pub fn from_environment(params: Option) -> Self { - Self::new(&AlgorandClientParams { - client_config: ClientManager::get_config_from_environment_or_localnet(), - composer_config: params, - }) - } - - pub fn set_signer(&mut self, sender: Address, signer: Arc) { - self.account_manager - .lock() - .unwrap() - .set_signer(sender, signer); - } - - /// Get a clone of the persistent AppDeployer (shares cache across clones) - pub fn app_deployer(&self) -> AppDeployer { - self.app_deployer.clone() - } -} diff --git a/crates/algokit_utils/src/clients/app_manager.rs b/crates/algokit_utils/src/clients/app_manager.rs deleted file mode 100644 index 3adc1123c..000000000 --- a/crates/algokit_utils/src/clients/app_manager.rs +++ /dev/null @@ -1,722 +0,0 @@ -use algod_client::{ - apis::{AlgodClient, Error as AlgodError}, - models::TealKeyValue, -}; -use algokit_abi::{ABIMethod, ABIReturn, ABIType, ABIValue}; -use algokit_transact::Address; -use base64::{Engine, engine::general_purpose::STANDARD as Base64}; -use sha2::{Digest, Sha256}; -use snafu::Snafu; -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; - -#[derive(Debug, Clone)] -pub enum TealTemplateValue { - Int(u64), - Bytes(Vec), - String(String), -} - -#[derive(Debug, Clone)] -pub struct DeploymentMetadata { - pub updatable: Option, - pub deletable: Option, -} - -pub type TealTemplateParams = HashMap; - -#[derive(Debug, Clone)] -pub struct CompiledTeal { - pub teal: String, - pub compiled: String, - pub compiled_hash: String, - pub compiled_base64_to_bytes: Vec, - pub source_map: Option, -} - -#[derive(Debug, Clone)] -pub struct CompiledPrograms { - pub approval: CompiledTeal, - pub clear: CompiledTeal, -} - -#[derive(Debug, Clone)] -pub enum AppState { - Uint(UintAppState), - Bytes(BytesAppState), -} - -#[derive(Debug, Clone)] -pub struct UintAppState { - pub key_raw: Vec, - pub key_base64: String, - pub value: u64, -} - -#[derive(Debug, Clone)] -pub struct BytesAppState { - pub key_raw: Vec, - pub key_base64: String, - pub value_raw: Vec, - pub value_base64: String, - pub value: String, -} - -#[derive(Debug, Clone)] -pub struct AppInformation { - /// The app ID - pub app_id: u64, - /// The address of the app account - pub app_address: Address, - /// The approval program as bytecode - pub approval_program: Vec, - /// The clear state program as bytecode - pub clear_state_program: Vec, - /// The creator address of the app - pub creator: String, - /// Number of local state integers allocated - pub local_ints: u32, - /// Number of local state byte slices allocated - pub local_byte_slices: u32, - /// Number of global state integers allocated - pub global_ints: u32, - /// Number of global state byte slices allocated - pub global_byte_slices: u32, - /// Number of extra program pages (if any) - pub extra_program_pages: Option, - /// The current global state of the app - /// Keys are stored as Vec for binary data support, matching TypeScript UInt8Array typing - pub global_state: HashMap, AppState>, -} - -#[derive(Debug, Clone)] -pub struct BoxName { - /// The raw box name as bytes - pub name_raw: Vec, - /// The box name encoded as base64 - pub name_base64: String, - /// The box name as a UTF-8 string (if valid) - pub name: String, -} - -/// Box identifier represented as binary data. -/// Box identifiers in Algorand are arbitrary binary data that can contain -/// non-UTF-8 bytes. They are base64-encoded when sent over HTTP APIs as JSON responses. -pub type BoxIdentifier = Vec; - -pub const UPDATABLE_TEMPLATE_NAME: &str = "TMPL_UPDATABLE"; -pub const DELETABLE_TEMPLATE_NAME: &str = "TMPL_DELETABLE"; - -/// Manages TEAL compilation and app state. -#[derive(Clone)] -pub struct AppManager { - algod_client: Arc, - compilation_results: Arc>>, -} - -impl AppManager { - pub fn new(algod_client: Arc) -> Self { - Self { - algod_client, - compilation_results: Arc::new(Mutex::new(HashMap::new())), - } - } - - /// Create a SHA256 hash of the TEAL code for use as cache key. - /// This optimization reduces memory usage by storing a fixed-size hash - /// instead of the full TEAL code string as the cache key. - fn hash_teal_code(teal_code: &str) -> String { - let mut hasher = Sha256::new(); - hasher.update(teal_code.as_bytes()); - hex::encode(hasher.finalize()) - } - - pub async fn compile_teal(&self, teal_code: &str) -> Result { - let cache_key = Self::hash_teal_code(teal_code); - - // Check cache first - { - let cache = self.compilation_results.lock().unwrap(); - if let Some(cached) = cache.get(&cache_key) { - return Ok(cached.clone()); - } - } - - let compile_response = self - .algod_client - .teal_compile(teal_code.as_bytes().to_vec(), Some(true)) - .await - .map_err(|e| AppManagerError::AlgodClientError { source: e })?; - - let result = CompiledTeal { - teal: teal_code.to_string(), - compiled: Base64.encode(&compile_response.result), - compiled_hash: compile_response.hash.clone(), - compiled_base64_to_bytes: compile_response.result.clone(), - source_map: compile_response.sourcemap, - }; - - // Cache the result - { - let mut cache = self.compilation_results.lock().unwrap(); - cache.insert(cache_key, result.clone()); - } - - Ok(result) - } - - pub async fn compile_teal_template( - &self, - teal_template_code: &str, - template_params: Option<&TealTemplateParams>, - deployment_metadata: Option<&DeploymentMetadata>, - ) -> Result { - let mut teal_code = Self::strip_teal_comments(teal_template_code); - - // When deployment metadata is provided, avoid replacing - // TMPL_UPDATABLE/TMPL_DELETABLE via generic template variables; let the - // deploy-time control function handle them. - if let Some(params) = template_params { - let filtered_params: TealTemplateParams = if let Some(meta) = deployment_metadata { - let mut clone = params.clone(); - // Only strip corresponding template params when an explicit override is provided - if meta.updatable.is_some() { - clone.remove("UPDATABLE"); - clone.remove("TMPL_UPDATABLE"); - } - if meta.deletable.is_some() { - clone.remove("DELETABLE"); - clone.remove("TMPL_DELETABLE"); - } - clone - } else { - params.clone() - }; - teal_code = Self::replace_template_variables(&teal_code, &filtered_params)?; - } - - if let Some(metadata) = deployment_metadata { - teal_code = - Self::replace_teal_template_deploy_time_control_params(&teal_code, metadata)?; - } - - self.compile_teal(&teal_code).await - } - - pub fn get_compilation_result(&self, teal_code: &str) -> Option { - let cache_key = Self::hash_teal_code(teal_code); - let cache = self.compilation_results.lock().unwrap(); - cache.get(&cache_key).cloned() - } - - pub async fn get_by_id(&self, app_id: u64) -> Result { - let app = self - .algod_client - .get_application_by_id(app_id) - .await - .map_err(|e| AppManagerError::AlgodClientError { source: e })?; - - Ok(AppInformation { - app_id, - app_address: Address::from_app_id(&app_id), - approval_program: app.params.approval_program, - clear_state_program: app.params.clear_state_program, - creator: app.params.creator, - local_ints: app - .params - .local_state_schema - .as_ref() - .map(|s| s.num_uint) - .unwrap_or(0), - local_byte_slices: app - .params - .local_state_schema - .as_ref() - .map(|s| s.num_byte_slice) - .unwrap_or(0), - global_ints: app - .params - .global_state_schema - .as_ref() - .map(|s| s.num_uint) - .unwrap_or(0), - global_byte_slices: app - .params - .global_state_schema - .as_ref() - .map(|s| s.num_byte_slice) - .unwrap_or(0), - extra_program_pages: app.params.extra_program_pages, - global_state: Self::decode_app_state(&app.params.global_state.unwrap_or_default())?, - }) - } - - /// Get global state of an application. - /// Returns state keys as Vec for binary data support, matching TypeScript UInt8Array typing. - pub async fn get_global_state( - &self, - app_id: u64, - ) -> Result, AppState>, AppManagerError> { - let app_info = self.get_by_id(app_id).await?; - Ok(app_info.global_state) - } - - /// Get local state for account in an application. - /// Returns state keys as Vec for binary data support, matching TypeScript UInt8Array typing. - pub async fn get_local_state( - &self, - app_id: u64, - address: &str, - ) -> Result, AppState>, AppManagerError> { - let app_info = self - .algod_client - .account_application_information(address, app_id, None) - .await - .map_err(|e| AppManagerError::AlgodClientError { source: e })?; - - let local_state = app_info - .app_local_state - .and_then(|state| state.key_value) - .ok_or(AppManagerError::StateNotFound)?; - - Self::decode_app_state(&local_state) - } - - /// Get names of all boxes for application. - pub async fn get_box_names(&self, app_id: u64) -> Result, AppManagerError> { - let box_result = self - .algod_client - .get_application_boxes(app_id, None) - .await - .map_err(|e| AppManagerError::AlgodClientError { source: e })?; - - let mut box_names = Vec::new(); - for b in box_result.boxes { - let name_raw = b.name; - let name_base64 = Base64.encode(&name_raw); - let name = - String::from_utf8(name_raw.clone()).unwrap_or_else(|_| format!("{:?}", name_raw)); - - box_names.push(BoxName { - name_raw, - name_base64, - name, - }); - } - Ok(box_names) - } - - /// Get value stored in box. - pub async fn get_box_value( - &self, - app_id: u64, - box_name: &BoxIdentifier, - ) -> Result, AppManagerError> { - let (_, name_bytes) = Self::get_box_reference(box_name); - // Algod expects goal-arg style encoding for box name query param in 'encoding:value'. - // However our HTTP client decodes base64 automatically into bytes for the Box model fields. - // The API still requires 'b64:' for the query parameter value. - let name_goal = format!("b64:{}", Base64.encode(&name_bytes)); - - let box_result = self - .algod_client - .get_application_box_by_name(app_id, &name_goal) - .await - .map_err(|e| AppManagerError::AlgodClientError { source: e })?; - - Ok(box_result.value) - } - - /// Get values for multiple boxes. - pub async fn get_box_values( - &self, - app_id: u64, - box_names: &[BoxIdentifier], - ) -> Result>, AppManagerError> { - let mut values = Vec::new(); - for box_name in box_names { - values.push(self.get_box_value(app_id, box_name).await?); - } - Ok(values) - } - - /// Decode box value using ABI type. - /// - /// This method takes an ABIType directly and uses it to decode the box value, - /// returning an ABIValue directly for simpler usage patterns. - /// - /// # Arguments - /// * `app_id` - The app ID - /// * `box_name` - The box name identifier - /// * `abi_type` - The ABI type to use for decoding - /// - /// # Returns - /// An ABIValue containing the decoded box value - pub async fn get_box_value_from_abi_type( - &self, - app_id: u64, - box_name: &BoxIdentifier, - abi_type: &ABIType, - ) -> Result { - let raw_value = self.get_box_value(app_id, box_name).await?; - let decoded_value = - abi_type - .decode(&raw_value) - .map_err(|e| AppManagerError::ABIDecodeError { - message: e.to_string(), - })?; - Ok(decoded_value) - } - - /// Decode multiple box values using ABI type. - /// - /// This method takes an ABIType directly and uses it to decode multiple box values, - /// returning ABIValue objects directly for simpler usage patterns. - /// - /// # Arguments - /// * `app_id` - The app ID - /// * `box_names` - The box name identifiers - /// * `abi_type` - The ABI type to use for decoding - /// - /// # Returns - /// A vector of ABIValue objects containing the decoded box values - pub async fn get_box_values_from_abi_type( - &self, - app_id: u64, - box_names: &[BoxIdentifier], - abi_type: &ABIType, - ) -> Result, AppManagerError> { - let mut values = Vec::new(); - for box_name in box_names { - values.push( - self.get_box_value_from_abi_type(app_id, box_name, abi_type) - .await?, - ); - } - Ok(values) - } - - /// Get ABI return value from transaction confirmation. - pub fn get_abi_return(confirmation_data: &[u8], method: &ABIMethod) -> Option { - if let Some(return_type) = &method.returns { - let return_value = match return_type.decode(confirmation_data) { - Ok(value) => ABIReturn { - method: method.clone(), - raw_return_value: confirmation_data.to_vec(), - return_value: Some(value), - decode_error: None, - }, - Err(e) => ABIReturn { - method: method.clone(), - raw_return_value: confirmation_data.to_vec(), - return_value: None, - decode_error: Some(e), - }, - }; - - Some(return_value) - } else { - None - } - } - - /// Get box reference from identifier. - pub fn get_box_reference(box_id: &BoxIdentifier) -> (u64, Vec) { - (0, box_id.clone()) - } - - /// Helper function to ensure bytes are decoded from base64 if needed. - /// When using `Bytes` deserializer with JSON, base64 strings are not decoded - /// but kept as ASCII bytes of the base64 string. This function detects and fixes that. - fn ensure_decoded_bytes(bytes: &[u8]) -> Vec { - // Check if bytes could be a base64 string - if let Ok(s) = std::str::from_utf8(bytes) { - // Base64 strings have specific characteristics: - // - Length is multiple of 4 (with padding) or would be after padding - // - Contains base64 chars - // - Successfully decodes to different bytes than the original - if !s.is_empty() - && s.chars() - .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=') - { - // Additional check: base64 strings typically have = padding or specific length - let looks_like_base64 = s.contains('=') - || s.contains('+') - || s.contains('/') - || (s.len() % 4 == 0 && s.len() >= 8); // Reasonable minimum for base64 - - if looks_like_base64 { - // Try to decode as base64 - if let Ok(decoded) = Base64.decode(s) { - // Only return decoded if it's actually different - // (prevents treating plain text as base64) - if decoded != bytes { - return decoded; - } - } - } - } - } - // Already raw bytes or not valid base64 - bytes.to_vec() - } - - /// Decode application state from raw format. - /// Keys are decoded from base64 to Vec for binary data support, matching TypeScript UInt8Array typing. - pub fn decode_app_state( - state: &[TealKeyValue], - ) -> Result, AppState>, AppManagerError> { - let mut state_values = HashMap::new(); - - for state_val in state { - let key_raw = - Base64 - .decode(&state_val.key) - .map_err(|e| AppManagerError::DecodingError { - message: e.to_string(), - })?; - - // TODO(stabilization): Consider r#type pattern consistency across API vs ABI types (PR #229 comment) - let app_state = match state_val.value.r#type { - 1 => { - // Handle both cases: raw bytes (from msgpack) or base64 string bytes (from JSON with Bytes deserializer) - let value_raw = Self::ensure_decoded_bytes(&state_val.value.bytes); - let value_base64 = Base64.encode(&value_raw); - let value_str = String::from_utf8(value_raw.clone()) - .unwrap_or_else(|_| hex::encode(&value_raw)); - AppState::Bytes(BytesAppState { - key_raw: key_raw.clone(), - key_base64: Base64.encode(&key_raw), - value_raw, - value_base64, - value: value_str, - }) - } - 2 => AppState::Uint(UintAppState { - key_raw: key_raw.clone(), - key_base64: Base64.encode(&key_raw), - value: state_val.value.uint, - }), - _ => { - return Err(AppManagerError::DecodingError { - message: format!("Unknown state data type: {}", state_val.value.r#type), - }); - } - }; - - state_values.insert(key_raw.clone(), app_state); - } - - Ok(state_values) - } - - /// Replace template variables in TEAL code. - pub fn replace_template_variables( - program: &str, - template_values: &TealTemplateParams, - ) -> Result { - let mut program_lines: Vec = program.lines().map(|line| line.to_string()).collect(); - - for (template_variable_name, template_value) in template_values { - let token = if template_variable_name.starts_with("TMPL_") { - template_variable_name.clone() - } else { - format!("TMPL_{}", template_variable_name) - }; - - let value = match template_value { - TealTemplateValue::Int(i) => i.to_string(), - TealTemplateValue::String(s) => { - if s.parse::().is_ok() { - s.clone() - } else { - format!("0x{}", hex::encode(s.as_bytes())) - } - } - TealTemplateValue::Bytes(b) => format!("0x{}", hex::encode(b)), - }; - - program_lines = Self::replace_template_variable(&program_lines, &token, &value); - } - - Ok(program_lines.join("\n")) - } - - /// Replace template variable with proper boundary checking. - fn replace_template_variable( - program_lines: &[String], - token: &str, - replacement: &str, - ) -> Vec { - let mut result = Vec::new(); - let token_index_offset = replacement.len() as i32 - token.len() as i32; - - for line in program_lines { - let comment_index = Self::find_unquoted_string(line, "//").unwrap_or(line.len()); - let mut code = line[..comment_index].to_string(); - let comment = &line[comment_index..]; - let mut trailing_index = 0; - - while let Some(token_index) = Self::find_template_token(&code, token, trailing_index) { - trailing_index = token_index + token.len(); - let prefix = &code[..token_index]; - let suffix = &code[trailing_index..]; - code = format!("{}{}{}", prefix, replacement, suffix); - trailing_index = ((trailing_index as i32) + token_index_offset).max(0) as usize; - } - - result.push(format!("{}{}", code, comment)); - } - - result - } - - /// Find template token with boundary checking. - fn find_template_token(line: &str, token: &str, start_index: usize) -> Option { - let end_index = line.len(); - let mut index = start_index; - - while index < end_index { - if let Some(token_index) = Self::find_unquoted_string(&line[index..], token) { - let actual_token_index = index + token_index; - let trailing_index = actual_token_index + token.len(); - - // Check boundaries - ensure it's a whole token - let valid_start = actual_token_index == 0 - || !Self::is_valid_token_character( - line.chars() - .nth(actual_token_index.saturating_sub(1)) - .unwrap_or(' '), - ); - let valid_end = trailing_index >= line.len() - || !Self::is_valid_token_character( - line.chars().nth(trailing_index).unwrap_or(' '), - ); - - if valid_start && valid_end { - return Some(actual_token_index); - } - index = trailing_index; - } else { - break; - } - } - None - } - - /// Check if character is valid for token. - fn is_valid_token_character(ch: char) -> bool { - ch.is_alphanumeric() || ch == '_' - } - - /// Replace deploy-time control parameters. - pub fn replace_teal_template_deploy_time_control_params( - teal_template_code: &str, - params: &DeploymentMetadata, - ) -> Result { - let mut result = teal_template_code.to_string(); - - if let Some(updatable) = params.updatable { - if !teal_template_code.contains(UPDATABLE_TEMPLATE_NAME) { - return Err(AppManagerError::TemplateVariableNotFound { - message: format!( - "Deploy-time updatability control requested, but {} not present in TEAL code", - UPDATABLE_TEMPLATE_NAME - ), - }); - } - result = result.replace(UPDATABLE_TEMPLATE_NAME, &(updatable as u8).to_string()); - } - - if let Some(deletable) = params.deletable { - if !teal_template_code.contains(DELETABLE_TEMPLATE_NAME) { - return Err(AppManagerError::TemplateVariableNotFound { - message: format!( - "Deploy-time deletability control requested, but {} not present in TEAL code", - DELETABLE_TEMPLATE_NAME - ), - }); - } - result = result.replace(DELETABLE_TEMPLATE_NAME, &(deletable as u8).to_string()); - } - - Ok(result) - } - - /// Strip comments from TEAL code. - pub fn strip_teal_comments(teal_code: &str) -> String { - teal_code - .lines() - .map(|line| { - if let Some(comment_pos) = Self::find_unquoted_string(line, "//") { - line[..comment_pos].trim_end() - } else { - line - } - }) - .collect::>() - .join("\n") - } - - /// Find unquoted string in TEAL line. - fn find_unquoted_string(line: &str, token: &str) -> Option { - let mut in_quotes = false; - let mut in_base64 = false; - let chars: Vec = line.chars().collect(); - let mut i = 0; - - while i < chars.len() { - match chars[i] { - ' ' | '(' if !in_quotes && Self::last_token_base64(line, i) => { - in_base64 = true; - } - ' ' | ')' if !in_quotes && in_base64 => { - in_base64 = false; - } - '\\' if in_quotes => { - // Skip next character - i += 1; - } - '"' => { - in_quotes = !in_quotes; - } - _ if !in_quotes && !in_base64 => { - if i + token.len() <= line.len() && &line[i..i + token.len()] == token { - return Some(i); - } - } - _ => {} - } - i += 1; - } - None - } - - /// Check if last token is base64. - fn last_token_base64(line: &str, index: usize) -> bool { - if let Some(last_token) = line[..index].split_whitespace().last() { - matches!(last_token, "base64" | "b64") - } else { - false - } - } -} - -/// Errors that can occur during app manager operations. -#[derive(Debug, Snafu)] -pub enum AppManagerError { - #[snafu(display("Algod client error: {source}"))] - AlgodClientError { source: AlgodError }, - - #[snafu(display("Template variable not found: {message}"))] - TemplateVariableNotFound { message: String }, - - #[snafu(display("Decoding error: {message}"))] - DecodingError { message: String }, - - #[snafu(display("State not found"))] - StateNotFound, - - #[snafu(display("ABI decode error: {message}"))] - ABIDecodeError { message: String }, -} diff --git a/crates/algokit_utils/src/clients/asset_manager.rs b/crates/algokit_utils/src/clients/asset_manager.rs deleted file mode 100644 index bbd713d6f..000000000 --- a/crates/algokit_utils/src/clients/asset_manager.rs +++ /dev/null @@ -1,470 +0,0 @@ -use algod_client::apis::{ - AlgodApiError, AlgodClient, Error as AlgodError, - account_asset_information::AccountAssetInformationError, get_asset_by_id::GetAssetByIdError, -}; -use algod_client::models::{AccountAssetInformation as AlgodAccountAssetInformation, Asset}; -use algokit_http_client::HttpError; -use algokit_transact::{Address, constants::MAX_TX_GROUP_SIZE}; -use snafu::Snafu; -use std::{ - collections::{HashMap, HashSet}, - str::FromStr, - sync::Arc, -}; - -use crate::transactions::{ - AssetOptInParams, AssetOptOutParams, ComposerError, TransactionComposer, - TransactionComposerConfig, -}; - -#[derive(Debug, Clone)] -pub struct BulkAssetOptInOutResult { - pub asset_id: u64, - pub transaction_id: String, -} - -/// Information about an Algorand Standard Asset (ASA). -/// -/// This type provides a flattened, developer-friendly interface to asset information. -#[derive(Debug, Clone)] -pub struct AssetInformation { - /// The ID of the asset. - pub asset_id: u64, - - /// The address of the account that created the 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 when - /// closing out an asset position and opting-out of the asset. - pub creator: String, - - /// The total amount of the smallest divisible (decimal) units that were created of the asset. - /// - /// For example, if `decimals` is, say, 2, then for every 100 `total` there is 1 whole unit. - pub total: u64, - - /// The amount of decimal places the asset was created with. - /// - /// * 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; - /// * If 3, the base unit of the asset is in thousandths; - /// * and so on up to 19 decimal places. - pub decimals: u32, - - /// Whether the asset was frozen by default for all accounts. - /// - /// If `true` then for anyone apart from the creator to hold the - /// asset it needs to be unfrozen per account using an asset freeze - /// transaction from the `freeze` account. - pub default_frozen: Option, - - /// The address of the optional account that can manage the configuration of the asset and destroy it. - /// - /// If not set the asset is permanently immutable. - pub manager: Option, - - /// The address of the optional account that holds the reserve (uncirculated supply) units of the asset. - /// - /// This address has no specific authority in the protocol itself and is informational only. - /// - /// Some standards like [ARC-19](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0019.md) - /// rely on this field to hold meaningful data. - /// - /// It can be used in the case where you want to signal to holders of your asset that the uncirculated units - /// of the asset reside in an account that is different from the default creator account. - /// - /// If not set the field is permanently empty. - pub reserve: Option, - - /// The address of the optional account that can be used to freeze or unfreeze holdings of this asset for any account. - /// - /// If empty, freezing is not permitted. - /// - /// If not set the field is permanently empty. - pub freeze: Option, - - /// The address of the optional account that can clawback holdings of this asset from any account. - /// - /// The clawback account has the ability to **unconditionally take assets from any account**. - /// - /// If empty, clawback is not permitted. - /// - /// If not set the field is permanently empty. - pub clawback: Option, - - /// The optional name of the unit of this asset (e.g. ticker name). - /// - /// Max size is 8 bytes. - pub unit_name: Option, - - /// The optional name of the unit of this asset as bytes. - /// - /// Max size is 8 bytes. - pub unit_name_bytes: Option>, - - /// The optional name of the asset. - /// - /// Max size is 32 bytes. - pub asset_name: Option, - - /// The optional name of the asset as bytes. - /// - /// Max size is 32 bytes. - pub asset_name_bytes: Option>, - - /// Optional URL where more information about the asset can be retrieved (e.g. metadata). - /// - /// Max size is 96 bytes. - pub url: Option, - - /// Optional URL where more information about the asset can be retrieved as bytes. - /// - /// Max size is 96 bytes. - pub url_bytes: Option>, - - /// 32-byte hash of some metadata that is relevant to the asset and/or asset holders. - /// - /// The format of this metadata is up to the application. - pub metadata_hash: Option>, -} - -impl From for AssetInformation { - fn from(asset: Asset) -> Self { - Self { - asset_id: asset.index, - creator: asset.params.creator, - total: asset.params.total, - decimals: asset.params.decimals as u32, - default_frozen: asset.params.default_frozen, - manager: asset.params.manager, - reserve: asset.params.reserve, - freeze: asset.params.freeze, - clawback: asset.params.clawback, - unit_name: asset.params.unit_name, - unit_name_bytes: asset.params.unit_name_b64, - asset_name: asset.params.name, - asset_name_bytes: asset.params.name_b64, - url: asset.params.url, - url_bytes: asset.params.url_b64, - metadata_hash: asset.params.metadata_hash, - } - } -} - -#[derive(Debug, Clone)] -pub struct AssetValidationError { - pub asset_id: u64, - pub error: String, -} - -/// Manages Algorand Standard Assets. -#[derive(Clone)] -pub struct AssetManager { - algod_client: Arc, - new_composer: Arc) -> TransactionComposer>, -} - -impl AssetManager { - pub fn new( - algod_client: Arc, - new_composer: impl Fn(Option) -> TransactionComposer + 'static, - ) -> Self { - Self { - algod_client, - new_composer: Arc::new(new_composer), - } - } - - /// Get asset information by asset ID - /// Returns a convenient, flattened view of the asset information. - pub async fn get_by_id(&self, asset_id: u64) -> Result { - let asset = self - .algod_client - .get_asset_by_id(asset_id) - .await - .map_err(|error| { - map_get_asset_by_id_error(&error, asset_id) - .unwrap_or(AssetManagerError::AlgodClientError { source: error }) - })?; - - Ok(asset.into()) - } - - /// Get account's asset information. - /// Returns the raw algod AccountAssetInformation type. - /// Access asset holding via `account_info.asset_holding` and asset params via `account_info.asset_params`. - pub async fn get_account_information( - &self, - sender: &Address, - asset_id: u64, - ) -> Result { - let sender_str = sender.to_string(); - self.algod_client - .account_asset_information(sender_str.as_str(), asset_id) - .await - .map_err(|error| { - map_account_asset_information_error(&error, sender_str.as_str(), asset_id) - .unwrap_or(AssetManagerError::AlgodClientError { source: error }) - }) - } - - pub async fn bulk_opt_in( - &self, - account: &Address, - asset_ids: &[u64], - ) -> Result, AssetManagerError> { - if asset_ids.is_empty() { - return Ok(Vec::new()); - } - - // Ignore duplicate asset IDs while preserving input order - let mut seen: HashSet = HashSet::with_capacity(asset_ids.len()); - let unique_ids: Vec = asset_ids - .iter() - .copied() - .filter(|id| seen.insert(*id)) - .collect(); - - let mut bulk_results = Vec::with_capacity(unique_ids.len()); - - for asset_chunk in unique_ids.chunks(MAX_TX_GROUP_SIZE) { - let mut composer = (self.new_composer)(None); - - for &asset_id in asset_chunk { - let opt_in_params = AssetOptInParams { - sender: account.clone(), - asset_id, - ..Default::default() - }; - - composer - .add_asset_opt_in(opt_in_params) - .map_err(|e| AssetManagerError::ComposerError { source: e })?; - } - - let composer_result = composer - .send(Default::default()) - .await - .map_err(|e| AssetManagerError::ComposerError { source: e })?; - - bulk_results.extend(asset_chunk.iter().zip(composer_result.results.iter()).map( - |(&asset_id, result)| BulkAssetOptInOutResult { - asset_id, - transaction_id: result.transaction_id.clone(), - }, - )); - } - - Ok(bulk_results) - } - - pub async fn bulk_opt_out( - &self, - account: &Address, - asset_ids: &[u64], - ensure_zero_balance: Option, - ) -> Result, AssetManagerError> { - if asset_ids.is_empty() { - return Ok(Vec::new()); - } - - // Ignore duplicate asset IDs while preserving input order - let mut seen: HashSet = HashSet::with_capacity(asset_ids.len()); - let unique_ids: Vec = asset_ids - .iter() - .copied() - .filter(|id| seen.insert(*id)) - .collect(); - - let should_check_balance = ensure_zero_balance.unwrap_or(false); - - // If we need to check balances, verify they are all zero - if should_check_balance { - for &asset_id in unique_ids.iter() { - let account_info = self.get_account_information(account, asset_id).await?; - let balance = account_info - .asset_holding - .as_ref() - .map(|h| h.amount) - .unwrap_or(0); - if balance > 0 { - return Err(AssetManagerError::NonZeroBalance { - address: account.to_string(), - asset_id, - balance, - }); - } - } - } - - // Fetch asset information to get creators - let mut asset_creators = HashMap::with_capacity(unique_ids.len()); - for &asset_id in unique_ids.iter() { - let asset_info = self.get_by_id(asset_id).await?; - let creator = Address::from_str(&asset_info.creator) - .map_err(|_| AssetManagerError::AssetNotFound { asset_id })?; - asset_creators.insert(asset_id, creator); - } - - let asset_creator_pairs: Vec<(u64, Address)> = unique_ids - .iter() - .map(|&asset_id| { - let creator = asset_creators - .remove(&asset_id) - .expect("Creator information should be available for all asset IDs"); - (asset_id, creator) - }) - .collect(); - - let mut bulk_results = Vec::with_capacity(asset_creator_pairs.len()); - - for asset_chunk in asset_creator_pairs.chunks(MAX_TX_GROUP_SIZE) { - let mut composer = (self.new_composer)(None); - - for (asset_id, creator) in asset_chunk.iter() { - let opt_out_params = AssetOptOutParams { - sender: account.clone(), - asset_id: *asset_id, - close_remainder_to: Some(creator.clone()), - ..Default::default() - }; - - composer - .add_asset_opt_out(opt_out_params) - .map_err(|e| AssetManagerError::ComposerError { source: e })?; - } - - let composer_result = composer - .send(Default::default()) - .await - .map_err(|e| AssetManagerError::ComposerError { source: e })?; - - bulk_results.extend(asset_chunk.iter().zip(composer_result.results.iter()).map( - |((asset_id, _), result)| BulkAssetOptInOutResult { - asset_id: *asset_id, - transaction_id: result.transaction_id.clone(), - }, - )); - } - - Ok(bulk_results) - } -} - -fn map_get_asset_by_id_error(error: &AlgodError, asset_id: u64) -> Option { - match error { - AlgodError::Api { - source: - AlgodApiError::GetAssetById { - error: GetAssetByIdError::Status404(_), - }, - } => Some(AssetManagerError::AssetNotFound { asset_id }), - AlgodError::Api { .. } => None, - AlgodError::Http { source } => { - // Prefer structured status when available, fallback to message matching for older clients - match source { - HttpError::StatusError { status, .. } if *status == 404 => { - Some(AssetManagerError::AssetNotFound { asset_id }) - } - _ => http_error_message(source).and_then(|message| { - if message.contains("status 404") { - Some(AssetManagerError::AssetNotFound { asset_id }) - } else { - None - } - }), - } - } - _ => None, - } -} - -fn map_account_asset_information_error( - error: &AlgodError, - address: &str, - asset_id: u64, -) -> Option { - match error { - AlgodError::Api { - source: - AlgodApiError::AccountAssetInformation { - error: AccountAssetInformationError::Status400(_), - }, - } => Some(AssetManagerError::AccountNotFound { - address: address.to_string(), - }), - AlgodError::Api { .. } => None, - AlgodError::Http { source } => { - // Prefer structured status when available, fallback to message matching for older clients - match source { - HttpError::StatusError { status, .. } if *status == 404 => { - Some(AssetManagerError::NotOptedIn { - address: address.to_string(), - asset_id, - }) - } - HttpError::StatusError { status, .. } if *status == 400 => { - Some(AssetManagerError::AccountNotFound { - address: address.to_string(), - }) - } - _ => http_error_message(source).and_then(|message| { - if message.contains("status 404") { - Some(AssetManagerError::NotOptedIn { - address: address.to_string(), - asset_id, - }) - } else if message.contains("status 400") - || message.to_ascii_lowercase().contains("account not found") - { - Some(AssetManagerError::AccountNotFound { - address: address.to_string(), - }) - } else { - None - } - }), - } - } - _ => None, - } -} - -fn http_error_message(error: &HttpError) -> Option<&str> { - match error { - HttpError::RequestError { message } => Some(message.as_str()), - HttpError::StatusError { message, .. } => Some(message.as_str()), - } -} - -#[derive(Debug, Snafu)] -pub enum AssetManagerError { - #[snafu(display("Algod client error: {source}"))] - AlgodClientError { source: AlgodError }, - - #[snafu(display("Composer error: {source}"))] - ComposerError { source: ComposerError }, - - #[snafu(display("Asset not found: {asset_id}"))] - AssetNotFound { asset_id: u64 }, - - #[snafu(display("Account not found: {address}"))] - AccountNotFound { address: String }, - - #[snafu(display("Account {address} is not opted into asset {asset_id}"))] - NotOptedIn { address: String, asset_id: u64 }, - - #[snafu(display("Account {address} has non-zero balance {balance} for asset {asset_id}"))] - NonZeroBalance { - address: String, - asset_id: u64, - balance: u64, - }, - - #[snafu(display("Asset {asset_id} is frozen for account {address}"))] - AssetFrozen { address: String, asset_id: u64 }, - - #[snafu(display("Method '{method}' not implemented: {reason}"))] - NotImplemented { method: String, reason: String }, -} diff --git a/crates/algokit_utils/src/clients/client_manager.rs b/crates/algokit_utils/src/clients/client_manager.rs deleted file mode 100644 index 0154b39ce..000000000 --- a/crates/algokit_utils/src/clients/client_manager.rs +++ /dev/null @@ -1,656 +0,0 @@ -use crate::AlgorandClient; -use crate::applications::app_client::{ - AppClient, AppClientError, AppClientParams, AppSourceMaps, CompilationParams, -}; -use crate::applications::app_factory::{AppFactory, AppFactoryParams}; -use crate::clients::network_client::{ - AlgoClientConfig, AlgoConfig, AlgorandService, NetworkDetails, TokenHeader, - genesis_id_is_localnet, -}; -use crate::transactions::{TransactionComposerConfig, TransactionSigner}; -use algod_client::{AlgodClient, apis::Error as AlgodError}; -use algokit_abi::Arc56Contract; -use algokit_http_client::DefaultHttpClient; -use base64::{Engine, engine::general_purpose}; -use indexer_client::IndexerClient; -use kmd_client::KmdClient; -use snafu::Snafu; -use std::{env, sync::Arc}; -use tokio::sync::RwLock; - -#[derive(Debug, Snafu)] -pub enum ClientManagerError { - #[snafu(display("Environment Error: {message}"))] - EnvironmentError { message: String }, - - #[snafu(display("Http Client Error: {message}"))] - HttpClientError { message: String }, - - #[snafu(display("Indexer Error: {message}"))] - IndexerError { message: String }, - - #[snafu(display("KMD Error: {message}"))] - KmdError { message: String }, - - #[snafu(display("Algod client error: {source}"))] - AlgodClientError { source: AlgodError }, -} - -impl From for ClientManagerError { - fn from(e: AlgodError) -> Self { - Self::AlgodClientError { source: e } - } -} - -pub struct ClientManager { - algod: Arc, - indexer: Option>, - kmd: Option>, - cached_network_details: RwLock>>, -} - -impl ClientManager { - pub fn new(config: &AlgoConfig) -> Result { - Ok(Self { - algod: Arc::new(Self::get_algod_client(&config.algod_config)?), - indexer: match config.indexer_config.as_ref() { - Some(indexer_config) => Some(Arc::new(Self::get_indexer_client(indexer_config)?)), - None => None, - }, - kmd: match config.kmd_config.as_ref() { - Some(kmd_config) => Some(Arc::new(Self::get_kmd_client(kmd_config)?)), - None => None, - }, - cached_network_details: RwLock::new(None), - }) - } - - pub fn algod(&self) -> Arc { - Arc::clone(&self.algod) - } - - pub fn indexer(&self) -> Result, ClientManagerError> { - self.indexer - .as_ref() - .map(Arc::clone) - .ok_or(ClientManagerError::IndexerError { - message: "Indexer client not configured".to_string(), - }) - } - - pub fn indexer_if_present(&self) -> Option> { - self.indexer.as_ref().map(Arc::clone) - } - - pub fn kmd(&self) -> Result, ClientManagerError> { - self.kmd - .as_ref() - .map(Arc::clone) - .ok_or(ClientManagerError::KmdError { - message: "KMD client not configured".to_string(), - }) - } - - pub fn kmd_if_present(&self) -> Option> { - self.kmd.as_ref().map(Arc::clone) - } - - pub async fn network(&self) -> Result, ClientManagerError> { - // Fast path: multiple readers can access concurrently - { - let cached = self.cached_network_details.read().await; - if let Some(ref details) = *cached { - return Ok(Arc::clone(details)); - } - } - - // Slow path: exclusive write access for initialization - let mut cached = self.cached_network_details.write().await; - - // Double-check: someone else might have initialized while we waited for write lock - if let Some(ref details) = *cached { - return Ok(Arc::clone(details)); - } - - // Initialize - errors are NOT cached, allowing retries for transient failures - let params = self.algod().transaction_params().await?; - let network_details = Arc::new(NetworkDetails::new( - params.genesis_id.clone(), - general_purpose::STANDARD.encode(¶ms.genesis_hash), - )); - - // Cache only on success - *cached = Some(Arc::clone(&network_details)); - Ok(network_details) - } - - pub fn genesis_id_is_localnet(genesis_id: &str) -> bool { - genesis_id_is_localnet(genesis_id) - } - - pub async fn is_localnet(&self) -> Result { - Ok(self.network().await?.is_localnet) - } - - pub async fn is_testnet(&self) -> Result { - Ok(self.network().await?.is_testnet) - } - - pub async fn is_mainnet(&self) -> Result { - Ok(self.network().await?.is_mainnet) - } - - pub fn get_config_from_environment_or_localnet() -> AlgoConfig { - match Self::get_algod_config_from_environment() { - Ok(algod_config) => { - let kmd_config = if !algod_config.server.contains("mainnet") - && !algod_config.server.contains("testnet") - { - Some(AlgoClientConfig { - port: env::var("KMD_PORT") - .ok() - .and_then(|p| p.parse().ok()) - .or(Some(4002)), - ..algod_config.clone() - }) - } else { - None - }; - - AlgoConfig { - algod_config, - indexer_config: match Self::get_indexer_config_from_environment() { - Ok(indexer_config) => Some(indexer_config), - Err(_) => None, - }, - kmd_config, - } - } - Err(_) => AlgoConfig { - algod_config: Self::get_default_localnet_config(AlgorandService::Algod), - indexer_config: Some(Self::get_default_localnet_config(AlgorandService::Indexer)), - kmd_config: Some(Self::get_default_localnet_config(AlgorandService::Kmd)), - }, - } - } - - pub fn get_indexer_config_from_environment() -> Result { - let server = - env::var("INDEXER_SERVER").map_err(|_| ClientManagerError::EnvironmentError { - message: String::from("INDEXER_SERVER environment variable not found"), - })?; - let port = env::var("INDEXER_PORT").ok().and_then(|p| p.parse().ok()); - let token = env::var("INDEXER_TOKEN").ok().map(TokenHeader::String); - - Ok(AlgoClientConfig { - server, - port, - token, - }) - } - - pub fn get_algod_config_from_environment() -> Result { - let server = - env::var("ALGOD_SERVER").map_err(|_| ClientManagerError::EnvironmentError { - message: String::from("ALGOD_SERVER environment variable not found"), - })?; - let port = env::var("ALGOD_PORT").ok().and_then(|p| p.parse().ok()); - let token = env::var("ALGOD_TOKEN").ok().map(TokenHeader::String); - - Ok(AlgoClientConfig { - server, - port, - token, - }) - } - - pub fn get_kmd_config_from_environment() -> Result { - let server = env::var("KMD_SERVER") - .or_else(|_| env::var("ALGOD_SERVER")) - .map_err(|_| ClientManagerError::EnvironmentError { - message: String::from("KMD_SERVER environment variable not found"), - })?; - - let port = env::var("KMD_PORT") - .ok() - .and_then(|p| p.parse().ok()) - .or_else(|| env::var("ALGOD_PORT").ok().and_then(|p| p.parse().ok())) - .or(Some(4002)); - - let token = env::var("KMD_TOKEN") - .ok() - .map(TokenHeader::String) - .or_else(|| env::var("ALGOD_TOKEN").ok().map(TokenHeader::String)); - - Ok(AlgoClientConfig { - server, - port, - token, - }) - } - - pub fn get_algonode_config(network: &str, service: AlgorandService) -> AlgoClientConfig { - let subdomain = match service { - AlgorandService::Algod => "api", - AlgorandService::Indexer => "idx", - AlgorandService::Kmd => panic!("KMD is not available on algonode"), - }; - - AlgoClientConfig { - server: format!("https://{}-{}.4160.nodely.dev", network, subdomain), - port: Some(443), - token: None, - } - } - - pub fn get_default_localnet_config(service: AlgorandService) -> AlgoClientConfig { - let port = match service { - AlgorandService::Algod => 4001, - AlgorandService::Indexer => 8980, - AlgorandService::Kmd => 4002, - }; - - AlgoClientConfig { - server: "http://localhost".to_string(), - port: Some(port), - token: Some(TokenHeader::String( - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), - )), - } - } - - pub fn get_algod_client(config: &AlgoClientConfig) -> Result { - let base_url = if let Some(port) = config.port { - format!("{}:{}", config.server, port) - } else { - config.server.clone() - }; - - let http_client = match &config.token { - Some(TokenHeader::String(token)) => Arc::new( - DefaultHttpClient::with_header(&base_url, "X-Algo-API-Token", token).map_err( - |e| ClientManagerError::HttpClientError { - message: format!("Failed to create HTTP client with token header: {:?}", e), - }, - )?, - ), - Some(TokenHeader::Headers(headers)) => { - let (header_name, header_value) = headers - .iter() - .next() - .map(|(k, v)| (k.as_str(), v.as_str())) - .unwrap_or(("X-Algo-API-Token", "")); - Arc::new( - DefaultHttpClient::with_header(&base_url, header_name, header_value).map_err( - |e| ClientManagerError::HttpClientError { - message: format!( - "Failed to create HTTP client with custom header: {:?}", - e - ), - }, - )?, - ) - } - None => Arc::new(DefaultHttpClient::new(&base_url)), - }; - - Ok(AlgodClient::new(http_client)) - } - - pub fn get_algod_client_from_environment() -> Result { - let config = Self::get_algod_config_from_environment()?; - Self::get_algod_client(&config) - } - - pub fn get_indexer_client( - config: &AlgoClientConfig, - ) -> Result { - let base_url = if let Some(port) = config.port { - format!("{}:{}", config.server, port) - } else { - config.server.clone() - }; - - let http_client = match &config.token { - Some(TokenHeader::String(token)) => Arc::new( - DefaultHttpClient::with_header(&base_url, "X-Indexer-API-Token", token).map_err( - |e| ClientManagerError::HttpClientError { - message: format!("Failed to create HTTP client with token header: {:?}", e), - }, - )?, - ), - Some(TokenHeader::Headers(headers)) => { - let (header_name, header_value) = headers - .iter() - .next() - .map(|(k, v)| (k.as_str(), v.as_str())) - .unwrap_or(("X-Indexer-API-Token", "")); - Arc::new( - DefaultHttpClient::with_header(&base_url, header_name, header_value).map_err( - |e| ClientManagerError::HttpClientError { - message: format!( - "Failed to create HTTP client with custom header: {:?}", - e - ), - }, - )?, - ) - } - None => Arc::new(DefaultHttpClient::new(&base_url)), - }; - - Ok(IndexerClient::new(http_client)) - } - - pub fn get_indexer_client_from_environment() -> Result { - let config = Self::get_indexer_config_from_environment()?; - Self::get_indexer_client(&config) - } - - pub fn get_kmd_client(config: &AlgoClientConfig) -> Result { - let base_url = if let Some(port) = config.port { - format!("{}:{}", config.server, port) - } else { - config.server.clone() - }; - - let token_value = match &config.token { - Some(TokenHeader::String(token)) => token.clone(), - Some(TokenHeader::Headers(headers)) => { - headers.values().next().cloned().unwrap_or_default() - } - None => String::new(), - }; - - let http_client = if token_value.is_empty() { - Arc::new(DefaultHttpClient::new(&base_url)) - } else { - Arc::new( - DefaultHttpClient::with_header(&base_url, "X-KMD-API-Token", &token_value) - .map_err(|e| ClientManagerError::HttpClientError { - message: format!( - "Failed to create HTTP client with KMD token header: {:?}", - e - ), - })?, - ) - }; - - Ok(KmdClient::new(http_client)) - } - - pub fn get_kmd_client_from_environment() -> Result { - let config = Self::get_kmd_config_from_environment()?; - Self::get_kmd_client(&config) - } - - #[allow(clippy::too_many_arguments)] - pub fn get_app_factory( - &self, - algorand: Arc, - app_spec: Arc56Contract, - app_name: Option, - default_sender: Option, - default_signer: Option>, - version: Option, - compilation_params: Option, - source_maps: Option, - transaction_composer_config: Option, - ) -> AppFactory { - AppFactory::new(AppFactoryParams { - algorand, - app_spec, - app_name, - default_sender, - default_signer, - version, - compilation_params, - source_maps, - transaction_composer_config, - }) - } - - /// Returns an AppClient resolved by creator address and name using indexer lookup. - #[allow(clippy::too_many_arguments)] - pub async fn get_app_client_by_creator_and_name( - &self, - algorand: Arc, - creator_address: &str, - app_name: &str, - app_spec: Arc56Contract, - default_sender: Option, - default_signer: Option>, - source_maps: Option, - ignore_cache: Option, - transaction_composer_config: Option, - ) -> Result { - AppClient::from_creator_and_name( - creator_address, - app_name, - app_spec, - algorand, - default_sender, - default_signer, - source_maps, - ignore_cache, - transaction_composer_config, - ) - .await - } - - /// Returns an AppClient for an existing application by ID. - #[allow(clippy::too_many_arguments)] - pub fn get_app_client_by_id( - &self, - algorand: Arc, - app_spec: Arc56Contract, - app_id: u64, - app_name: Option, - default_sender: Option, - default_signer: Option>, - source_maps: Option, - transaction_composer_config: Option, - ) -> AppClient { - AppClient::new(AppClientParams { - app_id, - app_spec, - algorand, - app_name, - default_sender, - default_signer, - source_maps, - transaction_composer_config, - }) - } - - /// Returns an AppClient resolved by network using app spec networks mapping. - #[allow(clippy::too_many_arguments)] - pub async fn get_app_client_by_network( - &self, - algorand: Arc, - app_spec: Arc56Contract, - app_name: Option, - default_sender: Option, - default_signer: Option>, - source_maps: Option, - transaction_composer_config: Option, - ) -> Result { - AppClient::from_network( - app_spec, - algorand, - app_name, - default_sender, - default_signer, - source_maps, - transaction_composer_config, - ) - .await - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::clients::network_client::AlgorandService; - use rstest::rstest; - - #[test] - fn test_cache_initially_empty() { - let config = AlgoConfig { - algod_config: AlgoClientConfig { - server: "http://localhost:4001".to_string(), - port: None, - token: None, - }, - indexer_config: Some(AlgoClientConfig { - server: "http://localhost:8980".to_string(), - port: None, - token: None, - }), - kmd_config: None, - }; - let manager = ClientManager::new(&config).unwrap(); - - // Cache should be initially empty - let cache = manager.cached_network_details.try_read().unwrap(); - assert!(cache.is_none()); - } - - #[tokio::test] - async fn test_error_not_cached() { - let config = AlgoConfig { - algod_config: AlgoClientConfig { - server: "http://invalid-host:65534".to_string(), - port: Some(65534), - token: None, - }, - indexer_config: Some(AlgoClientConfig { - server: "http://invalid-host:65535".to_string(), - port: Some(65535), - token: None, - }), - kmd_config: None, - }; - let manager = ClientManager::new(&config).unwrap(); - - // Both calls should fail - assert!(manager.network().await.is_err()); - assert!(manager.network().await.is_err()); - - // Cache should remain empty after errors - let cache = manager.cached_network_details.read().await; - assert!(cache.is_none()); - } - - #[test] - fn test_client_config_builder() { - let config = AlgoClientConfig { - server: "http://localhost".to_string(), - port: Some(4001), - token: Some(TokenHeader::String("test-token".to_string())), - }; - - assert_eq!(config.server, "http://localhost"); - assert_eq!(config.port, Some(4001)); - assert!(matches!(config.token, Some(TokenHeader::String(_)))); - } - - #[rstest] - #[case( - "mainnet", - AlgorandService::Algod, - "https://mainnet-api.4160.nodely.dev" - )] - #[case( - "mainnet", - AlgorandService::Indexer, - "https://mainnet-idx.4160.nodely.dev" - )] - #[case( - "testnet", - AlgorandService::Algod, - "https://testnet-api.4160.nodely.dev" - )] - #[case( - "testnet", - AlgorandService::Indexer, - "https://testnet-idx.4160.nodely.dev" - )] - fn test_algonode_config_variations( - #[case] network: &str, - #[case] service: AlgorandService, - #[case] expected_server: &str, - ) { - let config = ClientManager::get_algonode_config(network, service); - - assert_eq!(config.server, expected_server); - assert_eq!(config.port, Some(443)); - assert!(config.token.is_none()); - } - - #[rstest] - #[case("mainnet")] - #[case("testnet")] - #[should_panic(expected = "KMD is not available on algonode")] - fn test_algonode_config_panics_for_kmd(#[case] network: &str) { - ClientManager::get_algonode_config(network, AlgorandService::Kmd); - } - - #[test] - fn test_localnet_config() { - let config = ClientManager::get_default_localnet_config(AlgorandService::Algod); - assert_eq!(config.server, "http://localhost"); - assert_eq!(config.port, Some(4001)); - assert!(config.token.is_some()); - } - - #[test] - fn test_genesis_id_localnet_detection() { - assert!(ClientManager::genesis_id_is_localnet("devnet-v1")); - assert!(ClientManager::genesis_id_is_localnet("sandnet-v1")); - assert!(ClientManager::genesis_id_is_localnet("dockernet-v1")); - assert!(!ClientManager::genesis_id_is_localnet("testnet-v1.0")); - assert!(!ClientManager::genesis_id_is_localnet("mainnet-v1.0")); - } - - #[test] - fn test_kmd_optional_accessors_when_configured() { - let config = AlgoConfig { - algod_config: AlgoClientConfig { - server: "http://localhost".to_string(), - port: Some(4001), - token: None, - }, - indexer_config: None, - kmd_config: Some(AlgoClientConfig { - server: "http://localhost".to_string(), - port: Some(4002), - token: Some(TokenHeader::String("kmd-token".to_string())), - }), - }; - - let manager = ClientManager::new(&config).unwrap(); - assert!(manager.kmd_if_present().is_some()); - assert!(manager.kmd().is_ok()); - } - - #[test] - fn test_kmd_optional_accessors_when_missing() { - let config = AlgoConfig { - algod_config: AlgoClientConfig { - server: "http://localhost".to_string(), - port: Some(4001), - token: None, - }, - indexer_config: None, - kmd_config: None, - }; - - let manager = ClientManager::new(&config).unwrap(); - assert!(matches!( - manager.kmd(), - Err(ClientManagerError::KmdError { .. }) - )); - assert!(manager.kmd_if_present().is_none()); - } -} diff --git a/crates/algokit_utils/src/clients/mod.rs b/crates/algokit_utils/src/clients/mod.rs deleted file mode 100644 index 958f624ce..000000000 --- a/crates/algokit_utils/src/clients/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -pub mod account_manager; -pub mod algorand_client; -pub mod app_manager; -pub mod asset_manager; -pub mod client_manager; -pub mod network_client; - -// Re-export commonly used client types -pub use account_manager::AccountManager; -pub use algorand_client::AlgorandClient; -pub use app_manager::{AppManager, AppManagerError}; -pub use asset_manager::{ - AssetInformation, AssetManager, AssetManagerError, BulkAssetOptInOutResult, -}; -pub use client_manager::ClientManager; -pub use network_client::{ - AlgoClientConfig, AlgoConfig, AlgorandNetwork, AlgorandService, NetworkDetails, TokenHeader, - genesis_id_is_localnet, -}; diff --git a/crates/algokit_utils/src/clients/network_client.rs b/crates/algokit_utils/src/clients/network_client.rs deleted file mode 100644 index 02899a973..000000000 --- a/crates/algokit_utils/src/clients/network_client.rs +++ /dev/null @@ -1,147 +0,0 @@ -use std::collections::HashMap; - -const TESTNET_GENESIS_IDS: [&str; 3] = ["testnet-v1.0", "testnet-v1", "testnet"]; -const MAINNET_GENESIS_IDS: [&str; 3] = ["mainnet-v1.0", "mainnet-v1", "mainnet"]; - -#[derive(Debug, Clone)] -pub enum TokenHeader { - String(String), - Headers(HashMap), -} - -/// Represents the different Algorand networks -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AlgorandNetwork { - /// Local development network - LocalNet, - /// Algorand TestNet - TestNet, - /// Algorand MainNet - MainNet, -} - -impl AlgorandNetwork { - /// Get the string representation of the network - pub fn as_str(&self) -> &'static str { - match self { - AlgorandNetwork::LocalNet => "localnet", - AlgorandNetwork::TestNet => "testnet", - AlgorandNetwork::MainNet => "mainnet", - } - } - - /// Check if this network is a local development network - pub fn is_localnet(&self) -> bool { - matches!(self, AlgorandNetwork::LocalNet) - } - - /// Check if this network is TestNet - pub fn is_testnet(&self) -> bool { - matches!(self, AlgorandNetwork::TestNet) - } - - /// Check if this network is MainNet - pub fn is_mainnet(&self) -> bool { - matches!(self, AlgorandNetwork::MainNet) - } - - /// Get the expected genesis ID for this network - pub fn expected_genesis_id(&self) -> Option<&'static str> { - match self { - AlgorandNetwork::LocalNet => None, // LocalNet can have various genesis IDs - AlgorandNetwork::TestNet => Some("testnet-v1.0"), - AlgorandNetwork::MainNet => Some("mainnet-v1.0"), - } - } -} - -/// Represents the different Algorand services -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AlgorandService { - /// Algorand daemon (algod) - provides access to the blockchain - Algod, - /// Algorand indexer - provides historical blockchain data - Indexer, - /// Key Management Daemon (kmd) - provides key management functionality - Kmd, -} - -impl AlgorandService { - /// Get the string representation of the service - pub fn as_str(&self) -> &'static str { - match self { - AlgorandService::Algod => "algod", - AlgorandService::Indexer => "indexer", - AlgorandService::Kmd => "kmd", - } - } -} - -/// Config for an Algorand client. -#[derive(Debug, Clone)] -pub struct AlgoClientConfig { - /// Base URL of the server e.g. http://localhost, https://testnet-api.algonode.cloud/, etc. - pub server: String, - /// Optional port to use e.g. 4001, 443, etc. - pub port: Option, - /// Optional token to use for API authentication - pub token: Option, -} - -/// Configuration for algod, indexer and kmd clients. -#[derive(Debug, Clone)] -pub struct AlgoConfig { - /// Algod client configuration - pub algod_config: AlgoClientConfig, - /// Indexer client configuration - pub indexer_config: Option, - /// KMD client configuration - pub kmd_config: Option, -} - -impl AlgoConfig { - pub fn new( - algod_config: AlgoClientConfig, - indexer_config: Option, - kmd_config: Option, - ) -> Self { - Self { - algod_config, - indexer_config, - kmd_config, - } - } -} - -#[derive(Debug, Clone)] -pub struct NetworkDetails { - pub is_testnet: bool, - pub is_mainnet: bool, - pub is_localnet: bool, - pub genesis_id: String, - pub genesis_hash: String, -} - -impl NetworkDetails { - pub fn new(genesis_id: String, genesis_hash: String) -> Self { - let is_localnet = genesis_id_is_localnet(&genesis_id); - let is_testnet = TESTNET_GENESIS_IDS - .iter() - .any(|known| known.eq_ignore_ascii_case(&genesis_id)); - let is_mainnet = MAINNET_GENESIS_IDS - .iter() - .any(|known| known.eq_ignore_ascii_case(&genesis_id)); - - Self { - is_testnet, - is_mainnet, - is_localnet, - genesis_id, - genesis_hash, - } - } -} - -pub fn genesis_id_is_localnet(genesis_id: &str) -> bool { - genesis_id == "devnet-v1" || genesis_id == "sandnet-v1" || genesis_id == "dockernet-v1" -} diff --git a/crates/algokit_utils/src/config.rs b/crates/algokit_utils/src/config.rs deleted file mode 100644 index 4a04ab41e..000000000 --- a/crates/algokit_utils/src/config.rs +++ /dev/null @@ -1,85 +0,0 @@ -use once_cell::sync::Lazy; -use serde::{Deserialize, Serialize}; -use std::sync::atomic::{AtomicBool, Ordering}; -use tokio::sync::broadcast; - -/// Minimal lifecycle event types -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum EventType { - /// Emitted when an app is compiled (for source map capture) - AppCompiled, - /// Emitted when a transaction group is simulated (for AVM traces) - TxnGroupSimulated, -} - -/// Minimal event payloads -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AppCompiledEventData { - pub app_name: Option, - pub approval_source_map: Option, - pub clear_source_map: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TxnGroupSimulatedEventData { - pub simulate_response: serde_json::Value, -} - -#[derive(Debug, Clone)] -pub enum EventData { - AppCompiled(AppCompiledEventData), - TxnGroupSimulated(TxnGroupSimulatedEventData), -} - -/// Async event emitter using Tokio broadcast -#[derive(Clone)] -pub struct AsyncEventEmitter { - sender: broadcast::Sender<(EventType, EventData)>, -} - -impl AsyncEventEmitter { - pub fn new(buffer: usize) -> Self { - let (sender, _receiver) = broadcast::channel(buffer); - Self { sender } - } - - pub fn subscribe(&self) -> broadcast::Receiver<(EventType, EventData)> { - self.sender.subscribe() - } - - pub async fn emit(&self, event_type: EventType, data: EventData) { - // Ignore error if there are no subscribers - let _ = self.sender.send((event_type, data)); - } -} - -/// Global flags and event emitter -static DEBUG: AtomicBool = AtomicBool::new(false); -static TRACE_ALL: AtomicBool = AtomicBool::new(false); -static EVENTS: Lazy = Lazy::new(|| AsyncEventEmitter::new(32)); - -/// Global runtime config singleton -pub struct Config; - -impl Config { - pub fn debug() -> bool { - DEBUG.load(Ordering::Relaxed) - } - - pub fn trace_all() -> bool { - TRACE_ALL.load(Ordering::Relaxed) - } - - pub fn events() -> AsyncEventEmitter { - EVENTS.clone() - } - - pub fn configure(new_debug: Option, new_trace_all: Option) { - if let Some(d) = new_debug { - DEBUG.store(d, Ordering::Relaxed); - } - if let Some(t) = new_trace_all { - TRACE_ALL.store(t, Ordering::Relaxed); - } - } -} diff --git a/crates/algokit_utils/src/lib.rs b/crates/algokit_utils/src/lib.rs deleted file mode 100644 index a181ffc1f..000000000 --- a/crates/algokit_utils/src/lib.rs +++ /dev/null @@ -1,27 +0,0 @@ -pub mod applications; -pub mod clients; -pub mod config; -pub mod transactions; - -// Re-exports for clean UniFFI surface -pub use clients::{ - AccountManager, AlgoClientConfig, AlgoConfig, AlgorandClient, AlgorandNetwork, AlgorandService, - AppManager, AppManagerError, AssetInformation, AssetManager, AssetManagerError, - BulkAssetOptInOutResult, ClientManager, NetworkDetails, TokenHeader, genesis_id_is_localnet, -}; -// Re-export ABI types for convenience -pub use algokit_abi::ABIReturn; -pub use applications::app_client::{AppClient, AppClientError, AppClientParams, AppSourceMaps}; -pub use config::{Config, EventType}; -pub use transactions::{ - AccountCloseParams, AppCallMethodCallParams, AppCallParams, AppCreateMethodCallParams, - AppCreateParams, AppDeleteMethodCallParams, AppDeleteParams, AppMethodCallArg, - AppUpdateMethodCallParams, AppUpdateParams, AssetClawbackParams, AssetConfigParams, - AssetCreateParams, AssetDestroyParams, AssetFreezeParams, AssetOptInParams, AssetOptOutParams, - AssetTransferParams, AssetUnfreezeParams, ComposerError, ComposerTransaction, EmptySigner, - NonParticipationKeyRegistrationParams, OfflineKeyRegistrationParams, - OnlineKeyRegistrationParams, PaymentParams, ResourcePopulation, SendAppCreateMethodCallResult, - SendAppCreateResult, SendAppMethodCallResult, SendAssetCreateResult, SendParams, SendResult, - TransactionComposer, TransactionComposerSendResult, TransactionCreator, TransactionResult, - TransactionSender, TransactionSenderError, TransactionSigner, TransactionWithSigner, -}; diff --git a/crates/algokit_utils/src/transactions/app_call.rs b/crates/algokit_utils/src/transactions/app_call.rs deleted file mode 100644 index 38b4efc99..000000000 --- a/crates/algokit_utils/src/transactions/app_call.rs +++ /dev/null @@ -1,1218 +0,0 @@ -use super::composer::ComposerError; -use crate::{ - AccountCloseParams, AssetClawbackParams, AssetConfigParams, AssetCreateParams, - AssetDestroyParams, AssetFreezeParams, AssetOptInParams, AssetOptOutParams, - AssetTransferParams, AssetUnfreezeParams, NonParticipationKeyRegistrationParams, - OfflineKeyRegistrationParams, OnlineKeyRegistrationParams, PaymentParams, TransactionSigner, - TransactionWithSigner, create_transaction_params, -}; -use algokit_abi::{ - ABIMethod, ABIMethodArgType, ABIReferenceValue, ABIType, ABIValue, abi_type::BitSize, -}; -use algokit_transact::{ - Address, AppCallTransactionBuilder, AppCallTransactionFields, BoxReference, - OnApplicationComplete, StateSchema, Transaction, TransactionHeader, -}; -use derive_more::Debug; -use num_bigint::BigUint; -use std::{str::FromStr, sync::Arc}; - -#[derive(Debug, Clone)] -pub enum AppMethodCallArg { - ABIValue(ABIValue), - ABIReference(ABIReferenceValue), - /// Sentinel to request ARC-56 default resolution for this argument (handled by AppClient params builder) - DefaultValue, - /// Placeholder for a transaction-typed argument. Not encoded; satisfied by a transaction - /// included in the same group (extracted from other method call arguments). - TransactionPlaceholder, - Transaction(Transaction), - TransactionWithSigner(TransactionWithSigner), - Payment(PaymentParams), - AccountClose(AccountCloseParams), - AssetTransfer(AssetTransferParams), - AssetOptIn(AssetOptInParams), - AssetOptOut(AssetOptOutParams), - AssetClawback(AssetClawbackParams), - AssetCreate(AssetCreateParams), - AssetConfig(AssetConfigParams), - AssetDestroy(AssetDestroyParams), - AssetFreeze(AssetFreezeParams), - AssetUnfreeze(AssetUnfreezeParams), - AppCall(AppCallParams), - AppCreateCall(AppCreateParams), - AppUpdateCall(AppUpdateParams), - AppDeleteCall(AppDeleteParams), - AppCallMethodCall(AppCallMethodCallParams), - AppCreateMethodCall(AppCreateMethodCallParams), - AppUpdateMethodCall(AppUpdateMethodCallParams), - AppDeleteMethodCall(AppDeleteMethodCallParams), - OnlineKeyRegistration(OnlineKeyRegistrationParams), - OfflineKeyRegistration(OfflineKeyRegistrationParams), - NonParticipationKeyRegistration(NonParticipationKeyRegistrationParams), -} - -#[derive(Debug, Clone)] -pub enum ProcessedAppMethodCallArg { - ABIValue(ABIValue), - ABIReference(ABIReferenceValue), - TransactionPlaceholder, -} - -/// This pattern create a trait ValidMethodCallArg -/// that can only implemented by AppMethodCallArg and ProcessedAppMethodCallArg. -mod sealed { - pub trait ValidMethodCallArgSealed {} - impl ValidMethodCallArgSealed for super::AppMethodCallArg {} - impl ValidMethodCallArgSealed for super::ProcessedAppMethodCallArg {} -} -pub trait ValidMethodCallArg: sealed::ValidMethodCallArgSealed {} - -impl ValidMethodCallArg for AppMethodCallArg {} -impl ValidMethodCallArg for ProcessedAppMethodCallArg {} - -create_transaction_params! { - /// Parameters for creating an app call transaction. - #[derive(Clone, Default)] - pub struct AppCallParams { - /// ID of the app being called. - pub app_id: u64, - /// Defines what additional actions occur with the transaction. - pub on_complete: OnApplicationComplete, - /// Transaction specific arguments available in the app's - /// approval program and clear state program. - pub args: Option>>, - /// List of accounts in addition to the sender that may be accessed - /// from the app's approval program and clear state program. - pub account_references: Option>, - /// List of apps in addition to the current app that may be called - /// from the app's approval program and clear state program. - pub app_references: Option>, - /// Lists the assets whose parameters may be accessed by this app's - /// approval program and clear state program. - /// - /// The access is read-only. - pub asset_references: Option>, - /// The boxes that should be made available for the runtime of the program. - pub box_references: Option>, - } -} - -create_transaction_params! { - /// Parameters for creating an app create transaction. - #[derive(Clone, Default)] - pub struct AppCreateParams { - /// Defines what additional actions occur with the transaction. - pub on_complete: OnApplicationComplete, - /// Logic executed for every app call transaction, except when - /// on-completion is set to "clear". - /// - /// Approval programs may reject the transaction. - pub approval_program: Vec, - /// Logic executed for app call transactions with on-completion set to "clear". - /// - /// Clear state programs cannot reject the transaction. - pub clear_state_program: Vec, - /// Holds the maximum number of global state values. - /// - /// This cannot be changed after creation. - pub global_state_schema: Option, - /// Holds the maximum number of local state values. - /// - /// This cannot be changed after creation. - pub local_state_schema: Option, - /// Number of additional pages allocated to the app's approval - /// and clear state programs. - /// - /// Each extra program page is 2048 bytes. The sum of approval program - /// and clear state program may not exceed 2048*(1+extra_program_pages) bytes. - /// Currently, the maximum value is 3. - /// This cannot be changed after creation. - pub extra_program_pages: Option, - /// Transaction specific arguments available in the app's - /// approval program and clear state program. - pub args: Option>>, - /// List of accounts in addition to the sender that may be accessed - /// from the app's approval program and clear state program. - pub account_references: Option>, - /// List of apps in addition to the current app that may be called - /// from the app's approval program and clear state program. - pub app_references: Option>, - /// Lists the assets whose parameters may be accessed by this app's - /// approval program and clear state program. - /// - /// The access is read-only. - pub asset_references: Option>, - /// The boxes that should be made available for the runtime of the program. - pub box_references: Option>, - } -} - -create_transaction_params! { - /// Parameters for creating an app delete transaction. - #[derive(Clone, Default)] - pub struct AppDeleteParams { - /// ID of the app being deleted. - pub app_id: u64, - /// Transaction specific arguments available in the app's - /// approval program and clear state program. - pub args: Option>>, - /// List of accounts in addition to the sender that may be accessed - /// from the app's approval program and clear state program. - pub account_references: Option>, - /// List of apps in addition to the current app that may be called - /// from the app's approval program and clear state program. - pub app_references: Option>, - /// Lists the assets whose parameters may be accessed by this app's - /// approval program and clear state program. - /// - /// The access is read-only. - pub asset_references: Option>, - /// The boxes that should be made available for the runtime of the program. - pub box_references: Option>, - } -} - -create_transaction_params! { - /// Parameters for creating an app update transaction. - #[derive(Clone, Default)] - pub struct AppUpdateParams { - /// ID of the app being updated. - pub app_id: u64, - /// Logic executed for every app call transaction, except when - /// on-completion is set to "clear". - /// - /// Approval programs may reject the transaction. - pub approval_program: Vec, - /// Logic executed for app call transactions with on-completion set to "clear". - /// - /// Clear state programs cannot reject the transaction. - pub clear_state_program: Vec, - /// Transaction specific arguments available in the app's - /// approval program and clear state program. - pub args: Option>>, - /// List of accounts in addition to the sender that may be accessed - /// from the app's approval program and clear state program. - pub account_references: Option>, - /// List of apps in addition to the current app that may be called - /// from the app's approval program and clear state program. - pub app_references: Option>, - /// Lists the assets whose parameters may be accessed by this app's - /// approval program and clear state program. - /// - /// The access is read-only. - pub asset_references: Option>, - /// The boxes that should be made available for the runtime of the program. - pub box_references: Option>, - } -} - -/// Parameters for creating an app method call transaction. -#[derive(Debug, Clone)] -pub struct AppCallMethodCallParams -where - T: ValidMethodCallArg, -{ - #[debug(skip)] - /// A signer used to sign transaction(s); if not specified then - /// an attempt will be made to find a registered signer for the - /// given `sender` or use a default signer (if configured). - pub signer: Option>, - /// The address of the account sending the transaction. - pub sender: algokit_transact::Address, - /// Change the signing key of the sender to the given address. - /// **Warning:** Please be careful with this parameter and be sure to read the [official rekey guidance](https://dev.algorand.co/concepts/accounts/rekeying). - pub rekey_to: Option, - /// Note to attach to the transaction. Max of 1000 bytes. - pub note: Option>, - /// Prevent multiple transactions with the same lease being included within the validity window. - /// - /// A [lease](https://dev.algorand.co/concepts/transactions/leases) - /// enforces a mutually exclusive transaction (useful to prevent double-posting and other scenarios). - pub lease: Option<[u8; 32]>, - /// The static transaction fee. In most cases you want to use extra fee unless setting the fee to 0 to be covered by another transaction. - pub static_fee: Option, - /// The fee to pay IN ADDITION to the suggested fee. Useful for manually covering inner transaction fees. - pub extra_fee: Option, - /// Throw an error if the fee for the transaction is more than this amount; prevents overspending on fees during high congestion periods. - pub max_fee: Option, - /// How many rounds the transaction should be valid for, if not specified then the registered default validity window will be used. - pub validity_window: Option, - /// Set the first round this transaction is valid. - /// If left undefined, the value from algod will be used. - /// - /// We recommend you only set this when you intentionally want this to be some time in the future. - pub first_valid_round: Option, - /// The last round this transaction is valid. It is recommended to use validity window instead. - pub last_valid_round: Option, - /// ID of the app being called. - pub app_id: u64, - /// The ABI method to call. - pub method: ABIMethod, - /// Transaction specific arguments available in the app's - /// approval program and clear state program. - pub args: Vec, - /// List of accounts in addition to the sender that may be accessed - /// from the app's approval program and clear state program. - pub account_references: Option>, - /// List of apps in addition to the current app that may be called - /// from the app's approval program and clear state program. - pub app_references: Option>, - /// Lists the assets whose parameters may be accessed by this app's - /// approval program and clear state program. - /// - /// The access is read-only. - pub asset_references: Option>, - /// The boxes that should be made available for the runtime of the program. - pub box_references: Option>, - /// Defines what additional actions occur with the transaction. - pub on_complete: OnApplicationComplete, -} - -impl Default for AppCallMethodCallParams -where - T: ValidMethodCallArg, -{ - fn default() -> Self { - Self { - app_id: 0, - method: ABIMethod::default(), - args: Vec::new(), - account_references: None, - app_references: None, - asset_references: None, - box_references: None, - on_complete: OnApplicationComplete::NoOp, - sender: Address::default(), - 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, - } - } -} - -/// Parameters for creating an app create method call transaction. -#[derive(Debug, Default, Clone)] -pub struct AppCreateMethodCallParams -where - T: ValidMethodCallArg, -{ - #[debug(skip)] - /// A signer used to sign transaction(s); if not specified then - /// an attempt will be made to find a registered signer for the - /// given `sender` or use a default signer (if configured). - pub signer: Option>, - /// The address of the account sending the transaction. - pub sender: algokit_transact::Address, - /// Change the signing key of the sender to the given address. - /// **Warning:** Please be careful with this parameter and be sure to read the [official rekey guidance](https://dev.algorand.co/concepts/accounts/rekeying). - pub rekey_to: Option, - /// Note to attach to the transaction. Max of 1000 bytes. - pub note: Option>, - /// Prevent multiple transactions with the same lease being included within the validity window. - /// - /// A [lease](https://dev.algorand.co/concepts/transactions/leases) - /// enforces a mutually exclusive transaction (useful to prevent double-posting and other scenarios). - pub lease: Option<[u8; 32]>, - /// The static transaction fee. In most cases you want to use extra fee unless setting the fee to 0 to be covered by another transaction. - pub static_fee: Option, - /// The fee to pay IN ADDITION to the suggested fee. Useful for manually covering inner transaction fees. - pub extra_fee: Option, - /// Throw an error if the fee for the transaction is more than this amount; prevents overspending on fees during high congestion periods. - pub max_fee: Option, - /// How many rounds the transaction should be valid for, if not specified then the registered default validity window will be used. - pub validity_window: Option, - /// Set the first round this transaction is valid. - /// If left undefined, the value from algod will be used. - /// - /// We recommend you only set this when you intentionally want this to be some time in the future. - pub first_valid_round: Option, - /// The last round this transaction is valid. It is recommended to use validity window instead. - pub last_valid_round: Option, - /// Defines what additional actions occur with the transaction. - pub on_complete: OnApplicationComplete, - /// Logic executed for every app call transaction, except when - /// on-completion is set to "clear". - /// - /// Approval programs may reject the transaction. - pub approval_program: Vec, - /// Logic executed for app call transactions with on-completion set to "clear". - /// - /// Clear state programs cannot reject the transaction. - pub clear_state_program: Vec, - /// Holds the maximum number of global state values. - /// - /// This cannot be changed after creation. - pub global_state_schema: Option, - /// Holds the maximum number of local state values. - /// - /// This cannot be changed after creation. - pub local_state_schema: Option, - /// Number of additional pages allocated to the app's approval - /// and clear state programs. - /// - /// Each extra program page is 2048 bytes. The sum of approval program - /// and clear state program may not exceed 2048*(1+extra_program_pages) bytes. - /// Currently, the maximum value is 3. - /// This cannot be changed after creation. - pub extra_program_pages: Option, - /// The ABI method to call. - pub method: ABIMethod, - /// Transaction specific arguments available in the app's - /// approval program and clear state program. - pub args: Vec, - /// List of accounts in addition to the sender that may be accessed - /// from the app's approval program and clear state program. - pub account_references: Option>, - /// List of apps in addition to the current app that may be called - /// from the app's approval program and clear state program. - pub app_references: Option>, - /// Lists the assets whose parameters may be accessed by this app's - /// approval program and clear state program. - /// - /// The access is read-only. - pub asset_references: Option>, - /// The boxes that should be made available for the runtime of the program. - pub box_references: Option>, -} - -/// Parameters for creating an app update method call transaction. -#[derive(Debug, Default, Clone)] -pub struct AppUpdateMethodCallParams -where - T: ValidMethodCallArg, -{ - #[debug(skip)] - /// A signer used to sign transaction(s); if not specified then - /// an attempt will be made to find a registered signer for the - /// given `sender` or use a default signer (if configured). - pub signer: Option>, - /// The address of the account sending the transaction. - pub sender: algokit_transact::Address, - /// Change the signing key of the sender to the given address. - /// **Warning:** Please be careful with this parameter and be sure to read the [official rekey guidance](https://dev.algorand.co/concepts/accounts/rekeying). - pub rekey_to: Option, - /// Note to attach to the transaction. Max of 1000 bytes. - pub note: Option>, - /// Prevent multiple transactions with the same lease being included within the validity window. - /// - /// A [lease](https://dev.algorand.co/concepts/transactions/leases) - /// enforces a mutually exclusive transaction (useful to prevent double-posting and other scenarios). - pub lease: Option<[u8; 32]>, - /// The static transaction fee. In most cases you want to use extra fee unless setting the fee to 0 to be covered by another transaction. - pub static_fee: Option, - /// The fee to pay IN ADDITION to the suggested fee. Useful for manually covering inner transaction fees. - pub extra_fee: Option, - /// Throw an error if the fee for the transaction is more than this amount; prevents overspending on fees during high congestion periods. - pub max_fee: Option, - /// How many rounds the transaction should be valid for, if not specified then the registered default validity window will be used. - pub validity_window: Option, - /// Set the first round this transaction is valid. - /// If left undefined, the value from algod will be used. - /// - /// We recommend you only set this when you intentionally want this to be some time in the future. - pub first_valid_round: Option, - /// The last round this transaction is valid. It is recommended to use validity window instead. - pub last_valid_round: Option, - /// ID of the app being updated. - pub app_id: u64, - /// Logic executed for every app call transaction, except when - /// on-completion is set to "clear". - /// - /// Approval programs may reject the transaction. - pub approval_program: Vec, - /// Logic executed for app call transactions with on-completion set to "clear". - /// - /// Clear state programs cannot reject the transaction. - pub clear_state_program: Vec, - /// The ABI method to call. - pub method: ABIMethod, - /// Transaction specific arguments available in the app's - /// approval program and clear state program. - pub args: Vec, - /// List of accounts in addition to the sender that may be accessed - /// from the app's approval program and clear state program. - pub account_references: Option>, - /// List of apps in addition to the current app that may be called - /// from the app's approval program and clear state program. - pub app_references: Option>, - /// Lists the assets whose parameters may be accessed by this app's - /// approval program and clear state program. - /// - /// The access is read-only. - pub asset_references: Option>, - /// The boxes that should be made available for the runtime of the program. - pub box_references: Option>, -} - -/// Parameters for creating an app delete method call transaction. -#[derive(Debug, Default, Clone)] -pub struct AppDeleteMethodCallParams -where - T: ValidMethodCallArg, -{ - #[debug(skip)] - /// A signer used to sign transaction(s); if not specified then - /// an attempt will be made to find a registered signer for the - /// given `sender` or use a default signer (if configured). - pub signer: Option>, - /// The address of the account sending the transaction. - pub sender: algokit_transact::Address, - /// Change the signing key of the sender to the given address. - /// **Warning:** Please be careful with this parameter and be sure to read the [official rekey guidance](https://dev.algorand.co/concepts/accounts/rekeying). - pub rekey_to: Option, - /// Note to attach to the transaction. Max of 1000 bytes. - pub note: Option>, - /// Prevent multiple transactions with the same lease being included within the validity window. - /// - /// A [lease](https://dev.algorand.co/concepts/transactions/leases) - /// enforces a mutually exclusive transaction (useful to prevent double-posting and other scenarios). - pub lease: Option<[u8; 32]>, - /// The static transaction fee. In most cases you want to use extra fee unless setting the fee to 0 to be covered by another transaction. - pub static_fee: Option, - /// The fee to pay IN ADDITION to the suggested fee. Useful for manually covering inner transaction fees. - pub extra_fee: Option, - /// Throw an error if the fee for the transaction is more than this amount; prevents overspending on fees during high congestion periods. - pub max_fee: Option, - /// How many rounds the transaction should be valid for, if not specified then the registered default validity window will be used. - pub validity_window: Option, - /// Set the first round this transaction is valid. - /// If left undefined, the value from algod will be used. - /// - /// We recommend you only set this when you intentionally want this to be some time in the future. - pub first_valid_round: Option, - /// The last round this transaction is valid. It is recommended to use validity window instead. - pub last_valid_round: Option, - /// ID of the app being deleted. - pub app_id: u64, - /// The ABI method to call. - pub method: ABIMethod, - /// Transaction specific arguments available in the app's - /// approval program and clear state program. - pub args: Vec, - /// List of accounts in addition to the sender that may be accessed - /// from the app's approval program and clear state program. - pub account_references: Option>, - /// List of apps in addition to the current app that may be called - /// from the app's approval program and clear state program. - pub app_references: Option>, - /// Lists the assets whose parameters may be accessed by this app's - /// approval program and clear state program. - /// - /// The access is read-only. - pub asset_references: Option>, - /// The boxes that should be made available for the runtime of the program. - pub box_references: Option>, -} - -const ARGS_TUPLE_PACKING_THRESHOLD: usize = 14; // 14+ args trigger tuple packing, excluding the method selector (arg 0) - -fn process_app_method_call_args(args: &[AppMethodCallArg]) -> Vec { - args.iter() - .map(|arg| match arg { - AppMethodCallArg::ABIValue(value) => ProcessedAppMethodCallArg::ABIValue(value.clone()), - AppMethodCallArg::ABIReference(value) => { - ProcessedAppMethodCallArg::ABIReference(value.clone()) - } - _ => ProcessedAppMethodCallArg::TransactionPlaceholder, - }) - .collect() -} - -impl AppMethodCallCommonParams for AppCallMethodCallParams { - fn app_id(&self) -> u64 { - self.app_id - } - - fn method(&self) -> &ABIMethod { - &self.method - } - - fn args(&self) -> &[ProcessedAppMethodCallArg] { - &self.args - } - - fn account_references(&self) -> Option<&Vec
> { - self.account_references.as_ref() - } - - fn app_references(&self) -> Option<&Vec> { - self.app_references.as_ref() - } - - fn asset_references(&self) -> Option<&Vec> { - self.asset_references.as_ref() - } -} - -impl AppMethodCallCommonParams for AppCreateMethodCallParams { - fn app_id(&self) -> u64 { - 0 // Always 0 for creation - } - - fn method(&self) -> &ABIMethod { - &self.method - } - - fn args(&self) -> &[ProcessedAppMethodCallArg] { - &self.args - } - - fn account_references(&self) -> Option<&Vec
> { - self.account_references.as_ref() - } - - fn app_references(&self) -> Option<&Vec> { - self.app_references.as_ref() - } - - fn asset_references(&self) -> Option<&Vec> { - self.asset_references.as_ref() - } -} - -impl AppMethodCallCommonParams for AppUpdateMethodCallParams { - fn app_id(&self) -> u64 { - self.app_id - } - - fn method(&self) -> &ABIMethod { - &self.method - } - - fn args(&self) -> &[ProcessedAppMethodCallArg] { - &self.args - } - - fn account_references(&self) -> Option<&Vec
> { - self.account_references.as_ref() - } - - fn app_references(&self) -> Option<&Vec> { - self.app_references.as_ref() - } - - fn asset_references(&self) -> Option<&Vec> { - self.asset_references.as_ref() - } -} - -impl AppMethodCallCommonParams for AppDeleteMethodCallParams { - fn app_id(&self) -> u64 { - self.app_id - } - - fn method(&self) -> &ABIMethod { - &self.method - } - - fn args(&self) -> &[ProcessedAppMethodCallArg] { - &self.args - } - - fn account_references(&self) -> Option<&Vec
> { - self.account_references.as_ref() - } - - fn app_references(&self) -> Option<&Vec> { - self.app_references.as_ref() - } - - fn asset_references(&self) -> Option<&Vec> { - self.asset_references.as_ref() - } -} - -impl From<&AppCallMethodCallParams> for AppCallMethodCallParams { - fn from(params: &AppCallMethodCallParams) -> Self { - let processed_args = process_app_method_call_args(¶ms.args); - - Self { - sender: params.sender.clone(), - signer: params.signer.clone(), - rekey_to: params.rekey_to.clone(), - note: params.note.clone(), - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - app_id: params.app_id, - method: params.method.clone(), - args: processed_args, - account_references: params.account_references.clone(), - app_references: params.app_references.clone(), - asset_references: params.asset_references.clone(), - box_references: params.box_references.clone(), - on_complete: params.on_complete, - } - } -} - -impl From<&AppCreateMethodCallParams> for AppCreateMethodCallParams { - fn from(params: &AppCreateMethodCallParams) -> Self { - let processed_args = process_app_method_call_args(¶ms.args); - - Self { - sender: params.sender.clone(), - signer: params.signer.clone(), - rekey_to: params.rekey_to.clone(), - note: params.note.clone(), - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - method: params.method.clone(), - args: processed_args, - account_references: params.account_references.clone(), - app_references: params.app_references.clone(), - asset_references: params.asset_references.clone(), - box_references: params.box_references.clone(), - on_complete: params.on_complete, - approval_program: params.approval_program.clone(), - clear_state_program: params.clear_state_program.clone(), - global_state_schema: params.global_state_schema.clone(), - local_state_schema: params.local_state_schema.clone(), - extra_program_pages: params.extra_program_pages, - } - } -} - -impl From<&AppUpdateMethodCallParams> for AppUpdateMethodCallParams { - fn from(params: &AppUpdateMethodCallParams) -> Self { - let processed_args = process_app_method_call_args(¶ms.args); - - Self { - sender: params.sender.clone(), - signer: params.signer.clone(), - rekey_to: params.rekey_to.clone(), - note: params.note.clone(), - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - app_id: params.app_id, - method: params.method.clone(), - args: processed_args, - account_references: params.account_references.clone(), - app_references: params.app_references.clone(), - asset_references: params.asset_references.clone(), - box_references: params.box_references.clone(), - approval_program: params.approval_program.clone(), - clear_state_program: params.clear_state_program.clone(), - } - } -} - -impl From<&AppDeleteMethodCallParams> for AppDeleteMethodCallParams { - fn from(params: &AppDeleteMethodCallParams) -> Self { - let processed_args = process_app_method_call_args(¶ms.args); - - Self { - sender: params.sender.clone(), - signer: params.signer.clone(), - rekey_to: params.rekey_to.clone(), - note: params.note.clone(), - lease: params.lease, - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - app_id: params.app_id, - method: params.method.clone(), - args: processed_args, - account_references: params.account_references.clone(), - app_references: params.app_references.clone(), - asset_references: params.asset_references.clone(), - box_references: params.box_references.clone(), - } - } -} - -trait AppMethodCallCommonParams { - fn app_id(&self) -> u64; - fn method(&self) -> &ABIMethod; - fn args(&self) -> &[ProcessedAppMethodCallArg]; - fn account_references(&self) -> Option<&Vec
>; - fn app_references(&self) -> Option<&Vec>; - fn asset_references(&self) -> Option<&Vec>; -} - -fn populate_method_args_into_reference_arrays( - sender: &Address, - app_id: u64, - method_args: &[ProcessedAppMethodCallArg], - account_references: &mut Vec
, - app_references: &mut Vec, - asset_references: &mut Vec, -) -> Result<(), ComposerError> { - for method_arg in method_args.iter() { - if let ProcessedAppMethodCallArg::ABIReference(value) = method_arg { - match value { - ABIReferenceValue::Account(addr_str) => { - let address = Address::from_str(addr_str).map_err(|_e| { - ComposerError::TransactionError { - message: format!("Invalid address {}", addr_str), - } - })?; - - if address != *sender && !account_references.contains(&address) { - account_references.push(address); - } - } - ABIReferenceValue::Asset(asset_id) => { - if !asset_references.contains(asset_id) { - asset_references.push(*asset_id); - } - } - ABIReferenceValue::Application(app_id_ref) => { - if *app_id_ref != app_id && !app_references.contains(app_id_ref) { - app_references.push(*app_id_ref); - } - } - } - } - } - - Ok(()) -} - -fn calculate_method_arg_reference_array_index( - ref_value: &ABIReferenceValue, - sender: &Address, - app_id: u64, - account_references: &[Address], - app_references: &[u64], - asset_references: &[u64], -) -> Result { - match ref_value { - ABIReferenceValue::Account(addr_str) => { - let address = - Address::from_str(addr_str).map_err(|_e| ComposerError::TransactionError { - message: format!("Invalid address {}", addr_str), - })?; - - if address == *sender { - // If address is the same as sender, use index 0 - Ok(0) - } else if let Some(existing_index) = account_references - .iter() - .position(|ref_addr| *ref_addr == address) - { - // If address already exists in account_references, use existing index + 1 - Ok((existing_index + 1) as u8) - } else { - Err(ComposerError::ABIEncodingError { - message: format!("Account {} not found in reference array", addr_str), - }) - } - } - ABIReferenceValue::Asset(asset_id) => { - if let Some(existing_index) = asset_references - .iter() - .position(|&ref_id| ref_id == *asset_id) - { - // If asset already exists in asset_references, use existing index - Ok(existing_index as u8) - } else { - Err(ComposerError::ABIEncodingError { - message: format!("Asset {} not found in reference array", asset_id), - }) - } - } - ABIReferenceValue::Application(app_id_ref) => { - if *app_id_ref == app_id { - // If app ID is the same as the current app, use index 0 - Ok(0) - } else if let Some(existing_index) = app_references - .iter() - .position(|&ref_id| ref_id == *app_id_ref) - { - // If app already exists in app_references, use existing index + 1 - Ok((existing_index + 1) as u8) - } else { - Err(ComposerError::ABIEncodingError { - message: format!("Application {} not found in reference array", app_id_ref), - }) - } - } - } -} - -fn encode_method_arguments( - method: &ABIMethod, - args: &[ProcessedAppMethodCallArg], - sender: &Address, - app_id: u64, - account_references: &[Address], - app_references: &[u64], - asset_references: &[u64], -) -> Result>, ComposerError> { - let mut encoded_args = Vec::>::new(); - - // Insert method selector at the front - let method_selector = method - .selector() - .map_err(|e| ComposerError::ABIEncodingError { - message: format!("Failed to get method selector: {}", e), - })?; - encoded_args.push(method_selector); - - let abi_types = method - .args - .iter() - .filter_map(|arg| { - match &arg.arg_type { - ABIMethodArgType::Value(abi_type) => Some(abi_type.clone()), - // Reference and transaction types encoded as uint8 indexes - ABIMethodArgType::Reference(_) => Some(ABIType::Uint( - BitSize::new(8).expect("8 should always be a valid BitSize"), - )), - ABIMethodArgType::Transaction(_) => None, - } - }) - .collect::>(); - - let abi_values: Vec = args - .iter() - .filter_map(|arg_value| -> Option> { - match arg_value { - ProcessedAppMethodCallArg::ABIReference(value) => { - let foreign_index = calculate_method_arg_reference_array_index( - value, - sender, - app_id, - account_references, - app_references, - asset_references, - ); - Some(foreign_index.map(|index| ABIValue::Uint(BigUint::from(index)))) - } - ProcessedAppMethodCallArg::ABIValue(value) => Some(Ok(value.clone())), - ProcessedAppMethodCallArg::TransactionPlaceholder => None, - } - }) - .collect::, _>>()?; - - if abi_values.len() != abi_types.len() { - return Err(ComposerError::ABIEncodingError { - message: "Mismatch in length of non-transaction arguments".to_string(), - }); - } - - // Apply ARC-4 tuple packing for methods with more than 14 arguments - // 14 instead of 15 in the ARC-4 because the first argument (method selector) is added later on - if abi_types.len() > ARGS_TUPLE_PACKING_THRESHOLD { - encoded_args.extend(encode_args_with_tuple_packing(&abi_types, &abi_values)?); - } else { - encoded_args.extend(encode_args_individually(&abi_types, &abi_values)?); - } - - Ok(encoded_args) -} - -fn encode_args_with_tuple_packing( - abi_types: &[ABIType], - abi_values: &[ABIValue], -) -> Result>, ComposerError> { - // Encode first 14 arguments individually - let first_14_abi_types = &abi_types[..ARGS_TUPLE_PACKING_THRESHOLD]; - let first_14_abi_values = &abi_values[..ARGS_TUPLE_PACKING_THRESHOLD]; - let encoded_args: &mut Vec> = - &mut encode_args_individually(first_14_abi_types, first_14_abi_values)?; - - // Pack remaining arguments into tuple at position 14 - let remaining_abi_types = &abi_types[ARGS_TUPLE_PACKING_THRESHOLD..]; - let remaining_abi_values = &abi_values[ARGS_TUPLE_PACKING_THRESHOLD..]; - let tuple_type = ABIType::Tuple(remaining_abi_types.to_vec()); - let tuple_value = ABIValue::Array(remaining_abi_values.to_vec()); - let tuple_encoded = - tuple_type - .encode(&tuple_value) - .map_err(|e| ComposerError::ABIEncodingError { - message: format!("Failed to encode ABI value: {}", e), - })?; - - encoded_args.push(tuple_encoded); - - Ok(encoded_args.to_vec()) -} - -fn encode_args_individually( - abi_types: &[ABIType], - abi_values: &[ABIValue], -) -> Result>, ComposerError> { - let encoded_args: &mut Vec> = &mut Vec::new(); - - for (abi_value, abi_type) in abi_values.iter().zip(abi_types.iter()) { - let encoded = abi_type - .encode(abi_value) - .map_err(|e| ComposerError::ABIEncodingError { - message: format!("Failed to encode ABI value: {}", e), - })?; - encoded_args.push(encoded); - } - - Ok(encoded_args.to_vec()) -} - -pub fn build_app_call( - params: &AppCallParams, - header: TransactionHeader, -) -> Result { - let mut builder = AppCallTransactionBuilder::default(); - builder - .header(header) - .app_id(params.app_id) - .on_complete(params.on_complete); - - if let Some(ref args) = params.args { - builder.args(args.clone()); - } - - if let Some(ref account_references) = params.account_references { - builder.account_references(account_references.clone()); - } - - if let Some(ref app_references) = params.app_references { - builder.app_references(app_references.clone()); - } - - if let Some(ref asset_references) = params.asset_references { - builder.asset_references(asset_references.clone()); - } - - if let Some(ref box_references) = params.box_references { - builder.box_references(box_references.clone()); - } - - builder.build().map_err(|e| e.to_string()) -} - -pub fn build_app_create_call(params: &AppCreateParams, header: TransactionHeader) -> Transaction { - Transaction::AppCall(AppCallTransactionFields { - header, - app_id: 0, - on_complete: params.on_complete, - approval_program: Some(params.approval_program.clone()), - clear_state_program: Some(params.clear_state_program.clone()), - global_state_schema: params.global_state_schema.clone(), - local_state_schema: params.local_state_schema.clone(), - extra_program_pages: params.extra_program_pages, - args: params.args.clone(), - account_references: params.account_references.clone(), - app_references: params.app_references.clone(), - asset_references: params.asset_references.clone(), - box_references: params.box_references.clone(), - }) -} - -pub fn build_app_update_call(params: &AppUpdateParams, header: TransactionHeader) -> Transaction { - Transaction::AppCall(AppCallTransactionFields { - header, - app_id: params.app_id, - on_complete: OnApplicationComplete::UpdateApplication, - approval_program: Some(params.approval_program.clone()), - clear_state_program: Some(params.clear_state_program.clone()), - global_state_schema: None, - local_state_schema: None, - extra_program_pages: None, - args: params.args.clone(), - account_references: params.account_references.clone(), - app_references: params.app_references.clone(), - asset_references: params.asset_references.clone(), - box_references: params.box_references.clone(), - }) -} - -pub fn build_app_delete_call(params: &AppDeleteParams, header: TransactionHeader) -> Transaction { - Transaction::AppCall(AppCallTransactionFields { - header, - app_id: params.app_id, - on_complete: OnApplicationComplete::DeleteApplication, - approval_program: None, - clear_state_program: None, - global_state_schema: None, - local_state_schema: None, - extra_program_pages: None, - args: params.args.clone(), - account_references: params.account_references.clone(), - app_references: params.app_references.clone(), - asset_references: params.asset_references.clone(), - box_references: params.box_references.clone(), - }) -} - -fn build_method_call_common( - header: TransactionHeader, - params: &T, - transaction_builder: F, -) -> Result -where - T: AppMethodCallCommonParams, - F: FnOnce(TransactionHeader, Vec
, Vec, Vec, Vec>) -> Transaction, -{ - let mut account_references = params.account_references().cloned().unwrap_or_default(); - let mut app_references = params.app_references().cloned().unwrap_or_default(); - let mut asset_references = params.asset_references().cloned().unwrap_or_default(); - - populate_method_args_into_reference_arrays( - &header.sender, - params.app_id(), - params.args(), - &mut account_references, - &mut app_references, - &mut asset_references, - )?; - - let encoded_args = encode_method_arguments( - params.method(), - params.args(), - &header.sender, - params.app_id(), - &account_references, - &app_references, - &asset_references, - )?; - - Ok(transaction_builder( - header, - account_references, - app_references, - asset_references, - encoded_args, - )) -} - -pub fn build_app_call_method_call( - params: &AppCallMethodCallParams, - header: TransactionHeader, -) -> Result { - build_method_call_common( - header.clone(), - params, - |header, account_refs, app_refs, asset_refs, encoded_args| { - Transaction::AppCall(algokit_transact::AppCallTransactionFields { - header, - app_id: params.app_id, - on_complete: params.on_complete, - approval_program: None, - clear_state_program: None, - global_state_schema: None, - local_state_schema: None, - extra_program_pages: None, - args: Some(encoded_args), - account_references: Some(account_refs), - app_references: Some(app_refs), - asset_references: Some(asset_refs), - box_references: params.box_references.clone(), - }) - }, - ) -} - -pub fn build_app_create_method_call( - params: &AppCreateMethodCallParams, - header: TransactionHeader, -) -> Result { - build_method_call_common( - header.clone(), - params, - |header, account_refs, app_refs, asset_refs, encoded_args| { - Transaction::AppCall(algokit_transact::AppCallTransactionFields { - header, - app_id: 0, // 0 indicates app creation - on_complete: params.on_complete, - approval_program: Some(params.approval_program.clone()), - clear_state_program: Some(params.clear_state_program.clone()), - global_state_schema: params.global_state_schema.clone(), - local_state_schema: params.local_state_schema.clone(), - extra_program_pages: params.extra_program_pages, - args: Some(encoded_args), - account_references: Some(account_refs), - app_references: Some(app_refs), - asset_references: Some(asset_refs), - box_references: params.box_references.clone(), - }) - }, - ) -} - -pub fn build_app_update_method_call( - params: &AppUpdateMethodCallParams, - header: TransactionHeader, -) -> Result { - build_method_call_common( - header.clone(), - params, - |header, account_refs, app_refs, asset_refs, encoded_args| { - // Calculate extra program pages if not explicitly provided - let total_len = params.approval_program.len() + params.clear_state_program.len(); - let extra_pages = if total_len > 2048 { - // ceil(total_len / 2048) - 1 - Some(((total_len as u32 + 2047) / 2048) - 1) - } else { - Some(0) - }; - Transaction::AppCall(algokit_transact::AppCallTransactionFields { - header, - app_id: params.app_id, - on_complete: OnApplicationComplete::UpdateApplication, - approval_program: Some(params.approval_program.clone()), - clear_state_program: Some(params.clear_state_program.clone()), - global_state_schema: None, - local_state_schema: None, - extra_program_pages: extra_pages, - args: Some(encoded_args), - account_references: Some(account_refs), - app_references: Some(app_refs), - asset_references: Some(asset_refs), - box_references: params.box_references.clone(), - }) - }, - ) -} - -pub fn build_app_delete_method_call( - params: &AppDeleteMethodCallParams, - header: TransactionHeader, -) -> Result { - build_method_call_common( - header.clone(), - params, - |header, account_refs, app_refs, asset_refs, encoded_args| { - Transaction::AppCall(algokit_transact::AppCallTransactionFields { - header, - app_id: params.app_id, - on_complete: OnApplicationComplete::DeleteApplication, - approval_program: None, - clear_state_program: None, - global_state_schema: None, - local_state_schema: None, - extra_program_pages: None, - args: Some(encoded_args), - account_references: Some(account_refs), - app_references: Some(app_refs), - asset_references: Some(asset_refs), - box_references: params.box_references.clone(), - }) - }, - ) -} diff --git a/crates/algokit_utils/src/transactions/asset_config.rs b/crates/algokit_utils/src/transactions/asset_config.rs deleted file mode 100644 index dea54d0b4..000000000 --- a/crates/algokit_utils/src/transactions/asset_config.rs +++ /dev/null @@ -1,254 +0,0 @@ -use algokit_transact::{ - Address, AssetConfigTransactionBuilder, AssetConfigTransactionFields, Transaction, - TransactionHeader, -}; - -use crate::create_transaction_params; - -create_transaction_params! { - /// Parameters for creating an asset create transaction. - #[derive(Clone, Default)] - pub struct AssetCreateParams { - /// The total amount of the smallest divisible (decimal) unit to create. - /// - /// For example, if creating a asset with 2 decimals and wanting a total supply of 100 units, this value should be 10000. - /// - /// This field can only be specified upon asset creation. - pub total: u64, - - /// The amount of decimal places the asset should have. - /// - /// If unspecified then the asset will be in whole units (i.e. `0`). - /// * 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; - /// * If 3, the base unit of the asset is in thousandths; - /// - /// and so on up to 19 decimal places. - /// - /// This field can only be specified upon asset creation. - pub decimals: Option, - - /// Whether the asset is frozen by default for all accounts. - /// Defaults to `false`. - /// - /// If `true` then for anyone apart from the creator to hold the - /// asset it needs to be unfrozen per account using an asset freeze - /// transaction from the `freeze` account, which must be set on creation. - /// - /// This field can only be specified upon asset creation. - pub default_frozen: Option, - - /// The optional name of the asset. - /// - /// Max size is 32 bytes. - /// - /// This field can only be specified upon asset creation. - pub asset_name: Option, - - /// The optional name of the unit of this asset (e.g. ticker name). - /// - /// Max size is 8 bytes. - /// - /// This field can only be specified upon asset creation. - pub unit_name: Option, - - /// Specifies an optional URL where more information about the asset can be retrieved (e.g. metadata). - /// - /// Max size is 96 bytes. - /// - /// This field can only be specified upon asset creation. - pub url: Option, - - /// 32-byte hash of some metadata that is relevant to your asset and/or asset holders. - /// - /// The format of this metadata is up to the application. - /// - /// This field can only be specified upon asset creation. - pub metadata_hash: Option<[u8; 32]>, - - /// The address of the optional account that can manage the configuration of the asset and destroy it. - /// - /// The fields it can change are `manager`, `reserve`, `clawback`, and `freeze`. - /// - /// If not set or set to the Zero address, the asset becomes permanently immutable. - pub manager: Option
, - - /// The address of the optional account that holds the reserve (uncirculated supply) units of the asset. - /// - /// This address has no specific authority in the protocol itself and is informational only. - /// - /// Some standards like [ARC-19](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0019.md) - /// rely on this field to hold meaningful data. - /// - /// It can be used in the case where you want to signal to holders of your asset that the uncirculated units - /// of the asset reside in an account that is different from the default creator account. - /// - /// If not set or set to the Zero address, this field is permanently empty. - pub reserve: Option
, - - /// The address of the optional account that can be used to freeze or unfreeze holdings of this asset for any account. - /// - /// If empty, freezing is not permitted. - /// - /// If not set or set to the Zero address, this field is permanently empty. - pub freeze: Option
, - - /// The address of the optional account that can clawback holdings of this asset from any account. - /// - /// **This field should be used with caution** as the clawback account has the ability to **unconditionally take assets from any account**. - /// - /// If empty, clawback is not permitted. - /// - /// If not set or set to the Zero address is permanently empty. - pub clawback: Option
, - } -} - -create_transaction_params! { - /// Parameters for creating an asset reconfiguration transaction. - /// - /// Only fields manager, reserve, freeze, and clawback can be set. - /// - /// **Note:** The manager, reserve, freeze, and clawback addresses - /// are immutably empty if they are not set. If manager is not set then - /// all fields are immutable from that point forward. - #[derive(Clone, Default)] - pub struct AssetConfigParams { - /// ID of the asset to reconfigure - pub asset_id: u64, - - /// The address of the optional account that can manage the configuration of the asset and destroy it. - /// - /// The configuration fields it can change are `manager`, `reserve`, `clawback`, and `freeze`. - /// - /// If not set or set to the Zero address the asset becomes permanently immutable. - pub manager: Option
, - - /// The address of the optional account that holds the reserve (uncirculated supply) units of the asset. - /// - /// This address has no specific authority in the protocol itself and is informational only. - /// - /// Some standards like [ARC-19](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0019.md) - /// rely on this field to hold meaningful data. - /// - /// It can be used in the case where you want to signal to holders of your asset that the uncirculated units - /// of the asset reside in an account that is different from the default creator account. - /// - /// If not set or set to the Zero address is permanently empty. - pub reserve: Option
, - - /// The address of the optional account that can be used to freeze or unfreeze holdings of this asset for any account. - /// - /// If empty, freezing is not permitted. - /// - /// If not set or set to the Zero address is permanently empty. - pub freeze: Option
, - - /// The address of the optional account that can clawback holdings of this asset from any account. - /// - /// **This field should be used with caution** as the clawback account has the ability to **unconditionally take assets from any account**. - /// - /// If empty, clawback is not permitted. - /// - /// If not set or set to the Zero address is permanently empty. - pub clawback: Option
, - } -} - -create_transaction_params! { - /// Parameters for creating an asset destroy transaction. - /// - /// Created assets can be destroyed only by the asset manager account. All of the assets must be owned by the creator of the asset before the asset can be deleted. - #[derive(Clone, Default)] - pub struct AssetDestroyParams { - /// ID of the asset to destroy - pub asset_id: u64, - } -} - -pub fn build_asset_create( - params: &AssetCreateParams, - header: TransactionHeader, -) -> Result { - let mut builder = AssetConfigTransactionBuilder::default(); - builder.header(header).asset_id(0).total(params.total); - - if let Some(decimals) = params.decimals { - builder.decimals(decimals); - } - - if let Some(default_frozen) = params.default_frozen { - builder.default_frozen(default_frozen); - } - - if let Some(ref asset_name) = params.asset_name { - builder.asset_name(asset_name.clone()); - } - - if let Some(ref unit_name) = params.unit_name { - builder.unit_name(unit_name.clone()); - } - - if let Some(ref url) = params.url { - builder.url(url.clone()); - } - - if let Some(metadata_hash) = params.metadata_hash { - builder.metadata_hash(metadata_hash); - } - - if let Some(ref manager) = params.manager { - builder.manager(manager.clone()); - } - - if let Some(ref reserve) = params.reserve { - builder.reserve(reserve.clone()); - } - - if let Some(ref freeze) = params.freeze { - builder.freeze(freeze.clone()); - } - - if let Some(ref clawback) = params.clawback { - builder.clawback(clawback.clone()); - } - - builder.build().map_err(|e| e.to_string()) -} - -pub fn build_asset_config(params: &AssetConfigParams, header: TransactionHeader) -> Transaction { - Transaction::AssetConfig(AssetConfigTransactionFields { - header, - asset_id: params.asset_id, - total: None, - decimals: None, - default_frozen: None, - asset_name: None, - unit_name: None, - url: None, - metadata_hash: None, - manager: params.manager.clone(), - reserve: params.reserve.clone(), - freeze: params.freeze.clone(), - clawback: params.clawback.clone(), - }) -} - -pub fn build_asset_destroy(params: &AssetDestroyParams, header: TransactionHeader) -> Transaction { - Transaction::AssetConfig(AssetConfigTransactionFields { - header, - asset_id: params.asset_id, - total: None, - decimals: None, - default_frozen: None, - asset_name: None, - unit_name: None, - url: None, - metadata_hash: None, - manager: None, - reserve: None, - freeze: None, - clawback: None, - }) -} diff --git a/crates/algokit_utils/src/transactions/asset_freeze.rs b/crates/algokit_utils/src/transactions/asset_freeze.rs deleted file mode 100644 index 6b62510b2..000000000 --- a/crates/algokit_utils/src/transactions/asset_freeze.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::create_transaction_params; -use algokit_transact::{Address, AssetFreezeTransactionFields, Transaction, TransactionHeader}; - -create_transaction_params! { - /// Parameters for creating an asset freeze transaction. - #[derive(Clone, Default)] - pub struct AssetFreezeParams { - /// The ID of the asset to freeze - pub asset_id: u64, - /// The address of the account to freeze - pub target_address: Address, - } -} - -create_transaction_params! { - /// Parameters for creating an asset unfreeze transaction. - #[derive(Clone, Default)] - pub struct AssetUnfreezeParams { - /// The ID of the asset to unfreeze - pub asset_id: u64, - /// The address of the account to unfreeze - pub target_address: Address, - } -} - -pub fn build_asset_freeze(params: &AssetFreezeParams, header: TransactionHeader) -> Transaction { - Transaction::AssetFreeze(AssetFreezeTransactionFields { - header, - asset_id: params.asset_id, - freeze_target: params.target_address.clone(), - frozen: true, - }) -} - -pub fn build_asset_unfreeze( - params: &AssetUnfreezeParams, - header: TransactionHeader, -) -> Transaction { - Transaction::AssetFreeze(AssetFreezeTransactionFields { - header, - asset_id: params.asset_id, - freeze_target: params.target_address.clone(), - frozen: false, - }) -} diff --git a/crates/algokit_utils/src/transactions/asset_transfer.rs b/crates/algokit_utils/src/transactions/asset_transfer.rs deleted file mode 100644 index 56decb8f1..000000000 --- a/crates/algokit_utils/src/transactions/asset_transfer.rs +++ /dev/null @@ -1,173 +0,0 @@ -use crate::create_transaction_params; -use algokit_transact::{Address, AssetTransferTransactionFields, Transaction, TransactionHeader}; - -create_transaction_params! { - /// Parameters for creating an asset transfer transaction. - #[derive(Clone, Default)] - pub struct AssetTransferParams { - /// ID of the asset to transfer. - pub asset_id: u64, - /// The amount of the asset to transfer (in smallest divisible (decimal) units). - pub amount: u64, - /// The address of the account that will receive the asset unit(s). - pub receiver: Address, - } -} - -create_transaction_params! { - /// Parameters for creating an asset opt-in transaction. - #[derive(Clone, Default)] - pub struct AssetOptInParams { - /// ID of the asset that will be opted-in to. - pub asset_id: u64, - } -} - -create_transaction_params! { - /// Parameters for creating an asset opt-out transaction. - #[derive(Clone, Default)] - pub struct AssetOptOutParams { - /// ID of the asset that will be opted-out of. - pub asset_id: u64, - /// Optional address of an account to close the remaining asset position to. We recommend setting this to the asset creator. - /// - /// **Warning:** Be careful with this parameter as it can lead to loss of funds if not used correctly. - pub close_remainder_to: Option
, - } -} - -create_transaction_params! { - #[derive(Clone, Default)] - /// Parameters for creating an asset clawback transaction. - pub struct AssetClawbackParams { - /// ID of the asset to clawback. - pub asset_id: u64, - /// Amount of the asset to transfer (in smallest divisible (decimal) units). - pub amount: u64, - /// The address of the account that will receive the asset unit(s). - pub receiver: Address, - /// Address of an account to clawback the asset from. - /// - /// Requires the sender to be the clawback account. - /// - /// **Warning:** Be careful with this parameter as it can lead to unexpected loss of funds if not used correctly. - pub clawback_target: Address, - } -} -pub fn build_asset_transfer( - params: &AssetTransferParams, - header: TransactionHeader, -) -> Transaction { - Transaction::AssetTransfer(AssetTransferTransactionFields { - header, - asset_id: params.asset_id, - amount: params.amount, - receiver: params.receiver.clone(), - asset_sender: None, - close_remainder_to: None, - }) -} - -pub fn build_asset_opt_in(params: &AssetOptInParams, header: TransactionHeader) -> Transaction { - let sender = header.sender.clone(); - Transaction::AssetTransfer(AssetTransferTransactionFields { - header, - asset_id: params.asset_id, - amount: 0, - receiver: sender, - asset_sender: None, - close_remainder_to: None, - }) -} - -pub fn build_asset_opt_out(params: &AssetOptOutParams, header: TransactionHeader) -> Transaction { - let sender: Address = header.sender.clone(); - Transaction::AssetTransfer(AssetTransferTransactionFields { - header, - asset_id: params.asset_id, - amount: 0, - receiver: sender, - asset_sender: None, - close_remainder_to: params.close_remainder_to.clone(), - }) -} - -pub fn build_asset_clawback( - params: &AssetClawbackParams, - header: TransactionHeader, -) -> Transaction { - Transaction::AssetTransfer(AssetTransferTransactionFields { - header, - asset_id: params.asset_id, - amount: params.amount, - receiver: params.receiver.clone(), - asset_sender: Some(params.clawback_target.clone()), - close_remainder_to: None, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use algokit_transact::{TransactionHeader, test_utils::AccountMother}; - - #[test] - fn test_asset_opt_out_with_optional_close_remainder_to() { - // Use valid test addresses - let sender = AccountMother::neil().address(); - let creator = AccountMother::neil().address(); - - // Test with Some(creator) - explicit close_remainder_to - let params_with_creator = AssetOptOutParams { - sender: sender.clone(), - asset_id: 123, - close_remainder_to: Some(creator.clone()), - ..Default::default() - }; - - let header = TransactionHeader { - sender: sender.clone(), - fee: Some(1000), - first_valid: 1000, - last_valid: 1100, - genesis_hash: Some([0; 32]), - genesis_id: Some("test".to_string()), - lease: None, - note: None, - rekey_to: None, - group: None, - }; - - let tx = build_asset_opt_out(¶ms_with_creator, header.clone()); - - if let Transaction::AssetTransfer(fields) = tx { - assert_eq!(fields.asset_id, 123); - assert_eq!(fields.amount, 0); - assert_eq!(fields.receiver, sender); - assert_eq!(fields.close_remainder_to, Some(creator)); - } else { - panic!("Expected AssetTransfer transaction"); - } - - // Test with None - should pass None through (resolution happens at TransactionSender level) - let params_without_creator = AssetOptOutParams { - sender: sender.clone(), - asset_id: 456, - close_remainder_to: None, - ..Default::default() - }; - - let tx2 = build_asset_opt_out(¶ms_without_creator, header); - - if let Transaction::AssetTransfer(fields) = tx2 { - assert_eq!(fields.asset_id, 456); - assert_eq!(fields.amount, 0); - assert_eq!(fields.receiver, sender); - // When None is provided, build_asset_opt_out passes None through - // The actual creator resolution happens at the TransactionSender level - assert_eq!(fields.close_remainder_to, None); - } else { - panic!("Expected AssetTransfer transaction"); - } - } -} diff --git a/crates/algokit_utils/src/transactions/common.rs b/crates/algokit_utils/src/transactions/common.rs deleted file mode 100644 index 1203f3506..000000000 --- a/crates/algokit_utils/src/transactions/common.rs +++ /dev/null @@ -1,127 +0,0 @@ -use algokit_transact::{Address, EMPTY_SIGNATURE, SignedTransaction, Transaction}; -use async_trait::async_trait; -use derive_more::Debug; -use std::sync::{Arc, Mutex}; - -#[async_trait] -pub trait TransactionSigner: Send + Sync { - async fn sign_transactions( - &self, - transactions: &[Transaction], - indices: &[usize], - ) -> Result, String>; - - async fn sign_transaction( - &self, - transaction: &Transaction, - ) -> Result { - let result = self.sign_transactions(&[transaction.clone()], &[0]).await?; - Ok(result[0].clone()) - } -} - -pub trait TransactionSignerGetter: Send + Sync { - fn get_signer(&self, address: Address) -> Result, String>; -} - -impl TransactionSignerGetter for Mutex { - fn get_signer(&self, address: Address) -> Result, String> { - self.lock().map_err(|e| e.to_string())?.get_signer(address) - } -} - -#[derive(Clone)] -pub struct EmptySigner {} - -#[async_trait] -impl TransactionSigner for EmptySigner { - async fn sign_transactions( - &self, - txns: &[Transaction], - indices: &[usize], - ) -> Result, String> { - indices - .iter() - .map(|&idx| { - if idx < txns.len() { - Ok(SignedTransaction { - transaction: txns[idx].clone(), - signature: Some(EMPTY_SIGNATURE), - auth_address: None, - multisignature: None, - }) - } else { - Err(format!("Index {} out of bounds for transactions", idx)) - } - }) - .collect() - } -} - -impl TransactionSignerGetter for EmptySigner { - fn get_signer(&self, _address: Address) -> Result, String> { - Ok(Arc::new(self.clone())) - } -} - -#[macro_export] -macro_rules! create_transaction_params { - ( - $(#[$struct_attr:meta])* - pub struct $name:ident { - $( - $(#[$field_attr:meta])* - pub $field:ident: $field_type:ty, - )* - } - ) => { - $(#[$struct_attr])* - #[derive(derive_more::Debug)] - pub struct $name { - /// The address of the account sending the transaction. - pub sender: algokit_transact::Address, - #[debug(skip)] - /// A signer used to sign transaction(s); if not specified then - /// an attempt will be made to find a registered signer for the - /// given `sender` or use a default signer (if configured). - pub signer: Option>, - /// Change the signing key of the sender to the given address. - /// **Warning:** Please be careful with this parameter and be sure to read the [official rekey guidance](https://dev.algorand.co/concepts/accounts/rekeying). - pub rekey_to: Option, - /// Note to attach to the transaction. Max of 1000 bytes. - pub note: Option>, - /// Prevent multiple transactions with the same lease being included within the validity window. - /// - /// A [lease](https://dev.algorand.co/concepts/transactions/leases) - /// enforces a mutually exclusive transaction (useful to prevent double-posting and other scenarios). - pub lease: Option<[u8; 32]>, - /// The static transaction fee. In most cases you want to use extra fee unless setting the fee to 0 to be covered by another transaction. - pub static_fee: Option, - /// The fee to pay IN ADDITION to the suggested fee. Useful for manually covering inner transaction fees. - pub extra_fee: Option, - /// Throw an error if the fee for the transaction is more than this amount; prevents overspending on fees during high congestion periods. - pub max_fee: Option, - /// How many rounds the transaction should be valid for, if not specified then the registered default validity window will be used. - pub validity_window: Option, - /// Set the first round this transaction is valid. - /// If left undefined, the value from algod will be used. - /// - /// We recommend you only set this when you intentionally want this to be some time in the future. - pub first_valid_round: Option, - /// The last round this transaction is valid. It is recommended to use validity window instead. - pub last_valid_round: Option, - // Specific fields - $( - $(#[$field_attr])* - pub $field: $field_type, - )* - } - }; -} - -#[derive(Debug, Clone)] -pub struct TransactionWithSigner { - pub transaction: Transaction, - #[debug(skip)] - pub signer: Arc, -} diff --git a/crates/algokit_utils/src/transactions/composer.rs b/crates/algokit_utils/src/transactions/composer.rs deleted file mode 100644 index 78713bbd0..000000000 --- a/crates/algokit_utils/src/transactions/composer.rs +++ /dev/null @@ -1,2786 +0,0 @@ -use crate::config::{Config, EventData, EventType, TxnGroupSimulatedEventData}; -use crate::{ - genesis_id_is_localnet, - transactions::{ - common::TransactionSignerGetter, - key_registration::{ - build_non_participation_key_registration, build_offline_key_registration, - build_online_key_registration, - }, - payment::{build_account_close, build_payment}, - }, -}; -use algod_client::models::SimulateTransaction; -use algod_client::{ - AlgodClient, - apis::{Error as AlgodError, Format}, - models::{ - ApplicationLocalReference, AssetHoldingReference, BoxReference, PendingTransactionResponse, - SimulateRequest, SimulateRequestTransactionGroup, SimulateUnnamedResourcesAccessed, - TransactionParams, - }, -}; -use algokit_abi::{ABIError, ABIMethod, ABIReturn}; -use algokit_transact::{ - Address, AlgoKitTransactError, AlgorandMsgpack, Byte32, EMPTY_SIGNATURE, FeeParams, - MAX_ACCOUNT_REFERENCES, MAX_OVERALL_REFERENCES, MAX_TX_GROUP_SIZE, SignedTransaction, - Transaction, TransactionHeader, TransactionId, Transactions, -}; -use derive_more::Debug; -use snafu::Snafu; -use std::sync::Arc; - -use crate::{ - AppMethodCallArg, - transactions::{ - app_call::{ - AppCallMethodCallParams, ProcessedAppMethodCallArg, build_app_call_method_call, - build_app_create_method_call, build_app_delete_method_call, - build_app_update_method_call, - }, - common::TransactionWithSigner, - }, -}; - -use super::app_call::{ - AppCallParams, AppCreateMethodCallParams, AppCreateParams, AppDeleteMethodCallParams, - AppDeleteParams, AppUpdateMethodCallParams, AppUpdateParams, build_app_call, - build_app_create_call, build_app_delete_call, build_app_update_call, -}; -use super::asset_config::{ - AssetConfigParams, AssetCreateParams, AssetDestroyParams, build_asset_config, - build_asset_create, build_asset_destroy, -}; -use super::asset_freeze::{ - AssetFreezeParams, AssetUnfreezeParams, build_asset_freeze, build_asset_unfreeze, -}; -use super::asset_transfer::{ - AssetClawbackParams, AssetOptInParams, AssetOptOutParams, AssetTransferParams, - build_asset_clawback, build_asset_opt_in, build_asset_opt_out, build_asset_transfer, -}; -use super::common::TransactionSigner; -use super::key_registration::{ - NonParticipationKeyRegistrationParams, OfflineKeyRegistrationParams, - OnlineKeyRegistrationParams, -}; -use super::payment::{AccountCloseParams, PaymentParams}; - -// ABI return values are stored in logs with the prefix 0x151f7c75 -const ABI_RETURN_PREFIX: &[u8] = &[0x15, 0x1f, 0x7c, 0x75]; - -/// Configuration for application call resource population -#[derive(Debug, Clone)] -pub enum ResourcePopulation { - /// Resource population is disabled - Disabled, - /// Resource population is enabled with optional access list usage - Enabled { use_access_list: bool }, -} - -impl ResourcePopulation { - /// Returns true if resource population is enabled - pub fn is_enabled(&self) -> bool { - matches!(self, ResourcePopulation::Enabled { .. }) - } - - /// Returns true if access list should be used (only relevant when enabled) - pub fn use_access_list(&self) -> bool { - matches!( - self, - ResourcePopulation::Enabled { - use_access_list: true - } - ) - } -} - -impl Default for ResourcePopulation { - fn default() -> Self { - ResourcePopulation::Enabled { - use_access_list: false, - } - } -} - -trait HasTxnSigner { - fn signer_mut(&mut self) -> &mut Option>; -} - -fn set_method_signer_if_missing( - params: &mut impl HasTxnSigner, - method_signer: &Option>, -) { - if params.signer_mut().is_none() { - *params.signer_mut() = method_signer.clone(); - } -} - -impl HasTxnSigner for PaymentParams { - fn signer_mut(&mut self) -> &mut Option> { - &mut self.signer - } -} -impl HasTxnSigner for AccountCloseParams { - fn signer_mut(&mut self) -> &mut Option> { - &mut self.signer - } -} -impl HasTxnSigner for AssetTransferParams { - fn signer_mut(&mut self) -> &mut Option> { - &mut self.signer - } -} -impl HasTxnSigner for AssetOptInParams { - fn signer_mut(&mut self) -> &mut Option> { - &mut self.signer - } -} -impl HasTxnSigner for AssetOptOutParams { - fn signer_mut(&mut self) -> &mut Option> { - &mut self.signer - } -} -impl HasTxnSigner for AssetClawbackParams { - fn signer_mut(&mut self) -> &mut Option> { - &mut self.signer - } -} -impl HasTxnSigner for AssetCreateParams { - fn signer_mut(&mut self) -> &mut Option> { - &mut self.signer - } -} -impl HasTxnSigner for AssetConfigParams { - fn signer_mut(&mut self) -> &mut Option> { - &mut self.signer - } -} -impl HasTxnSigner for AssetDestroyParams { - fn signer_mut(&mut self) -> &mut Option> { - &mut self.signer - } -} -impl HasTxnSigner for AssetFreezeParams { - fn signer_mut(&mut self) -> &mut Option> { - &mut self.signer - } -} -impl HasTxnSigner for AssetUnfreezeParams { - fn signer_mut(&mut self) -> &mut Option> { - &mut self.signer - } -} - -/// Types of resources that can be populated at the group level -#[derive(Debug, Clone)] -enum GroupResourceToPopulate { - Account(String), - App(u64), - Asset(u64), - Box(BoxReference), - ExtraBoxRef, - AssetHolding(AssetHoldingReference), - AppLocal(ApplicationLocalReference), -} - -#[derive(Debug, Snafu)] -pub enum ComposerError { - #[snafu(display("Algod client error: {source}"))] - AlgodClientError { source: AlgodError }, - #[snafu(display("AlgoKit Transact error: {source}"))] - TransactError { source: AlgoKitTransactError }, - #[snafu(display("Decode Error: {message}"))] - DecodeError { message: String }, - #[snafu(display("Transaction Error: {message}"))] - TransactionError { message: String }, - #[snafu(display("Signing Error: {message}"))] - SigningError { message: String }, - #[snafu(display("Composer State Error: {message}"))] - StateError { message: String }, - #[snafu(display("Transaction pool error: {message}"))] - PoolError { message: String }, - #[snafu(display("Transaction group size exceeds the max limit of: {max}", max = MAX_TX_GROUP_SIZE))] - GroupSizeError, - #[snafu(display("Max wait round expired: {message}"))] - MaxWaitRoundExpired { message: String }, - #[snafu(display("ABI argument encoding error: {message}"))] - ABIEncodingError { message: String }, - #[snafu(display("ABI argument decoding error: {message}"))] - ABIDecodingError { message: String }, -} - -impl From for ComposerError { - fn from(e: AlgodError) -> Self { - Self::AlgodClientError { source: e } - } -} - -impl From for ComposerError { - fn from(e: AlgoKitTransactError) -> Self { - Self::TransactError { source: e } - } -} - -#[derive(Debug, Clone)] -pub struct TransactionResult { - pub transaction: Transaction, - pub transaction_id: String, - pub confirmation: PendingTransactionResponse, - pub abi_return: Option, -} - -#[derive(Debug)] -pub struct TransactionComposerSendResult { - pub group: Option, - pub results: Vec, -} - -#[derive(Debug, Clone, Default)] -pub struct SimulateParams { - pub allow_more_logging: Option, - pub allow_empty_signatures: Option, - pub allow_unnamed_resources: Option, - pub extra_opcode_budget: Option, - pub exec_trace_config: Option, - pub simulation_round: Option, - pub skip_signatures: bool, -} - -#[derive(Debug, Clone)] -pub struct TransactionComposerSimulateResult { - pub group: Option, - pub results: Vec, - pub simulate_response: SimulateTransaction, -} - -#[derive(Debug, Clone, Default)] -pub struct TransactionComposerConfig { - pub cover_app_call_inner_transaction_fees: bool, - pub populate_app_call_resources: ResourcePopulation, -} - -#[derive(Clone)] -pub struct TransactionComposerParams { - pub algod_client: Arc, - pub signer_getter: Arc, - pub composer_config: Option, -} - -#[derive(Debug, Clone, Default)] -pub struct SendParams { - pub max_rounds_to_wait_for_confirmation: Option, -} - -#[derive(Debug)] -struct TransactionAnalysis { - /// The fee difference required for this transaction - required_fee_delta: Option, - /// Resources that this specific transaction accessed but didn't declare - unnamed_resources_accessed: Option, -} - -#[derive(Debug)] -struct GroupAnalysis { - /// Analysis of each transaction in the group - transactions: Vec, - /// Resources accessed by the group that qualify for group resource sharing - unnamed_resources_accessed: Option, -} - -/// Represents the fee difference for a transaction -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum FeeDelta { - /// Transaction has an insufficient fee (needs more) - Deficit(u64), - /// Transaction has an excess fee (can cover other transactions in the group) - Surplus(u64), -} - -impl FeeDelta { - /// Create a FeeDelta from an i64 value (positive = deficit, negative = surplus, zero = none) - pub fn from_i64(value: i64) -> Option { - match value.cmp(&0) { - std::cmp::Ordering::Greater => Some(FeeDelta::Deficit(value as u64)), - std::cmp::Ordering::Less => Some(FeeDelta::Surplus((-value) as u64)), - std::cmp::Ordering::Equal => None, - } - } - - /// Convert to i64 representation (positive = deficit, negative = surplus) - pub fn to_i64(self) -> i64 { - match self { - FeeDelta::Deficit(amount) => amount as i64, - FeeDelta::Surplus(amount) => -(amount as i64), - } - } - - /// Check if this represents a deficit (needs more fees) - pub fn is_deficit(&self) -> bool { - matches!(self, FeeDelta::Deficit(_)) - } - - /// Check if this represents a surplus (has excess fees) - pub fn is_surplus(&self) -> bool { - matches!(self, FeeDelta::Surplus(_)) - } - - /// Get the amount regardless of whether it's deficit or surplus - pub fn amount(&self) -> u64 { - match self { - FeeDelta::Deficit(amount) | FeeDelta::Surplus(amount) => *amount, - } - } -} - -impl std::ops::Add for FeeDelta { - type Output = Option; - - fn add(self, rhs: FeeDelta) -> Self::Output { - FeeDelta::from_i64(self.to_i64() + rhs.to_i64()) - } -} - -// Priority levels for fee coverage -// By default PartialOrd and Ord provide the correct sorting logic, based the enum variant order. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -enum FeePriority { - /// Covered / Non-deficit transactions (lowest priority) - Covered, - /// App call transactions with deficits that can be modified - ModifiableDeficit(u64), - /// Non app call or immutable fee transactions with deficits (highest priority) - ImmutableDeficit(u64), -} - -#[derive(Debug, Clone)] -pub enum ComposerTransaction { - Transaction(Transaction), - TransactionWithSigner(TransactionWithSigner), - Payment(PaymentParams), - AccountClose(AccountCloseParams), - AssetTransfer(AssetTransferParams), - AssetOptIn(AssetOptInParams), - AssetOptOut(AssetOptOutParams), - AssetClawback(AssetClawbackParams), - AssetCreate(AssetCreateParams), - AssetConfig(AssetConfigParams), - AssetDestroy(AssetDestroyParams), - AssetFreeze(AssetFreezeParams), - AssetUnfreeze(AssetUnfreezeParams), - AppCall(AppCallParams), - AppCreateCall(AppCreateParams), - AppUpdateCall(AppUpdateParams), - AppDeleteCall(AppDeleteParams), - AppCallMethodCall(AppCallMethodCallParams), - AppCreateMethodCall(AppCreateMethodCallParams), - AppUpdateMethodCall(AppUpdateMethodCallParams), - AppDeleteMethodCall(AppDeleteMethodCallParams), - OnlineKeyRegistration(OnlineKeyRegistrationParams), - OfflineKeyRegistration(OfflineKeyRegistrationParams), - NonParticipationKeyRegistration(NonParticipationKeyRegistrationParams), -} - -macro_rules! get_composer_transaction_field { - ($field:ident, $field_type:ty, $get_expr:expr, $default_expr:expr) => { - pub fn $field(&self) -> $field_type { - match self { - $crate::transactions::composer::ComposerTransaction::Payment(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::AccountClose(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::AssetTransfer(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::AssetOptIn(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::AssetOptOut(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::AssetClawback(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::AssetCreate(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::AssetConfig(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::AssetDestroy(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::AssetFreeze(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::AssetUnfreeze(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::AppCall(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::AppCreateCall(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::AppUpdateCall(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::AppDeleteCall(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::AppCallMethodCall(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::AppCreateMethodCall(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::AppUpdateMethodCall(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::AppDeleteMethodCall(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::OnlineKeyRegistration(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::OfflineKeyRegistration(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::NonParticipationKeyRegistration(params) => $get_expr(¶ms.$field), - $crate::transactions::composer::ComposerTransaction::Transaction(_) => $default_expr, - $crate::transactions::composer::ComposerTransaction::TransactionWithSigner(_) => $default_expr, - } - } - }; -} - -impl ComposerTransaction { - // Generate field accessor methods - get_composer_transaction_field!( - sender, - algokit_transact::Address, - |x: &algokit_transact::Address| x.clone(), - Default::default() - ); - get_composer_transaction_field!( - signer, - Option>, - |x: &Option>| x.clone(), - None - ); - get_composer_transaction_field!( - rekey_to, - Option, - |x: &Option| x.clone(), - None - ); - get_composer_transaction_field!(note, Option>, |x: &Option>| x.clone(), None); - get_composer_transaction_field!(lease, Option<[u8; 32]>, |x: &Option<[u8; 32]>| *x, None); - get_composer_transaction_field!(static_fee, Option, |x: &Option| *x, None); - get_composer_transaction_field!(extra_fee, Option, |x: &Option| *x, None); - get_composer_transaction_field!(max_fee, Option, |x: &Option| *x, None); - get_composer_transaction_field!(validity_window, Option, |x: &Option| *x, None); - get_composer_transaction_field!(first_valid_round, Option, |x: &Option| *x, None); - get_composer_transaction_field!(last_valid_round, Option, |x: &Option| *x, None); - - /// Get the logical maximum fee based on static_fee and max_fee - pub fn logical_max_fee(&self) -> Option { - let max_fee = self.max_fee(); - let static_fee = self.static_fee(); - match (max_fee, static_fee) { - (Some(max_fee_value), static_fee) if max_fee_value > static_fee.unwrap_or(0) => max_fee, - _ => static_fee, - } - } - - pub fn is_app_call(&self) -> bool { - matches!( - self, - ComposerTransaction::AppCall(_) - | ComposerTransaction::AppCreateCall(_) - | ComposerTransaction::AppUpdateCall(_) - | ComposerTransaction::AppDeleteCall(_) - | ComposerTransaction::AppCallMethodCall(_) - | ComposerTransaction::AppCreateMethodCall(_) - | ComposerTransaction::AppUpdateMethodCall(_) - | ComposerTransaction::AppDeleteMethodCall(_) - | ComposerTransaction::Transaction(Transaction::AppCall(_)) - | ComposerTransaction::TransactionWithSigner(TransactionWithSigner { - transaction: Transaction::AppCall(_), - .. - }) - ) - } -} - -#[derive(Clone)] -pub struct TransactionComposer { - algod_client: Arc, - signer_getter: Arc, - composer_config: TransactionComposerConfig, - transactions: Vec, - built_group: Option>, - signed_group: Option>, -} - -impl TransactionComposer { - pub fn new(params: TransactionComposerParams) -> Self { - TransactionComposer { - algod_client: params.algod_client, - signer_getter: params.signer_getter, - composer_config: params.composer_config.unwrap_or_default(), - transactions: Vec::new(), - built_group: None, - signed_group: None, - } - } - - fn push(&mut self, txn: ComposerTransaction) -> Result<(), ComposerError> { - if self.built_group.is_some() { - return Err(ComposerError::StateError { - message: String::from("Cannot add new transactions after building"), - }); - } - - if self.transactions.len() >= MAX_TX_GROUP_SIZE { - return Err(ComposerError::GroupSizeError); - } - self.transactions.push(txn); - Ok(()) - } - - fn get_method_from_transaction<'a>( - &self, - transaction: &'a ComposerTransaction, - ) -> Option<&'a ABIMethod> { - match transaction { - ComposerTransaction::AppCallMethodCall(params) => Some(¶ms.method), - ComposerTransaction::AppCreateMethodCall(params) => Some(¶ms.method), - ComposerTransaction::AppUpdateMethodCall(params) => Some(¶ms.method), - ComposerTransaction::AppDeleteMethodCall(params) => Some(¶ms.method), - _ => None, - } - } - - pub fn add_payment(&mut self, params: PaymentParams) -> Result<(), ComposerError> { - self.push(ComposerTransaction::Payment(params)) - } - - pub fn add_account_close(&mut self, params: AccountCloseParams) -> Result<(), ComposerError> { - self.push(ComposerTransaction::AccountClose(params)) - } - - pub fn add_asset_transfer(&mut self, params: AssetTransferParams) -> Result<(), ComposerError> { - self.push(ComposerTransaction::AssetTransfer(params)) - } - - pub fn add_asset_opt_in(&mut self, params: AssetOptInParams) -> Result<(), ComposerError> { - self.push(ComposerTransaction::AssetOptIn(params)) - } - - pub fn add_asset_opt_out(&mut self, params: AssetOptOutParams) -> Result<(), ComposerError> { - self.push(ComposerTransaction::AssetOptOut(params)) - } - - pub fn add_asset_clawback(&mut self, params: AssetClawbackParams) -> Result<(), ComposerError> { - self.push(ComposerTransaction::AssetClawback(params)) - } - - pub fn add_asset_create(&mut self, params: AssetCreateParams) -> Result<(), ComposerError> { - self.push(ComposerTransaction::AssetCreate(params)) - } - - pub fn add_asset_config(&mut self, params: AssetConfigParams) -> Result<(), ComposerError> { - self.push(ComposerTransaction::AssetConfig(params)) - } - - pub fn add_asset_destroy(&mut self, params: AssetDestroyParams) -> Result<(), ComposerError> { - self.push(ComposerTransaction::AssetDestroy(params)) - } - - pub fn add_asset_freeze(&mut self, params: AssetFreezeParams) -> Result<(), ComposerError> { - self.push(ComposerTransaction::AssetFreeze(params)) - } - - pub fn add_asset_unfreeze(&mut self, params: AssetUnfreezeParams) -> Result<(), ComposerError> { - self.push(ComposerTransaction::AssetUnfreeze(params)) - } - - pub fn add_online_key_registration( - &mut self, - params: OnlineKeyRegistrationParams, - ) -> Result<(), ComposerError> { - self.push(ComposerTransaction::OnlineKeyRegistration(params)) - } - - pub fn add_offline_key_registration( - &mut self, - params: OfflineKeyRegistrationParams, - ) -> Result<(), ComposerError> { - self.push(ComposerTransaction::OfflineKeyRegistration(params)) - } - - pub fn add_non_participation_key_registration( - &mut self, - params: NonParticipationKeyRegistrationParams, - ) -> Result<(), ComposerError> { - self.push(ComposerTransaction::NonParticipationKeyRegistration(params)) - } - - pub fn add_app_call(&mut self, params: AppCallParams) -> Result<(), ComposerError> { - self.push(ComposerTransaction::AppCall(params)) - } - - pub fn add_app_create(&mut self, params: AppCreateParams) -> Result<(), ComposerError> { - self.push(ComposerTransaction::AppCreateCall(params)) - } - - pub fn add_app_update(&mut self, params: AppUpdateParams) -> Result<(), ComposerError> { - self.push(ComposerTransaction::AppUpdateCall(params)) - } - - pub fn add_app_delete(&mut self, params: AppDeleteParams) -> Result<(), ComposerError> { - self.push(ComposerTransaction::AppDeleteCall(params)) - } - - fn extract_composer_transactions_from_app_method_call_params( - method_call_args: &[AppMethodCallArg], - method_signer: Option>, - ) -> Vec { - let mut composer_transactions: Vec = vec![]; - - for arg in method_call_args.iter() { - match arg { - AppMethodCallArg::Transaction(transaction) => { - if let Some(ref signer) = method_signer { - composer_transactions.push(ComposerTransaction::TransactionWithSigner( - TransactionWithSigner { - transaction: transaction.clone(), - signer: signer.clone(), - }, - )); - } else { - composer_transactions - .push(ComposerTransaction::Transaction(transaction.clone())); - } - } - AppMethodCallArg::TransactionWithSigner(transaction) => { - composer_transactions.push(ComposerTransaction::TransactionWithSigner( - transaction.clone(), - )); - } - AppMethodCallArg::Payment(params) => { - let mut p = params.clone(); - set_method_signer_if_missing(&mut p, &method_signer); - composer_transactions.push(ComposerTransaction::Payment(p)); - } - AppMethodCallArg::AccountClose(params) => { - let mut p = params.clone(); - set_method_signer_if_missing(&mut p, &method_signer); - composer_transactions.push(ComposerTransaction::AccountClose(p)); - } - AppMethodCallArg::AssetTransfer(params) => { - let mut p = params.clone(); - set_method_signer_if_missing(&mut p, &method_signer); - composer_transactions.push(ComposerTransaction::AssetTransfer(p)); - } - AppMethodCallArg::AssetOptIn(params) => { - let mut p = params.clone(); - set_method_signer_if_missing(&mut p, &method_signer); - composer_transactions.push(ComposerTransaction::AssetOptIn(p)); - } - AppMethodCallArg::AssetOptOut(params) => { - let mut p = params.clone(); - set_method_signer_if_missing(&mut p, &method_signer); - composer_transactions.push(ComposerTransaction::AssetOptOut(p)); - } - AppMethodCallArg::AssetClawback(params) => { - let mut p = params.clone(); - set_method_signer_if_missing(&mut p, &method_signer); - composer_transactions.push(ComposerTransaction::AssetClawback(p)); - } - AppMethodCallArg::AssetCreate(params) => { - let mut p = params.clone(); - set_method_signer_if_missing(&mut p, &method_signer); - composer_transactions.push(ComposerTransaction::AssetCreate(p)); - } - AppMethodCallArg::AssetConfig(params) => { - let mut p = params.clone(); - set_method_signer_if_missing(&mut p, &method_signer); - composer_transactions.push(ComposerTransaction::AssetConfig(p)); - } - AppMethodCallArg::AssetDestroy(params) => { - let mut p = params.clone(); - set_method_signer_if_missing(&mut p, &method_signer); - composer_transactions.push(ComposerTransaction::AssetDestroy(p)); - } - AppMethodCallArg::AssetFreeze(params) => { - let mut p = params.clone(); - set_method_signer_if_missing(&mut p, &method_signer); - composer_transactions.push(ComposerTransaction::AssetFreeze(p)); - } - AppMethodCallArg::AssetUnfreeze(params) => { - let mut p = params.clone(); - set_method_signer_if_missing(&mut p, &method_signer); - composer_transactions.push(ComposerTransaction::AssetUnfreeze(p)); - } - AppMethodCallArg::AppCall(params) => { - composer_transactions.push(ComposerTransaction::AppCall(params.clone())); - } - AppMethodCallArg::AppCreateCall(params) => { - composer_transactions.push(ComposerTransaction::AppCreateCall(params.clone())); - } - AppMethodCallArg::AppUpdateCall(params) => { - composer_transactions.push(ComposerTransaction::AppUpdateCall(params.clone())); - } - AppMethodCallArg::AppDeleteCall(params) => { - composer_transactions.push(ComposerTransaction::AppDeleteCall(params.clone())); - } - AppMethodCallArg::AppCallMethodCall(params) => { - let nested_composer_transactions = - Self::extract_composer_transactions_from_app_method_call_params( - ¶ms.args, - params.signer.clone(), - ); - composer_transactions.extend(nested_composer_transactions); - - composer_transactions - .push(ComposerTransaction::AppCallMethodCall(params.into())); - } - AppMethodCallArg::AppCreateMethodCall(params) => { - let nested_composer_transactions = - Self::extract_composer_transactions_from_app_method_call_params( - ¶ms.args, - params.signer.clone(), - ); - composer_transactions.extend(nested_composer_transactions); - - composer_transactions - .push(ComposerTransaction::AppCreateMethodCall(params.into())); - } - AppMethodCallArg::AppUpdateMethodCall(params) => { - let nested_composer_transactions = - Self::extract_composer_transactions_from_app_method_call_params( - ¶ms.args, - params.signer.clone(), - ); - composer_transactions.extend(nested_composer_transactions); - - composer_transactions - .push(ComposerTransaction::AppUpdateMethodCall(params.into())); - } - AppMethodCallArg::AppDeleteMethodCall(params) => { - let nested_composer_transactions = - Self::extract_composer_transactions_from_app_method_call_params( - ¶ms.args, - params.signer.clone(), - ); - composer_transactions.extend(nested_composer_transactions); - - composer_transactions - .push(ComposerTransaction::AppDeleteMethodCall(params.into())); - } - _ => {} - }; - } - - composer_transactions - } - - fn add_app_method_call_internal( - &mut self, - args: &[AppMethodCallArg], - transaction: ComposerTransaction, - method_signer: Option>, - ) -> Result<(), ComposerError> { - let starting_index = self.transactions.len(); - let mut composer_transactions = - Self::extract_composer_transactions_from_app_method_call_params(args, method_signer); - composer_transactions.push(transaction); - - if self.transactions.len() + composer_transactions.len() > MAX_TX_GROUP_SIZE { - return Err(ComposerError::GroupSizeError); - } - - for (offset, composer_transaction) in composer_transactions.into_iter().enumerate() { - // If this is a method call with a signer set, attach it directly to preceding bare txn args - let maybe_signer: Option> = match &composer_transaction { - ComposerTransaction::AppCallMethodCall(p) => p.signer.clone(), - ComposerTransaction::AppCreateMethodCall(p) => p.signer.clone(), - ComposerTransaction::AppUpdateMethodCall(p) => p.signer.clone(), - ComposerTransaction::AppDeleteMethodCall(p) => p.signer.clone(), - _ => None, - }; - if let Some(signer) = maybe_signer { - let end_exclusive = starting_index + offset; - for idx in starting_index..end_exclusive { - match self.transactions.get_mut(idx) { - Some(ComposerTransaction::Transaction(tx)) => { - // Upgrade to TransactionWithSigner if not already signed - let tx_clone = tx.clone(); - *self.transactions.get_mut(idx).unwrap() = - ComposerTransaction::TransactionWithSigner(TransactionWithSigner { - transaction: tx_clone, - signer: signer.clone(), - }); - } - Some(other) => { - let signer_slot = match other { - ComposerTransaction::Payment(p) => Some(&mut p.signer), - ComposerTransaction::AccountClose(p) => Some(&mut p.signer), - ComposerTransaction::AssetTransfer(p) => Some(&mut p.signer), - ComposerTransaction::AssetOptIn(p) => Some(&mut p.signer), - ComposerTransaction::AssetOptOut(p) => Some(&mut p.signer), - ComposerTransaction::AssetClawback(p) => Some(&mut p.signer), - ComposerTransaction::AssetCreate(p) => Some(&mut p.signer), - ComposerTransaction::AssetConfig(p) => Some(&mut p.signer), - ComposerTransaction::AssetDestroy(p) => Some(&mut p.signer), - ComposerTransaction::AssetFreeze(p) => Some(&mut p.signer), - ComposerTransaction::AssetUnfreeze(p) => Some(&mut p.signer), - ComposerTransaction::AppCall(p) => Some(&mut p.signer), - ComposerTransaction::AppCreateCall(p) => Some(&mut p.signer), - ComposerTransaction::AppUpdateCall(p) => Some(&mut p.signer), - ComposerTransaction::AppDeleteCall(p) => Some(&mut p.signer), - ComposerTransaction::OnlineKeyRegistration(p) => { - Some(&mut p.signer) - } - ComposerTransaction::OfflineKeyRegistration(p) => { - Some(&mut p.signer) - } - ComposerTransaction::NonParticipationKeyRegistration(p) => { - Some(&mut p.signer) - } - _ => None, - }; - if let Some(slot) = signer_slot { - slot.get_or_insert_with(|| signer.clone()); - } - } - _ => {} - } - } - } - - self.push(composer_transaction)?; - } - - Ok(()) - } - - pub fn add_app_call_method_call( - &mut self, - params: AppCallMethodCallParams, - ) -> Result<(), ComposerError> { - self.add_app_method_call_internal( - ¶ms.args, - ComposerTransaction::AppCallMethodCall((¶ms).into()), - params.signer.clone(), - ) - } - - pub fn add_app_create_method_call( - &mut self, - params: AppCreateMethodCallParams, - ) -> Result<(), ComposerError> { - self.add_app_method_call_internal( - ¶ms.args, - ComposerTransaction::AppCreateMethodCall((¶ms).into()), - params.signer.clone(), - ) - } - - pub fn add_app_update_method_call( - &mut self, - params: AppUpdateMethodCallParams, - ) -> Result<(), ComposerError> { - self.add_app_method_call_internal( - ¶ms.args, - ComposerTransaction::AppUpdateMethodCall((¶ms).into()), - params.signer.clone(), - ) - } - - pub fn add_app_delete_method_call( - &mut self, - params: AppDeleteMethodCallParams, - ) -> Result<(), ComposerError> { - self.add_app_method_call_internal( - ¶ms.args, - ComposerTransaction::AppDeleteMethodCall((¶ms).into()), - params.signer.clone(), - ) - } - - fn parse_abi_return_values( - &self, - confirmations: &[PendingTransactionResponse], - ) -> Vec> { - confirmations - .iter() - .enumerate() - .map(|(i, confirmation)| { - self.transactions - .get(i) - .and_then(|transaction| self.get_method_from_transaction(transaction)) - .map(|method| Self::extract_abi_return_from_logs(confirmation, method)) - }) - .collect() - } - - pub(crate) fn extract_abi_return_from_logs( - confirmation: &PendingTransactionResponse, - method: &ABIMethod, - ) -> ABIReturn { - // Check if method has return type - let return_type = match method.returns.as_ref() { - Some(return_type) => return_type, - None => { - return ABIReturn { - method: method.clone(), - raw_return_value: vec![], - return_value: None, - decode_error: None, - }; - } // Method has no return type - }; - - // Non-void method - must examine the last log - let last_log = match confirmation.logs.as_ref().and_then(|logs| logs.last()) { - Some(log) => log, - None => { - return ABIReturn { - method: method.clone(), - raw_return_value: vec![], - return_value: None, - decode_error: Some(ABIError::DecodingError { - message: format!( - "No logs found for method {} which requires a return type", - method.name - ), - }), - }; - } - }; - - // Check if the last log entry has the ABI return prefix - if !last_log.starts_with(ABI_RETURN_PREFIX) { - return ABIReturn { - method: method.clone(), - raw_return_value: vec![], - return_value: None, - decode_error: Some(ABIError::DecodingError { - message: format!( - "Transaction log for method {} doesn't match with ABI return value format", - method.name - ), - }), - }; - } - - // Extract the return value bytes (skip the prefix) - let return_bytes = &last_log[ABI_RETURN_PREFIX.len()..]; - - // Decode the return value using the method's return type - match return_type.decode(return_bytes) { - Ok(return_value) => ABIReturn { - method: method.clone(), - raw_return_value: return_bytes.to_vec(), - return_value: Some(return_value), - decode_error: None, - }, - Err(e) => ABIReturn { - method: method.clone(), - raw_return_value: vec![], - return_value: None, - decode_error: Some(ABIError::DecodingError { - message: format!( - "Failed to decode ABI return value for method {}: {}", - method.name, e - ), - }), - }, - } - } - - pub fn add_transaction( - &mut self, - transaction: Transaction, - signer: Option>, - ) -> Result<(), ComposerError> { - if let Some(group) = &transaction.header().group { - if group != &[0u8; 32] { - return Err(ComposerError::TransactionError { - message: "Cannot add a transaction with nonzero group".to_string(), - }); - } - } - match signer { - Some(signer) => self.push(ComposerTransaction::TransactionWithSigner( - TransactionWithSigner { - transaction, - signer, - }, - )), - None => self.push(ComposerTransaction::Transaction(transaction)), - } - } - - async fn analyze_group_requirements( - &self, - suggested_params: &TransactionParams, - default_validity_window: &u32, - composer_config: &TransactionComposerConfig, - ) -> Result { - let mut app_call_indexes_without_max_fees = Vec::new(); - - let built_transactions = &self - .build_transactions(suggested_params, default_validity_window, None) - .await?; - - let mut transactions_to_simulate = built_transactions - .iter() - .enumerate() - .map(|(group_index, txn)| { - let ctxn = &self.transactions[group_index]; - let mut txn_to_simulate = txn.clone(); - let txn_header = txn_to_simulate.header_mut(); - txn_header.group = None; - if composer_config.cover_app_call_inner_transaction_fees { - if let Transaction::AppCall(_) = txn { - match ctxn.logical_max_fee() { - Some(logical_max_fee) => txn_header.fee = Some(logical_max_fee), - None => app_call_indexes_without_max_fees.push(group_index), - } - } - } - txn_to_simulate - }) - .collect::>(); - - // Regroup the transactions, as the transactions have likely been adjusted - if transactions_to_simulate.len() > 1 { - transactions_to_simulate = transactions_to_simulate.assign_group().map_err(|e| { - ComposerError::TransactionError { - message: format!("Failed to assign group: {}", e), - } - })?; - } - - if composer_config.cover_app_call_inner_transaction_fees - && !app_call_indexes_without_max_fees.is_empty() - { - return Err(ComposerError::StateError { - message: format!( - "Please provide a max fee for each app call transaction when inner transaction fee coverage is enabled. Required for transaction {}", - app_call_indexes_without_max_fees - .iter() - .map(|i| i.to_string()) - .collect::>() - .join(", ") - ), - }); - } - - let signed_transactions = transactions_to_simulate - .into_iter() - .map(|txn| SignedTransaction { - transaction: txn, - signature: Some(EMPTY_SIGNATURE), - auth_address: None, - multisignature: None, - }) - .collect(); - - let txn_group = SimulateRequestTransactionGroup { - txns: signed_transactions, - }; - let simulate_request = SimulateRequest { - txn_groups: vec![txn_group], - allow_unnamed_resources: Some(true), - allow_empty_signatures: Some(true), - fix_signers: Some(true), - ..Default::default() - }; - - let response: algod_client::models::SimulateTransaction = self - .algod_client - .simulate_transaction(simulate_request, Some(Format::Msgpack)) - .await - .map_err(|e| ComposerError::AlgodClientError { source: e })?; - let group_response = &response.txn_groups[0]; - - // Handle any simulation failures - if let Some(failure_message) = &group_response.failure_message { - if composer_config.cover_app_call_inner_transaction_fees - && failure_message.contains("fee too small") - { - return Err(ComposerError::StateError { - message: "Fees were too small to analyze group requirements via simulate. You may need to increase an app call transaction max fee.".to_string(), - }); - } - - let failed_at = group_response - .failed_at - .as_ref() - .map(|group_index| { - group_index - .iter() - .map(|i| i.to_string()) - .collect::>() - .join(", ") - }) - .unwrap_or_else(|| "unknown".to_string()); - - return Err(ComposerError::StateError { - message: format!( - "Error analyzing group requirements via simulate in transaction {}: {}", - failed_at, failure_message - ), - }); - } - - let txn_analysis_results: Result, ComposerError> = group_response - .txn_results - .iter() - .enumerate() - .map(|(group_index, simulate_txn_result)| { - let btxn = &built_transactions[group_index]; - - let required_fee_delta = if composer_config.cover_app_call_inner_transaction_fees { - let min_txn_fee: u64 = btxn - .calculate_fee(FeeParams { - fee_per_byte: suggested_params.fee, - min_fee: suggested_params.min_fee, - ..Default::default() - }) - .map_err(|e| ComposerError::TransactionError { - message: format!("Failed to calculate min transaction fee: {}", e), - })?; - - let txn_fee = btxn.header().fee.unwrap_or(0); - let txn_fee_delta = FeeDelta::from_i64(min_txn_fee as i64 - txn_fee as i64); - - match btxn { - Transaction::AppCall(_) => { - // Calculate inner transaction fee delta - let inner_txns_fee_delta = Self::calculate_inner_fee_delta( - &simulate_txn_result.txn_result.inner_txns, - suggested_params.min_fee, - None, - ); - FeeDelta::from_i64( - inner_txns_fee_delta.map(FeeDelta::to_i64).unwrap_or(0) - + txn_fee_delta.map(FeeDelta::to_i64).unwrap_or(0), - ) - } - _ => txn_fee_delta, - } - } else { - None - }; - - Ok(TransactionAnalysis { - required_fee_delta, - unnamed_resources_accessed: if composer_config - .populate_app_call_resources - .is_enabled() - { - simulate_txn_result.unnamed_resources_accessed.clone() - } else { - None - }, - }) - }) - .collect(); - - Ok(GroupAnalysis { - transactions: txn_analysis_results?, - unnamed_resources_accessed: if composer_config.populate_app_call_resources.is_enabled() - { - group_response.unnamed_resources_accessed.clone() - } else { - None - }, - }) - } - - fn calculate_inner_fee_delta( - inner_transactions: &Option>, - min_transaction_fee: u64, - acc: Option, - ) -> Option { - match inner_transactions { - Some(txns) => { - // Surplus inner transaction fees do not pool up to the parent transaction. - // Additionally surplus inner transaction fees only pool from sibling transactions that are sent prior to a given inner transaction, hence why we iterate in reverse order. - txns.iter().rev().fold(acc, |acc, inner_txn| { - let recursive_delta = Self::calculate_inner_fee_delta( - &inner_txn.inner_txns, - min_transaction_fee, - acc, - ); - let txn_fee_delta = FeeDelta::from_i64( - min_transaction_fee as i64 // Inner transactions don't require per byte fees - - inner_txn.txn.transaction.header().fee.unwrap_or(0) as i64, - ); - - let current_fee_delta = FeeDelta::from_i64( - recursive_delta.map(FeeDelta::to_i64).unwrap_or(0) - + txn_fee_delta.map(FeeDelta::to_i64).unwrap_or(0), - ); - - // If after the recursive inner fee calculations we have a surplus, - // return None to avoid pooling up surplus fees, which is not allowed. - match current_fee_delta { - Some(delta) if delta.is_surplus() => None, - _ => current_fee_delta, - } - }) - } - None => acc, - } - } - - fn build_transaction_header( - &self, - composer_transaction: &ComposerTransaction, - suggested_params: &TransactionParams, - default_validity_window: u32, - ) -> Result { - let first_valid = composer_transaction - .first_valid_round() - .unwrap_or(suggested_params.last_round); - - Ok(TransactionHeader { - sender: composer_transaction.sender(), - rekey_to: composer_transaction.rekey_to(), - note: composer_transaction.note(), - lease: composer_transaction.lease(), - fee: composer_transaction.static_fee(), - genesis_id: Some(suggested_params.genesis_id.clone()), - genesis_hash: Some( - suggested_params - .genesis_hash - .clone() - .try_into() - .map_err(|_e| ComposerError::DecodeError { - message: "Invalid genesis hash".to_string(), - })?, - ), - first_valid, - last_valid: composer_transaction.last_valid_round().unwrap_or_else(|| { - composer_transaction - .validity_window() - .map(|window| first_valid + window as u64) - .unwrap_or(first_valid + default_validity_window as u64) - }), - group: None, - }) - } - - pub async fn get_suggested_params(&self) -> Result { - Ok(self.algod_client.transaction_params().await?) - } - - async fn build_transactions( - &self, - suggested_params: &TransactionParams, - default_validity_window: &u32, - group_analysis: Option, - ) -> Result, ComposerError> { - let mut transactions = self - .transactions - .iter() - .map(|ctxn| -> Result { - let header = self.build_transaction_header( - ctxn, - suggested_params, - *default_validity_window, - )?; - let mut calculate_fee = header.fee.is_none(); - - let mut transaction = match ctxn { - ComposerTransaction::Transaction(tx) => { - calculate_fee = false; - tx.clone() - } - ComposerTransaction::TransactionWithSigner(tx_with_signer) => { - calculate_fee = false; - tx_with_signer.transaction.clone() - } - ComposerTransaction::Payment(params) => build_payment(params, header), - ComposerTransaction::AccountClose(params) => { - build_account_close(params, header) - } - ComposerTransaction::AssetTransfer(params) => { - build_asset_transfer(params, header) - } - ComposerTransaction::AssetOptIn(params) => build_asset_opt_in(params, header), - ComposerTransaction::AssetOptOut(params) => build_asset_opt_out(params, header), - ComposerTransaction::AssetClawback(params) => { - build_asset_clawback(params, header) - } - ComposerTransaction::AssetCreate(params) => build_asset_create(params, header) - .map_err(|e| ComposerError::TransactionError { - message: e.to_string(), - })?, - ComposerTransaction::AssetConfig(params) => build_asset_config(params, header), - ComposerTransaction::AssetDestroy(params) => { - build_asset_destroy(params, header) - } - ComposerTransaction::AssetFreeze(params) => build_asset_freeze(params, header), - ComposerTransaction::AssetUnfreeze(params) => { - build_asset_unfreeze(params, header) - } - ComposerTransaction::AppCall(params) => build_app_call(params, header) - .map_err(|e| ComposerError::TransactionError { - message: e.to_string(), - })?, - ComposerTransaction::AppCreateCall(params) => { - build_app_create_call(params, header) - } - ComposerTransaction::AppUpdateCall(params) => { - build_app_update_call(params, header) - } - ComposerTransaction::AppDeleteCall(params) => { - build_app_delete_call(params, header) - } - ComposerTransaction::AppCallMethodCall(method_call_params) => { - build_app_call_method_call(method_call_params, header)? - } - ComposerTransaction::AppCreateMethodCall(create_method_call_params) => { - build_app_create_method_call(create_method_call_params, header)? - } - ComposerTransaction::AppUpdateMethodCall(update_method_call_params) => { - build_app_update_method_call(update_method_call_params, header)? - } - ComposerTransaction::AppDeleteMethodCall(delete_method_call_params) => { - build_app_delete_method_call(delete_method_call_params, header)? - } - ComposerTransaction::OnlineKeyRegistration(params) => { - build_online_key_registration(params, header) - } - ComposerTransaction::OfflineKeyRegistration(params) => { - build_offline_key_registration(params, header) - } - ComposerTransaction::NonParticipationKeyRegistration(params) => { - build_non_participation_key_registration(params, header) - } - }; - - if calculate_fee { - transaction = transaction - .assign_fee(FeeParams { - fee_per_byte: suggested_params.fee, - min_fee: suggested_params.min_fee, - extra_fee: ctxn.extra_fee(), - max_fee: ctxn.max_fee(), - }) - .map_err(|e| ComposerError::TransactionError { - message: e.to_string(), - })?; - } - - Ok(transaction) - }) - .collect::, ComposerError>>()?; - - if let Some(mut group_analysis) = group_analysis { - // Process fee adjustments - let (mut surplus_group_fees, mut transaction_analysis): (u64, Vec<_>) = - group_analysis.transactions.iter().enumerate().fold( - (0, Vec::new()), - |(mut surplus_group_fees_acc, mut txn_analysis_acc), - (group_index, transaction_analysis)| { - // Accumulate surplus fees - if let Some(FeeDelta::Surplus(amount)) = - &transaction_analysis.required_fee_delta - { - surplus_group_fees_acc += amount; - } - - // Calculate priority and add to transaction info - let ctxn = &self.transactions[group_index]; - let txn = &transactions[group_index]; - let is_immutable_fee = if let Some(logical_max_fee) = ctxn.logical_max_fee() - { - logical_max_fee == txn.header().fee.unwrap_or(0) - } else { - false - }; - let priority = match &transaction_analysis.required_fee_delta { - Some(FeeDelta::Deficit(amount)) => { - if is_immutable_fee || !matches!(txn, Transaction::AppCall(_)) { - // High priority: transactions that can't be modified - FeePriority::ImmutableDeficit(*amount) - } else { - // Normal priority: app call transactions that can be modified - FeePriority::ModifiableDeficit(*amount) - } - } - _ => FeePriority::Covered, - }; - - txn_analysis_acc.push(( - group_index, - &transaction_analysis.required_fee_delta, - priority, - &transaction_analysis.unnamed_resources_accessed, - )); - - (surplus_group_fees_acc, txn_analysis_acc) - }, - ); - - // Sort transactions by priority (highest first) - transaction_analysis.sort_by_key(|&(_, _, priority, _)| std::cmp::Reverse(priority)); - - // Cover any additional fees required for the transactions - for (group_index, required_fee_delta, _, resources_accessed) in transaction_analysis { - if let Some(FeeDelta::Deficit(deficit_amount)) = *required_fee_delta { - // First allocate surplus group fees to cover deficits - let mut additional_fee_delta: Option = None; - if surplus_group_fees == 0 { - // No surplus groups fees, the transaction must cover its own deficit - additional_fee_delta = Some(FeeDelta::Deficit(deficit_amount)); - } else if surplus_group_fees >= deficit_amount { - // Surplus fully covers the deficit - surplus_group_fees -= deficit_amount; - } else { - // Surplus partially covers the deficit - additional_fee_delta = - Some(FeeDelta::Deficit(deficit_amount - surplus_group_fees)); - surplus_group_fees = 0; - } - - // If there is any additional fee deficit, the transaction must cover it by modifying the fee - if let Some(FeeDelta::Deficit(additional_deficit_amount)) = additional_fee_delta - { - match transactions[group_index] { - Transaction::AppCall(_) => { - let txn_header = transactions[group_index].header_mut(); - let current_fee = txn_header.fee.unwrap_or(0); - let transaction_fee = current_fee + additional_deficit_amount; - - let logical_max_fee = - self.transactions[group_index].logical_max_fee(); - if logical_max_fee.is_none() - || transaction_fee > logical_max_fee.unwrap() - { - return Err(ComposerError::TransactionError { - message: format!( - "Calculated transaction fee {} µALGO is greater than max of {} for transaction {}", - transaction_fee, - logical_max_fee.unwrap_or(0), - group_index - ), - }); - } - - txn_header.fee = Some(transaction_fee); - } - _ => { - return Err(ComposerError::TransactionError { - message: format!( - "An additional fee of {} µALGO is required for non app call transaction {}", - additional_deficit_amount, group_index - ), - }); - } - } - } - } - - if let Some(resources_accessed) = resources_accessed { - // Apply the transaction level resource population logic - if let Transaction::AppCall(ref mut app_call) = transactions[group_index] { - // Check for unexpected resources at transaction level - if resources_accessed.boxes.is_some() - || resources_accessed.extra_box_refs.is_some() - { - return Err(ComposerError::TransactionError { - message: "Unexpected boxes at the transaction level".to_string(), - }); - } - if resources_accessed.app_locals.is_some() { - return Err(ComposerError::TransactionError { - message: "Unexpected app locals at the transaction level" - .to_string(), - }); - } - if resources_accessed.asset_holdings.is_some() { - return Err(ComposerError::TransactionError { - message: "Unexpected asset holdings at the transaction level" - .to_string(), - }); - } - - let mut accounts_count = 0; - let mut apps_count = 0; - let mut assets_count = 0; - let boxes_count = app_call - .box_references - .as_ref() - .map(|b| b.len()) - .unwrap_or(0); - - // Populate accounts at the transaction level, apps, assets, and boxes from unnamed resources - if let Some(ref accessed_accounts) = resources_accessed.accounts { - let accounts = app_call.account_references.get_or_insert_with(Vec::new); - - for account_str in accessed_accounts { - let address = account_str.parse::
().map_err(|e| { - ComposerError::TransactionError { - message: format!("Invalid account address: {}", e), - } - })?; - if !accounts.contains(&address) { - accounts.push(address); - } - } - accounts_count = accounts.len(); - } - - // Populate apps at the transaction level - if let Some(ref accessed_apps) = resources_accessed.apps { - let apps = app_call.app_references.get_or_insert_with(Vec::new); - for app_id in accessed_apps { - if !apps.contains(app_id) { - apps.push(*app_id); - } - } - apps_count = apps.len(); - } - - // Populate asset at the transaction level - if let Some(ref accessed_assets) = resources_accessed.assets { - let assets = app_call.asset_references.get_or_insert_with(Vec::new); - for asset_id in accessed_assets { - if !assets.contains(asset_id) { - assets.push(*asset_id); - } - } - assets_count = assets.len(); - } - - //Validate reference limits - if accounts_count > MAX_ACCOUNT_REFERENCES { - return Err(ComposerError::TransactionError { - message: format!( - "Account reference limit of {} exceeded in transaction {}", - MAX_ACCOUNT_REFERENCES, group_index - ), - }); - } - - if (accounts_count + assets_count + apps_count + boxes_count) - > MAX_OVERALL_REFERENCES - { - return Err(ComposerError::TransactionError { - message: format!( - "Resource reference limit of {} exceeded in transaction {}", - MAX_OVERALL_REFERENCES, group_index - ), - }); - } - } - } - } - - // Apply the group level resource population logic - if let Some(group_resources) = group_analysis.unnamed_resources_accessed.take() { - TransactionComposer::populate_group_resources(&mut transactions, group_resources)?; - } - } - - if transactions.len() > 1 { - transactions = - transactions - .assign_group() - .map_err(|e| ComposerError::TransactionError { - message: format!("Failed to assign group: {}", e), - })?; - } - - Ok(transactions) - } - - /// Populate group-level resources for app call transactions - fn populate_group_resources( - transactions: &mut [Transaction], - group_resources: SimulateUnnamedResourcesAccessed, - ) -> Result<(), ComposerError> { - let mut remaining_accounts = group_resources.accounts.unwrap_or_default(); - let mut remaining_apps = group_resources.apps.unwrap_or_default(); - let mut remaining_assets = group_resources.assets.unwrap_or_default(); - let remaining_boxes = group_resources.boxes.unwrap_or_default(); - - // Process cross-reference resources first (app locals and asset holdings) as they are most restrictive - if let Some(app_locals) = group_resources.app_locals { - for app_local in app_locals { - let app_local_app = app_local.app; - let app_local_account = app_local.account.clone(); - - TransactionComposer::populate_group_resource( - transactions, - &GroupResourceToPopulate::AppLocal(app_local), - )?; - - // Remove resources from remaining if we're adding them here - remaining_accounts.retain(|acc| acc != &app_local_account); - remaining_apps.retain(|app| *app != app_local_app); - } - } - - if let Some(asset_holdings) = group_resources.asset_holdings { - for asset_holding in asset_holdings { - let asset_holding_asset = asset_holding.asset; - let asset_holding_account = asset_holding.account.clone(); - - TransactionComposer::populate_group_resource( - transactions, - &GroupResourceToPopulate::AssetHolding(asset_holding), - )?; - - // Remove resources from remaining if we're adding them here - remaining_accounts.retain(|acc| acc != &asset_holding_account); - remaining_assets.retain(|asset| *asset != asset_holding_asset); - } - } - - // Process accounts next because account limit is 4 - for account in remaining_accounts { - TransactionComposer::populate_group_resource( - transactions, - &GroupResourceToPopulate::Account(account), - )?; - } - - // Process boxes - for box_ref in remaining_boxes { - let box_ref_app = box_ref.app; - - TransactionComposer::populate_group_resource( - transactions, - &GroupResourceToPopulate::Box(box_ref), - )?; - - // Remove apps as resource if we're adding it here - remaining_apps.retain(|app| *app != box_ref_app); - } - - // Process assets - for asset in remaining_assets { - TransactionComposer::populate_group_resource( - transactions, - &GroupResourceToPopulate::Asset(asset), - )?; - } - - // Process remaining apps - for app in remaining_apps { - TransactionComposer::populate_group_resource( - transactions, - &GroupResourceToPopulate::App(app), - )?; - } - - // Handle extra box refs - if let Some(extra_box_refs) = group_resources.extra_box_refs { - for _ in 0..extra_box_refs { - TransactionComposer::populate_group_resource( - transactions, - &GroupResourceToPopulate::ExtraBoxRef, - )?; - } - } - - Ok(()) - } - - // Helper function to check if an application call transaction is below resource limit - fn is_app_call_below_resource_limit(txn: &Transaction) -> bool { - if let Transaction::AppCall(app_call) = txn { - let accounts_count = app_call - .account_references - .as_ref() - .map(|a| a.len()) - .unwrap_or(0); - let assets_count = app_call - .asset_references - .as_ref() - .map(|a| a.len()) - .unwrap_or(0); - let apps_count = app_call - .app_references - .as_ref() - .map(|a| a.len()) - .unwrap_or(0); - let boxes_count = app_call - .box_references - .as_ref() - .map(|b| b.len()) - .unwrap_or(0); - - (accounts_count + assets_count + apps_count + boxes_count) < MAX_OVERALL_REFERENCES - } else { - false - } - } - - /// Helper function to populate a specific resource into a transaction group - fn populate_group_resource( - transactions: &mut [Transaction], - resource: &GroupResourceToPopulate, - ) -> Result<(), ComposerError> { - // For asset holdings and app locals, first try to find a transaction that already has the account available - match resource { - GroupResourceToPopulate::AssetHolding(_) | GroupResourceToPopulate::AppLocal(_) => { - let account = match resource { - GroupResourceToPopulate::AssetHolding(asset_holding) => &asset_holding.account, - GroupResourceToPopulate::AppLocal(app_local) => &app_local.account, - _ => unreachable!(), - }; - - // Try to find a transaction that already has the account available - let group_index = transactions.iter().position(|txn| { - if !TransactionComposer::is_app_call_below_resource_limit(txn) { - return false; - } - - if let Transaction::AppCall(app_call) = txn { - // Check if account is in foreign accounts array - if let Some(ref accounts) = app_call.account_references { - let address = account.parse::
().unwrap_or_default(); - if accounts.contains(&address) { - return true; - } - } - - // Check if account is available as an app account - if let Some(ref apps) = app_call.app_references { - for app_id in apps { - if account == &Address::from_app_id(app_id).to_string() { - return true; - } - } - } - - // Check if account appears in any app call transaction fields - if app_call.header.sender.to_string() == *account { - return true; - } - } - - false - }); - - if let Some(group_index) = group_index { - if let Transaction::AppCall(ref mut app_call) = transactions[group_index] { - match resource { - GroupResourceToPopulate::AssetHolding(asset_holding) => { - let assets = app_call.asset_references.get_or_insert_with(Vec::new); - if !assets.contains(&asset_holding.asset) { - assets.push(asset_holding.asset); - } - } - GroupResourceToPopulate::AppLocal(app_local) => { - let apps = app_call.app_references.get_or_insert_with(Vec::new); - if !apps.contains(&app_local.app) { - apps.push(app_local.app); - } - } - _ => {} - } - } - - return Ok(()); - } - - // Try to find a transaction that already has the asset/app available and space for account - let group_index = transactions.iter().position(|txn| { - if !TransactionComposer::is_app_call_below_resource_limit(txn) { - return false; - } - - if let Transaction::AppCall(app_call) = txn { - // Check if there's space in the accounts array - if app_call - .account_references - .as_ref() - .map(|a| a.len()) - .unwrap_or(0) - >= MAX_ACCOUNT_REFERENCES - { - return false; - } - - match resource { - GroupResourceToPopulate::AssetHolding(asset_holding) => { - if let Some(ref assets) = app_call.asset_references { - return assets.contains(&asset_holding.asset); - } - } - GroupResourceToPopulate::AppLocal(app_local) => { - if let Some(ref apps) = app_call.app_references { - return apps.contains(&app_local.app); - } - return app_call.app_id == app_local.app; - } - _ => {} - } - } - - false - }); - - if let Some(group_index) = group_index { - if let Transaction::AppCall(ref mut app_call) = transactions[group_index] { - let accounts = app_call.account_references.get_or_insert_with(Vec::new); - let address = account.parse::
().map_err(|e| { - ComposerError::TransactionError { - message: format!("Invalid account address: {}", e), - } - })?; - if !accounts.contains(&address) { - accounts.push(address); - } - } - return Ok(()); - } - } - GroupResourceToPopulate::Box(box_ref) => { - // For boxes, first try to find a transaction that already has the app available - let group_index = transactions.iter().position(|txn| { - if !TransactionComposer::is_app_call_below_resource_limit(txn) { - return false; - } - - if let Transaction::AppCall(app_call) = txn { - // Check if the app is in the foreign array OR the app being called - if let Some(ref apps) = app_call.app_references { - if apps.contains(&box_ref.app) { - return true; - } - } - return app_call.app_id == box_ref.app; - } - - false - }); - - if let Some(group_index) = group_index { - if let Transaction::AppCall(ref mut app_call) = transactions[group_index] { - let boxes = app_call.box_references.get_or_insert_with(Vec::new); - if !boxes - .iter() - .any(|b| b.app_id == box_ref.app && b.name == box_ref.name) - { - boxes.push(algokit_transact::BoxReference { - app_id: box_ref.app, - name: box_ref.name.clone(), - }); - } - } - return Ok(()); - } - } - _ => {} - } - - // Find the transaction index to put the reference(s) - let group_index = transactions.iter().position(|txn| { - if let Transaction::AppCall(app_call) = txn { - let accounts_count = app_call - .account_references - .as_ref() - .map(|a| a.len()) - .unwrap_or(0); - let assets_count = app_call - .asset_references - .as_ref() - .map(|a| a.len()) - .unwrap_or(0); - let apps_count = app_call - .app_references - .as_ref() - .map(|a| a.len()) - .unwrap_or(0); - let boxes_count = app_call - .box_references - .as_ref() - .map(|b| b.len()) - .unwrap_or(0); - - match resource { - GroupResourceToPopulate::Account(_) => accounts_count < MAX_ACCOUNT_REFERENCES, - - GroupResourceToPopulate::AssetHolding(..) - | GroupResourceToPopulate::AppLocal(..) => { - // If we're adding local state or asset holding, we need space for the account and the other reference (asset or app) - (accounts_count + assets_count + apps_count + boxes_count) - < (MAX_OVERALL_REFERENCES - 1) - && accounts_count < MAX_ACCOUNT_REFERENCES - } - - GroupResourceToPopulate::Box(box_ref) => { - // If we're adding a box, we need space for both the box reference and the app reference - if box_ref.app != 0 { - (accounts_count + assets_count + apps_count + boxes_count) - < MAX_OVERALL_REFERENCES - 1 - } else { - (accounts_count + assets_count + apps_count + boxes_count) - < MAX_OVERALL_REFERENCES - } - } - _ => { - (accounts_count + assets_count + apps_count + boxes_count) - < MAX_OVERALL_REFERENCES - } - } - } else { - false - } - }); - - let group_index = group_index.ok_or_else(|| ComposerError::TransactionError { - message: - "No more transactions below reference limit. Add another app call to the group." - .to_string(), - })?; - - if let Transaction::AppCall(ref mut app_call) = transactions[group_index] { - match resource { - GroupResourceToPopulate::Account(account) => { - let accounts = app_call.account_references.get_or_insert_with(Vec::new); - let address = account.parse::
().map_err(|e| { - ComposerError::TransactionError { - message: format!("Invalid account address: {}", e), - } - })?; - if !accounts.contains(&address) { - accounts.push(address); - } - } - GroupResourceToPopulate::App(app_id) => { - let apps = app_call.app_references.get_or_insert_with(Vec::new); - if !apps.contains(app_id) { - apps.push(*app_id); - } - } - GroupResourceToPopulate::Box(box_ref) => { - let boxes = app_call.box_references.get_or_insert_with(Vec::new); - if !boxes - .iter() - .any(|b| b.app_id == box_ref.app && b.name == box_ref.name) - { - boxes.push(algokit_transact::BoxReference { - app_id: box_ref.app, - name: box_ref.name.clone(), - }); - } - if box_ref.app != 0 { - let apps = app_call.app_references.get_or_insert_with(Vec::new); - if !apps.contains(&box_ref.app) { - apps.push(box_ref.app); - } - } - } - GroupResourceToPopulate::ExtraBoxRef => { - let boxes = app_call.box_references.get_or_insert_with(Vec::new); - boxes.push(algokit_transact::BoxReference { - app_id: 0, - name: Vec::new(), - }); - } - GroupResourceToPopulate::AssetHolding(asset_holding) => { - let assets = app_call.asset_references.get_or_insert_with(Vec::new); - if !assets.contains(&asset_holding.asset) { - assets.push(asset_holding.asset); - } - - let accounts = app_call.account_references.get_or_insert_with(Vec::new); - let address = asset_holding.account.parse::
().map_err(|e| { - ComposerError::TransactionError { - message: format!("Invalid account address: {}", e), - } - })?; - if !accounts.contains(&address) { - accounts.push(address); - } - } - GroupResourceToPopulate::AppLocal(app_local) => { - let apps = app_call.app_references.get_or_insert_with(Vec::new); - if !apps.contains(&app_local.app) { - apps.push(app_local.app); - } - - let accounts = app_call.account_references.get_or_insert_with(Vec::new); - let address = app_local.account.parse::
().map_err(|e| { - ComposerError::TransactionError { - message: format!("Invalid account address: {}", e), - } - })?; - if !accounts.contains(&address) { - accounts.push(address); - } - } - GroupResourceToPopulate::Asset(asset_id) => { - let assets = app_call.asset_references.get_or_insert_with(Vec::new); - if !assets.contains(asset_id) { - assets.push(*asset_id); - } - } - } - } - - Ok(()) - } - - fn get_default_validity_window(genesis_id: &str) -> u32 { - if genesis_id_is_localnet(genesis_id) { - 1000 // LocalNet gets bigger window to avoid dead transactions - } else { - 10 // Standard default validity window - } - } - - pub async fn build(&mut self) -> Result<&Vec, ComposerError> { - if let Some(ref group) = self.built_group { - return Ok(group); - } - - let suggested_params = self.get_suggested_params().await?; - let default_validity_window = - Self::get_default_validity_window(&suggested_params.genesis_id); - - let group_analysis = if (self.composer_config.cover_app_call_inner_transaction_fees - || self - .composer_config - .populate_app_call_resources - .is_enabled()) - && self.transactions.iter().any(|ctxn| ctxn.is_app_call()) - { - Some( - self.analyze_group_requirements( - &suggested_params, - &default_validity_window, - &self.composer_config, - ) - .await?, - ) - } else { - None - }; - - let transactions = self - .build_transactions(&suggested_params, &default_validity_window, group_analysis) - .await?; - - let transactions_with_signers = self.gather_signers(transactions); - - self.built_group = Some(transactions_with_signers?); - Ok(self.built_group.as_ref().unwrap()) - } - - fn gather_signers( - &self, - transactions: Vec, - ) -> Result, ComposerError> { - transactions - .into_iter() - .enumerate() - .map(|(group_index, txn)| { - let ctxn = &self.transactions[group_index]; - let signer = match ctxn { - ComposerTransaction::TransactionWithSigner(tx_with_signer) => { - tx_with_signer.signer.clone() - } - _ => { - if let Some(transaction_signer) = ctxn.signer() { - transaction_signer - } else { - let sender_address = txn.header().sender.clone(); - self.signer_getter - .get_signer(sender_address.clone()) - .map_err(|e| ComposerError::SigningError { message: e })? - } - } - }; - Ok(TransactionWithSigner { - transaction: txn, - signer, - }) - }) - .collect() - } - - pub async fn gather_signatures(&mut self) -> Result<&Vec, ComposerError> { - if let Some(ref group) = self.signed_group { - return Ok(group); - } - - self.build().await?; - - let transactions_with_signers = self - .built_group - .as_ref() - .filter(|&txs| !txs.is_empty()) - .ok_or(ComposerError::StateError { - message: "No transactions available".to_string(), - })?; - - // Group transactions by signer - let mut transactions = Vec::new(); - let mut signer_groups: Vec<(Arc, Vec)> = Vec::new(); - - for (group_index, txn_with_signer) in transactions_with_signers.iter().enumerate() { - let found_group = signer_groups - .iter_mut() - .find(|(existing_signer, _)| Arc::ptr_eq(existing_signer, &txn_with_signer.signer)); - - match found_group { - Some((_, indices)) => indices.push(group_index), - None => signer_groups.push((txn_with_signer.signer.clone(), vec![group_index])), - } - transactions.push(txn_with_signer.transaction.to_owned()); - } - - let mut signed_transactions: Vec> = - vec![None; transactions_with_signers.len()]; - - for (signer, indices) in signer_groups { - // Sign all transactions for this signer - let signed_txns: Vec = signer - .sign_transactions(&transactions, &indices) - .await - .map_err(|e| ComposerError::SigningError { - message: e.to_string(), - })?; - - for (i, &index) in indices.iter().enumerate() { - signed_transactions[index] = Some(signed_txns[i].to_owned()); - } - } - - let (signed_transactions, unsigned_indexes) = - signed_transactions.into_iter().enumerate().fold( - (Vec::::new(), Vec::::new()), - |mut acc, (i, stx)| { - match stx { - Some(stx) => acc.0.push(stx), - None => acc.1.push(i), - } - acc - }, - ); - - if !unsigned_indexes.is_empty() - || transactions_with_signers.len() != signed_transactions.len() - { - return Err(ComposerError::SigningError { - message: format!( - "Transactions at indexes {:?} were not signed", - unsigned_indexes - ), - }); - } - - self.signed_group = Some(signed_transactions); - Ok(self.signed_group.as_ref().unwrap()) - } - - async fn wait_for_confirmation( - &self, - tx_id: &str, - max_rounds_to_wait: u32, - ) -> Result { - let status = - self.algod_client - .get_status() - .await - .map_err(|e| ComposerError::TransactionError { - message: format!("Failed to get status: {:?}", e), - })?; - - let start_round = status.last_round + 1; - let mut current_round = start_round; - - while current_round < start_round + max_rounds_to_wait as u64 { - match self - .algod_client - .pending_transaction_information(tx_id) - .await - { - Ok(response) => { - // Check for pool errors first - transaction was kicked out of pool - if !response.pool_error.is_empty() { - return Err(ComposerError::PoolError { - message: format!( - "Transaction {} was rejected; pool error: {}", - tx_id, - response.pool_error.clone() - ), - }); - } - - // Check if transaction is confirmed - if response.confirmed_round.is_some() { - return Ok(response); - } - } - Err(error) => { - // Only retry for 404 errors (transaction not found yet) - // All other errors indicate permanent issues and should fail fast - let is_retryable = matches!( - &error, - algod_client::apis::Error::Api { - source: algod_client::apis::AlgodApiError::PendingTransactionInformation { - error: algod_client::apis::pending_transaction_information::PendingTransactionInformationError::Status404(_) - } - } - ) || error.to_string().contains("404"); - - if is_retryable { - current_round += 1; - continue; - } else { - return Err(ComposerError::AlgodClientError { source: error }); - } - } - }; - - let _ = self.algod_client.wait_for_block(current_round).await; - current_round += 1; - } - - Err(ComposerError::MaxWaitRoundExpired { - message: format!( - "Transaction {} unconfirmed after {} rounds", - tx_id, max_rounds_to_wait - ), - }) - } - - pub async fn send( - &mut self, - params: Option, - ) -> Result { - self.gather_signatures().await?; - - let signed_transactions = self - .signed_group - .as_ref() - .filter(|&stxns| !stxns.is_empty()) - .ok_or(ComposerError::StateError { - message: "No transactions available".to_string(), - })?; - - let transactions: Vec = signed_transactions - .iter() - .map(|txn| txn.transaction.clone()) - .collect::>(); - - let transaction_ids: Vec = transactions - .iter() - .map(|txn| txn.id()) - .collect::, _>>()?; - - let group = signed_transactions[0].transaction.header().group; - - // Encode each signed transaction and concatenate them - let mut encoded_bytes = Vec::new(); - for signed_txn in signed_transactions { - let encoded_txn = signed_txn - .encode() - .map_err(|e| ComposerError::TransactionError { - message: format!("Failed to encode signed transaction: {}", e), - })?; - encoded_bytes.extend_from_slice(&encoded_txn); - } - - let last_valid_max = signed_transactions - .iter() - .map(|signed_transaction| signed_transaction.transaction.header().last_valid) - .max() - .ok_or(ComposerError::StateError { - message: "Failed to calculate last valid round".to_string(), - })?; - - let wait_rounds = if let Some(max_rounds_to_wait_for_confirmation) = - params.and_then(|p| p.max_rounds_to_wait_for_confirmation) - { - max_rounds_to_wait_for_confirmation - } else { - let suggested_params = self.get_suggested_params().await?; - let first_round: u64 = suggested_params.last_round; // The last round seen, so is the first round valid - ((last_valid_max - first_round) + 1) - .try_into() - .map_err(|e| ComposerError::TransactionError { - message: format!("Failed to calculate rounds to wait: {}", e), - })? - }; - - // If debugging with full tracing enabled, emit a simulate event before submission for AVM debugging - if Config::debug() && Config::trace_all() { - let simulate_params = SimulateParams { - allow_more_logging: Some(true), - allow_empty_signatures: Some(true), - allow_unnamed_resources: Some(true), - extra_opcode_budget: None, - exec_trace_config: Some(algod_client::models::SimulateTraceConfig { - enable: Some(true), - stack_change: Some(true), - scratch_change: Some(true), - state_change: Some(true), - }), - simulation_round: None, - skip_signatures: true, - }; - - if let Ok(simulated) = self.simulate(Some(simulate_params)).await { - let payload = serde_json::to_value(&simulated.simulate_response) - .unwrap_or_else(|_| serde_json::json!({})); - Config::events() - .emit( - EventType::TxnGroupSimulated, - EventData::TxnGroupSimulated(TxnGroupSimulatedEventData { - simulate_response: payload, - }), - ) - .await; - } - } - - let _ = self - .algod_client - .raw_transaction(encoded_bytes) - .await - .map_err(|e| ComposerError::TransactionError { - message: format!("Failed to submit transaction(s): {:?}", e), - })?; - - let mut confirmations = Vec::new(); - for id in &transaction_ids { - let confirmation = self.wait_for_confirmation(id, wait_rounds).await?; - confirmations.push(confirmation); - } - - // Parse ABI return values from the confirmations - let abi_returns = self.parse_abi_return_values(&confirmations); - - // Build results with 1:1 correspondence - let results = transactions - .into_iter() - .zip(transaction_ids) - .zip(confirmations) - .zip(abi_returns) - .map( - |(((transaction, transaction_id), confirmation), abi_return)| TransactionResult { - transaction, - transaction_id, - confirmation, - abi_return, - }, - ) - .collect(); - - Ok(TransactionComposerSendResult { group, results }) - } - - pub fn count(&self) -> usize { - self.transactions.len() - } - - pub async fn simulate( - &mut self, - simulate_params: Option, - ) -> Result { - let simulate_params = simulate_params.unwrap_or_default(); - - self.build().await?; - - let transactions_with_signers = - self.built_group.as_ref().ok_or(ComposerError::StateError { - message: "No transactions available".to_string(), - })?; - - let group = transactions_with_signers[0].transaction.header().group; - - let signed_transactions: Vec = match simulate_params.skip_signatures { - true => transactions_with_signers - .iter() - .map(|txn_with_signer| SignedTransaction { - transaction: txn_with_signer.transaction.clone(), - signature: Some(EMPTY_SIGNATURE), - auth_address: None, - multisignature: None, - }) - .collect(), - false => self.gather_signatures().await?.to_vec(), - }; - - let transactions: Vec = signed_transactions - .iter() - .map(|txn| txn.transaction.clone()) - .collect::>(); - let transaction_ids: Vec = transactions - .iter() - .map(|txn| txn.id()) - .collect::, _>>()?; - - let txn_group = SimulateRequestTransactionGroup { - txns: signed_transactions, - }; - let simulate_request = SimulateRequest { - txn_groups: vec![txn_group], - round: simulate_params.simulation_round, - allow_empty_signatures: if Config::debug() || simulate_params.skip_signatures { - Some(true) - } else { - simulate_params.allow_empty_signatures - }, - allow_more_logging: simulate_params.allow_more_logging, - allow_unnamed_resources: simulate_params.allow_unnamed_resources, - extra_opcode_budget: simulate_params.extra_opcode_budget, - exec_trace_config: simulate_params.exec_trace_config, - fix_signers: Some(true), - }; - - // Call simulate endpoint - let simulate_response = self - .algod_client - .simulate_transaction(simulate_request, Some(Format::Msgpack)) - .await - .map_err(|e| ComposerError::AlgodClientError { source: e })?; - - let simulated_group_result = &simulate_response.txn_groups[0]; - - if let Some(failure_message) = &simulated_group_result.failure_message { - let failed_at = simulated_group_result - .failed_at - .as_ref() - .map(|v| { - v.iter() - .map(|i| i.to_string()) - .collect::>() - .join(", ") - }) - .unwrap_or_else(|| "unknown".to_string()); - if Config::debug() { - let payload = serde_json::to_value(&simulate_response) - .unwrap_or_else(|_| serde_json::json!({})); - Config::events() - .emit( - EventType::TxnGroupSimulated, - EventData::TxnGroupSimulated(TxnGroupSimulatedEventData { - simulate_response: payload, - }), - ) - .await; - } - return Err(ComposerError::TransactionError { - message: format!( - "Transaction failed at transaction(s) {} in the group. {}", - failed_at, failure_message - ), - }); - } - - // Collect confirmations and ABI returns similar to send() - let confirmations: Vec = simulated_group_result - .txn_results - .iter() - .map(|r| r.txn_result.clone()) - .collect(); - - let abi_returns = self.parse_abi_return_values(&confirmations); - - // Build results with 1:1 correspondence - let results = transactions - .into_iter() - .zip(transaction_ids) - .zip(confirmations) - .zip(abi_returns) - .map( - |(((transaction, transaction_id), confirmation), abi_return)| TransactionResult { - transaction, - transaction_id, - confirmation, - abi_return, - }, - ) - .collect(); - - if Config::debug() && Config::trace_all() { - let payload = - serde_json::to_value(&simulate_response).unwrap_or_else(|_| serde_json::json!({})); - Config::events() - .emit( - EventType::TxnGroupSimulated, - EventData::TxnGroupSimulated(TxnGroupSimulatedEventData { - simulate_response: payload, - }), - ) - .await; - } - - Ok(TransactionComposerSimulateResult { - group, - results, - simulate_response, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::EmptySigner; - use algokit_transact::test_utils::{AccountMother, TransactionMother}; - use base64::{Engine, prelude::BASE64_STANDARD}; - - fn test_composer_params() -> TransactionComposerParams { - TransactionComposerParams { - algod_client: Arc::new(AlgodClient::testnet()), - signer_getter: Arc::new(EmptySigner {}), - composer_config: Some(TransactionComposerConfig { - populate_app_call_resources: ResourcePopulation::Disabled, - cover_app_call_inner_transaction_fees: false, - }), - } - } - - #[test] - fn test_add_transaction() { - let mut composer = TransactionComposer::new(test_composer_params()); - let txn = TransactionMother::simple_payment().build().unwrap(); - assert!(composer.add_transaction(txn, None).is_ok()); - } - - #[test] - fn test_add_too_many_transactions() { - let mut composer = TransactionComposer::new(test_composer_params()); - for _ in 0..16 { - let txn = TransactionMother::simple_payment().build().unwrap(); - assert!(composer.add_transaction(txn, None).is_ok()); - } - let txn = TransactionMother::simple_payment().build().unwrap(); - assert!(composer.add_transaction(txn, None).is_err()); - } - - #[tokio::test] - async fn test_get_suggested_params() { - let composer = TransactionComposer::new(test_composer_params()); - let response = composer.get_suggested_params().await.unwrap(); - - assert_eq!( - response.genesis_hash, - BASE64_STANDARD - .decode("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=") - .unwrap() - ); - } - - #[test] - fn test_add_payment() { - let mut composer = TransactionComposer::new(test_composer_params()); - let payment_params = PaymentParams { - sender: AccountMother::account().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, - receiver: AccountMother::account().address(), - amount: 1000, - }; - assert!(composer.add_payment(payment_params).is_ok()); - } - - #[tokio::test] - async fn test_gather_signatures() { - let mut composer = TransactionComposer::new(test_composer_params()); - - let payment_params = PaymentParams { - sender: AccountMother::account().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, - receiver: AccountMother::account().address(), - amount: 1000, - }; - composer.add_payment(payment_params).unwrap(); - - let result = composer.gather_signatures().await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_single_transaction_no_group() { - let mut composer = TransactionComposer::new(test_composer_params()); - let payment_params = PaymentParams { - sender: AccountMother::account().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, - receiver: AccountMother::account().address(), - amount: 1000, - }; - composer.add_payment(payment_params).unwrap(); - - composer.build().await.unwrap(); - - let built_group = composer.built_group.as_ref().unwrap(); - assert_eq!(built_group.len(), 1); - - // Single transaction should not have a group ID set - assert!(built_group[0].transaction.header().group.is_none()); - } - - #[tokio::test] - async fn test_multiple_transactions_have_group() { - let mut composer = TransactionComposer::new(test_composer_params()); - - for _ in 0..2 { - let payment_params = PaymentParams { - sender: AccountMother::account().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, - receiver: AccountMother::account().address(), - amount: 1000, - }; - composer.add_payment(payment_params).unwrap(); - } - - composer.build().await.unwrap(); - - let built_group = composer.built_group.as_ref().unwrap(); - assert_eq!(built_group.len(), 2); - - // Multiple transactions should have group IDs set - for transaction_with_signer in built_group { - assert!(transaction_with_signer.transaction.header().group.is_some()); - } - - // All transactions should have the same group ID - let group = built_group[0].transaction.header().group.as_ref().unwrap(); - for transaction_with_signer in &built_group[1..] { - assert_eq!( - transaction_with_signer - .transaction - .header() - .group - .as_ref() - .unwrap(), - group - ); - } - } - - #[test] - fn test_error_recoverability_logic() { - // Test string-based 404 detection (the primary retry mechanism) - let error_404_string = "Request failed with status 404: Transaction not found"; - let error_500_string = "Request failed with status 500: Server error"; - - // The main retry logic relies on string matching - assert!( - error_404_string.contains("404"), - "404 errors should be retryable" - ); - assert!( - !error_500_string.contains("404"), - "500 errors should not be retryable" - ); - } - - #[test] - fn test_fee_delta_operations() { - // Test creation from i64 - assert_eq!(FeeDelta::from_i64(100), Some(FeeDelta::Deficit(100))); - assert_eq!(FeeDelta::from_i64(-50), Some(FeeDelta::Surplus(50))); - assert_eq!(FeeDelta::from_i64(0), None); - - // Test conversion to i64 - assert_eq!(FeeDelta::Deficit(100).to_i64(), 100); - assert_eq!(FeeDelta::Surplus(50).to_i64(), -50); - - // Test is_deficit and is_surplus - assert!(FeeDelta::Deficit(100).is_deficit()); - assert!(!FeeDelta::Deficit(100).is_surplus()); - - assert!(FeeDelta::Surplus(50).is_surplus()); - assert!(!FeeDelta::Surplus(50).is_deficit()); - - // Test amount extraction - assert_eq!(FeeDelta::Deficit(100).amount(), 100); - assert_eq!(FeeDelta::Surplus(50).amount(), 50); - } - - #[test] - fn test_fee_priority_ordering() { - let covered = FeePriority::Covered; - let modifiable_small = FeePriority::ModifiableDeficit(100); - let modifiable_large = FeePriority::ModifiableDeficit(1000); - let immutable_small = FeePriority::ImmutableDeficit(100); - let immutable_large = FeePriority::ImmutableDeficit(1000); - - // Test basic ordering: ImmutableDeficit > ModifiableDeficit > Covered - assert!(immutable_small > modifiable_large); - assert!(modifiable_small > covered); - assert!(immutable_large > modifiable_large); - - // Test within same priority class, larger deficits have higher priority - assert!(immutable_large > immutable_small); - assert!(modifiable_large > modifiable_small); - - // Create a sorted vector to verify the ordering behavior - let mut priorities = [ - covered, - modifiable_small, - immutable_small, - modifiable_large, - immutable_large, - ]; - - // Sort in descending order (highest priority first) - priorities.sort_by(|a, b| b.cmp(a)); - - assert_eq!(priorities[0], FeePriority::ImmutableDeficit(1000)); - assert_eq!(priorities[1], FeePriority::ImmutableDeficit(100)); - assert_eq!(priorities[2], FeePriority::ModifiableDeficit(1000)); - assert_eq!(priorities[3], FeePriority::ModifiableDeficit(100)); - assert_eq!(priorities[4], FeePriority::Covered); - } - - #[test] - fn test_transaction_composer_config_default() { - let params = TransactionComposerConfig::default(); - assert!(!params.cover_app_call_inner_transaction_fees); - assert!(params.populate_app_call_resources.is_enabled()); - assert!(!params.populate_app_call_resources.use_access_list()); - } - - #[test] - fn test_add_transaction_with_non_empty_group_fails() { - let mut composer = TransactionComposer::new(test_composer_params()); - - // Create a transaction with a non-empty group - let mut txn = TransactionMother::simple_payment().build().unwrap(); - // Set a non-zero group (the check is for non-zero bytes) - txn.header_mut().group = Some([1u8; 32]); - - let result = composer.add_transaction(txn, None); - - assert!(result.is_err()); - if let Err(ComposerError::TransactionError { message }) = result { - assert!(message.contains("Cannot add a transaction with nonzero group")); - } - } - - #[tokio::test] - async fn test_add_transaction_after_build_fails() { - let mut composer = TransactionComposer::new(test_composer_params()); - - // Add a transaction and build the composer - let txn = TransactionMother::simple_payment().build().unwrap(); - composer.add_transaction(txn, None).unwrap(); - composer.build().await.unwrap(); - - // Now try to add another transaction, which should fail - let txn2 = TransactionMother::simple_payment().build().unwrap(); - let result = composer.add_transaction(txn2, None); - - assert!(result.is_err()); - if let Err(ComposerError::StateError { message }) = result { - assert!(message.contains("Cannot add new transactions after building")); - } - } -} diff --git a/crates/algokit_utils/src/transactions/creator.rs b/crates/algokit_utils/src/transactions/creator.rs deleted file mode 100644 index abbf1d67f..000000000 --- a/crates/algokit_utils/src/transactions/creator.rs +++ /dev/null @@ -1,414 +0,0 @@ -use algokit_transact::Transaction; -use std::sync::Arc; - -use crate::{AccountCloseParams, transactions::TransactionComposerConfig}; - -use super::{ - AppCallMethodCallParams, AppCallParams, AppCreateMethodCallParams, AppCreateParams, - AppDeleteMethodCallParams, AppDeleteParams, AppUpdateMethodCallParams, AppUpdateParams, - AssetClawbackParams, AssetConfigParams, AssetCreateParams, AssetDestroyParams, - AssetFreezeParams, AssetOptInParams, AssetOptOutParams, AssetTransferParams, - AssetUnfreezeParams, NonParticipationKeyRegistrationParams, OfflineKeyRegistrationParams, - OnlineKeyRegistrationParams, PaymentParams, - composer::{ComposerError, TransactionComposer}, -}; - -/// Creates Algorand transactions. -pub struct TransactionCreator { - new_composer: Arc) -> TransactionComposer>, -} - -impl TransactionCreator { - pub fn new( - new_composer: impl Fn(Option) -> TransactionComposer + 'static, - ) -> Self { - Self { - new_composer: Arc::new(new_composer), - } - } - - pub(crate) async fn transaction( - &self, - composer_method: F, - ) -> Result - where - F: FnOnce(&mut TransactionComposer) -> Result<(), ComposerError>, - { - let mut composer = (self.new_composer)(None); - composer_method(&mut composer)?; - let built_transactions = composer.build().await?; - - built_transactions - .last() - .map(|tx_with_signer| tx_with_signer.transaction.clone()) - .ok_or(ComposerError::StateError { - message: "No transactions were built by the composer".to_string(), - }) - } - - /// Create a payment transaction to transfer Algo between accounts. - /// - /// # Arguments - /// * `params` - The parameters for the payment transaction - /// - /// # Returns - /// The payment transaction - pub async fn payment(&self, params: PaymentParams) -> Result { - self.transaction(|composer| composer.add_payment(params)) - .await - } - - /// Create an account close transaction to close an account and transfer all remaining funds. - /// - /// # Arguments - /// * `params` - The parameters for the account close transaction - /// - /// # Returns - /// The account close transaction - pub async fn account_close( - &self, - params: AccountCloseParams, - ) -> Result { - self.transaction(|composer| composer.add_account_close(params)) - .await - } - - /// Create a create Algorand Standard Asset transaction. - /// - /// The account that sends this transaction will automatically be - /// opted in to the asset and will hold all units after creation. - /// - /// # Arguments - /// * `params` - The parameters for the asset creation transaction - /// - /// # Returns - /// The asset create transaction - pub async fn asset_create( - &self, - params: AssetCreateParams, - ) -> Result { - self.transaction(|composer| composer.add_asset_create(params)) - .await - } - - /// Create an Algorand Standard Asset transfer transaction. - /// - /// # Arguments - /// * `params` - The parameters for the asset transfer transaction - /// - /// # Returns - /// The asset transfer transaction - pub async fn asset_transfer( - &self, - params: AssetTransferParams, - ) -> Result { - if params.asset_id == 0 { - return Err(ComposerError::TransactionError { - message: "Asset ID must be greater than 0".to_string(), - }); - } - self.transaction(|composer| composer.add_asset_transfer(params)) - .await - } - - /// Create an Algorand Standard Asset opt-in transaction. - /// - /// # Arguments - /// * `params` - The parameters for the asset opt-in transaction - /// - /// # Returns - /// The asset opt-in transaction - pub async fn asset_opt_in( - &self, - params: AssetOptInParams, - ) -> Result { - self.transaction(|composer| composer.add_asset_opt_in(params)) - .await - } - - /// Create an asset opt-out transaction. - /// - /// **Note:** If the account has a balance of the asset, it will lose those assets - /// - /// # Arguments - /// * `params` - The parameters for the asset opt-out transaction - /// - /// # Returns - /// The asset opt-out transaction - pub async fn asset_opt_out( - &self, - params: AssetOptOutParams, - ) -> Result { - self.transaction(|composer| composer.add_asset_opt_out(params)) - .await - } - - /// Create an asset config transaction to reconfigure an existing Algorand Standard Asset. - /// - /// **Note:** The manager, reserve, freeze, and clawback addresses - /// are immutably empty if they are not set. If manager is not set then - /// all fields are immutable from that point forward. - /// - /// # Arguments - /// * `params` - The parameters for the asset config transaction - /// - /// # Returns - /// The asset config transaction - pub async fn asset_config( - &self, - params: AssetConfigParams, - ) -> Result { - self.transaction(|composer| composer.add_asset_config(params)) - .await - } - - /// Create an Algorand Standard Asset destroy transaction. - /// - /// Created assets can be destroyed only by the asset manager account. - /// All of the assets must be owned by the creator of the asset before - /// the asset can be deleted. - /// - /// # Arguments - /// * `params` - The parameters for the asset destroy transaction - /// - /// # Returns - /// The asset destroy transaction - pub async fn asset_destroy( - &self, - params: AssetDestroyParams, - ) -> Result { - self.transaction(|composer| composer.add_asset_destroy(params)) - .await - } - - /// Create an Algorand Standard Asset freeze transaction. - /// - /// # Arguments - /// * `params` - The parameters for the asset freeze transaction - /// - /// # Returns - /// The asset freeze transaction - pub async fn asset_freeze( - &self, - params: AssetFreezeParams, - ) -> Result { - self.transaction(|composer| composer.add_asset_freeze(params)) - .await - } - - /// Create an Algorand Standard Asset unfreeze transaction. - /// - /// # Arguments - /// * `params` - The parameters for the asset unfreeze transaction - /// - /// # Returns - /// The asset unfreeze transaction - pub async fn asset_unfreeze( - &self, - params: AssetUnfreezeParams, - ) -> Result { - self.transaction(|composer| composer.add_asset_unfreeze(params)) - .await - } - - /// Create an Algorand Standard Asset clawback transaction. - /// - /// # Arguments - /// * `params` - The parameters for the asset clawback transaction - /// - /// # Returns - /// The asset clawback transaction - pub async fn asset_clawback( - &self, - params: AssetClawbackParams, - ) -> Result { - self.transaction(|composer| composer.add_asset_clawback(params)) - .await - } - - /// Create an application call transaction. - /// - /// **Note**: you may prefer to use `algorand.client` to get an app client for more advanced functionality. - /// - /// # Arguments - /// * `params` - The parameters for the app call transaction - /// - /// # Returns - /// The application call transaction - pub async fn app_call(&self, params: AppCallParams) -> Result { - self.transaction(|composer| composer.add_app_call(params)) - .await - } - - /// Create an application create transaction. - /// - /// **Note**: you may prefer to use `algorand.client` to get an app client for more advanced functionality. - /// - /// # Arguments - /// * `params` - The parameters for the app creation transaction - /// - /// # Returns - /// The application create transaction - pub async fn app_create(&self, params: AppCreateParams) -> Result { - self.transaction(|composer| composer.add_app_create(params)) - .await - } - - /// Create an application update transaction. - /// - /// **Note**: you may prefer to use `algorand.client` to get an app client for more advanced functionality. - /// - /// # Arguments - /// * `params` - The parameters for the app update transaction - /// - /// # Returns - /// The application update transaction - pub async fn app_update(&self, params: AppUpdateParams) -> Result { - self.transaction(|composer| composer.add_app_update(params)) - .await - } - - /// Create an application delete transaction. - /// - /// **Note**: you may prefer to use `algorand.client` to get an app client for more advanced functionality. - /// - /// # Arguments - /// * `params` - The parameters for the app deletion transaction - /// - /// # Returns - /// The application delete transaction - pub async fn app_delete(&self, params: AppDeleteParams) -> Result { - self.transaction(|composer| composer.add_app_delete(params)) - .await - } - - /// Create an application call with ABI method call transaction. - /// - /// **Note**: you may prefer to use `algorand.client` to get an app client for more advanced functionality. - /// - /// # Arguments - /// * `params` - The parameters for the ABI method call transaction - /// - /// # Returns - /// The application ABI method call transaction - pub async fn app_call_method_call( - &self, - params: AppCallMethodCallParams, - ) -> Result, ComposerError> { - self.built_transactions(|composer| composer.add_app_call_method_call(params)) - .await - } - - /// Create an application create call with ABI method call transaction. - /// - /// **Note**: you may prefer to use `algorand.client` to get an app client for more advanced functionality. - /// - /// # Arguments - /// * `params` - The parameters for the ABI method creation transaction - /// - /// # Returns - /// The application ABI method create transaction - pub async fn app_create_method_call( - &self, - params: AppCreateMethodCallParams, - ) -> Result, ComposerError> { - self.built_transactions(|composer| composer.add_app_create_method_call(params)) - .await - } - - /// Create an application update call with ABI method call transaction. - /// - /// **Note**: you may prefer to use `algorand.client` to get an app client for more advanced functionality. - /// - /// # Arguments - /// * `params` - The parameters for the ABI method update transaction - /// - /// # Returns - /// The application ABI method update transaction - pub async fn app_update_method_call( - &self, - params: AppUpdateMethodCallParams, - ) -> Result, ComposerError> { - self.built_transactions(|composer| composer.add_app_update_method_call(params)) - .await - } - - /// Create an application delete call with ABI method call transaction. - /// - /// **Note**: you may prefer to use `algorand.client` to get an app client for more advanced functionality. - /// - /// # Arguments - /// * `params` - The parameters for the ABI method deletion transaction - /// - /// # Returns - /// The application ABI method delete transaction - pub async fn app_delete_method_call( - &self, - params: AppDeleteMethodCallParams, - ) -> Result, ComposerError> { - self.built_transactions(|composer| composer.add_app_delete_method_call(params)) - .await - } - - /// Create an online key registration transaction. - /// - /// # Arguments - /// * `params` - The parameters for the key registration transaction - /// - /// # Returns - /// The online key registration transaction - pub async fn online_key_registration( - &self, - params: OnlineKeyRegistrationParams, - ) -> Result { - self.transaction(|composer| composer.add_online_key_registration(params)) - .await - } - - /// Create an offline key registration transaction. - /// - /// # Arguments - /// * `params` - The parameters for the key registration transaction - /// - /// # Returns - /// The offline key registration transaction - pub async fn offline_key_registration( - &self, - params: OfflineKeyRegistrationParams, - ) -> Result { - self.transaction(|composer| composer.add_offline_key_registration(params)) - .await - } - - /// Create a non-participation key registration transaction. - /// - /// # Arguments - /// * `params` - The parameters for the non-participation key registration transaction - /// - /// # Returns - /// The non participating key registration transaction - pub async fn non_participation_key_registration( - &self, - params: NonParticipationKeyRegistrationParams, - ) -> Result { - self.transaction(|composer| composer.add_non_participation_key_registration(params)) - .await - } - - async fn built_transactions( - &self, - composer_method: F, - ) -> Result, ComposerError> - where - F: FnOnce(&mut TransactionComposer) -> Result<(), ComposerError>, - { - let mut composer = (self.new_composer)(None); - composer_method(&mut composer)?; - let transactions_with_signers = composer.build().await?; - - Ok(transactions_with_signers - .iter() - .map(|ts| ts.transaction.clone()) - .collect()) - } -} diff --git a/crates/algokit_utils/src/transactions/key_registration.rs b/crates/algokit_utils/src/transactions/key_registration.rs deleted file mode 100644 index 45f5fbd4b..000000000 --- a/crates/algokit_utils/src/transactions/key_registration.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::create_transaction_params; -use algokit_transact::{KeyRegistrationTransactionFields, Transaction, TransactionHeader}; - -create_transaction_params! { - /// Parameters for creating an online key registration transaction. - #[derive(Clone, Default)] - pub struct OnlineKeyRegistrationParams { - /// The root participation public key - pub vote_key: [u8; 32], - /// The VRF public key - pub selection_key: [u8; 32], - /// The first round that the participation key is valid. Not to be confused with the first valid round of the keyreg transaction - pub vote_first: u64, - /// The last round that the participation key is valid. Not to be confused with the last valid round of the keyreg transaction - pub vote_last: u64, - /// This is the dilution for the 2-level participation key. It determines the interval (number of rounds) for generating new ephemeral keys - pub vote_key_dilution: u64, - /// The 64 byte state proof public key commitment - pub state_proof_key: Option<[u8; 64]>, - } -} - -create_transaction_params! { - /// Parameters for creating an offline key registration transaction. - #[derive(Clone, Default)] - pub struct OfflineKeyRegistrationParams { - } -} - -create_transaction_params! { - /// Parameters for creating an non participation key registration transaction. - /// - /// **Warning:** This will prevent the sender account from ever participating again. The account will also no longer earn rewards. - #[derive(Clone, Default)] - pub struct NonParticipationKeyRegistrationParams { - } -} - -pub fn build_online_key_registration( - params: &OnlineKeyRegistrationParams, - header: TransactionHeader, -) -> Transaction { - Transaction::KeyRegistration(KeyRegistrationTransactionFields { - header, - vote_key: Some(params.vote_key), - selection_key: Some(params.selection_key), - vote_first: Some(params.vote_first), - vote_last: Some(params.vote_last), - vote_key_dilution: Some(params.vote_key_dilution), - state_proof_key: params.state_proof_key, - non_participation: None, - }) -} - -pub fn build_offline_key_registration( - _params: &OfflineKeyRegistrationParams, - header: TransactionHeader, -) -> Transaction { - Transaction::KeyRegistration(KeyRegistrationTransactionFields { - header, - vote_key: None, - selection_key: None, - vote_first: None, - vote_last: None, - vote_key_dilution: None, - state_proof_key: None, - non_participation: None, - }) -} - -pub fn build_non_participation_key_registration( - _params: &NonParticipationKeyRegistrationParams, - header: TransactionHeader, -) -> Transaction { - Transaction::KeyRegistration(KeyRegistrationTransactionFields { - header, - vote_key: None, - selection_key: None, - vote_first: None, - vote_last: None, - vote_key_dilution: None, - state_proof_key: None, - non_participation: Some(true), - }) -} diff --git a/crates/algokit_utils/src/transactions/mod.rs b/crates/algokit_utils/src/transactions/mod.rs deleted file mode 100644 index 60c94c625..000000000 --- a/crates/algokit_utils/src/transactions/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -pub mod app_call; -pub mod asset_config; -pub mod asset_freeze; -pub mod asset_transfer; -pub mod common; -pub mod composer; -pub mod creator; -pub mod key_registration; -pub mod payment; -pub mod sender; - -// Re-export commonly used transaction types -pub use app_call::{ - AppCallMethodCallParams, AppCallParams, AppCreateMethodCallParams, AppCreateParams, - AppDeleteMethodCallParams, AppDeleteParams, AppMethodCallArg, AppUpdateMethodCallParams, - AppUpdateParams, -}; -pub use asset_config::{AssetConfigParams, AssetCreateParams, AssetDestroyParams}; -pub use asset_freeze::{AssetFreezeParams, AssetUnfreezeParams}; -pub use asset_transfer::{ - AssetClawbackParams, AssetOptInParams, AssetOptOutParams, AssetTransferParams, -}; -pub use common::{EmptySigner, TransactionSigner, TransactionWithSigner}; -pub use composer::{ - ComposerError, ComposerTransaction, ResourcePopulation, SendParams, TransactionComposer, - TransactionComposerConfig, TransactionComposerParams, TransactionComposerSendResult, - TransactionResult, -}; -pub use creator::TransactionCreator; -pub use key_registration::{ - NonParticipationKeyRegistrationParams, OfflineKeyRegistrationParams, - OnlineKeyRegistrationParams, -}; -pub use payment::{AccountCloseParams, PaymentParams}; -pub use sender::{ - SendAppCreateMethodCallResult, SendAppCreateResult, SendAppMethodCallResult, - SendAssetCreateResult, SendResult, TransactionSender, TransactionSenderError, -}; diff --git a/crates/algokit_utils/src/transactions/payment.rs b/crates/algokit_utils/src/transactions/payment.rs deleted file mode 100644 index 95938a371..000000000 --- a/crates/algokit_utils/src/transactions/payment.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::create_transaction_params; -use algokit_transact::{Address, PaymentTransactionFields, Transaction, TransactionHeader}; - -create_transaction_params! { - /// Parameters for creating a payment transaction - #[derive(Clone, Default)] - pub struct PaymentParams { - /// The address of the account receiving the ALGO payment. - pub receiver: Address, - /// The amount of microALGO to send. - /// - /// Specified in microALGO (1 ALGO = 1,000,000 microALGO). - pub amount: u64, - } -} - -create_transaction_params! { - /// Parameters for creating an account close transaction. - #[derive(Clone, Default)] - pub struct AccountCloseParams { - /// Close the sender account and send the remaining balance to this address - /// - /// *Warning:* Be careful this can lead to loss of funds if not used correctly. - pub close_remainder_to: Address, - } -} - -pub fn build_payment(params: &PaymentParams, header: TransactionHeader) -> Transaction { - Transaction::Payment(PaymentTransactionFields { - header, - receiver: params.receiver.clone(), - amount: params.amount, - close_remainder_to: None, - }) -} - -pub fn build_account_close(params: &AccountCloseParams, header: TransactionHeader) -> Transaction { - let sender = header.sender.clone(); - Transaction::Payment(PaymentTransactionFields { - header, - receiver: sender, - amount: 0, - close_remainder_to: Some(params.close_remainder_to.clone()), - }) -} diff --git a/crates/algokit_utils/src/transactions/sender.rs b/crates/algokit_utils/src/transactions/sender.rs deleted file mode 100644 index c4eea9a5f..000000000 --- a/crates/algokit_utils/src/transactions/sender.rs +++ /dev/null @@ -1,769 +0,0 @@ -use super::{ - app_call::{ - AppCallMethodCallParams, AppCallParams, AppCreateMethodCallParams, AppCreateParams, - AppDeleteMethodCallParams, AppDeleteParams, AppUpdateMethodCallParams, AppUpdateParams, - }, - asset_config::{AssetConfigParams, AssetCreateParams, AssetDestroyParams}, - asset_freeze::{AssetFreezeParams, AssetUnfreezeParams}, - asset_transfer::{ - AssetClawbackParams, AssetOptInParams, AssetOptOutParams, AssetTransferParams, - }, - composer::{ComposerError, SendParams, TransactionComposer, TransactionResult}, - key_registration::{ - NonParticipationKeyRegistrationParams, OfflineKeyRegistrationParams, - OnlineKeyRegistrationParams, - }, - payment::{AccountCloseParams, PaymentParams}, -}; -use crate::clients::asset_manager::{AssetManager, AssetManagerError}; -use crate::{clients::app_manager::AppManagerError, transactions::TransactionComposerConfig}; -use algod_client::apis::AlgodApiError; -use algod_client::models::PendingTransactionResponse; -use algokit_transact::{Address, Byte32, Transaction}; -use snafu::Snafu; - -use std::{str::FromStr, sync::Arc}; - -#[derive(Debug, Snafu)] -pub enum TransactionSenderError { - #[snafu(display("Algod client error: {source}"))] - AlgodClientError { source: AlgodApiError }, - #[snafu(display("Composer error: {source}"))] - ComposerError { source: ComposerError }, - #[snafu(display("Asset manager error: {source}"))] - AssetManagerError { source: AssetManagerError }, - #[snafu(display("App manager error: {source}"))] - AppManagerError { source: AppManagerError }, - #[snafu(display("Invalid parameters: {message}"))] - InvalidParameters { message: String }, - #[snafu(display("Transaction validation error: {message}"))] - ValidationError { message: String }, -} - -impl From for TransactionSenderError { - fn from(e: AlgodApiError) -> Self { - Self::AlgodClientError { source: e } - } -} - -impl From for TransactionSenderError { - fn from(e: ComposerError) -> Self { - Self::ComposerError { source: e } - } -} - -impl From for TransactionSenderError { - fn from(e: AssetManagerError) -> Self { - Self::AssetManagerError { source: e } - } -} - -impl From for TransactionSenderError { - fn from(e: AppManagerError) -> Self { - Self::AppManagerError { source: e } - } -} - -/// Result from sending a single transaction. -#[derive(Debug, Clone)] -pub struct SendResult { - /// The transaction that has been sent - pub transaction: Transaction, - /// The response from sending and waiting for the transaction - pub confirmation: PendingTransactionResponse, - /// The transaction ID that has been sent - pub transaction_id: String, -} - -/// Result from sending an asset create transaction. -#[derive(Debug, Clone)] -pub struct SendAssetCreateResult { - /// The transaction that has been sent - pub transaction: Transaction, - /// The response from sending and waiting for the transaction - pub confirmation: PendingTransactionResponse, - /// The transaction ID that has been sent - pub transaction_id: String, - /// The ID of the created asset - pub asset_id: u64, -} - -/// Result from sending an app create transaction. -#[derive(Debug, Clone)] -pub struct SendAppCreateResult { - /// The transaction that has been sent - pub transaction: Transaction, - /// The response from sending and waiting for the transaction - pub confirmation: PendingTransactionResponse, - /// The transaction ID that has been sent - pub transaction_id: String, - /// The ID of the created app - pub app_id: u64, - /// The address of the created app - pub app_address: Address, -} - -/// Result from sending an app method call transaction. -#[derive(Debug, Clone)] -pub struct SendAppMethodCallResult { - /// The result of the primary (last) transaction - pub result: TransactionResult, - /// All transaction results from the composer - pub group_results: Vec, - /// The group ID (optional) - pub group: Option, -} - -/// Result from sending an app create method call transaction. -#[derive(Debug, Clone)] -pub struct SendAppCreateMethodCallResult { - /// The result of the primary (last) transaction - pub result: TransactionResult, - /// All transaction results from the composer - pub group_results: Vec, - /// The group ID (optional) - pub group: Option, - /// The ID of the created app - pub app_id: u64, - /// The address of the created app - pub app_address: Address, -} - -/// Sends transactions and groups with validation and result processing. -#[derive(Clone)] -pub struct TransactionSender { - asset_manager: AssetManager, - new_composer: Arc) -> TransactionComposer>, -} - -impl TransactionSender { - /// Create a new transaction sender. - /// - /// # Arguments - /// * `new_composer` - Factory function for creating new transaction composers - /// * `asset_manager` - Asset manager for handling asset operations - /// - /// # Returns - /// A new `TransactionSender` instance - pub fn new( - new_composer: impl Fn(Option) -> TransactionComposer + 'static, - asset_manager: AssetManager, - ) -> Self { - Self { - asset_manager, - new_composer: Arc::new(new_composer), - } - } - - /// Create a new transaction composer group. - /// - /// # Arguments - /// * `params` - Optional configuration for the transaction composer - /// - /// # Returns - /// A new `Composer` instance - pub fn new_composer(&self, params: Option) -> TransactionComposer { - (self.new_composer)(params) - } - - async fn send_single_transaction( - &self, - add_transaction: F, - send_params: Option, - ) -> Result - where - F: FnOnce(&mut TransactionComposer) -> Result<(), ComposerError>, - { - let mut composer = self.new_composer(None); - add_transaction(&mut composer)?; - let composer_results = composer.send(send_params).await?; - - let result = - composer_results - .results - .last() - .ok_or(TransactionSenderError::ValidationError { - message: "No transaction returned".to_string(), - })?; - - Ok(SendResult { - transaction: result.transaction.clone(), - confirmation: result.confirmation.clone(), - transaction_id: result.transaction_id.clone(), - }) - } - - async fn send_single_transaction_with_result( - &self, - add_transaction: F, - transform_result: T, - send_params: Option, - ) -> Result - where - F: FnOnce(&mut TransactionComposer) -> Result<(), ComposerError>, - T: FnOnce(SendResult) -> Result, - { - let base_result = self - .send_single_transaction(add_transaction, send_params) - .await?; - transform_result(base_result) - } - - async fn send_method_call( - &self, - add_transaction: F, - send_params: Option, - ) -> Result - where - F: FnOnce(&mut TransactionComposer) -> Result<(), ComposerError>, - { - let mut composer = self.new_composer(None); - add_transaction(&mut composer)?; - let composer_results = composer.send(send_params).await?; - - let result = composer_results - .results - .last() - .ok_or(TransactionSenderError::ValidationError { - message: "No transaction returned".to_string(), - })? - .clone(); - - Ok(SendAppMethodCallResult { - result, - group_results: composer_results.results, - group: composer_results.group, - }) - } - - async fn send_method_call_with_result( - &self, - add_transaction: F, - transform_result: T, - send_params: Option, - ) -> Result - where - F: FnOnce(&mut TransactionComposer) -> Result<(), ComposerError>, - T: FnOnce(SendAppMethodCallResult) -> Result, - { - let base_result = self.send_method_call(add_transaction, send_params).await?; - transform_result(base_result) - } - - /// Send a payment transaction to transfer Algo between accounts. - /// - /// # Arguments - /// * `params` - The parameters for the payment transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the payment transaction and the transaction that was sent - pub async fn payment( - &self, - params: PaymentParams, - send_params: Option, - ) -> Result { - self.send_single_transaction(|composer| composer.add_payment(params), send_params) - .await - } - - /// Close an account and transfer remaining balance to another account. - /// - /// **Warning:** Be careful this can lead to loss of funds if not used correctly. - /// - /// # Arguments - /// * `params` - The parameters for the account close transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the account close transaction and the transaction that was sent - pub async fn account_close( - &self, - params: AccountCloseParams, - send_params: Option, - ) -> Result { - self.send_single_transaction(|composer| composer.add_account_close(params), send_params) - .await - } - - /// Transfer an Algorand Standard Asset. - /// - /// # Arguments - /// * `params` - The parameters for the asset transfer transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the asset transfer transaction and the transaction that was sent - pub async fn asset_transfer( - &self, - params: AssetTransferParams, - send_params: Option, - ) -> Result { - self.send_single_transaction(|composer| composer.add_asset_transfer(params), send_params) - .await - } - - /// Opt an account into an Algorand Standard Asset. - /// - /// # Arguments - /// * `params` - The parameters for the asset opt-in transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the asset opt-in transaction and the transaction that was sent - pub async fn asset_opt_in( - &self, - params: AssetOptInParams, - send_params: Option, - ) -> Result { - self.send_single_transaction(|composer| composer.add_asset_opt_in(params), send_params) - .await - } - - /// Opt an account out of an Algorand Standard Asset. - /// - /// **Note:** If the account has a balance of the asset, - /// it will not be able to opt-out unless `ensure_zero_balance` - /// is set to `false` (but then the account will lose the assets). - /// When no close remainder to address is specified, the asset creator will be resolved and used. - /// - /// # Arguments - /// * `params` - The parameters for the asset opt-out transaction - /// * `send_params` - Optional parameters for sending the transaction - /// * `ensure_zero_balance` - Whether to ensure the account has zero balance before opting out - /// - /// # Returns - /// The result of the asset opt-out transaction and the transaction that was sent - pub async fn asset_opt_out( - &self, - params: AssetOptOutParams, - send_params: Option, - ensure_zero_balance: Option, - ) -> Result { - if ensure_zero_balance.unwrap_or(true) { - // Ensure account has zero balance before opting out - let account_info = self - .asset_manager - .get_account_information(¶ms.sender, params.asset_id) - .await - .map_err(|e| TransactionSenderError::ValidationError { - message: format!( - "Account {} validation failed for Asset {}: {}", - params.sender, params.asset_id, e - ), - })?; - - let balance = account_info - .asset_holding - .as_ref() - .map(|h| h.amount) - .unwrap_or(0); - if balance != 0 { - return Err(TransactionSenderError::ValidationError { - message: format!( - "Account {} does not have a zero balance for Asset {}; can't opt-out.", - params.sender, params.asset_id - ), - }); - } - } - - // Resolve close_remainder_to to asset creator if not specified - let params = if params.close_remainder_to.is_none() { - let asset_info = self - .asset_manager - .get_by_id(params.asset_id) - .await - .map_err(|e| TransactionSenderError::ValidationError { - message: format!("Failed to get asset {} information: {}", params.asset_id, e), - })?; - - let creator = Address::from_str(&asset_info.creator).map_err(|e| { - TransactionSenderError::ValidationError { - message: format!( - "Invalid creator address for asset {}: {}", - params.asset_id, e - ), - } - })?; - - AssetOptOutParams { - close_remainder_to: Some(creator), - ..params - } - } else { - params - }; - - self.send_single_transaction(|composer| composer.add_asset_opt_out(params), send_params) - .await - } - - /// Create a new Algorand Standard Asset. - /// - /// The account that sends this transaction will automatically be - /// opted in to the asset and will hold all units after creation. - /// - /// # Arguments - /// * `params` - The parameters for the asset creation transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the asset create transaction and the transaction that was sent - pub async fn asset_create( - &self, - params: AssetCreateParams, - send_params: Option, - ) -> Result { - self.send_single_transaction_with_result( - |composer| composer.add_asset_create(params), - |base_result| { - let asset_id = base_result.confirmation.asset_id.ok_or_else(|| { - TransactionSenderError::ValidationError { - message: "Asset creation confirmation missing asset-index".to_string(), - } - })?; - Ok(SendAssetCreateResult { - transaction: base_result.transaction, - confirmation: base_result.confirmation, - transaction_id: base_result.transaction_id, - asset_id, - }) - }, - send_params, - ) - .await - } - - /// Configure an existing Algorand Standard Asset. - /// - /// **Note:** The manager, reserve, freeze, and clawback addresses - /// are immutably empty if they are not set. If manager is not set then - /// all fields are immutable from that point forward. - /// - /// # Arguments - /// * `params` - The parameters for the asset config transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the asset config transaction and the transaction that was sent - pub async fn asset_config( - &self, - params: AssetConfigParams, - send_params: Option, - ) -> Result { - self.send_single_transaction(|composer| composer.add_asset_config(params), send_params) - .await - } - - /// Destroys an Algorand Standard Asset. - /// - /// Created assets can be destroyed only by the asset manager account. - /// All of the assets must be owned by the creator of the asset before - /// the asset can be deleted. - /// - /// # Arguments - /// * `params` - The parameters for the asset destroy transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the asset destroy transaction and the transaction that was sent - pub async fn asset_destroy( - &self, - params: AssetDestroyParams, - send_params: Option, - ) -> Result { - self.send_single_transaction(|composer| composer.add_asset_destroy(params), send_params) - .await - } - - /// Freeze an Algorand Standard Asset for an account. - /// - /// # Arguments - /// * `params` - The parameters for the asset freeze transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the asset freeze transaction and the transaction that was sent - pub async fn asset_freeze( - &self, - params: AssetFreezeParams, - send_params: Option, - ) -> Result { - self.send_single_transaction(|composer| composer.add_asset_freeze(params), send_params) - .await - } - - /// Unfreeze an Algorand Standard Asset for an account. - /// - /// # Arguments - /// * `params` - The parameters for the asset unfreeze transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the asset unfreeze transaction and the transaction that was sent - pub async fn asset_unfreeze( - &self, - params: AssetUnfreezeParams, - send_params: Option, - ) -> Result { - self.send_single_transaction(|composer| composer.add_asset_unfreeze(params), send_params) - .await - } - - /// Clawback an Algorand Standard Asset from an account. - /// - /// # Arguments - /// * `params` - The parameters for the asset clawback transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the asset clawback transaction and the transaction that was sent - pub async fn asset_clawback( - &self, - params: AssetClawbackParams, - send_params: Option, - ) -> Result { - self.send_single_transaction(|composer| composer.add_asset_clawback(params), send_params) - .await - } - - /// Call a smart contract. - /// - /// # Arguments - /// * `params` - The parameters for the app call transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the app call transaction and the transaction that was sent - pub async fn app_call( - &self, - params: AppCallParams, - send_params: Option, - ) -> Result { - self.send_single_transaction(|composer| composer.add_app_call(params), send_params) - .await - } - - /// Create a smart contract. - /// - /// # Arguments - /// * `params` - The parameters for the app creation transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the app create transaction and the transaction that was sent - pub async fn app_create( - &self, - params: AppCreateParams, - send_params: Option, - ) -> Result { - self.send_single_transaction_with_result( - |composer| composer.add_app_create(params), - |base_result| { - let app_id = base_result.confirmation.app_id.ok_or_else(|| { - TransactionSenderError::ValidationError { - message: "App creation confirmation missing application-index".to_string(), - } - })?; - Ok(SendAppCreateResult { - transaction: base_result.transaction, - confirmation: base_result.confirmation, - transaction_id: base_result.transaction_id, - app_id, - app_address: Address::from_app_id(&app_id), - }) - }, - send_params, - ) - .await - } - - /// Update a smart contract. - /// - /// # Arguments - /// * `params` - The parameters for the app update transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the app update transaction and the transaction that was sent - pub async fn app_update( - &self, - params: AppUpdateParams, - send_params: Option, - ) -> Result { - self.send_single_transaction(|composer| composer.add_app_update(params), send_params) - .await - } - - /// Delete a smart contract. - /// - /// # Arguments - /// * `params` - The parameters for the app deletion transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the app delete transaction and the transaction that was sent - pub async fn app_delete( - &self, - params: AppDeleteParams, - send_params: Option, - ) -> Result { - self.send_single_transaction(|composer| composer.add_app_delete(params), send_params) - .await - } - - /// Call a smart contract via an ABI method. - /// - /// # Arguments - /// * `params` - The parameters for the app call transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the application ABI method call transaction and the transaction that was sent - pub async fn app_call_method_call( - &self, - params: AppCallMethodCallParams, - send_params: Option, - ) -> Result { - self.send_method_call( - |composer| composer.add_app_call_method_call(params), - send_params, - ) - .await - } - - /// Create a smart contract via an ABI method. - /// - /// # Arguments - /// * `params` - The parameters for the app creation transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the application ABI method create transaction and the transaction that was sent - pub async fn app_create_method_call( - &self, - params: AppCreateMethodCallParams, - send_params: Option, - ) -> Result { - self.send_method_call_with_result( - |composer| composer.add_app_create_method_call(params), - |base_result| { - let app_id = base_result.result.confirmation.app_id.ok_or_else(|| { - TransactionSenderError::ValidationError { - message: "App creation confirmation missing application-index".to_string(), - } - })?; - Ok(SendAppCreateMethodCallResult { - result: base_result.result, - group_results: base_result.group_results, - group: base_result.group, - app_id, - app_address: Address::from_app_id(&app_id), - }) - }, - send_params, - ) - .await - } - - /// Update a smart contract via an ABI method. - /// - /// # Arguments - /// * `params` - The parameters for the app update transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the application ABI method update transaction and the transaction that was sent - pub async fn app_update_method_call( - &self, - params: AppUpdateMethodCallParams, - send_params: Option, - ) -> Result { - self.send_method_call( - |composer| composer.add_app_update_method_call(params), - send_params, - ) - .await - } - - /// Delete a smart contract via an ABI method. - /// - /// # Arguments - /// * `params` - The parameters for the app deletion transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the application ABI method delete transaction and the transaction that was sent - pub async fn app_delete_method_call( - &self, - params: AppDeleteMethodCallParams, - send_params: Option, - ) -> Result { - self.send_method_call( - |composer| composer.add_app_delete_method_call(params), - send_params, - ) - .await - } - - /// Register an online key. - /// - /// # Arguments - /// * `params` - The parameters for the key registration transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the online key registration transaction and the transaction that was sent - pub async fn online_key_registration( - &self, - params: OnlineKeyRegistrationParams, - send_params: Option, - ) -> Result { - self.send_single_transaction( - |composer| composer.add_online_key_registration(params), - send_params, - ) - .await - } - - /// Register an offline key. - /// - /// # Arguments - /// * `params` - The parameters for the key registration transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the offline key registration transaction and the transaction that was sent - pub async fn offline_key_registration( - &self, - params: OfflineKeyRegistrationParams, - send_params: Option, - ) -> Result { - self.send_single_transaction( - |composer| composer.add_offline_key_registration(params), - send_params, - ) - .await - } - - /// Register a non-participation key. - /// - /// # Arguments - /// * `params` - The parameters for the key registration transaction - /// * `send_params` - Optional parameters for sending the transaction - /// - /// # Returns - /// The result of the non-participation key registration transaction and the transaction that was sent - pub async fn non_participation_key_registration( - &self, - params: NonParticipationKeyRegistrationParams, - send_params: Option, - ) -> Result { - self.send_single_transaction( - |composer| composer.add_non_participation_key_registration(params), - send_params, - ) - .await - } -} diff --git a/crates/algokit_utils/tests/algod/block.rs b/crates/algokit_utils/tests/algod/block.rs deleted file mode 100644 index a613ebbf2..000000000 --- a/crates/algokit_utils/tests/algod/block.rs +++ /dev/null @@ -1,41 +0,0 @@ -// Block tests -// These tests demonstrate the integration test structure and API communication - -use algokit_utils::ClientManager; - -use crate::common::logging::init_test_logging; - -#[tokio::test] -async fn test_block_endpoint() { - init_test_logging(); - - let config = - ClientManager::get_algonode_config("testnet", algokit_utils::AlgorandService::Algod); - let algod_client = ClientManager::get_algod_client(&config).unwrap(); - let large_block_with_state_proof_txns = 24098947; - let block_response = algod_client - .get_block(large_block_with_state_proof_txns, Some(false)) - .await - .unwrap(); - - assert!(block_response.cert.is_some()); - assert!(block_response.block.state_proof_tracking.is_some()); - assert!(block_response.block.transactions.is_some()); - - // Validate deeply nested signed transaction fields are present and - // leverage transact crate model - let transactions = block_response - .block - .transactions - .as_ref() - .expect("expected transactions"); - assert!(!transactions.is_empty()); - assert_eq!( - transactions[0] - .signed_transaction - .transaction - .sender() - .as_str(), - "XM6FEYVJ2XDU2IBH4OT6VZGW75YM63CM4TC6AV6BD3JZXFJUIICYTVB5EU" - ); -} diff --git a/crates/algokit_utils/tests/algod/mod.rs b/crates/algokit_utils/tests/algod/mod.rs deleted file mode 100644 index 064e0e769..000000000 --- a/crates/algokit_utils/tests/algod/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod block; -pub mod pending_transaction_information; -pub mod raw_transaction; -pub mod simulate_transactions; -pub mod transaction_params; diff --git a/crates/algokit_utils/tests/algod/pending_transaction_information.rs b/crates/algokit_utils/tests/algod/pending_transaction_information.rs deleted file mode 100644 index a75c13349..000000000 --- a/crates/algokit_utils/tests/algod/pending_transaction_information.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::common::{ - AlgorandFixtureResult, NetworkType, TestAccountConfig, TestResult, algorand_fixture, -}; -use algokit_transact::{ - AlgorandMsgpack, PaymentTransactionBuilder, Transaction, TransactionHeaderBuilder, -}; -use algokit_utils::TransactionSigner; -use rstest::rstest; -use std::convert::TryInto; - -#[rstest] -#[tokio::test] -async fn test_pending_transaction_broadcast( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - - // Create algod client using ClientManager - let algod_client = algorand_fixture.algod.clone(); - - // Create account manager and generate test accounts - let sender_config = TestAccountConfig { - initial_funds: 10_000_000, // 10 ALGO - network_type: NetworkType::LocalNet, - funding_note: Some("Test sender account".to_string()), - }; - - let receiver_config = TestAccountConfig { - initial_funds: 1_000_000, // 1 ALGO - network_type: NetworkType::LocalNet, - funding_note: Some("Test receiver account".to_string()), - }; - - let sender = algorand_fixture - .generate_account(Some(sender_config)) - .await?; - - let receiver = algorand_fixture - .generate_account(Some(receiver_config)) - .await?; - - let sender_account = sender.account(); - let receiver_account = receiver.account(); - - // Get transaction parameters - let params = algod_client.transaction_params().await?; - - // 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_account.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"Test payment transaction".to_vec()) - .build()?; - - // Build payment transaction - let payment_fields = PaymentTransactionBuilder::default() - .header(header) - .receiver(receiver_account.address()) - .amount(500_000) // 0.5 ALGO - .build_fields()?; - - let transaction = Transaction::Payment(payment_fields); - let signed_transaction = sender.sign_transaction(&transaction).await?; - let signed_bytes = signed_transaction.encode().unwrap(); - - let response = algod_client.raw_transaction(signed_bytes).await?; - - assert!( - !response.tx_id.is_empty(), - "Response should contain a transaction ID" - ); - - let pending_transaction = algod_client - .pending_transaction_information(&response.tx_id) - .await?; - - assert_eq!(pending_transaction.pool_error, ""); - assert!(pending_transaction.confirmed_round.is_some()); - - Ok(()) -} diff --git a/crates/algokit_utils/tests/algod/raw_transaction.rs b/crates/algokit_utils/tests/algod/raw_transaction.rs deleted file mode 100644 index 2f36bbddc..000000000 --- a/crates/algokit_utils/tests/algod/raw_transaction.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::common::{ - AlgorandFixtureResult, NetworkType, TestAccountConfig, TestResult, algorand_fixture, -}; -use algokit_transact::{ - AlgorandMsgpack, PaymentTransactionBuilder, Transaction, TransactionHeaderBuilder, -}; -use algokit_utils::TransactionSigner; -use rstest::rstest; -use std::convert::TryInto; - -#[rstest] -#[tokio::test] -async fn test_raw_transaction_broadcast( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - - // Create algod client using ClientManager - let algod_client = algorand_fixture.algod.clone(); - - // Create account manager and generate test accounts - let sender_config = TestAccountConfig { - initial_funds: 10_000_000, // 10 ALGO - network_type: NetworkType::LocalNet, - funding_note: Some("Test sender account".to_string()), - }; - - let receiver_config = TestAccountConfig { - initial_funds: 1_000_000, // 1 ALGO - network_type: NetworkType::LocalNet, - funding_note: Some("Test receiver account".to_string()), - }; - - let sender = algorand_fixture - .generate_account(Some(sender_config)) - .await?; - - let receiver = algorand_fixture - .generate_account(Some(receiver_config)) - .await?; - - let sender_account = sender.account(); - let receiver_account = receiver.account(); - - // Get transaction parameters - let params = algod_client.transaction_params().await?; - - // 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_account.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"Test payment transaction".to_vec()) - .build()?; - - // Build payment transaction - let payment_fields = PaymentTransactionBuilder::default() - .header(header) - .receiver(receiver_account.address()) - .amount(500_000) // 0.5 ALGO - .build_fields()?; - - let transaction = Transaction::Payment(payment_fields); - let signed_transaction = sender.sign_transaction(&transaction).await?; - - let signed_bytes = signed_transaction.encode().unwrap(); - - let response = algod_client.raw_transaction(signed_bytes).await?; - - assert!( - !response.tx_id.is_empty(), - "Response should contain a transaction ID" - ); - - Ok(()) -} diff --git a/crates/algokit_utils/tests/algod/simulate_transactions.rs b/crates/algokit_utils/tests/algod/simulate_transactions.rs deleted file mode 100644 index 46138a684..000000000 --- a/crates/algokit_utils/tests/algod/simulate_transactions.rs +++ /dev/null @@ -1,84 +0,0 @@ -// Simulate transaction tests -// These tests demonstrate the integration test structure and API communication - -use algod_client::{ - apis::Format, - models::{SimulateRequest, SimulateRequestTransactionGroup, SimulateTraceConfig}, -}; -use algokit_transact::{SignedTransaction, test_utils::TransactionMother}; -use algokit_utils::ClientManager; - -use crate::common::logging::init_test_logging; - -#[tokio::test] -async fn test_simulate_transactions() { - init_test_logging(); - - // Create algod client using ClientManager - let config = ClientManager::get_config_from_environment_or_localnet(); - let algod_client = ClientManager::get_algod_client(&config.algod_config).unwrap(); - - // Create multiple transactions for group simulation using TransactionMother from algokit_transact - let transaction1 = TransactionMother::simple_payment().build().unwrap(); - let transaction2 = TransactionMother::payment_with_note().build().unwrap(); - - let signed_transactions = vec![ - SignedTransaction { - transaction: transaction1, - signature: None, - auth_address: None, - multisignature: None, - }, - SignedTransaction { - transaction: transaction2, - signature: None, - auth_address: None, - multisignature: 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 = 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/algokit_utils/tests/algod/transaction_params.rs b/crates/algokit_utils/tests/algod/transaction_params.rs deleted file mode 100644 index d6f6ca6cd..000000000 --- a/crates/algokit_utils/tests/algod/transaction_params.rs +++ /dev/null @@ -1,48 +0,0 @@ -use algod_client::AlgodClient; -use algokit_http_client::DefaultHttpClient; -use algokit_utils::ClientManager; -use std::sync::Arc; - -use crate::common::logging::init_test_logging; - -#[tokio::test] -async fn test_get_transaction_params() { - init_test_logging(); - - // Create algod client using ClientManager - let config = ClientManager::get_config_from_environment_or_localnet(); - let algod_client = ClientManager::get_algod_client(&config.algod_config).unwrap(); - - // Call the transaction params endpoint - let result = algod_client.transaction_params().await; - - // Verify the call succeeded - assert!( - result.is_ok(), - "Get transaction params call should succeed: {:?}", - result.err() - ); - - let params = result.unwrap(); - - // Basic validation of the response - assert!( - !params.genesis_id.is_empty(), - "Genesis ID should not be empty" - ); - assert!(params.min_fee > 0, "Min fee should be greater than 0"); -} - -#[tokio::test] -async fn test_transaction_params_error_handling() { - init_test_logging(); - - // Test with an invalid algod client (should fail) - let http_client = Arc::new(DefaultHttpClient::new("http://invalid-host:4001")); - let algod_client = AlgodClient::new(http_client); - - let result = algod_client.transaction_params().await; - - // This should fail due to invalid host - assert!(result.is_err(), "Call to invalid algod should fail"); -} diff --git a/crates/algokit_utils/tests/algod_tests.rs b/crates/algokit_utils/tests/algod_tests.rs deleted file mode 100644 index d59c55506..000000000 --- a/crates/algokit_utils/tests/algod_tests.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod algod; -mod common; diff --git a/crates/algokit_utils/tests/applications/app_client/client_management.rs b/crates/algokit_utils/tests/applications/app_client/client_management.rs deleted file mode 100644 index 653bdcf41..000000000 --- a/crates/algokit_utils/tests/applications/app_client/client_management.rs +++ /dev/null @@ -1,147 +0,0 @@ -use crate::common::{AlgorandFixtureResult, TestResult, algorand_fixture, deploy_arc56_contract}; -use algokit_abi::{ABIValue, Arc56Contract}; -use algokit_transact::{OnApplicationComplete, StateSchema}; -use algokit_utils::applications::app_client::{AppClient, AppClientMethodCallParams}; -use algokit_utils::clients::app_manager::AppManager; -use algokit_utils::{AlgorandClient as RootAlgorandClient, AppCreateParams, AppMethodCallArg}; -use rstest::*; -use std::collections::HashMap; -use std::sync::Arc; - -fn get_sandbox_spec() -> Arc56Contract { - Arc56Contract::from_json(algokit_test_artifacts::sandbox::APPLICATION_ARC56) - .expect("valid arc56") -} - -fn get_hello_world_spec() -> Arc56Contract { - Arc56Contract::from_json(algokit_test_artifacts::hello_world::APPLICATION_ARC56) - .expect("valid arc56") -} - -#[rstest] -#[tokio::test] -async fn from_network_resolves_id(#[future] algorand_fixture: AlgorandFixtureResult) -> TestResult { - let fixture = algorand_fixture.await?; - let sender = fixture.test_account.account().address(); - - let spec = get_hello_world_spec(); - let app_id = deploy_arc56_contract(&fixture, &sender, &spec, None, None, None).await?; - - let mut spec_with_networks = spec.clone(); - spec_with_networks.networks = Some(HashMap::from([( - "localnet".to_string(), - algokit_abi::arc56_contract::Network { app_id }, - )])); - - let client = AppClient::from_network( - spec_with_networks, - RootAlgorandClient::default_localnet(None).into(), - None, - None, - None, - None, - None, - ) - .await?; - - assert_eq!(client.app_id(), app_id); - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn from_creator_and_name_resolves_and_can_call( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let fixture = algorand_fixture.await?; - let sender = fixture.test_account.account().address(); - - let spec = get_sandbox_spec(); - let src = spec.source.as_ref().expect("source expected"); - let approval_teal = src.get_decoded_approval().expect("approval"); - let clear_teal = src.get_decoded_clear().expect("clear"); - - let app_manager: &AppManager = fixture.algorand_client.app(); - let compiled_approval = app_manager.compile_teal(&approval_teal).await?; - let compiled_clear = app_manager.compile_teal(&clear_teal).await?; - - let app_name = "MY_APP".to_string(); - let deploy_note = format!( - "{}:j{}", - "ALGOKIT_DEPLOYER", - serde_json::json!({ - "name": app_name, - "version": "1.0", - "updatable": false, - "deletable": false - }) - ); - - let create_params = AppCreateParams { - sender: sender.clone(), - on_complete: OnApplicationComplete::NoOp, - approval_program: compiled_approval.compiled_base64_to_bytes.clone(), - clear_state_program: compiled_clear.compiled_base64_to_bytes.clone(), - global_state_schema: Some(StateSchema { - num_uints: spec.state.schema.global_state.ints, - num_byte_slices: spec.state.schema.global_state.bytes, - }), - local_state_schema: Some(StateSchema { - num_uints: spec.state.schema.local_state.ints, - num_byte_slices: spec.state.schema.local_state.bytes, - }), - note: Some(deploy_note.into_bytes()), - ..Default::default() - }; - - let mut composer = fixture.algorand_client.new_composer(None); - composer.add_app_create(create_params)?; - let create_group = composer.send(None).await?; - let app_id = create_group.results[0] - .confirmation - .app_id - .expect("No app ID returned"); - - fixture - .wait_for_indexer_transaction(&create_group.results[0].transaction_id) - .await?; - - let algorand = RootAlgorandClient::default_localnet(None); - let client = AppClient::from_creator_and_name( - &sender.to_string(), - &app_name, - spec.clone(), - algorand.into(), - Some(sender.to_string()), - Some(Arc::new(fixture.test_account.clone())), - None, - None, - None, - ) - .await?; - - assert_eq!(client.app_id(), app_id); - assert_eq!(client.app_name(), Some(&app_name)); - - let res = client - .send() - .call( - AppClientMethodCallParams { - method: "hello_world".to_string(), - args: vec![AppMethodCallArg::ABIValue(ABIValue::from("test"))], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - - let abi_ret = res.result.abi_return.as_ref().expect("abi return"); - match &abi_ret.return_value { - Some(ABIValue::String(s)) => assert_eq!(s, "Hello, test"), - _ => return Err("expected string return".into()), - } - - Ok(()) -} diff --git a/crates/algokit_utils/tests/applications/app_client/compilation.rs b/crates/algokit_utils/tests/applications/app_client/compilation.rs deleted file mode 100644 index 39426d78e..000000000 --- a/crates/algokit_utils/tests/applications/app_client/compilation.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::common::TestResult; -use crate::common::app_fixture::testing_app_fixture; -use algokit_utils::applications::app_client::CompilationParams; -use algokit_utils::clients::app_manager::TealTemplateValue; -use algokit_utils::config::{AppCompiledEventData, EventData, EventType}; -use rstest::*; - -#[rstest] -#[tokio::test] -async fn compile_applies_template_params_and_emits_event( - #[future] testing_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - // Use an app name to assert AppCompiled event has a name - let f = testing_app_fixture.await?; - algokit_utils::config::Config::configure(Some(true), None); - let mut events = algokit_utils::config::Config::events().subscribe(); - let client = f.client; - - let compilation_params = CompilationParams { - deploy_time_params: Some( - [("VALUE", 1), ("UPDATABLE", 0), ("DELETABLE", 0)] - .into_iter() - .map(|(k, v)| (k.to_string(), TealTemplateValue::Int(v))) - .collect(), - ), - updatable: Some(false), - deletable: Some(false), - }; - client.compile(&compilation_params).await?; - - if let Ok((event_type, data)) = - tokio::time::timeout(std::time::Duration::from_millis(5000), events.recv()).await? - { - assert_eq!(event_type, EventType::AppCompiled); - match data { - EventData::AppCompiled(AppCompiledEventData { - app_name, - approval_source_map, - clear_source_map, - }) => { - assert!(app_name.is_none() || app_name.as_deref() == Some("TestingApp")); - assert!(approval_source_map.is_some()); - assert!(clear_source_map.is_some()); - } - _ => return Err("unexpected event data".into()), - } - } else { - return Err("expected AppCompiled event".into()); - } - - Ok(()) -} diff --git a/crates/algokit_utils/tests/applications/app_client/default_values.rs b/crates/algokit_utils/tests/applications/app_client/default_values.rs deleted file mode 100644 index 07ea8883f..000000000 --- a/crates/algokit_utils/tests/applications/app_client/default_values.rs +++ /dev/null @@ -1,300 +0,0 @@ -use crate::common::TestResult; -use crate::common::app_fixture::testing_app_fixture; -use algokit_abi::ABIValue; -use algokit_utils::AppMethodCallArg; -use algokit_utils::applications::app_client::AppClientMethodCallParams; -use num_bigint::BigUint; -use rstest::*; - -#[rstest] -#[tokio::test] -async fn test_default_value_from_literal( - #[future] testing_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let f = testing_app_fixture.await?; - let client = f.client; - let sender = f.sender_address; - - let defined = client - .send() - .call( - AppClientMethodCallParams { - method: "default_value".to_string(), - args: vec![AppMethodCallArg::ABIValue(ABIValue::from("defined value"))], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - let defined_ret = defined - .result - .abi_return - .and_then(|r| r.return_value) - .expect("Expected ABI return value"); - match defined_ret { - ABIValue::String(s) => assert_eq!(s, "defined value"), - _ => return Err("Expected string return".into()), - } - - let defaulted = client - .send() - .call( - AppClientMethodCallParams { - method: "default_value".to_string(), - args: vec![AppMethodCallArg::DefaultValue], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - let default_ret = defaulted - .result - .abi_return - .and_then(|r| r.return_value) - .expect("Expected ABI return value"); - match default_ret { - ABIValue::String(s) => assert_eq!(s, "default value"), - _ => return Err("Expected string return".into()), - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_default_value_from_method( - #[future] testing_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let f = testing_app_fixture.await?; - let client = f.client; - let sender = f.sender_address; - - let defined = client - .send() - .call( - AppClientMethodCallParams { - method: "default_value_from_abi".to_string(), - args: vec![AppMethodCallArg::ABIValue(ABIValue::from("defined value"))], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - let defined_ret = defined - .result - .abi_return - .and_then(|r| r.return_value) - .expect("Expected ABI return value"); - match defined_ret { - ABIValue::String(s) => assert_eq!(s, "ABI, defined value"), - _ => return Err("Expected string return".into()), - } - - let defaulted = client - .send() - .call( - AppClientMethodCallParams { - method: "default_value_from_abi".to_string(), - args: vec![AppMethodCallArg::DefaultValue], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - let default_ret = defaulted - .result - .abi_return - .and_then(|r| r.return_value) - .expect("Expected ABI return value"); - match default_ret { - ABIValue::String(s) => assert_eq!(s, "ABI, default value"), - _ => return Err("Expected string return".into()), - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_default_value_from_global_state( - #[future] testing_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let f = testing_app_fixture.await?; - let client = f.client; - let sender = f.sender_address; - - client - .send() - .call( - AppClientMethodCallParams { - method: "set_global".to_string(), - args: vec![ - AppMethodCallArg::ABIValue(ABIValue::from(456u64)), - AppMethodCallArg::ABIValue(ABIValue::from(2u64)), - AppMethodCallArg::ABIValue(ABIValue::from("asdf")), - AppMethodCallArg::ABIValue(ABIValue::Array(vec![ - ABIValue::from_byte(1), - ABIValue::from_byte(2), - ABIValue::from_byte(3), - ABIValue::from_byte(4), - ])), - ], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - - let defined = client - .send() - .call( - AppClientMethodCallParams { - method: "default_value_from_global_state".to_string(), - args: vec![AppMethodCallArg::ABIValue(ABIValue::from(123u64))], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - let defined_ret = defined - .result - .abi_return - .and_then(|r| r.return_value) - .expect("Expected ABI return value"); - match defined_ret { - ABIValue::Uint(v) => assert_eq!(v, BigUint::from(123u64)), - _ => return Err("Expected uint return".into()), - } - - let defaulted = client - .send() - .call( - AppClientMethodCallParams { - method: "default_value_from_global_state".to_string(), - args: vec![AppMethodCallArg::DefaultValue], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - let default_ret = defaulted - .result - .abi_return - .and_then(|r| r.return_value) - .expect("Expected ABI return value"); - match default_ret { - ABIValue::Uint(v) => assert_eq!(v, BigUint::from(456u64)), - _ => return Err("Expected uint return".into()), - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_default_value_from_local_state( - #[future] testing_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let f = testing_app_fixture.await?; - let client = f.client; - let sender = f.sender_address; - - client - .send() - .opt_in( - AppClientMethodCallParams { - method: "opt_in".to_string(), - args: vec![], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - ) - .await?; - - client - .send() - .call( - AppClientMethodCallParams { - method: "set_local".to_string(), - args: vec![ - AppMethodCallArg::ABIValue(ABIValue::from(1u64)), - AppMethodCallArg::ABIValue(ABIValue::from(2u64)), - AppMethodCallArg::ABIValue(ABIValue::from("banana")), - AppMethodCallArg::ABIValue(ABIValue::Array(vec![ - ABIValue::from_byte(1), - ABIValue::from_byte(2), - ABIValue::from_byte(3), - ABIValue::from_byte(4), - ])), - ], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - - let defined = client - .send() - .call( - AppClientMethodCallParams { - method: "default_value_from_local_state".to_string(), - args: vec![AppMethodCallArg::ABIValue(ABIValue::from("defined value"))], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - let defined_ret = defined - .result - .abi_return - .and_then(|r| r.return_value) - .expect("Expected ABI return value"); - match defined_ret { - ABIValue::String(s) => assert_eq!(s, "Local state, defined value"), - _ => return Err("Expected string return".into()), - } - - let defaulted = client - .send() - .call( - AppClientMethodCallParams { - method: "default_value_from_local_state".to_string(), - args: vec![AppMethodCallArg::DefaultValue], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - let default_ret = defaulted - .result - .abi_return - .and_then(|r| r.return_value) - .expect("Expected ABI return value"); - match default_ret { - ABIValue::String(s) => assert_eq!(s, "Local state, banana"), - _ => return Err("Expected string return".into()), - } - - Ok(()) -} diff --git a/crates/algokit_utils/tests/applications/app_client/error_handling.rs b/crates/algokit_utils/tests/applications/app_client/error_handling.rs deleted file mode 100644 index e87319b69..000000000 --- a/crates/algokit_utils/tests/applications/app_client/error_handling.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::common::TestResult; -use crate::common::app_fixture::testing_app_fixture; -use algokit_utils::applications::app_client::{AppClientError, AppClientMethodCallParams}; -use rstest::*; - -#[rstest] -#[tokio::test] -async fn test_exposing_logic_error_without_sourcemaps( - #[future] testing_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let f = testing_app_fixture.await?; - let sender = f.sender_address; - let client = f.client; - - let error_response = client - .send() - .call( - AppClientMethodCallParams { - method: "error".to_string(), - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await - .expect_err("expected logic error"); - - if let AppClientError::LogicError { logic, .. } = &error_response { - assert!(logic.message.contains("assert failed pc=885")); - } - - Ok(()) -} - -// NOTE: more comprehensive version with source maps will be added in app factory pr diff --git a/crates/algokit_utils/tests/applications/app_client/mod.rs b/crates/algokit_utils/tests/applications/app_client/mod.rs deleted file mode 100644 index 2b1242fe9..000000000 --- a/crates/algokit_utils/tests/applications/app_client/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod client_management; -pub mod compilation; -pub mod default_values; -pub mod error_handling; -pub mod params; -pub mod send; -pub mod state; -pub mod structs; diff --git a/crates/algokit_utils/tests/applications/app_client/params.rs b/crates/algokit_utils/tests/applications/app_client/params.rs deleted file mode 100644 index aae31e7a2..000000000 --- a/crates/algokit_utils/tests/applications/app_client/params.rs +++ /dev/null @@ -1,220 +0,0 @@ -use crate::common::TestResult; -use crate::common::app_fixture::{sandbox_app_fixture, testing_app_fixture}; -use algokit_abi::ABIValue; -use algokit_transact::BoxReference; -use algokit_utils::applications::app_client::AppClientBareCallParams; -use algokit_utils::applications::app_client::{AppClientMethodCallParams, FundAppAccountParams}; -use algokit_utils::{AppMethodCallArg, PaymentParams}; -use rstest::*; - -#[rstest] -#[tokio::test] -async fn params_build_method_call_and_defaults( - #[future] testing_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let f = testing_app_fixture.await?; - let sender = f.sender_address; - let client = f.client; - - client - .send() - .call( - AppClientMethodCallParams { - method: "set_global".to_string(), - args: vec![ - AppMethodCallArg::ABIValue(ABIValue::from(999u64)), - AppMethodCallArg::ABIValue(ABIValue::from(2u64)), - AppMethodCallArg::ABIValue(ABIValue::from("seed")), - AppMethodCallArg::ABIValue(ABIValue::Array(vec![ - ABIValue::from_byte(1), - ABIValue::from_byte(2), - ABIValue::from_byte(3), - ABIValue::from_byte(4), - ])), - ], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - - client - .send() - .opt_in( - AppClientMethodCallParams { - method: "opt_in".to_string(), - args: vec![], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - ) - .await?; - client - .send() - .call( - AppClientMethodCallParams { - method: "set_local".to_string(), - args: vec![ - AppMethodCallArg::ABIValue(ABIValue::from(1u64)), - AppMethodCallArg::ABIValue(ABIValue::from(2u64)), - AppMethodCallArg::ABIValue(ABIValue::from("bananas")), - AppMethodCallArg::ABIValue(ABIValue::Array(vec![ - ABIValue::from_byte(1), - ABIValue::from_byte(2), - ABIValue::from_byte(3), - ABIValue::from_byte(4), - ])), - ], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - - let built = client - .params() - .call( - AppClientMethodCallParams { - method: "default_value_from_local_state".to_string(), - args: vec![AppMethodCallArg::DefaultValue], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - ) - .await?; - - assert_eq!(built.method.name, "default_value_from_local_state"); - assert_eq!(built.args.len(), 1); - match &built.args[0] { - AppMethodCallArg::ABIValue(ABIValue::String(s)) => assert_eq!(s, "bananas"), - _ => return Err("expected string arg resolved from local state".into()), - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn params_build_includes_foreign_references_from_args( - #[future] testing_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let mut f = testing_app_fixture.await?; - let sender = f.sender_address.clone(); - let client = &f.client; - let extra = f.algorand_fixture.generate_account(None).await?; - let extra_addr = extra.account().address().to_string(); - - let built = client - .params() - .call( - AppClientMethodCallParams { - method: "call_abi_foreign_refs".to_string(), - args: vec![], - sender: Some(sender.to_string()), - account_references: Some(vec![extra_addr.clone()]), - app_references: Some(vec![345]), - asset_references: Some(vec![567]), - ..Default::default() - }, - None, - ) - .await?; - - assert!(!built.account_references.as_ref().unwrap().is_empty()); - assert!(built.app_references.as_ref().unwrap().contains(&345)); - assert!(built.asset_references.as_ref().unwrap().contains(&567)); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn params_build_bare_and_fund_payment( - #[future] sandbox_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let f = sandbox_app_fixture.await?; - let sender = f.sender_address; - let client = f.client; - - let bare = client.params().bare().call( - AppClientBareCallParams { - args: None, - sender: Some(sender.to_string()), - box_references: Some(vec![BoxReference { - app_id: 0, - name: b"1".to_vec(), - }]), - ..Default::default() - }, - None, - )?; - assert_eq!(bare.box_references.as_ref().unwrap()[0].name, b"1".to_vec()); - - let pay: PaymentParams = client.params().fund_app_account(&FundAppAccountParams { - amount: 200_000, - sender: Some(sender.to_string()), - ..Default::default() - })?; - assert_eq!(pay.amount, 200_000); - assert_eq!(pay.receiver, client.app_address()); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn params_construct_txn_with_abi_tx_arg_and_return( - #[future] sandbox_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let f = sandbox_app_fixture.await?; - let sender = f.sender_address; - let client = f.client; - - let payment = PaymentParams { - sender: sender.clone(), - 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, - receiver: sender.clone(), - amount: 123, - }; - - let result = client - .send() - .call( - AppClientMethodCallParams { - method: "get_pay_txn_amount".to_string(), - args: vec![AppMethodCallArg::Payment(payment)], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - - assert_eq!(result.group_results.len(), 2); - let abi_ret = result - .result - .abi_return - .as_ref() - .expect("abi return expected"); - match &abi_ret.return_value { - Some(ABIValue::Uint(u)) => assert_eq!(*u, num_bigint::BigUint::from(123u32)), - _ => return Err("expected uint return".into()), - } - Ok(()) -} diff --git a/crates/algokit_utils/tests/applications/app_client/send.rs b/crates/algokit_utils/tests/applications/app_client/send.rs deleted file mode 100644 index 1ad31e879..000000000 --- a/crates/algokit_utils/tests/applications/app_client/send.rs +++ /dev/null @@ -1,426 +0,0 @@ -use crate::common::app_fixture::{sandbox_app_fixture, testing_app_fixture, testing_app_spec}; -use crate::common::{TestResult, nested_contract_fixture}; -use algokit_abi::{ABIMethod, ABIValue}; -use algokit_transact::{BoxReference, SignedTransaction, Transaction}; -use algokit_utils::applications::app_client::AppClientMethodCallParams; -use algokit_utils::transactions::{PaymentParams, TransactionSigner, TransactionWithSigner}; -use algokit_utils::{AppCallMethodCallParams, AppManager, AppMethodCallArg}; -use async_trait::async_trait; -use num_bigint::BigUint; -use rand::Rng; -use rstest::*; -use std::str::FromStr; -use std::sync::{Arc, Mutex}; - -#[rstest] -#[tokio::test] -async fn test_create_then_call_app( - #[future] testing_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let f = testing_app_fixture.await?; - let client = f.client; - let sender = f.sender_address; - - let result = client - .send() - .call( - AppClientMethodCallParams { - method: "call_abi".to_string(), - args: vec![AppMethodCallArg::ABIValue(ABIValue::from("test"))], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - - let abi_return = result.result.abi_return.expect("Expected ABI return"); - match abi_return.return_value { - Some(ABIValue::String(s)) => assert_eq!(s, "Hello, test"), - _ => return Err("Expected string ABI return".into()), - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_construct_transaction_with_abi_encoding_including_transaction( - #[future] testing_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let mut f = testing_app_fixture.await?; - let funded_account = f.algorand_fixture.generate_account(None).await?; - let funded_addr = funded_account.account().address(); - - let mut rng = rand::thread_rng(); - let amount: u64 = rng.gen_range(1..=10000); - - let payment_txn = f - .algorand_fixture - .algorand_client - .create() - .payment(PaymentParams { - sender: funded_addr.clone(), - receiver: funded_addr.clone(), - amount, - ..Default::default() - }) - .await?; - let client = f.client; - - let result = client - .send() - .call( - AppClientMethodCallParams { - method: "call_abi_txn".to_string(), - args: vec![ - AppMethodCallArg::Transaction(payment_txn), - AppMethodCallArg::ABIValue(ABIValue::from("test")), - ], - sender: Some(funded_addr.to_string()), - signer: Some(Arc::new(funded_account.clone())), - ..Default::default() - }, - None, - None, - ) - .await?; - - assert_eq!(result.group_results.len(), 2); - - let abi_return = result - .result - .abi_return - .as_ref() - .expect("Expected ABI return"); - let expected_return = format!("Sent {}. {}", amount, "test"); - match &abi_return.return_value { - Some(ABIValue::String(s)) => assert_eq!(s, &expected_return), - _ => return Err("Expected string ABI return".into()), - } - - let method = testing_app_spec() - .find_abi_method("call_abi_txn") - .expect("ABI method"); - let decoded = AppManager::get_abi_return(&abi_return.raw_return_value, &method) - .expect("Decoded ABI return"); - match decoded.return_value { - Some(ABIValue::String(s)) => assert_eq!(s, expected_return), - _ => return Err("Expected string ABI return from AppManager decoding".into()), - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_call_app_with_too_many_args( - #[future] testing_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let f = testing_app_fixture.await?; - let sender = f.sender_address; - let client = f.client; - - let err = client - .send() - .call( - AppClientMethodCallParams { - method: "call_abi".to_string(), - args: vec![ - AppMethodCallArg::ABIValue(ABIValue::from("one")), - AppMethodCallArg::ABIValue(ABIValue::from("two")), - ], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await - .expect_err("Expected validation error due to too many args"); - - assert!( - err.to_string() - .contains("The number of provided arguments is"), - "Unexpected error message: {}", - err - ); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_call_app_with_rekey( - #[future] testing_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let mut f = testing_app_fixture.await?; - let sender = f.sender_address; - - let rekey_to_account = f.algorand_fixture.generate_account(None).await?; - let rekey_to_addr = rekey_to_account.account().address(); - let client = f.client; - - client - .send() - .opt_in( - AppClientMethodCallParams { - method: "opt_in".to_string(), - args: vec![], - sender: Some(sender.to_string()), - rekey_to: Some(rekey_to_addr.to_string()), - ..Default::default() - }, - None, - ) - .await?; - - let _payment_result = client - .algorand() - .send() - .payment( - PaymentParams { - sender: sender.clone(), - signer: Some(Arc::new(rekey_to_account.clone())), - receiver: sender.clone(), - amount: 0, - ..Default::default() - }, - None, - ) - .await?; - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_sign_all_transactions_in_group_with_abi_call_with_transaction_arg( - #[future] testing_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let mut f = testing_app_fixture.await?; - let _sender = f.sender_address; - - let funded_account = f.algorand_fixture.generate_account(None).await?; - let funded_addr = funded_account.account().address(); - - let mut rng = rand::thread_rng(); - let amount = rng.gen_range(1..=10000); - - let payment_txn = f - .algorand_fixture - .algorand_client - .create() - .payment(PaymentParams { - sender: funded_addr.clone(), - receiver: funded_addr.clone(), - amount, - ..Default::default() - }) - .await?; - - let called_indexes = Arc::new(Mutex::new(Vec::new())); - - struct IndexCapturingSigner { - original_signer: Arc, - called_indexes: Arc>>, - } - - #[async_trait] - impl TransactionSigner for IndexCapturingSigner { - async fn sign_transactions( - &self, - transactions: &[Transaction], - indices: &[usize], - ) -> Result, String> { - { - let mut indexes = self.called_indexes.lock().unwrap(); - indexes.extend_from_slice(indices); - } - self.original_signer - .sign_transactions(transactions, indices) - .await - } - } - - let client = f.client; - - client - .send() - .call( - AppClientMethodCallParams { - method: "call_abi_txn".to_string(), - args: vec![ - AppMethodCallArg::Transaction(payment_txn), - AppMethodCallArg::ABIValue(ABIValue::from("test")), - ], - sender: Some(funded_addr.to_string()), - signer: Some(Arc::new(IndexCapturingSigner { - original_signer: Arc::new(funded_account.clone()), - called_indexes: called_indexes.clone(), - })), - ..Default::default() - }, - None, - None, - ) - .await?; - - let indexes = called_indexes.lock().unwrap().clone(); - - assert_eq!(indexes, vec![0, 1], "Expected indexes 0 and 1 to be signed"); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_sign_transaction_in_group_with_different_signer_if_provided( - #[future] testing_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let mut f = testing_app_fixture.await?; - let sender = f.sender_address; - - let new_account = f.algorand_fixture.generate_account(None).await?; - let new_addr = new_account.account().address(); - - let payment_txn = f - .algorand_fixture - .algorand_client - .create() - .payment(PaymentParams { - sender: new_addr.clone(), - receiver: new_addr.clone(), - amount: 2_000, - ..Default::default() - }) - .await?; - let client = f.client; - - client - .send() - .call( - AppClientMethodCallParams { - method: "call_abi_txn".to_string(), - args: vec![ - AppMethodCallArg::TransactionWithSigner(TransactionWithSigner { - transaction: payment_txn, - signer: Arc::new(new_account.clone()), - }), - AppMethodCallArg::ABIValue(ABIValue::from("test")), - ], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_sign_nested_transactions_in_group_with_different_signers( - #[future] nested_contract_fixture: crate::common::AppFixtureResult, -) -> TestResult { - eprintln!("=== Starting test_sign_transaction_in_group_with_different_signer_if_provided2 ==="); - let mut f = nested_contract_fixture.await?; - let bob_account = f.algorand_fixture.generate_account(None).await?; - let bob_addr = bob_account.account().address(); - - let alice_account = f.algorand_fixture.generate_account(None).await?; - let alice_addr = alice_account.account().address(); - - let payment_txn = f - .algorand_fixture - .algorand_client - .create() - .payment(PaymentParams { - sender: bob_addr.clone(), - signer: Some(Arc::new(bob_account.clone())), - receiver: bob_addr.clone(), - amount: 2_000, - ..Default::default() - }) - .await?; - let client = f.client; - - let result = client - .send() - .call( - AppClientMethodCallParams { - method: "nestedTxnArg".to_string(), - args: vec![ - AppMethodCallArg::TransactionPlaceholder, - AppMethodCallArg::AppCallMethodCall(AppCallMethodCallParams { - sender: bob_addr.clone(), - signer: Some(Arc::new(bob_account.clone())), - app_id: f.app_id, - method: ABIMethod::from_str("txnArg(pay)address").unwrap(), - args: vec![AppMethodCallArg::Transaction(payment_txn)], - ..Default::default() - }), - ], - sender: Some(alice_addr.to_string()), - signer: Some(Arc::new(alice_account.clone())), - ..Default::default() - }, - None, - None, - ) - .await?; - - assert_eq!( - result.result.abi_return.as_ref().unwrap().return_value, - Some(ABIValue::Uint(BigUint::from(client.app_id()))) - ); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn bare_call_with_box_reference_builds_and_sends( - #[future] sandbox_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let f = sandbox_app_fixture.await?; - let sender = f.sender_address; - let client = f.client; - - let result = client - .send() - .call( - AppClientMethodCallParams { - method: "hello_world".to_string(), - args: vec![AppMethodCallArg::ABIValue(ABIValue::from("test"))], - sender: Some(sender.to_string()), - box_references: Some(vec![BoxReference { - app_id: 0, - name: b"1".to_vec(), - }]), - ..Default::default() - }, - None, - None, - ) - .await?; - - match &result.result.transaction { - algokit_transact::Transaction::AppCall(fields) => { - assert_eq!(fields.app_id, f.app_id); - assert_eq!( - fields.box_references.as_ref().unwrap(), - &vec![BoxReference { - app_id: 0, - name: b"1".to_vec() - }] - ); - } - _ => return Err("expected app call".into()), - } - - Ok(()) -} diff --git a/crates/algokit_utils/tests/applications/app_client/state.rs b/crates/algokit_utils/tests/applications/app_client/state.rs deleted file mode 100644 index e11b23bf6..000000000 --- a/crates/algokit_utils/tests/applications/app_client/state.rs +++ /dev/null @@ -1,609 +0,0 @@ -use crate::common::TestResult; -use crate::common::app_fixture::{ - boxmap_app_fixture, testing_app_fixture, testing_app_puya_fixture, -}; -use algokit_abi::{ABIType, ABIValue}; -use algokit_transact::BoxReference; -// client params not needed with fixtures -use algokit_utils::AppMethodCallArg; -use algokit_utils::applications::app_client::{AppClientMethodCallParams, FundAppAccountParams}; -use algokit_utils::clients::app_manager::{AppState, BoxName}; -use base64::{Engine, engine::general_purpose::STANDARD as Base64}; -use num_bigint::BigUint; -use rstest::*; -use std::collections::HashMap; -use std::str::FromStr; - -#[rstest] -#[tokio::test] -async fn test_global_state_retrieval( - #[future] testing_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let f = testing_app_fixture.await?; - let sender = f.sender_address; - let client = f.client; - - client - .send() - .call( - AppClientMethodCallParams { - method: "set_global".to_string(), - args: vec![ - AppMethodCallArg::ABIValue(ABIValue::from(1u64)), - AppMethodCallArg::ABIValue(ABIValue::from(2u64)), - AppMethodCallArg::ABIValue(ABIValue::from("asdf")), - AppMethodCallArg::ABIValue(ABIValue::Array(vec![ - ABIValue::from_byte(1), - ABIValue::from_byte(2), - ABIValue::from_byte(3), - ABIValue::from_byte(4), - ])), - ], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - - let global_state = client.get_global_state().await?; - - assert!(global_state.contains_key("int1".as_bytes())); - assert!(global_state.contains_key("int2".as_bytes())); - assert!(global_state.contains_key("bytes1".as_bytes())); - assert!(global_state.contains_key("bytes2".as_bytes())); - - let mut keys: Vec = global_state - .keys() - .map(|k| String::from_utf8_lossy(k).to_string()) - .collect(); - keys.sort(); - assert_eq!(keys, vec!["bytes1", "bytes2", "int1", "int2", "value"]); - - match global_state.get("int1".as_bytes()).unwrap() { - AppState::Uint(state) => assert_eq!(state.value, 1), - _ => return Err("Expected uint state".into()), - } - - match global_state.get("int2".as_bytes()).unwrap() { - AppState::Uint(state) => assert_eq!(state.value, 2), - _ => return Err("Expected uint state".into()), - } - - match global_state.get("bytes1".as_bytes()).unwrap() { - AppState::Bytes(state) => { - assert_eq!(String::from_utf8(state.value_raw.clone()).unwrap(), "asdf"); - } - _ => return Err("Expected bytes state".into()), - } - - match global_state.get("bytes2".as_bytes()).unwrap() { - AppState::Bytes(state) => { - assert_eq!(state.value_raw, vec![1, 2, 3, 4]); - } - _ => return Err("Expected bytes state".into()), - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_local_state_retrieval( - #[future] testing_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let f = testing_app_fixture.await?; - let sender = f.sender_address; - let client = f.client; - - client - .send() - .opt_in( - AppClientMethodCallParams { - method: "opt_in".to_string(), - args: vec![], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - ) - .await?; - - client - .send() - .call( - AppClientMethodCallParams { - method: "set_local".to_string(), - args: vec![ - AppMethodCallArg::ABIValue(ABIValue::from(1u64)), - AppMethodCallArg::ABIValue(ABIValue::from(2u64)), - AppMethodCallArg::ABIValue(ABIValue::from("asdf")), - AppMethodCallArg::ABIValue(ABIValue::Array(vec![ - ABIValue::from_byte(1), - ABIValue::from_byte(2), - ABIValue::from_byte(3), - ABIValue::from_byte(4), - ])), - ], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - - let local_state = client.get_local_state(&sender.to_string()).await?; - - assert!(local_state.contains_key("local_int1".as_bytes())); - assert!(local_state.contains_key("local_int2".as_bytes())); - assert!(local_state.contains_key("local_bytes1".as_bytes())); - assert!(local_state.contains_key("local_bytes2".as_bytes())); - - let mut keys: Vec = local_state - .keys() - .map(|k| String::from_utf8_lossy(k).to_string()) - .collect(); - keys.sort(); - assert_eq!( - keys, - vec!["local_bytes1", "local_bytes2", "local_int1", "local_int2"] - ); - - match local_state.get("local_int1".as_bytes()).unwrap() { - AppState::Uint(state) => assert_eq!(state.value, 1), - _ => return Err("Expected uint state".into()), - } - - match local_state.get("local_int2".as_bytes()).unwrap() { - AppState::Uint(state) => assert_eq!(state.value, 2), - _ => return Err("Expected uint state".into()), - } - - match local_state.get("local_bytes1".as_bytes()).unwrap() { - AppState::Bytes(state) => { - assert_eq!(String::from_utf8(state.value_raw.clone()).unwrap(), "asdf"); - } - _ => return Err("Expected bytes state".into()), - } - - match local_state.get("local_bytes2".as_bytes()).unwrap() { - AppState::Bytes(state) => { - assert_eq!(state.value_raw, vec![1, 2, 3, 4]); - } - _ => return Err("Expected bytes state".into()), - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_box_retrieval( - #[future] testing_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let f = testing_app_fixture.await?; - let sender = f.sender_address; - let client = f.client; - - let box_name1: Vec = vec![0, 0, 0, 1]; - let box_name2: Vec = vec![0, 0, 0, 2]; - - client - .fund_app_account( - FundAppAccountParams { - amount: 1_000_000, - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - ) - .await?; - - client - .send() - .call( - AppClientMethodCallParams { - method: "set_box".to_string(), - args: vec![ - AppMethodCallArg::ABIValue(ABIValue::Array( - box_name1.iter().copied().map(ABIValue::from_byte).collect(), - )), - AppMethodCallArg::ABIValue(ABIValue::from("value1")), - ], - sender: Some(sender.to_string()), - box_references: Some(vec![BoxReference { - app_id: 0, - name: box_name1.clone(), - }]), - ..Default::default() - }, - None, - None, - ) - .await?; - - client - .send() - .call( - AppClientMethodCallParams { - method: "set_box".to_string(), - args: vec![ - AppMethodCallArg::ABIValue(ABIValue::Array( - box_name2.iter().copied().map(ABIValue::from_byte).collect(), - )), - AppMethodCallArg::ABIValue(ABIValue::from("value2")), - ], - sender: Some(sender.to_string()), - box_references: Some(vec![BoxReference { - app_id: 0, - name: box_name2.clone(), - }]), - ..Default::default() - }, - None, - None, - ) - .await?; - - let box_names = client.get_box_names().await?; - let names: Vec> = box_names.iter().map(|n| n.name_raw.clone()).collect(); - assert!(names.contains(&box_name1)); - assert!(names.contains(&box_name2)); - - let box_values = client.get_box_values().await?; - let box1_value = client.get_box_value(&box_name1).await?; - - let box_name1_base64 = Base64.encode(&box_name1); - let box_name2_base64 = Base64.encode(&box_name2); - - let mut box_names_base64: Vec<_> = box_values.iter().map(|b| &b.name.name_base64).collect(); - box_names_base64.sort(); - let mut expected_names = vec![&box_name1_base64, &box_name2_base64]; - expected_names.sort(); - assert_eq!(box_names_base64, expected_names); - - let box1 = box_values - .iter() - .find(|b| b.name.name_base64 == box_name1_base64) - .expect("box1 should exist"); - assert_eq!(box1.value, b"value1"); - assert_eq!(box1_value, box1.value); - - let box2 = box_values - .iter() - .find(|b| b.name.name_base64 == box_name2_base64) - .expect("box2 should exist"); - assert_eq!(box2.value, b"value2"); - - let expected_value_decoded = "1234524352"; - let expected_value = format!("\x00\n{}", expected_value_decoded); - - client - .send() - .call( - AppClientMethodCallParams { - method: "set_box".to_string(), - args: vec![ - AppMethodCallArg::ABIValue(ABIValue::Array( - box_name1.iter().copied().map(ABIValue::from_byte).collect(), - )), - AppMethodCallArg::ABIValue(ABIValue::from(expected_value.as_str())), - ], - sender: Some(sender.to_string()), - box_references: Some(vec![BoxReference { - app_id: 0, - name: box_name1.clone(), - }]), - ..Default::default() - }, - None, - None, - ) - .await?; - - let abi_string_type = "string".parse::().unwrap(); - let box_name1_base64_for_filter = box_name1_base64.clone(); - let boxes_abi = client - .get_box_values_from_abi_type( - &abi_string_type, - Some(Box::new(move |name: &BoxName| { - name.name_base64 == box_name1_base64_for_filter - })), - ) - .await?; - - let box1_abi_value = client - .get_box_value_from_abi_type(&box_name1, &abi_string_type) - .await?; - - assert_eq!(boxes_abi.len(), 1); - if let ABIValue::String(decoded_str) = &boxes_abi[0].value { - assert_eq!(decoded_str, expected_value_decoded); - } else { - return Err("Expected string ABIValue".into()); - } - - if let ABIValue::String(decoded_str) = &box1_abi_value { - assert_eq!(decoded_str, expected_value_decoded); - } else { - return Err("Expected string ABIValue".into()); - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_box_maps( - #[future] boxmap_app_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let f = boxmap_app_fixture.await?; - let sender = f.sender_address; - let app_client = f.client; - - app_client - .fund_app_account( - FundAppAccountParams { - amount: 1_000_000, - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - ) - .await?; - - let _result = app_client - .send() - .call( - AppClientMethodCallParams { - method: "setValue".to_string(), - args: vec![ - AppMethodCallArg::ABIValue(ABIValue::Uint(BigUint::from(1u64))), - AppMethodCallArg::ABIValue(ABIValue::String("foo".to_string())), - ], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await; - - let box_map = app_client.state().box_storage().get_map("bMap").await?; - assert_eq!(box_map.len(), 1); - - let key = ABIValue::Uint(BigUint::from(1u64)); - let expected_value = ABIValue::String("foo".to_string()); - assert_eq!(box_map.get(&key), Some(&expected_value)); - - let box_map_value = app_client - .state() - .box_storage() - .get_map_value("bMap", &ABIValue::Uint(BigUint::from(1u64))) - .await?; - assert_eq!(box_map_value, Some(expected_value)); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn box_methods_with_manually_encoded_abi_args( - #[future] testing_app_puya_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let f = testing_app_puya_fixture.await?; - let sender = f.sender_address; - let client = f.client; - - client - .fund_app_account( - FundAppAccountParams { - amount: 1_000_000, - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - ) - .await?; - - let box_prefix = b"box_bytes".to_vec(); - let name_type = ABIType::String; - let box_name = "asdf"; - let box_name_encoded = name_type.encode(&ABIValue::from(box_name)).unwrap(); - let box_identifier = { - let mut v = box_prefix.clone(); - v.extend_from_slice(&box_name_encoded); - v - }; - - let value_type = ABIType::DynamicArray(Box::new(ABIType::Byte)); - let encoded = value_type - .encode(&ABIValue::from(vec![ - ABIValue::from_byte(116), - ABIValue::from_byte(101), - ABIValue::from_byte(115), - ABIValue::from_byte(116), - ABIValue::from_byte(95), - ABIValue::from_byte(98), - ABIValue::from_byte(121), - ABIValue::from_byte(116), - ABIValue::from_byte(101), - ABIValue::from_byte(115), - ])) - .unwrap(); - - client - .send() - .call( - AppClientMethodCallParams { - method: "set_box_bytes".to_string(), - args: vec![ - AppMethodCallArg::ABIValue(ABIValue::from("asdf")), - AppMethodCallArg::ABIValue(ABIValue::Array( - encoded.into_iter().map(ABIValue::from_byte).collect(), - )), - ], - sender: Some(sender.to_string()), - box_references: Some(vec![BoxReference { - app_id: 0, - name: box_identifier.clone(), - }]), - ..Default::default() - }, - None, - None, - ) - .await?; - - let retrieved = client - .algorand() - .app() - .get_box_value_from_abi_type(client.app_id(), &box_identifier, &value_type) - .await?; - assert_eq!( - retrieved, - ABIValue::Array(vec![ - ABIValue::from_byte(116), - ABIValue::from_byte(101), - ABIValue::from_byte(115), - ABIValue::from_byte(116), - ABIValue::from_byte(95), - ABIValue::from_byte(98), - ABIValue::from_byte(121), - ABIValue::from_byte(116), - ABIValue::from_byte(101), - ABIValue::from_byte(115), - ]) - ); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn box_methods_with_arc4_returns_parametrized( - #[future] testing_app_puya_fixture: crate::common::AppFixtureResult, -) -> TestResult { - let f = testing_app_puya_fixture.await?; - let sender = f.sender_address; - let client = f.client; - - client - .fund_app_account( - FundAppAccountParams { - amount: 1_000_000, - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - ) - .await?; - - let mut big = num_bigint::BigUint::from(1u64); - big <<= 256u32; - let cases: Vec<(Vec, &str, &str, ABIValue)> = vec![ - ( - b"box_str".to_vec(), - "set_box_str", - "string", - ABIValue::from("string"), - ), - ( - b"box_int".to_vec(), - "set_box_int", - "uint32", - ABIValue::from(123u32), - ), - ( - b"box_int512".to_vec(), - "set_box_int512", - "uint512", - ABIValue::from(big), - ), - ( - b"box_static".to_vec(), - "set_box_static", - "byte[4]", - ABIValue::Array(vec![ - ABIValue::from_byte(1), - ABIValue::from_byte(2), - ABIValue::from_byte(3), - ABIValue::from_byte(4), - ]), - ), - ( - b"".to_vec(), - "set_struct", - "(string,uint64)", - ABIValue::Array(vec![ABIValue::from("box1"), ABIValue::from(123u64)]), - ), - ]; - - for (box_prefix, method_sig, value_type_str, arg_val) in cases { - let name_type = ABIType::String; - let name_encoded = name_type.encode(&ABIValue::from("box1")).unwrap(); - let mut box_reference = box_prefix.clone(); - box_reference.extend_from_slice(&name_encoded); - - let method_arg_val = if method_sig == "set_struct" { - ABIValue::Struct(HashMap::from([ - ("name".to_string(), ABIValue::from("box1")), - ("id".to_string(), ABIValue::from(123u64)), - ])) - } else { - arg_val.clone() - }; - - client - .send() - .call( - AppClientMethodCallParams { - method: method_sig.to_string(), - args: vec![ - AppMethodCallArg::ABIValue(ABIValue::from("box1")), - AppMethodCallArg::ABIValue(method_arg_val), - ], - sender: Some(sender.to_string()), - box_references: Some(vec![BoxReference { - app_id: 0, - name: box_reference.clone(), - }]), - ..Default::default() - }, - None, - None, - ) - .await?; - - let expected_raw = algokit_abi::ABIType::from_str(value_type_str) - .unwrap() - .encode(&arg_val) - .unwrap(); - let actual_raw = client.get_box_value(&box_reference).await?; - assert_eq!(actual_raw, expected_raw); - - let decoded = client - .get_box_value_from_abi_type( - &box_reference, - &ABIType::from_str(value_type_str).unwrap(), - ) - .await?; - assert_eq!(decoded, arg_val); - - let box_name_for_filter = box_reference.clone(); - let values = client - .get_box_values_from_abi_type( - &ABIType::from_str(value_type_str).unwrap(), - Some(Box::new(move |name: &BoxName| { - name.name_raw == box_name_for_filter - })), - ) - .await?; - assert_eq!(values.len(), 1); - assert_eq!(values[0].value, decoded); - } - - Ok(()) -} diff --git a/crates/algokit_utils/tests/applications/app_client/structs.rs b/crates/algokit_utils/tests/applications/app_client/structs.rs deleted file mode 100644 index 27c08a914..000000000 --- a/crates/algokit_utils/tests/applications/app_client/structs.rs +++ /dev/null @@ -1,211 +0,0 @@ -use crate::common::{AlgorandFixtureResult, TestResult, algorand_fixture, deploy_arc56_contract}; -use algokit_abi::{ABIValue, Arc56Contract}; -use algokit_utils::applications::app_client::AppClientMethodCallParams; -use algokit_utils::transactions::TransactionComposerConfig; -use algokit_utils::{AlgorandClient as RootAlgorandClient, AppMethodCallArg, ResourcePopulation}; -use rstest::*; -use std::collections::HashMap; -use std::sync::Arc; - -fn get_nested_struct_spec() -> Arc56Contract { - let json = algokit_test_artifacts::nested_struct_storage::APPLICATION_ARC56; - Arc56Contract::from_json(json).expect("valid arc56") -} - -fn get_nested_struct_create_application_args() -> Vec> { - vec![vec![184u8, 68u8, 123u8, 54u8]] -} - -#[rstest] -#[tokio::test] -async fn test_nested_structs_described_by_structure( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let fixture = algorand_fixture.await?; - let sender = fixture.test_account.account().address(); - - let spec = get_nested_struct_spec(); - let app_id = deploy_arc56_contract( - &fixture, - &sender, - &spec, - None, - None, - Some(get_nested_struct_create_application_args()), - ) - .await?; - - let mut algorand = RootAlgorandClient::default_localnet(None); - algorand.set_signer(sender.clone(), Arc::new(fixture.test_account.clone())); - let app_client = algokit_utils::applications::app_client::AppClient::new( - algokit_utils::applications::app_client::AppClientParams { - app_id, - app_spec: spec, - algorand: algorand.into(), - app_name: None, - default_sender: Some(sender.to_string()), - default_signer: None, - source_maps: None, - transaction_composer_config: Some(TransactionComposerConfig { - populate_app_call_resources: ResourcePopulation::Enabled { - use_access_list: false, - }, - ..Default::default() - }), - }, - ); - - app_client - .send() - .call( - AppClientMethodCallParams { - method: "setValue".to_string(), - args: vec![ - AppMethodCallArg::ABIValue(ABIValue::from(1u64)), - AppMethodCallArg::ABIValue(ABIValue::from("hello")), - ], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - - let result = app_client - .send() - .call( - AppClientMethodCallParams { - method: "getValue".to_string(), - args: vec![AppMethodCallArg::ABIValue(ABIValue::from(1u64))], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - - let abi_ret = result.result.abi_return.expect("abi return"); - let value = abi_ret.return_value.expect("decoded value"); - match value { - ABIValue::Struct(ref outer) => { - let x = match outer.get("x").expect("x") { - ABIValue::Struct(m) => m, - _ => return Err("x should be a struct".into()), - }; - match x.get("a").expect("a") { - ABIValue::String(s) => assert_eq!(s, "hello"), - _ => return Err("a should be string".into()), - } - } - _ => return Err("expected struct return".into()), - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_nested_structs_referenced_by_name( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let fixture = algorand_fixture.await?; - let sender = fixture.test_account.account().address(); - - let mut spec = get_nested_struct_spec(); - spec.structs = HashMap::from([ - ( - "Struct1".to_string(), - vec![algokit_abi::arc56_contract::StructField { - name: "a".to_string(), - field_type: algokit_abi::arc56_contract::StructFieldType::Value( - "string".to_string(), - ), - }], - ), - ( - "Struct2".to_string(), - vec![algokit_abi::arc56_contract::StructField { - name: "x".to_string(), - field_type: algokit_abi::arc56_contract::StructFieldType::Value( - "Struct1".to_string(), - ), - }], - ), - ]); - - let app_id = deploy_arc56_contract( - &fixture, - &sender, - &spec, - None, - None, - Some(get_nested_struct_create_application_args()), - ) - .await?; - - let mut algorand = RootAlgorandClient::default_localnet(None); - algorand.set_signer(sender.clone(), Arc::new(fixture.test_account.clone())); - let app_client = algokit_utils::applications::app_client::AppClient::new( - algokit_utils::applications::app_client::AppClientParams { - app_id, - app_spec: spec, - algorand: algorand.into(), - app_name: None, - default_sender: Some(sender.to_string()), - default_signer: None, - source_maps: None, - transaction_composer_config: None, - }, - ); - - app_client - .send() - .call( - AppClientMethodCallParams { - method: "setValue".to_string(), - args: vec![ - AppMethodCallArg::ABIValue(ABIValue::from(1u64)), - AppMethodCallArg::ABIValue(ABIValue::from("hello")), - ], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - - let result = app_client - .send() - .call( - AppClientMethodCallParams { - method: "getValue".to_string(), - args: vec![AppMethodCallArg::ABIValue(ABIValue::from(1u64))], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - - let abi_ret = result.result.abi_return.expect("abi return"); - let value = abi_ret.return_value.expect("decoded value"); - match value { - ABIValue::Struct(ref outer) => { - let x = match outer.get("x").expect("x") { - ABIValue::Struct(m) => m, - _ => return Err("x should be a struct".into()), - }; - match x.get("a").expect("a") { - ABIValue::String(s) => assert_eq!(s, "hello"), - _ => return Err("a should be string".into()), - } - } - _ => return Err("expected struct return".into()), - } - - Ok(()) -} diff --git a/crates/algokit_utils/tests/applications/app_deployer.rs b/crates/algokit_utils/tests/applications/app_deployer.rs deleted file mode 100644 index 7acee4d61..000000000 --- a/crates/algokit_utils/tests/applications/app_deployer.rs +++ /dev/null @@ -1,1394 +0,0 @@ -use algokit_abi::ABIMethod; -use algokit_test_artifacts::{abi_create_and_delete, testing_app}; -use algokit_transact::{Address, OnApplicationComplete}; -use algokit_utils::applications::{ - AppDeployMetadata, AppDeployParams, AppDeployResult, AppDeployer, AppProgram, CreateParams, - DeleteParams, DeployAppCreateMethodCallParams, DeployAppCreateParams, - DeployAppDeleteMethodCallParams, DeployAppDeleteParams, DeployAppUpdateParams, OnSchemaBreak, - OnUpdate, UpdateParams, -}; -use algokit_utils::clients::app_manager::{AppManager, DeploymentMetadata, TealTemplateValue}; -use algokit_utils::{AppCreateParams, AppMethodCallArg, PaymentParams, TransactionSender}; -use algokit_utils::{AssetManager, SendParams}; -use base64::{Engine, prelude::BASE64_STANDARD}; -use rstest::*; -use serde_json; -use std::collections::HashMap; -use std::str::FromStr; - -use crate::common::{AlgorandFixture, AlgorandFixtureResult, TestResult, algorand_fixture}; - -#[fixture] -async fn fixture(#[future] algorand_fixture: AlgorandFixtureResult) -> FixtureResult { - let algorand_fixture = algorand_fixture.await?; - let test_account = algorand_fixture.test_account.account().address(); - let algod_client = algorand_fixture.algod.clone(); - let indexer_client = algorand_fixture.indexer.clone(); - - let composer = algorand_fixture.algorand_client.new_composer(None); - let asset_manager = AssetManager::new(algod_client.clone(), { - let new_composer = composer.clone(); - move |_params| new_composer.clone() - }); - let app_manager = AppManager::new(algod_client.clone()); - - let transaction_sender = TransactionSender::new( - { - let new_composer = composer.clone(); - move |_params| new_composer.clone() - }, - asset_manager, - ); - - let app_deployer = AppDeployer::new( - app_manager.clone(), - transaction_sender.clone(), - Some(indexer_client.clone()), - ); - - Ok(Fixture { - test_account, - algorand_fixture, - app_manager, - transaction_sender, - app_deployer, - }) -} - -#[rstest] -#[tokio::test] -async fn test_created_app_is_retrieved_by_name_with_deployment_metadata( - #[future] fixture: FixtureResult, -) -> TestResult { - let Fixture { - test_account, - app_manager, - transaction_sender, - algorand_fixture, - mut app_deployer, - .. - } = fixture.await?; - - let creation_metadata = get_metadata(AppDeployMetadataParams { - name: Some(String::from("MY_APP")), - version: Some(String::from("1.0")), - updatable: Some(true), - ..Default::default() - }); - - let create_params = - get_testing_app_create_params(&app_manager, &test_account, &creation_metadata).await?; - - let result = transaction_sender.app_create(create_params, None).await?; - - algorand_fixture - .wait_for_indexer_transaction(&result.transaction_id) - .await?; - - let apps = app_deployer - .get_creator_apps_by_name(&test_account, None) - .await?; - - assert_eq!(apps.creator, test_account); - assert_eq!(apps.apps.len(), 1); - assert!(apps.apps.contains_key("MY_APP")); - let app = &apps.apps["MY_APP"]; - assert_eq!(app.app_id, result.app_id); - assert_eq!(app.app_address, result.app_address); - assert_eq!( - app.created_round, - result.confirmation.confirmed_round.unwrap() - ); - assert_eq!(app.created_metadata, creation_metadata); - assert_eq!(app.updated_round, app.created_round); - assert_eq!(app.name, creation_metadata.name); - assert_eq!(app.version, creation_metadata.version); - assert_eq!(app.updatable, creation_metadata.updatable); - assert_eq!(app.deletable, creation_metadata.deletable); - assert!(!app.deleted); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_latest_created_app_is_retrieved(#[future] fixture: FixtureResult) -> TestResult { - let Fixture { - test_account, - app_manager, - transaction_sender, - algorand_fixture, - mut app_deployer, - .. - } = fixture.await?; - - let creation_metadata = get_metadata(AppDeployMetadataParams { - name: Some(String::from("MY_APP")), - version: Some(String::from("1.0")), - updatable: Some(true), - deletable: Some(false), - }); - - let mut create_params_1 = - get_testing_app_create_params(&app_manager, &test_account, &creation_metadata).await?; - create_params_1.lease = Some([1u8; 32]); - transaction_sender.app_create(create_params_1, None).await?; - - let mut create_params_2 = - get_testing_app_create_params(&app_manager, &test_account, &creation_metadata).await?; - create_params_2.lease = Some([2u8; 32]); - transaction_sender.app_create(create_params_2, None).await?; - - let mut create_params_3 = - get_testing_app_create_params(&app_manager, &test_account, &creation_metadata).await?; - create_params_3.lease = Some([3u8; 32]); - let result_3 = transaction_sender.app_create(create_params_3, None).await?; - - algorand_fixture - .wait_for_indexer_transaction(&result_3.transaction_id) - .await?; - - let apps = app_deployer - .get_creator_apps_by_name(&test_account, None) - .await?; - - assert_eq!(apps.apps["MY_APP"].app_id, result_3.app_id); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_created_updated_and_deleted_apps_are_retrieved_by_name_with_deployment_metadata( - #[future] fixture: FixtureResult, -) -> TestResult { - let Fixture { - test_account, - app_manager, - transaction_sender, - algorand_fixture, - mut app_deployer, - .. - } = fixture.await?; - - let creation_metadata = get_metadata(AppDeployMetadataParams { - name: Some(String::from("MY_APP")), - version: Some(String::from("1.0")), - updatable: Some(true), - deletable: Some(true), - }); - let create_params_1 = - get_testing_app_create_params(&app_manager, &test_account, &creation_metadata).await?; - let result_1 = transaction_sender.app_create(create_params_1, None).await?; - - let creation_metadata_2 = AppDeployMetadata { - name: String::from("APP_2"), - ..creation_metadata.clone() - }; - let create_params_2 = - get_testing_app_create_params(&app_manager, &test_account, &creation_metadata_2).await?; - let result_2 = transaction_sender.app_create(create_params_2, None).await?; - - let creation_metadata_3 = AppDeployMetadata { - name: String::from("APP_3"), - ..creation_metadata.clone() - }; - let create_params_3 = - get_testing_app_create_params(&app_manager, &test_account, &creation_metadata_3).await?; - let result_3 = transaction_sender.app_create(create_params_3, None).await?; - - // Update app 1 - let update_metadata = AppDeployMetadata { - version: String::from("2.0"), - ..creation_metadata.clone() - }; - let update_create_params = - get_testing_app_create_params(&app_manager, &test_account, &update_metadata).await?; - let update_params = algokit_utils::AppUpdateParams { - sender: update_create_params.sender.clone(), - signer: update_create_params.signer.clone(), - rekey_to: update_create_params.rekey_to.clone(), - note: update_create_params.note.clone(), - lease: update_create_params.lease, - static_fee: update_create_params.static_fee, - extra_fee: update_create_params.extra_fee, - max_fee: update_create_params.max_fee, - validity_window: update_create_params.validity_window, - first_valid_round: update_create_params.first_valid_round, - last_valid_round: update_create_params.last_valid_round, - app_id: result_1.app_id, - approval_program: update_create_params.approval_program, - clear_state_program: update_create_params.clear_state_program, - args: update_create_params.args, - account_references: update_create_params.account_references, - app_references: update_create_params.app_references, - asset_references: update_create_params.asset_references, - box_references: update_create_params.box_references, - }; - let update_result = transaction_sender.app_update(update_params, None).await?; - - // Delete app 3 - let delete_params = algokit_utils::AppDeleteParams { - sender: test_account.clone(), - app_id: result_3.app_id, - args: None, - account_references: None, - app_references: None, - asset_references: None, - box_references: None, - ..Default::default() - }; - let delete_result = transaction_sender.app_delete(delete_params, None).await?; - - algorand_fixture - .wait_for_indexer_transaction(&delete_result.transaction_id) - .await?; - - let apps = app_deployer - .get_creator_apps_by_name(&test_account, None) - .await?; - - assert_eq!(apps.creator, test_account); - let mut app_names: Vec = apps.apps.keys().cloned().collect(); - app_names.sort(); - let mut expected_names = vec![ - "MY_APP".to_string(), - "APP_2".to_string(), - "APP_3".to_string(), - ]; - expected_names.sort(); - assert_eq!(app_names, expected_names); - - // Check app 1 was updated - let app_1_data = &apps.apps["MY_APP"]; - assert_eq!(app_1_data.app_id, result_1.app_id); - assert_eq!(app_1_data.app_address, result_1.app_address); - assert_eq!( - app_1_data.created_round, - result_1.confirmation.confirmed_round.unwrap() - ); - assert_eq!(app_1_data.created_metadata, creation_metadata); - assert_ne!(app_1_data.created_round, app_1_data.updated_round); - assert_eq!( - app_1_data.updated_round, - update_result.confirmation.confirmed_round.unwrap() - ); - assert_eq!(app_1_data.name, update_metadata.name); - assert_eq!(app_1_data.updatable, update_metadata.updatable); - assert_eq!(app_1_data.deletable, update_metadata.deletable); - assert_eq!(app_1_data.version, update_metadata.version); - assert!(!app_1_data.deleted); - - // Check app 2 is unchanged - let app2_data = &apps.apps["APP_2"]; - assert_eq!(app2_data.app_id, result_2.app_id); - assert_eq!(app2_data.created_round, app2_data.updated_round); - assert!(!app2_data.deleted); - - // Check app 3 is deleted - let app3_data = &apps.apps["APP_3"]; - assert_eq!(app3_data.app_id, result_3.app_id); - assert!(app3_data.deleted); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_deploy_new_app(#[future] fixture: FixtureResult) -> TestResult { - let Fixture { - test_account, - mut app_deployer, - .. - } = fixture.await?; - - let metadata = get_metadata(AppDeployMetadataParams { - ..Default::default() - }); - let deployment = - get_testing_app_deploy_params(&test_account, &metadata, None, None, None, None).await?; - - let result = app_deployer.deploy(deployment).await?; - let (app, create_result) = match &result { - AppDeployResult::Create { - app, create_result, .. - } => (app, create_result), - _ => return Err("Expected Create result".into()), - }; - - assert_eq!(app.app_id, create_result.confirmation.app_id.unwrap()); - assert_eq!(app.app_address, Address::from_app_id(&app.app_id)); - assert_eq!(app.created_metadata, metadata); - assert_eq!( - app.created_round, - create_result.confirmation.confirmed_round.unwrap() - ); - assert_eq!(app.updated_round, app.created_round); - assert_eq!(app.name, metadata.name); - assert_eq!(app.version, metadata.version); - assert_eq!(app.updatable, metadata.updatable); - assert_eq!(app.deletable, metadata.deletable); - assert!(!app.deleted); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_fail_to_deploy_immutable_app_without_tmpl_updatable( - #[future] fixture: FixtureResult, -) -> TestResult { - let Fixture { - test_account, - mut app_deployer, - .. - } = fixture.await?; - - let metadata = get_metadata(AppDeployMetadataParams { - updatable: Some(true), - ..Default::default() - }); - let mut deployment = - get_testing_app_deploy_params(&test_account, &metadata, None, None, None, None).await?; - - // Remove TMPL_UPDATABLE from the approval program to simulate the validation failure - if let CreateParams::AppCreateCall(ref mut create_params) = deployment.create_params { - if let AppProgram::Teal(ref mut approval_program) = create_params.approval_program { - *approval_program = approval_program.replace("TMPL_UPDATABLE", "0"); - } - } - - let result = app_deployer.deploy(deployment).await; - - assert!(result.is_err()); - let error_message = result.unwrap_err().to_string(); - assert!(error_message.contains( - "Deploy-time updatability control requested, but TMPL_UPDATABLE not present in TEAL code" - )); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_fail_to_deploy_permanent_app_without_tmpl_deletable( - #[future] fixture: FixtureResult, -) -> TestResult { - let Fixture { - test_account, - mut app_deployer, - .. - } = fixture.await?; - - let metadata = get_metadata(AppDeployMetadataParams { - deletable: Some(true), - ..Default::default() - }); - let mut deployment = - get_testing_app_deploy_params(&test_account, &metadata, None, None, None, None).await?; - - // Remove TMPL_DELETABLE from the approval program to simulate the validation failure - if let CreateParams::AppCreateCall(ref mut create_params) = deployment.create_params { - if let AppProgram::Teal(ref mut approval_program) = create_params.approval_program { - *approval_program = approval_program.replace("TMPL_DELETABLE", "0"); - } - } - - let result = app_deployer.deploy(deployment).await; - - assert!(result.is_err()); - let error_message = result.unwrap_err().to_string(); - assert!(error_message.contains( - "Deploy-time deletability control requested, but TMPL_DELETABLE not present in TEAL code" - )); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_deploy_update_to_updatable_app(#[future] fixture: FixtureResult) -> TestResult { - let Fixture { - test_account, - mut app_deployer, - algorand_fixture, - .. - } = fixture.await?; - - // Deploy initial app with updatable=true - let metadata = get_metadata(AppDeployMetadataParams { - updatable: Some(true), - ..Default::default() - }); - let deployment_1 = - get_testing_app_deploy_params(&test_account, &metadata, None, None, None, None).await?; - let result_1 = app_deployer.deploy(deployment_1).await?; - let (app_1, app_1_id, tx_id) = match &result_1 { - AppDeployResult::Create { - app, create_result, .. - } => (app, app.app_id, create_result.transaction_id.clone()), - _ => return Err("Expected Create result".into()), - }; - - algorand_fixture - .wait_for_indexer_transaction(&tx_id) - .await?; - - // Deploy update with same metadata but different version - let metadata_2 = AppDeployMetadata { - version: String::from("2.0"), - ..metadata.clone() - }; - let deployment_2 = get_testing_app_deploy_params( - &test_account, - &metadata_2, - Some(2), - None, - Some(OnUpdate::Update), - None, - ) - .await?; - - let result_2 = app_deployer.deploy(deployment_2).await?; - let (app_2, update_result) = match result_2 { - AppDeployResult::Update { - app, update_result, .. - } => (app, update_result), - _ => return Err("Expected Update result".into()), - }; - - assert_eq!(app_2.app_id, app_1_id); - assert_eq!(app_2.created_metadata, metadata); - assert_eq!(app_2.created_round, app_1.created_round); - assert_ne!(app_2.updated_round, app_2.created_round); - assert_eq!( - app_2.updated_round, - update_result.confirmation.confirmed_round.unwrap() - ); - assert_eq!(app_2.name, metadata_2.name); - assert_eq!(app_2.version, metadata_2.version); - assert_eq!(app_2.updatable, metadata_2.updatable); - assert_eq!(app_2.deletable, metadata_2.deletable); - assert!(!app_2.deleted); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_deploy_update_to_immutable_app_fails(#[future] fixture: FixtureResult) -> TestResult { - let Fixture { - test_account, - mut app_deployer, - algorand_fixture, - .. - } = fixture.await?; - - // Deploy initial app (immutable) - let metadata = get_metadata(AppDeployMetadataParams { - updatable: Some(false), - ..Default::default() - }); - let deployment_1 = - get_testing_app_deploy_params(&test_account, &metadata, None, None, None, None).await?; - - let result_1 = app_deployer.deploy(deployment_1).await?; - - algorand_fixture - .wait_for_indexer_transaction(&match result_1 { - AppDeployResult::Create { create_result, .. } => create_result.transaction_id.clone(), - _ => return Err("Expected Create result".into()), - }) - .await?; - - // Attempt to update (should fail because app is immutable) - let metadata_2 = get_metadata(AppDeployMetadataParams { - version: Some(String::from("2.0")), - updatable: Some(false), - ..Default::default() - }); - let deployment_2 = get_testing_app_deploy_params( - &test_account, - &metadata_2, - Some(2), - None, - Some(OnUpdate::Update), - None, - ) - .await?; - - let result_2 = app_deployer.deploy(deployment_2).await; - - assert!(result_2.is_err()); - let error_message = result_2.unwrap_err().to_string(); - assert!(error_message.contains("logic eval error: assert failed")); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_deploy_failure_for_updated_app_when_on_update_fail( - #[future] fixture: FixtureResult, -) -> TestResult { - let Fixture { - test_account, - mut app_deployer, - algorand_fixture, - .. - } = fixture.await?; - - // Deploy initial app - let metadata = get_metadata(AppDeployMetadataParams { - ..Default::default() - }); - let deployment_1 = - get_testing_app_deploy_params(&test_account, &metadata, None, None, None, None).await?; - - let result_1 = app_deployer.deploy(deployment_1).await?; - - algorand_fixture - .wait_for_indexer_transaction(&match result_1 { - AppDeployResult::Create { create_result, .. } => create_result.transaction_id.clone(), - _ => return Err("Expected Create result".into()), - }) - .await?; - - // Attempt to deploy with changes but OnUpdate::Fail - let deployment_2 = get_testing_app_deploy_params( - &test_account, - &metadata, - Some(2), - None, - Some(OnUpdate::Fail), - None, - ) - .await?; - - let result_2 = app_deployer.deploy(deployment_2).await; - - assert!(result_2.is_err()); - let error_message = result_2.unwrap_err().to_string(); - assert!(error_message.contains("Executing the fail on update strategy, stopping deployment")); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_deploy_replacement_to_deletable_updated_app( - #[future] fixture: FixtureResult, -) -> TestResult { - let Fixture { - test_account, - mut app_deployer, - algorand_fixture, - .. - } = fixture.await?; - - // Deploy initial app (deletable) - let metadata = get_metadata(AppDeployMetadataParams { - deletable: Some(true), - ..Default::default() - }); - let deployment_1 = - get_testing_app_deploy_params(&test_account, &metadata, None, None, None, None).await?; - - let result_1 = app_deployer.deploy(deployment_1).await?; - let app_1_id = match &result_1 { - AppDeployResult::Create { app, .. } => app.app_id, - _ => return Err("Expected Create result".into()), - }; - - algorand_fixture - .wait_for_indexer_transaction(&match result_1 { - AppDeployResult::Create { create_result, .. } => create_result.transaction_id.clone(), - _ => return Err("Expected Create result".into()), - }) - .await?; - - // Deploy replacement with different code - let metadata_2 = get_metadata(AppDeployMetadataParams { - version: Some(String::from("2.0")), - deletable: Some(true), - ..Default::default() - }); - let deployment_2 = get_testing_app_deploy_params( - &test_account, - &metadata_2, - Some(2), - None, - Some(OnUpdate::Replace), - None, - ) - .await?; - - let result_2 = app_deployer.deploy(deployment_2).await?; - let (app_2, create_result) = match result_2 { - AppDeployResult::Replace { - app, create_result, .. - } => (app, create_result), - _ => return Err("Expected Replace result".into()), - }; - - assert_ne!(app_2.app_id, app_1_id); - assert_eq!(app_2.created_metadata, metadata_2); - assert_eq!(app_2.created_round, app_2.updated_round); - assert_eq!( - app_2.created_round, - create_result.confirmation.confirmed_round.unwrap() - ); - assert_eq!(app_2.name, metadata_2.name); - assert_eq!(app_2.version, metadata_2.version); - assert_eq!(app_2.updatable, metadata_2.updatable); - assert_eq!(app_2.deletable, metadata_2.deletable); - assert!(!app_2.deleted); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_deploy_failure_for_replacement_of_permanent_updated_app( - #[future] fixture: FixtureResult, -) -> TestResult { - let Fixture { - test_account, - mut app_deployer, - algorand_fixture, - .. - } = fixture.await?; - - // Deploy initial app (permanent) - let metadata = get_metadata(AppDeployMetadataParams { - deletable: Some(false), - ..Default::default() - }); - let deployment_1 = - get_testing_app_deploy_params(&test_account, &metadata, None, None, None, None).await?; - - let result_1 = app_deployer.deploy(deployment_1).await?; - - algorand_fixture - .wait_for_indexer_transaction(&match result_1 { - AppDeployResult::Create { create_result, .. } => create_result.transaction_id.clone(), - _ => return Err("Expected Create result".into()), - }) - .await?; - - // Attempt to replace permanent app (should fail) - let metadata_2 = get_metadata(AppDeployMetadataParams { - version: Some(String::from("2.0")), - deletable: Some(false), - ..Default::default() - }); - let deployment_2 = get_testing_app_deploy_params( - &test_account, - &metadata_2, - Some(2), - None, - Some(OnUpdate::Replace), - None, - ) - .await?; - - let result_2 = app_deployer.deploy(deployment_2).await; - - assert!(result_2.is_err()); - let error_message = result_2.unwrap_err().to_string(); - assert!(error_message.contains("logic eval error: assert failed")); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_deploy_replacement_of_deletable_schema_broken_app( - #[future] fixture: FixtureResult, -) -> TestResult { - let Fixture { - test_account, - mut app_deployer, - algorand_fixture, - .. - } = fixture.await?; - - // Deploy initial app (deletable) - let metadata = get_metadata(AppDeployMetadataParams { - deletable: Some(true), - ..Default::default() - }); - let deployment_1 = - get_testing_app_deploy_params(&test_account, &metadata, None, None, None, None).await?; - - let result_1 = app_deployer.deploy(deployment_1).await?; - let (app_1, tx_id) = match result_1 { - AppDeployResult::Create { - app, create_result, .. - } => (app, create_result.transaction_id.clone()), - _ => return Err("Expected Create result".into()), - }; - - algorand_fixture - .wait_for_indexer_transaction(&tx_id) - .await?; - - // Deploy replacement with schema break - let metadata_2 = get_metadata(AppDeployMetadataParams { - version: Some(String::from("2.0")), - deletable: Some(true), - ..Default::default() - }); - let deployment_2 = get_testing_app_deploy_params( - &test_account, - &metadata_2, - None, - Some(OnSchemaBreak::Replace), - None, - Some(true), // break_schema - ) - .await?; - - let result_2 = app_deployer.deploy(deployment_2).await?; - let (app_2, create_result) = match result_2 { - AppDeployResult::Replace { - app, create_result, .. - } => (app, create_result), - _ => return Err("Expected Replace result".into()), - }; - - // Verify the app was replaced - assert_ne!(app_2.app_id, app_1.app_id); - assert_eq!(app_2.created_metadata, metadata_2); - assert_eq!(app_2.created_round, app_2.updated_round); - assert_eq!( - app_2.created_round, - create_result.confirmation.confirmed_round.unwrap() - ); - assert_eq!(app_2.name, metadata_2.name); - assert_eq!(app_2.version, metadata_2.version); - assert!(!app_2.deleted); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_deploy_replacement_to_schema_broken_permanent_app_fails( - #[future] fixture: FixtureResult, -) -> TestResult { - let Fixture { - test_account, - mut app_deployer, - algorand_fixture, - .. - } = fixture.await?; - - // Deploy initial app (permanent) - let metadata = get_metadata(AppDeployMetadataParams { - deletable: Some(false), - ..Default::default() - }); - let deployment_1 = - get_testing_app_deploy_params(&test_account, &metadata, None, None, None, None).await?; - - let result_1 = app_deployer.deploy(deployment_1).await?; - - algorand_fixture - .wait_for_indexer_transaction(&match result_1 { - AppDeployResult::Create { create_result, .. } => create_result.transaction_id.clone(), - _ => return Err("Expected Create result".into()), - }) - .await?; - - // Attempt to replace permanent app with schema break (should fail) - let metadata_2 = get_metadata(AppDeployMetadataParams { - version: Some(String::from("2.0")), - deletable: Some(false), - ..Default::default() - }); - let deployment_2 = get_testing_app_deploy_params( - &test_account, - &metadata_2, - None, - Some(OnSchemaBreak::Replace), - None, - Some(true), // break_schema - ) - .await?; - - let result_2 = app_deployer.deploy(deployment_2).await; - - assert!(result_2.is_err()); - let error_message = result_2.unwrap_err().to_string(); - assert!(error_message.contains("logic eval error: assert failed")); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_deploy_failure_for_replacement_of_schema_broken_app_when_on_schema_break_fail( - #[future] fixture: FixtureResult, -) -> TestResult { - let Fixture { - test_account, - mut app_deployer, - algorand_fixture, - .. - } = fixture.await?; - - // Deploy initial app - let metadata = get_metadata(AppDeployMetadataParams { - ..Default::default() - }); - let deployment_1 = - get_testing_app_deploy_params(&test_account, &metadata, None, None, None, None).await?; - - let result_1 = app_deployer.deploy(deployment_1).await?; - - algorand_fixture - .wait_for_indexer_transaction(&match result_1 { - AppDeployResult::Create { create_result, .. } => create_result.transaction_id.clone(), - _ => return Err("Expected Create result".into()), - }) - .await?; - - // Attempt to deploy with schema break but OnSchemaBreak::Fail - let deployment_2 = get_testing_app_deploy_params( - &test_account, - &metadata, - None, - Some(OnSchemaBreak::Fail), - None, - Some(true), // break_schema - ) - .await?; - - let result_2 = app_deployer.deploy(deployment_2).await; - - assert!(result_2.is_err()); - let error_message = result_2.unwrap_err().to_string(); - assert!( - error_message.contains("Executing the fail on schema break strategy, stopping deployment") - ); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_do_nothing_if_deploying_app_with_no_changes( - #[future] fixture: FixtureResult, -) -> TestResult { - let Fixture { - test_account, - mut app_deployer, - algorand_fixture, - .. - } = fixture.await?; - - // Deploy initial app - let metadata = get_metadata(AppDeployMetadataParams { - ..Default::default() - }); - let deployment = - get_testing_app_deploy_params(&test_account, &metadata, None, None, None, None).await?; - - let result_1 = app_deployer.deploy(deployment.clone()).await?; - let (app_1, tx_id) = match result_1 { - AppDeployResult::Create { - app, create_result, .. - } => (app, create_result.transaction_id.clone()), - _ => return Err("Expected Create result".into()), - }; - - algorand_fixture - .wait_for_indexer_transaction(&tx_id) - .await?; - - // Deploy again with no changes - let result_2 = app_deployer.deploy(deployment).await?; - let app_2 = match result_2 { - AppDeployResult::Nothing { app } => app, - _ => return Err("Expected Nothing result".into()), - }; - - assert_eq!(app_2.app_id, app_1.app_id); - assert_eq!(app_2.app_address, app_1.app_address); - assert_eq!(app_2.created_metadata, metadata); - assert_eq!(app_2.created_round, app_1.created_round); - assert_eq!(app_2.updated_round, app_1.updated_round); - assert_eq!(app_2.name, metadata.name); - assert_eq!(app_2.version, metadata.version); - assert_eq!(app_2.updatable, metadata.updatable); - assert_eq!(app_2.deletable, metadata.deletable); - assert!(!app_2.deleted); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_deploy_append_for_schema_broken_app_when_on_schema_break_append_app( - #[future] fixture: FixtureResult, -) -> TestResult { - let Fixture { - test_account, - mut app_deployer, - algorand_fixture, - .. - } = fixture.await?; - - // Deploy initial app - let metadata = get_metadata(AppDeployMetadataParams { - ..Default::default() - }); - let deployment_1 = - get_testing_app_deploy_params(&test_account, &metadata, None, None, None, None).await?; - - let result_1 = app_deployer.deploy(deployment_1).await?; - let (app_1, tx_id) = match result_1 { - AppDeployResult::Create { - app, create_result, .. - } => (app, create_result.transaction_id.clone()), - _ => return Err("Expected Create result".into()), - }; - - algorand_fixture - .wait_for_indexer_transaction(&tx_id) - .await?; - - // Deploy with schema break and OnSchemaBreak::Append - let deployment_2 = get_testing_app_deploy_params( - &test_account, - &metadata, - None, - Some(OnSchemaBreak::Append), - None, - Some(true), // break_schema - ) - .await?; - - let result_2 = app_deployer.deploy(deployment_2).await?; - let (app_2, create_result) = match result_2 { - AppDeployResult::Create { - app, create_result, .. - } => (app, create_result), - _ => return Err("Expected Create result".into()), - }; - - assert_ne!(app_2.app_id, app_1.app_id); - assert_ne!(app_2.created_round, app_1.created_round); - assert_eq!( - app_2.created_round, - create_result.confirmation.confirmed_round.unwrap() - ); - assert_eq!(app_2.created_round, app_2.updated_round); - assert_eq!(app_2.name, metadata.name); - assert_eq!(app_2.version, metadata.version); - assert!(!app_2.deleted); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_deploy_append_for_update_app_when_on_update_append_app( - #[future] fixture: FixtureResult, -) -> TestResult { - let Fixture { - test_account, - mut app_deployer, - algorand_fixture, - .. - } = fixture.await?; - - // Deploy initial app - let metadata = get_metadata(AppDeployMetadataParams { - ..Default::default() - }); - let deployment_1 = - get_testing_app_deploy_params(&test_account, &metadata, None, None, None, None).await?; - - let result_1 = app_deployer.deploy(deployment_1).await?; - let (app_1, tx_id) = match result_1 { - AppDeployResult::Create { - app, create_result, .. - } => (app, create_result.transaction_id.clone()), - _ => return Err("Expected Create result".into()), - }; - - algorand_fixture - .wait_for_indexer_transaction(&tx_id) - .await?; - - // Deploy with code changes and OnUpdate::Append - let metadata_2 = get_metadata(AppDeployMetadataParams { - version: Some(String::from("2.0")), - ..Default::default() - }); - let deployment_2 = get_testing_app_deploy_params( - &test_account, - &metadata_2, - Some(3), // Different code injection value - None, - Some(OnUpdate::Append), - None, - ) - .await?; - - let result_2 = app_deployer.deploy(deployment_2).await?; - let (app_2, create_result) = match result_2 { - AppDeployResult::Create { - app, create_result, .. - } => (app, create_result), - _ => return Err("Expected Create result".into()), - }; - - assert_ne!(app_2.app_id, app_1.app_id); - assert_ne!(app_2.created_round, app_1.created_round); - assert_eq!( - app_2.created_round, - create_result.confirmation.confirmed_round.unwrap() - ); - assert_eq!(app_2.created_metadata, metadata_2); - assert_eq!(app_2.name, metadata_2.name); - assert_eq!(app_2.version, metadata_2.version); - assert!(!app_2.deleted); - - Ok(()) -} - -struct Fixture { - test_account: Address, - algorand_fixture: AlgorandFixture, - app_manager: AppManager, - transaction_sender: TransactionSender, - app_deployer: AppDeployer, -} - -type FixtureResult = Result>; - -#[derive(Debug, Default, Clone)] -struct AppDeployMetadataParams { - name: Option, - version: Option, - updatable: Option, - deletable: Option, -} - -fn get_metadata(params: AppDeployMetadataParams) -> AppDeployMetadata { - AppDeployMetadata { - name: params.name.unwrap_or(String::from("MY_APP")), - version: params.version.unwrap_or(String::from("1.0")), - updatable: Some(params.updatable.unwrap_or(false)), - deletable: Some(params.deletable.unwrap_or(false)), - } -} - -async fn get_testing_app_deploy_params( - sender: &Address, - metadata: &AppDeployMetadata, - code_injection_value: Option, - on_schema_break: Option, - on_update: Option, - break_schema: Option, -) -> Result> { - let app_spec: serde_json::Value = serde_json::from_str(testing_app::APPLICATION)?; - - let approval_program_b64 = app_spec["source"]["approval"] - .as_str() - .ok_or("Missing approval program")?; - let clear_program_b64 = app_spec["source"]["clear"] - .as_str() - .ok_or("Missing clear program")?; - - let approval_program = String::from_utf8(BASE64_STANDARD.decode(approval_program_b64)?)?; - let clear_program = String::from_utf8(BASE64_STANDARD.decode(clear_program_b64)?)?; - - let mut template_params = HashMap::new(); - template_params.insert( - "TMPL_VALUE".to_string(), - TealTemplateValue::Int(code_injection_value.unwrap_or(1)), - ); - - let global_schema = if break_schema.unwrap_or(false) { - algokit_transact::StateSchema { - num_byte_slices: 3, // +1 from default 2 - num_uints: 3, - } - } else { - algokit_transact::StateSchema { - num_byte_slices: 2, - num_uints: 3, - } - }; - - Ok(AppDeployParams { - metadata: metadata.clone(), - deploy_time_params: Some(template_params), - on_schema_break, - on_update, - create_params: CreateParams::AppCreateCall(DeployAppCreateParams { - sender: sender.clone(), - approval_program: AppProgram::Teal(approval_program), - clear_state_program: AppProgram::Teal(clear_program), - global_state_schema: Some(global_schema), - local_state_schema: Some(algokit_transact::StateSchema { - num_byte_slices: 2, - num_uints: 1, - }), - ..Default::default() - }), - update_params: UpdateParams::AppUpdateCall(DeployAppUpdateParams { - sender: sender.clone(), - ..Default::default() - }), - delete_params: DeleteParams::AppDeleteCall(DeployAppDeleteParams { - sender: sender.clone(), - ..Default::default() - }), - existing_deployments: None, - ignore_cache: None, - send_params: SendParams { - max_rounds_to_wait_for_confirmation: Some(100), - }, - }) -} - -async fn get_testing_app_create_params( - app_manager: &AppManager, - sender: &Address, - metadata: &AppDeployMetadata, -) -> Result> { - let app_spec: serde_json::Value = serde_json::from_str(testing_app::APPLICATION)?; - - // Decode base64 to get TEAL source - let approval_program_b64 = app_spec["source"]["approval"] - .as_str() - .ok_or("Missing approval program")?; - let clear_program_b64 = app_spec["source"]["clear"] - .as_str() - .ok_or("Missing clear program")?; - - let approval_program = String::from_utf8(BASE64_STANDARD.decode(approval_program_b64)?)?; - let clear_program = String::from_utf8(BASE64_STANDARD.decode(clear_program_b64)?)?; - - // Apply template parameters - let template_params = HashMap::from([( - "TMPL_VALUE".to_string(), - TealTemplateValue::String("1".to_string()), - )]); - - // Compile TEAL with template substitution - let approval_compiled = app_manager - .compile_teal_template( - &approval_program, - Some(&template_params), - Some(&DeploymentMetadata { - updatable: metadata.updatable, - deletable: metadata.deletable, - }), - ) - .await?; - let clear_compiled = app_manager - .compile_teal_template(&clear_program, Some(&template_params), None) - .await?; - - // Create note with metadata - let note_data = serde_json::json!({ - "name": metadata.name, - "version": metadata.version, - "updatable": metadata.updatable, - "deletable": metadata.deletable - }); - let note = format!("ALGOKIT_DEPLOYER:j{}", note_data); - - Ok(AppCreateParams { - sender: sender.clone(), - note: Some(note.into_bytes()), - approval_program: approval_compiled.compiled_base64_to_bytes, - clear_state_program: clear_compiled.compiled_base64_to_bytes, - on_complete: OnApplicationComplete::NoOp, - args: None, - account_references: None, - app_references: None, - asset_references: None, - box_references: None, - global_state_schema: Some(algokit_transact::StateSchema { - num_byte_slices: 2, - num_uints: 3, - }), - local_state_schema: Some(algokit_transact::StateSchema { - num_byte_slices: 2, - num_uints: 1, - }), - extra_program_pages: None, - ..Default::default() - }) -} - -async fn get_deploy_params_for_replacing_app_using_abi_methods( - sender: &Address, - metadata: &AppDeployMetadata, - break_schema: bool, -) -> Result> { - let app_spec: serde_json::Value = - serde_json::from_str(abi_create_and_delete::APPLICATION_ARC56)?; - - let approval_program_b64 = app_spec["source"]["approval"] - .as_str() - .ok_or("Missing approval program")?; - let clear_program_b64 = app_spec["source"]["clear"] - .as_str() - .ok_or("Missing clear program")?; - - let approval_program = String::from_utf8(BASE64_STANDARD.decode(approval_program_b64)?)?; - let clear_program = String::from_utf8(BASE64_STANDARD.decode(clear_program_b64)?)?; - - let global_schema = if break_schema { - algokit_transact::StateSchema { - num_byte_slices: 3, // +1 from default 2 - num_uints: 3, - } - } else { - algokit_transact::StateSchema { - num_byte_slices: 2, - num_uints: 3, - } - }; - - Ok(AppDeployParams { - metadata: metadata.clone(), - deploy_time_params: None, - on_schema_break: Some(OnSchemaBreak::Replace), - on_update: Some(OnUpdate::Replace), - create_params: CreateParams::AppCreateMethodCall(DeployAppCreateMethodCallParams { - sender: sender.clone(), - method: ABIMethod::from_str("create(pay)string")?, - approval_program: AppProgram::Teal(approval_program), - clear_state_program: AppProgram::Teal(clear_program), - global_state_schema: Some(global_schema), - local_state_schema: Some(algokit_transact::StateSchema { - num_byte_slices: 2, - num_uints: 1, - }), - args: vec![AppMethodCallArg::Payment(PaymentParams { - sender: sender.clone(), - receiver: sender.clone(), - amount: 1000, - ..Default::default() - })], - ..Default::default() - }), - update_params: UpdateParams::AppUpdateCall(DeployAppUpdateParams { - sender: sender.clone(), - ..Default::default() - }), - delete_params: DeleteParams::AppDeleteMethodCall(DeployAppDeleteMethodCallParams { - sender: sender.clone(), - method: ABIMethod::from_str("delete(pay)string")?, - args: vec![AppMethodCallArg::Payment(PaymentParams { - sender: sender.clone(), - receiver: sender.clone(), - amount: 2000, - ..Default::default() - })], - ..Default::default() - }), - existing_deployments: None, - ignore_cache: None, - send_params: SendParams { - max_rounds_to_wait_for_confirmation: Some(100), - }, - }) -} - -#[rstest] -#[tokio::test] -async fn test_replacing_app_using_abi_methods(#[future] fixture: FixtureResult) -> TestResult { - let Fixture { - test_account, - mut app_deployer, - algorand_fixture, - .. - } = fixture.await?; - - // Deploy initial app (deletable) - let metadata = get_metadata(AppDeployMetadataParams { - deletable: Some(true), - ..Default::default() - }); - let deployment_1 = - get_deploy_params_for_replacing_app_using_abi_methods(&test_account, &metadata, false) - .await?; - - let result_1 = app_deployer.deploy(deployment_1).await?; - let app_1_id = match &result_1 { - AppDeployResult::Create { app, .. } => app.app_id, - _ => return Err("Expected Create result".into()), - }; - - algorand_fixture - .wait_for_indexer_transaction(&match result_1 { - AppDeployResult::Create { create_result, .. } => create_result.transaction_id.clone(), - _ => return Err("Expected Create result".into()), - }) - .await?; - - // Deploy replacement with different code - let metadata_2 = get_metadata(AppDeployMetadataParams { - version: Some(String::from("2.0")), - deletable: Some(true), - ..Default::default() - }); - let deployment_2 = - get_deploy_params_for_replacing_app_using_abi_methods(&test_account, &metadata_2, true) - .await?; - - let result_2 = app_deployer.deploy(deployment_2).await?; - let (app_2, create_result, delete_result) = match result_2 { - AppDeployResult::Replace { - app, - create_result, - delete_result, - .. - } => (app, create_result, delete_result), - _ => return Err("Expected Replace result".into()), - }; - - assert_ne!(app_2.app_id, app_1_id); - assert_eq!(app_2.created_metadata, metadata_2); - assert_eq!(app_2.created_round, app_2.updated_round); - assert_eq!( - app_2.created_round, - create_result.confirmation.confirmed_round.unwrap() - ); - assert_eq!(app_2.name, metadata_2.name); - assert_eq!(app_2.version, metadata_2.version); - assert_eq!(app_2.updatable, metadata_2.updatable); - assert_eq!(app_2.deletable, metadata_2.deletable); - assert!(!app_2.deleted); - - // Check ABI return values - assert!(create_result.abi_return.is_some()); - let create_abi_return = create_result.abi_return.unwrap(); - assert!(create_abi_return.return_value.is_some()); - if let Some(algokit_abi::ABIValue::String(s)) = create_abi_return.return_value { - assert_eq!(s, "created"); - } else { - panic!("Expected string return value from create method"); - } - - assert!(delete_result.abi_return.is_some()); - let delete_abi_return = delete_result.abi_return.unwrap(); - assert!(delete_abi_return.return_value.is_some()); - if let Some(algokit_abi::ABIValue::String(s)) = delete_abi_return.return_value { - assert_eq!(s, "deleted"); - } else { - panic!("Expected string return value from delete method"); - } - - Ok(()) -} diff --git a/crates/algokit_utils/tests/applications/app_factory.rs b/crates/algokit_utils/tests/applications/app_factory.rs deleted file mode 100644 index afcd8673c..000000000 --- a/crates/algokit_utils/tests/applications/app_factory.rs +++ /dev/null @@ -1,1207 +0,0 @@ -use crate::common::TestAccount; -use crate::common::{ - AlgorandFixture, AlgorandFixtureResult, TestResult, algorand_fixture, testing_app_spec, -}; -use algokit_abi::{ABIValue, Arc56Contract}; -use algokit_transact::Address; -use algokit_transact::OnApplicationComplete; -use algokit_utils::applications::app_client::{AppClientMethodCallParams, CompilationParams}; -use algokit_utils::applications::app_factory::{ - AppFactory, AppFactoryCreateMethodCallParams, AppFactoryParams, -}; -use algokit_utils::applications::app_factory::{AppFactoryCreateParams, DeployArgs}; -use algokit_utils::applications::{AppDeployResult, OnSchemaBreak, OnUpdate}; -use algokit_utils::clients::app_manager::{TealTemplateParams, TealTemplateValue}; -use algokit_utils::transactions::TransactionComposerConfig; -use algokit_utils::{AlgorandClient, AppMethodCallArg}; -use rstest::*; -use std::collections::HashMap; -use std::sync::Arc; - -#[derive(Default)] -pub struct AppFactoryOptions { - pub app_name: Option, - pub updatable: Option, - pub deletable: Option, - pub deploy_time_params: Option>, - pub transaction_composer_config: Option, -} - -fn abi_str_arg(s: &str) -> AppMethodCallArg { - AppMethodCallArg::ABIValue(algokit_abi::ABIValue::from(s)) -} - -fn into_factory_inputs(fixture: AlgorandFixture) -> (Arc, TestAccount) { - let AlgorandFixture { - algorand_client, - test_account, - .. - } = fixture; - #[allow(clippy::arc_with_non_send_sync)] - (Arc::new(algorand_client), test_account) -} - -/// Construct an `AppFactory` for a provided ARC-56 spec with common defaults. -pub async fn build_app_factory_with_spec( - algorand_client: Arc, - test_account: TestAccount, - app_spec: Arc56Contract, - opts: AppFactoryOptions, -) -> AppFactory { - let sender: Address = test_account.account().address(); - - let compilation_params = if opts.deploy_time_params.is_some() - || opts.updatable.is_some() - || opts.deletable.is_some() - { - Some(CompilationParams { - deploy_time_params: opts.deploy_time_params, - updatable: opts.updatable, - deletable: opts.deletable, - }) - } else { - None - }; - - AppFactory::new(AppFactoryParams { - algorand: algorand_client, - app_spec, - app_name: opts.app_name, - default_sender: Some(sender.to_string()), - default_signer: Some(Arc::new(test_account.clone())), - version: None, - compilation_params, - source_maps: None, - transaction_composer_config: opts.transaction_composer_config, - }) -} - -async fn build_testing_app_factory( - algorand_client: Arc, - test_account: TestAccount, - opts: AppFactoryOptions, -) -> AppFactory { - build_app_factory_with_spec(algorand_client, test_account, testing_app_spec(), opts).await -} - -fn compilation_params(value: u64, updatable: bool, deletable: bool) -> CompilationParams { - let mut t = TealTemplateParams::default(); - t.insert("VALUE".to_string(), TealTemplateValue::Int(value)); - CompilationParams { - deploy_time_params: Some(t), - updatable: Some(updatable), - deletable: Some(deletable), - } -} - -#[rstest] -#[tokio::test] -async fn bare_create_with_deploy_time_params( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let fixture = algorand_fixture.await?; - let (algorand_client, test_account) = into_factory_inputs(fixture); - - let factory = build_testing_app_factory( - algorand_client, - test_account, - AppFactoryOptions { - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(1), - )])), - updatable: Some(false), - deletable: Some(false), - ..Default::default() - }, - ) - .await; - - let compilation_params = compilation_params(1, false, false); - - let (client, res) = factory - .send() - .bare() - .create( - Some(AppFactoryCreateParams::default()), - None, - Some(compilation_params), - ) - .await?; - - assert!(client.app_id() > 0); - assert_eq!( - client.app_address(), - algokit_transact::Address::from_app_id(&client.app_id()) - ); - assert!(res.app_id > 0); - assert!(!res.compiled_programs.approval.compiled.is_empty()); - assert!(!res.compiled_programs.clear.compiled.is_empty()); - assert!(res.confirmation.confirmed_round.is_some()); - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn constructor_compilation_params_precedence( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let fixture = algorand_fixture.await?; - let (algorand_client, test_account) = into_factory_inputs(fixture); - - let factory = build_testing_app_factory( - algorand_client, - test_account, - AppFactoryOptions { - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(1), - )])), - updatable: Some(false), - deletable: Some(false), - ..Default::default() - }, - ) - .await; - - let (client, result) = factory.send().bare().create(None, None, None).await?; - - assert!(result.app_id > 0); - assert_eq!(client.app_id(), result.app_id); - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn oncomplete_override_on_create( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let fixture = algorand_fixture.await?; - let (algorand_client, test_account) = into_factory_inputs(fixture); - - let factory = build_testing_app_factory( - algorand_client, - test_account, - AppFactoryOptions { - updatable: Some(true), - deletable: Some(true), - ..Default::default() - }, - ) - .await; - - let params = AppFactoryCreateParams { - on_complete: Some(OnApplicationComplete::OptIn), - ..Default::default() - }; - let compilation_params = compilation_params(1, true, true); - let (client, result) = factory - .send() - .bare() - .create(Some(params), None, Some(compilation_params)) - .await?; - - match &result.transaction { - algokit_transact::Transaction::AppCall(fields) => { - assert_eq!( - fields.on_complete, - algokit_transact::OnApplicationComplete::OptIn - ); - } - _ => return Err("expected app call".into()), - } - assert!(client.app_id() > 0); - assert_eq!( - client.app_address(), - algokit_transact::Address::from_app_id(&client.app_id()) - ); - assert!(!result.compiled_programs.approval.compiled.is_empty()); - assert!(!result.compiled_programs.clear.compiled.is_empty()); - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn abi_based_create_returns_value( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let fixture = algorand_fixture.await?; - let (algorand_client, test_account) = into_factory_inputs(fixture); - - let factory = build_testing_app_factory( - algorand_client, - test_account, - AppFactoryOptions { - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(1), - )])), - updatable: Some(true), - deletable: Some(true), - ..Default::default() - }, - ) - .await; - - let cp = compilation_params(1, true, false); - - let (_client, call_return) = factory - .send() - .create( - AppFactoryCreateMethodCallParams { - method: "create_abi(string)string".to_string(), - args: Some(vec![abi_str_arg("string_io")]), - ..Default::default() - }, - None, - Some(cp), - ) - .await?; - - match call_return.result.abi_return.and_then(|r| r.return_value) { - Some(ABIValue::String(s)) => assert_eq!(s, "string_io"), - other => return Err(format!("expected string return, got {other:?}").into()), - } - assert!(!call_return.compiled_programs.approval.compiled.is_empty()); - assert!(!call_return.compiled_programs.clear.compiled.is_empty()); - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn create_then_call_via_app_client( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let fixture = algorand_fixture.await?; - let sender = fixture.test_account.account().address(); - let (algorand_client, test_account) = into_factory_inputs(fixture); - - let factory = build_testing_app_factory( - algorand_client, - test_account, - AppFactoryOptions { - updatable: Some(true), - ..Default::default() - }, - ) - .await; - - let cp = compilation_params(1, true, true); - - let (client, _res) = factory.send().bare().create(None, None, Some(cp)).await?; - - let send_res = client - .send() - .call( - AppClientMethodCallParams { - method: "call_abi(string)string".to_string(), - args: vec![abi_str_arg("test")], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await?; - - let abi_ret = send_res - .result - .abi_return - .clone() - .expect("abi return expected"); - if let Some(algokit_abi::ABIValue::String(s)) = abi_ret.return_value { - assert_eq!(s, "Hello, test"); - } else { - return Err("expected string".into()); - } - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn call_app_with_too_many_args( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let fixture = algorand_fixture.await?; - let sender = fixture.test_account.account().address(); - let (algorand_client, test_account) = into_factory_inputs(fixture); - - let factory = build_testing_app_factory( - algorand_client, - test_account, - AppFactoryOptions { - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(1), - )])), - updatable: Some(false), - deletable: Some(false), - ..Default::default() - }, - ) - .await; - - let (client, _res) = factory - .send() - .bare() - .create(None, None, Some(compilation_params(1, false, false))) - .await?; - - let err = client - .send() - .call( - AppClientMethodCallParams { - method: "call_abi(string)string".to_string(), - args: vec![abi_str_arg("test"), abi_str_arg("extra")], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - None, - ) - .await - .expect_err("expected error for too many args"); - // The error is wrapped into a ValidationError; extract message via Display - let msg = err.to_string(); - // Accept the actual error message format from Rust implementation - assert!( - msg.contains("The number of provided arguments is 2 while the method expects 1 arguments"), - "Expected error message about too many arguments, got: {msg}" - ); - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn call_app_with_rekey(#[future] algorand_fixture: AlgorandFixtureResult) -> TestResult { - let mut fixture = algorand_fixture.await?; - let sender = fixture.test_account.account().address(); - // Generate a new account to rekey to before consuming the fixture - let rekey_to = fixture.generate_account(None).await?; - let rekey_to_addr = rekey_to.account().address(); - let (algorand_client, test_account) = into_factory_inputs(fixture); - - let factory = build_testing_app_factory( - Arc::clone(&algorand_client), - test_account, - AppFactoryOptions { - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(1), - )])), - updatable: Some(true), - deletable: Some(true), - ..Default::default() - }, - ) - .await; - - let (client, _res) = factory.send().bare().create(None, None, None).await?; - - // Opt-in with rekey_to - client - .send() - .opt_in( - AppClientMethodCallParams { - method: "opt_in()void".to_string(), - args: vec![], - sender: Some(sender.to_string()), - rekey_to: Some(rekey_to_addr.to_string()), - ..Default::default() - }, - None, - ) - .await?; - - // If rekey succeeded, a zero payment using the rekeyed signer should succeed - let pay = algokit_utils::PaymentParams { - sender: sender.clone(), - // signer will be picked up from account manager: set_signer already configured for original sender, - // but after rekey the auth address must be rekey_to's signer. Use explicit signer. - signer: Some(Arc::new(rekey_to.clone())), - receiver: sender.clone(), - amount: 0, - ..Default::default() - }; - let _ = algorand_client.send().payment(pay, None).await?; - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn delete_app_with_abi_direct( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let fixture = algorand_fixture.await?; - let sender = fixture.test_account.account().address(); - let (algorand_client, test_account) = into_factory_inputs(fixture); - - let factory = build_testing_app_factory( - algorand_client, - test_account, - AppFactoryOptions { - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(1), - )])), - updatable: Some(false), - deletable: Some(true), - ..Default::default() - }, - ) - .await; - - let (client, _res) = factory - .send() - .bare() - .create(None, None, Some(compilation_params(1, false, true))) - .await?; - - let delete_res = client - .send() - .delete( - AppClientMethodCallParams { - method: "delete_abi(string)string".to_string(), - args: vec![abi_str_arg("string_io")], - sender: Some(sender.to_string()), - ..Default::default() - }, - None, - ) - .await?; - - let abi_ret = delete_res - .result - .abi_return - .clone() - .expect("abi return expected"); - if let Some(algokit_abi::ABIValue::String(s)) = abi_ret.return_value { - assert_eq!(s, "string_io"); - } else { - return Err("expected string return".into()); - } - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn update_app_with_abi_direct( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let fixture = algorand_fixture.await?; - let sender = fixture.test_account.account().address(); - let (algorand_client, test_account) = into_factory_inputs(fixture); - - let factory = build_testing_app_factory( - algorand_client, - test_account, - AppFactoryOptions { - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(1), - )])), - updatable: Some(true), - deletable: Some(false), - ..Default::default() - }, - ) - .await; - - // Initial create - let (client, _create_res) = factory - .send() - .bare() - .create(None, None, Some(compilation_params(1, true, false))) - .await?; - - // Update via ABI (extra pages are auto-calculated internally) - let update_res = client - .send() - .update( - AppClientMethodCallParams { - method: "update_abi(string)string".to_string(), - args: vec![abi_str_arg("string_io")], - sender: Some(sender.to_string()), - ..Default::default() - }, - Some(compilation_params(1, true, false)), - None, - ) - .await?; - - let abi_ret = update_res - .result - .abi_return - .clone() - .expect("abi return expected"); - if let Some(algokit_abi::ABIValue::String(s)) = abi_ret.return_value { - assert_eq!(s, "string_io"); - } else { - return Err("expected string return".into()); - } - assert!(update_res.compiled_programs.approval.source_map.is_some()); - assert!(update_res.compiled_programs.clear.source_map.is_some()); - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn deploy_when_immutable_and_permanent( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let fixture = algorand_fixture.await?; - let (algorand_client, test_account) = into_factory_inputs(fixture); - - let factory = build_testing_app_factory( - algorand_client, - test_account, - AppFactoryOptions { - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(1), - )])), - updatable: Some(false), - deletable: Some(false), - ..Default::default() - }, - ) - .await; - - factory - .deploy( - DeployArgs { - on_update: Some(OnUpdate::Fail), - on_schema_break: Some(OnSchemaBreak::Fail), - ..Default::default() - }, - None, - ) - .await?; - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn deploy_app_create(#[future] algorand_fixture: AlgorandFixtureResult) -> TestResult { - let fixture = algorand_fixture.await?; - let (algorand_client, test_account) = into_factory_inputs(fixture); - - let factory = build_testing_app_factory( - algorand_client, - test_account, - AppFactoryOptions { - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(1), - )])), - updatable: Some(true), - ..Default::default() - }, - ) - .await; - - let (client, deploy_result) = factory.deploy(Default::default(), None).await?; - - let (app_metadata, compiled_programs, create_result) = match &deploy_result { - AppDeployResult::Create { - app, - compiled_programs, - create_result, - .. - } => (app, compiled_programs, create_result), - _ => return Err("expected Create".into()), - }; - assert!(client.app_id() > 0); - assert_eq!(client.app_id(), app_metadata.app_id); - assert!(!compiled_programs.approval.compiled.is_empty()); - assert!(!compiled_programs.clear.compiled.is_empty()); - assert_eq!( - create_result.confirmation.app_id.unwrap_or_default(), - app_metadata.app_id - ); - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn deploy_app_create_abi(#[future] algorand_fixture: AlgorandFixtureResult) -> TestResult { - let fixture = algorand_fixture.await?; - let (algorand_client, test_account) = into_factory_inputs(fixture); - - let factory = build_testing_app_factory( - algorand_client, - test_account, - AppFactoryOptions { - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(1), - )])), - updatable: Some(true), - deletable: Some(true), - ..Default::default() - }, - ) - .await; - - let create_params = AppFactoryCreateMethodCallParams { - method: "create_abi(string)string".to_string(), - args: Some(vec![algokit_utils::AppMethodCallArg::ABIValue( - algokit_abi::ABIValue::from("arg_io"), - )]), - ..Default::default() - }; - - let (client, deploy_result) = factory - .deploy( - DeployArgs { - create_params: Some(create_params), - ..Default::default() - }, - None, - ) - .await?; - - let (app_metadata, compiled_programs, create_result) = match &deploy_result { - AppDeployResult::Create { - app, - compiled_programs, - create_result, - .. - } => (app, compiled_programs, create_result), - _ => return Err("expected Create".into()), - }; - assert!(client.app_id() > 0); - assert_eq!(client.app_id(), app_metadata.app_id); - let abi_value = create_result - .abi_return - .clone() - .and_then(|r| r.return_value) - .expect("abi return expected"); - let abi_value = match abi_value { - algokit_abi::ABIValue::String(s) => s, - other => return Err(format!("expected string abi return, got {other:?}").into()), - }; - assert_eq!(abi_value, "arg_io"); - assert!(!compiled_programs.approval.compiled.is_empty()); - assert!(!compiled_programs.clear.compiled.is_empty()); - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn deploy_app_update(#[future] algorand_fixture: AlgorandFixtureResult) -> TestResult { - let fixture = algorand_fixture.await?; - let (algorand_client, test_account) = into_factory_inputs(fixture); - - let factory = build_testing_app_factory( - Arc::clone(&algorand_client), - test_account.clone(), - AppFactoryOptions { - app_name: Some("APP_NAME".to_string()), - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(1), - )])), - updatable: Some(true), - deletable: Some(true), - ..Default::default() - }, - ) - .await; - - // Initial create (updatable) - let (_client1, create_res) = factory.deploy(Default::default(), None).await?; - let (create_app_metadata, initial_compiled_programs, _initial_create) = match &create_res { - AppDeployResult::Create { - app, - compiled_programs, - create_result, - .. - } => (app, compiled_programs, create_result), - _ => return Err("expected Create".into()), - }; - - // Update - let factory2 = build_testing_app_factory( - Arc::clone(&algorand_client), - test_account, - AppFactoryOptions { - app_name: Some("APP_NAME".to_string()), - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(2), - )])), - updatable: Some(true), - deletable: Some(true), - ..Default::default() - }, - ) - .await; - - let (_client2, update_res) = factory2 - .deploy( - DeployArgs { - on_update: Some(OnUpdate::Update), - ..Default::default() - }, - None, - ) - .await?; - - let (update_app_metadata, updated_compiled_programs, updated) = match &update_res { - AppDeployResult::Update { - app, - compiled_programs, - update_result, - .. - } => (app, compiled_programs, update_result), - _ => return Err("expected Update".into()), - }; - assert_eq!(create_app_metadata.app_id, update_app_metadata.app_id); - assert_eq!( - create_app_metadata.app_address, - update_app_metadata.app_address - ); - assert!(update_app_metadata.updated_round >= create_app_metadata.created_round); - assert!(!initial_compiled_programs.approval.compiled.is_empty()); - assert!(!initial_compiled_programs.clear.compiled.is_empty()); - assert!(!updated_compiled_programs.approval.compiled.is_empty()); - assert!(!updated_compiled_programs.clear.compiled.is_empty()); - assert!(updated_compiled_programs.approval.source_map.is_some()); - assert!(updated_compiled_programs.clear.source_map.is_some()); - assert_eq!( - updated.confirmation.confirmed_round, - Some(update_app_metadata.updated_round) - ); - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn deploy_app_update_detects_extra_pages_as_breaking_change( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let fixture = algorand_fixture.await?; - // Factory with small program spec - let small_spec = algokit_abi::Arc56Contract::from_json( - algokit_test_artifacts::extra_pages_test::SMALL_ARC56, - ) - .expect("valid arc56"); - let (algorand_client, test_account) = into_factory_inputs(fixture); - let factory = build_app_factory_with_spec( - Arc::clone(&algorand_client), - test_account.clone(), - small_spec, - AppFactoryOptions { - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(1), - )])), - updatable: Some(true), - ..Default::default() - }, - ) - .await; - - // Create using small - let (_small_client, create_res) = factory.deploy(Default::default(), None).await?; - let (small_app_metadata, _, _) = match &create_res { - AppDeployResult::Create { - app, - compiled_programs, - create_result, - .. - } => (app, compiled_programs, create_result), - _ => return Err("expected Create for small".into()), - }; - - // Switch to large spec and attempt update with Append schema break - let large_spec = algokit_abi::Arc56Contract::from_json( - algokit_test_artifacts::extra_pages_test::LARGE_ARC56, - ) - .expect("valid arc56"); - let factory_large = build_app_factory_with_spec( - algorand_client, - test_account, - large_spec, - AppFactoryOptions { - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(2), - )])), - updatable: Some(true), - ..Default::default() - }, - ) - .await; - - let (large_client, update_res) = factory_large - .deploy( - DeployArgs { - on_update: Some(OnUpdate::Update), - on_schema_break: Some(OnSchemaBreak::Append), - ..Default::default() - }, - None, - ) - .await?; - - match &update_res { - AppDeployResult::Create { .. } => {} - _ => return Err("expected Create on schema break append".into()), - } - - // App id should differ between small and large - assert_ne!(small_app_metadata.app_id, large_client.app_id()); - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn deploy_app_update_detects_extra_pages_as_breaking_change_fail_case( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let fixture = algorand_fixture.await?; - // Start with small - let small_spec = algokit_abi::Arc56Contract::from_json( - algokit_test_artifacts::extra_pages_test::SMALL_ARC56, - ) - .expect("valid arc56"); - let (algorand_client, test_account) = into_factory_inputs(fixture); - let factory_small = build_app_factory_with_spec( - Arc::clone(&algorand_client), - test_account.clone(), - small_spec, - AppFactoryOptions { - updatable: Some(true), - ..Default::default() - }, - ) - .await; - - // Create using small - let (_small_client, _create_res) = factory_small.deploy(Default::default(), None).await?; - - // Switch to large and attempt update with Fail schema break - let large_spec = algokit_abi::Arc56Contract::from_json( - algokit_test_artifacts::extra_pages_test::LARGE_ARC56, - ) - .expect("valid arc56"); - let factory_fail = build_app_factory_with_spec( - algorand_client, - test_account, - large_spec, - AppFactoryOptions { - updatable: Some(true), - ..Default::default() - }, - ) - .await; - - let msg = match factory_fail - .deploy( - DeployArgs { - on_update: Some(OnUpdate::Update), - on_schema_break: Some(OnSchemaBreak::Fail), - ..Default::default() - }, - None, - ) - .await - { - Ok(_) => return Err("expected schema break fail error".into()), - Err(e) => e.to_string(), - }; - assert!(msg.contains("Executing the fail on schema break strategy, stopping deployment.")); - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn deploy_app_update_abi(#[future] algorand_fixture: AlgorandFixtureResult) -> TestResult { - let fixture = algorand_fixture.await?; - let (algorand_client, test_account) = into_factory_inputs(fixture); - - let factory = build_testing_app_factory( - Arc::clone(&algorand_client), - test_account.clone(), - AppFactoryOptions { - app_name: Some("APP_NAME".to_string()), - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(1), - )])), - updatable: Some(true), - deletable: Some(true), - ..Default::default() - }, - ) - .await; - - // Create updatable - let _ = factory.deploy(Default::default(), None).await?; - - // Update via ABI with VALUE=2 but same updatable/deletable - let update_params = AppClientMethodCallParams { - method: "update_abi(string)string".to_string(), - args: vec![algokit_utils::AppMethodCallArg::ABIValue( - algokit_abi::ABIValue::from("args_io"), - )], - ..Default::default() - }; - let factory2 = build_testing_app_factory( - Arc::clone(&algorand_client), - test_account, - AppFactoryOptions { - app_name: Some("APP_NAME".to_string()), - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(2), - )])), - updatable: Some(true), - deletable: Some(true), - ..Default::default() - }, - ) - .await; - let (_client2, update_res) = factory2 - .deploy( - DeployArgs { - on_update: Some(OnUpdate::Update), - update_params: Some(update_params), - ..Default::default() - }, - None, - ) - .await?; - let (_, update_compiled_programs, update_result) = match &update_res { - AppDeployResult::Update { - app, - compiled_programs, - update_result, - .. - } => (app, compiled_programs, update_result), - _ => return Err("expected Update".into()), - }; - let abi_value = update_result - .abi_return - .clone() - .and_then(|r| r.return_value) - .expect("abi return"); - let abi_return = match abi_value { - algokit_abi::ABIValue::String(s) => s, - other => return Err(format!("expected string return, got {other:?}").into()), - }; - assert_eq!(abi_return, "args_io"); - assert!(!update_compiled_programs.approval.compiled.is_empty()); - assert!(!update_compiled_programs.clear.compiled.is_empty()); - assert!(update_compiled_programs.approval.source_map.is_some()); - assert!(update_compiled_programs.clear.source_map.is_some()); - // Ensure update onComplete is UpdateApplication - match &update_result.transaction { - algokit_transact::Transaction::AppCall(fields) => { - assert_eq!( - fields.on_complete, - algokit_transact::OnApplicationComplete::UpdateApplication - ); - } - _ => return Err("expected app call".into()), - } - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn deploy_app_replace(#[future] algorand_fixture: AlgorandFixtureResult) -> TestResult { - let fixture = algorand_fixture.await?; - let (algorand_client, test_account) = into_factory_inputs(fixture); - - let factory = build_testing_app_factory( - Arc::clone(&algorand_client), - test_account.clone(), - AppFactoryOptions { - app_name: Some("APP_NAME".to_string()), - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(1), - )])), - updatable: Some(true), - deletable: Some(true), - ..Default::default() - }, - ) - .await; - - let (_client1, create_res) = factory.deploy(Default::default(), None).await?; - let old_app_id = match &create_res { - AppDeployResult::Create { app, .. } => app.app_id, - _ => return Err("expected Create".into()), - }; - - // Replace - let factory2 = build_testing_app_factory( - Arc::clone(&algorand_client), - test_account, - AppFactoryOptions { - app_name: Some("APP_NAME".to_string()), - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(2), - )])), - updatable: Some(true), - deletable: Some(true), - ..Default::default() - }, - ) - .await; - let (_client2, replace_res) = factory2 - .deploy( - DeployArgs { - on_update: Some(OnUpdate::Replace), - ..Default::default() - }, - None, - ) - .await?; - let (replace_app_metadata, replace_compiled_programs, replace_delete_result, _) = - match &replace_res { - AppDeployResult::Replace { - app, - compiled_programs, - delete_result, - create_result, - .. - } => (app, compiled_programs, delete_result, create_result), - _ => return Err("expected Replace".into()), - }; - assert!(replace_app_metadata.app_id > old_app_id); - assert!(!replace_compiled_programs.approval.compiled.is_empty()); - assert!(!replace_compiled_programs.clear.compiled.is_empty()); - assert!(replace_delete_result.confirmation.confirmed_round.is_some()); - // Ensure delete app call references old app id and correct onComplete - match &replace_delete_result.transaction { - algokit_transact::Transaction::AppCall(fields) => { - assert_eq!( - fields.on_complete, - algokit_transact::OnApplicationComplete::DeleteApplication - ); - assert_eq!(fields.app_id, old_app_id); - } - _ => return Err("expected app call".into()), - } - assert_eq!( - replace_app_metadata.app_address, - algokit_transact::Address::from_app_id(&replace_app_metadata.app_id) - ); - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn deploy_app_replace_abi(#[future] algorand_fixture: AlgorandFixtureResult) -> TestResult { - let fixture = algorand_fixture.await?; - let (algorand_client, test_account) = into_factory_inputs(fixture); - - let factory = build_testing_app_factory( - Arc::clone(&algorand_client), - test_account.clone(), - AppFactoryOptions { - app_name: Some("APP_NAME".to_string()), - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(1), - )])), - updatable: Some(true), - deletable: Some(true), - ..Default::default() - }, - ) - .await; - - // Initial create - let (_client1, create_res) = factory - .deploy( - DeployArgs { - app_name: Some("APP_NAME".to_string()), - ..Default::default() - }, - None, - ) - .await?; - - let old_app_id = match &create_res { - AppDeployResult::Create { app, .. } => app.app_id, - _ => return Err("expected Create".into()), - }; - - // Replace via ABI create/delete - let create_params = AppFactoryCreateMethodCallParams { - method: "create_abi(string)string".to_string(), - args: Some(vec![abi_str_arg("arg_io")]), - ..Default::default() - }; - let delete_params = AppClientMethodCallParams { - method: "delete_abi(string)string".to_string(), - args: vec![abi_str_arg("arg2_io")], - ..Default::default() - }; - let factory2 = build_testing_app_factory( - Arc::clone(&algorand_client), - test_account, - AppFactoryOptions { - app_name: Some("APP_NAME".to_string()), - deploy_time_params: Some(HashMap::from([( - "VALUE".to_string(), - TealTemplateValue::Int(2), - )])), - updatable: Some(true), - deletable: Some(true), - ..Default::default() - }, - ) - .await; - let (_client2, replace_res) = factory2 - .deploy( - DeployArgs { - on_update: Some(OnUpdate::Replace), - create_params: Some(create_params), - delete_params: Some(delete_params), - ..Default::default() - }, - None, - ) - .await?; - let (replace_app_metadata_2, _, replace_delete_result_2, replace_create_result_2) = - match &replace_res { - AppDeployResult::Replace { - app, - compiled_programs, - delete_result, - create_result, - .. - } => (app, compiled_programs, delete_result, create_result), - _ => return Err("expected Replace".into()), - }; - assert!(replace_app_metadata_2.app_id > old_app_id); - // Validate ABI return values for create/delete - - let create_value = replace_create_result_2 - .abi_return - .clone() - .and_then(|r| r.return_value) - .expect("create abi return"); - let create_ret = match create_value { - algokit_abi::ABIValue::String(s) => s, - _ => return Err("create abi return".into()), - }; - assert_eq!(create_ret, "arg_io"); - - if let Some(algokit_abi::ABIValue::String(s)) = replace_delete_result_2 - .abi_return - .clone() - .and_then(|r| r.return_value) - { - assert_eq!(s, "arg2_io"); - } - Ok(()) -} diff --git a/crates/algokit_utils/tests/applications/mod.rs b/crates/algokit_utils/tests/applications/mod.rs deleted file mode 100644 index 1a0af34f2..000000000 --- a/crates/algokit_utils/tests/applications/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod app_client; -pub mod app_deployer; -pub mod app_factory; diff --git a/crates/algokit_utils/tests/applications_tests.rs b/crates/algokit_utils/tests/applications_tests.rs deleted file mode 100644 index 543613749..000000000 --- a/crates/algokit_utils/tests/applications_tests.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod applications; -mod common; diff --git a/crates/algokit_utils/tests/clients/algorand_client.rs b/crates/algokit_utils/tests/clients/algorand_client.rs deleted file mode 100644 index 90954005e..000000000 --- a/crates/algokit_utils/tests/clients/algorand_client.rs +++ /dev/null @@ -1,66 +0,0 @@ -use algokit_utils::AlgorandClient; - -/// Test basic functionality of AlgorandClient -#[tokio::test] -async fn test_algorand_client_basic_functionality() --> Result<(), Box> { - let client = AlgorandClient::default_localnet(None); - - // Test that we can get suggested params (this verifies client connectivity) - let suggested_params = client - .get_suggested_params() - .await - .map_err(|e| format!("Failed to get suggested params: {}", e))?; - - // Basic validation that we got a valid response - // Note: fee might be 0 on localnet with flat fees, but min_fee should always be set - assert!(!suggested_params.genesis_id.is_empty()); - assert!(!suggested_params.genesis_hash.is_empty()); - assert!(suggested_params.last_round > 0); - assert!( - suggested_params.min_fee > 0, - "Min fee should always be greater than 0" - ); - - Ok(()) -} - -/// Test AlgorandClient initialization methods -#[tokio::test] -async fn test_algorand_client_initialization() { - // Test default localnet initialization - we can't access internal fields, - // so just verify the client can be created without panicking - let _client_localnet = AlgorandClient::default_localnet(None); - - // Test testnet initialization - let _client_testnet = AlgorandClient::testnet(None); - - // Test mainnet initialization - let _client_mainnet = AlgorandClient::mainnet(None); - - // Test from environment (should default to localnet if no env vars set) - let _client_env = AlgorandClient::from_environment(None); -} - -/// Test AlgorandClient with fixture integration -#[tokio::test] -async fn test_algorand_client_with_fixture() -> Result<(), Box> -{ - // Use from_environment since the fixture sets up the environment properly - let client = AlgorandClient::from_environment(None); - - // Test suggested params through fixture - let suggested_params = client - .get_suggested_params() - .await - .map_err(|e| format!("Failed to get suggested params: {}", e))?; - - // Basic validation - assert!(suggested_params.last_round > 0); - assert!( - suggested_params.min_fee > 0, - "Min fee should always be greater than 0" - ); - - Ok(()) -} diff --git a/crates/algokit_utils/tests/clients/app_manager.rs b/crates/algokit_utils/tests/clients/app_manager.rs deleted file mode 100644 index 4fbc64e15..000000000 --- a/crates/algokit_utils/tests/clients/app_manager.rs +++ /dev/null @@ -1,504 +0,0 @@ -use algokit_abi::{ABIType, abi_type::BitSize}; -use algokit_test_artifacts::template_variables; -use algokit_utils::clients::app_manager::*; -use base64::prelude::*; -use rstest::*; -use std::collections::HashMap; - -use crate::common::{AlgorandFixtureResult, TestResult, algorand_fixture}; - -/// Test template variable replacement behavior -#[rstest] -#[case("pushint TMPL_NUMBER\npushbytes TMPL_STRING", - &[("NUMBER", TealTemplateValue::Int(42)), ("STRING", TealTemplateValue::String("hello".to_string()))], - "pushint 42\npushbytes 0x68656c6c6f")] -#[case("pushint TMPL_UPDATABLE\npushint TMPL_DELETABLE", - &[("UPDATABLE", TealTemplateValue::Int(1)), ("DELETABLE", TealTemplateValue::Int(0))], - "pushint 1\npushint 0")] -#[case("pushbytes \"TMPL_NUMBER\"\npushint TMPL_NUMBER", - &[("NUMBER", TealTemplateValue::Int(42))], - "pushbytes \"TMPL_NUMBER\"\npushint 42")] -#[case("TMPL_X TMPL_X TMPL_X", - &[("X", TealTemplateValue::String("test".to_string()))], - "0x74657374 0x74657374 0x74657374")] -fn test_template_variable_replacement_behavior( - #[case] teal_code: &str, - #[case] template_vars: &[(&str, TealTemplateValue)], - #[case] expected: &str, -) { - let template_map = template_vars - .iter() - .map(|(k, v)| (k.to_string(), v.clone())) - .collect(); - - let result = AppManager::replace_template_variables(teal_code, &template_map).unwrap(); - assert_eq!(result.trim(), expected.trim()); -} - -/// Test comprehensive comment stripping behavior with all edge cases -#[test] -fn test_comprehensive_comment_stripping() { - let input = r#"//comment -op arg //comment -op "arg" //comment -op "//" //comment -op " //comment " //comment -op "\" //" //comment -op "// \" //" //comment -op "" //comment -// -op 123 -op 123 // something -op "" // more comments -op "//" //op "//" -op "//" -pushbytes base64(//8=) -pushbytes b64(//8=) - -pushbytes base64(//8=) // pushbytes base64(//8=) -pushbytes b64(//8=) // pushbytes b64(//8=) -pushbytes "base64(//8=)" // pushbytes "base64(//8=)" -pushbytes "b64(//8=)" // pushbytes "b64(//8=)" - -pushbytes base64 //8= -pushbytes b64 //8= - -pushbytes base64 //8= // pushbytes base64 //8= -pushbytes b64 //8= // pushbytes b64 //8= -pushbytes "base64 //8=" // pushbytes "base64 //8=" -pushbytes "b64 //8=" // pushbytes "b64 //8=""#; - - let expected = r#" -op arg -op "arg" -op "//" -op " //comment " -op "\" //" -op "// \" //" -op "" - -op 123 -op 123 -op "" -op "//" -op "//" -pushbytes base64(//8=) -pushbytes b64(//8=) - -pushbytes base64(//8=) -pushbytes b64(//8=) -pushbytes "base64(//8=)" -pushbytes "b64(//8=)" - -pushbytes base64 //8= -pushbytes b64 //8= - -pushbytes base64 //8= -pushbytes b64 //8= -pushbytes "base64 //8=" -pushbytes "b64 //8=""#; - - let result = AppManager::strip_teal_comments(input); - assert_eq!(result.trim(), expected.trim()); -} - -/// Test TEAL compilation and caching behavior -#[rstest] -#[tokio::test] -async fn test_teal_compilation(#[future] algorand_fixture: AlgorandFixtureResult) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let app_manager = algorand_fixture.algorand_client.app(); - - let teal = "#pragma version 3\npushint 1\nreturn"; - let result = app_manager.compile_teal(teal).await.unwrap(); - - assert_eq!(result.teal, teal); - // Verify deterministic compilation results - assert_eq!(result.compiled_base64_to_bytes, vec![3, 129, 1, 67]); - assert_eq!( - result.compiled_hash, - "LKKM53XYIPYORMMTKCCUXWFPADWRFYAYZ27QZ2HUWER4OU7TKTVW3C4BRQ" - ); - - // Test caching behavior by verifying consistent results across calls - let cached = app_manager.compile_teal(teal).await.unwrap(); - assert_eq!(result.compiled_hash, cached.compiled_hash); - assert_eq!(result.teal, cached.teal); - assert_eq!( - result.compiled_base64_to_bytes, - cached.compiled_base64_to_bytes - ); - - // Test with different TEAL code produces different results - let different_teal = "#pragma version 3\npushint 2\nreturn"; - let different_result = app_manager.compile_teal(different_teal).await.unwrap(); - assert_ne!(result.compiled_hash, different_result.compiled_hash); - - Ok(()) -} - -/// Test template compilation -#[rstest] -#[tokio::test] -async fn test_template_compilation( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let app_manager = algorand_fixture.algorand_client.app(); - - let template_params = HashMap::from([("VALUE".to_string(), TealTemplateValue::Int(42))]); - let result = app_manager - .compile_teal_template( - "#pragma version 3\npushint TMPL_VALUE\nreturn", - Some(&template_params), - None, - ) - .await - .unwrap(); - - assert!(result.teal.contains("pushint 42")); - assert!(!result.teal.contains("TMPL_")); - // Check deterministic compilation results for template with int 42 - assert_eq!(result.compiled_base64_to_bytes, vec![3, 129, 42, 67]); - - Ok(()) -} - -/// Test deploy-time control -#[rstest] -#[tokio::test] -async fn test_deploy_time_control(#[future] algorand_fixture: AlgorandFixtureResult) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let app_manager = algorand_fixture.algorand_client.app(); - - let template = format!( - "#pragma version 3\npushint {}\npushint {}\nreturn", - UPDATABLE_TEMPLATE_NAME, DELETABLE_TEMPLATE_NAME - ); - let metadata = DeploymentMetadata { - updatable: Some(true), - deletable: Some(false), - }; - - let result = app_manager - .compile_teal_template(&template, None, Some(&metadata)) - .await - .unwrap(); - - assert!(result.teal.contains("pushint 1")); - assert!(result.teal.contains("pushint 0")); - assert!(!result.teal.contains("TMPL_")); - - Ok(()) -} - -/// Test real contract compilation -#[rstest] -#[tokio::test] -async fn test_real_contract_compilation( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let app_manager = algorand_fixture.algorand_client.app(); - - let contract: serde_json::Value = - serde_json::from_str(template_variables::APPLICATION_ARC56).unwrap(); - let approval_teal = contract["source"]["approval"].as_str().unwrap(); - let approval_code = String::from_utf8(BASE64_STANDARD.decode(approval_teal).unwrap()).unwrap(); - - let template_params = HashMap::from([ - ("uint64TmplVar".to_string(), TealTemplateValue::Int(42)), - ("bytesTmplVar".to_string(), TealTemplateValue::String("hello".to_string())), - ("bytes32TmplVar".to_string(), TealTemplateValue::String("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string())), - ("bytes64TmplVar".to_string(), TealTemplateValue::String("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string())), - ]); - - let result = app_manager - .compile_teal_template(&approval_code, Some(&template_params), None) - .await - .unwrap(); - - // Check deterministic compilation results for the real contract with fixed template parameters - let expected_bytes = vec![ - 10, 32, 2, 1, 42, 38, 3, 5, 104, 101, 108, 108, 111, 128, 1, 48, 49, 50, 51, 52, 53, 54, - 55, 56, 57, 97, 98, 99, 100, 101, 102, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, - 100, 101, 102, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102, 48, 49, - 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102, 48, 49, 50, 51, 52, 53, 54, 55, - 56, 57, 97, 98, 99, 100, 101, 102, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, - 101, 102, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102, 48, 49, 50, - 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102, 64, 48, 49, 50, 51, 52, 53, 54, 55, - 56, 57, 97, 98, 99, 100, 101, 102, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, - 101, 102, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102, 48, 49, 50, - 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102, 49, 24, 20, 129, 6, 11, 49, 25, 8, - 141, 12, 0, 80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 136, - 0, 2, 34, 67, 138, 0, 0, 40, 176, 35, 68, 137, 136, 0, 2, 34, 67, 138, 0, 0, 40, 41, 42, - 132, 137, 136, 0, 2, 34, 67, 138, 0, 0, 0, 137, 128, 4, 21, 31, 124, 117, 136, 0, 12, 73, - 21, 22, 87, 6, 2, 76, 80, 80, 176, 34, 67, 138, 0, 1, 35, 22, 137, 34, 67, 128, 4, 184, 68, - 123, 54, 54, 26, 0, 142, 1, 255, 241, 0, 128, 4, 154, 113, 210, 180, 128, 4, 223, 77, 92, - 59, 128, 4, 61, 135, 13, 135, 128, 4, 188, 11, 23, 6, 54, 26, 0, 142, 4, 255, 140, 255, - 153, 255, 166, 255, 176, 0, - ]; - assert_eq!(result.compiled_base64_to_bytes, expected_bytes); - assert_eq!( - result.compiled_hash, - "P2FNVZSIY7ETR6HLNUMUA7SXEK5ZHQBWLFH3T2IJKHBKHMLKA5KAIWQZFE" - ); - - Ok(()) -} - -/// Test template substitution -#[test] -fn test_template_substitution() { - let program = r#"test TMPL_INT // TMPL_INT -test TMPL_INT -no change -test TMPL_STR // TMPL_STR -TMPL_STR -TMPL_STR // TMPL_INT -TMPL_STR // foo // -TMPL_STR // bar -test "TMPL_STR" // not replaced -test "TMPL_STRING" // not replaced -test TMPL_STRING // not replaced -test TMPL_STRI // not replaced -test TMPL_STR TMPL_INT TMPL_INT TMPL_STR // TMPL_STR TMPL_INT TMPL_INT TMPL_STR -test TMPL_INT TMPL_STR TMPL_STRING "TMPL_INT TMPL_STR TMPL_STRING" //TMPL_INT TMPL_STR TMPL_STRING -test TMPL_INT TMPL_INT TMPL_STRING TMPL_STRING TMPL_STRING TMPL_INT TMPL_STRING //keep -TMPL_STR TMPL_STR TMPL_STR -TMPL_STRING -test NOTTMPL_STR // not replaced -NOTTMPL_STR // not replaced -TMPL_STR // replaced"#; - - let mut template_values = HashMap::new(); - template_values.insert("INT".to_string(), TealTemplateValue::Int(123)); - template_values.insert( - "STR".to_string(), - TealTemplateValue::String("ABC".to_string()), - ); - - let result = AppManager::replace_template_variables(program, &template_values) - .expect("Template replacement should succeed"); - - let expected = r#"test 123 // TMPL_INT -test 123 -no change -test 0x414243 // TMPL_STR -0x414243 -0x414243 // TMPL_INT -0x414243 // foo // -0x414243 // bar -test "TMPL_STR" // not replaced -test "TMPL_STRING" // not replaced -test TMPL_STRING // not replaced -test TMPL_STRI // not replaced -test 0x414243 123 123 0x414243 // TMPL_STR TMPL_INT TMPL_INT TMPL_STR -test 123 0x414243 TMPL_STRING "TMPL_INT TMPL_STR TMPL_STRING" //TMPL_INT TMPL_STR TMPL_STRING -test 123 123 TMPL_STRING TMPL_STRING TMPL_STRING 123 TMPL_STRING //keep -0x414243 0x414243 0x414243 -TMPL_STRING -test NOTTMPL_STR // not replaced -NOTTMPL_STR // not replaced -0x414243 // replaced"#; - - // Verify the output matches exactly - assert_eq!(result.trim(), expected.trim()); -} - -/// Test compilation error handling -#[rstest] -#[tokio::test] -async fn test_compilation_errors(#[future] algorand_fixture: AlgorandFixtureResult) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let app_manager = algorand_fixture.algorand_client.app(); - - // Invalid TEAL should fail - let result = app_manager - .compile_teal("#pragma version 3\ninvalid_opcode_xyz") - .await; - assert!(result.is_err()); - - // Missing template variables should either preserve or fail - let result = app_manager - .compile_teal_template( - "#pragma version 3\npushint TMPL_MISSING\nreturn", - None, - None, - ) - .await; - - match result { - Ok(compiled) => assert!(compiled.teal.contains("TMPL_MISSING")), - Err(_) => {} // Both outcomes are acceptable - } - - Ok(()) -} - -/// Test that BoxIdentifier correctly handles binary data -#[test] -fn test_box_identifier_binary_handling() { - use base64::{Engine, engine::general_purpose::STANDARD as Base64}; - - // Test with UTF-8 string data (common case) - let text_data = "hello_world".as_bytes().to_vec(); - let (app_id, name_bytes) = AppManager::get_box_reference(&text_data); - assert_eq!(app_id, 0); - assert_eq!(name_bytes, text_data); - assert_eq!(name_bytes, b"hello_world".to_vec()); - - // Test with pure binary data (non-UTF-8) - let binary_data = vec![0xFF, 0xFE, 0xFD, 0x00, 0x01, 0x02]; - let (app_id, name_bytes) = AppManager::get_box_reference(&binary_data); - assert_eq!(app_id, 0); - assert_eq!(name_bytes, binary_data); - - // Test with empty data - let empty_data = vec![]; - let (app_id, name_bytes) = AppManager::get_box_reference(&empty_data); - assert_eq!(app_id, 0); - assert_eq!(name_bytes, empty_data); - - // Test that box identifiers can be constructed from different sources - - // From UTF-8 string - let string_box_id: BoxIdentifier = "my_box".as_bytes().to_vec(); - assert_eq!(string_box_id, b"my_box".to_vec()); - - // From hex data (representing binary data) - let hex_box_id: BoxIdentifier = vec![0xDE, 0xAD, 0xBE, 0xEF]; - assert_eq!(hex_box_id.len(), 4); - - // From base64-decoded data - let base64_str = "SGVsbG8gV29ybGQ="; // "Hello World" in base64 - let base64_box_id: BoxIdentifier = Base64.decode(base64_str).unwrap(); - assert_eq!(base64_box_id, b"Hello World".to_vec()); - - // Test that the box reference function works consistently - let (_, ref_bytes) = AppManager::get_box_reference(&string_box_id); - assert_eq!(ref_bytes, string_box_id); -} - -/// Test that app state keys and bytes are now Vec for TypeScript consistency -#[test] -fn test_app_state_keys_as_vec_u8() { - use algod_client::models::{TealKeyValue, TealValue}; - use base64::{Engine, engine::general_purpose::STANDARD as Base64}; - - // Create mock state data - let key_raw = b"test_key".to_vec(); - let key_base64 = Base64.encode(&key_raw); - - let state_val = TealKeyValue { - key: key_base64, - value: TealValue { - r#type: 2, // Uint type - bytes: Vec::new(), - uint: 42, - }, - }; - - let state = vec![state_val]; - - // Decode the app state - let result = AppManager::decode_app_state(&state).unwrap(); - - // Verify that the HashMap key is Vec - assert_eq!(result.len(), 1); - assert!(result.contains_key(&key_raw)); - - // Verify the actual data in AppState - let app_state = &result[&key_raw]; - match app_state { - AppState::Uint(uint_value) => { - assert_eq!(uint_value.key_raw, key_raw); - assert_eq!(uint_value.key_base64, Base64.encode(&key_raw)); - } - AppState::Bytes(_) => { - panic!("Expected AppState::Uint"); - } - } - - // Test with binary key data (non-UTF-8) - let binary_key = vec![0xFF, 0xFE, 0xFD, 0x00]; - let binary_key_base64 = Base64.encode(&binary_key); - - let binary_state_val = TealKeyValue { - key: binary_key_base64, - value: TealValue { - r#type: 2, // Uint type - bytes: Vec::new(), - uint: 123, - }, - }; - - let binary_state = vec![binary_state_val]; - let binary_result = AppManager::decode_app_state(&binary_state).unwrap(); - - // Verify binary key works correctly - assert!(binary_result.contains_key(&binary_key)); - let binary_app_state = &binary_result[&binary_key]; - match binary_app_state { - AppState::Uint(uint_app_state) => { - assert_eq!(uint_app_state.key_raw, binary_key); - assert_eq!(uint_app_state.value, 123); - } - AppState::Bytes(_) => { - panic!("Expected AppState::Uint"); - } - } - - // Test bytes value type with base64 deserialization - let bytes_key = b"bytes_key".to_vec(); - let bytes_key_base64 = Base64.encode(&bytes_key); - let bytes_value = b"Hello, World!".to_vec(); - - let bytes_state_val = TealKeyValue { - key: bytes_key_base64, - value: TealValue { - r#type: 1, // Bytes type - bytes: bytes_value.clone(), - uint: 0, - }, - }; - - let bytes_state = vec![bytes_state_val]; - let bytes_result = AppManager::decode_app_state(&bytes_state).unwrap(); - - // Verify bytes value handling - assert!(bytes_result.contains_key(&bytes_key)); - let bytes_app_state = &bytes_result[&bytes_key]; - match bytes_app_state { - AppState::Uint(_) => { - panic!("Expected AppState::Bytes"); - } - AppState::Bytes(value) => { - assert_eq!(value.key_raw, bytes_key); - assert_eq!(value.value_raw, bytes_value.clone()); - assert_eq!(value.value_base64, Base64.encode(&bytes_value)); - assert_eq!(value.value, "Hello, World!"); - } - } -} - -/// Test ABIType-based box value methods structure -#[test] -fn test_abi_type_box_value_methods() { - // This test demonstrates the ABIType-based box value approach: - // - // ABIType-based methods (get_box_value_from_abi_type, get_box_values_from_abi_type): - // - Take ABIType directly as parameter - // - Return ABIValue directly - // - Ideal for box data decoding based on actual storage format - - // Create a simple uint64 ABI type for testing - let uint64_type = ABIType::Uint(BitSize::new(64).unwrap()); - - // Verify the type can be created successfully - assert_eq!(format!("{}", uint64_type), "uint64"); - - // The actual network testing would be done in integration tests with real algod - // This unit test validates the correct approach for box data decoding - println!("ABIType approach for box data: Storage type -> ABIValue"); -} diff --git a/crates/algokit_utils/tests/clients/asset_manager.rs b/crates/algokit_utils/tests/clients/asset_manager.rs deleted file mode 100644 index 2cd3f8e91..000000000 --- a/crates/algokit_utils/tests/clients/asset_manager.rs +++ /dev/null @@ -1,422 +0,0 @@ -use algokit_transact::{Address, constants::MAX_TX_GROUP_SIZE}; -use algokit_utils::{ - clients::asset_manager::AssetManagerError, - transactions::{AssetCreateParams, AssetOptInParams}, -}; -use rstest::*; -use std::sync::Arc; - -use crate::common::{AlgorandFixture, AlgorandFixtureResult, TestResult, algorand_fixture}; - -/// Test asset information retrieval -#[rstest] -#[tokio::test] -async fn test_get_asset_by_id(#[future] algorand_fixture: AlgorandFixtureResult) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - - // Create test asset - let (asset_id, _) = create_test_asset_with_creator(&mut algorand_fixture).await?; - - // Test successful retrieval - let asset_manager = algorand_fixture.algorand_client.asset(); - let asset_info = asset_manager.get_by_id(asset_id).await?; - assert_eq!(asset_info.asset_id, asset_id); - assert_eq!(asset_info.total, 1000); - assert_eq!(asset_info.decimals, 0); - assert_eq!(asset_info.unit_name, Some("TEST".to_string())); - assert_eq!(asset_info.asset_name, Some("Test Asset".to_string())); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_get_asset_by_id_nonexistent( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let asset_manager = algorand_fixture.algorand_client.asset(); - - // Test non-existent asset - let error = asset_manager - .get_by_id(999_999_999) - .await - .expect_err("expected asset lookup to fail"); - assert!(matches!( - error, - AssetManagerError::AssetNotFound { - asset_id: 999_999_999 - } - )); - - Ok(()) -} - -/// Test account asset information retrieval -#[rstest] -#[tokio::test] -async fn test_get_account_information( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - - let (asset_id, creator_address) = create_test_asset_with_creator(&mut algorand_fixture).await?; - - // Test account information for asset creator (should be opted in by default) - let asset_manager = algorand_fixture.algorand_client.asset(); - let account_info = asset_manager - .get_account_information(&creator_address, asset_id) - .await?; - - let asset_holding = account_info - .asset_holding - .as_ref() - .expect("Creator should have asset holding"); - assert_eq!(asset_holding.asset_id, asset_id); - assert_eq!(asset_holding.amount, 1000); // Creator gets all initial supply - assert!(!asset_holding.is_frozen); - assert!(account_info.round > 0); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_get_account_information_not_opted_in( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - - let (asset_id, _) = create_test_asset_with_creator(&mut algorand_fixture).await?; - let test_account = algorand_fixture.generate_account(None).await?; - - let asset_manager = algorand_fixture.algorand_client.asset(); - - // Test account information for non-opted-in account should return error - let result = asset_manager - .get_account_information(&test_account.account().address(), asset_id) - .await; - - // For non-opted-in accounts, we should surface a dedicated NotOptedIn error - let error = result.expect_err("expected account asset lookup to fail"); - let expected_address = test_account.account().address().to_string(); - assert!(matches!( - error, - AssetManagerError::NotOptedIn { - ref address, - asset_id: err_asset_id - } if address == &expected_address && err_asset_id == asset_id - )); - - Ok(()) -} - -/// Helper function to create a test asset and return both asset ID and creator address -async fn create_test_asset_with_creator( - fixture: &mut AlgorandFixture, -) -> Result<(u64, Address), Box> { - let creator = fixture.generate_account(None).await?; - let creator_address = creator.account().address(); - - let params = AssetCreateParams { - sender: creator_address.clone(), - signer: Some(Arc::new(creator.clone())), - total: 1000, - decimals: Some(0), - unit_name: Some("TEST".to_string()), - asset_name: Some("Test Asset".to_string()), - ..Default::default() - }; - - let result = fixture - .algorand_client - .send() - .asset_create(params, None) - .await?; - - let asset_id = result.asset_id; - Ok((asset_id, creator_address)) -} - -/// Helper function to create multiple test assets -async fn create_multiple_test_assets( - fixture: &mut AlgorandFixture, - count: usize, -) -> Result, Box> { - let mut assets = Vec::new(); - for _ in 0..count { - assets.push(create_test_asset_with_creator(fixture).await?); - } - Ok(assets) -} - -/// Test bulk opt-in functionality -#[rstest] -#[tokio::test] -async fn test_bulk_opt_in_success(#[future] algorand_fixture: AlgorandFixtureResult) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - - // Create multiple test assets - let assets = create_multiple_test_assets(&mut algorand_fixture, 3).await?; - let asset_ids: Vec = assets.iter().map(|(id, _)| *id).collect(); - - // Create a test account that will opt into the assets - let opt_in_account = algorand_fixture.generate_account(None).await?; - let opt_in_address = opt_in_account.account().address(); - - // Perform bulk opt-in - let asset_manager = algorand_fixture.algorand_client.asset(); - let results = asset_manager - .bulk_opt_in(&opt_in_address, &asset_ids) - .await?; - - // Verify results - assert_eq!(results.len(), 3); - for (i, result) in results.iter().enumerate() { - assert_eq!(result.asset_id, asset_ids[i]); - assert!(!result.transaction_id.is_empty()); - } - - // Verify that account is now opted into all assets - for &asset_id in &asset_ids { - let account_info = asset_manager - .get_account_information(&opt_in_address, asset_id) - .await?; - let asset_holding = account_info - .asset_holding - .as_ref() - .expect("Account should be opted in"); - assert_eq!(asset_holding.asset_id, asset_id); - assert_eq!(asset_holding.amount, 0); // Should have zero balance after opt-in - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_bulk_opt_in_batches(#[future] algorand_fixture: AlgorandFixtureResult) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - let asset_count = MAX_TX_GROUP_SIZE + 4; - - let assets = create_multiple_test_assets(&mut algorand_fixture, asset_count).await?; - let asset_ids: Vec = assets.iter().map(|(id, _)| *id).collect(); - - let opt_in_account = algorand_fixture.generate_account(None).await?; - let opt_in_address = opt_in_account.account().address(); - - let asset_manager = algorand_fixture.algorand_client.asset(); - let results = asset_manager - .bulk_opt_in(&opt_in_address, &asset_ids) - .await?; - - assert_eq!(results.len(), asset_count); - for (expected, actual) in asset_ids.iter().zip(results.iter()) { - assert_eq!(expected, &actual.asset_id); - } - - Ok(()) -} - -/// Test bulk opt-in with empty asset list -#[rstest] -#[tokio::test] -async fn test_bulk_opt_in_empty_list( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - - let test_account = algorand_fixture.generate_account(None).await?; - let test_address = test_account.account().address(); - - let asset_manager = algorand_fixture.algorand_client.asset(); - let results = asset_manager.bulk_opt_in(&test_address, &[]).await?; - - assert!(results.is_empty()); - Ok(()) -} - -/// Test bulk opt-out functionality -#[rstest] -#[tokio::test] -async fn test_bulk_opt_out_success( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - - // Create test assets - let assets = create_multiple_test_assets(&mut algorand_fixture, 2).await?; - let asset_ids: Vec = assets.iter().map(|(id, _)| *id).collect(); - - // Create and opt-in an account - let test_account = algorand_fixture.generate_account(None).await?; - let test_address = test_account.account().address(); - - let asset_manager = algorand_fixture.algorand_client.asset(); - - // First, opt into the assets individually using the Composer - let mut composer = algorand_fixture.algorand_client.new_composer(None); - - for &asset_id in &asset_ids { - let opt_in_params = AssetOptInParams { - sender: test_address.clone(), - signer: Some(Arc::new(test_account.clone())), - asset_id, - ..Default::default() - }; - composer.add_asset_opt_in(opt_in_params)?; - } - - composer.send(Default::default()).await?; - - // Verify accounts are opted in - for &asset_id in &asset_ids { - let account_info = asset_manager - .get_account_information(&test_address, asset_id) - .await?; - let asset_holding = account_info - .asset_holding - .as_ref() - .expect("Account should be opted in"); - assert_eq!(asset_holding.amount, 0); // Should be zero balance - } - - // Now perform bulk opt-out - let results = asset_manager - .bulk_opt_out(&test_address, &asset_ids, Some(true)) - .await?; - - // Verify results - assert_eq!(results.len(), 2); - for (i, result) in results.iter().enumerate() { - assert_eq!(result.asset_id, asset_ids[i]); - assert!(!result.transaction_id.is_empty()); - } - - // Verify that account is no longer opted into the assets - for &asset_id in &asset_ids { - let result = asset_manager - .get_account_information(&test_address, asset_id) - .await; - // Should get an error because the account is no longer opted in - assert!(result.is_err()); - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_bulk_opt_out_batches( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - let asset_count = MAX_TX_GROUP_SIZE + 2; - - let assets = create_multiple_test_assets(&mut algorand_fixture, asset_count).await?; - let asset_ids: Vec = assets.iter().map(|(id, _)| *id).collect(); - - let test_account = algorand_fixture.generate_account(None).await?; - let test_address = test_account.account().address(); - - let asset_manager = algorand_fixture.algorand_client.asset(); - - asset_manager.bulk_opt_in(&test_address, &asset_ids).await?; - - let results = asset_manager - .bulk_opt_out(&test_address, &asset_ids, None) - .await?; - - assert_eq!(results.len(), asset_count); - for (expected, actual) in asset_ids.iter().zip(results.iter()) { - assert_eq!(expected, &actual.asset_id); - } - - Ok(()) -} - -/// Test bulk opt-out with empty list -#[rstest] -#[tokio::test] -async fn test_bulk_opt_out_empty_list( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - - let test_account = algorand_fixture.generate_account(None).await?; - let test_address = test_account.account().address(); - - let asset_manager = algorand_fixture.algorand_client.asset(); - let results = asset_manager.bulk_opt_out(&test_address, &[], None).await?; - - assert!(results.is_empty()); - Ok(()) -} - -/// Test bulk opt-out with non-zero balance (should fail) -#[rstest] -#[tokio::test] -async fn test_bulk_opt_out_non_zero_balance( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - - // Create a test asset - let (asset_id, creator_address) = create_test_asset_with_creator(&mut algorand_fixture).await?; - - let asset_manager = algorand_fixture.algorand_client.asset(); - - // The creator account has the entire supply (1000), so it has non-zero balance - let result = asset_manager - .bulk_opt_out(&creator_address, &[asset_id], Some(true)) - .await; - - // Should fail due to non-zero balance - assert!(result.is_err()); - assert!(matches!( - result.unwrap_err(), - AssetManagerError::NonZeroBalance { .. } - )); - - Ok(()) -} - -/// Test bulk opt-out without balance check -#[rstest] -#[tokio::test] -async fn test_bulk_opt_out_without_balance_check( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - - // Create a test asset - let (asset_id, _) = create_test_asset_with_creator(&mut algorand_fixture).await?; - - // Create a test account and opt it in - let test_account = algorand_fixture.generate_account(None).await?; - let test_address = test_account.account().address(); - - let asset_manager = algorand_fixture.algorand_client.asset(); - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - - let opt_in_params = AssetOptInParams { - sender: test_address.clone(), - signer: Some(Arc::new(test_account.clone())), - asset_id, - ..Default::default() - }; - composer.add_asset_opt_in(opt_in_params)?; - composer.send(Default::default()).await?; - - // Opt out without balance check (ensure_zero_balance = false) - let results = asset_manager - .bulk_opt_out(&test_address, &[asset_id], Some(false)) - .await?; - - assert_eq!(results.len(), 1); - assert_eq!(results[0].asset_id, asset_id); - assert!(!results[0].transaction_id.is_empty()); - - Ok(()) -} diff --git a/crates/algokit_utils/tests/clients/client_manager.rs b/crates/algokit_utils/tests/clients/client_manager.rs deleted file mode 100644 index b8e687ec5..000000000 --- a/crates/algokit_utils/tests/clients/client_manager.rs +++ /dev/null @@ -1,113 +0,0 @@ -use algokit_utils::ClientManager; -use std::{collections::HashSet, sync::Arc, time::Duration}; -use tokio::time::timeout; - -use crate::common::logging::init_test_logging; - -#[tokio::test] -async fn test_network_caching_with_localnet() { - init_test_logging(); - - let config = ClientManager::get_config_from_environment_or_localnet(); - let manager = ClientManager::new(&config).unwrap(); - - let first = manager.network().await.unwrap(); - let second = manager.network().await.unwrap(); - let third = manager.network().await.unwrap(); - - // All should be the same Arc instance - assert!(Arc::ptr_eq(&first, &second)); - assert!(Arc::ptr_eq(&second, &third)); - - // Content validation - assert!(!first.genesis_id.is_empty()); - assert!(!first.genesis_hash.is_empty()); - assert!(first.is_localnet); -} - -#[tokio::test] -async fn test_concurrent_network_calls() { - init_test_logging(); - - let config = ClientManager::get_config_from_environment_or_localnet(); - let manager = Arc::new(ClientManager::new(&config).unwrap()); - - // Spawn 10 concurrent tasks - let tasks: Vec<_> = (0..10) - .map(|i| { - let manager = Arc::clone(&manager); - tokio::spawn(async move { - // Add timing variation - if i % 2 == 0 { - tokio::time::sleep(Duration::from_millis(i * 5)).await; - } - manager.network().await - }) - }) - .collect(); - - let results: Vec<_> = timeout(Duration::from_secs(30), async { - futures::future::try_join_all(tasks).await.unwrap() - }) - .await - .expect("Concurrent calls should complete within timeout"); - - let successful: Vec<_> = results.into_iter().filter_map(|r| r.ok()).collect(); - assert!(!successful.is_empty(), "At least some calls should succeed"); - - // All successful results should be the same Arc instance - let unique_ptrs: HashSet<_> = successful.iter().map(Arc::as_ptr).collect(); - assert_eq!( - unique_ptrs.len(), - 1, - "All calls should return same cached instance" - ); -} - -#[tokio::test] -async fn test_convenience_methods_with_cache() { - init_test_logging(); - - let config = ClientManager::get_config_from_environment_or_localnet(); - let manager = ClientManager::new(&config).unwrap(); - - let network_details = manager.network().await.unwrap(); - - // Call convenience methods - let is_localnet = manager.is_localnet().await.unwrap(); - let is_testnet = manager.is_testnet().await.unwrap(); - let is_mainnet = manager.is_mainnet().await.unwrap(); - - // Should match cached values - assert_eq!(is_localnet, network_details.is_localnet); - assert_eq!(is_testnet, network_details.is_testnet); - assert_eq!(is_mainnet, network_details.is_mainnet); - - // Verify cache consistency - let cached = manager.network().await.unwrap(); - assert!(Arc::ptr_eq(&network_details, &cached)); -} - -#[tokio::test] -async fn test_network_details_localnet() { - init_test_logging(); - - let config = ClientManager::get_config_from_environment_or_localnet(); - let manager = ClientManager::new(&config).unwrap(); - - let details = manager.network().await.unwrap(); - - // Verify structure - assert!(!details.genesis_id.is_empty()); - assert!(!details.genesis_hash.is_empty()); - - // Verify exactly one network type is detected - let network_flags = [details.is_localnet, details.is_testnet, details.is_mainnet]; - assert_eq!(network_flags.iter().filter(|&&x| x).count(), 1); - - // Should detect as localnet - assert!( - details.is_localnet, - "Should detect localnet for local config" - ); -} diff --git a/crates/algokit_utils/tests/clients/mod.rs b/crates/algokit_utils/tests/clients/mod.rs deleted file mode 100644 index 76551a53b..000000000 --- a/crates/algokit_utils/tests/clients/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod algorand_client; -pub mod app_manager; -pub mod asset_manager; -pub mod client_manager; diff --git a/crates/algokit_utils/tests/clients_tests.rs b/crates/algokit_utils/tests/clients_tests.rs deleted file mode 100644 index 0f2d3cd3c..000000000 --- a/crates/algokit_utils/tests/clients_tests.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod clients; -mod common; diff --git a/crates/algokit_utils/tests/common/app_fixture.rs b/crates/algokit_utils/tests/common/app_fixture.rs deleted file mode 100644 index f332a1025..000000000 --- a/crates/algokit_utils/tests/common/app_fixture.rs +++ /dev/null @@ -1,214 +0,0 @@ -use crate::common::{ - AlgorandFixture, AlgorandFixtureResult, algorand_fixture, deploy_arc56_contract, -}; -use algokit_abi::Arc56Contract; -use algokit_transact::Address; -use algokit_utils::AlgorandClient; -use algokit_utils::ResourcePopulation; -use algokit_utils::applications::app_client::{AppClient, AppClientParams}; -use algokit_utils::clients::app_manager::{ - DeploymentMetadata, TealTemplateParams, TealTemplateValue, -}; -use algokit_utils::transactions::TransactionComposerConfig; -use rstest::fixture; -use std::sync::Arc; - -pub struct AppFixture { - pub algorand_fixture: AlgorandFixture, - pub sender_address: Address, - pub app_id: u64, - pub app_spec: Arc56Contract, - pub client: AppClient, -} - -pub type AppFixtureResult = Result>; - -#[derive(Default)] -pub struct AppFixtureOptions { - pub template_params: Option, - pub deploy_metadata: Option, - pub args: Option>>, - pub transaction_composer_config: Option, - pub default_sender_override: Option, - pub app_name: Option, -} - -pub async fn build_app_fixture( - fixture: AlgorandFixture, - spec: Arc56Contract, - opts: AppFixtureOptions, -) -> AppFixtureResult { - let sender = fixture.test_account.account().address(); - - let app_id = deploy_arc56_contract( - &fixture, - &sender, - &spec, - opts.template_params.clone(), - opts.deploy_metadata.clone(), - opts.args.clone(), - ) - .await?; - - let mut algorand = AlgorandClient::default_localnet(None); - algorand.set_signer(sender.clone(), Arc::new(fixture.test_account.clone())); - let client = AppClient::new(AppClientParams { - app_id, - app_spec: spec.clone(), - algorand: algorand.into(), - app_name: opts.app_name.clone(), - default_sender: Some( - opts.default_sender_override - .unwrap_or_else(|| sender.to_string()), - ), - default_signer: None, - source_maps: None, - transaction_composer_config: opts.transaction_composer_config, - }); - - Ok(AppFixture { - algorand_fixture: fixture, - sender_address: sender, - app_id, - app_spec: spec, - client, - }) -} - -pub fn default_teal_params(value: u64, updatable: bool, deletable: bool) -> TealTemplateParams { - let mut t = TealTemplateParams::default(); - t.insert("VALUE".to_string(), TealTemplateValue::Int(value)); - t.insert( - "UPDATABLE".to_string(), - TealTemplateValue::Int(if updatable { 1 } else { 0 }), - ); - t.insert( - "DELETABLE".to_string(), - TealTemplateValue::Int(if deletable { 1 } else { 0 }), - ); - t -} - -// ARC56 contract specs for test apps -pub fn testing_app_spec() -> Arc56Contract { - Arc56Contract::from_json(algokit_test_artifacts::testing_app::APPLICATION_ARC56).unwrap() -} - -pub fn nested_contract_spec() -> Arc56Contract { - Arc56Contract::from_json(algokit_test_artifacts::nested_contract::APPLICATION_ARC56).unwrap() -} - -pub fn sandbox_spec() -> Arc56Contract { - Arc56Contract::from_json(algokit_test_artifacts::sandbox::APPLICATION_ARC56).unwrap() -} - -pub fn hello_world_spec() -> Arc56Contract { - Arc56Contract::from_json(algokit_test_artifacts::hello_world::APPLICATION_ARC56).unwrap() -} - -pub fn boxmap_spec() -> Arc56Contract { - Arc56Contract::from_json(algokit_test_artifacts::box_map_test::APPLICATION_ARC56).unwrap() -} - -pub fn testing_app_puya_spec() -> Arc56Contract { - Arc56Contract::from_json(algokit_test_artifacts::testing_app_puya::APPLICATION_ARC56).unwrap() -} - -// Common fixtures for app_client tests -#[fixture] -pub async fn testing_app_fixture( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> AppFixtureResult { - let f = algorand_fixture.await?; - let spec = testing_app_spec(); - build_app_fixture( - f, - spec, - AppFixtureOptions { - template_params: Some(default_teal_params(0, false, false)), - ..Default::default() - }, - ) - .await -} - -#[fixture] -pub async fn nested_contract_fixture( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> AppFixtureResult { - let f = algorand_fixture.await?; - let spec = nested_contract_spec(); - build_app_fixture( - f, - spec, - AppFixtureOptions { - args: Some(vec![vec![184u8, 68u8, 123u8, 54u8]]), - transaction_composer_config: Some(TransactionComposerConfig { - populate_app_call_resources: ResourcePopulation::Enabled { - use_access_list: false, - }, - ..Default::default() - }), - ..Default::default() - }, - ) - .await -} - -#[fixture] -pub async fn sandbox_app_fixture( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> AppFixtureResult { - let f = algorand_fixture.await?; - let spec = sandbox_spec(); - build_app_fixture( - f, - spec, - AppFixtureOptions { - template_params: Some(default_teal_params(0, false, false)), - ..Default::default() - }, - ) - .await -} - -#[fixture] -pub async fn hello_world_app_fixture( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> AppFixtureResult { - let f = algorand_fixture.await?; - let spec = hello_world_spec(); - build_app_fixture(f, spec, AppFixtureOptions::default()).await -} - -#[fixture] -pub async fn boxmap_app_fixture( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> AppFixtureResult { - let f = algorand_fixture.await?; - let spec = boxmap_spec(); - build_app_fixture( - f, - spec, - AppFixtureOptions { - args: Some(vec![vec![184u8, 68u8, 123u8, 54u8]]), - transaction_composer_config: Some(TransactionComposerConfig { - populate_app_call_resources: ResourcePopulation::Enabled { - use_access_list: false, - }, - ..Default::default() - }), - ..Default::default() - }, - ) - .await -} - -#[fixture] -pub async fn testing_app_puya_fixture( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> AppFixtureResult { - let f = algorand_fixture.await?; - let spec = testing_app_puya_spec(); - build_app_fixture(f, spec, AppFixtureOptions::default()).await -} diff --git a/crates/algokit_utils/tests/common/fixture.rs b/crates/algokit_utils/tests/common/fixture.rs deleted file mode 100644 index 3e215d5ea..000000000 --- a/crates/algokit_utils/tests/common/fixture.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::sync::Arc; - -use crate::common::LocalNetDispenser; -use crate::common::logging::init_test_logging; - -use super::indexer_helpers::wait_for_indexer_transaction; -use super::test_account::{NetworkType, TestAccount, TestAccountConfig}; -use algod_client::AlgodClient; -use algokit_transact::Transaction; -use algokit_utils::clients::algorand_client::AlgorandClientParams; -use algokit_utils::transactions::TransactionComposerConfig; -use algokit_utils::{AlgoConfig, AlgorandClient, ClientManager}; -use indexer_client::IndexerClient; -use kmd_client::KmdClient; -use rstest::*; - -pub struct AlgorandFixture { - pub algod: Arc, - pub indexer: Arc, - pub kmd: Arc, - pub algorand_client: AlgorandClient, - pub test_account: TestAccount, -} - -pub type AlgorandFixtureResult = Result>; - -#[derive(Debug)] -pub struct TransactionResult { - pub transaction: Transaction, - pub tx_id: String, - pub signed_bytes: Vec, -} - -impl AlgorandFixture { - pub async fn new( - params: &AlgorandClientParams, - ) -> Result> { - let algod = - Arc::new(ClientManager::get_algod_client(¶ms.client_config.algod_config).unwrap()); - let indexer = Arc::new( - ClientManager::get_indexer_client( - ¶ms.client_config.indexer_config.clone().unwrap(), - ) - .unwrap(), - ); - let kmd = Arc::new( - ClientManager::get_kmd_client( - params - .client_config - .kmd_config - .as_ref() - .expect("KMD config required for localnet tests"), - ) - .unwrap(), - ); - - let mut algorand_client = AlgorandClient::new(params); - - let test_account = Self::generate_account_internal( - algod.clone(), - kmd.clone(), - &mut algorand_client, - Some(TestAccountConfig { - initial_funds: 10_000_000, - network_type: NetworkType::LocalNet, - funding_note: Some("AlgorandFixture test account".to_string()), - }), - ) - .await - .map_err(|e| format!("Failed to create test account: {}", e))?; - - Ok(Self { - algod, - indexer, - kmd, - algorand_client, - test_account, - }) - } - - async fn generate_account_internal( - algod: Arc, - kmd: Arc, - algorand_client: &mut AlgorandClient, - config: Option, - ) -> Result> { - let config = config.unwrap_or_default(); - let mut dispenser = LocalNetDispenser::new(algod.clone(), kmd.clone()); - - // Generate new account using ed25519_dalek - let test_account = TestAccount::generate()?; - let test_account_address = test_account.account().address(); - - // Fund the account based on network type - match config.network_type { - NetworkType::LocalNet => { - dispenser - .fund_account(&test_account_address.to_string(), config.initial_funds) - .await?; - } - NetworkType::TestNet => { - return Err(format!( - "⚠ TestNet funding not yet implemented. Please fund manually: {}", - test_account_address - ) - .into()); - } - NetworkType::MainNet => { - return Err(format!( - "⚠ MainNet detected. Account generated but not funded: {}", - test_account_address - ) - .into()); - } - } - - algorand_client.set_signer(test_account_address, Arc::new(test_account.clone())); - Ok(test_account) - } - - pub async fn generate_account( - &mut self, - config: Option, - ) -> Result> { - Self::generate_account_internal( - self.algod.clone(), - self.kmd.clone(), - &mut self.algorand_client, - config, - ) - .await - } -} - -impl AlgorandFixture { - /// Waits for a transaction to appear in the indexer - pub async fn wait_for_indexer_transaction( - &self, - transaction_id: &str, - ) -> Result<(), Box> { - wait_for_indexer_transaction(&self.indexer, transaction_id, None).await?; - Ok(()) - } -} - -#[fixture] -pub async fn algorand_fixture( - #[default(None)] composer_config: Option, -) -> AlgorandFixtureResult { - let client_config = ClientManager::get_config_from_environment_or_localnet(); - AlgorandFixture::new(&AlgorandClientParams { - client_config, - composer_config, - }) - .await -} diff --git a/crates/algokit_utils/tests/common/indexer_helpers.rs b/crates/algokit_utils/tests/common/indexer_helpers.rs deleted file mode 100644 index f52fae2c5..000000000 --- a/crates/algokit_utils/tests/common/indexer_helpers.rs +++ /dev/null @@ -1,206 +0,0 @@ -use indexer_client::{IndexerClient, apis::Error as IndexerError}; -use snafu::Snafu; -use std::future::Future; -use std::time::Duration; -use tokio::time::sleep; - -/// Configuration for indexer wait operations -#[derive(Debug, Clone)] -pub struct IndexerWaitConfig { - /// Maximum number of retry attempts (default: 100) - pub max_attempts: u32, - /// Delay between retry attempts (default: 200ms) - pub retry_delay: Duration, -} - -impl Default for IndexerWaitConfig { - fn default() -> Self { - Self { - max_attempts: 100, - retry_delay: Duration::from_millis(200), - } - } -} - -/// Error types for indexer wait operations -#[derive(Debug, Snafu)] -pub enum IndexerWaitError { - #[snafu(display("Indexer operation failed after {attempts} attempts: {last_error}"))] - MaxAttemptsExceeded { attempts: u32, last_error: String }, - #[snafu(display("Indexer client error: {message}"))] - ClientError { message: String }, - #[snafu(display("Transaction {tx_id} not found after {attempts} attempts"))] - TransactionNotFound { tx_id: String, attempts: u32 }, -} - -/// Runs the given indexer operation until it succeeds or max attempts are reached. -pub async fn wait_for_indexer( - operation: F, - config: Option, -) -> Result -where - F: Fn() -> Fut, - Fut: Future>, - E: std::fmt::Debug, -{ - let config = config.unwrap_or_default(); - let mut last_error = String::new(); - - for attempt in 1..=config.max_attempts { - match operation().await { - Ok(result) => return Ok(result), - Err(err) => { - last_error = format!("{:?}", err); - - // Check if this looks like a 404 error (indexer hasn't caught up) - let is_not_found = last_error.contains("404") - || last_error.contains("not found") - || last_error.contains("NotFound"); - - // If it's not a 404-like error, fail immediately - if !is_not_found { - return Err(IndexerWaitError::ClientError { - message: last_error, - }); - } - - // If we've reached max attempts, break out of the loop - if attempt >= config.max_attempts { - break; - } - - // Wait before next attempt - sleep(config.retry_delay).await; - } - } - } - - Err(IndexerWaitError::MaxAttemptsExceeded { - attempts: config.max_attempts, - last_error, - }) -} - -/// Waits for a specific transaction to appear in the indexer. -pub async fn wait_for_indexer_transaction( - indexer_client: &IndexerClient, - tx_id: &str, - config: Option, -) -> Result<(), IndexerWaitError> { - let config = config.unwrap_or_default(); - let tx_id = tx_id.to_string(); - - wait_for_indexer( - || { - let client = indexer_client.clone(); - let tx_id = tx_id.clone(); - - Box::pin(async move { - client - .search_for_transactions( - None, - None, - None, - None, - None, - None, - Some(&tx_id), - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - ) - .await - .and_then(|response| { - if response.transactions.is_empty() { - // Return a string error that will be treated as "not found" - Err(IndexerError::Serde { - message: "Transaction not found".to_string(), - }) - } else { - Ok(()) - } - }) - }) - }, - Some(config.clone()), - ) - .await - .map_err(|err| match err { - IndexerWaitError::MaxAttemptsExceeded { attempts, .. } => { - IndexerWaitError::TransactionNotFound { tx_id, attempts } - } - other => other, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn succeeds_immediately() { - let result = wait_for_indexer(|| async { Ok::<(), String>(()) }, None).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn retries_until_success() { - use std::sync::Arc; - use std::sync::atomic::{AtomicU32, Ordering}; - - let config = IndexerWaitConfig { - max_attempts: 5, - retry_delay: Duration::from_millis(1), - }; - - let attempts = Arc::new(AtomicU32::new(0)); - let attempts_clone = attempts.clone(); - - let result = wait_for_indexer( - move || { - let count = attempts_clone.fetch_add(1, Ordering::SeqCst); - async move { if count < 2 { Err("not found") } else { Ok(()) } } - }, - Some(config), - ) - .await; - - assert!(result.is_ok()); - } - - #[tokio::test] - async fn fails_after_max_attempts() { - let config = IndexerWaitConfig { - max_attempts: 2, - retry_delay: Duration::from_millis(1), - }; - - let result = - wait_for_indexer(|| async { Err::<(), &str>("not found") }, Some(config)).await; - - assert!(matches!( - result, - Err(IndexerWaitError::MaxAttemptsExceeded { .. }) - )); - } - - #[tokio::test] - async fn fails_immediately_on_non_retriable_error() { - let result = wait_for_indexer(|| async { Err::<(), &str>("server error") }, None).await; - - assert!(matches!( - result, - Err(IndexerWaitError::ClientError { message: _ }) - )); - } -} diff --git a/crates/algokit_utils/tests/common/local_net_dispenser.rs b/crates/algokit_utils/tests/common/local_net_dispenser.rs deleted file mode 100644 index 7ef03aa9b..000000000 --- a/crates/algokit_utils/tests/common/local_net_dispenser.rs +++ /dev/null @@ -1,241 +0,0 @@ -use std::{env, sync::Arc}; - -use algod_client::AlgodClient; -use algokit_transact::{ - AlgorandMsgpack, PaymentTransactionBuilder, Transaction, TransactionHeaderBuilder, -}; -use algokit_utils::TransactionSigner; -use kmd_client::KmdClient; -use kmd_client::models::{ - ExportKeyRequest, GenerateKeyRequest, InitWalletHandleTokenRequest, ListKeysRequest, - ReleaseWalletHandleTokenRequest, -}; - -use crate::common::TestAccount; - -/// LocalNet dispenser for funding test accounts using KMD -pub struct LocalNetDispenser { - client: Arc, - kmd_client: Arc, - kmd_wallet_name: String, - dispenser_account: Option, -} - -impl LocalNetDispenser { - /// Create a new LocalNet dispenser - pub fn new(client: Arc, kmd_client: Arc) -> Self { - let kmd_wallet_name = env::var("KMD_WALLET_NAME") - .unwrap_or_else(|_| "unencrypted-default-wallet".to_string()); - - Self { - client, - kmd_client, - kmd_wallet_name, - dispenser_account: None, - } - } - - /// Get the LocalNet dispenser account using KMD - 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_kmd().await?); - } - - Ok(self.dispenser_account.as_ref().unwrap()) - } - - /// Fetch the dispenser account using KMD - async fn fetch_dispenser_from_kmd( - &self, - ) -> Result> { - let wallets_response = self - .kmd_client - .list_wallets() - .await - .map_err(|e| format!("Failed to list KMD wallets: {:?}", e))?; - - let wallets = wallets_response.wallets.unwrap_or_default(); - - let selected_wallet = wallets - .iter() - .find(|wallet| wallet.name.as_deref() == Some(self.kmd_wallet_name.as_str())) - .or_else(|| wallets.first()) - .ok_or("No wallets available via KMD")?; - - let wallet_id = selected_wallet - .id - .clone() - .ok_or("KMD wallet is missing an id")?; - - let init_response = self - .kmd_client - .init_wallet_handle_token(InitWalletHandleTokenRequest { - wallet_id: Some(wallet_id.clone()), - wallet_password: None, - }) - .await - .map_err(|e| format!("Failed to initialize KMD wallet handle: {:?}", e))?; - - let wallet_handle_token = init_response - .wallet_handle_token - .ok_or("KMD did not return a wallet handle token")?; - - let release_request = ReleaseWalletHandleTokenRequest { - wallet_handle_token: Some(wallet_handle_token.clone()), - }; - - let result = async { - let mut addresses = self - .kmd_client - .list_keys_in_wallet(ListKeysRequest { - wallet_handle_token: Some(wallet_handle_token.clone()), - }) - .await - .map_err(|e| format!("Failed to list keys in KMD wallet: {:?}", e))? - .addresses - .unwrap_or_default(); - - if addresses.is_empty() { - let generated = self - .kmd_client - .generate_key(GenerateKeyRequest { - display_mnemonic: Some(false), - wallet_handle_token: Some(wallet_handle_token.clone()), - }) - .await - .map_err(|e| format!("Failed to generate key in KMD wallet: {:?}", e))?; - - if let Some(address) = generated.address { - addresses.push(address); - } - } - - if addresses.is_empty() { - return Err("KMD wallet does not contain any keys".into()); - } - - let mut best_address = None; - let mut highest_balance = 0u64; - - for address in &addresses { - match self.client.account_information(address, None).await { - Ok(info) => { - if info.amount > highest_balance { - highest_balance = info.amount; - best_address = Some(address.clone()); - } - } - Err(err) => { - println!( - "Warning: failed to fetch balance for {}: {:?}", - address, err - ); - } - } - } - - let dispenser_address = best_address.unwrap_or_else(|| addresses[0].clone()); - - let export_response = self - .kmd_client - .export_key(ExportKeyRequest { - address: Some(dispenser_address.clone()), - wallet_handle_token: Some(wallet_handle_token.clone()), - wallet_password: None, - }) - .await - .map_err(|e| format!("Failed to export dispenser key via KMD: {:?}", e))?; - - let private_key = export_response - .private_key - .ok_or("KMD export did not return a private key")?; - - let dispenser = TestAccount::from_secret_key(&private_key)?; - - println!( - "Found LocalNet dispenser account: {} with {} microALGOs", - dispenser_address, highest_balance - ); - - Ok(dispenser) - } - .await; - - if let Err(err) = self - .kmd_client - .release_wallet_handle_token(release_request) - .await - { - println!( - "Warning: failed to release KMD wallet handle token: {:?}", - err - ); - } - - result - } - - /// 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 = recipient_address.parse()?; - - // 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.account().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_transaction = dispenser.sign_transaction(&transaction).await?; - let signed_bytes = signed_transaction - .encode() - .map_err(|e| format!("Failed to encode signed transaction: {:?}", e))?; - - // 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) - } -} diff --git a/crates/algokit_utils/tests/common/logging.rs b/crates/algokit_utils/tests/common/logging.rs deleted file mode 100644 index e2c4ed99b..000000000 --- a/crates/algokit_utils/tests/common/logging.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::sync::Once; - -static INIT: Once = Once::new(); - -/// Initialize logging for tests. Call this once at the start of any test that needs logging. -/// Safe to call multiple times - will only initialize once across the entire test suite. -pub fn init_test_logging() { - INIT.call_once(|| { - let _ = env_logger::builder() - .is_test(true) - .filter_level(log::LevelFilter::Debug) - .format_target(true) // Include target in output for better debugging - .format_module_path(false) // Keep output cleaner - .try_init(); - }); -} diff --git a/crates/algokit_utils/tests/common/mnemonic.rs b/crates/algokit_utils/tests/common/mnemonic.rs deleted file mode 100644 index 95756a074..000000000 --- a/crates/algokit_utils/tests/common/mnemonic.rs +++ /dev/null @@ -1,368 +0,0 @@ -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 = " "; - -// NOTE: May need to be moved to different location if we are to expose mnemonic to pkey functionality to end user -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 - }; -} - -#[allow(clippy::enum_variant_names)] -/// 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/algokit_utils/tests/common/mod.rs b/crates/algokit_utils/tests/common/mod.rs deleted file mode 100644 index 10d2e10e8..000000000 --- a/crates/algokit_utils/tests/common/mod.rs +++ /dev/null @@ -1,98 +0,0 @@ -#![allow(dead_code)] -#![allow(unused_imports)] -pub mod app_fixture; -pub mod fixture; -pub mod indexer_helpers; -pub mod local_net_dispenser; -pub mod logging; -pub mod mnemonic; -pub mod test_account; - -use algokit_abi::Arc56Contract; -use algokit_utils::AppCreateParams; -use algokit_utils::applications::app_factory; -use algokit_utils::clients::app_manager::{ - AppManager, DeploymentMetadata, TealTemplateParams, TealTemplateValue, -}; -use algokit_utils::config::{AppCompiledEventData, Config, EventData, EventType}; -use base64::prelude::*; - -pub use app_fixture::{ - AppFixture, AppFixtureOptions, AppFixtureResult, boxmap_app_fixture, boxmap_spec, - build_app_fixture, default_teal_params, hello_world_app_fixture, hello_world_spec, - nested_contract_fixture, sandbox_app_fixture, sandbox_spec, testing_app_fixture, - testing_app_puya_fixture, testing_app_puya_spec, testing_app_spec, -}; -pub use fixture::{AlgorandFixture, AlgorandFixtureResult, algorand_fixture}; -pub use indexer_helpers::{ - IndexerWaitConfig, IndexerWaitError, wait_for_indexer, wait_for_indexer_transaction, -}; -pub use local_net_dispenser::LocalNetDispenser; -pub use test_account::{NetworkType, TestAccount, TestAccountConfig}; - -pub type TestResult = Result<(), Box>; - -pub async fn deploy_arc56_contract( - fixture: &AlgorandFixture, - sender: &algokit_transact::Address, - arc56_contract: &Arc56Contract, - template_params: Option, - deploy_metadata: Option, - args: Option>>, -) -> Result> { - let teal_source = arc56_contract - .source - .clone() - .expect("No source found in app spec"); - - // Decode TEAL source (templates) - let approval_src_bytes = BASE64_STANDARD.decode(teal_source.approval)?; - let clear_src_bytes = BASE64_STANDARD.decode(teal_source.clear)?; - let approval_teal = String::from_utf8(approval_src_bytes)?; - let clear_teal = String::from_utf8(clear_src_bytes)?; - - // Compile via AppManager with substitution and source-map support - let app_manager = AppManager::new(fixture.algod.clone()); - let approval_compile = app_manager - .compile_teal_template( - &approval_teal, - template_params.as_ref(), - deploy_metadata.as_ref(), - ) - .await?; - let clear_compile = app_manager - .compile_teal_template( - &clear_teal, - template_params.as_ref(), - deploy_metadata.as_ref(), - ) - .await?; - - let app_create_params = AppCreateParams { - sender: sender.clone(), - args, - approval_program: approval_compile.compiled_base64_to_bytes, - clear_state_program: clear_compile.compiled_base64_to_bytes, - global_state_schema: Some(algokit_transact::StateSchema { - num_uints: arc56_contract.state.schema.global_state.ints, - num_byte_slices: arc56_contract.state.schema.global_state.bytes, - }), - local_state_schema: Some(algokit_transact::StateSchema { - num_uints: arc56_contract.state.schema.local_state.ints, - num_byte_slices: arc56_contract.state.schema.local_state.bytes, - }), - ..Default::default() - }; - - let mut composer = fixture.algorand_client.new_composer(None); - composer.add_app_create(app_create_params)?; - - let result = composer.send(None).await?; - - let app_id = result.results[0] - .confirmation - .app_id - .expect("No app ID returned"); - - Ok(app_id) -} diff --git a/crates/algokit_utils/tests/common/test_account.rs b/crates/algokit_utils/tests/common/test_account.rs deleted file mode 100644 index 94b13d453..000000000 --- a/crates/algokit_utils/tests/common/test_account.rs +++ /dev/null @@ -1,206 +0,0 @@ -use algokit_transact::{ - ALGORAND_SECRET_KEY_BYTE_LENGTH, ALGORAND_SIGNATURE_BYTE_LENGTH, AlgorandMsgpack, - KeyPairAccount, SignedTransaction, Transaction, -}; -use algokit_utils::TransactionSigner; -use async_trait::async_trait; -use ed25519_dalek::{Signer, SigningKey, VerifyingKey}; -use hex; -use rand::rngs::OsRng; - -use super::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, - /// 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 - network_type: NetworkType::LocalNet, - funding_note: None, - } - } -} - -#[allow(clippy::enum_variant_names)] -/// 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 secret key used for signing transactions - secret_key: [u8; ALGORAND_SECRET_KEY_BYTE_LENGTH], -} - -#[async_trait] -impl TransactionSigner for TestAccount { - async fn sign_transactions( - &self, - txns: &[Transaction], - indices: &[usize], - ) -> Result, String> { - let signing_key = SigningKey::from_bytes(&self.secret_key); - let verifying_key: VerifyingKey = (&signing_key).into(); - let signer_account = KeyPairAccount::from_pubkey(&verifying_key.to_bytes()); - let signer_address = signer_account.address(); - - indices - .iter() - .map(|&idx| { - if idx < txns.len() { - let tx = txns[idx].clone(); - let encoded_tx = tx - .encode() - .map_err(|e| format!("Failed to encode transaction: {:?}", e))?; - let sig: [u8; ALGORAND_SIGNATURE_BYTE_LENGTH] = - signing_key.sign(&encoded_tx).to_bytes(); - - let auth_address = if tx.header().sender != signer_address { - Some(signer_address.clone()) - } else { - None - }; - - Ok(SignedTransaction { - transaction: tx, - signature: Some(sig), - auth_address, - multisignature: None, - }) - } else { - Err(format!("Index {} out of bounds for transactions", idx)) - } - }) - .collect() - } -} - -impl TestAccount { - /// Generate a new random test account using ed25519_dalek - pub fn generate() -> Result> { - // Generate a random signing key - let signing_key = SigningKey::generate(&mut OsRng); - - Ok(Self { - secret_key: signing_key.to_bytes(), - }) - } - - /// 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 secret_key = - to_key(mnemonic_str).map_err(|e| format!("Failed to parse mnemonic: {}", e))?; - - Ok(Self { secret_key }) - } - - /// Create an account directly from a 64-byte secret key (private + public key material) - pub fn from_secret_key( - secret_key: &[u8], - ) -> Result> { - let key_slice = match secret_key.len() { - ALGORAND_SECRET_KEY_BYTE_LENGTH => secret_key, - len if len == ALGORAND_SECRET_KEY_BYTE_LENGTH * 2 => { - &secret_key[..ALGORAND_SECRET_KEY_BYTE_LENGTH] - } - other => { - return Err(format!( - "Secret key must be {} or {} bytes, got {}", - ALGORAND_SECRET_KEY_BYTE_LENGTH, - ALGORAND_SECRET_KEY_BYTE_LENGTH * 2, - other - ) - .into()); - } - }; - - let mut key_bytes = [0u8; ALGORAND_SECRET_KEY_BYTE_LENGTH]; - key_bytes.copy_from_slice(key_slice); - - Ok(Self { - secret_key: key_bytes, - }) - } - - /// Get the account's address using algokit_transact - pub fn account(&self) -> KeyPairAccount { - let signing_key = SigningKey::from_bytes(&self.secret_key); - let public_key: VerifyingKey = (&signing_key).into(); - KeyPairAccount::from_pubkey(&public_key.to_bytes()) - } - - /// Get the account's mnemonic (proper Algorand 25-word mnemonic) - pub fn mnemonic(&self) -> String { - from_key(&self.secret_key).unwrap_or_else(|_| { - // Fallback to hex for debugging if mnemonic generation fails - hex::encode(self.secret_key) - }) - } -} - -#[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.account(); - - 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!("Generated account address: {}", address); - println!("Generated account mnemonic: {}", mnemonic); - - assert_eq!(word_count, 25); - } - - #[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_account = original.account(); - let recovered_account = recovered.account(); - - assert_eq!(original_account.to_string(), recovered_account.to_string()); - assert_eq!(original.mnemonic(), recovered.mnemonic()); - - println!("✓ Successfully recovered account from mnemonic"); - println!(" Original: {}", original_account); - println!(" Recovered: {}", recovered_account); - } else { - println!("⚠ Skipping mnemonic round-trip test (using hex fallback)"); - } - } -} diff --git a/crates/algokit_utils/tests/indexer/mod.rs b/crates/algokit_utils/tests/indexer/mod.rs deleted file mode 100644 index 06c474f9f..000000000 --- a/crates/algokit_utils/tests/indexer/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod search_applications; -pub mod search_transactions; diff --git a/crates/algokit_utils/tests/indexer/search_applications.rs b/crates/algokit_utils/tests/indexer/search_applications.rs deleted file mode 100644 index 39b4238eb..000000000 --- a/crates/algokit_utils/tests/indexer/search_applications.rs +++ /dev/null @@ -1,114 +0,0 @@ -use algokit_http_client::DefaultHttpClient; -use algokit_transact::{OnApplicationComplete, StateSchema}; -use algokit_utils::{AppCreateParams, ClientManager}; -use indexer_client::{IndexerClient, apis::Error as IndexerError}; -use rstest::rstest; -use std::sync::Arc; - -use crate::common::{ - AlgorandFixture, TestResult, - fixture::{AlgorandFixtureResult, algorand_fixture}, - indexer_helpers::wait_for_indexer, - logging::init_test_logging, -}; - -const HELLO_WORLD_APPROVAL_PROGRAM: [u8; 18] = [ - 10, 128, 7, 72, 101, 108, 108, 111, 44, 32, 54, 26, 0, 80, 176, 129, 1, 67, -]; - -const HELLO_WORLD_CLEAR_STATE_PROGRAM: [u8; 4] = [10, 129, 1, 67]; - -async fn create_app(algorand_fixture: &AlgorandFixture) -> Option { - let sender = algorand_fixture.test_account.account().address(); - let params = AppCreateParams { - sender, - on_complete: OnApplicationComplete::NoOp, - approval_program: HELLO_WORLD_APPROVAL_PROGRAM.to_vec(), - clear_state_program: HELLO_WORLD_CLEAR_STATE_PROGRAM.to_vec(), - global_state_schema: Some(StateSchema { - num_uints: 1, - num_byte_slices: 1, - }), - local_state_schema: Some(StateSchema { - num_uints: 1, - num_byte_slices: 1, - }), - extra_program_pages: None, - args: Some(vec![b"Create".to_vec()]), - account_references: None, - app_references: None, - asset_references: None, - box_references: None, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_app_create(params).unwrap(); - let result = composer.send(None).await.unwrap(); - result.results[0].confirmation.app_id -} - -#[rstest] -#[tokio::test] -async fn finds_created_application( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let app_id = create_app(&algorand_fixture).await.unwrap(); - - let config = ClientManager::get_config_from_environment_or_localnet(); - let indexer_config = config.indexer_config.unwrap(); - let base_url = if let Some(port) = indexer_config.port { - format!("{}:{}", indexer_config.server, port) - } else { - indexer_config.server.clone() - }; - let indexer_client = IndexerClient::new(Arc::new(DefaultHttpClient::new(&base_url))); - - wait_for_indexer( - || { - let client = indexer_client.clone(); - Box::pin(async move { - client - .search_for_applications(Some(app_id), None, None, None, None) - .await - .and_then(|response| { - if response.applications.is_empty() { - Err(IndexerError::Serde { - message: "Application not found".to_string(), - }) - } else { - Ok(()) - } - }) - }) - }, - None, - ) - .await - .unwrap(); - - let response = indexer_client - .search_for_applications(Some(app_id), None, None, None, None) - .await - .unwrap(); - - assert!(!response.applications.is_empty()); - assert_eq!(response.applications[0].id, app_id); - - Ok(()) -} - -#[tokio::test] -async fn handles_invalid_indexer() { - init_test_logging(); - - let indexer_client = - IndexerClient::new(Arc::new(DefaultHttpClient::new("http://invalid-host:8980"))); - - let result = indexer_client - .search_for_applications(None, None, None, None, None) - .await; - - assert!(result.is_err()); -} diff --git a/crates/algokit_utils/tests/indexer/search_transactions.rs b/crates/algokit_utils/tests/indexer/search_transactions.rs deleted file mode 100644 index 869d8ea99..000000000 --- a/crates/algokit_utils/tests/indexer/search_transactions.rs +++ /dev/null @@ -1,106 +0,0 @@ -use algokit_http_client::DefaultHttpClient; -use algokit_transact::TransactionId; -use algokit_utils::{ClientManager, PaymentParams}; -use indexer_client::IndexerClient; -use rstest::rstest; -use std::sync::Arc; - -use crate::common::{ - TestResult, - fixture::{AlgorandFixtureResult, algorand_fixture}, - indexer_helpers::wait_for_indexer_transaction, - logging::init_test_logging, -}; - -#[rstest] -#[tokio::test] -async fn finds_sent_transaction(#[future] algorand_fixture: AlgorandFixtureResult) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - - let sender = algorand_fixture.test_account.account().address(); - let receiver = algorand_fixture.generate_account(None).await?; - - let payment_params = PaymentParams { - sender: sender.clone(), - receiver: receiver.account().address(), - amount: 500_000, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_payment(payment_params).unwrap(); - let result = composer.send(None).await.unwrap(); - let txid = result.results[0].confirmation.txn.id().unwrap(); - - let config = ClientManager::get_config_from_environment_or_localnet(); - let indexer_config = config.indexer_config.unwrap(); - let base_url = if let Some(port) = indexer_config.port { - format!("{}:{}", indexer_config.server, port) - } else { - indexer_config.server.clone() - }; - let indexer_client = IndexerClient::new(Arc::new(DefaultHttpClient::new(&base_url))); - - wait_for_indexer_transaction(&indexer_client, &txid, None) - .await - .unwrap(); - - let response = indexer_client - .search_for_transactions( - None, - None, - None, - None, - None, - None, - Some(&txid), - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - ) - .await - .unwrap(); - - assert!(!response.transactions.is_empty()); - let found_tx = &response.transactions[0]; - assert_eq!(found_tx.id, Some(txid)); - assert_eq!(found_tx.sender, sender.to_string()); - assert_eq!(found_tx.tx_type, "pay"); - - if let Some(payment_tx) = &found_tx.payment_transaction { - assert_eq!(payment_tx.amount, 500_000); - assert_eq!( - payment_tx.receiver, - receiver.account().address().to_string() - ); - } - - Ok(()) -} - -#[tokio::test] -async fn handles_invalid_indexer() { - init_test_logging(); - - let indexer_client = - IndexerClient::new(Arc::new(DefaultHttpClient::new("http://invalid-host:8980"))); - - let result = indexer_client - .search_for_transactions( - None, None, None, None, None, None, None, None, None, None, None, None, None, None, - None, None, None, None, None, None, - ) - .await; - - assert!(result.is_err()); -} diff --git a/crates/algokit_utils/tests/indexer_tests.rs b/crates/algokit_utils/tests/indexer_tests.rs deleted file mode 100644 index 2f32c28b6..000000000 --- a/crates/algokit_utils/tests/indexer_tests.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod common; -mod indexer; diff --git a/crates/algokit_utils/tests/kmd/key_management.rs b/crates/algokit_utils/tests/kmd/key_management.rs deleted file mode 100644 index 3b8f5d837..000000000 --- a/crates/algokit_utils/tests/kmd/key_management.rs +++ /dev/null @@ -1,101 +0,0 @@ -use kmd_client::apis::client::KmdClient; -use kmd_client::models::{ - CreateWalletRequest, GenerateKeyRequest, InitWalletHandleTokenRequest, ListKeysRequest, - ReleaseWalletHandleTokenRequest, -}; -use rand::{Rng, distributions::Alphanumeric}; - -// Wallet key management flow: create wallet -> init handle token -> list keys -> generate key -> list keys (increment) -> release token -#[tokio::test] -async fn key_management_flow() -> Result<(), Box> { - let client = KmdClient::localnet(); - - let wallet_name: String = format!( - "test_wallet_keys_{}", - rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(12) - .map(char::from) - .collect::() - .to_lowercase() - ); - - // Create wallet - let create_response = client - .create_wallet(CreateWalletRequest { - wallet_name: Some(wallet_name.clone()), - wallet_driver_name: Some("sqlite".to_string()), - wallet_password: Some("testpass".to_string()), - ..Default::default() - }) - .await - .map_err(|e| { - format!( - "Failed to create wallet (possible KMD token/availability issue): {}", - e - ) - })?; - - let created_wallet = create_response - .wallet - .as_ref() - .expect("Expected created wallet in response"); - - let wallet_id = created_wallet - .id - .as_ref() - .expect("Wallet id should be present"); - assert_eq!(created_wallet.name.as_deref(), Some(wallet_name.as_str())); - - // Init wallet handle token - let init_response = client - .init_wallet_handle_token(InitWalletHandleTokenRequest { - wallet_id: Some(wallet_id.clone()), - wallet_password: Some("testpass".to_string()), - }) - .await?; - let wallet_handle_token = init_response - .wallet_handle_token - .as_ref() - .expect("Wallet handle token should be present") - .clone(); - - // Baseline keys list - let list_before = client - .list_keys_in_wallet(ListKeysRequest { - wallet_handle_token: Some(wallet_handle_token.clone()), - }) - .await?; - let before_addresses = list_before.addresses.unwrap_or_default(); - - // Generate new key - let _generate_response = client - .generate_key(GenerateKeyRequest { - wallet_handle_token: Some(wallet_handle_token.clone()), - display_mnemonic: Some(false), - }) - .await?; - - // List after - let list_after = client - .list_keys_in_wallet(ListKeysRequest { - wallet_handle_token: Some(wallet_handle_token.clone()), - }) - .await?; - let after_addresses = list_after.addresses.unwrap_or_default(); - - assert_eq!( - after_addresses.len(), - before_addresses.len() + 1, - "Expected one additional key after generation" - ); - - // Release handle token - let _release_response = client - .release_wallet_handle_token(ReleaseWalletHandleTokenRequest { - wallet_handle_token: Some(wallet_handle_token.clone()), - }) - .await?; - - Ok(()) -} diff --git a/crates/algokit_utils/tests/kmd/mod.rs b/crates/algokit_utils/tests/kmd/mod.rs deleted file mode 100644 index 38bf29994..000000000 --- a/crates/algokit_utils/tests/kmd/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod key_management; -pub mod wallet_lifecycle; diff --git a/crates/algokit_utils/tests/kmd/wallet_lifecycle.rs b/crates/algokit_utils/tests/kmd/wallet_lifecycle.rs deleted file mode 100644 index 67bbe8872..000000000 --- a/crates/algokit_utils/tests/kmd/wallet_lifecycle.rs +++ /dev/null @@ -1,53 +0,0 @@ -use kmd_client::apis::client::KmdClient; -use kmd_client::models::CreateWalletRequest; -use rand::{Rng, distributions::Alphanumeric}; - -// Basic wallet lifecycle: create wallet, ensure it appears in list -#[tokio::test] -async fn wallet_lifecycle() -> Result<(), Box> { - // Arrange - let client = KmdClient::localnet(); - - let wallet_name: String = format!( - "test_wallet_{}", - rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(12) - .map(char::from) - .collect::() - .to_lowercase() - ); - - // Act: create wallet - let create_response = client - .create_wallet(CreateWalletRequest { - wallet_name: Some(wallet_name.clone()), - wallet_driver_name: Some("sqlite".to_string()), - wallet_password: Some("testpass".to_string()), - ..Default::default() - }) - .await - .map_err(|e| { - format!( - "Failed to create wallet (possible KMD token/availability issue): {}", - e - ) - })?; - - // Assert create response basic invariants - let created_wallet = create_response - .wallet - .as_ref() - .expect("Expected created wallet in response"); - assert_eq!(created_wallet.name.as_deref(), Some(wallet_name.as_str())); - - // List wallets and ensure presence - let list_response = client.list_wallets().await?; - let wallets = list_response.wallets.unwrap_or_default(); - let found = wallets - .iter() - .any(|w| w.name.as_deref() == Some(wallet_name.as_str())); - assert!(found, "Created wallet should be present in list of wallets"); - - Ok(()) -} diff --git a/crates/algokit_utils/tests/kmd_tests.rs b/crates/algokit_utils/tests/kmd_tests.rs deleted file mode 100644 index 7bb21d00b..000000000 --- a/crates/algokit_utils/tests/kmd_tests.rs +++ /dev/null @@ -1 +0,0 @@ -mod kmd; diff --git a/crates/algokit_utils/tests/transactions/composer/app_call.rs b/crates/algokit_utils/tests/transactions/composer/app_call.rs deleted file mode 100644 index bf8892524..000000000 --- a/crates/algokit_utils/tests/transactions/composer/app_call.rs +++ /dev/null @@ -1,1434 +0,0 @@ -use crate::common::{ - AlgorandFixture, AlgorandFixtureResult, TestAccountConfig, TestResult, algorand_fixture, - deploy_arc56_contract, -}; -use algokit_abi::{ABIMethod, ABIReferenceValue, ABIReturn, ABIValue, Arc56Contract}; -use algokit_test_artifacts::{nested_contract, sandbox}; -use algokit_transact::{ - Address, OnApplicationComplete, PaymentTransactionFields, StateSchema, Transaction, - TransactionHeader, TransactionId, -}; -use algokit_utils::transactions::composer::SimulateParams; -use algokit_utils::{AppCallMethodCallParams, AssetCreateParams, ComposerError}; -use algokit_utils::{ - AppCallParams, AppCreateParams, AppDeleteParams, AppMethodCallArg, AppUpdateParams, - PaymentParams, -}; -use base64::{Engine, prelude::BASE64_STANDARD}; -use num_bigint::BigUint; -use rstest::*; -use serde::Deserialize; -use std::str::FromStr; -use std::sync::Arc; - -#[rstest] -#[tokio::test] -async fn test_app_call_transaction( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - - let app_id = create_test_app(&algorand_fixture, &sender_address).await?; - - let app_call_params = AppCallParams { - sender: sender_address.clone(), - app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![b"Call".to_vec()]), - account_references: None, - app_references: None, - asset_references: None, - box_references: None, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_app_call(app_call_params)?; - - let result = composer.send(None).await?; - let confirmation = &result.results[0].confirmation; - - assert!( - confirmation.confirmed_round.is_some(), - "Transaction should be confirmed" - ); - assert!( - confirmation.confirmed_round.unwrap() > 0, - "Confirmed round should be greater than 0" - ); - - let transaction = &confirmation.txn.transaction; - - match transaction { - algokit_transact::Transaction::AppCall(app_call_fields) => { - assert_eq!(app_call_fields.app_id, app_id, "App ID should match"); - assert_eq!( - app_call_fields.on_complete, - OnApplicationComplete::NoOp, - "On Complete should match" - ); - Ok(()) - } - _ => Err("Transaction should be an app call transaction".into()), - } -} - -#[rstest] -#[tokio::test] -async fn test_app_create_transaction( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - - let app_create_params = AppCreateParams { - sender: sender_address.clone(), - approval_program: HELLO_WORLD_APPROVAL_PROGRAM.to_vec(), - clear_state_program: HELLO_WORLD_CLEAR_STATE_PROGRAM.to_vec(), - args: Some(vec![b"Create".to_vec()]), - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_app_create(app_create_params)?; - - let result = composer.send(None).await?; - let confirmation = &result.results[0].confirmation; - - assert!( - confirmation.confirmed_round.is_some(), - "Transaction should be confirmed" - ); - assert!( - confirmation.confirmed_round.unwrap() > 0, - "Confirmed round should be greater than 0" - ); - - let transaction = &confirmation.txn.transaction; - - match transaction { - algokit_transact::Transaction::AppCall(app_call_fields) => { - assert_eq!( - app_call_fields.app_id, 0, - "Application ID should be 0 for create" - ); - assert_eq!( - app_call_fields.on_complete, - OnApplicationComplete::NoOp, - "Clear state program should match" - ); - assert_eq!( - app_call_fields.approval_program, - Some(HELLO_WORLD_APPROVAL_PROGRAM.to_vec()), - "Approval program should match" - ); - assert_eq!( - app_call_fields.clear_state_program, - Some(HELLO_WORLD_CLEAR_STATE_PROGRAM.to_vec()), - "Clear state program should match" - ); - Ok(()) - } - _ => Err("Transaction should be an app call transaction".into()), - } -} - -#[rstest] -#[tokio::test] -async fn test_app_delete_transaction( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - - let app_id = create_test_app(&algorand_fixture, &sender_address).await?; - - let app_delete_params = AppDeleteParams { - sender: sender_address.clone(), - app_id, - args: Some(vec![b"Delete".to_vec()]), - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_app_delete(app_delete_params)?; - - let result = composer.send(None).await?; - let confirmation = &result.results[0].confirmation; - - assert!( - confirmation.confirmed_round.is_some(), - "Transaction should be confirmed" - ); - assert!( - confirmation.confirmed_round.unwrap() > 0, - "Confirmed round should be greater than 0" - ); - - let transaction = &confirmation.txn.transaction; - - match transaction { - algokit_transact::Transaction::AppCall(app_call_fields) => { - assert_eq!( - app_call_fields.app_id, app_id, - "Application ID should match" - ); - assert_eq!( - app_call_fields.on_complete, - OnApplicationComplete::DeleteApplication, - "On Complete should be DeleteApplication" - ); - Ok(()) - } - _ => Err("Transaction should be an app delete transaction".into()), - } -} - -#[rstest] -#[tokio::test] -async fn test_app_update_transaction( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - - let app_id = create_test_app(&algorand_fixture, &sender_address).await?; - - let app_update_params = AppUpdateParams { - sender: sender_address.clone(), - app_id, - approval_program: HELLO_WORLD_CLEAR_STATE_PROGRAM.to_vec(), // Update the approval program - clear_state_program: HELLO_WORLD_CLEAR_STATE_PROGRAM.to_vec(), - args: Some(vec![b"Update".to_vec()]), - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_app_update(app_update_params)?; - - let result = composer.send(None).await?; - - let confirmation = &result.results[0].confirmation; - - assert!( - confirmation.confirmed_round.is_some(), - "Transaction should be confirmed" - ); - assert!( - confirmation.confirmed_round.unwrap() > 0, - "Confirmed round should be greater than 0" - ); - let transaction = &confirmation.txn.transaction; - - match transaction { - algokit_transact::Transaction::AppCall(app_call_fields) => { - assert_eq!( - app_call_fields.app_id, app_id, - "Application ID should match" - ); - assert_eq!( - app_call_fields.on_complete, - OnApplicationComplete::UpdateApplication, - "On Complete should be UpdateApplication" - ); - assert_eq!( - app_call_fields.approval_program, - Some(HELLO_WORLD_CLEAR_STATE_PROGRAM.to_vec()), - "Updated approval program should match" - ); - assert_eq!( - app_call_fields.clear_state_program, - Some(HELLO_WORLD_CLEAR_STATE_PROGRAM.to_vec()), - "Clear state program should match" - ); - Ok(()) - } - _ => Err("Transaction should be an app update transaction".into()), - } -} - -#[rstest] -#[tokio::test] -async fn test_hello_world_app_method_call( - #[future] arc56_algorand_fixture: Arc56AppFixtureResult, -) -> TestResult { - let Arc56AppFixture { - sender_address, - app_id, - arc56_contract, - algorand_fixture, - } = arc56_algorand_fixture.await?; - - let abi_method = get_abi_method(&arc56_contract, "hello_world")?; - - let args = vec![AppMethodCallArg::ABIValue(ABIValue::String( - "world".to_string(), - ))]; - - let method_call_params = AppCallMethodCallParams { - sender: sender_address.clone(), - app_id, - method: abi_method, - args, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_app_call_method_call(method_call_params)?; - - let result = composer.send(None).await?; - let abi_return = ensure_abi_return(&result.results[0].abi_return)?; - - match &abi_return.return_value { - Some(ABIValue::String(value)) => { - assert_eq!(value, "Hello, world",); - Ok(()) - } - _ => Err("Invalid return type".into()), - } -} - -#[rstest] -#[tokio::test] -async fn test_add_app_call_method_call( - #[future] arc56_algorand_fixture: Arc56AppFixtureResult, -) -> TestResult { - let Arc56AppFixture { - sender_address, - app_id, - arc56_contract, - algorand_fixture, - } = arc56_algorand_fixture.await?; - - let abi_method = get_abi_method(&arc56_contract, "add")?; - - let args = vec![ - AppMethodCallArg::ABIValue(ABIValue::Uint(BigUint::from(1u8))), - AppMethodCallArg::ABIValue(ABIValue::Uint(BigUint::from(2u8))), - ]; - - let method_call_params = AppCallMethodCallParams { - sender: sender_address.clone(), - app_id, - method: abi_method, - args, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_app_call_method_call(method_call_params)?; - - let result = composer.send(None).await?; - - let abi_return = ensure_abi_return(&result.results[0].abi_return)?; - - match &abi_return.return_value { - Some(ABIValue::Uint(value)) => { - assert_eq!(*value, BigUint::from(3u8)); - Ok(()) - } - _ => Err("Invalid return type".into()), - } -} - -#[rstest] -#[tokio::test] -async fn test_echo_byte_app_call_method_call( - #[future] arc56_algorand_fixture: Arc56AppFixtureResult, -) -> TestResult { - let Arc56AppFixture { - sender_address, - app_id, - arc56_contract, - algorand_fixture, - } = arc56_algorand_fixture.await?; - - let abi_method = get_abi_method(&arc56_contract, "echo_bytes")?; - - let test_array = vec![ - ABIValue::Byte(1u8), - ABIValue::Byte(2u8), - ABIValue::Byte(3u8), - ABIValue::Byte(4u8), - ]; - let args = vec![AppMethodCallArg::ABIValue(ABIValue::Array( - test_array.clone(), - ))]; - - let method_call_params = AppCallMethodCallParams { - sender: sender_address.clone(), - app_id, - method: abi_method, - args, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_app_call_method_call(method_call_params)?; - - let result = composer.send(None).await?; - - let abi_return = ensure_abi_return(&result.results[0].abi_return)?; - - match &abi_return.return_value { - Some(ABIValue::Array(value)) => { - assert_eq!(*value, test_array); - Ok(()) - } - _ => Err("Invalid return type".into()), - } -} - -#[rstest] -#[tokio::test] -async fn test_echo_static_array_app_call_method_call( - #[future] arc56_algorand_fixture: Arc56AppFixtureResult, -) -> TestResult { - let Arc56AppFixture { - sender_address, - app_id, - arc56_contract, - algorand_fixture, - } = arc56_algorand_fixture.await?; - - let abi_method = get_abi_method(&arc56_contract, "echo_static_array")?; - - let test_array = vec![ - ABIValue::Uint(BigUint::from(1u8)), - ABIValue::Uint(BigUint::from(2u8)), - ABIValue::Uint(BigUint::from(3u8)), - ABIValue::Uint(BigUint::from(4u8)), - ]; - let args = vec![AppMethodCallArg::ABIValue(ABIValue::Array( - test_array.clone(), - ))]; - - let method_call_params = AppCallMethodCallParams { - sender: sender_address.clone(), - app_id, - method: abi_method, - args, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_app_call_method_call(method_call_params)?; - - let result = composer.send(None).await?; - - let abi_return = ensure_abi_return(&result.results[0].abi_return)?; - - match &abi_return.return_value { - Some(ABIValue::Array(value)) => { - assert_eq!(*value, test_array); - Ok(()) - } - _ => Err("Invalid return type".into()), - } -} - -#[rstest] -#[tokio::test] -async fn test_echo_dynamic_array_app_call_method_call( - #[future] arc56_algorand_fixture: Arc56AppFixtureResult, -) -> TestResult { - let Arc56AppFixture { - sender_address, - app_id, - arc56_contract, - algorand_fixture, - } = arc56_algorand_fixture.await?; - - let abi_method = get_abi_method(&arc56_contract, "echo_dynamic_array")?; - - let test_array = vec![ - ABIValue::Uint(BigUint::from(10u8)), - ABIValue::Uint(BigUint::from(20u8)), - ABIValue::Uint(BigUint::from(30u8)), - ]; - let args = vec![AppMethodCallArg::ABIValue(ABIValue::Array( - test_array.clone(), - ))]; - - let method_call_params = AppCallMethodCallParams { - sender: sender_address.clone(), - app_id, - method: abi_method, - args, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_app_call_method_call(method_call_params)?; - - let result = composer.send(None).await?; - - let abi_return = ensure_abi_return(&result.results[0].abi_return)?; - - match &abi_return.return_value { - Some(ABIValue::Array(value)) => { - assert_eq!(*value, test_array, "Return array should match input array"); - Ok(()) - } - _ => Err("Return value should be an array".into()), - } -} - -#[rstest] -#[tokio::test] -async fn test_nest_array_and_tuple_app_call_method_call( - #[future] arc56_algorand_fixture: Arc56AppFixtureResult, -) -> TestResult { - let Arc56AppFixture { - sender_address, - app_id, - arc56_contract, - algorand_fixture, - } = arc56_algorand_fixture.await?; - - let abi_method = get_abi_method(&arc56_contract, "nest_array_and_tuple")?; - - let nested_array = vec![ - ABIValue::Array(vec![ - ABIValue::Uint(BigUint::from(1u8)), - ABIValue::Uint(BigUint::from(2u8)), - ]), - ABIValue::Array(vec![ - ABIValue::Uint(BigUint::from(3u8)), - ABIValue::Uint(BigUint::from(4u8)), - ABIValue::Uint(BigUint::from(5u8)), - ]), - ]; - - let tuple_arg = ABIValue::Array(vec![ - ABIValue::Array(vec![ - ABIValue::Uint(BigUint::from(10u8)), - ABIValue::Uint(BigUint::from(20u8)), - ]), - ABIValue::String("test string".to_string()), - ]); - - let args = vec![ - AppMethodCallArg::ABIValue(ABIValue::Array(nested_array.clone())), - AppMethodCallArg::ABIValue(tuple_arg.clone()), - ]; - - let method_call_params = AppCallMethodCallParams { - sender: sender_address.clone(), - app_id, - method: abi_method, - args, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_app_call_method_call(method_call_params)?; - - let result = composer.send(None).await?; - - let abi_return = ensure_abi_return(&result.results[0].abi_return)?; - - match &abi_return.return_value { - Some(ABIValue::Array(returned_tuple)) => { - assert_eq!( - returned_tuple.len(), - 2, - "Return tuple should have 2 elements" - ); - - match &returned_tuple[0] { - ABIValue::Array(returned_nested_array) => { - assert_eq!( - *returned_nested_array, nested_array, - "Returned nested array should match input" - ); - } - _ => return Err("First element should be a nested array".into()), - }; - - match &returned_tuple[1] { - ABIValue::Array(returned_inner_tuple) => { - assert_eq!( - *returned_inner_tuple, - match &tuple_arg { - ABIValue::Array(t) => t.clone(), - _ => return Err("tuple_arg should be an array".into()), - }, - "Returned inner tuple should match input" - ); - } - _ => return Err("Second element should be a tuple".into()), - }; - Ok(()) - } - _ => Err("Return value should be a tuple".into()), - } -} - -#[rstest] -#[tokio::test] -async fn test_get_pay_txn_amount_app_call_method_call( - #[future] arc56_algorand_fixture: Arc56AppFixtureResult, -) -> TestResult { - let Arc56AppFixture { - sender_address, - app_id, - arc56_contract, - mut algorand_fixture, - } = arc56_algorand_fixture.await?; - - let receiver = algorand_fixture.generate_account(None).await?; - let receiver_addr = receiver.account().address(); - - let abi_method = get_abi_method(&arc56_contract, "get_pay_txn_amount")?; - - let payment_amount = 1_234_567u64; - let args = vec![AppMethodCallArg::Payment(PaymentParams { - sender: sender_address.clone(), - receiver: receiver_addr, - amount: payment_amount, - ..Default::default() - })]; - - let method_call_params = AppCallMethodCallParams { - sender: sender_address.clone(), - app_id, - method: abi_method, - args, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_app_call_method_call(method_call_params)?; - - let result = composer.send(None).await?; - - // Transaction 0 is payment, Transaction 1 is the method call - let abi_return = ensure_abi_return(&result.results[1].abi_return)?; - - match &abi_return.return_value { - Some(ABIValue::Uint(returned_amount)) => { - assert_eq!( - *returned_amount, - BigUint::from(payment_amount), - "Returned amount should match payment amount" - ); - Ok(()) - } - _ => Err("Return value should be a UInt".into()), - } -} - -#[rstest] -#[tokio::test] -async fn test_get_pay_txn_amount_app_call_method_call_using_a_different_signer( - #[future] arc56_algorand_fixture: Arc56AppFixtureResult, -) -> TestResult { - let Arc56AppFixture { - sender_address, - app_id, - arc56_contract, - mut algorand_fixture, - } = arc56_algorand_fixture.await?; - - let receiver = algorand_fixture.generate_account(None).await?; - let receiver_addr = receiver.account().address(); - - let alice = algorand_fixture.generate_account(None).await?; - let alice_addr = alice.account().address(); - - let abi_method = get_abi_method(&arc56_contract, "get_pay_txn_amount")?; - - let payment_amount = 1_234_567u64; - let alice_signer = Arc::new(alice.clone()); - let args = vec![AppMethodCallArg::Payment(PaymentParams { - sender: alice_addr.clone(), - signer: Some(alice_signer), - receiver: receiver_addr, - amount: payment_amount, - ..Default::default() - })]; - - let method_call_params = AppCallMethodCallParams { - sender: sender_address.clone(), - app_id, - method: abi_method, - args, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_app_call_method_call(method_call_params)?; - - let result = composer.send(None).await?; - - // Transaction 0 is payment, Transaction 1 is the method call - let abi_return = ensure_abi_return(&result.results[1].abi_return)?; - - match &abi_return.return_value { - Some(ABIValue::Uint(returned_amount)) => { - assert_eq!( - *returned_amount, - BigUint::from(payment_amount), - "Returned amount should match payment amount" - ); - Ok(()) - } - _ => Err("Return value should be a UInt".into()), - } -} - -#[rstest] -#[tokio::test] -async fn test_get_returned_value_of_app_call_txn_app_call_method_call( - #[future] arc56_algorand_fixture: Arc56AppFixtureResult, -) -> TestResult { - let Arc56AppFixture { - sender_address, - app_id, - arc56_contract, - mut algorand_fixture, - } = arc56_algorand_fixture.await?; - - let receiver = algorand_fixture.generate_account(None).await?; - let receiver_addr = receiver.account().address(); - - let get_pay_txn_amount_method = get_abi_method(&arc56_contract, "get_pay_txn_amount")?; - - let get_returned_value_of_app_call_txn_method = - get_abi_method(&arc56_contract, "get_returned_value_of_app_call_txn")?; - - let payment_amount = 2_500_000u64; - let mut composer = algorand_fixture.algorand_client.new_composer(None); - - let first_method_call_params = AppCallMethodCallParams { - sender: sender_address.clone(), - app_id, - method: get_pay_txn_amount_method, - args: vec![AppMethodCallArg::Payment(PaymentParams { - sender: sender_address.clone(), - receiver: receiver_addr, - amount: payment_amount, - ..Default::default() - })], - ..Default::default() - }; - - let second_method_call_params = AppCallMethodCallParams { - sender: sender_address.clone(), - app_id, - method: get_returned_value_of_app_call_txn_method, - args: vec![AppMethodCallArg::AppCallMethodCall( - first_method_call_params, - )], - ..Default::default() - }; - - composer.add_app_call_method_call(second_method_call_params)?; - - let result = composer.send(None).await?; - - // Transaction order: [payment, first_method_call, second_method_call] - let abi_return = ensure_abi_return(&result.results[2].abi_return)?; - - match &abi_return.return_value { - Some(ABIValue::Uint(returned_amount)) => { - assert_eq!( - *returned_amount, - BigUint::from(payment_amount), - "Returned amount should match payment amount" - ); - Ok(()) - } - _ => Err("Return value should be a UInt".into()), - } -} - -#[rstest] -#[tokio::test] -async fn test_get_returned_value_of_nested_app_call_method_calls( - #[future] arc56_algorand_fixture: Arc56AppFixtureResult, -) -> TestResult { - let Arc56AppFixture { - sender_address, - app_id, - arc56_contract, - mut algorand_fixture, - } = arc56_algorand_fixture.await?; - - let receiver = algorand_fixture.generate_account(None).await?; - let receiver_addr = receiver.account().address(); - - let get_pay_txn_amount_method = get_abi_method(&arc56_contract, "get_pay_txn_amount")?; - - let get_pay_txns_amount_sum_method = - get_abi_method(&arc56_contract, "get_pay_txns_amount_sum")?; - - let payment_amount = 5_000u64; - let mut composer = algorand_fixture.algorand_client.new_composer(None); - - let get_pay_txn_amount_method_call_params = AppCallMethodCallParams { - sender: sender_address.clone(), - app_id, - method: get_pay_txn_amount_method, - args: vec![AppMethodCallArg::Payment(PaymentParams { - sender: sender_address.clone(), - receiver: receiver_addr.clone(), - amount: payment_amount, - ..Default::default() - })], - ..Default::default() - }; - - let get_pay_txns_amount_sum_method_method_call_params = AppCallMethodCallParams { - sender: sender_address.clone(), - app_id, - method: get_pay_txns_amount_sum_method, - args: vec![ - AppMethodCallArg::Payment(PaymentParams { - sender: sender_address.clone(), - receiver: receiver_addr.clone(), - amount: payment_amount, - note: Some("second txn".as_bytes().to_vec()), - ..Default::default() - }), - AppMethodCallArg::TransactionPlaceholder, - AppMethodCallArg::AppCallMethodCall(get_pay_txn_amount_method_call_params), - ], - ..Default::default() - }; - - composer.add_app_call_method_call(get_pay_txns_amount_sum_method_method_call_params)?; - - let result = composer.send(None).await?; - - // Transaction order: [payment1, payment2, inner_app_call, outer_app_call] - let abi_return = ensure_abi_return(&result.results[3].abi_return)?; - - let expected_result = BigUint::from(15_000u64); - match &abi_return.return_value { - Some(ABIValue::Uint(returned_amount)) => { - assert_eq!( - *returned_amount, expected_result, - "Returned amount should match payment amount" - ); - Ok(()) - } - _ => Err("Return value should be a UInt".into()), - } -} - -#[rstest] -#[tokio::test] -async fn group_simulate_matches_send( - #[future] arc56_algorand_fixture: Arc56AppFixtureResult, -) -> TestResult { - let Arc56AppFixture { - sender_address: sender, - app_id, - arc56_contract, - algorand_fixture, - } = arc56_algorand_fixture.await?; - - // Compose group: add(uint64,uint64)uint64 + payment + hello_world(string)string - let mut composer = algorand_fixture.algorand_client.new_composer(None); - - // 1) add(uint64,uint64)uint64 - let method_add = get_abi_method(&arc56_contract, "add")?; - let add_params = AppCallMethodCallParams { - sender: sender.clone(), - app_id, - method: method_add, - args: vec![ - AppMethodCallArg::ABIValue(ABIValue::Uint(BigUint::from(1u64))), - AppMethodCallArg::ABIValue(ABIValue::Uint(BigUint::from(2u64))), - ], - ..Default::default() - }; - composer.add_app_call_method_call(add_params)?; - - // 2) payment - let payment = PaymentParams { - sender: sender.clone(), - receiver: sender.clone(), - amount: 10_000, - ..Default::default() - }; - composer.add_payment(payment)?; - - // 3) hello_world(string)string - let method_hello = get_abi_method(&arc56_contract, "hello_world")?; - let call_params = AppCallMethodCallParams { - sender: sender.clone(), - app_id, - method: method_hello, - args: vec![AppMethodCallArg::ABIValue(ABIValue::String( - "test".to_string(), - ))], - ..Default::default() - }; - composer.add_app_call_method_call(call_params)?; - - let simulate = composer - .simulate(Some(SimulateParams { - skip_signatures: true, - ..Default::default() - })) - .await?; - let send = composer.send(None).await?; - - for (simulate_result, send_result) in simulate.results.iter().zip(send.results.iter()) { - // Compare ABI return values - match (&simulate_result.abi_return, &send_result.abi_return) { - (Some(sim_return), Some(send_return)) => { - assert_eq!( - sim_return.return_value, send_return.return_value, - "ABI return values should match between simulate and send" - ); - } - (None, None) => { - // Both are None, which is fine - } - _ => { - return Err("One result has ABI return while the other doesn't".into()); - } - } - assert_eq!(simulate_result.transaction_id, send_result.transaction_id); - } - Ok(()) -} - -struct Arc56AppFixture { - sender_address: Address, - app_id: u64, - arc56_contract: Arc56Contract, - algorand_fixture: AlgorandFixture, -} - -type Arc56AppFixtureResult = Result>; - -#[fixture] -async fn arc56_algorand_fixture( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> Arc56AppFixtureResult { - let algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - - let arc56_contract: Arc56Contract = serde_json::from_str(sandbox::APPLICATION_ARC56)?; - let app_id = deploy_arc56_contract( - &algorand_fixture, - &sender_address, - &arc56_contract, - None, - None, - None, - ) - .await?; - - Ok(Arc56AppFixture { - sender_address, - app_id, - arc56_contract, - algorand_fixture, - }) -} - -// Raw (non ABI) hello world approval program -const HELLO_WORLD_APPROVAL_PROGRAM: [u8; 18] = [ - 10, 128, 7, 72, 101, 108, 108, 111, 44, 32, 54, 26, 0, 80, 176, 129, 1, 67, -]; -// Raw (non ABI) hello world clear state program -const HELLO_WORLD_CLEAR_STATE_PROGRAM: [u8; 4] = [10, 129, 1, 67]; - -async fn create_test_app( - algorand_fixture: &AlgorandFixture, - sender: &Address, -) -> Result { - let app_create_params = AppCreateParams { - sender: sender.clone(), - approval_program: HELLO_WORLD_APPROVAL_PROGRAM.to_vec(), - clear_state_program: HELLO_WORLD_CLEAR_STATE_PROGRAM.to_vec(), - global_state_schema: Some(StateSchema { - num_uints: 1, - num_byte_slices: 1, - }), - local_state_schema: Some(StateSchema { - num_uints: 1, - num_byte_slices: 1, - }), - args: Some(vec![b"Create".to_vec()]), - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - - composer.add_app_create(app_create_params)?; - - let result = composer.send(None).await?; - - Ok(result.results[0] - .confirmation - .app_id - .expect("App Id must be returned")) -} - -#[rstest] -#[tokio::test] -async fn test_more_than_15_args_with_ref_types_app_call_method_call( - #[future] arc56_algorand_fixture: Arc56AppFixtureResult, -) -> TestResult { - let Arc56AppFixture { - sender_address, - app_id, - arc56_contract, - mut algorand_fixture, - } = arc56_algorand_fixture.await?; - - let receiver = algorand_fixture - .generate_account(Some(TestAccountConfig { - initial_funds: 0u64, - ..Default::default() - })) - .await?; - let receiver_addr = receiver.account().address(); - - let asset_create_params = AssetCreateParams { - sender: sender_address.clone(), - total: 1_000_000, - decimals: Some(2), - default_frozen: Some(false), - asset_name: Some("Test Asset".to_string()), - unit_name: Some("TEST".to_string()), - url: Some("https://example.com".to_string()), - metadata_hash: None, - manager: Some(sender_address.clone()), - reserve: Some(sender_address.clone()), - freeze: Some(sender_address.clone()), - clawback: Some(sender_address.clone()), - ..Default::default() - }; - - let mut asset_composer = algorand_fixture.algorand_client.new_composer(None); - asset_composer.add_asset_create(asset_create_params)?; - - let asset_result = asset_composer.send(None).await?; - let asset_id = asset_result.results[0] - .confirmation - .asset_id - .expect("No asset ID returned"); - - let abi_method = get_abi_method(&arc56_contract, "more_than_15_args_with_ref_types")?; - - let tx_params = algorand_fixture.algod.transaction_params().await?; - - let payment_amount = 200_000u64; - let genesis_hash: Option<[u8; 32]> = tx_params.genesis_hash.try_into().ok(); - let payment_transaction = Transaction::Payment(PaymentTransactionFields { - header: TransactionHeader { - sender: sender_address.clone(), - fee: Some(tx_params.min_fee), - first_valid: tx_params.last_round, - last_valid: tx_params.last_round + 1000, - genesis_hash, - genesis_id: Some(tx_params.genesis_id), - note: None, - rekey_to: None, - lease: None, - group: None, - }, - receiver: receiver_addr.clone(), - amount: payment_amount, - close_remainder_to: None, - }); - - let mut args = vec![]; - - for i in 1..=17 { - args.push(AppMethodCallArg::ABIValue(ABIValue::Uint(BigUint::from( - i as u64, - )))); - } - - args.push(AppMethodCallArg::ABIReference(ABIReferenceValue::Asset( - asset_id, - ))); - - args.push(AppMethodCallArg::ABIValue(ABIValue::Uint(BigUint::from( - 18u64, - )))); - - args.push(AppMethodCallArg::ABIReference( - ABIReferenceValue::Application(app_id), - )); - - args.push(AppMethodCallArg::Transaction(payment_transaction)); - - args.push(AppMethodCallArg::ABIReference(ABIReferenceValue::Account( - receiver_addr.to_string(), - ))); - - let method_call_params = AppCallMethodCallParams { - sender: sender_address.clone(), - app_id, - method: abi_method, - args, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_app_call_method_call(method_call_params)?; - - let result = composer.send(None).await?; - - // Transaction 0 is the payment from args, Transaction 1 is the method call - let abi_return = ensure_abi_return(&result.results[1].abi_return)?; - - match &abi_return.return_value { - Some(ABIValue::Array(returned_tuple)) => { - assert_eq!( - returned_tuple.len(), - 4, - "Return tuple should have 4 elements" - ); - - match &returned_tuple[0] { - ABIValue::Uint(returned_asset_id) => { - assert_eq!( - *returned_asset_id, - BigUint::from(asset_id), - "Returned asset ID should match created asset" - ); - } - _ => return Err("First element should be asset ID".into()), - } - - match &returned_tuple[1] { - ABIValue::Uint(returned_app_id) => { - assert_eq!( - *returned_app_id, - BigUint::from(app_id), - "Returned app ID should match deployed app" - ); - } - _ => return Err("Second element should be app ID".into()), - } - - match &returned_tuple[2] { - ABIValue::Uint(returned_balance) => { - assert_eq!(*returned_balance, BigUint::from(payment_amount),); - } - _ => return Err("Third element should be account balance".into()), - } - - match &returned_tuple[3] { - ABIValue::Array(txn_id_bytes) => { - assert!( - !txn_id_bytes.is_empty(), - "Transaction ID should not be empty" - ); - - let signed_group = composer - .gather_signatures() - .await - .expect("Signed group should be available after send"); - - let actual_txn_id = signed_group[0] - .id_raw() - .expect("Failed to get raw transaction ID") - .to_vec(); - - let returned_txn_id: Vec = txn_id_bytes - .iter() - .map(|byte_abi_value| match byte_abi_value { - ABIValue::Byte(b) => Ok(*b), - _ => Err("Transaction ID bytes should be ABIValue::Byte"), - }) - .collect::, _>>()?; - - assert_eq!( - returned_txn_id, actual_txn_id, - "Returned transaction ID should match actual transaction ID" - ); - } - _ => return Err("Fourth element should be transaction ID bytes".into()), - } - } - _ => return Err("Return value should be a tuple".into()), - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_more_than_15_args_app_call_method_call( - #[future] arc56_algorand_fixture: Arc56AppFixtureResult, -) -> TestResult { - let Arc56AppFixture { - sender_address, - app_id, - arc56_contract, - algorand_fixture, - } = arc56_algorand_fixture.await?; - - let abi_method = get_abi_method(&arc56_contract, "more_than_15_args")?; - - let mut args = vec![]; - for i in 1..=18 { - args.push(AppMethodCallArg::ABIValue(ABIValue::Uint(BigUint::from( - i as u64, - )))); - } - - let method_call_params = AppCallMethodCallParams { - sender: sender_address.clone(), - app_id, - method: abi_method, - args, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_app_call_method_call(method_call_params)?; - - let result = composer.send(None).await?; - - let abi_return = ensure_abi_return(&result.results[0].abi_return)?; - - match &abi_return.return_value { - Some(ABIValue::Array(returned_array)) => { - assert_eq!( - returned_array.len(), - 18, - "Return array should have 18 elements" - ); - - for (i, element) in returned_array.iter().enumerate() { - match element { - ABIValue::Uint(returned_value) => { - assert_eq!( - *returned_value, - BigUint::from((i + 1) as u64), - "Element {} should match input value {}", - i, - i + 1 - ); - } - _ => return Err(format!("Array element {} should be UInt64", i).into()), - } - } - } - _ => return Err("Return value should be a dynamic array".into()), - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_app_call_validation_errors( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - - // Test app call with invalid app_id (0) - let invalid_app_call_params = AppCallParams { - sender: sender_address.clone(), - app_id: 0, // Invalid: should be > 0 for app calls (0 is for app creation) - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer - .add_app_call(invalid_app_call_params) - .expect("Adding invalid app call should succeed at composer level"); - - // The validation should fail when building the transaction group - let result = composer.build().await; - - // The build should return an error due to validation failures - assert!( - result.is_err(), - "Build with invalid app call parameters should fail" - ); - - let error = result.unwrap_err(); - let error_string = error.to_string(); - - // Check that the error contains validation-related messages from the transact crate - assert!( - error_string.contains("validation") - || error_string.contains("app_id") - || error_string.contains("Application") - || error_string.contains("zero") - || error_string.contains("0"), - "Error should contain validation failure details: {}", - error_string - ); - - Ok(()) -} - -fn get_abi_method( - arc56_contract: &Arc56Contract, - name: &str, -) -> Result> { - Ok(arc56_contract - .find_abi_method(name) - .map_err(|e| format!("Failed to convert ARC56 method to ABI method: {}", e))?) -} - -fn ensure_abi_return( - abi_return: &Option, -) -> Result<&ABIReturn, Box> { - let abi_return = abi_return.as_ref().ok_or("No ABI return value")?; - match &abi_return.decode_error { - Some(err) => Err(format!("Failed to parse ABI result: {}", err).into()), - None => Ok(abi_return), - } -} - -#[rstest] -#[tokio::test] -async fn test_double_nested(#[future] algorand_fixture: AlgorandFixtureResult) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - let mut composer = algorand_fixture.algorand_client.new_composer(None); - - let sender_address = algorand_fixture.test_account.account().address(); - let receiver = algorand_fixture.generate_account(None).await?; - let receiver_address = receiver.account().address(); - - let app_id = deploy_nested_app(&algorand_fixture).await?; - - let first_txn_arg = AppCallMethodCallParams { - sender: sender_address.clone(), - note: Some("first_txn_arg".as_bytes().to_vec()), - app_id, - method: ABIMethod::from_str("txnArg(pay)address")?, - args: vec![AppMethodCallArg::Payment(PaymentParams { - sender: sender_address.clone(), - receiver: receiver_address.clone(), - amount: 2_500_000u64, - ..Default::default() - })], - ..Default::default() - }; - - let second_txn_arg = AppCallMethodCallParams { - sender: sender_address.clone(), - note: Some("second_txn_arg".as_bytes().to_vec()), - app_id, - method: ABIMethod::from_str("txnArg(pay)address")?, - args: vec![AppMethodCallArg::Payment(PaymentParams { - sender: sender_address.clone(), - receiver: receiver_address.clone(), - amount: 1_500_000u64, - ..Default::default() - })], - ..Default::default() - }; - - let method_call_params = AppCallMethodCallParams { - sender: sender_address.clone(), - app_id, - method: ABIMethod::from_str("doubleNestedTxnArg(pay,appl,pay,appl)uint64")?, - args: vec![ - AppMethodCallArg::AppCallMethodCall(first_txn_arg), - AppMethodCallArg::AppCallMethodCall(second_txn_arg), - ], - ..Default::default() - }; - - composer.add_app_call_method_call(method_call_params)?; - let result: algokit_utils::TransactionComposerSendResult = composer.send(None).await?; - - // result.results[0] is payment, result.results[1] is first method call - let abi_return_0 = ensure_abi_return(&result.results[1].abi_return)?; - if let Some(ABIValue::Address(value)) = &abi_return_0.return_value { - assert_eq!( - *value, - sender_address.as_str(), - "Returned address should match with sender address" - ); - } else { - return Err("First return value should be an Address".into()); - } - - // result.results[2] is payment, result.results[3] is second method call - let abi_return_1 = ensure_abi_return(&result.results[3].abi_return)?; - if let Some(ABIValue::Address(value)) = &abi_return_1.return_value { - assert_eq!( - *value, - sender_address.as_str(), - "Returned address should match with sender address" - ); - } else { - return Err("Second return value should be an Address".into()); - } - - // result.results[4] is the outer method call - let abi_return_2 = ensure_abi_return(&result.results[4].abi_return)?; - if let Some(ABIValue::Uint(value)) = &abi_return_2.return_value { - assert_eq!( - *value, - BigUint::from(app_id), - "Returned value should match with app ID" - ); - } else { - return Err("Third return value should be a Uint".into()); - } - - Ok(()) -} - -#[derive(Deserialize)] -struct TealSource { - approval: String, - clear: String, -} - -#[derive(Deserialize)] -struct Arc32AppSpec { - source: Option, -} - -async fn deploy_nested_app( - algorand_fixture: &AlgorandFixture, -) -> Result> { - let app_spec: Arc32AppSpec = serde_json::from_str(nested_contract::APPLICATION)?; - let teal_source = app_spec.source.unwrap(); - let approval_bytes = BASE64_STANDARD.decode(teal_source.approval)?; - let clear_state_bytes = BASE64_STANDARD.decode(teal_source.clear)?; - - let approval_compile_result = algorand_fixture - .algod - .teal_compile(approval_bytes, None) - .await?; - let clear_state_compile_result = algorand_fixture - .algod - .teal_compile(clear_state_bytes, None) - .await?; - - let create_method = ABIMethod::from_str("createApplication()void")?; - let create_method_selector = create_method.selector()?; - - let app_create_params = AppCreateParams { - sender: algorand_fixture.test_account.account().address(), - approval_program: approval_compile_result.result, - clear_state_program: clear_state_compile_result.result, - args: Some(vec![create_method_selector]), - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_app_create(app_create_params)?; - - let result = composer.send(None).await?; - - result.results[0] - .confirmation - .app_id - .ok_or_else(|| "No app id returned".into()) -} diff --git a/crates/algokit_utils/tests/transactions/composer/asset_config.rs b/crates/algokit_utils/tests/transactions/composer/asset_config.rs deleted file mode 100644 index eb5a42a7d..000000000 --- a/crates/algokit_utils/tests/transactions/composer/asset_config.rs +++ /dev/null @@ -1,270 +0,0 @@ -use crate::common::{AlgorandFixture, AlgorandFixtureResult, TestResult, algorand_fixture}; -use algokit_transact::Address; -use algokit_utils::{AssetConfigParams, AssetCreateParams, AssetDestroyParams}; -use rstest::*; - -#[rstest] -#[tokio::test] -async fn test_asset_create_transaction( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - - let asset_create_params = AssetCreateParams { - sender: sender_address.clone(), - total: 1_000_000, - decimals: Some(2), - default_frozen: Some(false), - asset_name: Some("Test Asset".to_string()), - unit_name: Some("TEST".to_string()), - url: Some("https://example.com".to_string()), - metadata_hash: None, - manager: Some(sender_address.clone()), - reserve: Some(sender_address.clone()), - freeze: Some(sender_address.clone()), - clawback: Some(sender_address), - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_asset_create(asset_create_params)?; - - let result = composer.send(None).await?; - let confirmation = &result.results[0].confirmation; - - // Assert transaction was confirmed - assert!( - confirmation.confirmed_round.is_some(), - "Transaction should be confirmed" - ); - assert!( - confirmation.confirmed_round.unwrap() > 0, - "Confirmed round should be greater than 0" - ); - - let transaction = &confirmation.txn.transaction; - - match transaction { - algokit_transact::Transaction::AssetConfig(asset_config_fields) => { - assert_eq!( - asset_config_fields.asset_id, 0, - "Asset ID should be 0 for creation" - ); - assert_eq!( - asset_config_fields.total, - Some(1_000_000), - "Total should be 1,000,000" - ); - assert_eq!( - asset_config_fields.decimals, - Some(2), - "Decimals should be 2" - ); - assert_eq!( - asset_config_fields.asset_name, - Some("Test Asset".to_string()), - "Asset name should match" - ); - assert_eq!( - asset_config_fields.unit_name, - Some("TEST".to_string()), - "Unit name should match" - ); - Ok(()) - } - _ => Err("Transaction should be an asset config transaction".into()), - } -} - -#[rstest] -#[tokio::test] -async fn test_asset_config_transaction( - #[future] algorand_fixture: Result>, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - - let new_manager_addr: Address = algorand_fixture - .generate_account(None) - .await? - .account() - .address(); - // First create an asset to reconfigure - let asset_create_params = AssetCreateParams { - sender: sender_address.clone(), - total: 1_000_000, - decimals: Some(0), - default_frozen: Some(false), - asset_name: Some("Reconfigure Test".to_string()), - unit_name: Some("RECONF".to_string()), - url: None, - metadata_hash: None, - manager: Some(sender_address.clone()), - reserve: None, - freeze: None, - clawback: None, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_asset_create(asset_create_params)?; - - let create_result = composer.send(None).await?; - let asset_id = create_result.results[0] - .confirmation - .asset_id - .ok_or("Failed to get asset ID")?; - - // Now reconfigure the asset - let asset_config_params = AssetConfigParams { - sender: sender_address.clone(), - asset_id, - manager: Some(new_manager_addr.clone()), - reserve: None, - freeze: None, - clawback: None, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_asset_config(asset_config_params)?; - - let result = composer.send(None).await?; - - let confirmation = &result.results[0].confirmation; - - // Assert transaction was confirmed - assert!( - confirmation.confirmed_round.is_some(), - "Transaction should be confirmed" - ); - assert!( - confirmation.confirmed_round.unwrap() > 0, - "Confirmed round should be greater than 0" - ); - - let transaction = &confirmation.txn.transaction; - - match transaction { - algokit_transact::Transaction::AssetConfig(asset_config_fields) => { - assert_eq!( - asset_config_fields.manager, - Some(new_manager_addr.clone()), - "Manager should be updated" - ); - Ok(()) - } - _ => Err("Transaction should be an asset config transaction".into()), - } -} - -#[rstest] -#[tokio::test] -async fn test_asset_destroy_transaction( - #[future] algorand_fixture: Result>, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - // First create an asset to destroy - let asset_create_params = AssetCreateParams { - sender: sender_address.clone(), - total: 1_000, - decimals: Some(0), - default_frozen: Some(false), - asset_name: Some("Destroy Test".to_string()), - unit_name: Some("DEST".to_string()), - url: None, - metadata_hash: None, - manager: Some(sender_address.clone()), - reserve: None, - freeze: None, - clawback: None, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_asset_create(asset_create_params)?; - - let create_result = composer.send(None).await?; - let asset_id = create_result.results[0] - .confirmation - .asset_id - .ok_or("Failed to get asset ID")?; - - // Now destroy the asset - let asset_destroy_params = AssetDestroyParams { - sender: sender_address.clone(), - asset_id, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_asset_destroy(asset_destroy_params)?; - - let result = composer.send(None).await?; - let confirmation = &result.results[0].confirmation; - - // Assert transaction was confirmed - assert!( - confirmation.confirmed_round.is_some(), - "Transaction should be confirmed" - ); - assert!( - confirmation.confirmed_round.unwrap() > 0, - "Confirmed round should be greater than 0" - ); - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_asset_create_validation_errors( - #[future] algorand_fixture: Result>, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - // Test asset creation with multiple validation errors - let invalid_asset_create_params = AssetCreateParams { - sender: sender_address.clone(), - total: 0, // Invalid: should be > 0 (will be caught by transact validation) - decimals: Some(25), // Invalid: should be <= 19 - default_frozen: Some(false), - asset_name: Some("a".repeat(50)), // Invalid: should be <= 32 bytes - unit_name: Some("VERYLONGUNITNAME".to_string()), // Invalid: should be <= 8 bytes - url: Some(format!("https://{}", "a".repeat(100))), // Invalid: should be <= 96 bytes - metadata_hash: None, - manager: Some(sender_address.clone()), - reserve: None, - freeze: None, - clawback: None, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_asset_create(invalid_asset_create_params)?; - - // The validation should fail when building the transaction group - let result = composer.build().await; - - // The build should return an error due to validation failures - match result { - Ok(_) => Err("Build with invalid asset create parameters should fail".into()), - Err(error) => { - let error_string = error.to_string(); - - // Check that the error contains validation-related messages from the transact crate - assert!( - error_string.contains("validation") - || error_string.contains("Total") - || error_string.contains("Decimals") - || error_string.contains("Asset name") - || error_string.contains("Unit name") - || error_string.contains("URL"), - "Error should contain validation failure details: {}", - error_string - ); - Ok(()) - } - } -} diff --git a/crates/algokit_utils/tests/transactions/composer/asset_freeze.rs b/crates/algokit_utils/tests/transactions/composer/asset_freeze.rs deleted file mode 100644 index fec69e59c..000000000 --- a/crates/algokit_utils/tests/transactions/composer/asset_freeze.rs +++ /dev/null @@ -1,270 +0,0 @@ -use crate::common::{AlgorandFixtureResult, TestResult, algorand_fixture}; -use algokit_transact::Transaction; -use algokit_utils::transactions::AssetOptInParams; -use algokit_utils::transactions::{ - AssetCreateParams, AssetFreezeParams, AssetTransferParams, AssetUnfreezeParams, -}; -use rstest::*; - -#[rstest] -#[tokio::test] -async fn test_asset_freeze_unfreeze( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - // This integration test validates the complete asset freeze/unfreeze cycle by: - // - // SETUP PHASE: - // 1. Create an asset with the asset creator account set as the freeze account (unfrozen by default) - // 2. Target account opts into the asset - // 3. Transfer asset units to target account - // - // FREEZE PHASE: - // 4. Freeze the asset for target account using AssetFreezeParams - // 5. Verify freeze transaction was confirmed and has correct structure - // 6. Verify account holding shows asset is frozen via algod API - // 7. Prove freeze works by attempting transfer (should fail) - // - // UNFREEZE PHASE: - // 8. Unfreeze the asset for target account using AssetUnfreezeParams - // 9. Verify unfreeze transaction was confirmed and has correct structure - // 10. Verify account holding shows asset is no longer frozen via algod API - // 11. Prove unfreeze works by successfully transferring the asset - - let mut algorand_fixture = algorand_fixture.await?; - let asset_creator_addr = algorand_fixture.test_account.account().address(); - - // Create a target account to hold the asset - let target_account = algorand_fixture.generate_account(None).await?; - let target_addr = target_account.account().address(); - - // Create a composer for the target account that can send transactions - let target_composer = algorand_fixture.algorand_client.new_composer(None); - - // SETUP PHASE - - // Step 1: Create an asset with the asset creator account set as the freeze account - let asset_create_params = AssetCreateParams { - sender: asset_creator_addr.clone(), - total: 1_000_000, - decimals: Some(0), - default_frozen: Some(false), - asset_name: Some("Freeze Test Asset".to_string()), - unit_name: Some("FTA".to_string()), - url: None, - metadata_hash: None, - manager: Some(asset_creator_addr.clone()), - reserve: None, - freeze: Some(asset_creator_addr.clone()), - clawback: None, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_asset_create(asset_create_params)?; - - let create_result = composer.send(None).await?; - let asset_id = create_result.results[0] - .confirmation - .asset_id - .expect("Failed to get asset ID"); - - // Step 2: Target account opts into the asset - let asset_opt_in_params = AssetOptInParams { - sender: target_addr.clone(), - asset_id, - ..Default::default() - }; - - let mut composer = target_composer.clone(); - composer.add_asset_opt_in(asset_opt_in_params)?; - - let opt_in_result = composer.send(None).await?; - - assert!( - opt_in_result.results[0] - .confirmation - .confirmed_round - .is_some(), - "Asset opt-in should be confirmed" - ); - - // Step 3: Send some asset units to the target account - let asset_transfer_params = AssetTransferParams { - sender: asset_creator_addr.clone(), - asset_id, - amount: 1000, - receiver: target_addr.clone(), - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_asset_transfer(asset_transfer_params)?; - - let transfer_result = composer.send(None).await?; - - assert!( - transfer_result.results[0] - .confirmation - .confirmed_round - .is_some(), - "Asset transfer should be confirmed" - ); - - // FREEZE PHASE - - // Step 4: Freeze the asset for the target account - let asset_freeze_params = AssetFreezeParams { - sender: asset_creator_addr.clone(), - asset_id, - target_address: target_addr.clone(), - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_asset_freeze(asset_freeze_params)?; - - let freeze_result = composer.send(None).await?; - - // Step 5: Verify freeze transaction was confirmed and has correct structure - let freeze_confirmation = &freeze_result.results[0].confirmation; - assert!( - freeze_confirmation.confirmed_round.is_some(), - "Asset freeze transaction should be confirmed" - ); - assert!( - freeze_confirmation.confirmed_round.unwrap() > 0, - "Freeze confirmed round should be greater than 0" - ); - - match &freeze_confirmation.txn.transaction { - Transaction::AssetFreeze(txn) => { - assert_eq!(txn.asset_id, asset_id, "Asset ID should match"); - assert_eq!(txn.freeze_target, target_addr, "Freeze target should match"); - assert!(txn.frozen, "Asset should be frozen"); - } - _ => return Err("Transaction should be an AssetFreeze transaction".into()), - } - - // Step 6: Verify account holding shows asset is frozen via algod API - let account_info = algorand_fixture - .algod - .account_information(&target_addr.to_string(), None) - .await?; - - let assets = account_info.assets.expect("Account should have assets"); - - let asset_holding = assets - .iter() - .find(|asset| asset.asset_id == asset_id) - .expect("Target account should have the asset"); - - assert!( - asset_holding.is_frozen, - "Asset should be frozen in account holding" - ); - - // Step 7: Prove freeze works by attempting transfer (should fail) - let attempt_transfer_params = AssetTransferParams { - sender: target_addr.clone(), - asset_id, - amount: 100, - receiver: asset_creator_addr.clone(), - ..Default::default() - }; - - let mut composer = target_composer.clone(); - composer.add_asset_transfer(attempt_transfer_params)?; - - let transfer_attempt_result = composer.send(None).await; - - assert!( - transfer_attempt_result.is_err(), - "Transfer of frozen asset should fail" - ); - // Verify the error is related to the asset being frozen - let error_message = transfer_attempt_result.unwrap_err().to_string(); - assert!( - error_message.contains(&format!("asset {} frozen", asset_id)), - "Error should indicate the asset is frozen: {}", - error_message - ); - - // UNFREEZE PHASE - - // Step 8: Unfreeze the asset for the target account - let asset_unfreeze_params = AssetUnfreezeParams { - sender: asset_creator_addr.clone(), - asset_id, - target_address: target_addr.clone(), - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_asset_unfreeze(asset_unfreeze_params)?; - - let unfreeze_result = composer.send(None).await?; - - // Step 9: Verify unfreeze transaction was confirmed and has correct structure - let unfreeze_confirmation = &unfreeze_result.results[0].confirmation; - assert!( - unfreeze_confirmation.confirmed_round.is_some(), - "Asset unfreeze transaction should be confirmed" - ); - assert!( - unfreeze_confirmation.confirmed_round.unwrap() > 0, - "Unfreeze confirmed round should be greater than 0" - ); - - match &unfreeze_confirmation.txn.transaction { - Transaction::AssetFreeze(txn) => { - assert_eq!(txn.asset_id, asset_id, "Asset ID should match"); - assert_eq!(txn.freeze_target, target_addr, "Freeze target should match"); - assert!(!txn.frozen, "Asset should be unfrozen"); - } - _ => return Err("Transaction should be an AssetFreeze transaction".into()), - } - - // Step 10: Verify account holding shows asset is no longer frozen via algod API - let account_info_after = algorand_fixture - .algod - .account_information(&target_addr.to_string(), None) - .await?; - - let assets_after = account_info_after - .assets - .expect("Account should still have assets"); - - let asset_holding_after = assets_after - .iter() - .find(|asset| asset.asset_id == asset_id) - .expect("Target account should still have the asset"); - - assert!( - !asset_holding_after.is_frozen, - "Asset should no longer be frozen in account holding" - ); - - // Step 11: Prove unfreeze works by successfully transferring the asset - let test_transfer_params = AssetTransferParams { - sender: target_addr.clone(), - asset_id, - amount: 100, - receiver: asset_creator_addr.clone(), - ..Default::default() - }; - - let mut composer = target_composer.clone(); - composer.add_asset_transfer(test_transfer_params)?; - - let test_transfer_result = composer.send(None).await?; - - assert!( - test_transfer_result.results[0] - .confirmation - .confirmed_round - .is_some(), - "Test asset transfer should be confirmed, proving asset is unfrozen" - ); - - Ok(()) -} diff --git a/crates/algokit_utils/tests/transactions/composer/asset_transfer.rs b/crates/algokit_utils/tests/transactions/composer/asset_transfer.rs deleted file mode 100644 index 1d4bd7173..000000000 --- a/crates/algokit_utils/tests/transactions/composer/asset_transfer.rs +++ /dev/null @@ -1,97 +0,0 @@ -use crate::common::{AlgorandFixtureResult, TestResult, algorand_fixture}; -use algokit_utils::AssetCreateParams; -use algokit_utils::{AssetOptInParams, AssetTransferParams}; -use rstest::*; - -#[rstest] -#[tokio::test] -async fn test_asset_transfer_transaction( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - let asset_creator_address = algorand_fixture.test_account.account().address(); - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - - composer.add_asset_create(AssetCreateParams { - sender: asset_creator_address.clone(), - total: 10, - decimals: Some(0), - default_frozen: Some(false), - ..Default::default() - })?; - - let asset_create_result = composer.send(None).await?; - let asset_id = asset_create_result.results[0] - .confirmation - .asset_id - .ok_or("Failed to get asset ID")?; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - - let asset_receiver = algorand_fixture.generate_account(None).await?; - let asset_receive_address = asset_receiver.account().address(); - - composer.add_asset_opt_in(AssetOptInParams { - sender: asset_receive_address.clone(), - asset_id, - ..Default::default() - })?; - composer.add_asset_transfer(AssetTransferParams { - sender: asset_creator_address.clone(), - asset_id, - receiver: asset_receive_address.clone(), - amount: 1, - ..Default::default() - })?; - - let send_result = composer.send(None).await?; - - let asset_opt_in_transaction = &send_result.results[0].confirmation.txn.transaction; - let asset_transfer_transaction = &send_result.results[1].confirmation.txn.transaction; - - match asset_opt_in_transaction { - algokit_transact::Transaction::AssetTransfer(asset_opt_in_fields) => { - assert_eq!( - asset_opt_in_fields.header.sender, - asset_receive_address.clone(), - "Account opting in should be the asset user" - ); - assert_eq!( - asset_opt_in_fields.receiver, - asset_receive_address.clone(), - "Sender and receiver should be the same for opt-in" - ); - assert_eq!( - asset_opt_in_fields.asset_id, - asset_id.clone(), - "Asset ID should match the created asset" - ); - assert_eq!( - asset_opt_in_fields.amount, 0, - "Amount should be 0 for opt-in" - ); - } - _ => return Err("Transaction should be an asset transfer transaction".into()), - } - match asset_transfer_transaction { - algokit_transact::Transaction::AssetTransfer(asset_transfer_fields) => { - assert_eq!( - asset_transfer_fields.header.sender, asset_creator_address, - "Sender should be the asset creator" - ); - assert_eq!( - asset_transfer_fields.receiver, asset_receive_address, - "Receiver should be the asset user" - ); - assert_eq!( - asset_transfer_fields.asset_id, asset_id, - "Asset ID should match the created asset" - ); - assert_eq!(asset_transfer_fields.amount, 1, "Amount should be 1"); - } - _ => return Err("Transaction should be an asset transfer transaction".into()), - } - - Ok(()) -} diff --git a/crates/algokit_utils/tests/transactions/composer/inner_fee_coverage.rs b/crates/algokit_utils/tests/transactions/composer/inner_fee_coverage.rs deleted file mode 100644 index 0166dc928..000000000 --- a/crates/algokit_utils/tests/transactions/composer/inner_fee_coverage.rs +++ /dev/null @@ -1,1895 +0,0 @@ -use crate::common::{ - AlgorandFixture, AlgorandFixtureResult, LocalNetDispenser, TestResult, algorand_fixture, -}; -use algokit_abi::abi_type::BitSize; -use algokit_abi::{ABIMethod, ABIType, ABIValue}; -use algokit_test_artifacts::{inner_fee_contract, nested_contract}; -use algokit_transact::{Address, TransactionId}; -use algokit_utils::TransactionComposer; -use algokit_utils::transactions::TransactionComposerConfig; -use algokit_utils::transactions::composer::ResourcePopulation; -use algokit_utils::{AppCallParams, AppCreateParams, PaymentParams}; -use base64::{Engine, prelude::BASE64_STANDARD}; -use rstest::*; -use serde::Deserialize; -use std::str::FromStr; - -#[fixture] -async fn fixture( - #[default(3)] inner_fee_app_count: u8, - #[default(0)] nested_app_count: u8, - #[future] algorand_fixture: AlgorandFixtureResult, -) -> FixtureResult { - let algorand_fixture = algorand_fixture.await?; - - let sender_address = algorand_fixture.test_account.account().address(); - - let mut app_ids = Vec::new(); - - for i in 1..=inner_fee_app_count { - let app_id = - deploy_inner_fee_app(&algorand_fixture, &format!("inner_fee_app_{}", i)).await?; - app_ids.push(app_id); - } - - for i in 1..=nested_app_count { - let app_id = deploy_nested_app(&algorand_fixture, &format!("nested_app_{}", i)).await?; - app_ids.push(app_id); - } - - fund_app_accounts(&algorand_fixture, &app_ids, 500_000).await?; - - Ok(Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors: MethodSelectors { - no_op: ABIMethod::from_str("no_op()void")?.selector()?, - send_inners_with_fees: ABIMethod::from_str( - "send_inners_with_fees(uint64,uint64,(uint64,uint64,uint64,uint64,uint64[]))void", - )? - .selector()?, - send_inners_with_fees_2: ABIMethod::from_str("send_inners_with_fees_2(uint64,uint64,(uint64,uint64,uint64[],uint64,uint64,uint64[]))void")? - .selector()?, - nested_txn_arg: ABIMethod::from_str("nestedTxnArg(pay,appl)uint64")?.selector()?, - burn_ops: ABIMethod::from_str("burn_ops(uint64)void")?.selector()?, - burn_ops_readonly: ABIMethod::from_str("burn_ops_readonly(uint64)void")?.selector()?, - }, - abi_types: ABITypes { - uint64: ABIType::Uint(BitSize::new(64)?), - fees_tuple: ABIType::from_str("(uint64,uint64,uint64,uint64,uint64[])")?, - fees_2_tuple: ABIType::from_str("(uint64,uint64,uint64[],uint64,uint64,uint64[])")?, - }, - }) -} - -#[rstest] -#[tokio::test] -/// Errors when no max fee is supplied -async fn test_errors_when_no_max_fee_supplied( - #[with(1)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types: _, - } = fixture.await?; - let app_id = app_ids[0]; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - - let params = AppCallParams { - sender: sender_address.clone(), - app_id, - args: Some(vec![method_selectors.no_op]), - ..Default::default() - }; - composer.add_app_call(params)?; - let result = composer.send(None).await; - - assert!( - result.is_err() - && result - .as_ref() - .unwrap_err() - .to_string() - .contains("Please provide a max fee"), - "Unexpected result, got: {:?}", - result - ); - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// Errors when inner transaction fees are not covered and coverAppCallInnerTransactionFees is disabled -async fn test_errors_when_inner_fees_not_covered_and_fee_coverage_disabled( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = - algorand_fixture - .algorand_client - .new_composer(Some(TransactionComposerConfig { - cover_app_call_inner_transaction_fees: false, - populate_app_call_resources: ResourcePopulation::Enabled { - use_access_list: false, - }, // Ensure the same behaviour when simulating due to resource population - })); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - - let fees_tuple = create_fees_tuple(0, 0, 0, 0, vec![0, 0]); - let txn_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(7000), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_params)?; - - let result = composer.send(None).await; - - assert!( - result.is_err() - && result - .as_ref() - .unwrap_err() - .to_string() - .contains("fee too small"), - "Unexpected result, got: {:?}", - result - ); - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// Does not alter fee when app call has no inners -async fn test_does_not_alter_fee_when_no_inners( - #[with(1)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types: _, - } = fixture.await?; - let app_id = app_ids[0]; - - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - - let expected_fee = 1000u64; - - let txn_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(2000), - app_id, - args: Some(vec![method_selectors.no_op]), - ..Default::default() - }; - composer.add_app_call(txn_params.clone())?; - - let result = composer.send(None).await?; - - assert_eq!(result.results.len(), 1); - let transaction_fee = result.results[0] - .confirmation - .txn - .transaction - .header() - .fee - .unwrap_or(0); - assert_eq!(transaction_fee, expected_fee); - assert_min_fee( - &|params| algorand_fixture.algorand_client.new_composer(params), - &txn_params, - transaction_fee, - ) - .await; - - Ok(()) -} - -#[rstest] -#[tokio::test] -// Alters fee, handling when no inner fees have been covered -async fn test_alters_fee_no_inner_fees_covered( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - let expected_fee = 7000u64; - - // Create an app call transaction that has no inner fees covered - let fees_tuple = create_fees_tuple(0, 0, 0, 0, vec![0, 0]); - let txn_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(expected_fee), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_params.clone())?; - - let result = composer.send(None).await?; - - assert_eq!(result.results.len(), 1); - let transaction_fee = result.results[0] - .confirmation - .txn - .transaction - .header() - .fee - .unwrap_or(0); - assert_eq!(transaction_fee, expected_fee); - assert_min_fee( - &|params| algorand_fixture.algorand_client.new_composer(params), - &txn_params, - transaction_fee, - ) - .await; - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// Alters fee, handling when all inner fees have been covered -async fn test_alters_fee_all_inner_fees_covered( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - - let expected_fee = 1000u64; - - // Create an app call transaction that has all inner fees covered - let fees_tuple = create_fees_tuple(1000, 1000, 1000, 1000, vec![1000, 1000]); - let txn_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(expected_fee), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_params.clone())?; - - let result = composer.send(None).await?; - - assert_eq!(result.results.len(), 1); - let actual_fees = result - .results - .iter() - .map(|r| &r.confirmation) - .map(|c| c.txn.transaction.header().fee.unwrap_or(0)) - .collect::>(); - assert_eq!(actual_fees[0], expected_fee); - assert_min_fee( - &|params| algorand_fixture.algorand_client.new_composer(params), - &txn_params, - expected_fee, - ) - .await; - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// Alters fee, handling when some inner fees have been covered or partially covered -async fn test_alters_fee_some_inner_fees_covered( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - - let expected_fee = 5300u64; - - // This tuple represents some inner fees being partially covered - let fees_tuple = create_fees_tuple(1000, 0, 200, 0, vec![500, 0]); - - let txn_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(expected_fee), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_params.clone())?; - - let result = composer.send(None).await?; - - assert_eq!(result.results.len(), 1); - let actual_fees = result - .results - .iter() - .map(|r| &r.confirmation) - .map(|c| c.txn.transaction.header().fee.unwrap_or(0)) - .collect::>(); - assert_eq!(actual_fees[0], expected_fee); - assert_min_fee( - &|params| algorand_fixture.algorand_client.new_composer(params), - &txn_params, - expected_fee, - ) - .await; - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// Alters fee, handling when some inner fees have a surplus -async fn test_alters_fee_some_inner_fees_surplus( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - - let expected_fee = 2000u64; - - // Create an app call transaction that has some inner fees with surplus - let fees_tuple = create_fees_tuple(0, 1000, 5000, 0, vec![0, 50]); - - let txn_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(expected_fee), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_params.clone())?; - - let result = composer.send(None).await?; - - assert_eq!(result.results.len(), 1); - let actual_fees = result - .results - .iter() - .map(|r| &r.confirmation) - .map(|c| c.txn.transaction.header().fee.unwrap_or(0)) - .collect::>(); - assert_eq!(actual_fees[0], expected_fee); - assert_min_fee( - &|params| algorand_fixture.algorand_client.new_composer(params), - &txn_params, - expected_fee, - ) - .await; - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// alters fee, handling expensive abi method calls that use ensure_budget to op-up -async fn test_alters_fee_expensive_abi_method_calls( - #[with(1)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let app_id = app_ids[0]; - let expected_fee = 10_000u64; - - let op_budget_encoded = abi_types.uint64.encode(&ABIValue::from(6200u64))?; - - let params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(expected_fee + 2_000), - app_id, - args: Some(vec![method_selectors.burn_ops, op_budget_encoded]), - app_references: None, - ..Default::default() - }; - composer.add_app_call(params.clone())?; - - let result = composer.send(None).await?; - - assert_eq!(result.results.len(), 1); - let actual_fees: Vec = result - .results - .iter() - .map(|r| &r.confirmation) - .map(|c| c.txn.transaction.header().fee.unwrap_or(0)) - .collect(); - assert_eq!(actual_fees[0], expected_fee); - - assert_min_fee( - &|params| algorand_fixture.algorand_client.new_composer(params), - ¶ms, - expected_fee, - ) - .await; - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// Errors when max fee is too small to cover inner transaction fees -async fn test_errors_when_max_fee_too_small( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - let expected_fee = 7000u64; - - // Create an app call transaction that has no inner fees covered - let fees_tuple = create_fees_tuple(0, 0, 0, 0, vec![0, 0]); - let params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(expected_fee - 1), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(params.clone())?; - - let result = composer.send(None).await; - - assert!( - result.is_err() - && result - .as_ref() - .unwrap_err() - .to_string() - .contains("Fees were too small"), - "Unexpected result, got: {:?}", - result - ); - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// Errors when static fee is too small to cover inner transaction fees -async fn test_errors_when_static_fee_too_small( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - let expected_fee = 7000u64; - - // Create an app call transaction that has no inner fees covered - let fees_tuple = create_fees_tuple(0, 0, 0, 0, vec![0, 0]); - let params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(expected_fee - 1), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(params.clone())?; - - let result = composer.send(None).await; - - assert!( - result.is_err() - && result - .as_ref() - .unwrap_err() - .to_string() - .contains("Fees were too small"), - "Unexpected result, got: {:?}", - result - ); - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// Does not alter a static fee with surplus -async fn test_does_not_alter_static_fee_with_surplus( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - let expected_fee = 6000u64; - - // Create an app call transaction that has a static fee with surplus - let fees_tuple = create_fees_tuple(1000, 0, 200, 0, vec![500, 0]); - let app_call_params = AppCallParams { - sender: sender_address.clone(), - static_fee: Some(expected_fee), // Static fee with surplus - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - - composer.add_app_call(app_call_params)?; - - let result = composer.send(None).await?; - - assert_eq!(result.results.len(), 1); - let actual_fees: Vec = result - .results - .iter() - .map(|r| &r.confirmation) - .map(|c| c.txn.transaction.header().fee.unwrap_or(0)) - .collect(); - assert_eq!(actual_fees[0], expected_fee); - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// alters fee, handling multiple app calls in a group that send inners with varying fees -async fn test_alters_fee_multiple_app_calls_in_group( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - - // Create an app call transaction that has varying inner fees - let txn_1_expected_fee = 5800u64; - let txn_1_fee_tuple = create_fees_tuple(0, 1000, 0, 0, vec![200, 0]); - let txn_1_params = AppCallParams { - sender: sender_address.clone(), - static_fee: Some(txn_1_expected_fee), - note: Some(b"txn1".to_vec()), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees.clone(), - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&txn_1_fee_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_1_params.clone())?; - - // Create an app call transaction that has different varying inner fees - let txn_2_expected_fee = 6000u64; - let txn_2_fee_tuple = create_fees_tuple(1000, 0, 0, 0, vec![0, 0]); - let txn_2_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(txn_2_expected_fee), - note: Some(b"txn2".to_vec()), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&txn_2_fee_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_2_params.clone())?; - - let result = composer.send(None).await?; - - assert_eq!(result.results.len(), 2); - let actual_fees: Vec = result - .results - .iter() - .map(|r| &r.confirmation) - .map(|c| c.txn.transaction.header().fee.unwrap_or(0)) - .collect(); - assert_eq!(actual_fees[0], txn_1_expected_fee); - assert_eq!(actual_fees[1], txn_2_expected_fee); - - let new_composer = &|params| algorand_fixture.algorand_client.new_composer(params); - assert_min_fee(new_composer, &txn_1_params, txn_1_expected_fee).await; - assert_min_fee(new_composer, &txn_2_params, txn_2_expected_fee).await; - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// Does not alter fee when another transaction in the group covers the inner fees -async fn test_does_not_alter_fee_when_group_covers_inner_fees( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - - // Create a payment transaction that will cover the inner fees of transaction 2 - let txn_1_expected_fee = 8000u64; - let txn_1_params = PaymentParams { - sender: sender_address.clone(), - static_fee: Some(txn_1_expected_fee), - receiver: sender_address.clone(), - amount: 0, - ..Default::default() - }; - composer.add_payment(txn_1_params)?; - - // Create an app call transaction that has inner fees covered by the above payment - let fees_tuple = create_fees_tuple(0, 0, 0, 0, vec![0, 0]); - let txn_2_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(txn_1_expected_fee), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_2_params)?; - - let result = composer.send(None).await?; - - assert_eq!(result.results.len(), 2); - let actual_fees: Vec = result - .results - .iter() - .map(|r| &r.confirmation) - .map(|c| c.txn.transaction.header().fee.unwrap_or(0)) - .collect(); - assert_eq!(actual_fees[0], txn_1_expected_fee); - // We could technically reduce this to 0, however it adds more complexity and is probably unlikely to be a common use case - assert_eq!(actual_fees[1], 1000); - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// Alters fee, handling nested abi method calls -async fn test_alters_fee_nested_abi_method_call( - #[with(3, 1)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let [app_id_1, app_id_2, app_id_3, app_id_4] = [app_ids[0], app_ids[1], app_ids[2], app_ids[3]]; - let expected_fee = 2000u64; - - // Create a payment transaction that will be used as a nested argument - let txn_1_params = PaymentParams { - sender: sender_address.clone(), - static_fee: Some(1500), - receiver: sender_address.clone(), - amount: 0, - ..Default::default() - }; - composer.add_payment(txn_1_params.clone())?; - - // Create an app call transaction that will be used as a nested argument - let fees_tuple = create_fees_tuple(0, 0, 2000, 0, vec![0, 0]); - let txn_2_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(6000), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_2_params.clone())?; - - // Create the app call that will use the nested transaction - let txn_3_params = AppCallParams { - sender: sender_address.clone(), - static_fee: Some(expected_fee), - app_id: app_id_4, - args: Some(vec![method_selectors.nested_txn_arg]), - app_references: None, - ..Default::default() - }; - composer.add_app_call(txn_3_params.clone())?; - - let result = composer.send(None).await?; - - assert_eq!(result.results.len(), 3); - let actual_fees = result - .results - .iter() - .map(|r| &r.confirmation) - .map(|c| c.txn.transaction.header().fee.unwrap_or(0)) - .collect::>(); - assert_eq!(actual_fees[0], 1500); - assert_eq!(actual_fees[1], 3500); - assert_eq!(actual_fees[2], expected_fee); - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// Errors when nested maxFee is below the calculated fee -async fn test_errors_when_nested_max_fee_below_calculated( - #[with(3, 1)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let [app_id_1, app_id_2, app_id_3, app_id_4] = [app_ids[0], app_ids[1], app_ids[2], app_ids[3]]; - - // Create a payment transaction that will be used as a nested argument - let txn_1_params = PaymentParams { - sender: sender_address.clone(), - receiver: sender_address.clone(), - amount: 0, - ..Default::default() - }; - composer.add_payment(txn_1_params)?; - - // Create an app call transaction that will be used as a nested argument - // This transaction has an insufficient max fee - let fees_tuple = create_fees_tuple(0, 0, 2000, 0, vec![0, 0]); - let txn_2_max_fee = 2000; // Too low for the calculated fee - let txn_2_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(txn_2_max_fee), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_2_params)?; - - // Create an app call transaction that will be used as a nested argument - let txn_3_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(10_000), - app_id: app_id_4, - args: Some(vec![method_selectors.nested_txn_arg]), - app_references: None, - ..Default::default() - }; - composer.add_app_call(txn_3_params)?; - - let result = composer.send(None).await; - - assert!( - result.is_err() - && result.as_ref().unwrap_err().to_string().contains( - format!( - "fee {} µALGO is greater than max of {}", - 5000, txn_2_max_fee - ) - .as_str() - ), - "Unexpected result, got: {:?}", - result - ); - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// Alters fee, allocating surplus fees to the most fee constrained transaction first -async fn test_alters_fee_allocating_surplus_to_most_constrained( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - - // Create an app call transaction with inners that have no fees - let fees_tuple_1 = create_fees_tuple(0, 0, 0, 0, vec![0, 0]); - let txn_1_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(2000), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees.clone(), - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple_1)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_1_params)?; - - // Create a payment transaction with large static fee - let txn_2_params = PaymentParams { - sender: sender_address.clone(), - static_fee: Some(7500), - receiver: sender_address.clone(), - amount: 0, - ..Default::default() - }; - composer.add_payment(txn_2_params)?; - - // Create a payment transaction with static fee of 0 - let txn_3_params = PaymentParams { - sender: sender_address.clone(), - static_fee: Some(0), - receiver: sender_address.clone(), - amount: 0, - ..Default::default() - }; - composer.add_payment(txn_3_params)?; - - let result = composer.send(None).await?; - - assert_eq!(result.results.len(), 3); - let actual_fees = result - .results - .iter() - .map(|r| &r.confirmation) - .map(|c| c.txn.transaction.header().fee.unwrap_or(0)) - .collect::>(); - assert_eq!(actual_fees[0], 1500); - assert_eq!(actual_fees[1], 7500); - assert_eq!(actual_fees[2], 0); - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// Alters fee, handling a large inner fee surplus pooling to lower siblings -async fn test_alters_fee_large_surplus_pooling_to_lower_siblings( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - let expected_fee = 7000u64; - - // Create an app call transaction that has a large inner fee surplus pooling to lower siblings - let fees_tuple = create_fees_tuple(0, 0, 0, 0, vec![0, 0, 20_000, 0, 0, 0]); - let txn_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(expected_fee), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_params.clone())?; - - let result = composer.send(None).await?; - - assert_eq!(result.results.len(), 1); - let actual_fees = result - .results - .iter() - .map(|r| &r.confirmation) - .map(|c| c.txn.transaction.header().fee.unwrap_or(0)) - .collect::>(); - assert_eq!(actual_fees[0], expected_fee); - assert_min_fee( - &|params| algorand_fixture.algorand_client.new_composer(params), - &txn_params, - expected_fee, - ) - .await; - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// alters fee, handling a inner fee surplus pooling to some lower siblings -async fn test_alters_fee_surplus_pooling_to_some_siblings( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - let expected_fee = 6300u64; - - // Create an app call transaction that has a inner fee surplus pooling to some lower siblings - let fees_tuple = create_fees_tuple(0, 0, 2200, 0, vec![0, 0, 2500, 0, 0, 0]); - let txn_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(expected_fee), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_params.clone())?; - - let result = composer.send(None).await?; - - assert_eq!(result.results.len(), 1); - let actual_fees = result - .results - .iter() - .map(|r| &r.confirmation) - .map(|c| c.txn.transaction.header().fee.unwrap_or(0)) - .collect::>(); - assert_eq!(actual_fees[0], expected_fee); - assert_min_fee( - &|params| algorand_fixture.algorand_client.new_composer(params), - &txn_params, - expected_fee, - ) - .await; - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// Alters fee, handling a large inner fee surplus with no pooling -async fn test_alters_fee_large_surplus_no_pooling( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - let expected_fee = 10_000u64; - - // Create an app call transaction that has a large inner fee surplus with no pooling - let fees_tuple = create_fees_tuple(0, 0, 0, 0, vec![0, 0, 0, 0, 0, 20_000]); - let txn_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(expected_fee), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_params.clone())?; - - let result = composer.send(None).await?; - - assert_eq!(result.results.len(), 1); - let actual_fees = result - .results - .iter() - .map(|r| &r.confirmation) - .map(|c| c.txn.transaction.header().fee.unwrap_or(0)) - .collect::>(); - assert_eq!(actual_fees[0], expected_fee); - assert_min_fee( - &|params| algorand_fixture.algorand_client.new_composer(params), - &txn_params, - expected_fee, - ) - .await; - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// Alters fee, handling multiple inner fee surplus poolings to lower siblings -async fn test_alters_fee_multiple_surplus_poolings( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - let expected_fee = 7100u64; - - // Create an app call transaction that has multiple inner fee surplus poolings to lower siblings - let fees_tuple = ABIValue::Array(vec![ - ABIValue::from(0u64), - ABIValue::from(1200u64), - ABIValue::from(vec![ - ABIValue::from(0u64), - ABIValue::from(0u64), - ABIValue::from(4900u64), - ABIValue::from(0u64), - ABIValue::from(0u64), - ABIValue::from(0u64), - ]), - ABIValue::from(200u64), - ABIValue::from(1100u64), - ABIValue::from(vec![ - ABIValue::from(0u64), - ABIValue::from(0u64), - ABIValue::from(2500u64), - ABIValue::from(0u64), - ABIValue::from(0u64), - ABIValue::from(0u64), - ]), - ]); - let txn_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(expected_fee), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees_2, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_2_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_params.clone())?; - - let result = composer.send(None).await?; - - assert_eq!(result.results.len(), 1); - let actual_fees = result - .results - .iter() - .map(|r| &r.confirmation) - .map(|c| c.txn.transaction.header().fee.unwrap_or(0)) - .collect::>(); - assert_eq!(actual_fees[0], expected_fee); - assert_min_fee( - &|params| algorand_fixture.algorand_client.new_composer(params), - &txn_params, - expected_fee, - ) - .await; - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// Errors when maxFee is below the calculated fee -async fn test_errors_when_max_fee_below_calculated( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - - // Create an app call transaction that has no inner fees covered - let fees_tuple = create_fees_tuple(0, 0, 0, 0, vec![0, 0]); - let txn_1_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(1200), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_1_params)?; - - // Create an app call transaction that has large max fee, - // without it the simulate call to get the execution info would fail - let txn_2_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(10_000), - app_id: app_id_1, - args: Some(vec![method_selectors.no_op]), - app_references: None, - ..Default::default() - }; - composer.add_app_call(txn_2_params)?; - - let result = composer.send(None).await; - - assert!( - result.is_err() - && result - .as_ref() - .unwrap_err() - .to_string() - .contains("fee 7000 µALGO is greater than max of 1200"), - "Unexpected result, got: {:?}", - result - ); - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// Errors when staticFee is below the calculated fee -async fn test_errors_when_static_fee_below_calculated( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - - // Create an app call transaction that has no inner fees covered - let fees_tuple = create_fees_tuple(0, 0, 0, 0, vec![0, 0]); - let params = AppCallParams { - sender: sender_address.clone(), - static_fee: Some(5000), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(params)?; - - // Create an app call transaction that has large max fee, - // without it the simulate call to get the execution info would fail - let txn_2_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(10_000), - app_id: app_id_1, - args: Some(vec![method_selectors.no_op]), - app_references: None, - ..Default::default() - }; - composer.add_app_call(txn_2_params)?; - - let result = composer.send(None).await; - - assert!( - result.is_err() - && result - .as_ref() - .unwrap_err() - .to_string() - .contains("fee 7000 µALGO is greater than max of 5000"), - "Unexpected result, got: {:?}", - result - ); - - Ok(()) -} - -#[rstest] -#[tokio::test] -/// Errors when staticFee for non app call transaction is too low -async fn test_errors_when_static_fee_too_low_for_non_app_call( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - let fees_tuple = create_fees_tuple(0, 0, 0, 0, vec![0, 0]); - - // Create an app call transaction with both high static and max fee - let txn_1_params = AppCallParams { - sender: sender_address.clone(), - static_fee: Some(13_000), - max_fee: Some(14_000), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees.clone(), - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_1_params)?; - - // Create an app call transaction with low static - let txn_2_params = AppCallParams { - sender: sender_address.clone(), - static_fee: Some(1000), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_2_params)?; - - // Payment transaction with insufficient static fee - let txn_3_params = PaymentParams { - sender: sender_address.clone(), - static_fee: Some(500), - receiver: sender_address.clone(), - amount: 0, - ..Default::default() - }; - composer.add_payment(txn_3_params)?; - - let result = composer.send(None).await; - - assert!( - result.is_err() - && result - .as_ref() - .unwrap_err() - .to_string() - .contains("fee of 500 µALGO is required for non app call transaction"), - "Unexpected result, got: {:?}", - result - ); - - Ok(()) -} - -#[rstest] -#[case(true)] -#[case(false)] -#[tokio::test] -#[ignore = "Readonly method support not yet implemented"] -/// Uses fixed opcode budget without op-up inner transactions -async fn test_readonly_fixed_opcode_budget( - #[with(1)] - #[future] - fixture: FixtureResult, - #[case] cover_inner_fees: bool, -) -> TestResult { - // This test verifies that readonly calls use the fixed max opcode budget and don't require inner transactions for op-ups, - // regardless of coverAppCallInnerTransactionFees setting. - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = - algorand_fixture - .algorand_client - .new_composer(Some(TransactionComposerConfig { - cover_app_call_inner_transaction_fees: cover_inner_fees, - ..Default::default() - })); - let app_id = app_ids[0]; - - let op_budget_encoded = abi_types.uint64.encode(&ABIValue::from(6200u64))?; // This would normally require op-ups via inner transactions - let txn_params = AppCallParams { - sender: sender_address.clone(), - app_id, - args: Some(vec![method_selectors.burn_ops_readonly, op_budget_encoded]), - app_references: None, - ..Default::default() - }; - composer.add_app_call(txn_params)?; - - let result = composer.send(None).await?; - - assert_eq!(result.results.len(), 1); - let actual_fees = result - .results - .iter() - .map(|r| &r.confirmation) - .map(|c| c.txn.transaction.header().fee.unwrap_or(0)) - .collect::>(); - assert_eq!(actual_fees[0], 1000); - assert!(result.results[0].confirmation.inner_txns.is_none()); // No op-up inner transactions needed - - Ok(()) -} - -#[rstest] -#[tokio::test] -#[ignore = "Readonly method support not yet implemented"] -/// Readonly method alters fee when handling inner transactions -async fn test_readonly_alters_fee_handling_inners( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - // TODO: When readonly support is added, some code will be need to force `send_inners_with_fees` to be marked as readonly. - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - let expected_fee = 12_000u64; - - // The expected_fee differs to non readonly method call, as we don't want to run simulate twice (once for resolving the minimum fee and once for the actual transaction result). - // Because no fees are actually paid with readonly calls, we simply use the max_fee value (if set) and skip any minimum fee calculations. - // If this method is running in a non readonly context, the minimum fee would be calculated as 5300. - let fees_tuple = create_fees_tuple(1000, 0, 200, 0, vec![500, 0]); - let txn_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(expected_fee), - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_params)?; - - let result = composer.send(None).await?; - - assert_eq!(result.results.len(), 1); - let actual_fees = result - .results - .iter() - .map(|r| &r.confirmation) - .map(|c| c.txn.transaction.header().fee.unwrap_or(0)) - .collect::>(); - assert_eq!(actual_fees[0], expected_fee); - println!( - "TxnId: {}", - result.results[0].confirmation.txn.transaction.id()? - ); - assert_eq!( - result.results[0] - .confirmation - .inner_txns - .as_ref() - .unwrap() - .len(), - 4 - ); - - Ok(()) -} - -#[rstest] -#[tokio::test] -#[ignore = "Readonly method support not yet implemented"] -/// Readonly Errors when max fee is too small to cover inner transaction fees -async fn test_readonly_errors_when_max_fee_too_small( - #[with(3)] - #[future] - fixture: FixtureResult, -) -> TestResult { - // TODO: When readonly support is added, some code will be need to force `send_inners_with_fees` to be marked as readonly. - let Fixture { - sender_address, - app_ids, - algorand_fixture, - method_selectors, - abi_types, - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(COVER_FEES_GROUP_PARAMS); - let (app_id_1, app_id_2, app_id_3) = (app_ids[0], app_ids[1], app_ids[2]); - - // This tuple represents partial inner fee coverage for readonly context - let fees_tuple = create_fees_tuple(1000, 0, 200, 0, vec![500, 0]); - let txn_params = AppCallParams { - sender: sender_address.clone(), - max_fee: Some(2000), // Too small for the inner fees - app_id: app_id_1, - args: Some(vec![ - method_selectors.send_inners_with_fees, - abi_types.uint64.encode(&app_id_2.into())?, - abi_types.uint64.encode(&app_id_3.into())?, - abi_types.fees_tuple.encode(&fees_tuple)?, - ]), - app_references: Some(vec![app_id_2, app_id_3]), - ..Default::default() - }; - composer.add_app_call(txn_params)?; - - let result = composer.send(None).await; - - assert!( - result.is_err() - && result - .as_ref() - .unwrap_err() - .to_string() - .contains("fees too small"), - "Unexpected result, got: {:?}", - result - ); - - Ok(()) -} - -struct Fixture { - sender_address: Address, - app_ids: Vec, - algorand_fixture: AlgorandFixture, - method_selectors: MethodSelectors, - abi_types: ABITypes, -} - -type FixtureResult = Result>; - -struct MethodSelectors { - no_op: Vec, - send_inners_with_fees: Vec, - send_inners_with_fees_2: Vec, - nested_txn_arg: Vec, - burn_ops: Vec, - burn_ops_readonly: Vec, -} - -struct ABITypes { - uint64: ABIType, - fees_tuple: ABIType, - fees_2_tuple: ABIType, -} - -#[derive(Deserialize)] -struct TealSource { - approval: String, - clear: String, -} - -#[derive(Deserialize)] -struct Arc56AppSpec { - source: Option, -} - -#[derive(Deserialize)] -struct Arc32AppSpec { - source: Option, -} - -const COVER_FEES_GROUP_PARAMS: Option = - Some(TransactionComposerConfig { - cover_app_call_inner_transaction_fees: true, - populate_app_call_resources: ResourcePopulation::Disabled, - }); - -fn get_inner_fee_teal_programs() --> Result<(Vec, Vec), Box> { - let app_spec: Arc56AppSpec = serde_json::from_str(inner_fee_contract::APPLICATION)?; - let teal_source = app_spec.source.unwrap(); - let approval_bytes = BASE64_STANDARD.decode(teal_source.approval)?; - let clear_state_bytes = BASE64_STANDARD.decode(teal_source.clear)?; - Ok((approval_bytes, clear_state_bytes)) -} - -fn get_nested_app_teal_programs() --> Result<(Vec, Vec), Box> { - let app_spec: Arc32AppSpec = serde_json::from_str(nested_contract::APPLICATION)?; - let teal_source = app_spec.source.unwrap(); - let approval_bytes = BASE64_STANDARD.decode(teal_source.approval)?; - let clear_state_bytes = BASE64_STANDARD.decode(teal_source.clear)?; - Ok((approval_bytes, clear_state_bytes)) -} - -async fn deploy_inner_fee_app( - algorand_fixture: &AlgorandFixture, - note: &str, -) -> Result> { - let (approval_teal, clear_state_teal) = get_inner_fee_teal_programs()?; - let approval_compile_result = algorand_fixture - .algod - .teal_compile(approval_teal, None) - .await?; - let clear_state_compile_result = algorand_fixture - .algod - .teal_compile(clear_state_teal, None) - .await?; - - deploy_app( - algorand_fixture, - approval_compile_result.result, - clear_state_compile_result.result, - None, - note, - ) - .await -} - -async fn deploy_nested_app( - algorand_fixture: &AlgorandFixture, - note: &str, -) -> Result> { - let (approval_teal, clear_state_teal) = get_nested_app_teal_programs()?; - let approval_compile_result = algorand_fixture - .algod - .teal_compile(approval_teal, None) - .await?; - let clear_state_compile_result = algorand_fixture - .algod - .teal_compile(clear_state_teal, None) - .await?; - - let create_method = ABIMethod::from_str("createApplication()void")?; - let create_method_selector = create_method.selector()?; - - deploy_app( - algorand_fixture, - approval_compile_result.result, - clear_state_compile_result.result, - Some(vec![create_method_selector]), - note, - ) - .await -} - -async fn deploy_app( - algorand_fixture: &AlgorandFixture, - approval_program: Vec, - clear_state_program: Vec, - args: Option>>, - note: &str, -) -> Result> { - let app_create_params = AppCreateParams { - sender: algorand_fixture.test_account.account().address(), - note: Some(note.as_bytes().to_vec()), - approval_program, - clear_state_program, - args, - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_app_create(app_create_params)?; - - let result = composer.send(None).await?; - - result.results[0] - .confirmation - .app_id - .ok_or_else(|| "No app id returned".into()) -} - -// Helper function to fund app accounts -async fn fund_app_accounts( - algorand_fixture: &AlgorandFixture, - app_ids: &Vec, - amount: u64, -) -> Result<(), Box> { - let mut dispenser = - LocalNetDispenser::new(algorand_fixture.algod.clone(), algorand_fixture.kmd.clone()); - - for app_id in app_ids { - let app_address = Address::from_app_id(app_id); - dispenser - .fund_account(&app_address.to_string(), amount) - .await?; - } - - Ok(()) -} - -async fn assert_min_fee( - new_composer: &impl Fn(Option) -> TransactionComposer, - params: &AppCallParams, - fee: u64, -) { - if fee == 1000 { - return; - } - - let params = AppCallParams { - static_fee: Some(fee - 1), - ..params.clone() - }; - - let mut composer = new_composer(Some(TransactionComposerConfig { - cover_app_call_inner_transaction_fees: false, // Run without fee coverage to confirm it fails - populate_app_call_resources: ResourcePopulation::default(), - })); - - composer - .add_app_call(params) - .expect("Failed to add app call"); - - let result = composer.send(None).await; - - assert!( - result.is_err() - && result - .as_ref() - .unwrap_err() - .to_string() - .contains("fee too small"), - "Unexpected result, got: {:?}", - result - ); -} - -fn create_fees_tuple( - fee1: u64, - fee2: u64, - fee3: u64, - fee4: u64, - nested_fees: Vec, -) -> ABIValue { - ABIValue::from(vec![ - ABIValue::from(fee1), - ABIValue::from(fee2), - ABIValue::from(fee3), - ABIValue::from(fee4), - ABIValue::Array(nested_fees.into_iter().map(ABIValue::from).collect()), - ]) -} diff --git a/crates/algokit_utils/tests/transactions/composer/key_registration.rs b/crates/algokit_utils/tests/transactions/composer/key_registration.rs deleted file mode 100644 index 69fd3bd64..000000000 --- a/crates/algokit_utils/tests/transactions/composer/key_registration.rs +++ /dev/null @@ -1,370 +0,0 @@ -use crate::common::{AlgorandFixtureResult, TestResult, algorand_fixture}; -use algokit_utils::{ - NonParticipationKeyRegistrationParams, OfflineKeyRegistrationParams, - OnlineKeyRegistrationParams, -}; -use base64::{Engine, engine::general_purpose}; -use rstest::*; - -#[rstest] -#[tokio::test] -async fn test_offline_key_registration_transaction( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let sender_addr = algorand_fixture.test_account.account().address(); - - let offline_key_reg_params = OfflineKeyRegistrationParams { - sender: sender_addr.clone(), - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_offline_key_registration(offline_key_reg_params)?; - - let result = composer.send(None).await?; - let confirmation = &result.results[0].confirmation; - // Assert transaction was confirmed - assert!( - confirmation.confirmed_round.is_some(), - "Transaction should be confirmed" - ); - assert!( - confirmation.confirmed_round.unwrap() > 0, - "Confirmed round should be greater than 0" - ); - - let transaction = &confirmation.txn.transaction; - - match transaction { - algokit_transact::Transaction::KeyRegistration(key_reg_fields) => { - assert!( - key_reg_fields.vote_key.is_none(), - "Vote key should be None for offline registration" - ); - assert!( - key_reg_fields.selection_key.is_none(), - "Selection key should be None for offline registration" - ); - assert!( - key_reg_fields.non_participation.is_none(), - "Non participation should be None for offline registration" - ); - } - _ => return Err("Transaction should be a key registration transaction".into()), - } - - // Verify account participation status - let account_info = algorand_fixture - .algod - .account_information(&sender_addr.to_string(), None) - .await?; - - // For offline registration, participation should be empty/none - assert!( - account_info.participation.is_none() - || account_info - .participation - .as_ref() - .is_none_or(|p| p.vote_participation_key.is_empty()), - "Account should not have participation keys after going offline" - ); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_non_participation_key_registration_transaction( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let sender_addr = algorand_fixture.test_account.account().address(); - - // Use real participation keys for initial online registration - let vote_key = general_purpose::STANDARD - .decode("G/lqTV6MKspW6J8wH2d8ZliZ5XZVZsruqSBJMwLwlmo=")? - .try_into() - .map_err(|_| "Vote key should be 32 bytes")?; - - let selection_key = general_purpose::STANDARD - .decode("LrpLhvzr+QpN/bivh6IPpOaKGbGzTTB5lJtVfixmmgk=")? - .try_into() - .map_err(|_| "Selection key should be 32 bytes")?; - - let state_proof_key = general_purpose::STANDARD.decode( - "RpUpNWfZMjZ1zOOjv3MF2tjO714jsBt0GKnNsw0ihJ4HSZwci+d9zvUi3i67LwFUJgjQ5Dz4zZgHgGduElnmSA==", - )? - .try_into() - .map_err(|_| "State proof key should be 64 bytes")?; - - // Step 1: First make the account online to demonstrate the permanent nature of non-participation - let params1 = algorand_fixture.algod.transaction_params().await?; - - let vote_first = params1.last_round; - let vote_last = vote_first + 10_000_000; - - let online_key_reg_params = OnlineKeyRegistrationParams { - sender: sender_addr.clone(), - vote_key, - selection_key, - vote_first, - vote_last, - vote_key_dilution: 100, - state_proof_key: Some(state_proof_key), - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_online_key_registration(online_key_reg_params)?; - - let online_result = composer.send(None).await?; - - assert!( - online_result.results[0] - .confirmation - .confirmed_round - .is_some(), - "Online transaction should be confirmed" - ); - - // Verify account is now online - let account_info = algorand_fixture - .algod - .account_information(&sender_addr.to_string(), None) - .await?; - - assert!( - account_info.participation.is_some(), - "Account should have participation information after going online" - ); - - // Step 2: Mark account as permanently non-participating - let non_participation_params = NonParticipationKeyRegistrationParams { - sender: sender_addr.clone(), - ..Default::default() - }; - - let mut composer2 = algorand_fixture.algorand_client.new_composer(None); - composer2.add_non_participation_key_registration(non_participation_params)?; - - let result = composer2.send(None).await?; - let confirmation = &result.results[0].confirmation; - - // Assert transaction was confirmed - assert!( - confirmation.confirmed_round.is_some(), - "Transaction should be confirmed" - ); - assert!( - confirmation.confirmed_round.unwrap() > 0, - "Confirmed round should be greater than 0" - ); - - let transaction = &confirmation.txn.transaction; - - match transaction { - algokit_transact::Transaction::KeyRegistration(key_reg_fields) => { - assert!( - key_reg_fields.vote_key.is_none(), - "Vote key should be None for non participation" - ); - assert!( - key_reg_fields.selection_key.is_none(), - "Selection key should be None for non participation" - ); - assert_eq!( - key_reg_fields.non_participation, - Some(true), - "Non participation should be true" - ); - } - _ => return Err("Transaction should be a key registration transaction".into()), - } - - // Verify account participation status - let account_info = algorand_fixture - .algod - .account_information(&sender_addr.to_string(), None) - .await?; - - // For non-participation, participation should be empty/none - assert!( - account_info.participation.is_none() - || account_info - .participation - .as_ref() - .is_none_or(|p| p.vote_participation_key.is_empty()), - "Account should not have participation keys after non-participation registration" - ); - - // Step 3: Verify that once marked as non-participating, account cannot be brought back online - let params3 = algorand_fixture.algod.transaction_params().await?; - - let vote_first_3 = params3.last_round; - let vote_last_3 = vote_first_3 + 10_000_000; - - let try_online_again_params = OnlineKeyRegistrationParams { - sender: sender_addr.clone(), - vote_key, - selection_key, - vote_first: vote_first_3, - vote_last: vote_last_3, - vote_key_dilution: 100, - state_proof_key: Some(state_proof_key), - ..Default::default() - }; - - let mut composer3 = algorand_fixture.algorand_client.new_composer(None); - composer3.add_online_key_registration(try_online_again_params)?; - - // This should fail because the account is permanently marked as non-participating - let online_again_result = composer3.send(None).await; - - assert!( - online_again_result.is_err(), - "Attempting to bring a non-participating account back online should fail" - ); - - // Verify the error is related to the account being marked as non-participating - let error_message = online_again_result.unwrap_err().to_string(); - assert!( - error_message.contains("nonparticipatory") - || error_message.contains("non-participating") - || error_message.contains("nonpart") - || error_message.contains("pool error") - || error_message.contains("rejected"), - "Error should indicate the account cannot participate: {}", - error_message - ); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_online_key_registration_transaction( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let sender_addr = algorand_fixture.test_account.account().address(); - - let vote_key = general_purpose::STANDARD - .decode("G/lqTV6MKspW6J8wH2d8ZliZ5XZVZsruqSBJMwLwlmo=")? - .try_into() - .map_err(|_| "Vote key should be 32 bytes")?; - - let selection_key = general_purpose::STANDARD - .decode("LrpLhvzr+QpN/bivh6IPpOaKGbGzTTB5lJtVfixmmgk=")? - .try_into() - .map_err(|_| "Selection key should be 32 bytes")?; - - let state_proof_key = general_purpose::STANDARD.decode( - "RpUpNWfZMjZ1zOOjv3MF2tjO714jsBt0GKnNsw0ihJ4HSZwci+d9zvUi3i67LwFUJgjQ5Dz4zZgHgGduElnmSA==", - )? - .try_into() - .map_err(|_| "State proof key should be 64 bytes")?; - - // Get fresh suggested params to use proper voting rounds - let params = algorand_fixture.algod.transaction_params().await?; - - let vote_first = params.last_round; - let vote_last = vote_first + 10_000_000; - - let online_key_reg_params = OnlineKeyRegistrationParams { - sender: sender_addr.clone(), - vote_key, - selection_key, - vote_first, - vote_last, - vote_key_dilution: 100, - state_proof_key: Some(state_proof_key), - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_online_key_registration(online_key_reg_params)?; - - // Submit the transaction - should succeed with proper keys and voting rounds - let result = composer.send(None).await?; - let confirmation = &result.results[0].confirmation; - - // Assert transaction was confirmed - assert!( - confirmation.confirmed_round.is_some(), - "Transaction should be confirmed" - ); - assert!( - confirmation.confirmed_round.unwrap() > 0, - "Confirmed round should be greater than 0" - ); - - let transaction = &confirmation.txn.transaction; - - match transaction { - algokit_transact::Transaction::KeyRegistration(key_reg_fields) => { - assert_eq!( - key_reg_fields.vote_key, - Some(vote_key), - "Vote key should match" - ); - assert_eq!( - key_reg_fields.selection_key, - Some(selection_key), - "Selection key should match" - ); - assert_eq!( - key_reg_fields.vote_first, - Some(vote_first), - "Vote first should match" - ); - assert_eq!( - key_reg_fields.vote_last, - Some(vote_last), - "Vote last should match" - ); - assert_eq!( - key_reg_fields.vote_key_dilution, - Some(100), - "Vote key dilution should match" - ); - assert_eq!( - key_reg_fields.state_proof_key, - Some(state_proof_key), - "State proof key should match" - ); - assert!( - key_reg_fields.non_participation.is_none(), - "Non participation should be None for online registration" - ); - } - _ => return Err("Transaction should be a key registration transaction".into()), - } - - // Verify account participation status - let account_info = algorand_fixture - .algod - .account_information(&sender_addr.to_string(), None) - .await?; - - // For online registration, participation should contain the keys - if let Some(participation) = account_info.participation { - assert!( - !participation.vote_participation_key.is_empty(), - "Account should have participation keys after going online" - ); - - // Verify the participation keys match what we submitted - assert_eq!( - participation.vote_participation_key, - vote_key.to_vec(), - "Vote participation key should match submitted key" - ); - } else { - return Err( - "Account should have participation information after online key registration".into(), - ); - } - - Ok(()) -} diff --git a/crates/algokit_utils/tests/transactions/composer/mod.rs b/crates/algokit_utils/tests/transactions/composer/mod.rs deleted file mode 100644 index 07d2416db..000000000 --- a/crates/algokit_utils/tests/transactions/composer/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod app_call; -pub mod asset_config; -pub mod asset_freeze; -pub mod asset_transfer; -pub mod inner_fee_coverage; -pub mod key_registration; -pub mod payment; -pub mod resource_population; -pub mod transaction_group; diff --git a/crates/algokit_utils/tests/transactions/composer/payment.rs b/crates/algokit_utils/tests/transactions/composer/payment.rs deleted file mode 100644 index 3646ea426..000000000 --- a/crates/algokit_utils/tests/transactions/composer/payment.rs +++ /dev/null @@ -1,137 +0,0 @@ -use algokit_utils::{AccountCloseParams, PaymentParams}; -use rstest::*; -use std::sync::Arc; - -use crate::common::{AlgorandFixtureResult, TestResult, algorand_fixture}; - -#[rstest] -#[tokio::test] -async fn test_basic_payment_transaction( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - - let receiver = algorand_fixture.generate_account(None).await?; - let receiver_account = receiver.account(); - - let payment_params = PaymentParams { - sender: sender_address, - receiver: receiver_account.address(), - amount: 500_000, // 0.5 ALGO - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_payment(payment_params)?; - - let result = composer.send(None).await?; - let transaction = &result.results[0].confirmation.txn.transaction; - - match transaction { - algokit_transact::Transaction::Payment(payment_fields) => { - assert_eq!( - payment_fields.amount, 500_000, - "Payment amount should be 500_000 microALGOs" - ); - } - _ => return Err("Transaction should be a payment transaction".into()), - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_basic_account_close_transaction( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - - let close_remainder_to = algorand_fixture.generate_account(None).await?; - let close_remainder_to_addr = close_remainder_to.account().address(); - - let account_close_params = AccountCloseParams { - sender: sender_address.clone(), - close_remainder_to: close_remainder_to_addr.clone(), - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_account_close(account_close_params)?; - - let result = composer.send(None).await?; - let transaction = result.results[0].confirmation.txn.transaction.clone(); - - match transaction { - algokit_transact::Transaction::Payment(payment_fields) => { - assert_eq!( - payment_fields.receiver, sender_address, - "receiver should be set to the sender address" - ); - assert_eq!(payment_fields.amount, 0, "Account close amount should be 0"); - assert!( - payment_fields.close_remainder_to.is_some(), - "close_remainder_to should be set for account close" - ); - assert_eq!( - payment_fields.close_remainder_to.unwrap(), - close_remainder_to_addr, - "close_remainder_to should match the provided address" - ); - } - _ => return Err("Transaction should be a payment transaction".into()), - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_payment_transactions_with_signers( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - let receiver_addr = algorand_fixture.test_account.account().address(); - - // Generate a new account that will be the sender - let sender_account = algorand_fixture.generate_account(None).await?; - let sender_addr = sender_account.account().address(); - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - let signer = Arc::new(sender_account.clone()); - - // Add two payment transactions with the same signer - for i in 0..2 { - let payment_params = PaymentParams { - sender: sender_addr.clone(), - signer: Some(signer.clone()), - receiver: receiver_addr.clone(), - amount: 50_000 + (i * 10_000), - ..Default::default() - }; - composer.add_payment(payment_params)?; - } - - let result = composer.send(None).await?; - - // Verify the transaction was processed successfully - let transaction = &result.results[0].confirmation.txn.transaction; - match transaction { - algokit_transact::Transaction::Payment(payment_fields) => { - // This will be the first transaction in the group - assert_eq!( - payment_fields.header.sender, sender_addr, - "Transaction sender should be the sender account" - ); - assert_eq!( - payment_fields.receiver, receiver_addr, - "Payment receiver should match test account address" - ); - } - _ => return Err("Transaction should be a payment transaction".into()), - } - - Ok(()) -} diff --git a/crates/algokit_utils/tests/transactions/composer/resource_population.rs b/crates/algokit_utils/tests/transactions/composer/resource_population.rs deleted file mode 100644 index 6995a4b18..000000000 --- a/crates/algokit_utils/tests/transactions/composer/resource_population.rs +++ /dev/null @@ -1,1199 +0,0 @@ -use crate::common::{ - AlgorandFixture, AlgorandFixtureResult, LocalNetDispenser, TestAccountConfig, TestResult, - algorand_fixture, -}; -use algokit_abi::{ABIMethod, ABIType, ABIValue}; -use algokit_test_artifacts::resource_population; -use algokit_transact::Transaction; -use algokit_transact::{Address, BoxReference, OnApplicationComplete, StateSchema}; -use algokit_utils::transactions::TransactionComposerConfig; -use algokit_utils::transactions::composer::ResourcePopulation; -use algokit_utils::{AppCallParams, AppCreateParams, PaymentParams}; -use base64::{Engine, prelude::BASE64_STANDARD}; -use rstest::*; -use std::str::FromStr; -use std::sync::Arc; -use std::vec; - -#[fixture] -async fn fixture( - #[default(8)] avm_version: u8, - #[future] algorand_fixture: AlgorandFixtureResult, -) -> FixtureResult { - let algorand_fixture = algorand_fixture.await?; - - let sender_address = algorand_fixture.test_account.account().address(); - let method_selectors = MethodSelectors { - create_application: ABIMethod::from_str("createApplication()void")?.selector()?, - bootstrap: ABIMethod::from_str("bootstrap()void")?.selector()?, - small_box: ABIMethod::from_str("smallBox()void")?.selector()?, - medium_box: ABIMethod::from_str("mediumBox()void")?.selector()?, - external_app_call: ABIMethod::from_str("externalAppCall()void")?.selector()?, - asset_total: ABIMethod::from_str("assetTotal()void")?.selector()?, - has_asset: ABIMethod::from_str("hasAsset(address)void")?.selector()?, - external_local: ABIMethod::from_str("externalLocal(address)void")?.selector()?, - address_balance: ABIMethod::from_str("addressBalance(address)void")?.selector()?, - box_with_payment: ABIMethod::from_str("boxWithPayment(pay)void")?.selector()?, - create_asset: ABIMethod::from_str("createAsset()void")?.selector()?, - sender_asset_balance: ABIMethod::from_str("senderAssetBalance()void")?.selector()?, - opt_in_to_application: ABIMethod::from_str("optInToApplication()void")?.selector()?, - error: ABIMethod::from_str("error()void")?.selector()?, - }; - - let app_id = - deploy_resource_population_app(&algorand_fixture, &method_selectors, avm_version).await?; - fund_app_account(&algorand_fixture, app_id, 2_334_300).await?; - bootstrap_resource_population_app(&algorand_fixture, &method_selectors, app_id).await?; - - let application_info = &algorand_fixture.algod.get_application_by_id(app_id).await?; - let external_app_id = application_info - .params - .global_state - .as_ref() - .unwrap() - .iter() - .find(|kv| kv.key == "ZXh0ZXJuYWxBcHBJRA==") - .ok_or("externalAppID not in global state")? - .value - .uint; - - Ok(Fixture { - sender_address, - app_id, - external_app_id, - algorand_fixture, - method_selectors, - abi_types: ABITypes { - address: ABIType::Address, - }, - }) -} - -#[rstest] -#[case(8)] -#[case(9)] -#[tokio::test] -async fn test_accounts_errors_when_resource_population_disabled( - #[case] _avm_version: u8, - #[with(_avm_version)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_id, - method_selectors, - mut algorand_fixture, - abi_types, - .. - } = fixture.await?; - let mut composer = - algorand_fixture - .algorand_client - .new_composer(Some(TransactionComposerConfig { - cover_app_call_inner_transaction_fees: true, // Ensure the same behaviour when simulating due to inner fee coverage - populate_app_call_resources: ResourcePopulation::Disabled, - })); - let alice = algorand_fixture - .generate_account(None) - .await? - .account() - .address(); - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - max_fee: Some(2000), - app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![ - method_selectors.address_balance, - abi_types - .address - .encode(&ABIValue::Address(alice.to_string()))?, - ]), - ..Default::default() - })?; - - let result = composer.send(None).await; - assert!( - result.is_err() - && result - .unwrap_err() - .to_string() - .contains("unavailable Account") - ); - - Ok(()) -} - -#[rstest] -#[case(8)] -#[case(9)] -#[tokio::test] -async fn test_accounts_populated_when_resource_population_enabled( - #[case] _avm_version: u8, - #[with(_avm_version)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_id, - method_selectors, - mut algorand_fixture, - abi_types, - .. - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(POPULATE_RESOURCES_GROUP_PARAMS); - let alice = algorand_fixture - .generate_account(None) - .await? - .account() - .address(); - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![ - method_selectors.address_balance, - abi_types - .address - .encode(&ABIValue::Address(alice.to_string()))?, - ]), - ..Default::default() - })?; - - let result = composer.send(None).await?; - - assert!(result.results.len() == 1); - if let Transaction::AppCall(app_call) = &result.results[0].confirmation.txn.transaction { - assert_eq!(app_call.account_references, Some(vec![alice])); - } else { - return Err("AppCall transaction expected".into()); - } - - Ok(()) -} - -#[rstest] -#[case(8, "small")] -#[case(8, "medium")] -#[case(9, "small")] -#[case(9, "medium")] -#[tokio::test] -async fn test_boxes_errors_when_resource_population_disabled( - #[case] _avm_version: u8, - #[case] box_size: &str, - #[with(_avm_version)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_id, - method_selectors, - algorand_fixture, - .. - } = fixture.await?; - let mut composer = - algorand_fixture - .algorand_client - .new_composer(Some(TransactionComposerConfig { - populate_app_call_resources: ResourcePopulation::Disabled, - ..Default::default() - })); - let method_selector = match box_size { - "small" => method_selectors.small_box, - "medium" => method_selectors.medium_box, - _ => return Err("Invalid box size".into()), - }; - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![method_selector]), - ..Default::default() - })?; - - let result = composer.send(None).await; - - assert!( - result.is_err() - && result - .unwrap_err() - .to_string() - .contains("invalid Box reference") - ); - Ok(()) -} - -#[rstest] -#[case(8, "small")] -#[case(8, "medium")] -#[case(9, "small")] -#[case(9, "medium")] -#[tokio::test] -async fn test_boxes_populated_when_resource_population_enabled( - #[case] _avm_version: u8, - #[case] box_size: &str, - #[with(_avm_version)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_id, - method_selectors, - algorand_fixture, - .. - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(POPULATE_RESOURCES_GROUP_PARAMS); - let (method_selector, box_refs) = match box_size { - "small" => ( - method_selectors.small_box, - vec![BoxReference { - app_id: 0, - name: vec![115], - }], - ), - "medium" => ( - method_selectors.medium_box, - vec![ - BoxReference { - app_id: 0, - name: vec![109], - }, - BoxReference { - app_id: 0, - name: vec![], - }, - BoxReference { - app_id: 0, - name: vec![], - }, - ], - ), - _ => return Err("Invalid box size".into()), - }; - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![method_selector]), - ..Default::default() - })?; - - let result = composer.send(None).await?; - - assert!(result.results.len() == 1); - if let Transaction::AppCall(app_call) = &result.results[0].confirmation.txn.transaction { - assert_eq!(app_call.box_references, Some(box_refs)); - } else { - return Err("AppCall transaction expected".into()); - } - - Ok(()) -} - -#[rstest] -#[case(8)] -#[case(9)] -#[tokio::test] -async fn test_apps_errors_when_resource_population_disabled( - #[case] _avm_version: u8, - #[with(_avm_version)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_id, - method_selectors, - algorand_fixture, - .. - } = fixture.await?; - let mut composer = - algorand_fixture - .algorand_client - .new_composer(Some(TransactionComposerConfig { - populate_app_call_resources: ResourcePopulation::Disabled, - ..Default::default() - })); - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - static_fee: Some(2000), - app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![method_selectors.external_app_call]), - ..Default::default() - })?; - - let result = composer.send(None).await; - - assert!(result.is_err() && result.unwrap_err().to_string().contains("unavailable App")); - Ok(()) -} - -#[rstest] -#[case(8)] -#[case(9)] -#[tokio::test] -async fn test_apps_populated_when_resource_population_enabled( - #[case] _avm_version: u8, - #[with(_avm_version)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_id, - method_selectors, - algorand_fixture, - .. - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(POPULATE_RESOURCES_GROUP_PARAMS); - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - static_fee: Some(2000), - app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![method_selectors.external_app_call]), - ..Default::default() - })?; - - let result = composer.send(None).await?; - - assert!(result.results.len() == 1); - if let Transaction::AppCall(app_call) = &result.results[0].confirmation.txn.transaction { - assert_eq!(app_call.app_references.as_ref().unwrap().len(), 1); - } else { - return Err("AppCall transaction expected".into()); - } - - Ok(()) -} - -#[rstest] -#[case(8)] -#[case(9)] -#[tokio::test] -async fn test_assets_errors_when_resource_population_disabled( - #[case] _avm_version: u8, - #[with(_avm_version)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_id, - method_selectors, - algorand_fixture, - .. - } = fixture.await?; - let mut composer = - algorand_fixture - .algorand_client - .new_composer(Some(TransactionComposerConfig { - populate_app_call_resources: ResourcePopulation::Disabled, - ..Default::default() - })); - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![method_selectors.asset_total]), - ..Default::default() - })?; - - let result = composer.send(None).await; - - assert!( - result.is_err() - && result - .unwrap_err() - .to_string() - .contains("unavailable Asset") - ); - Ok(()) -} - -#[rstest] -#[case(8)] -#[case(9)] -#[tokio::test] -async fn test_assets_populated_when_resource_population_enabled( - #[case] _avm_version: u8, - #[with(_avm_version)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_id, - method_selectors, - algorand_fixture, - .. - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(POPULATE_RESOURCES_GROUP_PARAMS); - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![method_selectors.asset_total]), - ..Default::default() - })?; - - let result = composer.send(None).await?; - - assert!(result.results.len() == 1); - if let Transaction::AppCall(app_call) = &result.results[0].confirmation.txn.transaction { - assert_eq!(app_call.asset_references.as_ref().unwrap().len(), 1); - } else { - return Err("AppCall transaction expected".into()); - } - - Ok(()) -} - -#[rstest] -#[case(8)] -#[case(9)] -#[tokio::test] -async fn test_cross_product_assets_and_accounts_errors_when_resource_population_disabled( - #[case] _avm_version: u8, - #[with(_avm_version)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_id, - method_selectors, - mut algorand_fixture, - abi_types, - .. - } = fixture.await?; - let mut composer = - algorand_fixture - .algorand_client - .new_composer(Some(TransactionComposerConfig { - populate_app_call_resources: ResourcePopulation::Disabled, - ..Default::default() - })); - let alice = algorand_fixture - .generate_account(None) - .await? - .account() - .address() - .to_string(); - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![ - method_selectors.has_asset, - abi_types.address.encode(&ABIValue::Address(alice))?, - ]), - ..Default::default() - })?; - - let result = composer.send(None).await; - - assert!( - result.is_err() - && result - .unwrap_err() - .to_string() - .contains("unavailable Account") - ); - - Ok(()) -} - -#[rstest] -#[case(8)] -#[case(9)] -#[tokio::test] -async fn test_cross_product_assets_and_accounts_populated_when_resource_population_enabled( - #[case] _avm_version: u8, - #[with(_avm_version)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_id, - method_selectors, - mut algorand_fixture, - abi_types, - .. - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(POPULATE_RESOURCES_GROUP_PARAMS); - let alice = algorand_fixture - .generate_account(None) - .await? - .account() - .address(); - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![ - method_selectors.has_asset, - abi_types - .address - .encode(&ABIValue::Address(alice.to_string()))?, - ]), - ..Default::default() - })?; - - let result = composer.send(None).await?; - - assert!(result.results.len() == 1); - if let Transaction::AppCall(app_call) = &result.results[0].confirmation.txn.transaction { - assert_eq!(app_call.account_references, Some(vec![alice])); - assert_eq!(app_call.asset_references.as_ref().unwrap().len(), 1); - } else { - return Err("AppCall transaction expected".into()); - } - - Ok(()) -} - -#[rstest] -#[case(8)] -#[case(9)] -#[tokio::test] -async fn test_cross_product_account_app_errors_when_resource_population_disabled( - #[case] _avm_version: u8, - #[with(_avm_version)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_id, - method_selectors, - mut algorand_fixture, - abi_types, - .. - } = fixture.await?; - let mut composer = - algorand_fixture - .algorand_client - .new_composer(Some(TransactionComposerConfig { - populate_app_call_resources: ResourcePopulation::Disabled, - ..Default::default() - })); - let alice = algorand_fixture - .generate_account(Some(TestAccountConfig { - initial_funds: 10_000_000, - ..Default::default() - })) - .await? - .account() - .address(); - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![ - method_selectors.external_local, - abi_types - .address - .encode(&ABIValue::Address(alice.to_string()))?, - ]), - ..Default::default() - })?; - - let result = composer.send(None).await; - - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("unavailable Account") - ); - - Ok(()) -} - -#[rstest] -#[case(8)] -#[case(9)] -#[tokio::test] -async fn test_cross_product_account_app_populated_when_resource_population_enabled( - #[case] avm_version: u8, - #[with(avm_version)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_id, - external_app_id, - method_selectors, - mut algorand_fixture, - abi_types, - .. - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(POPULATE_RESOURCES_GROUP_PARAMS); - let alice = algorand_fixture - .generate_account(Some(TestAccountConfig { - initial_funds: 1_000_000, - ..Default::default() - })) - .await?; - let alice_address = alice.account().address(); - let alice_signer = Arc::new(alice.clone()); - let (expected_account_refs, expected_app_refs) = if avm_version == 8 { - ( - Some(vec![alice_address.clone()]), - Some(vec![external_app_id]), - ) - } else { - (None, None) - }; - - composer.add_app_call(AppCallParams { - sender: alice_address.clone(), - signer: Some(alice_signer.clone()), - app_id: external_app_id, - on_complete: OnApplicationComplete::OptIn, - args: Some(vec![method_selectors.opt_in_to_application]), - ..Default::default() - })?; - - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![ - method_selectors.external_local, - abi_types - .address - .encode(&ABIValue::Address(alice_address.to_string()))?, - ]), - ..Default::default() - })?; - - let result = composer.send(None).await?; - - assert!(result.results.len() == 2); - if let Transaction::AppCall(app_call) = &result.results[1].confirmation.txn.transaction { - assert_eq!(app_call.account_references, expected_account_refs); - assert_eq!(app_call.app_references, expected_app_refs); - } else { - return Err("AppCall transaction expected".into()); - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_mixed_avm_version_same_account( - #[with(8)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_id: avm_8_app_id, - method_selectors, - mut algorand_fixture, - abi_types, - .. - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(POPULATE_RESOURCES_GROUP_PARAMS); - let avm_9_app_id = - deploy_resource_population_app(&algorand_fixture, &method_selectors, 9).await?; - let alice = algorand_fixture - .generate_account(Some(TestAccountConfig { - initial_funds: 1_000_000, - ..Default::default() - })) - .await? - .account() - .address(); - - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - app_id: avm_8_app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![ - method_selectors.address_balance.clone(), - abi_types - .address - .encode(&ABIValue::Address(alice.to_string()))?, - ]), - ..Default::default() - })?; - - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - app_id: avm_9_app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![ - method_selectors.address_balance, - abi_types - .address - .encode(&ABIValue::Address(alice.to_string()))?, - ]), - ..Default::default() - })?; - - let result = composer.send(None).await?; - - assert!(result.results.len() == 2); - if let (Transaction::AppCall(avm_8_app_call), Transaction::AppCall(avm_9_app_call)) = ( - &result.results[0].confirmation.txn.transaction, - &result.results[1].confirmation.txn.transaction, - ) { - assert_eq!(avm_8_app_call.account_references, Some(vec![alice])); - assert_eq!(avm_9_app_call.account_references, None); - } else { - return Err("AppCall transactions expected".into()); - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_mixed_avm_version_app_account( - #[with(8)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_id: avm_8_app_id, - external_app_id, - method_selectors, - algorand_fixture, - abi_types, - .. - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(POPULATE_RESOURCES_GROUP_PARAMS); - let avm_9_app_id = - deploy_resource_population_app(&algorand_fixture, &method_selectors, 9).await?; - - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - static_fee: Some(2000), - app_id: avm_8_app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![method_selectors.external_app_call]), - ..Default::default() - })?; - - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - app_id: avm_9_app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![ - method_selectors.address_balance, - abi_types.address.encode(&ABIValue::Address( - Address::from_app_id(&external_app_id).to_string(), - ))?, - ]), - ..Default::default() - })?; - - let result = composer.send(None).await?; - - assert!(result.results.len() == 2); - if let (Transaction::AppCall(avm_8_app_call), Transaction::AppCall(avm_9_app_call)) = ( - &result.results[0].confirmation.txn.transaction, - &result.results[1].confirmation.txn.transaction, - ) { - assert_eq!(avm_8_app_call.app_references, Some(vec![external_app_id])); - assert_eq!(avm_9_app_call.account_references, None); - } else { - return Err("AppCall transactions expected".into()); - } - - Ok(()) -} - -#[rstest] -#[case(None)] -#[case(Some(true))] -#[case(Some(false))] -#[tokio::test] -async fn test_error( - #[case] populate_resources: Option, - #[with(9)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - app_id, - method_selectors, - algorand_fixture, - .. - } = fixture.await?; - let mut composer = - algorand_fixture - .algorand_client - .new_composer(Some(TransactionComposerConfig { - populate_app_call_resources: match populate_resources.unwrap_or(true) { - true => ResourcePopulation::Enabled { - use_access_list: false, - }, - false => ResourcePopulation::Disabled, - }, // Default to enabled - ..Default::default() - })); - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![method_selectors.error]), - ..Default::default() - })?; - - let result = composer.send(None).await; - - let error_message = if populate_resources.is_none() || populate_resources.unwrap() { - // Checks that resource population is enabled by default - "Error analyzing group requirements via simulate in transaction 0" // Fails on simulate - } else { - "400 Bad Request" // Fails on send, as non population occurs - }; - assert!( - result.is_err() - && result - .as_ref() - .unwrap_err() - .to_string() - .contains("logic eval error: err opcode executed") - && result - .as_ref() - .unwrap_err() - .to_string() - .contains(error_message) - ); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_box_with_txn_arg( - #[with(9)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - external_app_id, - method_selectors, - algorand_fixture, - .. - } = fixture.await?; - fund_app_account(&algorand_fixture, external_app_id, 106_100).await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(POPULATE_RESOURCES_GROUP_PARAMS); - - composer.add_payment(PaymentParams { - sender: sender_address.clone(), - amount: 0, - receiver: sender_address.clone(), - ..Default::default() - })?; - - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - app_id: external_app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![method_selectors.box_with_payment]), - ..Default::default() - })?; - - let result = composer.send(None).await?; - - assert!(result.results.len() == 2); - if let Transaction::AppCall(app_call) = &result.results[1].confirmation.txn.transaction { - assert_eq!( - app_call.box_references, - Some(vec![BoxReference { - app_id: 0, - name: vec![98, 111, 120, 75, 101, 121], - }]) - ); - } else { - return Err("AppCall transaction expected".into()); - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_sender_asset_holding( - #[with(9)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - external_app_id, - method_selectors, - algorand_fixture, - .. - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(POPULATE_RESOURCES_GROUP_PARAMS); - fund_app_account(&algorand_fixture, external_app_id, 200_000).await?; - - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - static_fee: Some(2000), - app_id: external_app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![method_selectors.create_asset]), - ..Default::default() - })?; - - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - app_id: external_app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![method_selectors.sender_asset_balance]), - ..Default::default() - })?; - - let result = composer.send(None).await?; - - assert!(result.results.len() == 2); - if let Transaction::AppCall(app_call) = &result.results[1].confirmation.txn.transaction { - assert_eq!(app_call.account_references, None); - } else { - return Err("AppCall transaction expected".into()); - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_rekeyed_account( - #[with(9)] - #[future] - fixture: FixtureResult, -) -> TestResult { - let Fixture { - sender_address, - external_app_id, - method_selectors, - mut algorand_fixture, - .. - } = fixture.await?; - let mut composer = algorand_fixture - .algorand_client - .new_composer(POPULATE_RESOURCES_GROUP_PARAMS); - fund_app_account(&algorand_fixture, external_app_id, 200_001).await?; - let auth_account = algorand_fixture - .generate_account(Some(TestAccountConfig { - initial_funds: 1_000_000, - ..Default::default() - })) - .await?; - let auth_address = auth_account.account().address(); - let auth_signer = Arc::new(auth_account.clone()); - - // Rekey the account - composer.add_payment(PaymentParams { - sender: sender_address.clone(), - rekey_to: Some(auth_address.clone()), - amount: 0, - receiver: sender_address.clone(), - ..Default::default() - })?; - - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - signer: Some(auth_signer.clone()), - static_fee: Some(2001), - app_id: external_app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![method_selectors.create_asset]), - ..Default::default() - })?; - - composer.add_app_call(AppCallParams { - sender: sender_address.clone(), - signer: Some(auth_signer.clone()), - app_id: external_app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![method_selectors.sender_asset_balance]), - ..Default::default() - })?; - - let result = composer.send(None).await?; - - assert!(result.results.len() == 3); - if let Transaction::AppCall(app_call) = &result.results[2].confirmation.txn.transaction { - assert_eq!(app_call.account_references, None); - } else { - return Err("AppCall transaction expected".into()); - } - - Ok(()) -} - -struct Fixture { - sender_address: Address, - app_id: u64, - external_app_id: u64, - algorand_fixture: AlgorandFixture, - method_selectors: MethodSelectors, - abi_types: ABITypes, -} - -type FixtureResult = Result>; - -struct MethodSelectors { - create_application: Vec, - bootstrap: Vec, - small_box: Vec, - medium_box: Vec, - external_app_call: Vec, - asset_total: Vec, - has_asset: Vec, - external_local: Vec, - address_balance: Vec, - box_with_payment: Vec, - create_asset: Vec, - sender_asset_balance: Vec, - opt_in_to_application: Vec, - error: Vec, -} - -struct ABITypes { - address: ABIType, -} - -// TODO: This should be shared when we have ARC56 support -#[derive(serde::Deserialize)] -struct ARC32AppSpec { - source: Option, -} - -#[derive(serde::Deserialize)] -struct TealSource { - approval: String, - clear: String, -} - -const POPULATE_RESOURCES_GROUP_PARAMS: Option = - Some(TransactionComposerConfig { - populate_app_call_resources: ResourcePopulation::Enabled { - use_access_list: false, - }, - cover_app_call_inner_transaction_fees: false, - }); - -async fn deploy_resource_population_app( - context: &AlgorandFixture, - method_selectors: &MethodSelectors, - version: u8, -) -> Result> { - let (approval_teal, clear_state_teal) = get_resource_population_programs(version).await?; - let approval_compile_result = context.algod.teal_compile(approval_teal, None).await?; - let clear_state_compile_result = context.algod.teal_compile(clear_state_teal, None).await?; - - let mut composer = context.algorand_client.new_composer(None); - composer.add_app_create(AppCreateParams { - sender: context.test_account.account().address(), - on_complete: OnApplicationComplete::NoOp, - approval_program: approval_compile_result.result.clone(), - clear_state_program: clear_state_compile_result.result, - global_state_schema: Some(StateSchema { - num_uints: 2, - num_byte_slices: 0, - }), - local_state_schema: Some(StateSchema { - num_uints: 0, - num_byte_slices: 0, - }), - args: Some(vec![method_selectors.create_application.clone()]), - ..Default::default() - })?; - let result = composer.send(None).await?; - - result.results[0] - .confirmation - .app_id - .ok_or_else(|| "No app id returned".into()) -} - -async fn bootstrap_resource_population_app( - context: &AlgorandFixture, - method_selectors: &MethodSelectors, - app_id: u64, -) -> Result<(), Box> { - let mut composer = context.algorand_client.new_composer(None); - - composer.add_app_call(AppCallParams { - sender: context.test_account.account().address(), - static_fee: Some(3000), - app_id, - on_complete: OnApplicationComplete::NoOp, - args: Some(vec![method_selectors.bootstrap.clone()]), - ..Default::default() - })?; - composer.send(None).await?; - - Ok(()) -} - -async fn fund_app_account( - context: &AlgorandFixture, - app_id: u64, - amount: u64, -) -> Result<(), Box> { - let mut dispenser = LocalNetDispenser::new(context.algod.clone(), context.kmd.clone()); - let app_address = Address::from_app_id(&app_id); - dispenser - .fund_account(&app_address.to_string(), amount) - .await?; - Ok(()) -} - -async fn get_resource_population_programs( - version: u8, -) -> Result<(Vec, Vec), Box> { - let app_spec_path = if version == 8 { - resource_population::APPLICATION_V8 - } else { - resource_population::APPLICATION_V9 - }; - - let app_spec: ARC32AppSpec = serde_json::from_str(app_spec_path)?; - let teal_source = app_spec.source.unwrap(); - let approval_bytes = BASE64_STANDARD.decode(teal_source.approval)?; - let clear_state_bytes = BASE64_STANDARD.decode(teal_source.clear)?; - Ok((approval_bytes, clear_state_bytes)) -} diff --git a/crates/algokit_utils/tests/transactions/composer/transaction_group.rs b/crates/algokit_utils/tests/transactions/composer/transaction_group.rs deleted file mode 100644 index b58ca362e..000000000 --- a/crates/algokit_utils/tests/transactions/composer/transaction_group.rs +++ /dev/null @@ -1,215 +0,0 @@ -use algokit_transact::test_utils::TransactionGroupMother; -use algokit_transact::{MAX_TX_GROUP_SIZE, test_utils::TransactionMother}; -use algokit_utils::{AssetCreateParams, PaymentParams}; -use rstest::*; - -use crate::common::{AlgorandFixtureResult, TestResult, algorand_fixture}; - -#[rstest] -#[tokio::test] -async fn test_payment_and_asset_create_group( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - - let receiver = algorand_fixture.generate_account(None).await?; - let receiver_addr = receiver.account().address(); - - let payment_params = PaymentParams { - sender: sender_address.clone(), - receiver: receiver_addr, - amount: 1_000_000, - ..Default::default() - }; - - let asset_create_params = AssetCreateParams { - sender: sender_address.clone(), - total: 1_000_000, - decimals: Some(2), - default_frozen: Some(false), - asset_name: Some("Group Test Asset".to_string()), - unit_name: Some("GTA".to_string()), - url: Some("https://group-test.com".to_string()), - metadata_hash: None, - manager: Some(sender_address.clone()), - reserve: Some(sender_address.clone()), - freeze: Some(sender_address.clone()), - clawback: Some(sender_address), - ..Default::default() - }; - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - composer.add_payment(payment_params)?; - composer.add_asset_create(asset_create_params)?; - - let result = composer.send(None).await?; - - // Verify group properties - assert_eq!( - result.results.len(), - 2, - "Should have 2 transaction IDs in the group" - ); - assert_eq!( - result.results.len(), - 2, - "Should have 2 confirmations in the group" - ); - - assert!(result.group.is_some(), "Group ID should be set"); - - // Verify payment transaction - let payment_confirmation = &result.results[0].confirmation; - assert!( - payment_confirmation.confirmed_round.is_some(), - "Payment transaction should be confirmed" - ); - assert!( - payment_confirmation.confirmed_round.unwrap() > 0, - "Payment confirmed round should be greater than 0" - ); - - match &payment_confirmation.txn.transaction { - algokit_transact::Transaction::Payment(payment_fields) => { - assert_eq!( - payment_fields.amount, 1_000_000, - "Payment amount should be 1,000,000 microALGOs" - ); - } - _ => return Err("First transaction should be a payment transaction".into()), - } - - // Verify asset creation transaction - let asset_confirmation = &result.results[1].confirmation; - assert!( - asset_confirmation.confirmed_round.is_some(), - "Asset creation transaction should be confirmed" - ); - assert!( - asset_confirmation.confirmed_round.unwrap() > 0, - "Asset creation confirmed round should be greater than 0" - ); - - match &asset_confirmation.txn.transaction { - algokit_transact::Transaction::AssetConfig(asset_config_fields) => { - assert_eq!( - asset_config_fields.asset_id, 0, - "Asset ID should be 0 for creation" - ); - assert_eq!( - asset_config_fields.total, - Some(1_000_000), - "Total should be 1,000,000" - ); - assert_eq!( - asset_config_fields.decimals, - Some(2), - "Decimals should be 2" - ); - assert_eq!( - asset_config_fields.asset_name, - Some("Group Test Asset".to_string()), - "Asset name should match" - ); - assert_eq!( - asset_config_fields.unit_name, - Some("GTA".to_string()), - "Unit name should match" - ); - } - _ => return Err("Second transaction should be an asset config transaction".into()), - } - - // Verify that the asset was actually created - assert!( - asset_confirmation.asset_id.is_some(), - "Asset ID should be present for successful asset creation" - ); - assert!( - asset_confirmation.asset_id.unwrap() > 0, - "Asset index should be greater than 0" - ); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_add_transactions_to_group_max_size( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - - let receiver = algorand_fixture.generate_account(None).await?; - let receiver_addr = receiver.account().address(); - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - - for i in 0..MAX_TX_GROUP_SIZE - 2 { - let payment_params = PaymentParams { - sender: sender_address.clone(), - receiver: receiver_addr.clone(), - amount: i as u64, - ..Default::default() - }; - - composer.add_payment(payment_params)?; - } - - let new_transactions = TransactionGroupMother::group_of(2) - .iter() - .map(|tx| { - let mut tx = tx.clone(); - tx.header_mut().sender = sender_address.clone(); - tx - }) - .collect::>(); - - for tx in new_transactions { - composer.add_transaction(tx, None)?; - } - - assert!(composer.build().await.unwrap().len() == MAX_TX_GROUP_SIZE); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_add_transaction_to_group_too_big( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - - let receiver = algorand_fixture.generate_account(None).await?; - let receiver_addr = receiver.account().address(); - - let mut composer = algorand_fixture.algorand_client.new_composer(None); - - for i in 0..MAX_TX_GROUP_SIZE { - let payment_params = PaymentParams { - sender: sender_address.clone(), - receiver: receiver_addr.clone(), - amount: i as u64, - ..Default::default() - }; - - composer.add_payment(payment_params)?; - } - - let new_transaction = TransactionMother::simple_payment().build()?; - - let result = composer.add_transaction(new_transaction, None); - - assert!( - result - .unwrap_err() - .to_string() - .contains("Transaction group size exceeds the max limit of") - ); - - Ok(()) -} diff --git a/crates/algokit_utils/tests/transactions/creator.rs b/crates/algokit_utils/tests/transactions/creator.rs deleted file mode 100644 index 0c28b9c44..000000000 --- a/crates/algokit_utils/tests/transactions/creator.rs +++ /dev/null @@ -1,532 +0,0 @@ -use crate::common::{AlgorandFixtureResult, TestResult, algorand_fixture}; -use algokit_abi::{ABIMethod, ABIType, abi_type::BitSize}; -use algokit_transact::{Address, OnApplicationComplete, Transaction}; -use algokit_utils::transactions::{ - AppCallMethodCallParams, AppCallParams, AppCreateParams, AppDeleteParams, AppUpdateParams, - AssetClawbackParams, AssetCreateParams, AssetDestroyParams, AssetFreezeParams, - AssetOptInParams, AssetOptOutParams, AssetTransferParams, - NonParticipationKeyRegistrationParams, OfflineKeyRegistrationParams, - OnlineKeyRegistrationParams, PaymentParams, ResourcePopulation, TransactionComposerConfig, -}; -use rstest::*; - -const GROUP_ANALYSIS_DISABLED: Option = - Some(TransactionComposerConfig { - cover_app_call_inner_transaction_fees: false, - populate_app_call_resources: ResourcePopulation::Disabled, - }); - -#[rstest] -#[case::basic(1_000_000)] -#[case::minimum(1)] -#[case::large(100_000_000)] -#[tokio::test] -async fn payment_transaction( - #[case] amount: u64, - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - let receiver = algorand_fixture.generate_account(None).await?; - let creator = algorand_fixture.algorand_client.create(); - - let receiver_address = receiver.account().address(); - - let params = PaymentParams { - sender: sender_address.clone(), - receiver: receiver_address.clone(), - amount, - ..Default::default() - }; - - let tx = creator.payment(params).await?; - - match &tx { - Transaction::Payment(payment_fields) => { - assert_eq!(payment_fields.header.sender, sender_address); - assert_eq!(payment_fields.receiver, receiver_address); - assert_eq!(payment_fields.amount, amount); - } - _ => return Err("Expected Payment transaction".into()), - } - - Ok(()) -} - -#[rstest] -#[case::create(AssetTestCase::Create)] -#[case::transfer(AssetTestCase::Transfer)] -#[case::opt_in(AssetTestCase::OptIn)] -#[case::opt_out(AssetTestCase::OptOut)] -#[case::freeze(AssetTestCase::Freeze)] -#[case::destroy(AssetTestCase::Destroy)] -#[case::clawback(AssetTestCase::Clawback)] -#[tokio::test] -async fn asset_operations( - #[case] test_case: AssetTestCase, - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - - match test_case { - AssetTestCase::Create => { - let params = AssetCreateParams { - sender: sender_address.clone(), - total: 1_000_000, - asset_name: Some("TestAsset".to_string()), - unit_name: Some("TST".to_string()), - ..Default::default() - }; - let tx = algorand_fixture - .algorand_client - .create() - .asset_create(params) - .await?; - match &tx { - Transaction::AssetConfig(asset_fields) => { - assert_eq!(asset_fields.header.sender, sender_address); - assert_eq!(asset_fields.total, Some(1_000_000)); - } - _ => return Err("Expected AssetConfig transaction".into()), - } - } - AssetTestCase::Transfer => { - let receiver = algorand_fixture.generate_account(None).await?; - let receiver_address = receiver.account().address(); - let params = AssetTransferParams { - sender: sender_address.clone(), - receiver: receiver_address.clone(), - asset_id: 1, - amount: 100, - ..Default::default() - }; - let tx = algorand_fixture - .algorand_client - .create() - .asset_transfer(params) - .await?; - match &tx { - Transaction::AssetTransfer(transfer_fields) => { - assert_eq!(transfer_fields.header.sender, sender_address); - assert_eq!(transfer_fields.receiver, receiver_address); - assert_eq!(transfer_fields.amount, 100); - assert_eq!(transfer_fields.asset_id, 1); - } - _ => return Err("Expected AssetTransfer transaction".into()), - } - } - AssetTestCase::OptIn => { - let params = AssetOptInParams { - sender: sender_address.clone(), - asset_id: 1, - ..Default::default() - }; - let tx = algorand_fixture - .algorand_client - .create() - .asset_opt_in(params) - .await?; - match &tx { - Transaction::AssetTransfer(transfer_fields) => { - assert_eq!(transfer_fields.header.sender, sender_address); - assert_eq!(transfer_fields.receiver, sender_address); - assert_eq!(transfer_fields.amount, 0); - assert_eq!(transfer_fields.asset_id, 1); - } - _ => return Err("Expected AssetTransfer transaction".into()), - } - } - AssetTestCase::OptOut => { - let params = AssetOptOutParams { - sender: sender_address.clone(), - asset_id: 1, - close_remainder_to: Some(sender_address.clone()), - ..Default::default() - }; - let tx = algorand_fixture - .algorand_client - .create() - .asset_opt_out(params) - .await?; - match &tx { - Transaction::AssetTransfer(transfer_fields) => { - assert_eq!(transfer_fields.header.sender, sender_address); - assert_eq!(transfer_fields.receiver, sender_address); // Opts out by sending to self - assert_eq!( - transfer_fields.close_remainder_to, - Some(sender_address.clone()) - ); - assert_eq!(transfer_fields.asset_id, 1); - assert_eq!(transfer_fields.amount, 0); // Opt-out sends 0 amount - } - _ => return Err("Expected AssetTransfer transaction".into()), - } - } - AssetTestCase::Freeze => { - let target = algorand_fixture.generate_account(None).await?; - let target_address = target.account().address(); - let params = AssetFreezeParams { - sender: sender_address.clone(), - asset_id: 1, - target_address: target_address.clone(), - ..Default::default() - }; - let tx = algorand_fixture - .algorand_client - .create() - .asset_freeze(params) - .await?; - match &tx { - Transaction::AssetFreeze(freeze_fields) => { - assert_eq!(freeze_fields.header.sender, sender_address); - assert_eq!(freeze_fields.freeze_target, target_address); - assert_eq!(freeze_fields.asset_id, 1); - assert!(freeze_fields.frozen); - } - _ => return Err("Expected AssetFreeze transaction".into()), - } - } - AssetTestCase::Destroy => { - let params = AssetDestroyParams { - sender: sender_address.clone(), - asset_id: 1, - ..Default::default() - }; - let tx = algorand_fixture - .algorand_client - .create() - .asset_destroy(params) - .await?; - match &tx { - Transaction::AssetConfig(config_fields) => { - assert_eq!(config_fields.header.sender, sender_address); - assert_eq!(config_fields.asset_id, 1); - } - _ => return Err("Expected AssetConfig transaction".into()), - } - } - AssetTestCase::Clawback => { - let target = algorand_fixture.generate_account(None).await?; - let receiver = algorand_fixture.generate_account(None).await?; - let target_address = target.account().address(); - let receiver_address = receiver.account().address(); - let params = AssetClawbackParams { - sender: sender_address.clone(), - asset_id: 1, - clawback_target: target_address.clone(), - receiver: receiver_address.clone(), - amount: 50, - ..Default::default() - }; - let tx = algorand_fixture - .algorand_client - .create() - .asset_clawback(params) - .await?; - match &tx { - Transaction::AssetTransfer(transfer_fields) => { - assert_eq!(transfer_fields.header.sender, sender_address); - assert_eq!(transfer_fields.asset_sender, Some(target_address)); - assert_eq!(transfer_fields.receiver, receiver_address); - assert_eq!(transfer_fields.amount, 50); - assert_eq!(transfer_fields.asset_id, 1); - } - _ => return Err("Expected AssetTransfer transaction".into()), - } - } - } - - Ok(()) -} - -#[derive(Debug, Clone)] -enum AssetTestCase { - Create, - Transfer, - OptIn, - OptOut, - Freeze, - Destroy, - Clawback, -} - -#[rstest] -#[case::call(OnApplicationComplete::NoOp, 1)] -#[case::create(OnApplicationComplete::NoOp, 0)] -#[case::update(OnApplicationComplete::UpdateApplication, 1)] -#[case::delete(OnApplicationComplete::DeleteApplication, 1)] -#[tokio::test] -async fn application_operations( - #[case] on_complete: OnApplicationComplete, - #[case] app_id: u64, - #[with(GROUP_ANALYSIS_DISABLED)] - #[future] - algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - - let tx = match on_complete { - OnApplicationComplete::NoOp if app_id == 0 => algorand_fixture - .algorand_client - .create() - .app_create(AppCreateParams { - sender: sender_address.clone(), - approval_program: vec![0x06, 0x81, 0x01], - clear_state_program: vec![0x06, 0x81, 0x01], - ..Default::default() - }) - .await - .unwrap(), - OnApplicationComplete::NoOp => algorand_fixture - .algorand_client - .create() - .app_call(AppCallParams { - sender: sender_address.clone(), - app_id, - on_complete: OnApplicationComplete::NoOp, - ..Default::default() - }) - .await - .unwrap(), - OnApplicationComplete::UpdateApplication => algorand_fixture - .algorand_client - .create() - .app_update(AppUpdateParams { - sender: sender_address.clone(), - app_id, - approval_program: vec![0x06, 0x81, 0x01], - clear_state_program: vec![0x06, 0x81, 0x01], - ..Default::default() - }) - .await - .unwrap(), - OnApplicationComplete::DeleteApplication => algorand_fixture - .algorand_client - .create() - .app_delete(AppDeleteParams { - sender: sender_address.clone(), - app_id, - ..Default::default() - }) - .await - .unwrap(), - _ => unreachable!(), - }; - - match &tx { - Transaction::AppCall(app_fields) => { - assert_eq!(app_fields.header.sender, sender_address); - assert_eq!(app_fields.on_complete, on_complete); - if app_id == 0 { - assert_eq!(app_fields.app_id, 0); - assert!(app_fields.approval_program.is_some()); - assert!(app_fields.clear_state_program.is_some()); - } else { - assert_eq!(app_fields.app_id, app_id); - } - } - _ => return Err("Expected AppCall transaction".into()), - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn method_call_returns_built_transactions( - #[with(GROUP_ANALYSIS_DISABLED)] - #[future] - algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - - let creator = algorand_fixture.algorand_client.create(); - - let method = ABIMethod::new( - "simple_call".to_string(), - vec![], - Some(ABIType::Uint(BitSize::new(64)?)), - Some("Simple call method".to_string()), - ); - - let params = AppCallMethodCallParams { - sender: sender_address.clone(), - app_id: 1, - method, - args: vec![], - on_complete: OnApplicationComplete::NoOp, - account_references: None, - app_references: None, - asset_references: None, - box_references: None, - ..Default::default() - }; - - let result = creator.app_call_method_call(params).await?; - - assert!(!result.is_empty()); - - match &result[0] { - Transaction::AppCall(app_fields) => { - assert_eq!(app_fields.header.sender, sender_address); - assert_eq!(app_fields.app_id, 1); - } - _ => return Err("Expected AppCall transaction".into()), - } - - Ok(()) -} - -#[rstest] -#[case::online(KeyRegType::Online)] -#[case::offline(KeyRegType::Offline)] -#[case::nonpart(KeyRegType::NonParticipation)] -#[tokio::test] -async fn key_registration_operations( - #[case] key_type: KeyRegType, - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - - let tx = match key_type { - KeyRegType::Online => { - algorand_fixture - .algorand_client - .create() - .online_key_registration(OnlineKeyRegistrationParams { - sender: sender_address.clone(), - vote_key: [1u8; 32], - selection_key: [2u8; 32], - vote_first: 1000, - vote_last: 2000, - state_proof_key: Some([3u8; 64]), - vote_key_dilution: 10000, - ..Default::default() - }) - .await? - } - KeyRegType::Offline => { - algorand_fixture - .algorand_client - .create() - .offline_key_registration(OfflineKeyRegistrationParams { - sender: sender_address.clone(), - ..Default::default() - }) - .await? - } - KeyRegType::NonParticipation => { - algorand_fixture - .algorand_client - .create() - .non_participation_key_registration(NonParticipationKeyRegistrationParams { - sender: sender_address.clone(), - ..Default::default() - }) - .await? - } - }; - - match &tx { - Transaction::KeyRegistration(key_fields) => { - assert_eq!(key_fields.header.sender, sender_address); - - match key_type { - KeyRegType::Online => { - assert_eq!(key_fields.vote_key, Some([1u8; 32])); - assert_eq!(key_fields.selection_key, Some([2u8; 32])); - assert_eq!(key_fields.vote_first, Some(1000)); - assert_eq!(key_fields.vote_last, Some(2000)); - assert_eq!(key_fields.vote_key_dilution, Some(10000)); - } - KeyRegType::Offline => { - assert!(key_fields.vote_key.is_none()); - assert!(key_fields.selection_key.is_none()); - } - KeyRegType::NonParticipation => { - assert_eq!(key_fields.non_participation, Some(true)); - } - } - } - _ => return Err("Expected KeyRegistration transaction".into()), - } - - Ok(()) -} - -#[derive(Debug, Clone)] -enum KeyRegType { - Online, - Offline, - NonParticipation, -} - -#[rstest] -#[tokio::test] -async fn transaction_creator_accepts_all_parameters( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - - // Test that TransactionCreator accepts parameters and creates transactions - // Validation happens at the sending level, not creation level - let params = AssetTransferParams { - sender: sender_address, - asset_id: u64::MAX, // This is valid for transaction creation - receiver: Address::default(), // Zero address is also valid for creation - amount: 1, - ..Default::default() - }; - let result = algorand_fixture - .algorand_client - .create() - .asset_transfer(params) - .await?; - - // Verify it created the expected transaction structure - if let Transaction::AssetTransfer(transfer_fields) = result { - assert_eq!(transfer_fields.asset_id, u64::MAX); - assert_eq!(transfer_fields.amount, 1); - } else { - return Err("Expected AssetTransfer transaction".into()); - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn transaction_has_valid_defaults( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - let sender_address = algorand_fixture.test_account.account().address(); - let receiver = algorand_fixture.generate_account(None).await?; - - let creator = algorand_fixture.algorand_client.create(); - - let receiver_address = receiver.account().address(); - - let params = PaymentParams { - sender: sender_address, - receiver: receiver_address, - amount: 1_000_000, - ..Default::default() - }; - - let tx = creator.payment(params).await?; - - let header = tx.header(); - assert!(header.fee.unwrap_or(0) >= 1000); - assert!(header.first_valid > 0); - assert!(header.last_valid > header.first_valid); - assert!(header.genesis_id.is_some()); - assert!(header.genesis_hash.is_some()); - - Ok(()) -} diff --git a/crates/algokit_utils/tests/transactions/mod.rs b/crates/algokit_utils/tests/transactions/mod.rs deleted file mode 100644 index a7384d358..000000000 --- a/crates/algokit_utils/tests/transactions/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod composer; -pub mod creator; -pub mod sender; diff --git a/crates/algokit_utils/tests/transactions/sender.rs b/crates/algokit_utils/tests/transactions/sender.rs deleted file mode 100644 index 4e3b10220..000000000 --- a/crates/algokit_utils/tests/transactions/sender.rs +++ /dev/null @@ -1,415 +0,0 @@ -use crate::common::{ - AlgorandFixture, AlgorandFixtureResult, TestAccount, TestResult, algorand_fixture, - deploy_arc56_contract, -}; -use algokit_abi::{ABIValue, Arc56Contract}; -use algokit_test_artifacts::sandbox; -use algokit_transact::{Address, OnApplicationComplete}; -use algokit_utils::transactions::{ - AppCallMethodCallParams, AppCreateParams, AppMethodCallArg, AssetCreateParams, - AssetOptInParams, AssetOptOutParams, AssetTransferParams, PaymentParams, - TransactionSenderError, -}; -use rstest::*; -use std::sync::Arc; - -#[rstest] -#[tokio::test] -async fn test_payment_returns_rich_result( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - - let sender_address = algorand_fixture.test_account.account().address(); - let receiver = algorand_fixture.generate_account(None).await?; - - let params = PaymentParams { - sender: sender_address, - receiver: receiver.account().address(), - amount: 1_000_000, - ..Default::default() - }; - - let result = algorand_fixture - .algorand_client - .send() - .payment(params, None) - .await?; - - // Validate rich result orchestration - Sender's unique value - assert!(result.confirmation.confirmed_round.is_some()); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_zero_amount_payment_allowed( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - - let sender_address = algorand_fixture.test_account.account().address(); - let receiver = algorand_fixture.generate_account(None).await?; - - let params = PaymentParams { - sender: sender_address, - receiver: receiver.account().address(), - amount: 0, // Zero amount should be allowed - ..Default::default() - }; - - let result = algorand_fixture - .algorand_client - .send() - .payment(params, None) - .await?; - - // Validate that zero-amount payment succeeds - assert!(result.confirmation.confirmed_round.is_some()); - - // Verify the transaction has amount 0 - if let algokit_transact::Transaction::Payment(payment_fields) = &result.transaction { - assert_eq!(payment_fields.amount, 0); - } else { - return Err("Expected payment transaction".into()); - } - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_asset_create_extracts_asset_id( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - - let sender_address = algorand_fixture.test_account.account().address(); - - let params = AssetCreateParams { - sender: sender_address, - total: 1000, - decimals: Some(2), - unit_name: Some("TEST".to_string()), - asset_name: Some("Test Asset".to_string()), - ..Default::default() - }; - - let result = algorand_fixture - .algorand_client - .send() - .asset_create(params, None) - .await?; - - // Validate ID extraction from confirmation - Sender's orchestration value - assert!(result.asset_id > 0); - assert!(result.confirmation.confirmed_round.is_some()); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_app_create_extracts_app_id( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - - let sender_address: Address = algorand_fixture.test_account.account().address(); - - let params = AppCreateParams { - sender: sender_address, - on_complete: OnApplicationComplete::NoOp, - approval_program: vec![0x06, 0x81, 0x01], - clear_state_program: vec![0x06, 0x81, 0x01], - ..Default::default() - }; - - let result = algorand_fixture - .algorand_client - .send() - .app_create(params, None) - .await?; - - // Validate ID extraction from confirmation - Sender's orchestration value - assert!(result.app_id > 0); - assert!(result.confirmation.confirmed_round.is_some()); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_abi_method_returns_enhanced_processing( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - - let sender_address: Address = algorand_fixture.test_account.account().address(); - - // Deploy ABI app using existing pattern - let arc56_contract: Arc56Contract = serde_json::from_str(sandbox::APPLICATION_ARC56)?; - let app_id = deploy_arc56_contract( - &algorand_fixture, - &sender_address, - &arc56_contract, - None, - None, - None, - ) - .await?; - - let method = arc56_contract.find_abi_method("hello_world")?; - - let params = AppCallMethodCallParams { - sender: sender_address, - app_id, - method, - args: vec![AppMethodCallArg::ABIValue(ABIValue::String( - "world".to_string(), - ))], - on_complete: OnApplicationComplete::NoOp, - account_references: None, - app_references: None, - asset_references: None, - box_references: None, - ..Default::default() - }; - - let result = algorand_fixture - .algorand_client - .send() - .app_call_method_call(params, None) - .await?; - - // Validate enhanced ABI return processing with AppManager - Sender's orchestration value - assert!(!result.group_results.is_empty()); - assert!(result.result.confirmation.confirmed_round.is_some()); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_asset_opt_out_uses_asset_manager_coordination( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - let sender_address: Address = algorand_fixture.test_account.account().address(); - - // Create asset and opt-in account - let asset_id = create_test_asset(&algorand_fixture, &sender_address).await?; - let opt_out_account = algorand_fixture.generate_account(None).await?; - let opt_out_address = opt_out_account.account().address(); - - // Opt-in to asset - opt_in_to_asset( - &algorand_fixture, - opt_out_address.clone(), - asset_id, - opt_out_account.clone(), - ) - .await?; - - let params = AssetOptOutParams { - sender: opt_out_address, - signer: Some(Arc::new(opt_out_account)), - asset_id, - close_remainder_to: None, // Let it auto-resolve to creator - ..Default::default() - }; - - let result = algorand_fixture - .algorand_client - .send() - .asset_opt_out(params, None, Some(true)) - .await?; - - // Validate Sender orchestrated AssetManager to resolve creator automatically - assert!(result.confirmation.confirmed_round.is_some()); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_asset_opt_out_with_balance_validation( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - let sender_address: Address = algorand_fixture.test_account.account().address(); - - // Create asset and transfer some to account - let asset_id = create_test_asset(&algorand_fixture, &sender_address).await?; - let opt_out_account = algorand_fixture.generate_account(None).await?; - let opt_out_address = opt_out_account.account().address(); - - // Opt-in and receive assets - opt_in_to_asset( - &algorand_fixture, - opt_out_address.clone(), - asset_id, - opt_out_account.clone(), - ) - .await?; - - let transfer_params = AssetTransferParams { - sender: sender_address.clone(), - asset_id, - amount: 10, - receiver: opt_out_address.clone(), - ..Default::default() - }; - algorand_fixture - .algorand_client - .send() - .asset_transfer(transfer_params, None) - .await?; - - // Attempt opt-out with non-zero balance - let params = AssetOptOutParams { - sender: opt_out_address, - signer: Some(Arc::new(opt_out_account)), - asset_id, - close_remainder_to: None, // Let it auto-resolve to creator - ..Default::default() - }; - - let result = algorand_fixture - .algorand_client - .send() - .asset_opt_out(params, None, Some(true)) - .await; - - // Validate Sender's coordination with AssetManager for balance checking - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("does not have a zero balance") - ); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_validation_error_propagation( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - - let opt_out_account = algorand_fixture.generate_account(None).await?; - let opt_out_address = opt_out_account.account().address(); - - // Try to opt out of non-existent asset - this triggers validation - let params = AssetOptOutParams { - sender: opt_out_address, - signer: Some(Arc::new(opt_out_account)), - asset_id: 999999999, // Non-existent asset - close_remainder_to: None, // Let it try to auto-resolve (will fail for non-existent asset) - ..Default::default() - }; - - let result = algorand_fixture - .algorand_client - .send() - .asset_opt_out(params, None, Some(true)) - .await; - - // Validate Sender properly propagates validation errors from AssetManager coordination - assert!(result.is_err()); - let error = result.unwrap_err(); - assert!(matches!( - error, - TransactionSenderError::ValidationError { message: _ } - )); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_transaction_confirmation_integration( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let mut algorand_fixture = algorand_fixture.await?; - let sender_address: Address = algorand_fixture.test_account.account().address(); - let receiver = algorand_fixture.generate_account(None).await?; - - let params = PaymentParams { - sender: sender_address, - receiver: receiver.account().address(), - amount: 1_000_000, - ..Default::default() - }; - - let result = algorand_fixture - .algorand_client - .send() - .payment(params, None) - .await?; - - // Validate Sender's coordination of transaction confirmation - assert!(result.confirmation.confirmed_round.is_some()); - assert!(result.confirmation.confirmed_round.unwrap() > 0); - - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn test_new_composer_creates_composer( - #[future] algorand_fixture: AlgorandFixtureResult, -) -> TestResult { - let algorand_fixture = algorand_fixture.await?; - let _composer = algorand_fixture.algorand_client.send().new_composer(None); - - // Validate Sender's Composer orchestration capability - // Implementation details tested in composer tests - Ok(()) -} - -async fn create_test_asset( - algorand_fixture: &AlgorandFixture, - sender_address: &Address, -) -> Result> { - let params = AssetCreateParams { - sender: sender_address.clone(), - total: 1000, - decimals: Some(0), - unit_name: Some("TEST".to_string()), - asset_name: Some("Test Asset".to_string()), - ..Default::default() - }; - - let result = algorand_fixture - .algorand_client - .send() - .asset_create(params, None) - .await?; - Ok(result.asset_id) -} - -async fn opt_in_to_asset( - algorand_fixture: &AlgorandFixture, - address: Address, - asset_id: u64, - account: TestAccount, -) -> Result<(), Box> { - let params = AssetOptInParams { - sender: address, - signer: Some(Arc::new(account)), - asset_id, - ..Default::default() - }; - - algorand_fixture - .algorand_client - .send() - .asset_opt_in(params, None) - .await?; - Ok(()) -} diff --git a/crates/algokit_utils/tests/transactions_tests.rs b/crates/algokit_utils/tests/transactions_tests.rs deleted file mode 100644 index 50264c64e..000000000 --- a/crates/algokit_utils/tests/transactions_tests.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod common; -mod transactions; diff --git a/crates/algokit_utils_ffi/Cargo.toml b/crates/algokit_utils_ffi/Cargo.toml deleted file mode 100644 index 69d4899a0..000000000 --- a/crates/algokit_utils_ffi/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "algokit_utils_ffi" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["lib", "cdylib", "staticlib"] - -[dependencies] -algod_client = { version = "0.0.1", path = "../algod_client", default-features = false } -algokit_abi = { version = "0.1.0", path = "../algokit_abi" } -algokit_http_client = { version = "0.1.0", path = "../algokit_http_client", features = ["ffi_uniffi"], default-features = false } -algokit_transact = { version = "0.1.0", path = "../algokit_transact" } -algokit_transact_ffi = { version = "0.1.0", path = "../algokit_transact_ffi" } -algokit_utils = { version = "0.1.0", path = "../algokit_utils" } -async-trait = "0.1.89" -base32 = "0.5.1" -base64 = "0.22.1" -derive_more = "2.0.1" -ed25519-dalek = "2.1.1" -lazy_static = "1.5.0" -num-bigint = "0.4.6" -rand = "0.8" -regex = "1.11.1" -sha2 = "0.10" -snafu.workspace = true -tokio = { version = "1.47.1", features = ["time", "process"] } -uniffi = { workspace = true, features = [ - "scaffolding-ffi-buffer-fns", -] } diff --git a/crates/algokit_utils_ffi/src/abi/abi_type.rs b/crates/algokit_utils_ffi/src/abi/abi_type.rs deleted file mode 100644 index 6620f53ce..000000000 --- a/crates/algokit_utils_ffi/src/abi/abi_type.rs +++ /dev/null @@ -1,256 +0,0 @@ -use std::{str::FromStr, sync::Arc}; - -use algokit_abi::ABIType as RustABIType; -use algokit_abi::types::r#struct::ABIStruct as RustABIStruct; -use algokit_abi::types::r#struct::StructField as RustStructField; -use algokit_abi::types::r#struct::StructFieldType as RustStructFieldType; - -use crate::transactions::common::UtilsError; - -#[derive(Debug, Clone, uniffi::Enum)] -pub enum StructFieldType { - Type(Arc), - Fields(Vec>), -} - -impl From for StructFieldType { - fn from(value: RustStructFieldType) -> Self { - match value { - RustStructFieldType::Type(t) => { - StructFieldType::Type(Arc::new(ABIType { abi_type: t })) - } - RustStructFieldType::Fields(f) => StructFieldType::Fields( - f.into_iter() - .map(|sf| { - Arc::new(StructField { - name: sf.name, - field_type: sf.field_type.into(), - }) - }) - .collect(), - ), - } - } -} - -impl From for RustStructFieldType { - fn from(value: StructFieldType) -> Self { - match value { - StructFieldType::Type(t) => RustStructFieldType::Type(t.abi_type.clone()), - StructFieldType::Fields(f) => RustStructFieldType::Fields( - f.iter() - .map(|sf| RustStructField { - name: sf.name.clone(), - field_type: sf.field_type.clone().into(), - }) - .collect(), - ), - } - } -} - -/// Represents a field in a struct -#[derive(Debug, uniffi::Object)] -pub struct StructField { - pub name: String, - pub field_type: StructFieldType, -} - -#[uniffi::export] -impl StructField { - #[uniffi::constructor] - pub fn new(name: String, field_type: StructFieldType) -> Self { - StructField { name, field_type } - } -} - -impl From for StructField { - fn from(value: RustStructField) -> Self { - StructField { - name: value.name, - field_type: value.field_type.into(), - } - } -} - -impl From<&StructField> for RustStructField { - fn from(value: &StructField) -> Self { - RustStructField { - name: value.name.clone(), - field_type: value.field_type.clone().into(), - } - } -} - -#[derive(uniffi::Object)] -pub struct ABIStruct { - /// The name of the struct type - pub name: String, - /// The fields of the struct in order - pub fields: Vec, -} - -impl From for ABIStruct { - fn from(value: RustABIStruct) -> Self { - ABIStruct { - name: value.name, - fields: value.fields.into_iter().map(|f| f.into()).collect(), - } - } -} - -impl From for RustABIStruct { - fn from(value: ABIStruct) -> Self { - RustABIStruct { - name: value.name, - fields: value.fields.into_iter().map(|f| (&f).into()).collect(), - } - } -} - -#[derive(uniffi::Object, Debug)] -pub struct ABIType { - pub abi_type: RustABIType, -} - -#[uniffi::export] -impl ABIType { - #[uniffi::constructor] - pub fn from_string(type_str: &str) -> Result { - RustABIType::from_str(type_str) - .map(|abi_type| ABIType { abi_type }) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - #[allow(clippy::inherent_to_string)] - pub fn to_string(&self) -> String { - self.abi_type.to_string() - } - - pub fn encode(&self, value: &crate::abi::abi_value::ABIValue) -> Result, UtilsError> { - self.abi_type - .encode(&value.rust_value) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - pub fn decode(&self, data: &[u8]) -> Result { - self.abi_type - .decode(data) - .map(|v| crate::abi::abi_value::ABIValue { rust_value: v }) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - #[uniffi::constructor] - pub fn uint(bit_size: u16) -> Result { - let abi_type = - RustABIType::Uint(algokit_abi::abi_type::BitSize::new(bit_size).map_err(|_| { - UtilsError::UtilsError { - message: format!("Invalid bit size: {}", bit_size), - } - })?); - - Ok(ABIType { abi_type }) - } - - #[uniffi::constructor] - pub fn ufixed(bit_size: u16, precision: u8) -> Result { - let abi_type = RustABIType::UFixed( - algokit_abi::abi_type::BitSize::new(bit_size).map_err(|_| UtilsError::UtilsError { - message: format!("Invalid bit size: {}", bit_size), - })?, - algokit_abi::abi_type::Precision::new(precision).map_err(|_| { - UtilsError::UtilsError { - message: format!("Invalid precision: {}", precision), - } - })?, - ); - - Ok(ABIType { abi_type }) - } - - #[uniffi::constructor] - pub fn address() -> Self { - ABIType { - abi_type: RustABIType::Address, - } - } - - #[uniffi::constructor] - pub fn tuple(elements: Vec>) -> Self { - let rust_elements = elements.into_iter().map(|e| e.abi_type.clone()).collect(); - ABIType { - abi_type: RustABIType::Tuple(rust_elements), - } - } - - #[uniffi::constructor] - pub fn string() -> Self { - ABIType { - abi_type: RustABIType::String, - } - } - - #[uniffi::constructor] - pub fn byte() -> Self { - ABIType { - abi_type: RustABIType::Byte, - } - } - - #[uniffi::constructor] - pub fn bool() -> Self { - ABIType { - abi_type: RustABIType::Bool, - } - } - - #[uniffi::constructor] - pub fn static_array(element_type: Arc, length: u16) -> Result { - let abi_type = - RustABIType::StaticArray(Box::new(element_type.abi_type.clone()), length as usize); - Ok(ABIType { abi_type }) - } - - #[uniffi::constructor] - pub fn dynamic_array(element_type: Arc) -> Self { - let abi_type = RustABIType::DynamicArray(Box::new(element_type.abi_type.clone())); - ABIType { abi_type } - } - - #[uniffi::constructor] - pub fn struct_fields(name: String, fields: Vec>) -> Self { - let rust_fields = fields.into_iter().map(|f| f.as_ref().into()).collect(); - let abi_type = RustABIType::Struct(RustABIStruct { - name, - fields: rust_fields, - }); - ABIType { abi_type } - } - - #[uniffi::constructor] - pub fn avm_bytes() -> Self { - ABIType { - abi_type: RustABIType::AVMBytes, - } - } - - #[uniffi::constructor] - pub fn avm_string() -> Self { - ABIType { - abi_type: RustABIType::AVMString, - } - } - - #[uniffi::constructor] - pub fn avm_uint64() -> Self { - ABIType { - abi_type: RustABIType::AVMUint64, - } - } -} diff --git a/crates/algokit_utils_ffi/src/abi/abi_value.rs b/crates/algokit_utils_ffi/src/abi/abi_value.rs deleted file mode 100644 index 2e67cf2cd..000000000 --- a/crates/algokit_utils_ffi/src/abi/abi_value.rs +++ /dev/null @@ -1,200 +0,0 @@ -use std::{collections::HashMap, sync::Arc}; - -use algokit_abi::ABIValue as RustABIValue; -use num_bigint::BigUint; - -use crate::transactions::common::UtilsError; - -// NOTE: Once we get a release that enables custom types with Python (it's on main), we can use them to provide a better ux: https://github.com/mozilla/uniffi-rs/issues/2652#issuecomment-3307297845 - -#[derive(uniffi::Object, Debug, Clone, PartialEq)] -#[uniffi::export(Eq)] -pub struct ABIValue { - pub rust_value: RustABIValue, -} - -#[uniffi::export] -impl ABIValue { - #[uniffi::constructor] - pub fn bool(value: bool) -> Self { - ABIValue { - rust_value: RustABIValue::Bool(value), - } - } - - pub fn get_bool(&self) -> Result { - if let RustABIValue::Bool(b) = &self.rust_value { - Ok(*b) - } else { - Err(UtilsError::UtilsError { - message: "ABI value is not a bool".to_string(), - }) - } - } - - #[uniffi::constructor] - pub fn array(values: Vec>) -> Self { - ABIValue { - rust_value: RustABIValue::Array( - values.into_iter().map(|v| v.rust_value.clone()).collect(), - ), - } - } - - pub fn get_array(&self) -> Result>, UtilsError> { - if let RustABIValue::Array(arr) = &self.rust_value { - Ok(arr - .iter() - .cloned() - .map(|v| Arc::new(ABIValue { rust_value: v })) - .collect()) - } else { - Err(UtilsError::UtilsError { - message: "ABI value is not an array".to_string(), - }) - } - } - - #[uniffi::constructor] - pub fn uint(value: u64) -> Self { - ABIValue { - rust_value: RustABIValue::Uint(num_bigint::BigUint::from(value)), - } - } - - pub fn get_uint(&self) -> Result { - if let RustABIValue::Uint(u) = &self.rust_value { - let digits = u.to_u64_digits(); - if digits.len() == 1 { - Ok(digits[0]) - } else if digits.is_empty() { - Ok(0) - } else { - Err(UtilsError::UtilsError { - message: "ABI uint value is too large to fit in u64".to_string(), - }) - } - } else { - Err(UtilsError::UtilsError { - message: "ABI value is not a uint".to_string(), - }) - } - } - - #[uniffi::constructor] - pub fn biguint(value: Vec) -> Self { - ABIValue { - rust_value: RustABIValue::Uint(BigUint::from_bytes_be(&value)), - } - } - - pub fn get_big_uint(&self) -> Result, UtilsError> { - if let RustABIValue::Uint(u) = &self.rust_value { - Ok(u.to_bytes_be()) - } else { - Err(UtilsError::UtilsError { - message: "ABI value is not a uint".to_string(), - }) - } - } - - #[uniffi::constructor] - pub fn string(value: String) -> Self { - ABIValue { - rust_value: RustABIValue::String(value), - } - } - - pub fn get_string(&self) -> Result { - if let RustABIValue::String(s) = &self.rust_value { - Ok(s.clone()) - } else { - Err(UtilsError::UtilsError { - message: "ABI value is not a string".to_string(), - }) - } - } - - #[uniffi::constructor] - pub fn byte(value: u8) -> Self { - ABIValue { - rust_value: RustABIValue::Byte(value), - } - } - - pub fn get_byte(&self) -> Result { - if let RustABIValue::Byte(b) = &self.rust_value { - Ok(*b) - } else { - Err(UtilsError::UtilsError { - message: "ABI value is not a byte".to_string(), - }) - } - } - - #[uniffi::constructor] - pub fn address(value: String) -> Self { - ABIValue { - rust_value: RustABIValue::Address(value), - } - } - - pub fn get_address(&self) -> Result { - if let RustABIValue::Address(a) = &self.rust_value { - Ok(a.clone()) - } else { - Err(UtilsError::UtilsError { - message: "ABI value is not an address".to_string(), - }) - } - } - - #[uniffi::constructor] - pub fn bytes(value: Vec) -> Self { - ABIValue { - rust_value: RustABIValue::Bytes(value), - } - } - - pub fn get_bytes(&self) -> Result, UtilsError> { - if let RustABIValue::Bytes(b) = &self.rust_value { - Ok(b.clone()) - } else { - Err(UtilsError::UtilsError { - message: "ABI value is not bytes".to_string(), - }) - } - } - - #[uniffi::constructor] - pub fn struct_fields(fields: HashMap>) -> Self { - ABIValue { - rust_value: RustABIValue::Struct( - fields - .into_iter() - .map(|(k, v)| (k, v.rust_value.clone())) - .collect(), - ), - } - } - - pub fn get_struct_fields(&self) -> Result>, UtilsError> { - if let RustABIValue::Struct(map) = &self.rust_value { - Ok(map - .iter() - .map(|(k, v)| { - ( - k.clone(), - Arc::new(ABIValue { - rust_value: v.clone(), - }), - ) - }) - .collect()) - } else { - Err(UtilsError::UtilsError { - message: "ABI value is not a struct".to_string(), - }) - } - } -} diff --git a/crates/algokit_utils_ffi/src/abi/mod.rs b/crates/algokit_utils_ffi/src/abi/mod.rs deleted file mode 100644 index e070ff8ee..000000000 --- a/crates/algokit_utils_ffi/src/abi/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod abi_type; -pub mod abi_value; diff --git a/crates/algokit_utils_ffi/src/clients/algod_client.rs b/crates/algokit_utils_ffi/src/clients/algod_client.rs deleted file mode 100644 index 95da5afcd..000000000 --- a/crates/algokit_utils_ffi/src/clients/algod_client.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::transactions::common::UtilsError; -use async_trait::async_trait; - -#[derive(uniffi::Record)] -pub struct AccountInfo { - pub balance: u64, - pub min_balance: u64, - pub created_apps: Vec, - pub created_assets: Vec, -} - -#[derive(uniffi::Record)] -pub struct TransactionInfo { - pub tx_id: String, - pub confirmed_round: Option, - pub asset_id: Option, - pub app_id: Option, -} - -#[derive(uniffi::Record)] -pub struct SuggestedParams { - pub fee: u64, - pub first_valid_round: u64, - pub last_valid_round: u64, - pub genesis_hash: Vec, - pub genesis_id: String, -} - -#[uniffi::export(with_foreign)] -#[async_trait] -pub trait AlgodClientTrait: Send + Sync { - async fn send_transaction(&self, txn: Vec) -> Result; - async fn get_account_info(&self, address: String) -> Result; - async fn get_transaction_info(&self, tx_id: String) -> Result; - async fn wait_for_confirmation(&self, tx_id: String) -> Result; - async fn get_suggested_params(&self) -> Result; -} diff --git a/crates/algokit_utils_ffi/src/clients/mod.rs b/crates/algokit_utils_ffi/src/clients/mod.rs deleted file mode 100644 index f9b189e3e..000000000 --- a/crates/algokit_utils_ffi/src/clients/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod algod_client; diff --git a/crates/algokit_utils_ffi/src/lib.rs b/crates/algokit_utils_ffi/src/lib.rs deleted file mode 100644 index abbc78765..000000000 --- a/crates/algokit_utils_ffi/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod abi; -pub mod clients; -// TODO: put tests behind a testing feature flag -pub mod tests; -pub mod transactions; - -uniffi::setup_scaffolding!(); diff --git a/crates/algokit_utils_ffi/src/tests/asset_freeze_tests.rs b/crates/algokit_utils_ffi/src/tests/asset_freeze_tests.rs deleted file mode 100644 index c405e0804..000000000 --- a/crates/algokit_utils_ffi/src/tests/asset_freeze_tests.rs +++ /dev/null @@ -1,306 +0,0 @@ -use crate::{ - clients::algod_client::AlgodClientTrait, - tests::fixtures::{TestAccount, TestFixture}, - transactions::{ - asset_freeze::{AssetFreezeParams, AssetUnfreezeParams}, - asset_transfer::{AssetOptInParams, AssetTransferParams}, - common::{TransactionSignerGetter, UtilsError}, - composer::ComposerFactory, - }, -}; -use std::sync::Arc; -use std::time::Instant; - -/// Result of a single test -#[derive(uniffi::Record, Clone, Debug)] -pub struct TestResult { - pub name: String, - pub passed: bool, - pub duration_ms: u64, - pub error: Option, -} - -/// Result of an entire test suite -#[derive(uniffi::Record, Clone, Debug)] -pub struct TestSuiteResult { - pub name: String, - pub results: Vec, - pub all_passed: bool, - pub total_duration_ms: u64, -} - -/// Run the complete asset freeze test suite -/// This is the main entry point called from Python/Swift/Kotlin -#[uniffi::export] -pub async fn run_asset_freeze_test_suite( - algod_client: Arc, - composer_factory: Arc, - signer_getter: Arc, -) -> Result { - let suite_start = Instant::now(); - let mut results = Vec::new(); - - // Initialize test fixture with foreign traits - let fixture = TestFixture::new( - algod_client.clone(), - composer_factory.clone(), - signer_getter, - ) - .await?; - - // Test 1: Asset Creation and Setup - let test_start = Instant::now(); - let test1_result = run_asset_creation_setup_test(&fixture).await; - let (creator, freeze_manager, asset_id) = match &test1_result { - Ok(data) => data.clone(), - Err(e) => { - results.push(TestResult { - name: "Asset Creation and Setup".to_string(), - passed: false, - duration_ms: test_start.elapsed().as_millis() as u64, - error: Some(format!("{:?}", e)), - }); - - return Ok(TestSuiteResult { - name: "Asset Freeze Test Suite".to_string(), - results, - all_passed: false, - total_duration_ms: suite_start.elapsed().as_millis() as u64, - }); - } - }; - - results.push(TestResult { - name: "Asset Creation and Setup".to_string(), - passed: true, - duration_ms: test_start.elapsed().as_millis() as u64, - error: None, - }); - - // Test 2: Asset Freeze and Unfreeze Test (combined) - let test_start = Instant::now(); - let test2_result = - run_asset_freeze_and_unfreeze_test(&fixture, creator, freeze_manager, asset_id).await; - results.push(TestResult { - name: "Asset Freeze and Unfreeze Test".to_string(), - passed: test2_result.is_ok(), - duration_ms: test_start.elapsed().as_millis() as u64, - error: test2_result.err().map(|e| format!("{:?}", e)), - }); - - let all_passed = results.iter().all(|r| r.passed); - let total_duration_ms = suite_start.elapsed().as_millis() as u64; - - Ok(TestSuiteResult { - name: "Asset Freeze Test Suite".to_string(), - results, - all_passed, - total_duration_ms, - }) -} - -/// Test: Create asset with freeze manager -async fn run_asset_creation_setup_test( - fixture: &TestFixture, -) -> Result<(TestAccount, TestAccount, u64), UtilsError> { - // Generate creator and freeze manager accounts - let creator = fixture.generate_account()?; - let freeze_manager = fixture.generate_account()?; - - // Fund both accounts - fixture.fund_account(creator.clone(), 10_000_000).await?; - fixture - .fund_account(freeze_manager.clone(), 10_000_000) - .await?; - - // Create asset with freeze manager - let asset_id = fixture - .create_test_asset(creator.clone(), Some(freeze_manager.clone())) - .await?; - - Ok((creator, freeze_manager, asset_id)) -} - -/// Test: Freeze an account, verify transfers are blocked, then unfreeze and verify transfers work -async fn run_asset_freeze_and_unfreeze_test( - fixture: &TestFixture, - creator: TestAccount, - freeze_manager: TestAccount, - asset_id: u64, -) -> Result<(), UtilsError> { - // Step 1: Generate target account and fund it - let target = fixture.generate_account()?; - fixture.fund_account(target.clone(), 1_000_000).await?; - - // Step 2: Target opts into asset - let target_signer = fixture.signer_getter.get_signer(target.address.clone())?; - let opt_in_params = AssetOptInParams { - sender: target.address.clone(), - asset_id, - signer: Some(target_signer.clone()), - 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, - }; - - let opt_in_composer = fixture.composer_factory.create_composer(); - opt_in_composer.add_asset_opt_in(opt_in_params).await?; - opt_in_composer.build().await?; - opt_in_composer.send().await?; - - // Step 3: Transfer assets to target (initial balance) - let creator_signer = fixture.signer_getter.get_signer(creator.address.clone())?; - let transfer_params = AssetTransferParams { - sender: creator.address.clone(), - asset_id, - amount: 100, - receiver: target.address.clone(), - signer: Some(creator_signer.clone()), - 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, - }; - - let transfer_composer = fixture.composer_factory.create_composer(); - transfer_composer - .add_asset_transfer(transfer_params) - .await?; - transfer_composer.build().await?; - transfer_composer.send().await?; - - // Step 4: Freeze manager freezes target account - let freeze_signer = fixture - .signer_getter - .get_signer(freeze_manager.address.clone())?; - let freeze_params = AssetFreezeParams { - sender: freeze_manager.address.clone(), - asset_id, - target_address: target.address.clone(), - signer: Some(freeze_signer.clone()), - 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, - }; - - let freeze_composer = fixture.composer_factory.create_composer(); - freeze_composer.add_asset_freeze(freeze_params).await?; - freeze_composer.build().await?; - freeze_composer.send().await?; - - // Step 5: Try to transfer from frozen account (should fail) - let transfer_from_frozen_params = AssetTransferParams { - sender: target.address.clone(), - asset_id, - amount: 1, - receiver: creator.address.clone(), - signer: Some(target_signer.clone()), - 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, - }; - - let frozen_transfer_composer = fixture.composer_factory.create_composer(); - frozen_transfer_composer - .add_asset_transfer(transfer_from_frozen_params) - .await?; - frozen_transfer_composer.build().await?; - - // This should fail because account is frozen - let transfer_result = frozen_transfer_composer.send().await; - - match transfer_result { - Ok(_) => { - return Err(UtilsError::UtilsError { - message: "Transfer from frozen account should have failed but succeeded" - .to_string(), - }); - } - Err(e) => { - // Verify it failed for the RIGHT reason (asset frozen) - let error_msg = e.to_string(); - if !error_msg.contains("frozen") { - return Err(UtilsError::UtilsError { - message: format!( - "Transfer failed with unexpected error (expected 'frozen'): {}", - error_msg - ), - }); - } - } - } - - // Step 6: Unfreeze the account - let unfreeze_params = AssetUnfreezeParams { - sender: freeze_manager.address.clone(), - asset_id, - target_address: target.address.clone(), - signer: Some(freeze_signer), - 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, - }; - - let unfreeze_composer = fixture.composer_factory.create_composer(); - unfreeze_composer - .add_asset_unfreeze(unfreeze_params) - .await?; - unfreeze_composer.build().await?; - unfreeze_composer.send().await?; - - // Step 7: Transfer from unfrozen account (should now succeed) - let transfer_after_unfreeze_params = AssetTransferParams { - sender: target.address.clone(), - asset_id, - amount: 50, - receiver: creator.address.clone(), - signer: Some(target_signer), - 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, - }; - - let unfrozen_transfer_composer = fixture.composer_factory.create_composer(); - unfrozen_transfer_composer - .add_asset_transfer(transfer_after_unfreeze_params) - .await?; - unfrozen_transfer_composer.build().await?; - unfrozen_transfer_composer.send().await?; - - Ok(()) // Test passed - freeze blocked transfer, unfreeze allowed transfer -} diff --git a/crates/algokit_utils_ffi/src/tests/fixtures/localnet.rs b/crates/algokit_utils_ffi/src/tests/fixtures/localnet.rs deleted file mode 100644 index 008e4588d..000000000 --- a/crates/algokit_utils_ffi/src/tests/fixtures/localnet.rs +++ /dev/null @@ -1,159 +0,0 @@ -use crate::transactions::common::UtilsError; -use regex::Regex; -use std::process::Command; - -/// Fetch the dispenser mnemonic from LocalNet -pub async fn get_dispenser_mnemonic() -> Result { - // Check LocalNet status and start if needed - ensure_localnet_running().await?; - - // Get account with highest balance - let dispenser_address = find_dispenser_account().await?; - - // Export and return mnemonic - export_account_mnemonic(&dispenser_address).await -} - -/// Ensure LocalNet is running, start it if not -async fn ensure_localnet_running() -> Result<(), UtilsError> { - // Check LocalNet status - let status_output = Command::new("algokit") - .args(["localnet", "status"]) - .output() - .map_err(|e| UtilsError::UtilsError { - message: format!("Failed to check LocalNet status: {}", e), - })?; - - let status_str = String::from_utf8_lossy(&status_output.stdout); - - if !status_str.to_lowercase().contains("running") { - // Try to start LocalNet - let start_output = Command::new("algokit") - .args(["localnet", "start"]) - .output() - .map_err(|e| UtilsError::UtilsError { - message: format!("Failed to start LocalNet: {}", e), - })?; - - if !start_output.status.success() { - return Err(UtilsError::UtilsError { - message: format!( - "Failed to start LocalNet: {}", - String::from_utf8_lossy(&start_output.stderr) - ), - }); - } - - // Wait a bit for LocalNet to fully start - tokio::time::sleep(std::time::Duration::from_secs(3)).await; - } - - Ok(()) -} - -/// Find the LocalNet account with the highest balance (dispenser) -async fn find_dispenser_account() -> Result { - let accounts_output = Command::new("algokit") - .args([ - "goal", - "account", - "list", - "-w", - "unencrypted-default-wallet", - ]) - .output() - .map_err(|e| UtilsError::UtilsError { - message: format!("Failed to list accounts: {}", e), - })?; - - if !accounts_output.status.success() { - return Err(UtilsError::UtilsError { - message: format!( - "Failed to list accounts: {}", - String::from_utf8_lossy(&accounts_output.stderr) - ), - }); - } - - let output_str = String::from_utf8_lossy(&accounts_output.stdout); - - // Create regex pattern for parsing account lines - let re = - Regex::new(r"([A-Z0-9]{58})\s+(\d+)\s+microAlgos").map_err(|e| UtilsError::UtilsError { - message: format!("Regex error: {}", e), - })?; - - let mut highest_balance = 0u64; - let mut dispenser_address = String::new(); - - // Find account with highest balance - for cap in re.captures_iter(&output_str) { - 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(UtilsError::UtilsError { - message: "No funded accounts found in LocalNet".to_string(), - }); - } - - Ok(dispenser_address) -} - -/// Export the mnemonic for a given account address -async fn export_account_mnemonic(address: &str) -> Result { - let export_output = Command::new("algokit") - .args([ - "goal", - "account", - "export", - "-a", - address, - "-w", - "unencrypted-default-wallet", - ]) - .output() - .map_err(|e| UtilsError::UtilsError { - message: format!("Failed to export account {}: {}", address, e), - })?; - - if !export_output.status.success() { - return Err(UtilsError::UtilsError { - message: format!( - "Failed to export account {}: {}", - address, - String::from_utf8_lossy(&export_output.stderr) - ), - }); - } - - let export_str = String::from_utf8_lossy(&export_output.stdout); - - // Extract mnemonic from output - // The output format is typically: 'Exported key for account
: "mnemonic words"' - let mnemonic = if let Some(start_quote) = export_str.find('"') { - if let Some(end_quote) = export_str[start_quote + 1..].find('"') { - export_str[start_quote + 1..start_quote + 1 + end_quote].to_string() - } else { - // Fallback: take everything after the last colon - export_str - .rfind(": ") - .map(|idx| export_str[idx + 2..].trim().to_string()) - .unwrap_or_else(|| export_str.trim().to_string()) - } - } else { - // Fallback: take everything after the last colon or the whole string - export_str - .rfind(": ") - .map(|idx| export_str[idx + 2..].trim().to_string()) - .unwrap_or_else(|| export_str.trim().to_string()) - }; - - Ok(mnemonic.trim().to_string()) -} diff --git a/crates/algokit_utils_ffi/src/tests/fixtures/mnemonic.rs b/crates/algokit_utils_ffi/src/tests/fixtures/mnemonic.rs deleted file mode 100644 index bc5d57f3d..000000000 --- a/crates/algokit_utils_ffi/src/tests/fixtures/mnemonic.rs +++ /dev/null @@ -1,368 +0,0 @@ -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 = " "; - -// TODO: Move to a different location if we are to expose mnemonic to pkey functionality to end user. -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 - }; -} - -#[allow(clippy::enum_variant_names)] -/// 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/algokit_utils_ffi/src/tests/fixtures/mod.rs b/crates/algokit_utils_ffi/src/tests/fixtures/mod.rs deleted file mode 100644 index c32625830..000000000 --- a/crates/algokit_utils_ffi/src/tests/fixtures/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod localnet; -pub mod mnemonic; -pub mod test_account; -pub mod test_fixture; - -pub use test_account::TestAccount; -pub use test_fixture::TestFixture; diff --git a/crates/algokit_utils_ffi/src/tests/fixtures/test_account.rs b/crates/algokit_utils_ffi/src/tests/fixtures/test_account.rs deleted file mode 100644 index d54ec11d7..000000000 --- a/crates/algokit_utils_ffi/src/tests/fixtures/test_account.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::transactions::common::UtilsError; -use algokit_transact::{ALGORAND_SECRET_KEY_BYTE_LENGTH, KeyPairAccount}; -use ed25519_dalek::{SigningKey, VerifyingKey}; -use rand::rngs::OsRng; - -use super::mnemonic::{from_key, to_key}; - -#[derive(uniffi::Record, Clone)] -pub struct TestAccount { - pub address: String, - pub private_key: Vec, - pub mnemonic: String, -} - -impl TestAccount { - pub fn generate() -> Result { - // Generate a random signing key using ed25519_dalek - let signing_key = SigningKey::generate(&mut OsRng); - let private_key_bytes = signing_key.to_bytes(); - - // Get the public key and address - let verifying_key: VerifyingKey = (&signing_key).into(); - let account = KeyPairAccount::from_pubkey(&verifying_key.to_bytes()); - let address = account.address().to_string(); - - // Generate mnemonic from private key - let mnemonic = from_key(&private_key_bytes).map_err(|e| UtilsError::UtilsError { - message: format!("Failed to generate mnemonic: {}", e), - })?; - - Ok(TestAccount { - address, - private_key: private_key_bytes.to_vec(), - mnemonic, - }) - } - - pub fn from_mnemonic(mnemonic: String) -> Result { - // Convert 25-word mnemonic to 32-byte key using our mnemonic module - let private_key_bytes = to_key(&mnemonic).map_err(|e| UtilsError::UtilsError { - message: format!("Failed to parse mnemonic: {}", e), - })?; - - // Create signing key from bytes - let signing_key = SigningKey::from_bytes(&private_key_bytes); - let verifying_key: VerifyingKey = (&signing_key).into(); - let account = KeyPairAccount::from_pubkey(&verifying_key.to_bytes()); - let address = account.address().to_string(); - - Ok(TestAccount { - address, - private_key: private_key_bytes.to_vec(), - mnemonic, - }) - } - - pub fn account(&self) -> Result { - if self.private_key.len() != ALGORAND_SECRET_KEY_BYTE_LENGTH { - return Err(UtilsError::UtilsError { - message: "Invalid private key length".to_string(), - }); - } - - let mut key_bytes = [0u8; ALGORAND_SECRET_KEY_BYTE_LENGTH]; - key_bytes.copy_from_slice(&self.private_key); - - let signing_key = SigningKey::from_bytes(&key_bytes); - let verifying_key: VerifyingKey = (&signing_key).into(); - Ok(KeyPairAccount::from_pubkey(&verifying_key.to_bytes())) - } -} diff --git a/crates/algokit_utils_ffi/src/tests/fixtures/test_fixture.rs b/crates/algokit_utils_ffi/src/tests/fixtures/test_fixture.rs deleted file mode 100644 index 616ac6dd0..000000000 --- a/crates/algokit_utils_ffi/src/tests/fixtures/test_fixture.rs +++ /dev/null @@ -1,177 +0,0 @@ -use super::{localnet, test_account::TestAccount}; -use crate::{ - clients::algod_client::AlgodClientTrait, - transactions::{ - asset_config::AssetCreateParams, - common::{TransactionSignerGetter, UtilsError}, - composer::ComposerFactory, - payment::PaymentParams, - }, -}; -use std::sync::{Arc, Mutex}; - -/// Test fixture that provides high-level test operations using foreign traits -/// This enables test orchestration in Rust while delegating I/O to target languages -#[derive(uniffi::Object)] -pub struct TestFixture { - pub algod_client: Arc, - pub composer_factory: Arc, - pub signer_getter: Arc, - dispenser_account: TestAccount, - test_accounts: Mutex>, -} - -#[uniffi::export] -impl TestFixture { - /// Create a new test fixture with foreign trait dependencies - /// Gets the dispenser account mnemonic from localnet automatically - #[uniffi::constructor] - pub async fn new( - algod_client: Arc, - composer_factory: Arc, - signer_getter: Arc, - ) -> Result { - // Get dispenser mnemonic from localnet - let dispenser_mnemonic = localnet::get_dispenser_mnemonic().await?; - let dispenser_account = TestAccount::from_mnemonic(dispenser_mnemonic.clone())?; - - // Register dispenser account with signer getter - signer_getter.register_account( - dispenser_account.address.clone(), - dispenser_account.mnemonic.clone(), - )?; - - Ok(TestFixture { - algod_client, - composer_factory, - signer_getter, - dispenser_account, - test_accounts: Mutex::new(Vec::new()), - }) - } - - /// Generate a new test account and register it with the signer getter - pub fn generate_account(&self) -> Result { - let account = TestAccount::generate()?; - - // Register account with signer getter - self.signer_getter - .register_account(account.address.clone(), account.mnemonic.clone())?; - - // Track generated accounts - self.test_accounts.lock().unwrap().push(account.clone()); - Ok(account) - } - - /// Fund an account with ALGO from the dispenser - /// Uses the composer factory to create a fresh composer for this operation - pub async fn fund_account( - &self, - account: TestAccount, - amount: u64, - ) -> Result { - // Create a fresh composer for this operation (via factory) - let composer = self.composer_factory.create_composer(); - - let dispenser_signer = self - .signer_getter - .get_signer(self.dispenser_account.address.clone())?; - - // Build payment parameters - let payment_params = PaymentParams { - sender: self.dispenser_account.address.clone(), - receiver: account.address.clone(), - amount, - signer: Some(dispenser_signer), - 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, - }; - - // Add payment to composer - composer.add_payment(payment_params).await?; - - // Build and send transaction - composer.build().await?; - let tx_ids = composer.send().await?; - - // Return first transaction ID - tx_ids - .first() - .cloned() - .ok_or_else(|| UtilsError::UtilsError { - message: "No transaction ID returned".to_string(), - }) - } - - /// Create a test asset with optional freeze manager - /// Returns the asset ID - pub async fn create_test_asset( - &self, - creator: TestAccount, - freeze_manager: Option, - ) -> Result { - // Create a fresh composer - let composer = self.composer_factory.create_composer(); - - let creator_signer = self.signer_getter.get_signer(creator.address.clone())?; - - // Build asset creation parameters - let asset_params = AssetCreateParams { - sender: creator.address.clone(), - total: 1000000, - decimals: Some(0), - default_frozen: Some(false), - unit_name: Some("TEST".to_string()), - asset_name: Some("Test Asset".to_string()), - url: None, - metadata_hash: None, - manager: None, - reserve: None, - freeze: freeze_manager.map(|fm| fm.address), - clawback: None, - signer: Some(creator_signer), - 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, - }; - - // Add asset create to composer - composer.add_asset_create(asset_params).await?; - - // Build and send - composer.build().await?; - let tx_ids = composer.send().await?; - - // Wait for confirmation to get asset ID - let tx_id = tx_ids.first().ok_or_else(|| UtilsError::UtilsError { - message: "No transaction ID returned".to_string(), - })?; - - let info = self - .algod_client - .wait_for_confirmation(tx_id.clone()) - .await?; - - info.asset_id.ok_or_else(|| UtilsError::UtilsError { - message: "No asset ID in transaction result".to_string(), - }) - } - - /// Get the dispenser account (useful for tests) - pub fn dispenser_account(&self) -> TestAccount { - self.dispenser_account.clone() - } -} diff --git a/crates/algokit_utils_ffi/src/tests/mod.rs b/crates/algokit_utils_ffi/src/tests/mod.rs deleted file mode 100644 index bf33ea1b6..000000000 --- a/crates/algokit_utils_ffi/src/tests/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod asset_freeze_tests; -pub mod fixtures; - -pub use asset_freeze_tests::{TestResult, TestSuiteResult, run_asset_freeze_test_suite}; diff --git a/crates/algokit_utils_ffi/src/transactions/app_call.rs b/crates/algokit_utils_ffi/src/transactions/app_call.rs deleted file mode 100644 index 65f38fd6c..000000000 --- a/crates/algokit_utils_ffi/src/transactions/app_call.rs +++ /dev/null @@ -1,1557 +0,0 @@ -// Standard library imports -use std::sync::Arc; - -// Third-party imports -use derive_more::Debug; - -// Crate imports -use crate::abi::{abi_type::ABIType, abi_value::ABIValue}; -use crate::create_transaction_params; - -// External crate imports -// algokit_abi -use algokit_abi::{ - ABIMethod as RustABIMethod, ABIMethodArg as RustABIMethodArg, - ABIMethodArgType as RustABIMethodArgType, ABIReferenceType as RustABIReferenceType, - ABITransactionType as RustABITransactionType, - abi_method::ABIReferenceValue as RustABIReferenceValue, -}; - -// algokit_transact -use algokit_transact::Address; - -// algokit_transact_ffi -use algokit_transact_ffi::{ - Transaction, - transactions::app_call::{BoxReference, OnApplicationComplete, StateSchema}, -}; - -// algokit_utils -use algokit_utils::transactions::{ - app_call::{ - AppCallMethodCallParams as RustAppCallMethodCallParams, AppCallParams as RustAppCallParams, - AppCreateMethodCallParams as RustAppCreateMethodCallParams, - AppCreateParams as RustAppCreateParams, - AppDeleteMethodCallParams as RustAppDeleteMethodCallParams, - AppDeleteParams as RustAppDeleteParams, AppMethodCallArg as RustAppMethodCallArg, - AppUpdateMethodCallParams as RustAppUpdateMethodCallParams, - AppUpdateParams as RustAppUpdateParams, - }, - common::TransactionSigner as RustTransactionSigner, -}; - -// Local module imports -use super::asset_config::{AssetConfigParams, AssetCreateParams, AssetDestroyParams}; -use super::asset_freeze::{AssetFreezeParams, AssetUnfreezeParams}; -use super::asset_transfer::{ - AssetClawbackParams, AssetOptInParams, AssetOptOutParams, AssetTransferParams, -}; -use super::common::{ - FfiTransactionSignerFromRust, RustTransactionSignerFromFfi, TransactionSigner, - TransactionWithSigner, UtilsError, -}; -use super::key_registration::{ - NonParticipationKeyRegistrationParams, OfflineKeyRegistrationParams, - OnlineKeyRegistrationParams, -}; -use super::payment::{AccountCloseParams, PaymentParams}; - -#[derive(uniffi::Enum, Debug)] -pub enum ABIReferenceType { - /// Reference to an account in the Accounts reference array - Account, - /// Reference to an application in the Applications reference array - Application, - /// Reference to an asset in the Assets reference array - Asset, -} - -#[derive(uniffi::Enum, Debug)] -pub enum ABITransactionType { - /// Any transaction type - Txn, - /// Payment (algo transfer) - Payment, - /// Key registration (configure consensus participation) - KeyRegistration, - /// Asset configuration (create, configure, or destroy ASAs) - AssetConfig, - /// Asset transfer (ASA transfer) - AssetTransfer, - /// Asset freeze (freeze or unfreeze ASAs) - AssetFreeze, - /// App call (create, update, delete and call an app) - AppCall, -} - -#[derive(uniffi::Enum, Debug)] -pub enum ABIMethodArgType { - /// A value that is directly encoded in the app arguments. - Value(Arc), - Transaction(ABITransactionType), - Reference(ABIReferenceType), -} - -impl From for RustABIMethodArgType { - fn from(value: ABIMethodArgType) -> Self { - match value { - ABIMethodArgType::Value(abi_type) => { - RustABIMethodArgType::Value(abi_type.abi_type.clone()) - } - ABIMethodArgType::Transaction(txn_type) => { - let rust_txn_type = match txn_type { - ABITransactionType::Txn => RustABITransactionType::Txn, - ABITransactionType::Payment => RustABITransactionType::Payment, - ABITransactionType::KeyRegistration => RustABITransactionType::KeyRegistration, - ABITransactionType::AssetConfig => RustABITransactionType::AssetConfig, - ABITransactionType::AssetTransfer => RustABITransactionType::AssetTransfer, - ABITransactionType::AssetFreeze => RustABITransactionType::AssetFreeze, - ABITransactionType::AppCall => RustABITransactionType::AppCall, - }; - RustABIMethodArgType::Transaction(rust_txn_type) - } - ABIMethodArgType::Reference(ref_type) => { - let rust_ref_type = match ref_type { - ABIReferenceType::Account => RustABIReferenceType::Account, - ABIReferenceType::Application => RustABIReferenceType::Application, - ABIReferenceType::Asset => RustABIReferenceType::Asset, - }; - RustABIMethodArgType::Reference(rust_ref_type) - } - } - } -} - -impl From for ABIMethodArgType { - fn from(value: RustABIMethodArgType) -> Self { - match value { - RustABIMethodArgType::Value(abi_type) => { - ABIMethodArgType::Value(Arc::new(ABIType { abi_type })) - } - RustABIMethodArgType::Transaction(txn_type) => { - let ffi_txn_type = match txn_type { - RustABITransactionType::Txn => ABITransactionType::Txn, - RustABITransactionType::Payment => ABITransactionType::Payment, - RustABITransactionType::KeyRegistration => ABITransactionType::KeyRegistration, - RustABITransactionType::AssetConfig => ABITransactionType::AssetConfig, - RustABITransactionType::AssetTransfer => ABITransactionType::AssetTransfer, - RustABITransactionType::AssetFreeze => ABITransactionType::AssetFreeze, - RustABITransactionType::AppCall => ABITransactionType::AppCall, - }; - ABIMethodArgType::Transaction(ffi_txn_type) - } - RustABIMethodArgType::Reference(ref_type) => { - let ffi_ref_type = match ref_type { - RustABIReferenceType::Account => ABIReferenceType::Account, - RustABIReferenceType::Application => ABIReferenceType::Application, - RustABIReferenceType::Asset => ABIReferenceType::Asset, - }; - ABIMethodArgType::Reference(ffi_ref_type) - } - } - } -} - -#[derive(uniffi::Record, Debug)] -pub struct ABIMethodArg { - /// The type of the argument. - pub arg_type: ABIMethodArgType, - /// An optional name for the argument. - pub name: Option, - /// An optional description of the argument. - pub description: Option, -} - -impl From for RustABIMethodArg { - fn from(value: ABIMethodArg) -> Self { - RustABIMethodArg { - arg_type: value.arg_type.into(), - name: value.name, - description: value.description, - default_value: None, // FFI doesn't support default values yet - } - } -} - -impl From for ABIMethodArg { - fn from(value: RustABIMethodArg) -> Self { - ABIMethodArg { - arg_type: value.arg_type.into(), - name: value.name, - description: value.description, - } - } -} - -#[derive(uniffi::Record, Debug)] -pub struct ABIMethod { - /// The name of the method. - pub name: String, - /// A list of the method's arguments. - pub args: Vec, - /// The return type of the method, or `None` if the method does not return a value. - pub returns: Option>, - /// An optional description of the method. - pub description: Option, -} - -impl From for RustABIMethod { - fn from(value: ABIMethod) -> Self { - RustABIMethod { - name: value.name, - args: value.args.into_iter().map(|arg| arg.into()).collect(), - returns: value.returns.map(|r| r.abi_type.clone()), - description: value.description, - } - } -} - -impl From for ABIMethod { - fn from(value: RustABIMethod) -> Self { - ABIMethod { - name: value.name, - args: value.args.into_iter().map(|arg| arg.into()).collect(), - returns: value.returns.map(|r| Arc::new(ABIType { abi_type: r })), - description: value.description, - } - } -} - -#[derive(uniffi::Enum, Debug)] -pub enum ABIReferenceValue { - /// The address to an Algorand account. - Account(String), - /// An Algorand asset ID. - Asset(u64), - /// An Algorand app ID. - Application(u64), -} - -impl TryFrom for RustABIReferenceValue { - type Error = UtilsError; - fn try_from(value: ABIReferenceValue) -> Result { - Ok(match value { - ABIReferenceValue::Account(address) => { - RustABIReferenceValue::Account(address.parse().map_err(|e| { - UtilsError::UtilsError { - message: format!("Invalid account address: {}", e), - } - })?) - } - ABIReferenceValue::Asset(asset_id) => RustABIReferenceValue::Asset(asset_id), - ABIReferenceValue::Application(app_id) => RustABIReferenceValue::Application(app_id), - }) - } -} - -impl From for ABIReferenceValue { - fn from(value: RustABIReferenceValue) -> Self { - match value { - RustABIReferenceValue::Account(address) => { - ABIReferenceValue::Account(address.to_string()) - } - RustABIReferenceValue::Asset(asset_id) => ABIReferenceValue::Asset(asset_id), - RustABIReferenceValue::Application(app_id) => ABIReferenceValue::Application(app_id), - } - } -} - -#[derive(uniffi::Enum, Debug)] -#[allow(clippy::large_enum_variant)] -pub enum AppMethodCallArg { - ABIReference(ABIReferenceValue), - /// Sentinel to request ARC-56 default resolution for this argument (handled by AppClient params builder) - DefaultValue, - /// Placeholder for a transaction-typed argument. Not encoded; satisfied by a transaction - /// included in the same group (extracted from other method call arguments). - TransactionPlaceHolder, - ABIValue(Arc), - AppCreateCall(AppCreateParams), - AppUpdateCall(AppUpdateParams), - AppDeleteCall(AppDeleteParams), - AppCallMethodCall(AppCallMethodCallParams), - AppCreateMethodCall(AppCreateMethodCallParams), - AppUpdateMethodCall(AppUpdateMethodCallParams), - AppDeleteMethodCall(AppDeleteMethodCallParams), - Transaction(Transaction), - #[debug("TransactionWithSigner")] - TransactionWithSigner(TransactionWithSigner), - Payment(PaymentParams), - AccountClose(AccountCloseParams), - AssetTransfer(AssetTransferParams), - AssetOptIn(AssetOptInParams), - AssetOptOut(AssetOptOutParams), - AssetClawback(AssetClawbackParams), - AssetCreate(AssetCreateParams), - AssetConfig(AssetConfigParams), - AssetDestroy(AssetDestroyParams), - AssetFreeze(AssetFreezeParams), - AssetUnfreeze(AssetUnfreezeParams), - AppCall(AppCallParams), - OnlineKeyRegistration(OnlineKeyRegistrationParams), - OfflineKeyRegistration(OfflineKeyRegistrationParams), - NonParticipationKeyRegistration(NonParticipationKeyRegistrationParams), -} - -impl TryFrom for RustAppMethodCallArg { - type Error = UtilsError; - fn try_from(value: AppMethodCallArg) -> Result { - Ok(match value { - AppMethodCallArg::ABIValue(abi_value) => { - RustAppMethodCallArg::ABIValue(abi_value.rust_value.clone()) - } - AppMethodCallArg::AppCreateCall(app_create_params) => { - RustAppMethodCallArg::AppCreateCall(app_create_params.try_into()?) - } - AppMethodCallArg::AppUpdateCall(app_update_params) => { - RustAppMethodCallArg::AppUpdateCall(app_update_params.try_into()?) - } - AppMethodCallArg::AppDeleteCall(app_delete_params) => { - RustAppMethodCallArg::AppDeleteCall(app_delete_params.try_into()?) - } - AppMethodCallArg::AppCallMethodCall(app_call_method_params) => { - RustAppMethodCallArg::AppCallMethodCall(app_call_method_params.try_into()?) - } - AppMethodCallArg::AppCreateMethodCall(app_create_method_params) => { - RustAppMethodCallArg::AppCreateMethodCall(app_create_method_params.try_into()?) - } - AppMethodCallArg::AppUpdateMethodCall(app_update_method_params) => { - RustAppMethodCallArg::AppUpdateMethodCall(app_update_method_params.try_into()?) - } - AppMethodCallArg::AppDeleteMethodCall(app_delete_method_params) => { - RustAppMethodCallArg::AppDeleteMethodCall(app_delete_method_params.try_into()?) - } - AppMethodCallArg::Transaction(txn) => { - RustAppMethodCallArg::Transaction(txn.try_into().map_err(|e| { - UtilsError::UtilsError { - message: format!("Invalid transaction: {}", e), - } - })?) - } - AppMethodCallArg::TransactionWithSigner(txn_with_signer) => { - RustAppMethodCallArg::TransactionWithSigner(txn_with_signer.try_into()?) - } - AppMethodCallArg::Payment(payment_params) => { - RustAppMethodCallArg::Payment(payment_params.try_into()?) - } - AppMethodCallArg::AccountClose(account_close_params) => { - RustAppMethodCallArg::AccountClose(account_close_params.try_into()?) - } - AppMethodCallArg::AssetTransfer(asset_transfer_params) => { - RustAppMethodCallArg::AssetTransfer(asset_transfer_params.try_into()?) - } - AppMethodCallArg::AssetOptIn(asset_opt_in_params) => { - RustAppMethodCallArg::AssetOptIn(asset_opt_in_params.try_into()?) - } - AppMethodCallArg::AssetOptOut(asset_opt_out_params) => { - RustAppMethodCallArg::AssetOptOut(asset_opt_out_params.try_into()?) - } - AppMethodCallArg::AssetClawback(asset_clawback_params) => { - RustAppMethodCallArg::AssetClawback(asset_clawback_params.try_into()?) - } - AppMethodCallArg::AssetCreate(asset_create_params) => { - RustAppMethodCallArg::AssetCreate(asset_create_params.try_into()?) - } - AppMethodCallArg::AssetConfig(asset_config_params) => { - RustAppMethodCallArg::AssetConfig(asset_config_params.try_into()?) - } - AppMethodCallArg::AssetDestroy(asset_destroy_params) => { - RustAppMethodCallArg::AssetDestroy(asset_destroy_params.try_into()?) - } - AppMethodCallArg::AssetFreeze(asset_freeze_params) => { - RustAppMethodCallArg::AssetFreeze(asset_freeze_params.try_into()?) - } - AppMethodCallArg::AssetUnfreeze(asset_unfreeze_params) => { - RustAppMethodCallArg::AssetUnfreeze(asset_unfreeze_params.try_into()?) - } - AppMethodCallArg::AppCall(app_call_params) => { - RustAppMethodCallArg::AppCall(app_call_params.try_into()?) - } - AppMethodCallArg::OnlineKeyRegistration(online_key_registration_params) => { - RustAppMethodCallArg::OnlineKeyRegistration( - online_key_registration_params.try_into()?, - ) - } - AppMethodCallArg::OfflineKeyRegistration(offline_key_registration_params) => { - RustAppMethodCallArg::OfflineKeyRegistration( - offline_key_registration_params.try_into()?, - ) - } - AppMethodCallArg::NonParticipationKeyRegistration( - non_participation_key_registration_params, - ) => RustAppMethodCallArg::NonParticipationKeyRegistration( - non_participation_key_registration_params.try_into()?, - ), - AppMethodCallArg::DefaultValue => RustAppMethodCallArg::DefaultValue, - AppMethodCallArg::TransactionPlaceHolder => { - RustAppMethodCallArg::TransactionPlaceholder - } - AppMethodCallArg::ABIReference(abi_reference) => { - RustAppMethodCallArg::ABIReference(abi_reference.try_into()?) - } - }) - } -} - -impl TryFrom for AppMethodCallArg { - type Error = UtilsError; - - fn try_from(value: RustAppMethodCallArg) -> Result { - Ok(match value { - RustAppMethodCallArg::ABIValue(rust_value) => { - AppMethodCallArg::ABIValue(Arc::new(ABIValue { rust_value })) - } - RustAppMethodCallArg::AppCreateCall(app_create_params) => { - AppMethodCallArg::AppCreateCall(app_create_params.into()) - } - RustAppMethodCallArg::AppUpdateCall(app_update_params) => { - AppMethodCallArg::AppUpdateCall(app_update_params.into()) - } - RustAppMethodCallArg::AppDeleteCall(app_delete_params) => { - AppMethodCallArg::AppDeleteCall(app_delete_params.into()) - } - RustAppMethodCallArg::AppCallMethodCall(app_call_method_params) => { - AppMethodCallArg::AppCallMethodCall(app_call_method_params.try_into()?) - } - RustAppMethodCallArg::AppCreateMethodCall(app_create_method_params) => { - AppMethodCallArg::AppCreateMethodCall(app_create_method_params.try_into()?) - } - RustAppMethodCallArg::AppUpdateMethodCall(app_update_method_params) => { - AppMethodCallArg::AppUpdateMethodCall(app_update_method_params.try_into()?) - } - RustAppMethodCallArg::AppDeleteMethodCall(app_delete_method_params) => { - AppMethodCallArg::AppDeleteMethodCall(app_delete_method_params.try_into()?) - } - RustAppMethodCallArg::Transaction(txn) => AppMethodCallArg::Transaction(txn.into()), - RustAppMethodCallArg::TransactionWithSigner(txn_with_signer) => { - AppMethodCallArg::TransactionWithSigner(txn_with_signer.try_into()?) - } - RustAppMethodCallArg::Payment(payment_params) => { - AppMethodCallArg::Payment(payment_params.into()) - } - RustAppMethodCallArg::AccountClose(account_close_params) => { - AppMethodCallArg::AccountClose(account_close_params.into()) - } - RustAppMethodCallArg::AssetTransfer(asset_transfer_params) => { - AppMethodCallArg::AssetTransfer(asset_transfer_params.into()) - } - RustAppMethodCallArg::AssetOptIn(asset_opt_in_params) => { - AppMethodCallArg::AssetOptIn(asset_opt_in_params.into()) - } - RustAppMethodCallArg::AssetOptOut(asset_opt_out_params) => { - AppMethodCallArg::AssetOptOut(asset_opt_out_params.into()) - } - RustAppMethodCallArg::AssetClawback(asset_clawback_params) => { - AppMethodCallArg::AssetClawback(asset_clawback_params.into()) - } - RustAppMethodCallArg::AssetCreate(asset_create_params) => { - AppMethodCallArg::AssetCreate(asset_create_params.into()) - } - RustAppMethodCallArg::AssetConfig(asset_config_params) => { - AppMethodCallArg::AssetConfig(asset_config_params.into()) - } - RustAppMethodCallArg::AssetDestroy(asset_destroy_params) => { - AppMethodCallArg::AssetDestroy(asset_destroy_params.into()) - } - RustAppMethodCallArg::AssetFreeze(asset_freeze_params) => { - AppMethodCallArg::AssetFreeze(asset_freeze_params.into()) - } - RustAppMethodCallArg::AssetUnfreeze(asset_unfreeze_params) => { - AppMethodCallArg::AssetUnfreeze(asset_unfreeze_params.into()) - } - RustAppMethodCallArg::AppCall(app_call_params) => { - AppMethodCallArg::AppCall(app_call_params.into()) - } - RustAppMethodCallArg::OnlineKeyRegistration(online_key_registration_params) => { - AppMethodCallArg::OnlineKeyRegistration(online_key_registration_params.into()) - } - RustAppMethodCallArg::OfflineKeyRegistration(offline_key_registration_params) => { - AppMethodCallArg::OfflineKeyRegistration(offline_key_registration_params.into()) - } - RustAppMethodCallArg::NonParticipationKeyRegistration( - non_participation_key_registration_params, - ) => AppMethodCallArg::NonParticipationKeyRegistration( - non_participation_key_registration_params.into(), - ), - RustAppMethodCallArg::DefaultValue => AppMethodCallArg::DefaultValue, - RustAppMethodCallArg::ABIReference(_) => { - todo!() - } - RustAppMethodCallArg::TransactionPlaceholder => { - AppMethodCallArg::TransactionPlaceHolder - } - }) - } -} - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct AppCallMethodCallParams { - /// ID of the app being called. - pub app_id: u64, - /// The ABI method to call. - pub method: ABIMethod, - /// Transaction specific arguments available in the app's - /// approval program and clear state program. - pub args: Vec, - /// List of accounts in addition to the sender that may be accessed - /// from the app's approval program and clear state program. - #[uniffi(default = None)] - pub account_references: Option>, - /// List of apps in addition to the current app that may be called - /// from the app's approval program and clear state program. - #[uniffi(default = None)] - pub app_references: Option>, - /// Lists the assets whose parameters may be accessed by this app's - /// approval program and clear state program. - /// - /// The access is read-only. - #[uniffi(default = None)] - pub asset_references: Option>, - /// The boxes that should be made available for the runtime of the program. - #[uniffi(default = None)] - pub box_references: Option>, - /// Defines what additional actions occur with the transaction. - pub on_complete: OnApplicationComplete, - } -} - -impl TryFrom for RustAppCallMethodCallParams { - type Error = UtilsError; - - fn try_from(value: AppCallMethodCallParams) -> Result { - Ok(RustAppCallMethodCallParams { - sender: value.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: value.signer.map(|s| { - Arc::new(RustTransactionSignerFromFfi { ffi_signer: s }) - as Arc - }), - rekey_to: value.rekey_to.map(|r| r.parse()).transpose().map_err(|e| { - UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - } - })?, - note: value.note, - lease: value.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: value.static_fee, - extra_fee: value.extra_fee, - max_fee: value.max_fee, - validity_window: value.validity_window, - first_valid_round: value.first_valid_round, - last_valid_round: value.last_valid_round, - app_id: value.app_id, - method: value.method.into(), - args: value - .args - .into_iter() - .map(|arg| arg.try_into()) - .collect::>()?, - account_references: value - .account_references - .map(|accounts| { - accounts - .into_iter() - .map(|a| a.parse()) - .collect::, _>>() - }) - .transpose() - .map_err(|e: ::Err| { - UtilsError::UtilsError { - message: e.to_string(), - } - })?, - app_references: value.app_references, - asset_references: value.asset_references, - box_references: value - .box_references - .map(|boxes| boxes.into_iter().map(|b| b.into()).collect()), - on_complete: value.on_complete.into(), - }) - } -} - -impl TryFrom for AppCallMethodCallParams { - type Error = UtilsError; - - fn try_from(value: RustAppCallMethodCallParams) -> Result { - Ok(AppCallMethodCallParams { - sender: value.sender.to_string(), - signer: value.signer.map(|s| { - Arc::new(FfiTransactionSignerFromRust { rust_signer: s }) - as Arc - }), - rekey_to: value.rekey_to.map(|r| r.to_string()), - note: value.note, - lease: value.lease.map(|l| l.to_vec()), - static_fee: value.static_fee, - extra_fee: value.extra_fee, - max_fee: value.max_fee, - validity_window: value.validity_window, - first_valid_round: value.first_valid_round, - last_valid_round: value.last_valid_round, - app_id: value.app_id, - method: value.method.into(), - args: value - .args - .into_iter() - .map(|arg| arg.try_into()) - .collect::>()?, - account_references: value - .account_references - .map(|accounts| accounts.into_iter().map(|a| a.to_string()).collect()), - app_references: value.app_references, - asset_references: value.asset_references, - box_references: value - .box_references - .map(|boxes| boxes.into_iter().map(|b| b.into()).collect()), - on_complete: value.on_complete.into(), - }) - } -} - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct AppCallParams { - /// ID of the app being called. - pub app_id: u64, - /// Defines what additional actions occur with the transaction. - pub on_complete: OnApplicationComplete, - /// Transaction specific arguments available in the app's - /// approval program and clear state program. - #[uniffi(default = None)] - pub args: Option>>, - /// List of accounts in addition to the sender that may be accessed - /// from the app's approval program and clear state program. - #[uniffi(default = None)] - pub account_references: Option>, - /// List of apps in addition to the current app that may be called - /// from the app's approval program and clear state program. - #[uniffi(default = None)] - pub app_references: Option>, - /// Lists the assets whose parameters may be accessed by this app's - /// approval program and clear state program. - /// - /// The access is read-only. - #[uniffi(default = None)] - pub asset_references: Option>, - /// The boxes that should be made available for the runtime of the program. - #[uniffi(default = None)] - pub box_references: Option>, - } -} - -impl TryFrom for RustAppCallParams { - type Error = UtilsError; - - fn try_from(value: AppCallParams) -> Result { - Ok(RustAppCallParams { - sender: value.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: value.signer.map(|s| { - Arc::new(RustTransactionSignerFromFfi { ffi_signer: s }) - as Arc - }), - rekey_to: value.rekey_to.map(|r| r.parse()).transpose().map_err(|e| { - UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - } - })?, - note: value.note, - lease: value.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: value.static_fee, - extra_fee: value.extra_fee, - max_fee: value.max_fee, - validity_window: value.validity_window, - first_valid_round: value.first_valid_round, - last_valid_round: value.last_valid_round, - app_id: value.app_id, - on_complete: value.on_complete.into(), - args: value.args, - account_references: value - .account_references - .map(|accounts| { - accounts - .into_iter() - .map(|a| a.parse()) - .collect::, _>>() - }) - .transpose() - .map_err(|e: ::Err| { - UtilsError::UtilsError { - message: e.to_string(), - } - })?, - app_references: value.app_references, - asset_references: value.asset_references, - box_references: value - .box_references - .map(|boxes| boxes.into_iter().map(|b| b.into()).collect()), - }) - } -} - -impl From for AppCallParams { - fn from(value: RustAppCallParams) -> Self { - AppCallParams { - sender: value.sender.to_string(), - signer: value.signer.map(|s| { - Arc::new(FfiTransactionSignerFromRust { rust_signer: s }) - as Arc - }), - rekey_to: value.rekey_to.map(|r| r.to_string()), - note: value.note, - lease: value.lease.map(|l| l.to_vec()), - static_fee: value.static_fee, - extra_fee: value.extra_fee, - max_fee: value.max_fee, - validity_window: value.validity_window, - first_valid_round: value.first_valid_round, - last_valid_round: value.last_valid_round, - app_id: value.app_id, - on_complete: value.on_complete.into(), - args: value.args, - account_references: value - .account_references - .map(|accounts| accounts.into_iter().map(|a| a.to_string()).collect()), - app_references: value.app_references, - asset_references: value.asset_references, - box_references: value - .box_references - .map(|boxes| boxes.into_iter().map(|b| b.into()).collect()), - } - } -} - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct AppCreateParams { - /// Defines what additional actions occur with the transaction. - pub on_complete: OnApplicationComplete, - /// Logic executed for every app call transaction, except when - /// on-completion is set to "clear". - /// - /// Approval programs may reject the transaction. - pub approval_program: Vec, - /// Logic executed for app call transactions with on-completion set to "clear". - /// - /// Clear state programs cannot reject the transaction. - pub clear_state_program: Vec, - /// Holds the maximum number of global state values. - /// - /// This cannot be changed after creation. - #[uniffi(default = None)] - pub global_state_schema: Option, - /// Holds the maximum number of local state values. - /// - /// This cannot be changed after creation. - #[uniffi(default = None)] - pub local_state_schema: Option, - /// Number of additional pages allocated to the app's approval - /// and clear state programs. - /// - /// Each extra program page is 2048 bytes. The sum of approval program - /// and clear state program may not exceed 2048*(1+extra_program_pages) bytes. - /// Currently, the maximum value is 3. - /// This cannot be changed after creation. - #[uniffi(default = None)] - pub extra_program_pages: Option, - /// Transaction specific arguments available in the app's - /// approval program and clear state program. - #[uniffi(default = None)] - pub args: Option>>, - /// List of accounts in addition to the sender that may be accessed - /// from the app's approval program and clear state program. - #[uniffi(default = None)] - pub account_references: Option>, - /// List of apps in addition to the current app that may be called - /// from the app's approval program and clear state program. - #[uniffi(default = None)] - pub app_references: Option>, - /// Lists the assets whose parameters may be accessed by this app's - /// approval program and clear state program. - /// - /// The access is read-only. - #[uniffi(default = None)] - pub asset_references: Option>, - /// The boxes that should be made available for the runtime of the program. - #[uniffi(default = None)] - pub box_references: Option>, - } -} - -impl TryFrom for RustAppCreateParams { - type Error = UtilsError; - - fn try_from(value: AppCreateParams) -> Result { - Ok(RustAppCreateParams { - sender: value.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: value.signer.map(|s| { - Arc::new(RustTransactionSignerFromFfi { ffi_signer: s }) - as Arc - }), - rekey_to: value.rekey_to.map(|r| r.parse()).transpose().map_err(|e| { - UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - } - })?, - note: value.note, - lease: value.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: value.static_fee, - extra_fee: value.extra_fee, - max_fee: value.max_fee, - validity_window: value.validity_window, - first_valid_round: value.first_valid_round, - last_valid_round: value.last_valid_round, - on_complete: value.on_complete.into(), - approval_program: value.approval_program, - clear_state_program: value.clear_state_program, - global_state_schema: value.global_state_schema.map(|s| s.into()), - local_state_schema: value.local_state_schema.map(|s| s.into()), - extra_program_pages: value.extra_program_pages.map(|p| p as u32), - args: value.args, - account_references: value - .account_references - .map(|accounts| { - accounts - .into_iter() - .map(|a| a.parse()) - .collect::, _>>() - }) - .transpose() - .map_err( - |e:
::Err| UtilsError::UtilsError { - message: e.to_string(), - }, - )?, - app_references: value.app_references, - asset_references: value.asset_references, - box_references: value - .box_references - .map(|boxes| boxes.into_iter().map(|b| b.into()).collect()), - }) - } -} - -impl From for AppCreateParams { - fn from(value: RustAppCreateParams) -> Self { - AppCreateParams { - sender: value.sender.to_string(), - signer: value.signer.map(|s| { - Arc::new(FfiTransactionSignerFromRust { rust_signer: s }) - as Arc - }), - rekey_to: value.rekey_to.map(|r| r.to_string()), - note: value.note, - lease: value.lease.map(|l| l.to_vec()), - static_fee: value.static_fee, - extra_fee: value.extra_fee, - max_fee: value.max_fee, - validity_window: value.validity_window, - first_valid_round: value.first_valid_round, - last_valid_round: value.last_valid_round, - on_complete: value.on_complete.into(), - approval_program: value.approval_program, - clear_state_program: value.clear_state_program, - global_state_schema: value.global_state_schema.map(|s| s.into()), - local_state_schema: value.local_state_schema.map(|s| s.into()), - extra_program_pages: value.extra_program_pages.map(|p| p as u64), - args: value.args, - account_references: value - .account_references - .map(|accounts| accounts.into_iter().map(|a| a.to_string()).collect()), - app_references: value.app_references, - asset_references: value.asset_references, - box_references: value - .box_references - .map(|boxes| boxes.into_iter().map(|b| b.into()).collect()), - } - } -} - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct AppDeleteParams { - /// ID of the app being deleted. - pub app_id: u64, - /// Transaction specific arguments available in the app's - /// approval program and clear state program. - #[uniffi(default = None)] - pub args: Option>>, - /// List of accounts in addition to the sender that may be accessed - /// from the app's approval program and clear state program. - #[uniffi(default = None)] - pub account_references: Option>, - /// List of apps in addition to the current app that may be called - /// from the app's approval program and clear state program. - #[uniffi(default = None)] - pub app_references: Option>, - /// Lists the assets whose parameters may be accessed by this app's - /// approval program and clear state program. - /// - /// The access is read-only. - #[uniffi(default = None)] - pub asset_references: Option>, - /// The boxes that should be made available for the runtime of the program. - #[uniffi(default = None)] - pub box_references: Option>, - } -} - -impl TryFrom for RustAppDeleteParams { - type Error = UtilsError; - - fn try_from(value: AppDeleteParams) -> Result { - Ok(RustAppDeleteParams { - sender: value.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: value.signer.map(|s| { - Arc::new(RustTransactionSignerFromFfi { ffi_signer: s }) - as Arc - }), - rekey_to: value.rekey_to.map(|r| r.parse()).transpose().map_err(|e| { - UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - } - })?, - note: value.note, - lease: value.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: value.static_fee, - extra_fee: value.extra_fee, - max_fee: value.max_fee, - validity_window: value.validity_window, - first_valid_round: value.first_valid_round, - last_valid_round: value.last_valid_round, - app_id: value.app_id, - args: value.args, - account_references: value - .account_references - .map(|accounts| { - accounts - .into_iter() - .map(|a| a.parse()) - .collect::, _>>() - }) - .transpose() - .map_err(|e: ::Err| { - UtilsError::UtilsError { - message: e.to_string(), - } - })?, - app_references: value.app_references, - asset_references: value.asset_references, - box_references: value - .box_references - .map(|boxes| boxes.into_iter().map(|b| b.into()).collect()), - }) - } -} - -impl From for AppDeleteParams { - fn from(value: RustAppDeleteParams) -> Self { - AppDeleteParams { - sender: value.sender.to_string(), - signer: value.signer.map(|s| { - Arc::new(FfiTransactionSignerFromRust { rust_signer: s }) - as Arc - }), - rekey_to: value.rekey_to.map(|r| r.to_string()), - note: value.note, - lease: value.lease.map(|l| l.to_vec()), - static_fee: value.static_fee, - extra_fee: value.extra_fee, - max_fee: value.max_fee, - validity_window: value.validity_window, - first_valid_round: value.first_valid_round, - last_valid_round: value.last_valid_round, - app_id: value.app_id, - args: value.args, - account_references: value - .account_references - .map(|accounts| accounts.into_iter().map(|a| a.to_string()).collect()), - app_references: value.app_references, - asset_references: value.asset_references, - box_references: value - .box_references - .map(|boxes| boxes.into_iter().map(|b| b.into()).collect()), - } - } -} - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct AppUpdateParams { - /// ID of the app being updated. - pub app_id: u64, - /// Logic executed for every app call transaction, except when - /// on-completion is set to "clear". - /// - /// Approval programs may reject the transaction. - pub approval_program: Vec, - /// Logic executed for app call transactions with on-completion set to "clear". - /// - /// Clear state programs cannot reject the transaction. - pub clear_state_program: Vec, - /// Transaction specific arguments available in the app's - /// approval program and clear state program. - #[uniffi(default = None)] - pub args: Option>>, - /// List of accounts in addition to the sender that may be accessed - /// from the app's approval program and clear state program. - #[uniffi(default = None)] - pub account_references: Option>, - /// List of apps in addition to the current app that may be called - /// from the app's approval program and clear state program. - #[uniffi(default = None)] - pub app_references: Option>, - /// Lists the assets whose parameters may be accessed by this app's - /// approval program and clear state program. - /// - /// The access is read-only. - #[uniffi(default = None)] - pub asset_references: Option>, - /// The boxes that should be made available for the runtime of the program. - #[uniffi(default = None)] - pub box_references: Option>, - } -} - -impl TryFrom for RustAppUpdateParams { - type Error = UtilsError; - - fn try_from(value: AppUpdateParams) -> Result { - Ok(RustAppUpdateParams { - sender: value.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: value.signer.map(|s| { - Arc::new(RustTransactionSignerFromFfi { ffi_signer: s }) - as Arc - }), - rekey_to: value.rekey_to.map(|r| r.parse()).transpose().map_err(|e| { - UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - } - })?, - note: value.note, - lease: value.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: value.static_fee, - extra_fee: value.extra_fee, - max_fee: value.max_fee, - validity_window: value.validity_window, - first_valid_round: value.first_valid_round, - last_valid_round: value.last_valid_round, - app_id: value.app_id, - approval_program: value.approval_program, - clear_state_program: value.clear_state_program, - args: value.args, - account_references: value - .account_references - .map(|accounts| { - accounts - .into_iter() - .map(|a| a.parse()) - .collect::, _>>() - }) - .transpose() - .map_err(|e: ::Err| { - UtilsError::UtilsError { - message: e.to_string(), - } - })?, - app_references: value.app_references, - asset_references: value.asset_references, - box_references: value - .box_references - .map(|boxes| boxes.into_iter().map(|b| b.into()).collect()), - }) - } -} - -impl From for AppUpdateParams { - fn from(value: RustAppUpdateParams) -> Self { - AppUpdateParams { - sender: value.sender.to_string(), - signer: value.signer.map(|s| { - Arc::new(FfiTransactionSignerFromRust { rust_signer: s }) - as Arc - }), - rekey_to: value.rekey_to.map(|r| r.to_string()), - note: value.note, - lease: value.lease.map(|l| l.to_vec()), - static_fee: value.static_fee, - extra_fee: value.extra_fee, - max_fee: value.max_fee, - validity_window: value.validity_window, - first_valid_round: value.first_valid_round, - last_valid_round: value.last_valid_round, - app_id: value.app_id, - approval_program: value.approval_program, - clear_state_program: value.clear_state_program, - args: value.args, - account_references: value - .account_references - .map(|accounts| accounts.into_iter().map(|a| a.to_string()).collect()), - app_references: value.app_references, - asset_references: value.asset_references, - box_references: value - .box_references - .map(|boxes| boxes.into_iter().map(|b| b.into()).collect()), - } - } -} - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct AppCreateMethodCallParams { - /// Defines what additional actions occur with the transaction. - pub on_complete: OnApplicationComplete, - /// Logic executed for every app call transaction, except when - /// on-completion is set to "clear". - /// - /// Approval programs may reject the transaction. - pub approval_program: Vec, - /// Logic executed for app call transactions with on-completion set to "clear". - /// - /// Clear state programs cannot reject the transaction. - pub clear_state_program: Vec, - /// Holds the maximum number of global state values. - /// - /// This cannot be changed after creation. - #[uniffi(default = None)] - pub global_state_schema: Option, - /// Holds the maximum number of local state values. - /// - /// This cannot be changed after creation. - #[uniffi(default = None)] - pub local_state_schema: Option, - /// Number of additional pages allocated to the app's approval - /// and clear state programs. - /// - /// Each extra program page is 2048 bytes. The sum of approval program - /// and clear state program may not exceed 2048*(1+extra_program_pages) bytes. - /// Currently, the maximum value is 3. - /// This cannot be changed after creation. - #[uniffi(default = None)] - pub extra_program_pages: Option, - /// The ABI method to call. - pub method: ABIMethod, - /// Transaction specific arguments available in the app's - /// approval program and clear state program. - pub args: Vec, - /// List of accounts in addition to the sender that may be accessed - /// from the app's approval program and clear state program. - #[uniffi(default = None)] - pub account_references: Option>, - /// List of apps in addition to the current app that may be called - /// from the app's approval program and clear state program. - #[uniffi(default = None)] - pub app_references: Option>, - /// Lists the assets whose parameters may be accessed by this app's - /// approval program and clear state program. - /// - /// The access is read-only. - #[uniffi(default = None)] - pub asset_references: Option>, - /// The boxes that should be made available for the runtime of the program. - #[uniffi(default = None)] - pub box_references: Option>, - } -} - -impl TryFrom for RustAppCreateMethodCallParams { - type Error = UtilsError; - - fn try_from(value: AppCreateMethodCallParams) -> Result { - Ok(RustAppCreateMethodCallParams { - sender: value.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: value.signer.map(|s| { - Arc::new(RustTransactionSignerFromFfi { ffi_signer: s }) - as Arc - }), - rekey_to: value.rekey_to.map(|r| r.parse()).transpose().map_err(|e| { - UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - } - })?, - note: value.note, - lease: value.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: value.static_fee, - extra_fee: value.extra_fee, - max_fee: value.max_fee, - validity_window: value.validity_window, - first_valid_round: value.first_valid_round, - last_valid_round: value.last_valid_round, - on_complete: value.on_complete.into(), - approval_program: value.approval_program, - clear_state_program: value.clear_state_program, - global_state_schema: value.global_state_schema.map(|s| s.into()), - local_state_schema: value.local_state_schema.map(|s| s.into()), - extra_program_pages: value.extra_program_pages.map(|p| p as u32), - method: value.method.into(), - args: value - .args - .into_iter() - .map(|arg| arg.try_into()) - .collect::>()?, - - account_references: value - .account_references - .map(|accounts| { - accounts - .into_iter() - .map(|a| a.parse()) - .collect::, _>>() - }) - .transpose() - .map_err(|e: ::Err| { - UtilsError::UtilsError { - message: e.to_string(), - } - })?, - app_references: value.app_references, - asset_references: value.asset_references, - box_references: value - .box_references - .map(|boxes| boxes.into_iter().map(|b| b.into()).collect()), - }) - } -} - -impl TryFrom for AppCreateMethodCallParams { - type Error = UtilsError; - fn try_from(value: RustAppCreateMethodCallParams) -> Result { - Ok(AppCreateMethodCallParams { - sender: value.sender.to_string(), - signer: value.signer.map(|s| { - Arc::new(FfiTransactionSignerFromRust { rust_signer: s }) - as Arc - }), - rekey_to: value.rekey_to.map(|r| r.to_string()), - note: value.note, - lease: value.lease.map(|l| l.to_vec()), - static_fee: value.static_fee, - extra_fee: value.extra_fee, - max_fee: value.max_fee, - validity_window: value.validity_window, - first_valid_round: value.first_valid_round, - last_valid_round: value.last_valid_round, - on_complete: value.on_complete.into(), - approval_program: value.approval_program, - clear_state_program: value.clear_state_program, - global_state_schema: value.global_state_schema.map(|s| s.into()), - local_state_schema: value.local_state_schema.map(|s| s.into()), - extra_program_pages: value.extra_program_pages.map(|p| p as u64), - method: value.method.into(), - args: value - .args - .into_iter() - .map(|arg| arg.try_into()) - .collect::>()?, - account_references: value - .account_references - .map(|accounts| accounts.into_iter().map(|a| a.to_string()).collect()), - app_references: value.app_references, - asset_references: value.asset_references, - box_references: value - .box_references - .map(|boxes| boxes.into_iter().map(|b| b.into()).collect()), - }) - } -} - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct AppUpdateMethodCallParams { - /// ID of the app being updated. - pub app_id: u64, - /// Logic executed for every app call transaction, except when - /// on-completion is set to "clear". - /// - /// Approval programs may reject the transaction. - pub approval_program: Vec, - /// Logic executed for app call transactions with on-completion set to "clear". - /// - /// Clear state programs cannot reject the transaction. - pub clear_state_program: Vec, - /// The ABI method to call. - pub method: ABIMethod, - /// Transaction specific arguments available in the app's - /// approval program and clear state program. - pub args: Vec, - /// List of accounts in addition to the sender that may be accessed - /// from the app's approval program and clear state program. - #[uniffi(default = None)] - pub account_references: Option>, - /// List of apps in addition to the current app that may be called - /// from the app's approval program and clear state program. - #[uniffi(default = None)] - pub app_references: Option>, - /// Lists the assets whose parameters may be accessed by this app's - /// approval program and clear state program. - /// - /// The access is read-only. - #[uniffi(default = None)] - pub asset_references: Option>, - /// The boxes that should be made available for the runtime of the program. - #[uniffi(default = None)] - pub box_references: Option>, - } -} - -impl TryFrom for RustAppUpdateMethodCallParams { - type Error = UtilsError; - - fn try_from(value: AppUpdateMethodCallParams) -> Result { - Ok(RustAppUpdateMethodCallParams { - sender: value.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: value.signer.map(|s| { - Arc::new(RustTransactionSignerFromFfi { ffi_signer: s }) - as Arc - }), - rekey_to: value.rekey_to.map(|r| r.parse()).transpose().map_err(|e| { - UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - } - })?, - note: value.note, - lease: value.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: value.static_fee, - extra_fee: value.extra_fee, - max_fee: value.max_fee, - validity_window: value.validity_window, - first_valid_round: value.first_valid_round, - last_valid_round: value.last_valid_round, - app_id: value.app_id, - approval_program: value.approval_program, - clear_state_program: value.clear_state_program, - method: value.method.into(), - args: value - .args - .into_iter() - .map(|arg| arg.try_into()) - .collect::>()?, - account_references: value - .account_references - .map(|accounts| { - accounts - .into_iter() - .map(|a| a.parse()) - .collect::, _>>() - }) - .transpose() - .map_err(|e: ::Err| { - UtilsError::UtilsError { - message: e.to_string(), - } - })?, - app_references: value.app_references, - asset_references: value.asset_references, - box_references: value - .box_references - .map(|boxes| boxes.into_iter().map(|b| b.into()).collect()), - }) - } -} - -impl TryFrom for AppUpdateMethodCallParams { - type Error = UtilsError; - fn try_from(value: RustAppUpdateMethodCallParams) -> Result { - Ok(AppUpdateMethodCallParams { - sender: value.sender.to_string(), - signer: value.signer.map(|s| { - Arc::new(FfiTransactionSignerFromRust { rust_signer: s }) - as Arc - }), - rekey_to: value.rekey_to.map(|r| r.to_string()), - note: value.note, - lease: value.lease.map(|l| l.to_vec()), - static_fee: value.static_fee, - extra_fee: value.extra_fee, - max_fee: value.max_fee, - validity_window: value.validity_window, - first_valid_round: value.first_valid_round, - last_valid_round: value.last_valid_round, - app_id: value.app_id, - approval_program: value.approval_program, - clear_state_program: value.clear_state_program, - method: value.method.into(), - args: value - .args - .into_iter() - .map(|arg| arg.try_into()) - .collect::>()?, - account_references: value - .account_references - .map(|accounts| accounts.into_iter().map(|a| a.to_string()).collect()), - app_references: value.app_references, - asset_references: value.asset_references, - box_references: value - .box_references - .map(|boxes| boxes.into_iter().map(|b| b.into()).collect()), - }) - } -} - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct AppDeleteMethodCallParams { - /// ID of the app being deleted. - pub app_id: u64, - /// The ABI method to call. - pub method: ABIMethod, - /// Transaction specific arguments available in the app's - /// approval program and clear state program. - pub args: Vec, - /// List of accounts in addition to the sender that may be accessed - /// from the app's approval program and clear state program. - #[uniffi(default = None)] - pub account_references: Option>, - /// List of apps in addition to the current app that may be called - /// from the app's approval program and clear state program. - #[uniffi(default = None)] - pub app_references: Option>, - /// Lists the assets whose parameters may be accessed by this app's - /// approval program and clear state program. - /// - /// The access is read-only. - #[uniffi(default = None)] - pub asset_references: Option>, - /// The boxes that should be made available for the runtime of the program. - #[uniffi(default = None)] - pub box_references: Option>, - } -} - -impl TryFrom for RustAppDeleteMethodCallParams { - type Error = UtilsError; - - fn try_from(value: AppDeleteMethodCallParams) -> Result { - Ok(RustAppDeleteMethodCallParams { - sender: value.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: value.signer.map(|s| { - Arc::new(RustTransactionSignerFromFfi { ffi_signer: s }) - as Arc - }), - rekey_to: value.rekey_to.map(|r| r.parse()).transpose().map_err(|e| { - UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - } - })?, - note: value.note, - lease: value.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: value.static_fee, - extra_fee: value.extra_fee, - max_fee: value.max_fee, - validity_window: value.validity_window, - first_valid_round: value.first_valid_round, - last_valid_round: value.last_valid_round, - app_id: value.app_id, - method: value.method.into(), - args: value - .args - .into_iter() - .map(|arg| arg.try_into()) - .collect::>()?, - account_references: value - .account_references - .map(|accounts| { - accounts - .into_iter() - .map(|a| a.parse()) - .collect::, _>>() - }) - .transpose() - .map_err(|e: ::Err| { - UtilsError::UtilsError { - message: e.to_string(), - } - })?, - app_references: value.app_references, - asset_references: value.asset_references, - box_references: value - .box_references - .map(|boxes| boxes.into_iter().map(|b| b.into()).collect()), - }) - } -} - -impl TryFrom for AppDeleteMethodCallParams { - type Error = UtilsError; - fn try_from(value: RustAppDeleteMethodCallParams) -> Result { - Ok(AppDeleteMethodCallParams { - sender: value.sender.to_string(), - signer: value.signer.map(|s| { - Arc::new(FfiTransactionSignerFromRust { rust_signer: s }) - as Arc - }), - rekey_to: value.rekey_to.map(|r| r.to_string()), - note: value.note, - lease: value.lease.map(|l| l.to_vec()), - static_fee: value.static_fee, - extra_fee: value.extra_fee, - max_fee: value.max_fee, - validity_window: value.validity_window, - first_valid_round: value.first_valid_round, - last_valid_round: value.last_valid_round, - app_id: value.app_id, - method: value.method.into(), - args: value - .args - .into_iter() - .map(|arg| arg.try_into()) - .collect::>()?, - account_references: value - .account_references - .map(|accounts| accounts.into_iter().map(|a| a.to_string()).collect()), - app_references: value.app_references, - asset_references: value.asset_references, - box_references: value - .box_references - .map(|boxes| boxes.into_iter().map(|b| b.into()).collect()), - }) - } -} diff --git a/crates/algokit_utils_ffi/src/transactions/asset_config.rs b/crates/algokit_utils_ffi/src/transactions/asset_config.rs deleted file mode 100644 index cbd5c1ee9..000000000 --- a/crates/algokit_utils_ffi/src/transactions/asset_config.rs +++ /dev/null @@ -1,321 +0,0 @@ -use crate::create_transaction_params; -use crate::transactions::common::UtilsError; - -use algokit_utils::transactions::{ - AssetConfigParams as RustAssetConfigParams, AssetCreateParams as RustAssetCreateParams, - AssetDestroyParams as RustAssetDestroyParams, -}; - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct AssetCreateParams { - /// The total amount of the smallest divisible (decimal) unit to create. - pub total: u64, - - /// The amount of decimal places the asset should have. - #[uniffi(default = None)] - pub decimals: Option, - - /// Whether the asset is frozen by default for all accounts. - #[uniffi(default = None)] - pub default_frozen: Option, - - /// The optional name of the asset. - #[uniffi(default = None)] - pub asset_name: Option, - - /// The optional name of the unit of this asset. - #[uniffi(default = None)] - pub unit_name: Option, - - /// Specifies an optional URL where more information about the asset can be retrieved. - #[uniffi(default = None)] - pub url: Option, - - /// 32-byte hash of some metadata that is relevant to your asset. - #[uniffi(default = None)] - pub metadata_hash: Option>, - - /// The address of the optional account that can manage the configuration. - #[uniffi(default = None)] - pub manager: Option, - - /// The address of the optional account that holds the reserve units. - #[uniffi(default = None)] - pub reserve: Option, - - /// The address of the optional account that can freeze/unfreeze holdings. - #[uniffi(default = None)] - pub freeze: Option, - - /// The address of the optional account that can clawback holdings. - #[uniffi(default = None)] - pub clawback: Option, - } -} - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct AssetConfigParams { - /// ID of the existing asset to be reconfigured. - pub asset_id: u64, - - /// The address of the optional account that can manage the configuration. - #[uniffi(default = None)] - pub manager: Option, - - /// The address of the optional account that holds the reserve units. - #[uniffi(default = None)] - pub reserve: Option, - - /// The address of the optional account that can freeze/unfreeze holdings. - #[uniffi(default = None)] - pub freeze: Option, - - /// The address of the optional account that can clawback holdings. - #[uniffi(default = None)] - pub clawback: Option, - } -} - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct AssetDestroyParams { - /// ID of the existing asset to be destroyed. - pub asset_id: u64, - } -} - -fn parse_optional_address( - addr_opt: Option, - field_name: &str, -) -> Result, UtilsError> { - match addr_opt { - Some(addr_str) => { - let addr = addr_str.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Failed to parse {} address: {}", field_name, e), - })?; - Ok(Some(addr)) - } - None => Ok(None), - } -} - -impl TryFrom for RustAssetCreateParams { - type Error = UtilsError; - - fn try_from(params: AssetCreateParams) -> Result { - let metadata_hash = match params.metadata_hash { - Some(hash_vec) => { - if hash_vec.len() != 32 { - return Err(UtilsError::UtilsError { - message: format!( - "metadata_hash must be exactly 32 bytes, got {}", - hash_vec.len() - ), - }); - } - let mut hash_array = [0u8; 32]; - hash_array.copy_from_slice(&hash_vec); - Some(hash_array) - } - None => None, - }; - - Ok(RustAssetCreateParams { - sender: params.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::RustTransactionSignerFromFfi { ffi_signer: s }) - as std::sync::Arc - }), - rekey_to: params - .rekey_to - .map(|r| r.parse()) - .transpose() - .map_err(|e| UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - })?, - note: params.note, - lease: params.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - total: params.total, - decimals: params.decimals, - default_frozen: params.default_frozen, - asset_name: params.asset_name, - unit_name: params.unit_name, - url: params.url, - metadata_hash, - manager: parse_optional_address(params.manager, "manager")?, - reserve: parse_optional_address(params.reserve, "reserve")?, - freeze: parse_optional_address(params.freeze, "freeze")?, - clawback: parse_optional_address(params.clawback, "clawback")?, - }) - } -} - -impl TryFrom for RustAssetConfigParams { - type Error = UtilsError; - - fn try_from(params: AssetConfigParams) -> Result { - Ok(RustAssetConfigParams { - sender: params.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::RustTransactionSignerFromFfi { ffi_signer: s }) - as std::sync::Arc - }), - rekey_to: params - .rekey_to - .map(|r| r.parse()) - .transpose() - .map_err(|e| UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - })?, - note: params.note, - lease: params.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - asset_id: params.asset_id, - manager: parse_optional_address(params.manager, "manager")?, - reserve: parse_optional_address(params.reserve, "reserve")?, - freeze: parse_optional_address(params.freeze, "freeze")?, - clawback: parse_optional_address(params.clawback, "clawback")?, - }) - } -} - -impl TryFrom for RustAssetDestroyParams { - type Error = UtilsError; - - fn try_from(params: AssetDestroyParams) -> Result { - Ok(RustAssetDestroyParams { - sender: params.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::RustTransactionSignerFromFfi { ffi_signer: s }) - as std::sync::Arc - }), - rekey_to: params - .rekey_to - .map(|r| r.parse()) - .transpose() - .map_err(|e| UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - })?, - note: params.note, - lease: params.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - asset_id: params.asset_id, - }) - } -} - -impl From for AssetCreateParams { - fn from(params: RustAssetCreateParams) -> Self { - AssetCreateParams { - sender: params.sender.to_string(), - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::FfiTransactionSignerFromRust { rust_signer: s }) - as std::sync::Arc - }), - rekey_to: params.rekey_to.map(|r| r.to_string()), - note: params.note, - lease: params.lease.map(|l| l.to_vec()), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - total: params.total, - decimals: params.decimals, - default_frozen: params.default_frozen, - asset_name: params.asset_name, - unit_name: params.unit_name, - url: params.url, - metadata_hash: params.metadata_hash.map(|h| h.to_vec()), - manager: params.manager.map(|m| m.to_string()), - reserve: params.reserve.map(|r| r.to_string()), - freeze: params.freeze.map(|f| f.to_string()), - clawback: params.clawback.map(|c| c.to_string()), - } - } -} - -impl From for AssetConfigParams { - fn from(params: RustAssetConfigParams) -> Self { - AssetConfigParams { - sender: params.sender.to_string(), - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::FfiTransactionSignerFromRust { rust_signer: s }) - as std::sync::Arc - }), - rekey_to: params.rekey_to.map(|r| r.to_string()), - note: params.note, - lease: params.lease.map(|l| l.to_vec()), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - asset_id: params.asset_id, - manager: params.manager.map(|m| m.to_string()), - reserve: params.reserve.map(|r| r.to_string()), - freeze: params.freeze.map(|f| f.to_string()), - clawback: params.clawback.map(|c| c.to_string()), - } - } -} - -impl From for AssetDestroyParams { - fn from(params: RustAssetDestroyParams) -> Self { - AssetDestroyParams { - sender: params.sender.to_string(), - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::FfiTransactionSignerFromRust { rust_signer: s }) - as std::sync::Arc - }), - rekey_to: params.rekey_to.map(|r| r.to_string()), - note: params.note, - lease: params.lease.map(|l| l.to_vec()), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - asset_id: params.asset_id, - } - } -} diff --git a/crates/algokit_utils_ffi/src/transactions/asset_freeze.rs b/crates/algokit_utils_ffi/src/transactions/asset_freeze.rs deleted file mode 100644 index e978714ec..000000000 --- a/crates/algokit_utils_ffi/src/transactions/asset_freeze.rs +++ /dev/null @@ -1,160 +0,0 @@ -use crate::create_transaction_params; -use crate::transactions::common::UtilsError; - -use algokit_utils::transactions::{ - AssetFreezeParams as RustAssetFreezeParams, AssetUnfreezeParams as RustAssetUnfreezeParams, -}; - -use super::common::TransactionSigner; - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct AssetFreezeParams { - /// The ID of the asset being frozen. - pub asset_id: u64, - - /// The target account whose asset holdings will be frozen. - pub target_address: String, - } -} - -impl TryFrom for RustAssetFreezeParams { - type Error = UtilsError; - - fn try_from(params: AssetFreezeParams) -> Result { - Ok(RustAssetFreezeParams { - sender: params.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::RustTransactionSignerFromFfi { ffi_signer: s }) - as std::sync::Arc - }), - rekey_to: params - .rekey_to - .map(|r| r.parse()) - .transpose() - .map_err(|e| UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - })?, - note: params.note, - lease: params.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - asset_id: params.asset_id, - target_address: params - .target_address - .parse() - .map_err(|_| UtilsError::UtilsError { - message: "Invalid target address".to_string(), - })?, - }) - } -} - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct AssetUnfreezeParams { - /// The ID of the asset being unfrozen. - pub asset_id: u64, - - /// The target account whose asset holdings will be unfrozen. - pub target_address: String, - } -} - -impl TryFrom for RustAssetUnfreezeParams { - type Error = UtilsError; - - fn try_from(params: AssetUnfreezeParams) -> Result { - Ok(RustAssetUnfreezeParams { - sender: params.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::RustTransactionSignerFromFfi { ffi_signer: s }) - as std::sync::Arc - }), - rekey_to: params - .rekey_to - .map(|r| r.parse()) - .transpose() - .map_err(|e| UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - })?, - note: params.note, - lease: params.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - asset_id: params.asset_id, - target_address: params - .target_address - .parse() - .map_err(|_| UtilsError::UtilsError { - message: "Invalid target address".to_string(), - })?, - }) - } -} - -impl From for AssetFreezeParams { - fn from(params: RustAssetFreezeParams) -> Self { - AssetFreezeParams { - sender: params.sender.to_string(), - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::FfiTransactionSignerFromRust { rust_signer: s }) - as std::sync::Arc - }), - rekey_to: params.rekey_to.map(|r| r.to_string()), - note: params.note, - lease: params.lease.map(|l| l.to_vec()), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - asset_id: params.asset_id, - target_address: params.target_address.to_string(), - } - } -} - -impl From for AssetUnfreezeParams { - fn from(params: RustAssetUnfreezeParams) -> Self { - AssetUnfreezeParams { - sender: params.sender.to_string(), - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::FfiTransactionSignerFromRust { rust_signer: s }) - as std::sync::Arc - }), - rekey_to: params.rekey_to.map(|r| r.to_string()), - note: params.note, - lease: params.lease.map(|l| l.to_vec()), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - asset_id: params.asset_id, - target_address: params.target_address.to_string(), - } - } -} diff --git a/crates/algokit_utils_ffi/src/transactions/asset_transfer.rs b/crates/algokit_utils_ffi/src/transactions/asset_transfer.rs deleted file mode 100644 index 001368236..000000000 --- a/crates/algokit_utils_ffi/src/transactions/asset_transfer.rs +++ /dev/null @@ -1,347 +0,0 @@ -use crate::create_transaction_params; -use crate::transactions::common::UtilsError; - -use algokit_utils::transactions::{ - AssetClawbackParams as RustAssetClawbackParams, AssetOptInParams as RustAssetOptInParams, - AssetOptOutParams as RustAssetOptOutParams, AssetTransferParams as RustAssetTransferParams, -}; - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct AssetTransferParams { - /// The ID of the asset being transferred. - pub asset_id: u64, - - /// The amount of the asset to transfer. - pub amount: u64, - - /// The address that will receive the asset. - pub receiver: String, - } -} - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct AssetOptInParams { - /// The ID of the asset to opt into. - pub asset_id: u64, - } -} - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct AssetOptOutParams { - /// The ID of the asset to opt out of. - pub asset_id: u64, - - /// The address to close the remainder to. If None, defaults to the asset creator. - #[uniffi(default = None)] - pub close_remainder_to: Option, - } -} - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct AssetClawbackParams { - /// The ID of the asset being clawed back. - pub asset_id: u64, - - /// The amount of the asset to clawback. - pub amount: u64, - - /// The address that will receive the clawed back asset. - pub receiver: String, - - /// The address from which assets are taken. - pub clawback_target: String, - } -} - -impl TryFrom for RustAssetTransferParams { - type Error = UtilsError; - - fn try_from(params: AssetTransferParams) -> Result { - Ok(RustAssetTransferParams { - sender: params.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::RustTransactionSignerFromFfi { ffi_signer: s }) - as std::sync::Arc - }), - rekey_to: params - .rekey_to - .map(|r| r.parse()) - .transpose() - .map_err(|e| UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - })?, - note: params.note, - lease: params.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - asset_id: params.asset_id, - amount: params.amount, - receiver: params - .receiver - .parse() - .map_err(|e| UtilsError::UtilsError { - message: format!("Failed to parse receiver address: {}", e), - })?, - }) - } -} - -impl TryFrom for RustAssetOptInParams { - type Error = UtilsError; - - fn try_from(params: AssetOptInParams) -> Result { - Ok(RustAssetOptInParams { - sender: params.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::RustTransactionSignerFromFfi { ffi_signer: s }) - as std::sync::Arc - }), - rekey_to: params - .rekey_to - .map(|r| r.parse()) - .transpose() - .map_err(|e| UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - })?, - note: params.note, - lease: params.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - asset_id: params.asset_id, - }) - } -} - -impl TryFrom for RustAssetOptOutParams { - type Error = UtilsError; - - fn try_from(params: AssetOptOutParams) -> Result { - let close_remainder_to = match params.close_remainder_to { - Some(addr) => Some(addr.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Failed to parse close_remainder_to address: {}", e), - })?), - None => None, - }; - - Ok(RustAssetOptOutParams { - sender: params.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::RustTransactionSignerFromFfi { ffi_signer: s }) - as std::sync::Arc - }), - rekey_to: params - .rekey_to - .map(|r| r.parse()) - .transpose() - .map_err(|e| UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - })?, - note: params.note, - lease: params.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - asset_id: params.asset_id, - close_remainder_to, - }) - } -} - -impl TryFrom for RustAssetClawbackParams { - type Error = UtilsError; - - fn try_from(params: AssetClawbackParams) -> Result { - Ok(RustAssetClawbackParams { - sender: params.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::RustTransactionSignerFromFfi { ffi_signer: s }) - as std::sync::Arc - }), - rekey_to: params - .rekey_to - .map(|r| r.parse()) - .transpose() - .map_err(|e| UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - })?, - note: params.note, - lease: params.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - asset_id: params.asset_id, - amount: params.amount, - receiver: params - .receiver - .parse() - .map_err(|e| UtilsError::UtilsError { - message: format!("Failed to parse receiver address: {}", e), - })?, - clawback_target: params.clawback_target.parse().map_err(|e| { - UtilsError::UtilsError { - message: format!("Failed to parse clawback_target address: {}", e), - } - })?, - }) - } -} - -// impl From for PaymentParams { -// fn from(params: RustPaymentParams) -> Self { -// PaymentParams { -// sender: params.sender.to_string(), -// signer: params.signer.map(|s| { -// Arc::new(FfiTransactionSignerFromRust { rust_signer: s }) -// as Arc -// }), -// -// rekey_to: params.rekey_to.map(|r| r.to_string()), -// note: params.note, -// lease: params.lease.map(|l| l.to_vec()), -// static_fee: params.static_fee, -// extra_fee: params.extra_fee, -// max_fee: params.max_fee, -// validity_window: params.validity_window, -// first_valid_round: params.first_valid_round, -// last_valid_round: params.last_valid_round, -// receiver: params.receiver.to_string(), -// amount: params.amount, -// } -// } -// } - -impl From for AssetTransferParams { - fn from(params: RustAssetTransferParams) -> Self { - AssetTransferParams { - sender: params.sender.to_string(), - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::FfiTransactionSignerFromRust { rust_signer: s }) - as std::sync::Arc - }), - rekey_to: params.rekey_to.map(|r| r.to_string()), - note: params.note, - lease: params.lease.map(|l| l.to_vec()), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - asset_id: params.asset_id, - amount: params.amount, - receiver: params.receiver.to_string(), - } - } -} - -impl From for AssetOptInParams { - fn from(params: RustAssetOptInParams) -> Self { - AssetOptInParams { - sender: params.sender.to_string(), - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::FfiTransactionSignerFromRust { rust_signer: s }) - as std::sync::Arc - }), - rekey_to: params.rekey_to.map(|r| r.to_string()), - note: params.note, - lease: params.lease.map(|l| l.to_vec()), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - asset_id: params.asset_id, - } - } -} - -impl From for AssetOptOutParams { - fn from(params: RustAssetOptOutParams) -> Self { - AssetOptOutParams { - sender: params.sender.to_string(), - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::FfiTransactionSignerFromRust { rust_signer: s }) - as std::sync::Arc - }), - rekey_to: params.rekey_to.map(|r| r.to_string()), - note: params.note, - lease: params.lease.map(|l| l.to_vec()), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - asset_id: params.asset_id, - close_remainder_to: params.close_remainder_to.map(|addr| addr.to_string()), - } - } -} - -impl From for AssetClawbackParams { - fn from(params: RustAssetClawbackParams) -> Self { - AssetClawbackParams { - sender: params.sender.to_string(), - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::FfiTransactionSignerFromRust { rust_signer: s }) - as std::sync::Arc - }), - rekey_to: params.rekey_to.map(|r| r.to_string()), - note: params.note, - lease: params.lease.map(|l| l.to_vec()), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - asset_id: params.asset_id, - amount: params.amount, - receiver: params.receiver.to_string(), - clawback_target: params.clawback_target.to_string(), - } - } -} diff --git a/crates/algokit_utils_ffi/src/transactions/common.rs b/crates/algokit_utils_ffi/src/transactions/common.rs deleted file mode 100644 index ce46d46ab..000000000 --- a/crates/algokit_utils_ffi/src/transactions/common.rs +++ /dev/null @@ -1,289 +0,0 @@ -use std::sync::Arc; - -use algokit_transact::Address; -use algokit_transact::SignedTransaction as RustSignedTransaction; -use algokit_transact::Transaction as RustTransaction; -use algokit_transact_ffi::{SignedTransaction, Transaction}; -use algokit_utils::transactions::common::TransactionSigner as RustTransactionSigner; -use algokit_utils::transactions::common::TransactionSignerGetter as RustTransactionSignerGetter; -use algokit_utils::transactions::common::TransactionWithSigner as RustTransactionWithSigner; - -use async_trait::async_trait; -use derive_more::Debug; -use snafu::Snafu; - -// TODO: implement proper errors -#[derive(Debug, Snafu, uniffi::Error)] -pub enum UtilsError { - #[snafu(display("UtilsError: {message}"))] - UtilsError { message: String }, -} - -#[uniffi::export(with_foreign)] -#[async_trait] -pub trait TransactionSigner: Send + Sync { - async fn sign_transactions( - &self, - transactions: Vec, - indices: Vec, - ) -> Result, UtilsError>; - - async fn sign_transaction( - &self, - transaction: Transaction, - ) -> Result; -} - -pub struct RustTransactionSignerFromFfi { - pub ffi_signer: Arc, -} - -#[async_trait] -impl RustTransactionSigner for RustTransactionSignerFromFfi { - async fn sign_transactions( - &self, - transactions: &[RustTransaction], - indices: &[usize], - ) -> Result, String> { - let ffi_txns: Vec = transactions.iter().map(|t| t.to_owned().into()).collect(); - - let ffi_signed_txns = self - .ffi_signer - .sign_transactions(ffi_txns, indices.iter().map(|&i| i as u32).collect()) - .await - .map_err(|e| e.to_string())?; - - let signed_txns: Result, _> = ffi_signed_txns - .into_iter() - .map(|st| st.try_into()) - .collect(); - signed_txns.map_err(|e| format!("Failed to convert signed transactions: {}", e)) - } -} - -pub struct FfiTransactionSignerFromRust { - pub rust_signer: Arc, -} - -#[async_trait] -impl TransactionSigner for FfiTransactionSignerFromRust { - async fn sign_transactions( - &self, - transactions: Vec, - indices: Vec, - ) -> Result, UtilsError> { - let rust_txns: Result, _> = - transactions.into_iter().map(|t| t.try_into()).collect(); - let rust_txns = rust_txns.map_err(|e| UtilsError::UtilsError { - message: format!("Failed to convert transactions: {}", e), - })?; - - let signed_txns = self - .rust_signer - .sign_transactions( - &rust_txns, - &indices.iter().map(|&i| i as usize).collect::>(), - ) - .await - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - })?; - - Ok(signed_txns.into_iter().map(|st| st.into()).collect()) - } - - async fn sign_transaction( - &self, - transaction: Transaction, - ) -> Result { - let txns = vec![transaction]; - let indices = vec![0u32]; - let mut signed_txns = self.sign_transactions(txns, indices).await?; - signed_txns.pop().ok_or(UtilsError::UtilsError { - message: "No signed transaction returned".to_string(), - }) - } -} - -#[uniffi::export(with_foreign)] -pub trait TransactionSignerGetter: Send + Sync { - fn get_signer(&self, address: String) -> Result, UtilsError>; - - /// Register an account with the signer getter - /// This allows test fixtures to register accounts so they can be retrieved later via get_signer - fn register_account(&self, address: String, mnemonic: String) -> Result<(), UtilsError>; -} - -pub struct RustTransactionSignerGetterFromFfi { - pub ffi_signer_getter: Arc, -} - -impl RustTransactionSignerGetter for RustTransactionSignerGetterFromFfi { - fn get_signer(&self, address: Address) -> Result, String> { - self.ffi_signer_getter - .get_signer(address.to_string()) - .map(|ffi_signer| { - Arc::new(RustTransactionSignerFromFfi { ffi_signer }) - as Arc - }) - .map_err(|e| e.to_string()) - } -} - -pub struct FfiTransactionSignerGetterFromRust { - pub rust_signer_getter: Arc, -} - -impl TransactionSignerGetter for FfiTransactionSignerGetterFromRust { - fn get_signer(&self, address: String) -> Result, UtilsError> { - self.rust_signer_getter - .get_signer(address.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid address {address}: {e}"), - })?) - .map(|rust_signer| { - Arc::new(FfiTransactionSignerFromRust { rust_signer }) as Arc - }) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - fn register_account(&self, _address: String, _mnemonic: String) -> Result<(), UtilsError> { - // No-op: This adapter wraps a Rust signer getter which doesn't need registration - // This method is here for backwards compatibility with the FFI trait - Ok(()) - } -} - -#[derive(uniffi::Record)] -pub struct TransactionWithSigner { - pub transaction: Transaction, - pub signer: Arc, -} - -impl TryFrom for RustTransactionWithSigner { - type Error = UtilsError; - - fn try_from(value: TransactionWithSigner) -> Result { - let rust_txn: RustTransaction = - value - .transaction - .try_into() - .map_err(|e| UtilsError::UtilsError { - message: format!("Failed to convert transaction: {}", e), - })?; - - Ok(RustTransactionWithSigner { - transaction: rust_txn, - signer: Arc::new(RustTransactionSignerFromFfi { - ffi_signer: value.signer, - }), - }) - } -} - -impl TryFrom for TransactionWithSigner { - type Error = UtilsError; - - fn try_from(value: RustTransactionWithSigner) -> Result { - let ffi_txn: Transaction = value.transaction.into(); - - Ok(TransactionWithSigner { - transaction: ffi_txn, - signer: Arc::new(FfiTransactionSignerFromRust { - rust_signer: value.signer, - }), - }) - } -} - -#[derive(Debug, uniffi::Record)] -pub struct CommonParams { - pub sender: String, - #[debug(skip)] - #[uniffi(default = None)] - pub signer: Option>, - #[uniffi(default = None)] - pub rekey_to: Option, - #[uniffi(default = None)] - pub note: Option>, - #[uniffi(default = None)] - pub lease: Option>, - #[uniffi(default = None)] - pub static_fee: Option, - #[uniffi(default = None)] - pub extra_fee: Option, - #[uniffi(default = None)] - pub max_fee: Option, - #[uniffi(default = None)] - pub validity_window: Option, - #[uniffi(default = None)] - pub first_valid_round: Option, - #[uniffi(default = None)] - pub last_valid_round: Option, -} - -#[macro_export] -macro_rules! create_transaction_params { - ( - $(#[$struct_attr:meta])* - pub struct $name:ident { - $( - $(#[$field_attr:meta])* - pub $field:ident: $field_type:ty, - )* - } - ) => { - $(#[$struct_attr])* - #[derive(derive_more::Debug)] - pub struct $name { - /// The address of the account sending the transaction. - pub sender: String, - #[debug(skip)] - /// A signer used to sign transaction(s); if not specified then - /// an attempt will be made to find a registered signer for the - /// given `sender` or use a default signer (if configured). - #[uniffi(default = None)] - pub signer: Option>, - /// Change the signing key of the sender to the given address. - /// **Warning:** Please be careful with this parameter and be sure to read the [official rekey guidance](https://dev.algorand.co/concepts/accounts/rekeying). - #[uniffi(default = None)] - pub rekey_to: Option, - /// Note to attach to the transaction. Max of 1000 bytes. - #[uniffi(default = None)] - pub note: Option>, - /// Prevent multiple transactions with the same lease being included within the validity window. - /// - /// A [lease](https://dev.algorand.co/concepts/transactions/leases) - /// enforces a mutually exclusive transaction (useful to prevent double-posting and other scenarios). - #[uniffi(default = None)] - pub lease: Option>, - /// The static transaction fee. In most cases you want to use extra fee unless setting the fee to 0 to be covered by another transaction. - #[uniffi(default = None)] - pub static_fee: Option, - /// The fee to pay IN ADDITION to the suggested fee. Useful for manually covering inner transaction fees. - #[uniffi(default = None)] - pub extra_fee: Option, - /// Throw an error if the fee for the transaction is more than this amount; prevents overspending on fees during high congestion periods. - #[uniffi(default = None)] - pub max_fee: Option, - /// How many rounds the transaction should be valid for, if not specified then the registered default validity window will be used. - #[uniffi(default = None)] - pub validity_window: Option, - /// Set the first round this transaction is valid. - /// If left undefined, the value from algod will be used. - /// - /// We recommend you only set this when you intentionally want this to be some time in the future. - #[uniffi(default = None)] - pub first_valid_round: Option, - /// The last round this transaction is valid. It is recommended to use validity window instead. - #[uniffi(default = None)] - pub last_valid_round: Option, - // Specific fields - $( - $(#[$field_attr])* - pub $field: $field_type, - )* - } - }; -} diff --git a/crates/algokit_utils_ffi/src/transactions/composer.rs b/crates/algokit_utils_ffi/src/transactions/composer.rs deleted file mode 100644 index 16692e2f3..000000000 --- a/crates/algokit_utils_ffi/src/transactions/composer.rs +++ /dev/null @@ -1,512 +0,0 @@ -// Standard library imports -use std::sync::Arc; - -// Third-party imports -use async_trait::async_trait; -use tokio::sync::Mutex; - -// Crate imports -use crate::transactions::{ - app_call::{ - AppCallMethodCallParams, AppCallParams, AppCreateMethodCallParams, AppCreateParams, - AppDeleteMethodCallParams, AppDeleteParams, AppUpdateMethodCallParams, AppUpdateParams, - }, - asset_config::{AssetConfigParams, AssetCreateParams, AssetDestroyParams}, - asset_freeze::{AssetFreezeParams, AssetUnfreezeParams}, - asset_transfer::{ - AssetClawbackParams, AssetOptInParams, AssetOptOutParams, AssetTransferParams, - }, - common::{RustTransactionSignerGetterFromFfi, TransactionSignerGetter, UtilsError}, - payment::PaymentParams, -}; - -// External crate imports -// algod_client -use algod_client::AlgodClient as RustAlgodClient; - -// algokit_http_client -use algokit_http_client::HttpClient; - -// algokit_utils -use algokit_utils::transactions::{ - TransactionComposerParams, composer::TransactionComposer as RustComposer, -}; - -#[derive(uniffi::Object)] -pub struct AlgodClient { - inner_algod_client: Mutex, -} - -#[uniffi::export] -impl AlgodClient { - #[uniffi::constructor] - pub fn new(http_client: Arc) -> Self { - let algod_client = RustAlgodClient::new(http_client); - AlgodClient { - inner_algod_client: Mutex::new(algod_client), - } - } -} - -// NOTE: This struct is a temporary placeholder until we have a proper algod_api_ffi crate with the fully typed response -#[derive(uniffi::Record)] -pub struct TempSendResponse { - pub transaction_ids: Vec, - pub app_ids: Vec>, -} - -#[derive(uniffi::Object)] -pub struct Composer { - inner_composer: Mutex, -} - -#[uniffi::export] -impl Composer { - #[uniffi::constructor] - pub fn new( - algod_client: Arc, - signer_getter: Arc, - ) -> Self { - let rust_signer_getter = RustTransactionSignerGetterFromFfi { - ffi_signer_getter: signer_getter.clone(), - }; - - let rust_composer = { - let rust_algod_client = algod_client.inner_algod_client.blocking_lock(); - RustComposer::new(TransactionComposerParams { - algod_client: Arc::new(rust_algod_client.clone()), - signer_getter: Arc::new(rust_signer_getter), - composer_config: None, - }) - }; - - Composer { - inner_composer: Mutex::new(rust_composer), - } - } - - pub fn add_payment(&self, params: PaymentParams) -> Result<(), UtilsError> { - let mut composer = self.inner_composer.blocking_lock(); - composer - .add_payment(params.try_into()?) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - pub async fn send(&self) -> Result { - let mut composer = self.inner_composer.blocking_lock(); - let result = composer - .send(None) - .await - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - })?; - Ok(TempSendResponse { - transaction_ids: result - .results - .iter() - .map(|r| r.transaction_id.clone()) - .collect(), - app_ids: result - .results - .iter() - .map(|r| r.confirmation.app_id) - .collect(), - }) - } - - pub async fn build(&self) -> Result<(), UtilsError> { - let mut composer = self.inner_composer.blocking_lock(); - composer.build().await.map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - })?; - - Ok(()) - } - - pub fn add_asset_create(&self, params: AssetCreateParams) -> Result<(), UtilsError> { - let mut composer = self.inner_composer.blocking_lock(); - composer - .add_asset_create(params.try_into()?) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - pub fn add_asset_config(&self, params: AssetConfigParams) -> Result<(), UtilsError> { - let mut composer = self.inner_composer.blocking_lock(); - composer - .add_asset_config(params.try_into()?) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - pub fn add_asset_destroy(&self, params: AssetDestroyParams) -> Result<(), UtilsError> { - let mut composer = self.inner_composer.blocking_lock(); - composer - .add_asset_destroy(params.try_into()?) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - pub fn add_asset_freeze(&self, params: AssetFreezeParams) -> Result<(), UtilsError> { - let mut composer = self.inner_composer.blocking_lock(); - composer - .add_asset_freeze(params.try_into()?) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - pub fn add_asset_unfreeze(&self, params: AssetUnfreezeParams) -> Result<(), UtilsError> { - let mut composer = self.inner_composer.blocking_lock(); - composer - .add_asset_unfreeze(params.try_into()?) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - pub fn add_asset_transfer(&self, params: AssetTransferParams) -> Result<(), UtilsError> { - let mut composer = self.inner_composer.blocking_lock(); - composer - .add_asset_transfer(params.try_into()?) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - pub fn add_asset_opt_in(&self, params: AssetOptInParams) -> Result<(), UtilsError> { - let mut composer = self.inner_composer.blocking_lock(); - composer - .add_asset_opt_in(params.try_into()?) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - pub fn add_asset_opt_out(&self, params: AssetOptOutParams) -> Result<(), UtilsError> { - let mut composer = self.inner_composer.blocking_lock(); - composer - .add_asset_opt_out(params.try_into()?) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - pub fn add_asset_clawback(&self, params: AssetClawbackParams) -> Result<(), UtilsError> { - let mut composer = self.inner_composer.blocking_lock(); - composer - .add_asset_clawback(params.try_into()?) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - pub fn add_app_create(&self, params: AppCreateParams) -> Result<(), UtilsError> { - let mut composer = self.inner_composer.blocking_lock(); - composer - .add_app_create(params.try_into()?) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - pub fn add_app_call(&self, params: AppCallParams) -> Result<(), UtilsError> { - let mut composer = self.inner_composer.blocking_lock(); - composer - .add_app_call(params.try_into()?) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - pub fn add_app_update(&self, params: AppUpdateParams) -> Result<(), UtilsError> { - let mut composer = self.inner_composer.blocking_lock(); - composer - .add_app_update(params.try_into()?) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - pub fn add_app_delete(&self, params: AppDeleteParams) -> Result<(), UtilsError> { - let mut composer = self.inner_composer.blocking_lock(); - composer - .add_app_delete(params.try_into()?) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - pub fn add_app_call_method_call( - &self, - params: AppCallMethodCallParams, - ) -> Result<(), UtilsError> { - let mut composer = self.inner_composer.blocking_lock(); - composer - .add_app_call_method_call(params.try_into()?) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - pub fn add_app_create_method_call( - &self, - params: AppCreateMethodCallParams, - ) -> Result<(), UtilsError> { - let mut composer = self.inner_composer.blocking_lock(); - composer - .add_app_create_method_call(params.try_into()?) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - pub fn add_app_update_method_call( - &self, - params: AppUpdateMethodCallParams, - ) -> Result<(), UtilsError> { - let mut composer = self.inner_composer.blocking_lock(); - composer - .add_app_update_method_call(params.try_into()?) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } - - pub fn add_app_delete_method_call( - &self, - params: AppDeleteMethodCallParams, - ) -> Result<(), UtilsError> { - let mut composer = self.inner_composer.blocking_lock(); - composer - .add_app_delete_method_call(params.try_into()?) - .map_err(|e| UtilsError::UtilsError { - message: e.to_string(), - }) - } -} - -// Implement ComposerTrait for Composer to keep them in sync -#[async_trait] -impl ComposerTrait for Composer { - async fn build(&self) -> Result<(), UtilsError> { - Composer::build(self).await - } - - async fn send(&self) -> Result, UtilsError> { - let response = Composer::send(self).await?; - Ok(response.transaction_ids) - } - - async fn add_payment(&self, params: super::payment::PaymentParams) -> Result<(), UtilsError> { - Composer::add_payment(self, params) - } - - async fn add_asset_create(&self, params: AssetCreateParams) -> Result<(), UtilsError> { - Composer::add_asset_create(self, params) - } - - async fn add_asset_reconfigure(&self, params: AssetConfigParams) -> Result<(), UtilsError> { - Composer::add_asset_config(self, params) - } - - async fn add_asset_destroy(&self, params: AssetDestroyParams) -> Result<(), UtilsError> { - Composer::add_asset_destroy(self, params) - } - - async fn add_asset_freeze(&self, params: AssetFreezeParams) -> Result<(), UtilsError> { - Composer::add_asset_freeze(self, params) - } - - async fn add_asset_unfreeze(&self, params: AssetUnfreezeParams) -> Result<(), UtilsError> { - Composer::add_asset_unfreeze(self, params) - } - - async fn add_asset_transfer(&self, params: AssetTransferParams) -> Result<(), UtilsError> { - Composer::add_asset_transfer(self, params) - } - - async fn add_asset_opt_in(&self, params: AssetOptInParams) -> Result<(), UtilsError> { - Composer::add_asset_opt_in(self, params) - } - - async fn add_asset_opt_out(&self, params: AssetOptOutParams) -> Result<(), UtilsError> { - Composer::add_asset_opt_out(self, params) - } - - async fn add_asset_clawback(&self, params: AssetClawbackParams) -> Result<(), UtilsError> { - Composer::add_asset_clawback(self, params) - } - - async fn add_app_create( - &self, - params: super::app_call::AppCreateParams, - ) -> Result<(), UtilsError> { - Composer::add_app_create(self, params) - } - - async fn add_app_call(&self, params: super::app_call::AppCallParams) -> Result<(), UtilsError> { - Composer::add_app_call(self, params) - } - - async fn add_app_update( - &self, - params: super::app_call::AppUpdateParams, - ) -> Result<(), UtilsError> { - Composer::add_app_update(self, params) - } - - async fn add_app_delete( - &self, - params: super::app_call::AppDeleteParams, - ) -> Result<(), UtilsError> { - Composer::add_app_delete(self, params) - } - - async fn add_app_call_method_call( - &self, - params: super::app_call::AppCallMethodCallParams, - ) -> Result<(), UtilsError> { - Composer::add_app_call_method_call(self, params) - } - - async fn add_app_create_method_call( - &self, - params: super::app_call::AppCreateMethodCallParams, - ) -> Result<(), UtilsError> { - Composer::add_app_create_method_call(self, params) - } - - async fn add_app_update_method_call( - &self, - params: super::app_call::AppUpdateMethodCallParams, - ) -> Result<(), UtilsError> { - Composer::add_app_update_method_call(self, params) - } - - async fn add_app_delete_method_call( - &self, - params: super::app_call::AppDeleteMethodCallParams, - ) -> Result<(), UtilsError> { - Composer::add_app_delete_method_call(self, params) - } -} - -// -// Foreign trait for target language testing -// This trait is implemented by Python to enable async test orchestration -// where Python controls the async context and Rust handles business logic -// -// STATEFUL DESIGN: Implementations store algod_client and signer_getter -// internally, eliminating the need to pass them on every method call. -// -#[uniffi::export(with_foreign)] -#[async_trait] -pub trait ComposerTrait: Send + Sync { - async fn build(&self) -> Result<(), UtilsError>; - async fn send(&self) -> Result, UtilsError>; - - async fn add_payment(&self, params: super::payment::PaymentParams) -> Result<(), UtilsError>; - - async fn add_asset_create(&self, params: AssetCreateParams) -> Result<(), UtilsError>; - - async fn add_asset_reconfigure(&self, params: AssetConfigParams) -> Result<(), UtilsError>; - - async fn add_asset_destroy(&self, params: AssetDestroyParams) -> Result<(), UtilsError>; - - async fn add_asset_freeze(&self, params: AssetFreezeParams) -> Result<(), UtilsError>; - - async fn add_asset_unfreeze(&self, params: AssetUnfreezeParams) -> Result<(), UtilsError>; - - async fn add_asset_transfer(&self, params: AssetTransferParams) -> Result<(), UtilsError>; - - async fn add_asset_opt_in(&self, params: AssetOptInParams) -> Result<(), UtilsError>; - - async fn add_asset_opt_out(&self, params: AssetOptOutParams) -> Result<(), UtilsError>; - - async fn add_asset_clawback(&self, params: AssetClawbackParams) -> Result<(), UtilsError>; - - async fn add_app_create( - &self, - params: super::app_call::AppCreateParams, - ) -> Result<(), UtilsError>; - - async fn add_app_call(&self, params: super::app_call::AppCallParams) -> Result<(), UtilsError>; - - async fn add_app_update( - &self, - params: super::app_call::AppUpdateParams, - ) -> Result<(), UtilsError>; - - async fn add_app_delete( - &self, - params: super::app_call::AppDeleteParams, - ) -> Result<(), UtilsError>; - - async fn add_app_call_method_call( - &self, - params: super::app_call::AppCallMethodCallParams, - ) -> Result<(), UtilsError>; - - async fn add_app_create_method_call( - &self, - params: super::app_call::AppCreateMethodCallParams, - ) -> Result<(), UtilsError>; - - async fn add_app_update_method_call( - &self, - params: super::app_call::AppUpdateMethodCallParams, - ) -> Result<(), UtilsError>; - - async fn add_app_delete_method_call( - &self, - params: super::app_call::AppDeleteMethodCallParams, - ) -> Result<(), UtilsError>; -} - -// -// Foreign trait for creating fresh composer instances -// This enables proper composer lifecycle management for multi-operation tests -// -#[uniffi::export(with_foreign)] -pub trait ComposerFactory: Send + Sync { - fn create_composer(&self) -> Arc; -} - -// -// Concrete ComposerFactory implementation for Rust-side usage -// Creates fresh Composer instances (the FFI concrete type) -// -#[derive(uniffi::Object)] -pub struct DefaultComposerFactory { - algod_client: Arc, - signer_getter: Arc, -} - -#[uniffi::export] -impl DefaultComposerFactory { - #[uniffi::constructor] - pub fn new( - algod_client: Arc, - signer_getter: Arc, - ) -> Self { - DefaultComposerFactory { - algod_client, - signer_getter, - } - } -} - -impl ComposerFactory for DefaultComposerFactory { - fn create_composer(&self) -> Arc { - Arc::new(Composer::new( - self.algod_client.clone(), - self.signer_getter.clone(), - )) - } -} diff --git a/crates/algokit_utils_ffi/src/transactions/key_registration.rs b/crates/algokit_utils_ffi/src/transactions/key_registration.rs deleted file mode 100644 index b6117bfa7..000000000 --- a/crates/algokit_utils_ffi/src/transactions/key_registration.rs +++ /dev/null @@ -1,252 +0,0 @@ -use crate::create_transaction_params; -use crate::transactions::common::UtilsError; - -use algokit_utils::transactions::{ - NonParticipationKeyRegistrationParams as RustNonParticipationKeyRegistrationParams, - OfflineKeyRegistrationParams as RustOfflineKeyRegistrationParams, - OnlineKeyRegistrationParams as RustOnlineKeyRegistrationParams, -}; - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct OnlineKeyRegistrationParams { - /// The root participation public key. - pub vote_key: Vec, - - /// The VRF public key. - pub selection_key: Vec, - - /// The first round that the participation key is valid. - pub vote_first: u64, - - /// The last round that the participation key is valid. - pub vote_last: u64, - - /// This is the dilution for the 2-level participation key. - pub vote_key_dilution: u64, - - /// The 64 byte state proof public key commitment. - #[uniffi(default = None)] - pub state_proof_key: Option>, - } -} - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct OfflineKeyRegistrationParams { - // No additional fields beyond common params - } -} - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct NonParticipationKeyRegistrationParams { - // No additional fields beyond common params - } -} - -impl TryFrom for RustOnlineKeyRegistrationParams { - type Error = UtilsError; - - fn try_from(params: OnlineKeyRegistrationParams) -> Result { - let vote_key: [u8; 32] = - params - .vote_key - .try_into() - .map_err(|_| UtilsError::UtilsError { - message: "Vote key must be exactly 32 bytes".to_string(), - })?; - - let selection_key: [u8; 32] = - params - .selection_key - .try_into() - .map_err(|_| UtilsError::UtilsError { - message: "Selection key must be exactly 32 bytes".to_string(), - })?; - - let state_proof_key = match params.state_proof_key { - Some(key) => { - let key_array: [u8; 64] = key.try_into().map_err(|_| UtilsError::UtilsError { - message: "State proof key must be exactly 64 bytes".to_string(), - })?; - Some(key_array) - } - None => None, - }; - - Ok(RustOnlineKeyRegistrationParams { - sender: params.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::RustTransactionSignerFromFfi { ffi_signer: s }) - as std::sync::Arc - }), - rekey_to: params - .rekey_to - .map(|r| r.parse()) - .transpose() - .map_err(|e| UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - })?, - note: params.note, - lease: params.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - vote_key, - selection_key, - vote_first: params.vote_first, - vote_last: params.vote_last, - vote_key_dilution: params.vote_key_dilution, - state_proof_key, - }) - } -} - -impl TryFrom for RustOfflineKeyRegistrationParams { - type Error = UtilsError; - - fn try_from(params: OfflineKeyRegistrationParams) -> Result { - Ok(RustOfflineKeyRegistrationParams { - sender: params.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::RustTransactionSignerFromFfi { ffi_signer: s }) - as std::sync::Arc - }), - rekey_to: params - .rekey_to - .map(|r| r.parse()) - .transpose() - .map_err(|e| UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - })?, - note: params.note, - lease: params.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - }) - } -} - -impl TryFrom for RustNonParticipationKeyRegistrationParams { - type Error = UtilsError; - - fn try_from(params: NonParticipationKeyRegistrationParams) -> Result { - Ok(RustNonParticipationKeyRegistrationParams { - sender: params.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::RustTransactionSignerFromFfi { ffi_signer: s }) - as std::sync::Arc - }), - rekey_to: params - .rekey_to - .map(|r| r.parse()) - .transpose() - .map_err(|e| UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - })?, - note: params.note, - lease: params.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - }) - } -} - -impl From for OnlineKeyRegistrationParams { - fn from(params: RustOnlineKeyRegistrationParams) -> Self { - OnlineKeyRegistrationParams { - sender: params.sender.to_string(), - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::FfiTransactionSignerFromRust { rust_signer: s }) - as std::sync::Arc - }), - rekey_to: params.rekey_to.map(|r| r.to_string()), - note: params.note, - lease: params.lease.map(|l| l.to_vec()), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - vote_key: params.vote_key.to_vec(), - selection_key: params.selection_key.to_vec(), - vote_first: params.vote_first, - vote_last: params.vote_last, - vote_key_dilution: params.vote_key_dilution, - state_proof_key: params.state_proof_key.map(|k| k.to_vec()), - } - } -} - -impl From for OfflineKeyRegistrationParams { - fn from(params: RustOfflineKeyRegistrationParams) -> Self { - OfflineKeyRegistrationParams { - sender: params.sender.to_string(), - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::FfiTransactionSignerFromRust { rust_signer: s }) - as std::sync::Arc - }), - rekey_to: params.rekey_to.map(|r| r.to_string()), - note: params.note, - lease: params.lease.map(|l| l.to_vec()), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - } - } -} - -impl From for NonParticipationKeyRegistrationParams { - fn from(params: RustNonParticipationKeyRegistrationParams) -> Self { - NonParticipationKeyRegistrationParams { - sender: params.sender.to_string(), - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::FfiTransactionSignerFromRust { rust_signer: s }) - as std::sync::Arc - }), - rekey_to: params.rekey_to.map(|r| r.to_string()), - note: params.note, - lease: params.lease.map(|l| l.to_vec()), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - } - } -} diff --git a/crates/algokit_utils_ffi/src/transactions/mod.rs b/crates/algokit_utils_ffi/src/transactions/mod.rs deleted file mode 100644 index 4aded0897..000000000 --- a/crates/algokit_utils_ffi/src/transactions/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod app_call; -pub mod asset_config; -pub mod asset_freeze; -pub mod asset_transfer; -pub mod common; -pub mod composer; -pub mod key_registration; -pub mod payment; diff --git a/crates/algokit_utils_ffi/src/transactions/payment.rs b/crates/algokit_utils_ffi/src/transactions/payment.rs deleted file mode 100644 index 7d0d2ec77..000000000 --- a/crates/algokit_utils_ffi/src/transactions/payment.rs +++ /dev/null @@ -1,161 +0,0 @@ -use std::sync::Arc; - -use crate::create_transaction_params; -use crate::transactions::common::UtilsError; - -use algokit_utils::transactions::{ - AccountCloseParams as RustAccountCloseParams, PaymentParams as RustPaymentParams, -}; - -use super::common::{FfiTransactionSignerFromRust, TransactionSigner}; - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct PaymentParams { - /// The address of the account receiving the ALGO payment. - pub receiver: String, - - /// The amount of microALGO to send. - /// - /// Specified in microALGO (1 ALGO = 1,000,000 microALGO). - pub amount: u64, - } -} - -impl TryFrom for RustPaymentParams { - type Error = UtilsError; - - fn try_from(params: PaymentParams) -> Result { - Ok(RustPaymentParams { - sender: params.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::RustTransactionSignerFromFfi { ffi_signer: s }) - as std::sync::Arc - }), - rekey_to: params - .rekey_to - .map(|r| r.parse()) - .transpose() - .map_err(|e| UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - })?, - note: params.note, - lease: params.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - receiver: params - .receiver - .parse() - .map_err(|_| UtilsError::UtilsError { - message: "Invalid receiver address".to_string(), - })?, - amount: params.amount, - }) - } -} - -impl From for PaymentParams { - fn from(params: RustPaymentParams) -> Self { - PaymentParams { - sender: params.sender.to_string(), - signer: params.signer.map(|s| { - Arc::new(FfiTransactionSignerFromRust { rust_signer: s }) - as Arc - }), - - rekey_to: params.rekey_to.map(|r| r.to_string()), - note: params.note, - lease: params.lease.map(|l| l.to_vec()), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - receiver: params.receiver.to_string(), - amount: params.amount, - } - } -} - -create_transaction_params! { - #[derive(uniffi::Record)] - pub struct AccountCloseParams { - /// Close the sender account and send the remaining balance to this address - /// - /// *Warning:* Be careful this can lead to loss of funds if not used correctly. - pub close_remainder_to: String, - } -} - -impl TryFrom for RustAccountCloseParams { - type Error = UtilsError; - - fn try_from(params: AccountCloseParams) -> Result { - Ok(RustAccountCloseParams { - sender: params.sender.parse().map_err(|e| UtilsError::UtilsError { - message: format!("Invalid sender address: {}", e), - })?, - signer: params.signer.map(|s| { - std::sync::Arc::new(super::common::RustTransactionSignerFromFfi { ffi_signer: s }) - as std::sync::Arc - }), - rekey_to: params - .rekey_to - .map(|r| r.parse()) - .transpose() - .map_err(|e| UtilsError::UtilsError { - message: format!("Invalid rekey_to address: {}", e), - })?, - note: params.note, - lease: params.lease.map(|l| { - let mut lease_bytes = [0u8; 32]; - lease_bytes.copy_from_slice(&l[..32.min(l.len())]); - lease_bytes - }), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - close_remainder_to: params.close_remainder_to.parse().map_err(|_| { - UtilsError::UtilsError { - message: "Invalid close_remainder_to address".to_string(), - } - })?, - }) - } -} - -impl From for AccountCloseParams { - fn from(params: RustAccountCloseParams) -> Self { - AccountCloseParams { - sender: params.sender.to_string(), - signer: params.signer.map(|s| { - Arc::new(FfiTransactionSignerFromRust { rust_signer: s }) - as Arc - }), - rekey_to: params.rekey_to.map(|r| r.to_string()), - note: params.note, - lease: params.lease.map(|l| l.to_vec()), - static_fee: params.static_fee, - extra_fee: params.extra_fee, - max_fee: params.max_fee, - validity_window: params.validity_window, - first_valid_round: params.first_valid_round, - last_valid_round: params.last_valid_round, - close_remainder_to: params.close_remainder_to.to_string(), - } - } -} diff --git a/crates/algokit_utils_ffi/uniffi.toml b/crates/algokit_utils_ffi/uniffi.toml deleted file mode 100644 index d3c3c1cda..000000000 --- a/crates/algokit_utils_ffi/uniffi.toml +++ /dev/null @@ -1,5 +0,0 @@ -[bindings.python.external_packages] -algokit_transact_ffi = "algokit_transact" - -[bindings.swift] -module_name = "algokit_utils" diff --git a/packages/python/algokit_utils/algokit_utils.code-workspace b/packages/python/algokit_utils/algokit_utils.code-workspace deleted file mode 100644 index 879bda912..000000000 --- a/packages/python/algokit_utils/algokit_utils.code-workspace +++ /dev/null @@ -1,44 +0,0 @@ -{ - "folders": [ - { - "path": "." - } - ], - "settings": { - "python.defaultInterpreterPath": "./.venv/bin/python", - "python.languageServer": "Pylance" - }, - "extensions": { - "recommendations": ["ms-python.python", "ms-python.pylance"] - }, - "launch": { - "version": "0.2.0", - "configurations": [ - { - "name": "Python Debugger: Current File", - "type": "debugpy", - "request": "launch", - "program": "${file}", - "console": "integratedTerminal" - }, - { - "name": "Python Debugger: Test with Input", - "type": "debugpy", - "request": "launch", - "module": "pytest", - "args": ["${input:testFile}", "-v"], - "console": "integratedTerminal", - "cwd": "${workspaceFolder}", - "justMyCode": false - } - ], - "inputs": [ - { - "id": "testFile", - "description": "Test file to run", - "type": "promptString", - "default": "tests/" - } - ] - } -} diff --git a/packages/python/algokit_utils/algokit_utils/__init__.py b/packages/python/algokit_utils/algokit_utils/__init__.py deleted file mode 100644 index d64ad56a4..000000000 --- a/packages/python/algokit_utils/algokit_utils/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -AlgoKit Utils Library Python Bindings -""" - - -# Import all symbols from the Rust extension module and re-export them -from codecs import ignore_errors -from typing import override -from .algokit_utils_ffi import * -from . import algokit_transact_ffi as transact - diff --git a/packages/python/algokit_utils/algokit_utils/ffi_algod_client.py b/packages/python/algokit_utils/algokit_utils/ffi_algod_client.py deleted file mode 100644 index d34a983ac..000000000 --- a/packages/python/algokit_utils/algokit_utils/ffi_algod_client.py +++ /dev/null @@ -1,139 +0,0 @@ -""" -Python implementation of AlgodClientTrait foreign trait. - -This enables Python to provide async algod operations to Rust test code, -with Python controlling the async context while Rust orchestrates test logic. -""" - -import asyncio -import base64 -import json -from typing import List -from algokit_utils.algokit_http_client import HttpClient, HttpMethod -from algokit_utils.algokit_utils_ffi import ( - AlgodClientTrait, - AccountInfo, - TransactionInfo, - SuggestedParams, - UtilsError, -) - - -class PythonAlgodClient(AlgodClientTrait): # type: ignore - """Python implementation of async AlgodClient trait using HttpClient""" - - def __init__(self, http_client: HttpClient): - self.http_client = http_client - - async def send_transaction(self, txn: List[int]) -> str: # type: ignore - """Send transaction bytes and return transaction ID""" - txn_bytes = bytes(txn) # Convert from List[int] to bytes - - try: - response = await self.http_client.request( - method=HttpMethod.POST, - path="v2/transactions", - query=None, - body=txn_bytes, - headers={"Content-Type": "application/x-binary"}, - ) - - result = json.loads(response.body.decode("utf-8")) - return result["txId"] - except Exception as e: - raise UtilsError.UtilsError(f"Failed to send transaction: {e}") - - async def get_account_info(self, address: str) -> AccountInfo: # type: ignore - """Get account information from algod""" - try: - response = await self.http_client.request( - method=HttpMethod.GET, - path=f"v2/accounts/{address}", - query=None, - body=None, - headers=None, - ) - - data = json.loads(response.body.decode("utf-8")) - return AccountInfo( - balance=data.get("amount", 0), - min_balance=data.get("min-balance", 0), - created_apps=[app["id"] for app in data.get("created-apps", [])], - created_assets=[ - asset["index"] for asset in data.get("created-assets", []) - ], - ) - except Exception as e: - raise UtilsError.UtilsError(f"Failed to get account info: {e}") - - async def get_transaction_info(self, tx_id: str) -> TransactionInfo: # type: ignore - """Get transaction information by ID""" - try: - # Try pending transactions first - response = await self.http_client.request( - method=HttpMethod.GET, - path=f"v2/transactions/pending/{tx_id}", - query=None, - body=None, - headers=None, - ) - - data = json.loads(response.body.decode("utf-8")) - except Exception: - # If not found in pending, try confirmed transactions - try: - response = await self.http_client.request( - method=HttpMethod.GET, - path=f"v2/transactions/{tx_id}", - query=None, - body=None, - headers=None, - ) - - data = json.loads(response.body.decode("utf-8")) - except Exception as e: - raise UtilsError.UtilsError(f"Transaction not found: {e}") - - return TransactionInfo( - tx_id=tx_id, - confirmed_round=data.get("confirmed-round"), - asset_id=data.get("asset-index"), - app_id=data.get("application-index"), - ) - - async def wait_for_confirmation(self, tx_id: str) -> TransactionInfo: # type: ignore - """Wait for transaction confirmation""" - for attempt in range(10): # Wait up to 10 rounds - try: - info = await self.get_transaction_info(tx_id) - if info.confirmed_round: - return info - except: - pass - await asyncio.sleep(1) - - raise UtilsError.UtilsError( - f"Transaction {tx_id} not confirmed after 10 rounds" - ) - - async def get_suggested_params(self) -> SuggestedParams: # type: ignore - """Get suggested transaction parameters""" - try: - response = await self.http_client.request( - method=HttpMethod.GET, - path="v2/transactions/params", - query=None, - body=None, - headers=None, - ) - - data = json.loads(response.body.decode("utf-8")) - return SuggestedParams( - fee=data.get("fee", 1000), - first_valid_round=data["last-round"], - last_valid_round=data["last-round"] + 1000, - genesis_hash=list(base64.b64decode(data["genesis-hash"])), - genesis_id=data["genesis-id"], - ) - except Exception as e: - raise UtilsError.UtilsError(f"Failed to get suggested params: {e}") \ No newline at end of file diff --git a/packages/python/algokit_utils/algokit_utils/ffi_composer.py b/packages/python/algokit_utils/algokit_utils/ffi_composer.py deleted file mode 100644 index 2670b83db..000000000 --- a/packages/python/algokit_utils/algokit_utils/ffi_composer.py +++ /dev/null @@ -1,129 +0,0 @@ -""" -Python implementation of ComposerTrait and ComposerFactory foreign traits. - -The PythonComposer wraps the concrete Rust Composer (FFI object) to provide -async trait compatibility for the foreign trait testing pattern. - -STATEFUL DESIGN: PythonComposer stores algod_client and signer_getter internally, -eliminating the need to pass them on every method call. -""" - -from typing import List - - -class PythonComposer: - """Python implementation wrapping the concrete Rust Composer for async trait compatibility""" - - def __init__(self, algod_client, signer_getter): - """ - Args: - algod_client: The concrete AlgodClient FFI object - signer_getter: The TransactionSignerGetter implementation - """ - # Import here to avoid circular dependency - from algokit_utils.algokit_utils_ffi import Composer - - # Store dependencies for stateful API - self.algod_client = algod_client - self.signer_getter = signer_getter - - # Create the concrete Rust Composer with dependencies - self._composer = Composer(algod_client, signer_getter) - - async def build(self) -> None: - """Build the composed transactions""" - await self._composer.build() - - async def send(self) -> List[str]: - """Send the composed transactions""" - result = await self._composer.send() - return result.transaction_ids - - async def add_payment(self, params) -> None: - """Add payment transaction to composition""" - self._composer.add_payment(params) - - async def add_asset_create(self, params) -> None: - """Add asset create transaction to composition""" - self._composer.add_asset_create(params) - - async def add_asset_reconfigure(self, params) -> None: - """Add asset reconfigure transaction to composition""" - self._composer.add_asset_reconfigure(params) - - async def add_asset_destroy(self, params) -> None: - """Add asset destroy transaction to composition""" - self._composer.add_asset_destroy(params) - - async def add_asset_freeze(self, params) -> None: - """Add asset freeze transaction to composition""" - self._composer.add_asset_freeze(params) - - async def add_asset_unfreeze(self, params) -> None: - """Add asset unfreeze transaction to composition""" - self._composer.add_asset_unfreeze(params) - - async def add_asset_transfer(self, params) -> None: - """Add asset transfer transaction to composition""" - self._composer.add_asset_transfer(params) - - async def add_asset_opt_in(self, params) -> None: - """Add asset opt-in transaction to composition""" - self._composer.add_asset_opt_in(params) - - async def add_asset_opt_out(self, params) -> None: - """Add asset opt-out transaction to composition""" - self._composer.add_asset_opt_out(params) - - async def add_asset_clawback(self, params) -> None: - """Add asset clawback transaction to composition""" - self._composer.add_asset_clawback(params) - - async def add_app_create(self, params) -> None: - """Add app create transaction to composition""" - self._composer.add_app_create(params) - - async def add_app_call(self, params) -> None: - """Add app call transaction to composition""" - self._composer.add_app_call(params) - - async def add_app_update(self, params) -> None: - """Add app update transaction to composition""" - self._composer.add_app_update(params) - - async def add_app_delete(self, params) -> None: - """Add app delete transaction to composition""" - self._composer.add_app_delete(params) - - async def add_app_call_method_call(self, params) -> None: - """Add app call method call transaction to composition""" - self._composer.add_app_call_method_call(params) - - async def add_app_create_method_call(self, params) -> None: - """Add app create method call transaction to composition""" - self._composer.add_app_create_method_call(params) - - async def add_app_update_method_call(self, params) -> None: - """Add app update method call transaction to composition""" - self._composer.add_app_update_method_call(params) - - async def add_app_delete_method_call(self, params) -> None: - """Add app delete method call transaction to composition""" - self._composer.add_app_delete_method_call(params) - - -class PythonComposerFactory: - """Python implementation of ComposerFactory that creates fresh composer instances""" - - def __init__(self, algod_client, signer_getter): - """ - Args: - algod_client: The concrete AlgodClient FFI object - signer_getter: The TransactionSignerGetter implementation - """ - self.algod_client = algod_client - self.signer_getter = signer_getter - - def create_composer(self) -> PythonComposer: - """Create a fresh composer instance with stored dependencies""" - return PythonComposer(self.algod_client, self.signer_getter) diff --git a/packages/python/algokit_utils/algokit_utils/py.typed b/packages/python/algokit_utils/algokit_utils/py.typed deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/python/algokit_utils/poetry.lock b/packages/python/algokit_utils/poetry.lock deleted file mode 100644 index 2da8afae0..000000000 --- a/packages/python/algokit_utils/poetry.lock +++ /dev/null @@ -1,712 +0,0 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. - -[[package]] -name = "algokit-transact" -version = "1.0.0a67" -description = "" -optional = false -python-versions = ">=3.9,<4.0" -groups = ["main"] -files = [] -develop = false - -[package.source] -type = "directory" -url = "../algokit_transact" - -[[package]] -name = "algosdk" -version = "2.7.0" -description = "Algorand SDK in Python" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "algosdk-2.7.0-py3-none-any.whl", hash = "sha256:be201e6a389ca77f0e40e0273708a8affe0793d2fa82b56db04e3c574b19bd21"}, - {file = "algosdk-2.7.0.tar.gz", hash = "sha256:9b1926d06cafbe698b77bc3a02b3adbcae398195451c50fbab4c9ccec82e2115"}, -] - -[package.dependencies] -msgpack = ">=1.0.0,<2" -pycryptodomex = ">=3.6.0,<4" -pynacl = ">=1.4.0,<2" - -[[package]] -name = "asyncio" -version = "3.4.3" -description = "reference implementation of PEP 3156" -optional = false -python-versions = "*" -groups = ["test"] -files = [ - {file = "asyncio-3.4.3-cp33-none-win32.whl", hash = "sha256:b62c9157d36187eca799c378e572c969f0da87cd5fc42ca372d92cdb06e7e1de"}, - {file = "asyncio-3.4.3-cp33-none-win_amd64.whl", hash = "sha256:c46a87b48213d7464f22d9a497b9eef8c1928b68320a2fa94240f969f6fec08c"}, - {file = "asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d"}, - {file = "asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41"}, -] - -[[package]] -name = "auditwheel" -version = "6.4.2" -description = "Cross-distribution Linux wheels" -optional = false -python-versions = ">=3.9" -groups = ["build"] -files = [ - {file = "auditwheel-6.4.2-py3-none-any.whl", hash = "sha256:4302ae79dcff242e799a37173cfeeae727d0924843eca4b3f622d3bcb28de2db"}, - {file = "auditwheel-6.4.2.tar.gz", hash = "sha256:b7a61afc9183b6b5c661de59ca586f9c7200445a409c58cdf2049d6f71636d51"}, -] - -[package.dependencies] -packaging = ">=20.9" -pyelftools = ">=0.24" - -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." -optional = false -python-versions = "<3.11,>=3.8" -groups = ["test"] -markers = "python_version < \"3.11\"" -files = [ - {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, - {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, -] - -[[package]] -name = "certifi" -version = "2025.8.3" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.7" -groups = ["dev", "test"] -files = [ - {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, - {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, -] - -[[package]] -name = "cffi" -version = "1.17.1" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -groups = ["dev", "test"] -files = [ - {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, - {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, - {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, - {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, - {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, - {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, - {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, - {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, - {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, - {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, - {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, - {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, - {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, - {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, - {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, - {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, -] - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "charset-normalizer" -version = "3.4.3" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -groups = ["dev", "test"] -files = [ - {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"}, - {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, - {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev", "test"] -markers = "sys_platform == \"win32\"" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.0" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["dev", "test"] -markers = "python_version < \"3.11\"" -files = [ - {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, - {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -groups = ["dev", "test"] -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "iniconfig" -version = "2.1.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.8" -groups = ["dev", "test"] -files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, -] - -[[package]] -name = "msgpack" -version = "1.1.1" -description = "MessagePack serializer" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "msgpack-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:353b6fc0c36fde68b661a12949d7d49f8f51ff5fa019c1e47c87c4ff34b080ed"}, - {file = "msgpack-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:79c408fcf76a958491b4e3b103d1c417044544b68e96d06432a189b43d1215c8"}, - {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78426096939c2c7482bf31ef15ca219a9e24460289c00dd0b94411040bb73ad2"}, - {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b17ba27727a36cb73aabacaa44b13090feb88a01d012c0f4be70c00f75048b4"}, - {file = "msgpack-1.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a17ac1ea6ec3c7687d70201cfda3b1e8061466f28f686c24f627cae4ea8efd0"}, - {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88d1e966c9235c1d4e2afac21ca83933ba59537e2e2727a999bf3f515ca2af26"}, - {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f6d58656842e1b2ddbe07f43f56b10a60f2ba5826164910968f5933e5178af75"}, - {file = "msgpack-1.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96decdfc4adcbc087f5ea7ebdcfd3dee9a13358cae6e81d54be962efc38f6338"}, - {file = "msgpack-1.1.1-cp310-cp310-win32.whl", hash = "sha256:6640fd979ca9a212e4bcdf6eb74051ade2c690b862b679bfcb60ae46e6dc4bfd"}, - {file = "msgpack-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:8b65b53204fe1bd037c40c4148d00ef918eb2108d24c9aaa20bc31f9810ce0a8"}, - {file = "msgpack-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:71ef05c1726884e44f8b1d1773604ab5d4d17729d8491403a705e649116c9558"}, - {file = "msgpack-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:36043272c6aede309d29d56851f8841ba907a1a3d04435e43e8a19928e243c1d"}, - {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a32747b1b39c3ac27d0670122b57e6e57f28eefb725e0b625618d1b59bf9d1e0"}, - {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a8b10fdb84a43e50d38057b06901ec9da52baac6983d3f709d8507f3889d43f"}, - {file = "msgpack-1.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0c325c3f485dc54ec298d8b024e134acf07c10d494ffa24373bea729acf704"}, - {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:88daaf7d146e48ec71212ce21109b66e06a98e5e44dca47d853cbfe171d6c8d2"}, - {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8b55ea20dc59b181d3f47103f113e6f28a5e1c89fd5b67b9140edb442ab67f2"}, - {file = "msgpack-1.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a28e8072ae9779f20427af07f53bbb8b4aa81151054e882aee333b158da8752"}, - {file = "msgpack-1.1.1-cp311-cp311-win32.whl", hash = "sha256:7da8831f9a0fdb526621ba09a281fadc58ea12701bc709e7b8cbc362feabc295"}, - {file = "msgpack-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fd1b58e1431008a57247d6e7cc4faa41c3607e8e7d4aaf81f7c29ea013cb458"}, - {file = "msgpack-1.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae497b11f4c21558d95de9f64fff7053544f4d1a17731c866143ed6bb4591238"}, - {file = "msgpack-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:33be9ab121df9b6b461ff91baac6f2731f83d9b27ed948c5b9d1978ae28bf157"}, - {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f64ae8fe7ffba251fecb8408540c34ee9df1c26674c50c4544d72dbf792e5ce"}, - {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a494554874691720ba5891c9b0b39474ba43ffb1aaf32a5dac874effb1619e1a"}, - {file = "msgpack-1.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb643284ab0ed26f6957d969fe0dd8bb17beb567beb8998140b5e38a90974f6c"}, - {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d275a9e3c81b1093c060c3837e580c37f47c51eca031f7b5fb76f7b8470f5f9b"}, - {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fd6b577e4541676e0cc9ddc1709d25014d3ad9a66caa19962c4f5de30fc09ef"}, - {file = "msgpack-1.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb29aaa613c0a1c40d1af111abf025f1732cab333f96f285d6a93b934738a68a"}, - {file = "msgpack-1.1.1-cp312-cp312-win32.whl", hash = "sha256:870b9a626280c86cff9c576ec0d9cbcc54a1e5ebda9cd26dab12baf41fee218c"}, - {file = "msgpack-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:5692095123007180dca3e788bb4c399cc26626da51629a31d40207cb262e67f4"}, - {file = "msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0"}, - {file = "msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9"}, - {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8"}, - {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a"}, - {file = "msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac"}, - {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b"}, - {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7"}, - {file = "msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5"}, - {file = "msgpack-1.1.1-cp313-cp313-win32.whl", hash = "sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323"}, - {file = "msgpack-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69"}, - {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bba1be28247e68994355e028dcd668316db30c1f758d3241a7b903ac78dcd285"}, - {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8f93dcddb243159c9e4109c9750ba5b335ab8d48d9522c5308cd05d7e3ce600"}, - {file = "msgpack-1.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fbbc0b906a24038c9958a1ba7ae0918ad35b06cb449d398b76a7d08470b0ed9"}, - {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:61e35a55a546a1690d9d09effaa436c25ae6130573b6ee9829c37ef0f18d5e78"}, - {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:1abfc6e949b352dadf4bce0eb78023212ec5ac42f6abfd469ce91d783c149c2a"}, - {file = "msgpack-1.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:996f2609ddf0142daba4cefd767d6db26958aac8439ee41db9cc0db9f4c4c3a6"}, - {file = "msgpack-1.1.1-cp38-cp38-win32.whl", hash = "sha256:4d3237b224b930d58e9d83c81c0dba7aacc20fcc2f89c1e5423aa0529a4cd142"}, - {file = "msgpack-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:da8f41e602574ece93dbbda1fab24650d6bf2a24089f9e9dbb4f5730ec1e58ad"}, - {file = "msgpack-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5be6b6bc52fad84d010cb45433720327ce886009d862f46b26d4d154001994b"}, - {file = "msgpack-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a89cd8c087ea67e64844287ea52888239cbd2940884eafd2dcd25754fb72232"}, - {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d75f3807a9900a7d575d8d6674a3a47e9f227e8716256f35bc6f03fc597ffbf"}, - {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d182dac0221eb8faef2e6f44701812b467c02674a322c739355c39e94730cdbf"}, - {file = "msgpack-1.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b13fe0fb4aac1aa5320cd693b297fe6fdef0e7bea5518cbc2dd5299f873ae90"}, - {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:435807eeb1bc791ceb3247d13c79868deb22184e1fc4224808750f0d7d1affc1"}, - {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4835d17af722609a45e16037bb1d4d78b7bdf19d6c0128116d178956618c4e88"}, - {file = "msgpack-1.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8ef6e342c137888ebbfb233e02b8fbd689bb5b5fcc59b34711ac47ebd504478"}, - {file = "msgpack-1.1.1-cp39-cp39-win32.whl", hash = "sha256:61abccf9de335d9efd149e2fff97ed5974f2481b3353772e8e2dd3402ba2bd57"}, - {file = "msgpack-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:40eae974c873b2992fd36424a5d9407f93e97656d999f43fca9d29f820899084"}, - {file = "msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd"}, -] - -[[package]] -name = "packaging" -version = "25.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["build", "dev", "test"] -files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, -] - -[[package]] -name = "patchelf" -version = "0.17.2.4" -description = "A small utility to modify the dynamic linker and RPATH of ELF executables." -optional = false -python-versions = ">=3.7" -groups = ["build"] -markers = "sys_platform == \"linux\"" -files = [ - {file = "patchelf-0.17.2.4-py3-none-macosx_10_9_universal2.whl", hash = "sha256:343bb1b94e959f9070ca9607453b04390e36bbaa33c88640b989cefad0aa049e"}, - {file = "patchelf-0.17.2.4-py3-none-manylinux1_i686.manylinux_2_5_i686.musllinux_1_1_i686.whl", hash = "sha256:09fd848d625a165fc7b7e07745508c24077129b019c4415a882938781d43adf8"}, - {file = "patchelf-0.17.2.4-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:d9b35ebfada70c02679ad036407d9724ffe1255122ba4ac5e4be5868618a5689"}, - {file = "patchelf-0.17.2.4-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2931a1b5b85f3549661898af7bf746afbda7903c7c9a967cfc998a3563f84fad"}, - {file = "patchelf-0.17.2.4-py3-none-manylinux2014_armv7l.manylinux_2_17_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:ae44cb3c857d50f54b99e5697aa978726ada33a8a6129d4b8b7ffd28b996652d"}, - {file = "patchelf-0.17.2.4-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:680a266a70f60a7a4f4c448482c5bdba80cc8e6bb155a49dcc24238ba49927b0"}, - {file = "patchelf-0.17.2.4-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.musllinux_1_1_s390x.whl", hash = "sha256:d842b51f0401460f3b1f3a3a67d2c266a8f515a5adfbfa6e7b656cb3ac2ed8bc"}, - {file = "patchelf-0.17.2.4-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:7076d9e127230982e20a81a6e2358d3343004667ba510d9f822d4fdee29b0d71"}, - {file = "patchelf-0.17.2.4.tar.gz", hash = "sha256:970ee5cd8af33e5ea2099510b2f9013fa1b8d5cd763bf3fd3961281c18101a09"}, -] - -[package.extras] -test = ["importlib_metadata (>=2.0)", "pytest (>=6.0)"] - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.9" -groups = ["dev", "test"] -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "pycparser" -version = "2.22" -description = "C parser in Python" -optional = false -python-versions = ">=3.8" -groups = ["dev", "test"] -files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] - -[[package]] -name = "pycryptodomex" -version = "3.23.0" -description = "Cryptographic library for Python" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -files = [ - {file = "pycryptodomex-3.23.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:add243d204e125f189819db65eed55e6b4713f70a7e9576c043178656529cec7"}, - {file = "pycryptodomex-3.23.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1c6d919fc8429e5cb228ba8c0d4d03d202a560b421c14867a65f6042990adc8e"}, - {file = "pycryptodomex-3.23.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:1c3a65ad441746b250d781910d26b7ed0a396733c6f2dbc3327bd7051ec8a541"}, - {file = "pycryptodomex-3.23.0-cp27-cp27m-win32.whl", hash = "sha256:47f6d318fe864d02d5e59a20a18834819596c4ed1d3c917801b22b92b3ffa648"}, - {file = "pycryptodomex-3.23.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:d9825410197a97685d6a1fa2a86196430b01877d64458a20e95d4fd00d739a08"}, - {file = "pycryptodomex-3.23.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:267a3038f87a8565bd834317dbf053a02055915acf353bf42ededb9edaf72010"}, - {file = "pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7b37e08e3871efe2187bc1fd9320cc81d87caf19816c648f24443483005ff886"}, - {file = "pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:91979028227543010d7b2ba2471cf1d1e398b3f183cb105ac584df0c36dac28d"}, - {file = "pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8962204c47464d5c1c4038abeadd4514a133b28748bcd9fa5b6d62e3cec6fa"}, - {file = "pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a33986a0066860f7fcf7c7bd2bc804fa90e434183645595ae7b33d01f3c91ed8"}, - {file = "pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7947ab8d589e3178da3d7cdeabe14f841b391e17046954f2fbcd941705762b5"}, - {file = "pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c25e30a20e1b426e1f0fa00131c516f16e474204eee1139d1603e132acffc314"}, - {file = "pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:da4fa650cef02db88c2b98acc5434461e027dce0ae8c22dd5a69013eaf510006"}, - {file = "pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58b851b9effd0d072d4ca2e4542bf2a4abcf13c82a29fd2c93ce27ee2a2e9462"}, - {file = "pycryptodomex-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:a9d446e844f08299236780f2efa9898c818fe7e02f17263866b8550c7d5fb328"}, - {file = "pycryptodomex-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bc65bdd9fc8de7a35a74cab1c898cab391a4add33a8fe740bda00f5976ca4708"}, - {file = "pycryptodomex-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c885da45e70139464f082018ac527fdaad26f1657a99ee13eecdce0f0ca24ab4"}, - {file = "pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:06698f957fe1ab229a99ba2defeeae1c09af185baa909a31a5d1f9d42b1aaed6"}, - {file = "pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2c2537863eccef2d41061e82a881dcabb04944c5c06c5aa7110b577cc487545"}, - {file = "pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587"}, - {file = "pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f489c4765093fb60e2edafdf223397bc716491b2b69fe74367b70d6999257a5c"}, - {file = "pycryptodomex-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdc69d0d3d989a1029df0eed67cc5e8e5d968f3724f4519bd03e0ec68df7543c"}, - {file = "pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bbcb1dd0f646484939e142462d9e532482bc74475cecf9c4903d4e1cd21f003"}, - {file = "pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:8a4fcd42ccb04c31268d1efeecfccfd1249612b4de6374205376b8f280321744"}, - {file = "pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:55ccbe27f049743a4caf4f4221b166560d3438d0b1e5ab929e07ae1702a4d6fd"}, - {file = "pycryptodomex-3.23.0-cp37-abi3-win32.whl", hash = "sha256:189afbc87f0b9f158386bf051f720e20fa6145975f1e76369303d0f31d1a8d7c"}, - {file = "pycryptodomex-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:52e5ca58c3a0b0bd5e100a9fbc8015059b05cffc6c66ce9d98b4b45e023443b9"}, - {file = "pycryptodomex-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51"}, - {file = "pycryptodomex-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:febec69c0291efd056c65691b6d9a339f8b4bc43c6635b8699471248fe897fea"}, - {file = "pycryptodomex-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:c84b239a1f4ec62e9c789aafe0543f0594f0acd90c8d9e15bcece3efe55eca66"}, - {file = "pycryptodomex-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ebfff755c360d674306e5891c564a274a47953562b42fb74a5c25b8fc1fb1cb5"}, - {file = "pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eca54f4bb349d45afc17e3011ed4264ef1cc9e266699874cdd1349c504e64798"}, - {file = "pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2596e643d4365e14d0879dc5aafe6355616c61c2176009270f3048f6d9a61f"}, - {file = "pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdfac7cda115bca3a5abb2f9e43bc2fb66c2b65ab074913643803ca7083a79ea"}, - {file = "pycryptodomex-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:14c37aaece158d0ace436f76a7bb19093db3b4deade9797abfc39ec6cd6cc2fe"}, - {file = "pycryptodomex-3.23.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7de1e40a41a5d7f1ac42b6569b10bcdded34339950945948529067d8426d2785"}, - {file = "pycryptodomex-3.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bffc92138d75664b6d543984db7893a628559b9e78658563b0395e2a5fb47ed9"}, - {file = "pycryptodomex-3.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df027262368334552db2c0ce39706b3fb32022d1dce34673d0f9422df004b96a"}, - {file = "pycryptodomex-3.23.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e79f1aaff5a3a374e92eb462fa9e598585452135012e2945f96874ca6eeb1ff"}, - {file = "pycryptodomex-3.23.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:27e13c80ac9a0a1d050ef0a7e0a18cc04c8850101ec891815b6c5a0375e8a245"}, - {file = "pycryptodomex-3.23.0.tar.gz", hash = "sha256:71909758f010c82bc99b0abf4ea12012c98962fbf0583c2164f8b84533c2e4da"}, -] - -[[package]] -name = "pyelftools" -version = "0.32" -description = "Library for analyzing ELF files and DWARF debugging information" -optional = false -python-versions = "*" -groups = ["build"] -files = [ - {file = "pyelftools-0.32-py3-none-any.whl", hash = "sha256:013df952a006db5e138b1edf6d8a68ecc50630adbd0d83a2d41e7f846163d738"}, - {file = "pyelftools-0.32.tar.gz", hash = "sha256:6de90ee7b8263e740c8715a925382d4099b354f29ac48ea40d840cf7aa14ace5"}, -] - -[[package]] -name = "pygments" -version = "2.19.2" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -groups = ["dev", "test"] -files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pynacl" -version = "1.5.0" -description = "Python binding to the Networking and Cryptography (NaCl) library" -optional = false -python-versions = ">=3.6" -groups = ["dev", "test"] -files = [ - {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, - {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, -] - -[package.dependencies] -cffi = ">=1.4.1" - -[package.extras] -docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] -tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] - -[[package]] -name = "pytest" -version = "8.4.1" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.9" -groups = ["dev", "test"] -files = [ - {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, - {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, -] - -[package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} -iniconfig = ">=1" -packaging = ">=20" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "1.1.0" -description = "Pytest support for asyncio" -optional = false -python-versions = ">=3.9" -groups = ["test"] -files = [ - {file = "pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf"}, - {file = "pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea"}, -] - -[package.dependencies] -backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} -pytest = ">=8.2,<9" - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] - -[[package]] -name = "requests" -version = "2.32.5" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.9" -groups = ["dev", "test"] -files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "tomli" -version = "2.2.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev", "test"] -markers = "python_version < \"3.11\"" -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] - -[[package]] -name = "types-requests" -version = "2.32.4.20250809" -description = "Typing stubs for requests" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "types_requests-2.32.4.20250809-py3-none-any.whl", hash = "sha256:f73d1832fb519ece02c85b1f09d5f0dd3108938e7d47e7f94bbfa18a6782b163"}, - {file = "types_requests-2.32.4.20250809.tar.gz", hash = "sha256:d8060de1c8ee599311f56ff58010fb4902f462a1470802cf9f6ed27bc46c4df3"}, -] - -[package.dependencies] -urllib3 = ">=2" - -[[package]] -name = "typing-extensions" -version = "4.14.1" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -groups = ["dev", "test"] -markers = "python_version < \"3.11\"" -files = [ - {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, - {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, -] - -[[package]] -name = "urllib3" -version = "2.5.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -groups = ["dev", "test"] -files = [ - {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, - {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[metadata] -lock-version = "2.1" -python-versions = ">=3.10,<4.0" -content-hash = "4db5cf818e68b07540c3689f79495107956c698687e9b5efb32d85f8c585e6c8" diff --git a/packages/python/algokit_utils/poetry.toml b/packages/python/algokit_utils/poetry.toml deleted file mode 100644 index ab1033bd3..000000000 --- a/packages/python/algokit_utils/poetry.toml +++ /dev/null @@ -1,2 +0,0 @@ -[virtualenvs] -in-project = true diff --git a/packages/python/algokit_utils/pyproject.toml b/packages/python/algokit_utils/pyproject.toml deleted file mode 100644 index b7c0b3643..000000000 --- a/packages/python/algokit_utils/pyproject.toml +++ /dev/null @@ -1,43 +0,0 @@ -[project] -requires-python = ">=3.10,<4.0" -name = "algokit_utils" -version = "0.1.0" - -[build-system] -requires = ["poetry-core>=1.0.0", "setuptools", "wheel"] -build-backend = "poetry.core.masonry.api" - -[tool.poetry] -name = "algokit_utils" -version = "0.1.0" -description = "" -packages = [ - { include = "algokit_utils" } -] -include = [ - { path = "algokit_utils/*.so", format = "wheel" }, - { path = "algokit_utils/*.dll", format = "wheel" }, - { path = "algokit_utils/*.dylib", format = "wheel" } -] - - -[tool.poetry.dependencies] -algokit-transact = {path = "../algokit_transact"} - -[tool.poetry.group.test.dependencies] -pynacl = ">=1.5.0,<2.0.0" -pytest = ">=7.4.4" -requests = "^2.32.3" -asyncio = "^3.4.3" -pytest-asyncio = "^1.0.0" - -[tool.poetry.group.build.dependencies] -auditwheel = "^6.4.0" -patchelf = { version = "^0.17.2.2", markers = "sys_platform == 'linux'" } - -[tool.poetry.group.dev.dependencies] -algosdk = "^2.7.0" -pytest = "^8.4.1" -requests = "^2.32.5" -types-requests = "^2.32.4.20250809" - diff --git a/packages/python/algokit_utils/tests/test_ffi_async.py b/packages/python/algokit_utils/tests/test_ffi_async.py deleted file mode 100644 index a1bd1901a..000000000 --- a/packages/python/algokit_utils/tests/test_ffi_async.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -Foreign trait testing for asset freeze operations. - -This test demonstrates the foreign trait testing pattern where: -1. Python controls the async context (asyncio) -2. Rust orchestrates test logic -3. Python provides I/O implementations (AlgodClient, Composer, Signer) -""" - -import pytest -from algokit_utils.ffi_algod_client import PythonAlgodClient -from algokit_utils.ffi_composer import PythonComposerFactory -from tests.test_utils import HttpClientImpl, MultiAccountSignerGetter -from algokit_utils.algokit_utils_ffi import AlgodClient - - -@pytest.mark.asyncio -async def test_asset_freeze_comprehensive(): - """Test the full async FFI pipeline for asset freeze operations""" - - # Create Python implementations of async traits using existing HttpClient - http_client = HttpClientImpl() - algod_client = PythonAlgodClient(http_client) - - # Create the concrete FFI components - ffi_algod = AlgodClient(http_client) - ffi_signer_getter = MultiAccountSignerGetter() - - # Create composer factory - composer_factory = PythonComposerFactory(ffi_algod, ffi_signer_getter) - - # Run the async Rust test suite - dispenser mnemonic is now fetched internally - try: - from algokit_utils.algokit_utils_ffi import run_asset_freeze_test_suite - - # Run the async Rust test suite - # Rust will fetch dispenser mnemonic from localnet internally - result = await run_asset_freeze_test_suite( - algod_client, # PythonAlgodClient (foreign trait impl) - composer_factory, # PythonComposerFactory (foreign trait impl) - ffi_signer_getter # MultiAccountSignerGetter (foreign trait impl) - ) - - # Assert all tests passed - assert result.all_passed, f"Test suite failed: {result.name}" - - # Print detailed results - print(f"\n{'='*60}") - print(f"Test Suite: {result.name}") - print(f"Total Duration: {result.total_duration_ms}ms") - print(f"{'='*60}") - - for test in result.results: - status = "✓" if test.passed else "✗" - print(f"{status} {test.name} ({test.duration_ms}ms)") - if not test.passed and test.error: - print(f" Error: {test.error}") - - print(f"{'='*60}") - print(f"Overall Result: {'PASS' if result.all_passed else 'FAIL'}") - - except ImportError: - pytest.skip("FFI bindings need to be regenerated to include run_asset_freeze_test_suite") - - -if __name__ == "__main__": - pass \ No newline at end of file diff --git a/packages/python/algokit_utils/tests/test_utils.py b/packages/python/algokit_utils/tests/test_utils.py deleted file mode 100644 index 1f6ff0528..000000000 --- a/packages/python/algokit_utils/tests/test_utils.py +++ /dev/null @@ -1,280 +0,0 @@ -from typing import override -import typing -from algokit_utils.algokit_http_client import HttpClient, HttpMethod, HttpResponse -from algokit_transact import ( - OnApplicationComplete, - SignedTransaction, - Transaction, - encode_transaction, -) -from algokit_utils import AlgodClient, TransactionSigner -from algokit_utils.algokit_utils_ffi import ( - AbiMethod, - AbiMethodArg, - AbiMethodArgType, - AppCallMethodCallParams, - AppCallParams, - AppCreateParams, - AppMethodCallArg, - Composer, - PaymentParams, - TransactionSignerGetter, - AbiValue, -) -from algosdk.mnemonic import to_private_key -from nacl.signing import SigningKey -import base64 -import pytest -import requests - -MN = "gas net tragic valid celery want good neglect maid nuclear core false chunk place asthma three acoustic moon box million finish bargain onion ability shallow" -SEED_B64: str = to_private_key(MN) # type: ignore -SEED_BYTES = base64.b64decode(SEED_B64) -KEY = SigningKey(SEED_BYTES[:32]) -ADDR = "ON6AOPBATSSEL47ML7EPXATHGH7INOWONHWITMQEDRPXHTMDJYMPQXROMA" - - -class TestSigner(TransactionSigner): - def __init__(self, private_key: bytes): - self.signing_key = SigningKey(private_key) - - @override - async def sign_transactions( # type: ignore - self, transactions: list[Transaction], indices: list[int] - ) -> list[SignedTransaction]: - stxns = [] - for transaction in transactions: - tx_for_signing = encode_transaction(transaction) - sig = self.signing_key.sign(tx_for_signing) - stxns.append( - SignedTransaction(transaction=transaction, signature=sig.signature) - ) - - return stxns - - @override - async def sign_transaction(self, transaction: Transaction) -> SignedTransaction: # type: ignore - return (await self.sign_transactions([transaction], [0]))[0] - - -class SignerGetter(TransactionSignerGetter): - @override - def get_signer(self, address: str) -> TransactionSigner: # type: ignore - return TestSigner(SEED_BYTES[:32]) - - @override - def register_account(self, address: str, mnemonic: str) -> None: # type: ignore - # No-op: backwards compatibility - pass - - -class MultiAccountSignerGetter(TransactionSignerGetter): - """TransactionSignerGetter implementation that manages multiple test accounts""" - - def __init__(self): - self.signers: dict[str, TestSigner] = {} - # Register default test account - self.register_account(ADDR, MN) - - @override - def get_signer(self, address: str) -> TransactionSigner: # type: ignore - if address in self.signers: - return self.signers[address] - - raise Exception(f"No signer registered for address: {address}") - - @override - def register_account(self, address: str, mnemonic_phrase: str) -> None: # type: ignore - """Register an account with its mnemonic for signing""" - # Convert mnemonic to private key - private_key = to_private_key(mnemonic_phrase) # type: ignore - private_key_bytes = base64.b64decode(private_key) - - # Create and store the signer - self.signers[address] = TestSigner(private_key_bytes[:32]) - - -class HttpClientImpl(HttpClient): - @override - async def request( # type: ignore - self, - method: HttpMethod, - path: str, - query: typing.Optional[dict[str, str]], - body: typing.Optional[bytes], - headers: typing.Optional[dict[str, str]], - ) -> HttpResponse: - headers = headers or {} - headers["X-Algo-API-Token"] = "a" * 64 - - if method == HttpMethod.GET: - res = requests.get( - f"http://localhost:4001/{path}", params=query, headers=headers - ) - elif method == HttpMethod.POST: - res = requests.post( - f"http://localhost:4001/{path}", - params=query, - data=body, - headers=headers, - ) - else: - raise NotImplementedError( - f"HTTP method {method} not implemented in test client" - ) - - if res.status_code != 200: - from algokit_utils.algokit_http_client import HttpError - - raise HttpError.RequestError( - f"HTTP request failed: {res.status_code} {res.text}" - ) - - # NOTE: Headers needing to be lowercase was a bit surprising, so we need to make sure we document that - headers = {k.lower(): v for k, v in res.headers.items()} - - return HttpResponse(body=res.content, headers=headers) - - -@pytest.mark.skip(reason="Will be refactored later. Keeping test for reference") -@pytest.mark.asyncio -async def test_composer(): - algod = AlgodClient(HttpClientImpl()) - - composer = Composer( - algod_client=algod, - signer_getter=MultiAccountSignerGetter(), - ) - - composer.add_payment( - params=PaymentParams( - amount=1, - receiver=ADDR, - sender=ADDR, - 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, - ) - ) - - await composer.build() - response = await composer.send() - assert len(response.transaction_ids) == 1 - assert len(response.transaction_ids[0]) == 52 - print(response.transaction_ids) - - -INT_1_PROG = bytes.fromhex("0b810143") - - -@pytest.mark.skip(reason="Will be refactored later. Keeping test for reference") -@pytest.mark.asyncio -async def test_app_create_and_call(): - algod = AlgodClient(HttpClientImpl()) - - create_composer = Composer( - algod_client=algod, - signer_getter=MultiAccountSignerGetter(), - ) - - create_composer.add_app_create( - params=AppCreateParams( - sender=ADDR, - on_complete=OnApplicationComplete.NO_OP, - approval_program=INT_1_PROG, - clear_state_program=INT_1_PROG, - 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, - ) - ) - - await create_composer.build() - response = await create_composer.send() - assert len(response.transaction_ids) == 1 - assert len(response.transaction_ids[0]) == 52 - - app_id = response.app_ids[0] - assert app_id - - call_composer = Composer( - algod_client=algod, - signer_getter=MultiAccountSignerGetter(), - ) - - call_composer.add_app_call( - params=AppCallParams( - sender=ADDR, - app_id=app_id, - on_complete=OnApplicationComplete.NO_OP, - 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, - ) - ) - - await call_composer.build() - response = await call_composer.send() - assert len(response.transaction_ids) == 1 - assert len(response.transaction_ids[0]) == 52 - - method_composer = Composer( - algod_client=algod, - signer_getter=MultiAccountSignerGetter(), - ) - - method_composer.add_app_call_method_call( - params=AppCallMethodCallParams( - sender=ADDR, - app_id=app_id, - args=[AppMethodCallArg.ABI_VALUE(AbiValue.bool(True))], - on_complete=OnApplicationComplete.NO_OP, - method=AbiMethod( - name="myMethod", - args=[ - AbiMethodArg( - arg_type=AbiMethodArgType.VALUE(bool_type), - name="a", - description="", - ) - ], - returns=None, - description="", - ), - 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, - ) - ) - - method_response = await method_composer.send() - assert len(method_response.transaction_ids) == 1 - assert len(method_response.transaction_ids[0]) == 52 diff --git a/tools/build_pkgs/src/main.rs b/tools/build_pkgs/src/main.rs index cd3f7ef06..a7dfd61aa 100644 --- a/tools/build_pkgs/src/main.rs +++ b/tools/build_pkgs/src/main.rs @@ -44,15 +44,12 @@ impl Language { enum Package { #[value(alias = "algokit_transact")] Transact, - #[value(alias = "algokit_utils")] - Utils, } impl Display for Package { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Package::Transact => f.write_str("algokit_transact"), - Package::Utils => f.write_str("algokit_utils"), } } } @@ -61,7 +58,6 @@ impl Package { fn crate_name(&self) -> String { match self { Self::Transact => "algokit_transact_ffi", - Self::Utils => "algokit_utils_ffi", } .to_string() }