From 1ad15076b52a11e5acc17ac4c2e492ff1c688551 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Mon, 3 Feb 2025 16:13:17 +0100 Subject: [PATCH 1/7] fix: ensure server+port is assembled when instantiating algosdk abstractions --- src/algokit_utils/clients/client_manager.py | 20 ++++++++++++++------ src/algokit_utils/models/network.py | 4 ++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/algokit_utils/clients/client_manager.py b/src/algokit_utils/clients/client_manager.py index 283ff6c1..c9581fc3 100644 --- a/src/algokit_utils/clients/client_manager.py +++ b/src/algokit_utils/clients/client_manager.py @@ -77,8 +77,8 @@ def _get_config_from_environment(environment_prefix: str) -> AlgoClientNetworkCo port = os.getenv(f"{environment_prefix}_PORT") if port: parsed = parse.urlparse(server) - server = parsed._replace(netloc=f"{parsed.hostname}:{port}").geturl() - return AlgoClientNetworkConfig(server, os.getenv(f"{environment_prefix}_TOKEN", "")) + server = parsed._replace(netloc=f"{parsed.hostname}").geturl() + return AlgoClientNetworkConfig(server, os.getenv(f"{environment_prefix}_TOKEN", ""), port=port) class ClientManager: @@ -359,7 +359,11 @@ def get_algod_client(config: AlgoClientNetworkConfig | None = None) -> AlgodClie """ config = config or _get_config_from_environment("ALGOD") headers = {"X-Algo-API-Token": config.token or ""} - return AlgodClient(algod_token=config.token or "", algod_address=config.server, headers=headers) + return AlgodClient( + algod_token=config.token or "", + algod_address=f'{config.server}{f':{config.port}' if config.port else ''}', + headers=headers, + ) @staticmethod def get_algod_client_from_environment() -> AlgodClient: @@ -377,7 +381,7 @@ def get_kmd_client(config: AlgoClientNetworkConfig | None = None) -> KMDClient: :return: KMD client instance """ config = config or _get_config_from_environment("KMD") - return KMDClient(config.token, config.server) + return KMDClient(config.token, f'{config.server}{f':{config.port}' if config.port else ''}') @staticmethod def get_kmd_client_from_environment() -> KMDClient: @@ -396,7 +400,11 @@ def get_indexer_client(config: AlgoClientNetworkConfig | None = None) -> Indexer """ config = config or _get_config_from_environment("INDEXER") headers = {"X-Indexer-API-Token": config.token} - return IndexerClient(indexer_token=config.token, indexer_address=config.server, headers=headers) + return IndexerClient( + indexer_token=config.token, + indexer_address=f'{config.server}{f":{config.port}" if config.port else ''}', + headers=headers, + ) @staticmethod def get_indexer_client_from_environment() -> IndexerClient: @@ -611,7 +619,7 @@ def get_default_localnet_config( else {"algod": 4001, "indexer": 8980, "kmd": 4002}[config_or_port] ) - return AlgoClientNetworkConfig(server=f"http://localhost:{port}", token="a" * 64) + return AlgoClientNetworkConfig(server="http://localhost", token="a" * 64, port=port) @staticmethod def get_algod_config_from_environment() -> AlgoClientNetworkConfig: diff --git a/src/algokit_utils/models/network.py b/src/algokit_utils/models/network.py index 6b4b6226..28c0b192 100644 --- a/src/algokit_utils/models/network.py +++ b/src/algokit_utils/models/network.py @@ -12,9 +12,9 @@ class AlgoClientNetworkConfig: {py:class}`algosdk.v2client.indexer.IndexerClient`""" server: str - """URL for the service e.g. `http://localhost:4001` or `https://testnet-api.algonode.cloud`""" + """URL for the service e.g. `http://localhost` or `https://testnet-api.algonode.cloud`""" token: str | None = None - """API Token to authenticate with the service""" + """API Token to authenticate with the service e.g '4001' or '8980'""" port: str | int | None = None From 2da92db128ddc6960d8d34e46f48fee843a86efe Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Mon, 3 Feb 2025 23:34:11 +0100 Subject: [PATCH 2/7] fix: algosdk configs respecting port field; Edge case for auto fee handling for readonly calls --- .../algokit_utils/models/network/index.md | 4 +- .../transaction_composer/index.md | 2 +- .../capabilities/transaction-composer.md | 117 ++++++++++++++++++ .../capabilities/transaction-composer.md | 117 ++++++++++++++++++ src/algokit_utils/clients/client_manager.py | 6 +- src/algokit_utils/models/network.py | 4 + .../transactions/transaction_composer.py | 31 +++-- tests/artifacts/inner-fee/application.json | 71 +++++++++-- tests/artifacts/inner-fee/contract.py | 11 +- tests/transactions/test_resource_packing.py | 36 ++++++ 10 files changed, 370 insertions(+), 29 deletions(-) diff --git a/docs/markdown/autoapi/algokit_utils/models/network/index.md b/docs/markdown/autoapi/algokit_utils/models/network/index.md index 4d94f90c..b49c51a8 100644 --- a/docs/markdown/autoapi/algokit_utils/models/network/index.md +++ b/docs/markdown/autoapi/algokit_utils/models/network/index.md @@ -15,11 +15,11 @@ Connection details for connecting to an {py:class}\`algosdk.v2client.algod.Algod #### server *: str* -URL for the service e.g. http://localhost:4001 or https://testnet-api.algonode.cloud +URL for the service e.g. http://localhost or https://testnet-api.algonode.cloud #### token *: str | None* *= None* -API Token to authenticate with the service +API Token to authenticate with the service e.g ‘4001’ or ‘8980’ #### port *: str | int | None* *= None* diff --git a/docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md b/docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md index 6000abcd..20d0483e 100644 --- a/docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md +++ b/docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md @@ -819,7 +819,7 @@ Simulate transaction group execution with configurable validation rules. * **Parameters:** * **allow_more_logs** – Whether to allow more logs than the standard limit * **allow_empty_signatures** – Whether to allow transactions with empty signatures - * **allow_unnamed_resources** – Whether to allow unnamed resources + * **allow_unnamed_resources** – Whether to allow unnamed resources. * **extra_opcode_budget** – Additional opcode budget to allocate * **exec_trace_config** – Configuration for execution tracing * **simulation_round** – Round number to simulate at diff --git a/docs/markdown/capabilities/transaction-composer.md b/docs/markdown/capabilities/transaction-composer.md index 330b757b..9c09519a 100644 --- a/docs/markdown/capabilities/transaction-composer.md +++ b/docs/markdown/capabilities/transaction-composer.md @@ -226,3 +226,120 @@ This feature is particularly useful when: - Developing applications where resource requirements may change dynamically Note: Resource population uses simulation under the hood to detect required resources, so it may add a small overhead to transaction preparation time. + +### Covering App Call Inner Transaction Fees + +`cover_app_call_inner_transaction_fees` automatically calculate the required fee for a parent app call transaction that sends inner transactions. It leverages the simulate endpoint to discover the inner transactions sent and calculates a fee delta to resolve the optimal fee. This feature also takes care of accounting for any surplus transaction fee at the various levels, so as to effectively minimise the fees needed to successfully handle complex scenarios. This setting only applies when you have constucted at least one app call transaction. + +For example: + +```python +myMethod = algosdk.ABIMethod.fromSignature('my_method()void') +result = algorand + .new_group() + .add_app_call_method_call(AppCallMethodCallParams( + sender: 'SENDER', + app_id=123, + method=myMethod, + args=[1, 2, 3], + max_fee=AlgoAmount.from_micro_algo(5000), # NOTE: a maxFee value is required when enabling coverAppCallInnerTransactionFees + )) + .send(send_params={"cover_app_call_inner_transaction_fees": True}) +``` + +Assuming the app account is not covering any of the inner transaction fees, if `my_method` in the above example sends 2 inner transactions, then the fee calculated for the parent transaction will be 3000 µALGO when the transaction is sent to the network. + +The above example also has a `max_fee` of 5000 µALGO specified. An exception will be thrown if the transaction fee execeeds that value, which allows you to set fee limits. The `max_fee` field is required when enabling `cover_app_call_inner_transaction_fees`. + +Because `max_fee` is required and an `algosdk.Transaction` does not hold any max fee information, you cannot use the generic `add_transaction()` method on the composer with `cover_app_call_inner_transaction_fees` enabled. Instead use the below, which provides a better overall experience: + +```python +my_method = algosdk.abi.Method.from_signature('my_method()void') + +# Does not work +result = algorand + .new_group() + .add_transaction(localnet.algorand.create_transaction.app_call_method_call( + AppCallMethodCallParams( + sender='SENDER', + app_id=123, + method=my_method, + args=[1, 2, 3], + max_fee=AlgoAmount.from_micro_algos(5000), # This is only used to create the algosdk.Transaction object and isn't made available to the composer. + ) + ).transactions[0] + ) + .send(send_params={"cover_app_call_inner_transaction_fees": True}) + +# Works as expected +result = algorand + .new_group() + .add_app_call_method_call(AppCallMethodCallParams( + sender='SENDER', + app_id=123, + method=my_method, + args=[1, 2, 3], + max_fee=AlgoAmount.from_micro_algos(5000), + )) + .send(send_params={"cover_app_call_inner_transaction_fees": True}) +``` + +A more complex valid scenario which leverages an app client to send an ABI method call with ABI method call transactions argument is below: + +```python +app_factory = algorand.client.get_app_factory( + app_spec='APP_SPEC', + default_sender=sender.addr, +) + +app_client_1, _ = app_factory.send.bare.create() +app_client_2, _ = app_factory.send.bare.create() + +payment_arg = algorand.create_transaction.payment( + PaymentParams( + sender=sender.addr, + receiver=receiver.addr, + amount=AlgoAmount.from_micro_algos(1), + ) +) + +# Note the use of .params. here, this ensure that maxFee is still available to the composer +app_call_arg = app_client_2.params.call( + AppCallMethodCallParams( + method='my_other_method', + args=[], + max_fee=AlgoAmount.from_micro_algos(2000), + ) +) + +result = app_client_1.algorand + .new_group() + .add_app_call_method_call( + app_client_1.params.call( + AppClientMethodCallParams( + method='my_method', + args=[payment_arg, app_call_arg], + max_fee=AlgoAmount.from_micro_algos(5000), + ) + ), + ) + .send({"cover_app_call_inner_transaction_fees": True}) +``` + +This feature should efficiently calculate the minimum fee needed to execute an app call transaction with inners, however we always recommend testing your specific scenario behaves as expected before releasing. + +#### Read-only calls + +When interacting with read-only calls, the transactions are not sent to the network, instead a simulation is performed to evaluate the transaction. +However, a read-only call will still consume the op budget, so to prevent this, by the default, the following logic is applied: + +1. If `max_fee` is not specified, `algokit-utils` will automatically set `max_fee` to `10` Algo. +2. If `max_fee` is specified, provided `max_fee` value will be respected when calculating required fee per read-only call. + +In either cases, resource population and app call inner transaction fees will still be automatically calculated and applied as per the rules above however there is no need to explicitly specify `populate_app_call_resources=True` or `cover_app_call_inner_transaction_fees=True` when sending read-only calls. + +### Covering App Call Op Budget + +The high level Algorand contract authoring languages all have support for ensuring appropriate app op budget is available via `ensure_budget` in Algorand Python, `ensureBudget` in Algorand TypeScript and `increaseOpcodeBudget` in TEALScript. This is great, as it allows contract authors to ensure appropriate budget is available by automatically sending op-up inner transactions to increase the budget available. These op-up inner transactions require the fees to be covered by an account, which is generally the responsibility of the application consumer. + +Application consumers may not be immediately aware of the number of op-up inner transactions sent, so it can be difficult for them to determine the exact fees required to successfully execute an application call. Fortunately the `cover_app_call_inner_transaction_fees` setting above can be leveraged to automatically cover the fees for any op-up inner transaction that an application sends. Additionally if a contract author decides to cover the fee for an op-up inner transaction, then the application consumer will not be charged a fee for that transaction. diff --git a/docs/source/capabilities/transaction-composer.md b/docs/source/capabilities/transaction-composer.md index f3c5df0e..7eb503e4 100644 --- a/docs/source/capabilities/transaction-composer.md +++ b/docs/source/capabilities/transaction-composer.md @@ -226,3 +226,120 @@ This feature is particularly useful when: - Developing applications where resource requirements may change dynamically Note: Resource population uses simulation under the hood to detect required resources, so it may add a small overhead to transaction preparation time. + +### Covering App Call Inner Transaction Fees + +`cover_app_call_inner_transaction_fees` automatically calculate the required fee for a parent app call transaction that sends inner transactions. It leverages the simulate endpoint to discover the inner transactions sent and calculates a fee delta to resolve the optimal fee. This feature also takes care of accounting for any surplus transaction fee at the various levels, so as to effectively minimise the fees needed to successfully handle complex scenarios. This setting only applies when you have constucted at least one app call transaction. + +For example: + +```python +myMethod = algosdk.ABIMethod.fromSignature('my_method()void') +result = algorand + .new_group() + .add_app_call_method_call(AppCallMethodCallParams( + sender: 'SENDER', + app_id=123, + method=myMethod, + args=[1, 2, 3], + max_fee=AlgoAmount.from_micro_algo(5000), # NOTE: a maxFee value is required when enabling coverAppCallInnerTransactionFees + )) + .send(send_params={"cover_app_call_inner_transaction_fees": True}) +``` + +Assuming the app account is not covering any of the inner transaction fees, if `my_method` in the above example sends 2 inner transactions, then the fee calculated for the parent transaction will be 3000 µALGO when the transaction is sent to the network. + +The above example also has a `max_fee` of 5000 µALGO specified. An exception will be thrown if the transaction fee execeeds that value, which allows you to set fee limits. The `max_fee` field is required when enabling `cover_app_call_inner_transaction_fees`. + +Because `max_fee` is required and an `algosdk.Transaction` does not hold any max fee information, you cannot use the generic `add_transaction()` method on the composer with `cover_app_call_inner_transaction_fees` enabled. Instead use the below, which provides a better overall experience: + +```python +my_method = algosdk.abi.Method.from_signature('my_method()void') + +# Does not work +result = algorand + .new_group() + .add_transaction(localnet.algorand.create_transaction.app_call_method_call( + AppCallMethodCallParams( + sender='SENDER', + app_id=123, + method=my_method, + args=[1, 2, 3], + max_fee=AlgoAmount.from_micro_algos(5000), # This is only used to create the algosdk.Transaction object and isn't made available to the composer. + ) + ).transactions[0] + ) + .send(send_params={"cover_app_call_inner_transaction_fees": True}) + +# Works as expected +result = algorand + .new_group() + .add_app_call_method_call(AppCallMethodCallParams( + sender='SENDER', + app_id=123, + method=my_method, + args=[1, 2, 3], + max_fee=AlgoAmount.from_micro_algos(5000), + )) + .send(send_params={"cover_app_call_inner_transaction_fees": True}) +``` + +A more complex valid scenario which leverages an app client to send an ABI method call with ABI method call transactions argument is below: + +```python +app_factory = algorand.client.get_app_factory( + app_spec='APP_SPEC', + default_sender=sender.addr, +) + +app_client_1, _ = app_factory.send.bare.create() +app_client_2, _ = app_factory.send.bare.create() + +payment_arg = algorand.create_transaction.payment( + PaymentParams( + sender=sender.addr, + receiver=receiver.addr, + amount=AlgoAmount.from_micro_algos(1), + ) +) + +# Note the use of .params. here, this ensure that maxFee is still available to the composer +app_call_arg = app_client_2.params.call( + AppCallMethodCallParams( + method='my_other_method', + args=[], + max_fee=AlgoAmount.from_micro_algos(2000), + ) +) + +result = app_client_1.algorand + .new_group() + .add_app_call_method_call( + app_client_1.params.call( + AppClientMethodCallParams( + method='my_method', + args=[payment_arg, app_call_arg], + max_fee=AlgoAmount.from_micro_algos(5000), + ) + ), + ) + .send({"cover_app_call_inner_transaction_fees": True}) +``` + +This feature should efficiently calculate the minimum fee needed to execute an app call transaction with inners, however we always recommend testing your specific scenario behaves as expected before releasing. + +#### Read-only calls + +When interacting with read-only calls, the transactions are not sent to the network, instead a simulation is performed to evaluate the transaction. +However, a read-only call will still consume the op budget, so to prevent this, by the default, the following logic is applied: + +1. If `max_fee` is not specified, `algokit-utils` will automatically set `max_fee` to `10` Algo. +2. If `max_fee` is specified, provided `max_fee` value will be respected when calculating required fee per read-only call. + +In either cases, resource population and app call inner transaction fees will still be automatically calculated and applied as per the rules above however there is no need to explicitly specify `populate_app_call_resources=True` or `cover_app_call_inner_transaction_fees=True` when sending read-only calls. + +### Covering App Call Op Budget + +The high level Algorand contract authoring languages all have support for ensuring appropriate app op budget is available via `ensure_budget` in Algorand Python, `ensureBudget` in Algorand TypeScript and `increaseOpcodeBudget` in TEALScript. This is great, as it allows contract authors to ensure appropriate budget is available by automatically sending op-up inner transactions to increase the budget available. These op-up inner transactions require the fees to be covered by an account, which is generally the responsibility of the application consumer. + +Application consumers may not be immediately aware of the number of op-up inner transactions sent, so it can be difficult for them to determine the exact fees required to successfully execute an application call. Fortunately the `cover_app_call_inner_transaction_fees` setting above can be leveraged to automatically cover the fees for any op-up inner transaction that an application sends. Additionally if a contract author decides to cover the fee for an op-up inner transaction, then the application consumer will not be charged a fee for that transaction. diff --git a/src/algokit_utils/clients/client_manager.py b/src/algokit_utils/clients/client_manager.py index c9581fc3..f1bf8583 100644 --- a/src/algokit_utils/clients/client_manager.py +++ b/src/algokit_utils/clients/client_manager.py @@ -361,7 +361,7 @@ def get_algod_client(config: AlgoClientNetworkConfig | None = None) -> AlgodClie headers = {"X-Algo-API-Token": config.token or ""} return AlgodClient( algod_token=config.token or "", - algod_address=f'{config.server}{f':{config.port}' if config.port else ''}', + algod_address=config.full_url(), headers=headers, ) @@ -381,7 +381,7 @@ def get_kmd_client(config: AlgoClientNetworkConfig | None = None) -> KMDClient: :return: KMD client instance """ config = config or _get_config_from_environment("KMD") - return KMDClient(config.token, f'{config.server}{f':{config.port}' if config.port else ''}') + return KMDClient(config.token, config.full_url()) @staticmethod def get_kmd_client_from_environment() -> KMDClient: @@ -402,7 +402,7 @@ def get_indexer_client(config: AlgoClientNetworkConfig | None = None) -> Indexer headers = {"X-Indexer-API-Token": config.token} return IndexerClient( indexer_token=config.token, - indexer_address=f'{config.server}{f":{config.port}" if config.port else ''}', + indexer_address=config.full_url(), headers=headers, ) diff --git a/src/algokit_utils/models/network.py b/src/algokit_utils/models/network.py index 28c0b192..d1f3c190 100644 --- a/src/algokit_utils/models/network.py +++ b/src/algokit_utils/models/network.py @@ -17,6 +17,10 @@ class AlgoClientNetworkConfig: """API Token to authenticate with the service e.g '4001' or '8980'""" port: str | int | None = None + def full_url(self) -> str: + """Returns the full URL for the service""" + return f"{self.server}{f':{self.port}' if self.port else ''}" + @dataclasses.dataclass class AlgoClientConfigs: diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index e2224677..1dbf5066 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -614,6 +614,12 @@ class _TransactionWithPriority: NULL_SIGNER: TransactionSigner = algosdk.atomic_transaction_composer.EmptySigner() +def _get_dummy_max_fees_for_simulated_opups(group_len: int) -> dict[int, AlgoAmount]: + from algokit_utils.models.amount import AlgoAmount + + return {i: AlgoAmount(algo=10) for i in range(group_len)} + + def _encode_lease(lease: str | bytes | None) -> bytes | None: if lease is None: return None @@ -801,16 +807,6 @@ def _num_extra_program_pages(approval: bytes | None, clear: bytes | None) -> int return max(0, (total - 1) // algosdk.constants.APP_PAGE_MAX_SIZE) -def populate_app_call_resources(atc: AtomicTransactionComposer, algod: AlgodClient) -> AtomicTransactionComposer: - """Populate application call resources based on simulation results. - - :param atc: The AtomicTransactionComposer containing transactions - :param algod: Algod client for simulation - :return: Modified AtomicTransactionComposer with populated resources - """ - return prepare_group_for_sending(atc, algod, populate_app_call_resources=True) - - def prepare_group_for_sending( # noqa: C901, PLR0912, PLR0915 atc: AtomicTransactionComposer, algod: AlgodClient, @@ -1600,6 +1596,8 @@ def build_transactions(self) -> BuiltTransactions: signers[idx] = ts.signer if isinstance(ts, TransactionWithSignerAndContext) and ts.context.abi_method: method_calls[idx] = ts.context.abi_method + if ts.context.max_fee: + self._txn_max_fees[idx] = ts.context.max_fee idx += 1 return BuiltTransactions(transactions=transactions, method_calls=method_calls, signers=signers) @@ -1681,7 +1679,7 @@ def simulate( :param allow_more_logs: Whether to allow more logs than the standard limit :param allow_empty_signatures: Whether to allow transactions with empty signatures - :param allow_unnamed_resources: Whether to allow unnamed resources + :param allow_unnamed_resources: Whether to allow unnamed resources. :param extra_opcode_budget: Additional opcode budget to allocate :param exec_trace_config: Configuration for execution tracing :param simulation_round: Round number to simulate at @@ -1701,6 +1699,17 @@ def simulate( else: self.build() + atc = prepare_group_for_sending( + atc, + self._algod, + populate_app_call_resources=allow_unnamed_resources, + cover_app_call_inner_transaction_fees=allow_unnamed_resources, + additional_atc_context=AdditionalAtcContext( + suggested_params=self._get_suggested_params(), + max_fees=self._txn_max_fees or _get_dummy_max_fees_for_simulated_opups(atc.get_tx_count()), + ), + ) + if config.debug and config.project_root and config.trace_all: response = simulate_and_persist_response( atc, diff --git a/tests/artifacts/inner-fee/application.json b/tests/artifacts/inner-fee/application.json index e124cbd4..f223df51 100644 --- a/tests/artifacts/inner-fee/application.json +++ b/tests/artifacts/inner-fee/application.json @@ -2,6 +2,27 @@ "name": "InnerFeeContract", "structs": {}, "methods": [ + { + "name": "burn_ops_readonly", + "args": [ + { + "type": "uint64", + "name": "op_budget" + } + ], + "returns": { + "type": "void" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": true, + "events": [], + "recommendations": {} + }, { "name": "burn_ops", "args": [ @@ -161,27 +182,43 @@ "sourceInfo": [ { "pc": [ - 75, - 91, + 298, + 328, + 348, + 368, + 388, + 433, + 453, + 501, + 521 + ], + "errorMessage": "Index access is out of bounds" + }, + { + "pc": [ + 77, 100, - 119, - 142 + 123, + 142, + 151, + 167 ], "errorMessage": "OnCompletion is not NoOp" }, { "pc": [ - 170 + 188 ], "errorMessage": "can only call when creating" }, { "pc": [ - 78, - 94, + 80, 103, - 122, - 145 + 126, + 145, + 154, + 170 ], "errorMessage": "can only call when not creating" } @@ -194,8 +231,20 @@ } }, "source": { - "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMudGVzdF9jb250cmFjdC5jb250cmFjdC5Jbm5lckZlZUNvbnRyYWN0LmFwcHJvdmFsX3Byb2dyYW06CiAgICBpbnRjYmxvY2sgMSA2IDAgOAogICAgYnl0ZWNibG9jayAweDc3MjllYjMyIDB4YzJjNDg5ZTUgMHgwNjgxMDEKICAgIGNhbGxzdWIgX19wdXlhX2FyYzRfcm91dGVyX18KICAgIHJldHVybgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy50ZXN0X2NvbnRyYWN0LmNvbnRyYWN0LklubmVyRmVlQ29udHJhY3QuX19wdXlhX2FyYzRfcm91dGVyX18oKSAtPiB1aW50NjQ6Cl9fcHV5YV9hcmM0X3JvdXRlcl9fOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgcHJvdG8gMCAxCiAgICB0eG4gTnVtQXBwQXJncwogICAgYnogX19wdXlhX2FyYzRfcm91dGVyX19fYmFyZV9yb3V0aW5nQDkKICAgIHB1c2hieXRlcyAweGRkMzc4MjQ3IC8vIG1ldGhvZCAiYnVybl9vcHModWludDY0KXZvaWQiCiAgICBieXRlY18wIC8vIG1ldGhvZCAibm9fb3AoKXZvaWQiCiAgICBieXRlY18xIC8vIG1ldGhvZCAic2VuZF94X2lubmVyc193aXRoX2ZlZXModWludDY0LHVpbnQ2NFtdKXZvaWQiCiAgICBwdXNoYnl0ZXNzIDB4MzQzNjgyY2QgMHgxY2YyZjU5MCAvLyBtZXRob2QgInNlbmRfaW5uZXJzX3dpdGhfZmVlcyh1aW50NjQsdWludDY0LCh1aW50NjQsdWludDY0LHVpbnQ2NCx1aW50NjQsdWludDY0W10pKXZvaWQiLCBtZXRob2QgInNlbmRfaW5uZXJzX3dpdGhfZmVlc18yKHVpbnQ2NCx1aW50NjQsKHVpbnQ2NCx1aW50NjQsdWludDY0W10sdWludDY0LHVpbnQ2NCx1aW50NjRbXSkpdm9pZCIKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDAKICAgIG1hdGNoIF9fcHV5YV9hcmM0X3JvdXRlcl9fX2J1cm5fb3BzX3JvdXRlQDIgX19wdXlhX2FyYzRfcm91dGVyX19fbm9fb3Bfcm91dGVAMyBfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZW5kX3hfaW5uZXJzX3dpdGhfZmVlc19yb3V0ZUA0IF9fcHV5YV9hcmM0X3JvdXRlcl9fX3NlbmRfaW5uZXJzX3dpdGhfZmVlc19yb3V0ZUA1IF9fcHV5YV9hcmM0X3JvdXRlcl9fX3NlbmRfaW5uZXJzX3dpdGhfZmVlc18yX3JvdXRlQDYKICAgIGludGNfMiAvLyAwCiAgICByZXRzdWIKCl9fcHV5YV9hcmM0X3JvdXRlcl9fX2J1cm5fb3BzX3JvdXRlQDI6CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTo1CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgYnRvaQogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NQogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICBjYWxsc3ViIGJ1cm5fb3BzCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19ub19vcF9yb3V0ZUAzOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTQKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBub3QgTm9PcAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGFzc2VydCAvLyBjYW4gb25seSBjYWxsIHdoZW4gbm90IGNyZWF0aW5nCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZW5kX3hfaW5uZXJzX3dpdGhfZmVlc19yb3V0ZUA0OgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTgKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBub3QgTm9PcAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGFzc2VydCAvLyBjYW4gb25seSBjYWxsIHdoZW4gbm90IGNyZWF0aW5nCiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTo0CiAgICAvLyBjbGFzcyBJbm5lckZlZUNvbnRyYWN0KEFSQzRDb250cmFjdCk6CiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICBidG9pCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAyCiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToxOAogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICBjYWxsc3ViIHNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZW5kX2lubmVyc193aXRoX2ZlZXNfcm91dGVANToKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjIzCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgYnRvaQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgYnRvaQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMwogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MjMKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgY2FsbHN1YiBzZW5kX2lubmVyc193aXRoX2ZlZXMKICAgIGludGNfMCAvLyAxCiAgICByZXRzdWIKCl9fcHV5YV9hcmM0X3JvdXRlcl9fX3NlbmRfaW5uZXJzX3dpdGhfZmVlc18yX3JvdXRlQDY6CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTozNAogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjQKICAgIC8vIGNsYXNzIElubmVyRmVlQ29udHJhY3QoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDIKICAgIGJ0b2kKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDMKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjM0CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIGNhbGxzdWIgc2VuZF9pbm5lcnNfd2l0aF9mZWVzXzIKICAgIGludGNfMCAvLyAxCiAgICByZXRzdWIKCl9fcHV5YV9hcmM0X3JvdXRlcl9fX2JhcmVfcm91dGluZ0A5OgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuIE9uQ29tcGxldGlvbgogICAgYm56IF9fcHV5YV9hcmM0X3JvdXRlcl9fX2FmdGVyX2lmX2Vsc2VAMTMKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICAhCiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIGNyZWF0aW5nCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19hZnRlcl9pZl9lbHNlQDEzOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgaW50Y18yIC8vIDAKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy50ZXN0X2NvbnRyYWN0LmNvbnRyYWN0LklubmVyRmVlQ29udHJhY3QuYnVybl9vcHMob3BfYnVkZ2V0OiB1aW50NjQpIC0+IHZvaWQ6CmJ1cm5fb3BzOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NS02CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIC8vIGRlZiBidXJuX29wcyhzZWxmLCBvcF9idWRnZXQ6IFVJbnQ2NCkgLT4gTm9uZToKICAgIHByb3RvIDEgMAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6Ny04CiAgICAvLyAjIFVzZXMgYXBwcm94IDYwIG9wIGJ1ZGdldCBwZXIgaXRlcmF0aW9uCiAgICAvLyBjb3VudCA9IG9wX2J1ZGdldCAvLyA2MAogICAgZnJhbWVfZGlnIC0xCiAgICBwdXNoaW50IDYwIC8vIDYwCiAgICAvCiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTo5CiAgICAvLyBlbnN1cmVfYnVkZ2V0KG9wX2J1ZGdldCkKICAgIGZyYW1lX2RpZyAtMQogICAgaW50Y18yIC8vIDAKICAgIGNhbGxzdWIgZW5zdXJlX2J1ZGdldAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTAKICAgIC8vIGZvciBpIGluIHVyYW5nZShjb3VudCk6CiAgICBpbnRjXzIgLy8gMAoKYnVybl9vcHNfZm9yX2hlYWRlckAxOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTAKICAgIC8vIGZvciBpIGluIHVyYW5nZShjb3VudCk6CiAgICBmcmFtZV9kaWcgMQogICAgZnJhbWVfZGlnIDAKICAgIDwKICAgIGJ6IGJ1cm5fb3BzX2FmdGVyX2ZvckA0CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToxMQogICAgLy8gc3FydCA9IG9wLmJzcXJ0KEJpZ1VJbnQoaSkpCiAgICBmcmFtZV9kaWcgMQogICAgZHVwCiAgICBpdG9iCiAgICBic3FydAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTIKICAgIC8vIGFzc2VydChzcXJ0ID49IDApICMgUHJldmVudCBvcHRpbWlzZXIgcmVtb3ZpbmcgdGhlIHNxcnQKICAgIHB1c2hieXRlcyAweAogICAgYj49CiAgICBhc3NlcnQKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjEwCiAgICAvLyBmb3IgaSBpbiB1cmFuZ2UoY291bnQpOgogICAgaW50Y18wIC8vIDEKICAgICsKICAgIGZyYW1lX2J1cnkgMQogICAgYiBidXJuX29wc19mb3JfaGVhZGVyQDEKCmJ1cm5fb3BzX2FmdGVyX2ZvckA0OgogICAgcmV0c3ViCgoKLy8gc21hcnRfY29udHJhY3RzLnRlc3RfY29udHJhY3QuY29udHJhY3QuSW5uZXJGZWVDb250cmFjdC5zZW5kX3hfaW5uZXJzX3dpdGhfZmVlcyhhcHBfaWQ6IHVpbnQ2NCwgZmVlczogYnl0ZXMpIC0+IHZvaWQ6CnNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTgtMTkKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgLy8gZGVmIHNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzKHNlbGYsIGFwcF9pZDogVUludDY0LCBmZWVzOiBhcmM0LkR5bmFtaWNBcnJheVthcmM0LlVJbnQ2NF0pIC0+IE5vbmU6CiAgICBwcm90byAyIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjIwCiAgICAvLyBmb3IgZmVlIGluIGZlZXM6CiAgICBmcmFtZV9kaWcgLTEKICAgIGludGNfMiAvLyAwCiAgICBleHRyYWN0X3VpbnQxNgogICAgaW50Y18yIC8vIDAKCnNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzX2Zvcl9oZWFkZXJAMToKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjIwCiAgICAvLyBmb3IgZmVlIGluIGZlZXM6CiAgICBmcmFtZV9kaWcgMQogICAgZnJhbWVfZGlnIDAKICAgIDwKICAgIGJ6IHNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzX2FmdGVyX2ZvckA1CiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMiAwCiAgICBmcmFtZV9kaWcgMQogICAgZHVwCiAgICBjb3ZlciAyCiAgICBpbnRjXzMgLy8gOAogICAgKgogICAgaW50Y18zIC8vIDgKICAgIGV4dHJhY3QzIC8vIG9uIGVycm9yOiBJbmRleCBhY2Nlc3MgaXMgb3V0IG9mIGJvdW5kcwogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MjEKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ25vX29wJywgYXBwX2lkPWFwcF9pZCwgZmVlPWZlZS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBidG9pCiAgICBmcmFtZV9kaWcgLTIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25JRAogICAgYnl0ZWNfMCAvLyBtZXRob2QgIm5vX29wKCl2b2lkIgogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGludGNfMSAvLyBhcHBsCiAgICBpdHhuX2ZpZWxkIFR5cGVFbnVtCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgaXR4bl9zdWJtaXQKICAgIGludGNfMCAvLyAxCiAgICArCiAgICBmcmFtZV9idXJ5IDEKICAgIGIgc2VuZF94X2lubmVyc193aXRoX2ZlZXNfZm9yX2hlYWRlckAxCgpzZW5kX3hfaW5uZXJzX3dpdGhfZmVlc19hZnRlcl9mb3JANToKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy50ZXN0X2NvbnRyYWN0LmNvbnRyYWN0LklubmVyRmVlQ29udHJhY3Quc2VuZF9pbm5lcnNfd2l0aF9mZWVzKGFwcF9pZF8xOiB1aW50NjQsIGFwcF9pZF8yOiB1aW50NjQsIGZlZXM6IGJ5dGVzKSAtPiB2b2lkOgpzZW5kX2lubmVyc193aXRoX2ZlZXM6CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToyMy0yNAogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICAvLyBkZWYgc2VuZF9pbm5lcnNfd2l0aF9mZWVzKHNlbGYsIGFwcF9pZF8xOiBVSW50NjQsIGFwcF9pZF8yOiBVSW50NjQsIGZlZXM6IGFyYzQuVHVwbGVbYXJjNC5VSW50NjQsIGFyYzQuVUludDY0LCBhcmM0LlVJbnQ2NCwgYXJjNC5VSW50NjQsIGFyYzQuRHluYW1pY0FycmF5W2FyYzQuVUludDY0XV0pIC0+IE5vbmU6CiAgICBwcm90byAzIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjI1CiAgICAvLyBhcmM0LmFiaV9jYWxsKCdub19vcCcsIGFwcF9pZD1hcHBfaWRfMSwgZmVlPWZlZXNbMF0ubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgZnJhbWVfZGlnIC0xCiAgICBleHRyYWN0IDAgOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18wIC8vIG1ldGhvZCAibm9fb3AoKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18xIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MjYKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ25vX29wJywgYXBwX2lkPWFwcF9pZF8xLCBmZWU9ZmVlc1sxXS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgOCA4IC8vIG9uIGVycm9yOiBJbmRleCBhY2Nlc3MgaXMgb3V0IG9mIGJvdW5kcwogICAgYnRvaQogICAgZnJhbWVfZGlnIC0zCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKICAgIGJ5dGVjXzAgLy8gbWV0aG9kICJub19vcCgpdm9pZCIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBpbnRjXzEgLy8gYXBwbAogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaXR4bl9maWVsZCBGZWUKICAgIGl0eG5fc3VibWl0CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToyNy0zMQogICAgLy8gaXR4bi5QYXltZW50KAogICAgLy8gICAgIGFtb3VudD0wLAogICAgLy8gICAgIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgZmVlPWZlZXNbMl0ubmF0aXZlCiAgICAvLyApLnN1Ym1pdCgpCiAgICBpdHhuX2JlZ2luCiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTozMAogICAgLy8gZmVlPWZlZXNbMl0ubmF0aXZlCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMTYgOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjI5CiAgICAvLyByZWNlaXZlcj1HbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzLAogICAgZ2xvYmFsIEN1cnJlbnRBcHBsaWNhdGlvbkFkZHJlc3MKICAgIGl0eG5fZmllbGQgUmVjZWl2ZXIKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjI4CiAgICAvLyBhbW91bnQ9MCwKICAgIGludGNfMiAvLyAwCiAgICBpdHhuX2ZpZWxkIEFtb3VudAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MjcKICAgIC8vIGl0eG4uUGF5bWVudCgKICAgIGludGNfMCAvLyBwYXkKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToyNy0zMQogICAgLy8gaXR4bi5QYXltZW50KAogICAgLy8gICAgIGFtb3VudD0wLAogICAgLy8gICAgIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICAvLyAgICAgZmVlPWZlZXNbMl0ubmF0aXZlCiAgICAvLyApLnN1Ym1pdCgpCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MzIKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ3NlbmRfeF9pbm5lcnNfd2l0aF9mZWVzJywgYXBwX2lkXzIsIGZlZXNbNF0sIGFwcF9pZD1hcHBfaWRfMSwgZmVlPWZlZXNbM10ubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgZnJhbWVfZGlnIC0xCiAgICBleHRyYWN0IDI0IDggLy8gb24gZXJyb3I6IEluZGV4IGFjY2VzcyBpcyBvdXQgb2YgYm91bmRzCiAgICBidG9pCiAgICBmcmFtZV9kaWcgLTIKICAgIGl0b2IKICAgIGZyYW1lX2RpZyAtMQogICAgcHVzaGludCAzMiAvLyAzMgogICAgZXh0cmFjdF91aW50MTYKICAgIGZyYW1lX2RpZyAtMQogICAgbGVuCiAgICBmcmFtZV9kaWcgLTEKICAgIGNvdmVyIDIKICAgIHN1YnN0cmluZzMKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18xIC8vIG1ldGhvZCAic2VuZF94X2lubmVyc193aXRoX2ZlZXModWludDY0LHVpbnQ2NFtdKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgc3dhcAogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBpbnRjXzEgLy8gYXBwbAogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaXR4bl9maWVsZCBGZWUKICAgIGl0eG5fc3VibWl0CiAgICByZXRzdWIKCgovLyBzbWFydF9jb250cmFjdHMudGVzdF9jb250cmFjdC5jb250cmFjdC5Jbm5lckZlZUNvbnRyYWN0LnNlbmRfaW5uZXJzX3dpdGhfZmVlc18yKGFwcF9pZF8xOiB1aW50NjQsIGFwcF9pZF8yOiB1aW50NjQsIGZlZXM6IGJ5dGVzKSAtPiB2b2lkOgpzZW5kX2lubmVyc193aXRoX2ZlZXNfMjoKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjM0LTM1CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIC8vIGRlZiBzZW5kX2lubmVyc193aXRoX2ZlZXNfMihzZWxmLCBhcHBfaWRfMTogVUludDY0LCBhcHBfaWRfMjogVUludDY0LCBmZWVzOiBhcmM0LlR1cGxlW2FyYzQuVUludDY0LCBhcmM0LlVJbnQ2NCwgYXJjNC5EeW5hbWljQXJyYXlbYXJjNC5VSW50NjRdLCBhcmM0LlVJbnQ2NCwgYXJjNC5VSW50NjQsIGFyYzQuRHluYW1pY0FycmF5W2FyYzQuVUludDY0XV0pIC0+IE5vbmU6CiAgICBwcm90byAzIDAKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjM2CiAgICAvLyBhcmM0LmFiaV9jYWxsKCdub19vcCcsIGFwcF9pZD1hcHBfaWRfMSwgZmVlPWZlZXNbMF0ubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgZnJhbWVfZGlnIC0xCiAgICBleHRyYWN0IDAgOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18wIC8vIG1ldGhvZCAibm9fb3AoKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18xIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MzcKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ3NlbmRfeF9pbm5lcnNfd2l0aF9mZWVzJywgYXBwX2lkXzIsIGZlZXNbMl0sIGFwcF9pZD1hcHBfaWRfMSwgZmVlPWZlZXNbMV0ubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgZnJhbWVfZGlnIC0xCiAgICBleHRyYWN0IDggOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMgogICAgaXRvYgogICAgZnJhbWVfZGlnIC0xCiAgICBwdXNoaW50IDE2IC8vIDE2CiAgICBleHRyYWN0X3VpbnQxNgogICAgZnJhbWVfZGlnIC0xCiAgICBwdXNoaW50IDM0IC8vIDM0CiAgICBleHRyYWN0X3VpbnQxNgogICAgZnJhbWVfZGlnIC0xCiAgICB1bmNvdmVyIDIKICAgIGRpZyAyCiAgICBzdWJzdHJpbmczCiAgICBmcmFtZV9kaWcgLTMKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25JRAogICAgYnl0ZWNfMSAvLyBtZXRob2QgInNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzKHVpbnQ2NCx1aW50NjRbXSl2b2lkIgogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGRpZyAyCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGludGNfMSAvLyBhcHBsCiAgICBpdHhuX2ZpZWxkIFR5cGVFbnVtCiAgICB1bmNvdmVyIDIKICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MzgKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ25vX29wJywgYXBwX2lkPWFwcF9pZF8xLCBmZWU9ZmVlc1szXS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMTggOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18wIC8vIG1ldGhvZCAibm9fb3AoKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18xIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MzkKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ3NlbmRfeF9pbm5lcnNfd2l0aF9mZWVzJywgYXBwX2lkXzIsIGZlZXNbNV0sIGFwcF9pZD1hcHBfaWRfMSwgZmVlPWZlZXNbNF0ubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgZnJhbWVfZGlnIC0xCiAgICBleHRyYWN0IDI2IDggLy8gb24gZXJyb3I6IEluZGV4IGFjY2VzcyBpcyBvdXQgb2YgYm91bmRzCiAgICBidG9pCiAgICBmcmFtZV9kaWcgLTEKICAgIGxlbgogICAgZnJhbWVfZGlnIC0xCiAgICB1bmNvdmVyIDMKICAgIHVuY292ZXIgMgogICAgc3Vic3RyaW5nMwogICAgZnJhbWVfZGlnIC0zCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKICAgIGJ5dGVjXzEgLy8gbWV0aG9kICJzZW5kX3hfaW5uZXJzX3dpdGhfZmVlcyh1aW50NjQsdWludDY0W10pdm9pZCIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICB1bmNvdmVyIDIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18xIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgcmV0c3ViCgoKLy8gX3B1eWFfbGliLnV0aWwuZW5zdXJlX2J1ZGdldChyZXF1aXJlZF9idWRnZXQ6IHVpbnQ2NCwgZmVlX3NvdXJjZTogdWludDY0KSAtPiB2b2lkOgplbnN1cmVfYnVkZ2V0OgogICAgcHJvdG8gMiAwCiAgICBmcmFtZV9kaWcgLTIKICAgIHB1c2hpbnQgMTAgLy8gMTAKICAgICsKCmVuc3VyZV9idWRnZXRfd2hpbGVfdG9wQDE6CiAgICBmcmFtZV9kaWcgMAogICAgZ2xvYmFsIE9wY29kZUJ1ZGdldAogICAgPgogICAgYnogZW5zdXJlX2J1ZGdldF9hZnRlcl93aGlsZUA3CiAgICBpdHhuX2JlZ2luCiAgICBpbnRjXzEgLy8gYXBwbAogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgcHVzaGludCA1IC8vIERlbGV0ZUFwcGxpY2F0aW9uCiAgICBpdHhuX2ZpZWxkIE9uQ29tcGxldGlvbgogICAgYnl0ZWNfMiAvLyAweDA2ODEwMQogICAgaXR4bl9maWVsZCBBcHByb3ZhbFByb2dyYW0KICAgIGJ5dGVjXzIgLy8gMHgwNjgxMDEKICAgIGl0eG5fZmllbGQgQ2xlYXJTdGF0ZVByb2dyYW0KICAgIGZyYW1lX2RpZyAtMQogICAgc3dpdGNoIGVuc3VyZV9idWRnZXRfc3dpdGNoX2Nhc2VfMEAzIGVuc3VyZV9idWRnZXRfc3dpdGNoX2Nhc2VfMUA0CiAgICBiIGVuc3VyZV9idWRnZXRfc3dpdGNoX2Nhc2VfbmV4dEA2CgplbnN1cmVfYnVkZ2V0X3N3aXRjaF9jYXNlXzBAMzoKICAgIGludGNfMiAvLyAwCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgYiBlbnN1cmVfYnVkZ2V0X3N3aXRjaF9jYXNlX25leHRANgoKZW5zdXJlX2J1ZGdldF9zd2l0Y2hfY2FzZV8xQDQ6CiAgICBnbG9iYWwgTWluVHhuRmVlCiAgICBpdHhuX2ZpZWxkIEZlZQoKZW5zdXJlX2J1ZGdldF9zd2l0Y2hfY2FzZV9uZXh0QDY6CiAgICBpdHhuX3N1Ym1pdAogICAgYiBlbnN1cmVfYnVkZ2V0X3doaWxlX3RvcEAxCgplbnN1cmVfYnVkZ2V0X2FmdGVyX3doaWxlQDc6CiAgICByZXRzdWIK", - "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMudGVzdF9jb250cmFjdC5jb250cmFjdC5Jbm5lckZlZUNvbnRyYWN0LmNsZWFyX3N0YXRlX3Byb2dyYW06CiAgICBwdXNoaW50IDEgLy8gMQogICAgcmV0dXJuCg==" + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBhbGdvcHkuYXJjNC5BUkM0Q29udHJhY3QuYXBwcm92YWxfcHJvZ3JhbSgpIC0+IHVpbnQ2NDoKbWFpbjoKICAgIGludGNibG9jayAxIDAgNiA2MAogICAgYnl0ZWNibG9jayAweDc3MjllYjMyIDB4YzJjNDg5ZTUgMHggMHgwNjgxMDEKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToxNAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuIE51bUFwcEFyZ3MKICAgIGJ6IG1haW5fYmFyZV9yb3V0aW5nQDExCiAgICBwdXNoYnl0ZXNzIDB4OWQ4OTI5YzcgMHhkZDM3ODI0NyAvLyBtZXRob2QgImJ1cm5fb3BzX3JlYWRvbmx5KHVpbnQ2NCl2b2lkIiwgbWV0aG9kICJidXJuX29wcyh1aW50NjQpdm9pZCIKICAgIGJ5dGVjXzAgLy8gbWV0aG9kICJub19vcCgpdm9pZCIKICAgIGJ5dGVjXzEgLy8gbWV0aG9kICJzZW5kX3hfaW5uZXJzX3dpdGhfZmVlcyh1aW50NjQsdWludDY0W10pdm9pZCIKICAgIHB1c2hieXRlc3MgMHgzNDM2ODJjZCAweDFjZjJmNTkwIC8vIG1ldGhvZCAic2VuZF9pbm5lcnNfd2l0aF9mZWVzKHVpbnQ2NCx1aW50NjQsKHVpbnQ2NCx1aW50NjQsdWludDY0LHVpbnQ2NCx1aW50NjRbXSkpdm9pZCIsIG1ldGhvZCAic2VuZF9pbm5lcnNfd2l0aF9mZWVzXzIodWludDY0LHVpbnQ2NCwodWludDY0LHVpbnQ2NCx1aW50NjRbXSx1aW50NjQsdWludDY0LHVpbnQ2NFtdKSl2b2lkIgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMAogICAgbWF0Y2ggbWFpbl9idXJuX29wc19yZWFkb25seV9yb3V0ZUAzIG1haW5fYnVybl9vcHNfcm91dGVANCBtYWluX25vX29wX3JvdXRlQDUgbWFpbl9zZW5kX3hfaW5uZXJzX3dpdGhfZmVlc19yb3V0ZUA2IG1haW5fc2VuZF9pbm5lcnNfd2l0aF9mZWVzX3JvdXRlQDcgbWFpbl9zZW5kX2lubmVyc193aXRoX2ZlZXNfMl9yb3V0ZUA4CgptYWluX2FmdGVyX2lmX2Vsc2VAMTU6CiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6MTQKICAgIC8vIGNsYXNzIElubmVyRmVlQ29udHJhY3QoQVJDNENvbnRyYWN0KToKICAgIGludGNfMSAvLyAwCiAgICByZXR1cm4KCm1haW5fc2VuZF9pbm5lcnNfd2l0aF9mZWVzXzJfcm91dGVAODoKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weTo1MwogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToxNAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgYnRvaQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgYnRvaQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMwogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjUzCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIGNhbGxzdWIgc2VuZF9pbm5lcnNfd2l0aF9mZWVzXzIKICAgIGludGNfMCAvLyAxCiAgICByZXR1cm4KCm1haW5fc2VuZF9pbm5lcnNfd2l0aF9mZWVzX3JvdXRlQDc6CiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6NDIKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBub3QgTm9PcAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGFzc2VydCAvLyBjYW4gb25seSBjYWxsIHdoZW4gbm90IGNyZWF0aW5nCiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6MTQKICAgIC8vIGNsYXNzIElubmVyRmVlQ29udHJhY3QoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDIKICAgIGJ0b2kKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDMKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weTo0MgogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICBjYWxsc3ViIHNlbmRfaW5uZXJzX3dpdGhfZmVlcwogICAgaW50Y18wIC8vIDEKICAgIHJldHVybgoKbWFpbl9zZW5kX3hfaW5uZXJzX3dpdGhfZmVlc19yb3V0ZUA2OgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjM3CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjE0CiAgICAvLyBjbGFzcyBJbm5lckZlZUNvbnRyYWN0KEFSQzRDb250cmFjdCk6CiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICBidG9pCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAyCiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6MzcKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgY2FsbHN1YiBzZW5kX3hfaW5uZXJzX3dpdGhfZmVlcwogICAgaW50Y18wIC8vIDEKICAgIHJldHVybgoKbWFpbl9ub19vcF9yb3V0ZUA1OgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjMzCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgaW50Y18wIC8vIDEKICAgIHJldHVybgoKbWFpbl9idXJuX29wc19yb3V0ZUA0OgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjI0CiAgICAvLyBAYXJjNC5hYmltZXRob2QoKQogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBub3QgTm9PcAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGFzc2VydCAvLyBjYW4gb25seSBjYWxsIHdoZW4gbm90IGNyZWF0aW5nCiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6MTQKICAgIC8vIGNsYXNzIElubmVyRmVlQ29udHJhY3QoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToyNAogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIGNhbGxzdWIgYnVybl9vcHMKICAgIGludGNfMCAvLyAxCiAgICByZXR1cm4KCm1haW5fYnVybl9vcHNfcmVhZG9ubHlfcm91dGVAMzoKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToxNQogICAgLy8gQGFyYzQuYWJpbWV0aG9kKHJlYWRvbmx5PVRydWUpCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToxNAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgYnRvaQogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjE1CiAgICAvLyBAYXJjNC5hYmltZXRob2QocmVhZG9ubHk9VHJ1ZSkKICAgIGNhbGxzdWIgYnVybl9vcHNfcmVhZG9ubHkKICAgIGludGNfMCAvLyAxCiAgICByZXR1cm4KCm1haW5fYmFyZV9yb3V0aW5nQDExOgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjE0CiAgICAvLyBjbGFzcyBJbm5lckZlZUNvbnRyYWN0KEFSQzRDb250cmFjdCk6CiAgICB0eG4gT25Db21wbGV0aW9uCiAgICBibnogbWFpbl9hZnRlcl9pZl9lbHNlQDE1CiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgIQogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBjcmVhdGluZwogICAgaW50Y18wIC8vIDEKICAgIHJldHVybgoKCi8vIGNvbnRyYWN0LklubmVyRmVlQ29udHJhY3QuYnVybl9vcHNfcmVhZG9ubHkob3BfYnVkZ2V0OiB1aW50NjQpIC0+IHZvaWQ6CmJ1cm5fb3BzX3JlYWRvbmx5OgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjE1LTE2CiAgICAvLyBAYXJjNC5hYmltZXRob2QocmVhZG9ubHk9VHJ1ZSkKICAgIC8vIGRlZiBidXJuX29wc19yZWFkb25seShzZWxmLCBvcF9idWRnZXQ6IFVJbnQ2NCkgLT4gTm9uZToKICAgIHByb3RvIDEgMAogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjE3LTE4CiAgICAvLyAjIFVzZXMgYXBwcm94IDYwIG9wIGJ1ZGdldCBwZXIgaXRlcmF0aW9uCiAgICAvLyBjb3VudCA9IG9wX2J1ZGdldCAvLyA2MAogICAgZnJhbWVfZGlnIC0xCiAgICBpbnRjXzMgLy8gNjAKICAgIC8KICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToxOQogICAgLy8gZW5zdXJlX2J1ZGdldChvcF9idWRnZXQpCiAgICBmcmFtZV9kaWcgLTEKICAgIGludGNfMSAvLyAwCiAgICBjYWxsc3ViIGVuc3VyZV9idWRnZXQKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToyMAogICAgLy8gZm9yIGkgaW4gdXJhbmdlKGNvdW50KToKICAgIGludGNfMSAvLyAwCgpidXJuX29wc19yZWFkb25seV9mb3JfaGVhZGVyQDE6CiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6MjAKICAgIC8vIGZvciBpIGluIHVyYW5nZShjb3VudCk6CiAgICBmcmFtZV9kaWcgMQogICAgZnJhbWVfZGlnIDAKICAgIDwKICAgIGJ6IGJ1cm5fb3BzX3JlYWRvbmx5X2FmdGVyX2ZvckA0CiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6MjEKICAgIC8vIHNxcnQgPSBvcC5ic3FydChCaWdVSW50KGkpKQogICAgZnJhbWVfZGlnIDEKICAgIGR1cAogICAgaXRvYgogICAgYnNxcnQKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToyMgogICAgLy8gYXNzZXJ0KHNxcnQgPj0gMCkgIyBQcmV2ZW50IG9wdGltaXNlciByZW1vdmluZyB0aGUgc3FydAogICAgYnl0ZWNfMiAvLyAweAogICAgYj49CiAgICBhc3NlcnQKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToyMAogICAgLy8gZm9yIGkgaW4gdXJhbmdlKGNvdW50KToKICAgIGludGNfMCAvLyAxCiAgICArCiAgICBmcmFtZV9idXJ5IDEKICAgIGIgYnVybl9vcHNfcmVhZG9ubHlfZm9yX2hlYWRlckAxCgpidXJuX29wc19yZWFkb25seV9hZnRlcl9mb3JANDoKICAgIHJldHN1YgoKCi8vIGNvbnRyYWN0LklubmVyRmVlQ29udHJhY3QuYnVybl9vcHMob3BfYnVkZ2V0OiB1aW50NjQpIC0+IHZvaWQ6CmJ1cm5fb3BzOgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjI0LTI1CiAgICAvLyBAYXJjNC5hYmltZXRob2QoKQogICAgLy8gZGVmIGJ1cm5fb3BzKHNlbGYsIG9wX2J1ZGdldDogVUludDY0KSAtPiBOb25lOgogICAgcHJvdG8gMSAwCiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6MjYtMjcKICAgIC8vICMgVXNlcyBhcHByb3ggNjAgb3AgYnVkZ2V0IHBlciBpdGVyYXRpb24KICAgIC8vIGNvdW50ID0gb3BfYnVkZ2V0IC8vIDYwCiAgICBmcmFtZV9kaWcgLTEKICAgIGludGNfMyAvLyA2MAogICAgLwogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjI4CiAgICAvLyBlbnN1cmVfYnVkZ2V0KG9wX2J1ZGdldCkKICAgIGZyYW1lX2RpZyAtMQogICAgaW50Y18xIC8vIDAKICAgIGNhbGxzdWIgZW5zdXJlX2J1ZGdldAogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjI5CiAgICAvLyBmb3IgaSBpbiB1cmFuZ2UoY291bnQpOgogICAgaW50Y18xIC8vIDAKCmJ1cm5fb3BzX2Zvcl9oZWFkZXJAMToKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToyOQogICAgLy8gZm9yIGkgaW4gdXJhbmdlKGNvdW50KToKICAgIGZyYW1lX2RpZyAxCiAgICBmcmFtZV9kaWcgMAogICAgPAogICAgYnogYnVybl9vcHNfYWZ0ZXJfZm9yQDQKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weTozMAogICAgLy8gc3FydCA9IG9wLmJzcXJ0KEJpZ1VJbnQoaSkpCiAgICBmcmFtZV9kaWcgMQogICAgZHVwCiAgICBpdG9iCiAgICBic3FydAogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjMxCiAgICAvLyBhc3NlcnQoc3FydCA+PSAwKSAjIFByZXZlbnQgb3B0aW1pc2VyIHJlbW92aW5nIHRoZSBzcXJ0CiAgICBieXRlY18yIC8vIDB4CiAgICBiPj0KICAgIGFzc2VydAogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjI5CiAgICAvLyBmb3IgaSBpbiB1cmFuZ2UoY291bnQpOgogICAgaW50Y18wIC8vIDEKICAgICsKICAgIGZyYW1lX2J1cnkgMQogICAgYiBidXJuX29wc19mb3JfaGVhZGVyQDEKCmJ1cm5fb3BzX2FmdGVyX2ZvckA0OgogICAgcmV0c3ViCgoKLy8gY29udHJhY3QuSW5uZXJGZWVDb250cmFjdC5zZW5kX3hfaW5uZXJzX3dpdGhfZmVlcyhhcHBfaWQ6IHVpbnQ2NCwgZmVlczogYnl0ZXMpIC0+IHZvaWQ6CnNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzOgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjM3LTM4CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIC8vIGRlZiBzZW5kX3hfaW5uZXJzX3dpdGhfZmVlcyhzZWxmLCBhcHBfaWQ6IFVJbnQ2NCwgZmVlczogYXJjNC5EeW5hbWljQXJyYXlbYXJjNC5VSW50NjRdKSAtPiBOb25lOgogICAgcHJvdG8gMiAwCiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6MzkKICAgIC8vIGZvciBmZWUgaW4gZmVlczoKICAgIGZyYW1lX2RpZyAtMQogICAgaW50Y18xIC8vIDAKICAgIGV4dHJhY3RfdWludDE2CiAgICBpbnRjXzEgLy8gMAoKc2VuZF94X2lubmVyc193aXRoX2ZlZXNfZm9yX2hlYWRlckAxOgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjM5CiAgICAvLyBmb3IgZmVlIGluIGZlZXM6CiAgICBmcmFtZV9kaWcgMQogICAgZnJhbWVfZGlnIDAKICAgIDwKICAgIGJ6IHNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzX2FmdGVyX2ZvckA1CiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMiAwCiAgICBmcmFtZV9kaWcgMQogICAgZHVwCiAgICBjb3ZlciAyCiAgICBwdXNoaW50IDggLy8gOAogICAgKgogICAgcHVzaGludCA4IC8vIDgKICAgIGV4dHJhY3QzIC8vIG9uIGVycm9yOiBJbmRleCBhY2Nlc3MgaXMgb3V0IG9mIGJvdW5kcwogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjQwCiAgICAvLyBhcmM0LmFiaV9jYWxsKCdub19vcCcsIGFwcF9pZD1hcHBfaWQsIGZlZT1mZWUubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgYnRvaQogICAgZnJhbWVfZGlnIC0yCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKICAgIGJ5dGVjXzAgLy8gbWV0aG9kICJub19vcCgpdm9pZCIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBpbnRjXzIgLy8gYXBwbAogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaXR4bl9maWVsZCBGZWUKICAgIGl0eG5fc3VibWl0CiAgICBpbnRjXzAgLy8gMQogICAgKwogICAgZnJhbWVfYnVyeSAxCiAgICBiIHNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzX2Zvcl9oZWFkZXJAMQoKc2VuZF94X2lubmVyc193aXRoX2ZlZXNfYWZ0ZXJfZm9yQDU6CiAgICByZXRzdWIKCgovLyBjb250cmFjdC5Jbm5lckZlZUNvbnRyYWN0LnNlbmRfaW5uZXJzX3dpdGhfZmVlcyhhcHBfaWRfMTogdWludDY0LCBhcHBfaWRfMjogdWludDY0LCBmZWVzOiBieXRlcykgLT4gdm9pZDoKc2VuZF9pbm5lcnNfd2l0aF9mZWVzOgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjQyLTQzCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIC8vIGRlZiBzZW5kX2lubmVyc193aXRoX2ZlZXMoc2VsZiwgYXBwX2lkXzE6IFVJbnQ2NCwgYXBwX2lkXzI6IFVJbnQ2NCwgZmVlczogYXJjNC5UdXBsZVthcmM0LlVJbnQ2NCwgYXJjNC5VSW50NjQsIGFyYzQuVUludDY0LCBhcmM0LlVJbnQ2NCwgYXJjNC5EeW5hbWljQXJyYXlbYXJjNC5VSW50NjRdXSkgLT4gTm9uZToKICAgIHByb3RvIDMgMAogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjQ0CiAgICAvLyBhcmM0LmFiaV9jYWxsKCdub19vcCcsIGFwcF9pZD1hcHBfaWRfMSwgZmVlPWZlZXNbMF0ubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgZnJhbWVfZGlnIC0xCiAgICBleHRyYWN0IDAgOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18wIC8vIG1ldGhvZCAibm9fb3AoKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18yIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjQ1CiAgICAvLyBhcmM0LmFiaV9jYWxsKCdub19vcCcsIGFwcF9pZD1hcHBfaWRfMSwgZmVlPWZlZXNbMV0ubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgZnJhbWVfZGlnIC0xCiAgICBleHRyYWN0IDggOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18wIC8vIG1ldGhvZCAibm9fb3AoKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18yIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjQ2LTUwCiAgICAvLyBpdHhuLlBheW1lbnQoCiAgICAvLyAgICAgYW1vdW50PTAsCiAgICAvLyAgICAgcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICBmZWU9ZmVlc1syXS5uYXRpdmUKICAgIC8vICkuc3VibWl0KCkKICAgIGl0eG5fYmVnaW4KICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weTo0OQogICAgLy8gZmVlPWZlZXNbMl0ubmF0aXZlCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMTYgOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weTo0OAogICAgLy8gcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICBpdHhuX2ZpZWxkIFJlY2VpdmVyCiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6NDcKICAgIC8vIGFtb3VudD0wLAogICAgaW50Y18xIC8vIDAKICAgIGl0eG5fZmllbGQgQW1vdW50CiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6NDYKICAgIC8vIGl0eG4uUGF5bWVudCgKICAgIGludGNfMCAvLyBwYXkKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6NDYtNTAKICAgIC8vIGl0eG4uUGF5bWVudCgKICAgIC8vICAgICBhbW91bnQ9MCwKICAgIC8vICAgICByZWNlaXZlcj1HbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzLAogICAgLy8gICAgIGZlZT1mZWVzWzJdLm5hdGl2ZQogICAgLy8gKS5zdWJtaXQoKQogICAgaXR4bl9zdWJtaXQKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weTo1MQogICAgLy8gYXJjNC5hYmlfY2FsbCgnc2VuZF94X2lubmVyc193aXRoX2ZlZXMnLCBhcHBfaWRfMiwgZmVlc1s0XSwgYXBwX2lkPWFwcF9pZF8xLCBmZWU9ZmVlc1szXS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMjQgOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMgogICAgaXRvYgogICAgZnJhbWVfZGlnIC0xCiAgICBwdXNoaW50IDMyIC8vIDMyCiAgICBleHRyYWN0X3VpbnQxNgogICAgZnJhbWVfZGlnIC0xCiAgICBsZW4KICAgIGZyYW1lX2RpZyAtMQogICAgY292ZXIgMgogICAgc3Vic3RyaW5nMwogICAgZnJhbWVfZGlnIC0zCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKICAgIGJ5dGVjXzEgLy8gbWV0aG9kICJzZW5kX3hfaW5uZXJzX3dpdGhfZmVlcyh1aW50NjQsdWludDY0W10pdm9pZCIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBzd2FwCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGludGNfMiAvLyBhcHBsCiAgICBpdHhuX2ZpZWxkIFR5cGVFbnVtCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgaXR4bl9zdWJtaXQKICAgIHJldHN1YgoKCi8vIGNvbnRyYWN0LklubmVyRmVlQ29udHJhY3Quc2VuZF9pbm5lcnNfd2l0aF9mZWVzXzIoYXBwX2lkXzE6IHVpbnQ2NCwgYXBwX2lkXzI6IHVpbnQ2NCwgZmVlczogYnl0ZXMpIC0+IHZvaWQ6CnNlbmRfaW5uZXJzX3dpdGhfZmVlc18yOgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjUzLTU0CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIC8vIGRlZiBzZW5kX2lubmVyc193aXRoX2ZlZXNfMihzZWxmLCBhcHBfaWRfMTogVUludDY0LCBhcHBfaWRfMjogVUludDY0LCBmZWVzOiBhcmM0LlR1cGxlW2FyYzQuVUludDY0LCBhcmM0LlVJbnQ2NCwgYXJjNC5EeW5hbWljQXJyYXlbYXJjNC5VSW50NjRdLCBhcmM0LlVJbnQ2NCwgYXJjNC5VSW50NjQsIGFyYzQuRHluYW1pY0FycmF5W2FyYzQuVUludDY0XV0pIC0+IE5vbmU6CiAgICBwcm90byAzIDAKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weTo1NQogICAgLy8gYXJjNC5hYmlfY2FsbCgnbm9fb3AnLCBhcHBfaWQ9YXBwX2lkXzEsIGZlZT1mZWVzWzBdLm5hdGl2ZSkKICAgIGl0eG5fYmVnaW4KICAgIGZyYW1lX2RpZyAtMQogICAgZXh0cmFjdCAwIDggLy8gb24gZXJyb3I6IEluZGV4IGFjY2VzcyBpcyBvdXQgb2YgYm91bmRzCiAgICBidG9pCiAgICBmcmFtZV9kaWcgLTMKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25JRAogICAgYnl0ZWNfMCAvLyBtZXRob2QgIm5vX29wKCl2b2lkIgogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGludGNfMiAvLyBhcHBsCiAgICBpdHhuX2ZpZWxkIFR5cGVFbnVtCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgaXR4bl9zdWJtaXQKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weTo1NgogICAgLy8gYXJjNC5hYmlfY2FsbCgnc2VuZF94X2lubmVyc193aXRoX2ZlZXMnLCBhcHBfaWRfMiwgZmVlc1syXSwgYXBwX2lkPWFwcF9pZF8xLCBmZWU9ZmVlc1sxXS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgOCA4IC8vIG9uIGVycm9yOiBJbmRleCBhY2Nlc3MgaXMgb3V0IG9mIGJvdW5kcwogICAgYnRvaQogICAgZnJhbWVfZGlnIC0yCiAgICBpdG9iCiAgICBmcmFtZV9kaWcgLTEKICAgIHB1c2hpbnQgMTYgLy8gMTYKICAgIGV4dHJhY3RfdWludDE2CiAgICBmcmFtZV9kaWcgLTEKICAgIHB1c2hpbnQgMzQgLy8gMzQKICAgIGV4dHJhY3RfdWludDE2CiAgICBmcmFtZV9kaWcgLTEKICAgIHVuY292ZXIgMgogICAgZGlnIDIKICAgIHN1YnN0cmluZzMKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18xIC8vIG1ldGhvZCAic2VuZF94X2lubmVyc193aXRoX2ZlZXModWludDY0LHVpbnQ2NFtdKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgZGlnIDIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18yIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIHVuY292ZXIgMgogICAgaXR4bl9maWVsZCBGZWUKICAgIGl0eG5fc3VibWl0CiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6NTcKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ25vX29wJywgYXBwX2lkPWFwcF9pZF8xLCBmZWU9ZmVlc1szXS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMTggOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18wIC8vIG1ldGhvZCAibm9fb3AoKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18yIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjU4CiAgICAvLyBhcmM0LmFiaV9jYWxsKCdzZW5kX3hfaW5uZXJzX3dpdGhfZmVlcycsIGFwcF9pZF8yLCBmZWVzWzVdLCBhcHBfaWQ9YXBwX2lkXzEsIGZlZT1mZWVzWzRdLm5hdGl2ZSkKICAgIGl0eG5fYmVnaW4KICAgIGZyYW1lX2RpZyAtMQogICAgZXh0cmFjdCAyNiA4IC8vIG9uIGVycm9yOiBJbmRleCBhY2Nlc3MgaXMgb3V0IG9mIGJvdW5kcwogICAgYnRvaQogICAgZnJhbWVfZGlnIC0xCiAgICBsZW4KICAgIGZyYW1lX2RpZyAtMQogICAgdW5jb3ZlciAzCiAgICB1bmNvdmVyIDIKICAgIHN1YnN0cmluZzMKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18xIC8vIG1ldGhvZCAic2VuZF94X2lubmVyc193aXRoX2ZlZXModWludDY0LHVpbnQ2NFtdKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgdW5jb3ZlciAyCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGludGNfMiAvLyBhcHBsCiAgICBpdHhuX2ZpZWxkIFR5cGVFbnVtCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgaXR4bl9zdWJtaXQKICAgIHJldHN1YgoKCi8vIF9wdXlhX2xpYi51dGlsLmVuc3VyZV9idWRnZXQocmVxdWlyZWRfYnVkZ2V0OiB1aW50NjQsIGZlZV9zb3VyY2U6IHVpbnQ2NCkgLT4gdm9pZDoKZW5zdXJlX2J1ZGdldDoKICAgIHByb3RvIDIgMAogICAgZnJhbWVfZGlnIC0yCiAgICBwdXNoaW50IDEwIC8vIDEwCiAgICArCgplbnN1cmVfYnVkZ2V0X3doaWxlX3RvcEAxOgogICAgZnJhbWVfZGlnIDAKICAgIGdsb2JhbCBPcGNvZGVCdWRnZXQKICAgID4KICAgIGJ6IGVuc3VyZV9idWRnZXRfYWZ0ZXJfd2hpbGVANwogICAgaXR4bl9iZWdpbgogICAgaW50Y18yIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIHB1c2hpbnQgNSAvLyBEZWxldGVBcHBsaWNhdGlvbgogICAgaXR4bl9maWVsZCBPbkNvbXBsZXRpb24KICAgIGJ5dGVjXzMgLy8gMHgwNjgxMDEKICAgIGl0eG5fZmllbGQgQXBwcm92YWxQcm9ncmFtCiAgICBieXRlY18zIC8vIDB4MDY4MTAxCiAgICBpdHhuX2ZpZWxkIENsZWFyU3RhdGVQcm9ncmFtCiAgICBmcmFtZV9kaWcgLTEKICAgIHN3aXRjaCBlbnN1cmVfYnVkZ2V0X3N3aXRjaF9jYXNlXzBAMyBlbnN1cmVfYnVkZ2V0X3N3aXRjaF9jYXNlXzFANAoKZW5zdXJlX2J1ZGdldF9zd2l0Y2hfY2FzZV9uZXh0QDY6CiAgICBpdHhuX3N1Ym1pdAogICAgYiBlbnN1cmVfYnVkZ2V0X3doaWxlX3RvcEAxCgplbnN1cmVfYnVkZ2V0X3N3aXRjaF9jYXNlXzFANDoKICAgIGdsb2JhbCBNaW5UeG5GZWUKICAgIGl0eG5fZmllbGQgRmVlCiAgICBiIGVuc3VyZV9idWRnZXRfc3dpdGNoX2Nhc2VfbmV4dEA2CgplbnN1cmVfYnVkZ2V0X3N3aXRjaF9jYXNlXzBAMzoKICAgIGludGNfMSAvLyAwCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgYiBlbnN1cmVfYnVkZ2V0X3N3aXRjaF9jYXNlX25leHRANgoKZW5zdXJlX2J1ZGdldF9hZnRlcl93aGlsZUA3OgogICAgcmV0c3ViCg==", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBhbGdvcHkuYXJjNC5BUkM0Q29udHJhY3QuY2xlYXJfc3RhdGVfcHJvZ3JhbSgpIC0+IHVpbnQ2NDoKbWFpbjoKICAgIHB1c2hpbnQgMSAvLyAxCiAgICByZXR1cm4K" + }, + "byteCode": { + "approval": "CiAEAQAGPCYEBHcp6zIEwsSJ5QADBoEBMRtBAJeCAgSdiSnHBN03gkcoKYICBDQ2gs0EHPL1kDYaAI4GAFwATABDADAAGQACI0MxGRREMRhENhoBFzYaAhc2GgOIAUwiQzEZFEQxGEQ2GgEXNhoCFzYaA4gAzCJDMRkURDEYRDYaARc2GgKIAIIiQzEZFEQxGEQiQzEZFEQxGEQ2GgEXiABDIkMxGRREMRhENhoBF4gADSJDMRlA/48xGBREIkOKAQCL/yUKi/8jiAFfI4sBiwAMQQAPiwFJFpYqp0QiCIwBQv/piYoBAIv/JQqL/yOIATkjiwGLAAxBAA+LAUkWliqnRCIIjAFC/+mJigIAi/8jWSOLAYsADEEAJov/VwIAiwFJTgKBCAuBCFixF4v+shgoshokshCyAbMiCIwBQv/SiYoDALGL/1cACBeL/bIYKLIaJLIQsgGzsYv/VwgIF4v9shgoshokshCyAbOxi/9XEAgXMgqyByOyCCKyELIBs7GL/1cYCBeL/haL/4EgWYv/FYv/TgJSi/2yGCmyGkyyGrIaJLIQsgGziYoDALGL/1cACBeL/bIYKLIaJLIQsgGzsYv/VwgIF4v+Fov/gRBZi/+BIlmL/08CSwJSi/2yGCmyGksCshqyGiSyEE8CsgGzsYv/VxIIF4v9shgoshokshCyAbOxi/9XGggXi/8Vi/9PA08CUov9shgpshpPArIashokshCyAbOJigIAi/6BCgiLADIMDUEAJ7EkshCBBbIZK7IeK7Ifi/+NAgALAASzQv/eMgCyAUL/9SOyAUL/74k=", + "clear": "CoEBQw==" + }, + "compilerInfo": { + "compiler": "puya", + "compilerVersion": { + "major": 4, + "minor": 1, + "patch": 1 + } }, "events": [], "templateVariables": {} diff --git a/tests/artifacts/inner-fee/contract.py b/tests/artifacts/inner-fee/contract.py index bdc1bf4f..bd991895 100644 --- a/tests/artifacts/inner-fee/contract.py +++ b/tests/artifacts/inner-fee/contract.py @@ -12,7 +12,16 @@ class InnerFeeContract(ARC4Contract): - @arc4.abimethod + @arc4.abimethod(readonly=True) + def burn_ops_readonly(self, op_budget: UInt64) -> None: + # Uses approx 60 op budget per iteration + count = op_budget // 60 + ensure_budget(op_budget) + for i in urange(count): + sqrt = op.bsqrt(BigUInt(i)) + assert sqrt >= 0 # Prevent optimiser removing the sqrt + + @arc4.abimethod() def burn_ops(self, op_budget: UInt64) -> None: # Uses approx 60 op budget per iteration count = op_budget // 60 diff --git a/tests/transactions/test_resource_packing.py b/tests/transactions/test_resource_packing.py index cb277d26..ca6b08c4 100644 --- a/tests/transactions/test_resource_packing.py +++ b/tests/transactions/test_resource_packing.py @@ -506,6 +506,42 @@ def setup(self, algorand: AlgorandClient, funded_account: SigningAccount) -> Gen config.configure(populate_app_call_resources=False) + def test_runs_auto_opup_implicitly_on_readonly_calls(self) -> None: + """Test that auto top-up is run implicitly on readonly calls""" + + # Below must pass without explicit `populate_app_call_resources` flag + # Passing 'cover_app_call_inner_transaction_fees' is not required for readonly calls + self.app_client1.send.call( + AppClientMethodCallParams( + args=[6200], + method="burn_ops_readonly", + ), + ) + + # For fresh accounts with balance lower than max dummy assumed max_fee pre filled when no + # max_fee provided explicitly, it will fail + new_account_with_less_than_10_algo = self.app_client1.algorand.account.random() + self.app_client1.algorand.account.ensure_funded_from_environment( + account_to_fund=new_account_with_less_than_10_algo, min_spending_balance=AlgoAmount.from_algos(5) + ) + with pytest.raises(ValueError, match=r"tried to spend \{10000000\}"): + self.app_client1.send.call( + AppClientMethodCallParams( + args=[6200], method="burn_ops_readonly", sender=new_account_with_less_than_10_algo.address + ), + ) + + # But user can explicitly set a max_fee value in such cases + # while not having to set cover_app_call_inner_transaction_fees + self.app_client1.send.call( + AppClientMethodCallParams( + args=[6200], + method="burn_ops_readonly", + max_fee=AlgoAmount.from_micro_algos(10000), + signer=new_account_with_less_than_10_algo.signer, + ), + ) + def test_throws_when_no_max_fee(self) -> None: """Test that error is thrown when no max fee is supplied""" with pytest.raises(ValueError, match="Please provide a `max_fee` for each app call transaction"): From f74a0175bb34e3197a7a5a800681660b74090e4f Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Mon, 3 Feb 2025 23:50:47 +0100 Subject: [PATCH 3/7] refactor: remove plural forms from AlgoAmount --- .../algokit_utils/models/amount/index.md | 58 +----------- .../algokit_utils/models/network/index.md | 4 + docs/markdown/capabilities/amount.md | 12 +-- docs/source/capabilities/amount.md | 12 +-- src/algokit_utils/models/amount.py | 94 ++++--------------- .../transactions/transaction_composer.py | 14 +-- tests/accounts/test_account_manager.py | 6 +- tests/applications/test_app_client.py | 24 ++--- tests/applications/test_app_factory.py | 2 +- tests/applications/test_app_manager.py | 2 +- tests/assets/test_asset_manager.py | 8 +- .../clients/algorand_client/test_transfer.py | 54 +++++------ tests/models/test_algo_amount.py | 75 +++++++-------- tests/test_debug_utils.py | 2 +- tests/transactions/test_resource_packing.py | 88 ++++++++--------- .../transactions/test_transaction_composer.py | 26 ++--- .../transactions/test_transaction_creator.py | 8 +- tests/transactions/test_transaction_sender.py | 10 +- 18 files changed, 197 insertions(+), 302 deletions(-) diff --git a/docs/markdown/autoapi/algokit_utils/models/amount/index.md b/docs/markdown/autoapi/algokit_utils/models/amount/index.md index a1fde03c..d865b298 100644 --- a/docs/markdown/autoapi/algokit_utils/models/amount/index.md +++ b/docs/markdown/autoapi/algokit_utils/models/amount/index.md @@ -19,12 +19,8 @@ ## Module Contents -### *class* algokit_utils.models.amount.AlgoAmount(\*, micro_algos: int) - ### *class* algokit_utils.models.amount.AlgoAmount(\*, micro_algo: int) -### *class* algokit_utils.models.amount.AlgoAmount(\*, algos: int | decimal.Decimal) - ### *class* algokit_utils.models.amount.AlgoAmount(\*, algo: int | decimal.Decimal) Wrapper class to ensure safe, explicit conversion between µAlgo, Algo and numbers. @@ -32,23 +28,12 @@ Wrapper class to ensure safe, explicit conversion between µAlgo, Algo and numbe * **Example:** ```pycon ->>> amount = AlgoAmount(algos=1) >>> amount = AlgoAmount(algo=1) ->>> amount = AlgoAmount.from_algos(1) >>> amount = AlgoAmount.from_algo(1) ->>> amount = AlgoAmount(micro_algos=1_000_000) >>> amount = AlgoAmount(micro_algo=1_000_000) ->>> amount = AlgoAmount.from_micro_algos(1_000_000) >>> amount = AlgoAmount.from_micro_algo(1_000_000) ``` -#### *property* micro_algos *: int* - -Return the amount as a number in µAlgo. - -* **Returns:** - The amount in µAlgo. - #### *property* micro_algo *: int* Return the amount as a number in µAlgo. @@ -56,13 +41,6 @@ Return the amount as a number in µAlgo. * **Returns:** The amount in µAlgo. -#### *property* algos *: decimal.Decimal* - -Return the amount as a number in Algo. - -* **Returns:** - The amount in Algo. - #### *property* algo *: decimal.Decimal* Return the amount as a number in Algo. @@ -70,20 +48,6 @@ Return the amount as a number in Algo. * **Returns:** The amount in Algo. -#### *static* from_algos(amount: int | decimal.Decimal) → [AlgoAmount](#algokit_utils.models.amount.AlgoAmount) - -Create an AlgoAmount object representing the given number of Algo. - -* **Parameters:** - **amount** – The amount in Algo. -* **Returns:** - An AlgoAmount instance. -* **Example:** - -```pycon ->>> amount = AlgoAmount.from_algos(1) -``` - #### *static* from_algo(amount: int | decimal.Decimal) → [AlgoAmount](#algokit_utils.models.amount.AlgoAmount) Create an AlgoAmount object representing the given number of Algo. @@ -98,20 +62,6 @@ Create an AlgoAmount object representing the given number of Algo. >>> amount = AlgoAmount.from_algo(1) ``` -#### *static* from_micro_algos(amount: int) → [AlgoAmount](#algokit_utils.models.amount.AlgoAmount) - -Create an AlgoAmount object representing the given number of µAlgo. - -* **Parameters:** - **amount** – The amount in µAlgo. -* **Returns:** - An AlgoAmount instance. -* **Example:** - -```pycon ->>> amount = AlgoAmount.from_micro_algos(1_000_000) -``` - #### *static* from_micro_algo(amount: int) → [AlgoAmount](#algokit_utils.models.amount.AlgoAmount) Create an AlgoAmount object representing the given number of µAlgo. @@ -126,21 +76,21 @@ Create an AlgoAmount object representing the given number of µAlgo. >>> amount = AlgoAmount.from_micro_algo(1_000_000) ``` -### algokit_utils.models.amount.algo(algos: int) → [AlgoAmount](#algokit_utils.models.amount.AlgoAmount) +### algokit_utils.models.amount.algo(algo: int) → [AlgoAmount](#algokit_utils.models.amount.AlgoAmount) Create an AlgoAmount object representing the given number of Algo. * **Parameters:** - **algos** – The number of Algo to create an AlgoAmount object for. + **algo** – The number of Algo to create an AlgoAmount object for. * **Returns:** An AlgoAmount object representing the given number of Algo. -### algokit_utils.models.amount.micro_algo(microalgos: int) → [AlgoAmount](#algokit_utils.models.amount.AlgoAmount) +### algokit_utils.models.amount.micro_algo(micro_algo: int) → [AlgoAmount](#algokit_utils.models.amount.AlgoAmount) Create an AlgoAmount object representing the given number of µAlgo. * **Parameters:** - **microalgos** – The number of µAlgo to create an AlgoAmount object for. + **micro_algo** – The number of µAlgo to create an AlgoAmount object for. * **Returns:** An AlgoAmount object representing the given number of µAlgo. diff --git a/docs/markdown/autoapi/algokit_utils/models/network/index.md b/docs/markdown/autoapi/algokit_utils/models/network/index.md index b49c51a8..8b67ed9f 100644 --- a/docs/markdown/autoapi/algokit_utils/models/network/index.md +++ b/docs/markdown/autoapi/algokit_utils/models/network/index.md @@ -23,6 +23,10 @@ API Token to authenticate with the service e.g ‘4001’ or ‘8980’ #### port *: str | int | None* *= None* +#### full_url() → str + +Returns the full URL for the service + ### *class* algokit_utils.models.network.AlgoClientConfigs #### algod_config *: [AlgoClientNetworkConfig](#algokit_utils.models.network.AlgoClientNetworkConfig)* diff --git a/docs/markdown/capabilities/amount.md b/docs/markdown/capabilities/amount.md index 41e2f683..bdf607ff 100644 --- a/docs/markdown/capabilities/amount.md +++ b/docs/markdown/capabilities/amount.md @@ -21,18 +21,18 @@ from algokit_utils import AlgoAmount There are a few ways to create an `AlgoAmount`: - Algo - - Constructor: `AlgoAmount(algo=10)` or `AlgoAmount(algos=10)` - - Static helper: `AlgoAmount.from_algo(10)` or `AlgoAmount.from_algos(10)` + - Constructor: `AlgoAmount(algo=10)` + - Static helper: `AlgoAmount.from_algo(10)` - microAlgo - - Constructor: `AlgoAmount(micro_algo=10_000)` or `AlgoAmount(micro_algos=10_000)` - - Static helper: `AlgoAmount.from_micro_algo(10_000)` or `AlgoAmount.from_micro_algos(10_000)` + - Constructor: `AlgoAmount(micro_algo=10_000)` + - Static helper: `AlgoAmount.from_micro_algo(10_000)` ### Extracting a value from `AlgoAmount` The `AlgoAmount` class has properties to return Algo and microAlgo: -- `amount.algo` or `amount.algos` - Returns the value in Algo as a python `Decimal` object -- `amount.micro_algo` or `amount.micro_algos` - Returns the value in microAlgo as an integer +- `amount.algo` - Returns the value in Algo as a python `Decimal` object +- `amount.micro_algo` - Returns the value in microAlgo as an integer `AlgoAmount` will coerce to an integer automatically (in microAlgo) when using `int(amount)`, which allows you to use `AlgoAmount` objects in comparison operations such as `<` and `>=` etc. diff --git a/docs/source/capabilities/amount.md b/docs/source/capabilities/amount.md index e9b9bdcc..9c79ba58 100644 --- a/docs/source/capabilities/amount.md +++ b/docs/source/capabilities/amount.md @@ -21,18 +21,18 @@ from algokit_utils import AlgoAmount There are a few ways to create an `AlgoAmount`: - Algo - - Constructor: `AlgoAmount(algo=10)` or `AlgoAmount(algos=10)` - - Static helper: `AlgoAmount.from_algo(10)` or `AlgoAmount.from_algos(10)` + - Constructor: `AlgoAmount(algo=10)` + - Static helper: `AlgoAmount.from_algo(10)` - microAlgo - - Constructor: `AlgoAmount(micro_algo=10_000)` or `AlgoAmount(micro_algos=10_000)` - - Static helper: `AlgoAmount.from_micro_algo(10_000)` or `AlgoAmount.from_micro_algos(10_000)` + - Constructor: `AlgoAmount(micro_algo=10_000)` + - Static helper: `AlgoAmount.from_micro_algo(10_000)` ### Extracting a value from `AlgoAmount` The `AlgoAmount` class has properties to return Algo and microAlgo: -- `amount.algo` or `amount.algos` - Returns the value in Algo as a python `Decimal` object -- `amount.micro_algo` or `amount.micro_algos` - Returns the value in microAlgo as an integer +- `amount.algo` - Returns the value in Algo as a python `Decimal` object +- `amount.micro_algo` - Returns the value in microAlgo as an integer `AlgoAmount` will coerce to an integer automatically (in microAlgo) when using `int(amount)`, which allows you to use `AlgoAmount` objects in comparison operations such as `<` and `>=` etc. diff --git a/src/algokit_utils/models/amount.py b/src/algokit_utils/models/amount.py index 37fbf73f..11cd6d07 100644 --- a/src/algokit_utils/models/amount.py +++ b/src/algokit_utils/models/amount.py @@ -13,58 +13,34 @@ class AlgoAmount: """Wrapper class to ensure safe, explicit conversion between µAlgo, Algo and numbers. :example: - >>> amount = AlgoAmount(algos=1) >>> amount = AlgoAmount(algo=1) - >>> amount = AlgoAmount.from_algos(1) >>> amount = AlgoAmount.from_algo(1) - >>> amount = AlgoAmount(micro_algos=1_000_000) >>> amount = AlgoAmount(micro_algo=1_000_000) - >>> amount = AlgoAmount.from_micro_algos(1_000_000) >>> amount = AlgoAmount.from_micro_algo(1_000_000) """ - @overload - def __init__(self, *, micro_algos: int) -> None: ... - @overload def __init__(self, *, micro_algo: int) -> None: ... - @overload - def __init__(self, *, algos: int | Decimal) -> None: ... - @overload def __init__(self, *, algo: int | Decimal) -> None: ... def __init__( self, *, - micro_algos: int | None = None, micro_algo: int | None = None, - algos: int | Decimal | None = None, algo: int | Decimal | None = None, ): - if micro_algos is None and micro_algo is None and algos is None and algo is None: + if micro_algo is None and algo is None: raise ValueError("No amount provided") - if micro_algos is not None: - self.amount_in_micro_algo = int(micro_algos) - elif micro_algo is not None: + if micro_algo is not None: self.amount_in_micro_algo = int(micro_algo) - elif algos is not None: - self.amount_in_micro_algo = int(algos * algosdk.constants.MICROALGOS_TO_ALGOS_RATIO) elif algo is not None: self.amount_in_micro_algo = int(algo * algosdk.constants.MICROALGOS_TO_ALGOS_RATIO) else: raise ValueError("Invalid amount provided") - @property - def micro_algos(self) -> int: - """Return the amount as a number in µAlgo. - - :returns: The amount in µAlgo. - """ - return self.amount_in_micro_algo - @property def micro_algo(self) -> int: """Return the amount as a number in µAlgo. @@ -73,14 +49,6 @@ def micro_algo(self) -> int: """ return self.amount_in_micro_algo - @property - def algos(self) -> Decimal: - """Return the amount as a number in Algo. - - :returns: The amount in Algo. - """ - return algosdk.util.microalgos_to_algos(self.amount_in_micro_algo) # type: ignore[no-any-return] - @property def algo(self) -> Decimal: """Return the amount as a number in Algo. @@ -89,18 +57,6 @@ def algo(self) -> Decimal: """ return algosdk.util.microalgos_to_algos(self.amount_in_micro_algo) # type: ignore[no-any-return] - @staticmethod - def from_algos(amount: int | Decimal) -> AlgoAmount: - """Create an AlgoAmount object representing the given number of Algo. - - :param amount: The amount in Algo. - :returns: An AlgoAmount instance. - - :example: - >>> amount = AlgoAmount.from_algos(1) - """ - return AlgoAmount(algos=amount) - @staticmethod def from_algo(amount: int | Decimal) -> AlgoAmount: """Create an AlgoAmount object representing the given number of Algo. @@ -113,18 +69,6 @@ def from_algo(amount: int | Decimal) -> AlgoAmount: """ return AlgoAmount(algo=amount) - @staticmethod - def from_micro_algos(amount: int) -> AlgoAmount: - """Create an AlgoAmount object representing the given number of µAlgo. - - :param amount: The amount in µAlgo. - :returns: An AlgoAmount instance. - - :example: - >>> amount = AlgoAmount.from_micro_algos(1_000_000) - """ - return AlgoAmount(micro_algos=amount) - @staticmethod def from_micro_algo(amount: int) -> AlgoAmount: """Create an AlgoAmount object representing the given number of µAlgo. @@ -141,21 +85,21 @@ def __str__(self) -> str: return f"{self.micro_algo:,} µALGO" def __int__(self) -> int: - return self.micro_algos + return self.micro_algo def __add__(self, other: AlgoAmount) -> AlgoAmount: if isinstance(other, AlgoAmount): - total_micro_algos = self.micro_algos + other.micro_algos + total_micro_algos = self.micro_algo + other.micro_algo else: raise TypeError(f"Unsupported operand type(s) for +: 'AlgoAmount' and '{type(other).__name__}'") - return AlgoAmount.from_micro_algos(total_micro_algos) + return AlgoAmount.from_micro_algo(total_micro_algos) def __radd__(self, other: AlgoAmount) -> AlgoAmount: return self.__add__(other) def __iadd__(self, other: AlgoAmount) -> Self: if isinstance(other, AlgoAmount): - self.amount_in_micro_algo += other.micro_algos + self.amount_in_micro_algo += other.micro_algo else: raise TypeError(f"Unsupported operand type(s) for +: 'AlgoAmount' and '{type(other).__name__}'") return self @@ -204,42 +148,42 @@ def __ge__(self, other: object) -> bool: def __sub__(self, other: AlgoAmount) -> AlgoAmount: if isinstance(other, AlgoAmount): - total_micro_algos = self.micro_algos - other.micro_algos + total_micro_algos = self.micro_algo - other.micro_algo else: raise TypeError(f"Unsupported operand type(s) for -: 'AlgoAmount' and '{type(other).__name__}'") - return AlgoAmount.from_micro_algos(total_micro_algos) + return AlgoAmount.from_micro_algo(total_micro_algos) def __rsub__(self, other: int) -> AlgoAmount: if isinstance(other, (int)): - total_micro_algos = int(other) - self.micro_algos - return AlgoAmount.from_micro_algos(total_micro_algos) + total_micro_algos = int(other) - self.micro_algo + return AlgoAmount.from_micro_algo(total_micro_algos) raise TypeError(f"Unsupported operand type(s) for -: '{type(other).__name__}' and 'AlgoAmount'") def __isub__(self, other: AlgoAmount) -> Self: if isinstance(other, AlgoAmount): - self.amount_in_micro_algo -= other.micro_algos + self.amount_in_micro_algo -= other.micro_algo else: raise TypeError(f"Unsupported operand type(s) for -: 'AlgoAmount' and '{type(other).__name__}'") return self # Helper functions -def algo(algos: int) -> AlgoAmount: +def algo(algo: int) -> AlgoAmount: """Create an AlgoAmount object representing the given number of Algo. - :param algos: The number of Algo to create an AlgoAmount object for. + :param algo: The number of Algo to create an AlgoAmount object for. :return: An AlgoAmount object representing the given number of Algo. """ - return AlgoAmount.from_algos(algos) + return AlgoAmount.from_algo(algo) -def micro_algo(microalgos: int) -> AlgoAmount: +def micro_algo(micro_algo: int) -> AlgoAmount: """Create an AlgoAmount object representing the given number of µAlgo. - :param microalgos: The number of µAlgo to create an AlgoAmount object for. + :param micro_algo: The number of µAlgo to create an AlgoAmount object for. :return: An AlgoAmount object representing the given number of µAlgo. """ - return AlgoAmount.from_micro_algos(microalgos) + return AlgoAmount.from_micro_algo(micro_algo) ALGORAND_MIN_TX_FEE = micro_algo(1_000) @@ -252,5 +196,5 @@ def transaction_fees(number_of_transactions: int) -> AlgoAmount: :return: The total transaction fees. """ - total_micro_algos = number_of_transactions * ALGORAND_MIN_TX_FEE.micro_algos - return micro_algo(total_micro_algos) + total_micro_algos = number_of_transactions * ALGORAND_MIN_TX_FEE.micro_algo + return AlgoAmount.from_micro_algo(total_micro_algos) diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index 1dbf5066..ec9db600 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -679,7 +679,7 @@ def _get_group_execution_info( # noqa: C901, PLR0912 if not suggested_params: raise ValueError("suggested_params required when cover_app_call_inner_transaction_fees enabled") - max_fee = max_fees.get(i).micro_algos if max_fees and i in max_fees else None # type: ignore[union-attr] + max_fee = max_fees.get(i).micro_algo if max_fees and i in max_fees else None # type: ignore[union-attr] if max_fee is None: app_call_indexes_without_max_fees.append(i) else: @@ -839,7 +839,7 @@ def prepare_group_for_sending( # noqa: C901, PLR0912, PLR0915 if not txn_info: continue txn = group[i].txn - max_fee = max_fees.get(i).micro_algos if max_fees and i in max_fees else None # type: ignore[union-attr] + max_fee = max_fees.get(i).micro_algo if max_fees and i in max_fees else None # type: ignore[union-attr] immutable_fee = max_fee is not None and max_fee == txn.fee priority_multiplier = ( 1000 @@ -1092,7 +1092,7 @@ def is_appl_below_limit(t: TransactionWithSigner) -> bool: ) transaction_fee = cur_txn.fee + additional_fee - max_fee = max_fees.get(i).micro_algos if max_fees and i in max_fees else None # type: ignore[union-attr] + max_fee = max_fees.get(i).micro_algo if max_fees and i in max_fees else None # type: ignore[union-attr] if max_fee is None or transaction_fee > max_fee: raise ValueError( @@ -1848,7 +1848,7 @@ def _common_txn_build_step( # noqa: C901 txn_params["sp"].last = txn_params["sp"].first + window if params.static_fee is not None and txn_params["sp"]: - txn_params["sp"].fee = params.static_fee.micro_algos + txn_params["sp"].fee = params.static_fee.micro_algo txn_params["sp"].flat_fee = True if isinstance(txn_params.get("method"), Arc56Method): @@ -1857,9 +1857,9 @@ def _common_txn_build_step( # noqa: C901 txn = build_txn(txn_params) if params.extra_fee: - txn.fee += params.extra_fee.micro_algos + txn.fee += params.extra_fee.micro_algo - if params.max_fee and txn.fee > params.max_fee.micro_algos: + if params.max_fee and txn.fee > params.max_fee.micro_algo: raise ValueError(f"Transaction fee {txn.fee} is greater than max_fee {params.max_fee}") use_max_fee = params.max_fee and params.max_fee.micro_algo > ( params.static_fee.micro_algo if params.static_fee else 0 @@ -2046,7 +2046,7 @@ def _build_payment( "sender": params.sender, "sp": suggested_params, "receiver": params.receiver, - "amt": params.amount.micro_algos, + "amt": params.amount.micro_algo, "close_remainder_to": params.close_remainder_to, } diff --git a/tests/accounts/test_account_manager.py b/tests/accounts/test_account_manager.py index 3f4b55e0..9576a510 100644 --- a/tests/accounts/test_account_manager.py +++ b/tests/accounts/test_account_manager.py @@ -17,7 +17,7 @@ def funded_account(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( - new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + new_account, dispenser, AlgoAmount.from_algo(100), min_funding_increment=AlgoAmount.from_algo(1) ) algorand.set_signer(sender=new_account.address, signer=new_account.signer) return new_account @@ -78,7 +78,7 @@ def test_random_account_creation(algorand: AlgorandClient) -> None: def test_ensure_funded_from_environment(algorand: AlgorandClient) -> None: # Arrange account = algorand.account.random() - min_balance = AlgoAmount.from_algos(1) + min_balance = AlgoAmount.from_algo(1) # Act result = algorand.account.ensure_funded_from_environment( @@ -90,7 +90,7 @@ def test_ensure_funded_from_environment(algorand: AlgorandClient) -> None: assert result is not None assert result.amount_funded is not None account_info = algorand.account.get_information(account.address) - assert account_info.amount_without_pending_rewards >= min_balance.micro_algos + assert account_info.amount_without_pending_rewards >= min_balance.micro_algo def test_get_account_information(algorand: AlgorandClient) -> None: diff --git a/tests/applications/test_app_client.py b/tests/applications/test_app_client.py index 6aa6e8e5..572ab639 100644 --- a/tests/applications/test_app_client.py +++ b/tests/applications/test_app_client.py @@ -36,7 +36,7 @@ def funded_account(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( - new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + new_account, dispenser, AlgoAmount.from_algo(100), min_funding_increment=AlgoAmount.from_algo(1) ) algorand.set_signer(sender=new_account.address, signer=new_account.signer) return new_account @@ -334,7 +334,7 @@ def test_construct_transaction_with_abi_encoding_including_transaction( algorand: AlgorandClient, funded_account: SigningAccount, test_app_client: AppClient ) -> None: # Create a payment transaction with random amount - amount = AlgoAmount.from_micro_algos(random.randint(1, 10000)) + amount = AlgoAmount.from_micro_algo(random.randint(1, 10000)) payment_txn = algorand.create_transaction.payment( PaymentParams( sender=funded_account.address, @@ -356,7 +356,7 @@ def test_construct_transaction_with_abi_encoding_including_transaction( response = AppManager.get_abi_return( result.confirmation, test_app_client.app_spec.get_arc56_method("call_abi_txn").to_abi_method() ) - expected_return = f"Sent {amount.micro_algos}. test" + expected_return = f"Sent {amount.micro_algo}. test" assert result.abi_return == expected_return assert response assert response.value == result.abi_return @@ -366,7 +366,7 @@ def test_sign_all_transactions_in_group_with_abi_call_with_transaction_arg( algorand: AlgorandClient, test_app_client: AppClient, funded_account: SigningAccount ) -> None: # Create a payment transaction with a random amount - amount = AlgoAmount.from_micro_algos(random.randint(1, 10000)) + amount = AlgoAmount.from_micro_algo(random.randint(1, 10000)) txn = algorand.create_transaction.payment( PaymentParams( sender=funded_account.address, @@ -405,8 +405,8 @@ def test_sign_transaction_in_group_with_different_signer_if_provided( algorand.account.ensure_funded( account_to_fund=test_account, dispenser_account=funded_account, - min_spending_balance=AlgoAmount.from_algos(10), - min_funding_increment=AlgoAmount.from_algos(1), + min_spending_balance=AlgoAmount.from_algo(10), + min_funding_increment=AlgoAmount.from_algo(1), ) # Fund the account with 1 Algo @@ -414,7 +414,7 @@ def test_sign_transaction_in_group_with_different_signer_if_provided( PaymentParams( sender=test_account.address, receiver=test_account.address, - amount=AlgoAmount.from_algos(random.randint(1, 5)), + amount=AlgoAmount.from_algo(random.randint(1, 5)), ) ) @@ -434,8 +434,8 @@ def test_construct_transaction_with_abi_encoding_including_foreign_references_no algorand.account.ensure_funded( account_to_fund=test_account, dispenser_account=funded_account, - min_spending_balance=AlgoAmount.from_algos(10), - min_funding_increment=AlgoAmount.from_algos(1), + min_spending_balance=AlgoAmount.from_algo(10), + min_funding_increment=AlgoAmount.from_algo(1), ) result = test_app_client.send.call( @@ -495,7 +495,7 @@ def test_retrieve_state(test_app_client: AppClient, funded_account: SigningAccou box_name2 = bytes([0, 0, 0, 2]) box_name2_base64 = base64.b64encode(box_name2).decode() - test_app_client.fund_app_account(params=FundAppAccountParams(amount=AlgoAmount.from_algos(1))) + test_app_client.fund_app_account(params=FundAppAccountParams(amount=AlgoAmount.from_algo(1))) test_app_client.send.call( AppClientMethodCallParams( @@ -591,7 +591,7 @@ def test_box_methods_with_manually_encoded_abi_args( # Fund the app account box_prefix = b"box_bytes" - test_app_client_puya.fund_app_account(params=FundAppAccountParams(amount=AlgoAmount.from_algos(1))) + test_app_client_puya.fund_app_account(params=FundAppAccountParams(amount=AlgoAmount.from_algo(1))) # Encode the box reference box_identifier = box_prefix + ABIType.from_string("string").encode(box_name) @@ -633,7 +633,7 @@ def test_box_methods_with_arc4_returns_parametrized( box_prefix = box_prefix_str.encode() # Fund the app account with 1 Algo - test_app_client_puya.fund_app_account(params=FundAppAccountParams(amount=AlgoAmount.from_algos(1))) + test_app_client_puya.fund_app_account(params=FundAppAccountParams(amount=AlgoAmount.from_algo(1))) # Encode the box name "box1" using ABIType "string" box_name_encoded = ABIType.from_string("string").encode("box1") diff --git a/tests/applications/test_app_factory.py b/tests/applications/test_app_factory.py index 5a783fef..f164b25e 100644 --- a/tests/applications/test_app_factory.py +++ b/tests/applications/test_app_factory.py @@ -34,7 +34,7 @@ def funded_account(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( - new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + new_account, dispenser, AlgoAmount.from_algo(100), min_funding_increment=AlgoAmount.from_algo(1) ) algorand.set_signer(sender=new_account.address, signer=new_account.signer) return new_account diff --git a/tests/applications/test_app_manager.py b/tests/applications/test_app_manager.py index 660c7039..61084d69 100644 --- a/tests/applications/test_app_manager.py +++ b/tests/applications/test_app_manager.py @@ -17,7 +17,7 @@ def funded_account(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( - new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + new_account, dispenser, AlgoAmount.from_algo(100), min_funding_increment=AlgoAmount.from_algo(1) ) algorand.set_signer(sender=new_account.address, signer=new_account.signer) return new_account diff --git a/tests/assets/test_asset_manager.py b/tests/assets/test_asset_manager.py index 2c1987cb..71ff08d4 100644 --- a/tests/assets/test_asset_manager.py +++ b/tests/assets/test_asset_manager.py @@ -25,7 +25,7 @@ def sender(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( - new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + new_account, dispenser, AlgoAmount.from_algo(100), min_funding_increment=AlgoAmount.from_algo(1) ) algorand.set_signer(sender=new_account.address, signer=new_account.signer) return new_account @@ -36,7 +36,7 @@ def receiver(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( - new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + new_account, dispenser, AlgoAmount.from_algo(100), min_funding_increment=AlgoAmount.from_algo(1) ) return new_account @@ -171,7 +171,7 @@ def test_bulk_opt_in_with_address(algorand: AlgorandClient, sender: SigningAccou PaymentParams( sender=sender.address, receiver=receiver.address, - amount=AlgoAmount.from_algos(1), + amount=AlgoAmount.from_algo(1), ) ) @@ -207,7 +207,7 @@ def test_bulk_opt_out_not_opted_in_fails( PaymentParams( sender=sender.address, receiver=receiver.address, - amount=AlgoAmount.from_algos(1), + amount=AlgoAmount.from_algo(1), ) ) diff --git a/tests/clients/algorand_client/test_transfer.py b/tests/clients/algorand_client/test_transfer.py index 40f11e09..244f3b0e 100644 --- a/tests/clients/algorand_client/test_transfer.py +++ b/tests/clients/algorand_client/test_transfer.py @@ -24,7 +24,7 @@ def funded_account(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( - new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + new_account, dispenser, AlgoAmount.from_algo(100), min_funding_increment=AlgoAmount.from_algo(1) ) algorand.set_signer(sender=new_account.address, signer=new_account.signer) return new_account @@ -37,7 +37,7 @@ def test_transfer_algo_is_sent_and_waited_for(algorand: AlgorandClient, funded_a PaymentParams( sender=funded_account.address, receiver=second_account.address, - amount=AlgoAmount.from_algos(5), + amount=AlgoAmount.from_algo(5), note=b"Transfer 5 Algos", ) ) @@ -58,7 +58,7 @@ def test_transfer_algo_respects_string_lease(algorand: AlgorandClient, funded_ac PaymentParams( sender=funded_account.address, receiver=second_account.address, - amount=AlgoAmount.from_algos(1), + amount=AlgoAmount.from_algo(1), lease=b"test", ) ) @@ -68,7 +68,7 @@ def test_transfer_algo_respects_string_lease(algorand: AlgorandClient, funded_ac PaymentParams( sender=funded_account.address, receiver=second_account.address, - amount=AlgoAmount.from_algos(2), + amount=AlgoAmount.from_algo(2), lease=b"test", ) ) @@ -81,7 +81,7 @@ def test_transfer_algo_respects_byte_array_lease(algorand: AlgorandClient, funde PaymentParams( sender=funded_account.address, receiver=second_account.address, - amount=AlgoAmount.from_algos(1), + amount=AlgoAmount.from_algo(1), lease=b"\x01\x02\x03\x04", ) ) @@ -91,7 +91,7 @@ def test_transfer_algo_respects_byte_array_lease(algorand: AlgorandClient, funde PaymentParams( sender=funded_account.address, receiver=second_account.address, - amount=AlgoAmount.from_algos(2), + amount=AlgoAmount.from_algo(2), lease=b"\x01\x02\x03\x04", ) ) @@ -104,8 +104,8 @@ def test_transfer_asa_respects_lease(algorand: AlgorandClient, funded_account: S algorand.account.ensure_funded( account_to_fund=second_account, dispenser_account=funded_account, - min_spending_balance=AlgoAmount.from_algos(1), - min_funding_increment=AlgoAmount.from_algos(1), + min_spending_balance=AlgoAmount.from_algo(1), + min_funding_increment=AlgoAmount.from_algo(1), ) algorand.send.asset_opt_in( @@ -162,8 +162,8 @@ def test_transfer_asa_sender_not_opted_in(algorand: AlgorandClient, funded_accou algorand.account.ensure_funded( account_to_fund=second_account, dispenser_account=funded_account, - min_spending_balance=AlgoAmount.from_algos(1), - min_funding_increment=AlgoAmount.from_algos(1), + min_spending_balance=AlgoAmount.from_algo(1), + min_funding_increment=AlgoAmount.from_algo(1), ) with pytest.raises(Exception, match=f"asset {test_asset_id} missing from {second_account.address}"): @@ -183,8 +183,8 @@ def test_transfer_asa_asset_doesnt_exist(algorand: AlgorandClient, funded_accoun algorand.account.ensure_funded( account_to_fund=second_account, dispenser_account=funded_account, - min_spending_balance=AlgoAmount.from_algos(1), - min_funding_increment=AlgoAmount.from_algos(1), + min_spending_balance=AlgoAmount.from_algo(1), + min_funding_increment=AlgoAmount.from_algo(1), ) with pytest.raises(Exception, match=f"asset 123123 missing from {funded_account.address}"): @@ -205,8 +205,8 @@ def test_transfer_asa_to_another_account(algorand: AlgorandClient, funded_accoun algorand.account.ensure_funded( account_to_fund=second_account, dispenser_account=funded_account, - min_spending_balance=AlgoAmount.from_algos(1), - min_funding_increment=AlgoAmount.from_algos(1), + min_spending_balance=AlgoAmount.from_algo(1), + min_funding_increment=AlgoAmount.from_algo(1), ) with pytest.raises(Exception, match="account asset info not found"): @@ -244,14 +244,14 @@ def test_transfer_asa_from_revocation_target(algorand: AlgorandClient, funded_ac algorand.account.ensure_funded( account_to_fund=second_account, dispenser_account=funded_account, - min_spending_balance=AlgoAmount.from_algos(1), - min_funding_increment=AlgoAmount.from_algos(1), + min_spending_balance=AlgoAmount.from_algo(1), + min_funding_increment=AlgoAmount.from_algo(1), ) algorand.account.ensure_funded( account_to_fund=clawback_account, dispenser_account=funded_account, - min_spending_balance=AlgoAmount.from_algos(1), - min_funding_increment=AlgoAmount.from_algos(1), + min_spending_balance=AlgoAmount.from_algo(1), + min_funding_increment=AlgoAmount.from_algo(1), ) algorand.send.asset_opt_in( @@ -302,7 +302,7 @@ def test_transfer_asa_from_revocation_target(algorand: AlgorandClient, funded_ac assert test_account_info.balance == 95 -MINIMUM_BALANCE = AlgoAmount.from_micro_algos( +MINIMUM_BALANCE = AlgoAmount.from_micro_algo( 100_000 ) # see https://developer.algorand.org/docs/get-details/accounts/#minimum-balance @@ -312,12 +312,12 @@ def test_ensure_funded(algorand: AlgorandClient, funded_account: SigningAccount) response = algorand.account.ensure_funded( account_to_fund=test_account, dispenser_account=funded_account, - min_spending_balance=AlgoAmount.from_algos(1), + min_spending_balance=AlgoAmount.from_algo(1), ) assert response is not None to_account_info = algorand.account.get_information(test_account) - assert to_account_info.amount == MINIMUM_BALANCE + AlgoAmount.from_algos(1) + assert to_account_info.amount == MINIMUM_BALANCE + AlgoAmount.from_algo(1) def test_ensure_funded_uses_dispenser_by_default( @@ -328,8 +328,8 @@ def test_ensure_funded_uses_dispenser_by_default( result = algorand.account.ensure_funded_from_environment( account_to_fund=second_account, - min_spending_balance=AlgoAmount.from_algos(1), - min_funding_increment=AlgoAmount.from_algos(1), + min_spending_balance=AlgoAmount.from_algo(1), + min_funding_increment=AlgoAmount.from_algo(1), ) assert result is not None @@ -337,7 +337,7 @@ def test_ensure_funded_uses_dispenser_by_default( assert result.transaction.payment.sender == dispenser.address account_info = algorand.account.get_information(second_account) - assert account_info.amount == MINIMUM_BALANCE + AlgoAmount.from_algos(1) + assert account_info.amount == MINIMUM_BALANCE + AlgoAmount.from_algo(1) def test_ensure_funded_respects_minimum_funding_increment( @@ -348,12 +348,12 @@ def test_ensure_funded_respects_minimum_funding_increment( account_to_fund=test_account, dispenser_account=funded_account, min_spending_balance=AlgoAmount.from_micro_algo(1), - min_funding_increment=AlgoAmount.from_algos(1), + min_funding_increment=AlgoAmount.from_algo(1), ) assert response is not None to_account_info = algorand.account.get_information(test_account) - assert to_account_info.amount == AlgoAmount.from_algos(1) + assert to_account_info.amount == AlgoAmount.from_algo(1) def test_ensure_funded_testnet_api_success(monkeypatch: pytest.MonkeyPatch, httpx_mock: HTTPXMock) -> None: @@ -422,7 +422,7 @@ def test_rekey_works(algorand: AlgorandClient, funded_account: SigningAccount) - PaymentParams( sender=funded_account.address, receiver=funded_account.address, - amount=AlgoAmount.from_micro_algos(1), + amount=AlgoAmount.from_micro_algo(1), signer=second_account.signer, ) ) diff --git a/tests/models/test_algo_amount.py b/tests/models/test_algo_amount.py index 5161d37b..51bcdd0f 100644 --- a/tests/models/test_algo_amount.py +++ b/tests/models/test_algo_amount.py @@ -7,54 +7,51 @@ def test_initialization() -> None: # Test valid initialization formats - assert AlgoAmount(micro_algos=1_000_000).micro_algos == 1_000_000 - assert AlgoAmount(micro_algo=500_000).micro_algos == 500_000 - assert AlgoAmount(algos=1).micro_algos == 1_000_000 - assert AlgoAmount(algo=Decimal("0.5")).micro_algos == 500_000 + assert AlgoAmount(micro_algo=500_000).micro_algo == 500_000 + assert AlgoAmount(algo=1).micro_algo == 1_000_000 + assert AlgoAmount(algo=Decimal("0.5")).micro_algo == 500_000 # Test decimal precision - assert AlgoAmount(algos=Decimal("0.000001")).micro_algos == 1 - assert AlgoAmount(algo=Decimal("123.456789")).micro_algos == 123_456_789 + assert AlgoAmount(algo=Decimal("0.000001")).micro_algo == 1 + assert AlgoAmount(algo=Decimal("123.456789")).micro_algo == 123_456_789 def test_from_methods() -> None: - assert AlgoAmount.from_micro_algos(500_000).micro_algos == 500_000 - assert AlgoAmount.from_micro_algo(250_000).micro_algos == 250_000 - assert AlgoAmount.from_algos(2).micro_algos == 2_000_000 - assert AlgoAmount.from_algo(Decimal("0.75")).micro_algos == 750_000 + assert AlgoAmount.from_micro_algo(500_000).micro_algo == 500_000 + assert AlgoAmount.from_micro_algo(250_000).micro_algo == 250_000 + assert AlgoAmount.from_algo(2).micro_algo == 2_000_000 + assert AlgoAmount.from_algo(Decimal("0.75")).micro_algo == 750_000 def test_properties() -> None: - amount = AlgoAmount.from_micro_algos(1_234_567) - assert amount.micro_algos == 1_234_567 + amount = AlgoAmount.from_micro_algo(1_234_567) assert amount.micro_algo == 1_234_567 - assert amount.algos == Decimal("1.234567") assert amount.algo == Decimal("1.234567") def test_arithmetic_operations() -> None: - a = AlgoAmount.from_algos(5) - b = AlgoAmount.from_algos(3) + a = AlgoAmount.from_algo(5) + b = AlgoAmount.from_algo(3) # Addition - assert (a + b).micro_algos == 8_000_000 + assert (a + b).micro_algo == 8_000_000 a += b - assert a.micro_algos == 8_000_000 + assert a.micro_algo == 8_000_000 # Subtraction - assert (a - b).micro_algos == 5_000_000 + assert (a - b).micro_algo == 5_000_000 a -= b - assert a.micro_algos == 5_000_000 + assert a.micro_algo == 5_000_000 # Right operations - assert (AlgoAmount.from_micro_algo(1000) + a).micro_algos == 5_001_000 - assert (AlgoAmount.from_algos(10) - a).micro_algos == 5_000_000 + assert (AlgoAmount.from_micro_algo(1000) + a).micro_algo == 5_001_000 + assert (AlgoAmount.from_algo(10) - a).micro_algo == 5_000_000 def test_comparison_operators() -> None: - base = AlgoAmount.from_algos(5) - same = AlgoAmount.from_algos(5) - larger = AlgoAmount.from_algos(10) + base = AlgoAmount.from_algo(5) + same = AlgoAmount.from_algo(5) + larger = AlgoAmount.from_algo(10) assert base == same assert base != larger @@ -71,35 +68,35 @@ def test_comparison_operators() -> None: def test_edge_cases() -> None: # Zero value - zero = AlgoAmount.from_micro_algos(0) - assert zero.micro_algos == 0 - assert zero.algos == 0 + zero = AlgoAmount.from_micro_algo(0) + assert zero.micro_algo == 0 + assert zero.algo == 0 # Very large values - large = AlgoAmount.from_algos(Decimal("1e9")) - assert large.micro_algos == 1e9 * 1e6 + large = AlgoAmount.from_algo(Decimal("1e9")) + assert large.micro_algo == 1e9 * 1e6 # Decimal precision limits - precise = AlgoAmount(algos=Decimal("0.123456789")) - assert precise.micro_algos == 123_456 + precise = AlgoAmount(algo=Decimal("0.123456789")) + assert precise.micro_algo == 123_456 def test_string_representation() -> None: - assert str(AlgoAmount.from_micro_algos(1_000_000)) == "1,000,000 µALGO" - assert str(AlgoAmount.from_algos(Decimal("2.5"))) == "2,500,000 µALGO" + assert str(AlgoAmount.from_micro_algo(1_000_000)) == "1,000,000 µALGO" + assert str(AlgoAmount.from_algo(Decimal("2.5"))) == "2,500,000 µALGO" def test_type_safety() -> None: with pytest.raises(TypeError, match="Unsupported operand type"): # int is not AlgoAmount - AlgoAmount.from_algos(5) + 1000 # type: ignore # noqa: PGH003 + AlgoAmount.from_algo(5) + 1000 # type: ignore # noqa: PGH003 with pytest.raises(TypeError, match="Unsupported operand type"): - AlgoAmount.from_algos(5) - "invalid" # type: ignore # noqa: PGH003 + AlgoAmount.from_algo(5) - "invalid" # type: ignore # noqa: PGH003 def test_helper_functions() -> None: - assert algo(1).micro_algos == 1_000_000 - assert micro_algo(1_000_000).micro_algos == 1_000_000 - assert ALGORAND_MIN_TX_FEE.micro_algos == 1_000 - assert transaction_fees(1).micro_algos == 1_000 + assert algo(1).micro_algo == 1_000_000 + assert micro_algo(1_000_000).micro_algo == 1_000_000 + assert ALGORAND_MIN_TX_FEE.micro_algo == 1_000 + assert transaction_fees(1).micro_algo == 1_000 diff --git a/tests/test_debug_utils.py b/tests/test_debug_utils.py index 1dab0291..41cbf687 100644 --- a/tests/test_debug_utils.py +++ b/tests/test_debug_utils.py @@ -254,7 +254,7 @@ def test_simulate_response_filename_generation( PaymentParams( sender=funded_account.address, receiver=client_fixture.app_address, - amount=AlgoAmount.from_micro_algos(1_000_000 * (i + 1)), + amount=AlgoAmount.from_micro_algo(1_000_000 * (i + 1)), note=f"Payment{i+1}".encode(), ) ) diff --git a/tests/transactions/test_resource_packing.py b/tests/transactions/test_resource_packing.py index ca6b08c4..b3db3b3a 100644 --- a/tests/transactions/test_resource_packing.py +++ b/tests/transactions/test_resource_packing.py @@ -27,7 +27,7 @@ def algorand() -> AlgorandClient: def funded_account(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() - algorand.account.ensure_funded(new_account, dispenser, AlgoAmount.from_algos(100)) + algorand.account.ensure_funded(new_account, dispenser, AlgoAmount.from_algo(100)) return new_account @@ -500,7 +500,7 @@ def setup(self, algorand: AlgorandClient, funded_account: SigningAccount) -> Gen # Fund app accounts for client in [self.app_client1, self.app_client2, self.app_client3]: - client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_algos(2))) + client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_algo(2))) yield @@ -522,7 +522,7 @@ def test_runs_auto_opup_implicitly_on_readonly_calls(self) -> None: # max_fee provided explicitly, it will fail new_account_with_less_than_10_algo = self.app_client1.algorand.account.random() self.app_client1.algorand.account.ensure_funded_from_environment( - account_to_fund=new_account_with_less_than_10_algo, min_spending_balance=AlgoAmount.from_algos(5) + account_to_fund=new_account_with_less_than_10_algo, min_spending_balance=AlgoAmount.from_algo(5) ) with pytest.raises(ValueError, match=r"tried to spend \{10000000\}"): self.app_client1.send.call( @@ -537,7 +537,7 @@ def test_runs_auto_opup_implicitly_on_readonly_calls(self) -> None: AppClientMethodCallParams( args=[6200], method="burn_ops_readonly", - max_fee=AlgoAmount.from_micro_algos(10000), + max_fee=AlgoAmount.from_micro_algo(10000), signer=new_account_with_less_than_10_algo.signer, ), ) @@ -561,7 +561,7 @@ def test_throws_when_inner_fees_not_covered(self) -> None: params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - max_fee=AlgoAmount.from_micro_algos(expected_fee), + max_fee=AlgoAmount.from_micro_algo(expected_fee), ) with pytest.raises(Exception, match="fee too small"): @@ -578,7 +578,7 @@ def test_does_not_alter_fee_without_inners(self) -> None: expected_fee = 1000 params = AppClientMethodCallParams( method="no_op", - max_fee=AlgoAmount.from_micro_algos(2000), + max_fee=AlgoAmount.from_micro_algo(2000), ) result = self.app_client1.send.call( params, @@ -597,7 +597,7 @@ def test_throws_when_max_fee_too_small(self) -> None: params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - max_fee=AlgoAmount.from_micro_algos(expected_fee - 1), + max_fee=AlgoAmount.from_micro_algo(expected_fee - 1), ) with pytest.raises(ValueError, match="Fees were too small to resolve execution info"): @@ -615,7 +615,7 @@ def test_throws_when_static_fee_too_small_for_inner_fees(self) -> None: params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - static_fee=AlgoAmount.from_micro_algos(expected_fee - 1), + static_fee=AlgoAmount.from_micro_algo(expected_fee - 1), ) with pytest.raises(ValueError, match="Fees were too small to resolve execution info"): @@ -633,7 +633,7 @@ def test_alters_fee_handling_when_no_itxns_covered(self) -> None: params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - max_fee=AlgoAmount.from_micro_algos(expected_fee), + max_fee=AlgoAmount.from_micro_algo(expected_fee), ) result = self.app_client1.send.call( params, @@ -652,7 +652,7 @@ def test_alters_fee_handling_when_all_inners_covered(self) -> None: params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 1000, 1000, 1000, [1000, 1000]]], - max_fee=AlgoAmount.from_micro_algos(expected_fee), + max_fee=AlgoAmount.from_micro_algo(expected_fee), ) result = self.app_client1.send.call( params, @@ -671,7 +671,7 @@ def test_alters_fee_handling_when_some_inners_covered(self) -> None: params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 0, 200, 0, [500, 0]]], - max_fee=AlgoAmount.from_micro_algos(expected_fee), + max_fee=AlgoAmount.from_micro_algo(expected_fee), ) result = self.app_client1.send.call( params, @@ -690,7 +690,7 @@ def test_alters_fee_when_some_inners_have_surplus(self) -> None: params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 1000, 5000, 0, [0, 50]]], - max_fee=AlgoAmount.from_micro_algos(expected_fee), + max_fee=AlgoAmount.from_micro_algo(expected_fee), ) result = self.app_client1.send.call( params, @@ -709,14 +709,14 @@ def test_alters_handling_multiple_app_calls_in_group_with_inners_with_varying_fe txn_1_params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 1000, 0, 0, [200, 0]]], - static_fee=AlgoAmount.from_micro_algos(txn_1_expected_fee), + static_fee=AlgoAmount.from_micro_algo(txn_1_expected_fee), note=b"txn_1", ) txn_2_params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 0, 0, 0, [0, 0]]], - max_fee=AlgoAmount.from_micro_algos(txn_2_expected_fee), + max_fee=AlgoAmount.from_micro_algo(txn_2_expected_fee), note=b"txn_2", ) @@ -739,7 +739,7 @@ def test_does_not_alter_static_fee_with_surplus(self) -> None: params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 0, 200, 0, [500, 0]]], - static_fee=AlgoAmount.from_micro_algos(expected_fee), + static_fee=AlgoAmount.from_micro_algo(expected_fee), ) result = self.app_client1.send.call( params, @@ -757,7 +757,7 @@ def test_alters_fee_with_large_inner_surplus_pooling(self) -> None: params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0, 20_000, 0, 0, 0]]], - max_fee=AlgoAmount.from_micro_algos(expected_fee), + max_fee=AlgoAmount.from_micro_algo(expected_fee), ) result = self.app_client1.send.call( params, @@ -776,7 +776,7 @@ def test_alters_fee_with_partial_inner_surplus_pooling(self) -> None: params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 2200, 0, [0, 0, 2500, 0, 0, 0]]], - max_fee=AlgoAmount.from_micro_algos(expected_fee), + max_fee=AlgoAmount.from_micro_algo(expected_fee), ) result = self.app_client1.send.call( params, @@ -795,7 +795,7 @@ def test_alters_fee_with_large_inner_surplus_no_pooling(self) -> None: params = AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0, 0, 0, 0, 20_000]]], - max_fee=AlgoAmount.from_micro_algos(expected_fee), + max_fee=AlgoAmount.from_micro_algo(expected_fee), ) result = self.app_client1.send.call( params, @@ -818,7 +818,7 @@ def test_alters_fee_with_multiple_inner_surplus_poolings_to_lower_siblings(self) self.app_client3.app_id, [0, 1200, [0, 0, 4900, 0, 0, 0], 200, 1100, [0, 0, 2500, 0, 0, 0]], ], - max_fee=AlgoAmount.from_micro_algos(expected_fee), + max_fee=AlgoAmount.from_micro_algo(expected_fee), ) result = self.app_client1.send.call(params, send_params={"cover_app_call_inner_transaction_fees": True}) @@ -836,8 +836,8 @@ def test_does_not_alter_fee_when_group_covers_inner_fees(self, funded_account: S params=PaymentParams( sender=funded_account.address, receiver=funded_account.address, - amount=AlgoAmount.from_micro_algos(0), - static_fee=AlgoAmount.from_micro_algos(expected_fee), + amount=AlgoAmount.from_micro_algo(0), + static_fee=AlgoAmount.from_micro_algo(expected_fee), ) ) .add_app_call_method_call( @@ -845,7 +845,7 @@ def test_does_not_alter_fee_when_group_covers_inner_fees(self, funded_account: S AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - max_fee=AlgoAmount.from_micro_algos(expected_fee), + max_fee=AlgoAmount.from_micro_algo(expected_fee), ) ) ) @@ -867,7 +867,7 @@ def test_allocates_surplus_fees_to_most_constrained_first(self, funded_account: AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - max_fee=AlgoAmount.from_micro_algos(2000), + max_fee=AlgoAmount.from_micro_algo(2000), ) ) ) @@ -875,16 +875,16 @@ def test_allocates_surplus_fees_to_most_constrained_first(self, funded_account: params=PaymentParams( sender=funded_account.address, receiver=funded_account.address, - amount=AlgoAmount.from_micro_algos(0), - static_fee=AlgoAmount.from_micro_algos(7500), + amount=AlgoAmount.from_micro_algo(0), + static_fee=AlgoAmount.from_micro_algo(7500), ) ) .add_payment( params=PaymentParams( sender=funded_account.address, receiver=funded_account.address, - amount=AlgoAmount.from_micro_algos(0), - static_fee=AlgoAmount.from_micro_algos(0), + amount=AlgoAmount.from_micro_algo(0), + static_fee=AlgoAmount.from_micro_algo(0), ) ) .send({"cover_app_call_inner_transaction_fees": True}) @@ -912,15 +912,15 @@ def test_handles_nested_abi_method_calls(self, funded_account: SigningAccount) - AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 2000, 0, [0, 0]]], - max_fee=AlgoAmount.from_micro_algos(4000), + max_fee=AlgoAmount.from_micro_algo(4000), ) ) payment_params = PaymentParams( sender=funded_account.address, receiver=funded_account.address, - amount=AlgoAmount.from_micro_algos(0), - static_fee=AlgoAmount.from_micro_algos(1500), + amount=AlgoAmount.from_micro_algo(0), + static_fee=AlgoAmount.from_micro_algo(1500), ) expected_fee = 2000 @@ -930,7 +930,7 @@ def test_handles_nested_abi_method_calls(self, funded_account: SigningAccount) - self.app_client1.algorand.create_transaction.payment(payment_params), txn_arg_call, ], - static_fee=AlgoAmount.from_micro_algos(expected_fee), + static_fee=AlgoAmount.from_micro_algo(expected_fee), ) result = nested_client.send.call(params, send_params={"cover_app_call_inner_transaction_fees": True}) @@ -961,7 +961,7 @@ def test_throws_when_max_fee_below_calculated(self) -> None: AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - max_fee=AlgoAmount.from_micro_algos(1200), + max_fee=AlgoAmount.from_micro_algo(1200), ) ) ) @@ -971,7 +971,7 @@ def test_throws_when_max_fee_below_calculated(self) -> None: self.app_client1.params.call( AppClientMethodCallParams( method="no_op", - max_fee=AlgoAmount.from_micro_algos(10_000), + max_fee=AlgoAmount.from_micro_algo(10_000), ) ) ) @@ -995,7 +995,7 @@ def test_throws_when_nested_max_fee_below_calculated(self, funded_account: Signi AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 2000, 0, [0, 0]]], - max_fee=AlgoAmount.from_micro_algos(2000), + max_fee=AlgoAmount.from_micro_algo(2000), ) ) @@ -1010,12 +1010,12 @@ def test_throws_when_nested_max_fee_below_calculated(self, funded_account: Signi PaymentParams( sender=funded_account.address, receiver=funded_account.address, - amount=AlgoAmount.from_micro_algos(0), + amount=AlgoAmount.from_micro_algo(0), ) ), txn_arg_call, ], - max_fee=AlgoAmount.from_micro_algos(10_000), + max_fee=AlgoAmount.from_micro_algo(10_000), ), send_params={ "cover_app_call_inner_transaction_fees": True, @@ -1035,7 +1035,7 @@ def test_throws_when_static_fee_below_calculated(self) -> None: AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - static_fee=AlgoAmount.from_micro_algos(5000), + static_fee=AlgoAmount.from_micro_algo(5000), ) ) ) @@ -1045,7 +1045,7 @@ def test_throws_when_static_fee_below_calculated(self) -> None: self.app_client1.params.call( AppClientMethodCallParams( method="no_op", - max_fee=AlgoAmount.from_micro_algos(10_000), + max_fee=AlgoAmount.from_micro_algo(10_000), ) ) ) @@ -1065,8 +1065,8 @@ def test_throws_when_non_app_call_static_fee_too_low(self, funded_account: Signi AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - static_fee=AlgoAmount.from_micro_algos(13_000), - max_fee=AlgoAmount.from_micro_algos(14_000), + static_fee=AlgoAmount.from_micro_algo(13_000), + max_fee=AlgoAmount.from_micro_algo(14_000), ) ) ) @@ -1075,7 +1075,7 @@ def test_throws_when_non_app_call_static_fee_too_low(self, funded_account: Signi AppClientMethodCallParams( method="send_inners_with_fees", args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - static_fee=AlgoAmount.from_micro_algos(1000), + static_fee=AlgoAmount.from_micro_algo(1000), ) ) ) @@ -1083,8 +1083,8 @@ def test_throws_when_non_app_call_static_fee_too_low(self, funded_account: Signi params=PaymentParams( sender=funded_account.address, receiver=funded_account.address, - amount=AlgoAmount.from_micro_algos(0), - static_fee=AlgoAmount.from_micro_algos(500), + amount=AlgoAmount.from_micro_algo(0), + static_fee=AlgoAmount.from_micro_algo(500), ) ) .send({"cover_app_call_inner_transaction_fees": True}) @@ -1097,7 +1097,7 @@ def test_handles_expensive_abi_calls_with_ensure_budget(self) -> None: params = AppClientMethodCallParams( method="burn_ops", args=[6200], - max_fee=AlgoAmount.from_micro_algos(12_000), + max_fee=AlgoAmount.from_micro_algo(12_000), ) result = self.app_client1.send.call(params, send_params={"cover_app_call_inner_transaction_fees": True}) diff --git a/tests/transactions/test_transaction_composer.py b/tests/transactions/test_transaction_composer.py index 473b6b73..81a61799 100644 --- a/tests/transactions/test_transaction_composer.py +++ b/tests/transactions/test_transaction_composer.py @@ -50,7 +50,7 @@ def funded_account(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( - new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + new_account, dispenser, AlgoAmount.from_algo(100), min_funding_increment=AlgoAmount.from_algo(1) ) algorand.set_signer(sender=new_account.address, signer=new_account.signer) return new_account @@ -71,7 +71,7 @@ def test_add_transaction(algorand: AlgorandClient, funded_account: SigningAccoun sender=funded_account.address, sp=algorand.client.algod.suggested_params(), receiver=funded_account.address, - amt=AlgoAmount.from_algos(1).micro_algos, + amt=AlgoAmount.from_algo(1).micro_algo, ) composer.add_transaction(txn) built = composer.build_transactions() @@ -80,7 +80,7 @@ def test_add_transaction(algorand: AlgorandClient, funded_account: SigningAccoun assert isinstance(built.transactions[0], PaymentTxn) assert built.transactions[0].sender == funded_account.address assert built.transactions[0].receiver == funded_account.address - assert built.transactions[0].amt == AlgoAmount.from_algos(1).micro_algos + assert built.transactions[0].amt == AlgoAmount.from_algo(1).micro_algo def test_add_asset_create(algorand: AlgorandClient, funded_account: SigningAccount) -> None: @@ -238,7 +238,7 @@ def test_simulate(algorand: AlgorandClient, funded_account: SigningAccount) -> N PaymentParams( sender=funded_account.address, receiver=funded_account.address, - amount=AlgoAmount.from_algos(1), + amount=AlgoAmount.from_algo(1), ) ) composer.build() @@ -255,7 +255,7 @@ def test_send(algorand: AlgorandClient, funded_account: SigningAccount) -> None: PaymentParams( sender=funded_account.address, receiver=funded_account.address, - amount=AlgoAmount.from_algos(1), + amount=AlgoAmount.from_algo(1), ) ) response = composer.send() @@ -314,7 +314,7 @@ def _get_test_transaction( return { "sender": sender.address if sender else default_account.address, "receiver": default_account.address, - "amount": amount or AlgoAmount.from_algos(1), + "amount": amount or AlgoAmount.from_algo(1), } @@ -337,10 +337,10 @@ def test_transaction_cap_is_ignored_if_higher_than_fee( def test_transaction_fee_is_overridable(algorand: AlgorandClient, funded_account: SigningAccount) -> None: response = algorand.send.payment( - PaymentParams(**_get_test_transaction(funded_account), static_fee=AlgoAmount.from_algos(1)) + PaymentParams(**_get_test_transaction(funded_account), static_fee=AlgoAmount.from_algo(1)) ) assert isinstance(response.confirmation, dict) - assert response.confirmation["txn"]["txn"]["fee"] == AlgoAmount.from_algos(1) + assert response.confirmation["txn"]["txn"]["fee"] == AlgoAmount.from_algo(1) def test_transaction_group_is_sent(algorand: AlgorandClient, funded_account: SigningAccount) -> None: @@ -348,8 +348,8 @@ def test_transaction_group_is_sent(algorand: AlgorandClient, funded_account: Sig algod=algorand.client.algod, get_signer=lambda _: funded_account.signer, ) - composer.add_payment(PaymentParams(**_get_test_transaction(funded_account, amount=AlgoAmount.from_algos(1)))) - composer.add_payment(PaymentParams(**_get_test_transaction(funded_account, amount=AlgoAmount.from_algos(2)))) + composer.add_payment(PaymentParams(**_get_test_transaction(funded_account, amount=AlgoAmount.from_algo(1)))) + composer.add_payment(PaymentParams(**_get_test_transaction(funded_account, amount=AlgoAmount.from_algo(2)))) response = composer.send() assert isinstance(response.confirmations[0], dict) @@ -381,7 +381,7 @@ def test_multisig_single_account(algorand: AlgorandClient, funded_account: Signi signing_accounts=[funded_account], ) algorand.send.payment( - PaymentParams(sender=funded_account.address, receiver=multisig.address, amount=AlgoAmount.from_algos(1)) + PaymentParams(sender=funded_account.address, receiver=multisig.address, amount=AlgoAmount.from_algo(1)) ) algorand.send.payment( PaymentParams(sender=multisig.address, receiver=funded_account.address, amount=AlgoAmount.from_micro_algo(500)) @@ -390,7 +390,7 @@ def test_multisig_single_account(algorand: AlgorandClient, funded_account: Signi def test_multisig_double_account(algorand: AlgorandClient, funded_account: SigningAccount) -> None: account2 = algorand.account.random() - algorand.account.ensure_funded(account2, funded_account, AlgoAmount.from_algos(10)) + algorand.account.ensure_funded(account2, funded_account, AlgoAmount.from_algo(10)) # Setup multisig multisig = algorand.account.multisig( @@ -404,7 +404,7 @@ def test_multisig_double_account(algorand: AlgorandClient, funded_account: Signi # Fund multisig algorand.send.payment( - PaymentParams(sender=funded_account.address, receiver=multisig.address, amount=AlgoAmount.from_algos(1)) + PaymentParams(sender=funded_account.address, receiver=multisig.address, amount=AlgoAmount.from_algo(1)) ) # Use multisig diff --git a/tests/transactions/test_transaction_creator.py b/tests/transactions/test_transaction_creator.py index a9916f96..a5852bd0 100644 --- a/tests/transactions/test_transaction_creator.py +++ b/tests/transactions/test_transaction_creator.py @@ -41,7 +41,7 @@ def funded_account(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( - new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + new_account, dispenser, AlgoAmount.from_algo(100), min_funding_increment=AlgoAmount.from_algo(1) ) algorand.set_signer(sender=new_account.address, signer=new_account.signer) return new_account @@ -51,7 +51,7 @@ def funded_account(algorand: AlgorandClient) -> SigningAccount: def funded_secondary_account(algorand: AlgorandClient, funded_account: SigningAccount) -> SigningAccount: account = algorand.account.random() algorand.send.payment( - PaymentParams(sender=funded_account.address, receiver=account.address, amount=AlgoAmount.from_algos(1)) + PaymentParams(sender=funded_account.address, receiver=account.address, amount=AlgoAmount.from_algo(1)) ) return account @@ -61,14 +61,14 @@ def test_create_payment_transaction(algorand: AlgorandClient, funded_account: Si PaymentParams( sender=funded_account.address, receiver=funded_account.address, - amount=AlgoAmount.from_algos(1), + amount=AlgoAmount.from_algo(1), ) ) assert isinstance(txn, PaymentTxn) assert txn.sender == funded_account.address assert txn.receiver == funded_account.address - assert txn.amt == AlgoAmount.from_algos(1).micro_algos + assert txn.amt == AlgoAmount.from_algo(1).micro_algo def test_create_asset_create_transaction(algorand: AlgorandClient, funded_account: SigningAccount) -> None: diff --git a/tests/transactions/test_transaction_sender.py b/tests/transactions/test_transaction_sender.py index 0820638d..51c34c97 100644 --- a/tests/transactions/test_transaction_sender.py +++ b/tests/transactions/test_transaction_sender.py @@ -40,7 +40,7 @@ def funded_account(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( - new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + new_account, dispenser, AlgoAmount.from_algo(100), min_funding_increment=AlgoAmount.from_algo(1) ) algorand.set_signer(sender=new_account.address, signer=new_account.signer) return new_account @@ -56,7 +56,7 @@ def receiver(algorand: AlgorandClient) -> SigningAccount: new_account = algorand.account.random() dispenser = algorand.account.localnet_dispenser() algorand.account.ensure_funded( - new_account, dispenser, AlgoAmount.from_algos(100), min_funding_increment=AlgoAmount.from_algos(1) + new_account, dispenser, AlgoAmount.from_algo(100), min_funding_increment=AlgoAmount.from_algo(1) ) return new_account @@ -114,7 +114,7 @@ def new_group() -> TransactionComposer: def test_payment( transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount, receiver: SigningAccount ) -> None: - amount = AlgoAmount.from_algos(1) + amount = AlgoAmount.from_algo(1) result = transaction_sender.payment( PaymentParams( sender=sender.address, @@ -129,7 +129,7 @@ def test_payment( assert txn assert txn.sender == sender.address assert txn.receiver == receiver.address - assert txn.amt == amount.micro_algos + assert txn.amt == amount.micro_algo def test_asset_create(transaction_sender: AlgorandClientTransactionSender, sender: SigningAccount) -> None: @@ -442,7 +442,7 @@ def test_payment_logging( sender: SigningAccount, receiver: SigningAccount, ) -> None: - amount = AlgoAmount.from_algos(1) + amount = AlgoAmount.from_algo(1) transaction_sender.payment( PaymentParams( sender=sender.address, From aa7d969b55b0a8192cc2d443640ccfb5e58bffa4 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Tue, 4 Feb 2025 14:37:03 +0100 Subject: [PATCH 4/7] chore: addressing pr comments --- .../transaction_composer/index.md | 29 +++++++++++++++++-- src/algokit_utils/models/network.py | 2 +- .../transactions/transaction_composer.py | 12 ++++++++ tests/transactions/test_resource_packing.py | 2 +- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md b/docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md index 20d0483e..a1337a7f 100644 --- a/docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md +++ b/docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md @@ -36,8 +36,10 @@ ## Functions -| [`send_atomic_transaction_composer`](#algokit_utils.transactions.transaction_composer.send_atomic_transaction_composer)(...) | Send an AtomicTransactionComposer transaction group. | -|--------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| +| [`populate_app_call_resources`](#algokit_utils.transactions.transaction_composer.populate_app_call_resources)(...) | Populate application call resources based on simulation results. | +|------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------| +| [`prepare_group_for_sending`](#algokit_utils.transactions.transaction_composer.prepare_group_for_sending)(...) | Prepare a transaction group for sending by handling execution info and resources. | +| [`send_atomic_transaction_composer`](#algokit_utils.transactions.transaction_composer.send_atomic_transaction_composer)(...) | Send an AtomicTransactionComposer transaction group. | ## Module Contents @@ -553,6 +555,29 @@ Results from sending an AtomicTransactionComposer transaction group. #### simulate_response *: dict[str, Any] | None* *= None* +### algokit_utils.transactions.transaction_composer.populate_app_call_resources(atc: algosdk.atomic_transaction_composer.AtomicTransactionComposer, algod: algosdk.v2client.algod.AlgodClient) → algosdk.atomic_transaction_composer.AtomicTransactionComposer + +Populate application call resources based on simulation results. + +* **Parameters:** + * **atc** – The AtomicTransactionComposer containing transactions + * **algod** – Algod client for simulation +* **Returns:** + Modified AtomicTransactionComposer with populated resources + +### algokit_utils.transactions.transaction_composer.prepare_group_for_sending(atc: algosdk.atomic_transaction_composer.AtomicTransactionComposer, algod: algosdk.v2client.algod.AlgodClient, populate_app_call_resources: bool | None = None, cover_app_call_inner_transaction_fees: bool | None = None, additional_atc_context: AdditionalAtcContext | None = None) → algosdk.atomic_transaction_composer.AtomicTransactionComposer + +Prepare a transaction group for sending by handling execution info and resources. + +* **Parameters:** + * **atc** – The AtomicTransactionComposer containing transactions + * **algod** – Algod client for simulation + * **populate_app_call_resources** – Whether to populate app call resources + * **cover_app_call_inner_transaction_fees** – Whether to cover inner txn fees + * **additional_atc_context** – Additional context for the AtomicTransactionComposer +* **Returns:** + Modified AtomicTransactionComposer ready for sending + ### algokit_utils.transactions.transaction_composer.send_atomic_transaction_composer(atc: algosdk.atomic_transaction_composer.AtomicTransactionComposer, algod: algosdk.v2client.algod.AlgodClient, \*, max_rounds_to_wait: int | None = 5, skip_waiting: bool = False, suppress_log: bool | None = None, populate_app_call_resources: bool | None = None, cover_app_call_inner_transaction_fees: bool | None = None, additional_atc_context: AdditionalAtcContext | None = None) → [SendAtomicTransactionComposerResults](#algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults) Send an AtomicTransactionComposer transaction group. diff --git a/src/algokit_utils/models/network.py b/src/algokit_utils/models/network.py index d1f3c190..5a7dfb99 100644 --- a/src/algokit_utils/models/network.py +++ b/src/algokit_utils/models/network.py @@ -19,7 +19,7 @@ class AlgoClientNetworkConfig: def full_url(self) -> str: """Returns the full URL for the service""" - return f"{self.server}{f':{self.port}' if self.port else ''}" + return f"{self.server.rstrip('/')}{f':{self.port}' if self.port else ''}" @dataclasses.dataclass diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index ec9db600..06705571 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -68,6 +68,8 @@ "TransactionComposer", "TransactionComposerBuildResult", "TxnParams", + "populate_app_call_resources", + "prepare_group_for_sending", "send_atomic_transaction_composer", ] @@ -807,6 +809,16 @@ def _num_extra_program_pages(approval: bytes | None, clear: bytes | None) -> int return max(0, (total - 1) // algosdk.constants.APP_PAGE_MAX_SIZE) +def populate_app_call_resources(atc: AtomicTransactionComposer, algod: AlgodClient) -> AtomicTransactionComposer: + """Populate application call resources based on simulation results. + + :param atc: The AtomicTransactionComposer containing transactions + :param algod: Algod client for simulation + :return: Modified AtomicTransactionComposer with populated resources + """ + return prepare_group_for_sending(atc, algod, populate_app_call_resources=True) + + def prepare_group_for_sending( # noqa: C901, PLR0912, PLR0915 atc: AtomicTransactionComposer, algod: AlgodClient, diff --git a/tests/transactions/test_resource_packing.py b/tests/transactions/test_resource_packing.py index b3db3b3a..f768058c 100644 --- a/tests/transactions/test_resource_packing.py +++ b/tests/transactions/test_resource_packing.py @@ -506,7 +506,7 @@ def setup(self, algorand: AlgorandClient, funded_account: SigningAccount) -> Gen config.configure(populate_app_call_resources=False) - def test_runs_auto_opup_implicitly_on_readonly_calls(self) -> None: + def test_runs_auto_fee_coverage_implicitly_on_readonly_calls(self) -> None: """Test that auto top-up is run implicitly on readonly calls""" # Below must pass without explicit `populate_app_call_resources` flag From 44a0abf48c986386d4e6d2beaae174cb2bbda2b7 Mon Sep 17 00:00:00 2001 From: Neil Campbell Date: Wed, 5 Feb 2025 23:30:15 +0800 Subject: [PATCH 5/7] fix: adjust handling of readonly method fee coverage --- .../capabilities/transaction-composer.md | 10 +- .../capabilities/transaction-composer.md | 10 +- src/algokit_utils/applications/app_client.py | 51 +- .../transactions/transaction_composer.py | 17 - tests/artifacts/inner-fee/application.json | 66 +- tests/artifacts/inner-fee/contract.py | 60 +- tests/transactions/test_fee_coverage.py | 697 ++++++++++++++++++ tests/transactions/test_resource_packing.py | 645 +--------------- 8 files changed, 786 insertions(+), 770 deletions(-) create mode 100644 tests/transactions/test_fee_coverage.py diff --git a/docs/markdown/capabilities/transaction-composer.md b/docs/markdown/capabilities/transaction-composer.md index 9c09519a..8ed0c1b5 100644 --- a/docs/markdown/capabilities/transaction-composer.md +++ b/docs/markdown/capabilities/transaction-composer.md @@ -330,13 +330,13 @@ This feature should efficiently calculate the minimum fee needed to execute an a #### Read-only calls -When interacting with read-only calls, the transactions are not sent to the network, instead a simulation is performed to evaluate the transaction. -However, a read-only call will still consume the op budget, so to prevent this, by the default, the following logic is applied: +When invoking a readonly method, the transaction is simulated rather than being fully processed by the network. This allows users to call these methods without paying a fee. -1. If `max_fee` is not specified, `algokit-utils` will automatically set `max_fee` to `10` Algo. -2. If `max_fee` is specified, provided `max_fee` value will be respected when calculating required fee per read-only call. +Even though no actual fee is paid, the simulation still evaluates the transaction as if a fee was being paid, therefore op budget and fee coverage checks are still performed. -In either cases, resource population and app call inner transaction fees will still be automatically calculated and applied as per the rules above however there is no need to explicitly specify `populate_app_call_resources=True` or `cover_app_call_inner_transaction_fees=True` when sending read-only calls. +Because no fee is actually paid, calculating the minimum fee required to successfully execute the transaction is not required, and therefore we don’t need to send an additional simulate call to calculate the minimum fee, like we do with a non readonly method call. + +The behaviour of enabling `cover_app_call_inner_transaction_fees` for readonly method calls is very similar to non readonly method calls, however is subtly different as we use `max_fee` as the transaction fee when executing the readonly method call. ### Covering App Call Op Budget diff --git a/docs/source/capabilities/transaction-composer.md b/docs/source/capabilities/transaction-composer.md index 7eb503e4..ad4ba9a6 100644 --- a/docs/source/capabilities/transaction-composer.md +++ b/docs/source/capabilities/transaction-composer.md @@ -330,13 +330,13 @@ This feature should efficiently calculate the minimum fee needed to execute an a #### Read-only calls -When interacting with read-only calls, the transactions are not sent to the network, instead a simulation is performed to evaluate the transaction. -However, a read-only call will still consume the op budget, so to prevent this, by the default, the following logic is applied: +When invoking a readonly method, the transaction is simulated rather than being fully processed by the network. This allows users to call these methods without paying a fee. -1. If `max_fee` is not specified, `algokit-utils` will automatically set `max_fee` to `10` Algo. -2. If `max_fee` is specified, provided `max_fee` value will be respected when calculating required fee per read-only call. +Even though no actual fee is paid, the simulation still evaluates the transaction as if a fee was being paid, therefore op budget and fee coverage checks are still performed. -In either cases, resource population and app call inner transaction fees will still be automatically calculated and applied as per the rules above however there is no need to explicitly specify `populate_app_call_resources=True` or `cover_app_call_inner_transaction_fees=True` when sending read-only calls. +Because no fee is actually paid, calculating the minimum fee required to successfully execute the transaction is not required, and therefore we don't need to send an additional simulate call to calculate the minimum fee, like we do with a non readonly method call. + +The behaviour of enabling `cover_app_call_inner_transaction_fees` for readonly method calls is very similar to non readonly method calls, however is subtly different as we use `max_fee` as the transaction fee when executing the readonly method call. ### Covering App Call Op Budget diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py index 6e67f2c7..7ec293a0 100644 --- a/src/algokit_utils/applications/app_client.py +++ b/src/algokit_utils/applications/app_client.py @@ -5,7 +5,7 @@ import json import os from collections.abc import Sequence -from dataclasses import asdict, dataclass, fields +from dataclasses import asdict, dataclass, fields, replace from typing import TYPE_CHECKING, Any, Generic, Literal, TypedDict, TypeVar import algosdk @@ -54,6 +54,7 @@ AppUpdateParams, BuiltTransactions, PaymentParams, + SendAtomicTransactionComposerResults, ) from algokit_utils.transactions.transaction_sender import ( SendAppTransactionResult, @@ -1195,22 +1196,44 @@ def call( ) and self._app_spec.get_arc56_method(params.method).readonly if is_read_only_call: + readonly_params = params + readonly_send_params = send_params or SendParams() + + # Read-only calls do not require fees to be paid, as they are only simulated on the network. + # Therefore there is no value in calculating the minimum fee needed for a successful app call with inners. + # As a a result we only need to send a single simulate call, + # however to do this successfully we need to ensure fees for the transaction are fully covered using maxFee. + if readonly_send_params.get("cover_app_call_inner_transaction_fees"): + if params.max_fee is None: + raise ValueError( + "Please provide a `max_fee` for the transaction when `cover_app_call_inner_transaction_fees` is enabled." # noqa: E501 + ) + readonly_params = replace(readonly_params, static_fee=params.max_fee, extra_fee=None) + method_call_to_simulate = self._algorand.new_group().add_app_call_method_call( - self._client.params.call(params) - ) - send_params = send_params or SendParams() - simulate_response = self._client._handle_call_errors( - lambda: method_call_to_simulate.simulate( - allow_unnamed_resources=send_params.get("populate_app_call_resources") or True, - skip_signatures=True, - allow_more_logs=True, - allow_empty_signatures=True, - extra_opcode_budget=None, - exec_trace_config=None, - simulation_round=None, - ) + self._client.params.call(readonly_params) ) + def run_simulate() -> SendAtomicTransactionComposerResults: + try: + return method_call_to_simulate.simulate( + allow_unnamed_resources=readonly_send_params.get("populate_app_call_resources") or True, + skip_signatures=True, + allow_more_logs=True, + allow_empty_signatures=True, + extra_opcode_budget=None, + exec_trace_config=None, + simulation_round=None, + ) + except Exception as e: + if readonly_send_params.get("cover_app_call_inner_transaction_fees") and "fee too small" in str(e): + raise ValueError( + "Fees were too small. You may need to increase the transaction `maxFee`." + ) from e + raise + + simulate_response = self._client._handle_call_errors(run_simulate) + return SendAppTransactionResult[Arc56ReturnValueType]( tx_ids=simulate_response.tx_ids, transactions=simulate_response.transactions, diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index 06705571..cf9c04f8 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -616,12 +616,6 @@ class _TransactionWithPriority: NULL_SIGNER: TransactionSigner = algosdk.atomic_transaction_composer.EmptySigner() -def _get_dummy_max_fees_for_simulated_opups(group_len: int) -> dict[int, AlgoAmount]: - from algokit_utils.models.amount import AlgoAmount - - return {i: AlgoAmount(algo=10) for i in range(group_len)} - - def _encode_lease(lease: str | bytes | None) -> bytes | None: if lease is None: return None @@ -1711,17 +1705,6 @@ def simulate( else: self.build() - atc = prepare_group_for_sending( - atc, - self._algod, - populate_app_call_resources=allow_unnamed_resources, - cover_app_call_inner_transaction_fees=allow_unnamed_resources, - additional_atc_context=AdditionalAtcContext( - suggested_params=self._get_suggested_params(), - max_fees=self._txn_max_fees or _get_dummy_max_fees_for_simulated_opups(atc.get_tx_count()), - ), - ) - if config.debug and config.project_root and config.trace_all: response = simulate_and_persist_response( atc, diff --git a/tests/artifacts/inner-fee/application.json b/tests/artifacts/inner-fee/application.json index f223df51..9e4ffd2e 100644 --- a/tests/artifacts/inner-fee/application.json +++ b/tests/artifacts/inner-fee/application.json @@ -3,7 +3,7 @@ "structs": {}, "methods": [ { - "name": "burn_ops_readonly", + "name": "burn_ops", "args": [ { "type": "uint64", @@ -19,12 +19,12 @@ "NoOp" ] }, - "readonly": true, + "readonly": false, "events": [], "recommendations": {} }, { - "name": "burn_ops", + "name": "burn_ops_readonly", "args": [ { "type": "uint64", @@ -40,7 +40,7 @@ "NoOp" ] }, - "readonly": false, + "readonly": true, "events": [], "recommendations": {} }, @@ -182,43 +182,29 @@ "sourceInfo": [ { "pc": [ - 298, - 328, - 348, - 368, - 388, - 433, - 453, - 501, - 521 - ], - "errorMessage": "Index access is out of bounds" - }, - { - "pc": [ - 77, - 100, - 123, - 142, - 151, - 167 + 83, + 99, + 115, + 124, + 143, + 166 ], "errorMessage": "OnCompletion is not NoOp" }, { "pc": [ - 188 + 194 ], "errorMessage": "can only call when creating" }, { "pc": [ - 80, - 103, - 126, - 145, - 154, - 170 + 86, + 102, + 118, + 127, + 146, + 169 ], "errorMessage": "can only call when not creating" } @@ -231,21 +217,9 @@ } }, "source": { - "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBhbGdvcHkuYXJjNC5BUkM0Q29udHJhY3QuYXBwcm92YWxfcHJvZ3JhbSgpIC0+IHVpbnQ2NDoKbWFpbjoKICAgIGludGNibG9jayAxIDAgNiA2MAogICAgYnl0ZWNibG9jayAweDc3MjllYjMyIDB4YzJjNDg5ZTUgMHggMHgwNjgxMDEKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToxNAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuIE51bUFwcEFyZ3MKICAgIGJ6IG1haW5fYmFyZV9yb3V0aW5nQDExCiAgICBwdXNoYnl0ZXNzIDB4OWQ4OTI5YzcgMHhkZDM3ODI0NyAvLyBtZXRob2QgImJ1cm5fb3BzX3JlYWRvbmx5KHVpbnQ2NCl2b2lkIiwgbWV0aG9kICJidXJuX29wcyh1aW50NjQpdm9pZCIKICAgIGJ5dGVjXzAgLy8gbWV0aG9kICJub19vcCgpdm9pZCIKICAgIGJ5dGVjXzEgLy8gbWV0aG9kICJzZW5kX3hfaW5uZXJzX3dpdGhfZmVlcyh1aW50NjQsdWludDY0W10pdm9pZCIKICAgIHB1c2hieXRlc3MgMHgzNDM2ODJjZCAweDFjZjJmNTkwIC8vIG1ldGhvZCAic2VuZF9pbm5lcnNfd2l0aF9mZWVzKHVpbnQ2NCx1aW50NjQsKHVpbnQ2NCx1aW50NjQsdWludDY0LHVpbnQ2NCx1aW50NjRbXSkpdm9pZCIsIG1ldGhvZCAic2VuZF9pbm5lcnNfd2l0aF9mZWVzXzIodWludDY0LHVpbnQ2NCwodWludDY0LHVpbnQ2NCx1aW50NjRbXSx1aW50NjQsdWludDY0LHVpbnQ2NFtdKSl2b2lkIgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMAogICAgbWF0Y2ggbWFpbl9idXJuX29wc19yZWFkb25seV9yb3V0ZUAzIG1haW5fYnVybl9vcHNfcm91dGVANCBtYWluX25vX29wX3JvdXRlQDUgbWFpbl9zZW5kX3hfaW5uZXJzX3dpdGhfZmVlc19yb3V0ZUA2IG1haW5fc2VuZF9pbm5lcnNfd2l0aF9mZWVzX3JvdXRlQDcgbWFpbl9zZW5kX2lubmVyc193aXRoX2ZlZXNfMl9yb3V0ZUA4CgptYWluX2FmdGVyX2lmX2Vsc2VAMTU6CiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6MTQKICAgIC8vIGNsYXNzIElubmVyRmVlQ29udHJhY3QoQVJDNENvbnRyYWN0KToKICAgIGludGNfMSAvLyAwCiAgICByZXR1cm4KCm1haW5fc2VuZF9pbm5lcnNfd2l0aF9mZWVzXzJfcm91dGVAODoKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weTo1MwogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToxNAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgYnRvaQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgYnRvaQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMwogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjUzCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIGNhbGxzdWIgc2VuZF9pbm5lcnNfd2l0aF9mZWVzXzIKICAgIGludGNfMCAvLyAxCiAgICByZXR1cm4KCm1haW5fc2VuZF9pbm5lcnNfd2l0aF9mZWVzX3JvdXRlQDc6CiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6NDIKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBub3QgTm9PcAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGFzc2VydCAvLyBjYW4gb25seSBjYWxsIHdoZW4gbm90IGNyZWF0aW5nCiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6MTQKICAgIC8vIGNsYXNzIElubmVyRmVlQ29udHJhY3QoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDIKICAgIGJ0b2kKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDMKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weTo0MgogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICBjYWxsc3ViIHNlbmRfaW5uZXJzX3dpdGhfZmVlcwogICAgaW50Y18wIC8vIDEKICAgIHJldHVybgoKbWFpbl9zZW5kX3hfaW5uZXJzX3dpdGhfZmVlc19yb3V0ZUA2OgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjM3CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjE0CiAgICAvLyBjbGFzcyBJbm5lckZlZUNvbnRyYWN0KEFSQzRDb250cmFjdCk6CiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICBidG9pCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAyCiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6MzcKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgY2FsbHN1YiBzZW5kX3hfaW5uZXJzX3dpdGhfZmVlcwogICAgaW50Y18wIC8vIDEKICAgIHJldHVybgoKbWFpbl9ub19vcF9yb3V0ZUA1OgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjMzCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgaW50Y18wIC8vIDEKICAgIHJldHVybgoKbWFpbl9idXJuX29wc19yb3V0ZUA0OgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjI0CiAgICAvLyBAYXJjNC5hYmltZXRob2QoKQogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBub3QgTm9PcAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGFzc2VydCAvLyBjYW4gb25seSBjYWxsIHdoZW4gbm90IGNyZWF0aW5nCiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6MTQKICAgIC8vIGNsYXNzIElubmVyRmVlQ29udHJhY3QoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToyNAogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIGNhbGxzdWIgYnVybl9vcHMKICAgIGludGNfMCAvLyAxCiAgICByZXR1cm4KCm1haW5fYnVybl9vcHNfcmVhZG9ubHlfcm91dGVAMzoKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToxNQogICAgLy8gQGFyYzQuYWJpbWV0aG9kKHJlYWRvbmx5PVRydWUpCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToxNAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgYnRvaQogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjE1CiAgICAvLyBAYXJjNC5hYmltZXRob2QocmVhZG9ubHk9VHJ1ZSkKICAgIGNhbGxzdWIgYnVybl9vcHNfcmVhZG9ubHkKICAgIGludGNfMCAvLyAxCiAgICByZXR1cm4KCm1haW5fYmFyZV9yb3V0aW5nQDExOgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjE0CiAgICAvLyBjbGFzcyBJbm5lckZlZUNvbnRyYWN0KEFSQzRDb250cmFjdCk6CiAgICB0eG4gT25Db21wbGV0aW9uCiAgICBibnogbWFpbl9hZnRlcl9pZl9lbHNlQDE1CiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgIQogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBjcmVhdGluZwogICAgaW50Y18wIC8vIDEKICAgIHJldHVybgoKCi8vIGNvbnRyYWN0LklubmVyRmVlQ29udHJhY3QuYnVybl9vcHNfcmVhZG9ubHkob3BfYnVkZ2V0OiB1aW50NjQpIC0+IHZvaWQ6CmJ1cm5fb3BzX3JlYWRvbmx5OgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjE1LTE2CiAgICAvLyBAYXJjNC5hYmltZXRob2QocmVhZG9ubHk9VHJ1ZSkKICAgIC8vIGRlZiBidXJuX29wc19yZWFkb25seShzZWxmLCBvcF9idWRnZXQ6IFVJbnQ2NCkgLT4gTm9uZToKICAgIHByb3RvIDEgMAogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjE3LTE4CiAgICAvLyAjIFVzZXMgYXBwcm94IDYwIG9wIGJ1ZGdldCBwZXIgaXRlcmF0aW9uCiAgICAvLyBjb3VudCA9IG9wX2J1ZGdldCAvLyA2MAogICAgZnJhbWVfZGlnIC0xCiAgICBpbnRjXzMgLy8gNjAKICAgIC8KICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToxOQogICAgLy8gZW5zdXJlX2J1ZGdldChvcF9idWRnZXQpCiAgICBmcmFtZV9kaWcgLTEKICAgIGludGNfMSAvLyAwCiAgICBjYWxsc3ViIGVuc3VyZV9idWRnZXQKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToyMAogICAgLy8gZm9yIGkgaW4gdXJhbmdlKGNvdW50KToKICAgIGludGNfMSAvLyAwCgpidXJuX29wc19yZWFkb25seV9mb3JfaGVhZGVyQDE6CiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6MjAKICAgIC8vIGZvciBpIGluIHVyYW5nZShjb3VudCk6CiAgICBmcmFtZV9kaWcgMQogICAgZnJhbWVfZGlnIDAKICAgIDwKICAgIGJ6IGJ1cm5fb3BzX3JlYWRvbmx5X2FmdGVyX2ZvckA0CiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6MjEKICAgIC8vIHNxcnQgPSBvcC5ic3FydChCaWdVSW50KGkpKQogICAgZnJhbWVfZGlnIDEKICAgIGR1cAogICAgaXRvYgogICAgYnNxcnQKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToyMgogICAgLy8gYXNzZXJ0KHNxcnQgPj0gMCkgIyBQcmV2ZW50IG9wdGltaXNlciByZW1vdmluZyB0aGUgc3FydAogICAgYnl0ZWNfMiAvLyAweAogICAgYj49CiAgICBhc3NlcnQKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToyMAogICAgLy8gZm9yIGkgaW4gdXJhbmdlKGNvdW50KToKICAgIGludGNfMCAvLyAxCiAgICArCiAgICBmcmFtZV9idXJ5IDEKICAgIGIgYnVybl9vcHNfcmVhZG9ubHlfZm9yX2hlYWRlckAxCgpidXJuX29wc19yZWFkb25seV9hZnRlcl9mb3JANDoKICAgIHJldHN1YgoKCi8vIGNvbnRyYWN0LklubmVyRmVlQ29udHJhY3QuYnVybl9vcHMob3BfYnVkZ2V0OiB1aW50NjQpIC0+IHZvaWQ6CmJ1cm5fb3BzOgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjI0LTI1CiAgICAvLyBAYXJjNC5hYmltZXRob2QoKQogICAgLy8gZGVmIGJ1cm5fb3BzKHNlbGYsIG9wX2J1ZGdldDogVUludDY0KSAtPiBOb25lOgogICAgcHJvdG8gMSAwCiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6MjYtMjcKICAgIC8vICMgVXNlcyBhcHByb3ggNjAgb3AgYnVkZ2V0IHBlciBpdGVyYXRpb24KICAgIC8vIGNvdW50ID0gb3BfYnVkZ2V0IC8vIDYwCiAgICBmcmFtZV9kaWcgLTEKICAgIGludGNfMyAvLyA2MAogICAgLwogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjI4CiAgICAvLyBlbnN1cmVfYnVkZ2V0KG9wX2J1ZGdldCkKICAgIGZyYW1lX2RpZyAtMQogICAgaW50Y18xIC8vIDAKICAgIGNhbGxzdWIgZW5zdXJlX2J1ZGdldAogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjI5CiAgICAvLyBmb3IgaSBpbiB1cmFuZ2UoY291bnQpOgogICAgaW50Y18xIC8vIDAKCmJ1cm5fb3BzX2Zvcl9oZWFkZXJAMToKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weToyOQogICAgLy8gZm9yIGkgaW4gdXJhbmdlKGNvdW50KToKICAgIGZyYW1lX2RpZyAxCiAgICBmcmFtZV9kaWcgMAogICAgPAogICAgYnogYnVybl9vcHNfYWZ0ZXJfZm9yQDQKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weTozMAogICAgLy8gc3FydCA9IG9wLmJzcXJ0KEJpZ1VJbnQoaSkpCiAgICBmcmFtZV9kaWcgMQogICAgZHVwCiAgICBpdG9iCiAgICBic3FydAogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjMxCiAgICAvLyBhc3NlcnQoc3FydCA+PSAwKSAjIFByZXZlbnQgb3B0aW1pc2VyIHJlbW92aW5nIHRoZSBzcXJ0CiAgICBieXRlY18yIC8vIDB4CiAgICBiPj0KICAgIGFzc2VydAogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjI5CiAgICAvLyBmb3IgaSBpbiB1cmFuZ2UoY291bnQpOgogICAgaW50Y18wIC8vIDEKICAgICsKICAgIGZyYW1lX2J1cnkgMQogICAgYiBidXJuX29wc19mb3JfaGVhZGVyQDEKCmJ1cm5fb3BzX2FmdGVyX2ZvckA0OgogICAgcmV0c3ViCgoKLy8gY29udHJhY3QuSW5uZXJGZWVDb250cmFjdC5zZW5kX3hfaW5uZXJzX3dpdGhfZmVlcyhhcHBfaWQ6IHVpbnQ2NCwgZmVlczogYnl0ZXMpIC0+IHZvaWQ6CnNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzOgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjM3LTM4CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIC8vIGRlZiBzZW5kX3hfaW5uZXJzX3dpdGhfZmVlcyhzZWxmLCBhcHBfaWQ6IFVJbnQ2NCwgZmVlczogYXJjNC5EeW5hbWljQXJyYXlbYXJjNC5VSW50NjRdKSAtPiBOb25lOgogICAgcHJvdG8gMiAwCiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6MzkKICAgIC8vIGZvciBmZWUgaW4gZmVlczoKICAgIGZyYW1lX2RpZyAtMQogICAgaW50Y18xIC8vIDAKICAgIGV4dHJhY3RfdWludDE2CiAgICBpbnRjXzEgLy8gMAoKc2VuZF94X2lubmVyc193aXRoX2ZlZXNfZm9yX2hlYWRlckAxOgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjM5CiAgICAvLyBmb3IgZmVlIGluIGZlZXM6CiAgICBmcmFtZV9kaWcgMQogICAgZnJhbWVfZGlnIDAKICAgIDwKICAgIGJ6IHNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzX2FmdGVyX2ZvckA1CiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMiAwCiAgICBmcmFtZV9kaWcgMQogICAgZHVwCiAgICBjb3ZlciAyCiAgICBwdXNoaW50IDggLy8gOAogICAgKgogICAgcHVzaGludCA4IC8vIDgKICAgIGV4dHJhY3QzIC8vIG9uIGVycm9yOiBJbmRleCBhY2Nlc3MgaXMgb3V0IG9mIGJvdW5kcwogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjQwCiAgICAvLyBhcmM0LmFiaV9jYWxsKCdub19vcCcsIGFwcF9pZD1hcHBfaWQsIGZlZT1mZWUubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgYnRvaQogICAgZnJhbWVfZGlnIC0yCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKICAgIGJ5dGVjXzAgLy8gbWV0aG9kICJub19vcCgpdm9pZCIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBpbnRjXzIgLy8gYXBwbAogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaXR4bl9maWVsZCBGZWUKICAgIGl0eG5fc3VibWl0CiAgICBpbnRjXzAgLy8gMQogICAgKwogICAgZnJhbWVfYnVyeSAxCiAgICBiIHNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzX2Zvcl9oZWFkZXJAMQoKc2VuZF94X2lubmVyc193aXRoX2ZlZXNfYWZ0ZXJfZm9yQDU6CiAgICByZXRzdWIKCgovLyBjb250cmFjdC5Jbm5lckZlZUNvbnRyYWN0LnNlbmRfaW5uZXJzX3dpdGhfZmVlcyhhcHBfaWRfMTogdWludDY0LCBhcHBfaWRfMjogdWludDY0LCBmZWVzOiBieXRlcykgLT4gdm9pZDoKc2VuZF9pbm5lcnNfd2l0aF9mZWVzOgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjQyLTQzCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIC8vIGRlZiBzZW5kX2lubmVyc193aXRoX2ZlZXMoc2VsZiwgYXBwX2lkXzE6IFVJbnQ2NCwgYXBwX2lkXzI6IFVJbnQ2NCwgZmVlczogYXJjNC5UdXBsZVthcmM0LlVJbnQ2NCwgYXJjNC5VSW50NjQsIGFyYzQuVUludDY0LCBhcmM0LlVJbnQ2NCwgYXJjNC5EeW5hbWljQXJyYXlbYXJjNC5VSW50NjRdXSkgLT4gTm9uZToKICAgIHByb3RvIDMgMAogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjQ0CiAgICAvLyBhcmM0LmFiaV9jYWxsKCdub19vcCcsIGFwcF9pZD1hcHBfaWRfMSwgZmVlPWZlZXNbMF0ubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgZnJhbWVfZGlnIC0xCiAgICBleHRyYWN0IDAgOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18wIC8vIG1ldGhvZCAibm9fb3AoKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18yIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjQ1CiAgICAvLyBhcmM0LmFiaV9jYWxsKCdub19vcCcsIGFwcF9pZD1hcHBfaWRfMSwgZmVlPWZlZXNbMV0ubmF0aXZlKQogICAgaXR4bl9iZWdpbgogICAgZnJhbWVfZGlnIC0xCiAgICBleHRyYWN0IDggOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18wIC8vIG1ldGhvZCAibm9fb3AoKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18yIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjQ2LTUwCiAgICAvLyBpdHhuLlBheW1lbnQoCiAgICAvLyAgICAgYW1vdW50PTAsCiAgICAvLyAgICAgcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICBmZWU9ZmVlc1syXS5uYXRpdmUKICAgIC8vICkuc3VibWl0KCkKICAgIGl0eG5fYmVnaW4KICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weTo0OQogICAgLy8gZmVlPWZlZXNbMl0ubmF0aXZlCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMTYgOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weTo0OAogICAgLy8gcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIGdsb2JhbCBDdXJyZW50QXBwbGljYXRpb25BZGRyZXNzCiAgICBpdHhuX2ZpZWxkIFJlY2VpdmVyCiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6NDcKICAgIC8vIGFtb3VudD0wLAogICAgaW50Y18xIC8vIDAKICAgIGl0eG5fZmllbGQgQW1vdW50CiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6NDYKICAgIC8vIGl0eG4uUGF5bWVudCgKICAgIGludGNfMCAvLyBwYXkKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6NDYtNTAKICAgIC8vIGl0eG4uUGF5bWVudCgKICAgIC8vICAgICBhbW91bnQ9MCwKICAgIC8vICAgICByZWNlaXZlcj1HbG9iYWwuY3VycmVudF9hcHBsaWNhdGlvbl9hZGRyZXNzLAogICAgLy8gICAgIGZlZT1mZWVzWzJdLm5hdGl2ZQogICAgLy8gKS5zdWJtaXQoKQogICAgaXR4bl9zdWJtaXQKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weTo1MQogICAgLy8gYXJjNC5hYmlfY2FsbCgnc2VuZF94X2lubmVyc193aXRoX2ZlZXMnLCBhcHBfaWRfMiwgZmVlc1s0XSwgYXBwX2lkPWFwcF9pZF8xLCBmZWU9ZmVlc1szXS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMjQgOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMgogICAgaXRvYgogICAgZnJhbWVfZGlnIC0xCiAgICBwdXNoaW50IDMyIC8vIDMyCiAgICBleHRyYWN0X3VpbnQxNgogICAgZnJhbWVfZGlnIC0xCiAgICBsZW4KICAgIGZyYW1lX2RpZyAtMQogICAgY292ZXIgMgogICAgc3Vic3RyaW5nMwogICAgZnJhbWVfZGlnIC0zCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKICAgIGJ5dGVjXzEgLy8gbWV0aG9kICJzZW5kX3hfaW5uZXJzX3dpdGhfZmVlcyh1aW50NjQsdWludDY0W10pdm9pZCIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBzd2FwCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGludGNfMiAvLyBhcHBsCiAgICBpdHhuX2ZpZWxkIFR5cGVFbnVtCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgaXR4bl9zdWJtaXQKICAgIHJldHN1YgoKCi8vIGNvbnRyYWN0LklubmVyRmVlQ29udHJhY3Quc2VuZF9pbm5lcnNfd2l0aF9mZWVzXzIoYXBwX2lkXzE6IHVpbnQ2NCwgYXBwX2lkXzI6IHVpbnQ2NCwgZmVlczogYnl0ZXMpIC0+IHZvaWQ6CnNlbmRfaW5uZXJzX3dpdGhfZmVlc18yOgogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjUzLTU0CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIC8vIGRlZiBzZW5kX2lubmVyc193aXRoX2ZlZXNfMihzZWxmLCBhcHBfaWRfMTogVUludDY0LCBhcHBfaWRfMjogVUludDY0LCBmZWVzOiBhcmM0LlR1cGxlW2FyYzQuVUludDY0LCBhcmM0LlVJbnQ2NCwgYXJjNC5EeW5hbWljQXJyYXlbYXJjNC5VSW50NjRdLCBhcmM0LlVJbnQ2NCwgYXJjNC5VSW50NjQsIGFyYzQuRHluYW1pY0FycmF5W2FyYzQuVUludDY0XV0pIC0+IE5vbmU6CiAgICBwcm90byAzIDAKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weTo1NQogICAgLy8gYXJjNC5hYmlfY2FsbCgnbm9fb3AnLCBhcHBfaWQ9YXBwX2lkXzEsIGZlZT1mZWVzWzBdLm5hdGl2ZSkKICAgIGl0eG5fYmVnaW4KICAgIGZyYW1lX2RpZyAtMQogICAgZXh0cmFjdCAwIDggLy8gb24gZXJyb3I6IEluZGV4IGFjY2VzcyBpcyBvdXQgb2YgYm91bmRzCiAgICBidG9pCiAgICBmcmFtZV9kaWcgLTMKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25JRAogICAgYnl0ZWNfMCAvLyBtZXRob2QgIm5vX29wKCl2b2lkIgogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGludGNfMiAvLyBhcHBsCiAgICBpdHhuX2ZpZWxkIFR5cGVFbnVtCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgaXR4bl9zdWJtaXQKICAgIC8vIHRlc3RzL2V4YW1wbGUtY29udHJhY3RzL2lubmVyLWZlZS9jb250cmFjdC5weTo1NgogICAgLy8gYXJjNC5hYmlfY2FsbCgnc2VuZF94X2lubmVyc193aXRoX2ZlZXMnLCBhcHBfaWRfMiwgZmVlc1syXSwgYXBwX2lkPWFwcF9pZF8xLCBmZWU9ZmVlc1sxXS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgOCA4IC8vIG9uIGVycm9yOiBJbmRleCBhY2Nlc3MgaXMgb3V0IG9mIGJvdW5kcwogICAgYnRvaQogICAgZnJhbWVfZGlnIC0yCiAgICBpdG9iCiAgICBmcmFtZV9kaWcgLTEKICAgIHB1c2hpbnQgMTYgLy8gMTYKICAgIGV4dHJhY3RfdWludDE2CiAgICBmcmFtZV9kaWcgLTEKICAgIHB1c2hpbnQgMzQgLy8gMzQKICAgIGV4dHJhY3RfdWludDE2CiAgICBmcmFtZV9kaWcgLTEKICAgIHVuY292ZXIgMgogICAgZGlnIDIKICAgIHN1YnN0cmluZzMKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18xIC8vIG1ldGhvZCAic2VuZF94X2lubmVyc193aXRoX2ZlZXModWludDY0LHVpbnQ2NFtdKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgZGlnIDIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18yIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIHVuY292ZXIgMgogICAgaXR4bl9maWVsZCBGZWUKICAgIGl0eG5fc3VibWl0CiAgICAvLyB0ZXN0cy9leGFtcGxlLWNvbnRyYWN0cy9pbm5lci1mZWUvY29udHJhY3QucHk6NTcKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ25vX29wJywgYXBwX2lkPWFwcF9pZF8xLCBmZWU9ZmVlc1szXS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMTggOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18wIC8vIG1ldGhvZCAibm9fb3AoKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18yIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgLy8gdGVzdHMvZXhhbXBsZS1jb250cmFjdHMvaW5uZXItZmVlL2NvbnRyYWN0LnB5OjU4CiAgICAvLyBhcmM0LmFiaV9jYWxsKCdzZW5kX3hfaW5uZXJzX3dpdGhfZmVlcycsIGFwcF9pZF8yLCBmZWVzWzVdLCBhcHBfaWQ9YXBwX2lkXzEsIGZlZT1mZWVzWzRdLm5hdGl2ZSkKICAgIGl0eG5fYmVnaW4KICAgIGZyYW1lX2RpZyAtMQogICAgZXh0cmFjdCAyNiA4IC8vIG9uIGVycm9yOiBJbmRleCBhY2Nlc3MgaXMgb3V0IG9mIGJvdW5kcwogICAgYnRvaQogICAgZnJhbWVfZGlnIC0xCiAgICBsZW4KICAgIGZyYW1lX2RpZyAtMQogICAgdW5jb3ZlciAzCiAgICB1bmNvdmVyIDIKICAgIHN1YnN0cmluZzMKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18xIC8vIG1ldGhvZCAic2VuZF94X2lubmVyc193aXRoX2ZlZXModWludDY0LHVpbnQ2NFtdKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgdW5jb3ZlciAyCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGludGNfMiAvLyBhcHBsCiAgICBpdHhuX2ZpZWxkIFR5cGVFbnVtCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgaXR4bl9zdWJtaXQKICAgIHJldHN1YgoKCi8vIF9wdXlhX2xpYi51dGlsLmVuc3VyZV9idWRnZXQocmVxdWlyZWRfYnVkZ2V0OiB1aW50NjQsIGZlZV9zb3VyY2U6IHVpbnQ2NCkgLT4gdm9pZDoKZW5zdXJlX2J1ZGdldDoKICAgIHByb3RvIDIgMAogICAgZnJhbWVfZGlnIC0yCiAgICBwdXNoaW50IDEwIC8vIDEwCiAgICArCgplbnN1cmVfYnVkZ2V0X3doaWxlX3RvcEAxOgogICAgZnJhbWVfZGlnIDAKICAgIGdsb2JhbCBPcGNvZGVCdWRnZXQKICAgID4KICAgIGJ6IGVuc3VyZV9idWRnZXRfYWZ0ZXJfd2hpbGVANwogICAgaXR4bl9iZWdpbgogICAgaW50Y18yIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIHB1c2hpbnQgNSAvLyBEZWxldGVBcHBsaWNhdGlvbgogICAgaXR4bl9maWVsZCBPbkNvbXBsZXRpb24KICAgIGJ5dGVjXzMgLy8gMHgwNjgxMDEKICAgIGl0eG5fZmllbGQgQXBwcm92YWxQcm9ncmFtCiAgICBieXRlY18zIC8vIDB4MDY4MTAxCiAgICBpdHhuX2ZpZWxkIENsZWFyU3RhdGVQcm9ncmFtCiAgICBmcmFtZV9kaWcgLTEKICAgIHN3aXRjaCBlbnN1cmVfYnVkZ2V0X3N3aXRjaF9jYXNlXzBAMyBlbnN1cmVfYnVkZ2V0X3N3aXRjaF9jYXNlXzFANAoKZW5zdXJlX2J1ZGdldF9zd2l0Y2hfY2FzZV9uZXh0QDY6CiAgICBpdHhuX3N1Ym1pdAogICAgYiBlbnN1cmVfYnVkZ2V0X3doaWxlX3RvcEAxCgplbnN1cmVfYnVkZ2V0X3N3aXRjaF9jYXNlXzFANDoKICAgIGdsb2JhbCBNaW5UeG5GZWUKICAgIGl0eG5fZmllbGQgRmVlCiAgICBiIGVuc3VyZV9idWRnZXRfc3dpdGNoX2Nhc2VfbmV4dEA2CgplbnN1cmVfYnVkZ2V0X3N3aXRjaF9jYXNlXzBAMzoKICAgIGludGNfMSAvLyAwCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgYiBlbnN1cmVfYnVkZ2V0X3N3aXRjaF9jYXNlX25leHRANgoKZW5zdXJlX2J1ZGdldF9hZnRlcl93aGlsZUA3OgogICAgcmV0c3ViCg==", - "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBhbGdvcHkuYXJjNC5BUkM0Q29udHJhY3QuY2xlYXJfc3RhdGVfcHJvZ3JhbSgpIC0+IHVpbnQ2NDoKbWFpbjoKICAgIHB1c2hpbnQgMSAvLyAxCiAgICByZXR1cm4K" - }, - "byteCode": { - "approval": "CiAEAQAGPCYEBHcp6zIEwsSJ5QADBoEBMRtBAJeCAgSdiSnHBN03gkcoKYICBDQ2gs0EHPL1kDYaAI4GAFwATABDADAAGQACI0MxGRREMRhENhoBFzYaAhc2GgOIAUwiQzEZFEQxGEQ2GgEXNhoCFzYaA4gAzCJDMRkURDEYRDYaARc2GgKIAIIiQzEZFEQxGEQiQzEZFEQxGEQ2GgEXiABDIkMxGRREMRhENhoBF4gADSJDMRlA/48xGBREIkOKAQCL/yUKi/8jiAFfI4sBiwAMQQAPiwFJFpYqp0QiCIwBQv/piYoBAIv/JQqL/yOIATkjiwGLAAxBAA+LAUkWliqnRCIIjAFC/+mJigIAi/8jWSOLAYsADEEAJov/VwIAiwFJTgKBCAuBCFixF4v+shgoshokshCyAbMiCIwBQv/SiYoDALGL/1cACBeL/bIYKLIaJLIQsgGzsYv/VwgIF4v9shgoshokshCyAbOxi/9XEAgXMgqyByOyCCKyELIBs7GL/1cYCBeL/haL/4EgWYv/FYv/TgJSi/2yGCmyGkyyGrIaJLIQsgGziYoDALGL/1cACBeL/bIYKLIaJLIQsgGzsYv/VwgIF4v+Fov/gRBZi/+BIlmL/08CSwJSi/2yGCmyGksCshqyGiSyEE8CsgGzsYv/VxIIF4v9shgoshokshCyAbOxi/9XGggXi/8Vi/9PA08CUov9shgpshpPArIashokshCyAbOJigIAi/6BCgiLADIMDUEAJ7EkshCBBbIZK7IeK7Ifi/+NAgALAASzQv/eMgCyAUL/9SOyAUL/74k=", - "clear": "CoEBQw==" - }, - "compilerInfo": { - "compiler": "puya", - "compilerVersion": { - "major": 4, - "minor": 1, - "patch": 1 - } + "approval": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMudGVzdF9jb250cmFjdC5jb250cmFjdC5Jbm5lckZlZUNvbnRyYWN0LmFwcHJvdmFsX3Byb2dyYW06CiAgICBpbnRjYmxvY2sgMSA2IDAgOAogICAgYnl0ZWNibG9jayAweDc3MjllYjMyIDB4YzJjNDg5ZTUgMHgwNjgxMDEKICAgIGNhbGxzdWIgX19wdXlhX2FyYzRfcm91dGVyX18KICAgIHJldHVybgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy50ZXN0X2NvbnRyYWN0LmNvbnRyYWN0LklubmVyRmVlQ29udHJhY3QuX19wdXlhX2FyYzRfcm91dGVyX18oKSAtPiB1aW50NjQ6Cl9fcHV5YV9hcmM0X3JvdXRlcl9fOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgcHJvdG8gMCAxCiAgICB0eG4gTnVtQXBwQXJncwogICAgYnogX19wdXlhX2FyYzRfcm91dGVyX19fYmFyZV9yb3V0aW5nQDEwCiAgICBwdXNoYnl0ZXNzIDB4ZGQzNzgyNDcgMHg5ZDg5MjljNyAvLyBtZXRob2QgImJ1cm5fb3BzKHVpbnQ2NCl2b2lkIiwgbWV0aG9kICJidXJuX29wc19yZWFkb25seSh1aW50NjQpdm9pZCIKICAgIGJ5dGVjXzAgLy8gbWV0aG9kICJub19vcCgpdm9pZCIKICAgIGJ5dGVjXzEgLy8gbWV0aG9kICJzZW5kX3hfaW5uZXJzX3dpdGhfZmVlcyh1aW50NjQsdWludDY0W10pdm9pZCIKICAgIHB1c2hieXRlc3MgMHgzNDM2ODJjZCAweDFjZjJmNTkwIC8vIG1ldGhvZCAic2VuZF9pbm5lcnNfd2l0aF9mZWVzKHVpbnQ2NCx1aW50NjQsKHVpbnQ2NCx1aW50NjQsdWludDY0LHVpbnQ2NCx1aW50NjRbXSkpdm9pZCIsIG1ldGhvZCAic2VuZF9pbm5lcnNfd2l0aF9mZWVzXzIodWludDY0LHVpbnQ2NCwodWludDY0LHVpbnQ2NCx1aW50NjRbXSx1aW50NjQsdWludDY0LHVpbnQ2NFtdKSl2b2lkIgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMAogICAgbWF0Y2ggX19wdXlhX2FyYzRfcm91dGVyX19fYnVybl9vcHNfcm91dGVAMiBfX3B1eWFfYXJjNF9yb3V0ZXJfX19idXJuX29wc19yZWFkb25seV9yb3V0ZUAzIF9fcHV5YV9hcmM0X3JvdXRlcl9fX25vX29wX3JvdXRlQDQgX19wdXlhX2FyYzRfcm91dGVyX19fc2VuZF94X2lubmVyc193aXRoX2ZlZXNfcm91dGVANSBfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZW5kX2lubmVyc193aXRoX2ZlZXNfcm91dGVANiBfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZW5kX2lubmVyc193aXRoX2ZlZXNfMl9yb3V0ZUA3CiAgICBpbnRjXzIgLy8gMAogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19idXJuX29wc19yb3V0ZUAyOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NQogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjQKICAgIC8vIGNsYXNzIElubmVyRmVlQ29udHJhY3QoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjUKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgY2FsbHN1YiBidXJuX29wcwogICAgaW50Y18wIC8vIDEKICAgIHJldHN1YgoKX19wdXlhX2FyYzRfcm91dGVyX19fYnVybl9vcHNfcmVhZG9ubHlfcm91dGVAMzoKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjE0CiAgICAvLyBAYXJjNC5hYmltZXRob2QocmVhZG9ubHk9VHJ1ZSkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgYnRvaQogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTQKICAgIC8vIEBhcmM0LmFiaW1ldGhvZChyZWFkb25seT1UcnVlKQogICAgY2FsbHN1YiBidXJuX29wc19yZWFkb25seQogICAgaW50Y18wIC8vIDEKICAgIHJldHN1YgoKX19wdXlhX2FyYzRfcm91dGVyX19fbm9fb3Bfcm91dGVANDoKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjE4CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgaW50Y18wIC8vIDEKICAgIHJldHN1YgoKX19wdXlhX2FyYzRfcm91dGVyX19fc2VuZF94X2lubmVyc193aXRoX2ZlZXNfcm91dGVANToKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjIyCiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NAogICAgLy8gY2xhc3MgSW5uZXJGZWVDb250cmFjdChBUkM0Q29udHJhY3QpOgogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQogICAgYnRvaQogICAgdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MjIKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgY2FsbHN1YiBzZW5kX3hfaW5uZXJzX3dpdGhfZmVlcwogICAgaW50Y18wIC8vIDEKICAgIHJldHN1YgoKX19wdXlhX2FyYzRfcm91dGVyX19fc2VuZF9pbm5lcnNfd2l0aF9mZWVzX3JvdXRlQDY6CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToyNwogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICB0eG4gT25Db21wbGV0aW9uCiAgICAhCiAgICBhc3NlcnQgLy8gT25Db21wbGV0aW9uIGlzIG5vdCBOb09wCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBub3QgY3JlYXRpbmcKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjQKICAgIC8vIGNsYXNzIElubmVyRmVlQ29udHJhY3QoQVJDNENvbnRyYWN0KToKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDEKICAgIGJ0b2kKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDIKICAgIGJ0b2kKICAgIHR4bmEgQXBwbGljYXRpb25BcmdzIDMKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjI3CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIGNhbGxzdWIgc2VuZF9pbm5lcnNfd2l0aF9mZWVzCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19zZW5kX2lubmVyc193aXRoX2ZlZXNfMl9yb3V0ZUA3OgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MzgKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgdHhuIE9uQ29tcGxldGlvbgogICAgIQogICAgYXNzZXJ0IC8vIE9uQ29tcGxldGlvbiBpcyBub3QgTm9PcAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgIGFzc2VydCAvLyBjYW4gb25seSBjYWxsIHdoZW4gbm90IGNyZWF0aW5nCiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTo0CiAgICAvLyBjbGFzcyBJbm5lckZlZUNvbnRyYWN0KEFSQzRDb250cmFjdCk6CiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAxCiAgICBidG9pCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAyCiAgICBidG9pCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAzCiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTozOAogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICBjYWxsc3ViIHNlbmRfaW5uZXJzX3dpdGhfZmVlc18yCiAgICBpbnRjXzAgLy8gMQogICAgcmV0c3ViCgpfX3B1eWFfYXJjNF9yb3V0ZXJfX19iYXJlX3JvdXRpbmdAMTA6CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTo0CiAgICAvLyBjbGFzcyBJbm5lckZlZUNvbnRyYWN0KEFSQzRDb250cmFjdCk6CiAgICB0eG4gT25Db21wbGV0aW9uCiAgICBibnogX19wdXlhX2FyYzRfcm91dGVyX19fYWZ0ZXJfaWZfZWxzZUAxNAogICAgdHhuIEFwcGxpY2F0aW9uSUQKICAgICEKICAgIGFzc2VydCAvLyBjYW4gb25seSBjYWxsIHdoZW4gY3JlYXRpbmcKICAgIGludGNfMCAvLyAxCiAgICByZXRzdWIKCl9fcHV5YV9hcmM0X3JvdXRlcl9fX2FmdGVyX2lmX2Vsc2VAMTQ6CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTo0CiAgICAvLyBjbGFzcyBJbm5lckZlZUNvbnRyYWN0KEFSQzRDb250cmFjdCk6CiAgICBpbnRjXzIgLy8gMAogICAgcmV0c3ViCgoKLy8gc21hcnRfY29udHJhY3RzLnRlc3RfY29udHJhY3QuY29udHJhY3QuSW5uZXJGZWVDb250cmFjdC5idXJuX29wcyhvcF9idWRnZXQ6IHVpbnQ2NCkgLT4gdm9pZDoKYnVybl9vcHM6CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTo1LTYKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgLy8gZGVmIGJ1cm5fb3BzKHNlbGYsIG9wX2J1ZGdldDogVUludDY0KSAtPiBOb25lOgogICAgcHJvdG8gMSAwCiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTo3LTgKICAgIC8vICMgVXNlcyBhcHByb3ggNjAgb3AgYnVkZ2V0IHBlciBpdGVyYXRpb24KICAgIC8vIGNvdW50ID0gb3BfYnVkZ2V0IC8vIDYwCiAgICBmcmFtZV9kaWcgLTEKICAgIHB1c2hpbnQgNjAgLy8gNjAKICAgIC8KICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjkKICAgIC8vIGVuc3VyZV9idWRnZXQob3BfYnVkZ2V0KQogICAgZnJhbWVfZGlnIC0xCiAgICBpbnRjXzIgLy8gMAogICAgY2FsbHN1YiBlbnN1cmVfYnVkZ2V0CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToxMAogICAgLy8gZm9yIGkgaW4gdXJhbmdlKGNvdW50KToKICAgIGludGNfMiAvLyAwCgpidXJuX29wc19mb3JfaGVhZGVyQDE6CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToxMAogICAgLy8gZm9yIGkgaW4gdXJhbmdlKGNvdW50KToKICAgIGZyYW1lX2RpZyAxCiAgICBmcmFtZV9kaWcgMAogICAgPAogICAgYnogYnVybl9vcHNfYWZ0ZXJfZm9yQDQKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjExCiAgICAvLyBzcXJ0ID0gb3AuYnNxcnQoQmlnVUludChpKSkKICAgIGZyYW1lX2RpZyAxCiAgICBkdXAKICAgIGl0b2IKICAgIGJzcXJ0CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToxMgogICAgLy8gYXNzZXJ0KHNxcnQgPj0gMCkgIyBQcmV2ZW50IG9wdGltaXNlciByZW1vdmluZyB0aGUgc3FydAogICAgcHVzaGJ5dGVzIDB4CiAgICBiPj0KICAgIGFzc2VydAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTAKICAgIC8vIGZvciBpIGluIHVyYW5nZShjb3VudCk6CiAgICBpbnRjXzAgLy8gMQogICAgKwogICAgZnJhbWVfYnVyeSAxCiAgICBiIGJ1cm5fb3BzX2Zvcl9oZWFkZXJAMQoKYnVybl9vcHNfYWZ0ZXJfZm9yQDQ6CiAgICByZXRzdWIKCgovLyBzbWFydF9jb250cmFjdHMudGVzdF9jb250cmFjdC5jb250cmFjdC5Jbm5lckZlZUNvbnRyYWN0LmJ1cm5fb3BzX3JlYWRvbmx5KG9wX2J1ZGdldDogdWludDY0KSAtPiB2b2lkOgpidXJuX29wc19yZWFkb25seToKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjE0LTE1CiAgICAvLyBAYXJjNC5hYmltZXRob2QocmVhZG9ubHk9VHJ1ZSkKICAgIC8vIGRlZiBidXJuX29wc19yZWFkb25seShzZWxmLCBvcF9idWRnZXQ6IFVJbnQ2NCkgLT4gTm9uZToKICAgIHByb3RvIDEgMAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MTYKICAgIC8vIHNlbGYuYnVybl9vcHMob3BfYnVkZ2V0KQogICAgZnJhbWVfZGlnIC0xCiAgICBjYWxsc3ViIGJ1cm5fb3BzCiAgICByZXRzdWIKCgovLyBzbWFydF9jb250cmFjdHMudGVzdF9jb250cmFjdC5jb250cmFjdC5Jbm5lckZlZUNvbnRyYWN0LnNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzKGFwcF9pZDogdWludDY0LCBmZWVzOiBieXRlcykgLT4gdm9pZDoKc2VuZF94X2lubmVyc193aXRoX2ZlZXM6CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToyMi0yMwogICAgLy8gQGFyYzQuYWJpbWV0aG9kCiAgICAvLyBkZWYgc2VuZF94X2lubmVyc193aXRoX2ZlZXMoc2VsZiwgYXBwX2lkOiBVSW50NjQsIGZlZXM6IGFyYzQuRHluYW1pY0FycmF5W2FyYzQuVUludDY0XSkgLT4gTm9uZToKICAgIHByb3RvIDIgMAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MjQKICAgIC8vIGZvciBmZWUgaW4gZmVlczoKICAgIGZyYW1lX2RpZyAtMQogICAgaW50Y18yIC8vIDAKICAgIGV4dHJhY3RfdWludDE2CiAgICBpbnRjXzIgLy8gMAoKc2VuZF94X2lubmVyc193aXRoX2ZlZXNfZm9yX2hlYWRlckAxOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MjQKICAgIC8vIGZvciBmZWUgaW4gZmVlczoKICAgIGZyYW1lX2RpZyAxCiAgICBmcmFtZV9kaWcgMAogICAgPAogICAgYnogc2VuZF94X2lubmVyc193aXRoX2ZlZXNfYWZ0ZXJfZm9yQDUKICAgIGZyYW1lX2RpZyAtMQogICAgZXh0cmFjdCAyIDAKICAgIGZyYW1lX2RpZyAxCiAgICBkdXAKICAgIGNvdmVyIDIKICAgIGludGNfMyAvLyA4CiAgICAqCiAgICBpbnRjXzMgLy8gOAogICAgZXh0cmFjdDMgLy8gb24gZXJyb3I6IEluZGV4IGFjY2VzcyBpcyBvdXQgb2YgYm91bmRzCiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weToyNQogICAgLy8gYXJjNC5hYmlfY2FsbCgnbm9fb3AnLCBhcHBfaWQ9YXBwX2lkLCBmZWU9ZmVlLm5hdGl2ZSkKICAgIGl0eG5fYmVnaW4KICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMgogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18wIC8vIG1ldGhvZCAibm9fb3AoKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18xIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIGl0eG5fZmllbGQgRmVlCiAgICBpdHhuX3N1Ym1pdAogICAgaW50Y18wIC8vIDEKICAgICsKICAgIGZyYW1lX2J1cnkgMQogICAgYiBzZW5kX3hfaW5uZXJzX3dpdGhfZmVlc19mb3JfaGVhZGVyQDEKCnNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzX2FmdGVyX2ZvckA1OgogICAgcmV0c3ViCgoKLy8gc21hcnRfY29udHJhY3RzLnRlc3RfY29udHJhY3QuY29udHJhY3QuSW5uZXJGZWVDb250cmFjdC5zZW5kX2lubmVyc193aXRoX2ZlZXMoYXBwX2lkXzE6IHVpbnQ2NCwgYXBwX2lkXzI6IHVpbnQ2NCwgZmVlczogYnl0ZXMpIC0+IHZvaWQ6CnNlbmRfaW5uZXJzX3dpdGhfZmVlczoKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjI3LTI4CiAgICAvLyBAYXJjNC5hYmltZXRob2QKICAgIC8vIGRlZiBzZW5kX2lubmVyc193aXRoX2ZlZXMoc2VsZiwgYXBwX2lkXzE6IFVJbnQ2NCwgYXBwX2lkXzI6IFVJbnQ2NCwgZmVlczogYXJjNC5UdXBsZVthcmM0LlVJbnQ2NCwgYXJjNC5VSW50NjQsIGFyYzQuVUludDY0LCBhcmM0LlVJbnQ2NCwgYXJjNC5EeW5hbWljQXJyYXlbYXJjNC5VSW50NjRdXSkgLT4gTm9uZToKICAgIHByb3RvIDMgMAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MjkKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ25vX29wJywgYXBwX2lkPWFwcF9pZF8xLCBmZWU9ZmVlc1swXS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMCA4IC8vIG9uIGVycm9yOiBJbmRleCBhY2Nlc3MgaXMgb3V0IG9mIGJvdW5kcwogICAgYnRvaQogICAgZnJhbWVfZGlnIC0zCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKICAgIGJ5dGVjXzAgLy8gbWV0aG9kICJub19vcCgpdm9pZCIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBpbnRjXzEgLy8gYXBwbAogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaXR4bl9maWVsZCBGZWUKICAgIGl0eG5fc3VibWl0CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTozMAogICAgLy8gYXJjNC5hYmlfY2FsbCgnbm9fb3AnLCBhcHBfaWQ9YXBwX2lkXzEsIGZlZT1mZWVzWzFdLm5hdGl2ZSkKICAgIGl0eG5fYmVnaW4KICAgIGZyYW1lX2RpZyAtMQogICAgZXh0cmFjdCA4IDggLy8gb24gZXJyb3I6IEluZGV4IGFjY2VzcyBpcyBvdXQgb2YgYm91bmRzCiAgICBidG9pCiAgICBmcmFtZV9kaWcgLTMKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25JRAogICAgYnl0ZWNfMCAvLyBtZXRob2QgIm5vX29wKCl2b2lkIgogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGludGNfMSAvLyBhcHBsCiAgICBpdHhuX2ZpZWxkIFR5cGVFbnVtCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgaXR4bl9zdWJtaXQKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjMxLTM1CiAgICAvLyBpdHhuLlBheW1lbnQoCiAgICAvLyAgICAgYW1vdW50PTAsCiAgICAvLyAgICAgcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICBmZWU9ZmVlc1syXS5uYXRpdmUKICAgIC8vICkuc3VibWl0KCkKICAgIGl0eG5fYmVnaW4KICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjM0CiAgICAvLyBmZWU9ZmVlc1syXS5uYXRpdmUKICAgIGZyYW1lX2RpZyAtMQogICAgZXh0cmFjdCAxNiA4IC8vIG9uIGVycm9yOiBJbmRleCBhY2Nlc3MgaXMgb3V0IG9mIGJvdW5kcwogICAgYnRvaQogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MzMKICAgIC8vIHJlY2VpdmVyPUdsb2JhbC5jdXJyZW50X2FwcGxpY2F0aW9uX2FkZHJlc3MsCiAgICBnbG9iYWwgQ3VycmVudEFwcGxpY2F0aW9uQWRkcmVzcwogICAgaXR4bl9maWVsZCBSZWNlaXZlcgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MzIKICAgIC8vIGFtb3VudD0wLAogICAgaW50Y18yIC8vIDAKICAgIGl0eG5fZmllbGQgQW1vdW50CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTozMQogICAgLy8gaXR4bi5QYXltZW50KAogICAgaW50Y18wIC8vIHBheQogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaXR4bl9maWVsZCBGZWUKICAgIC8vIHNtYXJ0X2NvbnRyYWN0cy90ZXN0X2NvbnRyYWN0L2NvbnRyYWN0LnB5OjMxLTM1CiAgICAvLyBpdHhuLlBheW1lbnQoCiAgICAvLyAgICAgYW1vdW50PTAsCiAgICAvLyAgICAgcmVjZWl2ZXI9R2xvYmFsLmN1cnJlbnRfYXBwbGljYXRpb25fYWRkcmVzcywKICAgIC8vICAgICBmZWU9ZmVlc1syXS5uYXRpdmUKICAgIC8vICkuc3VibWl0KCkKICAgIGl0eG5fc3VibWl0CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTozNgogICAgLy8gYXJjNC5hYmlfY2FsbCgnc2VuZF94X2lubmVyc193aXRoX2ZlZXMnLCBhcHBfaWRfMiwgZmVlc1s0XSwgYXBwX2lkPWFwcF9pZF8xLCBmZWU9ZmVlc1szXS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMjQgOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMgogICAgaXRvYgogICAgZnJhbWVfZGlnIC0xCiAgICBwdXNoaW50IDMyIC8vIDMyCiAgICBleHRyYWN0X3VpbnQxNgogICAgZnJhbWVfZGlnIC0xCiAgICBsZW4KICAgIGZyYW1lX2RpZyAtMQogICAgY292ZXIgMgogICAgc3Vic3RyaW5nMwogICAgZnJhbWVfZGlnIC0zCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKICAgIGJ5dGVjXzEgLy8gbWV0aG9kICJzZW5kX3hfaW5uZXJzX3dpdGhfZmVlcyh1aW50NjQsdWludDY0W10pdm9pZCIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBzd2FwCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGludGNfMSAvLyBhcHBsCiAgICBpdHhuX2ZpZWxkIFR5cGVFbnVtCiAgICBpdHhuX2ZpZWxkIEZlZQogICAgaXR4bl9zdWJtaXQKICAgIHJldHN1YgoKCi8vIHNtYXJ0X2NvbnRyYWN0cy50ZXN0X2NvbnRyYWN0LmNvbnRyYWN0LklubmVyRmVlQ29udHJhY3Quc2VuZF9pbm5lcnNfd2l0aF9mZWVzXzIoYXBwX2lkXzE6IHVpbnQ2NCwgYXBwX2lkXzI6IHVpbnQ2NCwgZmVlczogYnl0ZXMpIC0+IHZvaWQ6CnNlbmRfaW5uZXJzX3dpdGhfZmVlc18yOgogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6MzgtMzkKICAgIC8vIEBhcmM0LmFiaW1ldGhvZAogICAgLy8gZGVmIHNlbmRfaW5uZXJzX3dpdGhfZmVlc18yKHNlbGYsIGFwcF9pZF8xOiBVSW50NjQsIGFwcF9pZF8yOiBVSW50NjQsIGZlZXM6IGFyYzQuVHVwbGVbYXJjNC5VSW50NjQsIGFyYzQuVUludDY0LCBhcmM0LkR5bmFtaWNBcnJheVthcmM0LlVJbnQ2NF0sIGFyYzQuVUludDY0LCBhcmM0LlVJbnQ2NCwgYXJjNC5EeW5hbWljQXJyYXlbYXJjNC5VSW50NjRdXSkgLT4gTm9uZToKICAgIHByb3RvIDMgMAogICAgLy8gc21hcnRfY29udHJhY3RzL3Rlc3RfY29udHJhY3QvY29udHJhY3QucHk6NDAKICAgIC8vIGFyYzQuYWJpX2NhbGwoJ25vX29wJywgYXBwX2lkPWFwcF9pZF8xLCBmZWU9ZmVlc1swXS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMCA4IC8vIG9uIGVycm9yOiBJbmRleCBhY2Nlc3MgaXMgb3V0IG9mIGJvdW5kcwogICAgYnRvaQogICAgZnJhbWVfZGlnIC0zCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKICAgIGJ5dGVjXzAgLy8gbWV0aG9kICJub19vcCgpdm9pZCIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBpbnRjXzEgLy8gYXBwbAogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaXR4bl9maWVsZCBGZWUKICAgIGl0eG5fc3VibWl0CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTo0MQogICAgLy8gYXJjNC5hYmlfY2FsbCgnc2VuZF94X2lubmVyc193aXRoX2ZlZXMnLCBhcHBfaWRfMiwgZmVlc1syXSwgYXBwX2lkPWFwcF9pZF8xLCBmZWU9ZmVlc1sxXS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgOCA4IC8vIG9uIGVycm9yOiBJbmRleCBhY2Nlc3MgaXMgb3V0IG9mIGJvdW5kcwogICAgYnRvaQogICAgZnJhbWVfZGlnIC0yCiAgICBpdG9iCiAgICBmcmFtZV9kaWcgLTEKICAgIHB1c2hpbnQgMTYgLy8gMTYKICAgIGV4dHJhY3RfdWludDE2CiAgICBmcmFtZV9kaWcgLTEKICAgIHB1c2hpbnQgMzQgLy8gMzQKICAgIGV4dHJhY3RfdWludDE2CiAgICBmcmFtZV9kaWcgLTEKICAgIHVuY292ZXIgMgogICAgZGlnIDIKICAgIHN1YnN0cmluZzMKICAgIGZyYW1lX2RpZyAtMwogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbklECiAgICBieXRlY18xIC8vIG1ldGhvZCAic2VuZF94X2lubmVyc193aXRoX2ZlZXModWludDY0LHVpbnQ2NFtdKXZvaWQiCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgZGlnIDIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uQXJncwogICAgaW50Y18xIC8vIGFwcGwKICAgIGl0eG5fZmllbGQgVHlwZUVudW0KICAgIHVuY292ZXIgMgogICAgaXR4bl9maWVsZCBGZWUKICAgIGl0eG5fc3VibWl0CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTo0MgogICAgLy8gYXJjNC5hYmlfY2FsbCgnbm9fb3AnLCBhcHBfaWQ9YXBwX2lkXzEsIGZlZT1mZWVzWzNdLm5hdGl2ZSkKICAgIGl0eG5fYmVnaW4KICAgIGZyYW1lX2RpZyAtMQogICAgZXh0cmFjdCAxOCA4IC8vIG9uIGVycm9yOiBJbmRleCBhY2Nlc3MgaXMgb3V0IG9mIGJvdW5kcwogICAgYnRvaQogICAgZnJhbWVfZGlnIC0zCiAgICBpdHhuX2ZpZWxkIEFwcGxpY2F0aW9uSUQKICAgIGJ5dGVjXzAgLy8gbWV0aG9kICJub19vcCgpdm9pZCIKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBpbnRjXzEgLy8gYXBwbAogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaXR4bl9maWVsZCBGZWUKICAgIGl0eG5fc3VibWl0CiAgICAvLyBzbWFydF9jb250cmFjdHMvdGVzdF9jb250cmFjdC9jb250cmFjdC5weTo0MwogICAgLy8gYXJjNC5hYmlfY2FsbCgnc2VuZF94X2lubmVyc193aXRoX2ZlZXMnLCBhcHBfaWRfMiwgZmVlc1s1XSwgYXBwX2lkPWFwcF9pZF8xLCBmZWU9ZmVlc1s0XS5uYXRpdmUpCiAgICBpdHhuX2JlZ2luCiAgICBmcmFtZV9kaWcgLTEKICAgIGV4dHJhY3QgMjYgOCAvLyBvbiBlcnJvcjogSW5kZXggYWNjZXNzIGlzIG91dCBvZiBib3VuZHMKICAgIGJ0b2kKICAgIGZyYW1lX2RpZyAtMQogICAgbGVuCiAgICBmcmFtZV9kaWcgLTEKICAgIHVuY292ZXIgMwogICAgdW5jb3ZlciAyCiAgICBzdWJzdHJpbmczCiAgICBmcmFtZV9kaWcgLTMKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25JRAogICAgYnl0ZWNfMSAvLyBtZXRob2QgInNlbmRfeF9pbm5lcnNfd2l0aF9mZWVzKHVpbnQ2NCx1aW50NjRbXSl2b2lkIgogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIHVuY292ZXIgMgogICAgaXR4bl9maWVsZCBBcHBsaWNhdGlvbkFyZ3MKICAgIGl0eG5fZmllbGQgQXBwbGljYXRpb25BcmdzCiAgICBpbnRjXzEgLy8gYXBwbAogICAgaXR4bl9maWVsZCBUeXBlRW51bQogICAgaXR4bl9maWVsZCBGZWUKICAgIGl0eG5fc3VibWl0CiAgICByZXRzdWIKCgovLyBfcHV5YV9saWIudXRpbC5lbnN1cmVfYnVkZ2V0KHJlcXVpcmVkX2J1ZGdldDogdWludDY0LCBmZWVfc291cmNlOiB1aW50NjQpIC0+IHZvaWQ6CmVuc3VyZV9idWRnZXQ6CiAgICBwcm90byAyIDAKICAgIGZyYW1lX2RpZyAtMgogICAgcHVzaGludCAxMCAvLyAxMAogICAgKwoKZW5zdXJlX2J1ZGdldF93aGlsZV90b3BAMToKICAgIGZyYW1lX2RpZyAwCiAgICBnbG9iYWwgT3Bjb2RlQnVkZ2V0CiAgICA+CiAgICBieiBlbnN1cmVfYnVkZ2V0X2FmdGVyX3doaWxlQDcKICAgIGl0eG5fYmVnaW4KICAgIGludGNfMSAvLyBhcHBsCiAgICBpdHhuX2ZpZWxkIFR5cGVFbnVtCiAgICBwdXNoaW50IDUgLy8gRGVsZXRlQXBwbGljYXRpb24KICAgIGl0eG5fZmllbGQgT25Db21wbGV0aW9uCiAgICBieXRlY18yIC8vIDB4MDY4MTAxCiAgICBpdHhuX2ZpZWxkIEFwcHJvdmFsUHJvZ3JhbQogICAgYnl0ZWNfMiAvLyAweDA2ODEwMQogICAgaXR4bl9maWVsZCBDbGVhclN0YXRlUHJvZ3JhbQogICAgZnJhbWVfZGlnIC0xCiAgICBzd2l0Y2ggZW5zdXJlX2J1ZGdldF9zd2l0Y2hfY2FzZV8wQDMgZW5zdXJlX2J1ZGdldF9zd2l0Y2hfY2FzZV8xQDQKICAgIGIgZW5zdXJlX2J1ZGdldF9zd2l0Y2hfY2FzZV9uZXh0QDYKCmVuc3VyZV9idWRnZXRfc3dpdGNoX2Nhc2VfMEAzOgogICAgaW50Y18yIC8vIDAKICAgIGl0eG5fZmllbGQgRmVlCiAgICBiIGVuc3VyZV9idWRnZXRfc3dpdGNoX2Nhc2VfbmV4dEA2CgplbnN1cmVfYnVkZ2V0X3N3aXRjaF9jYXNlXzFANDoKICAgIGdsb2JhbCBNaW5UeG5GZWUKICAgIGl0eG5fZmllbGQgRmVlCgplbnN1cmVfYnVkZ2V0X3N3aXRjaF9jYXNlX25leHRANjoKICAgIGl0eG5fc3VibWl0CiAgICBiIGVuc3VyZV9idWRnZXRfd2hpbGVfdG9wQDEKCmVuc3VyZV9idWRnZXRfYWZ0ZXJfd2hpbGVANzoKICAgIHJldHN1Ygo=", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDEwCgpzbWFydF9jb250cmFjdHMudGVzdF9jb250cmFjdC5jb250cmFjdC5Jbm5lckZlZUNvbnRyYWN0LmNsZWFyX3N0YXRlX3Byb2dyYW06CiAgICBwdXNoaW50IDEgLy8gMQogICAgcmV0dXJuCg==" }, "events": [], "templateVariables": {} -} +} \ No newline at end of file diff --git a/tests/artifacts/inner-fee/contract.py b/tests/artifacts/inner-fee/contract.py index bd991895..e33d75b3 100644 --- a/tests/artifacts/inner-fee/contract.py +++ b/tests/artifacts/inner-fee/contract.py @@ -12,23 +12,18 @@ class InnerFeeContract(ARC4Contract): - @arc4.abimethod(readonly=True) - def burn_ops_readonly(self, op_budget: UInt64) -> None: - # Uses approx 60 op budget per iteration - count = op_budget // 60 - ensure_budget(op_budget) - for i in urange(count): - sqrt = op.bsqrt(BigUInt(i)) - assert sqrt >= 0 # Prevent optimiser removing the sqrt - - @arc4.abimethod() + @arc4.abimethod def burn_ops(self, op_budget: UInt64) -> None: # Uses approx 60 op budget per iteration count = op_budget // 60 ensure_budget(op_budget) for i in urange(count): sqrt = op.bsqrt(BigUInt(i)) - assert sqrt >= 0 # Prevent optimiser removing the sqrt + assert(sqrt >= 0) # Prevent optimiser removing the sqrt + + @arc4.abimethod(readonly=True) + def burn_ops_readonly(self, op_budget: UInt64) -> None: + self.burn_ops(op_budget) @arc4.abimethod def no_op(self) -> None: @@ -37,35 +32,22 @@ def no_op(self) -> None: @arc4.abimethod def send_x_inners_with_fees(self, app_id: UInt64, fees: arc4.DynamicArray[arc4.UInt64]) -> None: for fee in fees: - arc4.abi_call("no_op", app_id=app_id, fee=fee.native) + arc4.abi_call('no_op', app_id=app_id, fee=fee.native) @arc4.abimethod - def send_inners_with_fees( - self, - app_id_1: UInt64, - app_id_2: UInt64, - fees: arc4.Tuple[arc4.UInt64, arc4.UInt64, arc4.UInt64, arc4.UInt64, arc4.DynamicArray[arc4.UInt64]], - ) -> None: - arc4.abi_call("no_op", app_id=app_id_1, fee=fees[0].native) - arc4.abi_call("no_op", app_id=app_id_1, fee=fees[1].native) - itxn.Payment(amount=0, receiver=Global.current_application_address, fee=fees[2].native).submit() - arc4.abi_call("send_x_inners_with_fees", app_id_2, fees[4], app_id=app_id_1, fee=fees[3].native) + def send_inners_with_fees(self, app_id_1: UInt64, app_id_2: UInt64, fees: arc4.Tuple[arc4.UInt64, arc4.UInt64, arc4.UInt64, arc4.UInt64, arc4.DynamicArray[arc4.UInt64]]) -> None: + arc4.abi_call('no_op', app_id=app_id_1, fee=fees[0].native) + arc4.abi_call('no_op', app_id=app_id_1, fee=fees[1].native) + itxn.Payment( + amount=0, + receiver=Global.current_application_address, + fee=fees[2].native + ).submit() + arc4.abi_call('send_x_inners_with_fees', app_id_2, fees[4], app_id=app_id_1, fee=fees[3].native) @arc4.abimethod - def send_inners_with_fees_2( - self, - app_id_1: UInt64, - app_id_2: UInt64, - fees: arc4.Tuple[ - arc4.UInt64, - arc4.UInt64, - arc4.DynamicArray[arc4.UInt64], - arc4.UInt64, - arc4.UInt64, - arc4.DynamicArray[arc4.UInt64], - ], - ) -> None: - arc4.abi_call("no_op", app_id=app_id_1, fee=fees[0].native) - arc4.abi_call("send_x_inners_with_fees", app_id_2, fees[2], app_id=app_id_1, fee=fees[1].native) - arc4.abi_call("no_op", app_id=app_id_1, fee=fees[3].native) - arc4.abi_call("send_x_inners_with_fees", app_id_2, fees[5], app_id=app_id_1, fee=fees[4].native) + def send_inners_with_fees_2(self, app_id_1: UInt64, app_id_2: UInt64, fees: arc4.Tuple[arc4.UInt64, arc4.UInt64, arc4.DynamicArray[arc4.UInt64], arc4.UInt64, arc4.UInt64, arc4.DynamicArray[arc4.UInt64]]) -> None: + arc4.abi_call('no_op', app_id=app_id_1, fee=fees[0].native) + arc4.abi_call('send_x_inners_with_fees', app_id_2, fees[2], app_id=app_id_1, fee=fees[1].native) + arc4.abi_call('no_op', app_id=app_id_1, fee=fees[3].native) + arc4.abi_call('send_x_inners_with_fees', app_id_2, fees[5], app_id=app_id_1, fee=fees[4].native) diff --git a/tests/transactions/test_fee_coverage.py b/tests/transactions/test_fee_coverage.py new file mode 100644 index 00000000..517bfa6a --- /dev/null +++ b/tests/transactions/test_fee_coverage.py @@ -0,0 +1,697 @@ +import dataclasses +import json +from collections.abc import Generator +from pathlib import Path + +import pytest + +from algokit_utils import SigningAccount +from algokit_utils.algorand import AlgorandClient +from algokit_utils.applications.app_client import AppClient, AppClientMethodCallParams, FundAppAccountParams +from algokit_utils.applications.app_factory import AppFactoryCreateMethodCallParams, AppFactoryCreateParams +from algokit_utils.config import config +from algokit_utils.models.amount import AlgoAmount +from algokit_utils.transactions.transaction_composer import PaymentParams + + +@pytest.fixture +def algorand() -> AlgorandClient: + return AlgorandClient.default_localnet() + + +@pytest.fixture +def funded_account(algorand: AlgorandClient) -> SigningAccount: + new_account = algorand.account.random() + dispenser = algorand.account.localnet_dispenser() + algorand.account.ensure_funded(new_account, dispenser, AlgoAmount.from_algo(100)) + return new_account + + +class TestCoverAppCallInnerFees: + """Test covering app call inner transaction fees""" + + @pytest.fixture(autouse=True) + def setup(self, algorand: AlgorandClient, funded_account: SigningAccount) -> Generator[None, None, None]: + config.configure(populate_app_call_resources=True) + + # Load inner fee contract spec + spec_path = Path(__file__).parent.parent / "artifacts" / "inner-fee" / "application.json" + inner_fee_spec = json.loads(spec_path.read_text()) + + # Create app factory + factory = algorand.client.get_app_factory(app_spec=inner_fee_spec, default_sender=funded_account.address) + + # Create 3 app instances + self.app_client1, _ = factory.send.bare.create(params=AppFactoryCreateParams(note=b"app1")) + self.app_client2, _ = factory.send.bare.create(params=AppFactoryCreateParams(note=b"app2")) + self.app_client3, _ = factory.send.bare.create(params=AppFactoryCreateParams(note=b"app3")) + + # Fund app accounts + for client in [self.app_client1, self.app_client2, self.app_client3]: + client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_algo(2))) + + yield + + config.configure(populate_app_call_resources=False) + + def test_throws_when_no_max_fee(self) -> None: + """Test that error is thrown when no max fee is supplied""" + with pytest.raises(ValueError, match="Please provide a `max_fee` for each app call transaction"): + self.app_client1.send.call( + AppClientMethodCallParams( + method="no_op", + ), + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + def test_throws_when_inner_fees_not_covered(self) -> None: + """Test that error is thrown when inner transaction fees are not covered""" + + expected_fee = 7000 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algo(expected_fee), + ) + + with pytest.raises(Exception, match="fee too small"): + self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": False, + }, + ) + + def test_does_not_alter_fee_without_inners(self) -> None: + """Test that fee is not altered when app call has no inner transactions""" + + expected_fee = 1000 + params = AppClientMethodCallParams( + method="no_op", + max_fee=AlgoAmount.from_micro_algo(2000), + ) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_throws_when_max_fee_too_small(self) -> None: + """Test that error is thrown when max fee is too small to cover inner fees""" + + expected_fee = 7000 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algo(expected_fee - 1), + ) + + with pytest.raises(ValueError, match="Fees were too small to resolve execution info"): + self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + def test_throws_when_static_fee_too_small_for_inner_fees(self) -> None: + """Test that error is thrown when static fee is too small for inner transaction fees""" + + expected_fee = 7000 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + static_fee=AlgoAmount.from_micro_algo(expected_fee - 1), + ) + + with pytest.raises(ValueError, match="Fees were too small to resolve execution info"): + self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + def test_alters_fee_handling_when_no_itxns_covered(self) -> None: + """Test that fee handling is altered when no inner transaction fees are covered""" + + expected_fee = 7000 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algo(expected_fee), + ) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_fee_handling_when_all_inners_covered(self) -> None: + """Test that fee handling is altered when all inner transaction fees are covered""" + + expected_fee = 1000 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 1000, 1000, 1000, [1000, 1000]]], + max_fee=AlgoAmount.from_micro_algo(expected_fee), + ) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_fee_handling_when_some_inners_covered(self) -> None: + """Test that fee handling is altered when some inner transaction fees are covered""" + + expected_fee = 5300 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 0, 200, 0, [500, 0]]], + max_fee=AlgoAmount.from_micro_algo(expected_fee), + ) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_fee_when_some_inners_have_surplus(self) -> None: + """Test that fee handling is altered when some inner transaction fees are covered""" + + expected_fee = 2000 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 1000, 5000, 0, [0, 50]]], + max_fee=AlgoAmount.from_micro_algo(expected_fee), + ) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_handling_multiple_app_calls_in_group_with_inners_with_varying_fees(self) -> None: + """Test that fee handling is altered when multiple app calls are in a group with inners with varying fees""" + txn_1_expected_fee = 5800 + txn_2_expected_fee = 6000 + + txn_1_params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 1000, 0, 0, [200, 0]]], + static_fee=AlgoAmount.from_micro_algo(txn_1_expected_fee), + note=b"txn_1", + ) + + txn_2_params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algo(txn_2_expected_fee), + note=b"txn_2", + ) + + result = ( + self.app_client1.algorand.new_group() + .add_app_call_method_call(self.app_client1.params.call(txn_1_params)) + .add_app_call_method_call(self.app_client1.params.call(txn_2_params)) + .send({"cover_app_call_inner_transaction_fees": True}) + ) + + assert result.transactions[0].raw.fee == txn_1_expected_fee + self._assert_min_fee(self.app_client1, txn_1_params, txn_1_expected_fee) + assert result.transactions[1].raw.fee == txn_2_expected_fee + self._assert_min_fee(self.app_client1, txn_2_params, txn_2_expected_fee) + + def test_does_not_alter_static_fee_with_surplus(self) -> None: + """Test that a static fee with surplus is not altered""" + + expected_fee = 6000 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 0, 200, 0, [500, 0]]], + static_fee=AlgoAmount.from_micro_algo(expected_fee), + ) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + assert result.transaction.raw.fee == expected_fee + + def test_alters_fee_with_large_inner_surplus_pooling(self) -> None: + """Test fee handling with large inner fee surplus pooling to lower siblings""" + + expected_fee = 7000 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0, 20_000, 0, 0, 0]]], + max_fee=AlgoAmount.from_micro_algo(expected_fee), + ) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_fee_with_partial_inner_surplus_pooling(self) -> None: + """Test fee handling with inner fee surplus pooling to some lower siblings""" + + expected_fee = 6300 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 2200, 0, [0, 0, 2500, 0, 0, 0]]], + max_fee=AlgoAmount.from_micro_algo(expected_fee), + ) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_fee_with_large_inner_surplus_no_pooling(self) -> None: + """Test fee handling with large inner fee surplus but no pooling""" + + expected_fee = 10_000 + params = AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0, 0, 0, 0, 20_000]]], + max_fee=AlgoAmount.from_micro_algo(expected_fee), + ) + result = self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_alters_fee_with_multiple_inner_surplus_poolings_to_lower_siblings(self) -> None: + """Test fee handling with multiple inner fee surplus poolings to lower siblings""" + + expected_fee = 7100 + params = AppClientMethodCallParams( + method="send_inners_with_fees_2", + args=[ + self.app_client2.app_id, + self.app_client3.app_id, + [0, 1200, [0, 0, 4900, 0, 0, 0], 200, 1100, [0, 0, 2500, 0, 0, 0]], + ], + max_fee=AlgoAmount.from_micro_algo(expected_fee), + ) + result = self.app_client1.send.call(params, send_params={"cover_app_call_inner_transaction_fees": True}) + + assert result.transaction.raw.fee == expected_fee + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_does_not_alter_fee_when_group_covers_inner_fees(self, funded_account: SigningAccount) -> None: + """Test that fee is not altered when another transaction in group covers inner fees""" + + expected_fee = 8000 + + result = ( + self.app_client1.algorand.new_group() + .add_payment( + params=PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algo(0), + static_fee=AlgoAmount.from_micro_algo(expected_fee), + ) + ) + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algo(expected_fee), + ) + ) + ) + .send({"cover_app_call_inner_transaction_fees": True}) + ) + + assert result.transactions[0].raw.fee == expected_fee + # We could technically reduce the below to 0, however it adds more complexity + # and is probably unlikely to be a common use case + assert result.transactions[1].raw.fee == 1000 + + def test_allocates_surplus_fees_to_most_constrained_first(self, funded_account: SigningAccount) -> None: + """Test that surplus fees are allocated to the most fee constrained transaction first""" + + result = ( + self.app_client1.algorand.new_group() + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algo(2000), + ) + ) + ) + .add_payment( + params=PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algo(0), + static_fee=AlgoAmount.from_micro_algo(7500), + ) + ) + .add_payment( + params=PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algo(0), + static_fee=AlgoAmount.from_micro_algo(0), + ) + ) + .send({"cover_app_call_inner_transaction_fees": True}) + ) + + assert result.transactions[0].raw.fee == 1500 + assert result.transactions[1].raw.fee == 7500 + assert result.transactions[2].raw.fee == 0 + + def test_handles_nested_abi_method_calls(self, funded_account: SigningAccount) -> None: + """Test fee handling with nested ABI method calls""" + + # Create nested contract app + app_spec = (Path(__file__).parent.parent / "artifacts" / "nested_contract" / "application.json").read_text() + nested_factory = self.app_client1.algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=funded_account.address, + ) + nested_client, _ = nested_factory.send.create( + params=AppFactoryCreateMethodCallParams(method="createApplication") + ) + + # Setup transaction parameters + txn_arg_call = self.app_client1.params.call( + AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 2000, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algo(4000), + ) + ) + + payment_params = PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algo(0), + static_fee=AlgoAmount.from_micro_algo(1500), + ) + + expected_fee = 2000 + params = AppClientMethodCallParams( + method="nestedTxnArg", + args=[ + self.app_client1.algorand.create_transaction.payment(payment_params), + txn_arg_call, + ], + static_fee=AlgoAmount.from_micro_algo(expected_fee), + ) + result = nested_client.send.call(params, send_params={"cover_app_call_inner_transaction_fees": True}) + + assert len(result.transactions) == 3 + assert result.transactions[0].raw.fee == 1500 + assert result.transactions[1].raw.fee == 3500 + assert result.transactions[2].raw.fee == expected_fee + + self._assert_min_fee( + nested_client, + dataclasses.replace( + params, + args=[self.app_client1.algorand.create_transaction.payment(payment_params), txn_arg_call], + ), + expected_fee, + ) + + def test_throws_when_max_fee_below_calculated(self) -> None: + """Test that error is thrown when max fee is below calculated fee""" + + with pytest.raises( + ValueError, match="Calculated transaction fee 7000 µALGO is greater than max of 1200 for transaction 0" + ): + ( + self.app_client1.algorand.new_group() + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algo(1200), + ) + ) + ) + # This transaction allows this state to be possible, without it the simulate call + # to get the execution info would fail + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallParams( + method="no_op", + max_fee=AlgoAmount.from_micro_algo(10_000), + ) + ) + ) + .send({"cover_app_call_inner_transaction_fees": True}) + ) + + def test_throws_when_nested_max_fee_below_calculated(self, funded_account: SigningAccount) -> None: + """Test that error is thrown when nested max fee is below calculated fee""" + + # Create nested contract app + app_spec = (Path(__file__).parent.parent / "artifacts" / "nested_contract" / "application.json").read_text() + nested_factory = self.app_client1.algorand.client.get_app_factory( + app_spec=app_spec, + default_sender=funded_account.address, + ) + nested_client, _ = nested_factory.send.create( + params=AppFactoryCreateMethodCallParams(method="createApplication") + ) + + txn_arg_call = self.app_client1.params.call( + AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 2000, 0, [0, 0]]], + max_fee=AlgoAmount.from_micro_algo(2000), + ) + ) + + with pytest.raises( + ValueError, match="Calculated transaction fee 5000 µALGO is greater than max of 2000 for transaction 1" + ): + nested_client.send.call( + AppClientMethodCallParams( + method="nestedTxnArg", + args=[ + self.app_client1.algorand.create_transaction.payment( + PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algo(0), + ) + ), + txn_arg_call, + ], + max_fee=AlgoAmount.from_micro_algo(10_000), + ), + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + def test_throws_when_static_fee_below_calculated(self) -> None: + """Test that error is thrown when static fee is below calculated fee""" + + with pytest.raises( + ValueError, match="Calculated transaction fee 7000 µALGO is greater than max of 5000 for transaction 0" + ): + ( + self.app_client1.algorand.new_group() + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + static_fee=AlgoAmount.from_micro_algo(5000), + ) + ) + ) + # This transaction allows this state to be possible, without it the simulate call + # to get the execution info would fail + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallParams( + method="no_op", + max_fee=AlgoAmount.from_micro_algo(10_000), + ) + ) + ) + .send({"cover_app_call_inner_transaction_fees": True}) + ) + + def test_throws_when_non_app_call_static_fee_too_low(self, funded_account: SigningAccount) -> None: + """Test that error is thrown when static fee for non-app-call transaction is too low""" + + with pytest.raises( + ValueError, match="An additional fee of 500 µALGO is required for non app call transaction 2" + ): + ( + self.app_client1.algorand.new_group() + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + static_fee=AlgoAmount.from_micro_algo(13_000), + max_fee=AlgoAmount.from_micro_algo(14_000), + ) + ) + ) + .add_app_call_method_call( + self.app_client1.params.call( + AppClientMethodCallParams( + method="send_inners_with_fees", + args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], + static_fee=AlgoAmount.from_micro_algo(1000), + ) + ) + ) + .add_payment( + params=PaymentParams( + sender=funded_account.address, + receiver=funded_account.address, + amount=AlgoAmount.from_micro_algo(0), + static_fee=AlgoAmount.from_micro_algo(500), + ) + ) + .send({"cover_app_call_inner_transaction_fees": True}) + ) + + def test_handles_expensive_abi_calls_with_ensure_budget(self) -> None: + """Test fee handling with expensive ABI method calls that use ensure_budget to op-up""" + + expected_fee = 10_000 + params = AppClientMethodCallParams( + method="burn_ops", + args=[6200], + max_fee=AlgoAmount.from_micro_algo(12_000), + ) + result = self.app_client1.send.call(params, send_params={"cover_app_call_inner_transaction_fees": True}) + + assert result.transaction.raw.fee == expected_fee + assert len(result.confirmation.get("inner-txns", [])) == 9 # type: ignore[union-attr] + self._assert_min_fee(self.app_client1, params, expected_fee) + + def test_readonly_handles_expensive_abi_calls_with_ensure_budget(self) -> None: + """Test fee handling with expensive readonly ABI method calls that use ensure_budget to op-up""" + + expected_fee = 12_000 + params = AppClientMethodCallParams( + method="burn_ops_readonly", + args=[6200], + max_fee=AlgoAmount.from_micro_algo(expected_fee), + ) + result = self.app_client1.send.call(params, send_params={"cover_app_call_inner_transaction_fees": True}) + + assert result.transaction.raw.fee == expected_fee + assert len(result.confirmation.get("inner-txns", [])) == 9 # type: ignore[union-attr] + + def test_readonly_throws_when_no_max_fee(self) -> None: + """Test that error is thrown when no max fee is supplied for a readonly method call""" + with pytest.raises( + ValueError, + match="Please provide a `max_fee` for the transaction when `cover_app_call_inner_transaction_fees` is enabled", # noqa: E501 + ): + self.app_client1.send.call( + AppClientMethodCallParams( + method="burn_ops_readonly", + args=[6200], + ), + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + def test_readonly_throws_when_inner_fees_not_covered(self) -> None: + """Test that error is thrown when a readonly method call inner transaction fees are not covered""" + + expected_fee = 7000 + params = AppClientMethodCallParams( + method="burn_ops_readonly", + args=[6200], + max_fee=AlgoAmount.from_micro_algo(expected_fee), + ) + + with pytest.raises(Exception, match="fee too small"): + self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": False, + }, + ) + + def test_readonly_throws_when_max_fee_too_small(self) -> None: + """Test that error is thrown when readonly method call max fee is too small to cover inner transaction fees""" + + expected_fee = 7000 + params = AppClientMethodCallParams( + method="burn_ops_readonly", + args=[6200], + max_fee=AlgoAmount.from_micro_algo(expected_fee), + ) + + with pytest.raises(ValueError, match="Fees were too small. You may need to increase the transaction `maxFee`."): + self.app_client1.send.call( + params, + send_params={ + "cover_app_call_inner_transaction_fees": True, + }, + ) + + def _assert_min_fee(self, app_client: AppClient, params: AppClientMethodCallParams, fee: int) -> None: + """Helper to assert minimum required fee""" + if fee == 1000: + return + params_copy = dataclasses.replace( + params, + static_fee=AlgoAmount.from_micro_algo(fee - 1), + extra_fee=None, + ) + + with pytest.raises(Exception, match="fee too small"): + app_client.send.call(params_copy) diff --git a/tests/transactions/test_resource_packing.py b/tests/transactions/test_resource_packing.py index f768058c..98fc4e6c 100644 --- a/tests/transactions/test_resource_packing.py +++ b/tests/transactions/test_resource_packing.py @@ -1,5 +1,3 @@ -import dataclasses -import json from collections.abc import Generator from pathlib import Path @@ -11,11 +9,10 @@ from algokit_utils import SigningAccount from algokit_utils.algorand import AlgorandClient from algokit_utils.applications.app_client import AppClient, AppClientMethodCallParams, FundAppAccountParams -from algokit_utils.applications.app_factory import AppFactoryCreateMethodCallParams, AppFactoryCreateParams +from algokit_utils.applications.app_factory import AppFactoryCreateMethodCallParams from algokit_utils.config import config from algokit_utils.errors.logic_error import LogicError from algokit_utils.models.amount import AlgoAmount -from algokit_utils.transactions.transaction_composer import PaymentParams @pytest.fixture @@ -477,643 +474,3 @@ def test_rekeyed_account(self, algorand: AlgorandClient, funded_account: Signing result = self.external_client.send.call(AppClientMethodCallParams(method="senderAssetBalance")) assert len(getattr(result.transaction.application_call, "accounts", None) or []) == 0 - - -class TestCoverAppCallInnerFees: - """Test covering app call inner transaction fees""" - - @pytest.fixture(autouse=True) - def setup(self, algorand: AlgorandClient, funded_account: SigningAccount) -> Generator[None, None, None]: - config.configure(populate_app_call_resources=True) - - # Load inner fee contract spec - spec_path = Path(__file__).parent.parent / "artifacts" / "inner-fee" / "application.json" - inner_fee_spec = json.loads(spec_path.read_text()) - - # Create app factory - factory = algorand.client.get_app_factory(app_spec=inner_fee_spec, default_sender=funded_account.address) - - # Create 3 app instances - self.app_client1, _ = factory.send.bare.create(params=AppFactoryCreateParams(note=b"app1")) - self.app_client2, _ = factory.send.bare.create(params=AppFactoryCreateParams(note=b"app2")) - self.app_client3, _ = factory.send.bare.create(params=AppFactoryCreateParams(note=b"app3")) - - # Fund app accounts - for client in [self.app_client1, self.app_client2, self.app_client3]: - client.fund_app_account(FundAppAccountParams(amount=AlgoAmount.from_algo(2))) - - yield - - config.configure(populate_app_call_resources=False) - - def test_runs_auto_fee_coverage_implicitly_on_readonly_calls(self) -> None: - """Test that auto top-up is run implicitly on readonly calls""" - - # Below must pass without explicit `populate_app_call_resources` flag - # Passing 'cover_app_call_inner_transaction_fees' is not required for readonly calls - self.app_client1.send.call( - AppClientMethodCallParams( - args=[6200], - method="burn_ops_readonly", - ), - ) - - # For fresh accounts with balance lower than max dummy assumed max_fee pre filled when no - # max_fee provided explicitly, it will fail - new_account_with_less_than_10_algo = self.app_client1.algorand.account.random() - self.app_client1.algorand.account.ensure_funded_from_environment( - account_to_fund=new_account_with_less_than_10_algo, min_spending_balance=AlgoAmount.from_algo(5) - ) - with pytest.raises(ValueError, match=r"tried to spend \{10000000\}"): - self.app_client1.send.call( - AppClientMethodCallParams( - args=[6200], method="burn_ops_readonly", sender=new_account_with_less_than_10_algo.address - ), - ) - - # But user can explicitly set a max_fee value in such cases - # while not having to set cover_app_call_inner_transaction_fees - self.app_client1.send.call( - AppClientMethodCallParams( - args=[6200], - method="burn_ops_readonly", - max_fee=AlgoAmount.from_micro_algo(10000), - signer=new_account_with_less_than_10_algo.signer, - ), - ) - - def test_throws_when_no_max_fee(self) -> None: - """Test that error is thrown when no max fee is supplied""" - with pytest.raises(ValueError, match="Please provide a `max_fee` for each app call transaction"): - self.app_client1.send.call( - AppClientMethodCallParams( - method="no_op", - ), - send_params={ - "cover_app_call_inner_transaction_fees": True, - }, - ) - - def test_throws_when_inner_fees_not_covered(self) -> None: - """Test that error is thrown when inner transaction fees are not covered""" - - expected_fee = 7000 - params = AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - max_fee=AlgoAmount.from_micro_algo(expected_fee), - ) - - with pytest.raises(Exception, match="fee too small"): - self.app_client1.send.call( - params, - send_params={ - "cover_app_call_inner_transaction_fees": False, - }, - ) - - def test_does_not_alter_fee_without_inners(self) -> None: - """Test that fee is not altered when app call has no inner transactions""" - - expected_fee = 1000 - params = AppClientMethodCallParams( - method="no_op", - max_fee=AlgoAmount.from_micro_algo(2000), - ) - result = self.app_client1.send.call( - params, - send_params={ - "cover_app_call_inner_transaction_fees": True, - }, - ) - - assert result.transaction.raw.fee == expected_fee - self._assert_min_fee(self.app_client1, params, expected_fee) - - def test_throws_when_max_fee_too_small(self) -> None: - """Test that error is thrown when max fee is too small to cover inner fees""" - - expected_fee = 7000 - params = AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - max_fee=AlgoAmount.from_micro_algo(expected_fee - 1), - ) - - with pytest.raises(ValueError, match="Fees were too small to resolve execution info"): - self.app_client1.send.call( - params, - send_params={ - "cover_app_call_inner_transaction_fees": True, - }, - ) - - def test_throws_when_static_fee_too_small_for_inner_fees(self) -> None: - """Test that error is thrown when static fee is too small for inner transaction fees""" - - expected_fee = 7000 - params = AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - static_fee=AlgoAmount.from_micro_algo(expected_fee - 1), - ) - - with pytest.raises(ValueError, match="Fees were too small to resolve execution info"): - self.app_client1.send.call( - params, - send_params={ - "cover_app_call_inner_transaction_fees": True, - }, - ) - - def test_alters_fee_handling_when_no_itxns_covered(self) -> None: - """Test that fee handling is altered when no inner transaction fees are covered""" - - expected_fee = 7000 - params = AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - max_fee=AlgoAmount.from_micro_algo(expected_fee), - ) - result = self.app_client1.send.call( - params, - send_params={ - "cover_app_call_inner_transaction_fees": True, - }, - ) - - assert result.transaction.raw.fee == expected_fee - self._assert_min_fee(self.app_client1, params, expected_fee) - - def test_alters_fee_handling_when_all_inners_covered(self) -> None: - """Test that fee handling is altered when all inner transaction fees are covered""" - - expected_fee = 1000 - params = AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 1000, 1000, 1000, [1000, 1000]]], - max_fee=AlgoAmount.from_micro_algo(expected_fee), - ) - result = self.app_client1.send.call( - params, - send_params={ - "cover_app_call_inner_transaction_fees": True, - }, - ) - - assert result.transaction.raw.fee == expected_fee - self._assert_min_fee(self.app_client1, params, expected_fee) - - def test_alters_fee_handling_when_some_inners_covered(self) -> None: - """Test that fee handling is altered when some inner transaction fees are covered""" - - expected_fee = 5300 - params = AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 0, 200, 0, [500, 0]]], - max_fee=AlgoAmount.from_micro_algo(expected_fee), - ) - result = self.app_client1.send.call( - params, - send_params={ - "cover_app_call_inner_transaction_fees": True, - }, - ) - - assert result.transaction.raw.fee == expected_fee - self._assert_min_fee(self.app_client1, params, expected_fee) - - def test_alters_fee_when_some_inners_have_surplus(self) -> None: - """Test that fee handling is altered when some inner transaction fees are covered""" - - expected_fee = 2000 - params = AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [0, 1000, 5000, 0, [0, 50]]], - max_fee=AlgoAmount.from_micro_algo(expected_fee), - ) - result = self.app_client1.send.call( - params, - send_params={ - "cover_app_call_inner_transaction_fees": True, - }, - ) - assert result.transaction.raw.fee == expected_fee - self._assert_min_fee(self.app_client1, params, expected_fee) - - def test_alters_handling_multiple_app_calls_in_group_with_inners_with_varying_fees(self) -> None: - """Test that fee handling is altered when multiple app calls are in a group with inners with varying fees""" - txn_1_expected_fee = 5800 - txn_2_expected_fee = 6000 - - txn_1_params = AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [0, 1000, 0, 0, [200, 0]]], - static_fee=AlgoAmount.from_micro_algo(txn_1_expected_fee), - note=b"txn_1", - ) - - txn_2_params = AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 0, 0, 0, [0, 0]]], - max_fee=AlgoAmount.from_micro_algo(txn_2_expected_fee), - note=b"txn_2", - ) - - result = ( - self.app_client1.algorand.new_group() - .add_app_call_method_call(self.app_client1.params.call(txn_1_params)) - .add_app_call_method_call(self.app_client1.params.call(txn_2_params)) - .send({"cover_app_call_inner_transaction_fees": True}) - ) - - assert result.transactions[0].raw.fee == txn_1_expected_fee - self._assert_min_fee(self.app_client1, txn_1_params, txn_1_expected_fee) - assert result.transactions[1].raw.fee == txn_2_expected_fee - self._assert_min_fee(self.app_client1, txn_2_params, txn_2_expected_fee) - - def test_does_not_alter_static_fee_with_surplus(self) -> None: - """Test that a static fee with surplus is not altered""" - - expected_fee = 6000 - params = AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [1000, 0, 200, 0, [500, 0]]], - static_fee=AlgoAmount.from_micro_algo(expected_fee), - ) - result = self.app_client1.send.call( - params, - send_params={ - "cover_app_call_inner_transaction_fees": True, - }, - ) - - assert result.transaction.raw.fee == expected_fee - - def test_alters_fee_with_large_inner_surplus_pooling(self) -> None: - """Test fee handling with large inner fee surplus pooling to lower siblings""" - - expected_fee = 7000 - params = AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0, 20_000, 0, 0, 0]]], - max_fee=AlgoAmount.from_micro_algo(expected_fee), - ) - result = self.app_client1.send.call( - params, - send_params={ - "cover_app_call_inner_transaction_fees": True, - }, - ) - - assert result.transaction.raw.fee == expected_fee - self._assert_min_fee(self.app_client1, params, expected_fee) - - def test_alters_fee_with_partial_inner_surplus_pooling(self) -> None: - """Test fee handling with inner fee surplus pooling to some lower siblings""" - - expected_fee = 6300 - params = AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 2200, 0, [0, 0, 2500, 0, 0, 0]]], - max_fee=AlgoAmount.from_micro_algo(expected_fee), - ) - result = self.app_client1.send.call( - params, - send_params={ - "cover_app_call_inner_transaction_fees": True, - }, - ) - - assert result.transaction.raw.fee == expected_fee - self._assert_min_fee(self.app_client1, params, expected_fee) - - def test_alters_fee_with_large_inner_surplus_no_pooling(self) -> None: - """Test fee handling with large inner fee surplus but no pooling""" - - expected_fee = 10_000 - params = AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0, 0, 0, 0, 20_000]]], - max_fee=AlgoAmount.from_micro_algo(expected_fee), - ) - result = self.app_client1.send.call( - params, - send_params={ - "cover_app_call_inner_transaction_fees": True, - }, - ) - - assert result.transaction.raw.fee == expected_fee - self._assert_min_fee(self.app_client1, params, expected_fee) - - def test_alters_fee_with_multiple_inner_surplus_poolings_to_lower_siblings(self) -> None: - """Test fee handling with multiple inner fee surplus poolings to lower siblings""" - - expected_fee = 7100 - params = AppClientMethodCallParams( - method="send_inners_with_fees_2", - args=[ - self.app_client2.app_id, - self.app_client3.app_id, - [0, 1200, [0, 0, 4900, 0, 0, 0], 200, 1100, [0, 0, 2500, 0, 0, 0]], - ], - max_fee=AlgoAmount.from_micro_algo(expected_fee), - ) - result = self.app_client1.send.call(params, send_params={"cover_app_call_inner_transaction_fees": True}) - - assert result.transaction.raw.fee == expected_fee - self._assert_min_fee(self.app_client1, params, expected_fee) - - def test_does_not_alter_fee_when_group_covers_inner_fees(self, funded_account: SigningAccount) -> None: - """Test that fee is not altered when another transaction in group covers inner fees""" - - expected_fee = 8000 - - result = ( - self.app_client1.algorand.new_group() - .add_payment( - params=PaymentParams( - sender=funded_account.address, - receiver=funded_account.address, - amount=AlgoAmount.from_micro_algo(0), - static_fee=AlgoAmount.from_micro_algo(expected_fee), - ) - ) - .add_app_call_method_call( - self.app_client1.params.call( - AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - max_fee=AlgoAmount.from_micro_algo(expected_fee), - ) - ) - ) - .send({"cover_app_call_inner_transaction_fees": True}) - ) - - assert result.transactions[0].raw.fee == expected_fee - # We could technically reduce the below to 0, however it adds more complexity - # and is probably unlikely to be a common use case - assert result.transactions[1].raw.fee == 1000 - - def test_allocates_surplus_fees_to_most_constrained_first(self, funded_account: SigningAccount) -> None: - """Test that surplus fees are allocated to the most fee constrained transaction first""" - - result = ( - self.app_client1.algorand.new_group() - .add_app_call_method_call( - self.app_client1.params.call( - AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - max_fee=AlgoAmount.from_micro_algo(2000), - ) - ) - ) - .add_payment( - params=PaymentParams( - sender=funded_account.address, - receiver=funded_account.address, - amount=AlgoAmount.from_micro_algo(0), - static_fee=AlgoAmount.from_micro_algo(7500), - ) - ) - .add_payment( - params=PaymentParams( - sender=funded_account.address, - receiver=funded_account.address, - amount=AlgoAmount.from_micro_algo(0), - static_fee=AlgoAmount.from_micro_algo(0), - ) - ) - .send({"cover_app_call_inner_transaction_fees": True}) - ) - - assert result.transactions[0].raw.fee == 1500 - assert result.transactions[1].raw.fee == 7500 - assert result.transactions[2].raw.fee == 0 - - def test_handles_nested_abi_method_calls(self, funded_account: SigningAccount) -> None: - """Test fee handling with nested ABI method calls""" - - # Create nested contract app - app_spec = (Path(__file__).parent.parent / "artifacts" / "nested_contract" / "application.json").read_text() - nested_factory = self.app_client1.algorand.client.get_app_factory( - app_spec=app_spec, - default_sender=funded_account.address, - ) - nested_client, _ = nested_factory.send.create( - params=AppFactoryCreateMethodCallParams(method="createApplication") - ) - - # Setup transaction parameters - txn_arg_call = self.app_client1.params.call( - AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 2000, 0, [0, 0]]], - max_fee=AlgoAmount.from_micro_algo(4000), - ) - ) - - payment_params = PaymentParams( - sender=funded_account.address, - receiver=funded_account.address, - amount=AlgoAmount.from_micro_algo(0), - static_fee=AlgoAmount.from_micro_algo(1500), - ) - - expected_fee = 2000 - params = AppClientMethodCallParams( - method="nestedTxnArg", - args=[ - self.app_client1.algorand.create_transaction.payment(payment_params), - txn_arg_call, - ], - static_fee=AlgoAmount.from_micro_algo(expected_fee), - ) - result = nested_client.send.call(params, send_params={"cover_app_call_inner_transaction_fees": True}) - - assert len(result.transactions) == 3 - assert result.transactions[0].raw.fee == 1500 - assert result.transactions[1].raw.fee == 3500 - assert result.transactions[2].raw.fee == expected_fee - - self._assert_min_fee( - nested_client, - dataclasses.replace( - params, - args=[self.app_client1.algorand.create_transaction.payment(payment_params), txn_arg_call], - ), - expected_fee, - ) - - def test_throws_when_max_fee_below_calculated(self) -> None: - """Test that error is thrown when max fee is below calculated fee""" - - with pytest.raises( - ValueError, match="Calculated transaction fee 7000 µALGO is greater than max of 1200 for transaction 0" - ): - ( - self.app_client1.algorand.new_group() - .add_app_call_method_call( - self.app_client1.params.call( - AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - max_fee=AlgoAmount.from_micro_algo(1200), - ) - ) - ) - # This transaction allows this state to be possible, without it the simulate call - # to get the execution info would fail - .add_app_call_method_call( - self.app_client1.params.call( - AppClientMethodCallParams( - method="no_op", - max_fee=AlgoAmount.from_micro_algo(10_000), - ) - ) - ) - .send({"cover_app_call_inner_transaction_fees": True}) - ) - - def test_throws_when_nested_max_fee_below_calculated(self, funded_account: SigningAccount) -> None: - """Test that error is thrown when nested max fee is below calculated fee""" - - # Create nested contract app - app_spec = (Path(__file__).parent.parent / "artifacts" / "nested_contract" / "application.json").read_text() - nested_factory = self.app_client1.algorand.client.get_app_factory( - app_spec=app_spec, - default_sender=funded_account.address, - ) - nested_client, _ = nested_factory.send.create( - params=AppFactoryCreateMethodCallParams(method="createApplication") - ) - - txn_arg_call = self.app_client1.params.call( - AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 2000, 0, [0, 0]]], - max_fee=AlgoAmount.from_micro_algo(2000), - ) - ) - - with pytest.raises( - ValueError, match="Calculated transaction fee 5000 µALGO is greater than max of 2000 for transaction 1" - ): - nested_client.send.call( - AppClientMethodCallParams( - method="nestedTxnArg", - args=[ - self.app_client1.algorand.create_transaction.payment( - PaymentParams( - sender=funded_account.address, - receiver=funded_account.address, - amount=AlgoAmount.from_micro_algo(0), - ) - ), - txn_arg_call, - ], - max_fee=AlgoAmount.from_micro_algo(10_000), - ), - send_params={ - "cover_app_call_inner_transaction_fees": True, - }, - ) - - def test_throws_when_static_fee_below_calculated(self) -> None: - """Test that error is thrown when static fee is below calculated fee""" - - with pytest.raises( - ValueError, match="Calculated transaction fee 7000 µALGO is greater than max of 5000 for transaction 0" - ): - ( - self.app_client1.algorand.new_group() - .add_app_call_method_call( - self.app_client1.params.call( - AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - static_fee=AlgoAmount.from_micro_algo(5000), - ) - ) - ) - # This transaction allows this state to be possible, without it the simulate call - # to get the execution info would fail - .add_app_call_method_call( - self.app_client1.params.call( - AppClientMethodCallParams( - method="no_op", - max_fee=AlgoAmount.from_micro_algo(10_000), - ) - ) - ) - .send({"cover_app_call_inner_transaction_fees": True}) - ) - - def test_throws_when_non_app_call_static_fee_too_low(self, funded_account: SigningAccount) -> None: - """Test that error is thrown when static fee for non-app-call transaction is too low""" - - with pytest.raises( - ValueError, match="An additional fee of 500 µALGO is required for non app call transaction 2" - ): - ( - self.app_client1.algorand.new_group() - .add_app_call_method_call( - self.app_client1.params.call( - AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - static_fee=AlgoAmount.from_micro_algo(13_000), - max_fee=AlgoAmount.from_micro_algo(14_000), - ) - ) - ) - .add_app_call_method_call( - self.app_client1.params.call( - AppClientMethodCallParams( - method="send_inners_with_fees", - args=[self.app_client2.app_id, self.app_client3.app_id, [0, 0, 0, 0, [0, 0]]], - static_fee=AlgoAmount.from_micro_algo(1000), - ) - ) - ) - .add_payment( - params=PaymentParams( - sender=funded_account.address, - receiver=funded_account.address, - amount=AlgoAmount.from_micro_algo(0), - static_fee=AlgoAmount.from_micro_algo(500), - ) - ) - .send({"cover_app_call_inner_transaction_fees": True}) - ) - - def test_handles_expensive_abi_calls_with_ensure_budget(self) -> None: - """Test fee handling with expensive ABI method calls that use ensure_budget to op-up""" - - expected_fee = 10_000 - params = AppClientMethodCallParams( - method="burn_ops", - args=[6200], - max_fee=AlgoAmount.from_micro_algo(12_000), - ) - result = self.app_client1.send.call(params, send_params={"cover_app_call_inner_transaction_fees": True}) - - assert result.transaction.raw.fee == expected_fee - assert len(result.confirmation.get("inner-txns", [])) == 9 # type: ignore[union-attr] - self._assert_min_fee(self.app_client1, params, expected_fee) - - def _assert_min_fee(self, app_client: AppClient, params: AppClientMethodCallParams, fee: int) -> None: - """Helper to assert minimum required fee""" - if fee == 1000: - return - params_copy = dataclasses.replace( - params, - static_fee=None, - extra_fee=None, - ) - - with pytest.raises(Exception, match="fee too small"): - app_client.send.call(params_copy) From 36337eb0a8bd78bd5228293b789d9db42ea8df87 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Wed, 5 Feb 2025 17:45:27 +0100 Subject: [PATCH 6/7] refactor: further align static get_{}_client methods in ClientManager with ts implementation --- .github/workflows/check-python.yaml | 3 ++ .../accounts/kmd_account_manager.py | 2 + src/algokit_utils/applications/app_client.py | 1 + src/algokit_utils/clients/client_manager.py | 9 ++-- tests/artifacts/inner-fee/contract.py | 45 ++++++++++++------- 5 files changed, 38 insertions(+), 22 deletions(-) diff --git a/.github/workflows/check-python.yaml b/.github/workflows/check-python.yaml index 1e9256f4..e70bde62 100644 --- a/.github/workflows/check-python.yaml +++ b/.github/workflows/check-python.yaml @@ -45,6 +45,9 @@ jobs: - name: Check types with mypy run: poetry run mypy + - name: Check docstrings are up to date + run: poetry run poe docstrings-check + # TODO: Restore before prod release of v3 # - name: Check docs are up to date # run: | diff --git a/src/algokit_utils/accounts/kmd_account_manager.py b/src/algokit_utils/accounts/kmd_account_manager.py index ae3c8c7b..8dd6e6ab 100644 --- a/src/algokit_utils/accounts/kmd_account_manager.py +++ b/src/algokit_utils/accounts/kmd_account_manager.py @@ -48,6 +48,8 @@ def kmd(self) -> KMDClient: if self._kmd is None: if self._client_manager.is_localnet(): kmd_config = ClientManager.get_config_from_environment_or_localnet() + if not kmd_config.kmd_config: + raise Exception("Attempt to use KMD client with no KMD configured") self._kmd = ClientManager.get_kmd_client(kmd_config.kmd_config) return self._kmd raise Exception("Attempt to use KMD client with no KMD configured") diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py index 7ec293a0..ca23738f 100644 --- a/src/algokit_utils/applications/app_client.py +++ b/src/algokit_utils/applications/app_client.py @@ -1190,6 +1190,7 @@ def call( :param params: Parameters for the application call including method and transaction options :param send_params: Send parameters :return: The result of sending or simulating the transaction, including ABI return value if applicable + :raises ValueError: If the transaction is read-only and `max_fee` is not provided """ is_read_only_call = ( params.on_complete == algosdk.transaction.OnComplete.NoOpOC or params.on_complete is None diff --git a/src/algokit_utils/clients/client_manager.py b/src/algokit_utils/clients/client_manager.py index f1bf8583..2e57f6b6 100644 --- a/src/algokit_utils/clients/client_manager.py +++ b/src/algokit_utils/clients/client_manager.py @@ -351,13 +351,12 @@ def get_app_client_by_creator_and_name( ) @staticmethod - def get_algod_client(config: AlgoClientNetworkConfig | None = None) -> AlgodClient: + def get_algod_client(config: AlgoClientNetworkConfig) -> AlgodClient: """Get an Algod client from config or environment. :param config: Optional client configuration :return: Algod client instance """ - config = config or _get_config_from_environment("ALGOD") headers = {"X-Algo-API-Token": config.token or ""} return AlgodClient( algod_token=config.token or "", @@ -374,13 +373,12 @@ def get_algod_client_from_environment() -> AlgodClient: return ClientManager.get_algod_client(ClientManager.get_algod_config_from_environment()) @staticmethod - def get_kmd_client(config: AlgoClientNetworkConfig | None = None) -> KMDClient: + def get_kmd_client(config: AlgoClientNetworkConfig) -> KMDClient: """Get a KMD client from config or environment. :param config: Optional client configuration :return: KMD client instance """ - config = config or _get_config_from_environment("KMD") return KMDClient(config.token, config.full_url()) @staticmethod @@ -392,13 +390,12 @@ def get_kmd_client_from_environment() -> KMDClient: return ClientManager.get_kmd_client(ClientManager.get_kmd_config_from_environment()) @staticmethod - def get_indexer_client(config: AlgoClientNetworkConfig | None = None) -> IndexerClient: + def get_indexer_client(config: AlgoClientNetworkConfig) -> IndexerClient: """Get an Indexer client from config or environment. :param config: Optional client configuration :return: Indexer client instance """ - config = config or _get_config_from_environment("INDEXER") headers = {"X-Indexer-API-Token": config.token} return IndexerClient( indexer_token=config.token, diff --git a/tests/artifacts/inner-fee/contract.py b/tests/artifacts/inner-fee/contract.py index e33d75b3..0850802e 100644 --- a/tests/artifacts/inner-fee/contract.py +++ b/tests/artifacts/inner-fee/contract.py @@ -19,7 +19,7 @@ def burn_ops(self, op_budget: UInt64) -> None: ensure_budget(op_budget) for i in urange(count): sqrt = op.bsqrt(BigUInt(i)) - assert(sqrt >= 0) # Prevent optimiser removing the sqrt + assert sqrt >= 0 # Prevent optimiser removing the sqrt @arc4.abimethod(readonly=True) def burn_ops_readonly(self, op_budget: UInt64) -> None: @@ -32,22 +32,35 @@ def no_op(self) -> None: @arc4.abimethod def send_x_inners_with_fees(self, app_id: UInt64, fees: arc4.DynamicArray[arc4.UInt64]) -> None: for fee in fees: - arc4.abi_call('no_op', app_id=app_id, fee=fee.native) + arc4.abi_call("no_op", app_id=app_id, fee=fee.native) @arc4.abimethod - def send_inners_with_fees(self, app_id_1: UInt64, app_id_2: UInt64, fees: arc4.Tuple[arc4.UInt64, arc4.UInt64, arc4.UInt64, arc4.UInt64, arc4.DynamicArray[arc4.UInt64]]) -> None: - arc4.abi_call('no_op', app_id=app_id_1, fee=fees[0].native) - arc4.abi_call('no_op', app_id=app_id_1, fee=fees[1].native) - itxn.Payment( - amount=0, - receiver=Global.current_application_address, - fee=fees[2].native - ).submit() - arc4.abi_call('send_x_inners_with_fees', app_id_2, fees[4], app_id=app_id_1, fee=fees[3].native) + def send_inners_with_fees( + self, + app_id_1: UInt64, + app_id_2: UInt64, + fees: arc4.Tuple[arc4.UInt64, arc4.UInt64, arc4.UInt64, arc4.UInt64, arc4.DynamicArray[arc4.UInt64]], + ) -> None: + arc4.abi_call("no_op", app_id=app_id_1, fee=fees[0].native) + arc4.abi_call("no_op", app_id=app_id_1, fee=fees[1].native) + itxn.Payment(amount=0, receiver=Global.current_application_address, fee=fees[2].native).submit() + arc4.abi_call("send_x_inners_with_fees", app_id_2, fees[4], app_id=app_id_1, fee=fees[3].native) @arc4.abimethod - def send_inners_with_fees_2(self, app_id_1: UInt64, app_id_2: UInt64, fees: arc4.Tuple[arc4.UInt64, arc4.UInt64, arc4.DynamicArray[arc4.UInt64], arc4.UInt64, arc4.UInt64, arc4.DynamicArray[arc4.UInt64]]) -> None: - arc4.abi_call('no_op', app_id=app_id_1, fee=fees[0].native) - arc4.abi_call('send_x_inners_with_fees', app_id_2, fees[2], app_id=app_id_1, fee=fees[1].native) - arc4.abi_call('no_op', app_id=app_id_1, fee=fees[3].native) - arc4.abi_call('send_x_inners_with_fees', app_id_2, fees[5], app_id=app_id_1, fee=fees[4].native) + def send_inners_with_fees_2( + self, + app_id_1: UInt64, + app_id_2: UInt64, + fees: arc4.Tuple[ + arc4.UInt64, + arc4.UInt64, + arc4.DynamicArray[arc4.UInt64], + arc4.UInt64, + arc4.UInt64, + arc4.DynamicArray[arc4.UInt64], + ], + ) -> None: + arc4.abi_call("no_op", app_id=app_id_1, fee=fees[0].native) + arc4.abi_call("send_x_inners_with_fees", app_id_2, fees[2], app_id=app_id_1, fee=fees[1].native) + arc4.abi_call("no_op", app_id=app_id_1, fee=fees[3].native) + arc4.abi_call("send_x_inners_with_fees", app_id_2, fees[5], app_id=app_id_1, fee=fees[4].native) From 2d4eeacabb7ad1fe639518ea26ca871107eaa3af Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Wed, 5 Feb 2025 18:23:03 +0100 Subject: [PATCH 7/7] docs: fixing autogenerated urls; fixing docstring warnings --- .../accounts/account_manager/index.md | 368 +++++++++--------- .../assets/asset_manager/index.md | 31 +- .../clients/client_manager/index.md | 6 +- .../algokit_utils/models/amount/index.md | 27 +- .../transaction_composer/index.md | 59 ++- docs/markdown/capabilities/account.md | 44 +-- docs/markdown/capabilities/app-client.md | 22 +- docs/markdown/capabilities/app-deploy.md | 26 +- docs/markdown/index.md | 6 +- docs/source/capabilities/account.md | 46 +-- docs/source/capabilities/app-client.md | 22 +- docs/source/capabilities/app-deploy.md | 26 +- docs/source/index.md | 6 +- src/algokit_utils/_legacy_v2/account.py | 8 +- src/algokit_utils/accounts/account_manager.py | 196 +++++----- src/algokit_utils/assets/asset_manager.py | 10 +- src/algokit_utils/models/amount.py | 12 +- .../transactions/transaction_composer.py | 14 +- 18 files changed, 450 insertions(+), 479 deletions(-) diff --git a/docs/markdown/autoapi/algokit_utils/accounts/account_manager/index.md b/docs/markdown/autoapi/algokit_utils/accounts/account_manager/index.md index d72789bb..fccd2f7b 100644 --- a/docs/markdown/autoapi/algokit_utils/accounts/account_manager/index.md +++ b/docs/markdown/autoapi/algokit_utils/accounts/account_manager/index.md @@ -130,10 +130,9 @@ mnemonic-based, rekeyed, multisig, and logic signature accounts. * **Parameters:** **client_manager** – The ClientManager client to use for algod and kmd clients * **Example:** - -```pycon ->>> account_manager = AccountManager(client_manager) -``` + ```pycon + >>> account_manager = AccountManager(client_manager) + ``` #### *property* kmd *: [algokit_utils.accounts.kmd_account_manager.KmdAccountManager](../kmd_account_manager/index.md#algokit_utils.accounts.kmd_account_manager.KmdAccountManager)* @@ -149,14 +148,13 @@ then an error will be thrown from get_signer / get_account. * **Returns:** The AccountManager so method calls can be chained * **Example:** - -```pycon ->>> signer_account = account_manager.random() ->>> account_manager.set_default_signer(signer_account.signer) ->>> # When signing a transaction, if there is no signer registered for the sender ->>> # then the default signer will be used ->>> signer = account_manager.get_signer("{SENDERADDRESS}") -``` + ```pycon + >>> signer_account = account_manager.random() + >>> account_manager.set_default_signer(signer_account.signer) + >>> # When signing a transaction, if there is no signer registered for the sender + >>> # then the default signer will be used + >>> signer = account_manager.get_signer("{SENDERADDRESS}") + ``` #### set_signer(sender: str, signer: algosdk.atomic_transaction_composer.TransactionSigner) → typing_extensions.Self @@ -168,10 +166,9 @@ Tracks the given TransactionSigner against the given sender address for later si * **Returns:** The AccountManager instance for method chaining * **Example:** - -```pycon ->>> account_manager.set_signer("SENDERADDRESS", transaction_signer) -``` + ```pycon + >>> account_manager.set_signer("SENDERADDRESS", transaction_signer) + ``` #### set_signers(\*, another_account_manager: [AccountManager](#algokit_utils.accounts.account_manager.AccountManager), overwrite_existing: bool = True) → typing_extensions.Self @@ -195,13 +192,12 @@ Note: If you are generating accounts via the various methods on AccountManager * **Returns:** The AccountManager instance for method chaining * **Example:** - -```pycon ->>> account_manager = AccountManager(client_manager) ->>> account_manager.set_signer_from_account(SigningAccount(private_key=algosdk.account.generate_account()[0])) ->>> account_manager.set_signer_from_account(LogicSigAccount(AlgosdkLogicSigAccount(program, args))) ->>> account_manager.set_signer_from_account(MultiSigAccount(multisig_params, [account1, account2])) -``` + ```pycon + >>> account_manager = AccountManager(client_manager) + >>> account_manager.set_signer_from_account(SigningAccount(private_key=algosdk.account.generate_account()[0])) + >>> account_manager.set_signer_from_account(LogicSigAccount(AlgosdkLogicSigAccount(program, args))) + >>> account_manager.set_signer_from_account(MultiSigAccount(multisig_params, [account1, account2])) + ``` #### get_signer(sender: str | [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol)) → algosdk.atomic_transaction_composer.TransactionSigner @@ -216,10 +212,9 @@ If no signer has been registered for that address then the default signer is use * **Raises:** **ValueError** – If no signer is found and no default signer is set * **Example:** - -```pycon ->>> signer = account_manager.get_signer("SENDERADDRESS") -``` + ```pycon + >>> signer = account_manager.get_signer("SENDERADDRESS") + ``` #### get_account(sender: str) → [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol) @@ -232,13 +227,12 @@ Returns the TransactionSignerAccountProtocol for the given sender address. * **Raises:** **ValueError** – If no account is found or if the account is not a regular account * **Example:** - -```pycon ->>> sender = account_manager.random().address ->>> # ... ->>> # Returns the `TransactionSignerAccountProtocol` for `sender` that has previously been registered ->>> account = account_manager.get_account(sender) -``` + ```pycon + >>> sender = account_manager.random().address + >>> # ... + >>> # Returns the `TransactionSignerAccountProtocol` for `sender` that has previously been registered + >>> account = account_manager.get_account(sender) + ``` #### get_information(sender: str | [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol)) → [AccountInformation](#algokit_utils.accounts.account_manager.AccountInformation) @@ -252,11 +246,10 @@ for response data schema details. * **Returns:** The account information * **Example:** - -```pycon ->>> address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA" ->>> account_info = account_manager.get_information(address) -``` + ```pycon + >>> address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA" + >>> account_info = account_manager.get_information(address) + ``` #### from_mnemonic(\*, mnemonic: str, sender: str | None = None) → [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) @@ -273,10 +266,9 @@ Be careful how the mnemonic is handled. Never commit it into source control and from the environment (ideally via a secret storage service) rather than the file system. * **Example:** - -```pycon ->>> account = account_manager.from_mnemonic("mnemonic secret ...") -``` + ```pycon + >>> account = account_manager.from_mnemonic("mnemonic secret ...") + ``` #### from_environment(name: str, fund_with: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None) → [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) @@ -288,10 +280,11 @@ without manual config locally (including when you reset the LocalNet). * **Parameters:** * **name** – The name identifier of the account * **fund_with** – Optional amount to fund the account with when it gets created - -(when targeting LocalNet) -:returns: The account -:raises ValueError: If environment variable {NAME}_MNEMONIC is missing when looking for account {NAME} + (when targeting LocalNet) +* **Returns:** + The account +* **Raises:** + **ValueError** – If environment variable {NAME}_MNEMONIC is missing when looking for account {NAME} #### NOTE Convention: @@ -302,13 +295,12 @@ Convention: it will create it and fund the account for you * **Example:** - -```pycon ->>> # If you have a mnemonic secret loaded into `MY_ACCOUNT_MNEMONIC` then you can call: ->>> account = account_manager.from_environment('MY_ACCOUNT') ->>> # If that code runs against LocalNet then a wallet called `MY_ACCOUNT` will automatically be created ->>> # with an account that is automatically funded with the specified amount from the default LocalNet dispenser -``` + ```pycon + >>> # If you have a mnemonic secret loaded into `MY_ACCOUNT_MNEMONIC` then you can call: + >>> account = account_manager.from_environment('MY_ACCOUNT') + >>> # If that code runs against LocalNet then a wallet called `MY_ACCOUNT` will automatically be created + >>> # with an account that is automatically funded with the specified amount from the LocalNet dispenser + ``` #### from_kmd(name: str, predicate: collections.abc.Callable[[dict[str, Any]], bool] | None = None, sender: str | None = None) → [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) @@ -323,13 +315,12 @@ Tracks and returns an Algorand account with private key loaded from the given KM * **Raises:** **ValueError** – If unable to find KMD account with given name and predicate * **Example:** - -```pycon ->>> # Get default funded account in a LocalNet: ->>> defaultDispenserAccount = account.from_kmd('unencrypted-default-wallet', -... lambda a: a.status != 'Offline' and a.amount > 1_000_000_000 -... ) -``` + ```pycon + >>> # Get default funded account in a LocalNet: + >>> defaultDispenserAccount = account.from_kmd('unencrypted-default-wallet', + ... lambda a: a.status != 'Offline' and a.amount > 1_000_000_000 + ... ) + ``` #### logicsig(program: bytes, args: list[bytes] | None = None) → algokit_utils.models.account.LogicSigAccount @@ -341,10 +332,9 @@ Tracks and returns an account that represents a logic signature. * **Returns:** A logic signature account wrapper * **Example:** - -```pycon ->>> account = account.logic_sig(program, [new Uint8Array(3, ...)]) -``` + ```pycon + >>> account = account.logic_sig(program, [new Uint8Array(3, ...)]) + ``` #### multisig(metadata: [algokit_utils.models.account.MultisigMetadata](../../models/account/index.md#algokit_utils.models.account.MultisigMetadata), signing_accounts: list[[algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount)]) → [algokit_utils.models.account.MultiSigAccount](../../models/account/index.md#algokit_utils.models.account.MultiSigAccount) @@ -356,15 +346,14 @@ Tracks and returns an account that supports partial or full multisig signing. * **Returns:** A multisig account wrapper * **Example:** - -```pycon ->>> account = account_manager.multi_sig( -... version=1, -... threshold=1, -... addrs=["ADDRESS1...", "ADDRESS2..."], -... signing_accounts=[account1, account2] -... ) -``` + ```pycon + >>> account = account_manager.multi_sig( + ... version=1, + ... threshold=1, + ... addrs=["ADDRESS1...", "ADDRESS2..."], + ... signing_accounts=[account1, account2] + ... ) + ``` #### random() → [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) @@ -373,10 +362,9 @@ Tracks and returns a new, random Algorand account. * **Returns:** The account * **Example:** - -```pycon ->>> account = account_manager.random() -``` + ```pycon + >>> account = account_manager.random() + ``` #### localnet_dispenser() → [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) @@ -387,10 +375,9 @@ This account can be used to fund other accounts. * **Returns:** The account * **Example:** - -```pycon ->>> account = account_manager.localnet_dispenser() -``` + ```pycon + >>> account = account_manager.localnet_dispenser() + ``` #### dispenser_from_environment() → [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) @@ -401,10 +388,9 @@ If environment variables are not present, returns the default LocalNet dispenser * **Returns:** The account * **Example:** - -```pycon ->>> account = account_manager.dispenser_from_environment() -``` + ```pycon + >>> account = account_manager.dispenser_from_environment() + ``` #### rekeyed(\*, sender: str, account: [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol)) → [algokit_utils.models.account.TransactionSignerAccount](../../models/account/index.md#algokit_utils.models.account.TransactionSignerAccount) | [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount) @@ -416,11 +402,10 @@ Tracks and returns an Algorand account that is a rekeyed version of the given ac * **Returns:** The rekeyed account * **Example:** - -```pycon ->>> account = account.from_mnemonic("mnemonic secret ...") ->>> rekeyed_account = account_manager.rekeyed(account, "SENDERADDRESS...") -``` + ```pycon + >>> account = account.from_mnemonic("mnemonic secret ...") + >>> rekeyed_account = account_manager.rekeyed(account, "SENDERADDRESS...") + ``` #### rekey_account(account: str, rekey_to: str | [algokit_utils.protocols.account.TransactionSignerAccountProtocol](../../protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol), \*, signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, note: bytes | None = None, lease: bytes | None = None, static_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, extra_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, max_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, validity_window: int | None = None, first_valid_round: int | None = None, last_valid_round: int | None = None, suppress_log: bool | None = None) → [algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults](../../transactions/transaction_composer/index.md#algokit_utils.transactions.transaction_composer.SendAtomicTransactionComposerResults) @@ -447,26 +432,25 @@ Please be careful with this function and be sure to read the [official rekey guidance](https://developer.algorand.org/docs/get-details/accounts/rekey/). * **Example:** - -```pycon ->>> # Basic example (with string addresses): ->>> algorand.account.rekey_account({account: "ACCOUNTADDRESS", rekey_to: "NEWADDRESS"}) ->>> # Basic example (with signer accounts): ->>> algorand.account.rekey_account({account: account1, rekey_to: newSignerAccount}) ->>> # Advanced example: ->>> algorand.account.rekey_account({ -... account: "ACCOUNTADDRESS", -... rekey_to: "NEWADDRESS", -... lease: 'lease', -... note: 'note', -... first_valid_round: 1000, -... validity_window: 10, -... extra_fee: AlgoAmount.from_micro_algo(1000), -... static_fee: AlgoAmount.from_micro_algo(1000), -... max_fee: AlgoAmount.from_micro_algo(3000), -... suppress_log: True, -... }) -``` + ```pycon + >>> # Basic example (with string addresses): + >>> algorand.account.rekey_account({account: "ACCOUNTADDRESS", rekey_to: "NEWADDRESS"}) + >>> # Basic example (with signer accounts): + >>> algorand.account.rekey_account({account: account1, rekey_to: newSignerAccount}) + >>> # Advanced example: + >>> algorand.account.rekey_account({ + ... account: "ACCOUNTADDRESS", + ... rekey_to: "NEWADDRESS", + ... lease: 'lease', + ... note: 'note', + ... first_valid_round: 1000, + ... validity_window: 10, + ... extra_fee: AlgoAmount.from_micro_algo(1000), + ... static_fee: AlgoAmount.from_micro_algo(1000), + ... max_fee: AlgoAmount.from_micro_algo(3000), + ... suppress_log: True, + ... }) + ``` #### ensure_funded(account_to_fund: str | [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount), dispenser_account: str | [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount), min_spending_balance: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount), min_funding_increment: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None, signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, rekey_to: str | None = None, note: bytes | None = None, lease: bytes | None = None, static_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, extra_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, max_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, validity_window: int | None = None, first_valid_round: int | None = None, last_valid_round: int | None = None) → [EnsureFundedResult](#algokit_utils.accounts.account_manager.EnsureFundedResult) | None @@ -481,38 +465,36 @@ See [https://developer.algorand.org/docs/get-details/accounts/#minimum-balance]( * **account_to_fund** – The account to fund * **dispenser_account** – The account to use as a dispenser funding source * **min_spending_balance** – The minimum balance of Algo that the account - -should have available to spend -:param min_funding_increment: Optional minimum funding increment -:param send_params: Parameters for the send operation, defaults to None -:param signer: Optional transaction signer -:param rekey_to: Optional rekey address -:param note: Optional transaction note -:param lease: Optional transaction lease -:param static_fee: Optional static fee -:param extra_fee: Optional extra fee -:param max_fee: Optional maximum fee -:param validity_window: Optional validity window -:param first_valid_round: Optional first valid round -:param last_valid_round: Optional last valid round -:returns: The result of executing the dispensing transaction and the amountFunded if funds were needed, -or None if no funds were needed - + should have available to spend + * **min_funding_increment** – Optional minimum funding increment + * **send_params** – Parameters for the send operation, defaults to None + * **signer** – Optional transaction signer + * **rekey_to** – Optional rekey address + * **note** – Optional transaction note + * **lease** – Optional transaction lease + * **static_fee** – Optional static fee + * **extra_fee** – Optional extra fee + * **max_fee** – Optional maximum fee + * **validity_window** – Optional validity window + * **first_valid_round** – Optional first valid round + * **last_valid_round** – Optional last valid round +* **Returns:** + The result of executing the dispensing transaction and the amountFunded if funds were needed, + or None if no funds were needed * **Example:** - -```pycon ->>> # Basic example: ->>> algorand.account.ensure_funded("ACCOUNTADDRESS", "DISPENSERADDRESS", algokit.algo(1)) ->>> # With configuration: ->>> algorand.account.ensure_funded( -... "ACCOUNTADDRESS", -... "DISPENSERADDRESS", -... algokit.algo(1), -... min_funding_increment=algokit.algo(2), -... fee=AlgoAmount.from_micro_algo(1000), -... suppress_log=True -... ) -``` + ```pycon + >>> # Basic example: + >>> algorand.account.ensure_funded("ACCOUNTADDRESS", "DISPENSERADDRESS", algokit.algo(1)) + >>> # With configuration: + >>> algorand.account.ensure_funded( + ... "ACCOUNTADDRESS", + ... "DISPENSERADDRESS", + ... algokit.algo(1), + ... min_funding_increment=algokit.algo(2), + ... fee=AlgoAmount.from_micro_algo(1000), + ... suppress_log=True + ... ) + ``` #### ensure_funded_from_environment(account_to_fund: str | [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount), min_spending_balance: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount), \*, min_funding_increment: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, send_params: [algokit_utils.models.transaction.SendParams](../../models/transaction/index.md#algokit_utils.models.transaction.SendParams) | None = None, signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None, rekey_to: str | None = None, note: bytes | None = None, lease: bytes | None = None, static_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, extra_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, max_fee: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None, validity_window: int | None = None, first_valid_round: int | None = None, last_valid_round: int | None = None) → [EnsureFundedResult](#algokit_utils.accounts.account_manager.EnsureFundedResult) | None @@ -527,22 +509,22 @@ See [https://developer.algorand.org/docs/get-details/accounts/#minimum-balance]( * **Parameters:** * **account_to_fund** – The account to fund * **min_spending_balance** – The minimum balance of Algo that the account should have available to - -spend -:param min_funding_increment: Optional minimum funding increment -:param send_params: Parameters for the send operation, defaults to None -:param signer: Optional transaction signer -:param rekey_to: Optional rekey address -:param note: Optional transaction note -:param lease: Optional transaction lease -:param static_fee: Optional static fee -:param extra_fee: Optional extra fee -:param max_fee: Optional maximum fee -:param validity_window: Optional validity window -:param first_valid_round: Optional first valid round -:param last_valid_round: Optional last valid round -:returns: The result of executing the dispensing transaction and the amountFunded if funds were needed, or -None if no funds were needed + spend + * **min_funding_increment** – Optional minimum funding increment + * **send_params** – Parameters for the send operation, defaults to None + * **signer** – Optional transaction signer + * **rekey_to** – Optional rekey address + * **note** – Optional transaction note + * **lease** – Optional transaction lease + * **static_fee** – Optional static fee + * **extra_fee** – Optional extra fee + * **max_fee** – Optional maximum fee + * **validity_window** – Optional validity window + * **first_valid_round** – Optional first valid round + * **last_valid_round** – Optional last valid round +* **Returns:** + The result of executing the dispensing transaction and the amountFunded if funds were needed, or + None if no funds were needed #### NOTE The dispenser account is retrieved from the account mnemonic stored in @@ -550,19 +532,18 @@ process.env.DISPENSER_MNEMONIC and optionally process.env.DISPENSER_SENDER if it’s a rekeyed account, or against default LocalNet if no environment variables present. * **Example:** - -```pycon ->>> # Basic example: ->>> algorand.account.ensure_funded_from_environment("ACCOUNTADDRESS", algokit.algo(1)) ->>> # With configuration: ->>> algorand.account.ensure_funded_from_environment( -... "ACCOUNTADDRESS", -... algokit.algo(1), -... min_funding_increment=algokit.algo(2), -... fee=AlgoAmount.from_micro_algo(1000), -... suppress_log=True -... ) -``` + ```pycon + >>> # Basic example: + >>> algorand.account.ensure_funded_from_environment("ACCOUNTADDRESS", algokit.algo(1)) + >>> # With configuration: + >>> algorand.account.ensure_funded_from_environment( + ... "ACCOUNTADDRESS", + ... algokit.algo(1), + ... min_funding_increment=algokit.algo(2), + ... fee=AlgoAmount.from_micro_algo(1000), + ... suppress_log=True + ... ) + ``` #### ensure_funded_from_testnet_dispenser_api(account_to_fund: str | [algokit_utils.models.account.SigningAccount](../../models/account/index.md#algokit_utils.models.account.SigningAccount), dispenser_client: [algokit_utils.clients.dispenser_api_client.TestNetDispenserApiClient](../../clients/dispenser_api_client/index.md#algokit_utils.clients.dispenser_api_client.TestNetDispenserApiClient), min_spending_balance: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount), \*, min_funding_increment: [algokit_utils.models.amount.AlgoAmount](../../models/amount/index.md#algokit_utils.models.amount.AlgoAmount) | None = None) → [EnsureFundedFromTestnetDispenserApiResult](#algokit_utils.accounts.account_manager.EnsureFundedFromTestnetDispenserApiResult) | None @@ -577,27 +558,26 @@ See [https://developer.algorand.org/docs/get-details/accounts/#minimum-balance]( * **account_to_fund** – The account to fund * **dispenser_client** – The TestNet dispenser funding client * **min_spending_balance** – The minimum balance of Algo that the account should have - -available to spend -:param min_funding_increment: Optional minimum funding increment -:returns: The result of executing the dispensing transaction and the amountFunded if funds were needed, or -None if no funds were needed -:raises ValueError: If attempting to fund on non-TestNet network - + available to spend + * **min_funding_increment** – Optional minimum funding increment +* **Returns:** + The result of executing the dispensing transaction and the amountFunded if funds were needed, or + None if no funds were needed +* **Raises:** + **ValueError** – If attempting to fund on non-TestNet network * **Example:** - -```pycon ->>> # Basic example: ->>> algorand.account.ensure_funded_from_testnet_dispenser_api( -... "ACCOUNTADDRESS", -... algorand.client.get_testnet_dispenser_from_environment(), -... algokit.algo(1) -... ) ->>> # With configuration: ->>> algorand.account.ensure_funded_from_testnet_dispenser_api( -... "ACCOUNTADDRESS", -... algorand.client.get_testnet_dispenser_from_environment(), -... algokit.algo(1), -... min_funding_increment=algokit.algo(2) -... ) -``` + ```pycon + >>> # Basic example: + >>> algorand.account.ensure_funded_from_testnet_dispenser_api( + ... "ACCOUNTADDRESS", + ... algorand.client.get_testnet_dispenser_from_environment(), + ... algokit.algo(1) + ... ) + >>> # With configuration: + >>> algorand.account.ensure_funded_from_testnet_dispenser_api( + ... "ACCOUNTADDRESS", + ... algorand.client.get_testnet_dispenser_from_environment(), + ... algokit.algo(1), + ... min_funding_increment=algokit.algo(2) + ... ) + ``` diff --git a/docs/markdown/autoapi/algokit_utils/assets/asset_manager/index.md b/docs/markdown/autoapi/algokit_utils/assets/asset_manager/index.md index 7b8afc34..9c30327a 100644 --- a/docs/markdown/autoapi/algokit_utils/assets/asset_manager/index.md +++ b/docs/markdown/autoapi/algokit_utils/assets/asset_manager/index.md @@ -39,22 +39,21 @@ Information about an Algorand Standard Asset (ASA). * **decimals** – The amount of decimal places the asset was created with * **default_frozen** – Whether the asset was frozen by default for all accounts, defaults to None * **manager** – The address of the optional account that can manage the configuration of the asset and destroy it, - -defaults to None -:ivar reserve: The address of the optional account that holds the reserve (uncirculated supply) units of the asset, -defaults to None -:ivar freeze: The address of the optional account that can be used to freeze or unfreeze holdings of this asset, -defaults to None -:ivar clawback: The address of the optional account that can clawback holdings of this asset from any account, -defaults to None -:ivar unit_name: The optional name of the unit of this asset (e.g. ticker name), defaults to None -:ivar unit_name_b64: The optional name of the unit of this asset as bytes, defaults to None -:ivar asset_name: The optional name of the asset, defaults to None -:ivar asset_name_b64: The optional name of the asset as bytes, defaults to None -:ivar url: Optional URL where more information about the asset can be retrieved, defaults to None -:ivar url_b64: Optional URL where more information about the asset can be retrieved as bytes, defaults to None -:ivar metadata_hash: 32-byte hash of some metadata that is relevant to the asset and/or asset holders, -defaults to None + defaults to None + * **reserve** – The address of the optional account that holds the reserve (uncirculated supply) units of the asset, + defaults to None + * **freeze** – The address of the optional account that can be used to freeze or unfreeze holdings of this asset, + defaults to None + * **clawback** – The address of the optional account that can clawback holdings of this asset from any account, + defaults to None + * **unit_name** – The optional name of the unit of this asset (e.g. ticker name), defaults to None + * **unit_name_b64** – The optional name of the unit of this asset as bytes, defaults to None + * **asset_name** – The optional name of the asset, defaults to None + * **asset_name_b64** – The optional name of the asset as bytes, defaults to None + * **url** – Optional URL where more information about the asset can be retrieved, defaults to None + * **url_b64** – Optional URL where more information about the asset can be retrieved as bytes, defaults to None + * **metadata_hash** – 32-byte hash of some metadata that is relevant to the asset and/or asset holders, + defaults to None #### asset_id *: int* diff --git a/docs/markdown/autoapi/algokit_utils/clients/client_manager/index.md b/docs/markdown/autoapi/algokit_utils/clients/client_manager/index.md index 55dfc287..2f1ea030 100644 --- a/docs/markdown/autoapi/algokit_utils/clients/client_manager/index.md +++ b/docs/markdown/autoapi/algokit_utils/clients/client_manager/index.md @@ -188,7 +188,7 @@ Get an application client by creator address and name. * **Returns:** Application client instance -#### *static* get_algod_client(config: [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) | None = None) → algosdk.v2client.algod.AlgodClient +#### *static* get_algod_client(config: [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig)) → algosdk.v2client.algod.AlgodClient Get an Algod client from config or environment. @@ -204,7 +204,7 @@ Get an Algod client from environment variables. * **Returns:** Algod client instance -#### *static* get_kmd_client(config: [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) | None = None) → algosdk.kmd.KMDClient +#### *static* get_kmd_client(config: [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig)) → algosdk.kmd.KMDClient Get a KMD client from config or environment. @@ -220,7 +220,7 @@ Get a KMD client from environment variables. * **Returns:** KMD client instance -#### *static* get_indexer_client(config: [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig) | None = None) → algosdk.v2client.indexer.IndexerClient +#### *static* get_indexer_client(config: [algokit_utils.models.network.AlgoClientNetworkConfig](../../models/network/index.md#algokit_utils.models.network.AlgoClientNetworkConfig)) → algosdk.v2client.indexer.IndexerClient Get an Indexer client from config or environment. diff --git a/docs/markdown/autoapi/algokit_utils/models/amount/index.md b/docs/markdown/autoapi/algokit_utils/models/amount/index.md index d865b298..27fb5de3 100644 --- a/docs/markdown/autoapi/algokit_utils/models/amount/index.md +++ b/docs/markdown/autoapi/algokit_utils/models/amount/index.md @@ -26,13 +26,12 @@ Wrapper class to ensure safe, explicit conversion between µAlgo, Algo and numbers. * **Example:** - -```pycon ->>> amount = AlgoAmount(algo=1) ->>> amount = AlgoAmount.from_algo(1) ->>> amount = AlgoAmount(micro_algo=1_000_000) ->>> amount = AlgoAmount.from_micro_algo(1_000_000) -``` + ```pycon + >>> amount = AlgoAmount(algo=1) + >>> amount = AlgoAmount.from_algo(1) + >>> amount = AlgoAmount(micro_algo=1_000_000) + >>> amount = AlgoAmount.from_micro_algo(1_000_000) + ``` #### *property* micro_algo *: int* @@ -57,10 +56,9 @@ Create an AlgoAmount object representing the given number of Algo. * **Returns:** An AlgoAmount instance. * **Example:** - -```pycon ->>> amount = AlgoAmount.from_algo(1) -``` + ```pycon + >>> amount = AlgoAmount.from_algo(1) + ``` #### *static* from_micro_algo(amount: int) → [AlgoAmount](#algokit_utils.models.amount.AlgoAmount) @@ -71,10 +69,9 @@ Create an AlgoAmount object representing the given number of µAlgo. * **Returns:** An AlgoAmount instance. * **Example:** - -```pycon ->>> amount = AlgoAmount.from_micro_algo(1_000_000) -``` + ```pycon + >>> amount = AlgoAmount.from_micro_algo(1_000_000) + ``` ### algokit_utils.models.amount.algo(algo: int) → [AlgoAmount](#algokit_utils.models.amount.AlgoAmount) diff --git a/docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md b/docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md index a1337a7f..2bd8257e 100644 --- a/docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md +++ b/docs/markdown/autoapi/algokit_utils/transactions/transaction_composer/index.md @@ -53,8 +53,7 @@ Parameters for a payment transaction. * **receiver** – The account that will receive the ALGO * **amount** – Amount to send * **close_remainder_to** – If given, close the sender account and send the remaining balance to this address, - -defaults to None + defaults to None #### receiver *: str* @@ -319,19 +318,18 @@ Bases: `_CommonTxnParams` Parameters for creating an application. * **Variables:** - **approval_program** – The program to execute for all OnCompletes other than ClearState as raw teal (string) - -or compiled teal (bytes) -:ivar clear_state_program: The program to execute for ClearState OnComplete as raw teal (string) -or compiled teal (bytes) -:ivar schema: The state schema for the app. This is immutable, defaults to None -:ivar on_complete: The OnComplete action (cannot be ClearState), defaults to None -:ivar args: Application arguments, defaults to None -:ivar account_references: Account references, defaults to None -:ivar app_references: App references, defaults to None -:ivar asset_references: Asset references, defaults to None -:ivar box_references: Box references, defaults to None -:ivar extra_program_pages: Number of extra pages required for the programs, defaults to None + * **approval_program** – The program to execute for all OnCompletes other than ClearState as raw teal (string) + or compiled teal (bytes) + * **clear_state_program** – The program to execute for ClearState OnComplete as raw teal (string) + or compiled teal (bytes) + * **schema** – The state schema for the app. This is immutable, defaults to None + * **on_complete** – The OnComplete action (cannot be ClearState), defaults to None + * **args** – Application arguments, defaults to None + * **account_references** – Account references, defaults to None + * **app_references** – App references, defaults to None + * **asset_references** – Asset references, defaults to None + * **box_references** – Box references, defaults to None + * **extra_program_pages** – Number of extra pages required for the programs, defaults to None #### approval_program *: str | bytes* @@ -362,16 +360,15 @@ Parameters for updating an application. * **Variables:** * **app_id** – ID of the application * **approval_program** – The program to execute for all OnCompletes other than ClearState as raw teal (string) - -or compiled teal (bytes) -:ivar clear_state_program: The program to execute for ClearState OnComplete as raw teal (string) -or compiled teal (bytes) -:ivar args: Application arguments, defaults to None -:ivar account_references: Account references, defaults to None -:ivar app_references: App references, defaults to None -:ivar asset_references: Asset references, defaults to None -:ivar box_references: Box references, defaults to None -:ivar on_complete: The OnComplete action, defaults to None + or compiled teal (bytes) + * **clear_state_program** – The program to execute for ClearState OnComplete as raw teal (string) + or compiled teal (bytes) + * **args** – Application arguments, defaults to None + * **account_references** – Account references, defaults to None + * **app_references** – App references, defaults to None + * **asset_references** – Asset references, defaults to None + * **box_references** – Box references, defaults to None + * **on_complete** – The OnComplete action, defaults to None #### app_id *: int* @@ -430,9 +427,8 @@ Parameters for a regular ABI method call. * **app_id** – ID of the application * **method** – The ABI method to call * **args** – Arguments to the ABI method, either an ABI value, transaction with explicit signer, - -transaction, another method call, or None -:ivar on_complete: The OnComplete action (cannot be UpdateApplication or ClearState), defaults to None + transaction, another method call, or None + * **on_complete** – The OnComplete action (cannot be UpdateApplication or ClearState), defaults to None #### app_id *: int* @@ -610,10 +606,9 @@ Supports various transaction types including payments, asset operations, applica * **algod** – An instance of AlgodClient used to get suggested params and send transactions * **get_signer** – A function that takes an address and returns a TransactionSigner for that address * **get_suggested_params** – Optional function to get suggested transaction parameters, - -defaults to using algod.suggested_params() -:param default_validity_window: Optional default validity window for transactions in rounds, defaults to 10 -:param app_manager: Optional AppManager instance for compiling TEAL programs, defaults to None + defaults to using algod.suggested_params() + * **default_validity_window** – Optional default validity window for transactions in rounds, defaults to 10 + * **app_manager** – Optional AppManager instance for compiling TEAL programs, defaults to None #### add_transaction(transaction: algosdk.transaction.Transaction, signer: algosdk.atomic_transaction_composer.TransactionSigner | None = None) → [TransactionComposer](#algokit_utils.transactions.transaction_composer.TransactionComposer) diff --git a/docs/markdown/capabilities/account.md b/docs/markdown/capabilities/account.md index cfbc2c71..1dc8aefd 100644 --- a/docs/markdown/capabilities/account.md +++ b/docs/markdown/capabilities/account.md @@ -4,7 +4,7 @@ Account management is one of the core capabilities provided by AlgoKit Utils. It ## `AccountManager` -The [`AccountManager`]() is a class that is used to get, create, and fund accounts and perform account-related actions such as funding. The `AccountManager` also keeps track of signers for each address so when using the [`TransactionComposer`](transaction-composer.md) to send transactions, a signer function does not need to manually be specified for each transaction - instead it can be inferred from the sender address automatically! +The [`AccountManager`](../autoapi/algokit_utils/accounts/account_manager/index.md#algokit_utils.accounts.account_manager.AccountManager) is a class that is used to get, create, and fund accounts and perform account-related actions such as funding. The `AccountManager` also keeps track of signers for each address so when using the [`TransactionComposer`](transaction-composer.md) to send transactions, a signer function does not need to manually be specified for each transaction - instead it can be inferred from the sender address automatically! To get an instance of `AccountManager`, you can use either [`AlgorandClient`](algorand-client.md) via `algorand.account` or instantiate it directly: @@ -16,14 +16,14 @@ account_manager = AccountManager(client_manager) ## `TransactionSignerAccountProtocol` -The core internal type that holds information about a signer/sender pair for a transaction is [`TransactionSignerAccountProtocol`](), which represents an `algosdk.transaction.TransactionSigner` (`signer`) along with a sender address (`address`) as the encoded string address. +The core internal type that holds information about a signer/sender pair for a transaction is [`TransactionSignerAccountProtocol`](../autoapi/algokit_utils/protocols/account/index.md#algokit_utils.protocols.account.TransactionSignerAccountProtocol), which represents an `algosdk.transaction.TransactionSigner` (`signer`) along with a sender address (`address`) as the encoded string address. The following conform to `TransactionSignerAccountProtocol`: -- [`TransactionSignerAccount`]() - a basic transaction signer account that holds an address and a signer conforming to `TransactionSignerAccountProtocol` -- [`SigningAccount`]() - an abstraction that used to be available under `Account` in previous versions of AlgoKit Utils. Renamed for consistency with equivalent `ts` version. Holds private key and conforms to `TransactionSignerAccountProtocol` -- [`LogicSigAccount`]() - a wrapper class around `algosdk` logicsig abstractions conforming to `TransactionSignerAccountProtocol` -- [`MultisigAccount`]() - a wrapper class around `algosdk` multisig abstractions conforming to `TransactionSignerAccountProtocol` +- [`TransactionSignerAccount`](../autoapi/algokit_utils/models/account/index.md#algokit_utils.models.account.TransactionSignerAccount) - a basic transaction signer account that holds an address and a signer conforming to `TransactionSignerAccountProtocol` +- [`SigningAccount`](../autoapi/algokit_utils/models/account/index.md#algokit_utils.models.account.SigningAccount) - an abstraction that used to be available under `Account` in previous versions of AlgoKit Utils. Renamed for consistency with equivalent `ts` version. Holds private key and conforms to `TransactionSignerAccountProtocol` +- `LogicSigAccount` - a wrapper class around `algosdk` logicsig abstractions conforming to `TransactionSignerAccountProtocol` +- `MultisigAccount` - a wrapper class around `algosdk` multisig abstractions conforming to `TransactionSignerAccountProtocol` ## Registering a signer @@ -61,7 +61,7 @@ algorand.account.set_default_signer(my_default_signer) ## Get a signer -[`AlgorandClient`](algorand-client.md) will automatically retrieve a signer when signing a transaction, but if you need to get a `TransactionSigner` externally to do something more custom then you can [retrieve the signer]() for a given sender address: +[`AlgorandClient`](algorand-client.md) will automatically retrieve a signer when signing a transaction, but if you need to get a `TransactionSigner` externally to do something more custom then you can [`get_signer`](../autoapi/algokit_utils/accounts/account_manager/index.md#algokit_utils.accounts.account_manager.AccountManager.get_signer) for a given sender address: ```python signer = algorand.account.get_signer("SENDER_ADDRESS") @@ -73,29 +73,29 @@ If there is no signer registered for that sender address it will either return t In order to get/register accounts for signing operations you can use the following methods on [`AccountManager`]() (expressed here as `algorand.account` to denote the syntax via an [`AlgorandClient`](algorand-client.md)): -- [`algorand.account.from_environment(name, fund_with)`]() - Registers and returns an account with private key loaded by convention based on the given name identifier - either by idempotently creating the account in KMD or from environment variable via `process.env['{NAME}_MNEMONIC']` and (optionally) `process.env['{NAME}_SENDER']` (if account is rekeyed) +- [`from_environment`](../autoapi/algokit_utils/accounts/account_manager/index.md#algokit_utils.accounts.account_manager.AccountManager.from_environment) - Registers and returns an account with private key loaded by convention based on the given name identifier - either by idempotently creating the account in KMD or from environment variable via `process.env['{NAME}_MNEMONIC']` and (optionally) `process.env['{NAME}_SENDER']` (if account is rekeyed) - This allows you to have powerful code that will automatically create and fund an account by name locally and when deployed against TestNet/MainNet will automatically resolve from environment variables, without having to have different code - Note: `fund_with` allows you to control how many Algo are seeded into an account created in KMD -- [`algorand.account.from_mnemonic(mnemonic_secret, sender?)`]() - Registers and returns an account with secret key loaded by taking the mnemonic secret -- [`algorand.account.multisig(multisig_params, signing_accounts)`]() - Registers and returns a multisig account with one or more signing keys loaded -- [`algorand.account.rekeyed(sender, signer)`]() - Registers and returns an account representing the given rekeyed sender/signer combination -- [`algorand.account.random()`]() - Returns a new, cryptographically randomly generated account with private key loaded -- [`algorand.account.from_kmd()`]() - Returns an account with private key loaded from the given KMD wallet (identified by name) -- [`algorand.account.logicsig(program, args?)`]() - Returns an account that represents a logic signature +- [`from_mnemonic`](../autoapi/algokit_utils/accounts/account_manager/index.md#algokit_utils.accounts.account_manager.AccountManager.from_mnemonic) - Registers and returns an account with secret key loaded by taking the mnemonic secret +- [`multisig`](../autoapi/algokit_utils/accounts/account_manager/index.md#algokit_utils.accounts.account_manager.AccountManager.multisig) - Registers and returns a multisig account with one or more signing keys loaded +- [`rekeyed`](../autoapi/algokit_utils/accounts/account_manager/index.md#algokit_utils.accounts.account_manager.AccountManager.rekeyed) - Registers and returns an account representing the given rekeyed sender/signer combination +- [`random`](../autoapi/algokit_utils/accounts/account_manager/index.md#algokit_utils.accounts.account_manager.AccountManager.random) - Returns a new, cryptographically randomly generated account with private key loaded +- [`from_kmd`](../autoapi/algokit_utils/accounts/account_manager/index.md#algokit_utils.accounts.account_manager.AccountManager.from_kmd) - Returns an account with private key loaded from the given KMD wallet (identified by name) +- [`logicsig`](../autoapi/algokit_utils/accounts/account_manager/index.md#algokit_utils.accounts.account_manager.AccountManager.logicsig) - Returns an account that represents a logic signature ### Underlying account classes While `TransactionSignerAccount` is the main class used to represent an account that can sign, there are underlying account classes that can underpin the signer within the transaction signer account. -- `Account` - An in-built `algosdk.Account` object that has an address and private signing key, this can be created -- [`SigningAccount`]() - An abstraction around `algosdk.Account` that supports rekeyed accounts +- [`TransactionSignerAccount`](../autoapi/algokit_utils/models/account/index.md#algokit_utils.models.account.TransactionSignerAccount) - A default class conforming to `TransactionSignerAccountProtocol` that holds an address and a signer +- [`SigningAccount`](../autoapi/algokit_utils/models/account/index.md#algokit_utils.models.account.SigningAccount) - An abstraction around `algosdk.Account` that supports rekeyed accounts - `LogicSigAccount` - An in-built algosdk `algosdk.LogicSigAccount` object -- [`MultisigAccount`]() - An abstraction around `algosdk.MultisigMetadata`, `algosdk.makeMultiSigAccountTransactionSigner`, `algosdk.multisigAddress`, `algosdk.signMultisigTransaction` and `algosdk.appendSignMultisigTransaction` that supports multisig accounts with one or more signers present +- `MultisigAccount` - An abstraction around `algosdk.MultisigMetadata`, `algosdk.makeMultiSigAccountTransactionSigner`, `algosdk.multisigAddress`, `algosdk.signMultisigTransaction` and `algosdk.appendSignMultisigTransaction` that supports multisig accounts with one or more signers present ### Dispenser -- [`algorand.account.dispenserFromEnvironment()`]() - Returns an account (with private key loaded) that can act as a dispenser from environment variables, or against default LocalNet if no environment variables present -- [`algorand.account.localNetDispenser()`]() - Returns an account with private key loaded that can act as a dispenser for the default LocalNet dispenser account +- [`dispenser_from_environment`](../autoapi/algokit_utils/accounts/account_manager/index.md#algokit_utils.accounts.account_manager.AccountManager.dispenser_from_environment) - Returns an account (with private key loaded) that can act as a dispenser from environment variables, or against default LocalNet if no environment variables present +- [`localnet_dispenser`](../autoapi/algokit_utils/accounts/account_manager/index.md#algokit_utils.accounts.account_manager.AccountManager.localnet_dispenser) - Returns an account with private key loaded that can act as a dispenser for the default LocalNet dispenser account ## Rekey account @@ -177,9 +177,9 @@ kmd_account_manager = KmdAccountManager(client_manager) The methods that are available are: -- [`get_wallet_account(wallet_name, predicate?, sender?)`]()\` - Returns an Algorand signing account with private key loaded from the given KMD wallet (identified by name). -- [`get_or_create_wallet_account(name, fund_with?)`]()\` - Gets an account with private key loaded from a KMD wallet of the given name, or alternatively creates one with funds in it via a KMD wallet of the given name. -- [`get_localnet_dispenser_account()`]()\` - Returns an Algorand account with private key loaded for the default LocalNet dispenser account (that can be used to fund other accounts) +- [`get_wallet_account`](../autoapi/algokit_utils/accounts/kmd_account_manager/index.md#algokit_utils.accounts.kmd_account_manager.KmdAccountManager.get_wallet_account) - Returns an Algorand signing account with private key loaded from the given KMD wallet (identified by name). +- [`get_or_create_wallet_account`](../autoapi/algokit_utils/accounts/kmd_account_manager/index.md#algokit_utils.accounts.kmd_account_manager.KmdAccountManager.get_or_create_wallet_account) - Gets an account with private key loaded from a KMD wallet of the given name, or alternatively creates one with funds in it via a KMD wallet of the given name. +- [`get_localnet_dispenser_account`](../autoapi/algokit_utils/accounts/kmd_account_manager/index.md#algokit_utils.accounts.kmd_account_manager.KmdAccountManager.get_localnet_dispenser_account) - Returns an Algorand account with private key loaded for the default LocalNet dispenser account (that can be used to fund other accounts) ```python # Get a wallet account that seeded the LocalNet network diff --git a/docs/markdown/capabilities/app-client.md b/docs/markdown/capabilities/app-client.md index 5953e5d2..886a5c70 100644 --- a/docs/markdown/capabilities/app-client.md +++ b/docs/markdown/capabilities/app-client.md @@ -116,7 +116,7 @@ Once you have an app factory you can perform the following actions: - `factory.send.bare.create(...)` - Signs and sends a transaction to create an app and returns the result of that call and an `AppClient` instance for the created app - `factory.deploy(...)` - Uses the creator address and app name pattern to find if the app has already been deployed or not and either creates, updates or replaces that app based on the deployment rules (i.e. it’s an idempotent deployment) and returns the result of the deployment and an `AppClient` instance for the created/updated/existing app. -> See [API docs]() for details on parameter signatures. +> See `API docs` for details on parameter signatures. ### Create @@ -124,7 +124,7 @@ The create method is a wrapper over the `app_create` (bare calls) and `app_creat - You don’t need to specify the `approval_program`, `clear_state_program`, or `schema` because these are all specified or calculated from the app spec - `sender` is optional and if not specified then the `default_sender` from the `AppFactory` constructor is used -- `deploy_time_params`, `updatable` and `deletable` can be passed in to control deploy-time parameter replacements and deploy-time immutability and permanence control. Note these are consolidated under the `compilation_params` `TypedDict`, see [API docs]() for details. +- `deploy_time_params`, `updatable` and `deletable` can be passed in to control deploy-time parameter replacements and deploy-time immutability and permanence control. Note these are consolidated under the `compilation_params` `TypedDict`, see `API docs` for details. ```python # Use no-argument bare-call @@ -275,13 +275,13 @@ map_dict = app_client.state.global_state.get_map("myMap") There are various methods defined that let you read state from the smart contract app: -- `get_global_state()` - Gets the current global state using [`algorand.app.get_global_state`]() -- `get_local_state(address: str)` - Gets the current local state for the given account address using [`algorand.app.get_local_state`](). -- `get_box_names()` - Gets the current box names using [`algorand.app.get_box_names`](). -- `get_box_value(name)` - Gets the current value of the given box using [`algorand.app.get_box_value`](). -- `get_box_value_from_abi_type(name)` - Gets the current value of the given box from an ABI type using [`algorand.app.get_box_value_from_abi_type`](). -- `get_box_values(filter)` - Gets the current values of the boxes using [`algorand.app.get_box_values`](). -- `get_box_values_from_abi_type(type, filter)` - Gets the current values of the boxes from an ABI type using [`algorand.app.get_box_values_from_abi_type`](). +- `get_global_state()` - Gets the current global state using `algorand.app.get_global_state`. +- `get_local_state(address: str)` - Gets the current local state for the given account address using `algorand.app.get_local_state`. +- `get_box_names()` - Gets the current box names using `algorand.app.get_box_names`. +- `get_box_value(name)` - Gets the current value of the given box using `algorand.app.get_box_value`. +- `get_box_value_from_abi_type(name)` - Gets the current value of the given box from an ABI type using `algorand.app.get_box_value_from_abi_type`. +- `get_box_values(filter)` - Gets the current values of the boxes using `algorand.app.get_box_values`. +- `get_box_values_from_abi_type(type, filter)` - Gets the current values of the boxes from an ABI type using `algorand.app.get_box_values_from_abi_type`. ```python global_state = app_client.get_global_state() @@ -309,11 +309,11 @@ Often when calling a smart contract during development you will get logic errors When this occurs, you will generally get an error that looks something like: `TransactionPool.Remember: transaction {TRANSACTION_ID}: logic eval error: {ERROR_MESSAGE}. Details: pc={PROGRAM_COUNTER_VALUE}, opcodes={LIST_OF_OP_CODES}`. -The information in that error message can be parsed and when combined with the [source map from compilation]() you can expose debugging information that makes it much easier to understand what’s happening. The ARC-56 app spec, if provided, can also specify human-readable error messages against certain program counter values and further augment the error message. +The information in that error message can be parsed and when combined with the [source map from compilation](app-deploy.md#compilation-and-template-substitution) you can expose debugging information that makes it much easier to understand what’s happening. The ARC-56 app spec, if provided, can also specify human-readable error messages against certain program counter values and further augment the error message. The app client and app factory automatically provide this functionality for all smart contract calls. They also expose a function that can be used for any custom calls you manually construct and need to add into your own try/catch `expose_logic_error(e: Error, is_clear: bool = False)`. -When an error is thrown then the resulting error that is re-thrown will be a [`LogicError` object](), which has the following fields: +When an error is thrown then the resulting error that is re-thrown will be a [`LogicError`](../autoapi/algokit_utils/errors/logic_error/index.md#algokit_utils.errors.logic_error.LogicError), which has the following fields: - `logic_error: Exception` - The original logic error exception - `logic_error_str: str` - The string representation of the logic error diff --git a/docs/markdown/capabilities/app-deploy.md b/docs/markdown/capabilities/app-deploy.md index ff7cb202..3f8f60a8 100644 --- a/docs/markdown/capabilities/app-deploy.md +++ b/docs/markdown/capabilities/app-deploy.md @@ -41,7 +41,7 @@ This design allows you to have the same deployment code across environments with ## `AppDeployer` -The [`AppDeployer`]() is a class that is used to manage app deployments and deployment metadata. +The `AppDeployer` is a class that is used to manage app deployments and deployment metadata. To get an instance of `AppDeployer` you can use either [`AlgorandClient`](algorand-client.md) via `algorand.appDeployer` or instantiate it directly (passing in an [`AppManager`](app.md#appmanager), [`AlgorandClientTransactionSender`](algorand-client.md#sending-a-single-transaction) and optionally an indexer client instance): @@ -55,7 +55,7 @@ app_deployer = AppDeployer(app_manager, transaction_sender, indexer) When AlgoKit performs a deployment of an app it creates metadata to describe that deployment and includes this metadata in an [ARC-2](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md) transaction note on any creation and update transactions. -The deployment metadata is defined in [`AppDeployMetadata`](), which is an object with: +The deployment metadata is defined in `AppDeployMetadata`, which is an object with: - `name: str` - The unique name identifier of the app within the creator account - `version: str` - The version of app that is / will be deployed; can be an arbitrary string, but we recommend using [semver](https://semver.org/) @@ -79,7 +79,7 @@ app1_metadata = app_lookup.apps["app1"] This method caches the result of the lookup, since it’s a reasonably heavyweight call (N+1 indexer calls for N deployed apps by the creator). If you want to skip the cache to get a fresh version then you can pass in a second parameter `ignore_cache=True`. This should only be needed if you are performing parallel deployments outside of the current `AppDeployer` instance, since it will keep its cache updated based on its own deployments. -The return type of `get_creator_apps_by_name` is [`ApplicationLookup`](): +The return type of `get_creator_apps_by_name` is `ApplicationLookup`, which is an object with: ```python @dataclasses.dataclass @@ -88,13 +88,13 @@ class ApplicationLookup: apps: dict[str, ApplicationMetaData] = dataclasses.field(default_factory=dict) ``` -The `apps` property contains a lookup by app name that resolves to the current [`ApplicationMetaData`](). +The `apps` property contains a lookup by app name that resolves to the current `ApplicationMetaData`. -> Refer to the [API docs]() for latest information on exact types. +> Refer to the `ApplicationLookup` for latest information on exact types. ## Performing a deployment -In order to perform a deployment, AlgoKit provides the `algorand.app_deployer.deploy(deployment)` method. +In order to perform a deployment, AlgoKit provides the `deploy` method. For example: @@ -151,19 +151,19 @@ It will automatically [add metadata to the transaction note of the create or upd ### Input parameters -The first parameter `deployment` is an [`AppDeployParams`](), which is an object with: +The first parameter `deployment` is an `AppDeployParams`, which is an object with: - `metadata: AppDeployMetadata` - determines the [deployment metadata]() of the deployment - `create_params: AppCreateParams | CreateCallABI` - the parameters for an [app creation call](app.md#creation) (raw parameters or ABI method call) - `update_params: AppUpdateParams | UpdateCallABI` - the parameters for an [app update call](app.md#updating) (raw parameters or ABI method call) without the `app_id`, `approval_program`, or `clear_state_program` as these are handled by the deploy logic - `delete_params: AppDeleteParams | DeleteCallABI` - the parameters for an [app delete call](app.md#deleting) (raw parameters or ABI method call) without the `app_id` parameter - `deploy_time_params: TealTemplateParams | None` - optional parameters for [TEAL template substitution]() - - [`TealTemplateParams`]() is a dict that replaces `TMPL_{key}` with `value` (strings/Uint8Arrays are properly encoded) -- `on_schema_break: OnSchemaBreak | str | None` - determines [what happens]() if schema requirements increase (values: ‘replace’, ‘fail’, ‘append’) -- `on_update: OnUpdate | str | None` - determines [what happens]() if contract logic changes (values: ‘update’, ‘replace’, ‘fail’, ‘append’) + - `TealTemplateParams` is a dict that replaces `TMPL_{key}` with `value` (strings/Uint8Arrays are properly encoded) +- `on_schema_break: OnSchemaBreak | str | None` - determines `OnSchemaBreak` if schema requirements increase (values: ‘replace’, ‘fail’, ‘append’) +- `on_update: OnUpdate | str | None` - determines `OnUpdate` if contract logic changes (values: ‘update’, ‘replace’, ‘fail’, ‘append’) - `existing_deployments: ApplicationLookup | None` - optional pre-fetched app lookup data to skip indexer queries - `ignore_cache: bool | None` - if True, bypasses cached deployment metadata -- Additional fields from [`SendParams`]() - transaction execution parameters +- Additional fields from `SendParams` - transaction execution parameters ### Idempotency @@ -181,7 +181,7 @@ In order for a smart contract to opt-in to use this functionality, it must have If you are building a smart contract using the production [AlgoKit init templates](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/init.md) provide a reference implementation out of the box for the deploy-time immutability and permanence control. -If you passed in a TEAL template for the `approval_program` or `clear_state_program` (i.e. a `str` rather than a `bytes`) then `deploy` will return the [compilation result]() of substituting then compiling the TEAL template(s) in the following properties of the return value: +If you passed in a TEAL template for the `approval_program` or `clear_state_program` (i.e. a `str` rather than a `bytes`) then `deploy` will return the `CompiledTeal` of substituting then compiling the TEAL template(s) in the following properties of the return value: - `compiled_approval: CompiledTeal | None` - `compiled_clear: CompiledTeal | None` @@ -195,7 +195,7 @@ Template substitution is done by executing `algorand.app.compile_teal_template(t ### Return value -When `deploy` executes it will return a [comprehensive result]() object that describes exactly what it did and has comprehensive metadata to describe the end result of the deployed app. +When `deploy` executes it will return a `AppDeployResult` object that describes exactly what it did and has comprehensive metadata to describe the end result of the deployed app. The `deploy` call itself may do one of the following (which you can determine by looking at the `operation_performed` field on the return value from the function): diff --git a/docs/markdown/index.md b/docs/markdown/index.md index f3ecf895..2312d193 100644 --- a/docs/markdown/index.md +++ b/docs/markdown/index.md @@ -180,7 +180,7 @@ The AlgoKit Utils configuration singleton can be updated using `config.configure ## Logging -AlgoKit has an in-built logging abstraction through the [`AlgoKitLogger`]() class that provides standardized logging capabilities. The logger is accessible through the `config.logger` property and provides various logging levels. +AlgoKit has an in-built logging abstraction through the [`algokit_utils.config.AlgoKitLogger`](autoapi/algokit_utils/config/index.md#algokit_utils.config.AlgoKitLogger) class that provides standardized logging capabilities. The logger is accessible through the `config.logger` property and provides various logging levels. Each method supports optional suppression of output using the `suppress_log` parameter. @@ -195,7 +195,7 @@ config.configure(debug=True) To retrieve the current debug state you can use `debug` property. -This will turn on things like automatic tracing, more verbose logging and [advanced debugging](). It’s likely this option will result in extra HTTP calls to algod os worth being careful when it’s turned on. +This will turn on things like automatic tracing, more verbose logging and [advanced debugging](capabilities/debugging.md). It’s likely this option will result in extra HTTP calls to algod and it’s worth being careful when it’s turned on. @@ -222,4 +222,4 @@ The library helps you interact with and develop against the Algorand blockchain # Reference documentation -For detailed API documentation, see the [auto-generated reference documentation](). +For detailed API documentation, see the [`algokit_utils`](autoapi/algokit_utils/index.md#module-algokit_utils) diff --git a/docs/source/capabilities/account.md b/docs/source/capabilities/account.md index 25d87437..6b64bae4 100644 --- a/docs/source/capabilities/account.md +++ b/docs/source/capabilities/account.md @@ -4,7 +4,7 @@ Account management is one of the core capabilities provided by AlgoKit Utils. It ## `AccountManager` -The [`AccountManager`](../apidocs/algokit_utils/accounts/index) is a class that is used to get, create, and fund accounts and perform account-related actions such as funding. The `AccountManager` also keeps track of signers for each address so when using the [`TransactionComposer`](./transaction-composer.md) to send transactions, a signer function does not need to manually be specified for each transaction - instead it can be inferred from the sender address automatically! +The {py:obj}`AccountManager ` is a class that is used to get, create, and fund accounts and perform account-related actions such as funding. The `AccountManager` also keeps track of signers for each address so when using the [`TransactionComposer`](./transaction-composer.md) to send transactions, a signer function does not need to manually be specified for each transaction - instead it can be inferred from the sender address automatically! To get an instance of `AccountManager`, you can use either [`AlgorandClient`](./algorand-client.md) via `algorand.account` or instantiate it directly: @@ -16,14 +16,14 @@ account_manager = AccountManager(client_manager) ## `TransactionSignerAccountProtocol` -The core internal type that holds information about a signer/sender pair for a transaction is [`TransactionSignerAccountProtocol`](../apidocs/algokit_utils/protocols/account/index), which represents an `algosdk.transaction.TransactionSigner` (`signer`) along with a sender address (`address`) as the encoded string address. +The core internal type that holds information about a signer/sender pair for a transaction is {py:obj}`TransactionSignerAccountProtocol `, which represents an `algosdk.transaction.TransactionSigner` (`signer`) along with a sender address (`address`) as the encoded string address. The following conform to `TransactionSignerAccountProtocol`: -- [`TransactionSignerAccount`](../apidocs/algokit_utils/models/account/index) - a basic transaction signer account that holds an address and a signer conforming to `TransactionSignerAccountProtocol` -- [`SigningAccount`](../apidocs/algokit_utils/models/account/index) - an abstraction that used to be available under `Account` in previous versions of AlgoKit Utils. Renamed for consistency with equivalent `ts` version. Holds private key and conforms to `TransactionSignerAccountProtocol` -- [`LogicSigAccount`](../apidocs/algokit_utils/models/account/index) - a wrapper class around `algosdk` logicsig abstractions conforming to `TransactionSignerAccountProtocol` -- [`MultisigAccount`](../apidocs/algokit_utils/models/account/index) - a wrapper class around `algosdk` multisig abstractions conforming to `TransactionSignerAccountProtocol` +- {py:obj}`TransactionSignerAccount ` - a basic transaction signer account that holds an address and a signer conforming to `TransactionSignerAccountProtocol` +- {py:obj}`SigningAccount ` - an abstraction that used to be available under `Account` in previous versions of AlgoKit Utils. Renamed for consistency with equivalent `ts` version. Holds private key and conforms to `TransactionSignerAccountProtocol` +- {py:obj}`LogicSigAccount ` - a wrapper class around `algosdk` logicsig abstractions conforming to `TransactionSignerAccountProtocol` +- {py:obj}`MultisigAccount ` - a wrapper class around `algosdk` multisig abstractions conforming to `TransactionSignerAccountProtocol` ## Registering a signer @@ -61,7 +61,7 @@ algorand.account.set_default_signer(my_default_signer) ## Get a signer -[`AlgorandClient`](./algorand-client.md) will automatically retrieve a signer when signing a transaction, but if you need to get a `TransactionSigner` externally to do something more custom then you can [retrieve the signer](../apidocs/algokit_utils/accounts/account_manager/index#getsigner) for a given sender address: +[`AlgorandClient`](./algorand-client.md) will automatically retrieve a signer when signing a transaction, but if you need to get a `TransactionSigner` externally to do something more custom then you can {py:meth}`get_signer ` for a given sender address: ```python signer = algorand.account.get_signer("SENDER_ADDRESS") @@ -73,29 +73,29 @@ If there is no signer registered for that sender address it will either return t In order to get/register accounts for signing operations you can use the following methods on [`AccountManager`](#accountmanager) (expressed here as `algorand.account` to denote the syntax via an [`AlgorandClient`](./algorand-client.md)): -- [`algorand.account.from_environment(name, fund_with)`](../apidocs/algokit_utils/accounts/account_manager/index#from_environment) - Registers and returns an account with private key loaded by convention based on the given name identifier - either by idempotently creating the account in KMD or from environment variable via `process.env['{NAME}_MNEMONIC']` and (optionally) `process.env['{NAME}_SENDER']` (if account is rekeyed) +- {py:meth}`from_environment ` - Registers and returns an account with private key loaded by convention based on the given name identifier - either by idempotently creating the account in KMD or from environment variable via `process.env['{NAME}_MNEMONIC']` and (optionally) `process.env['{NAME}_SENDER']` (if account is rekeyed) - This allows you to have powerful code that will automatically create and fund an account by name locally and when deployed against TestNet/MainNet will automatically resolve from environment variables, without having to have different code - Note: `fund_with` allows you to control how many Algo are seeded into an account created in KMD -- [`algorand.account.from_mnemonic(mnemonic_secret, sender?)`](../apidocs/algokit_utils/accounts/account_manager/index#from_mnemonic) - Registers and returns an account with secret key loaded by taking the mnemonic secret -- [`algorand.account.multisig(multisig_params, signing_accounts)`](../apidocs/algokit_utils/accounts/account_manager/index#multisig) - Registers and returns a multisig account with one or more signing keys loaded -- [`algorand.account.rekeyed(sender, signer)`](../apidocs/algokit_utils/accounts/account_manager/index#rekeyed) - Registers and returns an account representing the given rekeyed sender/signer combination -- [`algorand.account.random()`](../apidocs/algokit_utils/accounts/account_manager/index#random) - Returns a new, cryptographically randomly generated account with private key loaded -- [`algorand.account.from_kmd()`](../apidocs/algokit_utils/accounts/account_manager/index#from_kmd) - Returns an account with private key loaded from the given KMD wallet (identified by name) -- [`algorand.account.logicsig(program, args?)`](../apidocs/algokit_utils/accounts/account_manager/index#logicsig) - Returns an account that represents a logic signature +- {py:meth}`from_mnemonic ` - Registers and returns an account with secret key loaded by taking the mnemonic secret +- {py:meth}`multisig ` - Registers and returns a multisig account with one or more signing keys loaded +- {py:meth}`rekeyed ` - Registers and returns an account representing the given rekeyed sender/signer combination +- {py:meth}`random ` - Returns a new, cryptographically randomly generated account with private key loaded +- {py:meth}`from_kmd ` - Returns an account with private key loaded from the given KMD wallet (identified by name) +- {py:meth}`logicsig ` - Returns an account that represents a logic signature ### Underlying account classes While `TransactionSignerAccount` is the main class used to represent an account that can sign, there are underlying account classes that can underpin the signer within the transaction signer account. -- `Account` - An in-built `algosdk.Account` object that has an address and private signing key, this can be created -- [`SigningAccount`](../code/classes/types_account.SigningAccount.md) - An abstraction around `algosdk.Account` that supports rekeyed accounts -- `LogicSigAccount` - An in-built algosdk `algosdk.LogicSigAccount` object -- [`MultisigAccount`](../code/classes/types_account.MultisigAccount.md) - An abstraction around `algosdk.MultisigMetadata`, `algosdk.makeMultiSigAccountTransactionSigner`, `algosdk.multisigAddress`, `algosdk.signMultisigTransaction` and `algosdk.appendSignMultisigTransaction` that supports multisig accounts with one or more signers present +- {py:obj}`TransactionSignerAccount ` - A default class conforming to `TransactionSignerAccountProtocol` that holds an address and a signer +- {py:obj}`SigningAccount ` - An abstraction around `algosdk.Account` that supports rekeyed accounts +- {py:obj}`LogicSigAccount ` - An in-built algosdk `algosdk.LogicSigAccount` object +- {py:obj}`MultisigAccount ` - An abstraction around `algosdk.MultisigMetadata`, `algosdk.makeMultiSigAccountTransactionSigner`, `algosdk.multisigAddress`, `algosdk.signMultisigTransaction` and `algosdk.appendSignMultisigTransaction` that supports multisig accounts with one or more signers present ### Dispenser -- [`algorand.account.dispenserFromEnvironment()`](../code/classes/types_account_manager.AccountManager.md#dispenserfromenvironment) - Returns an account (with private key loaded) that can act as a dispenser from environment variables, or against default LocalNet if no environment variables present -- [`algorand.account.localNetDispenser()`](../code/classes/types_account_manager.AccountManager.md#localnetdispenser) - Returns an account with private key loaded that can act as a dispenser for the default LocalNet dispenser account +- {py:meth}`dispenser_from_environment ` - Returns an account (with private key loaded) that can act as a dispenser from environment variables, or against default LocalNet if no environment variables present +- {py:meth}`localnet_dispenser ` - Returns an account with private key loaded that can act as a dispenser for the default LocalNet dispenser account ## Rekey account @@ -177,9 +177,9 @@ kmd_account_manager = KmdAccountManager(client_manager) The methods that are available are: -- [`get_wallet_account(wallet_name, predicate?, sender?)`](../apidocs/algokit_utils/accounts/kmd_account_manager/index#get_wallet_account)` - Returns an Algorand signing account with private key loaded from the given KMD wallet (identified by name). -- [`get_or_create_wallet_account(name, fund_with?)`](../apidocs/algokit_utils/accounts/kmd_account_manager/index#get_or_create_wallet_account)` - Gets an account with private key loaded from a KMD wallet of the given name, or alternatively creates one with funds in it via a KMD wallet of the given name. -- [`get_localnet_dispenser_account()`](../apidocs/algokit_utils/accounts/kmd_account_manager/index#get_localnet_dispenser_account)` - Returns an Algorand account with private key loaded for the default LocalNet dispenser account (that can be used to fund other accounts) +- {py:meth}`get_wallet_account ` - Returns an Algorand signing account with private key loaded from the given KMD wallet (identified by name). +- {py:meth}`get_or_create_wallet_account ` - Gets an account with private key loaded from a KMD wallet of the given name, or alternatively creates one with funds in it via a KMD wallet of the given name. +- {py:meth}`get_localnet_dispenser_account ` - Returns an Algorand account with private key loaded for the default LocalNet dispenser account (that can be used to fund other accounts) ```python # Get a wallet account that seeded the LocalNet network diff --git a/docs/source/capabilities/app-client.md b/docs/source/capabilities/app-client.md index 71357a46..0b257d34 100644 --- a/docs/source/capabilities/app-client.md +++ b/docs/source/capabilities/app-client.md @@ -116,7 +116,7 @@ Once you have an app factory you can perform the following actions: - `factory.send.bare.create(...)` - Signs and sends a transaction to create an app and returns the result of that call and an `AppClient` instance for the created app - `factory.deploy(...)` - Uses the creator address and app name pattern to find if the app has already been deployed or not and either creates, updates or replaces that app based on the deployment rules (i.e. it's an idempotent deployment) and returns the result of the deployment and an `AppClient` instance for the created/updated/existing app. -> See [API docs](../api/app-factory.md#deploy) for details on parameter signatures. +> See {py:func}`API docs ` for details on parameter signatures. ### Create @@ -124,7 +124,7 @@ The create method is a wrapper over the `app_create` (bare calls) and `app_creat - You don't need to specify the `approval_program`, `clear_state_program`, or `schema` because these are all specified or calculated from the app spec - `sender` is optional and if not specified then the `default_sender` from the `AppFactory` constructor is used -- `deploy_time_params`, `updatable` and `deletable` can be passed in to control deploy-time parameter replacements and deploy-time immutability and permanence control. Note these are consolidated under the `compilation_params` `TypedDict`, see [API docs](../api/app-factory.md#deploy) for details. +- `deploy_time_params`, `updatable` and `deletable` can be passed in to control deploy-time parameter replacements and deploy-time immutability and permanence control. Note these are consolidated under the `compilation_params` `TypedDict`, see {py:func}`API docs ` for details. ```python # Use no-argument bare-call @@ -275,13 +275,13 @@ map_dict = app_client.state.global_state.get_map("myMap") There are various methods defined that let you read state from the smart contract app: -- `get_global_state()` - Gets the current global state using [`algorand.app.get_global_state`](../api/app.md#get_global_state) -- `get_local_state(address: str)` - Gets the current local state for the given account address using [`algorand.app.get_local_state`](../api/app.md#get_local_state). -- `get_box_names()` - Gets the current box names using [`algorand.app.get_box_names`](../api/app.md#get_box_names). -- `get_box_value(name)` - Gets the current value of the given box using [`algorand.app.get_box_value`](../api/app.md#get_box_value). -- `get_box_value_from_abi_type(name)` - Gets the current value of the given box from an ABI type using [`algorand.app.get_box_value_from_abi_type`](../api/app.md#get_box_value_from_abi_type). -- `get_box_values(filter)` - Gets the current values of the boxes using [`algorand.app.get_box_values`](../api/app.md#get_box_values). -- `get_box_values_from_abi_type(type, filter)` - Gets the current values of the boxes from an ABI type using [`algorand.app.get_box_values_from_abi_type`](../api/app.md#get_box_values_from_abi_type). +- `get_global_state()` - Gets the current global state using {py:func}`algorand.app.get_global_state `. +- `get_local_state(address: str)` - Gets the current local state for the given account address using {py:func}`algorand.app.get_local_state `. +- `get_box_names()` - Gets the current box names using {py:func}`algorand.app.get_box_names `. +- `get_box_value(name)` - Gets the current value of the given box using {py:func}`algorand.app.get_box_value `. +- `get_box_value_from_abi_type(name)` - Gets the current value of the given box from an ABI type using {py:func}`algorand.app.get_box_value_from_abi_type `. +- `get_box_values(filter)` - Gets the current values of the boxes using {py:func}`algorand.app.get_box_values `. +- `get_box_values_from_abi_type(type, filter)` - Gets the current values of the boxes from an ABI type using {py:func}`algorand.app.get_box_values_from_abi_type `. ```python global_state = app_client.get_global_state() @@ -309,11 +309,11 @@ Often when calling a smart contract during development you will get logic errors When this occurs, you will generally get an error that looks something like: `TransactionPool.Remember: transaction {TRANSACTION_ID}: logic eval error: {ERROR_MESSAGE}. Details: pc={PROGRAM_COUNTER_VALUE}, opcodes={LIST_OF_OP_CODES}`. -The information in that error message can be parsed and when combined with the [source map from compilation](../api/app-deploy.md#compilation-and-template-substitution) you can expose debugging information that makes it much easier to understand what's happening. The ARC-56 app spec, if provided, can also specify human-readable error messages against certain program counter values and further augment the error message. +The information in that error message can be parsed and when combined with the [source map from compilation](./app-deploy.md#compilation-and-template-substitution) you can expose debugging information that makes it much easier to understand what's happening. The ARC-56 app spec, if provided, can also specify human-readable error messages against certain program counter values and further augment the error message. The app client and app factory automatically provide this functionality for all smart contract calls. They also expose a function that can be used for any custom calls you manually construct and need to add into your own try/catch `expose_logic_error(e: Error, is_clear: bool = False)`. -When an error is thrown then the resulting error that is re-thrown will be a [`LogicError` object](todo_paste_url), which has the following fields: +When an error is thrown then the resulting error that is re-thrown will be a {py:obj}`LogicError `, which has the following fields: - `logic_error: Exception` - The original logic error exception - `logic_error_str: str` - The string representation of the logic error diff --git a/docs/source/capabilities/app-deploy.md b/docs/source/capabilities/app-deploy.md index 82fc97f4..f29bda6d 100644 --- a/docs/source/capabilities/app-deploy.md +++ b/docs/source/capabilities/app-deploy.md @@ -41,7 +41,7 @@ This design allows you to have the same deployment code across environments with ## `AppDeployer` -The [`AppDeployer`](../apidocs/algokit_utils/algokit_utils.md#appdeployer) is a class that is used to manage app deployments and deployment metadata. +The {py:obj}`AppDeployer ` is a class that is used to manage app deployments and deployment metadata. To get an instance of `AppDeployer` you can use either [`AlgorandClient`](./algorand-client.md) via `algorand.appDeployer` or instantiate it directly (passing in an [`AppManager`](./app.md#appmanager), [`AlgorandClientTransactionSender`](./algorand-client.md#sending-a-single-transaction) and optionally an indexer client instance): @@ -55,7 +55,7 @@ app_deployer = AppDeployer(app_manager, transaction_sender, indexer) When AlgoKit performs a deployment of an app it creates metadata to describe that deployment and includes this metadata in an [ARC-2](https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md) transaction note on any creation and update transactions. -The deployment metadata is defined in [`AppDeployMetadata`](../apidocs/algokit_utils/algokit_utils.md#appdeploymetadata), which is an object with: +The deployment metadata is defined in {py:obj}`AppDeployMetadata `, which is an object with: - `name: str` - The unique name identifier of the app within the creator account - `version: str` - The version of app that is / will be deployed; can be an arbitrary string, but we recommend using [semver](https://semver.org/) @@ -79,7 +79,7 @@ app1_metadata = app_lookup.apps["app1"] This method caches the result of the lookup, since it's a reasonably heavyweight call (N+1 indexer calls for N deployed apps by the creator). If you want to skip the cache to get a fresh version then you can pass in a second parameter `ignore_cache=True`. This should only be needed if you are performing parallel deployments outside of the current `AppDeployer` instance, since it will keep its cache updated based on its own deployments. -The return type of `get_creator_apps_by_name` is [`ApplicationLookup`](../apidocs/algokit_utils/algokit_utils.md#applicationlookup): +The return type of `get_creator_apps_by_name` is {py:obj}`ApplicationLookup `, which is an object with: ```python @dataclasses.dataclass @@ -88,13 +88,13 @@ class ApplicationLookup: apps: dict[str, ApplicationMetaData] = dataclasses.field(default_factory=dict) ``` -The `apps` property contains a lookup by app name that resolves to the current [`ApplicationMetaData`](../apidocs/algokit_utils/algokit_utils.md#applicationmetadata). +The `apps` property contains a lookup by app name that resolves to the current {py:obj}`ApplicationMetaData `. -> Refer to the [API docs](../apidocs/algokit_utils/algokit_utils.md#applicationlookup) for latest information on exact types. +> Refer to the {py:obj}`ApplicationLookup ` for latest information on exact types. ## Performing a deployment -In order to perform a deployment, AlgoKit provides the `algorand.app_deployer.deploy(deployment)` method. +In order to perform a deployment, AlgoKit provides the {py:meth}`deploy ` method. For example: @@ -151,19 +151,19 @@ It will automatically [add metadata to the transaction note of the create or upd ### Input parameters -The first parameter `deployment` is an [`AppDeployParams`](../apidocs/algokit_utils/algokit_utils.md#appdeployparams), which is an object with: +The first parameter `deployment` is an {py:obj}`AppDeployParams `, which is an object with: - `metadata: AppDeployMetadata` - determines the [deployment metadata](#deployment-metadata) of the deployment - `create_params: AppCreateParams | CreateCallABI` - the parameters for an [app creation call](./app.md#creation) (raw parameters or ABI method call) - `update_params: AppUpdateParams | UpdateCallABI` - the parameters for an [app update call](./app.md#updating) (raw parameters or ABI method call) without the `app_id`, `approval_program`, or `clear_state_program` as these are handled by the deploy logic - `delete_params: AppDeleteParams | DeleteCallABI` - the parameters for an [app delete call](./app.md#deleting) (raw parameters or ABI method call) without the `app_id` parameter - `deploy_time_params: TealTemplateParams | None` - optional parameters for [TEAL template substitution](#compilation-and-template-substitution) - - [`TealTemplateParams`](../apidocs/algokit_utils/algokit_utils.md#tealtemplateparams) is a dict that replaces `TMPL_{key}` with `value` (strings/Uint8Arrays are properly encoded) -- `on_schema_break: OnSchemaBreak | str | None` - determines [what happens](../apidocs/algokit_utils/algokit_utils.md#onschemabreak) if schema requirements increase (values: 'replace', 'fail', 'append') -- `on_update: OnUpdate | str | None` - determines [what happens](../apidocs/algokit_utils/algokit_utils.md#onupdate) if contract logic changes (values: 'update', 'replace', 'fail', 'append') + - {py:obj}`TealTemplateParams ` is a dict that replaces `TMPL_{key}` with `value` (strings/Uint8Arrays are properly encoded) +- `on_schema_break: OnSchemaBreak | str | None` - determines {py:obj}`OnSchemaBreak ` if schema requirements increase (values: 'replace', 'fail', 'append') +- `on_update: OnUpdate | str | None` - determines {py:obj}`OnUpdate ` if contract logic changes (values: 'update', 'replace', 'fail', 'append') - `existing_deployments: ApplicationLookup | None` - optional pre-fetched app lookup data to skip indexer queries - `ignore_cache: bool | None` - if True, bypasses cached deployment metadata -- Additional fields from [`SendParams`](../apidocs/algokit_utils/algokit_utils.md#sendparams) - transaction execution parameters +- Additional fields from {py:obj}`SendParams ` - transaction execution parameters ### Idempotency @@ -181,7 +181,7 @@ In order for a smart contract to opt-in to use this functionality, it must have If you are building a smart contract using the production [AlgoKit init templates](https://github.com/algorandfoundation/algokit-cli/blob/main/docs/features/init.md) provide a reference implementation out of the box for the deploy-time immutability and permanence control. -If you passed in a TEAL template for the `approval_program` or `clear_state_program` (i.e. a `str` rather than a `bytes`) then `deploy` will return the [compilation result](../apidocs/algokit_utils/algokit_utils.md#compiledteal) of substituting then compiling the TEAL template(s) in the following properties of the return value: +If you passed in a TEAL template for the `approval_program` or `clear_state_program` (i.e. a `str` rather than a `bytes`) then `deploy` will return the {py:obj}`CompiledTeal ` of substituting then compiling the TEAL template(s) in the following properties of the return value: - `compiled_approval: CompiledTeal | None` - `compiled_clear: CompiledTeal | None` @@ -195,7 +195,7 @@ Template substitution is done by executing `algorand.app.compile_teal_template(t ### Return value -When `deploy` executes it will return a [comprehensive result](../apidocs/algokit_utils/algokit_utils.md#appdeployresult) object that describes exactly what it did and has comprehensive metadata to describe the end result of the deployed app. +When `deploy` executes it will return a {py:obj}`AppDeployResult ` object that describes exactly what it did and has comprehensive metadata to describe the end result of the deployed app. The `deploy` call itself may do one of the following (which you can determine by looking at the `operation_performed` field on the return value from the function): diff --git a/docs/source/index.md b/docs/source/index.md index 1cc22eff..f3378aed 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -110,7 +110,7 @@ The AlgoKit Utils configuration singleton can be updated using `config.configure ## Logging -AlgoKit has an in-built logging abstraction through the [`AlgoKitLogger`](apidocs/algokit_utils/config/index) class that provides standardized logging capabilities. The logger is accessible through the `config.logger` property and provides various logging levels. +AlgoKit has an in-built logging abstraction through the {py:obj}`algokit_utils.config.AlgoKitLogger` class that provides standardized logging capabilities. The logger is accessible through the `config.logger` property and provides various logging levels. Each method supports optional suppression of output using the `suppress_log` parameter. @@ -125,7 +125,7 @@ config.configure(debug=True) To retrieve the current debug state you can use `debug` property. -This will turn on things like automatic tracing, more verbose logging and [advanced debugging](capabilities/debugger). It's likely this option will result in extra HTTP calls to algod os worth being careful when it's turned on. +This will turn on things like automatic tracing, more verbose logging and [advanced debugging](capabilities/debugging). It's likely this option will result in extra HTTP calls to algod and it's worth being careful when it's turned on. (capabilities)= @@ -152,4 +152,4 @@ The library helps you interact with and develop against the Algorand blockchain # Reference documentation -For detailed API documentation, see the [auto-generated reference documentation](apidocs/algokit_utils/algokit_utils.md). +For detailed API documentation, see the {py:obj}`algokit_utils` diff --git a/src/algokit_utils/_legacy_v2/account.py b/src/algokit_utils/_legacy_v2/account.py index fa0bfa52..6ce3cab6 100644 --- a/src/algokit_utils/_legacy_v2/account.py +++ b/src/algokit_utils/_legacy_v2/account.py @@ -177,10 +177,10 @@ def get_account( For LocalNet environments, loads or creates an account from a KMD wallet named {name}. :example: - >>> # If you have a mnemonic secret loaded into `os.environ["ACCOUNT_MNEMONIC"]` then you can call: - >>> account = get_account('ACCOUNT', algod) - >>> # If that code runs against LocalNet then a wallet called 'ACCOUNT' will automatically be created - >>> # with an account that is automatically funded with 1000 (default) ALGOs from the default LocalNet dispenser. + >>> # If you have a mnemonic secret loaded into `os.environ["ACCOUNT_MNEMONIC"]` then you can call: + >>> account = get_account('ACCOUNT', algod) + >>> # If that code runs against LocalNet then a wallet called 'ACCOUNT' will automatically be created + >>> # with an account that is automatically funded with 1000 (default) ALGOs from the default LocalNet dispenser. :param client: The Algorand client to use :param name: The name identifier to use for loading/creating the account diff --git a/src/algokit_utils/accounts/account_manager.py b/src/algokit_utils/accounts/account_manager.py index 363da650..296e4b93 100644 --- a/src/algokit_utils/accounts/account_manager.py +++ b/src/algokit_utils/accounts/account_manager.py @@ -148,7 +148,7 @@ class AccountManager: :param client_manager: The ClientManager client to use for algod and kmd clients :example: - >>> account_manager = AccountManager(client_manager) + >>> account_manager = AccountManager(client_manager) """ def __init__(self, client_manager: ClientManager): @@ -172,11 +172,11 @@ def set_default_signer(self, signer: TransactionSigner | TransactionSignerAccoun :returns: The `AccountManager` so method calls can be chained :example: - >>> signer_account = account_manager.random() - >>> account_manager.set_default_signer(signer_account.signer) - >>> # When signing a transaction, if there is no signer registered for the sender - >>> # then the default signer will be used - >>> signer = account_manager.get_signer("{SENDERADDRESS}") + >>> signer_account = account_manager.random() + >>> account_manager.set_default_signer(signer_account.signer) + >>> # When signing a transaction, if there is no signer registered for the sender + >>> # then the default signer will be used + >>> signer = account_manager.get_signer("{SENDERADDRESS}") """ self._default_signer = signer if isinstance(signer, TransactionSigner) else signer.signer return self @@ -190,7 +190,7 @@ def set_signer(self, sender: str, signer: TransactionSigner) -> Self: :returns: The `AccountManager` instance for method chaining :example: - >>> account_manager.set_signer("SENDERADDRESS", transaction_signer) + >>> account_manager.set_signer("SENDERADDRESS", transaction_signer) """ self._accounts[sender] = TransactionSignerAccount(address=sender, signer=signer) return self @@ -221,11 +221,11 @@ def set_signer_from_account(self, account: TransactionSignerAccountProtocol) -> :returns: The `AccountManager` instance for method chaining :example: - >>> account_manager = AccountManager(client_manager) - >>> account_manager.set_signer_from_account(SigningAccount(private_key=algosdk.account.generate_account()[0])) - >>> account_manager.set_signer_from_account(LogicSigAccount(AlgosdkLogicSigAccount(program, args))) - >>> account_manager.set_signer_from_account(MultiSigAccount(multisig_params, [account1, account2])) - """ + >>> account_manager = AccountManager(client_manager) + >>> account_manager.set_signer_from_account(SigningAccount(private_key=algosdk.account.generate_account()[0])) + >>> account_manager.set_signer_from_account(LogicSigAccount(AlgosdkLogicSigAccount(program, args))) + >>> account_manager.set_signer_from_account(MultiSigAccount(multisig_params, [account1, account2])) + """ # noqa: E501 self._accounts[account.address] = account return self @@ -240,7 +240,7 @@ def get_signer(self, sender: str | TransactionSignerAccountProtocol) -> Transact :raises ValueError: If no signer is found and no default signer is set :example: - >>> signer = account_manager.get_signer("SENDERADDRESS") + >>> signer = account_manager.get_signer("SENDERADDRESS") """ signer = self._accounts.get(self._get_address(sender)) or self._default_signer if not signer: @@ -256,10 +256,10 @@ def get_account(self, sender: str) -> TransactionSignerAccountProtocol: :raises ValueError: If no account is found or if the account is not a regular account :example: - >>> sender = account_manager.random().address - >>> # ... - >>> # Returns the `TransactionSignerAccountProtocol` for `sender` that has previously been registered - >>> account = account_manager.get_account(sender) + >>> sender = account_manager.random().address + >>> # ... + >>> # Returns the `TransactionSignerAccountProtocol` for `sender` that has previously been registered + >>> account = account_manager.get_account(sender) """ account = self._accounts.get(sender) if not account: @@ -279,8 +279,8 @@ def get_information(self, sender: str | TransactionSignerAccountProtocol) -> Acc :returns: The account information :example: - >>> address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA" - >>> account_info = account_manager.get_information(address) + >>> address = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA" + >>> account_info = account_manager.get_information(address) """ info = self._client_manager.algod.account_info(self._get_address(sender)) assert isinstance(info, dict) @@ -342,7 +342,7 @@ def from_mnemonic(self, *, mnemonic: str, sender: str | None = None) -> SigningA from the environment (ideally via a secret storage service) rather than the file system. :example: - >>> account = account_manager.from_mnemonic("mnemonic secret ...") + >>> account = account_manager.from_mnemonic("mnemonic secret ...") """ return self._register_account(to_private_key(mnemonic), sender) @@ -355,7 +355,7 @@ def from_environment(self, name: str, fund_with: AlgoAmount | None = None) -> Si :param name: The name identifier of the account :param fund_with: Optional amount to fund the account with when it gets created - (when targeting LocalNet) + (when targeting LocalNet) :returns: The account :raises ValueError: If environment variable {NAME}_MNEMONIC is missing when looking for account {NAME} @@ -368,10 +368,10 @@ def from_environment(self, name: str, fund_with: AlgoAmount | None = None) -> Si it will create it and fund the account for you :example: - >>> # If you have a mnemonic secret loaded into `MY_ACCOUNT_MNEMONIC` then you can call: - >>> account = account_manager.from_environment('MY_ACCOUNT') - >>> # If that code runs against LocalNet then a wallet called `MY_ACCOUNT` will automatically be created - >>> # with an account that is automatically funded with the specified amount from the default LocalNet dispenser + >>> # If you have a mnemonic secret loaded into `MY_ACCOUNT_MNEMONIC` then you can call: + >>> account = account_manager.from_environment('MY_ACCOUNT') + >>> # If that code runs against LocalNet then a wallet called `MY_ACCOUNT` will automatically be created + >>> # with an account that is automatically funded with the specified amount from the LocalNet dispenser """ account_mnemonic = os.getenv(f"{name.upper()}_MNEMONIC") @@ -398,10 +398,10 @@ def from_kmd( :raises ValueError: If unable to find KMD account with given name and predicate :example: - >>> # Get default funded account in a LocalNet: - >>> defaultDispenserAccount = account.from_kmd('unencrypted-default-wallet', - ... lambda a: a.status != 'Offline' and a.amount > 1_000_000_000 - ... ) + >>> # Get default funded account in a LocalNet: + >>> defaultDispenserAccount = account.from_kmd('unencrypted-default-wallet', + ... lambda a: a.status != 'Offline' and a.amount > 1_000_000_000 + ... ) """ kmd_account = self._kmd_account_manager.get_wallet_account(name, predicate, sender) if not kmd_account: @@ -418,7 +418,7 @@ def logicsig(self, program: bytes, args: list[bytes] | None = None) -> LogicSigA :returns: A logic signature account wrapper :example: - >>> account = account.logic_sig(program, [new Uint8Array(3, ...)]) + >>> account = account.logic_sig(program, [new Uint8Array(3, ...)]) """ return self._register_logicsig(program, args) @@ -431,12 +431,12 @@ def multisig(self, metadata: MultisigMetadata, signing_accounts: list[SigningAcc :returns: A multisig account wrapper :example: - >>> account = account_manager.multi_sig( - ... version=1, - ... threshold=1, - ... addrs=["ADDRESS1...", "ADDRESS2..."], - ... signing_accounts=[account1, account2] - ... ) + >>> account = account_manager.multi_sig( + ... version=1, + ... threshold=1, + ... addrs=["ADDRESS1...", "ADDRESS2..."], + ... signing_accounts=[account1, account2] + ... ) """ return self._register_multisig(metadata, signing_accounts) @@ -447,7 +447,7 @@ def random(self) -> SigningAccount: :returns: The account :example: - >>> account = account_manager.random() + >>> account = account_manager.random() """ private_key, _ = algosdk.account.generate_account() return self._register_account(private_key) @@ -461,7 +461,7 @@ def localnet_dispenser(self) -> SigningAccount: :returns: The account :example: - >>> account = account_manager.localnet_dispenser() + >>> account = account_manager.localnet_dispenser() """ kmd_account = self._kmd_account_manager.get_localnet_dispenser_account() return self._register_account(kmd_account.private_key) @@ -475,7 +475,7 @@ def dispenser_from_environment(self) -> SigningAccount: :returns: The account :example: - >>> account = account_manager.dispenser_from_environment() + >>> account = account_manager.dispenser_from_environment() """ name = os.getenv(f"{DISPENSER_ACCOUNT_NAME}_MNEMONIC") if name: @@ -493,8 +493,8 @@ def rekeyed( :returns: The rekeyed account :example: - >>> account = account.from_mnemonic("mnemonic secret ...") - >>> rekeyed_account = account_manager.rekeyed(account, "SENDERADDRESS...") + >>> account = account.from_mnemonic("mnemonic secret ...") + >>> rekeyed_account = account_manager.rekeyed(account, "SENDERADDRESS...") """ sender_address = sender.address if isinstance(sender, SigningAccount) else sender self._accounts[sender_address] = TransactionSignerAccount(address=sender_address, signer=account.signer) @@ -540,23 +540,23 @@ def rekey_account( # noqa: PLR0913 `official rekey guidance `_. :example: - >>> # Basic example (with string addresses): - >>> algorand.account.rekey_account({account: "ACCOUNTADDRESS", rekey_to: "NEWADDRESS"}) - >>> # Basic example (with signer accounts): - >>> algorand.account.rekey_account({account: account1, rekey_to: newSignerAccount}) - >>> # Advanced example: - >>> algorand.account.rekey_account({ - ... account: "ACCOUNTADDRESS", - ... rekey_to: "NEWADDRESS", - ... lease: 'lease', - ... note: 'note', - ... first_valid_round: 1000, - ... validity_window: 10, - ... extra_fee: AlgoAmount.from_micro_algo(1000), - ... static_fee: AlgoAmount.from_micro_algo(1000), - ... max_fee: AlgoAmount.from_micro_algo(3000), - ... suppress_log: True, - ... }) + >>> # Basic example (with string addresses): + >>> algorand.account.rekey_account({account: "ACCOUNTADDRESS", rekey_to: "NEWADDRESS"}) + >>> # Basic example (with signer accounts): + >>> algorand.account.rekey_account({account: account1, rekey_to: newSignerAccount}) + >>> # Advanced example: + >>> algorand.account.rekey_account({ + ... account: "ACCOUNTADDRESS", + ... rekey_to: "NEWADDRESS", + ... lease: 'lease', + ... note: 'note', + ... first_valid_round: 1000, + ... validity_window: 10, + ... extra_fee: AlgoAmount.from_micro_algo(1000), + ... static_fee: AlgoAmount.from_micro_algo(1000), + ... max_fee: AlgoAmount.from_micro_algo(3000), + ... suppress_log: True, + ... }) """ sender_address = self._get_address(account) rekey_address = self._get_address(rekey_to) @@ -623,7 +623,7 @@ def ensure_funded( # noqa: PLR0913 :param account_to_fund: The account to fund :param dispenser_account: The account to use as a dispenser funding source :param min_spending_balance: The minimum balance of Algo that the account - should have available to spend + should have available to spend :param min_funding_increment: Optional minimum funding increment :param send_params: Parameters for the send operation, defaults to None :param signer: Optional transaction signer @@ -637,20 +637,20 @@ def ensure_funded( # noqa: PLR0913 :param first_valid_round: Optional first valid round :param last_valid_round: Optional last valid round :returns: The result of executing the dispensing transaction and the `amountFunded` if funds were needed, - or None if no funds were needed + or None if no funds were needed :example: - >>> # Basic example: - >>> algorand.account.ensure_funded("ACCOUNTADDRESS", "DISPENSERADDRESS", algokit.algo(1)) - >>> # With configuration: - >>> algorand.account.ensure_funded( - ... "ACCOUNTADDRESS", - ... "DISPENSERADDRESS", - ... algokit.algo(1), - ... min_funding_increment=algokit.algo(2), - ... fee=AlgoAmount.from_micro_algo(1000), - ... suppress_log=True - ... ) + >>> # Basic example: + >>> algorand.account.ensure_funded("ACCOUNTADDRESS", "DISPENSERADDRESS", algokit.algo(1)) + >>> # With configuration: + >>> algorand.account.ensure_funded( + ... "ACCOUNTADDRESS", + ... "DISPENSERADDRESS", + ... algokit.algo(1), + ... min_funding_increment=algokit.algo(2), + ... fee=AlgoAmount.from_micro_algo(1000), + ... suppress_log=True + ... ) """ account_to_fund = self._get_address(account_to_fund) dispenser_account = self._get_address(dispenser_account) @@ -724,7 +724,7 @@ def ensure_funded_from_environment( # noqa: PLR0913 :param account_to_fund: The account to fund :param min_spending_balance: The minimum balance of Algo that the account should have available to - spend + spend :param min_funding_increment: Optional minimum funding increment :param send_params: Parameters for the send operation, defaults to None :param signer: Optional transaction signer @@ -738,7 +738,7 @@ def ensure_funded_from_environment( # noqa: PLR0913 :param first_valid_round: Optional first valid round :param last_valid_round: Optional last valid round :returns: The result of executing the dispensing transaction and the `amountFunded` if funds were needed, or - None if no funds were needed + None if no funds were needed .. note:: The dispenser account is retrieved from the account mnemonic stored in @@ -746,16 +746,16 @@ def ensure_funded_from_environment( # noqa: PLR0913 if it's a rekeyed account, or against default LocalNet if no environment variables present. :example: - >>> # Basic example: - >>> algorand.account.ensure_funded_from_environment("ACCOUNTADDRESS", algokit.algo(1)) - >>> # With configuration: - >>> algorand.account.ensure_funded_from_environment( - ... "ACCOUNTADDRESS", - ... algokit.algo(1), - ... min_funding_increment=algokit.algo(2), - ... fee=AlgoAmount.from_micro_algo(1000), - ... suppress_log=True - ... ) + >>> # Basic example: + >>> algorand.account.ensure_funded_from_environment("ACCOUNTADDRESS", algokit.algo(1)) + >>> # With configuration: + >>> algorand.account.ensure_funded_from_environment( + ... "ACCOUNTADDRESS", + ... algokit.algo(1), + ... min_funding_increment=algokit.algo(2), + ... fee=AlgoAmount.from_micro_algo(1000), + ... suppress_log=True + ... ) """ account_to_fund = self._get_address(account_to_fund) dispenser_account = self.dispenser_from_environment() @@ -818,26 +818,26 @@ def ensure_funded_from_testnet_dispenser_api( :param account_to_fund: The account to fund :param dispenser_client: The TestNet dispenser funding client :param min_spending_balance: The minimum balance of Algo that the account should have - available to spend + available to spend :param min_funding_increment: Optional minimum funding increment :returns: The result of executing the dispensing transaction and the `amountFunded` if funds were needed, or - None if no funds were needed + None if no funds were needed :raises ValueError: If attempting to fund on non-TestNet network :example: - >>> # Basic example: - >>> algorand.account.ensure_funded_from_testnet_dispenser_api( - ... "ACCOUNTADDRESS", - ... algorand.client.get_testnet_dispenser_from_environment(), - ... algokit.algo(1) - ... ) - >>> # With configuration: - >>> algorand.account.ensure_funded_from_testnet_dispenser_api( - ... "ACCOUNTADDRESS", - ... algorand.client.get_testnet_dispenser_from_environment(), - ... algokit.algo(1), - ... min_funding_increment=algokit.algo(2) - ... ) + >>> # Basic example: + >>> algorand.account.ensure_funded_from_testnet_dispenser_api( + ... "ACCOUNTADDRESS", + ... algorand.client.get_testnet_dispenser_from_environment(), + ... algokit.algo(1) + ... ) + >>> # With configuration: + >>> algorand.account.ensure_funded_from_testnet_dispenser_api( + ... "ACCOUNTADDRESS", + ... algorand.client.get_testnet_dispenser_from_environment(), + ... algokit.algo(1), + ... min_funding_increment=algokit.algo(2) + ... ) """ account_to_fund = self._get_address(account_to_fund) diff --git a/src/algokit_utils/assets/asset_manager.py b/src/algokit_utils/assets/asset_manager.py index 571b748b..0f48aebe 100644 --- a/src/algokit_utils/assets/asset_manager.py +++ b/src/algokit_utils/assets/asset_manager.py @@ -43,13 +43,13 @@ class AssetInformation: :ivar decimals: The amount of decimal places the asset was created with :ivar default_frozen: Whether the asset was frozen by default for all accounts, defaults to None :ivar manager: The address of the optional account that can manage the configuration of the asset and destroy it, - defaults to None + defaults to None :ivar reserve: The address of the optional account that holds the reserve (uncirculated supply) units of the asset, - defaults to None + defaults to None :ivar freeze: The address of the optional account that can be used to freeze or unfreeze holdings of this asset, - defaults to None + defaults to None :ivar clawback: The address of the optional account that can clawback holdings of this asset from any account, - defaults to None + defaults to None :ivar unit_name: The optional name of the unit of this asset (e.g. ticker name), defaults to None :ivar unit_name_b64: The optional name of the unit of this asset as bytes, defaults to None :ivar asset_name: The optional name of the asset, defaults to None @@ -57,7 +57,7 @@ class AssetInformation: :ivar url: Optional URL where more information about the asset can be retrieved, defaults to None :ivar url_b64: Optional URL where more information about the asset can be retrieved as bytes, defaults to None :ivar metadata_hash: 32-byte hash of some metadata that is relevant to the asset and/or asset holders, - defaults to None + defaults to None """ asset_id: int diff --git a/src/algokit_utils/models/amount.py b/src/algokit_utils/models/amount.py index 11cd6d07..01017f6f 100644 --- a/src/algokit_utils/models/amount.py +++ b/src/algokit_utils/models/amount.py @@ -13,10 +13,10 @@ class AlgoAmount: """Wrapper class to ensure safe, explicit conversion between µAlgo, Algo and numbers. :example: - >>> amount = AlgoAmount(algo=1) - >>> amount = AlgoAmount.from_algo(1) - >>> amount = AlgoAmount(micro_algo=1_000_000) - >>> amount = AlgoAmount.from_micro_algo(1_000_000) + >>> amount = AlgoAmount(algo=1) + >>> amount = AlgoAmount.from_algo(1) + >>> amount = AlgoAmount(micro_algo=1_000_000) + >>> amount = AlgoAmount.from_micro_algo(1_000_000) """ @overload @@ -65,7 +65,7 @@ def from_algo(amount: int | Decimal) -> AlgoAmount: :returns: An AlgoAmount instance. :example: - >>> amount = AlgoAmount.from_algo(1) + >>> amount = AlgoAmount.from_algo(1) """ return AlgoAmount(algo=amount) @@ -77,7 +77,7 @@ def from_micro_algo(amount: int) -> AlgoAmount: :returns: An AlgoAmount instance. :example: - >>> amount = AlgoAmount.from_micro_algo(1_000_000) + >>> amount = AlgoAmount.from_micro_algo(1_000_000) """ return AlgoAmount(micro_algo=amount) diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index cf9c04f8..4dcd083e 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -109,7 +109,7 @@ class PaymentParams(_CommonTxnParams): :ivar receiver: The account that will receive the ALGO :ivar amount: Amount to send :ivar close_remainder_to: If given, close the sender account and send the remaining balance to this address, - defaults to None + defaults to None """ receiver: str @@ -301,9 +301,9 @@ class AppCreateParams(_CommonTxnParams): """Parameters for creating an application. :ivar approval_program: The program to execute for all OnCompletes other than ClearState as raw teal (string) - or compiled teal (bytes) + or compiled teal (bytes) :ivar clear_state_program: The program to execute for ClearState OnComplete as raw teal (string) - or compiled teal (bytes) + or compiled teal (bytes) :ivar schema: The state schema for the app. This is immutable, defaults to None :ivar on_complete: The OnComplete action (cannot be ClearState), defaults to None :ivar args: Application arguments, defaults to None @@ -332,9 +332,9 @@ class AppUpdateParams(_CommonTxnParams): :ivar app_id: ID of the application :ivar approval_program: The program to execute for all OnCompletes other than ClearState as raw teal (string) - or compiled teal (bytes) + or compiled teal (bytes) :ivar clear_state_program: The program to execute for ClearState OnComplete as raw teal (string) - or compiled teal (bytes) + or compiled teal (bytes) :ivar args: Application arguments, defaults to None :ivar account_references: Account references, defaults to None :ivar app_references: App references, defaults to None @@ -419,7 +419,7 @@ class AppCallMethodCallParams(_BaseAppMethodCall): :ivar app_id: ID of the application :ivar method: The ABI method to call :ivar args: Arguments to the ABI method, either an ABI value, transaction with explicit signer, - transaction, another method call, or None + transaction, another method call, or None :ivar on_complete: The OnComplete action (cannot be UpdateApplication or ClearState), defaults to None """ @@ -1326,7 +1326,7 @@ class TransactionComposer: :param algod: An instance of AlgodClient used to get suggested params and send transactions :param get_signer: A function that takes an address and returns a TransactionSigner for that address :param get_suggested_params: Optional function to get suggested transaction parameters, - defaults to using algod.suggested_params() + defaults to using algod.suggested_params() :param default_validity_window: Optional default validity window for transactions in rounds, defaults to 10 :param app_manager: Optional AppManager instance for compiling TEAL programs, defaults to None """