1use crate::config::DevKitConfig;
2use crate::package::topo_sort;
3use std::collections::HashMap;
4use std::fmt;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, PartialEq)]
8pub enum ProjectError {
9 MissingConfig,
10 CircularDependency(String),
11 UnknownDependency(String),
12 SelfDependency(String),
13 DuplicateDependency(String),
14 MissingArtifact(String),
15 BuildRequired(String),
16}
17
18impl fmt::Display for ProjectError {
19 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20 match self {
21 ProjectError::MissingConfig => write!(f, "No [contracts] configured in .sdkt.toml"),
22 ProjectError::CircularDependency(path) => write!(
23 f,
24 "Circular dependency detected in contract deployment graph: {}",
25 path
26 ),
27 ProjectError::UnknownDependency(dep) => write!(
28 f,
29 "Contract specifies a dependency that is not defined in [contracts]: {}",
30 dep
31 ),
32 ProjectError::SelfDependency(alias) => write!(
33 f,
34 "Contract '{}' lists itself as a dependency (self-dependency)",
35 alias
36 ),
37 ProjectError::DuplicateDependency(alias) => write!(
38 f,
39 "Contract '{}' declares the same dependency more than once",
40 alias
41 ),
42 ProjectError::MissingArtifact(path) => {
43 write!(f, "Expected WASM artifact not found at: {}", path)
44 }
45 ProjectError::BuildRequired(alias) => write!(
46 f,
47 "WASM artifact missing for contract '{}'. Run `sdkt build` first.",
48 alias
49 ),
50 }
51 }
52}
53
54impl std::error::Error for ProjectError {}
55
56#[derive(Debug, PartialEq)]
58pub struct ResolvedContract {
59 pub alias: String,
60 pub path: String,
61 pub wasm_artifact: PathBuf,
62}
63
64pub fn resolve_deploy_order(config: &DevKitConfig) -> Result<Vec<String>, ProjectError> {
77 if config.contracts.is_empty() {
78 return Err(ProjectError::MissingConfig);
79 }
80
81 let mut graph: HashMap<String, Vec<String>> = HashMap::new();
85 for alias in config.contracts.keys() {
86 graph.entry(alias.clone()).or_default();
87 }
88
89 for (alias, contract_cfg) in &config.contracts {
90 let mut merged: Vec<String> = Vec::new();
94 for dep in contract_cfg
95 .deploy_after
96 .iter()
97 .chain(contract_cfg.depends_on.iter())
98 {
99 if merged.contains(dep) {
100 return Err(ProjectError::DuplicateDependency(alias.clone()));
101 }
102 if dep == alias {
104 return Err(ProjectError::SelfDependency(alias.clone()));
105 }
106 if !config.contracts.contains_key(dep) {
108 return Err(ProjectError::UnknownDependency(dep.clone()));
109 }
110 merged.push(dep.clone());
111 }
112
113 graph.get_mut(alias).unwrap().extend(merged);
115 }
116
117 topo_sort(&graph).map_err(ProjectError::CircularDependency)
119}
120
121pub fn validate_project(config: &DevKitConfig) -> Result<(), ProjectError> {
128 resolve_deploy_order(config).map(|_| ())
129}
130
131pub fn resolve_project(config: &DevKitConfig) -> Result<Vec<ResolvedContract>, ProjectError> {
134 let ordered_aliases = resolve_deploy_order(config)?;
135 let mut resolved = Vec::new();
136
137 for alias in ordered_aliases {
138 let cfg = config.contracts.get(&alias).unwrap();
139 let path = Path::new(&cfg.path);
140
141 let target_dir = path
143 .join("target")
144 .join(crate::build::WASM_BUILD_TARGET)
145 .join("release");
146
147 let mut found_wasm = None;
148 if target_dir.exists() {
149 if let Ok(entries) = std::fs::read_dir(&target_dir) {
150 for entry in entries.flatten() {
151 let p = entry.path();
152 if p.extension().is_some_and(|ext| ext == "wasm") {
153 found_wasm = Some(p);
155 break;
156 }
157 }
158 }
159 }
160
161 let wasm_artifact = found_wasm.ok_or_else(|| ProjectError::BuildRequired(alias.clone()))?;
162
163 resolved.push(ResolvedContract {
164 alias,
165 path: cfg.path.clone(),
166 wasm_artifact,
167 });
168 }
169
170 Ok(resolved)
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::config::{ContractConfig, DevKitConfig};
177 use std::collections::HashMap;
178
179 fn cfg(pairs: Vec<(&str, Vec<String>, Vec<String>)>) -> DevKitConfig {
180 let mut map = HashMap::new();
181 for (alias, deploy_after, depends_on) in pairs {
182 map.insert(
183 alias.to_string(),
184 ContractConfig {
185 path: format!("contracts/{}", alias),
186 deploy_after,
187 depends_on,
188 },
189 );
190 }
191 DevKitConfig {
192 contracts: map,
193 ..Default::default()
194 }
195 }
196
197 #[test]
198 fn test_missing_config() {
199 let config = DevKitConfig::default();
200 let res = resolve_deploy_order(&config);
201 assert!(matches!(res, Err(ProjectError::MissingConfig)));
202 }
203
204 #[test]
205 fn test_single_contract() {
206 let config = cfg(vec![("token", vec![], vec![])]);
207 let res = resolve_deploy_order(&config).unwrap();
208 assert_eq!(res, vec!["token".to_string()]);
209 }
210
211 #[test]
212 fn test_multiple_with_dependencies() {
213 let config = cfg(vec![
214 (
215 "router",
216 vec!["token".to_string(), "amm".to_string()],
217 vec![],
218 ),
219 ("token", vec![], vec![]),
220 ("amm", vec!["token".to_string()], vec![]),
221 ]);
222 let res = resolve_deploy_order(&config).unwrap();
223 assert_eq!(
224 res,
225 vec!["token".to_string(), "amm".to_string(), "router".to_string()]
226 );
227 }
228
229 #[test]
230 fn test_depends_on_field() {
231 let config = cfg(vec![
233 ("router", vec![], vec!["token".to_string()]),
234 ("token", vec![], vec![]),
235 ]);
236 let res = resolve_deploy_order(&config).unwrap();
237 assert_eq!(res, vec!["token".to_string(), "router".to_string()]);
238 }
239
240 #[test]
241 fn test_merged_deploy_after_and_depends_on() {
242 let config = cfg(vec![
244 ("router", vec!["token".to_string()], vec!["amm".to_string()]),
245 ("token", vec![], vec![]),
246 ("amm", vec![], vec!["token".to_string()]),
247 ]);
248 let res = resolve_deploy_order(&config).unwrap();
249 assert_eq!(
250 res,
251 vec!["token".to_string(), "amm".to_string(), "router".to_string()]
252 );
253 }
254
255 #[test]
256 fn test_unknown_dependency() {
257 let config = cfg(vec![("router", vec!["does_not_exist".to_string()], vec![])]);
258 match resolve_deploy_order(&config) {
259 Err(ProjectError::UnknownDependency(d)) => assert_eq!(d, "does_not_exist"),
260 _ => panic!("Expected UnknownDependency"),
261 }
262 }
263
264 #[test]
265 fn test_self_dependency() {
266 let config = cfg(vec![("token", vec!["token".to_string()], vec![])]);
267 match resolve_deploy_order(&config) {
268 Err(ProjectError::SelfDependency(a)) => assert_eq!(a, "token"),
269 _ => panic!("Expected SelfDependency"),
270 }
271 }
272
273 #[test]
274 fn test_duplicate_dependency() {
275 let config = cfg(vec![
277 (
278 "router",
279 vec!["token".to_string()],
280 vec!["token".to_string()],
281 ),
282 ("token", vec![], vec![]),
283 ]);
284 match resolve_deploy_order(&config) {
285 Err(ProjectError::DuplicateDependency(a)) => assert_eq!(a, "router"),
286 other => panic!("Expected DuplicateDependency, got {:?}", other),
287 }
288 }
289
290 #[test]
291 fn test_circular_dependency() {
292 let config = cfg(vec![
293 ("a", vec!["b".to_string()], vec![]),
294 ("b", vec!["a".to_string()], vec![]),
295 ]);
296 match resolve_deploy_order(&config) {
297 Err(ProjectError::CircularDependency(d)) => assert!(d == "a" || d == "b"),
298 _ => panic!("Expected CircularDependency"),
299 }
300 }
301
302 #[test]
303 fn test_deterministic_ordering() {
304 let config = cfg(vec![
306 ("d", vec!["b".to_string(), "c".to_string()], vec![]),
307 ("b", vec!["a".to_string()], vec![]),
308 ("c", vec!["a".to_string()], vec![]),
309 ("a", vec![], vec![]),
310 ]);
311 let first = resolve_deploy_order(&config).unwrap();
312 for _ in 0..20 {
313 assert_eq!(resolve_deploy_order(&config).unwrap(), first);
314 }
315 let pos = |s: &str| first.iter().position(|x| x == s).unwrap();
317 assert!(pos("a") < pos("b"));
318 assert!(pos("a") < pos("c"));
319 assert!(pos("b") < pos("d"));
320 assert!(pos("c") < pos("d"));
321 }
322
323 #[test]
324 fn test_validate_project_ok_and_err() {
325 let ok = cfg(vec![
326 ("router", vec![], vec!["token".to_string()]),
327 ("token", vec![], vec![]),
328 ]);
329 assert!(validate_project(&ok).is_ok());
330
331 let bad = cfg(vec![("token", vec!["token".to_string()], vec![])]);
332 assert!(matches!(
333 validate_project(&bad),
334 Err(ProjectError::SelfDependency(_))
335 ));
336 }
337}