Skip to content

Commit

Permalink
feat: TransactionComposer & AppManager implementation; various ongoin…
Browse files Browse the repository at this point in the history
…g refactoring efforts (#120)

* feat: Initial AppManager implementation; wip on Composer; mypy tweaks; initial tests

- Add AppManager class with methods for compiling TEAL, managing app state, and handling template variables
- Update TransactionComposer to use AppManager
- Move Account model to a separate file and update imports
- Add new models for ABI values and application constants
- Improve type annotations and remove unnecessary type ignores
- Add initial tests for AppManager template substitution and comment stripping
- Update mypy configuration to *globally* exclude untyped calls in algosdk -> removing ~50 individual mypy type ignore for algosdk

* chore: removing models.py folders in favour of  granular modules in root models namespace

* chore: wip

* feat: initial implementation of TransactionComposer
  • Loading branch information
aorumbayev authored Nov 4, 2024
1 parent 9389170 commit 1c77ad8
Show file tree
Hide file tree
Showing 41 changed files with 2,328 additions and 592 deletions.
6 changes: 3 additions & 3 deletions legacy_v2_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,11 @@ def generate_test_asset(algod_client: "AlgodClient", sender: Account, total: int
note=None,
lease=None,
rekey_to=None,
) # type: ignore[no-untyped-call]
)

signed_transaction = txn.sign(sender.private_key) # type: ignore[no-untyped-call]
signed_transaction = txn.sign(sender.private_key)
algod_client.send_transaction(signed_transaction)
ptx = algod_client.pending_transaction_info(txn.get_txid()) # type: ignore[no-untyped-call]
ptx = algod_client.pending_transaction_info(txn.get_txid())

if isinstance(ptx, dict) and "asset-index" in ptx and isinstance(ptx["asset-index"], int):
return ptx["asset-index"]
Expand Down
9 changes: 9 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ suppress-none-returning = true
[tool.ruff.lint.per-file-ignores]
"src/algokit_utils/beta/*" = ["ERA001", "E501", "PLR0911"]
"path/to/file.py" = ["E402"]
"tests/clients/test_algorand_client.py" = ["ERA001"]

[tool.poe.tasks]
docs = ["docs-html-only", "docs-md-only"]
Expand All @@ -149,6 +150,14 @@ disallow_any_generics = false
implicit_reexport = false
show_error_codes = true

untyped_calls_exclude = [
"algosdk",
]

[[tool.mypy.overrides]]
module = ["algosdk", "algosdk.*"]
disallow_untyped_calls = false

[tool.semantic_release]
version_toml = "pyproject.toml:tool.poetry.version"
remove_dist = false
Expand Down
5 changes: 2 additions & 3 deletions src/algokit_utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@
from algokit_utils._legacy_v2.asset import opt_in, opt_out
from algokit_utils._legacy_v2.common import Program
from algokit_utils._legacy_v2.deploy import (
DELETABLE_TEMPLATE_NAME,
NOTE_PREFIX,
UPDATABLE_TEMPLATE_NAME,
ABICallArgs,
ABICallArgsDict,
ABICreateCallArgs,
Expand Down Expand Up @@ -61,7 +59,6 @@
ABIArgsDict,
ABIMethod,
ABITransactionResponse,
Account,
CommonCallParameters,
CommonCallParametersDict,
CreateCallParameters,
Expand Down Expand Up @@ -91,6 +88,8 @@
DispenserLimitResponse,
TestNetDispenserApiClient,
)
from algokit_utils.models.account import Account
from algokit_utils.models.application import DELETABLE_TEMPLATE_NAME, UPDATABLE_TEMPLATE_NAME

