From 12a8e0e35b2a8e04c3909bfdcac37b4f9a1434e5 Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Thu, 23 Mar 2023 14:59:38 +0800 Subject: [PATCH 01/24] fix: expose additional imports --- src/algokit_utils/app.py | 1 + src/algokit_utils/application_client.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/algokit_utils/app.py b/src/algokit_utils/app.py index 0269ab64..2afd9bd5 100644 --- a/src/algokit_utils/app.py +++ b/src/algokit_utils/app.py @@ -20,6 +20,7 @@ "AppDeployMetaData", "AppMetaData", "AppLookup", + "TemplateValueDict", "get_creator_apps", "replace_template_variables", ] diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index 5edb896b..817f55db 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -70,6 +70,7 @@ "OperationPerformed", "Program", "TransactionResponse", + "execute_atc_with_logic_error", "get_app_id_from_tx_id", "get_next_version", "num_extra_program_pages", From cb4f11e895a0f49bc37d4d1be1681fa97f9d45cc Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Thu, 23 Mar 2023 21:41:53 +0800 Subject: [PATCH 02/24] feat: extract call parameters into their own datatypes --- src/algokit_utils/application_client.py | 403 +++++++++--------------- tests/test_app_client.py | 2 +- 2 files changed, 156 insertions(+), 249 deletions(-) diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index 817f55db..3223efb4 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -5,7 +5,7 @@ from collections.abc import Sequence from enum import Enum from math import ceil -from typing import Any, Literal, cast, overload +from typing import Any, Literal, TypedDict, cast, overload import algosdk from algosdk import transaction @@ -140,6 +140,66 @@ class ABITransactionResponse(TransactionResponse): abi_result: ABIResult +@dataclasses.dataclass(kw_only=True) +class CommonCallParameters: + signer: TransactionSigner | None = None + sender: str | None = None + suggested_params: transaction.SuggestedParams | None = None + note: bytes | str | None = None + lease: bytes | str | None = None + + +@dataclasses.dataclass(kw_only=True) +class OnCompleteCallParameters(CommonCallParameters): + on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC + + +@dataclasses.dataclass(kw_only=True) +class FullCallParameters(OnCompleteCallParameters): + accounts: list[str] | None = None + foreign_apps: list[int] | None = None + foreign_assets: list[int] | None = None + boxes: Sequence[tuple[int, bytes | bytearray | str | int]] | None = None + rekey_to: str | None = None + + +class CommonCallParametersDict(TypedDict, total=False): + signer: TransactionSigner + sender: str + suggested_params: transaction.SuggestedParams + note: bytes | str + lease: bytes | str + + +class OnCompleteCallParametersDict(TypedDict, CommonCallParametersDict, total=False): + on_complete: transaction.OnComplete + + +class FullCallParametersDict(TypedDict, OnCompleteCallParametersDict, total=False): + accounts: list[str] | None + foreign_apps: list[int] | None + foreign_assets: list[int] | None + boxes: Sequence[tuple[int, bytes | bytearray | str | int]] | None + rekey_to: str | None + + +def _convert_parameters(args: CommonCallParameters | CommonCallParametersDict | None) -> FullCallParameters: + if args is None: + return FullCallParameters() + _args = dataclasses.asdict(args) if isinstance(args, CommonCallParameters) else args + if not isinstance(_args, dict): + raise Exception(f"Unexpected parameters type: {args}") + return FullCallParameters(**_args) + + +def _add_lease_parameter( + parameters: CommonCallParameters | OnCompleteCallParameters, lease: bytes | None = None +) -> OnCompleteCallParameters: + copy = OnCompleteCallParameters(**dataclasses.asdict(parameters)) + copy.lease = lease + return copy + + class ApplicationClient: @overload def __init__( @@ -313,17 +373,16 @@ def create_metadata( ) return app_metadata + base_parameters = CommonCallParameters(note=app_spec_note.encode(), signer=signer, sender=sender) + def create_app() -> DeployResponse: assert self.existing_deployments method, args, lease = unpack_args(create_args) create_result = self.create( abi_method=method, args=args, - note=app_spec_note.encode(), - signer=signer, - sender=sender, + parameters=_add_lease_parameter(base_parameters, lease=lease), template_values=template_values, - lease=lease, ) logger.info(f"{name} ({version}) deployed successfully, with app id {self.app_id}.") assert create_result.confirmed_round is not None @@ -368,19 +427,14 @@ def create_and_delete_app() -> DeployResponse: atc, abi_method=create_method, args=c_args, - note=app_spec_note.encode(), - signer=signer, - sender=sender, + parameters=_add_lease_parameter(base_parameters, lease=create_lease), template_values=template_values, - lease=create_lease, ) self.compose_delete( atc, abi_method=delete_method, args=d_args, - signer=signer, - sender=sender, - lease=delete_lease, + parameters=_add_lease_parameter(base_parameters, lease=delete_lease), ) create_delete_result = self._execute_atc(atc) self._set_app_id_from_tx_id(create_delete_result.tx_ids[0]) @@ -401,11 +455,8 @@ def update_app() -> DeployResponse: update_result = self.update( abi_method=method, args=args, - note=app_spec_note.encode(), - signer=signer, - sender=sender, + parameters=_add_lease_parameter(base_parameters, lease=lease), template_values=template_values, - lease=lease, ) app_metadata = create_metadata( app.created_round, updated_round=update_result.confirmed_round, original_metadata=app.created_metadata @@ -490,14 +541,9 @@ def compose_create( abi_method: Method | str | bool | None = None, args: ABIArgsDict | None = None, *, - signer: TransactionSigner | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC, + parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, template_values: TemplateValueDict | None = None, extra_pages: int | None = None, - note: bytes | str | None = None, - lease: bytes | None = None, ) -> tuple[Program, Program]: """Adds a signed transaction with application id == 0 and the schema and source of client's app_spec to atc""" @@ -506,23 +552,20 @@ def compose_create( if extra_pages is None: extra_pages = num_extra_program_pages(approval_program.raw_binary, clear_program.raw_binary) - create_method = self._resolve_method(abi_method, args, on_complete, CallConfig.CREATE) + parameters = _convert_parameters(parameters) self._add_method_call( atc, app_id=0, - method=create_method, + abi_method=abi_method, abi_args=args, - signer=signer, - sender=sender, - suggested_params=suggested_params, - on_complete=on_complete, + parameters=parameters, + on_complete=parameters.on_complete, approval_program=approval_program.raw_binary, clear_program=clear_program.raw_binary, global_schema=self.app_spec.global_state_schema, local_schema=self.app_spec.local_state_schema, extra_pages=extra_pages, - note=note, - lease=lease, + call_config=CallConfig.CREATE, ) return approval_program, clear_program @@ -532,14 +575,9 @@ def create( abi_method: Method | str | bool | None = None, args: ABIArgsDict | None = None, *, - signer: TransactionSigner | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC, + parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, template_values: TemplateValueDict | None = None, extra_pages: int | None = None, - note: bytes | str | None = None, - lease: bytes | None = None, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with application id == 0 and the schema and source of client's app_spec""" @@ -549,14 +587,9 @@ def create( atc, abi_method, args, - signer=signer, - sender=sender, - suggested_params=suggested_params, - on_complete=on_complete, + parameters=parameters, template_values=template_values, extra_pages=extra_pages, - note=note, - lease=lease, ) create_result = self._execute_atc_tr(atc) self._set_app_id_from_tx_id(create_result.tx_id) @@ -568,31 +601,24 @@ def compose_update( abi_method: Method | str | bool | None = None, args: ABIArgsDict | None = None, *, - signer: TransactionSigner | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, + parameters: CommonCallParameters | CommonCallParametersDict | None = None, template_values: TemplateValueDict | None = None, - note: bytes | str | None = None, - lease: bytes | None = None, ) -> tuple[Program, Program]: """Adds a signed transaction with on_complete=UpdateApplication to atc""" self._load_reference_and_check_app_id() approval_program, clear_program = self._substitute_template_and_compile(template_values) - update_method = self._resolve_method(abi_method, args, transaction.OnComplete.UpdateApplicationOC) + _parameters = _convert_parameters(parameters) + create_method = self._resolve_method(abi_method, args, _parameters.on_complete, CallConfig.CREATE) self._add_method_call( atc=atc, - method=update_method, + abi_method=create_method, abi_args=args, - signer=signer, - sender=sender, - suggested_params=suggested_params, + parameters=_parameters, on_complete=transaction.OnComplete.UpdateApplicationOC, approval_program=approval_program.raw_binary, clear_program=clear_program.raw_binary, - note=note, - lease=lease, ) return approval_program, clear_program @@ -602,12 +628,8 @@ def update( abi_method: Method | str | bool | None = None, args: ABIArgsDict | None = None, *, - signer: TransactionSigner | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, + parameters: CommonCallParameters | CommonCallParametersDict | None = None, template_values: TemplateValueDict | None = None, - note: bytes | str | None = None, - lease: bytes | None = None, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with on_complete=UpdateApplication""" @@ -616,12 +638,8 @@ def update( atc, abi_method, args, - signer=signer, - sender=sender, - suggested_params=suggested_params, + parameters=parameters, template_values=template_values, - note=note, - lease=lease, ) return self._execute_atc_tr(atc) @@ -631,23 +649,17 @@ def compose_delete( abi_method: Method | str | bool | None = None, args: ABIArgsDict | None = None, *, - signer: TransactionSigner | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - lease: bytes | None = None, + parameters: CommonCallParameters | CommonCallParametersDict | None = None, ) -> None: """Adds a signed transaction with on_complete=DeleteApplication to atc""" delete_method = self._resolve_method(abi_method, args, on_complete=transaction.OnComplete.DeleteApplicationOC) - self.compose_call( + self._add_method_call( atc, delete_method, - args, - signer=signer, - sender=sender, - suggested_params=suggested_params, + abi_args=args, + parameters=_convert_parameters(parameters), on_complete=transaction.OnComplete.DeleteApplicationOC, - lease=lease, ) def delete( @@ -655,10 +667,7 @@ def delete( abi_method: Method | str | bool | None = None, args: ABIArgsDict | None = None, *, - signer: TransactionSigner | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - lease: bytes | None = None, + parameters: CommonCallParameters | CommonCallParametersDict | None = None, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with on_complete=DeleteApplication""" @@ -667,10 +676,7 @@ def delete( atc, abi_method, args, - signer=signer, - sender=sender, - suggested_params=suggested_params, - lease=lease, + parameters=parameters, ) return self._execute_atc_tr(atc) @@ -680,37 +686,23 @@ def compose_call( abi_method: Method | str | bool | None = None, args: ABIArgsDict | None = None, *, - signer: TransactionSigner | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC, - accounts: list[str] | None = None, - foreign_apps: list[int] | None = None, - foreign_assets: list[int] | None = None, - boxes: Sequence[tuple[int, bytes | bytearray | str | int]] | None = None, - note: bytes | str | None = None, - lease: bytes | None = None, - rekey_to: str | None = None, + parameters: FullCallParameters | FullCallParametersDict | None = None, ) -> None: """Adds a signed transaction with specified parameters to atc""" self._load_reference_and_check_app_id() - method = self._resolve_method(abi_method, args, on_complete) + _parameters = _convert_parameters(parameters) self._add_method_call( atc, - method=method, + abi_method=abi_method, abi_args=args, - signer=signer, - sender=sender, - suggested_params=suggested_params, - on_complete=on_complete, - accounts=accounts, - foreign_apps=foreign_apps, - foreign_assets=foreign_assets, - boxes=boxes, - note=note, - lease=lease, - rekey_to=rekey_to, + parameters=_parameters, + on_complete=_parameters.on_complete, + accounts=_parameters.accounts, + foreign_apps=_parameters.foreign_apps, + foreign_assets=_parameters.foreign_assets, + boxes=_parameters.boxes, + rekey_to=_parameters.rekey_to, ) @overload @@ -719,17 +711,7 @@ def call( abi_method: Method | str | Literal[True], args: ABIArgsDict | None = None, *, - signer: TransactionSigner | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC, - accounts: list[str] | None = None, - foreign_apps: list[int] | None = None, - foreign_assets: list[int] | None = None, - boxes: Sequence[tuple[int, bytes | bytearray | str | int]] | None = None, - note: bytes | str | None = None, - lease: bytes | None = None, - rekey_to: str | None = None, + parameters: FullCallParameters | FullCallParametersDict | None = None, ) -> ABITransactionResponse: ... @@ -739,17 +721,7 @@ def call( abi_method: Literal[False], args: ABIArgsDict | None = None, *, - signer: TransactionSigner | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC, - accounts: list[str] | None = None, - foreign_apps: list[int] | None = None, - foreign_assets: list[int] | None = None, - boxes: Sequence[tuple[int, bytes | bytearray | str | int]] | None = None, - note: bytes | str | None = None, - lease: bytes | None = None, - rekey_to: str | None = None, + parameters: FullCallParameters | FullCallParametersDict | None = None, ) -> TransactionResponse: ... @@ -758,78 +730,56 @@ def call( abi_method: Method | str | bool | None = None, args: ABIArgsDict | None = None, *, - signer: TransactionSigner | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC, - accounts: list[str] | None = None, - foreign_apps: list[int] | None = None, - foreign_assets: list[int] | None = None, - boxes: Sequence[tuple[int, bytes | bytearray | str | int]] | None = None, - note: bytes | str | None = None, - lease: bytes | None = None, - rekey_to: str | None = None, + parameters: FullCallParameters | FullCallParametersDict | None = None, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with specified parameters""" - atc = AtomicTransactionComposer() - method = self._resolve_method(abi_method, args, on_complete) + _parameters = _convert_parameters(parameters) self.compose_call( atc, - abi_method=method, + abi_method=abi_method, args=args, - signer=signer, - sender=sender, - suggested_params=suggested_params, - on_complete=on_complete, - accounts=accounts, - foreign_apps=foreign_apps, - foreign_assets=foreign_assets, - boxes=boxes, - note=note, - lease=lease, - rekey_to=rekey_to, + parameters=_parameters, ) + method = self._resolve_method(abi_method, args, _parameters.on_complete) # If its a read-only method, use dryrun (TODO: swap with simulate later?) if method: - hints = self._method_hints(method) - if hints and hints.read_only: - dr_req = transaction.create_dryrun(self.algod_client, atc.gather_signatures()) # type: ignore[arg-type] - dr_result = self.algod_client.dryrun(dr_req) # type: ignore[arg-type] - for txn in dr_result["txns"]: - if "app-call-messages" in txn and "REJECT" in txn["app-call-messages"]: - msg = ", ".join(txn["app-call-messages"]) - raise Exception(f"Dryrun for readonly method failed: {msg}") - - method_results = _parse_result({0: method}, dr_result["txns"], atc.tx_ids) - return ABITransactionResponse(abi_result=method_results[0], tx_id=atc.tx_ids[0], confirmed_round=None) + response = self._dry_run_call(method, atc) + if response: + return response return self._execute_atc_tr(atc) + def _dry_run_call(self, method: Method, atc: AtomicTransactionComposer) -> ABITransactionResponse | None: + hints = self._method_hints(method) + if hints and hints.read_only: + dr_req = transaction.create_dryrun(self.algod_client, atc.gather_signatures()) # type: ignore[arg-type] + dr_result = self.algod_client.dryrun(dr_req) # type: ignore[arg-type] + for txn in dr_result["txns"]: + if "app-call-messages" in txn and "REJECT" in txn["app-call-messages"]: + msg = ", ".join(txn["app-call-messages"]) + raise Exception(f"Dryrun for readonly method failed: {msg}") + + method_results = _parse_result({0: method}, dr_result["txns"], atc.tx_ids) + return ABITransactionResponse(abi_result=method_results[0], tx_id=atc.tx_ids[0], confirmed_round=None) + return None + def compose_opt_in( self, atc: AtomicTransactionComposer, abi_method: Method | str | bool | None = None, args: ABIArgsDict | None = None, *, - signer: TransactionSigner | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - note: bytes | str | None = None, - lease: bytes | None = None, + parameters: CommonCallParameters | CommonCallParametersDict | None = None, ) -> None: """Adds a signed transaction with on_complete=OptIn to atc""" - self.compose_call( + self._add_method_call( atc, abi_method=abi_method, - args=args, - signer=signer, - sender=sender, - suggested_params=suggested_params, + abi_args=args, + parameters=_convert_parameters(parameters), on_complete=transaction.OnComplete.OptInOC, - note=note, - lease=lease, ) def opt_in( @@ -837,11 +787,7 @@ def opt_in( abi_method: Method | str | bool | None = None, args: ABIArgsDict | None = None, *, - signer: TransactionSigner | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - note: bytes | str | None = None, - lease: bytes | None = None, + parameters: CommonCallParameters | CommonCallParametersDict | None = None, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with on_complete=OptIn""" atc = AtomicTransactionComposer() @@ -849,11 +795,7 @@ def opt_in( atc, abi_method=abi_method, args=args, - signer=signer, - sender=sender, - suggested_params=suggested_params, - note=note, - lease=lease, + parameters=parameters, ) return self._execute_atc_tr(atc) @@ -863,23 +805,15 @@ def compose_close_out( abi_method: Method | str | bool | None = None, args: ABIArgsDict | None = None, *, - signer: TransactionSigner | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - note: bytes | str | None = None, - lease: bytes | None = None, + parameters: CommonCallParameters | CommonCallParametersDict | None = None, ) -> None: """Adds a signed transaction with on_complete=CloseOut to ac""" - return self.compose_call( + self._add_method_call( atc, abi_method=abi_method, - args=args, - signer=signer, - sender=sender, + abi_args=args, + parameters=_convert_parameters(parameters), on_complete=transaction.OnComplete.CloseOutOC, - suggested_params=suggested_params, - note=note, - lease=lease, ) def close_out( @@ -887,11 +821,7 @@ def close_out( abi_method: Method | str | bool | None = None, args: ABIArgsDict | None = None, *, - signer: TransactionSigner | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - note: bytes | str | None = None, - lease: bytes | None = None, + parameters: CommonCallParameters | CommonCallParametersDict | None = None, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with on_complete=CloseOut""" atc = AtomicTransactionComposer() @@ -899,11 +829,7 @@ def close_out( atc, abi_method=abi_method, args=args, - signer=signer, - sender=sender, - suggested_params=suggested_params, - note=note, - lease=lease, + parameters=parameters, ) return self._execute_atc_tr(atc) @@ -911,41 +837,25 @@ def compose_clear_state( self, atc: AtomicTransactionComposer, *, - signer: TransactionSigner | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - note: bytes | str | None = None, - lease: bytes | None = None, + parameters: CommonCallParameters | CommonCallParametersDict | None = None, ) -> None: """Adds a signed transaction with on_complete=ClearState to atc""" - return self.compose_call( + return self._add_method_call( atc, - signer=signer, - sender=sender, - suggested_params=suggested_params, + parameters=_convert_parameters(parameters), on_complete=transaction.OnComplete.ClearStateOC, - note=note, - lease=lease, ) def clear_state( self, *, - signer: TransactionSigner | None = None, - sender: str | None = None, - suggested_params: transaction.SuggestedParams | None = None, - note: bytes | str | None = None, - lease: bytes | None = None, + parameters: CommonCallParameters | CommonCallParametersDict | None = None, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with on_complete=ClearState""" atc = AtomicTransactionComposer() self.compose_clear_state( atc, - signer=signer, - sender=sender, - suggested_params=suggested_params, - note=note, - lease=lease, + parameters=parameters, ) return self._execute_atc_tr(atc) @@ -1049,11 +959,13 @@ def _resolve_method( elif len(matches) > 1: # ambiguous match signatures = ", ".join((m.get_signature() if isinstance(m, Method) else "bare") for m in matches) raise Exception( - f"Could not find an exact method to use for {on_complete} with call_config of {call_config}, " + f"Could not find an exact method to use for {on_complete.name} with call_config of {call_config.name}, " f"specify the exact method using abi_method and args parameters, considered: {signatures}" ) else: # no match - raise Exception(f"Could not find any methods to use for {on_complete} with call_config of {call_config}") + raise Exception( + f"Could not find any methods to use for {on_complete.name} with call_config of {call_config.name}" + ) def _substitute_template_and_compile( self, @@ -1078,12 +990,10 @@ def _get_approval_source_map(self) -> SourceMap | None: def _add_method_call( self, atc: AtomicTransactionComposer, - method: Method | None = None, + abi_method: Method | str | bool | None = None, abi_args: ABIArgsDict | None = None, app_id: int | None = None, - sender: str | None = None, - signer: TransactionSigner | None = None, - suggested_params: transaction.SuggestedParams | None = None, + parameters: CommonCallParameters | None = None, on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC, local_schema: transaction.StateSchema | None = None, global_schema: transaction.StateSchema | None = None, @@ -1094,25 +1004,24 @@ def _add_method_call( foreign_apps: list[int] | None = None, foreign_assets: list[int] | None = None, boxes: Sequence[tuple[int, bytes | bytearray | str | int]] | None = None, - note: bytes | str | None = None, - lease: bytes | str | None = None, rekey_to: str | None = None, - ) -> AtomicTransactionComposer: + call_config: CallConfig = CallConfig.CALL, + ) -> None: """Adds a transaction to the AtomicTransactionComposer passed""" if app_id is None: app_id = self.app_id - sp = suggested_params or self.suggested_params or self.algod_client.suggested_params() - signer, sender = self._resolve_signer_sender(signer, sender) + if parameters is None: + parameters = CommonCallParameters() + method = self._resolve_method(abi_method, abi_args, on_complete, call_config) + sp = parameters.suggested_params or self.suggested_params or self.algod_client.suggested_params() + signer, sender = self._resolve_signer_sender(parameters.signer, parameters.sender) if boxes is not None: # TODO: algosdk actually does this, but it's type hints say otherwise... encoded_boxes = [(id_, algosdk.encoding.encode_as_bytes(name)) for id_, name in boxes] else: encoded_boxes = None - if lease is not None: - encoded_lease = lease.encode("utf-8") if isinstance(lease, str) else lease - else: - encoded_lease = None + encoded_lease = parameters.lease.encode("utf-8") if isinstance(parameters.lease, str) else parameters.lease if not method: # not an abi method, treat as a regular call if abi_args: @@ -1133,7 +1042,7 @@ def _add_method_call( foreign_apps=foreign_apps, foreign_assets=foreign_assets, boxes=encoded_boxes, - note=note, + note=parameters.note, lease=encoded_lease, rekey_to=rekey_to, ), @@ -1186,13 +1095,11 @@ def _add_method_call( foreign_apps=foreign_apps, foreign_assets=foreign_assets, boxes=encoded_boxes, - note=note.encode("utf-8") if isinstance(note, str) else note, + note=parameters.note.encode("utf-8") if isinstance(parameters.note, str) else parameters.note, lease=encoded_lease, rekey_to=rekey_to, ) - return atc - def _method_matches( self, method: Method, args: ABIArgsDict | None, on_complete: transaction.OnComplete, call_config: CallConfig ) -> bool: diff --git a/tests/test_app_client.py b/tests/test_app_client.py index 50246d30..0be53e61 100644 --- a/tests/test_app_client.py +++ b/tests/test_app_client.py @@ -31,7 +31,7 @@ def test_abi_create_args(client_fixture: ApplicationClient, app_spec: Applicatio def test_create_auto_find(client_fixture: ApplicationClient) -> None: - client_fixture.create(on_complete=OnComplete.OptInOC) + client_fixture.create(parameters={"on_complete": OnComplete.OptInOC}) assert client_fixture.call("hello", args={"name": "test"}).abi_result.return_value == "Opt In, test" From dab18bdf09c14904633c4399805395d6f714404a Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Thu, 23 Mar 2023 21:42:17 +0800 Subject: [PATCH 03/24] chore: fix linting --- src/algokit_utils/application_client.py | 1 - src/algokit_utils/logic_error.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index 3223efb4..f95c6cab 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -100,7 +100,6 @@ def num_extra_program_pages(approval: bytes, clear: bytes) -> int: class ABICallArgs: method: Method | str | bool | None args: ABIArgsDict = dataclasses.field(default_factory=dict) - bare: bool = False lease: str | bytes | None = dataclasses.field(default=None) diff --git a/src/algokit_utils/logic_error.py b/src/algokit_utils/logic_error.py index b796d0d3..836ab587 100644 --- a/src/algokit_utils/logic_error.py +++ b/src/algokit_utils/logic_error.py @@ -6,6 +6,7 @@ __all__ = [ "LogicError", + "parse_logic_error", ] LOGIC_ERROR = ( From 4438ad5b09d29836d309933c3152c8a418a4ac3e Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Thu, 23 Mar 2023 22:12:33 +0800 Subject: [PATCH 04/24] fix: add transaction details to deploy response --- src/algokit_utils/application_client.py | 95 ++++++++++++++++--------- 1 file changed, 60 insertions(+), 35 deletions(-) diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index f95c6cab..8b796a47 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -122,18 +122,21 @@ class OperationPerformed(Enum): Replace = 3 -@dataclasses.dataclass -class DeployResponse: - app: AppMetaData - action_taken: OperationPerformed = OperationPerformed.Nothing - - @dataclasses.dataclass(kw_only=True) class TransactionResponse: tx_id: str confirmed_round: int | None +@dataclasses.dataclass(kw_only=True) +class DeployResponse: + app: AppMetaData + create_response: TransactionResponse | None = None + delete_response: TransactionResponse | None = None + update_response: TransactionResponse | None = None + action_taken: OperationPerformed = OperationPerformed.Nothing + + @dataclasses.dataclass(kw_only=True) class ABITransactionResponse(TransactionResponse): abi_result: ABIResult @@ -372,22 +375,24 @@ def create_metadata( ) return app_metadata - base_parameters = CommonCallParameters(note=app_spec_note.encode(), signer=signer, sender=sender) + common_parameters = CommonCallParameters(note=app_spec_note.encode(), signer=signer, sender=sender) def create_app() -> DeployResponse: assert self.existing_deployments method, args, lease = unpack_args(create_args) - create_result = self.create( + create_response = self.create( abi_method=method, args=args, - parameters=_add_lease_parameter(base_parameters, lease=lease), + parameters=_add_lease_parameter(common_parameters, lease), template_values=template_values, ) logger.info(f"{name} ({version}) deployed successfully, with app id {self.app_id}.") - assert create_result.confirmed_round is not None - app_metadata = create_metadata(create_result.confirmed_round) + assert create_response.confirmed_round is not None + app_metadata = create_metadata(create_response.confirmed_round) self.existing_deployments.apps[name] = app_metadata - return DeployResponse(app_metadata, action_taken=OperationPerformed.Create) + return DeployResponse( + app=app_metadata, create_response=create_response, action_taken=OperationPerformed.Create + ) if app.app_id == 0: logger.info(f"{name} not found in {self._creator} account, deploying app.") @@ -426,24 +431,31 @@ def create_and_delete_app() -> DeployResponse: atc, abi_method=create_method, args=c_args, - parameters=_add_lease_parameter(base_parameters, lease=create_lease), + parameters=_add_lease_parameter(common_parameters, create_lease), template_values=template_values, ) self.compose_delete( atc, abi_method=delete_method, args=d_args, - parameters=_add_lease_parameter(base_parameters, lease=delete_lease), + parameters=_add_lease_parameter(common_parameters, delete_lease), ) - create_delete_result = self._execute_atc(atc) - self._set_app_id_from_tx_id(create_delete_result.tx_ids[0]) + create_delete_response = self._execute_atc(atc) + create_response = _tr_from_atr(atc, create_delete_response, 0) + delete_response = _tr_from_atr(atc, create_delete_response, 1) + self._set_app_id_from_tx_id(create_response.tx_id) logger.info(f"{name} ({version}) deployed successfully, with app id {self.app_id}.") logger.info(f"{name} ({app.version}) with app id {app.app_id}, deleted successfully.") - app_metadata = create_metadata(create_delete_result.confirmed_round) + app_metadata = create_metadata(create_delete_response.confirmed_round) self.existing_deployments.apps[name] = app_metadata - # TODO: include transaction responses - return DeployResponse(app_metadata, action_taken=OperationPerformed.Replace) + + return DeployResponse( + app=app_metadata, + create_response=create_response, + delete_response=delete_response, + action_taken=OperationPerformed.Replace, + ) def update_app() -> DeployResponse: assert on_update == OnUpdate.UpdateApp @@ -451,17 +463,19 @@ def update_app() -> DeployResponse: assert self.existing_deployments logger.info(f"Updating {name} to {version} in {self._creator} account, with app id {app.app_id}") method, args, lease = unpack_args(update_args) - update_result = self.update( + update_response = self.update( abi_method=method, args=args, - parameters=_add_lease_parameter(base_parameters, lease=lease), + parameters=_add_lease_parameter(common_parameters, lease=lease), template_values=template_values, ) app_metadata = create_metadata( - app.created_round, updated_round=update_result.confirmed_round, original_metadata=app.created_metadata + app.created_round, updated_round=update_response.confirmed_round, original_metadata=app.created_metadata ) self.existing_deployments.apps[name] = app_metadata - return DeployResponse(app_metadata, action_taken=OperationPerformed.Update) + return DeployResponse( + app=app_metadata, update_response=update_response, action_taken=OperationPerformed.Update + ) if schema_breaking_change: logger.warning( @@ -532,7 +546,7 @@ def update_app() -> DeployResponse: logger.info("No detected changes in app, nothing to do.") - return DeployResponse(app) + return DeployResponse(app=app) def compose_create( self, @@ -1138,17 +1152,7 @@ def _method_hints(self, method: Method) -> MethodHints: def _execute_atc_tr(self, atc: AtomicTransactionComposer) -> TransactionResponse: result = self._execute_atc(atc) - if result.abi_results: - return ABITransactionResponse( - tx_id=result.tx_ids[0], - abi_result=result.abi_results[0], - confirmed_round=result.confirmed_round, - ) - else: - return TransactionResponse( - tx_id=result.tx_ids[0], - confirmed_round=result.confirmed_round, - ) + return _tr_from_atr(atc, result) def _execute_atc(self, atc: AtomicTransactionComposer) -> AtomicTransactionResponse: return execute_atc_with_logic_error( @@ -1345,6 +1349,27 @@ def _get_deploy_control( ) +def _tr_from_atr( + atc: AtomicTransactionComposer, result: AtomicTransactionResponse, transaction_index: int = 0 +) -> TransactionResponse: + if result.abi_results and transaction_index in atc.method_dict: # expecting an ABI result + abi_index = 0 + # count how many of the earlier transactions were also ABI + for index in range(transaction_index): + if index in atc.method_dict: + abi_index += 1 + return ABITransactionResponse( + tx_id=result.tx_ids[transaction_index], + abi_result=result.abi_results[abi_index], + confirmed_round=result.confirmed_round, + ) + else: + return TransactionResponse( + tx_id=result.tx_ids[transaction_index], + confirmed_round=result.confirmed_round, + ) + + def execute_atc_with_logic_error( atc: AtomicTransactionComposer, algod_client: AlgodClient, From c93ae11ab40efaf724b35dab1deb113bef507e7c Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Thu, 23 Mar 2023 22:18:04 +0800 Subject: [PATCH 05/24] chore: remove usages of cast --- src/algokit_utils/account.py | 8 +++++--- src/algokit_utils/application_client.py | 24 +++++++++++++++++------- tests/test_transfer.py | 8 +++----- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/algokit_utils/account.py b/src/algokit_utils/account.py index ada62b6c..9508449c 100644 --- a/src/algokit_utils/account.py +++ b/src/algokit_utils/account.py @@ -1,7 +1,7 @@ import logging import os from collections.abc import Callable -from typing import Any, cast +from typing import Any from algosdk.account import address_from_private_key from algosdk.kmd import KMDClient @@ -39,7 +39,8 @@ def get_or_create_kmd_wallet_account( account = get_kmd_wallet_account(client, kmd_client, name) if account: - account_info = cast(dict[str, Any], client.account_info(account.address)) + account_info = client.account_info(account.address) + assert isinstance(account_info, dict) if account_info["amount"] > 0: return account logger.debug(f"Found existing account in Sandbox with name '{name}'." f"But no funds in the account.") @@ -105,7 +106,8 @@ def get_kmd_wallet_account( matched_account_key = None if predicate: for key in key_ids: - account = cast(dict[str, Any], client.account_info(key)) + account = client.account_info(key) + assert isinstance(account, dict) if predicate(account): matched_account_key = key else: diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index 8b796a47..4ef42d80 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -401,7 +401,8 @@ def create_app() -> DeployResponse: assert isinstance(app, AppMetaData) logger.debug(f"{name} found in {self._creator} account, with app id {app.app_id}, version={app.version}.") - application_info = cast(dict[str, Any], self.algod_client.application_info(app.app_id)) + application_info = self.algod_client.application_info(app.app_id) + assert isinstance(application_info, dict) application_create_params = application_info["params"] current_approval = base64.b64decode(application_create_params["approval-program"]) @@ -874,7 +875,8 @@ def clear_state( def get_global_state(self, *, raw: bool = False) -> dict[bytes | str, bytes | str | int]: """gets the global state info for the app id set""" - global_state = cast(dict[str, Any], self.algod_client.application_info(self.app_id)) + global_state = self.algod_client.application_info(self.app_id) + assert isinstance(global_state, dict) return cast( dict[bytes | str, bytes | str | int], _decode_state(global_state.get("params", {}).get("global-state", {}), raw=raw), @@ -886,7 +888,8 @@ def get_local_state(self, account: str | None = None, *, raw: bool = False) -> d if account is None: _, account = self._resolve_signer_sender(self.signer, self.sender) - acct_state = cast(dict[str, Any], self.algod_client.account_application_info(account, self.app_id)) + acct_state = self.algod_client.account_application_info(account, self.app_id) + assert isinstance(acct_state, dict) return cast( dict[bytes | str, bytes | str | int], _decode_state(acct_state.get("app-local-state", {}).get("key-value", {}), raw=raw), @@ -1181,8 +1184,11 @@ def _resolve_signer_sender( def get_app_id_from_tx_id(algod_client: AlgodClient, tx_id: str) -> int: - result = cast(dict[str, Any], algod_client.pending_transaction_info(tx_id)) - return cast(int, result["application-index"]) + result = algod_client.pending_transaction_info(tx_id) + assert isinstance(result, dict) + app_id = result["application-index"] + assert isinstance(app_id, int) + return app_id def get_next_version(current_version: str) -> str: @@ -1203,9 +1209,13 @@ def replacement(m: re.Match) -> str: def _get_sender_from_signer(signer: TransactionSigner) -> str: if isinstance(signer, AccountTransactionSigner): - return cast(str, address_from_private_key(signer.private_key)) # type: ignore[no-untyped-call] + sender = address_from_private_key(signer.private_key) # type: ignore[no-untyped-call] + assert isinstance(sender, str) + return sender elif isinstance(signer, MultisigTransactionSigner): - return cast(str, signer.msig.address()) # type: ignore[no-untyped-call] + sender = signer.msig.address() # type: ignore[no-untyped-call] + assert isinstance(sender, str) + return sender elif isinstance(signer, LogicSigTransactionSigner): return signer.lsig.address() else: diff --git a/tests/test_transfer.py b/tests/test_transfer.py index 82e39e06..42d6e65d 100644 --- a/tests/test_transfer.py +++ b/tests/test_transfer.py @@ -1,5 +1,3 @@ -from typing import Any, cast - from algokit_utils import ABICallArgs, Account, ApplicationClient, TransferParameters, transfer @@ -17,7 +15,7 @@ def test_transfer(client_fixture: ApplicationClient, creator: Account) -> None: client_fixture.algod_client, ) - actual_amount = cast(dict[str, Any], client_fixture.algod_client.account_info(client_fixture.app_address)).get( - "amount" - ) + account_info = client_fixture.algod_client.account_info(client_fixture.app_address) + assert isinstance(account_info, dict) + actual_amount = account_info.get("amount") assert actual_amount == requested_amount From 912e6e959651c8f2dd9560dd1533b88adee76e72 Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Fri, 24 Mar 2023 00:23:54 +0800 Subject: [PATCH 06/24] feat: move ABI args to kwargs --- src/algokit_utils/application_client.py | 225 ++++++++++++------------ tests/test_app_client.py | 28 +-- tests/test_deploy_scenarios.py | 10 +- 3 files changed, 130 insertions(+), 133 deletions(-) diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index 4ef42d80..943e3cd2 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -98,9 +98,15 @@ def num_extra_program_pages(approval: bytes, clear: bytes) -> int: @dataclasses.dataclass(kw_only=True) class ABICallArgs: - method: Method | str | bool | None + method: Method | str | bool | None = None args: ABIArgsDict = dataclasses.field(default_factory=dict) - lease: str | bytes | None = dataclasses.field(default=None) + lease: str | bytes | None = None + + +class ABICallArgsDict(TypedDict, total=False): + method: Method | str | bool | None + args: ABIArgsDict + lease: str | bytes | None class OnUpdate(Enum): @@ -185,23 +191,6 @@ class FullCallParametersDict(TypedDict, OnCompleteCallParametersDict, total=Fals rekey_to: str | None -def _convert_parameters(args: CommonCallParameters | CommonCallParametersDict | None) -> FullCallParameters: - if args is None: - return FullCallParameters() - _args = dataclasses.asdict(args) if isinstance(args, CommonCallParameters) else args - if not isinstance(_args, dict): - raise Exception(f"Unexpected parameters type: {args}") - return FullCallParameters(**_args) - - -def _add_lease_parameter( - parameters: CommonCallParameters | OnCompleteCallParameters, lease: bytes | None = None -) -> OnCompleteCallParameters: - copy = OnCompleteCallParameters(**dataclasses.asdict(parameters)) - copy.lease = lease - return copy - - class ApplicationClient: @overload def __init__( @@ -308,9 +297,9 @@ def deploy( on_update: OnUpdate = OnUpdate.Fail, on_schema_break: OnSchemaBreak = OnSchemaBreak.Fail, template_values: TemplateValueDict | None = None, - create_args: ABICallArgs | None = None, - update_args: ABICallArgs | None = None, - delete_args: ABICallArgs | None = None, + create_args: ABICallArgs | ABICallArgsDict | None = None, + update_args: ABICallArgs | ABICallArgsDict | None = None, + delete_args: ABICallArgs | ABICallArgsDict | None = None, ) -> DeployResponse: """Ensures app associated with app client's creator is present and up to date""" if self.app_id: @@ -325,8 +314,13 @@ def deploy( f"Attempt to deploy contract with a sender address {sender} that differs " f"from the given creator address for this application client: {self._creator}" ) + + _create_args = _convert_deploy_args(create_args) + _update_args = _convert_deploy_args(update_args) + _delete_args = _convert_deploy_args(delete_args) + # make a copy - template_values = template_values or {} + template_values = dict(template_values or {}) _add_deploy_template_variables(template_values, allow_update=allow_update, allow_delete=allow_delete) approval_program, clear_program = self._substitute_template_and_compile(template_values) @@ -354,13 +348,6 @@ def deploy( version = get_next_version(app.version) app_spec_note = AppDeployMetaData(name, version, updatable=updatable, deletable=deletable) - def unpack_args( - args: ABICallArgs | None, - ) -> tuple[Method | str | bool | None, ABIArgsDict | None, bytes | None]: - if args is None: - return None, None, None - return args.method, args.args, args.lease.encode("utf-8") if isinstance(args.lease, str) else args.lease - def create_metadata( created_round: int, updated_round: int | None = None, original_metadata: AppDeployMetaData | None = None ) -> AppMetaData: @@ -379,11 +366,10 @@ def create_metadata( def create_app() -> DeployResponse: assert self.existing_deployments - method, args, lease = unpack_args(create_args) create_response = self.create( - abi_method=method, - args=args, - parameters=_add_lease_parameter(common_parameters, lease), + abi_method=_create_args.method, + **_create_args.args, + parameters=_add_lease_parameter(common_parameters, _create_args.lease), template_values=template_values, ) logger.info(f"{name} ({version}) deployed successfully, with app id {self.app_id}.") @@ -425,21 +411,19 @@ def create_and_delete_app() -> DeployResponse: assert isinstance(app, AppMetaData) assert self.existing_deployments logger.info(f"Replacing {name} ({app.version}) with {name} ({version}) in {self._creator} account.") - create_method, c_args, create_lease = unpack_args(create_args) - delete_method, d_args, delete_lease = unpack_args(delete_args) atc = AtomicTransactionComposer() self.compose_create( atc, - abi_method=create_method, - args=c_args, - parameters=_add_lease_parameter(common_parameters, create_lease), + abi_method=_create_args.method, + **_create_args.args, + parameters=_add_lease_parameter(common_parameters, _create_args.lease), template_values=template_values, ) self.compose_delete( atc, - abi_method=delete_method, - args=d_args, - parameters=_add_lease_parameter(common_parameters, delete_lease), + abi_method=_delete_args.method, + **_delete_args.args, + parameters=_add_lease_parameter(common_parameters, _delete_args.lease), ) create_delete_response = self._execute_atc(atc) create_response = _tr_from_atr(atc, create_delete_response, 0) @@ -463,11 +447,10 @@ def update_app() -> DeployResponse: assert isinstance(app, AppMetaData) assert self.existing_deployments logger.info(f"Updating {name} to {version} in {self._creator} account, with app id {app.app_id}") - method, args, lease = unpack_args(update_args) update_response = self.update( - abi_method=method, - args=args, - parameters=_add_lease_parameter(common_parameters, lease=lease), + abi_method=_update_args.method, + **_update_args.args, + parameters=_add_lease_parameter(common_parameters, lease=_update_args.lease), template_values=template_values, ) app_metadata = create_metadata( @@ -553,11 +536,11 @@ def compose_create( self, atc: AtomicTransactionComposer, abi_method: Method | str | bool | None = None, - args: ABIArgsDict | None = None, *, parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, template_values: TemplateValueDict | None = None, extra_pages: int | None = None, + **kwargs: Any, ) -> tuple[Program, Program]: """Adds a signed transaction with application id == 0 and the schema and source of client's app_spec to atc""" @@ -566,20 +549,20 @@ def compose_create( if extra_pages is None: extra_pages = num_extra_program_pages(approval_program.raw_binary, clear_program.raw_binary) - parameters = _convert_parameters(parameters) + parameters = _convert_call_parameters(parameters) self._add_method_call( atc, app_id=0, abi_method=abi_method, - abi_args=args, - parameters=parameters, + abi_args=kwargs, on_complete=parameters.on_complete, + call_config=CallConfig.CREATE, + parameters=parameters, approval_program=approval_program.raw_binary, clear_program=clear_program.raw_binary, global_schema=self.app_spec.global_state_schema, local_schema=self.app_spec.local_state_schema, extra_pages=extra_pages, - call_config=CallConfig.CREATE, ) return approval_program, clear_program @@ -587,11 +570,11 @@ def compose_create( def create( self, abi_method: Method | str | bool | None = None, - args: ABIArgsDict | None = None, *, parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, template_values: TemplateValueDict | None = None, extra_pages: int | None = None, + **kwargs: Any, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with application id == 0 and the schema and source of client's app_spec""" @@ -600,10 +583,10 @@ def create( self._approval_program, self._clear_program = self.compose_create( atc, abi_method, - args, parameters=parameters, template_values=template_values, extra_pages=extra_pages, + **kwargs, ) create_result = self._execute_atc_tr(atc) self._set_app_id_from_tx_id(create_result.tx_id) @@ -613,23 +596,20 @@ def compose_update( self, atc: AtomicTransactionComposer, abi_method: Method | str | bool | None = None, - args: ABIArgsDict | None = None, *, parameters: CommonCallParameters | CommonCallParametersDict | None = None, template_values: TemplateValueDict | None = None, + **kwargs: Any, ) -> tuple[Program, Program]: """Adds a signed transaction with on_complete=UpdateApplication to atc""" - self._load_reference_and_check_app_id() approval_program, clear_program = self._substitute_template_and_compile(template_values) - _parameters = _convert_parameters(parameters) - create_method = self._resolve_method(abi_method, args, _parameters.on_complete, CallConfig.CREATE) self._add_method_call( atc=atc, - abi_method=create_method, - abi_args=args, - parameters=_parameters, + abi_method=abi_method, + abi_args=kwargs, + parameters=parameters, on_complete=transaction.OnComplete.UpdateApplicationOC, approval_program=approval_program.raw_binary, clear_program=clear_program.raw_binary, @@ -640,10 +620,10 @@ def compose_update( def update( self, abi_method: Method | str | bool | None = None, - args: ABIArgsDict | None = None, *, parameters: CommonCallParameters | CommonCallParametersDict | None = None, template_values: TemplateValueDict | None = None, + **kwargs: Any, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with on_complete=UpdateApplication""" @@ -651,9 +631,9 @@ def update( self._approval_program, self._clear_program = self.compose_update( atc, abi_method, - args, parameters=parameters, template_values=template_values, + **kwargs, ) return self._execute_atc_tr(atc) @@ -661,27 +641,27 @@ def compose_delete( self, atc: AtomicTransactionComposer, abi_method: Method | str | bool | None = None, - args: ABIArgsDict | None = None, *, parameters: CommonCallParameters | CommonCallParametersDict | None = None, + **kwargs: Any, ) -> None: """Adds a signed transaction with on_complete=DeleteApplication to atc""" - delete_method = self._resolve_method(abi_method, args, on_complete=transaction.OnComplete.DeleteApplicationOC) + delete_method = self._resolve_method(abi_method, kwargs, on_complete=transaction.OnComplete.DeleteApplicationOC) self._add_method_call( atc, delete_method, - abi_args=args, - parameters=_convert_parameters(parameters), + abi_args=kwargs, + parameters=parameters, on_complete=transaction.OnComplete.DeleteApplicationOC, ) def delete( self, abi_method: Method | str | bool | None = None, - args: ABIArgsDict | None = None, *, parameters: CommonCallParameters | CommonCallParametersDict | None = None, + **kwargs: Any, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with on_complete=DeleteApplication""" @@ -689,8 +669,8 @@ def delete( self.compose_delete( atc, abi_method, - args, parameters=parameters, + **kwargs, ) return self._execute_atc_tr(atc) @@ -698,18 +678,17 @@ def compose_call( self, atc: AtomicTransactionComposer, abi_method: Method | str | bool | None = None, - args: ABIArgsDict | None = None, *, parameters: FullCallParameters | FullCallParametersDict | None = None, + **kwargs: Any, ) -> None: """Adds a signed transaction with specified parameters to atc""" - self._load_reference_and_check_app_id() - _parameters = _convert_parameters(parameters) + _parameters = _convert_call_parameters(parameters) self._add_method_call( atc, abi_method=abi_method, - abi_args=args, + abi_args=kwargs, parameters=_parameters, on_complete=_parameters.on_complete, accounts=_parameters.accounts, @@ -723,9 +702,9 @@ def compose_call( def call( self, abi_method: Method | str | Literal[True], - args: ABIArgsDict | None = None, *, parameters: FullCallParameters | FullCallParametersDict | None = None, + **kwargs: Any, ) -> ABITransactionResponse: ... @@ -733,30 +712,30 @@ def call( def call( self, abi_method: Literal[False], - args: ABIArgsDict | None = None, *, parameters: FullCallParameters | FullCallParametersDict | None = None, + **kwargs: Any, ) -> TransactionResponse: ... def call( self, abi_method: Method | str | bool | None = None, - args: ABIArgsDict | None = None, *, parameters: FullCallParameters | FullCallParametersDict | None = None, + **kwargs: Any, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with specified parameters""" atc = AtomicTransactionComposer() - _parameters = _convert_parameters(parameters) + _parameters = _convert_call_parameters(parameters) self.compose_call( atc, abi_method=abi_method, - args=args, parameters=_parameters, + **kwargs, ) - method = self._resolve_method(abi_method, args, _parameters.on_complete) + method = self._resolve_method(abi_method, kwargs, _parameters.on_complete) # If its a read-only method, use dryrun (TODO: swap with simulate later?) if method: response = self._dry_run_call(method, atc) @@ -783,33 +762,33 @@ def compose_opt_in( self, atc: AtomicTransactionComposer, abi_method: Method | str | bool | None = None, - args: ABIArgsDict | None = None, *, parameters: CommonCallParameters | CommonCallParametersDict | None = None, + **kwargs: Any, ) -> None: """Adds a signed transaction with on_complete=OptIn to atc""" self._add_method_call( atc, abi_method=abi_method, - abi_args=args, - parameters=_convert_parameters(parameters), + abi_args=kwargs, + parameters=parameters, on_complete=transaction.OnComplete.OptInOC, ) def opt_in( self, abi_method: Method | str | bool | None = None, - args: ABIArgsDict | None = None, *, parameters: CommonCallParameters | CommonCallParametersDict | None = None, + **kwargs: Any, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with on_complete=OptIn""" atc = AtomicTransactionComposer() self.compose_opt_in( atc, abi_method=abi_method, - args=args, parameters=parameters, + **kwargs, ) return self._execute_atc_tr(atc) @@ -817,33 +796,33 @@ def compose_close_out( self, atc: AtomicTransactionComposer, abi_method: Method | str | bool | None = None, - args: ABIArgsDict | None = None, *, parameters: CommonCallParameters | CommonCallParametersDict | None = None, + **kwargs: Any, ) -> None: """Adds a signed transaction with on_complete=CloseOut to ac""" self._add_method_call( atc, abi_method=abi_method, - abi_args=args, - parameters=_convert_parameters(parameters), + abi_args=kwargs, + parameters=parameters, on_complete=transaction.OnComplete.CloseOutOC, ) def close_out( self, abi_method: Method | str | bool | None = None, - args: ABIArgsDict | None = None, *, parameters: CommonCallParameters | CommonCallParametersDict | None = None, + **kwargs: Any, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with on_complete=CloseOut""" atc = AtomicTransactionComposer() self.compose_close_out( atc, abi_method=abi_method, - args=args, parameters=parameters, + **kwargs, ) return self._execute_atc_tr(atc) @@ -856,7 +835,7 @@ def compose_clear_state( """Adds a signed transaction with on_complete=ClearState to atc""" return self._add_method_call( atc, - parameters=_convert_parameters(parameters), + parameters=parameters, on_complete=transaction.OnComplete.ClearStateOC, ) @@ -1009,7 +988,7 @@ def _add_method_call( abi_method: Method | str | bool | None = None, abi_args: ABIArgsDict | None = None, app_id: int | None = None, - parameters: CommonCallParameters | None = None, + parameters: CommonCallParameters | CommonCallParametersDict | None = None, on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC, local_schema: transaction.StateSchema | None = None, global_schema: transaction.StateSchema | None = None, @@ -1025,9 +1004,9 @@ def _add_method_call( ) -> None: """Adds a transaction to the AtomicTransactionComposer passed""" if app_id is None: + self._load_reference_and_check_app_id() app_id = self.app_id - if parameters is None: - parameters = CommonCallParameters() + parameters = _convert_call_parameters(parameters) method = self._resolve_method(abi_method, abi_args, on_complete, call_config) sp = parameters.suggested_params or self.suggested_params or self.algod_client.suggested_params() signer, sender = self._resolve_signer_sender(parameters.signer, parameters.sender) @@ -1207,6 +1186,46 @@ def replacement(m: re.Match) -> str: ) +def execute_atc_with_logic_error( + atc: AtomicTransactionComposer, + algod_client: AlgodClient, + wait_rounds: int = 4, + approval_program: str | None = None, + approval_source_map: SourceMap | None = None, +) -> AtomicTransactionResponse: + try: + return atc.execute(algod_client, wait_rounds=wait_rounds) + except Exception as ex: + if approval_source_map and approval_program: + logic_error_data = parse_logic_error(str(ex)) + if logic_error_data is not None: + raise LogicError( + logic_error=ex, + program=approval_program, + source_map=approval_source_map, + **logic_error_data, + ) from ex + raise ex + + +def _convert_call_parameters(args: CommonCallParameters | CommonCallParametersDict | None) -> FullCallParameters: + _args = dataclasses.asdict(args) if isinstance(args, CommonCallParameters) else (args or {}) + return FullCallParameters(**_args) + + +def _convert_deploy_args(args: ABICallArgs | ABICallArgsDict | None) -> ABICallArgs: + _args = dataclasses.asdict(args) if isinstance(args, ABICallArgs) else (args or {}) + return ABICallArgs(**_args) + + +def _add_lease_parameter( + parameters: CommonCallParameters | OnCompleteCallParameters, lease: bytes | str | None +) -> OnCompleteCallParameters: + copy = OnCompleteCallParameters(**dataclasses.asdict(parameters)) + copy.lease = lease + return copy + + def _get_sender_from_signer(signer: TransactionSigner) -> str: if isinstance(signer, AccountTransactionSigner): sender = address_from_private_key(signer.private_key) # type: ignore[no-untyped-call] @@ -1378,25 +1397,3 @@ def _tr_from_atr( tx_id=result.tx_ids[transaction_index], confirmed_round=result.confirmed_round, ) - - -def execute_atc_with_logic_error( - atc: AtomicTransactionComposer, - algod_client: AlgodClient, - wait_rounds: int = 4, - approval_program: str | None = None, - approval_source_map: SourceMap | None = None, -) -> AtomicTransactionResponse: - try: - return atc.execute(algod_client, wait_rounds=wait_rounds) - except Exception as ex: - if approval_source_map and approval_program: - logic_error_data = parse_logic_error(str(ex)) - if logic_error_data is not None: - raise LogicError( - logic_error=ex, - program=approval_program, - source_map=approval_source_map, - **logic_error_data, - ) from ex - raise ex diff --git a/tests/test_app_client.py b/tests/test_app_client.py index 0be53e61..aa314367 100644 --- a/tests/test_app_client.py +++ b/tests/test_app_client.py @@ -14,26 +14,26 @@ def test_bare_create(client_fixture: ApplicationClient) -> None: client_fixture.create(abi_method=False) - assert client_fixture.call("hello", args={"name": "test"}).abi_result.return_value == "Hello Bare, test" + assert client_fixture.call("hello", name="test").abi_result.return_value == "Hello Bare, test" def test_abi_create(client_fixture: ApplicationClient) -> None: client_fixture.create("create") - assert client_fixture.call("hello", args={"name": "test"}).abi_result.return_value == "Hello ABI, test" + assert client_fixture.call("hello", name="test").abi_result.return_value == "Hello ABI, test" def test_abi_create_args(client_fixture: ApplicationClient, app_spec: ApplicationSpecification) -> None: create = next(m for m in app_spec.contract.methods if m.name == "create_args") - client_fixture.create(create, args={"greeting": "ahoy"}) + client_fixture.create(create, greeting="ahoy") - assert client_fixture.call("hello", args={"name": "test"}).abi_result.return_value == "ahoy, test" + assert client_fixture.call("hello", name="test").abi_result.return_value == "ahoy, test" def test_create_auto_find(client_fixture: ApplicationClient) -> None: client_fixture.create(parameters={"on_complete": OnComplete.OptInOC}) - assert client_fixture.call("hello", args={"name": "test"}).abi_result.return_value == "Opt In, test" + assert client_fixture.call("hello", name="test").abi_result.return_value == "Opt In, test" def test_abi_create_with_atc(client_fixture: ApplicationClient) -> None: @@ -43,7 +43,7 @@ def test_abi_create_with_atc(client_fixture: ApplicationClient) -> None: create_result = atc.execute(client_fixture.algod_client, 4) client_fixture.app_id = get_app_id_from_tx_id(client_fixture.algod_client, create_result.tx_ids[0]) - assert client_fixture.call("hello", args={"name": "test"}).abi_result.return_value == "Hello ABI, test" + assert client_fixture.call("hello", name="test").abi_result.return_value == "Hello ABI, test" def test_bare_create_with_atc(client_fixture: ApplicationClient) -> None: @@ -53,13 +53,13 @@ def test_bare_create_with_atc(client_fixture: ApplicationClient) -> None: create_result = atc.execute(client_fixture.algod_client, 4) client_fixture.app_id = get_app_id_from_tx_id(client_fixture.algod_client, create_result.tx_ids[0]) - assert client_fixture.call("hello", args={"name": "test"}).abi_result.return_value == "Hello Bare, test" + assert client_fixture.call("hello", name="test").abi_result.return_value == "Hello Bare, test" def test_abi_call_with_atc(client_fixture: ApplicationClient) -> None: client_fixture.create("create") atc = AtomicTransactionComposer() - client_fixture.compose_call(atc, "hello", args={"name": "test"}) + client_fixture.compose_call(atc, "hello", name="test") result = atc.execute(client_fixture.algod_client, 4) assert result.abi_results[0].return_value == "Hello ABI, test" @@ -68,9 +68,9 @@ def test_abi_call_with_atc(client_fixture: ApplicationClient) -> None: def test_abi_call_multiple_times_with_atc(client_fixture: ApplicationClient) -> None: client_fixture.create("create") atc = AtomicTransactionComposer() - client_fixture.compose_call(atc, "hello", args={"name": "test"}) - client_fixture.compose_call(atc, "hello", args={"name": "test2"}) - client_fixture.compose_call(atc, "hello", args={"name": "test3"}) + client_fixture.compose_call(atc, "hello", name="test") + client_fixture.compose_call(atc, "hello", name="test2") + client_fixture.compose_call(atc, "hello", name="test3") result = atc.execute(client_fixture.algod_client, 4) assert result.abi_results[0].return_value == "Hello ABI, test" @@ -91,14 +91,14 @@ def test_deploy_with_create(client_fixture: ApplicationClient, creator: Account) client_fixture.algod_client, ) - assert client_fixture.call("hello", args={"name": "test"}).abi_result.return_value == "Hello ABI, test" + assert client_fixture.call("hello", name="test").abi_result.return_value == "Hello ABI, test" def test_deploy_with_create_args(client_fixture: ApplicationClient, app_spec: ApplicationSpecification) -> None: create_args = next(m for m in app_spec.contract.methods if m.name == "create_args") client_fixture.deploy("v1", create_args=ABICallArgs(method=create_args, args={"greeting": "deployed"})) - assert client_fixture.call("hello", args={"name": "test"}).abi_result.return_value == "deployed, test" + assert client_fixture.call("hello", name="test").abi_result.return_value == "deployed, test" def test_deploy_with_bare_create(client_fixture: ApplicationClient) -> None: @@ -109,4 +109,4 @@ def test_deploy_with_bare_create(client_fixture: ApplicationClient) -> None: ), ) - assert client_fixture.call("hello", args={"name": "test"}).abi_result.return_value == "Hello Bare, test" + assert client_fixture.call("hello", name="test").abi_result.return_value == "Hello Bare, test" diff --git a/tests/test_deploy_scenarios.py b/tests/test_deploy_scenarios.py index 0b9f3792..d7d9ba52 100644 --- a/tests/test_deploy_scenarios.py +++ b/tests/test_deploy_scenarios.py @@ -235,7 +235,7 @@ def test_deploy_templated_app_with_changing_parameters_succeeds(deploy_fixture: allow_update=True, ) - response = app_client.call("hello", args={"name": "call_1"}) + response = app_client.call("hello", name="call_1") assert response.abi_result logger.info(f"Called hello: {response.abi_result.return_value}") @@ -246,7 +246,7 @@ def test_deploy_templated_app_with_changing_parameters_succeeds(deploy_fixture: allow_update=False, ) - response = app_client.call("hello", args={"name": "call_2"}) + response = app_client.call("hello", name="call_2") assert response.abi_result logger.info(f"Called hello: {response.abi_result.return_value}") @@ -260,7 +260,7 @@ def test_deploy_templated_app_with_changing_parameters_succeeds(deploy_fixture: ) logger.error(f"LogicException: {exc_info.value.message}") - response = app_client.call("hello", args={"name": "call_3"}) + response = app_client.call("hello", name="call_3") assert response.abi_result logger.info(f"Called hello: {response.abi_result.return_value}") @@ -273,7 +273,7 @@ def test_deploy_templated_app_with_changing_parameters_succeeds(deploy_fixture: allow_delete=True, allow_update=True, ) - response = app_client.call("hello", args={"name": "call_4"}) + response = app_client.call("hello", name="call_4") assert response.abi_result logger.info(f"Called hello: {response.abi_result.return_value}") app_id = app_client.app_id @@ -284,7 +284,7 @@ def test_deploy_templated_app_with_changing_parameters_succeeds(deploy_fixture: app_id=app_id, signer=deploy_fixture.creator, ) - response = app_client.call("hello", args={"name": "call_5"}) + response = app_client.call("hello", name="call_5") assert response.abi_result logger.info(f"Called hello: {response.abi_result.return_value}") From 4a6b7ded36135d85c7a4d573d35e76b4e6692962 Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Fri, 24 Mar 2023 08:45:54 +0800 Subject: [PATCH 07/24] chore: import fixes --- src/algokit_utils/__init__.py | 17 +++++++++++++++++ src/algokit_utils/application_client.py | 8 ++++++++ 2 files changed, 25 insertions(+) diff --git a/src/algokit_utils/__init__.py b/src/algokit_utils/__init__.py index 412470bc..33406411 100644 --- a/src/algokit_utils/__init__.py +++ b/src/algokit_utils/__init__.py @@ -24,14 +24,22 @@ ) from algokit_utils.application_client import ( ABICallArgs, + ABICallArgsDict, ABITransactionResponse, ApplicationClient, + CommonCallParameters, + CommonCallParametersDict, DeployResponse, + FullCallParameters, + FullCallParametersDict, + OnCompleteCallParameters, + OnCompleteCallParametersDict, OnSchemaBreak, OnUpdate, OperationPerformed, Program, TransactionResponse, + execute_atc_with_logic_error, get_app_id_from_tx_id, get_next_version, num_extra_program_pages, @@ -74,14 +82,23 @@ "get_creator_apps", "replace_template_variables", "ABICallArgs", + "ABICallArgsDict", "ABITransactionResponse", "ApplicationClient", + "CommonCallParameters", + "CommonCallParametersDict", + "OnCompleteCallParameters", + "OnCompleteCallParametersDict", + "FullCallParameters", + "FullCallParametersDict", + "ApplicationClient", "DeployResponse", "OnUpdate", "OnSchemaBreak", "OperationPerformed", "Program", "TransactionResponse", + "execute_atc_with_logic_error", "get_app_id_from_tx_id", "get_next_version", "num_extra_program_pages", diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index 943e3cd2..31964197 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -63,7 +63,15 @@ __all__ = [ "ABICallArgs", + "ABICallArgsDict", + "ABITransactionResponse", "ApplicationClient", + "CommonCallParameters", + "CommonCallParametersDict", + "OnCompleteCallParameters", + "OnCompleteCallParametersDict", + "FullCallParameters", + "FullCallParametersDict", "DeployResponse", "OnUpdate", "OnSchemaBreak", From 7201b44b1fa05dbcd68cd62f424d5185a0d7a735 Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Fri, 24 Mar 2023 12:11:25 +0800 Subject: [PATCH 08/24] fix: expose execute_atc --- src/algokit_utils/application_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index 31964197..c5cbb945 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -433,7 +433,7 @@ def create_and_delete_app() -> DeployResponse: **_delete_args.args, parameters=_add_lease_parameter(common_parameters, _delete_args.lease), ) - create_delete_response = self._execute_atc(atc) + create_delete_response = self.execute_atc(atc) create_response = _tr_from_atr(atc, create_delete_response, 0) delete_response = _tr_from_atr(atc, create_delete_response, 1) self._set_app_id_from_tx_id(create_response.tx_id) @@ -1141,10 +1141,10 @@ def _method_hints(self, method: Method) -> MethodHints: return self.app_spec.hints[sig] def _execute_atc_tr(self, atc: AtomicTransactionComposer) -> TransactionResponse: - result = self._execute_atc(atc) + result = self.execute_atc(atc) return _tr_from_atr(atc, result) - def _execute_atc(self, atc: AtomicTransactionComposer) -> AtomicTransactionResponse: + def execute_atc(self, atc: AtomicTransactionComposer) -> AtomicTransactionResponse: return execute_atc_with_logic_error( atc, self.algod_client, From db068163991c983dfa621c614a22401200ba958a Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Fri, 24 Mar 2023 12:11:37 +0800 Subject: [PATCH 09/24] fix: sort method_config when exporting --- src/algokit_utils/application_specification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/algokit_utils/application_specification.py b/src/algokit_utils/application_specification.py index ff31aa9f..5a85463f 100644 --- a/src/algokit_utils/application_specification.py +++ b/src/algokit_utils/application_specification.py @@ -97,7 +97,7 @@ def undictify(data: dict[str, Any]) -> "MethodHints": def _encode_method_config(mc: MethodConfigDict) -> dict[str, str | None]: - return {k: v.name for k, v in mc.items() if v != CallConfig.NEVER} + return {k: mc[k].name for k in sorted(mc) if mc[k] != CallConfig.NEVER} def _decode_method_config(data: dict[OnCompleteActionName, Any]) -> MethodConfigDict: From 2044a801e0428e4aded4e686468cb1fb069f38b9 Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Fri, 24 Mar 2023 13:17:41 +0800 Subject: [PATCH 10/24] fix: flatten ABIResult into responses --- src/algokit_utils/application_client.py | 17 ++++++++++------- tests/test_app_client.py | 18 +++++++++--------- tests/test_deploy_scenarios.py | 15 +++++---------- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index c5cbb945..db64311c 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -153,7 +153,11 @@ class DeployResponse: @dataclasses.dataclass(kw_only=True) class ABITransactionResponse(TransactionResponse): - abi_result: ABIResult + raw_value: bytes + return_value: Any + decode_error: Exception | None + tx_info: dict + method: Method @dataclasses.dataclass(kw_only=True) @@ -763,7 +767,7 @@ def _dry_run_call(self, method: Method, atc: AtomicTransactionComposer) -> ABITr raise Exception(f"Dryrun for readonly method failed: {msg}") method_results = _parse_result({0: method}, dr_result["txns"], atc.tx_ids) - return ABITransactionResponse(abi_result=method_results[0], tx_id=atc.tx_ids[0], confirmed_round=None) + return ABITransactionResponse(**method_results[0].__dict__, confirmed_round=None) return None def compose_opt_in( @@ -884,9 +888,9 @@ def get_local_state(self, account: str | None = None, *, raw: bool = False) -> d def resolve(self, to_resolve: DefaultArgumentDict) -> int | str | bytes: def _data_check(value: Any) -> int | str | bytes: - if isinstance(value, (str, bytes, bytes)): + if isinstance(value, (int, str, bytes)): return value - raise ValueError("Unexpected type for constant data") + raise ValueError(f"Unexpected type for constant data: {value}") match to_resolve: case {"source": "constant", "data": data}: @@ -901,7 +905,7 @@ def _data_check(value: Any) -> int | str | bytes: case {"source": "abi-method", "data": dict() as method_dict}: method = Method.undictify(method_dict) response = self.call(method) - assert isinstance(response, ABIResult) + assert isinstance(response, ABITransactionResponse) return _data_check(response.return_value) case {"source": source}: @@ -1396,8 +1400,7 @@ def _tr_from_atr( if index in atc.method_dict: abi_index += 1 return ABITransactionResponse( - tx_id=result.tx_ids[transaction_index], - abi_result=result.abi_results[abi_index], + **result.abi_results[abi_index].__dict__, confirmed_round=result.confirmed_round, ) else: diff --git a/tests/test_app_client.py b/tests/test_app_client.py index aa314367..f5ef9b79 100644 --- a/tests/test_app_client.py +++ b/tests/test_app_client.py @@ -14,26 +14,26 @@ def test_bare_create(client_fixture: ApplicationClient) -> None: client_fixture.create(abi_method=False) - assert client_fixture.call("hello", name="test").abi_result.return_value == "Hello Bare, test" + assert client_fixture.call("hello", name="test").return_value == "Hello Bare, test" def test_abi_create(client_fixture: ApplicationClient) -> None: client_fixture.create("create") - assert client_fixture.call("hello", name="test").abi_result.return_value == "Hello ABI, test" + assert client_fixture.call("hello", name="test").return_value == "Hello ABI, test" def test_abi_create_args(client_fixture: ApplicationClient, app_spec: ApplicationSpecification) -> None: create = next(m for m in app_spec.contract.methods if m.name == "create_args") client_fixture.create(create, greeting="ahoy") - assert client_fixture.call("hello", name="test").abi_result.return_value == "ahoy, test" + assert client_fixture.call("hello", name="test").return_value == "ahoy, test" def test_create_auto_find(client_fixture: ApplicationClient) -> None: client_fixture.create(parameters={"on_complete": OnComplete.OptInOC}) - assert client_fixture.call("hello", name="test").abi_result.return_value == "Opt In, test" + assert client_fixture.call("hello", name="test").return_value == "Opt In, test" def test_abi_create_with_atc(client_fixture: ApplicationClient) -> None: @@ -43,7 +43,7 @@ def test_abi_create_with_atc(client_fixture: ApplicationClient) -> None: create_result = atc.execute(client_fixture.algod_client, 4) client_fixture.app_id = get_app_id_from_tx_id(client_fixture.algod_client, create_result.tx_ids[0]) - assert client_fixture.call("hello", name="test").abi_result.return_value == "Hello ABI, test" + assert client_fixture.call("hello", name="test").return_value == "Hello ABI, test" def test_bare_create_with_atc(client_fixture: ApplicationClient) -> None: @@ -53,7 +53,7 @@ def test_bare_create_with_atc(client_fixture: ApplicationClient) -> None: create_result = atc.execute(client_fixture.algod_client, 4) client_fixture.app_id = get_app_id_from_tx_id(client_fixture.algod_client, create_result.tx_ids[0]) - assert client_fixture.call("hello", name="test").abi_result.return_value == "Hello Bare, test" + assert client_fixture.call("hello", name="test").return_value == "Hello Bare, test" def test_abi_call_with_atc(client_fixture: ApplicationClient) -> None: @@ -91,14 +91,14 @@ def test_deploy_with_create(client_fixture: ApplicationClient, creator: Account) client_fixture.algod_client, ) - assert client_fixture.call("hello", name="test").abi_result.return_value == "Hello ABI, test" + assert client_fixture.call("hello", name="test").return_value == "Hello ABI, test" def test_deploy_with_create_args(client_fixture: ApplicationClient, app_spec: ApplicationSpecification) -> None: create_args = next(m for m in app_spec.contract.methods if m.name == "create_args") client_fixture.deploy("v1", create_args=ABICallArgs(method=create_args, args={"greeting": "deployed"})) - assert client_fixture.call("hello", name="test").abi_result.return_value == "deployed, test" + assert client_fixture.call("hello", name="test").return_value == "deployed, test" def test_deploy_with_bare_create(client_fixture: ApplicationClient) -> None: @@ -109,4 +109,4 @@ def test_deploy_with_bare_create(client_fixture: ApplicationClient) -> None: ), ) - assert client_fixture.call("hello", name="test").abi_result.return_value == "Hello Bare, test" + assert client_fixture.call("hello", name="test").return_value == "Hello Bare, test" diff --git a/tests/test_deploy_scenarios.py b/tests/test_deploy_scenarios.py index d7d9ba52..e2f05cca 100644 --- a/tests/test_deploy_scenarios.py +++ b/tests/test_deploy_scenarios.py @@ -236,8 +236,7 @@ def test_deploy_templated_app_with_changing_parameters_succeeds(deploy_fixture: ) response = app_client.call("hello", name="call_1") - assert response.abi_result - logger.info(f"Called hello: {response.abi_result.return_value}") + logger.info(f"Called hello: {response.return_value}") logger.info("Deploy V2 as immutable, deletable") app_client = deploy_fixture.deploy( @@ -247,8 +246,7 @@ def test_deploy_templated_app_with_changing_parameters_succeeds(deploy_fixture: ) response = app_client.call("hello", name="call_2") - assert response.abi_result - logger.info(f"Called hello: {response.abi_result.return_value}") + logger.info(f"Called hello: {response.return_value}") logger.info("Attempt to deploy V3 as updatable, deletable, it will fail because V2 was immutable") with pytest.raises(LogicError) as exc_info: @@ -261,8 +259,7 @@ def test_deploy_templated_app_with_changing_parameters_succeeds(deploy_fixture: logger.error(f"LogicException: {exc_info.value.message}") response = app_client.call("hello", name="call_3") - assert response.abi_result - logger.info(f"Called hello: {response.abi_result.return_value}") + logger.info(f"Called hello: {response.return_value}") logger.info("2nd Attempt to deploy V3 as updatable, deletable, it will succeed as on_update=OnUpdate.DeleteApp") # deploy with allow_delete=True, so we can replace it @@ -274,8 +271,7 @@ def test_deploy_templated_app_with_changing_parameters_succeeds(deploy_fixture: allow_update=True, ) response = app_client.call("hello", name="call_4") - assert response.abi_result - logger.info(f"Called hello: {response.abi_result.return_value}") + logger.info(f"Called hello: {response.return_value}") app_id = app_client.app_id app_client = ApplicationClient( @@ -285,8 +281,7 @@ def test_deploy_templated_app_with_changing_parameters_succeeds(deploy_fixture: signer=deploy_fixture.creator, ) response = app_client.call("hello", name="call_5") - assert response.abi_result - logger.info(f"Called hello: {response.abi_result.return_value}") + logger.info(f"Called hello: {response.return_value}") deploy_fixture.check_log_stability() From 9af032879cbcd930ad5ea50934ef6974523bf71b Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Fri, 24 Mar 2023 14:30:17 +0800 Subject: [PATCH 11/24] test: add application client resolve tests --- tests/app_resolve.json | 141 +++++++++++++++++++++++++++++++++++++++ tests/test_app_client.py | 46 +++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 tests/app_resolve.json diff --git a/tests/app_resolve.json b/tests/app_resolve.json new file mode 100644 index 00000000..cccae640 --- /dev/null +++ b/tests/app_resolve.json @@ -0,0 +1,141 @@ +{ + "hints": { + "update()void": { + "call_config": { + "update_application": "CALL" + } + }, + "delete()void": { + "call_config": { + "delete_application": "CALL" + } + }, + "opt_in()void": { + "call_config": { + "opt_in": "CALL" + } + }, + "close_out()void": { + "call_config": { + "close_out": "CALL" + } + }, + "add(uint64,uint64)uint64": { + "call_config": { + "no_op": "CALL" + } + }, + "dummy()string": { + "read_only": true, + "call_config": { + "no_op": "CALL" + } + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDAgMSAyCmJ5dGVjYmxvY2sgMHgxNTFmN2M3NQp0eG4gTnVtQXBwQXJncwppbnRjXzAgLy8gMAo9PQpibnogbWFpbl9sMTQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhhMGU4MTg3MiAvLyAidXBkYXRlKCl2b2lkIgo9PQpibnogbWFpbl9sMTMKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgyNDM3OGQzYyAvLyAiZGVsZXRlKCl2b2lkIgo9PQpibnogbWFpbl9sMTIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgzMGM2ZDU4YSAvLyAib3B0X2luKCl2b2lkIgo9PQpibnogbWFpbl9sMTEKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgxNjU4YWEyZiAvLyAiY2xvc2Vfb3V0KCl2b2lkIgo9PQpibnogbWFpbl9sMTAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhmZTZiZGY2OSAvLyAiYWRkKHVpbnQ2NCx1aW50NjQpdWludDY0Igo9PQpibnogbWFpbl9sOQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDU0YTlkMTdiIC8vICJkdW1teSgpc3RyaW5nIgo9PQpibnogbWFpbl9sOAplcnIKbWFpbl9sODoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBkdW1teV82CnN0b3JlIDMKYnl0ZWNfMCAvLyAweDE1MWY3Yzc1CmxvYWQgMwpjb25jYXQKbG9nCmludGNfMSAvLyAxCnJldHVybgptYWluX2w5Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmJ0b2kKc3RvcmUgMAp0eG5hIEFwcGxpY2F0aW9uQXJncyAyCmJ0b2kKc3RvcmUgMQpsb2FkIDAKbG9hZCAxCmNhbGxzdWIgYWRkXzUKc3RvcmUgMgpieXRlY18wIC8vIDB4MTUxZjdjNzUKbG9hZCAyCml0b2IKY29uY2F0CmxvZwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTA6CnR4biBPbkNvbXBsZXRpb24KaW50Y18yIC8vIENsb3NlT3V0Cj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGNsb3Nlb3V0XzQKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDExOgp0eG4gT25Db21wbGV0aW9uCmludGNfMSAvLyBPcHRJbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBvcHRpbl8zCmludGNfMSAvLyAxCnJldHVybgptYWluX2wxMjoKdHhuIE9uQ29tcGxldGlvbgpwdXNoaW50IDUgLy8gRGVsZXRlQXBwbGljYXRpb24KPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgZGVsZXRlXzIKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDEzOgp0eG4gT25Db21wbGV0aW9uCnB1c2hpbnQgNCAvLyBVcGRhdGVBcHBsaWNhdGlvbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiB1cGRhdGVfMQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTQ6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KYm56IG1haW5fbDE2CmVycgptYWluX2wxNjoKdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KYXNzZXJ0CmNhbGxzdWIgY3JlYXRlXzAKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBjcmVhdGUKY3JlYXRlXzA6CnByb3RvIDAgMApwdXNoYnl0ZXMgMHg2NzZjNmY2MjYxNmM1ZjczNzQ2MTc0NjU1Zjc2NjE2YzVmNjI3OTc0NjUgLy8gImdsb2JhbF9zdGF0ZV92YWxfYnl0ZSIKcHVzaGJ5dGVzIDB4NzQ2NTczNzQgLy8gInRlc3QiCmFwcF9nbG9iYWxfcHV0CnB1c2hieXRlcyAweDY3NmM2ZjYyNjE2YzVmNzM3NDYxNzQ2NTVmNzY2MTZjNWY2OTZlNzQgLy8gImdsb2JhbF9zdGF0ZV92YWxfaW50IgppbnRjXzEgLy8gMQphcHBfZ2xvYmFsX3B1dAp0eG4gTm90ZQpsZW4KaW50Y18wIC8vIDAKPT0KYXNzZXJ0CmludGNfMSAvLyAxCnJldHVybgoKLy8gdXBkYXRlCnVwZGF0ZV8xOgpwcm90byAwIDAKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGRlbGV0ZQpkZWxldGVfMjoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBvcHRfaW4Kb3B0aW5fMzoKcHJvdG8gMCAwCnR4biBTZW5kZXIKcHVzaGJ5dGVzIDB4NjE2MzYzNzQ1ZjczNzQ2MTc0NjU1Zjc2NjE2YzVmNjI3OTc0NjUgLy8gImFjY3Rfc3RhdGVfdmFsX2J5dGUiCnB1c2hieXRlcyAweDZjNmY2MzYxNmMyZDc0NjU3Mzc0IC8vICJsb2NhbC10ZXN0IgphcHBfbG9jYWxfcHV0CnR4biBTZW5kZXIKcHVzaGJ5dGVzIDB4NjE2MzYzNzQ1ZjczNzQ2MTc0NjU1Zjc2NjE2YzVmNjk2ZTc0IC8vICJhY2N0X3N0YXRlX3ZhbF9pbnQiCmludGNfMiAvLyAyCmFwcF9sb2NhbF9wdXQKdHhuIE5vdGUKbGVuCmludGNfMCAvLyAwCj09CmFzc2VydAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNsb3NlX291dApjbG9zZW91dF80Ogpwcm90byAwIDAKdHhuIE5vdGUKbGVuCmludGNfMCAvLyAwCj09CmFzc2VydAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGFkZAphZGRfNToKcHJvdG8gMiAxCmludGNfMCAvLyAwCmZyYW1lX2RpZyAtMgpmcmFtZV9kaWcgLTEKKwpmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBkdW1teQpkdW1teV82Ogpwcm90byAwIDEKcHVzaGJ5dGVzIDB4IC8vICIiCnB1c2hieXRlcyAweDAwMDg2NDY1NjE2NDYyNjU2NTY2IC8vIDB4MDAwODY0NjU2MTY0NjI2NTY1NjYKZnJhbWVfYnVyeSAwCnJldHN1Yg==", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDEKY2FsbHN1YiBjbGVhcnN0YXRlXzAKaW50Y18wIC8vIDEKcmV0dXJuCgovLyBjbGVhcl9zdGF0ZQpjbGVhcnN0YXRlXzA6CnByb3RvIDAgMAp0eG4gTm90ZQpsZW4KcHVzaGludCAwIC8vIDAKPT0KYXNzZXJ0CmludGNfMCAvLyAxCnJldHVybg==" + }, + "state": { + "global": { + "num_byte_slices": 1, + "num_uints": 1 + }, + "local": { + "num_byte_slices": 1, + "num_uints": 1 + } + }, + "schema": { + "global": { + "declared": { + "global_state_val_byte": { + "type": "bytes", + "key": "global_state_val_byte", + "descr": "" + }, + "global_state_val_int": { + "type": "uint64", + "key": "global_state_val_int", + "descr": "" + } + }, + "reserved": {} + }, + "local": { + "declared": { + "acct_state_val_byte": { + "type": "bytes", + "key": "acct_state_val_byte", + "descr": "" + }, + "acct_state_val_int": { + "type": "uint64", + "key": "acct_state_val_int", + "descr": "" + } + }, + "reserved": {} + } + }, + "contract": { + "name": "App", + "methods": [ + { + "name": "update", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "delete", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "opt_in", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "close_out", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "add", + "args": [ + { + "type": "uint64", + "name": "a" + }, + { + "type": "uint64", + "name": "b" + } + ], + "returns": { + "type": "uint64" + } + }, + { + "name": "dummy", + "args": [], + "returns": { + "type": "string" + } + } + ], + "networks": {} + }, + "bare_call_config": { + "no_op": "CREATE" + } +} \ No newline at end of file diff --git a/tests/test_app_client.py b/tests/test_app_client.py index f5ef9b79..46520248 100644 --- a/tests/test_app_client.py +++ b/tests/test_app_client.py @@ -3,12 +3,15 @@ Account, ApplicationClient, ApplicationSpecification, + DefaultArgumentDict, TransferParameters, get_app_id_from_tx_id, transfer, ) from algosdk.atomic_transaction_composer import AtomicTransactionComposer from algosdk.transaction import OnComplete +from algosdk.v2client.algod import AlgodClient +from conftest import read_spec def test_bare_create(client_fixture: ApplicationClient) -> None: @@ -110,3 +113,46 @@ def test_deploy_with_bare_create(client_fixture: ApplicationClient) -> None: ) assert client_fixture.call("hello", name="test").return_value == "Hello Bare, test" + + +def test_resolve(algod_client: AlgodClient, creator: Account) -> None: + app_spec = read_spec("app_resolve.json") + client_fixture = ApplicationClient(algod_client, app_spec, signer=creator) + client_fixture.create() + client_fixture.opt_in() + + int_default_argument: DefaultArgumentDict = {"source": "constant", "data": 1} + assert client_fixture.resolve(int_default_argument) == 1 + + string_default_argument: DefaultArgumentDict = {"source": "constant", "data": "stringy"} + assert client_fixture.resolve(string_default_argument) == "stringy" + + global_state_int_default_argument: DefaultArgumentDict = { + "source": "global-state", + "data": "global_state_val_int", + } + assert client_fixture.resolve(global_state_int_default_argument) == 1 + + global_state_byte_default_argument: DefaultArgumentDict = { + "source": "global-state", + "data": "global_state_val_byte", + } + assert client_fixture.resolve(global_state_byte_default_argument) == b"test" + + local_state_int_default_argument: DefaultArgumentDict = { + "source": "local-state", + "data": "acct_state_val_int", + } + assert client_fixture.resolve(local_state_int_default_argument) == 2 + + local_state_byte_default_argument: DefaultArgumentDict = { + "source": "local-state", + "data": "acct_state_val_byte", + } + assert client_fixture.resolve(local_state_byte_default_argument) == b"local-test" + + method_default_argument: DefaultArgumentDict = { + "source": "abi-method", + "data": {"name": "dummy", "args": [], "returns": {"type": "string"}}, + } + assert client_fixture.resolve(method_default_argument) == "deadbeef" From 4dec59fb15f4b2c72bca68e76c62fc9d6ec1f7d7 Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Fri, 24 Mar 2023 14:50:15 +0800 Subject: [PATCH 12/24] test: move tests around --- tests/test_app_client.py | 158 ------------------------------- tests/test_app_client_call.py | 26 +++++ tests/test_app_client_create.py | 52 ++++++++++ tests/test_app_client_deploy.py | 42 ++++++++ tests/test_app_client_resolve.py | 50 ++++++++++ 5 files changed, 170 insertions(+), 158 deletions(-) delete mode 100644 tests/test_app_client.py create mode 100644 tests/test_app_client_call.py create mode 100644 tests/test_app_client_create.py create mode 100644 tests/test_app_client_deploy.py create mode 100644 tests/test_app_client_resolve.py diff --git a/tests/test_app_client.py b/tests/test_app_client.py deleted file mode 100644 index 46520248..00000000 --- a/tests/test_app_client.py +++ /dev/null @@ -1,158 +0,0 @@ -from algokit_utils import ( - ABICallArgs, - Account, - ApplicationClient, - ApplicationSpecification, - DefaultArgumentDict, - TransferParameters, - get_app_id_from_tx_id, - transfer, -) -from algosdk.atomic_transaction_composer import AtomicTransactionComposer -from algosdk.transaction import OnComplete -from algosdk.v2client.algod import AlgodClient -from conftest import read_spec - - -def test_bare_create(client_fixture: ApplicationClient) -> None: - client_fixture.create(abi_method=False) - - assert client_fixture.call("hello", name="test").return_value == "Hello Bare, test" - - -def test_abi_create(client_fixture: ApplicationClient) -> None: - client_fixture.create("create") - - assert client_fixture.call("hello", name="test").return_value == "Hello ABI, test" - - -def test_abi_create_args(client_fixture: ApplicationClient, app_spec: ApplicationSpecification) -> None: - create = next(m for m in app_spec.contract.methods if m.name == "create_args") - client_fixture.create(create, greeting="ahoy") - - assert client_fixture.call("hello", name="test").return_value == "ahoy, test" - - -def test_create_auto_find(client_fixture: ApplicationClient) -> None: - client_fixture.create(parameters={"on_complete": OnComplete.OptInOC}) - - assert client_fixture.call("hello", name="test").return_value == "Opt In, test" - - -def test_abi_create_with_atc(client_fixture: ApplicationClient) -> None: - atc = AtomicTransactionComposer() - client_fixture.compose_create(atc, "create") - - create_result = atc.execute(client_fixture.algod_client, 4) - client_fixture.app_id = get_app_id_from_tx_id(client_fixture.algod_client, create_result.tx_ids[0]) - - assert client_fixture.call("hello", name="test").return_value == "Hello ABI, test" - - -def test_bare_create_with_atc(client_fixture: ApplicationClient) -> None: - atc = AtomicTransactionComposer() - client_fixture.compose_create(atc, abi_method=False) - - create_result = atc.execute(client_fixture.algod_client, 4) - client_fixture.app_id = get_app_id_from_tx_id(client_fixture.algod_client, create_result.tx_ids[0]) - - assert client_fixture.call("hello", name="test").return_value == "Hello Bare, test" - - -def test_abi_call_with_atc(client_fixture: ApplicationClient) -> None: - client_fixture.create("create") - atc = AtomicTransactionComposer() - client_fixture.compose_call(atc, "hello", name="test") - result = atc.execute(client_fixture.algod_client, 4) - - assert result.abi_results[0].return_value == "Hello ABI, test" - - -def test_abi_call_multiple_times_with_atc(client_fixture: ApplicationClient) -> None: - client_fixture.create("create") - atc = AtomicTransactionComposer() - client_fixture.compose_call(atc, "hello", name="test") - client_fixture.compose_call(atc, "hello", name="test2") - client_fixture.compose_call(atc, "hello", name="test3") - result = atc.execute(client_fixture.algod_client, 4) - - assert result.abi_results[0].return_value == "Hello ABI, test" - assert result.abi_results[1].return_value == "Hello ABI, test2" - assert result.abi_results[2].return_value == "Hello ABI, test3" - - -def test_deploy_with_create(client_fixture: ApplicationClient, creator: Account) -> None: - client_fixture.deploy( - "v1", - create_args=ABICallArgs( - method="create", - ), - ) - - transfer( - TransferParameters(from_account=creator, to_address=client_fixture.app_address, amount=100_000), - client_fixture.algod_client, - ) - - assert client_fixture.call("hello", name="test").return_value == "Hello ABI, test" - - -def test_deploy_with_create_args(client_fixture: ApplicationClient, app_spec: ApplicationSpecification) -> None: - create_args = next(m for m in app_spec.contract.methods if m.name == "create_args") - client_fixture.deploy("v1", create_args=ABICallArgs(method=create_args, args={"greeting": "deployed"})) - - assert client_fixture.call("hello", name="test").return_value == "deployed, test" - - -def test_deploy_with_bare_create(client_fixture: ApplicationClient) -> None: - client_fixture.deploy( - "v1", - create_args=ABICallArgs( - method=False, - ), - ) - - assert client_fixture.call("hello", name="test").return_value == "Hello Bare, test" - - -def test_resolve(algod_client: AlgodClient, creator: Account) -> None: - app_spec = read_spec("app_resolve.json") - client_fixture = ApplicationClient(algod_client, app_spec, signer=creator) - client_fixture.create() - client_fixture.opt_in() - - int_default_argument: DefaultArgumentDict = {"source": "constant", "data": 1} - assert client_fixture.resolve(int_default_argument) == 1 - - string_default_argument: DefaultArgumentDict = {"source": "constant", "data": "stringy"} - assert client_fixture.resolve(string_default_argument) == "stringy" - - global_state_int_default_argument: DefaultArgumentDict = { - "source": "global-state", - "data": "global_state_val_int", - } - assert client_fixture.resolve(global_state_int_default_argument) == 1 - - global_state_byte_default_argument: DefaultArgumentDict = { - "source": "global-state", - "data": "global_state_val_byte", - } - assert client_fixture.resolve(global_state_byte_default_argument) == b"test" - - local_state_int_default_argument: DefaultArgumentDict = { - "source": "local-state", - "data": "acct_state_val_int", - } - assert client_fixture.resolve(local_state_int_default_argument) == 2 - - local_state_byte_default_argument: DefaultArgumentDict = { - "source": "local-state", - "data": "acct_state_val_byte", - } - assert client_fixture.resolve(local_state_byte_default_argument) == b"local-test" - - method_default_argument: DefaultArgumentDict = { - "source": "abi-method", - "data": {"name": "dummy", "args": [], "returns": {"type": "string"}}, - } - assert client_fixture.resolve(method_default_argument) == "deadbeef" diff --git a/tests/test_app_client_call.py b/tests/test_app_client_call.py new file mode 100644 index 00000000..9e727481 --- /dev/null +++ b/tests/test_app_client_call.py @@ -0,0 +1,26 @@ +from algokit_utils import ( + ApplicationClient, +) +from algosdk.atomic_transaction_composer import AtomicTransactionComposer + + +def test_abi_call_with_atc(client_fixture: ApplicationClient) -> None: + client_fixture.create("create") + atc = AtomicTransactionComposer() + client_fixture.compose_call(atc, "hello", name="test") + result = atc.execute(client_fixture.algod_client, 4) + + assert result.abi_results[0].return_value == "Hello ABI, test" + + +def test_abi_call_multiple_times_with_atc(client_fixture: ApplicationClient) -> None: + client_fixture.create("create") + atc = AtomicTransactionComposer() + client_fixture.compose_call(atc, "hello", name="test") + client_fixture.compose_call(atc, "hello", name="test2") + client_fixture.compose_call(atc, "hello", name="test3") + result = atc.execute(client_fixture.algod_client, 4) + + assert result.abi_results[0].return_value == "Hello ABI, test" + assert result.abi_results[1].return_value == "Hello ABI, test2" + assert result.abi_results[2].return_value == "Hello ABI, test3" diff --git a/tests/test_app_client_create.py b/tests/test_app_client_create.py new file mode 100644 index 00000000..01e77569 --- /dev/null +++ b/tests/test_app_client_create.py @@ -0,0 +1,52 @@ +from algokit_utils import ( + ApplicationClient, + ApplicationSpecification, + get_app_id_from_tx_id, +) +from algosdk.atomic_transaction_composer import AtomicTransactionComposer +from algosdk.transaction import OnComplete + + +def test_bare_create(client_fixture: ApplicationClient) -> None: + client_fixture.create(abi_method=False) + + assert client_fixture.call("hello", name="test").return_value == "Hello Bare, test" + + +def test_abi_create(client_fixture: ApplicationClient) -> None: + client_fixture.create("create") + + assert client_fixture.call("hello", name="test").return_value == "Hello ABI, test" + + +def test_abi_create_args(client_fixture: ApplicationClient, app_spec: ApplicationSpecification) -> None: + create = next(m for m in app_spec.contract.methods if m.name == "create_args") + client_fixture.create(create, greeting="ahoy") + + assert client_fixture.call("hello", name="test").return_value == "ahoy, test" + + +def test_create_auto_find(client_fixture: ApplicationClient) -> None: + client_fixture.create(parameters={"on_complete": OnComplete.OptInOC}) + + assert client_fixture.call("hello", name="test").return_value == "Opt In, test" + + +def test_abi_create_with_atc(client_fixture: ApplicationClient) -> None: + atc = AtomicTransactionComposer() + client_fixture.compose_create(atc, "create") + + create_result = atc.execute(client_fixture.algod_client, 4) + client_fixture.app_id = get_app_id_from_tx_id(client_fixture.algod_client, create_result.tx_ids[0]) + + assert client_fixture.call("hello", name="test").return_value == "Hello ABI, test" + + +def test_bare_create_with_atc(client_fixture: ApplicationClient) -> None: + atc = AtomicTransactionComposer() + client_fixture.compose_create(atc, abi_method=False) + + create_result = atc.execute(client_fixture.algod_client, 4) + client_fixture.app_id = get_app_id_from_tx_id(client_fixture.algod_client, create_result.tx_ids[0]) + + assert client_fixture.call("hello", name="test").return_value == "Hello Bare, test" diff --git a/tests/test_app_client_deploy.py b/tests/test_app_client_deploy.py new file mode 100644 index 00000000..fbd0a529 --- /dev/null +++ b/tests/test_app_client_deploy.py @@ -0,0 +1,42 @@ +from algokit_utils import ( + ABICallArgs, + Account, + ApplicationClient, + ApplicationSpecification, + TransferParameters, + transfer, +) + + +def test_deploy_with_create(client_fixture: ApplicationClient, creator: Account) -> None: + client_fixture.deploy( + "v1", + create_args=ABICallArgs( + method="create", + ), + ) + + transfer( + TransferParameters(from_account=creator, to_address=client_fixture.app_address, amount=100_000), + client_fixture.algod_client, + ) + + assert client_fixture.call("hello", name="test").return_value == "Hello ABI, test" + + +def test_deploy_with_create_args(client_fixture: ApplicationClient, app_spec: ApplicationSpecification) -> None: + create_args = next(m for m in app_spec.contract.methods if m.name == "create_args") + client_fixture.deploy("v1", create_args=ABICallArgs(method=create_args, args={"greeting": "deployed"})) + + assert client_fixture.call("hello", name="test").return_value == "deployed, test" + + +def test_deploy_with_bare_create(client_fixture: ApplicationClient) -> None: + client_fixture.deploy( + "v1", + create_args=ABICallArgs( + method=False, + ), + ) + + assert client_fixture.call("hello", name="test").return_value == "Hello Bare, test" diff --git a/tests/test_app_client_resolve.py b/tests/test_app_client_resolve.py new file mode 100644 index 00000000..7dabb4b6 --- /dev/null +++ b/tests/test_app_client_resolve.py @@ -0,0 +1,50 @@ +from algokit_utils import ( + Account, + ApplicationClient, + DefaultArgumentDict, +) +from algosdk.v2client.algod import AlgodClient +from conftest import read_spec + + +def test_resolve(algod_client: AlgodClient, creator: Account) -> None: + app_spec = read_spec("app_resolve.json") + client_fixture = ApplicationClient(algod_client, app_spec, signer=creator) + client_fixture.create() + client_fixture.opt_in() + + int_default_argument: DefaultArgumentDict = {"source": "constant", "data": 1} + assert client_fixture.resolve(int_default_argument) == 1 + + string_default_argument: DefaultArgumentDict = {"source": "constant", "data": "stringy"} + assert client_fixture.resolve(string_default_argument) == "stringy" + + global_state_int_default_argument: DefaultArgumentDict = { + "source": "global-state", + "data": "global_state_val_int", + } + assert client_fixture.resolve(global_state_int_default_argument) == 1 + + global_state_byte_default_argument: DefaultArgumentDict = { + "source": "global-state", + "data": "global_state_val_byte", + } + assert client_fixture.resolve(global_state_byte_default_argument) == b"test" + + local_state_int_default_argument: DefaultArgumentDict = { + "source": "local-state", + "data": "acct_state_val_int", + } + assert client_fixture.resolve(local_state_int_default_argument) == 2 + + local_state_byte_default_argument: DefaultArgumentDict = { + "source": "local-state", + "data": "acct_state_val_byte", + } + assert client_fixture.resolve(local_state_byte_default_argument) == b"local-test" + + method_default_argument: DefaultArgumentDict = { + "source": "abi-method", + "data": {"name": "dummy", "args": [], "returns": {"type": "string"}}, + } + assert client_fixture.resolve(method_default_argument) == "deadbeef" From 7c4805819b1a504cd842a094be17767859b94d74 Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Fri, 24 Mar 2023 15:28:05 +0800 Subject: [PATCH 13/24] fix: add additional call parameters --- src/algokit_utils/__init__.py | 4 -- src/algokit_utils/application_client.py | 68 +++++++++++-------------- tests/test_app_client_create.py | 9 ++-- 3 files changed, 36 insertions(+), 45 deletions(-) diff --git a/src/algokit_utils/__init__.py b/src/algokit_utils/__init__.py index 33406411..2658fad1 100644 --- a/src/algokit_utils/__init__.py +++ b/src/algokit_utils/__init__.py @@ -30,8 +30,6 @@ CommonCallParameters, CommonCallParametersDict, DeployResponse, - FullCallParameters, - FullCallParametersDict, OnCompleteCallParameters, OnCompleteCallParametersDict, OnSchemaBreak, @@ -89,8 +87,6 @@ "CommonCallParametersDict", "OnCompleteCallParameters", "OnCompleteCallParametersDict", - "FullCallParameters", - "FullCallParametersDict", "ApplicationClient", "DeployResponse", "OnUpdate", diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index db64311c..432fba68 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -70,8 +70,6 @@ "CommonCallParametersDict", "OnCompleteCallParameters", "OnCompleteCallParametersDict", - "FullCallParameters", - "FullCallParametersDict", "DeployResponse", "OnUpdate", "OnSchemaBreak", @@ -136,6 +134,9 @@ class OperationPerformed(Enum): Replace = 3 +# TODO: consider using prepare so signer, sender are only defined at app_client instantiation + + @dataclasses.dataclass(kw_only=True) class TransactionResponse: tx_id: str @@ -167,15 +168,6 @@ class CommonCallParameters: suggested_params: transaction.SuggestedParams | None = None note: bytes | str | None = None lease: bytes | str | None = None - - -@dataclasses.dataclass(kw_only=True) -class OnCompleteCallParameters(CommonCallParameters): - on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC - - -@dataclasses.dataclass(kw_only=True) -class FullCallParameters(OnCompleteCallParameters): accounts: list[str] | None = None foreign_apps: list[int] | None = None foreign_assets: list[int] | None = None @@ -183,6 +175,11 @@ class FullCallParameters(OnCompleteCallParameters): rekey_to: str | None = None +@dataclasses.dataclass(kw_only=True) +class OnCompleteCallParameters(CommonCallParameters): + on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC + + class CommonCallParametersDict(TypedDict, total=False): signer: TransactionSigner sender: str @@ -691,11 +688,10 @@ def compose_call( atc: AtomicTransactionComposer, abi_method: Method | str | bool | None = None, *, - parameters: FullCallParameters | FullCallParametersDict | None = None, + parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, **kwargs: Any, ) -> None: """Adds a signed transaction with specified parameters to atc""" - _parameters = _convert_call_parameters(parameters) self._add_method_call( atc, @@ -703,11 +699,6 @@ def compose_call( abi_args=kwargs, parameters=_parameters, on_complete=_parameters.on_complete, - accounts=_parameters.accounts, - foreign_apps=_parameters.foreign_apps, - foreign_assets=_parameters.foreign_assets, - boxes=_parameters.boxes, - rekey_to=_parameters.rekey_to, ) @overload @@ -715,7 +706,7 @@ def call( self, abi_method: Method | str | Literal[True], *, - parameters: FullCallParameters | FullCallParametersDict | None = None, + parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, **kwargs: Any, ) -> ABITransactionResponse: ... @@ -725,7 +716,7 @@ def call( self, abi_method: Literal[False], *, - parameters: FullCallParameters | FullCallParametersDict | None = None, + parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, **kwargs: Any, ) -> TransactionResponse: ... @@ -734,7 +725,7 @@ def call( self, abi_method: Method | str | bool | None = None, *, - parameters: FullCallParameters | FullCallParametersDict | None = None, + parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, **kwargs: Any, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with specified parameters""" @@ -843,24 +834,28 @@ def compose_clear_state( atc: AtomicTransactionComposer, *, parameters: CommonCallParameters | CommonCallParametersDict | None = None, + app_args: list[bytes] | None = None, ) -> None: """Adds a signed transaction with on_complete=ClearState to atc""" return self._add_method_call( atc, parameters=parameters, on_complete=transaction.OnComplete.ClearStateOC, + app_args=app_args, ) def clear_state( self, *, parameters: CommonCallParameters | CommonCallParametersDict | None = None, + app_args: list[bytes] | None = None, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with on_complete=ClearState""" atc = AtomicTransactionComposer() self.compose_clear_state( atc, parameters=parameters, + app_args=app_args, ) return self._execute_atc_tr(atc) @@ -1007,11 +1002,7 @@ def _add_method_call( approval_program: bytes | None = None, clear_program: bytes | None = None, extra_pages: int | None = None, - accounts: list[str] | None = None, - foreign_apps: list[int] | None = None, - foreign_assets: list[int] | None = None, - boxes: Sequence[tuple[int, bytes | bytearray | str | int]] | None = None, - rekey_to: str | None = None, + app_args: list[bytes] | None = None, call_config: CallConfig = CallConfig.CALL, ) -> None: """Adds a transaction to the AtomicTransactionComposer passed""" @@ -1022,9 +1013,9 @@ def _add_method_call( method = self._resolve_method(abi_method, abi_args, on_complete, call_config) sp = parameters.suggested_params or self.suggested_params or self.algod_client.suggested_params() signer, sender = self._resolve_signer_sender(parameters.signer, parameters.sender) - if boxes is not None: + if parameters.boxes is not None: # TODO: algosdk actually does this, but it's type hints say otherwise... - encoded_boxes = [(id_, algosdk.encoding.encode_as_bytes(name)) for id_, name in boxes] + encoded_boxes = [(id_, algosdk.encoding.encode_as_bytes(name)) for id_, name in parameters.boxes] else: encoded_boxes = None @@ -1045,13 +1036,14 @@ def _add_method_call( global_schema=global_schema, local_schema=local_schema, extra_pages=extra_pages, - accounts=accounts, - foreign_apps=foreign_apps, - foreign_assets=foreign_assets, + accounts=parameters.accounts, + foreign_apps=parameters.foreign_apps, + foreign_assets=parameters.foreign_assets, boxes=encoded_boxes, note=parameters.note, lease=encoded_lease, - rekey_to=rekey_to, + rekey_to=parameters.rekey_to, + app_args=app_args, ), signer=signer, ) @@ -1098,13 +1090,13 @@ def _add_method_call( approval_program=approval_program, clear_program=clear_program, extra_pages=extra_pages or 0, - accounts=accounts, - foreign_apps=foreign_apps, - foreign_assets=foreign_assets, + accounts=parameters.accounts, + foreign_apps=parameters.foreign_apps, + foreign_assets=parameters.foreign_assets, boxes=encoded_boxes, note=parameters.note.encode("utf-8") if isinstance(parameters.note, str) else parameters.note, lease=encoded_lease, - rekey_to=rekey_to, + rekey_to=parameters.rekey_to, ) def _method_matches( @@ -1220,9 +1212,9 @@ def execute_atc_with_logic_error( raise ex -def _convert_call_parameters(args: CommonCallParameters | CommonCallParametersDict | None) -> FullCallParameters: +def _convert_call_parameters(args: CommonCallParameters | CommonCallParametersDict | None) -> OnCompleteCallParameters: _args = dataclasses.asdict(args) if isinstance(args, CommonCallParameters) else (args or {}) - return FullCallParameters(**_args) + return OnCompleteCallParameters(**_args) def _convert_deploy_args(args: ABICallArgs | ABICallArgsDict | None) -> ABICallArgs: diff --git a/tests/test_app_client_create.py b/tests/test_app_client_create.py index 01e77569..d30613c9 100644 --- a/tests/test_app_client_create.py +++ b/tests/test_app_client_create.py @@ -1,3 +1,4 @@ +import pytest from algokit_utils import ( ApplicationClient, ApplicationSpecification, @@ -19,9 +20,11 @@ def test_abi_create(client_fixture: ApplicationClient) -> None: assert client_fixture.call("hello", name="test").return_value == "Hello ABI, test" -def test_abi_create_args(client_fixture: ApplicationClient, app_spec: ApplicationSpecification) -> None: - create = next(m for m in app_spec.contract.methods if m.name == "create_args") - client_fixture.create(create, greeting="ahoy") +@pytest.mark.parametrize("method", ["create_args", "create_args(string)void", True]) +def test_abi_create_args( + method: str | bool, client_fixture: ApplicationClient, app_spec: ApplicationSpecification +) -> None: + client_fixture.create(method, greeting="ahoy") assert client_fixture.call("hello", name="test").return_value == "ahoy, test" From 25a3f864547a5ca9350a5d116fa77ef33acebb63 Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Fri, 24 Mar 2023 17:26:06 +0800 Subject: [PATCH 14/24] feat: move template substitution to constructor --- src/algokit_utils/__init__.py | 24 +- src/algokit_utils/application_client.py | 267 ++++++++---------- src/algokit_utils/{app.py => deploy.py} | 98 ++++++- ...reak_equals_replace_app_fails.approved.txt | 2 +- ..._existing_permanent_app_fails.approved.txt | 2 +- ...il-Updatable.No-Deletable.No].approved.txt | 2 +- ...l-Updatable.No-Deletable.Yes].approved.txt | 2 +- ...l-Updatable.Yes-Deletable.No].approved.txt | 2 +- ...-Updatable.Yes-Deletable.Yes].approved.txt | 2 +- ...pp-Updatable.No-Deletable.No].approved.txt | 2 +- ...p-Updatable.No-Deletable.Yes].approved.txt | 2 +- ...p-Updatable.Yes-Deletable.No].approved.txt | 2 +- ...-Updatable.Yes-Deletable.Yes].approved.txt | 2 +- 13 files changed, 233 insertions(+), 176 deletions(-) rename src/algokit_utils/{app.py => deploy.py} (70%) diff --git a/src/algokit_utils/__init__.py b/src/algokit_utils/__init__.py index 2658fad1..31b091fa 100644 --- a/src/algokit_utils/__init__.py +++ b/src/algokit_utils/__init__.py @@ -10,18 +10,6 @@ get_or_create_kmd_wallet_account, get_sandbox_default_account, ) -from algokit_utils.app import ( - DELETABLE_TEMPLATE_NAME, - NOTE_PREFIX, - UPDATABLE_TEMPLATE_NAME, - AppDeployMetaData, - AppLookup, - AppMetaData, - AppReference, - DeploymentFailedError, - get_creator_apps, - replace_template_variables, -) from algokit_utils.application_client import ( ABICallArgs, ABICallArgsDict, @@ -52,6 +40,18 @@ MethodHints, OnCompleteActionName, ) +from algokit_utils.deploy import ( + DELETABLE_TEMPLATE_NAME, + NOTE_PREFIX, + UPDATABLE_TEMPLATE_NAME, + AppDeployMetaData, + AppLookup, + AppMetaData, + AppReference, + DeploymentFailedError, + get_creator_apps, + replace_template_variables, +) from algokit_utils.logic_error import LogicError from algokit_utils.models import Account from algokit_utils.network_clients import ( diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index 432fba68..321522c1 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -28,31 +28,12 @@ from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient -from algokit_utils.app import ( - DELETABLE_TEMPLATE_NAME, - UPDATABLE_TEMPLATE_NAME, - AppDeployMetaData, - AppLookup, - AppMetaData, - AppReference, - DeploymentFailedError, - TemplateValueDict, - _add_deploy_template_variables, - _check_template_variables, - _schema_is_less, - _schema_str, - _state_schema, - _strip_comments, - get_creator_apps, - replace_template_variables, -) +import algokit_utils.deploy as au_deploy from algokit_utils.application_specification import ( ApplicationSpecification, CallConfig, DefaultArgumentDict, - MethodConfigDict, MethodHints, - OnCompleteActionName, ) from algokit_utils.logic_error import LogicError, parse_logic_error from algokit_utils.models import Account @@ -134,9 +115,6 @@ class OperationPerformed(Enum): Replace = 3 -# TODO: consider using prepare so signer, sender are only defined at app_client instantiation - - @dataclasses.dataclass(kw_only=True) class TransactionResponse: tx_id: str @@ -145,7 +123,7 @@ class TransactionResponse: @dataclasses.dataclass(kw_only=True) class DeployResponse: - app: AppMetaData + app: au_deploy.AppMetaData create_response: TransactionResponse | None = None delete_response: TransactionResponse | None = None update_response: TransactionResponse | None = None @@ -211,6 +189,7 @@ def __init__( signer: TransactionSigner | Account | None = None, sender: str | None = None, suggested_params: transaction.SuggestedParams | None = None, + template_values: au_deploy.TemplateValueDict | None = None, ): ... @@ -222,10 +201,11 @@ def __init__( *, creator: str | Account, indexer_client: IndexerClient | None = None, - existing_deployments: AppLookup | None = None, + existing_deployments: au_deploy.AppLookup | None = None, signer: TransactionSigner | Account | None = None, sender: str | None = None, suggested_params: transaction.SuggestedParams | None = None, + template_values: au_deploy.TemplateValueDict | None = None, ): ... @@ -237,15 +217,28 @@ def __init__( app_id: int = 0, creator: str | Account | None = None, indexer_client: IndexerClient | None = None, - existing_deployments: AppLookup | None = None, + existing_deployments: au_deploy.AppLookup | None = None, signer: TransactionSigner | Account | None = None, sender: str | None = None, suggested_params: transaction.SuggestedParams | None = None, + template_values: au_deploy.TemplateValueDict | None = None, ): self.algod_client = algod_client self.app_spec = app_spec - self._approval_program: Program | None = None - self._clear_program: Program | None = None + self._approval_program: Program | None + self._clear_program: Program | None + + if template_values: + self._approval_program, self._clear_program = substitute_template_and_compile( + self.algod_client, app_spec, template_values + ) + elif not au_deploy.has_template_vars(app_spec): + self._approval_program = Program(self.app_spec.approval_program, self.algod_client) + self._clear_program = Program(self.app_spec.clear_program, self.algod_client) + else: # can't compile programs yet + self._approval_program = None + self._clear_program = None + self.approval_source_map: SourceMap | None = None self.existing_deployments = existing_deployments self._indexer_client = indexer_client @@ -295,6 +288,37 @@ def approval(self) -> Program | None: def clear(self) -> Program | None: return self._clear_program + def prepare( + self, + signer: TransactionSigner | Account | None = None, + sender: str | None = None, + app_id: int | None = None, + template_values: au_deploy.TemplateValueDict | None = None, + ) -> "ApplicationClient": + import copy + + new_client = copy.copy(self) + self._prepare(new_client, signer=signer, sender=sender, app_id=app_id, template_values=template_values) + return new_client + + def _prepare( + self, + target: "ApplicationClient", + signer: TransactionSigner | Account | None = None, + sender: str | None = None, + app_id: int | None = None, + template_values: au_deploy.TemplateValueDict | None = None, + ) -> None: + target.app_id = self.app_id if app_id is None else app_id + if signer or sender: + target.signer, target.sender = target._resolve_signer_sender( + AccountTransactionSigner(signer.private_key) if isinstance(signer, Account) else signer, sender + ) + if template_values: + target._approval_program, target._clear_program = substitute_template_and_compile( + target.algod_client, target.app_spec, template_values + ) + def deploy( self, version: str | None = None, @@ -305,21 +329,23 @@ def deploy( allow_delete: bool | None = None, on_update: OnUpdate = OnUpdate.Fail, on_schema_break: OnSchemaBreak = OnSchemaBreak.Fail, - template_values: TemplateValueDict | None = None, + template_values: au_deploy.TemplateValueDict | None = None, create_args: ABICallArgs | ABICallArgsDict | None = None, update_args: ABICallArgs | ABICallArgsDict | None = None, delete_args: ABICallArgs | ABICallArgsDict | None = None, ) -> DeployResponse: """Ensures app associated with app client's creator is present and up to date""" if self.app_id: - raise DeploymentFailedError(f"Attempt to deploy app which already has an app index of {self.app_id}") + raise au_deploy.DeploymentFailedError( + f"Attempt to deploy app which already has an app index of {self.app_id}" + ) signer, sender = self._resolve_signer_sender(signer, sender) if not sender: - raise DeploymentFailedError("No sender provided, unable to deploy app") + raise au_deploy.DeploymentFailedError("No sender provided, unable to deploy app") if not self._creator: - raise DeploymentFailedError("No creator provided, unable to deploy app") + raise au_deploy.DeploymentFailedError("No creator provided, unable to deploy app") if self._creator != sender: - raise DeploymentFailedError( + raise au_deploy.DeploymentFailedError( f"Attempt to deploy contract with a sender address {sender} that differs " f"from the given creator address for this application client: {self._creator}" ) @@ -330,18 +356,25 @@ def deploy( # make a copy template_values = dict(template_values or {}) - _add_deploy_template_variables(template_values, allow_update=allow_update, allow_delete=allow_delete) - approval_program, clear_program = self._substitute_template_and_compile(template_values) + au_deploy.add_deploy_template_variables(template_values, allow_update=allow_update, allow_delete=allow_delete) + + # TODO: how to undo this if deployment fails?! + self._prepare(self, template_values=template_values, sender=sender, signer=signer) + approval_program, clear_program = self._check_is_compiled() updatable = ( allow_update if allow_update is not None - else _get_deploy_control(self.app_spec, UPDATABLE_TEMPLATE_NAME, transaction.OnComplete.UpdateApplicationOC) + else au_deploy.get_deploy_control( + self.app_spec, au_deploy.UPDATABLE_TEMPLATE_NAME, transaction.OnComplete.UpdateApplicationOC + ) ) deletable = ( allow_delete if allow_delete is not None - else _get_deploy_control(self.app_spec, DELETABLE_TEMPLATE_NAME, transaction.OnComplete.DeleteApplicationOC) + else au_deploy.get_deploy_control( + self.app_spec, au_deploy.DELETABLE_TEMPLATE_NAME, transaction.OnComplete.DeleteApplicationOC + ) ) name = self.app_spec.contract.name @@ -353,14 +386,16 @@ def deploy( if app.app_id == 0: version = "v1.0" else: - assert isinstance(app, AppDeployMetaData) + assert isinstance(app, au_deploy.AppDeployMetaData) version = get_next_version(app.version) - app_spec_note = AppDeployMetaData(name, version, updatable=updatable, deletable=deletable) + app_spec_note = au_deploy.AppDeployMetaData(name, version, updatable=updatable, deletable=deletable) def create_metadata( - created_round: int, updated_round: int | None = None, original_metadata: AppDeployMetaData | None = None - ) -> AppMetaData: - app_metadata = AppMetaData( + created_round: int, + updated_round: int | None = None, + original_metadata: au_deploy.AppDeployMetaData | None = None, + ) -> au_deploy.AppMetaData: + app_metadata = au_deploy.AppMetaData( app_id=self.app_id, app_address=self.app_address, created_metadata=original_metadata or app_spec_note, @@ -379,7 +414,6 @@ def create_app() -> DeployResponse: abi_method=_create_args.method, **_create_args.args, parameters=_add_lease_parameter(common_parameters, _create_args.lease), - template_values=template_values, ) logger.info(f"{name} ({version}) deployed successfully, with app id {self.app_id}.") assert create_response.confirmed_round is not None @@ -393,31 +427,20 @@ def create_app() -> DeployResponse: logger.info(f"{name} not found in {self._creator} account, deploying app.") return create_app() - assert isinstance(app, AppMetaData) + assert isinstance(app, au_deploy.AppMetaData) logger.debug(f"{name} found in {self._creator} account, with app id {app.app_id}, version={app.version}.") - application_info = self.algod_client.application_info(app.app_id) - assert isinstance(application_info, dict) - application_create_params = application_info["params"] - - current_approval = base64.b64decode(application_create_params["approval-program"]) - current_clear = base64.b64decode(application_create_params["clear-state-program"]) - current_global_schema = _state_schema(application_create_params["global-state-schema"]) - current_local_schema = _state_schema(application_create_params["local-state-schema"]) - - required_global_schema = self.app_spec.global_state_schema - required_local_schema = self.app_spec.local_state_schema - new_approval = approval_program.raw_binary - new_clear = clear_program.raw_binary - - app_updated = current_approval != new_approval or current_clear != new_clear - - schema_breaking_change = _schema_is_less(current_global_schema, required_global_schema) or _schema_is_less( - current_local_schema, required_local_schema + app_changes = au_deploy.check_for_app_changes( + self.algod_client, + new_approval=approval_program.raw_binary, + new_clear=clear_program.raw_binary, + new_global_schema=self.app_spec.global_state_schema, + new_local_schema=self.app_spec.local_state_schema, + app_id=app.app_id, ) def create_and_delete_app() -> DeployResponse: - assert isinstance(app, AppMetaData) + assert isinstance(app, au_deploy.AppMetaData) assert self.existing_deployments logger.info(f"Replacing {name} ({app.version}) with {name} ({version}) in {self._creator} account.") atc = AtomicTransactionComposer() @@ -426,7 +449,6 @@ def create_and_delete_app() -> DeployResponse: abi_method=_create_args.method, **_create_args.args, parameters=_add_lease_parameter(common_parameters, _create_args.lease), - template_values=template_values, ) self.compose_delete( atc, @@ -453,14 +475,13 @@ def create_and_delete_app() -> DeployResponse: def update_app() -> DeployResponse: assert on_update == OnUpdate.UpdateApp - assert isinstance(app, AppMetaData) + assert isinstance(app, au_deploy.AppMetaData) assert self.existing_deployments logger.info(f"Updating {name} to {version} in {self._creator} account, with app id {app.app_id}") update_response = self.update( abi_method=_update_args.method, **_update_args.args, parameters=_add_lease_parameter(common_parameters, lease=_update_args.lease), - template_values=template_values, ) app_metadata = create_metadata( app.created_round, updated_round=update_response.confirmed_round, original_metadata=app.created_metadata @@ -470,15 +491,11 @@ def update_app() -> DeployResponse: app=app_metadata, update_response=update_response, action_taken=OperationPerformed.Update ) - if schema_breaking_change: - logger.warning( - f"Detected a breaking app schema change from: " - f"{_schema_str(current_global_schema, current_local_schema)} to " - f"{_schema_str(required_global_schema, required_local_schema)}." - ) + if app_changes.schema_breaking_change: + logger.warning(f"Detected a breaking app schema change: {app_changes.schema_change_description}") if on_schema_break == OnSchemaBreak.Fail: - raise DeploymentFailedError( + raise au_deploy.DeploymentFailedError( "Schema break detected and on_schema_break=OnSchemaBreak.Fail, stopping deployment. " "If you want to try deleting and recreating the app then " "re-run with on_schema_break=OnSchemaBreak.ReplaceApp" @@ -497,11 +514,11 @@ def update_app() -> DeployResponse: "Cannot determine if App is deletable but on_schema_break=ReplaceApp, " "will attempt to delete app" ) return create_and_delete_app() - elif app_updated: + elif app_changes.app_updated: logger.info(f"Detected a TEAL update in app id {app.app_id}") if on_update == OnUpdate.Fail: - raise DeploymentFailedError( + raise au_deploy.DeploymentFailedError( "Update detected and on_update=Fail, stopping deployment. " "If you want to try updating the app then re-run with on_update=UpdateApp" ) @@ -541,19 +558,24 @@ def update_app() -> DeployResponse: return DeployResponse(app=app) + def _check_is_compiled(self) -> tuple[Program, Program]: + if self._approval_program is None or self._clear_program is None: + raise Exception( + "Compiled programs are not available, " "please provide template_values before creating or updating" + ) + return self._approval_program, self._clear_program + def compose_create( self, atc: AtomicTransactionComposer, abi_method: Method | str | bool | None = None, *, parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, - template_values: TemplateValueDict | None = None, extra_pages: int | None = None, **kwargs: Any, - ) -> tuple[Program, Program]: + ) -> None: """Adds a signed transaction with application id == 0 and the schema and source of client's app_spec to atc""" - - approval_program, clear_program = self._substitute_template_and_compile(template_values) + approval_program, clear_program = self._check_is_compiled() if extra_pages is None: extra_pages = num_extra_program_pages(approval_program.raw_binary, clear_program.raw_binary) @@ -574,14 +596,11 @@ def compose_create( extra_pages=extra_pages, ) - return approval_program, clear_program - def create( self, abi_method: Method | str | bool | None = None, *, parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, - template_values: TemplateValueDict | None = None, extra_pages: int | None = None, **kwargs: Any, ) -> TransactionResponse | ABITransactionResponse: @@ -589,11 +608,10 @@ def create( atc = AtomicTransactionComposer() - self._approval_program, self._clear_program = self.compose_create( + self.compose_create( atc, abi_method, parameters=parameters, - template_values=template_values, extra_pages=extra_pages, **kwargs, ) @@ -607,12 +625,10 @@ def compose_update( abi_method: Method | str | bool | None = None, *, parameters: CommonCallParameters | CommonCallParametersDict | None = None, - template_values: TemplateValueDict | None = None, **kwargs: Any, - ) -> tuple[Program, Program]: + ) -> None: """Adds a signed transaction with on_complete=UpdateApplication to atc""" - - approval_program, clear_program = self._substitute_template_and_compile(template_values) + approval_program, clear_program = self._check_is_compiled() self._add_method_call( atc=atc, @@ -624,24 +640,20 @@ def compose_update( clear_program=clear_program.raw_binary, ) - return approval_program, clear_program - def update( self, abi_method: Method | str | bool | None = None, *, parameters: CommonCallParameters | CommonCallParametersDict | None = None, - template_values: TemplateValueDict | None = None, **kwargs: Any, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with on_complete=UpdateApplication""" atc = AtomicTransactionComposer() - self._approval_program, self._clear_program = self.compose_update( + self.compose_update( atc, abi_method, parameters=parameters, - template_values=template_values, **kwargs, ) return self._execute_atc_tr(atc) @@ -912,10 +924,10 @@ def _load_reference_and_check_app_id(self) -> None: self._load_app_reference() self._check_app_id() - def _load_app_reference(self) -> AppReference | AppMetaData: + def _load_app_reference(self) -> au_deploy.AppReference | au_deploy.AppMetaData: if not self.existing_deployments and self._creator: assert self._indexer_client - self.existing_deployments = get_creator_apps(self._indexer_client, self._creator) + self.existing_deployments = au_deploy.get_creator_apps(self._indexer_client, self._creator) if self.existing_deployments and self.app_id == 0: app = self.existing_deployments.apps.get(self.app_spec.contract.name) @@ -923,7 +935,7 @@ def _load_app_reference(self) -> AppReference | AppMetaData: self.app_id = app.app_id return app - return AppReference(self.app_id, self.app_address) + return au_deploy.AppReference(self.app_id, self.app_address) def _check_app_id(self) -> None: if self.app_id == 0: @@ -947,7 +959,7 @@ def _resolve_method( return self._resolve_abi_method(abi_method) case bool() | None: # find abi method has_bare_config = ( - call_config in _get_call_config(self.app_spec.bare_call_config, on_complete) + call_config in au_deploy.get_call_config(self.app_spec.bare_call_config, on_complete) or on_complete == transaction.OnComplete.ClearStateOC ) abi_methods = self._find_abi_methods(args, on_complete, call_config) @@ -969,20 +981,6 @@ def _resolve_method( f"Could not find any methods to use for {on_complete.name} with call_config of {call_config.name}" ) - def _substitute_template_and_compile( - self, - template_values: TemplateValueDict | None, - ) -> tuple[Program, Program]: - template_values = dict(template_values or {}) - clear = replace_template_variables(self.app_spec.clear_program, template_values) - - _check_template_variables(self.app_spec.approval_program, template_values) - approval = replace_template_variables(self.app_spec.approval_program, template_values) - - self._approval_program = Program(approval, self.algod_client) - self._clear_program = Program(clear, self.algod_client) - return self._approval_program, self._clear_program - def _get_approval_source_map(self) -> SourceMap | None: if self.approval: return self.approval.source_map @@ -1103,7 +1101,7 @@ def _method_matches( self, method: Method, args: ABIArgsDict | None, on_complete: transaction.OnComplete, call_config: CallConfig ) -> bool: hints = self._method_hints(method) - if call_config not in _get_call_config(hints.call_config, on_complete): + if call_config not in au_deploy.get_call_config(hints.call_config, on_complete): return False method_args = {m.name for m in method.args} provided_args = set(args or {}) | set(hints.default_arguments) @@ -1166,6 +1164,20 @@ def _resolve_signer_sender( return resolved_signer, resolved_sender +def substitute_template_and_compile( + algod_client: AlgodClient, + app_spec: ApplicationSpecification, + template_values: au_deploy.TemplateValueDict, +) -> tuple[Program, Program]: + template_values = dict(template_values or {}) + clear = au_deploy.replace_template_variables(app_spec.clear_program, template_values) + + au_deploy.check_template_variables(app_spec.approval_program, template_values) + approval = au_deploy.replace_template_variables(app_spec.approval_program, template_values) + + return Program(approval, algod_client), Program(clear, algod_client) + + def get_app_id_from_tx_id(algod_client: AlgodClient, tx_id: str) -> int: result = algod_client.pending_transaction_info(tx_id) assert isinstance(result, dict) @@ -1185,7 +1197,7 @@ def replacement(m: re.Match) -> str: return f"{m.group('prefix')}{new_version}{m.group('suffix')}" return re.sub(pattern, replacement, current_version) - raise DeploymentFailedError( + raise au_deploy.DeploymentFailedError( f"Could not auto increment {current_version}, please specify the next version using the version parameter" ) @@ -1317,25 +1329,6 @@ def _increment_version(version: str) -> str: return ".".join(str(x) for x in split) -def _get_call_config(method_config: MethodConfigDict, on_complete: transaction.OnComplete) -> CallConfig: - def get(key: OnCompleteActionName) -> CallConfig: - return method_config.get(key, CallConfig.NEVER) - - match on_complete: - case transaction.OnComplete.NoOpOC: - return get("no_op") - case transaction.OnComplete.UpdateApplicationOC: - return get("update_application") - case transaction.OnComplete.DeleteApplicationOC: - return get("delete_application") - case transaction.OnComplete.OptInOC: - return get("opt_in") - case transaction.OnComplete.CloseOutOC: - return get("close_out") - case transaction.OnComplete.ClearStateOC: - return get("clear_state") - - def _str_or_hex(v: bytes) -> str: decoded: str try: @@ -1372,16 +1365,6 @@ def _decode_state(state: list[dict[str, Any]], *, raw: bool = False) -> dict[str return decoded_state -def _get_deploy_control( - app_spec: ApplicationSpecification, template_var: str, on_complete: transaction.OnComplete -) -> bool | None: - if template_var not in _strip_comments(app_spec.approval_program): - return None - return _get_call_config(app_spec.bare_call_config, on_complete) != CallConfig.NEVER or any( - h for h in app_spec.hints.values() if _get_call_config(h.call_config, on_complete) != CallConfig.NEVER - ) - - def _tr_from_atr( atc: AtomicTransactionComposer, result: AtomicTransactionResponse, transaction_index: int = 0 ) -> TransactionResponse: diff --git a/src/algokit_utils/app.py b/src/algokit_utils/deploy.py similarity index 70% rename from src/algokit_utils/app.py rename to src/algokit_utils/deploy.py index 2afd9bd5..bc78f7ad 100644 --- a/src/algokit_utils/app.py +++ b/src/algokit_utils/deploy.py @@ -3,12 +3,20 @@ import json import logging import re -from collections.abc import Mapping +from collections.abc import Iterable, Mapping +from algosdk import transaction from algosdk.logic import get_application_address from algosdk.transaction import StateSchema +from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient +from algokit_utils.application_specification import ( + ApplicationSpecification, + CallConfig, + MethodConfigDict, + OnCompleteActionName, +) from algokit_utils.models import Account __all__ = [ @@ -130,9 +138,9 @@ def get_creator_apps(indexer: IndexerClient, creator_account: Account | str) -> if t["application-transaction"]["application-id"] == 0 and t["sender"] == creator_address ) - def sort_by_round(transaction: dict) -> tuple[int, int]: - confirmed = transaction["confirmed-round"] - offset = transaction["intra-round-offset"] + def sort_by_round(txn: dict) -> tuple[int, int]: + confirmed = txn["confirmed-round"] + offset = txn["intra-round-offset"] return confirmed, offset transactions.sort(key=sort_by_round, reverse=True) @@ -174,14 +182,47 @@ def _state_schema(schema: dict[str, int]) -> StateSchema: return StateSchema(schema.get("num-uint", 0), schema.get("num-byte-slice", 0)) # type: ignore[no-untyped-call] -def _schema_is_less(a: StateSchema, b: StateSchema) -> bool: - return bool(a.num_uints < b.num_uints or a.num_byte_slices < b.num_byte_slices) +def _describe_schema_breaks(prefix: str, from_schema: StateSchema, to_schema: StateSchema) -> Iterable[str]: + if to_schema.num_uints > from_schema.num_uints: + yield f"{prefix} uints increased from {from_schema.num_uints} to {to_schema.num_uints}" + if to_schema.num_byte_slices > from_schema.num_byte_slices: + yield f"{prefix} byte slices increased from {from_schema.num_byte_slices} to {to_schema.num_byte_slices}" -def _schema_str(global_schema: StateSchema, local_schema: StateSchema) -> str: - return ( - f"Global: uints={global_schema.num_uints}, byte_slices={global_schema.num_byte_slices}, " - f"Local: uints={local_schema.num_uints}, byte_slices={local_schema.num_byte_slices}" +@dataclasses.dataclass(kw_only=True) +class AppChanges: + app_updated: bool + schema_breaking_change: bool + schema_change_description: str | None + + +def check_for_app_changes( + algod_client: AlgodClient, + new_approval: bytes, + new_clear: bytes, + new_global_schema: StateSchema, + new_local_schema: StateSchema, + app_id: int, +) -> AppChanges: + application_info = algod_client.application_info(app_id) + assert isinstance(application_info, dict) + application_create_params = application_info["params"] + + current_approval = base64.b64decode(application_create_params["approval-program"]) + current_clear = base64.b64decode(application_create_params["clear-state-program"]) + current_global_schema = _state_schema(application_create_params["global-state-schema"]) + current_local_schema = _state_schema(application_create_params["local-state-schema"]) + + app_updated = current_approval != new_approval or current_clear != new_clear + + schema_changes: list[str] = [] + schema_changes.extend(_describe_schema_breaks("Global", current_global_schema, new_global_schema)) + schema_changes.extend(_describe_schema_breaks("Local", current_local_schema, new_local_schema)) + + return AppChanges( + app_updated=app_updated, + schema_breaking_change=bool(schema_changes), + schema_change_description=", ".join(schema_changes), ) @@ -204,7 +245,7 @@ def replacement(m: re.Match) -> str: return result, match_count -def _add_deploy_template_variables( +def add_deploy_template_variables( template_values: TemplateValueDict, allow_update: bool | None, allow_delete: bool | None ) -> None: if allow_update is not None: @@ -218,7 +259,7 @@ def _strip_comments(program: str) -> str: return "\n".join(lines) -def _check_template_variables(approval_program: str, template_values: TemplateValueDict) -> None: +def check_template_variables(approval_program: str, template_values: TemplateValueDict) -> None: approval_program = _strip_comments(approval_program) if UPDATABLE_TEMPLATE_NAME in approval_program and _UPDATABLE not in template_values: raise DeploymentFailedError( @@ -260,3 +301,36 @@ def replace_template_variables(program: str, template_values: TemplateValueMappi program_lines, matches = _replace_template_variable(program_lines, template_variable_name, value) return "\n".join(program_lines) + + +def has_template_vars(app_spec: ApplicationSpecification) -> bool: + return "TMPL_" in _strip_comments(app_spec.approval_program) or "TMPL_" in _strip_comments(app_spec.clear_program) + + +def get_deploy_control( + app_spec: ApplicationSpecification, template_var: str, on_complete: transaction.OnComplete +) -> bool | None: + if template_var not in _strip_comments(app_spec.approval_program): + return None + return get_call_config(app_spec.bare_call_config, on_complete) != CallConfig.NEVER or any( + h for h in app_spec.hints.values() if get_call_config(h.call_config, on_complete) != CallConfig.NEVER + ) + + +def get_call_config(method_config: MethodConfigDict, on_complete: transaction.OnComplete) -> CallConfig: + def get(key: OnCompleteActionName) -> CallConfig: + return method_config.get(key, CallConfig.NEVER) + + match on_complete: + case transaction.OnComplete.NoOpOC: + return get("no_op") + case transaction.OnComplete.UpdateApplicationOC: + return get("update_application") + case transaction.OnComplete.DeleteApplicationOC: + return get("delete_application") + case transaction.OnComplete.OptInOC: + return get("opt_in") + case transaction.OnComplete.CloseOutOC: + return get("close_out") + case transaction.OnComplete.ClearStateOC: + return get("clear_state") diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_and_on_schema_break_equals_replace_app_fails.approved.txt b/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_and_on_schema_break_equals_replace_app_fails.approved.txt index 761d83b2..9f4a0361 100644 --- a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_and_on_schema_break_equals_replace_app_fails.approved.txt +++ b/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_and_on_schema_break_equals_replace_app_fails.approved.txt @@ -1,7 +1,7 @@ INFO: SampleApp not found in {creator_account} account, deploying app. INFO: SampleApp (1.0) deployed successfully, with app id {app0}. DEBUG: SampleApp found in {creator_account} account, with app id {app0}, version=1.0. -WARNING: Detected a breaking app schema change from: Global: uints=0, byte_slices=0, Local: uints=0, byte_slices=0 to Global: uints=1, byte_slices=0, Local: uints=0, byte_slices=0. +WARNING: Detected a breaking app schema change: Global uints increased from 0 to 1 WARNING: App is not deletable but on_schema_break=ReplaceApp, will attempt to delete app, delete will most likely fail INFO: Replacing SampleApp (1.0) with SampleApp (3.0) in {creator_account} account. ERROR: Deployment failed: assert failed pc=153 \ No newline at end of file diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_fails.approved.txt b/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_fails.approved.txt index e466c9f1..cc5ea20d 100644 --- a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_fails.approved.txt +++ b/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_permanent_app_fails.approved.txt @@ -1,5 +1,5 @@ INFO: SampleApp not found in {creator_account} account, deploying app. INFO: SampleApp (1.0) deployed successfully, with app id {app0}. DEBUG: SampleApp found in {creator_account} account, with app id {app0}, version=1.0. -WARNING: Detected a breaking app schema change from: Global: uints=0, byte_slices=0, Local: uints=0, byte_slices=0 to Global: uints=1, byte_slices=0, Local: uints=0, byte_slices=0. +WARNING: Detected a breaking app schema change: Global uints increased from 0 to 1 ERROR: DeploymentFailedError: Schema break detected and on_schema_break=OnSchemaBreak.Fail, stopping deployment. If you want to try deleting and recreating the app then re-run with on_schema_break=OnSchemaBreak.ReplaceApp \ No newline at end of file diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.No].approved.txt b/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.No].approved.txt index e466c9f1..cc5ea20d 100644 --- a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.No].approved.txt +++ b/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.No].approved.txt @@ -1,5 +1,5 @@ INFO: SampleApp not found in {creator_account} account, deploying app. INFO: SampleApp (1.0) deployed successfully, with app id {app0}. DEBUG: SampleApp found in {creator_account} account, with app id {app0}, version=1.0. -WARNING: Detected a breaking app schema change from: Global: uints=0, byte_slices=0, Local: uints=0, byte_slices=0 to Global: uints=1, byte_slices=0, Local: uints=0, byte_slices=0. +WARNING: Detected a breaking app schema change: Global uints increased from 0 to 1 ERROR: DeploymentFailedError: Schema break detected and on_schema_break=OnSchemaBreak.Fail, stopping deployment. If you want to try deleting and recreating the app then re-run with on_schema_break=OnSchemaBreak.ReplaceApp \ No newline at end of file diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.Yes].approved.txt b/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.Yes].approved.txt index e466c9f1..cc5ea20d 100644 --- a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.Yes].approved.txt +++ b/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.No-Deletable.Yes].approved.txt @@ -1,5 +1,5 @@ INFO: SampleApp not found in {creator_account} account, deploying app. INFO: SampleApp (1.0) deployed successfully, with app id {app0}. DEBUG: SampleApp found in {creator_account} account, with app id {app0}, version=1.0. -WARNING: Detected a breaking app schema change from: Global: uints=0, byte_slices=0, Local: uints=0, byte_slices=0 to Global: uints=1, byte_slices=0, Local: uints=0, byte_slices=0. +WARNING: Detected a breaking app schema change: Global uints increased from 0 to 1 ERROR: DeploymentFailedError: Schema break detected and on_schema_break=OnSchemaBreak.Fail, stopping deployment. If you want to try deleting and recreating the app then re-run with on_schema_break=OnSchemaBreak.ReplaceApp \ No newline at end of file diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.No].approved.txt b/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.No].approved.txt index e466c9f1..cc5ea20d 100644 --- a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.No].approved.txt +++ b/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.No].approved.txt @@ -1,5 +1,5 @@ INFO: SampleApp not found in {creator_account} account, deploying app. INFO: SampleApp (1.0) deployed successfully, with app id {app0}. DEBUG: SampleApp found in {creator_account} account, with app id {app0}, version=1.0. -WARNING: Detected a breaking app schema change from: Global: uints=0, byte_slices=0, Local: uints=0, byte_slices=0 to Global: uints=1, byte_slices=0, Local: uints=0, byte_slices=0. +WARNING: Detected a breaking app schema change: Global uints increased from 0 to 1 ERROR: DeploymentFailedError: Schema break detected and on_schema_break=OnSchemaBreak.Fail, stopping deployment. If you want to try deleting and recreating the app then re-run with on_schema_break=OnSchemaBreak.ReplaceApp \ No newline at end of file diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.Yes].approved.txt b/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.Yes].approved.txt index e466c9f1..cc5ea20d 100644 --- a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.Yes].approved.txt +++ b/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.Fail-Updatable.Yes-Deletable.Yes].approved.txt @@ -1,5 +1,5 @@ INFO: SampleApp not found in {creator_account} account, deploying app. INFO: SampleApp (1.0) deployed successfully, with app id {app0}. DEBUG: SampleApp found in {creator_account} account, with app id {app0}, version=1.0. -WARNING: Detected a breaking app schema change from: Global: uints=0, byte_slices=0, Local: uints=0, byte_slices=0 to Global: uints=1, byte_slices=0, Local: uints=0, byte_slices=0. +WARNING: Detected a breaking app schema change: Global uints increased from 0 to 1 ERROR: DeploymentFailedError: Schema break detected and on_schema_break=OnSchemaBreak.Fail, stopping deployment. If you want to try deleting and recreating the app then re-run with on_schema_break=OnSchemaBreak.ReplaceApp \ No newline at end of file diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.No].approved.txt b/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.No].approved.txt index e591c4df..d6059fe8 100644 --- a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.No].approved.txt +++ b/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.No].approved.txt @@ -1,7 +1,7 @@ INFO: SampleApp not found in {creator_account} account, deploying app. INFO: SampleApp (1.0) deployed successfully, with app id {app0}. DEBUG: SampleApp found in {creator_account} account, with app id {app0}, version=1.0. -WARNING: Detected a breaking app schema change from: Global: uints=0, byte_slices=0, Local: uints=0, byte_slices=0 to Global: uints=1, byte_slices=0, Local: uints=0, byte_slices=0. +WARNING: Detected a breaking app schema change: Global uints increased from 0 to 1 WARNING: App is not deletable but on_schema_break=ReplaceApp, will attempt to delete app, delete will most likely fail INFO: Replacing SampleApp (1.0) with SampleApp (3.0) in {creator_account} account. ERROR: LogicException: assert failed pc=153 \ No newline at end of file diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt b/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt index 5c34930f..5ffb7726 100644 --- a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt +++ b/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.No-Deletable.Yes].approved.txt @@ -1,7 +1,7 @@ INFO: SampleApp not found in {creator_account} account, deploying app. INFO: SampleApp (1.0) deployed successfully, with app id {app0}. DEBUG: SampleApp found in {creator_account} account, with app id {app0}, version=1.0. -WARNING: Detected a breaking app schema change from: Global: uints=0, byte_slices=0, Local: uints=0, byte_slices=0 to Global: uints=1, byte_slices=0, Local: uints=0, byte_slices=0. +WARNING: Detected a breaking app schema change: Global uints increased from 0 to 1 INFO: App is deletable and on_schema_break=ReplaceApp, will attempt to create new app and delete old app INFO: Replacing SampleApp (1.0) with SampleApp (3.0) in {creator_account} account. INFO: SampleApp (3.0) deployed successfully, with app id {app1}. diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt b/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt index e591c4df..d6059fe8 100644 --- a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt +++ b/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.No].approved.txt @@ -1,7 +1,7 @@ INFO: SampleApp not found in {creator_account} account, deploying app. INFO: SampleApp (1.0) deployed successfully, with app id {app0}. DEBUG: SampleApp found in {creator_account} account, with app id {app0}, version=1.0. -WARNING: Detected a breaking app schema change from: Global: uints=0, byte_slices=0, Local: uints=0, byte_slices=0 to Global: uints=1, byte_slices=0, Local: uints=0, byte_slices=0. +WARNING: Detected a breaking app schema change: Global uints increased from 0 to 1 WARNING: App is not deletable but on_schema_break=ReplaceApp, will attempt to delete app, delete will most likely fail INFO: Replacing SampleApp (1.0) with SampleApp (3.0) in {creator_account} account. ERROR: LogicException: assert failed pc=153 \ No newline at end of file diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt b/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt index 5c34930f..5ffb7726 100644 --- a/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt +++ b/tests/test_deploy_scenarios.approvals/test_deploy_with_schema_breaking_change[OnSchemaBreak.ReplaceApp-Updatable.Yes-Deletable.Yes].approved.txt @@ -1,7 +1,7 @@ INFO: SampleApp not found in {creator_account} account, deploying app. INFO: SampleApp (1.0) deployed successfully, with app id {app0}. DEBUG: SampleApp found in {creator_account} account, with app id {app0}, version=1.0. -WARNING: Detected a breaking app schema change from: Global: uints=0, byte_slices=0, Local: uints=0, byte_slices=0 to Global: uints=1, byte_slices=0, Local: uints=0, byte_slices=0. +WARNING: Detected a breaking app schema change: Global uints increased from 0 to 1 INFO: App is deletable and on_schema_break=ReplaceApp, will attempt to create new app and delete old app INFO: Replacing SampleApp (1.0) with SampleApp (3.0) in {creator_account} account. INFO: SampleApp (3.0) deployed successfully, with app id {app1}. From 00d84cd5f6aeda9664bd0d0941cadef851f24505 Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Fri, 24 Mar 2023 18:19:41 +0800 Subject: [PATCH 15/24] fix: improve method signatures --- src/algokit_utils/__init__.py | 4 + src/algokit_utils/application_client.py | 343 ++++++++++++------------ src/algokit_utils/deploy.py | 1 - tests/test_app_client_create.py | 6 +- 4 files changed, 173 insertions(+), 181 deletions(-) diff --git a/src/algokit_utils/__init__.py b/src/algokit_utils/__init__.py index 31b091fa..ac458499 100644 --- a/src/algokit_utils/__init__.py +++ b/src/algokit_utils/__init__.py @@ -17,6 +17,8 @@ ApplicationClient, CommonCallParameters, CommonCallParametersDict, + CreateCallParameters, + CreateCallParametersDict, DeployResponse, OnCompleteCallParameters, OnCompleteCallParametersDict, @@ -85,6 +87,8 @@ "ApplicationClient", "CommonCallParameters", "CommonCallParametersDict", + "CreateCallParameters", + "CreateCallParametersDict", "OnCompleteCallParameters", "OnCompleteCallParametersDict", "ApplicationClient", diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index 321522c1..fe1a29ff 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -28,19 +28,15 @@ from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient +import algokit_utils.application_specification as au_spec import algokit_utils.deploy as au_deploy -from algokit_utils.application_specification import ( - ApplicationSpecification, - CallConfig, - DefaultArgumentDict, - MethodHints, -) from algokit_utils.logic_error import LogicError, parse_logic_error from algokit_utils.models import Account logger = logging.getLogger(__name__) -ABIArgsDict = dict[str, Any] +ABIArgType = Any +ABIArgsDict = dict[str, ABIArgType] __all__ = [ "ABICallArgs", @@ -51,6 +47,8 @@ "CommonCallParametersDict", "OnCompleteCallParameters", "OnCompleteCallParametersDict", + "CreateCallParameters", + "CreateCallParametersDict", "DeployResponse", "OnUpdate", "OnSchemaBreak", @@ -158,6 +156,11 @@ class OnCompleteCallParameters(CommonCallParameters): on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC +@dataclasses.dataclass(kw_only=True) +class CreateCallParameters(OnCompleteCallParameters): + extra_pages: int | None = None + + class CommonCallParametersDict(TypedDict, total=False): signer: TransactionSigner sender: str @@ -170,12 +173,8 @@ class OnCompleteCallParametersDict(TypedDict, CommonCallParametersDict, total=Fa on_complete: transaction.OnComplete -class FullCallParametersDict(TypedDict, OnCompleteCallParametersDict, total=False): - accounts: list[str] | None - foreign_apps: list[int] | None - foreign_assets: list[int] | None - boxes: Sequence[tuple[int, bytes | bytearray | str | int]] | None - rekey_to: str | None +class CreateCallParametersDict(TypedDict, OnCompleteCallParametersDict, total=False): + extra_pages: int class ApplicationClient: @@ -183,7 +182,7 @@ class ApplicationClient: def __init__( self, algod_client: AlgodClient, - app_spec: ApplicationSpecification, + app_spec: au_spec.ApplicationSpecification, *, app_id: int = 0, signer: TransactionSigner | Account | None = None, @@ -197,7 +196,7 @@ def __init__( def __init__( self, algod_client: AlgodClient, - app_spec: ApplicationSpecification, + app_spec: au_spec.ApplicationSpecification, *, creator: str | Account, indexer_client: IndexerClient | None = None, @@ -212,7 +211,7 @@ def __init__( def __init__( self, algod_client: AlgodClient, - app_spec: ApplicationSpecification, + app_spec: au_spec.ApplicationSpecification, *, app_id: int = 0, creator: str | Account | None = None, @@ -411,9 +410,9 @@ def create_metadata( def create_app() -> DeployResponse: assert self.existing_deployments create_response = self.create( - abi_method=_create_args.method, + call_abi_method=_create_args.method, **_create_args.args, - parameters=_add_lease_parameter(common_parameters, _create_args.lease), + transaction_parameters=_add_lease_parameter(common_parameters, _create_args.lease), ) logger.info(f"{name} ({version}) deployed successfully, with app id {self.app_id}.") assert create_response.confirmed_round is not None @@ -446,15 +445,15 @@ def create_and_delete_app() -> DeployResponse: atc = AtomicTransactionComposer() self.compose_create( atc, - abi_method=_create_args.method, + call_abi_method=_create_args.method, **_create_args.args, - parameters=_add_lease_parameter(common_parameters, _create_args.lease), + transaction_parameters=_add_lease_parameter(common_parameters, _create_args.lease), ) self.compose_delete( atc, - abi_method=_delete_args.method, + call_abi_method=_delete_args.method, **_delete_args.args, - parameters=_add_lease_parameter(common_parameters, _delete_args.lease), + transaction_parameters=_add_lease_parameter(common_parameters, _delete_args.lease), ) create_delete_response = self.execute_atc(atc) create_response = _tr_from_atr(atc, create_delete_response, 0) @@ -479,9 +478,9 @@ def update_app() -> DeployResponse: assert self.existing_deployments logger.info(f"Updating {name} to {version} in {self._creator} account, with app id {app.app_id}") update_response = self.update( - abi_method=_update_args.method, + call_abi_method=_update_args.method, **_update_args.args, - parameters=_add_lease_parameter(common_parameters, lease=_update_args.lease), + transaction_parameters=_add_lease_parameter(common_parameters, lease=_update_args.lease), ) app_metadata = create_metadata( app.created_round, updated_round=update_response.confirmed_round, original_metadata=app.created_metadata @@ -558,37 +557,30 @@ def update_app() -> DeployResponse: return DeployResponse(app=app) - def _check_is_compiled(self) -> tuple[Program, Program]: - if self._approval_program is None or self._clear_program is None: - raise Exception( - "Compiled programs are not available, " "please provide template_values before creating or updating" - ) - return self._approval_program, self._clear_program - def compose_create( self, atc: AtomicTransactionComposer, - abi_method: Method | str | bool | None = None, - *, - parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, - extra_pages: int | None = None, - **kwargs: Any, + /, + call_abi_method: Method | str | bool | None = None, + transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = None, + **abi_kwargs: ABIArgType, ) -> None: """Adds a signed transaction with application id == 0 and the schema and source of client's app_spec to atc""" approval_program, clear_program = self._check_is_compiled() + transaction_parameters = _convert_call_parameters(transaction_parameters) - if extra_pages is None: - extra_pages = num_extra_program_pages(approval_program.raw_binary, clear_program.raw_binary) + extra_pages = transaction_parameters.extra_pages or num_extra_program_pages( + approval_program.raw_binary, clear_program.raw_binary + ) - parameters = _convert_call_parameters(parameters) - self._add_method_call( + self.add_method_call( atc, app_id=0, - abi_method=abi_method, - abi_args=kwargs, - on_complete=parameters.on_complete, - call_config=CallConfig.CREATE, - parameters=parameters, + abi_method=call_abi_method, + abi_args=abi_kwargs, + on_complete=transaction_parameters.on_complete, + call_config=au_spec.CallConfig.CREATE, + parameters=transaction_parameters, approval_program=approval_program.raw_binary, clear_program=clear_program.raw_binary, global_schema=self.app_spec.global_state_schema, @@ -598,11 +590,9 @@ def compose_create( def create( self, - abi_method: Method | str | bool | None = None, - *, - parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, - extra_pages: int | None = None, - **kwargs: Any, + call_abi_method: Method | str | bool | None = None, + transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = None, + **abi_kwargs: ABIArgType, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with application id == 0 and the schema and source of client's app_spec""" @@ -610,10 +600,9 @@ def create( self.compose_create( atc, - abi_method, - parameters=parameters, - extra_pages=extra_pages, - **kwargs, + call_abi_method, + transaction_parameters, + **abi_kwargs, ) create_result = self._execute_atc_tr(atc) self._set_app_id_from_tx_id(create_result.tx_id) @@ -622,19 +611,19 @@ def create( def compose_update( self, atc: AtomicTransactionComposer, - abi_method: Method | str | bool | None = None, - *, - parameters: CommonCallParameters | CommonCallParametersDict | None = None, - **kwargs: Any, + /, + call_abi_method: Method | str | bool | None = None, + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = None, + **abi_kwargs: ABIArgType, ) -> None: """Adds a signed transaction with on_complete=UpdateApplication to atc""" approval_program, clear_program = self._check_is_compiled() - self._add_method_call( + self.add_method_call( atc=atc, - abi_method=abi_method, - abi_args=kwargs, - parameters=parameters, + abi_method=call_abi_method, + abi_args=abi_kwargs, + parameters=transaction_parameters, on_complete=transaction.OnComplete.UpdateApplicationOC, approval_program=approval_program.raw_binary, clear_program=clear_program.raw_binary, @@ -642,73 +631,70 @@ def compose_update( def update( self, - abi_method: Method | str | bool | None = None, - *, - parameters: CommonCallParameters | CommonCallParametersDict | None = None, - **kwargs: Any, + call_abi_method: Method | str | bool | None = None, + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = None, + **abi_kwargs: ABIArgType, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with on_complete=UpdateApplication""" atc = AtomicTransactionComposer() self.compose_update( atc, - abi_method, - parameters=parameters, - **kwargs, + call_abi_method, + transaction_parameters=transaction_parameters, + **abi_kwargs, ) return self._execute_atc_tr(atc) def compose_delete( self, atc: AtomicTransactionComposer, - abi_method: Method | str | bool | None = None, - *, - parameters: CommonCallParameters | CommonCallParametersDict | None = None, - **kwargs: Any, + /, + call_abi_method: Method | str | bool | None = None, + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = None, + **abi_kwargs: ABIArgType, ) -> None: """Adds a signed transaction with on_complete=DeleteApplication to atc""" - delete_method = self._resolve_method(abi_method, kwargs, on_complete=transaction.OnComplete.DeleteApplicationOC) - self._add_method_call( + self.add_method_call( atc, - delete_method, - abi_args=kwargs, - parameters=parameters, + call_abi_method, + abi_args=abi_kwargs, + parameters=transaction_parameters, on_complete=transaction.OnComplete.DeleteApplicationOC, ) def delete( self, - abi_method: Method | str | bool | None = None, - *, - parameters: CommonCallParameters | CommonCallParametersDict | None = None, - **kwargs: Any, + call_abi_method: Method | str | bool | None = None, + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = None, + **abi_kwargs: ABIArgType, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with on_complete=DeleteApplication""" atc = AtomicTransactionComposer() self.compose_delete( atc, - abi_method, - parameters=parameters, - **kwargs, + call_abi_method, + transaction_parameters=transaction_parameters, + **abi_kwargs, ) return self._execute_atc_tr(atc) def compose_call( self, atc: AtomicTransactionComposer, - abi_method: Method | str | bool | None = None, - *, - parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, - **kwargs: Any, + /, + call_abi_method: Method | str | bool | None = None, + transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, + **abi_kwargs: ABIArgType, ) -> None: """Adds a signed transaction with specified parameters to atc""" - _parameters = _convert_call_parameters(parameters) - self._add_method_call( + _parameters = _convert_call_parameters(transaction_parameters) + self.add_method_call( atc, - abi_method=abi_method, - abi_args=kwargs, + abi_method=call_abi_method, + abi_args=abi_kwargs, parameters=_parameters, on_complete=_parameters.on_complete, ) @@ -716,157 +702,137 @@ def compose_call( @overload def call( self, - abi_method: Method | str | Literal[True], - *, - parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, - **kwargs: Any, + call_abi_method: Method | str | Literal[True], + transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, + **abi_kwargs: ABIArgType, ) -> ABITransactionResponse: ... @overload def call( self, - abi_method: Literal[False], - *, - parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, - **kwargs: Any, + call_abi_method: Literal[False], + transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, + **abi_kwargs: ABIArgType, ) -> TransactionResponse: ... def call( self, - abi_method: Method | str | bool | None = None, - *, - parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, - **kwargs: Any, + call_abi_method: Method | str | bool | None = None, + transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, + **abi_kwargs: ABIArgType, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with specified parameters""" atc = AtomicTransactionComposer() - _parameters = _convert_call_parameters(parameters) + _parameters = _convert_call_parameters(transaction_parameters) self.compose_call( atc, - abi_method=abi_method, - parameters=_parameters, - **kwargs, + call_abi_method=call_abi_method, + transaction_parameters=_parameters, + **abi_kwargs, ) - method = self._resolve_method(abi_method, kwargs, _parameters.on_complete) + method = self._resolve_method(call_abi_method, abi_kwargs, _parameters.on_complete) # If its a read-only method, use dryrun (TODO: swap with simulate later?) if method: - response = self._dry_run_call(method, atc) + response = self._try_dry_run_call(method, atc) if response: return response return self._execute_atc_tr(atc) - def _dry_run_call(self, method: Method, atc: AtomicTransactionComposer) -> ABITransactionResponse | None: - hints = self._method_hints(method) - if hints and hints.read_only: - dr_req = transaction.create_dryrun(self.algod_client, atc.gather_signatures()) # type: ignore[arg-type] - dr_result = self.algod_client.dryrun(dr_req) # type: ignore[arg-type] - for txn in dr_result["txns"]: - if "app-call-messages" in txn and "REJECT" in txn["app-call-messages"]: - msg = ", ".join(txn["app-call-messages"]) - raise Exception(f"Dryrun for readonly method failed: {msg}") - - method_results = _parse_result({0: method}, dr_result["txns"], atc.tx_ids) - return ABITransactionResponse(**method_results[0].__dict__, confirmed_round=None) - return None - def compose_opt_in( self, atc: AtomicTransactionComposer, - abi_method: Method | str | bool | None = None, - *, - parameters: CommonCallParameters | CommonCallParametersDict | None = None, - **kwargs: Any, + /, + call_abi_method: Method | str | bool | None = None, + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = None, + **abi_kwargs: ABIArgType, ) -> None: """Adds a signed transaction with on_complete=OptIn to atc""" - self._add_method_call( + self.add_method_call( atc, - abi_method=abi_method, - abi_args=kwargs, - parameters=parameters, + abi_method=call_abi_method, + abi_args=abi_kwargs, + parameters=transaction_parameters, on_complete=transaction.OnComplete.OptInOC, ) def opt_in( self, - abi_method: Method | str | bool | None = None, - *, - parameters: CommonCallParameters | CommonCallParametersDict | None = None, - **kwargs: Any, + call_abi_method: Method | str | bool | None = None, + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = None, + **abi_kwargs: ABIArgType, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with on_complete=OptIn""" atc = AtomicTransactionComposer() self.compose_opt_in( atc, - abi_method=abi_method, - parameters=parameters, - **kwargs, + call_abi_method=call_abi_method, + transaction_parameters=transaction_parameters, + **abi_kwargs, ) return self._execute_atc_tr(atc) def compose_close_out( self, atc: AtomicTransactionComposer, - abi_method: Method | str | bool | None = None, - *, - parameters: CommonCallParameters | CommonCallParametersDict | None = None, - **kwargs: Any, + /, + call_abi_method: Method | str | bool | None = None, + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = None, + **abi_kwargs: ABIArgType, ) -> None: """Adds a signed transaction with on_complete=CloseOut to ac""" - self._add_method_call( + self.add_method_call( atc, - abi_method=abi_method, - abi_args=kwargs, - parameters=parameters, + abi_method=call_abi_method, + abi_args=abi_kwargs, + parameters=transaction_parameters, on_complete=transaction.OnComplete.CloseOutOC, ) def close_out( self, - abi_method: Method | str | bool | None = None, - *, - parameters: CommonCallParameters | CommonCallParametersDict | None = None, - **kwargs: Any, + call_abi_method: Method | str | bool | None = None, + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = None, + **abi_kwargs: ABIArgType, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with on_complete=CloseOut""" atc = AtomicTransactionComposer() self.compose_close_out( atc, - abi_method=abi_method, - parameters=parameters, - **kwargs, + call_abi_method=call_abi_method, + transaction_parameters=transaction_parameters, + **abi_kwargs, ) return self._execute_atc_tr(atc) def compose_clear_state( self, atc: AtomicTransactionComposer, - *, - parameters: CommonCallParameters | CommonCallParametersDict | None = None, + /, + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = None, app_args: list[bytes] | None = None, ) -> None: """Adds a signed transaction with on_complete=ClearState to atc""" - return self._add_method_call( + return self.add_method_call( atc, - parameters=parameters, + parameters=transaction_parameters, on_complete=transaction.OnComplete.ClearStateOC, app_args=app_args, ) def clear_state( self, - *, - parameters: CommonCallParameters | CommonCallParametersDict | None = None, + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = None, app_args: list[bytes] | None = None, ) -> TransactionResponse | ABITransactionResponse: """Submits a signed transaction with on_complete=ClearState""" atc = AtomicTransactionComposer() self.compose_clear_state( atc, - parameters=parameters, + transaction_parameters=transaction_parameters, app_args=app_args, ) return self._execute_atc_tr(atc) @@ -893,7 +859,7 @@ def get_local_state(self, account: str | None = None, *, raw: bool = False) -> d _decode_state(acct_state.get("app-local-state", {}).get("key-value", {}), raw=raw), ) - def resolve(self, to_resolve: DefaultArgumentDict) -> int | str | bytes: + def resolve(self, to_resolve: au_spec.DefaultArgumentDict) -> int | str | bytes: def _data_check(value: Any) -> int | str | bytes: if isinstance(value, (int, str, bytes)): return value @@ -920,6 +886,27 @@ def _data_check(value: Any) -> int | str | bytes: case _: raise TypeError("Unable to interpret default argument specification") + def _check_is_compiled(self) -> tuple[Program, Program]: + if self._approval_program is None or self._clear_program is None: + raise Exception( + "Compiled programs are not available, please provide template_values before creating or updating" + ) + return self._approval_program, self._clear_program + + def _try_dry_run_call(self, method: Method, atc: AtomicTransactionComposer) -> ABITransactionResponse | None: + hints = self._method_hints(method) + if hints and hints.read_only: + dr_req = transaction.create_dryrun(self.algod_client, atc.gather_signatures()) # type: ignore[arg-type] + dr_result = self.algod_client.dryrun(dr_req) # type: ignore[arg-type] + for txn in dr_result["txns"]: + if "app-call-messages" in txn and "REJECT" in txn["app-call-messages"]: + msg = ", ".join(txn["app-call-messages"]) + raise Exception(f"Dryrun for readonly method failed: {msg}") + + method_results = _parse_result({0: method}, dr_result["txns"], atc.tx_ids) + return ABITransactionResponse(**method_results[0].__dict__, confirmed_round=None) + return None + def _load_reference_and_check_app_id(self) -> None: self._load_app_reference() self._check_app_id() @@ -951,7 +938,7 @@ def _resolve_method( abi_method: Method | str | bool | None, args: ABIArgsDict | None, on_complete: transaction.OnComplete, - call_config: CallConfig = CallConfig.CALL, + call_config: au_spec.CallConfig = au_spec.CallConfig.CALL, ) -> Method | None: matches: list[Method | None] = [] match abi_method: @@ -987,7 +974,7 @@ def _get_approval_source_map(self) -> SourceMap | None: return self.approval_source_map - def _add_method_call( + def add_method_call( self, atc: AtomicTransactionComposer, abi_method: Method | str | bool | None = None, @@ -1001,7 +988,7 @@ def _add_method_call( clear_program: bytes | None = None, extra_pages: int | None = None, app_args: list[bytes] | None = None, - call_config: CallConfig = CallConfig.CALL, + call_config: au_spec.CallConfig = au_spec.CallConfig.CALL, ) -> None: """Adds a transaction to the AtomicTransactionComposer passed""" if app_id is None: @@ -1098,7 +1085,11 @@ def _add_method_call( ) def _method_matches( - self, method: Method, args: ABIArgsDict | None, on_complete: transaction.OnComplete, call_config: CallConfig + self, + method: Method, + args: ABIArgsDict | None, + on_complete: transaction.OnComplete, + call_config: au_spec.CallConfig, ) -> bool: hints = self._method_hints(method) if call_config not in au_deploy.get_call_config(hints.call_config, on_complete): @@ -1110,7 +1101,7 @@ def _method_matches( return method_args == provided_args def _find_abi_methods( - self, args: ABIArgsDict | None, on_complete: transaction.OnComplete, call_config: CallConfig + self, args: ABIArgsDict | None, on_complete: transaction.OnComplete, call_config: au_spec.CallConfig ) -> list[Method]: return [ method @@ -1128,10 +1119,10 @@ def _resolve_abi_method(self, method: Method | str) -> Method: else: return method - def _method_hints(self, method: Method) -> MethodHints: + def _method_hints(self, method: Method) -> au_spec.MethodHints: sig = method.get_signature() if sig not in self.app_spec.hints: - return MethodHints() + return au_spec.MethodHints() return self.app_spec.hints[sig] def _execute_atc_tr(self, atc: AtomicTransactionComposer) -> TransactionResponse: @@ -1166,7 +1157,7 @@ def _resolve_signer_sender( def substitute_template_and_compile( algod_client: AlgodClient, - app_spec: ApplicationSpecification, + app_spec: au_spec.ApplicationSpecification, template_values: au_deploy.TemplateValueDict, ) -> tuple[Program, Program]: template_values = dict(template_values or {}) @@ -1224,9 +1215,9 @@ def execute_atc_with_logic_error( raise ex -def _convert_call_parameters(args: CommonCallParameters | CommonCallParametersDict | None) -> OnCompleteCallParameters: +def _convert_call_parameters(args: CommonCallParameters | CommonCallParametersDict | None) -> CreateCallParameters: _args = dataclasses.asdict(args) if isinstance(args, CommonCallParameters) else (args or {}) - return OnCompleteCallParameters(**_args) + return CreateCallParameters(**_args) def _convert_deploy_args(args: ABICallArgs | ABICallArgsDict | None) -> ABICallArgs: @@ -1234,10 +1225,8 @@ def _convert_deploy_args(args: ABICallArgs | ABICallArgsDict | None) -> ABICallA return ABICallArgs(**_args) -def _add_lease_parameter( - parameters: CommonCallParameters | OnCompleteCallParameters, lease: bytes | str | None -) -> OnCompleteCallParameters: - copy = OnCompleteCallParameters(**dataclasses.asdict(parameters)) +def _add_lease_parameter(parameters: CommonCallParameters, lease: bytes | str | None) -> CreateCallParameters: + copy = CreateCallParameters(**dataclasses.asdict(parameters)) copy.lease = lease return copy diff --git a/src/algokit_utils/deploy.py b/src/algokit_utils/deploy.py index bc78f7ad..6bc889ef 100644 --- a/src/algokit_utils/deploy.py +++ b/src/algokit_utils/deploy.py @@ -177,7 +177,6 @@ def parse_note(metadata_b64: str | None) -> AppDeployMetaData | None: return AppLookup(creator_address, apps) -# TODO: put these somewhere more useful def _state_schema(schema: dict[str, int]) -> StateSchema: return StateSchema(schema.get("num-uint", 0), schema.get("num-byte-slice", 0)) # type: ignore[no-untyped-call] diff --git a/tests/test_app_client_create.py b/tests/test_app_client_create.py index d30613c9..4db67631 100644 --- a/tests/test_app_client_create.py +++ b/tests/test_app_client_create.py @@ -9,7 +9,7 @@ def test_bare_create(client_fixture: ApplicationClient) -> None: - client_fixture.create(abi_method=False) + client_fixture.create(call_abi_method=False) assert client_fixture.call("hello", name="test").return_value == "Hello Bare, test" @@ -30,7 +30,7 @@ def test_abi_create_args( def test_create_auto_find(client_fixture: ApplicationClient) -> None: - client_fixture.create(parameters={"on_complete": OnComplete.OptInOC}) + client_fixture.create(transaction_parameters={"on_complete": OnComplete.OptInOC}) assert client_fixture.call("hello", name="test").return_value == "Opt In, test" @@ -47,7 +47,7 @@ def test_abi_create_with_atc(client_fixture: ApplicationClient) -> None: def test_bare_create_with_atc(client_fixture: ApplicationClient) -> None: atc = AtomicTransactionComposer() - client_fixture.compose_create(atc, abi_method=False) + client_fixture.compose_create(atc, call_abi_method=False) create_result = atc.execute(client_fixture.algod_client, 4) client_fixture.app_id = get_app_id_from_tx_id(client_fixture.algod_client, create_result.tx_ids[0]) From 4a5c6ed15b136780ad0b8964eb0dbece55306ff9 Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Fri, 24 Mar 2023 22:35:36 +0800 Subject: [PATCH 16/24] chore: reorder some arguments --- src/algokit_utils/application_client.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index fe1a29ff..185b730b 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -410,9 +410,9 @@ def create_metadata( def create_app() -> DeployResponse: assert self.existing_deployments create_response = self.create( - call_abi_method=_create_args.method, + _create_args.method, + _add_lease_parameter(common_parameters, _create_args.lease), **_create_args.args, - transaction_parameters=_add_lease_parameter(common_parameters, _create_args.lease), ) logger.info(f"{name} ({version}) deployed successfully, with app id {self.app_id}.") assert create_response.confirmed_round is not None @@ -445,15 +445,15 @@ def create_and_delete_app() -> DeployResponse: atc = AtomicTransactionComposer() self.compose_create( atc, - call_abi_method=_create_args.method, + _create_args.method, + _add_lease_parameter(common_parameters, _create_args.lease), **_create_args.args, - transaction_parameters=_add_lease_parameter(common_parameters, _create_args.lease), ) self.compose_delete( atc, - call_abi_method=_delete_args.method, + _delete_args.method, + _add_lease_parameter(common_parameters, _delete_args.lease), **_delete_args.args, - transaction_parameters=_add_lease_parameter(common_parameters, _delete_args.lease), ) create_delete_response = self.execute_atc(atc) create_response = _tr_from_atr(atc, create_delete_response, 0) @@ -478,9 +478,9 @@ def update_app() -> DeployResponse: assert self.existing_deployments logger.info(f"Updating {name} to {version} in {self._creator} account, with app id {app.app_id}") update_response = self.update( - call_abi_method=_update_args.method, + _update_args.method, + _add_lease_parameter(common_parameters, lease=_update_args.lease), **_update_args.args, - transaction_parameters=_add_lease_parameter(common_parameters, lease=_update_args.lease), ) app_metadata = create_metadata( app.created_round, updated_round=update_response.confirmed_round, original_metadata=app.created_metadata From a6264d6d0270ddb639cbb0a5c4bba08f788b77f3 Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Mon, 27 Mar 2023 10:50:12 +0800 Subject: [PATCH 17/24] refactor: extract some methods and move into other modules --- src/algokit_utils/__init__.py | 20 +- src/algokit_utils/application_client.py | 393 +++++++++++------------- src/algokit_utils/deploy.py | 114 ++++++- src/algokit_utils/models.py | 18 ++ tests/test_app_client_deploy.py | 8 +- tests/test_transfer.py | 4 +- 6 files changed, 333 insertions(+), 224 deletions(-) diff --git a/src/algokit_utils/__init__.py b/src/algokit_utils/__init__.py index ac458499..2d5dbd8f 100644 --- a/src/algokit_utils/__init__.py +++ b/src/algokit_utils/__init__.py @@ -13,20 +13,16 @@ from algokit_utils.application_client import ( ABICallArgs, ABICallArgsDict, - ABITransactionResponse, + ABICreateCallArgs, + ABICreateCallArgsDict, ApplicationClient, CommonCallParameters, CommonCallParametersDict, CreateCallParameters, CreateCallParametersDict, - DeployResponse, OnCompleteCallParameters, OnCompleteCallParametersDict, - OnSchemaBreak, - OnUpdate, - OperationPerformed, Program, - TransactionResponse, execute_atc_with_logic_error, get_app_id_from_tx_id, get_next_version, @@ -51,11 +47,15 @@ AppMetaData, AppReference, DeploymentFailedError, + DeployResponse, + OnSchemaBreak, + OnUpdate, + OperationPerformed, get_creator_apps, replace_template_variables, ) from algokit_utils.logic_error import LogicError -from algokit_utils.models import Account +from algokit_utils.models import ABITransactionResponse, Account, TransactionResponse from algokit_utils.network_clients import ( AlgoClientConfig, get_algod_client, @@ -83,7 +83,8 @@ "replace_template_variables", "ABICallArgs", "ABICallArgsDict", - "ABITransactionResponse", + "ABICreateCallArgs", + "ABICreateCallArgsDict", "ApplicationClient", "CommonCallParameters", "CommonCallParametersDict", @@ -97,7 +98,6 @@ "OnSchemaBreak", "OperationPerformed", "Program", - "TransactionResponse", "execute_atc_with_logic_error", "get_app_id_from_tx_id", "get_next_version", @@ -111,7 +111,9 @@ "OnCompleteActionName", "MethodHints", "LogicError", + "ABITransactionResponse", "Account", + "TransactionResponse", "AlgoClientConfig", "get_algod_client", "get_indexer_client", diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index 185b730b..a42249c6 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -3,7 +3,6 @@ import logging import re from collections.abc import Sequence -from enum import Enum from math import ceil from typing import Any, Literal, TypedDict, cast, overload @@ -30,8 +29,9 @@ import algokit_utils.application_specification as au_spec import algokit_utils.deploy as au_deploy +from algokit_utils.deploy import check_app_and_deploy from algokit_utils.logic_error import LogicError, parse_logic_error -from algokit_utils.models import Account +from algokit_utils.models import ABITransactionResponse, Account, TransactionResponse logger = logging.getLogger(__name__) @@ -41,7 +41,6 @@ __all__ = [ "ABICallArgs", "ABICallArgsDict", - "ABITransactionResponse", "ApplicationClient", "CommonCallParameters", "CommonCallParametersDict", @@ -49,12 +48,7 @@ "OnCompleteCallParametersDict", "CreateCallParameters", "CreateCallParametersDict", - "DeployResponse", - "OnUpdate", - "OnSchemaBreak", - "OperationPerformed", "Program", - "TransactionResponse", "execute_atc_with_logic_error", "get_app_id_from_tx_id", "get_next_version", @@ -81,62 +75,6 @@ def num_extra_program_pages(approval: bytes, clear: bytes) -> int: return ceil(((len(approval) + len(clear)) - APP_PAGE_MAX_SIZE) / APP_PAGE_MAX_SIZE) -@dataclasses.dataclass(kw_only=True) -class ABICallArgs: - method: Method | str | bool | None = None - args: ABIArgsDict = dataclasses.field(default_factory=dict) - lease: str | bytes | None = None - - -class ABICallArgsDict(TypedDict, total=False): - method: Method | str | bool | None - args: ABIArgsDict - lease: str | bytes | None - - -class OnUpdate(Enum): - Fail = 0 - UpdateApp = 1 - ReplaceApp = 2 - # TODO: AppendApp - - -class OnSchemaBreak(Enum): - Fail = 0 - ReplaceApp = 2 - - -class OperationPerformed(Enum): - Nothing = 0 - Create = 1 - Update = 2 - Replace = 3 - - -@dataclasses.dataclass(kw_only=True) -class TransactionResponse: - tx_id: str - confirmed_round: int | None - - -@dataclasses.dataclass(kw_only=True) -class DeployResponse: - app: au_deploy.AppMetaData - create_response: TransactionResponse | None = None - delete_response: TransactionResponse | None = None - update_response: TransactionResponse | None = None - action_taken: OperationPerformed = OperationPerformed.Nothing - - -@dataclasses.dataclass(kw_only=True) -class ABITransactionResponse(TransactionResponse): - raw_value: bytes - return_value: Any - decode_error: Exception | None - tx_info: dict - method: Method - - @dataclasses.dataclass(kw_only=True) class CommonCallParameters: signer: TransactionSigner | None = None @@ -153,7 +91,7 @@ class CommonCallParameters: @dataclasses.dataclass(kw_only=True) class OnCompleteCallParameters(CommonCallParameters): - on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC + on_complete: transaction.OnComplete | None = None @dataclasses.dataclass(kw_only=True) @@ -177,6 +115,42 @@ class CreateCallParametersDict(TypedDict, OnCompleteCallParametersDict, total=Fa extra_pages: int +@dataclasses.dataclass(kw_only=True) +class ABICallArgs: + method: Method | str | bool | None = None + args: ABIArgsDict = dataclasses.field(default_factory=dict) + suggested_params: transaction.SuggestedParams | None = None + lease: bytes | str | None = None + accounts: list[str] | None = None + foreign_apps: list[int] | None = None + foreign_assets: list[int] | None = None + boxes: Sequence[tuple[int, bytes | bytearray | str | int]] | None = None + rekey_to: str | None = None + + +@dataclasses.dataclass(kw_only=True) +class ABICreateCallArgs(ABICallArgs): + extra_pages: int | None = None + on_complete: transaction.OnComplete | None = None + + +class ABICallArgsDict(TypedDict, total=False): + method: Method | str | bool + args: ABIArgsDict + suggested_params: transaction.SuggestedParams + lease: bytes | str + accounts: list[str] + foreign_apps: list[int] + foreign_assets: list[int] + boxes: Sequence[tuple[int, bytes | bytearray | str | int]] + rekey_to: str + + +class ABICreateCallArgsDict(TypedDict, ABICallArgsDict, total=False): + extra_pages: int | None + on_complete: transaction.OnComplete + + class ApplicationClient: @overload def __init__( @@ -297,7 +271,7 @@ def prepare( import copy new_client = copy.copy(self) - self._prepare(new_client, signer=signer, sender=sender, app_id=app_id, template_values=template_values) + new_client._prepare(new_client, signer=signer, sender=sender, app_id=app_id, template_values=template_values) return new_client def _prepare( @@ -326,13 +300,48 @@ def deploy( sender: str | None = None, allow_update: bool | None = None, allow_delete: bool | None = None, - on_update: OnUpdate = OnUpdate.Fail, - on_schema_break: OnSchemaBreak = OnSchemaBreak.Fail, + on_update: au_deploy.OnUpdate = au_deploy.OnUpdate.Fail, + on_schema_break: au_deploy.OnSchemaBreak = au_deploy.OnSchemaBreak.Fail, template_values: au_deploy.TemplateValueDict | None = None, - create_args: ABICallArgs | ABICallArgsDict | None = None, + create_args: ABICreateCallArgs | ABICreateCallArgsDict | None = None, update_args: ABICallArgs | ABICallArgsDict | None = None, delete_args: ABICallArgs | ABICallArgsDict | None = None, - ) -> DeployResponse: + ) -> au_deploy.DeployResponse: + before = self._approval_program, self._clear_program, self.sender, self.signer, self.app_id + try: + return self._deploy( + version, + signer=signer, + sender=sender, + allow_update=allow_update, + allow_delete=allow_delete, + on_update=on_update, + on_schema_break=on_schema_break, + template_values=template_values, + create_args=create_args, + update_args=update_args, + delete_args=delete_args, + ) + except Exception as ex: + # undo any prepare changes if there was an error + self._approval_program, self._clear_program, self.sender, self.signer, self.app_id = before + raise ex from None + + def _deploy( + self, + version: str | None, + *, + signer: TransactionSigner | None, + sender: str | None, + allow_update: bool | None, + allow_delete: bool | None, + on_update: au_deploy.OnUpdate, + on_schema_break: au_deploy.OnSchemaBreak, + template_values: au_deploy.TemplateValueDict | None, + create_args: ABICallArgs | ABICallArgsDict | None, + update_args: ABICallArgs | ABICallArgsDict | None, + delete_args: ABICallArgs | ABICallArgsDict | None, + ) -> au_deploy.DeployResponse: """Ensures app associated with app client's creator is present and up to date""" if self.app_id: raise au_deploy.DeploymentFailedError( @@ -349,16 +358,11 @@ def deploy( f"from the given creator address for this application client: {self._creator}" ) - _create_args = _convert_deploy_args(create_args) - _update_args = _convert_deploy_args(update_args) - _delete_args = _convert_deploy_args(delete_args) - # make a copy template_values = dict(template_values or {}) au_deploy.add_deploy_template_variables(template_values, allow_update=allow_update, allow_delete=allow_delete) - # TODO: how to undo this if deployment fails?! - self._prepare(self, template_values=template_values, sender=sender, signer=signer) + self._prepare(self, template_values=template_values) approval_program, clear_program = self._check_is_compiled() updatable = ( @@ -389,71 +393,50 @@ def deploy( version = get_next_version(app.version) app_spec_note = au_deploy.AppDeployMetaData(name, version, updatable=updatable, deletable=deletable) - def create_metadata( - created_round: int, - updated_round: int | None = None, - original_metadata: au_deploy.AppDeployMetaData | None = None, - ) -> au_deploy.AppMetaData: - app_metadata = au_deploy.AppMetaData( - app_id=self.app_id, - app_address=self.app_address, - created_metadata=original_metadata or app_spec_note, - created_round=created_round, - updated_round=updated_round or created_round, - **app_spec_note.__dict__, - deleted=False, - ) - return app_metadata - - common_parameters = CommonCallParameters(note=app_spec_note.encode(), signer=signer, sender=sender) - - def create_app() -> DeployResponse: + def create_app() -> au_deploy.DeployResponse: assert self.existing_deployments + # TODO: extra pages + method, abi_args, parameters = _convert_deploy_args(create_args, app_spec_note, signer, sender) create_response = self.create( - _create_args.method, - _add_lease_parameter(common_parameters, _create_args.lease), - **_create_args.args, + method, + parameters, + **abi_args, ) logger.info(f"{name} ({version}) deployed successfully, with app id {self.app_id}.") assert create_response.confirmed_round is not None - app_metadata = create_metadata(create_response.confirmed_round) + app_metadata = _create_metadata(app_spec_note, self.app_id, create_response.confirmed_round) self.existing_deployments.apps[name] = app_metadata - return DeployResponse( - app=app_metadata, create_response=create_response, action_taken=OperationPerformed.Create + return au_deploy.DeployResponse( + app=app_metadata, create_response=create_response, action_taken=au_deploy.OperationPerformed.Create ) if app.app_id == 0: logger.info(f"{name} not found in {self._creator} account, deploying app.") return create_app() - assert isinstance(app, au_deploy.AppMetaData) - logger.debug(f"{name} found in {self._creator} account, with app id {app.app_id}, version={app.version}.") - - app_changes = au_deploy.check_for_app_changes( - self.algod_client, - new_approval=approval_program.raw_binary, - new_clear=clear_program.raw_binary, - new_global_schema=self.app_spec.global_state_schema, - new_local_schema=self.app_spec.local_state_schema, - app_id=app.app_id, - ) - - def create_and_delete_app() -> DeployResponse: + def create_and_delete_app() -> au_deploy.DeployResponse: assert isinstance(app, au_deploy.AppMetaData) assert self.existing_deployments + logger.info(f"Replacing {name} ({app.version}) with {name} ({version}) in {self._creator} account.") atc = AtomicTransactionComposer() + create_method, create_abi_args, create_parameters = _convert_deploy_args( + create_args, app_spec_note, signer, sender + ) self.compose_create( atc, - _create_args.method, - _add_lease_parameter(common_parameters, _create_args.lease), - **_create_args.args, + create_method, + create_parameters, + **create_abi_args, + ) + delete_method, delete_abi_args, delete_parameters = _convert_deploy_args( + delete_args, app_spec_note, signer, sender ) self.compose_delete( atc, - _delete_args.method, - _add_lease_parameter(common_parameters, _delete_args.lease), - **_delete_args.args, + delete_method, + delete_parameters, + **delete_abi_args, ) create_delete_response = self.execute_atc(atc) create_response = _tr_from_atr(atc, create_delete_response, 0) @@ -462,100 +445,59 @@ def create_and_delete_app() -> DeployResponse: logger.info(f"{name} ({version}) deployed successfully, with app id {self.app_id}.") logger.info(f"{name} ({app.version}) with app id {app.app_id}, deleted successfully.") - app_metadata = create_metadata(create_delete_response.confirmed_round) + app_metadata = _create_metadata(app_spec_note, self.app_id, create_delete_response.confirmed_round) self.existing_deployments.apps[name] = app_metadata - return DeployResponse( + return au_deploy.DeployResponse( app=app_metadata, create_response=create_response, delete_response=delete_response, - action_taken=OperationPerformed.Replace, + action_taken=au_deploy.OperationPerformed.Replace, ) - def update_app() -> DeployResponse: - assert on_update == OnUpdate.UpdateApp + def update_app() -> au_deploy.DeployResponse: + assert on_update == au_deploy.OnUpdate.UpdateApp assert isinstance(app, au_deploy.AppMetaData) assert self.existing_deployments logger.info(f"Updating {name} to {version} in {self._creator} account, with app id {app.app_id}") + method, abi_args, parameters = _convert_deploy_args(update_args, app_spec_note, signer, sender) update_response = self.update( - _update_args.method, - _add_lease_parameter(common_parameters, lease=_update_args.lease), - **_update_args.args, + method, + parameters, + **abi_args, ) - app_metadata = create_metadata( - app.created_round, updated_round=update_response.confirmed_round, original_metadata=app.created_metadata + app_metadata = _create_metadata( + app_spec_note, + self.app_id, + app.created_round, + updated_round=update_response.confirmed_round, + original_metadata=app.created_metadata, ) self.existing_deployments.apps[name] = app_metadata - return DeployResponse( - app=app_metadata, update_response=update_response, action_taken=OperationPerformed.Update + return au_deploy.DeployResponse( + app=app_metadata, update_response=update_response, action_taken=au_deploy.OperationPerformed.Update ) - if app_changes.schema_breaking_change: - logger.warning(f"Detected a breaking app schema change: {app_changes.schema_change_description}") - - if on_schema_break == OnSchemaBreak.Fail: - raise au_deploy.DeploymentFailedError( - "Schema break detected and on_schema_break=OnSchemaBreak.Fail, stopping deployment. " - "If you want to try deleting and recreating the app then " - "re-run with on_schema_break=OnSchemaBreak.ReplaceApp" - ) - if app.deletable: - logger.info( - "App is deletable and on_schema_break=ReplaceApp, will attempt to create new app and delete old app" - ) - elif app.deletable is False: - logger.warning( - "App is not deletable but on_schema_break=ReplaceApp, " - "will attempt to delete app, delete will most likely fail" - ) - else: - logger.warning( - "Cannot determine if App is deletable but on_schema_break=ReplaceApp, " "will attempt to delete app" - ) - return create_and_delete_app() - elif app_changes.app_updated: - logger.info(f"Detected a TEAL update in app id {app.app_id}") - - if on_update == OnUpdate.Fail: - raise au_deploy.DeploymentFailedError( - "Update detected and on_update=Fail, stopping deployment. " - "If you want to try updating the app then re-run with on_update=UpdateApp" - ) - if app.updatable and on_update == OnUpdate.UpdateApp: - logger.info("App is updatable and on_update=UpdateApp, will update app") - return update_app() - elif app.updatable and on_update == OnUpdate.ReplaceApp: - logger.warning( - "App is updatable but on_update=ReplaceApp, will attempt to create new app and delete old app" - ) - return create_and_delete_app() - elif on_update == OnUpdate.ReplaceApp: - if app.updatable is False: - logger.warning( - "App is not updatable and on_update=ReplaceApp, " - "will attempt to create new app and delete old app" - ) - else: - logger.warning( - "Cannot determine if App is updatable and on_update=ReplaceApp, " - "will attempt to create new app and delete old app" - ) - return create_and_delete_app() - else: - if app.updatable is False: - logger.warning( - "App is not updatable but on_update=UpdateApp, " - "will attempt to update app, update will most likely fail" - ) - else: - logger.warning( - "Cannot determine if App is updatable and on_update=UpdateApp, " "will attempt to update app" - ) - return update_app() + assert isinstance(app, au_deploy.AppMetaData) + logger.debug(f"{name} found in {self._creator} account, with app id {app.app_id}, version={app.version}.") - logger.info("No detected changes in app, nothing to do.") + app_changes = au_deploy.check_for_app_changes( + self.algod_client, + new_approval=approval_program.raw_binary, + new_clear=clear_program.raw_binary, + new_global_schema=self.app_spec.global_state_schema, + new_local_schema=self.app_spec.local_state_schema, + app_id=app.app_id, + ) - return DeployResponse(app=app) + return check_app_and_deploy( + app, + app_changes, + on_update=on_update, + on_schema_break=on_schema_break, + update_app=update_app, + create_and_delete_app=create_and_delete_app, + ) def compose_create( self, @@ -578,7 +520,7 @@ def compose_create( app_id=0, abi_method=call_abi_method, abi_args=abi_kwargs, - on_complete=transaction_parameters.on_complete, + on_complete=transaction_parameters.on_complete or transaction.OnComplete.NoOpOC, call_config=au_spec.CallConfig.CREATE, parameters=transaction_parameters, approval_program=approval_program.raw_binary, @@ -696,7 +638,7 @@ def compose_call( abi_method=call_abi_method, abi_args=abi_kwargs, parameters=_parameters, - on_complete=_parameters.on_complete, + on_complete=_parameters.on_complete or transaction.OnComplete.NoOpOC, ) @overload @@ -733,7 +675,9 @@ def call( **abi_kwargs, ) - method = self._resolve_method(call_abi_method, abi_kwargs, _parameters.on_complete) + method = self._resolve_method( + call_abi_method, abi_kwargs, _parameters.on_complete or transaction.OnComplete.NoOpOC + ) # If its a read-only method, use dryrun (TODO: swap with simulate later?) if method: response = self._try_dry_run_call(method, atc) @@ -1215,20 +1159,55 @@ def execute_atc_with_logic_error( raise ex +def _create_metadata( + app_spec_note: au_deploy.AppDeployMetaData, + app_id: int, + created_round: int, + updated_round: int | None = None, + original_metadata: au_deploy.AppDeployMetaData | None = None, +) -> au_deploy.AppMetaData: + app_metadata = au_deploy.AppMetaData( + app_id=app_id, + app_address=get_application_address(app_id), + created_metadata=original_metadata or app_spec_note, + created_round=created_round, + updated_round=updated_round or created_round, + **app_spec_note.__dict__, + deleted=False, + ) + return app_metadata + + def _convert_call_parameters(args: CommonCallParameters | CommonCallParametersDict | None) -> CreateCallParameters: _args = dataclasses.asdict(args) if isinstance(args, CommonCallParameters) else (args or {}) return CreateCallParameters(**_args) -def _convert_deploy_args(args: ABICallArgs | ABICallArgsDict | None) -> ABICallArgs: - _args = dataclasses.asdict(args) if isinstance(args, ABICallArgs) else (args or {}) - return ABICallArgs(**_args) - +def _convert_deploy_args( + _args: ABICallArgs | ABICallArgsDict | None, + note: au_deploy.AppDeployMetaData, + signer: TransactionSigner | None, + sender: str | None, +) -> tuple[Method | str | bool | None, ABIArgsDict, CreateCallParameters]: + args = dataclasses.asdict(_args) if isinstance(_args, ABICallArgs) else (_args or {}) + + # return most derived type, unused parameters are ignored + parameters = CreateCallParameters( + note=note.encode(), + signer=signer, + sender=sender, + suggested_params=args.get("suggested_params"), + lease=args.get("lease"), + accounts=args.get("accounts"), + foreign_assets=args.get("foreign_assets"), + foreign_apps=args.get("foreign_apps"), + boxes=args.get("boxes"), + rekey_to=args.get("rekey_to"), + extra_pages=args.get("extra_pages"), + on_complete=args.get("on_complete"), + ) -def _add_lease_parameter(parameters: CommonCallParameters, lease: bytes | str | None) -> CreateCallParameters: - copy = CreateCallParameters(**dataclasses.asdict(parameters)) - copy.lease = lease - return copy + return args.get("method"), args.get("args") or {}, parameters def _get_sender_from_signer(signer: TransactionSigner) -> str: diff --git a/src/algokit_utils/deploy.py b/src/algokit_utils/deploy.py index 6bc889ef..3f855a56 100644 --- a/src/algokit_utils/deploy.py +++ b/src/algokit_utils/deploy.py @@ -3,7 +3,8 @@ import json import logging import re -from collections.abc import Iterable, Mapping +from collections.abc import Callable, Iterable, Mapping +from enum import Enum from algosdk import transaction from algosdk.logic import get_application_address @@ -17,7 +18,7 @@ MethodConfigDict, OnCompleteActionName, ) -from algokit_utils.models import Account +from algokit_utils.models import Account, TransactionResponse __all__ = [ "UPDATABLE_TEMPLATE_NAME", @@ -28,7 +29,12 @@ "AppDeployMetaData", "AppMetaData", "AppLookup", + "DeployResponse", + "OnUpdate", + "OnSchemaBreak", + "OperationPerformed", "TemplateValueDict", + "check_app_and_deploy", "get_creator_apps", "replace_template_variables", ] @@ -333,3 +339,107 @@ def get(key: OnCompleteActionName) -> CallConfig: return get("close_out") case transaction.OnComplete.ClearStateOC: return get("clear_state") + + +class OnUpdate(Enum): + Fail = 0 + UpdateApp = 1 + ReplaceApp = 2 + # TODO: AppendApp + + +class OnSchemaBreak(Enum): + Fail = 0 + ReplaceApp = 2 + + +class OperationPerformed(Enum): + Nothing = 0 + Create = 1 + Update = 2 + Replace = 3 + + +@dataclasses.dataclass(kw_only=True) +class DeployResponse: + app: AppMetaData + create_response: TransactionResponse | None = None + delete_response: TransactionResponse | None = None + update_response: TransactionResponse | None = None + action_taken: OperationPerformed = OperationPerformed.Nothing + + +def check_app_and_deploy( + app: AppMetaData, + app_changes: AppChanges, + on_schema_break: OnSchemaBreak, + on_update: OnUpdate, + update_app: Callable[[], DeployResponse], + create_and_delete_app: Callable[[], DeployResponse], +) -> DeployResponse: + if app_changes.schema_breaking_change: + logger.warning(f"Detected a breaking app schema change: {app_changes.schema_change_description}") + + if on_schema_break == OnSchemaBreak.Fail: + raise DeploymentFailedError( + "Schema break detected and on_schema_break=OnSchemaBreak.Fail, stopping deployment. " + "If you want to try deleting and recreating the app then " + "re-run with on_schema_break=OnSchemaBreak.ReplaceApp" + ) + if app.deletable: + logger.info( + "App is deletable and on_schema_break=ReplaceApp, will attempt to create new app and delete old app" + ) + elif app.deletable is False: + logger.warning( + "App is not deletable but on_schema_break=ReplaceApp, " + "will attempt to delete app, delete will most likely fail" + ) + else: + logger.warning( + "Cannot determine if App is deletable but on_schema_break=ReplaceApp, " "will attempt to delete app" + ) + return create_and_delete_app() + elif app_changes.app_updated: + logger.info(f"Detected a TEAL update in app id {app.app_id}") + + if on_update == OnUpdate.Fail: + raise DeploymentFailedError( + "Update detected and on_update=Fail, stopping deployment. " + "If you want to try updating the app then re-run with on_update=UpdateApp" + ) + if app.updatable and on_update == OnUpdate.UpdateApp: + logger.info("App is updatable and on_update=UpdateApp, will update app") + return update_app() + elif app.updatable and on_update == OnUpdate.ReplaceApp: + logger.warning( + "App is updatable but on_update=ReplaceApp, will attempt to create new app and delete old app" + ) + return create_and_delete_app() + elif on_update == OnUpdate.ReplaceApp: + if app.updatable is False: + logger.warning( + "App is not updatable and on_update=ReplaceApp, " + "will attempt to create new app and delete old app" + ) + else: + logger.warning( + "Cannot determine if App is updatable and on_update=ReplaceApp, " + "will attempt to create new app and delete old app" + ) + return create_and_delete_app() + else: + if app.updatable is False: + logger.warning( + "App is not updatable but on_update=UpdateApp, " + "will attempt to update app, update will most likely fail" + ) + else: + logger.warning( + "Cannot determine if App is updatable and on_update=UpdateApp, " "will attempt to update app" + ) + return update_app() + + logger.info("No detected changes in app, nothing to do.") + + return DeployResponse(app=app) diff --git a/src/algokit_utils/models.py b/src/algokit_utils/models.py index 2908216e..4cfc0490 100644 --- a/src/algokit_utils/models.py +++ b/src/algokit_utils/models.py @@ -1,7 +1,25 @@ import dataclasses +from typing import Any + +from algosdk.abi import Method @dataclasses.dataclass class Account: private_key: str address: str + + +@dataclasses.dataclass(kw_only=True) +class TransactionResponse: + tx_id: str + confirmed_round: int | None + + +@dataclasses.dataclass(kw_only=True) +class ABITransactionResponse(TransactionResponse): + raw_value: bytes + return_value: Any + decode_error: Exception | None + tx_info: dict + method: Method diff --git a/tests/test_app_client_deploy.py b/tests/test_app_client_deploy.py index fbd0a529..ca9f05f0 100644 --- a/tests/test_app_client_deploy.py +++ b/tests/test_app_client_deploy.py @@ -1,5 +1,5 @@ from algokit_utils import ( - ABICallArgs, + ABICreateCallArgs, Account, ApplicationClient, ApplicationSpecification, @@ -11,7 +11,7 @@ def test_deploy_with_create(client_fixture: ApplicationClient, creator: Account) -> None: client_fixture.deploy( "v1", - create_args=ABICallArgs( + create_args=ABICreateCallArgs( method="create", ), ) @@ -26,7 +26,7 @@ def test_deploy_with_create(client_fixture: ApplicationClient, creator: Account) def test_deploy_with_create_args(client_fixture: ApplicationClient, app_spec: ApplicationSpecification) -> None: create_args = next(m for m in app_spec.contract.methods if m.name == "create_args") - client_fixture.deploy("v1", create_args=ABICallArgs(method=create_args, args={"greeting": "deployed"})) + client_fixture.deploy("v1", create_args=ABICreateCallArgs(method=create_args, args={"greeting": "deployed"})) assert client_fixture.call("hello", name="test").return_value == "deployed, test" @@ -34,7 +34,7 @@ def test_deploy_with_create_args(client_fixture: ApplicationClient, app_spec: Ap def test_deploy_with_bare_create(client_fixture: ApplicationClient) -> None: client_fixture.deploy( "v1", - create_args=ABICallArgs( + create_args=ABICreateCallArgs( method=False, ), ) diff --git a/tests/test_transfer.py b/tests/test_transfer.py index 42d6e65d..c0dbfa9c 100644 --- a/tests/test_transfer.py +++ b/tests/test_transfer.py @@ -1,10 +1,10 @@ -from algokit_utils import ABICallArgs, Account, ApplicationClient, TransferParameters, transfer +from algokit_utils import ABICreateCallArgs, Account, ApplicationClient, TransferParameters, transfer def test_transfer(client_fixture: ApplicationClient, creator: Account) -> None: client_fixture.deploy( "v1", - create_args=ABICallArgs( + create_args=ABICreateCallArgs( method="create", ), ) From 8488940c20052178610eeaa54a7c8bb11c5a1888 Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Mon, 27 Mar 2023 11:08:39 +0800 Subject: [PATCH 18/24] tests: reorganize tests --- tests/__init__.py | 0 tests/conftest.py | 6 +-- tests/test_account.py | 3 +- tests/test_app_client.py | 29 ++++++++++++ tests/test_app_client_create.py | 19 ++++++++ tests/test_app_client_resolve.py | 3 +- .../test_template_substitution.approved.txt | 0 tests/test_deploy.py | 20 +++++++++ tests/test_deploy_scenarios.py | 44 +------------------ 9 files changed, 77 insertions(+), 47 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_app_client.py rename tests/{test_deploy_scenarios.approvals => test_deploy.approvals}/test_template_substitution.approved.txt (100%) create mode 100644 tests/test_deploy.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/conftest.py b/tests/conftest.py index d0674c8a..fe8a9be5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -102,12 +102,12 @@ def get_unique_name() -> str: return name -@pytest.fixture() +@pytest.fixture(scope="session") def algod_client() -> AlgodClient: return get_algod_client() -@pytest.fixture() +@pytest.fixture(scope="session") def indexer_client() -> IndexerClient: return get_indexer_client() @@ -119,7 +119,7 @@ def creator(algod_client: AlgodClient) -> Account: return creator -@pytest.fixture() +@pytest.fixture(scope="session") def app_spec() -> ApplicationSpecification: app_spec = read_spec("app_client_test.json", deletable=True, updatable=True) return app_spec diff --git a/tests/test_account.py b/tests/test_account.py index 8e4e66af..d0dedc63 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -1,6 +1,7 @@ from algokit_utils import get_account from algosdk.v2client.algod import AlgodClient -from conftest import get_unique_name + +from tests.conftest import get_unique_name def test_account_can_be_called_twice(algod_client: AlgodClient) -> None: diff --git a/tests/test_app_client.py b/tests/test_app_client.py new file mode 100644 index 00000000..87826175 --- /dev/null +++ b/tests/test_app_client.py @@ -0,0 +1,29 @@ +import pytest +from algokit_utils import ( + DeploymentFailedError, + get_next_version, +) + + +@pytest.mark.parametrize( + ("current", "expected_next"), + [ + ("1", "2"), + ("v1", "v2"), + ("v1-alpha", "v2-alpha"), + ("1.0", "1.1"), + ("v1.0", "v1.1"), + ("v1.0-alpha", "v1.1-alpha"), + ("1.0.0", "1.0.1"), + ("v1.0.0", "v1.0.1"), + ("v1.0.0-alpha", "v1.0.1-alpha"), + ], +) +def test_auto_version_increment(current: str, expected_next: str) -> None: + value = get_next_version(current) + assert value == expected_next + + +def test_auto_version_increment_failure() -> None: + with pytest.raises(DeploymentFailedError): + get_next_version("teapot") diff --git a/tests/test_app_client_create.py b/tests/test_app_client_create.py index 4db67631..d9aee590 100644 --- a/tests/test_app_client_create.py +++ b/tests/test_app_client_create.py @@ -2,10 +2,25 @@ from algokit_utils import ( ApplicationClient, ApplicationSpecification, + get_account, get_app_id_from_tx_id, ) from algosdk.atomic_transaction_composer import AtomicTransactionComposer from algosdk.transaction import OnComplete +from algosdk.v2client.algod import AlgodClient +from algosdk.v2client.indexer import IndexerClient + +from tests.conftest import get_unique_name + + +@pytest.fixture(scope="module") +def client_fixture( + algod_client: AlgodClient, indexer_client: IndexerClient, app_spec: ApplicationSpecification +) -> ApplicationClient: + creator_name = get_unique_name() + creator = get_account(algod_client, creator_name) + client = ApplicationClient(algod_client, app_spec, creator=creator, indexer_client=indexer_client) + return client def test_bare_create(client_fixture: ApplicationClient) -> None: @@ -53,3 +68,7 @@ def test_bare_create_with_atc(client_fixture: ApplicationClient) -> None: client_fixture.app_id = get_app_id_from_tx_id(client_fixture.algod_client, create_result.tx_ids[0]) assert client_fixture.call("hello", name="test").return_value == "Hello Bare, test" + + +def test_create_parameters(client_fixture: ApplicationClient) -> None: + pass diff --git a/tests/test_app_client_resolve.py b/tests/test_app_client_resolve.py index 7dabb4b6..7339f324 100644 --- a/tests/test_app_client_resolve.py +++ b/tests/test_app_client_resolve.py @@ -4,7 +4,8 @@ DefaultArgumentDict, ) from algosdk.v2client.algod import AlgodClient -from conftest import read_spec + +from tests.conftest import read_spec def test_resolve(algod_client: AlgodClient, creator: Account) -> None: diff --git a/tests/test_deploy_scenarios.approvals/test_template_substitution.approved.txt b/tests/test_deploy.approvals/test_template_substitution.approved.txt similarity index 100% rename from tests/test_deploy_scenarios.approvals/test_template_substitution.approved.txt rename to tests/test_deploy.approvals/test_template_substitution.approved.txt diff --git a/tests/test_deploy.py b/tests/test_deploy.py new file mode 100644 index 00000000..5487c488 --- /dev/null +++ b/tests/test_deploy.py @@ -0,0 +1,20 @@ +from algokit_utils import ( + replace_template_variables, +) + +from tests.conftest import check_output_stability + + +def test_template_substitution() -> None: + program = """ +test TMPL_INT // TMPL_INT +test TMPL_INT +no change +test TMPL_STR // TMPL_STR +TMPL_STR +TMPL_STR // TMPL_INT +TMPL_STR // foo // +TMPL_STR // bar +""" + result = replace_template_variables(program, {"INT": 123, "STR": "ABC"}) + check_output_stability(result) diff --git a/tests/test_deploy_scenarios.py b/tests/test_deploy_scenarios.py index e2f05cca..1cb97d2e 100644 --- a/tests/test_deploy_scenarios.py +++ b/tests/test_deploy_scenarios.py @@ -13,11 +13,10 @@ get_account, get_algod_client, get_indexer_client, - get_next_version, get_sandbox_default_account, - replace_template_variables, ) -from conftest import check_output_stability, get_specs, get_unique_name, read_spec + +from tests.conftest import check_output_stability, get_specs, get_unique_name, read_spec logger = logging.getLogger(__name__) @@ -362,42 +361,3 @@ def test_deploy_with_update( logger.error(f"LogicException: {error.message}") deploy_fixture.check_log_stability() - - -def test_template_substitution() -> None: - program = """ -test TMPL_INT // TMPL_INT -test TMPL_INT -no change -test TMPL_STR // TMPL_STR -TMPL_STR -TMPL_STR // TMPL_INT -TMPL_STR // foo // -TMPL_STR // bar -""" - result = replace_template_variables(program, {"INT": 123, "STR": "ABC"}) - check_output_stability(result) - - -@pytest.mark.parametrize( - ("current", "expected_next"), - [ - ("1", "2"), - ("v1", "v2"), - ("v1-alpha", "v2-alpha"), - ("1.0", "1.1"), - ("v1.0", "v1.1"), - ("v1.0-alpha", "v1.1-alpha"), - ("1.0.0", "1.0.1"), - ("v1.0.0", "v1.0.1"), - ("v1.0.0-alpha", "v1.0.1-alpha"), - ], -) -def test_auto_version_increment(current: str, expected_next: str) -> None: - value = get_next_version(current) - assert value == expected_next - - -def test_auto_version_increment_failure() -> None: - with pytest.raises(DeploymentFailedError): - get_next_version("teapot") From 58440795ea973fa9238483d9fb00b8ef3c352df2 Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Mon, 27 Mar 2023 13:27:26 +0800 Subject: [PATCH 19/24] test: add call parameter tests --- tests/app_client_test.json | 2 +- tests/test_app_client_call.py | 29 +++ ...st_create_auto_find_ambiguous.approved.txt | 1 + tests/test_app_client_create.py | 168 +++++++++++++++++- 4 files changed, 193 insertions(+), 7 deletions(-) create mode 100644 tests/test_app_client_create.approvals/test_create_auto_find_ambiguous.approved.txt diff --git a/tests/app_client_test.json b/tests/app_client_test.json index 4bb52736..d9c51842 100644 --- a/tests/app_client_test.json +++ b/tests/app_client_test.json @@ -16,7 +16,7 @@ } }, "update_greeting(string)void": { - "read_only": true, + "read_only": false, "call_config": { "no_op": "CALL" } diff --git a/tests/test_app_client_call.py b/tests/test_app_client_call.py index 9e727481..9d2df8bc 100644 --- a/tests/test_app_client_call.py +++ b/tests/test_app_client_call.py @@ -1,7 +1,9 @@ from algokit_utils import ( ApplicationClient, + CreateCallParameters, ) from algosdk.atomic_transaction_composer import AtomicTransactionComposer +from algosdk.transaction import ApplicationCallTxn def test_abi_call_with_atc(client_fixture: ApplicationClient) -> None: @@ -24,3 +26,30 @@ def test_abi_call_multiple_times_with_atc(client_fixture: ApplicationClient) -> assert result.abi_results[0].return_value == "Hello ABI, test" assert result.abi_results[1].return_value == "Hello ABI, test2" assert result.abi_results[2].return_value == "Hello ABI, test3" + + +def test_call_parameters_from_derived_type_ignored(client_fixture: ApplicationClient) -> None: + parameters = CreateCallParameters( + extra_pages=1, + ) + + client_fixture.app_id = 123 + atc = AtomicTransactionComposer() + client_fixture.compose_call(atc, "hello", transaction_parameters=parameters, name="test") + + signed_txn = atc.txn_list[0] + app_txn = signed_txn.txn + assert isinstance(app_txn, ApplicationCallTxn) + assert app_txn.extra_pages == 0 + + +def test_create_parameters_extra_pages(client_fixture: ApplicationClient) -> None: + extra_pages = 1 + + atc = AtomicTransactionComposer() + client_fixture.compose_create(atc, "create", transaction_parameters=CreateCallParameters(extra_pages=extra_pages)) + + signed_txn = atc.txn_list[0] + app_txn = signed_txn.txn + assert isinstance(app_txn, ApplicationCallTxn) + assert app_txn.extra_pages == extra_pages diff --git a/tests/test_app_client_create.approvals/test_create_auto_find_ambiguous.approved.txt b/tests/test_app_client_create.approvals/test_create_auto_find_ambiguous.approved.txt new file mode 100644 index 00000000..ddc62386 --- /dev/null +++ b/tests/test_app_client_create.approvals/test_create_auto_find_ambiguous.approved.txt @@ -0,0 +1 @@ +Could not find an exact method to use for NoOpOC with call_config of CREATE, specify the exact method using abi_method and args parameters, considered: create()void, bare \ No newline at end of file diff --git a/tests/test_app_client_create.py b/tests/test_app_client_create.py index d9aee590..05425aca 100644 --- a/tests/test_app_client_create.py +++ b/tests/test_app_client_create.py @@ -2,15 +2,16 @@ from algokit_utils import ( ApplicationClient, ApplicationSpecification, + CreateCallParameters, get_account, get_app_id_from_tx_id, ) -from algosdk.atomic_transaction_composer import AtomicTransactionComposer -from algosdk.transaction import OnComplete +from algosdk.atomic_transaction_composer import AccountTransactionSigner, AtomicTransactionComposer +from algosdk.transaction import ApplicationCallTxn, OnComplete from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient -from tests.conftest import get_unique_name +from tests.conftest import check_output_stability, get_unique_name @pytest.fixture(scope="module") @@ -45,11 +46,17 @@ def test_abi_create_args( def test_create_auto_find(client_fixture: ApplicationClient) -> None: - client_fixture.create(transaction_parameters={"on_complete": OnComplete.OptInOC}) + client_fixture.create(transaction_parameters=CreateCallParameters(on_complete=OnComplete.OptInOC)) assert client_fixture.call("hello", name="test").return_value == "Opt In, test" +def test_create_auto_find_ambiguous(client_fixture: ApplicationClient) -> None: + with pytest.raises(Exception) as ex: + client_fixture.create() + check_output_stability(str(ex.value)) + + def test_abi_create_with_atc(client_fixture: ApplicationClient) -> None: atc = AtomicTransactionComposer() client_fixture.compose_create(atc, "create") @@ -70,5 +77,154 @@ def test_bare_create_with_atc(client_fixture: ApplicationClient) -> None: assert client_fixture.call("hello", name="test").return_value == "Hello Bare, test" -def test_create_parameters(client_fixture: ApplicationClient) -> None: - pass +def test_create_parameters_lease(client_fixture: ApplicationClient) -> None: + lease = b"a" * 32 + + atc = AtomicTransactionComposer() + client_fixture.compose_create( + atc, + "create", + transaction_parameters=CreateCallParameters( + lease=lease, + ), + ) + + signed_txn = atc.txn_list[0] + assert signed_txn.txn.lease == lease + + +def test_create_parameters_note(client_fixture: ApplicationClient) -> None: + note = b"test note" + + atc = AtomicTransactionComposer() + client_fixture.compose_create( + atc, + "create", + transaction_parameters=CreateCallParameters( + note=note, + ), + ) + + signed_txn = atc.txn_list[0] + assert signed_txn.txn.note == note + + +def test_create_parameters_on_complete(client_fixture: ApplicationClient) -> None: + on_complete = OnComplete.OptInOC + + atc = AtomicTransactionComposer() + client_fixture.compose_create( + atc, "create", transaction_parameters=CreateCallParameters(on_complete=OnComplete.OptInOC) + ) + + signed_txn = atc.txn_list[0] + app_txn = signed_txn.txn + assert isinstance(app_txn, ApplicationCallTxn) + assert app_txn.on_complete == on_complete + + +def test_create_parameters_extra_pages(client_fixture: ApplicationClient) -> None: + extra_pages = 1 + + atc = AtomicTransactionComposer() + client_fixture.compose_create(atc, "create", transaction_parameters=CreateCallParameters(extra_pages=extra_pages)) + + signed_txn = atc.txn_list[0] + app_txn = signed_txn.txn + assert isinstance(app_txn, ApplicationCallTxn) + assert app_txn.extra_pages == extra_pages + + +def test_create_parameters_signer(client_fixture: ApplicationClient) -> None: + another_account_name = get_unique_name() + account = get_account(client_fixture.algod_client, another_account_name) + signer = AccountTransactionSigner(account.private_key) + + atc = AtomicTransactionComposer() + client_fixture.compose_create(atc, "create", transaction_parameters=CreateCallParameters(signer=signer)) + + signed_txn = atc.txn_list[0] + assert isinstance(signed_txn.signer, AccountTransactionSigner) + assert signed_txn.signer.private_key == signer.private_key + + +def test_create_parameters_sender(client_fixture: ApplicationClient) -> None: + another_account_name = get_unique_name() + account = get_account(client_fixture.algod_client, another_account_name) + + atc = AtomicTransactionComposer() + client_fixture.compose_create(atc, "create", transaction_parameters=CreateCallParameters(sender=account.address)) + + signed_txn = atc.txn_list[0] + assert signed_txn.txn.sender == account.address + + +def test_create_parameters_rekey_to(client_fixture: ApplicationClient) -> None: + another_account_name = get_unique_name() + account = get_account(client_fixture.algod_client, another_account_name) + + atc = AtomicTransactionComposer() + client_fixture.compose_create(atc, "create", transaction_parameters=CreateCallParameters(rekey_to=account.address)) + + signed_txn = atc.txn_list[0] + assert signed_txn.txn.rekey_to == account.address + + +def test_create_parameters_suggested_params(client_fixture: ApplicationClient) -> None: + sp = client_fixture.algod_client.suggested_params() + sp.gen = "test-genesis" + + atc = AtomicTransactionComposer() + client_fixture.compose_create(atc, "create", transaction_parameters=CreateCallParameters(suggested_params=sp)) + + signed_txn = atc.txn_list[0] + assert signed_txn.txn.genesis_id == sp.gen + + +def test_create_parameters_boxes(client_fixture: ApplicationClient) -> None: + boxes = [(0, b"one"), (0, b"two")] + + atc = AtomicTransactionComposer() + client_fixture.compose_create(atc, "create", transaction_parameters=CreateCallParameters(boxes=boxes)) + + signed_txn = atc.txn_list[0] + assert isinstance(signed_txn.txn, ApplicationCallTxn) + assert [(b.app_index, b.name) for b in signed_txn.txn.boxes] == boxes + + +def test_create_parameters_accounts(client_fixture: ApplicationClient) -> None: + another_account_name = get_unique_name() + account = get_account(client_fixture.algod_client, another_account_name) + + atc = AtomicTransactionComposer() + client_fixture.compose_create( + atc, "create", transaction_parameters=CreateCallParameters(accounts=[account.address]) + ) + + signed_txn = atc.txn_list[0] + assert isinstance(signed_txn.txn, ApplicationCallTxn) + assert signed_txn.txn.accounts == [account.address] + + +def test_create_parameters_foreign_apps(client_fixture: ApplicationClient) -> None: + foreign_apps = [1, 2, 3] + + atc = AtomicTransactionComposer() + client_fixture.compose_create(atc, "create", transaction_parameters=CreateCallParameters(foreign_apps=foreign_apps)) + + signed_txn = atc.txn_list[0] + assert isinstance(signed_txn.txn, ApplicationCallTxn) + assert signed_txn.txn.foreign_apps == foreign_apps + + +def test_create_parameters_foreign_assets(client_fixture: ApplicationClient) -> None: + foreign_assets = [10, 20, 30] + + atc = AtomicTransactionComposer() + client_fixture.compose_create( + atc, "create", transaction_parameters=CreateCallParameters(foreign_assets=foreign_assets) + ) + + signed_txn = atc.txn_list[0] + assert isinstance(signed_txn.txn, ApplicationCallTxn) + assert signed_txn.txn.foreign_assets == foreign_assets From 490ff73e0802fcad1715f766c9575e06f8ef42b9 Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Mon, 27 Mar 2023 15:13:27 +0800 Subject: [PATCH 20/24] test: add update and delete tests --- tests/app_client_test.json | 78 ++++++++++++++++++- tests/test_app_client_call.py | 12 --- .../test_abi_delete_args_fails.approved.txt | 12 +++ tests/test_app_client_delete.py | 53 +++++++++++++ .../test_abi_update_args_fails.approved.txt | 12 +++ tests/test_app_client_update.py | 50 ++++++++++++ 6 files changed, 201 insertions(+), 16 deletions(-) create mode 100644 tests/test_app_client_delete.approvals/test_abi_delete_args_fails.approved.txt create mode 100644 tests/test_app_client_delete.py create mode 100644 tests/test_app_client_update.approvals/test_abi_update_args_fails.approved.txt create mode 100644 tests/test_app_client_update.py diff --git a/tests/app_client_test.json b/tests/app_client_test.json index d9c51842..0dcc1e6c 100644 --- a/tests/app_client_test.json +++ b/tests/app_client_test.json @@ -5,18 +5,27 @@ "update_application": "CALL" } }, + "update_args(string)void": { + "call_config": { + "update_application": "CALL" + } + }, "delete()void": { "call_config": { "delete_application": "CALL" } }, + "delete_args(string)void": { + "call_config": { + "delete_application": "CALL" + } + }, "create_opt_in()void": { "call_config": { "opt_in": "CREATE" } }, "update_greeting(string)void": { - "read_only": false, "call_config": { "no_op": "CALL" } @@ -36,15 +45,26 @@ "call_config": { "no_op": "CALL" } + }, + "hello_remember(string)string": { + "call_config": { + "no_op": "CALL" + } + }, + "get_last()string": { + "read_only": true, + "call_config": { + "no_op": "CALL" + } } }, "source": { - "approval": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDAgMQpieXRlY2Jsb2NrIDB4Njc3MjY1NjU3NDY5NmU2Nwp0eG4gTnVtQXBwQXJncwppbnRjXzAgLy8gMAo9PQpibnogbWFpbl9sMTYKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhhMGU4MTg3MiAvLyAidXBkYXRlKCl2b2lkIgo9PQpibnogbWFpbl9sMTUKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgyNDM3OGQzYyAvLyAiZGVsZXRlKCl2b2lkIgo9PQpibnogbWFpbl9sMTQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg4YmRmOWViMCAvLyAiY3JlYXRlX29wdF9pbigpdm9pZCIKPT0KYm56IG1haW5fbDEzCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4MDA1NWYwMDYgLy8gInVwZGF0ZV9ncmVldGluZyhzdHJpbmcpdm9pZCIKPT0KYm56IG1haW5fbDEyCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4NGM1YzYxYmEgLy8gImNyZWF0ZSgpdm9pZCIKPT0KYm56IG1haW5fbDExCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4ZDE0NTRjNzggLy8gImNyZWF0ZV9hcmdzKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMTAKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgwMmJlY2UxMSAvLyAiaGVsbG8oc3RyaW5nKXN0cmluZyIKPT0KYm56IG1haW5fbDkKZXJyCm1haW5fbDk6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CnR4bmEgQXBwbGljYXRpb25BcmdzIDEKY2FsbHN1YiBoZWxsb183CnN0b3JlIDAKcHVzaGJ5dGVzIDB4MTUxZjdjNzUgLy8gMHgxNTFmN2M3NQpsb2FkIDAKY29uY2F0CmxvZwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTA6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KJiYKYXNzZXJ0CnR4bmEgQXBwbGljYXRpb25BcmdzIDEKY2FsbHN1YiBjcmVhdGVhcmdzXzYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDExOgp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCj09CiYmCmFzc2VydApjYWxsc3ViIGNyZWF0ZV80CmludGNfMSAvLyAxCnJldHVybgptYWluX2wxMjoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpjYWxsc3ViIHVwZGF0ZWdyZWV0aW5nXzMKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDEzOgp0eG4gT25Db21wbGV0aW9uCmludGNfMSAvLyBPcHRJbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAo9PQomJgphc3NlcnQKY2FsbHN1YiBjcmVhdGVvcHRpbl8yCmludGNfMSAvLyAxCnJldHVybgptYWluX2wxNDoKdHhuIE9uQ29tcGxldGlvbgpwdXNoaW50IDUgLy8gRGVsZXRlQXBwbGljYXRpb24KPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgZGVsZXRlXzEKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDE1Ogp0eG4gT25Db21wbGV0aW9uCnB1c2hpbnQgNCAvLyBVcGRhdGVBcHBsaWNhdGlvbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiB1cGRhdGVfMAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTY6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KYm56IG1haW5fbDE4CmVycgptYWluX2wxODoKdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KYXNzZXJ0CmNhbGxzdWIgY3JlYXRlYmFyZV81CmludGNfMSAvLyAxCnJldHVybgoKLy8gdXBkYXRlCnVwZGF0ZV8wOgpwcm90byAwIDAKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydApwdXNoaW50IFRNUExfVVBEQVRBQkxFIC8vIFRNUExfVVBEQVRBQkxFCi8vIENoZWNrIGFwcCBpcyB1cGRhdGFibGUKYXNzZXJ0CnJldHN1YgoKLy8gZGVsZXRlCmRlbGV0ZV8xOgpwcm90byAwIDAKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydApwdXNoaW50IFRNUExfREVMRVRBQkxFIC8vIFRNUExfREVMRVRBQkxFCi8vIENoZWNrIGFwcCBpcyBkZWxldGFibGUKYXNzZXJ0CnJldHN1YgoKLy8gY3JlYXRlX29wdF9pbgpjcmVhdGVvcHRpbl8yOgpwcm90byAwIDAKYnl0ZWNfMCAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDRmNzA3NDIwNDk2ZSAvLyAiT3B0IEluIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIHVwZGF0ZV9ncmVldGluZwp1cGRhdGVncmVldGluZ18zOgpwcm90byAxIDAKYnl0ZWNfMCAvLyAiZ3JlZXRpbmciCmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMAphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIGNyZWF0ZQpjcmVhdGVfNDoKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gImdyZWV0aW5nIgpwdXNoYnl0ZXMgMHg0ODY1NmM2YzZmMjA0MTQyNDkgLy8gIkhlbGxvIEFCSSIKYXBwX2dsb2JhbF9wdXQKcmV0c3ViCgovLyBjcmVhdGVfYmFyZQpjcmVhdGViYXJlXzU6CnByb3RvIDAgMApieXRlY18wIC8vICJncmVldGluZyIKcHVzaGJ5dGVzIDB4NDg2NTZjNmM2ZjIwNDI2MTcyNjUgLy8gIkhlbGxvIEJhcmUiCmFwcF9nbG9iYWxfcHV0CnJldHN1YgoKLy8gY3JlYXRlX2FyZ3MKY3JlYXRlYXJnc182Ogpwcm90byAxIDAKYnl0ZWNfMCAvLyAiZ3JlZXRpbmciCmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMAphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIGhlbGxvCmhlbGxvXzc6CnByb3RvIDEgMQpwdXNoYnl0ZXMgMHggLy8gIiIKYnl0ZWNfMCAvLyAiZ3JlZXRpbmciCmFwcF9nbG9iYWxfZ2V0CnB1c2hieXRlcyAweDJjMjAgLy8gIiwgIgpjb25jYXQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmNvbmNhdApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWI=", + "approval": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDAgMSA1IDQgVE1QTF9VUERBVEFCTEUgVE1QTF9ERUxFVEFCTEUKYnl0ZWNibG9jayAweDY3NzI2NTY1NzQ2OTZlNjcgMHgxNTFmN2M3NSAweCAweDU5NjU3MyAweDJjMjAgMHg2YzYxNzM3NAp0eG4gTnVtQXBwQXJncwppbnRjXzAgLy8gMAo9PQpibnogbWFpbl9sMjQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhhMGU4MTg3MiAvLyAidXBkYXRlKCl2b2lkIgo9PQpibnogbWFpbl9sMjMKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg3ZDA4NTE4YiAvLyAidXBkYXRlX2FyZ3Moc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2wyMgp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDI0Mzc4ZDNjIC8vICJkZWxldGUoKXZvaWQiCj09CmJueiBtYWluX2wyMQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDU4NjFiYjUwIC8vICJkZWxldGVfYXJncyhzdHJpbmcpdm9pZCIKPT0KYm56IG1haW5fbDIwCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4OGJkZjllYjAgLy8gImNyZWF0ZV9vcHRfaW4oKXZvaWQiCj09CmJueiBtYWluX2wxOQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDAwNTVmMDA2IC8vICJ1cGRhdGVfZ3JlZXRpbmcoc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2wxOAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDRjNWM2MWJhIC8vICJjcmVhdGUoKXZvaWQiCj09CmJueiBtYWluX2wxNwp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGQxNDU0Yzc4IC8vICJjcmVhdGVfYXJncyhzdHJpbmcpdm9pZCIKPT0KYm56IG1haW5fbDE2CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4MDJiZWNlMTEgLy8gImhlbGxvKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wxNQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGJjMWMxZGQ0IC8vICJoZWxsb19yZW1lbWJlcihzdHJpbmcpc3RyaW5nIgo9PQpibnogbWFpbl9sMTQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhhOWFlNzYyNyAvLyAiZ2V0X2xhc3QoKXN0cmluZyIKPT0KYm56IG1haW5fbDEzCmVycgptYWluX2wxMzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBnZXRsYXN0XzEzCnN0b3JlIDIKYnl0ZWNfMSAvLyAweDE1MWY3Yzc1CmxvYWQgMgpjb25jYXQKbG9nCmludGNfMSAvLyAxCnJldHVybgptYWluX2wxNDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpjYWxsc3ViIGhlbGxvcmVtZW1iZXJfMTIKc3RvcmUgMQpieXRlY18xIC8vIDB4MTUxZjdjNzUKbG9hZCAxCmNvbmNhdApsb2cKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDE1Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmNhbGxzdWIgaGVsbG9fMTEKc3RvcmUgMApieXRlY18xIC8vIDB4MTUxZjdjNzUKbG9hZCAwCmNvbmNhdApsb2cKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDE2Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCj09CiYmCmFzc2VydAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmNhbGxzdWIgY3JlYXRlYXJnc18xMAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTc6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KJiYKYXNzZXJ0CmNhbGxzdWIgY3JlYXRlXzkKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDE4Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmNhbGxzdWIgdXBkYXRlZ3JlZXRpbmdfNwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTk6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCj09CiYmCmFzc2VydApjYWxsc3ViIGNyZWF0ZW9wdGluXzYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDIwOgp0eG4gT25Db21wbGV0aW9uCmludGNfMiAvLyBEZWxldGVBcHBsaWNhdGlvbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpjYWxsc3ViIGRlbGV0ZWFyZ3NfNQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjE6CnR4biBPbkNvbXBsZXRpb24KaW50Y18yIC8vIERlbGV0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGRlbGV0ZV8zCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyMjoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzMgLy8gVXBkYXRlQXBwbGljYXRpb24KPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CnR4bmEgQXBwbGljYXRpb25BcmdzIDEKY2FsbHN1YiB1cGRhdGVhcmdzXzIKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDIzOgp0eG4gT25Db21wbGV0aW9uCmludGNfMyAvLyBVcGRhdGVBcHBsaWNhdGlvbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiB1cGRhdGVfMAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjQ6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KYm56IG1haW5fbDMwCnR4biBPbkNvbXBsZXRpb24KaW50Y18zIC8vIFVwZGF0ZUFwcGxpY2F0aW9uCj09CmJueiBtYWluX2wyOQp0eG4gT25Db21wbGV0aW9uCmludGNfMiAvLyBEZWxldGVBcHBsaWNhdGlvbgo9PQpibnogbWFpbl9sMjgKZXJyCm1haW5fbDI4Ogp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBkZWxldGViYXJlXzQKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDI5Ogp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiB1cGRhdGViYXJlXzEKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDMwOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAo9PQphc3NlcnQKY2FsbHN1YiBjcmVhdGViYXJlXzgKaW50Y18xIC8vIDEKcmV0dXJuCgovLyB1cGRhdGUKdXBkYXRlXzA6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmdsb2JhbCBDcmVhdG9yQWRkcmVzcwo9PQovLyB1bmF1dGhvcml6ZWQKYXNzZXJ0CmludGMgNCAvLyBUTVBMX1VQREFUQUJMRQovLyBpcyB1cGRhdGFibGUKYXNzZXJ0CmJ5dGVjXzAgLy8gImdyZWV0aW5nIgpwdXNoYnl0ZXMgMHg1NTcwNjQ2MTc0NjU2NDIwNDE0MjQ5IC8vICJVcGRhdGVkIEFCSSIKYXBwX2dsb2JhbF9wdXQKcmV0c3ViCgovLyB1cGRhdGVfYmFyZQp1cGRhdGViYXJlXzE6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmdsb2JhbCBDcmVhdG9yQWRkcmVzcwo9PQovLyB1bmF1dGhvcml6ZWQKYXNzZXJ0CmludGMgNCAvLyBUTVBMX1VQREFUQUJMRQovLyBpcyB1cGRhdGFibGUKYXNzZXJ0CmJ5dGVjXzAgLy8gImdyZWV0aW5nIgpwdXNoYnl0ZXMgMHg1NTcwNjQ2MTc0NjU2NDIwNDI2MTcyNjUgLy8gIlVwZGF0ZWQgQmFyZSIKYXBwX2dsb2JhbF9wdXQKcmV0c3ViCgovLyB1cGRhdGVfYXJncwp1cGRhdGVhcmdzXzI6CnByb3RvIDEgMAp0eG4gU2VuZGVyCmdsb2JhbCBDcmVhdG9yQWRkcmVzcwo9PQovLyB1bmF1dGhvcml6ZWQKYXNzZXJ0CmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApieXRlY18zIC8vICJZZXMiCj09Ci8vIHBhc3NlcyB1cGRhdGUgY2hlY2sKYXNzZXJ0CmludGMgNCAvLyBUTVBMX1VQREFUQUJMRQovLyBpcyB1cGRhdGFibGUKYXNzZXJ0CmJ5dGVjXzAgLy8gImdyZWV0aW5nIgpwdXNoYnl0ZXMgMHg1NTcwNjQ2MTc0NjU2NDIwNDE3MjY3NzMgLy8gIlVwZGF0ZWQgQXJncyIKYXBwX2dsb2JhbF9wdXQKcmV0c3ViCgovLyBkZWxldGUKZGVsZXRlXzM6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmdsb2JhbCBDcmVhdG9yQWRkcmVzcwo9PQovLyB1bmF1dGhvcml6ZWQKYXNzZXJ0CmludGMgNSAvLyBUTVBMX0RFTEVUQUJMRQovLyBpcyBkZWxldGFibGUKYXNzZXJ0CnJldHN1YgoKLy8gZGVsZXRlX2JhcmUKZGVsZXRlYmFyZV80Ogpwcm90byAwIDAKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydAppbnRjIDUgLy8gVE1QTF9ERUxFVEFCTEUKLy8gaXMgZGVsZXRhYmxlCmFzc2VydApyZXRzdWIKCi8vIGRlbGV0ZV9hcmdzCmRlbGV0ZWFyZ3NfNToKcHJvdG8gMSAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjXzMgLy8gIlllcyIKPT0KLy8gcGFzc2VzIGRlbGV0ZSBjaGVjawphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIGlzIGRlbGV0YWJsZQphc3NlcnQKcmV0c3ViCgovLyBjcmVhdGVfb3B0X2luCmNyZWF0ZW9wdGluXzY6CnByb3RvIDAgMApieXRlY18wIC8vICJncmVldGluZyIKcHVzaGJ5dGVzIDB4NGY3MDc0MjA0OTZlIC8vICJPcHQgSW4iCmFwcF9nbG9iYWxfcHV0CmludGNfMSAvLyAxCnJldHVybgoKLy8gdXBkYXRlX2dyZWV0aW5nCnVwZGF0ZWdyZWV0aW5nXzc6CnByb3RvIDEgMApieXRlY18wIC8vICJncmVldGluZyIKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmFwcF9nbG9iYWxfcHV0CnJldHN1YgoKLy8gY3JlYXRlX2JhcmUKY3JlYXRlYmFyZV84Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDQ4NjU2YzZjNmYyMDQyNjE3MjY1IC8vICJIZWxsbyBCYXJlIgphcHBfZ2xvYmFsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNyZWF0ZQpjcmVhdGVfOToKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gImdyZWV0aW5nIgpwdXNoYnl0ZXMgMHg0ODY1NmM2YzZmMjA0MTQyNDkgLy8gIkhlbGxvIEFCSSIKYXBwX2dsb2JhbF9wdXQKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBjcmVhdGVfYXJncwpjcmVhdGVhcmdzXzEwOgpwcm90byAxIDAKYnl0ZWNfMCAvLyAiZ3JlZXRpbmciCmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMAphcHBfZ2xvYmFsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGhlbGxvCmhlbGxvXzExOgpwcm90byAxIDEKYnl0ZWNfMiAvLyAiIgpieXRlY18wIC8vICJncmVldGluZyIKYXBwX2dsb2JhbF9nZXQKYnl0ZWMgNCAvLyAiLCAiCmNvbmNhdApmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKY29uY2F0CmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApsZW4KaXRvYgpleHRyYWN0IDYgMApmcmFtZV9kaWcgMApjb25jYXQKZnJhbWVfYnVyeSAwCnJldHN1YgoKLy8gaGVsbG9fcmVtZW1iZXIKaGVsbG9yZW1lbWJlcl8xMjoKcHJvdG8gMSAxCmJ5dGVjXzIgLy8gIiIKYnl0ZWMgNSAvLyAibGFzdCIKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmFwcF9nbG9iYWxfcHV0CmJ5dGVjXzAgLy8gImdyZWV0aW5nIgphcHBfZ2xvYmFsX2dldApieXRlYyA0IC8vICIsICIKY29uY2F0CmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApjb25jYXQKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBnZXRfbGFzdApnZXRsYXN0XzEzOgpwcm90byAwIDEKYnl0ZWNfMiAvLyAiIgpieXRlYyA1IC8vICJsYXN0IgphcHBfZ2xvYmFsX2dldApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWI=", "clear": "I3ByYWdtYSB2ZXJzaW9uIDgKcHVzaGludCAwIC8vIDAKcmV0dXJu" }, "state": { "global": { - "num_byte_slices": 1, + "num_byte_slices": 2, "num_uints": 0 }, "local": { @@ -59,6 +79,11 @@ "type": "bytes", "key": "greeting", "descr": "" + }, + "last": { + "type": "bytes", + "key": "last", + "descr": "" } }, "reserved": {} @@ -78,6 +103,18 @@ "type": "void" } }, + { + "name": "update_args", + "args": [ + { + "type": "string", + "name": "check" + } + ], + "returns": { + "type": "void" + } + }, { "name": "delete", "args": [], @@ -85,6 +122,18 @@ "type": "void" } }, + { + "name": "delete_args", + "args": [ + { + "type": "string", + "name": "check" + } + ], + "returns": { + "type": "void" + } + }, { "name": "create_opt_in", "args": [], @@ -134,11 +183,32 @@ "returns": { "type": "string" } + }, + { + "name": "hello_remember", + "args": [ + { + "type": "string", + "name": "name" + } + ], + "returns": { + "type": "string" + } + }, + { + "name": "get_last", + "args": [], + "returns": { + "type": "string" + } } ], "networks": {} }, "bare_call_config": { - "no_op": "CREATE" + "no_op": "CREATE", + "update_application": "CALL", + "delete_application": "CALL" } } \ No newline at end of file diff --git a/tests/test_app_client_call.py b/tests/test_app_client_call.py index 9d2df8bc..f21608d2 100644 --- a/tests/test_app_client_call.py +++ b/tests/test_app_client_call.py @@ -41,15 +41,3 @@ def test_call_parameters_from_derived_type_ignored(client_fixture: ApplicationCl app_txn = signed_txn.txn assert isinstance(app_txn, ApplicationCallTxn) assert app_txn.extra_pages == 0 - - -def test_create_parameters_extra_pages(client_fixture: ApplicationClient) -> None: - extra_pages = 1 - - atc = AtomicTransactionComposer() - client_fixture.compose_create(atc, "create", transaction_parameters=CreateCallParameters(extra_pages=extra_pages)) - - signed_txn = atc.txn_list[0] - app_txn = signed_txn.txn - assert isinstance(app_txn, ApplicationCallTxn) - assert app_txn.extra_pages == extra_pages diff --git a/tests/test_app_client_delete.approvals/test_abi_delete_args_fails.approved.txt b/tests/test_app_client_delete.approvals/test_abi_delete_args_fails.approved.txt new file mode 100644 index 00000000..e80c73fa --- /dev/null +++ b/tests/test_app_client_delete.approvals/test_abi_delete_args_fails.approved.txt @@ -0,0 +1,12 @@ +Txn {txn} had error 'assert failed pc=581' at PC 581 and Source Line 337: + + frame_dig -1 + extract 2 0 + bytec_3 // "Yes" + == + // passes delete check + assert <-- Error + intc 5 // deletable + // is deletable + assert + retsub \ No newline at end of file diff --git a/tests/test_app_client_delete.py b/tests/test_app_client_delete.py new file mode 100644 index 00000000..e38c5643 --- /dev/null +++ b/tests/test_app_client_delete.py @@ -0,0 +1,53 @@ +import pytest +from algokit_utils import ( + Account, + ApplicationClient, + ApplicationSpecification, + LogicError, + get_account, +) +from algosdk.v2client.algod import AlgodClient +from algosdk.v2client.indexer import IndexerClient + +from tests.conftest import check_output_stability, get_unique_name + + +@pytest.fixture(scope="module") +def creator(algod_client: AlgodClient) -> Account: + creator_name = get_unique_name() + creator = get_account(algod_client, creator_name) + return creator + + +@pytest.fixture +def client_fixture( + algod_client: AlgodClient, indexer_client: IndexerClient, creator: Account, app_spec: ApplicationSpecification +) -> ApplicationClient: + client = ApplicationClient(algod_client, app_spec, creator=creator, indexer_client=indexer_client) + client.create("create") + return client + + +def test_abi_delete(client_fixture: ApplicationClient) -> None: + delete_response = client_fixture.delete("delete") + + assert delete_response.tx_id + + +def test_bare_delete(client_fixture: ApplicationClient) -> None: + delete_response = client_fixture.delete(call_abi_method=False) + + assert delete_response.tx_id + + +def test_abi_delete_args(client_fixture: ApplicationClient) -> None: + delete_response = client_fixture.delete("delete_args", check="Yes") + + assert delete_response.tx_id + + +def test_abi_delete_args_fails(client_fixture: ApplicationClient) -> None: + with pytest.raises(LogicError) as ex: + client_fixture.delete("delete_args", check="No") + + check_output_stability(str(ex.value).replace(ex.value.transaction_id, "{txn}")) diff --git a/tests/test_app_client_update.approvals/test_abi_update_args_fails.approved.txt b/tests/test_app_client_update.approvals/test_abi_update_args_fails.approved.txt new file mode 100644 index 00000000..71ce0510 --- /dev/null +++ b/tests/test_app_client_update.approvals/test_abi_update_args_fails.approved.txt @@ -0,0 +1,12 @@ +Txn {txn} had error 'assert failed pc=518' at PC 518 and Source Line 289: + + frame_dig -1 + extract 2 0 + bytec_3 // "Yes" + == + // passes update check + assert <-- Error + intc 4 // updatable + // is updatable + assert + bytec_0 // "greeting" \ No newline at end of file diff --git a/tests/test_app_client_update.py b/tests/test_app_client_update.py new file mode 100644 index 00000000..14994400 --- /dev/null +++ b/tests/test_app_client_update.py @@ -0,0 +1,50 @@ +import pytest +from algokit_utils import ( + ApplicationClient, + ApplicationSpecification, + LogicError, + get_account, +) +from algosdk.v2client.algod import AlgodClient +from algosdk.v2client.indexer import IndexerClient + +from tests.conftest import check_output_stability, get_unique_name + + +@pytest.fixture(scope="module") +def client_fixture( + algod_client: AlgodClient, indexer_client: IndexerClient, app_spec: ApplicationSpecification +) -> ApplicationClient: + creator_name = get_unique_name() + creator = get_account(algod_client, creator_name) + client = ApplicationClient(algod_client, app_spec, creator=creator, indexer_client=indexer_client) + client.create("create") + return client + + +def test_abi_update(client_fixture: ApplicationClient) -> None: + update_response = client_fixture.update("update") + + assert update_response.tx_id + assert client_fixture.call("hello", name="test").return_value == "Updated ABI, test" + + +def test_bare_update(client_fixture: ApplicationClient) -> None: + update_response = client_fixture.update(call_abi_method=False) + + assert update_response.tx_id + assert client_fixture.call("hello", name="test").return_value == "Updated Bare, test" + + +def test_abi_update_args(client_fixture: ApplicationClient) -> None: + update_response = client_fixture.update("update_args", check="Yes") + + assert update_response.tx_id + assert client_fixture.call("hello", name="test").return_value == "Updated Args, test" + + +def test_abi_update_args_fails(client_fixture: ApplicationClient) -> None: + with pytest.raises(LogicError) as ex: + client_fixture.update("update_args", check="No") + + check_output_stability(str(ex.value).replace(ex.value.transaction_id, "{txn}")) From 62624819b2c02d735c87449d1fcfc0953a2412c0 Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Mon, 27 Mar 2023 23:48:38 +0800 Subject: [PATCH 21/24] test: add opt_in, close_out, clear_state tests --- src/algokit_utils/application_client.py | 2 +- tests/app_client_test.json | 79 ++++++++++++++++--- tests/conftest.py | 8 ++ tests/test_app_client_clear_state.py | 59 ++++++++++++++ ...test_abi_close_out_args_fails.approved.txt | 10 +++ tests/test_app_client_close_out.py | 63 +++++++++++++++ .../test_abi_delete_args_fails.approved.txt | 4 +- .../test_abi_update_args_fails.approved.txt | 12 +++ tests/test_app_client_opt_in.py | 54 +++++++++++++ .../test_abi_update_args_fails.approved.txt | 4 +- ...isting_deletable_app_succeeds.approved.txt | 8 -- tests/test_deploy_scenarios.py | 13 --- 12 files changed, 281 insertions(+), 35 deletions(-) create mode 100644 tests/test_app_client_clear_state.py create mode 100644 tests/test_app_client_close_out.approvals/test_abi_close_out_args_fails.approved.txt create mode 100644 tests/test_app_client_close_out.py create mode 100644 tests/test_app_client_opt_in.approvals/test_abi_update_args_fails.approved.txt create mode 100644 tests/test_app_client_opt_in.py delete mode 100644 tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_deletable_app_succeeds.approved.txt diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index a42249c6..368877ef 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -771,7 +771,7 @@ def clear_state( self, transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = None, app_args: list[bytes] | None = None, - ) -> TransactionResponse | ABITransactionResponse: + ) -> TransactionResponse: """Submits a signed transaction with on_complete=ClearState""" atc = AtomicTransactionComposer() self.compose_clear_state( diff --git a/tests/app_client_test.json b/tests/app_client_test.json index 0dcc1e6c..9cb1c32f 100644 --- a/tests/app_client_test.json +++ b/tests/app_client_test.json @@ -56,19 +56,39 @@ "call_config": { "no_op": "CALL" } + }, + "opt_in()void": { + "call_config": { + "opt_in": "CALL" + } + }, + "opt_in_args(string)void": { + "call_config": { + "opt_in": "CALL" + } + }, + "close_out()void": { + "call_config": { + "close_out": "CALL" + } + }, + "close_out_args(string)void": { + "call_config": { + "close_out": "CALL" + } } }, "source": { - "approval": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDAgMSA1IDQgVE1QTF9VUERBVEFCTEUgVE1QTF9ERUxFVEFCTEUKYnl0ZWNibG9jayAweDY3NzI2NTY1NzQ2OTZlNjcgMHgxNTFmN2M3NSAweCAweDU5NjU3MyAweDJjMjAgMHg2YzYxNzM3NAp0eG4gTnVtQXBwQXJncwppbnRjXzAgLy8gMAo9PQpibnogbWFpbl9sMjQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhhMGU4MTg3MiAvLyAidXBkYXRlKCl2b2lkIgo9PQpibnogbWFpbl9sMjMKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg3ZDA4NTE4YiAvLyAidXBkYXRlX2FyZ3Moc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2wyMgp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDI0Mzc4ZDNjIC8vICJkZWxldGUoKXZvaWQiCj09CmJueiBtYWluX2wyMQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDU4NjFiYjUwIC8vICJkZWxldGVfYXJncyhzdHJpbmcpdm9pZCIKPT0KYm56IG1haW5fbDIwCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4OGJkZjllYjAgLy8gImNyZWF0ZV9vcHRfaW4oKXZvaWQiCj09CmJueiBtYWluX2wxOQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDAwNTVmMDA2IC8vICJ1cGRhdGVfZ3JlZXRpbmcoc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2wxOAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDRjNWM2MWJhIC8vICJjcmVhdGUoKXZvaWQiCj09CmJueiBtYWluX2wxNwp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGQxNDU0Yzc4IC8vICJjcmVhdGVfYXJncyhzdHJpbmcpdm9pZCIKPT0KYm56IG1haW5fbDE2CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4MDJiZWNlMTEgLy8gImhlbGxvKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wxNQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGJjMWMxZGQ0IC8vICJoZWxsb19yZW1lbWJlcihzdHJpbmcpc3RyaW5nIgo9PQpibnogbWFpbl9sMTQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhhOWFlNzYyNyAvLyAiZ2V0X2xhc3QoKXN0cmluZyIKPT0KYm56IG1haW5fbDEzCmVycgptYWluX2wxMzoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBnZXRsYXN0XzEzCnN0b3JlIDIKYnl0ZWNfMSAvLyAweDE1MWY3Yzc1CmxvYWQgMgpjb25jYXQKbG9nCmludGNfMSAvLyAxCnJldHVybgptYWluX2wxNDoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzAgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpjYWxsc3ViIGhlbGxvcmVtZW1iZXJfMTIKc3RvcmUgMQpieXRlY18xIC8vIDB4MTUxZjdjNzUKbG9hZCAxCmNvbmNhdApsb2cKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDE1Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmNhbGxzdWIgaGVsbG9fMTEKc3RvcmUgMApieXRlY18xIC8vIDB4MTUxZjdjNzUKbG9hZCAwCmNvbmNhdApsb2cKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDE2Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCj09CiYmCmFzc2VydAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmNhbGxzdWIgY3JlYXRlYXJnc18xMAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTc6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKPT0KJiYKYXNzZXJ0CmNhbGxzdWIgY3JlYXRlXzkKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDE4Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmNhbGxzdWIgdXBkYXRlZ3JlZXRpbmdfNwppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMTk6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCj09CiYmCmFzc2VydApjYWxsc3ViIGNyZWF0ZW9wdGluXzYKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDIwOgp0eG4gT25Db21wbGV0aW9uCmludGNfMiAvLyBEZWxldGVBcHBsaWNhdGlvbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpjYWxsc3ViIGRlbGV0ZWFyZ3NfNQppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjE6CnR4biBPbkNvbXBsZXRpb24KaW50Y18yIC8vIERlbGV0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMCAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGRlbGV0ZV8zCmludGNfMSAvLyAxCnJldHVybgptYWluX2wyMjoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzMgLy8gVXBkYXRlQXBwbGljYXRpb24KPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18wIC8vIDAKIT0KJiYKYXNzZXJ0CnR4bmEgQXBwbGljYXRpb25BcmdzIDEKY2FsbHN1YiB1cGRhdGVhcmdzXzIKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDIzOgp0eG4gT25Db21wbGV0aW9uCmludGNfMyAvLyBVcGRhdGVBcHBsaWNhdGlvbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiB1cGRhdGVfMAppbnRjXzEgLy8gMQpyZXR1cm4KbWFpbl9sMjQ6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE5vT3AKPT0KYm56IG1haW5fbDMwCnR4biBPbkNvbXBsZXRpb24KaW50Y18zIC8vIFVwZGF0ZUFwcGxpY2F0aW9uCj09CmJueiBtYWluX2wyOQp0eG4gT25Db21wbGV0aW9uCmludGNfMiAvLyBEZWxldGVBcHBsaWNhdGlvbgo9PQpibnogbWFpbl9sMjgKZXJyCm1haW5fbDI4Ogp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiBkZWxldGViYXJlXzQKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDI5Ogp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAohPQphc3NlcnQKY2FsbHN1YiB1cGRhdGViYXJlXzEKaW50Y18xIC8vIDEKcmV0dXJuCm1haW5fbDMwOgp0eG4gQXBwbGljYXRpb25JRAppbnRjXzAgLy8gMAo9PQphc3NlcnQKY2FsbHN1YiBjcmVhdGViYXJlXzgKaW50Y18xIC8vIDEKcmV0dXJuCgovLyB1cGRhdGUKdXBkYXRlXzA6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmdsb2JhbCBDcmVhdG9yQWRkcmVzcwo9PQovLyB1bmF1dGhvcml6ZWQKYXNzZXJ0CmludGMgNCAvLyBUTVBMX1VQREFUQUJMRQovLyBpcyB1cGRhdGFibGUKYXNzZXJ0CmJ5dGVjXzAgLy8gImdyZWV0aW5nIgpwdXNoYnl0ZXMgMHg1NTcwNjQ2MTc0NjU2NDIwNDE0MjQ5IC8vICJVcGRhdGVkIEFCSSIKYXBwX2dsb2JhbF9wdXQKcmV0c3ViCgovLyB1cGRhdGVfYmFyZQp1cGRhdGViYXJlXzE6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmdsb2JhbCBDcmVhdG9yQWRkcmVzcwo9PQovLyB1bmF1dGhvcml6ZWQKYXNzZXJ0CmludGMgNCAvLyBUTVBMX1VQREFUQUJMRQovLyBpcyB1cGRhdGFibGUKYXNzZXJ0CmJ5dGVjXzAgLy8gImdyZWV0aW5nIgpwdXNoYnl0ZXMgMHg1NTcwNjQ2MTc0NjU2NDIwNDI2MTcyNjUgLy8gIlVwZGF0ZWQgQmFyZSIKYXBwX2dsb2JhbF9wdXQKcmV0c3ViCgovLyB1cGRhdGVfYXJncwp1cGRhdGVhcmdzXzI6CnByb3RvIDEgMAp0eG4gU2VuZGVyCmdsb2JhbCBDcmVhdG9yQWRkcmVzcwo9PQovLyB1bmF1dGhvcml6ZWQKYXNzZXJ0CmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApieXRlY18zIC8vICJZZXMiCj09Ci8vIHBhc3NlcyB1cGRhdGUgY2hlY2sKYXNzZXJ0CmludGMgNCAvLyBUTVBMX1VQREFUQUJMRQovLyBpcyB1cGRhdGFibGUKYXNzZXJ0CmJ5dGVjXzAgLy8gImdyZWV0aW5nIgpwdXNoYnl0ZXMgMHg1NTcwNjQ2MTc0NjU2NDIwNDE3MjY3NzMgLy8gIlVwZGF0ZWQgQXJncyIKYXBwX2dsb2JhbF9wdXQKcmV0c3ViCgovLyBkZWxldGUKZGVsZXRlXzM6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmdsb2JhbCBDcmVhdG9yQWRkcmVzcwo9PQovLyB1bmF1dGhvcml6ZWQKYXNzZXJ0CmludGMgNSAvLyBUTVBMX0RFTEVUQUJMRQovLyBpcyBkZWxldGFibGUKYXNzZXJ0CnJldHN1YgoKLy8gZGVsZXRlX2JhcmUKZGVsZXRlYmFyZV80Ogpwcm90byAwIDAKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydAppbnRjIDUgLy8gVE1QTF9ERUxFVEFCTEUKLy8gaXMgZGVsZXRhYmxlCmFzc2VydApyZXRzdWIKCi8vIGRlbGV0ZV9hcmdzCmRlbGV0ZWFyZ3NfNToKcHJvdG8gMSAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjXzMgLy8gIlllcyIKPT0KLy8gcGFzc2VzIGRlbGV0ZSBjaGVjawphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIGlzIGRlbGV0YWJsZQphc3NlcnQKcmV0c3ViCgovLyBjcmVhdGVfb3B0X2luCmNyZWF0ZW9wdGluXzY6CnByb3RvIDAgMApieXRlY18wIC8vICJncmVldGluZyIKcHVzaGJ5dGVzIDB4NGY3MDc0MjA0OTZlIC8vICJPcHQgSW4iCmFwcF9nbG9iYWxfcHV0CmludGNfMSAvLyAxCnJldHVybgoKLy8gdXBkYXRlX2dyZWV0aW5nCnVwZGF0ZWdyZWV0aW5nXzc6CnByb3RvIDEgMApieXRlY18wIC8vICJncmVldGluZyIKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmFwcF9nbG9iYWxfcHV0CnJldHN1YgoKLy8gY3JlYXRlX2JhcmUKY3JlYXRlYmFyZV84Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDQ4NjU2YzZjNmYyMDQyNjE3MjY1IC8vICJIZWxsbyBCYXJlIgphcHBfZ2xvYmFsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGNyZWF0ZQpjcmVhdGVfOToKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gImdyZWV0aW5nIgpwdXNoYnl0ZXMgMHg0ODY1NmM2YzZmMjA0MTQyNDkgLy8gIkhlbGxvIEFCSSIKYXBwX2dsb2JhbF9wdXQKaW50Y18xIC8vIDEKcmV0dXJuCgovLyBjcmVhdGVfYXJncwpjcmVhdGVhcmdzXzEwOgpwcm90byAxIDAKYnl0ZWNfMCAvLyAiZ3JlZXRpbmciCmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMAphcHBfZ2xvYmFsX3B1dAppbnRjXzEgLy8gMQpyZXR1cm4KCi8vIGhlbGxvCmhlbGxvXzExOgpwcm90byAxIDEKYnl0ZWNfMiAvLyAiIgpieXRlY18wIC8vICJncmVldGluZyIKYXBwX2dsb2JhbF9nZXQKYnl0ZWMgNCAvLyAiLCAiCmNvbmNhdApmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKY29uY2F0CmZyYW1lX2J1cnkgMApmcmFtZV9kaWcgMApsZW4KaXRvYgpleHRyYWN0IDYgMApmcmFtZV9kaWcgMApjb25jYXQKZnJhbWVfYnVyeSAwCnJldHN1YgoKLy8gaGVsbG9fcmVtZW1iZXIKaGVsbG9yZW1lbWJlcl8xMjoKcHJvdG8gMSAxCmJ5dGVjXzIgLy8gIiIKYnl0ZWMgNSAvLyAibGFzdCIKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmFwcF9nbG9iYWxfcHV0CmJ5dGVjXzAgLy8gImdyZWV0aW5nIgphcHBfZ2xvYmFsX2dldApieXRlYyA0IC8vICIsICIKY29uY2F0CmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApjb25jYXQKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBnZXRfbGFzdApnZXRsYXN0XzEzOgpwcm90byAwIDEKYnl0ZWNfMiAvLyAiIgpieXRlYyA1IC8vICJsYXN0IgphcHBfZ2xvYmFsX2dldApmcmFtZV9idXJ5IDAKZnJhbWVfZGlnIDAKbGVuCml0b2IKZXh0cmFjdCA2IDAKZnJhbWVfZGlnIDAKY29uY2F0CmZyYW1lX2J1cnkgMApyZXRzdWI=", - "clear": "I3ByYWdtYSB2ZXJzaW9uIDgKcHVzaGludCAwIC8vIDAKcmV0dXJu" + "approval": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDEgMCAyIDUgVE1QTF9VUERBVEFCTEUgVE1QTF9ERUxFVEFCTEUKYnl0ZWNibG9jayAweDY3NzI2NTY1NzQ2OTZlNjcgMHg2YzYxNzM3NCAweDU5NjU3MyAweDE1MWY3Yzc1IDB4IDB4MmMyMAp0eG4gTnVtQXBwQXJncwppbnRjXzEgLy8gMAo9PQpibnogbWFpbl9sMzIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhhMGU4MTg3MiAvLyAidXBkYXRlKCl2b2lkIgo9PQpibnogbWFpbl9sMzEKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHg3ZDA4NTE4YiAvLyAidXBkYXRlX2FyZ3Moc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2wzMAp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDI0Mzc4ZDNjIC8vICJkZWxldGUoKXZvaWQiCj09CmJueiBtYWluX2wyOQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDU4NjFiYjUwIC8vICJkZWxldGVfYXJncyhzdHJpbmcpdm9pZCIKPT0KYm56IG1haW5fbDI4CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4OGJkZjllYjAgLy8gImNyZWF0ZV9vcHRfaW4oKXZvaWQiCj09CmJueiBtYWluX2wyNwp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDAwNTVmMDA2IC8vICJ1cGRhdGVfZ3JlZXRpbmcoc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2wyNgp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweDRjNWM2MWJhIC8vICJjcmVhdGUoKXZvaWQiCj09CmJueiBtYWluX2wyNQp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGQxNDU0Yzc4IC8vICJjcmVhdGVfYXJncyhzdHJpbmcpdm9pZCIKPT0KYm56IG1haW5fbDI0CnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4MDJiZWNlMTEgLy8gImhlbGxvKHN0cmluZylzdHJpbmciCj09CmJueiBtYWluX2wyMwp0eG5hIEFwcGxpY2F0aW9uQXJncyAwCnB1c2hieXRlcyAweGJjMWMxZGQ0IC8vICJoZWxsb19yZW1lbWJlcihzdHJpbmcpc3RyaW5nIgo9PQpibnogbWFpbl9sMjIKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhhOWFlNzYyNyAvLyAiZ2V0X2xhc3QoKXN0cmluZyIKPT0KYm56IG1haW5fbDIxCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4MzBjNmQ1OGEgLy8gIm9wdF9pbigpdm9pZCIKPT0KYm56IG1haW5fbDIwCnR4bmEgQXBwbGljYXRpb25BcmdzIDAKcHVzaGJ5dGVzIDB4MjJjN2RlZGEgLy8gIm9wdF9pbl9hcmdzKHN0cmluZyl2b2lkIgo9PQpibnogbWFpbl9sMTkKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHgxNjU4YWEyZiAvLyAiY2xvc2Vfb3V0KCl2b2lkIgo9PQpibnogbWFpbl9sMTgKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMApwdXNoYnl0ZXMgMHhkZTg0ZDlhZCAvLyAiY2xvc2Vfb3V0X2FyZ3Moc3RyaW5nKXZvaWQiCj09CmJueiBtYWluX2wxNwplcnIKbWFpbl9sMTc6CnR4biBPbkNvbXBsZXRpb24KaW50Y18yIC8vIENsb3NlT3V0Cj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMSAvLyAwCiE9CiYmCmFzc2VydAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmNhbGxzdWIgY2xvc2VvdXRhcmdzXzE5CmludGNfMCAvLyAxCnJldHVybgptYWluX2wxODoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzIgLy8gQ2xvc2VPdXQKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18xIC8vIDAKIT0KJiYKYXNzZXJ0CmNhbGxzdWIgY2xvc2VvdXRfMTcKaW50Y18wIC8vIDEKcmV0dXJuCm1haW5fbDE5Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMCAvLyBPcHRJbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzEgLy8gMAohPQomJgphc3NlcnQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpjYWxsc3ViIG9wdGluYXJnc18xNgppbnRjXzAgLy8gMQpyZXR1cm4KbWFpbl9sMjA6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMSAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIG9wdGluXzE0CmludGNfMCAvLyAxCnJldHVybgptYWluX2wyMToKdHhuIE9uQ29tcGxldGlvbgppbnRjXzEgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzEgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiBnZXRsYXN0XzEzCnN0b3JlIDIKYnl0ZWNfMyAvLyAweDE1MWY3Yzc1CmxvYWQgMgpjb25jYXQKbG9nCmludGNfMCAvLyAxCnJldHVybgptYWluX2wyMjoKdHhuIE9uQ29tcGxldGlvbgppbnRjXzEgLy8gTm9PcAo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzEgLy8gMAohPQomJgphc3NlcnQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpjYWxsc3ViIGhlbGxvcmVtZW1iZXJfMTIKc3RvcmUgMQpieXRlY18zIC8vIDB4MTUxZjdjNzUKbG9hZCAxCmNvbmNhdApsb2cKaW50Y18wIC8vIDEKcmV0dXJuCm1haW5fbDIzOgp0eG4gT25Db21wbGV0aW9uCmludGNfMSAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMSAvLyAwCiE9CiYmCmFzc2VydAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmNhbGxzdWIgaGVsbG9fMTEKc3RvcmUgMApieXRlY18zIC8vIDB4MTUxZjdjNzUKbG9hZCAwCmNvbmNhdApsb2cKaW50Y18wIC8vIDEKcmV0dXJuCm1haW5fbDI0Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMSAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMSAvLyAwCj09CiYmCmFzc2VydAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmNhbGxzdWIgY3JlYXRlYXJnc18xMAppbnRjXzAgLy8gMQpyZXR1cm4KbWFpbl9sMjU6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE5vT3AKPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18xIC8vIDAKPT0KJiYKYXNzZXJ0CmNhbGxzdWIgY3JlYXRlXzkKaW50Y18wIC8vIDEKcmV0dXJuCm1haW5fbDI2Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMSAvLyBOb09wCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMSAvLyAwCiE9CiYmCmFzc2VydAp0eG5hIEFwcGxpY2F0aW9uQXJncyAxCmNhbGxzdWIgdXBkYXRlZ3JlZXRpbmdfNwppbnRjXzAgLy8gMQpyZXR1cm4KbWFpbl9sMjc6CnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE9wdEluCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMSAvLyAwCj09CiYmCmFzc2VydApjYWxsc3ViIGNyZWF0ZW9wdGluXzYKaW50Y18wIC8vIDEKcmV0dXJuCm1haW5fbDI4Ogp0eG4gT25Db21wbGV0aW9uCmludGNfMyAvLyBEZWxldGVBcHBsaWNhdGlvbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzEgLy8gMAohPQomJgphc3NlcnQKdHhuYSBBcHBsaWNhdGlvbkFyZ3MgMQpjYWxsc3ViIGRlbGV0ZWFyZ3NfNQppbnRjXzAgLy8gMQpyZXR1cm4KbWFpbl9sMjk6CnR4biBPbkNvbXBsZXRpb24KaW50Y18zIC8vIERlbGV0ZUFwcGxpY2F0aW9uCj09CnR4biBBcHBsaWNhdGlvbklECmludGNfMSAvLyAwCiE9CiYmCmFzc2VydApjYWxsc3ViIGRlbGV0ZV8zCmludGNfMCAvLyAxCnJldHVybgptYWluX2wzMDoKdHhuIE9uQ29tcGxldGlvbgpwdXNoaW50IDQgLy8gVXBkYXRlQXBwbGljYXRpb24KPT0KdHhuIEFwcGxpY2F0aW9uSUQKaW50Y18xIC8vIDAKIT0KJiYKYXNzZXJ0CnR4bmEgQXBwbGljYXRpb25BcmdzIDEKY2FsbHN1YiB1cGRhdGVhcmdzXzIKaW50Y18wIC8vIDEKcmV0dXJuCm1haW5fbDMxOgp0eG4gT25Db21wbGV0aW9uCnB1c2hpbnQgNCAvLyBVcGRhdGVBcHBsaWNhdGlvbgo9PQp0eG4gQXBwbGljYXRpb25JRAppbnRjXzEgLy8gMAohPQomJgphc3NlcnQKY2FsbHN1YiB1cGRhdGVfMAppbnRjXzAgLy8gMQpyZXR1cm4KbWFpbl9sMzI6CnR4biBPbkNvbXBsZXRpb24KaW50Y18xIC8vIE5vT3AKPT0KYm56IG1haW5fbDQyCnR4biBPbkNvbXBsZXRpb24KaW50Y18wIC8vIE9wdEluCj09CmJueiBtYWluX2w0MQp0eG4gT25Db21wbGV0aW9uCmludGNfMiAvLyBDbG9zZU91dAo9PQpibnogbWFpbl9sNDAKdHhuIE9uQ29tcGxldGlvbgpwdXNoaW50IDQgLy8gVXBkYXRlQXBwbGljYXRpb24KPT0KYm56IG1haW5fbDM5CnR4biBPbkNvbXBsZXRpb24KaW50Y18zIC8vIERlbGV0ZUFwcGxpY2F0aW9uCj09CmJueiBtYWluX2wzOAplcnIKbWFpbl9sMzg6CnR4biBBcHBsaWNhdGlvbklECmludGNfMSAvLyAwCiE9CmFzc2VydApjYWxsc3ViIGRlbGV0ZWJhcmVfNAppbnRjXzAgLy8gMQpyZXR1cm4KbWFpbl9sMzk6CnR4biBBcHBsaWNhdGlvbklECmludGNfMSAvLyAwCiE9CmFzc2VydApjYWxsc3ViIHVwZGF0ZWJhcmVfMQppbnRjXzAgLy8gMQpyZXR1cm4KbWFpbl9sNDA6CnR4biBBcHBsaWNhdGlvbklECmludGNfMSAvLyAwCiE9CmFzc2VydApjYWxsc3ViIGNsb3Nlb3V0YmFyZV8xOAppbnRjXzAgLy8gMQpyZXR1cm4KbWFpbl9sNDE6CnR4biBBcHBsaWNhdGlvbklECmludGNfMSAvLyAwCiE9CmFzc2VydApjYWxsc3ViIG9wdGluYmFyZV8xNQppbnRjXzAgLy8gMQpyZXR1cm4KbWFpbl9sNDI6CnR4biBBcHBsaWNhdGlvbklECmludGNfMSAvLyAwCj09CmFzc2VydApjYWxsc3ViIGNyZWF0ZWJhcmVfOAppbnRjXzAgLy8gMQpyZXR1cm4KCi8vIHVwZGF0ZQp1cGRhdGVfMDoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMCAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MTQyNDkgLy8gIlVwZGF0ZWQgQUJJIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIHVwZGF0ZV9iYXJlCnVwZGF0ZWJhcmVfMToKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMCAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MjYxNzI2NSAvLyAiVXBkYXRlZCBCYXJlIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIHVwZGF0ZV9hcmdzCnVwZGF0ZWFyZ3NfMjoKcHJvdG8gMSAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjXzIgLy8gIlllcyIKPT0KLy8gcGFzc2VzIHVwZGF0ZSBjaGVjawphc3NlcnQKaW50YyA0IC8vIFRNUExfVVBEQVRBQkxFCi8vIGlzIHVwZGF0YWJsZQphc3NlcnQKYnl0ZWNfMCAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDU1NzA2NDYxNzQ2NTY0MjA0MTcyNjc3MyAvLyAiVXBkYXRlZCBBcmdzIgphcHBfZ2xvYmFsX3B1dApyZXRzdWIKCi8vIGRlbGV0ZQpkZWxldGVfMzoKcHJvdG8gMCAwCnR4biBTZW5kZXIKZ2xvYmFsIENyZWF0b3JBZGRyZXNzCj09Ci8vIHVuYXV0aG9yaXplZAphc3NlcnQKaW50YyA1IC8vIFRNUExfREVMRVRBQkxFCi8vIGlzIGRlbGV0YWJsZQphc3NlcnQKcmV0c3ViCgovLyBkZWxldGVfYmFyZQpkZWxldGViYXJlXzQ6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmdsb2JhbCBDcmVhdG9yQWRkcmVzcwo9PQovLyB1bmF1dGhvcml6ZWQKYXNzZXJ0CmludGMgNSAvLyBUTVBMX0RFTEVUQUJMRQovLyBpcyBkZWxldGFibGUKYXNzZXJ0CnJldHN1YgoKLy8gZGVsZXRlX2FyZ3MKZGVsZXRlYXJnc181Ogpwcm90byAxIDAKdHhuIFNlbmRlcgpnbG9iYWwgQ3JlYXRvckFkZHJlc3MKPT0KLy8gdW5hdXRob3JpemVkCmFzc2VydApmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYnl0ZWNfMiAvLyAiWWVzIgo9PQovLyBwYXNzZXMgZGVsZXRlIGNoZWNrCmFzc2VydAppbnRjIDUgLy8gVE1QTF9ERUxFVEFCTEUKLy8gaXMgZGVsZXRhYmxlCmFzc2VydApyZXRzdWIKCi8vIGNyZWF0ZV9vcHRfaW4KY3JlYXRlb3B0aW5fNjoKcHJvdG8gMCAwCmJ5dGVjXzAgLy8gImdyZWV0aW5nIgpwdXNoYnl0ZXMgMHg0ZjcwNzQyMDQ5NmUgLy8gIk9wdCBJbiIKYXBwX2dsb2JhbF9wdXQKaW50Y18wIC8vIDEKcmV0dXJuCgovLyB1cGRhdGVfZ3JlZXRpbmcKdXBkYXRlZ3JlZXRpbmdfNzoKcHJvdG8gMSAwCmJ5dGVjXzAgLy8gImdyZWV0aW5nIgpmcmFtZV9kaWcgLTEKZXh0cmFjdCAyIDAKYXBwX2dsb2JhbF9wdXQKcmV0c3ViCgovLyBjcmVhdGVfYmFyZQpjcmVhdGViYXJlXzg6CnByb3RvIDAgMApieXRlY18wIC8vICJncmVldGluZyIKcHVzaGJ5dGVzIDB4NDg2NTZjNmM2ZjIwNDI2MTcyNjUgLy8gIkhlbGxvIEJhcmUiCmFwcF9nbG9iYWxfcHV0CmludGNfMCAvLyAxCnJldHVybgoKLy8gY3JlYXRlCmNyZWF0ZV85Ogpwcm90byAwIDAKYnl0ZWNfMCAvLyAiZ3JlZXRpbmciCnB1c2hieXRlcyAweDQ4NjU2YzZjNmYyMDQxNDI0OSAvLyAiSGVsbG8gQUJJIgphcHBfZ2xvYmFsX3B1dAppbnRjXzAgLy8gMQpyZXR1cm4KCi8vIGNyZWF0ZV9hcmdzCmNyZWF0ZWFyZ3NfMTA6CnByb3RvIDEgMApieXRlY18wIC8vICJncmVldGluZyIKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmFwcF9nbG9iYWxfcHV0CmludGNfMCAvLyAxCnJldHVybgoKLy8gaGVsbG8KaGVsbG9fMTE6CnByb3RvIDEgMQpieXRlYyA0IC8vICIiCmJ5dGVjXzAgLy8gImdyZWV0aW5nIgphcHBfZ2xvYmFsX2dldApieXRlYyA1IC8vICIsICIKY29uY2F0CmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApjb25jYXQKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBoZWxsb19yZW1lbWJlcgpoZWxsb3JlbWVtYmVyXzEyOgpwcm90byAxIDEKYnl0ZWMgNCAvLyAiIgp0eG4gU2VuZGVyCmJ5dGVjXzEgLy8gImxhc3QiCmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMAphcHBfbG9jYWxfcHV0CmJ5dGVjXzAgLy8gImdyZWV0aW5nIgphcHBfZ2xvYmFsX2dldApieXRlYyA1IC8vICIsICIKY29uY2F0CmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApjb25jYXQKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBnZXRfbGFzdApnZXRsYXN0XzEzOgpwcm90byAwIDEKYnl0ZWMgNCAvLyAiIgp0eG4gU2VuZGVyCmJ5dGVjXzEgLy8gImxhc3QiCmFwcF9sb2NhbF9nZXQKZnJhbWVfYnVyeSAwCmZyYW1lX2RpZyAwCmxlbgppdG9iCmV4dHJhY3QgNiAwCmZyYW1lX2RpZyAwCmNvbmNhdApmcmFtZV9idXJ5IDAKcmV0c3ViCgovLyBvcHRfaW4Kb3B0aW5fMTQ6CnByb3RvIDAgMAp0eG4gU2VuZGVyCmJ5dGVjXzEgLy8gImxhc3QiCnB1c2hieXRlcyAweDRmNzA3NDIwNDk2ZTIwNDE0MjQ5IC8vICJPcHQgSW4gQUJJIgphcHBfbG9jYWxfcHV0CmludGNfMCAvLyAxCnJldHVybgoKLy8gb3B0X2luX2JhcmUKb3B0aW5iYXJlXzE1Ogpwcm90byAwIDAKdHhuIFNlbmRlcgpieXRlY18xIC8vICJsYXN0IgpwdXNoYnl0ZXMgMHg0ZjcwNzQyMDQ5NmUyMDQyNjE3MjY1IC8vICJPcHQgSW4gQmFyZSIKYXBwX2xvY2FsX3B1dAppbnRjXzAgLy8gMQpyZXR1cm4KCi8vIG9wdF9pbl9hcmdzCm9wdGluYXJnc18xNjoKcHJvdG8gMSAwCmZyYW1lX2RpZyAtMQpleHRyYWN0IDIgMApieXRlY18yIC8vICJZZXMiCj09Ci8vIHBhc3NlcyBvcHRfaW4gY2hlY2sKYXNzZXJ0CnR4biBTZW5kZXIKYnl0ZWNfMSAvLyAibGFzdCIKcHVzaGJ5dGVzIDB4NGY3MDc0MjA0OTZlMjA0MTcyNjc3MyAvLyAiT3B0IEluIEFyZ3MiCmFwcF9sb2NhbF9wdXQKaW50Y18wIC8vIDEKcmV0dXJuCgovLyBjbG9zZV9vdXQKY2xvc2VvdXRfMTc6CnByb3RvIDAgMAppbnRjXzAgLy8gMQpyZXR1cm4KCi8vIGNsb3NlX291dF9iYXJlCmNsb3Nlb3V0YmFyZV8xODoKcHJvdG8gMCAwCmludGNfMCAvLyAxCnJldHVybgoKLy8gY2xvc2Vfb3V0X2FyZ3MKY2xvc2VvdXRhcmdzXzE5Ogpwcm90byAxIDAKZnJhbWVfZGlnIC0xCmV4dHJhY3QgMiAwCmJ5dGVjXzIgLy8gIlllcyIKPT0KLy8gcGFzc2VzIGNsb3NlX291dCBjaGVjawphc3NlcnQKaW50Y18wIC8vIDEKcmV0dXJu", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDgKaW50Y2Jsb2NrIDEKY2FsbHN1YiBjbGVhcnN0YXRlXzAKaW50Y18wIC8vIDEKcmV0dXJuCgovLyBjbGVhcl9zdGF0ZQpjbGVhcnN0YXRlXzA6CnByb3RvIDAgMAppbnRjXzAgLy8gMQpyZXR1cm4=" }, "state": { "global": { - "num_byte_slices": 2, + "num_byte_slices": 1, "num_uints": 0 }, "local": { - "num_byte_slices": 0, + "num_byte_slices": 1, "num_uints": 0 } }, @@ -79,7 +99,12 @@ "type": "bytes", "key": "greeting", "descr": "" - }, + } + }, + "reserved": {} + }, + "local": { + "declared": { "last": { "type": "bytes", "key": "last", @@ -87,10 +112,6 @@ } }, "reserved": {} - }, - "local": { - "declared": {}, - "reserved": {} } }, "contract": { @@ -202,12 +223,52 @@ "returns": { "type": "string" } + }, + { + "name": "opt_in", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "opt_in_args", + "args": [ + { + "type": "string", + "name": "check" + } + ], + "returns": { + "type": "void" + } + }, + { + "name": "close_out", + "args": [], + "returns": { + "type": "void" + } + }, + { + "name": "close_out_args", + "args": [ + { + "type": "string", + "name": "check" + } + ], + "returns": { + "type": "void" + } } ], "networks": {} }, "bare_call_config": { "no_op": "CREATE", + "opt_in": "CALL", + "close_out": "CALL", "update_application": "CALL", "delete_application": "CALL" } diff --git a/tests/conftest.py b/tests/conftest.py index fe8a9be5..921cee0e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -102,6 +102,14 @@ def get_unique_name() -> str: return name +def is_opted_in(client_fixture: ApplicationClient) -> bool: + assert client_fixture.sender + account_info = client_fixture.algod_client.account_info(client_fixture.sender) + assert isinstance(account_info, dict) + apps_local_state = account_info["apps-local-state"] + return any(x for x in apps_local_state if x["id"] == client_fixture.app_id) + + @pytest.fixture(scope="session") def algod_client() -> AlgodClient: return get_algod_client() diff --git a/tests/test_app_client_clear_state.py b/tests/test_app_client_clear_state.py new file mode 100644 index 00000000..1260740b --- /dev/null +++ b/tests/test_app_client_clear_state.py @@ -0,0 +1,59 @@ +import base64 + +import pytest +from algokit_utils import ( + ApplicationClient, + ApplicationSpecification, + get_account, +) +from algosdk.v2client.algod import AlgodClient +from algosdk.v2client.indexer import IndexerClient + +from tests.conftest import get_unique_name, is_opted_in + + +@pytest.fixture +def client_fixture( + algod_client: AlgodClient, indexer_client: IndexerClient, app_spec: ApplicationSpecification +) -> ApplicationClient: + creator_name = get_unique_name() + creator = get_account(algod_client, creator_name) + client = ApplicationClient(algod_client, app_spec, creator=creator, indexer_client=indexer_client) + create_response = client.create("create") + assert create_response.tx_id + opt_in_response = client.opt_in("opt_in") + assert opt_in_response.tx_id + return client + + +def test_clear_state(client_fixture: ApplicationClient) -> None: + assert is_opted_in(client_fixture) + + close_out_response = client_fixture.clear_state() + assert close_out_response.tx_id + + assert not is_opted_in(client_fixture) + + +def test_clear_state_app_already_deleted(client_fixture: ApplicationClient) -> None: + assert is_opted_in(client_fixture) + + client_fixture.delete("delete") + assert is_opted_in(client_fixture) + + close_out_response = client_fixture.clear_state() + assert close_out_response.tx_id + + assert not is_opted_in(client_fixture) + + +def test_clear_state_app_args(client_fixture: ApplicationClient) -> None: + assert is_opted_in(client_fixture) + app_args = [b"test", b"data"] + + close_out_response = client_fixture.clear_state(app_args=app_args) + assert close_out_response.tx_id + + tx_info = client_fixture.algod_client.pending_transaction_info(close_out_response.tx_id) + assert isinstance(tx_info, dict) + assert [base64.b64decode(x) for x in tx_info["txn"]["txn"]["apaa"]] == app_args diff --git a/tests/test_app_client_close_out.approvals/test_abi_close_out_args_fails.approved.txt b/tests/test_app_client_close_out.approvals/test_abi_close_out_args_fails.approved.txt new file mode 100644 index 00000000..a71c629b --- /dev/null +++ b/tests/test_app_client_close_out.approvals/test_abi_close_out_args_fails.approved.txt @@ -0,0 +1,10 @@ +Txn {txn} had error 'assert failed pc=1004' at PC 1004 and Source Line 599: + + frame_dig -1 + extract 2 0 + bytec_2 // "Yes" + == + // passes close_out check + assert <-- Error + intc_0 // 1 + return \ No newline at end of file diff --git a/tests/test_app_client_close_out.py b/tests/test_app_client_close_out.py new file mode 100644 index 00000000..dfb2acac --- /dev/null +++ b/tests/test_app_client_close_out.py @@ -0,0 +1,63 @@ +import pytest +from algokit_utils import ( + ApplicationClient, + ApplicationSpecification, + LogicError, + get_account, +) +from algosdk.v2client.algod import AlgodClient +from algosdk.v2client.indexer import IndexerClient + +from tests.conftest import check_output_stability, get_unique_name, is_opted_in + + +@pytest.fixture +def client_fixture( + algod_client: AlgodClient, indexer_client: IndexerClient, app_spec: ApplicationSpecification +) -> ApplicationClient: + creator_name = get_unique_name() + creator = get_account(algod_client, creator_name) + client = ApplicationClient(algod_client, app_spec, creator=creator, indexer_client=indexer_client) + create_response = client.create("create") + assert create_response.tx_id + opt_in_response = client.opt_in("opt_in") + assert opt_in_response.tx_id + return client + + +def test_abi_close_out(client_fixture: ApplicationClient) -> None: + assert is_opted_in(client_fixture) + + close_out_response = client_fixture.close_out("close_out") + assert close_out_response.tx_id + + assert not is_opted_in(client_fixture) + + +def test_bare_close_out(client_fixture: ApplicationClient) -> None: + assert is_opted_in(client_fixture) + + close_out_response = client_fixture.close_out(call_abi_method=False) + assert close_out_response.tx_id + + assert not is_opted_in(client_fixture) + + +def test_abi_close_out_args(client_fixture: ApplicationClient) -> None: + assert is_opted_in(client_fixture) + + close_out_response = client_fixture.close_out("close_out_args", check="Yes") + assert close_out_response.tx_id + + assert not is_opted_in(client_fixture) + + +def test_abi_close_out_args_fails(client_fixture: ApplicationClient) -> None: + assert is_opted_in(client_fixture) + + with pytest.raises(LogicError) as ex: + client_fixture.close_out("close_out_args", check="No") + + check_output_stability(str(ex.value).replace(ex.value.transaction_id, "{txn}")) + + assert is_opted_in(client_fixture) diff --git a/tests/test_app_client_delete.approvals/test_abi_delete_args_fails.approved.txt b/tests/test_app_client_delete.approvals/test_abi_delete_args_fails.approved.txt index e80c73fa..c25585ea 100644 --- a/tests/test_app_client_delete.approvals/test_abi_delete_args_fails.approved.txt +++ b/tests/test_app_client_delete.approvals/test_abi_delete_args_fails.approved.txt @@ -1,8 +1,8 @@ -Txn {txn} had error 'assert failed pc=581' at PC 581 and Source Line 337: +Txn {txn} had error 'assert failed pc=736' at PC 736 and Source Line 427: frame_dig -1 extract 2 0 - bytec_3 // "Yes" + bytec_2 // "Yes" == // passes delete check assert <-- Error diff --git a/tests/test_app_client_opt_in.approvals/test_abi_update_args_fails.approved.txt b/tests/test_app_client_opt_in.approvals/test_abi_update_args_fails.approved.txt new file mode 100644 index 00000000..5a797ca1 --- /dev/null +++ b/tests/test_app_client_opt_in.approvals/test_abi_update_args_fails.approved.txt @@ -0,0 +1,12 @@ +Txn {txn} had error 'assert failed pc=964' at PC 964 and Source Line 571: + + frame_dig -1 + extract 2 0 + bytec_2 // "Yes" + == + // passes opt_in check + assert <-- Error + txn Sender + bytec_1 // "last" + pushbytes 0x4f707420496e2041726773 // "Opt In Args" + app_local_put \ No newline at end of file diff --git a/tests/test_app_client_opt_in.py b/tests/test_app_client_opt_in.py new file mode 100644 index 00000000..5107f5d8 --- /dev/null +++ b/tests/test_app_client_opt_in.py @@ -0,0 +1,54 @@ +import pytest +from algokit_utils import ( + ApplicationClient, + ApplicationSpecification, + LogicError, + get_account, +) +from algosdk.v2client.algod import AlgodClient +from algosdk.v2client.indexer import IndexerClient + +from tests.conftest import check_output_stability, get_unique_name, is_opted_in + + +@pytest.fixture +def client_fixture( + algod_client: AlgodClient, indexer_client: IndexerClient, app_spec: ApplicationSpecification +) -> ApplicationClient: + creator_name = get_unique_name() + creator = get_account(algod_client, creator_name) + client = ApplicationClient(algod_client, app_spec, creator=creator, indexer_client=indexer_client) + client.create("create") + return client + + +def test_abi_opt_in(client_fixture: ApplicationClient) -> None: + opt_in_response = client_fixture.opt_in("opt_in") + + assert opt_in_response.tx_id + assert client_fixture.call("get_last").return_value == "Opt In ABI" + assert is_opted_in(client_fixture) + + +def test_bare_opt_in(client_fixture: ApplicationClient) -> None: + opt_in_response = client_fixture.opt_in(call_abi_method=False) + + assert opt_in_response.tx_id + assert client_fixture.call("get_last").return_value == "Opt In Bare" + assert is_opted_in(client_fixture) + + +def test_abi_opt_in_args(client_fixture: ApplicationClient) -> None: + update_response = client_fixture.opt_in("opt_in_args", check="Yes") + + assert update_response.tx_id + assert client_fixture.call("get_last").return_value == "Opt In Args" + assert is_opted_in(client_fixture) + + +def test_abi_update_args_fails(client_fixture: ApplicationClient) -> None: + with pytest.raises(LogicError) as ex: + client_fixture.opt_in("opt_in_args", check="No") + + check_output_stability(str(ex.value).replace(ex.value.transaction_id, "{txn}")) + assert not is_opted_in(client_fixture) diff --git a/tests/test_app_client_update.approvals/test_abi_update_args_fails.approved.txt b/tests/test_app_client_update.approvals/test_abi_update_args_fails.approved.txt index 71ce0510..db1e1086 100644 --- a/tests/test_app_client_update.approvals/test_abi_update_args_fails.approved.txt +++ b/tests/test_app_client_update.approvals/test_abi_update_args_fails.approved.txt @@ -1,8 +1,8 @@ -Txn {txn} had error 'assert failed pc=518' at PC 518 and Source Line 289: +Txn {txn} had error 'assert failed pc=673' at PC 673 and Source Line 379: frame_dig -1 extract 2 0 - bytec_3 // "Yes" + bytec_2 // "Yes" == // passes update check assert <-- Error diff --git a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_deletable_app_succeeds.approved.txt b/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_deletable_app_succeeds.approved.txt deleted file mode 100644 index c3bd7846..00000000 --- a/tests/test_deploy_scenarios.approvals/test_deploy_app_with_existing_deletable_app_succeeds.approved.txt +++ /dev/null @@ -1,8 +0,0 @@ -INFO: SampleApp not found in {creator_account} account, deploying app. -INFO: SampleApp (1.0) deployed successfully, with app id {app0}. -DEBUG: SampleApp found in {creator_account} account, with app id {app0}, version=1.0. -INFO: Detected a TEAL update in app id {app0} -WARNING: App is not updatable and on_update=ReplaceApp, will attempt to create new app and delete old app -INFO: Replacing SampleApp (1.0) with SampleApp (2.0) in {creator_account} account. -INFO: SampleApp (2.0) deployed successfully, with app id {app1}. -INFO: SampleApp (1.0) with app id {app0}, deleted successfully. \ No newline at end of file diff --git a/tests/test_deploy_scenarios.py b/tests/test_deploy_scenarios.py index 1cb97d2e..93cead47 100644 --- a/tests/test_deploy_scenarios.py +++ b/tests/test_deploy_scenarios.py @@ -134,19 +134,6 @@ def test_deploy_app_with_existing_immutable_app_and_on_update_equals_replace_app deploy_fixture.check_log_stability() -def test_deploy_app_with_existing_deletable_app_succeeds(deploy_fixture: DeployFixture) -> None: - v1, v2, _ = get_specs() - - app_v1 = deploy_fixture.deploy(v1, version="1.0", allow_update=False, allow_delete=True) - assert app_v1.app_id - - app_v2 = deploy_fixture.deploy( - v2, version="2.0", allow_update=False, allow_delete=True, on_update=OnUpdate.ReplaceApp - ) - assert app_v1.app_id != app_v2.app_id - deploy_fixture.check_log_stability() - - def test_deploy_app_with_existing_permanent_app_fails(deploy_fixture: DeployFixture) -> None: v1, _, v3 = get_specs() From 0cea97d9c4179e953d489c995bdf8e083833e9ea Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Tue, 28 Mar 2023 09:08:57 +0800 Subject: [PATCH 22/24] feat: add overloads for application client methods --- src/algokit_utils/application_client.py | 146 +++++++++++++++++++++++- 1 file changed, 142 insertions(+), 4 deletions(-) diff --git a/src/algokit_utils/application_client.py b/src/algokit_utils/application_client.py index 368877ef..8efc6db0 100644 --- a/src/algokit_utils/application_client.py +++ b/src/algokit_utils/application_client.py @@ -395,7 +395,7 @@ def _deploy( def create_app() -> au_deploy.DeployResponse: assert self.existing_deployments - # TODO: extra pages + method, abi_args, parameters = _convert_deploy_args(create_args, app_spec_note, signer, sender) create_response = self.create( method, @@ -530,6 +530,32 @@ def compose_create( extra_pages=extra_pages, ) + @overload + def create( + self, + call_abi_method: Literal[False], + transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = ..., + ) -> TransactionResponse: + ... + + @overload + def create( + self, + call_abi_method: Method | str | Literal[True], + transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> ABITransactionResponse: + ... + + @overload + def create( + self, + call_abi_method: Method | str | bool | None = ..., + transaction_parameters: CreateCallParameters | CreateCallParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: + ... + def create( self, call_abi_method: Method | str | bool | None = None, @@ -571,6 +597,32 @@ def compose_update( clear_program=clear_program.raw_binary, ) + @overload + def update( + self, + call_abi_method: Method | str | Literal[True], + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> ABITransactionResponse: + ... + + @overload + def update( + self, + call_abi_method: Literal[False], + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = ..., + ) -> TransactionResponse: + ... + + @overload + def update( + self, + call_abi_method: Method | str | bool | None = ..., + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: + ... + def update( self, call_abi_method: Method | str | bool | None = None, @@ -606,6 +658,32 @@ def compose_delete( on_complete=transaction.OnComplete.DeleteApplicationOC, ) + @overload + def delete( + self, + call_abi_method: Method | str | Literal[True], + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> ABITransactionResponse: + ... + + @overload + def delete( + self, + call_abi_method: Literal[False], + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = ..., + ) -> TransactionResponse: + ... + + @overload + def delete( + self, + call_abi_method: Method | str | bool | None = ..., + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: + ... + def delete( self, call_abi_method: Method | str | bool | None = None, @@ -645,7 +723,7 @@ def compose_call( def call( self, call_abi_method: Method | str | Literal[True], - transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, + transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = ..., **abi_kwargs: ABIArgType, ) -> ABITransactionResponse: ... @@ -654,11 +732,19 @@ def call( def call( self, call_abi_method: Literal[False], - transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = None, - **abi_kwargs: ABIArgType, + transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = ..., ) -> TransactionResponse: ... + @overload + def call( + self, + call_abi_method: Method | str | bool | None = ..., + transaction_parameters: OnCompleteCallParameters | OnCompleteCallParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: + ... + def call( self, call_abi_method: Method | str | bool | None = None, @@ -703,6 +789,32 @@ def compose_opt_in( on_complete=transaction.OnComplete.OptInOC, ) + @overload + def opt_in( + self, + call_abi_method: Method | str | Literal[True] = ..., + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = None, + **abi_kwargs: ABIArgType, + ) -> ABITransactionResponse: + ... + + @overload + def opt_in( + self, + call_abi_method: Literal[False] = ..., + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = None, + ) -> TransactionResponse: + ... + + @overload + def opt_in( + self, + call_abi_method: Method | str | bool | None = ..., + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: + ... + def opt_in( self, call_abi_method: Method | str | bool | None = None, @@ -736,6 +848,32 @@ def compose_close_out( on_complete=transaction.OnComplete.CloseOutOC, ) + @overload + def close_out( + self, + call_abi_method: Method | str | Literal[True], + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> ABITransactionResponse: + ... + + @overload + def close_out( + self, + call_abi_method: Literal[False], + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = ..., + ) -> TransactionResponse: + ... + + @overload + def close_out( + self, + call_abi_method: Method | str | bool | None = ..., + transaction_parameters: CommonCallParameters | CommonCallParametersDict | None = ..., + **abi_kwargs: ABIArgType, + ) -> TransactionResponse | ABITransactionResponse: + ... + def close_out( self, call_abi_method: Method | str | bool | None = None, From fb40d7af7861b5c7c2f740b6e3fb5522e0962b71 Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Tue, 28 Mar 2023 10:56:01 +0800 Subject: [PATCH 23/24] feat: finish transfer implementation --- src/algokit_utils/__init__.py | 2 + src/algokit_utils/_transfer.py | 72 ++++++++++++++----- src/algokit_utils/account.py | 46 +++++++----- tests/conftest.py | 14 ++++ tests/test_app_client_deploy.py | 2 +- .../test_transfer_max_fee_fails.approved.txt | 1 + tests/test_transfer.py | 67 +++++++++++++---- 7 files changed, 153 insertions(+), 51 deletions(-) create mode 100644 tests/test_transfer.approvals/test_transfer_max_fee_fails.approved.txt diff --git a/src/algokit_utils/__init__.py b/src/algokit_utils/__init__.py index 2d5dbd8f..3e440ea9 100644 --- a/src/algokit_utils/__init__.py +++ b/src/algokit_utils/__init__.py @@ -3,6 +3,7 @@ transfer, ) from algokit_utils.account import ( + create_kmd_wallet_account, get_account, get_account_from_mnemonic, get_dispenser_account, @@ -65,6 +66,7 @@ ) __all__ = [ + "create_kmd_wallet_account", "get_account_from_mnemonic", "get_or_create_kmd_wallet_account", "get_sandbox_default_account", diff --git a/src/algokit_utils/_transfer.py b/src/algokit_utils/_transfer.py index 6ca1680d..ab65d7b6 100644 --- a/src/algokit_utils/_transfer.py +++ b/src/algokit_utils/_transfer.py @@ -1,7 +1,10 @@ import dataclasses import logging -from algosdk.transaction import PaymentTxn +import algosdk.transaction +from algosdk.account import address_from_private_key +from algosdk.atomic_transaction_composer import AccountTransactionSigner +from algosdk.transaction import PaymentTxn, SuggestedParams from algosdk.v2client.algod import AlgodClient from algokit_utils.models import Account @@ -12,30 +15,63 @@ @dataclasses.dataclass(kw_only=True) class TransferParameters: - from_account: Account + from_account: Account | AccountTransactionSigner + """The account (with private key) or signer that will send the µALGOs""" to_address: str - amount: int - note: str | None = None - max_fee_in_algos: float | None = None + """The account address that will receive the µALGOs""" + micro_algos: int + """The amount of µALGOs to send""" + suggested_params: SuggestedParams | None = None + """(optional) transaction parameters""" + note: str | bytes | None = None + """(optional) transaction note""" + fee_micro_algos: int | None = None + """(optional) The flat fee you want to pay, useful for covering extra fees in a transaction group or app call""" + max_fee_micro_algos: int | None = None + """(optional)The maximum fee that you are happy to pay (default: unbounded) - + if this is set it's possible the transaction could get rejected during network congestion""" -def transfer(transfer_parameters: TransferParameters, client: AlgodClient) -> tuple[PaymentTxn, str]: - suggested_params = client.suggested_params() +def _check_fee(transaction: PaymentTxn, max_fee: int | None) -> None: + if max_fee is not None: + # Once a transaction has been constructed by algosdk, transaction.fee indicates what the total transaction fee + # Will be based on the current suggested fee-per-byte value. + if transaction.fee > max_fee: + raise Exception( + f"Cancelled transaction due to high network congestion fees. " + f"Algorand suggested fees would cause this transaction to cost {transaction.fee} µALGOs. " + f"Cap for this transaction is {max_fee} µALGOs." + ) + elif transaction.fee > algosdk.constants.MIN_TXN_FEE: + logger.warning( + f"Algorand network congestion fees are in effect. " + f"This transaction will incur a fee of {transaction.fee} µALGOs." + ) + + +def transfer(client: AlgodClient, parameters: TransferParameters) -> PaymentTxn: + suggested_params = parameters.suggested_params or client.suggested_params() + from_account = parameters.from_account + sender = address_from_private_key(from_account.private_key) # type: ignore[no-untyped-call] transaction = PaymentTxn( - sender=transfer_parameters.from_account.address, + sender=sender, + receiver=parameters.to_address, + amt=parameters.micro_algos, + note=parameters.note.encode("utf-8") if isinstance(parameters.note, str) else parameters.note, sp=suggested_params, - receiver=transfer_parameters.to_address, - amt=transfer_parameters.amount, - close_remainder_to=None, - note=transfer_parameters.note.encode("utf-8") if transfer_parameters.note else None, - rekey_to=None, ) # type: ignore[no-untyped-call] - # TODO: max fee - from_account = transfer_parameters.from_account + if parameters.fee_micro_algos: + transaction.fee = parameters.fee_micro_algos + + if not suggested_params.flat_fee: + _check_fee(transaction, parameters.max_fee_micro_algos) signed_transaction = transaction.sign(from_account.private_key) # type: ignore[no-untyped-call] - send_response = client.send_transaction(signed_transaction) + client.send_transaction(signed_transaction) txid = transaction.get_txid() # type: ignore[no-untyped-call] - logger.debug(f"Sent transaction {txid} type={transaction.type} from {from_account.address}") + logger.debug( + f"Sent transaction {txid} type={transaction.type} from " + f"{address_from_private_key(from_account.private_key)}" # type: ignore[no-untyped-call] + ) - return transaction, send_response + return transaction diff --git a/src/algokit_utils/account.py b/src/algokit_utils/account.py index 9508449c..5a03b091 100644 --- a/src/algokit_utils/account.py +++ b/src/algokit_utils/account.py @@ -31,11 +31,22 @@ def get_account_from_mnemonic(mnemonic: str) -> Account: return Account(private_key, address) +def create_kmd_wallet_account(kmd_client: KMDClient, name: str) -> Account: + wallet_id = kmd_client.create_wallet(name, "")["id"] # type: ignore[no-untyped-call] + wallet_handle = kmd_client.init_wallet_handle(wallet_id, "") # type: ignore[no-untyped-call] + kmd_client.generate_key(wallet_handle) # type: ignore[no-untyped-call] + + key_ids: list[str] = kmd_client.list_keys(wallet_handle) # type: ignore[no-untyped-call] + account_key = key_ids[0] + + private_account_key = kmd_client.export_key(wallet_handle, "", account_key) # type: ignore[no-untyped-call] + return get_account_from_mnemonic(from_private_key(private_account_key)) # type: ignore[no-untyped-call] + + def get_or_create_kmd_wallet_account( - client: AlgodClient, name: str, fund_with: int | None, kmd_client: KMDClient | None = None + client: AlgodClient, name: str, fund_with_algos: float = 1000, kmd_client: KMDClient | None = None ) -> Account: kmd_client = kmd_client or get_kmd_client_from_algod_client(client) - fund_with = 1000 if fund_with is None else fund_with account = get_kmd_wallet_account(client, kmd_client, name) if account: @@ -43,29 +54,26 @@ def get_or_create_kmd_wallet_account( assert isinstance(account_info, dict) if account_info["amount"] > 0: return account - logger.debug(f"Found existing account in Sandbox with name '{name}'." f"But no funds in the account.") + logger.debug(f"Found existing account in Sandbox with name '{name}', but no funds in the account.") else: - wallet_id = kmd_client.create_wallet(name, "")["id"] # type: ignore[no-untyped-call] - wallet_handle = kmd_client.init_wallet_handle(wallet_id, "") # type: ignore[no-untyped-call] - kmd_client.generate_key(wallet_handle) # type: ignore[no-untyped-call] + account = create_kmd_wallet_account(kmd_client, name) - account = get_kmd_wallet_account(client, kmd_client, name) - assert account logger.debug( f"Couldn't find existing account in Sandbox with name '{name}'. " f"So created account {account.address} with keys stored in KMD." ) - logger.debug(f"Funding account {account.address} with {fund_with} ALGOs") + logger.debug(f"Funding account {account.address} with {fund_with_algos} ALGOs") - transfer( - TransferParameters( - from_account=get_dispenser_account(client), - to_address=account.address, - amount=algos_to_microalgos(fund_with), # type: ignore[no-untyped-call] - ), - client, - ) + if fund_with_algos: + transfer( + client, + TransferParameters( + from_account=get_dispenser_account(client), + to_address=account.address, + micro_algos=algos_to_microalgos(fund_with_algos), # type: ignore[no-untyped-call] + ), + ) return account @@ -121,7 +129,7 @@ def get_kmd_wallet_account( def get_account( - client: AlgodClient, name: str, fund_with: int | None = None, kmd_client: KMDClient | None = None + client: AlgodClient, name: str, fund_with_algos: float = 1000, kmd_client: KMDClient | None = None ) -> Account: mnemonic_key = f"{name.upper()}_MNEMONIC" mnemonic = os.getenv(mnemonic_key) @@ -129,7 +137,7 @@ def get_account( return get_account_from_mnemonic(mnemonic) if is_sandbox(client): - account = get_or_create_kmd_wallet_account(client, name, fund_with, kmd_client) + account = get_or_create_kmd_wallet_account(client, name, fund_with_algos, kmd_client) os.environ[mnemonic_key] = from_private_key(account.private_key) # type: ignore[no-untyped-call] return account diff --git a/tests/conftest.py b/tests/conftest.py index 921cee0e..5fbb75b2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,8 +13,10 @@ get_account, get_algod_client, get_indexer_client, + get_kmd_client_from_algod_client, replace_template_variables, ) +from algosdk.kmd import KMDClient from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient from dotenv import load_dotenv @@ -115,6 +117,11 @@ def algod_client() -> AlgodClient: return get_algod_client() +@pytest.fixture(scope="session") +def kmd_client(algod_client: AlgodClient) -> KMDClient: + return get_kmd_client_from_algod_client(algod_client) + + @pytest.fixture(scope="session") def indexer_client() -> IndexerClient: return get_indexer_client() @@ -127,6 +134,13 @@ def creator(algod_client: AlgodClient) -> Account: return creator +@pytest.fixture(scope="session") +def funded_account(algod_client: AlgodClient) -> Account: + creator_name = get_unique_name() + creator = get_account(algod_client, creator_name) + return creator + + @pytest.fixture(scope="session") def app_spec() -> ApplicationSpecification: app_spec = read_spec("app_client_test.json", deletable=True, updatable=True) diff --git a/tests/test_app_client_deploy.py b/tests/test_app_client_deploy.py index ca9f05f0..50ff884b 100644 --- a/tests/test_app_client_deploy.py +++ b/tests/test_app_client_deploy.py @@ -17,8 +17,8 @@ def test_deploy_with_create(client_fixture: ApplicationClient, creator: Account) ) transfer( - TransferParameters(from_account=creator, to_address=client_fixture.app_address, amount=100_000), client_fixture.algod_client, + TransferParameters(from_account=creator, to_address=client_fixture.app_address, micro_algos=100_000), ) assert client_fixture.call("hello", name="test").return_value == "Hello ABI, test" diff --git a/tests/test_transfer.approvals/test_transfer_max_fee_fails.approved.txt b/tests/test_transfer.approvals/test_transfer_max_fee_fails.approved.txt new file mode 100644 index 00000000..ebb60988 --- /dev/null +++ b/tests/test_transfer.approvals/test_transfer_max_fee_fails.approved.txt @@ -0,0 +1 @@ +Cancelled transaction due to high network congestion fees. Algorand suggested fees would cause this transaction to cost 1000 µALGOs. Cap for this transaction is 123 µALGOs. \ No newline at end of file diff --git a/tests/test_transfer.py b/tests/test_transfer.py index c0dbfa9c..4a977b89 100644 --- a/tests/test_transfer.py +++ b/tests/test_transfer.py @@ -1,21 +1,62 @@ -from algokit_utils import ABICreateCallArgs, Account, ApplicationClient, TransferParameters, transfer +import pytest +from algokit_utils import Account, TransferParameters, create_kmd_wallet_account, transfer +from algosdk.kmd import KMDClient +from algosdk.v2client.algod import AlgodClient +from tests.conftest import check_output_stability, get_unique_name + + +@pytest.fixture(scope="session") +def to_account(kmd_client: KMDClient) -> Account: + return create_kmd_wallet_account(kmd_client, get_unique_name()) -def test_transfer(client_fixture: ApplicationClient, creator: Account) -> None: - client_fixture.deploy( - "v1", - create_args=ABICreateCallArgs( - method="create", - ), - ) +def test_transfer(algod_client: AlgodClient, to_account: Account, funded_account: Account) -> None: requested_amount = 100_000 transfer( - TransferParameters(from_account=creator, to_address=client_fixture.app_address, amount=requested_amount), - client_fixture.algod_client, + algod_client, + TransferParameters( + from_account=funded_account, + to_address=to_account.address, + micro_algos=requested_amount, + ), ) - account_info = client_fixture.algod_client.account_info(client_fixture.app_address) - assert isinstance(account_info, dict) - actual_amount = account_info.get("amount") + to_account_info = algod_client.account_info(to_account.address) + assert isinstance(to_account_info, dict) + actual_amount = to_account_info.get("amount") assert actual_amount == requested_amount + + +def test_transfer_max_fee_fails(algod_client: AlgodClient, to_account: Account, funded_account: Account) -> None: + requested_amount = 100_000 + max_fee = 123 + + with pytest.raises(Exception) as ex: + transfer( + algod_client, + TransferParameters( + from_account=funded_account, + to_address=to_account.address, + micro_algos=requested_amount, + max_fee_micro_algos=max_fee, + ), + ) + + check_output_stability(str(ex.value)) + + +def test_transfer_fee(algod_client: AlgodClient, to_account: Account, funded_account: Account) -> None: + requested_amount = 100_000 + fee = 1234 + txn = transfer( + algod_client, + TransferParameters( + from_account=funded_account, + to_address=to_account.address, + micro_algos=requested_amount, + fee_micro_algos=fee, + ), + ) + + assert txn.fee == fee From b77334d721b351663887891bc01bf7418563e097 Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Tue, 28 Mar 2023 11:35:58 +0800 Subject: [PATCH 24/24] test: improve test performance by reducing number of accounts created --- tests/conftest.py | 26 +++---- tests/test_app_client_call.py | 18 ++++- tests/test_app_client_clear_state.py | 13 ++-- tests/test_app_client_close_out.py | 13 ++-- tests/test_app_client_create.py | 10 +-- tests/test_app_client_delete.py | 17 ++--- tests/test_app_client_deploy.py | 16 +++++ tests/test_app_client_opt_in.py | 13 ++-- tests/test_app_client_update.py | 13 ++-- tests/test_deploy_scenarios.py | 103 +++++++++++++++++++-------- tests/test_transfer.py | 2 +- 11 files changed, 158 insertions(+), 86 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5fbb75b2..14b29e62 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -67,7 +67,11 @@ def check_output_stability(logs: str, *, test_name: str | None = None) -> None: def read_spec( - file_name: str, *, updatable: bool | None = None, deletable: bool | None = None + file_name: str, + *, + updatable: bool | None = None, + deletable: bool | None = None, + name: str | None = None, ) -> ApplicationSpecification: path = Path(__file__).parent / file_name spec = ApplicationSpecification.from_json(Path(path).read_text(encoding="utf-8")) @@ -84,16 +88,20 @@ def read_spec( .replace(f"// {UPDATABLE_TEMPLATE_NAME}", "// updatable") .replace(f"// {DELETABLE_TEMPLATE_NAME}", "// deletable") ) + if name is not None: + spec.contract.name = name return spec def get_specs( - updatable: bool | None = None, deletable: bool | None = None + updatable: bool | None = None, + deletable: bool | None = None, + name: str | None = None, ) -> tuple[ApplicationSpecification, ApplicationSpecification, ApplicationSpecification]: specs = ( - read_spec("app_v1.json", updatable=updatable, deletable=deletable), - read_spec("app_v2.json", updatable=updatable, deletable=deletable), - read_spec("app_v3.json", updatable=updatable, deletable=deletable), + read_spec("app_v1.json", updatable=updatable, deletable=deletable, name=name), + read_spec("app_v2.json", updatable=updatable, deletable=deletable, name=name), + read_spec("app_v3.json", updatable=updatable, deletable=deletable, name=name), ) return specs @@ -145,11 +153,3 @@ def funded_account(algod_client: AlgodClient) -> Account: def app_spec() -> ApplicationSpecification: app_spec = read_spec("app_client_test.json", deletable=True, updatable=True) return app_spec - - -@pytest.fixture() -def client_fixture( - algod_client: AlgodClient, indexer_client: IndexerClient, creator: Account, app_spec: ApplicationSpecification -) -> ApplicationClient: - client = ApplicationClient(algod_client, app_spec, creator=creator, indexer_client=indexer_client) - return client diff --git a/tests/test_app_client_call.py b/tests/test_app_client_call.py index f21608d2..b67b269a 100644 --- a/tests/test_app_client_call.py +++ b/tests/test_app_client_call.py @@ -1,13 +1,28 @@ +import pytest from algokit_utils import ( ApplicationClient, + ApplicationSpecification, CreateCallParameters, + get_account, ) from algosdk.atomic_transaction_composer import AtomicTransactionComposer from algosdk.transaction import ApplicationCallTxn +from algosdk.v2client.algod import AlgodClient + +from tests.conftest import get_unique_name + + +@pytest.fixture(scope="module") +def client_fixture(algod_client: AlgodClient, app_spec: ApplicationSpecification) -> ApplicationClient: + creator_name = get_unique_name() + creator = get_account(algod_client, creator_name) + client = ApplicationClient(algod_client, app_spec, signer=creator) + create_response = client.create("create") + assert create_response.tx_id + return client def test_abi_call_with_atc(client_fixture: ApplicationClient) -> None: - client_fixture.create("create") atc = AtomicTransactionComposer() client_fixture.compose_call(atc, "hello", name="test") result = atc.execute(client_fixture.algod_client, 4) @@ -16,7 +31,6 @@ def test_abi_call_with_atc(client_fixture: ApplicationClient) -> None: def test_abi_call_multiple_times_with_atc(client_fixture: ApplicationClient) -> None: - client_fixture.create("create") atc = AtomicTransactionComposer() client_fixture.compose_call(atc, "hello", name="test") client_fixture.compose_call(atc, "hello", name="test2") diff --git a/tests/test_app_client_clear_state.py b/tests/test_app_client_clear_state.py index 1260740b..db5bd6ef 100644 --- a/tests/test_app_client_clear_state.py +++ b/tests/test_app_client_clear_state.py @@ -2,23 +2,24 @@ import pytest from algokit_utils import ( + Account, ApplicationClient, ApplicationSpecification, - get_account, ) from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient -from tests.conftest import get_unique_name, is_opted_in +from tests.conftest import is_opted_in @pytest.fixture def client_fixture( - algod_client: AlgodClient, indexer_client: IndexerClient, app_spec: ApplicationSpecification + algod_client: AlgodClient, + indexer_client: IndexerClient, + app_spec: ApplicationSpecification, + funded_account: Account, ) -> ApplicationClient: - creator_name = get_unique_name() - creator = get_account(algod_client, creator_name) - client = ApplicationClient(algod_client, app_spec, creator=creator, indexer_client=indexer_client) + client = ApplicationClient(algod_client, app_spec, creator=funded_account, indexer_client=indexer_client) create_response = client.create("create") assert create_response.tx_id opt_in_response = client.opt_in("opt_in") diff --git a/tests/test_app_client_close_out.py b/tests/test_app_client_close_out.py index dfb2acac..25fac27e 100644 --- a/tests/test_app_client_close_out.py +++ b/tests/test_app_client_close_out.py @@ -1,23 +1,24 @@ import pytest from algokit_utils import ( + Account, ApplicationClient, ApplicationSpecification, LogicError, - get_account, ) from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient -from tests.conftest import check_output_stability, get_unique_name, is_opted_in +from tests.conftest import check_output_stability, is_opted_in @pytest.fixture def client_fixture( - algod_client: AlgodClient, indexer_client: IndexerClient, app_spec: ApplicationSpecification + algod_client: AlgodClient, + indexer_client: IndexerClient, + app_spec: ApplicationSpecification, + funded_account: Account, ) -> ApplicationClient: - creator_name = get_unique_name() - creator = get_account(algod_client, creator_name) - client = ApplicationClient(algod_client, app_spec, creator=creator, indexer_client=indexer_client) + client = ApplicationClient(algod_client, app_spec, creator=funded_account, indexer_client=indexer_client) create_response = client.create("create") assert create_response.tx_id opt_in_response = client.opt_in("opt_in") diff --git a/tests/test_app_client_create.py b/tests/test_app_client_create.py index 05425aca..4263cce8 100644 --- a/tests/test_app_client_create.py +++ b/tests/test_app_client_create.py @@ -1,5 +1,6 @@ import pytest from algokit_utils import ( + Account, ApplicationClient, ApplicationSpecification, CreateCallParameters, @@ -16,11 +17,12 @@ @pytest.fixture(scope="module") def client_fixture( - algod_client: AlgodClient, indexer_client: IndexerClient, app_spec: ApplicationSpecification + algod_client: AlgodClient, + indexer_client: IndexerClient, + app_spec: ApplicationSpecification, + funded_account: Account, ) -> ApplicationClient: - creator_name = get_unique_name() - creator = get_account(algod_client, creator_name) - client = ApplicationClient(algod_client, app_spec, creator=creator, indexer_client=indexer_client) + client = ApplicationClient(algod_client, app_spec, creator=funded_account, indexer_client=indexer_client) return client diff --git a/tests/test_app_client_delete.py b/tests/test_app_client_delete.py index e38c5643..52b0b25c 100644 --- a/tests/test_app_client_delete.py +++ b/tests/test_app_client_delete.py @@ -4,26 +4,21 @@ ApplicationClient, ApplicationSpecification, LogicError, - get_account, ) from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient -from tests.conftest import check_output_stability, get_unique_name - - -@pytest.fixture(scope="module") -def creator(algod_client: AlgodClient) -> Account: - creator_name = get_unique_name() - creator = get_account(algod_client, creator_name) - return creator +from tests.conftest import check_output_stability @pytest.fixture def client_fixture( - algod_client: AlgodClient, indexer_client: IndexerClient, creator: Account, app_spec: ApplicationSpecification + algod_client: AlgodClient, + indexer_client: IndexerClient, + funded_account: Account, + app_spec: ApplicationSpecification, ) -> ApplicationClient: - client = ApplicationClient(algod_client, app_spec, creator=creator, indexer_client=indexer_client) + client = ApplicationClient(algod_client, app_spec, creator=funded_account, indexer_client=indexer_client) client.create("create") return client diff --git a/tests/test_app_client_deploy.py b/tests/test_app_client_deploy.py index 50ff884b..f6e3b715 100644 --- a/tests/test_app_client_deploy.py +++ b/tests/test_app_client_deploy.py @@ -1,3 +1,4 @@ +import pytest from algokit_utils import ( ABICreateCallArgs, Account, @@ -6,6 +7,21 @@ TransferParameters, transfer, ) +from algosdk.v2client.algod import AlgodClient +from algosdk.v2client.indexer import IndexerClient + +from tests.conftest import get_unique_name, read_spec + + +@pytest.fixture +def client_fixture( + algod_client: AlgodClient, + indexer_client: IndexerClient, + funded_account: Account, +) -> ApplicationClient: + app_spec = read_spec("app_client_test.json", deletable=True, updatable=True, name=get_unique_name()) + client = ApplicationClient(algod_client, app_spec, creator=funded_account, indexer_client=indexer_client) + return client def test_deploy_with_create(client_fixture: ApplicationClient, creator: Account) -> None: diff --git a/tests/test_app_client_opt_in.py b/tests/test_app_client_opt_in.py index 5107f5d8..a76b1671 100644 --- a/tests/test_app_client_opt_in.py +++ b/tests/test_app_client_opt_in.py @@ -1,23 +1,24 @@ import pytest from algokit_utils import ( + Account, ApplicationClient, ApplicationSpecification, LogicError, - get_account, ) from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient -from tests.conftest import check_output_stability, get_unique_name, is_opted_in +from tests.conftest import check_output_stability, is_opted_in @pytest.fixture def client_fixture( - algod_client: AlgodClient, indexer_client: IndexerClient, app_spec: ApplicationSpecification + algod_client: AlgodClient, + indexer_client: IndexerClient, + app_spec: ApplicationSpecification, + funded_account: Account, ) -> ApplicationClient: - creator_name = get_unique_name() - creator = get_account(algod_client, creator_name) - client = ApplicationClient(algod_client, app_spec, creator=creator, indexer_client=indexer_client) + client = ApplicationClient(algod_client, app_spec, creator=funded_account, indexer_client=indexer_client) client.create("create") return client diff --git a/tests/test_app_client_update.py b/tests/test_app_client_update.py index 14994400..9a210ebe 100644 --- a/tests/test_app_client_update.py +++ b/tests/test_app_client_update.py @@ -1,23 +1,24 @@ import pytest from algokit_utils import ( + Account, ApplicationClient, ApplicationSpecification, LogicError, - get_account, ) from algosdk.v2client.algod import AlgodClient from algosdk.v2client.indexer import IndexerClient -from tests.conftest import check_output_stability, get_unique_name +from tests.conftest import check_output_stability @pytest.fixture(scope="module") def client_fixture( - algod_client: AlgodClient, indexer_client: IndexerClient, app_spec: ApplicationSpecification + algod_client: AlgodClient, + indexer_client: IndexerClient, + app_spec: ApplicationSpecification, + funded_account: Account, ) -> ApplicationClient: - creator_name = get_unique_name() - creator = get_account(algod_client, creator_name) - client = ApplicationClient(algod_client, app_spec, creator=creator, indexer_client=indexer_client) + client = ApplicationClient(algod_client, app_spec, creator=funded_account, indexer_client=indexer_client) client.create("create") return client diff --git a/tests/test_deploy_scenarios.py b/tests/test_deploy_scenarios.py index 93cead47..7b9117e3 100644 --- a/tests/test_deploy_scenarios.py +++ b/tests/test_deploy_scenarios.py @@ -4,6 +4,7 @@ import pytest from algokit_utils import ( + Account, ApplicationClient, ApplicationSpecification, DeploymentFailedError, @@ -22,14 +23,22 @@ class DeployFixture: - def __init__(self, caplog: pytest.LogCaptureFixture, request: pytest.FixtureRequest): + def __init__( + self, + caplog: pytest.LogCaptureFixture, + request: pytest.FixtureRequest, + creator_name: str, + creator: Account, + app_name: str, + ): self.app_ids: list[int] = [] self.caplog = caplog self.request = request self.algod_client = get_algod_client() self.indexer_client = get_indexer_client() - self.creator_name = get_unique_name() - self.creator = get_account(self.algod_client, self.creator_name) + self.creator_name = creator_name + self.creator = creator + self.app_name = app_name def deploy( self, @@ -55,10 +64,15 @@ def deploy( self.app_ids.append(app_client.app_id) return app_client - def check_log_stability(self, suffix: str = "") -> None: + def check_log_stability(self, replacements: dict[str, str] | None = None, suffix: str = "") -> None: + if replacements is None: + replacements = {} + replacements[self.app_name] = "SampleApp" records = self.caplog.get_records("call") logs = "\n".join(f"{r.levelname}: {r.message}" for r in records) logs = self._normalize_logs(logs) + for find, replace in (replacements or {}).items(): + logs = logs.replace(find, replace) check_output_stability(logs, test_name=self.request.node.name + suffix) def _normalize_logs(self, logs: str) -> str: @@ -78,14 +92,31 @@ def _wait_for_indexer_round(self, round_target: int, max_attempts: int = 100) -> break -@pytest.fixture() -def deploy_fixture(caplog: pytest.LogCaptureFixture, request: pytest.FixtureRequest) -> DeployFixture: +@pytest.fixture(scope="module") +def creator_name() -> str: + return get_unique_name() + + +@pytest.fixture(scope="module") +def creator(creator_name: str) -> Account: + return get_account(get_algod_client(), creator_name) + + +@pytest.fixture +def app_name() -> str: + return get_unique_name() + + +@pytest.fixture +def deploy_fixture( + caplog: pytest.LogCaptureFixture, request: pytest.FixtureRequest, creator_name: str, creator: Account, app_name: str +) -> DeployFixture: caplog.set_level(logging.DEBUG) - return DeployFixture(caplog, request) + return DeployFixture(caplog, request, creator_name=creator_name, creator=creator, app_name=app_name) -def test_deploy_app_with_no_existing_app_succeeds(deploy_fixture: DeployFixture) -> None: - v1, _, _ = get_specs() +def test_deploy_app_with_no_existing_app_succeeds(deploy_fixture: DeployFixture, app_name: str) -> None: + v1, _, _ = get_specs(name=app_name) app = deploy_fixture.deploy(v1, version="1.0", allow_update=False, allow_delete=False) @@ -93,8 +124,8 @@ def test_deploy_app_with_no_existing_app_succeeds(deploy_fixture: DeployFixture) deploy_fixture.check_log_stability() -def test_deploy_app_with_existing_updatable_app_succeeds(deploy_fixture: DeployFixture) -> None: - v1, v2, _ = get_specs() +def test_deploy_app_with_existing_updatable_app_succeeds(deploy_fixture: DeployFixture, app_name: str) -> None: + v1, v2, _ = get_specs(name=app_name) app_v1 = deploy_fixture.deploy(v1, version="1.0", allow_update=True, allow_delete=False) assert app_v1.app_id @@ -105,8 +136,8 @@ def test_deploy_app_with_existing_updatable_app_succeeds(deploy_fixture: DeployF deploy_fixture.check_log_stability() -def test_deploy_app_with_existing_immutable_app_fails(deploy_fixture: DeployFixture) -> None: - v1, v2, _ = get_specs() +def test_deploy_app_with_existing_immutable_app_fails(deploy_fixture: DeployFixture, app_name: str) -> None: + v1, v2, _ = get_specs(name=app_name) app_v1 = deploy_fixture.deploy(v1, version="1.0", allow_update=False, allow_delete=False) assert app_v1.app_id @@ -120,8 +151,9 @@ def test_deploy_app_with_existing_immutable_app_fails(deploy_fixture: DeployFixt def test_deploy_app_with_existing_immutable_app_and_on_update_equals_replace_app_succeeds( deploy_fixture: DeployFixture, + app_name: str, ) -> None: - v1, v2, _ = get_specs() + v1, v2, _ = get_specs(name=app_name) app_v1 = deploy_fixture.deploy(v1, version="1.0", allow_update=False, allow_delete=True) assert app_v1.app_id @@ -134,8 +166,8 @@ def test_deploy_app_with_existing_immutable_app_and_on_update_equals_replace_app deploy_fixture.check_log_stability() -def test_deploy_app_with_existing_permanent_app_fails(deploy_fixture: DeployFixture) -> None: - v1, _, v3 = get_specs() +def test_deploy_app_with_existing_permanent_app_fails(deploy_fixture: DeployFixture, app_name: str) -> None: + v1, _, v3 = get_specs(name=app_name) app_v1 = deploy_fixture.deploy(v1, version="1.0", allow_update=False, allow_delete=False) assert app_v1.app_id @@ -146,8 +178,10 @@ def test_deploy_app_with_existing_permanent_app_fails(deploy_fixture: DeployFixt deploy_fixture.check_log_stability() -def test_deploy_app_with_existing_immutable_app_cannot_determine_if_updatable(deploy_fixture: DeployFixture) -> None: - v1, v2, _ = get_specs(updatable=False, deletable=False) +def test_deploy_app_with_existing_immutable_app_cannot_determine_if_updatable( + deploy_fixture: DeployFixture, app_name: str +) -> None: + v1, v2, _ = get_specs(updatable=False, deletable=False, name=app_name) app_v1 = deploy_fixture.deploy(v1) assert app_v1.app_id @@ -158,8 +192,10 @@ def test_deploy_app_with_existing_immutable_app_cannot_determine_if_updatable(de deploy_fixture.check_log_stability() -def test_deploy_app_with_existing_permanent_app_cannot_determine_if_deletable(deploy_fixture: DeployFixture) -> None: - v1, v2, _ = get_specs(updatable=False, deletable=False) +def test_deploy_app_with_existing_permanent_app_cannot_determine_if_deletable( + deploy_fixture: DeployFixture, app_name: str +) -> None: + v1, v2, _ = get_specs(updatable=False, deletable=False, name=app_name) app_v1 = deploy_fixture.deploy(v1) assert app_v1.app_id @@ -172,21 +208,25 @@ def test_deploy_app_with_existing_permanent_app_cannot_determine_if_deletable(de def test_deploy_app_with_existing_permanent_app_on_update_equals_replace_app_fails_and_doesnt_create_2nd_app( deploy_fixture: DeployFixture, + app_name: str, ) -> None: - v1, v2, _ = get_specs() + v1, v2, _ = get_specs(name=app_name) app_v1 = deploy_fixture.deploy(v1, version="1.0", allow_update=False, allow_delete=False) assert app_v1.app_id + apps_before = deploy_fixture.indexer_client.lookup_account_application_by_creator( + deploy_fixture.creator.address + ) # type: ignore[no-untyped-call] + with pytest.raises(LogicError) as error: deploy_fixture.deploy(v2, version="3.0", allow_update=False, allow_delete=False, on_update=OnUpdate.ReplaceApp) - lookup_response = deploy_fixture.indexer_client.lookup_account_application_by_creator( + apps_after = deploy_fixture.indexer_client.lookup_account_application_by_creator( deploy_fixture.creator.address ) # type: ignore[no-untyped-call] - all_apps = lookup_response["applications"] # ensure no other apps were created - assert len(all_apps) == 1 + assert len(apps_before["applications"]) == len(apps_after["applications"]) logger.error(f"DeploymentFailedError: {error.value.message}") deploy_fixture.check_log_stability() @@ -194,8 +234,9 @@ def test_deploy_app_with_existing_permanent_app_on_update_equals_replace_app_fai def test_deploy_app_with_existing_permanent_app_and_on_schema_break_equals_replace_app_fails( deploy_fixture: DeployFixture, + app_name: str, ) -> None: - v1, _, v3 = get_specs() + v1, _, v3 = get_specs(name=app_name) app_v1 = deploy_fixture.deploy(v1, allow_update=False, allow_delete=False, version="1.0") assert app_v1.app_id @@ -210,8 +251,8 @@ def test_deploy_app_with_existing_permanent_app_and_on_schema_break_equals_repla deploy_fixture.check_log_stability() -def test_deploy_templated_app_with_changing_parameters_succeeds(deploy_fixture: DeployFixture) -> None: - app_spec = read_spec("app_v1.json") +def test_deploy_templated_app_with_changing_parameters_succeeds(deploy_fixture: DeployFixture, app_name: str) -> None: + app_spec = read_spec("app_v1.json", name=app_name) logger.info("Deploy V1 as updatable, deletable") app_client = deploy_fixture.deploy( @@ -291,9 +332,9 @@ def test_deploy_with_schema_breaking_change( deletable: Deletable, updatable: Updatable, on_schema_break: OnSchemaBreak, + app_name: str, ) -> None: - v1 = read_spec("app_v1.json") - v3 = read_spec("app_v3.json") + v1, _, v3 = get_specs(name=app_name) app_v1 = deploy_fixture.deploy( v1, version="1.0", allow_delete=deletable == Deletable.Yes, allow_update=updatable == Updatable.Yes @@ -325,9 +366,9 @@ def test_deploy_with_update( deletable: Deletable, updatable: Updatable, on_update: OnUpdate, + app_name: str, ) -> None: - v1 = read_spec("app_v1.json") - v2 = read_spec("app_v2.json") + v1, v2, _ = get_specs(name=app_name) app_v1 = deploy_fixture.deploy( v1, version="1.0", allow_delete=deletable == Deletable.Yes, allow_update=updatable == Updatable.Yes diff --git a/tests/test_transfer.py b/tests/test_transfer.py index 4a977b89..e37c9029 100644 --- a/tests/test_transfer.py +++ b/tests/test_transfer.py @@ -6,7 +6,7 @@ from tests.conftest import check_output_stability, get_unique_name -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def to_account(kmd_client: KMDClient) -> Account: return create_kmd_wallet_account(kmd_client, get_unique_name())