algokit-utils 3.0.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 -183
  2. algokit_utils/_debugging.py +123 -97
  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} +19 -142
  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 -1453
  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 -892
  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 -80
  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 -152
  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-3.0.0b1.dist-info → algokit_utils-3.0.0b2.dist-info}/METADATA +12 -7
  67. algokit_utils-3.0.0b2.dist-info/RECORD +70 -0
  68. {algokit_utils-3.0.0b1.dist-info → algokit_utils-3.0.0b2.dist-info}/WHEEL +1 -1
  69. algokit_utils-3.0.0b1.dist-info/RECORD +0 -24
  70. {algokit_utils-3.0.0b1.dist-info → algokit_utils-3.0.0b2.dist-info}/LICENSE +0 -0
@@ -0,0 +1,1023 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import json
5
+ from base64 import b64encode
6
+ from collections.abc import Callable, Sequence
7
+ from dataclasses import asdict, dataclass
8
+ from enum import Enum
9
+ from typing import Any, Literal, overload
10
+
11
+ import algosdk
12
+ from algosdk.abi import Method as AlgosdkMethod
13
+
14
+ from algokit_utils.applications.app_spec.arc32 import Arc32Contract
15
+
16
+ __all__ = [
17
+ "Actions",
18
+ "Arc56Contract",
19
+ "BareActions",
20
+ "Boxes",
21
+ "ByteCode",
22
+ "CallEnum",
23
+ "Compiler",
24
+ "CompilerInfo",
25
+ "CompilerVersion",
26
+ "CreateEnum",
27
+ "DefaultValue",
28
+ "Event",
29
+ "EventArg",
30
+ "Global",
31
+ "Keys",
32
+ "Local",
33
+ "Maps",
34
+ "Method",
35
+ "MethodArg",
36
+ "Network",
37
+ "PcOffsetMethod",
38
+ "ProgramSourceInfo",
39
+ "Recommendations",
40
+ "Returns",
41
+ "Schema",
42
+ "ScratchVariables",
43
+ "Source",
44
+ "SourceInfo",
45
+ "SourceInfoModel",
46
+ "State",
47
+ "StorageKey",
48
+ "StorageMap",
49
+ "StructField",
50
+ "TemplateVariables",
51
+ ]
52
+
53
+
54
+ class _ActionType(str, Enum):
55
+ CALL = "CALL"
56
+ CREATE = "CREATE"
57
+
58
+
59
+ @dataclass
60
+ class StructField:
61
+ """Represents a field in a struct type.
62
+
63
+ :ivar name: Name of the struct field
64
+ :ivar type: Type of the struct field, either a string or list of StructFields
65
+ """
66
+
67
+ name: str
68
+ type: list[StructField] | str
69
+
70
+ @staticmethod
71
+ def from_dict(data: dict[str, Any]) -> StructField:
72
+ if isinstance(data["type"], list):
73
+ data["type"] = [StructField.from_dict(item) for item in data["type"]]
74
+ return StructField(**data)
75
+
76
+
77
+ class CallEnum(str, Enum):
78
+ """Enum representing different call types for application transactions."""
79
+
80
+ CLEAR_STATE = "ClearState"
81
+ CLOSE_OUT = "CloseOut"
82
+ DELETE_APPLICATION = "DeleteApplication"
83
+ NO_OP = "NoOp"
84
+ OPT_IN = "OptIn"
85
+ UPDATE_APPLICATION = "UpdateApplication"
86
+
87
+
88
+ class CreateEnum(str, Enum):
89
+ """Enum representing different create types for application transactions."""
90
+
91
+ DELETE_APPLICATION = "DeleteApplication"
92
+ NO_OP = "NoOp"
93
+ OPT_IN = "OptIn"
94
+
95
+
96
+ @dataclass
97
+ class BareActions:
98
+ """Represents bare call and create actions for an application.
99
+
100
+ :ivar call: List of allowed call actions
101
+ :ivar create: List of allowed create actions
102
+ """
103
+
104
+ call: list[CallEnum]
105
+ create: list[CreateEnum]
106
+
107
+ @staticmethod
108
+ def from_dict(data: dict[str, Any]) -> BareActions:
109
+ return BareActions(**data)
110
+
111
+
112
+ @dataclass
113
+ class ByteCode:
114
+ """Represents the approval and clear program bytecode.
115
+
116
+ :ivar approval: Base64 encoded approval program bytecode
117
+ :ivar clear: Base64 encoded clear program bytecode
118
+ """
119
+
120
+ approval: str
121
+ clear: str
122
+
123
+ @staticmethod
124
+ def from_dict(data: dict[str, Any]) -> ByteCode:
125
+ return ByteCode(**data)
126
+
127
+
128
+ class Compiler(str, Enum):
129
+ """Enum representing different compiler types."""
130
+
131
+ ALGOD = "algod"
132
+ PUYA = "puya"
133
+
134
+
135
+ @dataclass
136
+ class CompilerVersion:
137
+ """Represents compiler version information.
138
+
139
+ :ivar commit_hash: Git commit hash of the compiler
140
+ :ivar major: Major version number
141
+ :ivar minor: Minor version number
142
+ :ivar patch: Patch version number
143
+ """
144
+
145
+ commit_hash: str | None = None
146
+ major: int | None = None
147
+ minor: int | None = None
148
+ patch: int | None = None
149
+
150
+ @staticmethod
151
+ def from_dict(data: dict[str, Any]) -> CompilerVersion:
152
+ return CompilerVersion(**data)
153
+
154
+
155
+ @dataclass
156
+ class CompilerInfo:
157
+ """Information about the compiler used.
158
+
159
+ :ivar compiler: Type of compiler used
160
+ :ivar compiler_version: Version information for the compiler
161
+ """
162
+
163
+ compiler: Compiler
164
+ compiler_version: CompilerVersion
165
+
166
+ @staticmethod
167
+ def from_dict(data: dict[str, Any]) -> CompilerInfo:
168
+ data["compiler_version"] = CompilerVersion.from_dict(data["compiler_version"])
169
+ return CompilerInfo(**data)
170
+
171
+
172
+ @dataclass
173
+ class Network:
174
+ """Network-specific application information.
175
+
176
+ :ivar app_id: Application ID on the network
177
+ """
178
+
179
+ app_id: int
180
+
181
+ @staticmethod
182
+ def from_dict(data: dict[str, Any]) -> Network:
183
+ return Network(**data)
184
+
185
+
186
+ @dataclass
187
+ class ScratchVariables:
188
+ """Information about scratch space variables.
189
+
190
+ :ivar slot: Scratch slot number
191
+ :ivar type: Type of the scratch variable
192
+ """
193
+
194
+ slot: int
195
+ type: str
196
+
197
+ @staticmethod
198
+ def from_dict(data: dict[str, Any]) -> ScratchVariables:
199
+ return ScratchVariables(**data)
200
+
201
+
202
+ @dataclass
203
+ class Source:
204
+ """Source code for approval and clear programs.
205
+
206
+ :ivar approval: Base64 encoded approval program source
207
+ :ivar clear: Base64 encoded clear program source
208
+ """
209
+
210
+ approval: str
211
+ clear: str
212
+
213
+ @staticmethod
214
+ def from_dict(data: dict[str, Any]) -> Source:
215
+ return Source(**data)
216
+
217
+ def get_decoded_approval(self) -> str:
218
+ """Get decoded approval program source.
219
+
220
+ :return: Decoded approval program source code
221
+ """
222
+ return self._decode_source(self.approval)
223
+
224
+ def get_decoded_clear(self) -> str:
225
+ """Get decoded clear program source.
226
+
227
+ :return: Decoded clear program source code
228
+ """
229
+ return self._decode_source(self.clear)
230
+
231
+ def _decode_source(self, b64_text: str) -> str:
232
+ return base64.b64decode(b64_text).decode("utf-8")
233
+
234
+
235
+ @dataclass
236
+ class Global:
237
+ """Global state schema.
238
+
239
+ :ivar bytes: Number of byte slices in global state
240
+ :ivar ints: Number of integers in global state
241
+ """
242
+
243
+ bytes: int
244
+ ints: int
245
+
246
+ @staticmethod
247
+ def from_dict(data: dict[str, Any]) -> Global:
248
+ return Global(**data)
249
+
250
+
251
+ @dataclass
252
+ class Local:
253
+ """Local state schema.
254
+
255
+ :ivar bytes: Number of byte slices in local state
256
+ :ivar ints: Number of integers in local state
257
+ """
258
+
259
+ bytes: int
260
+ ints: int
261
+
262
+ @staticmethod
263
+ def from_dict(data: dict[str, Any]) -> Local:
264
+ return Local(**data)
265
+
266
+
267
+ @dataclass
268
+ class Schema:
269
+ """Application state schema.
270
+
271
+ :ivar global_state: Global state schema
272
+ :ivar local_state: Local state schema
273
+ """
274
+
275
+ global_state: Global # actual schema field is "global" since it's a reserved word
276
+ local_state: Local # actual schema field is "local" for consistency with renamed "global"
277
+
278
+ @staticmethod
279
+ def from_dict(data: dict[str, Any]) -> Schema:
280
+ global_state = Global.from_dict(data["global"])
281
+ local_state = Local.from_dict(data["local"])
282
+ return Schema(global_state=global_state, local_state=local_state)
283
+
284
+
285
+ @dataclass
286
+ class TemplateVariables:
287
+ """Template variable information.
288
+
289
+ :ivar type: Type of the template variable
290
+ :ivar value: Optional value of the template variable
291
+ """
292
+
293
+ type: str
294
+ value: str | None = None
295
+
296
+ @staticmethod
297
+ def from_dict(data: dict[str, Any]) -> TemplateVariables:
298
+ return TemplateVariables(**data)
299
+
300
+
301
+ @dataclass
302
+ class EventArg:
303
+ """Event argument information.
304
+
305
+ :ivar type: Type of the event argument
306
+ :ivar desc: Optional description of the argument
307
+ :ivar name: Optional name of the argument
308
+ :ivar struct: Optional struct type name
309
+ """
310
+
311
+ type: str
312
+ desc: str | None = None
313
+ name: str | None = None
314
+ struct: str | None = None
315
+
316
+ @staticmethod
317
+ def from_dict(data: dict[str, Any]) -> EventArg:
318
+ return EventArg(**data)
319
+
320
+
321
+ @dataclass
322
+ class Event:
323
+ """Event information.
324
+
325
+ :ivar args: List of event arguments
326
+ :ivar name: Name of the event
327
+ :ivar desc: Optional description of the event
328
+ """
329
+
330
+ args: list[EventArg]
331
+ name: str
332
+ desc: str | None = None
333
+
334
+ @staticmethod
335
+ def from_dict(data: dict[str, Any]) -> Event:
336
+ data["args"] = [EventArg.from_dict(item) for item in data["args"]]
337
+ return Event(**data)
338
+
339
+
340
+ @dataclass
341
+ class Actions:
342
+ """Method actions information.
343
+
344
+ :ivar call: Optional list of allowed call actions
345
+ :ivar create: Optional list of allowed create actions
346
+ """
347
+
348
+ call: list[CallEnum] | None = None
349
+ create: list[CreateEnum] | None = None
350
+
351
+ @staticmethod
352
+ def from_dict(data: dict[str, Any]) -> Actions:
353
+ return Actions(**data)
354
+
355
+
356
+ @dataclass
357
+ class DefaultValue:
358
+ """Default value information for method arguments.
359
+
360
+ :ivar data: Default value data
361
+ :ivar source: Source of the default value
362
+ :ivar type: Optional type of the default value
363
+ """
364
+
365
+ data: str
366
+ source: Literal["box", "global", "local", "literal", "method"]
367
+ type: str | None = None
368
+
369
+ @staticmethod
370
+ def from_dict(data: dict[str, Any]) -> DefaultValue:
371
+ return DefaultValue(**data)
372
+
373
+
374
+ @dataclass
375
+ class MethodArg:
376
+ """Method argument information.
377
+
378
+ :ivar type: Type of the argument
379
+ :ivar default_value: Optional default value
380
+ :ivar desc: Optional description
381
+ :ivar name: Optional name
382
+ :ivar struct: Optional struct type name
383
+ """
384
+
385
+ type: str
386
+ default_value: DefaultValue | None = None
387
+ desc: str | None = None
388
+ name: str | None = None
389
+ struct: str | None = None
390
+
391
+ @staticmethod
392
+ def from_dict(data: dict[str, Any]) -> MethodArg:
393
+ if data.get("default_value"):
394
+ data["default_value"] = DefaultValue.from_dict(data["default_value"])
395
+ return MethodArg(**data)
396
+
397
+
398
+ @dataclass
399
+ class Boxes:
400
+ """Box storage requirements.
401
+
402
+ :ivar key: Box key
403
+ :ivar read_bytes: Number of bytes to read
404
+ :ivar write_bytes: Number of bytes to write
405
+ :ivar app: Optional application ID
406
+ """
407
+
408
+ key: str
409
+ read_bytes: int
410
+ write_bytes: int
411
+ app: int | None = None
412
+
413
+ @staticmethod
414
+ def from_dict(data: dict[str, Any]) -> Boxes:
415
+ return Boxes(**data)
416
+
417
+
418
+ @dataclass
419
+ class Recommendations:
420
+ """Method execution recommendations.
421
+
422
+ :ivar accounts: Optional list of accounts
423
+ :ivar apps: Optional list of applications
424
+ :ivar assets: Optional list of assets
425
+ :ivar boxes: Optional box storage requirements
426
+ :ivar inner_transaction_count: Optional inner transaction count
427
+ """
428
+
429
+ accounts: list[str] | None = None
430
+ apps: list[int] | None = None
431
+ assets: list[int] | None = None
432
+ boxes: Boxes | None = None
433
+ inner_transaction_count: int | None = None
434
+
435
+ @staticmethod
436
+ def from_dict(data: dict[str, Any]) -> Recommendations:
437
+ if data.get("boxes"):
438
+ data["boxes"] = Boxes.from_dict(data["boxes"])
439
+ return Recommendations(**data)
440
+
441
+
442
+ @dataclass
443
+ class Returns:
444
+ """Method return information.
445
+
446
+ :ivar type: Return type
447
+ :ivar desc: Optional description
448
+ :ivar struct: Optional struct type name
449
+ """
450
+
451
+ type: str
452
+ desc: str | None = None
453
+ struct: str | None = None
454
+
455
+ @staticmethod
456
+ def from_dict(data: dict[str, Any]) -> Returns:
457
+ return Returns(**data)
458
+
459
+
460
+ @dataclass
461
+ class Method:
462
+ """Method information.
463
+
464
+ :ivar actions: Allowed actions
465
+ :ivar args: Method arguments
466
+ :ivar name: Method name
467
+ :ivar returns: Return information
468
+ :ivar desc: Optional description
469
+ :ivar events: Optional list of events
470
+ :ivar readonly: Optional readonly flag
471
+ :ivar recommendations: Optional execution recommendations
472
+ """
473
+
474
+ actions: Actions
475
+ args: list[MethodArg]
476
+ name: str
477
+ returns: Returns
478
+ desc: str | None = None
479
+ events: list[Event] | None = None
480
+ readonly: bool | None = None
481
+ recommendations: Recommendations | None = None
482
+
483
+ _abi_method: AlgosdkMethod | None = None
484
+
485
+ def __post_init__(self) -> None:
486
+ self._abi_method = AlgosdkMethod.undictify(asdict(self))
487
+
488
+ def to_abi_method(self) -> AlgosdkMethod:
489
+ """Convert to ABI method.
490
+
491
+ :raises ValueError: If underlying ABI method is not initialized
492
+ :return: ABI method
493
+ """
494
+ if self._abi_method is None:
495
+ raise ValueError("Underlying core ABI method class is not initialized!")
496
+ return self._abi_method
497
+
498
+ @staticmethod
499
+ def from_dict(data: dict[str, Any]) -> Method:
500
+ data["actions"] = Actions.from_dict(data["actions"])
501
+ data["args"] = [MethodArg.from_dict(item) for item in data["args"]]
502
+ data["returns"] = Returns.from_dict(data["returns"])
503
+ if data.get("events"):
504
+ data["events"] = [Event.from_dict(item) for item in data["events"]]
505
+ if data.get("recommendations"):
506
+ data["recommendations"] = Recommendations.from_dict(data["recommendations"])
507
+ return Method(**data)
508
+
509
+
510
+ class PcOffsetMethod(str, Enum):
511
+ """PC offset method types."""
512
+
513
+ CBLOCKS = "cblocks"
514
+ NONE = "none"
515
+
516
+
517
+ @dataclass
518
+ class SourceInfo:
519
+ """Source code location information.
520
+
521
+ :ivar pc: List of program counter values
522
+ :ivar error_message: Optional error message
523
+ :ivar source: Optional source code
524
+ :ivar teal: Optional TEAL version
525
+ """
526
+
527
+ pc: list[int]
528
+ error_message: str | None = None
529
+ source: str | None = None
530
+ teal: int | None = None
531
+
532
+ @staticmethod
533
+ def from_dict(data: dict[str, Any]) -> SourceInfo:
534
+ return SourceInfo(**data)
535
+
536
+
537
+ @dataclass
538
+ class StorageKey:
539
+ """Storage key information.
540
+
541
+ :ivar key: Storage key
542
+ :ivar key_type: Type of the key
543
+ :ivar value_type: Type of the value
544
+ :ivar desc: Optional description
545
+ """
546
+
547
+ key: str
548
+ key_type: str
549
+ value_type: str
550
+ desc: str | None = None
551
+
552
+ @staticmethod
553
+ def from_dict(data: dict[str, Any]) -> StorageKey:
554
+ return StorageKey(**data)
555
+
556
+
557
+ @dataclass
558
+ class StorageMap:
559
+ """Storage map information.
560
+
561
+ :ivar key_type: Type of map keys
562
+ :ivar value_type: Type of map values
563
+ :ivar desc: Optional description
564
+ :ivar prefix: Optional key prefix
565
+ """
566
+
567
+ key_type: str
568
+ value_type: str
569
+ desc: str | None = None
570
+ prefix: str | None = None
571
+
572
+ @staticmethod
573
+ def from_dict(data: dict[str, Any]) -> StorageMap:
574
+ return StorageMap(**data)
575
+
576
+
577
+ @dataclass
578
+ class Keys:
579
+ """Storage keys for different storage types.
580
+
581
+ :ivar box: Box storage keys
582
+ :ivar global_state: Global state storage keys
583
+ :ivar local_state: Local state storage keys
584
+ """
585
+
586
+ box: dict[str, StorageKey]
587
+ global_state: dict[str, StorageKey] # actual schema field is "global" since it's a reserved word
588
+ local_state: dict[str, StorageKey] # actual schema field is "local" for consistency with renamed "global"
589
+
590
+ @staticmethod
591
+ def from_dict(data: dict[str, Any]) -> Keys:
592
+ box = {key: StorageKey.from_dict(value) for key, value in data["box"].items()}
593
+ global_state = {key: StorageKey.from_dict(value) for key, value in data["global"].items()}
594
+ local_state = {key: StorageKey.from_dict(value) for key, value in data["local"].items()}
595
+ return Keys(box=box, global_state=global_state, local_state=local_state)
596
+
597
+
598
+ @dataclass
599
+ class Maps:
600
+ """Storage maps for different storage types.
601
+
602
+ :ivar box: Box storage maps
603
+ :ivar global_state: Global state storage maps
604
+ :ivar local_state: Local state storage maps
605
+ """
606
+
607
+ box: dict[str, StorageMap]
608
+ global_state: dict[str, StorageMap] # actual schema field is "global" since it's a reserved word
609
+ local_state: dict[str, StorageMap] # actual schema field is "local" for consistency with renamed "global"
610
+
611
+ @staticmethod
612
+ def from_dict(data: dict[str, Any]) -> Maps:
613
+ box = {key: StorageMap.from_dict(value) for key, value in data["box"].items()}
614
+ global_state = {key: StorageMap.from_dict(value) for key, value in data["global"].items()}
615
+ local_state = {key: StorageMap.from_dict(value) for key, value in data["local"].items()}
616
+ return Maps(box=box, global_state=global_state, local_state=local_state)
617
+
618
+
619
+ @dataclass
620
+ class State:
621
+ """Application state information.
622
+
623
+ :ivar keys: Storage keys
624
+ :ivar maps: Storage maps
625
+ :ivar schema: State schema
626
+ """
627
+
628
+ keys: Keys
629
+ maps: Maps
630
+ schema: Schema
631
+
632
+ @staticmethod
633
+ def from_dict(data: dict[str, Any]) -> State:
634
+ data["keys"] = Keys.from_dict(data["keys"])
635
+ data["maps"] = Maps.from_dict(data["maps"])
636
+ data["schema"] = Schema.from_dict(data["schema"])
637
+ return State(**data)
638
+
639
+
640
+ @dataclass
641
+ class ProgramSourceInfo:
642
+ """Program source information.
643
+
644
+ :ivar pc_offset_method: PC offset method
645
+ :ivar source_info: List of source info entries
646
+ """
647
+
648
+ pc_offset_method: PcOffsetMethod
649
+ source_info: list[SourceInfo]
650
+
651
+ @staticmethod
652
+ def from_dict(data: dict[str, Any]) -> ProgramSourceInfo:
653
+ data["source_info"] = [SourceInfo.from_dict(item) for item in data["source_info"]]
654
+ return ProgramSourceInfo(**data)
655
+
656
+
657
+ @dataclass
658
+ class SourceInfoModel:
659
+ """Source information for approval and clear programs.
660
+
661
+ :ivar approval: Approval program source info
662
+ :ivar clear: Clear program source info
663
+ """
664
+
665
+ approval: ProgramSourceInfo
666
+ clear: ProgramSourceInfo
667
+
668
+ @staticmethod
669
+ def from_dict(data: dict[str, Any]) -> SourceInfoModel:
670
+ data["approval"] = ProgramSourceInfo.from_dict(data["approval"])
671
+ data["clear"] = ProgramSourceInfo.from_dict(data["clear"])
672
+ return SourceInfoModel(**data)
673
+
674
+
675
+ def _dict_keys_to_snake_case(
676
+ value: Any, # noqa: ANN401
677
+ ) -> Any: # noqa: ANN401
678
+ def camel_to_snake(s: str) -> str:
679
+ return "".join(["_" + c.lower() if c.isupper() else c for c in s]).lstrip("_")
680
+
681
+ match value:
682
+ case dict():
683
+ new_dict: dict[str, Any] = {}
684
+ for key, val in value.items():
685
+ new_dict[camel_to_snake(str(key))] = _dict_keys_to_snake_case(val)
686
+ return new_dict
687
+ case list():
688
+ return [_dict_keys_to_snake_case(item) for item in value]
689
+ case _:
690
+ return value
691
+
692
+
693
+ class _Arc32ToArc56Converter:
694
+ def __init__(self, arc32_application_spec: str):
695
+ self.arc32 = json.loads(arc32_application_spec)
696
+
697
+ def convert(self) -> Arc56Contract:
698
+ source_data = self.arc32.get("source")
699
+ return Arc56Contract(
700
+ name=self.arc32["contract"]["name"],
701
+ desc=self.arc32["contract"].get("desc"),
702
+ arcs=[],
703
+ methods=self._convert_methods(self.arc32),
704
+ structs=self._convert_structs(self.arc32),
705
+ state=self._convert_state(self.arc32),
706
+ source=Source(**source_data) if source_data else None,
707
+ bare_actions=BareActions(
708
+ call=self._convert_actions(self.arc32.get("bare_call_config"), _ActionType.CALL),
709
+ create=self._convert_actions(self.arc32.get("bare_call_config"), _ActionType.CREATE),
710
+ ),
711
+ )
712
+
713
+ def _convert_storage_keys(self, schema: dict) -> dict[str, StorageKey]:
714
+ """Convert ARC32 schema declared fields to ARC56 storage keys."""
715
+ return {
716
+ name: StorageKey(
717
+ key=b64encode(field["key"].encode()).decode(),
718
+ key_type="AVMString",
719
+ value_type="AVMUint64" if field["type"] == "uint64" else "AVMBytes",
720
+ desc=field.get("descr"),
721
+ )
722
+ for name, field in schema.items()
723
+ }
724
+
725
+ def _convert_state(self, arc32: dict) -> State:
726
+ """Convert ARC32 state and schema to ARC56 state specification."""
727
+ state_data = arc32.get("state", {})
728
+ return State(
729
+ schema=Schema(
730
+ global_state=Global(
731
+ ints=state_data.get("global", {}).get("num_uints", 0),
732
+ bytes=state_data.get("global", {}).get("num_byte_slices", 0),
733
+ ),
734
+ local_state=Local(
735
+ ints=state_data.get("local", {}).get("num_uints", 0),
736
+ bytes=state_data.get("local", {}).get("num_byte_slices", 0),
737
+ ),
738
+ ),
739
+ keys=Keys(
740
+ global_state=self._convert_storage_keys(arc32.get("schema", {}).get("global", {}).get("declared", {})),
741
+ local_state=self._convert_storage_keys(arc32.get("schema", {}).get("local", {}).get("declared", {})),
742
+ box={},
743
+ ),
744
+ maps=Maps(global_state={}, local_state={}, box={}),
745
+ )
746
+
747
+ def _convert_structs(self, arc32: dict) -> dict[str, list[StructField]]:
748
+ """Extract and convert struct definitions from hints."""
749
+ return {
750
+ struct["name"]: [StructField(name=elem[0], type=elem[1]) for elem in struct["elements"]]
751
+ for hint in arc32.get("hints", {}).values()
752
+ for struct in hint.get("structs", {}).values()
753
+ }
754
+
755
+ def _convert_default_value(self, arg_type: str, default_arg: dict[str, Any] | None) -> DefaultValue | None:
756
+ """Convert ARC32 default argument to ARC56 format."""
757
+ if not default_arg or not default_arg.get("source"):
758
+ return None
759
+
760
+ source_mapping = {
761
+ "constant": "literal",
762
+ "global-state": "global",
763
+ "local-state": "local",
764
+ "abi-method": "method",
765
+ }
766
+
767
+ mapped_source = source_mapping.get(default_arg["source"])
768
+ if not mapped_source:
769
+ return None
770
+ elif mapped_source == "method":
771
+ return DefaultValue(
772
+ source=mapped_source, # type: ignore[arg-type]
773
+ data=default_arg.get("data", {}).get("name"),
774
+ )
775
+
776
+ arg_data = default_arg.get("data")
777
+
778
+ if isinstance(arg_data, int):
779
+ arg_data = algosdk.abi.ABIType.from_string("uint64").encode(arg_data)
780
+ elif isinstance(arg_data, str):
781
+ arg_data = arg_data.encode()
782
+ else:
783
+ raise ValueError(f"Invalid default argument data type: {type(arg_data)}")
784
+
785
+ return DefaultValue(
786
+ source=mapped_source, # type: ignore[arg-type]
787
+ data=base64.b64encode(arg_data).decode("utf-8"),
788
+ type=arg_type if arg_type != "string" else "AVMString",
789
+ )
790
+
791
+ @overload
792
+ def _convert_actions(self, config: dict | None, action_type: Literal[_ActionType.CALL]) -> list[CallEnum]: ...
793
+
794
+ @overload
795
+ def _convert_actions(self, config: dict | None, action_type: Literal[_ActionType.CREATE]) -> list[CreateEnum]: ...
796
+
797
+ def _convert_actions(self, config: dict | None, action_type: _ActionType) -> Sequence[CallEnum | CreateEnum]:
798
+ """Extract supported actions from call config."""
799
+ if not config:
800
+ return []
801
+
802
+ actions: list[CallEnum | CreateEnum] = []
803
+ mappings = {
804
+ "no_op": (CallEnum.NO_OP, CreateEnum.NO_OP),
805
+ "opt_in": (CallEnum.OPT_IN, CreateEnum.OPT_IN),
806
+ "close_out": (CallEnum.CLOSE_OUT, None),
807
+ "delete_application": (CallEnum.DELETE_APPLICATION, CreateEnum.DELETE_APPLICATION),
808
+ "update_application": (CallEnum.UPDATE_APPLICATION, None),
809
+ }
810
+
811
+ for action, (call_enum, create_enum) in mappings.items():
812
+ if action in config and config[action] in ["ALL", action_type]:
813
+ if action_type == "CALL" and call_enum:
814
+ actions.append(call_enum)
815
+ elif action_type == "CREATE" and create_enum:
816
+ actions.append(create_enum)
817
+
818
+ return actions
819
+
820
+ def _convert_method_actions(self, hint: dict | None) -> Actions:
821
+ """Convert method call config to ARC56 actions."""
822
+ config = hint.get("call_config", {}) if hint else {}
823
+ return Actions(
824
+ call=self._convert_actions(config, _ActionType.CALL),
825
+ create=self._convert_actions(config, _ActionType.CREATE),
826
+ )
827
+
828
+ def _convert_methods(self, arc32: dict) -> list[Method]:
829
+ """Convert ARC32 methods to ARC56 format."""
830
+ methods = []
831
+ contract = arc32["contract"]
832
+ hints = arc32.get("hints", {})
833
+
834
+ for method in contract["methods"]:
835
+ args_sig = ",".join(a["type"] for a in method["args"])
836
+ signature = f"{method['name']}({args_sig}){method['returns']['type']}"
837
+ hint = hints.get(signature, {})
838
+
839
+ methods.append(
840
+ Method(
841
+ name=method["name"],
842
+ desc=method.get("desc"),
843
+ readonly=hint.get("read_only"),
844
+ args=[
845
+ MethodArg(
846
+ name=arg.get("name"),
847
+ type=arg["type"],
848
+ desc=arg.get("desc"),
849
+ struct=hint.get("structs", {}).get(arg.get("name", ""), {}).get("name"),
850
+ default_value=self._convert_default_value(
851
+ arg["type"], hint.get("default_arguments", {}).get(arg.get("name"))
852
+ ),
853
+ )
854
+ for arg in method["args"]
855
+ ],
856
+ returns=Returns(
857
+ type=method["returns"]["type"],
858
+ desc=method["returns"].get("desc"),
859
+ struct=hint.get("structs", {}).get("output", {}).get("name"),
860
+ ),
861
+ actions=self._convert_method_actions(hint),
862
+ events=[], # ARC32 doesn't specify events
863
+ )
864
+ )
865
+ return methods
866
+
867
+
868
+ def _arc56_dict_factory() -> Callable[[list[tuple[str, Any]]], dict[str, Any]]:
869
+ """Creates a dict factory that handles ARC-56 JSON field naming conventions."""
870
+
871
+ word_map = {"global_state": "global", "local_state": "local"}
872
+ blocklist = ["_abi_method"]
873
+
874
+ def to_camel(key: str) -> str:
875
+ key = word_map.get(key, key)
876
+ words = key.split("_")
877
+ return words[0] + "".join(word.capitalize() for word in words[1:])
878
+
879
+ def dict_factory(entries: list[tuple[str, Any]]) -> dict[str, Any]:
880
+ return {to_camel(k): v for k, v in entries if v is not None and k not in blocklist}
881
+
882
+ return dict_factory
883
+
884
+
885
+ @dataclass
886
+ class Arc56Contract:
887
+ """ARC-0056 application specification.
888
+
889
+ See https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0056.md
890
+
891
+ :ivar arcs: List of supported ARC version numbers
892
+ :ivar bare_actions: Bare call and create actions
893
+ :ivar methods: List of contract methods
894
+ :ivar name: Contract name
895
+ :ivar state: Contract state information
896
+ :ivar structs: Contract struct definitions
897
+ :ivar byte_code: Optional bytecode for approval and clear programs
898
+ :ivar compiler_info: Optional compiler information
899
+ :ivar desc: Optional contract description
900
+ :ivar events: Optional list of contract events
901
+ :ivar networks: Optional network deployment information
902
+ :ivar scratch_variables: Optional scratch variable information
903
+ :ivar source: Optional source code
904
+ :ivar source_info: Optional source code information
905
+ :ivar template_variables: Optional template variable information
906
+ """
907
+
908
+ arcs: list[int]
909
+ bare_actions: BareActions
910
+ methods: list[Method]
911
+ name: str
912
+ state: State
913
+ structs: dict[str, list[StructField]]
914
+ byte_code: ByteCode | None = None
915
+ compiler_info: CompilerInfo | None = None
916
+ desc: str | None = None
917
+ events: list[Event] | None = None
918
+ networks: dict[str, Network] | None = None
919
+ scratch_variables: dict[str, ScratchVariables] | None = None
920
+ source: Source | None = None
921
+ source_info: SourceInfoModel | None = None
922
+ template_variables: dict[str, TemplateVariables] | None = None
923
+
924
+ @staticmethod
925
+ def from_dict(application_spec: dict) -> Arc56Contract:
926
+ """Create Arc56Contract from dictionary.
927
+
928
+ :param application_spec: Dictionary containing contract specification
929
+ :return: Arc56Contract instance
930
+ """
931
+ data = _dict_keys_to_snake_case(application_spec)
932
+ data["bare_actions"] = BareActions.from_dict(data["bare_actions"])
933
+ data["methods"] = [Method.from_dict(item) for item in data["methods"]]
934
+ data["state"] = State.from_dict(data["state"])
935
+ data["structs"] = {
936
+ key: [StructField.from_dict(item) for item in value] for key, value in application_spec["structs"].items()
937
+ }
938
+ if data.get("byte_code"):
939
+ data["byte_code"] = ByteCode.from_dict(data["byte_code"])
940
+ if data.get("compiler_info"):
941
+ data["compiler_info"] = CompilerInfo.from_dict(data["compiler_info"])
942
+ if data.get("events"):
943
+ data["events"] = [Event.from_dict(item) for item in data["events"]]
944
+ if data.get("networks"):
945
+ data["networks"] = {key: Network.from_dict(value) for key, value in data["networks"].items()}
946
+ if data.get("scratch_variables"):
947
+ data["scratch_variables"] = {
948
+ key: ScratchVariables.from_dict(value) for key, value in data["scratch_variables"].items()
949
+ }
950
+ if data.get("source"):
951
+ data["source"] = Source.from_dict(data["source"])
952
+ if data.get("source_info"):
953
+ data["source_info"] = SourceInfoModel.from_dict(data["source_info"])
954
+ if data.get("template_variables"):
955
+ data["template_variables"] = {
956
+ key: TemplateVariables.from_dict(value) for key, value in data["template_variables"].items()
957
+ }
958
+ return Arc56Contract(**data)
959
+
960
+ @staticmethod
961
+ def from_json(application_spec: str) -> Arc56Contract:
962
+ return Arc56Contract.from_dict(json.loads(application_spec))
963
+
964
+ @staticmethod
965
+ def from_arc32(arc32_application_spec: str | Arc32Contract) -> Arc56Contract:
966
+ return _Arc32ToArc56Converter(
967
+ arc32_application_spec.to_json()
968
+ if isinstance(arc32_application_spec, Arc32Contract)
969
+ else arc32_application_spec
970
+ ).convert()
971
+
972
+ @staticmethod
973
+ def get_abi_struct_from_abi_tuple(
974
+ decoded_tuple: Any, # noqa: ANN401
975
+ struct_fields: list[StructField],
976
+ structs: dict[str, list[StructField]],
977
+ ) -> dict[str, Any]:
978
+ result = {}
979
+ for i, field in enumerate(struct_fields):
980
+ key = field.name
981
+ field_type = field.type
982
+ value = decoded_tuple[i]
983
+ if isinstance(field_type, str):
984
+ if field_type in structs:
985
+ value = Arc56Contract.get_abi_struct_from_abi_tuple(value, structs[field_type], structs)
986
+ elif isinstance(field_type, list):
987
+ value = Arc56Contract.get_abi_struct_from_abi_tuple(value, field_type, structs)
988
+ result[key] = value
989
+ return result
990
+
991
+ def to_json(self, indent: int | None = None) -> str:
992
+ return json.dumps(self.dictify(), indent=indent)
993
+
994
+ def dictify(self) -> dict:
995
+ return asdict(self, dict_factory=_arc56_dict_factory())
996
+
997
+ def get_arc56_method(self, method_name_or_signature: str) -> Method:
998
+ if "(" not in method_name_or_signature:
999
+ # Filter by method name
1000
+ methods = [m for m in self.methods if m.name == method_name_or_signature]
1001
+ if not methods:
1002
+ raise ValueError(f"Unable to find method {method_name_or_signature} in {self.name} app.")
1003
+ if len(methods) > 1:
1004
+ signatures = [AlgosdkMethod.undictify(m.__dict__).get_signature() for m in self.methods]
1005
+ raise ValueError(
1006
+ f"Received a call to method {method_name_or_signature} in contract {self.name}, "
1007
+ f"but this resolved to multiple methods; please pass in an ABI signature instead: "
1008
+ f"{', '.join(signatures)}"
1009
+ )
1010
+ method = methods[0]
1011
+ else:
1012
+ # Find by signature
1013
+ method = None
1014
+ for m in self.methods:
1015
+ abi_method = AlgosdkMethod.undictify(asdict(m))
1016
+ if abi_method.get_signature() == method_name_or_signature:
1017
+ method = m
1018
+ break
1019
+
1020
+ if method is None:
1021
+ raise ValueError(f"Unable to find method {method_name_or_signature} in {self.name} app.")
1022
+
1023
+ return method