1
+ import dataclasses
2
+ import importlib
1
3
import logging
4
+ import subprocess
2
5
import sys
6
+ from collections .abc import Callable
3
7
from pathlib import Path
8
+ from shutil import rmtree
4
9
10
+ from algokit_utils .config import config
5
11
from dotenv import load_dotenv
6
12
7
- from smart_contracts ._helpers .build import build
8
- from smart_contracts ._helpers .config import contracts
9
- from smart_contracts ._helpers .deploy import deploy
10
-
11
- # Uncomment the following lines to enable auto generation of AVM Debugger compliant sourcemap and simulation trace file.
13
+ # Set trace_all to True to capture all transactions, defaults to capturing traces only on failure
12
14
# Learn more about using AlgoKit AVM Debugger to debug your TEAL source codes and inspect various kinds of
13
15
# Algorand transactions in atomic groups -> https://github.com/algorandfoundation/algokit-avm-vscode-debugger
14
- # from algokit_utils.config import config
15
- # config.configure(debug=True, trace_all=True)
16
+ config .configure (debug = True , trace_all = False )
17
+
18
+ # Set up logging and load environment variables.
16
19
logging .basicConfig (
17
20
level = logging .DEBUG , format = "%(asctime)s %(levelname)-10s: %(message)s"
18
21
)
19
22
logger = logging .getLogger (__name__ )
20
23
logger .info ("Loading .env" )
21
- # For manual script execution (bypassing `algokit project deploy`) with a custom .env,
22
- # modify `load_dotenv()` accordingly. For example, `load_dotenv('.env.localnet')`.
23
24
load_dotenv ()
25
+
26
+ # Determine the root path based on this file's location.
24
27
root_path = Path (__file__ ).parent
25
28
29
+ # ----------------------- Contract Configuration ----------------------- #
30
+
31
+
32
+ @dataclasses .dataclass
33
+ class SmartContract :
34
+ path : Path
35
+ name : str
36
+ deploy : Callable [[], None ] | None = None
37
+
38
+
39
+ def import_contract (folder : Path ) -> Path :
40
+ """Imports the contract from a folder if it exists."""
41
+ contract_path = folder / "contract.py"
42
+ if contract_path .exists ():
43
+ return contract_path
44
+ else :
45
+ raise Exception (f"Contract not found in { folder } " )
46
+
47
+
48
+ def import_deploy_if_exists (folder : Path ) -> Callable [[], None ] | None :
49
+ """Imports the deploy function from a folder if it exists."""
50
+ try :
51
+ module_name = f"{ folder .parent .name } .{ folder .name } .deploy_config"
52
+ deploy_module = importlib .import_module (module_name )
53
+ return deploy_module .deploy # type: ignore[no-any-return, misc]
54
+ except ImportError :
55
+ return None
56
+
57
+
58
+ def has_contract_file (directory : Path ) -> bool :
59
+ """Checks whether the directory contains a contract.py file."""
60
+ return (directory / "contract.py" ).exists ()
61
+
62
+
63
+ # Use the current directory (root_path) as the base for contract folders and exclude
64
+ # folders that start with '_' (internal helpers).
65
+ contracts : list [SmartContract ] = [
66
+ SmartContract (
67
+ path = import_contract (folder ),
68
+ name = folder .name ,
69
+ deploy = import_deploy_if_exists (folder ),
70
+ )
71
+ for folder in root_path .iterdir ()
72
+ if folder .is_dir () and has_contract_file (folder ) and not folder .name .startswith ("_" )
73
+ ]
74
+
75
+ # -------------------------- Build Logic -------------------------- #
76
+
77
+ deployment_extension = "py"
78
+
79
+
80
+ def _get_output_path (output_dir : Path , deployment_extension : str ) -> Path :
81
+ """Constructs the output path for the generated client file."""
82
+ return output_dir / Path (
83
+ "{contract_name}"
84
+ + ("_client" if deployment_extension == "py" else "Client" )
85
+ + f".{ deployment_extension } "
86
+ )
87
+
88
+
89
+ def build (output_dir : Path , contract_path : Path ) -> Path :
90
+ """
91
+ Builds the contract by exporting (compiling) its source and generating a client.
92
+ If the output directory already exists, it is cleared.
93
+ """
94
+ output_dir = output_dir .resolve ()
95
+ if output_dir .exists ():
96
+ rmtree (output_dir )
97
+ output_dir .mkdir (exist_ok = True , parents = True )
98
+ logger .info (f"Exporting { contract_path } to { output_dir } " )
99
+
100
+ build_result = subprocess .run (
101
+ [
102
+ "algokit" ,
103
+ "--no-color" ,
104
+ "compile" ,
105
+ "python" ,
106
+ str (contract_path .resolve ()),
107
+ f"--out-dir={ output_dir } " ,
108
+ "--no-output-arc32" ,
109
+ "--output-arc56" ,
110
+ "--output-source-map" ,
111
+ ],
112
+ stdout = subprocess .PIPE ,
113
+ stderr = subprocess .STDOUT ,
114
+ text = True ,
115
+ )
116
+ if build_result .returncode :
117
+ raise Exception (f"Could not build contract:\n { build_result .stdout } " )
118
+
119
+ # Look for arc56.json files and generate the client based on them.
120
+ app_spec_file_names : list [str ] = [
121
+ file .name for file in output_dir .glob ("*.arc56.json" )
122
+ ]
123
+
124
+ client_file : str | None = None
125
+ if not app_spec_file_names :
126
+ logger .warning (
127
+ "No '*.arc56.json' file found (likely a logic signature being compiled). Skipping client generation."
128
+ )
129
+ else :
130
+ for file_name in app_spec_file_names :
131
+ client_file = file_name
132
+ print (file_name )
133
+ generate_result = subprocess .run (
134
+ [
135
+ "algokit" ,
136
+ "generate" ,
137
+ "client" ,
138
+ str (output_dir ),
139
+ "--output" ,
140
+ str (_get_output_path (output_dir , deployment_extension )),
141
+ ],
142
+ stdout = subprocess .PIPE ,
143
+ stderr = subprocess .STDOUT ,
144
+ text = True ,
145
+ )
146
+ if generate_result .returncode :
147
+ if "No such command" in generate_result .stdout :
148
+ raise Exception (
149
+ "Could not generate typed client, requires AlgoKit 2.0.0 or later. Please update AlgoKit"
150
+ )
151
+ else :
152
+ raise Exception (
153
+ f"Could not generate typed client:\n { generate_result .stdout } "
154
+ )
155
+ if client_file :
156
+ return output_dir / client_file
157
+ return output_dir
158
+
159
+
160
+ # --------------------------- Main Logic --------------------------- #
161
+
26
162
27
163
def main (action : str , contract_name : str | None = None ) -> None :
164
+ """Main entry point to build and/or deploy smart contracts."""
28
165
artifact_path = root_path / "artifacts"
29
-
30
- # Filter contracts if a specific contract name is provided
166
+ # Filter contracts based on an optional specific contract name.
31
167
filtered_contracts = [
32
- c for c in contracts if contract_name is None or c .name == contract_name
168
+ contract
169
+ for contract in contracts
170
+ if contract_name is None or contract .name == contract_name
33
171
]
34
172
35
173
match action :
@@ -44,23 +182,24 @@ def main(action: str, contract_name: str | None = None) -> None:
44
182
(
45
183
file .name
46
184
for file in output_dir .iterdir ()
47
- if file .is_file () and file .suffixes == [".arc32 " , ".json" ]
185
+ if file .is_file () and file .suffixes == [".arc56 " , ".json" ]
48
186
),
49
187
None ,
50
188
)
51
189
if app_spec_file_name is None :
52
- raise Exception ("Could not deploy app, .arc32.json file not found" )
53
- app_spec_path = output_dir / app_spec_file_name
190
+ raise Exception ("Could not deploy app, .arc56.json file not found" )
54
191
if contract .deploy :
55
192
logger .info (f"Deploying app { contract .name } " )
56
- deploy ( app_spec_path , contract .deploy )
193
+ contract .deploy ( )
57
194
case "all" :
58
195
for contract in filtered_contracts :
59
196
logger .info (f"Building app at { contract .path } " )
60
- app_spec_path = build (artifact_path / contract .name , contract .path )
197
+ build (artifact_path / contract .name , contract .path )
61
198
if contract .deploy :
62
- logger .info (f"Deploying { contract .path .name } " )
63
- deploy (app_spec_path , contract .deploy )
199
+ logger .info (f"Deploying { contract .name } " )
200
+ contract .deploy ()
201
+ case _:
202
+ logger .error (f"Unknown action: { action } " )
64
203
65
204
66
205
if __name__ == "__main__" :
0 commit comments