From 85173583b2723763f68c1f4901b12310386c65e9 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Wed, 27 Nov 2024 19:18:43 +0300 Subject: [PATCH] chore: finalizing initial tests around call --- src/algokit_utils/_debugging.py | 25 ++++- src/algokit_utils/_legacy_v2/deploy.py | 6 +- src/algokit_utils/applications/app_client.py | 96 +++++++++---------- src/algokit_utils/transactions/models.py | 13 +++ .../transactions/transaction_composer.py | 63 +++++++----- .../transactions/transaction_sender.py | 33 ++++--- tests/applications/test_app_client.py | 17 +++- 7 files changed, 160 insertions(+), 93 deletions(-) diff --git a/src/algokit_utils/_debugging.py b/src/algokit_utils/_debugging.py index de5ed18..d054878 100644 --- a/src/algokit_utils/_debugging.py +++ b/src/algokit_utils/_debugging.py @@ -15,6 +15,7 @@ from algosdk.v2client.models import SimulateRequest, SimulateRequestTransactionGroup, SimulateTraceConfig from algokit_utils._legacy_v2.common import Program +from algokit_utils.transactions.models import SimulateOptions if typing.TYPE_CHECKING: from algosdk.v2client.algod import AlgodClient @@ -201,7 +202,11 @@ def persist_sourcemaps( _upsert_debug_sourcemaps(sourcemaps, project_root) -def simulate_response(atc: AtomicTransactionComposer, algod_client: "AlgodClient") -> SimulateAtomicTransactionResponse: +def simulate_response( + atc: AtomicTransactionComposer, + algod_client: "AlgodClient", + simulate_options: SimulateOptions | dict[str, typing.Any] | None = None, +) -> SimulateAtomicTransactionResponse: """ Simulate and fetch response for the given AtomicTransactionComposer and AlgodClient. @@ -219,15 +224,27 @@ def simulate_response(atc: AtomicTransactionComposer, algod_client: "AlgodClient fake_signed_transactions = empty_signer.sign_transactions(txn_list, []) txn_group = [SimulateRequestTransactionGroup(txns=fake_signed_transactions)] trace_config = SimulateTraceConfig(enable=True, stack_change=True, scratch_change=True, state_change=True) + simulate_params: SimulateOptions = simulate_options or {} # type: ignore[assignment] simulate_request = SimulateRequest( - txn_groups=txn_group, allow_more_logs=True, allow_empty_signatures=True, exec_trace_config=trace_config + txn_groups=txn_group, + allow_more_logs=True, + round=simulate_params.get("round") or None, + extra_opcode_budget=simulate_params.get("extra_opcode_budget") or 0, + allow_unnamed_resources=simulate_params.get("allow_unnamed_resources") or True, + allow_empty_signatures=simulate_params.get("allow_empty_signatures") or True, + exec_trace_config=simulate_params.get("exec_trace_config") or trace_config, ) + return atc.simulate(algod_client, simulate_request) def simulate_and_persist_response( - atc: AtomicTransactionComposer, project_root: Path, algod_client: "AlgodClient", buffer_size_mb: float = 256 + atc: AtomicTransactionComposer, + project_root: Path, + algod_client: "AlgodClient", + buffer_size_mb: float = 256, + simulate_options: SimulateOptions | dict[str, typing.Any] | None = None, ) -> SimulateAtomicTransactionResponse: """ Simulates the atomic transactions using the provided `AtomicTransactionComposer` object and `AlgodClient` object, @@ -252,7 +269,7 @@ def simulate_and_persist_response( txn_with_sign.txn.last_valid_round = sp.last txn_with_sign.txn.genesis_hash = sp.gh - response = simulate_response(atc_to_simulate, algod_client) + response = simulate_response(atc_to_simulate, algod_client, simulate_options) txn_results = response.simulate_response["txn-groups"] txn_types = [txn_result["txn-results"][0]["txn-result"]["txn"]["txn"]["type"] for txn_result in txn_results] diff --git a/src/algokit_utils/_legacy_v2/deploy.py b/src/algokit_utils/_legacy_v2/deploy.py index ed0bd0e..799fe08 100644 --- a/src/algokit_utils/_legacy_v2/deploy.py +++ b/src/algokit_utils/_legacy_v2/deploy.py @@ -7,9 +7,9 @@ from enum import Enum from typing import TYPE_CHECKING, TypeAlias, TypedDict +import algosdk from algosdk import transaction from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionSigner -from algosdk.logic import get_application_address from algosdk.transaction import StateSchema from deprecated import deprecated @@ -222,7 +222,7 @@ def get_creator_apps(indexer: "IndexerClient", creator_account: Account | str) - if create_metadata and create_metadata.name: apps[create_metadata.name] = AppMetaData( app_id=app_id, - app_address=get_application_address(app_id), + app_address=algosdk.logic.get_application_address(app_id), created_metadata=create_metadata, created_round=app_created_at_round, **(update_metadata or create_metadata).__dict__, @@ -809,7 +809,7 @@ def _create_metadata( ) -> AppMetaData: return AppMetaData( app_id=app_id, - app_address=get_application_address(app_id), + app_address=algosdk.logic.get_application_address(app_id), created_metadata=original_metadata or app_spec_note, created_round=created_round, updated_round=updated_round or created_round, diff --git a/src/algokit_utils/applications/app_client.py b/src/algokit_utils/applications/app_client.py index 84a3ac5..63a7a7b 100644 --- a/src/algokit_utils/applications/app_client.py +++ b/src/algokit_utils/applications/app_client.py @@ -3,12 +3,12 @@ import base64 import copy import json +import typing from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Protocol +from typing import TYPE_CHECKING, Any, Protocol, TypedDict import algosdk from algosdk.box_reference import BoxReference -from algosdk.logic import get_application_address from algosdk.transaction import OnComplete, Transaction from algokit_utils._legacy_v2.application_specification import ApplicationSpecification @@ -16,7 +16,6 @@ from algokit_utils.applications.utils import ( get_abi_decoded_value, get_abi_encoded_value, - get_abi_tuple_from_abi_struct, get_arc56_method, ) from algokit_utils.models.abi import ABIStruct @@ -410,7 +409,7 @@ class AppClientCallParams: class AppClientMethodCallParams: method: str sender: str | None = None - args: list[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] + args: list[ABIValue | ABIStruct | AppMethodCallTransactionArgument | None] | None = None signer: TransactionSigner | None = None rekey_to: str | None = None note: bytes | None = None @@ -423,10 +422,12 @@ class AppClientMethodCallParams: last_valid_round: int | None = None # OnComplete on_complete: algosdk.transaction.OnComplete | None = None - # # SendParams - max_rounds_to_wait: int | None = None - suppress_log: bool | None = None - populate_app_call_resources: bool | None = None + + +class SendParams(TypedDict, total=False): + max_rounds_to_wait: int | None + suppress_log: bool | None + populate_app_call_resources: bool | None @dataclass(kw_only=True) @@ -480,6 +481,10 @@ def fund_app_account(self, params: FundAppAccountParams) -> PaymentParams: close_remainder_to=params.close_remainder_to, ) + def opt_in(self, params: AppClientMethodCallParams) -> AppCallMethodCall: + input_params = self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.OptInOC) + return AppCallMethodCall(**input_params) + def call(self, params: AppClientMethodCallParams) -> AppCallMethodCall: input_params = self._get_abi_params(params.__dict__, on_complete=algosdk.transaction.OnComplete.NoOpOC) return AppCallMethodCall(**input_params) @@ -558,7 +563,12 @@ def __init__(self, client: AppClient) -> None: def fund_app_account(self, params: FundAppAccountParams) -> SendSingleTransactionResult: return self._algorand.send.payment(self._client.params.fund_app_account(params)) - def call(self, params: AppClientMethodCallParams) -> SendAppTransactionResult: + def opt_in(self, params: AppClientMethodCallParams) -> SendAppTransactionResult: + return self._algorand.send.app_call_method_call(self._client.params.opt_in(params)) + + def call( + self, params: AppClientMethodCallParams, **send_params: typing.Unpack[SendParams] + ) -> SendAppTransactionResult: is_read_only_call = ( params.on_complete == algosdk.transaction.OnComplete.NoOpOC or not params.on_complete @@ -566,47 +576,34 @@ def call(self, params: AppClientMethodCallParams) -> SendAppTransactionResult: ) if is_read_only_call: - return ( - self._algorand.new_group() - .add_app_call_method_call(self._client.params.call(params)) - .simulate(allow_unnamed_resources=params.populate_app_call_resources or True, skip_signature=True) + method_call_to_simulate = self._algorand.new_group().add_app_call_method_call( + self._client.params.call(params) ) - return self._algorand.send.app_call_method_call(self._client.params.call(params)) + simulate_response = method_call_to_simulate.simulate( + allow_unnamed_resources=send_params["populate_app_call_resources"] if send_params else True, + skip_signatures=True, + allow_more_logs=True, + allow_empty_signatures=True, + extra_opcode_budget=None, + exec_trace_config=None, + round=None, + fix_signers=None, # TODO: double check on whether algosdk py even has this param + ) - # call: async (params: AppClientMethodCallParams & CallOnComplete & SendParams) => { - # // Read-only call - do it via simulate - # if ( - # (params.onComplete === OnApplicationComplete.NoOpOC || !params.onComplete) && - # getArc56Method(params.method, this._appSpec).method.readonly - # ) { - # const result = await this._algorand - # .newGroup() - # .addAppCallMethodCall(await this.params.call(params)) - # .simulate({ - # allowUnnamedResources: params.populateAppCallResources ?? true, - # // Simulate calls for a readonly method shouldn't invoke signing - # skipSignatures: true, - # }) - # return this.processMethodCallReturn( - # { - # ...result, - # transaction: result.transactions.at(-1)!, - # confirmation: result.confirmations.at(-1)!, - # // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - # return: result.returns?.length ?? 0 > 0 ? result.returns?.at(-1)! : undefined, - # } satisfies SendAppTransactionResult, - # getArc56Method(params.method, this._appSpec), - # ) - # } - - # return this.handleCallErrors(async () => - # this.processMethodCallReturn( - # this._algorand.send.appCallMethodCall(await this.params.call(params)), - # getArc56Method(params.method, this._appSpec), - # ), - # ) - # }, + return SendAppTransactionResult( + tx_id=simulate_response.tx_ids[-1], + tx_ids=simulate_response.tx_ids, + transactions=simulate_response.transactions, + transaction=simulate_response.transactions[-1], + confirmation=simulate_response.confirmations[-1] if simulate_response.confirmations else b"", + confirmations=simulate_response.confirmations, + group_id=simulate_response.group_id or "", + returns=simulate_response.returns, + return_value=simulate_response.returns[-1].return_value, + ) + + return self._algorand.send.app_call_method_call(self._client.params.call(params)) class AppClient: @@ -614,7 +611,7 @@ def __init__(self, params: AppClientParams) -> None: self._app_id = params.app_id self._app_spec = self.normalise_app_spec(params.app_spec) self._algorand = params.algorand - self._app_address = get_application_address(self._app_id) + self._app_address = algosdk.logic.get_application_address(self._app_id) self._app_name = params.app_name or self._app_spec.name self._default_sender = params.default_sender self._default_signer = params.default_signer @@ -741,6 +738,9 @@ def compile( compiled_clear=compiled_clear, ) + def process_method_call_return(): + pass + # NOTE: No method overloads hence slightly different name, in TS its both instance/static methods named 'compile' def compile_and_persist_sourcemaps( self, compilation: AppClientCompilationParams | None = None diff --git a/src/algokit_utils/transactions/models.py b/src/algokit_utils/transactions/models.py index 251bbf9..cd61f5a 100644 --- a/src/algokit_utils/transactions/models.py +++ b/src/algokit_utils/transactions/models.py @@ -1,5 +1,7 @@ from typing import Any, Literal, TypedDict +from algosdk.v2client.models.simulate_request import SimulateTraceConfig + # Define specific types for different formats class BaseArc2Note(TypedDict): @@ -22,6 +24,17 @@ class JsonFormatArc2Note(BaseArc2Note): data: str | dict[str, Any] | list[Any] | int | None +class SimulateOptions(TypedDict): + allow_more_logs: bool | None + allow_empty_signatures: bool | None + allow_unnamed_resources: bool | None + extra_opcode_budget: int | None + exec_trace_config: SimulateTraceConfig | None + round: int | None + skip_signatures: int | None + fix_signers: bool | None + + # Combined type for all valid ARC-0002 notes # See: https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0002.md Arc2TransactionNote = StringFormatArc2Note | JsonFormatArc2Note diff --git a/src/algokit_utils/transactions/transaction_composer.py b/src/algokit_utils/transactions/transaction_composer.py index ed0bec2..218f94b 100644 --- a/src/algokit_utils/transactions/transaction_composer.py +++ b/src/algokit_utils/transactions/transaction_composer.py @@ -1,10 +1,8 @@ from __future__ import annotations import logging -import math -import typing from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, TypedDict, Union +from typing import TYPE_CHECKING, Any, Union, Unpack import algosdk import algosdk.atomic_transaction_composer @@ -17,12 +15,12 @@ from algosdk.error import AlgodHTTPError from algosdk.transaction import OnComplete, Transaction from algosdk.v2client.algod import AlgodClient -from algosdk.v2client.models.simulate_request import SimulateTraceConfig from deprecated import deprecated from algokit_utils._debugging import simulate_and_persist_response, simulate_response from algokit_utils.applications.app_manager import AppManager from algokit_utils.config import config +from algokit_utils.transactions.models import SimulateOptions if TYPE_CHECKING: from collections.abc import Callable @@ -585,8 +583,9 @@ class SendAtomicTransactionComposerResults: """The transaction IDs that were sent""" transactions: list[Transaction] """The transactions that were sent""" - returns: list[Any] + returns: list[Any] | list[algosdk.atomic_transaction_composer.ABIResult] """The ABI return values from any ABI method calls""" + simulate_response: dict[str, Any] | None = None def send_atomic_transaction_composer( # noqa: C901, PLR0912, PLR0913 @@ -703,16 +702,6 @@ def send_atomic_transaction_composer( # noqa: C901, PLR0912, PLR0913 raise Exception(f"Transaction failed: {e}") from e -class TransactionComposerSimulateOptions(TypedDict): - allow_more_logs: bool | None - allow_empty_signatures: bool | None - allow_unnamed_resources: bool | None - extra_opcode_budget: int | None - exec_trace_config: SimulateTraceConfig | None - round: int | None - skip_signature: int | None - - class TransactionComposer: """ A class for composing and managing Algorand transactions using the Algosdk library. @@ -935,21 +924,47 @@ def send( def simulate( self, - **params: typing.Unpack[TransactionComposerSimulateOptions], - ) -> algosdk.atomic_transaction_composer.SimulateAtomicTransactionResponse: - # TODO: propagate simulation options to the underlying algosdk.atomic_transaction_composer.AtomicTransactionComposer + **simulate_options: Unpack[SimulateOptions], + ) -> SendAtomicTransactionComposerResults: + atc = AtomicTransactionComposer() if simulate_options["skip_signatures"] else self.atc + + if simulate_options["skip_signatures"]: + simulate_options["allow_empty_signatures"] = True + simulate_options["fix_signers"] = True + transactions = self.build_transactions() + for txn in transactions.transactions: + atc.add_transaction(TransactionWithSigner(txn=txn, signer=TransactionComposer.NULL_SIGNER)) + atc.method_dict = transactions.method_calls + else: + self.build() if config.debug and config.project_root and config.trace_all: - return simulate_and_persist_response( - self.atc, + response = simulate_and_persist_response( + atc, config.project_root, self.algod, config.trace_buffer_size_mb, + simulate_options, + ) + + return SendAtomicTransactionComposerResults( + confirmations=[], # TODO: extract confirmations, + transactions=[txn.txn for txn in atc.txn_list], + tx_ids=response.tx_ids, + group_id=atc.txn_list[-1].txn.group or "", + simulate_response=response.simulate_response, + returns=response.abi_results, ) - return simulate_response( - self.atc, - self.algod, + response = simulate_response(atc, self.algod, simulate_options) + + return SendAtomicTransactionComposerResults( + confirmations=[], # TODO: extract confirmations, + transactions=[txn.txn for txn in atc.txn_list], + tx_ids=response.tx_ids, + group_id=atc.txn_list[-1].txn.group or "", + simulate_response=response.simulate_response, + returns=response.abi_results, ) @staticmethod @@ -1077,7 +1092,7 @@ def _build_method_call( # noqa: C901, PLR0912 sp=suggested_params, signer=params.signer or self.get_signer(params.sender), method_args=method_args, - on_complete=algosdk.transaction.OnComplete.NoOpOC, + on_complete=params.on_complete or algosdk.transaction.OnComplete.NoOpOC, note=params.note, lease=params.lease, boxes=[(ref.app_index, ref.name) for ref in params.box_references] if params.box_references else None, diff --git a/src/algokit_utils/transactions/transaction_sender.py b/src/algokit_utils/transactions/transaction_sender.py index 3050dcb..fae7b75 100644 --- a/src/algokit_utils/transactions/transaction_sender.py +++ b/src/algokit_utils/transactions/transaction_sender.py @@ -1,6 +1,7 @@ from collections.abc import Callable from dataclasses import dataclass from logging import getLogger +from pathlib import Path from typing import Any, TypedDict, TypeVar import algosdk @@ -8,6 +9,7 @@ from algosdk.atomic_transaction_composer import AtomicTransactionResponse from algosdk.transaction import Transaction +from algokit_utils._debugging import simulate_and_persist_response from algokit_utils.applications.app_manager import AppManager from algokit_utils.assets.asset_manager import AssetManager from algokit_utils.models.abi import ABIValue @@ -115,17 +117,26 @@ def send_transaction(params: T) -> SendSingleTransactionResult: transaction = composer.build().transactions[-1].txn logger.debug(pre_log(params, transaction)) - raw_result = composer.send() - - result = SendSingleTransactionResult( - **raw_result.__dict__, - confirmation=raw_result.confirmations[-1], - transaction=raw_result.transactions[-1], - tx_id=raw_result.tx_ids[-1], - ) - - if post_log: - logger.debug(post_log(params, result)) + try: + raw_result = composer.send() + raw_result_dict = raw_result.__dict__.copy() + del raw_result_dict["simulate_response"] + + result = SendSingleTransactionResult( + **raw_result_dict, + confirmation=raw_result.confirmations[-1], + transaction=raw_result.transactions[-1], + tx_id=raw_result.tx_ids[-1], + ) + + if post_log: + logger.debug(post_log(params, result)) + except Exception: + simulate_and_persist_response( + composer.atc, + Path("/Users/aorumbayev/MakerX/projects/algokit/algokit-utils/utils/algokit-utils-py"), + self._algod, + ) return result diff --git a/tests/applications/test_app_client.py b/tests/applications/test_app_client.py index 4b250fc..7df50c7 100644 --- a/tests/applications/test_app_client.py +++ b/tests/applications/test_app_client.py @@ -218,13 +218,24 @@ def test_abi_with_default_arg_method( default_signer=funded_account.signer, ) ) + # app_client.send. + app_client.send.opt_in(AppClientMethodCallParams(method="opt_in")) + app_client.send.call( + AppClientMethodCallParams( + method="set_local", + args=[1, 2, "banana", [1, 2, 3, 4]], + ) + ) + + method_signature = "default_value_from_local_state(string)string" + defined_value = "defined value" # Test with defined value defined_value_result = app_client.send.call( - AppClientMethodCallParams(method="default_value_from_local_state(string)string", args=["defined value"]) + AppClientMethodCallParams(method=method_signature, args=[defined_value]) ) assert defined_value_result.return_value == "Local state, defined value" # Test with default value - default_value_result = app_client.send.call(AppClientMethodCallParams(method="hello(string)string", args=[None])) - assert default_value_result.return_value == "Hello, default" + default_value_result = app_client.send.call(AppClientMethodCallParams(method=method_signature, args=[None])) + assert default_value_result.return_value == "Local state, banana"