Skip to content

Commit

Permalink
chore: finalizing initial tests around call
Browse files Browse the repository at this point in the history
  • Loading branch information
aorumbayev committed Nov 27, 2024
1 parent 1e3fa62 commit 8517358
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 93 deletions.
25 changes: 21 additions & 4 deletions src/algokit_utils/_debugging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -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]
Expand Down
6 changes: 3 additions & 3 deletions src/algokit_utils/_legacy_v2/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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__,
Expand Down Expand Up @@ -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,
Expand Down
96 changes: 48 additions & 48 deletions src/algokit_utils/applications/app_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,19 @@
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
from algokit_utils.applications.app_manager import AppManager, CompiledTeal, TealTemplateParams
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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -558,63 +563,55 @@ 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
and get_arc56_method(params.method, self._app_spec).method.readonly
)

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:
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
Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions src/algokit_utils/transactions/models.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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
Expand Down
63 changes: 39 additions & 24 deletions src/algokit_utils/transactions/transaction_composer.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 8517358

Please sign in to comment.