1use crate::config::DevKitConfig;
2use std::collections::HashMap;
3use std::fmt;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, PartialEq)]
7pub enum ProjectError {
8 MissingConfig,
9 CircularDependency(String),
10 UnknownDependency(String),
11 MissingArtifact(String),
12 BuildRequired(String),
13}
14
15impl fmt::Display for ProjectError {
16 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17 match self {
18 ProjectError::MissingConfig => write!(f, "No [contracts] configured in .sdkt.toml"),
19 ProjectError::CircularDependency(path) => write!(
20 f,
21 "Circular dependency detected in contract deployment graph: {}",
22 path
23 ),
24 ProjectError::UnknownDependency(dep) => write!(
25 f,
26 "Contract specifies a deploy_after dependency that is not defined: {}",
27 dep
28 ),
29 ProjectError::MissingArtifact(path) => {
30 write!(f, "Expected WASM artifact not found at: {}", path)
31 }
32 ProjectError::BuildRequired(alias) => write!(
33 f,
34 "WASM artifact missing for contract '{}'. Run `sdkt build` first.",
35 alias
36 ),
37 }
38 }
39}
40
41impl std::error::Error for ProjectError {}
42
43#[derive(Debug, PartialEq)]
45pub struct ResolvedContract {
46 pub alias: String,
47 pub path: String,
48 pub wasm_artifact: PathBuf,
49}
50
51pub fn resolve_deploy_order(config: &DevKitConfig) -> Result<Vec<String>, ProjectError> {
54 if config.contracts.is_empty() {
55 return Err(ProjectError::MissingConfig);
56 }
57
58 let mut in_degree: HashMap<String, usize> = HashMap::new();
59 let mut adj_list: HashMap<String, Vec<String>> = HashMap::new();
60
61 for alias in config.contracts.keys() {
63 in_degree.insert(alias.clone(), 0);
64 adj_list.insert(alias.clone(), Vec::new());
65 }
66
67 for (alias, contract_cfg) in &config.contracts {
69 for dep in &contract_cfg.deploy_after {
70 if !config.contracts.contains_key(dep) {
71 return Err(ProjectError::UnknownDependency(dep.clone()));
72 }
73 adj_list.get_mut(dep).unwrap().push(alias.clone());
75 *in_degree.get_mut(alias).unwrap() += 1;
76 }
77 }
78
79 let mut queue: Vec<String> = Vec::new();
81 for (alias, °ree) in &in_degree {
83 if degree == 0 {
84 queue.push(alias.clone());
85 }
86 }
87
88 queue.sort();
90
91 let mut ordered = Vec::new();
92
93 while !queue.is_empty() {
94 queue.sort();
96 let current = queue.remove(0);
97 ordered.push(current.clone());
98
99 if let Some(neighbors) = adj_list.get(¤t) {
100 for neighbor in neighbors {
101 if let Some(degree) = in_degree.get_mut(neighbor) {
102 *degree -= 1;
103 if *degree == 0 {
104 queue.push(neighbor.clone());
105 }
106 }
107 }
108 }
109 }
110
111 if ordered.len() != config.contracts.len() {
112 for (alias, °ree) in &in_degree {
114 if degree > 0 {
115 return Err(ProjectError::CircularDependency(alias.clone()));
116 }
117 }
118 return Err(ProjectError::CircularDependency("Unknown".to_string()));
119 }
120
121 Ok(ordered)
122}
123
124pub fn resolve_project(config: &DevKitConfig) -> Result<Vec<ResolvedContract>, ProjectError> {
127 let ordered_aliases = resolve_deploy_order(config)?;
128 let mut resolved = Vec::new();
129
130 for alias in ordered_aliases {
131 let cfg = config.contracts.get(&alias).unwrap();
132 let path = Path::new(&cfg.path);
133
134 let target_dir = path
136 .join("target")
137 .join("wasm32-unknown-unknown")
138 .join("release");
139
140 let mut found_wasm = None;
141 if target_dir.exists() {
142 if let Ok(entries) = std::fs::read_dir(&target_dir) {
143 for entry in entries.flatten() {
144 let p = entry.path();
145 if p.extension().is_some_and(|ext| ext == "wasm") {
146 found_wasm = Some(p);
148 break;
149 }
150 }
151 }
152 }
153
154 let wasm_artifact = found_wasm.ok_or_else(|| ProjectError::BuildRequired(alias.clone()))?;
155
156 resolved.push(ResolvedContract {
157 alias,
158 path: cfg.path.clone(),
159 wasm_artifact,
160 });
161 }
162
163 Ok(resolved)
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use crate::config::{ContractConfig, DevKitConfig};
170 use std::collections::HashMap;
171
172 #[test]
173 fn test_missing_config() {
174 let config = DevKitConfig::default();
175 let res = resolve_deploy_order(&config);
176 assert!(matches!(res, Err(ProjectError::MissingConfig)));
177 }
178
179 #[test]
180 fn test_single_contract() {
181 let mut config = DevKitConfig::default();
182 let mut map = HashMap::new();
183 map.insert(
184 "token".to_string(),
185 ContractConfig {
186 path: "contracts/token".to_string(),
187 deploy_after: vec![],
188 },
189 );
190 config.contracts = map;
191
192 let res = resolve_deploy_order(&config).unwrap();
193 assert_eq!(res, vec!["token".to_string()]);
194 }
195
196 #[test]
197 fn test_multiple_with_dependencies() {
198 let mut config = DevKitConfig::default();
199 let mut map = HashMap::new();
200 map.insert(
201 "router".to_string(),
202 ContractConfig {
203 path: "contracts/router".to_string(),
204 deploy_after: vec!["token".to_string(), "amm".to_string()],
205 },
206 );
207 map.insert(
208 "token".to_string(),
209 ContractConfig {
210 path: "contracts/token".to_string(),
211 deploy_after: vec![],
212 },
213 );
214 map.insert(
215 "amm".to_string(),
216 ContractConfig {
217 path: "contracts/amm".to_string(),
218 deploy_after: vec!["token".to_string()],
219 },
220 );
221 config.contracts = map;
222
223 let res = resolve_deploy_order(&config).unwrap();
224 assert_eq!(
228 res,
229 vec!["token".to_string(), "amm".to_string(), "router".to_string()]
230 );
231 }
232
233 #[test]
234 fn test_unknown_dependency() {
235 let mut config = DevKitConfig::default();
236 let mut map = HashMap::new();
237 map.insert(
238 "router".to_string(),
239 ContractConfig {
240 path: "contracts/router".to_string(),
241 deploy_after: vec!["does_not_exist".to_string()],
242 },
243 );
244 config.contracts = map;
245
246 let res = resolve_deploy_order(&config);
247 match res {
248 Err(ProjectError::UnknownDependency(d)) => assert_eq!(d, "does_not_exist"),
249 _ => panic!("Expected UnknownDependency"),
250 }
251 }
252
253 #[test]
254 fn test_circular_dependency() {
255 let mut config = DevKitConfig::default();
256 let mut map = HashMap::new();
257 map.insert(
258 "a".to_string(),
259 ContractConfig {
260 path: "contracts/a".to_string(),
261 deploy_after: vec!["b".to_string()],
262 },
263 );
264 map.insert(
265 "b".to_string(),
266 ContractConfig {
267 path: "contracts/b".to_string(),
268 deploy_after: vec!["a".to_string()],
269 },
270 );
271 config.contracts = map;
272
273 let res = resolve_deploy_order(&config);
274 match res {
275 Err(ProjectError::CircularDependency(d)) => assert!(d == "a" || d == "b"),
276 _ => panic!("Expected CircularDependency"),
277 }
278 }
279}