__all__ = [
# ==== LEGACY V2 EXPORTS BEGIN ====
Expand Down
4 changes: 1 addition & 3 deletions src/algokit_utils/_debugging.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,7 @@ def _build_avm_sourcemap( # noqa: PLR0913
raise ValueError("Either raw teal or compiled teal must be provided")

result = compiled_teal if compiled_teal else Program(str(raw_teal), client=client)
program_hash = base64.b64encode(
checksum(result.raw_binary) # type: ignore[no-untyped-call]
).decode()
program_hash = base64.b64encode(checksum(result.raw_binary)).decode()
source_map = result.source_map.__dict__
source_map["sources"] = [f"{file_name}{TEAL_FILE_EXT}"] if with_sources else []

Expand Down
6 changes: 3 additions & 3 deletions src/algokit_utils/_legacy_v2/_ensure_funded.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@

from algokit_utils._legacy_v2._transfer import TransferParameters, transfer
from algokit_utils._legacy_v2.account import get_dispenser_account
from algokit_utils._legacy_v2.models import Account
from algokit_utils._legacy_v2.network_clients import is_testnet
from algokit_utils.clients.dispenser_api_client import (
DispenserAssetName,
TestNetDispenserApiClient,
)
from algokit_utils.models.account import Account


@dataclass(kw_only=True)
Expand Down Expand Up @@ -63,7 +63,7 @@ def _get_address_to_fund(parameters: EnsureBalanceParameters) -> str:
if isinstance(parameters.account_to_fund, str):
return parameters.account_to_fund
else:
return str(address_from_private_key(parameters.account_to_fund.private_key)) # type: ignore[no-untyped-call]
return str(address_from_private_key(parameters.account_to_fund.private_key))


def _get_account_info(client: AlgodClient, address_to_fund: str) -> dict:
Expand Down Expand Up @@ -111,7 +111,7 @@ def _fund_using_transfer(
fee_micro_algos=parameters.fee_micro_algos,
),
)
transaction_id = response.get_txid() # type: ignore[no-untyped-call]
transaction_id = response.get_txid()
return EnsureFundedResponse(transaction_id=transaction_id, amount=response.amt)


Expand Down
8 changes: 4 additions & 4 deletions src/algokit_utils/_legacy_v2/_transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from algosdk.atomic_transaction_composer import AccountTransactionSigner
from algosdk.transaction import AssetTransferTxn, PaymentTxn, SuggestedParams

from algokit_utils._legacy_v2.models import Account
from algokit_utils.models.account import Account

if TYPE_CHECKING:
from algosdk.v2client.algod import AlgodClient
Expand Down Expand Up @@ -93,7 +93,7 @@ def transfer(client: "AlgodClient", parameters: TransferParameters) -> PaymentTx
amt=params.micro_algos,
note=params.note.encode("utf-8") if isinstance(params.note, str) else params.note,
sp=params.suggested_params,
) # type: ignore[no-untyped-call]
)

result = _send_transaction(client=client, transaction=transaction, parameters=params)
assert isinstance(result, PaymentTxn)
Expand All @@ -117,7 +117,7 @@ def transfer_asset(client: "AlgodClient", parameters: TransferAssetParameters) -
note=params.note,
index=params.asset_id,
rekey_to=None,
) # type: ignore[no-untyped-call]
)

result = _send_transaction(client=client, transaction=xfer_txn, parameters=params)
assert isinstance(result, AssetTransferTxn)
Expand Down Expand Up @@ -148,5 +148,5 @@ def _get_address(account: Account | AccountTransactionSigner) -> str:
if type(account) is Account:
return account.address
else:
address = address_from_private_key(account.private_key) # type: ignore[no-untyped-call]
address = address_from_private_key(account.private_key)
return str(address)
14 changes: 7 additions & 7 deletions src/algokit_utils/_legacy_v2/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from algosdk.util import algos_to_microalgos

from algokit_utils._legacy_v2._transfer import TransferParameters, transfer
from algokit_utils._legacy_v2.models import Account
from algokit_utils._legacy_v2.network_clients import get_kmd_client_from_algod_client, is_localnet
from algokit_utils.models.account import Account

if TYPE_CHECKING:
from collections.abc import Callable
Expand All @@ -32,8 +32,8 @@

def get_account_from_mnemonic(mnemonic: str) -> Account:
"""Convert a mnemonic (25 word passphrase) into an Account"""
private_key = to_private_key(mnemonic) # type: ignore[no-untyped-call]
address = address_from_private_key(private_key) # type: ignore[no-untyped-call]
private_key = to_private_key(mnemonic)
address = address_from_private_key(private_key)
return Account(private_key=private_key, address=address)


