algokit-utils 2.4.0b1__py3-none-any.whl → 3.0.0b2__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} +19 -18
  5. algokit_utils/{_transfer.py → _legacy_v2/_transfer.py} +24 -23
  6. algokit_utils/_legacy_v2/account.py +203 -0
  7. algokit_utils/_legacy_v2/application_client.py +1471 -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 +140 -0
  15. algokit_utils/account.py +12 -183
  16. algokit_utils/accounts/__init__.py +2 -0
  17. algokit_utils/accounts/account_manager.py +909 -0
  18. algokit_utils/accounts/kmd_account_manager.py +159 -0
  19. algokit_utils/algorand.py +265 -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 +276 -0
  24. algokit_utils/applications/app_client.py +2056 -0
  25. algokit_utils/applications/app_deployer.py +600 -0
  26. algokit_utils/applications/app_factory.py +826 -0
  27. algokit_utils/applications/app_manager.py +470 -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 +1023 -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 +320 -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 +656 -0
  42. algokit_utils/clients/dispenser_api_client.py +192 -0
  43. algokit_utils/common.py +8 -26
  44. algokit_utils/config.py +71 -18
  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 +193 -0
  52. algokit_utils/models/amount.py +198 -0
  53. algokit_utils/models/application.py +61 -0
  54. algokit_utils/models/network.py +25 -0
  55. algokit_utils/models/simulate.py +11 -0
  56. algokit_utils/models/state.py +59 -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 +2293 -0
  64. algokit_utils/transactions/transaction_creator.py +156 -0
  65. algokit_utils/transactions/transaction_sender.py +574 -0
  66. {algokit_utils-2.4.0b1.dist-info → algokit_utils-3.0.0b2.dist-info}/METADATA +11 -7
  67. algokit_utils-3.0.0b2.dist-info/RECORD +70 -0
  68. {algokit_utils-2.4.0b1.dist-info → algokit_utils-3.0.0b2.dist-info}/WHEEL +1 -1
  69. algokit_utils-2.4.0b1.dist-info/RECORD +0 -24
  70. {algokit_utils-2.4.0b1.dist-info → algokit_utils-3.0.0b2.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 deprecated 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 `arc32_to_arc52` function from algokit_utils.applications.utils.
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,276 @@
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
+ :ivar result: The ABIResult object containing the method call results
50
+ :ivar raw_value: The raw return value from the method call
51
+ :ivar value: The decoded return value from the method call
52
+ :ivar method: The ABI method definition
53
+ :ivar decode_error: The exception that occurred during decoding, if any
54
+ """
55
+
56
+ raw_value: bytes | None = None
57
+ value: ABIValue | None = None
58
+ method: AlgorandABIMethod | None = None
59
+ decode_error: Exception | None = None
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
+
68
+ @property
69
+ def is_success(self) -> bool:
70
+ """Returns True if the ABI call was successful (no decode error)
71
+
72
+ :return: True if no decode error occurred, False otherwise
73
+ """
74
+ return self.decode_error is None
75
+
76
+ def get_arc56_value(
77
+ self, method: Arc56Method | AlgorandABIMethod, structs: dict[str, list[StructField]]
78
+ ) -> Arc56ReturnValueType:
79
+ """Gets the ARC-56 formatted return value.
80
+
81
+ :param method: The ABI method definition
82
+ :param structs: Dictionary of struct definitions
83
+ :return: The decoded return value in ARC-56 format
84
+ """
85
+ return get_arc56_value(self, method, structs)
86
+
87
+
88
+ def get_arc56_value(
89
+ abi_return: ABIReturn, method: Arc56Method | AlgorandABIMethod, structs: dict[str, list[StructField]]
90
+ ) -> Arc56ReturnValueType:
91
+ """Gets the ARC-56 formatted return value from an ABI return.
92
+
93
+ :param abi_return: The ABI return value to decode
94
+ :param method: The ABI method definition
95
+ :param structs: Dictionary of struct definitions
96
+ :raises ValueError: If there was an error decoding the return value
97
+ :return: The decoded return value in ARC-56 format
98
+ """
99
+ if isinstance(method, AlgorandABIMethod):
100
+ type_str = method.returns.type
101
+ struct = None # AlgorandABIMethod doesn't have struct info
102
+ else:
103
+ type_str = method.returns.type
104
+ struct = method.returns.struct
105
+
106
+ if type_str == "void" or abi_return.value is None:
107
+ return None
108
+
109
+ if abi_return.decode_error:
110
+ raise ValueError(abi_return.decode_error)
111
+
112
+ raw_value = abi_return.raw_value
113
+
114
+ # Handle AVM types
115
+ if type_str == "AVMBytes":
116
+ return raw_value
117
+ if type_str == "AVMString" and raw_value:
118
+ return raw_value.decode("utf-8")
119
+ if type_str == "AVMUint64" and raw_value:
120
+ return ABIType.from_string("uint64").decode(raw_value) # type: ignore[no-any-return]
121
+
122
+ # Handle structs
123
+ if struct and struct in structs:
124
+ return_tuple = abi_return.value
125
+ return Arc56Contract.get_abi_struct_from_abi_tuple(return_tuple, structs[struct], structs)
126
+
127
+ # Return as-is
128
+ return abi_return.value
129
+
130
+
131
+ def get_abi_encoded_value(value: Any, type_str: str, structs: dict[str, list[StructField]]) -> bytes: # noqa: PLR0911, ANN401
132
+ """Encodes a value according to its ABI type.
133
+
134
+ :param value: The value to encode
135
+ :param type_str: The ABI type string
136
+ :param structs: Dictionary of struct definitions
137
+ :raises ValueError: If the value cannot be encoded for the given type
138
+ :return: The ABI encoded bytes
139
+ """
140
+ if isinstance(value, (bytes | bytearray)):
141
+ return value
142
+ if type_str == "AVMUint64":
143
+ return ABIType.from_string("uint64").encode(value)
144
+ if type_str in ("AVMBytes", "AVMString"):
145
+ if isinstance(value, str):
146
+ return value.encode("utf-8")
147
+ if not isinstance(value, (bytes | bytearray)):
148
+ raise ValueError(f"Expected bytes value for {type_str}, but got {type(value)}")
149
+ return value
150
+ if type_str in structs:
151
+ tuple_type = get_abi_tuple_type_from_abi_struct_definition(structs[type_str], structs)
152
+ if isinstance(value, (list | tuple)):
153
+ return tuple_type.encode(value) # type: ignore[arg-type]
154
+ else:
155
+ tuple_values = get_abi_tuple_from_abi_struct(value, structs[type_str], structs)
156
+ return tuple_type.encode(tuple_values)
157
+ else:
158
+ abi_type = ABIType.from_string(type_str)
159
+ return abi_type.encode(value)
160
+
161
+
162
+ def get_abi_decoded_value(
163
+ value: bytes | int | str, type_str: str | ABIArgumentType, structs: dict[str, list[StructField]]
164
+ ) -> ABIValue:
165
+ """Decodes a value according to its ABI type.
166
+
167
+ :param value: The value to decode
168
+ :param type_str: The ABI type string or type object
169
+ :param structs: Dictionary of struct definitions
170
+ :return: The decoded ABI value
171
+ """
172
+ type_value = str(type_str)
173
+
174
+ if type_value == "AVMBytes" or not isinstance(value, bytes):
175
+ return value
176
+ if type_value == "AVMString":
177
+ return value.decode("utf-8")
178
+ if type_value == "AVMUint64":
179
+ return ABIType.from_string("uint64").decode(value) # type: ignore[no-any-return]
180
+ if type_value in structs:
181
+ tuple_type = get_abi_tuple_type_from_abi_struct_definition(structs[type_value], structs)
182
+ decoded_tuple = tuple_type.decode(value)
183
+ return get_abi_struct_from_abi_tuple(decoded_tuple, structs[type_value], structs)
184
+ return ABIType.from_string(type_value).decode(value) # type: ignore[no-any-return]
185
+
186
+
187
+ def get_abi_tuple_from_abi_struct(
188
+ struct_value: dict[str, Any],
189
+ struct_fields: list[StructField],
190
+ structs: dict[str, list[StructField]],
191
+ ) -> list[Any]:
192
+ """Converts an ABI struct to a tuple representation.
193
+
194
+ :param struct_value: The struct value as a dictionary
195
+ :param struct_fields: List of struct field definitions
196
+ :param structs: Dictionary of struct definitions
197
+ :raises ValueError: If a required field is missing from the struct
198
+ :return: The struct as a tuple
199
+ """
200
+ result = []
201
+ for field in struct_fields:
202
+ key = field.name
203
+ if key not in struct_value:
204
+ raise ValueError(f"Missing value for field '{key}'")
205
+ value = struct_value[key]
206
+ field_type = field.type
207
+ if isinstance(field_type, str):
208
+ if field_type in structs:
209
+ value = get_abi_tuple_from_abi_struct(value, structs[field_type], structs)
210
+ elif isinstance(field_type, list):
211
+ value = get_abi_tuple_from_abi_struct(value, field_type, structs)
212
+ result.append(value)
213
+ return result
214
+
215
+
216
+ def get_abi_tuple_type_from_abi_struct_definition(
217
+ struct_def: list[StructField], structs: dict[str, list[StructField]]
218
+ ) -> algosdk.abi.TupleType:
219
+ """Creates a TupleType from a struct definition.
220
+
221
+ :param struct_def: The struct field definitions
222
+ :param structs: Dictionary of struct definitions
223
+ :raises ValueError: If a field type is invalid
224
+ :return: The TupleType representing the struct
225
+ """
226
+ types = []
227
+ for field in struct_def:
228
+ field_type = field.type
229
+ if isinstance(field_type, str):
230
+ if field_type in structs:
231
+ types.append(get_abi_tuple_type_from_abi_struct_definition(structs[field_type], structs))
232
+ else:
233
+ types.append(ABIType.from_string(field_type)) # type: ignore[arg-type]
234
+ elif isinstance(field_type, list):
235
+ types.append(get_abi_tuple_type_from_abi_struct_definition(field_type, structs))
236
+ else:
237
+ raise ValueError(f"Invalid field type: {field_type}")
238
+ return algosdk.abi.TupleType(types)
239
+
240
+
241
+ def get_abi_struct_from_abi_tuple(
242
+ decoded_tuple: Any, # noqa: ANN401
243
+ struct_fields: list[StructField],
244
+ structs: dict[str, list[StructField]],
245
+ ) -> dict[str, Any]:
246
+ """Converts a decoded tuple to an ABI struct.
247
+
248
+ :param decoded_tuple: The tuple to convert
249
+ :param struct_fields: List of struct field definitions
250
+ :param structs: Dictionary of struct definitions
251
+ :return: The tuple as a struct dictionary
252
+ """
253
+ result = {}
254
+ for i, field in enumerate(struct_fields):
255
+ key = field.name
256
+ field_type = field.type
257
+ value = decoded_tuple[i]
258
+ if isinstance(field_type, str):
259
+ if field_type in structs:
260
+ value = get_abi_struct_from_abi_tuple(value, structs[field_type], structs)
261
+ elif isinstance(field_type, list):
262
+ value = get_abi_struct_from_abi_tuple(value, field_type, structs)
263
+ result[key] = value
264
+ return result
265
+
266
+
267
+ @dataclass(kw_only=True, frozen=True)
268
+ class BoxABIValue:
269
+ """Represents an ABI value stored in a box.
270
+
271
+ :ivar name: The name of the box
272
+ :ivar value: The ABI value stored in the box
273
+ """
274
+
275
+ name: BoxName
276
+ value: ABIValue