use crate::config::DevKitConfig;
use crate::package::topo_sort;
use std::collections::HashMap;
use std::fmt;
use std::path::{Path, PathBuf};
#[derive(Debug, PartialEq)]
pub enum ProjectError {
MissingConfig,
CircularDependency(String),
UnknownDependency(String),
SelfDependency(String),
DuplicateDependency(String),
MissingArtifact(String),
BuildRequired(String),
}
impl fmt::Display for ProjectError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ProjectError::MissingConfig => write!(f, "No [contracts] configured in .sdkt.toml"),
ProjectError::CircularDependency(path) => write!(
f,
"Circular dependency detected in contract deployment graph: {}",
path
),
ProjectError::UnknownDependency(dep) => write!(
f,
"Contract specifies a dependency that is not defined in [contracts]: {}",
dep
),
ProjectError::SelfDependency(alias) => write!(
f,
"Contract '{}' lists itself as a dependency (self-dependency)",
alias
),
ProjectError::DuplicateDependency(alias) => write!(
f,
"Contract '{}' declares the same dependency more than once",
alias
),
ProjectError::MissingArtifact(path) => {
write!(f, "Expected WASM artifact not found at: {}", path)
}
ProjectError::BuildRequired(alias) => write!(
f,
"WASM artifact missing for contract '{}'. Run `sdkt build` first.",
alias
),
}
}
}
impl std::error::Error for ProjectError {}
#[derive(Debug, PartialEq)]
pub struct ResolvedContract {
pub alias: String,
pub path: String,
pub wasm_artifact: PathBuf,
}
pub fn resolve_deploy_order(config: &DevKitConfig) -> Result<Vec<String>, ProjectError> {
if config.contracts.is_empty() {
return Err(ProjectError::MissingConfig);
}
let mut graph: HashMap<String, Vec<String>> = HashMap::new();
for alias in config.contracts.keys() {
graph.entry(alias.clone()).or_default();
}
for (alias, contract_cfg) in &config.contracts {
let mut merged: Vec<String> = Vec::new();
for dep in contract_cfg
.deploy_after
.iter()
.chain(contract_cfg.depends_on.iter())
{
if merged.contains(dep) {
return Err(ProjectError::DuplicateDependency(alias.clone()));
}
if dep == alias {
return Err(ProjectError::SelfDependency(alias.clone()));
}
if !config.contracts.contains_key(dep) {
return Err(ProjectError::UnknownDependency(dep.clone()));
}
merged.push(dep.clone());
}
graph.get_mut(alias).unwrap().extend(merged);
}
topo_sort(&graph).map_err(ProjectError::CircularDependency)
}
pub fn validate_project(config: &DevKitConfig) -> Result<(), ProjectError> {
resolve_deploy_order(config).map(|_| ())
}
pub fn resolve_project(config: &DevKitConfig) -> Result<Vec<ResolvedContract>, ProjectError> {
let ordered_aliases = resolve_deploy_order(config)?;
let mut resolved = Vec::new();
for alias in ordered_aliases {
let cfg = config.contracts.get(&alias).unwrap();
let path = Path::new(&cfg.path);
let target_dir = path
.join("target")
.join(crate::build::WASM_BUILD_TARGET)
.join("release");
let mut found_wasm = None;
if target_dir.exists() {
if let Ok(entries) = std::fs::read_dir(&target_dir) {
for entry in entries.flatten() {
let p = entry.path();
if p.extension().is_some_and(|ext| ext == "wasm") {
found_wasm = Some(p);
break;
}
}
}
}
let wasm_artifact = found_wasm.ok_or_else(|| ProjectError::BuildRequired(alias.clone()))?;
resolved.push(ResolvedContract {
alias,
path: cfg.path.clone(),
wasm_artifact,
});
}
Ok(resolved)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{ContractConfig, DevKitConfig};
use std::collections::HashMap;
fn cfg(pairs: Vec<(&str, Vec<String>, Vec<String>)>) -> DevKitConfig {
let mut map = HashMap::new();
for (alias, deploy_after, depends_on) in pairs {
map.insert(
alias.to_string(),
ContractConfig {
path: format!("contracts/{}", alias),
deploy_after,
depends_on,
},
);
}
DevKitConfig {
contracts: map,
..Default::default()
}
}
#[test]
fn test_missing_config() {
let config = DevKitConfig::default();
let res = resolve_deploy_order(&config);
assert!(matches!(res, Err(ProjectError::MissingConfig)));
}
#[test]
fn test_single_contract() {
let config = cfg(vec![("token", vec![], vec![])]);
let res = resolve_deploy_order(&config).unwrap();
assert_eq!(res, vec!["token".to_string()]);
}
#[test]
fn test_multiple_with_dependencies() {
let config = cfg(vec![
(
"router",
vec!["token".to_string(), "amm".to_string()],
vec![],
),
("token", vec![], vec![]),
("amm", vec!["token".to_string()], vec![]),
]);
let res = resolve_deploy_order(&config).unwrap();
assert_eq!(
res,
vec!["token".to_string(), "amm".to_string(), "router".to_string()]
);
}
#[test]
fn test_depends_on_field() {
let config = cfg(vec![
("router", vec![], vec!["token".to_string()]),
("token", vec![], vec![]),
]);
let res = resolve_deploy_order(&config).unwrap();
assert_eq!(res, vec!["token".to_string(), "router".to_string()]);
}
#[test]
fn test_merged_deploy_after_and_depends_on() {
let config = cfg(vec![
("router", vec!["token".to_string()], vec!["amm".to_string()]),
("token", vec![], vec![]),
("amm", vec![], vec!["token".to_string()]),
]);
let res = resolve_deploy_order(&config).unwrap();
assert_eq!(
res,
vec!["token".to_string(), "amm".to_string(), "router".to_string()]
);
}
#[test]
fn test_unknown_dependency() {
let config = cfg(vec![("router", vec!["does_not_exist".to_string()], vec![])]);
match resolve_deploy_order(&config) {
Err(ProjectError::UnknownDependency(d)) => assert_eq!(d, "does_not_exist"),
_ => panic!("Expected UnknownDependency"),
}
}
#[test]
fn test_self_dependency() {
let config = cfg(vec![("token", vec!["token".to_string()], vec![])]);
match resolve_deploy_order(&config) {
Err(ProjectError::SelfDependency(a)) => assert_eq!(a, "token"),
_ => panic!("Expected SelfDependency"),
}
}
#[test]
fn test_duplicate_dependency() {
let config = cfg(vec![
(
"router",
vec!["token".to_string()],
vec!["token".to_string()],
),
("token", vec![], vec![]),
]);
match resolve_deploy_order(&config) {
Err(ProjectError::DuplicateDependency(a)) => assert_eq!(a, "router"),
other => panic!("Expected DuplicateDependency, got {:?}", other),
}
}
#[test]
fn test_circular_dependency() {
let config = cfg(vec![
("a", vec!["b".to_string()], vec![]),
("b", vec!["a".to_string()], vec![]),
]);
match resolve_deploy_order(&config) {
Err(ProjectError::CircularDependency(d)) => assert!(d == "a" || d == "b"),
_ => panic!("Expected CircularDependency"),
}
}
#[test]
fn test_deterministic_ordering() {
let config = cfg(vec![
("d", vec!["b".to_string(), "c".to_string()], vec![]),
("b", vec!["a".to_string()], vec![]),
("c", vec!["a".to_string()], vec![]),
("a", vec![], vec![]),
]);
let first = resolve_deploy_order(&config).unwrap();
for _ in 0..20 {
assert_eq!(resolve_deploy_order(&config).unwrap(), first);
}
let pos = |s: &str| first.iter().position(|x| x == s).unwrap();
assert!(pos("a") < pos("b"));
assert!(pos("a") < pos("c"));
assert!(pos("b") < pos("d"));
assert!(pos("c") < pos("d"));
}
#[test]
fn test_validate_project_ok_and_err() {
let ok = cfg(vec![
("router", vec![], vec!["token".to_string()]),
("token", vec![], vec![]),
]);
assert!(validate_project(&ok).is_ok());
let bad = cfg(vec![("token", vec!["token".to_string()], vec![])]);
assert!(matches!(
validate_project(&bad),
Err(ProjectError::SelfDependency(_))
));
}
}