Expand All @@ -47,7 +47,7 @@ def create_kmd_wallet_account(kmd_client: "KMDClient", name: str) -> Account:
account_key = key_ids[0]

private_account_key = kmd_client.export_key(wallet_handle, "", account_key)
return get_account_from_mnemonic(from_private_key(private_account_key)) # type: ignore[no-untyped-call]
return get_account_from_mnemonic(from_private_key(private_account_key))


def get_or_create_kmd_wallet_account(
Expand Down Expand Up @@ -79,7 +79,7 @@ def get_or_create_kmd_wallet_account(
TransferParameters(
from_account=get_dispenser_account(client),
to_address=account.address,
micro_algos=algos_to_microalgos(fund_with_algos), # type: ignore[no-untyped-call]
micro_algos=algos_to_microalgos(fund_with_algos),
),
)

Expand Down Expand Up @@ -139,7 +139,7 @@ def get_kmd_wallet_account(
return None

private_account_key = kmd_client.export_key(wallet_handle, "", matched_account_key)
return get_account_from_mnemonic(from_private_key(private_account_key)) # type: ignore[no-untyped-call]
return get_account_from_mnemonic(from_private_key(private_account_key))


def get_account(
Expand Down Expand Up @@ -177,7 +177,7 @@ def get_account(

if is_localnet(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]
os.environ[mnemonic_key] = from_private_key(account.private_key)
return account

raise Exception(f"Missing environment variable '{mnemonic_key}' when looking for account '{name}'")
8 changes: 4 additions & 4 deletions src/algokit_utils/_legacy_v2/application_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
ABIArgType,
ABIMethod,
ABITransactionResponse,
Account,
CreateCallParameters,
CreateCallParametersDict,
OnCompleteCallParameters,
Expand All @@ -54,6 +53,7 @@
TransactionResponse,
)
from algokit_utils.config import config
from algokit_utils.models.account import Account

if typing.TYPE_CHECKING:
from algosdk.v2client.algod import AlgodClient
Expand Down Expand Up @@ -1021,7 +1021,7 @@ def add_method_call( # noqa: PLR0913
raise Exception(f"ABI arguments specified on a bare call: {', '.join(abi_args)}")
atc.add_transaction(
TransactionWithSigner(
txn=transaction.ApplicationCallTxn( # type: ignore[no-untyped-call]
txn=transaction.ApplicationCallTxn(
sender=sender,
sp=sp,
index=app_id,
Expand Down Expand Up @@ -1329,11 +1329,11 @@ def get_sender_from_signer(signer: TransactionSigner | None) -> str | None:
"""Returns the associated address of a signer, return None if no address found"""

if isinstance(signer, AccountTransactionSigner):
sender = address_from_private_key(signer.private_key) # type: ignore[no-untyped-call]
sender = address_from_private_key(signer.private_key)
assert isinstance(sender, str)
return sender
elif isinstance(signer, MultisigTransactionSigner):
sender = signer.msig.address() # type: ignore[no-untyped-call]
sender = signer.msig.address()
assert isinstance(sender, str)
return sender
elif isinstance(signer, LogicSigTransactionSigner):
Expand Down
4 changes: 2 additions & 2 deletions src/algokit_utils/_legacy_v2/application_specification.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def _encode_state_schema(schema: StateSchema) -> dict[str, int]:


def _decode_state_schema(data: dict[str, int]) -> StateSchema:
return StateSchema( # type: ignore[no-untyped-call]
return StateSchema(
num_byte_slices=data.get("num_byte_slices", 0),
num_uints=data.get("num_uints", 0),
)
Expand Down Expand Up @@ -203,4 +203,4 @@ def export(self, directory: Path | str | None = None) -> None:


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]
return StateSchema(schema.get("num-uint", 0), schema.get("num-byte-slice", 0))
2 changes: 1 addition & 1 deletion src/algokit_utils/_legacy_v2/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from enum import Enum, auto

from algokit_utils._legacy_v2.models import Account
from algokit_utils.models.account import Account

__all__ = ["opt_in", "opt_out"]
logger = logging.getLogger(__name__)
Expand Down
56 changes: 8 additions & 48 deletions src/algokit_utils/_legacy_v2/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from algosdk.atomic_transaction_composer import AtomicTransactionComposer, TransactionSigner
from algosdk.logic import get_application_address
from algosdk.transaction import StateSchema
from deprecated import deprecated

from algokit_utils._legacy_v2.application_specification import (
ApplicationSpecification,
Expand All @@ -21,10 +22,11 @@
from algokit_utils._legacy_v2.models import (
ABIArgsDict,
ABIMethod,
Account,
CreateCallParameters,
TransactionResponse,
)
from algokit_utils.applications.app_manager import AppManager
from algokit_utils.models.account import Account

if TYPE_CHECKING:
from algosdk.v2client.algod import AlgodClient
Expand Down Expand Up @@ -185,7 +187,7 @@ def get_creator_apps(indexer: "IndexerClient", creator_account: Account | str) -
while True:
response = indexer.lookup_account_application_by_creator(
creator_address, limit=DEFAULT_INDEXER_MAX_API_RESOURCES_PER_ACCOUNT, next_page=token
) # type: ignore[no-untyped-call]
)
if "message" in response: # an error occurred
raise Exception(f"Error querying applications for {creator_address}: {response}")
for app in response["applications"]:
Expand All @@ -199,7 +201,7 @@ def get_creator_apps(indexer: "IndexerClient", creator_account: Account | str) -
address=creator_address,
address_role="sender",
note_prefix=NOTE_PREFIX.encode("utf-8"),
) # type: ignore[no-untyped-call]
)
transactions: list[dict] = search_transactions_response["transactions"]
if not transactions:
continue
Expand Down Expand Up @@ -236,7 +238,7 @@ def get_creator_apps(indexer: "IndexerClient", creator_account: Account | str) -


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]
return StateSchema(schema.get("num-uint", 0), schema.get("num-byte-slice", 0))


def _describe_schema_breaks(prefix: str, from_schema: StateSchema, to_schema: StateSchema) -> Iterable[str]:
Expand Down Expand Up @@ -288,33 +290,6 @@ def _is_valid_token_character(char: str) -> bool:
return char.isalnum() or char == "_"


def _replace_template_variable(program_lines: list[str], template_variable: str, value: str) -> tuple[list[str], int]:
result: list[str] = []
match_count = 0
token = f"TMPL_{template_variable}"
token_idx_offset = len(value) - len(token)
for line in program_lines:
comment_idx = _find_unquoted_string(line, "//")
if comment_idx is None:
comment_idx = len(line)
code = line[:comment_idx]
comment = line[comment_idx:]
trailing_idx = 0
while True:
token_idx = _find_template_token(code, token, trailing_idx)
if token_idx is None:
break

trailing_idx = token_idx + len(token)
prefix = code[:token_idx]
suffix = code[trailing_idx:]
code = f"{prefix}{value}{suffix}"
match_count += 1
trailing_idx += token_idx_offset
result.append(code + comment)
return result, match_count


def add_deploy_template_variables(
template_values: TemplateValueDict, allow_update: bool | None, allow_delete: bool | None
) -> None:
Expand Down Expand Up @@ -437,30 +412,15 @@ def check_template_variables(approval_program: str, template_values: TemplateVal
logger.warning(f"{tmpl_variable} not found in approval program, but variable was provided")


@deprecated(reason="Use `AppManager.replace_template_variables` instead", version="3.0.0")
def replace_template_variables(program: str, template_values: TemplateValueMapping) -> str:
"""Replaces `TMPL_*` variables in `program` with `template_values`
```{note}
`template_values` keys should *NOT* be prefixed with `TMPL_`
```
"""
program_lines = program.splitlines()
for template_variable_name, template_value in template_values.items():
match template_value:
case int():
value = str(template_value)
case str():
value = "0x" + template_value.encode("utf-8").hex()
case bytes():
value = "0x" + template_value.hex()
case _:
raise DeploymentFailedError(
f"Unexpected template value type {template_variable_name}: {template_value.__class__}"
)

program_lines, matches = _replace_template_variable(program_lines, template_variable_name, value)

return "\n".join(program_lines)
return AppManager.replace_template_variables(program, template_values)


def has_template_vars(app_spec: ApplicationSpecification) -> bool:
Expand Down
Loading

0 comments on commit 1c77ad8

Please sign in to comment.