algokit-utils 2.4.0__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of algokit-utils might be problematic. Click here for more details.

Files changed (70) hide show
  1. algokit_utils/__init__.py +23 -181
  2. algokit_utils/_debugging.py +89 -45
  3. algokit_utils/_legacy_v2/__init__.py +177 -0
  4. algokit_utils/{_ensure_funded.py → _legacy_v2/_ensure_funded.py} +21 -24
  5. algokit_utils/{_transfer.py → _legacy_v2/_transfer.py} +26 -23
  6. algokit_utils/_legacy_v2/account.py +203 -0
  7. algokit_utils/_legacy_v2/application_client.py +1472 -0
  8. algokit_utils/_legacy_v2/application_specification.py +21 -0
  9. algokit_utils/_legacy_v2/asset.py +168 -0
  10. algokit_utils/_legacy_v2/common.py +28 -0
  11. algokit_utils/_legacy_v2/deploy.py +822 -0
  12. algokit_utils/_legacy_v2/logic_error.py +14 -0
  13. algokit_utils/{models.py → _legacy_v2/models.py} +16 -45
  14. algokit_utils/_legacy_v2/network_clients.py +144 -0
  15. algokit_utils/account.py +12 -183
  16. algokit_utils/accounts/__init__.py +2 -0
  17. algokit_utils/accounts/account_manager.py +912 -0
  18. algokit_utils/accounts/kmd_account_manager.py +161 -0
  19. algokit_utils/algorand.py +359 -0
  20. algokit_utils/application_client.py +9 -1447
  21. algokit_utils/application_specification.py +39 -197
  22. algokit_utils/applications/__init__.py +7 -0
  23. algokit_utils/applications/abi.py +275 -0
  24. algokit_utils/applications/app_client.py +2108 -0
  25. algokit_utils/applications/app_deployer.py +725 -0
  26. algokit_utils/applications/app_factory.py +1134 -0
  27. algokit_utils/applications/app_manager.py +578 -0
  28. algokit_utils/applications/app_spec/__init__.py +2 -0
  29. algokit_utils/applications/app_spec/arc32.py +207 -0
  30. algokit_utils/applications/app_spec/arc56.py +989 -0
  31. algokit_utils/applications/enums.py +40 -0
  32. algokit_utils/asset.py +32 -168
  33. algokit_utils/assets/__init__.py +1 -0
  34. algokit_utils/assets/asset_manager.py +336 -0
  35. algokit_utils/beta/_utils.py +36 -0
  36. algokit_utils/beta/account_manager.py +4 -195
  37. algokit_utils/beta/algorand_client.py +4 -314
  38. algokit_utils/beta/client_manager.py +5 -74
  39. algokit_utils/beta/composer.py +5 -712
  40. algokit_utils/clients/__init__.py +2 -0
  41. algokit_utils/clients/client_manager.py +738 -0
  42. algokit_utils/clients/dispenser_api_client.py +224 -0
  43. algokit_utils/common.py +8 -26
  44. algokit_utils/config.py +76 -29
  45. algokit_utils/deploy.py +7 -894
  46. algokit_utils/dispenser_api.py +8 -176
  47. algokit_utils/errors/__init__.py +1 -0
  48. algokit_utils/errors/logic_error.py +121 -0
  49. algokit_utils/logic_error.py +7 -82
  50. algokit_utils/models/__init__.py +8 -0
  51. algokit_utils/models/account.py +217 -0
  52. algokit_utils/models/amount.py +200 -0
  53. algokit_utils/models/application.py +91 -0
  54. algokit_utils/models/network.py +29 -0
  55. algokit_utils/models/simulate.py +11 -0
  56. algokit_utils/models/state.py +68 -0
  57. algokit_utils/models/transaction.py +100 -0
  58. algokit_utils/network_clients.py +7 -128
  59. algokit_utils/protocols/__init__.py +2 -0
  60. algokit_utils/protocols/account.py +22 -0
  61. algokit_utils/protocols/typed_clients.py +108 -0
  62. algokit_utils/transactions/__init__.py +3 -0
  63. algokit_utils/transactions/transaction_composer.py +2499 -0
  64. algokit_utils/transactions/transaction_creator.py +688 -0
  65. algokit_utils/transactions/transaction_sender.py +1219 -0
  66. {algokit_utils-2.4.0.dist-info → algokit_utils-3.0.0.dist-info}/METADATA +11 -7
  67. algokit_utils-3.0.0.dist-info/RECORD +70 -0
  68. {algokit_utils-2.4.0.dist-info → algokit_utils-3.0.0.dist-info}/WHEEL +1 -1
  69. algokit_utils-2.4.0.dist-info/RECORD +0 -24
  70. {algokit_utils-2.4.0.dist-info → algokit_utils-3.0.0.dist-info}/LICENSE +0 -0
