Skip to main content

sdkt_core/
project.rs

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/// A resolved contract ready for deployment.
44#[derive(Debug, PartialEq)]
45pub struct ResolvedContract {
46    pub alias: String,
47    pub path: String,
48    pub wasm_artifact: PathBuf,
49}
50
51/// Resolves the deployment order based on `deploy_after` rules using a topological sort.
52/// Returns the ordered list of contract aliases to deploy.
53pub 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    // Initialize tracking structures
62    for alias in config.contracts.keys() {
63        in_degree.insert(alias.clone(), 0);
64        adj_list.insert(alias.clone(), Vec::new());
65    }
66
67    // Build the graph
68    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            // `dep` must be deployed before `alias`. So edge is dep -> alias.
74            adj_list.get_mut(dep).unwrap().push(alias.clone());
75            *in_degree.get_mut(alias).unwrap() += 1;
76        }
77    }
78
79    // Kahn's algorithm for topological sort
80    let mut queue: Vec<String> = Vec::new();
81    // Enqueue nodes with no dependencies (in-degree 0)
82    for (alias, &degree) in &in_degree {
83        if degree == 0 {
84            queue.push(alias.clone());
85        }
86    }
87
88    // Sort queue initially to ensure deterministic ordering for nodes at the same tier
89    queue.sort();
90
91    let mut ordered = Vec::new();
92
93    while !queue.is_empty() {
94        // Sort queue at each step to maintain strict deterministic output regardless of HashMap iteration order.
95        queue.sort();
96        let current = queue.remove(0);
97        ordered.push(current.clone());
98
99        if let Some(neighbors) = adj_list.get(&current) {
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        // We have a cycle. Find one of the nodes that couldn't be resolved.
113        for (alias, &degree) 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
124/// Resolves the deployment sequence and validates that all required WASM artifacts exist.
125/// Does NOT perform actual deployment (which requires sdkt-rpc).
126pub 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        // Attempt to locate the WASM artifact in the standard target directory
135        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                        // For determinism, if multiple exist, pick the first (or ideally, we'd enforce one).
147                        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        // token has 0 deps.
225        // amm depends on token.
226        // router depends on token and amm.
227        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}