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