@@ -1,206 +1,48 @@
1
- import base64
2
- import dataclasses
3
- import json
4
- from enum import IntFlag
5
- from pathlib import Path
6
- from typing import Any, Literal, TypeAlias, TypedDict
1
+ import warnings
2
+
3
+ from typing_extensions import deprecated
4
+
5
+ warnings.warn(
6
+ """The legacy v2 application_specification module is deprecated and will be removed in a future version.
7
+ Use `from algokit_utils.applications.app_spec.arc32 import ...` to access Arc32 app spec instead.
8
+ By default, the ARC52Contract is a recommended app spec to use, serving as a replacement
9
+ for legacy 'ApplicationSpecification' class.
10
+ To convert legacy app specs to ARC52, use `Arc56Contract.from_arc32`.
11
+ """,
12
+ DeprecationWarning,
13
+ stacklevel=2,
14
+ )
15
+
16
+ from algokit_utils.applications.app_spec.arc32 import ( # noqa: E402 # noqa: E402
17
+ AppSpecStateDict,
18
+ Arc32Contract,
19
+ CallConfig,
20
+ DefaultArgumentDict,
21
+ DefaultArgumentType,
22
+ MethodConfigDict,
23
+ MethodHints,
24
+ OnCompleteActionName,
25
+ )
26
+
27
+
28
+ @deprecated(
29
+ "Use `Arc32Contract` from algokit_utils.applications instead. Example:\n"
30
+ "```python\n"
31
+ "from algokit_utils.applications import Arc32Contract\n"
32
+ "app_spec = Arc32Contract.from_json(app_spec_json)\n"
33
+ "```"
34
+ )
35
+ class ApplicationSpecification(Arc32Contract):
36
+ """Deprecated class for ARC-0032 application specification"""
7
37
 
8
- from algosdk.abi import Contract
9
- from algosdk.abi.method import MethodDict
10
- from algosdk.transaction import StateSchema
11
38
 
12
39
  __all__ = [
40
+ "AppSpecStateDict",
41
+ "ApplicationSpecification",
13
42
  "CallConfig",
14
43
  "DefaultArgumentDict",
15
44
  "DefaultArgumentType",
16
45
  "MethodConfigDict",
17
- "OnCompleteActionName",
18
46
  "MethodHints",
19
- "ApplicationSpecification",
20
- "AppSpecStateDict",
21
- ]
22
-
23
-
24
- AppSpecStateDict: TypeAlias = dict[str, dict[str, dict]]
25
- """Type defining Application Specification state entries"""
26
-
27
-
28
- class CallConfig(IntFlag):
29
- """Describes the type of calls a method can be used for based on {py:class}`algosdk.transaction.OnComplete` type"""
30
-
31
- NEVER = 0
32
- """Never handle the specified on completion type"""
33
- CALL = 1
34
- """Only handle the specified on completion type for application calls"""
35
- CREATE = 2
36
- """Only handle the specified on completion type for application create calls"""
37
- ALL = 3
38
- """Handle the specified on completion type for both create and normal application calls"""
39
-
40
-
41
- class StructArgDict(TypedDict):
42
- name: str
43
- elements: list[list[str]]
44
-
45
-
46
- OnCompleteActionName: TypeAlias = Literal[
47
- "no_op", "opt_in", "close_out", "clear_state", "update_application", "delete_application"
47
+ "OnCompleteActionName",
48
48
  ]
