Skip to content

Commit

Permalink
chore: wip
Browse files Browse the repository at this point in the history
  • Loading branch information
aorumbayev committed Dec 8, 2024
1 parent 77dbd8a commit 25c084b
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 31 deletions.
9 changes: 1 addition & 8 deletions src/algokit_utils/applications/app_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,6 @@
T = TypeVar("T") # For generic return type in _handle_call_errors


def camel_to_snake_case(name: str) -> str:
import re

return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()


def get_constant_block_offset(program: bytes) -> int: # noqa: C901
"""Calculate the offset after constant blocks in TEAL program.
Expand Down Expand Up @@ -885,8 +879,7 @@ def normalise_app_spec(app_spec: Arc56Contract | ApplicationSpecification | str)
return arc32_to_arc56(spec)
elif isinstance(spec, dict):
# normalize field names to lowercase to python camel
transformed_spec = {camel_to_snake_case(k): v for k, v in spec.items()}
return Arc56Contract(**transformed_spec)
return Arc56Contract.from_json(spec)
else:
raise ValueError("Invalid app spec format")

Expand Down
3 changes: 2 additions & 1 deletion src/algokit_utils/applications/app_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from algosdk.atomic_transaction_composer import ABIResult, AccountTransactionSigner
from algosdk.box_reference import BoxReference as AlgosdkBoxReference
from algosdk.logic import get_application_address
from algosdk.source_map import SourceMap
from algosdk.v2client import algod

from algokit_utils.models.abi import ABIType, ABIValue
Expand Down Expand Up @@ -169,7 +170,7 @@ def compile_teal(self, teal_code: str) -> CompiledTeal:
compiled=compiled["result"],
compiled_hash=compiled["hash"],
compiled_base64_to_bytes=base64.b64decode(compiled["result"]),
source_map=compiled.get("sourcemap"),
source_map=SourceMap(compiled.get("sourcemap", {})),
)
self._compilation_results[teal_code] = result
return result
Expand Down
3 changes: 1 addition & 2 deletions src/algokit_utils/applications/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
Arc56Contract,
Arc56ContractState,
Arc56Method,
ARCType,
CallConfig,
DefaultValue,
Method,
Expand Down Expand Up @@ -399,7 +398,7 @@ def get_action_value(key: str) -> str | None:
}

return Arc56Contract(
arcs=[ARCType.ARC56],
arcs=[],
name=app_spec.contract.name,
desc=app_spec.contract.desc,
structs=structs,
Expand Down
8 changes: 6 additions & 2 deletions src/algokit_utils/errors/logic_error.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import base64
import dataclasses
import re
from copy import copy
Expand Down Expand Up @@ -68,9 +69,12 @@ def __init__(
):
self.logic_error = logic_error
self.logic_error_str = logic_error_str
self.program = program
try:
self.program = base64.b64decode(program).decode("utf-8")
except Exception:
self.program = program
self.source_map = source_map
self.lines = program.split("\n")
self.lines = self.program.split("\n")
self.transaction_id = transaction_id
self.message = message
self.pc = pc
Expand Down
138 changes: 122 additions & 16 deletions src/algokit_utils/models/application.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import json
from dataclasses import asdict, dataclass, field, is_dataclass
from enum import IntEnum
from typing import Any, Literal

import algosdk
Expand All @@ -25,6 +25,20 @@
DefaultValueSource = Literal["box", "global", "local", "literal", "method"]


def convert_key_to_snake_case(name: str) -> str:
import re

return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()


def convert_keys_to_snake_case(obj: Any) -> Any: # noqa: ANN401
if isinstance(obj, dict):
return {convert_key_to_snake_case(k): convert_keys_to_snake_case(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [convert_keys_to_snake_case(item) for item in obj]
return obj


@dataclass
class CallConfig:
no_op: str | None = None
Expand All @@ -35,11 +49,6 @@ class CallConfig:
delete_application: str | None = None


class ARCType(IntEnum):
ARC56 = 56
ARC32 = 32


@dataclass(kw_only=True)
class StructField:
name: str
Expand Down Expand Up @@ -194,7 +203,7 @@ def __init__(self, method: Method):

@dataclass(kw_only=True)
class Arc56Contract:
arcs: list[ARCType]
arcs: list[int]
name: str
desc: str | None = None
networks: dict[str, dict[str, int]] | None = None
Expand All @@ -210,15 +219,112 @@ class Arc56Contract:
template_variables: dict[str, dict[str, ABITypeAlias | AVMType | StructName | str]] | None = None
scratch_variables: dict[str, dict[str, int | ABITypeAlias | AVMType | StructName]] | None = None

def __init__(self, **kwargs: Any) -> None:
if isinstance(kwargs.get("state"), dict):
kwargs["state"] = Arc56ContractState(**kwargs["state"])
if isinstance(kwargs.get("methods"), list):
kwargs["methods"] = [Method(**method) for method in kwargs["methods"]]
if isinstance(kwargs.get("source_info"), dict):
kwargs["source_info"] = {k: ProgramSourceInfo(**v) for k, v in kwargs["source_info"].items()}

super().__init__(**kwargs)
@staticmethod
def from_json(application_spec: str | dict) -> "Arc56Contract":
"""Convert a JSON dictionary into an Arc56Contract instance.
Args:
json_data (dict): The JSON data representing an Arc56Contract
Returns:
Arc56Contract: The constructed Arc56Contract instance
"""
# Convert networks if present
json_data = json.loads(application_spec) if isinstance(application_spec, str) else application_spec
json_data = convert_keys_to_snake_case(json_data)
networks = json_data.get("networks")

# Convert structs
structs = {
name: [StructField(**field) if isinstance(field, dict) else field for field in struct_fields]
for name, struct_fields in json_data.get("structs", {}).items()
}

# Convert methods
methods = []
for method_data in json_data.get("methods", []):
# Convert method args
args = [MethodArg(**arg) for arg in method_data.get("args", [])]

# Convert method returns
returns_data = method_data.get("returns", {"type": "void"})
returns = MethodReturns(**returns_data)

# Convert method actions
actions_data = method_data.get("actions", {"create": [], "call": []})
actions = MethodActions(**actions_data)

# Convert events if present
events = None
if "events" in method_data:
events = [Event(**event) for event in method_data["events"]]

# Convert recommendations if present
recommendations = None
if "recommendations" in method_data:
recommendations = Recommendations(**method_data["recommendations"])

methods.append(
Method(
name=method_data["name"],
desc=method_data.get("desc"),
args=args,
returns=returns,
actions=actions,
readonly=method_data.get("readonly", False),
events=events,
recommendations=recommendations,
)
)

# Convert state
state_data = json_data["state"]
state = Arc56ContractState(
keys={
category: {name: StorageKey(**key_data) for name, key_data in keys.items()}
for category, keys in state_data.get("keys", {}).items()
},
maps={
category: {name: StorageMap(**map_data) for name, map_data in maps.items()}
for category, maps in state_data.get("maps", {}).items()
},
schemas=state_data.get("schema", {}),
)

# Convert compiler info if present
compiler_info = None
if "compiler_info" in json_data:
compiler_version = CompilerVersion(**json_data["compiler_info"]["compiler_version"])
compiler_info = CompilerInfo(
compiler=json_data["compiler_info"]["compiler"], compiler_version=compiler_version
)

# Convert events if present
events = None
if "events" in json_data:
events = [Event(**event) for event in json_data["events"]]

source_info = {}
if "source_info" in json_data:
source_info = {key: ProgramSourceInfo(**val) for key, val in json_data["source_info"].items()}

return Arc56Contract(
arcs=json_data.get("arcs", []),
name=json_data["name"],
desc=json_data.get("desc"),
networks=networks,
structs=structs,
methods=methods,
state=state,
bare_actions=json_data.get("bare_actions", {}),
source_info=source_info,
source=json_data.get("source"),
byte_code=json_data.get("byte_code"),
compiler_info=compiler_info,
events=events,
template_variables=json_data.get("template_variables"),
scratch_variables=json_data.get("scratch_variables"),
)


@dataclass(kw_only=True, frozen=True)
Expand Down
51 changes: 49 additions & 2 deletions tests/applications/test_app_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from algosdk.logic import get_application_address
from algosdk.transaction import ApplicationCallTxn, ApplicationCreateTxn, OnComplete

from algokit_utils import OnSchemaBreak, OnUpdate, OperationPerformed
from algokit_utils.applications.app_client import (
AppClient,
AppClientMethodCallParams,
Expand Down Expand Up @@ -452,7 +453,53 @@ def test_arc56_error_messages_with_dynamic_template_vars_cblock_offset(
},
)

with pytest.raises(Exception) as exc_info:
with pytest.raises(LogicError) as exc_info:
client.send.call(AppClientMethodCallWithSendParams(method="throwError"))

assert "this is an error" in str(exc_info.value)
assert "this is an error" in exc_info.value.stack


def test_arc56_undefined_error_message_with_dynamic_template_vars_cblock_offset(
arc56_factory: AppFactory,
algorand: AlgorandClient,
funded_account: Account,
) -> None:
# Deploy app with template parameters
client, result = arc56_factory.deploy(
create_params=AppClientMethodCallParams(method="createApplication"),
deploy_time_params={
"bytes64TmplVar": "0" * 64,
"uint64TmplVar": 0,
"bytes32TmplVar": "0" * 32,
"bytesTmplVar": "foo",
},
)
app_id = result.app_id

# Create new client without source map from compilation
app_client = AppClient(
AppClientParams(
app_id=app_id,
default_sender=funded_account.address,
default_signer=funded_account.signer,
algorand=algorand,
app_spec=client.app_spec,
)
)

# Test error handling
with pytest.raises(LogicError) as exc_info:
app_client.send.call(AppClientMethodCallWithSendParams(method="tmpl"))

expected_error = """log
// tests/example-contracts/arc56_templates/templates.algo.ts:14
// assert(this.uint64TmplVar)
intc 1 // TMPL_uint64TmplVar
assert <--- Error
retsub
// specificLengthTemplateVar()void
*abi_route_specificLengthTemplateVar:"""

assert expected_error in exc_info.value.stack

0 comments on commit 25c084b

Please sign in to comment.