49
- """String literals representing on completion transaction types"""
50
- MethodConfigDict: TypeAlias = dict[OnCompleteActionName, CallConfig]
51
- """Dictionary of `dict[OnCompletionActionName, CallConfig]` representing allowed actions for each on completion type"""
52
- DefaultArgumentType: TypeAlias = Literal["abi-method", "local-state", "global-state", "constant"]
53
- """Literal values describing the types of default argument sources"""
54
-
55
-
56
- class DefaultArgumentDict(TypedDict):
57
- """
58
- DefaultArgument is a container for any arguments that may
59
- be resolved prior to calling some target method
60
- """
61
-
62
- source: DefaultArgumentType
63
- data: int | str | bytes | MethodDict
64
-
65
-
66
- StateDict = TypedDict( # need to use function-form of TypedDict here since "global" is a reserved keyword
67
- "StateDict", {"global": AppSpecStateDict, "local": AppSpecStateDict}
68
- )
69
-
70
-
71
- @dataclasses.dataclass(kw_only=True)
72
- class MethodHints:
73
- """MethodHints provides hints to the caller about how to call the method"""
74
-
75
- #: hint to indicate this method can be called through Dryrun
76
- read_only: bool = False
77
- #: hint to provide names for tuple argument indices
78
- #: method_name=>param_name=>{name:str, elements:[str,str]}
79
- structs: dict[str, StructArgDict] = dataclasses.field(default_factory=dict)
80
- #: defaults
81
- default_arguments: dict[str, DefaultArgumentDict] = dataclasses.field(default_factory=dict)
82
- call_config: MethodConfigDict = dataclasses.field(default_factory=dict)
83
-
84
- def empty(self) -> bool:
85
- return not self.dictify()
86
-
87
- def dictify(self) -> dict[str, Any]:
88
- d: dict[str, Any] = {}
89
- if self.read_only:
90
- d["read_only"] = True
91
- if self.default_arguments:
92
- d["default_arguments"] = self.default_arguments
93
- if self.structs:
94
- d["structs"] = self.structs
95
- if any(v for v in self.call_config.values() if v != CallConfig.NEVER):
96
- d["call_config"] = _encode_method_config(self.call_config)
97
- return d
98
-
99
- @staticmethod
100
- def undictify(data: dict[str, Any]) -> "MethodHints":
101
- return MethodHints(
102
- read_only=data.get("read_only", False),
103
- default_arguments=data.get("default_arguments", {}),
104
- structs=data.get("structs", {}),
105
- call_config=_decode_method_config(data.get("call_config", {})),
106
- )
107
-
108
-
109
- def _encode_method_config(mc: MethodConfigDict) -> dict[str, str | None]:
110
- return {k: mc[k].name for k in sorted(mc) if mc[k] != CallConfig.NEVER}
111
-
112
-
113
- def _decode_method_config(data: dict[OnCompleteActionName, Any]) -> MethodConfigDict:
114
- return {k: CallConfig[v] for k, v in data.items()}
115
-
116
-
117
- def _encode_source(teal_text: str) -> str:
118
- return base64.b64encode(teal_text.encode()).decode("utf-8")
119
-
120
-
121
- def _decode_source(b64_text: str) -> str:
122
- return base64.b64decode(b64_text).decode("utf-8")
123
-
124
-
125
- def _encode_state_schema(schema: StateSchema) -> dict[str, int]:
126
- return {
127
- "num_byte_slices": schema.num_byte_slices,
128
- "num_uints": schema.num_uints,
129
- }
130
-
131
-
132
- def _decode_state_schema(data: dict[str, int]) -> StateSchema:
133
- return StateSchema( # type: ignore[no-untyped-call]
134
- num_byte_slices=data.get("num_byte_slices", 0),
135
- num_uints=data.get("num_uints", 0),
136
- )
137
-
138
-
139
- @dataclasses.dataclass(kw_only=True)
140
- class ApplicationSpecification:
141
- """ARC-0032 application specification
142
-
143
- See <https://github.com/algorandfoundation/ARCs/pull/150>"""
144
-
145
- approval_program: str
146
- clear_program: str
147
- contract: Contract
148
- hints: dict[str, MethodHints]
149
- schema: StateDict
150
- global_state_schema: StateSchema
151
- local_state_schema: StateSchema
152
- bare_call_config: MethodConfigDict
153
-
154
- def dictify(self) -> dict:
155
- return {
156
- "hints": {k: v.dictify() for k, v in self.hints.items() if not v.empty()},
157
- "source": {
158
- "approval": _encode_source(self.approval_program),
159
- "clear": _encode_source(self.clear_program),
160
- },
161
- "state": {
162
- "global": _encode_state_schema(self.global_state_schema),
163
- "local": _encode_state_schema(self.local_state_schema),
164
- },
165
- "schema": self.schema,
166
- "contract": self.contract.dictify(),
167
- "bare_call_config": _encode_method_config(self.bare_call_config),
168
- }
169
-
170
- def to_json(self) -> str:
171
- return json.dumps(self.dictify(), indent=4)
172
-
173
- @staticmethod
174
- def from_json(application_spec: str) -> "ApplicationSpecification":
175
- json_spec = json.loads(application_spec)
176
- return ApplicationSpecification(
177
- approval_program=_decode_source(json_spec["source"]["approval"]),
178
- clear_program=_decode_source(json_spec["source"]["clear"]),
179
- schema=json_spec["schema"],
180
- global_state_schema=_decode_state_schema(json_spec["state"]["global"]),
181
- local_state_schema=_decode_state_schema(json_spec["state"]["local"]),
182
- contract=Contract.undictify(json_spec["contract"]),
183
- hints={k: MethodHints.undictify(v) for k, v in json_spec["hints"].items()},
184
- bare_call_config=_decode_method_config(json_spec.get("bare_call_config", {})),
185
- )
186
-
187
- def export(self, directory: Path | str | None = None) -> None:
188
- """write out the artifacts generated by the application to disk
189
-
190
- Args:
191
- directory(optional): path to the directory where the artifacts should be written
192
- """
193
- if directory is None:
194
- output_dir = Path.cwd()
195
- else:
196
- output_dir = Path(directory)
197
- output_dir.mkdir(exist_ok=True, parents=True)
198
-
199
- (output_dir / "approval.teal").write_text(self.approval_program)
200
- (output_dir / "clear.teal").write_text(self.clear_program)
201
- (output_dir / "contract.json").write_text(json.dumps(self.contract.dictify(), indent=4))
202
- (output_dir / "application.json").write_text(self.to_json())
203
-
204
-
205
- def _state_schema(schema: dict[str, int]) -> StateSchema:
206
- return StateSchema(schema.get("num-uint", 0), schema.get("num-byte-slice", 0)) # type: ignore[no-untyped-call]
@@ -0,0 +1,7 @@
1
+ from algokit_utils.applications.abi import * # noqa: F403
2
+ from algokit_utils.applications.app_client import * # noqa: F403
3
+ from algokit_utils.applications.app_deployer import * # noqa: F403
4
+ from algokit_utils.applications.app_factory import * # noqa: F403
5
+ from algokit_utils.applications.app_manager import * # noqa: F403
6
+ from algokit_utils.applications.app_spec import * # noqa: F403
7
+ from algokit_utils.applications.enums import * # noqa: F403
@@ -0,0 +1,275 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Any, TypeAlias
5
+
6
+ import algosdk
7
+ from algosdk.abi.method import Method as AlgorandABIMethod
8
+ from algosdk.atomic_transaction_composer import ABIResult
9
+
10
+ from algokit_utils.applications.app_spec.arc56 import Arc56Contract, StructField
11
+ from algokit_utils.applications.app_spec.arc56 import Method as Arc56Method
12
+
13
+ if TYPE_CHECKING:
14
+ from algokit_utils.models.state import BoxName
15
+
16
+ ABIValue: TypeAlias = (
17
+ bool | int | str | bytes | bytearray | list["ABIValue"] | tuple["ABIValue"] | dict[str, "ABIValue"]
18
+ )
19
+ ABIStruct: TypeAlias = dict[str, list[dict[str, "ABIValue"]]]
20
+ Arc56ReturnValueType: TypeAlias = ABIValue | ABIStruct | None
21
+
22
+
23
+ ABIType: TypeAlias = algosdk.abi.ABIType
24
+ ABIArgumentType: TypeAlias = algosdk.abi.ABIType | algosdk.abi.ABITransactionType | algosdk.abi.ABIReferenceType
25
+
26
+ __all__ = [
27
+ "ABIArgumentType",
28
+ "ABIReturn",
29
+ "ABIStruct",
30
+ "ABIType",
31
+ "ABIValue",
32
+ "Arc56ReturnValueType",
33
+ "BoxABIValue",
34
+ "get_abi_decoded_value",
35
+ "get_abi_encoded_value",
36
+ "get_abi_struct_from_abi_tuple",
37
+ "get_abi_tuple_from_abi_struct",
38
+ "get_abi_tuple_type_from_abi_struct_definition",
39
+ "get_arc56_value",
40
+ ]
41
+
42
+
43
+ @dataclass(kw_only=True)
44
+ class ABIReturn:
45
+ """Represents the return value from an ABI method call.
46
+
47
+ Wraps the raw return value and decoded value along with any decode errors.
48
+ """
49
+
50
+ raw_value: bytes | None = None
51
+ """The raw return value from the method call"""
52
+ value: ABIValue | None = None
53
+ """The decoded return value from the method call"""
54
+ method: AlgorandABIMethod | None = None
55
+ """The ABI method definition"""
56
+ decode_error: Exception | None = None
57
+ """The exception that occurred during decoding, if any"""
58
+ tx_info: dict[str, Any] | None = None
59
+ """The transaction info for the method call from raw algosdk `ABIResult`"""
60
+
61
+ def __init__(self, result: ABIResult) -> None:
62
+ self.decode_error = result.decode_error
63
+ if not self.decode_error:
64
+ self.raw_value = result.raw_value
65
+ self.value = result.return_value
66
+ self.method = result.method
67
+ self.tx_info = result.tx_info
68
+
69
+ @property
70
+ def is_success(self) -> bool:
71
+ """Returns True if the ABI call was successful (no decode error)
72
+
73
+ :return: True if no decode error occurred, False otherwise
74
+ """
75
+ return self.decode_error is None
76
+
77
+ def get_arc56_value(
78
+ self, method: Arc56Method | AlgorandABIMethod, structs: dict[str, list[StructField]]
79
+ ) -> Arc56ReturnValueType:
80
+ """Gets the ARC-56 formatted return value.
81
+
82
+ :param method: The ABI method definition
83
+ :param structs: Dictionary of struct definitions
84
+ :return: The decoded return value in ARC-56 format
85
+ """
86
+ return get_arc56_value(self, method, structs)
87
+
88
+
89
+ def get_arc56_value(
90
+ abi_return: ABIReturn, method: Arc56Method | AlgorandABIMethod, structs: dict[str, list[StructField]]
91
+ ) -> Arc56ReturnValueType:
92
+ """Gets the ARC-56 formatted return value from an ABI return.
93
+
94
+ :param abi_return: The ABI return value to decode
95
+ :param method: The ABI method definition
96
+ :param structs: Dictionary of struct definitions
97
+ :raises ValueError: If there was an error decoding the return value
98
+ :return: The decoded return value in ARC-56 format
99
+ """
100
+ if isinstance(method, AlgorandABIMethod):
101
+ type_str = method.returns.type
102
+ struct = None # AlgorandABIMethod doesn't have struct info
103
+ else:
104
+ type_str = method.returns.type
105
+ struct = method.returns.struct
106
+
107
+ if type_str == "void" or abi_return.value is None:
108
+ return None
109
+
110
+ if abi_return.decode_error:
111
+ raise ValueError(abi_return.decode_error)
112
+
113
+ raw_value = abi_return.raw_value
114
+
115
+ # Handle AVM types
116
+ if type_str == "AVMBytes":
117
+ return raw_value
118
+ if type_str == "AVMString" and raw_value:
119
+ return raw_value.decode("utf-8")
120
+ if type_str == "AVMUint64" and raw_value:
121
+ return ABIType.from_string("uint64").decode(raw_value) # type: ignore[no-any-return]
122
+
123
+ # Handle structs
124
+ if struct and struct in structs:
125
+ return_tuple = abi_return.value
126
+ return Arc56Contract.get_abi_struct_from_abi_tuple(return_tuple, structs[struct], structs)
127
+
128
+ # Return as-is
129
+ return abi_return.value
130
+
131
+
132
+ def get_abi_encoded_value(value: Any, type_str: str, structs: dict[str, list[StructField]]) -> bytes: # noqa: PLR0911, ANN401
133
+ """Encodes a value according to its ABI type.
134
+
135
+ :param value: The value to encode
136
+ :param type_str: The ABI type string
137
+ :param structs: Dictionary of struct definitions
138
+ :raises ValueError: If the value cannot be encoded for the given type
139
+ :return: The ABI encoded bytes
140
+ """
141
+ if isinstance(value, (bytes | bytearray)):
142
+ return value
143
+ if type_str == "AVMUint64":
144
+ return ABIType.from_string("uint64").encode(value)
145
+ if type_str in ("AVMBytes", "AVMString"):
146
+ if isinstance(value, str):
147
+ return value.encode("utf-8")
148
+ if not isinstance(value, (bytes | bytearray)):
149
+ raise ValueError(f"Expected bytes value for {type_str}, but got {type(value)}")
150
+ return value
151
+ if type_str in structs:
152
+ tuple_type = get_abi_tuple_type_from_abi_struct_definition(structs[type_str], structs)
153
+ if isinstance(value, (list | tuple)):
154
+ return tuple_type.encode(value) # type: ignore[arg-type]
155
+ else:
156
+ tuple_values = get_abi_tuple_from_abi_struct(value, structs[type_str], structs)
157
+ return tuple_type.encode(tuple_values)
158
+ else:
159
+ abi_type = ABIType.from_string(type_str)
160
+ return abi_type.encode(value)
161
+
162
+
163
+ def get_abi_decoded_value(
164
+ value: bytes | int | str, type_str: str | ABIArgumentType, structs: dict[str, list[StructField]]
165
+ ) -> ABIValue:
166
+ """Decodes a value according to its ABI type.
167
+
168
+ :param value: The value to decode
169
+ :param type_str: The ABI type string or type object
170
+ :param structs: Dictionary of struct definitions
171
+ :return: The decoded ABI value
172
+ """
173
+ type_value = str(type_str)
174
+
175
+ if type_value == "AVMBytes" or not isinstance(value, bytes):
176
+ return value
177
+ if type_value == "AVMString":
178
+ return value.decode("utf-8")
179
+ if type_value == "AVMUint64":
180
+ return ABIType.from_string("uint64").decode(value) # type: ignore[no-any-return]
181
+ if type_value in structs:
182
+ tuple_type = get_abi_tuple_type_from_abi_struct_definition(structs[type_value], structs)
183
+ decoded_tuple = tuple_type.decode(value)
184
+ return get_abi_struct_from_abi_tuple(decoded_tuple, structs[type_value], structs)
185
+ return ABIType.from_string(type_value).decode(value) # type: ignore[no-any-return]
186
+
187
+
188
+ def get_abi_tuple_from_abi_struct(
189
+ struct_value: dict[str, Any],
190
+ struct_fields: list[StructField],
191
+ structs: dict[str, list[StructField]],
192
+ ) -> list[Any]:
193
+ """Converts an ABI struct to a tuple representation.
194
+
195
+ :param struct_value: The struct value as a dictionary
196
+ :param struct_fields: List of struct field definitions
197
+ :param structs: Dictionary of struct definitions
198
+ :raises ValueError: If a required field is missing from the struct
199
+ :return: The struct as a tuple
200
+ """
201
+ result = []
202
+ for field in struct_fields:
203
+ key = field.name
204
+ if key not in struct_value:
205
+ raise ValueError(f"Missing value for field '{key}'")
206
+ value = struct_value[key]
207
+ field_type = field.type
208
+ if isinstance(field_type, str):
209
+ if field_type in structs:
210
+ value = get_abi_tuple_from_abi_struct(value, structs[field_type], structs)
211
+ elif isinstance(field_type, list):
212
+ value = get_abi_tuple_from_abi_struct(value, field_type, structs)
213
+ result.append(value)
214
+ return result
215
+
216
+
217
+ def get_abi_tuple_type_from_abi_struct_definition(
218
+ struct_def: list[StructField], structs: dict[str, list[StructField]]
219
+ ) -> algosdk.abi.TupleType:
220
+ """Creates a TupleType from a struct definition.
221
+
222
+ :param struct_def: The struct field definitions
223
+ :param structs: Dictionary of struct definitions
224
+ :raises ValueError: If a field type is invalid
225
+ :return: The TupleType representing the struct
226
+ """
227
+ types = []
228
+ for field in struct_def:
229
+ field_type = field.type
230
+ if isinstance(field_type, str):
231
+ if field_type in structs:
232
+ types.append(get_abi_tuple_type_from_abi_struct_definition(structs[field_type], structs))
233
+ else:
234
+ types.append(ABIType.from_string(field_type)) # type: ignore[arg-type]
235
+ elif isinstance(field_type, list):
236
+ types.append(get_abi_tuple_type_from_abi_struct_definition(field_type, structs))
237
+ else:
238
+ raise ValueError(f"Invalid field type: {field_type}")
239
+ return algosdk.abi.TupleType(types)
240
+
241
+
242
+ def get_abi_struct_from_abi_tuple(
243
+ decoded_tuple: Any, # noqa: ANN401
244
+ struct_fields: list[StructField],
245
+ structs: dict[str, list[StructField]],
246
+ ) -> dict[str, Any]:
247
+ """Converts a decoded tuple to an ABI struct.
248
+
249
+ :param decoded_tuple: The tuple to convert
250
+ :param struct_fields: List of struct field definitions
251
+ :param structs: Dictionary of struct definitions
252
+ :return: The tuple as a struct dictionary
253
+ """
254
+ result = {}
255
+ for i, field in enumerate(struct_fields):
256
+ key = field.name
257
+ field_type = field.type
258
+ value = decoded_tuple[i]
259
+ if isinstance(field_type, str):
260
+ if field_type in structs:
261
+ value = get_abi_struct_from_abi_tuple(value, structs[field_type], structs)
262
+ elif isinstance(field_type, list):
263
+ value = get_abi_struct_from_abi_tuple(value, field_type, structs)
264
+ result[key] = value
265
+ return result
266
+
267
+
268
+ @dataclass(kw_only=True, frozen=True)
269
+ class BoxABIValue:
270
+ """Represents an ABI value stored in a box."""
271
+
272
+ name: BoxName
273
+ """The name of the box"""
274
+ value: ABIValue
275
+ """The ABI value stored in the box"""