cargo-tangle 0.5.0-alpha.5

A command-line tool to create and deploy blueprints on Tangle Network
Documentation
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr;

use alloy_primitives::Address;
use blueprint_manager::config::SourceType;
use blueprint_runner::config::{Protocol, ProtocolSettings};
use blueprint_runner::eigenlayer::config::EigenlayerProtocolSettings;
use blueprint_runner::error::ConfigError;
use blueprint_runner::tangle::config::TangleProtocolSettings;
use dotenv::from_path;

pub fn load_protocol_settings(
    protocol: Protocol,
    settings_file: &PathBuf,
) -> Result<ProtocolSettings, ConfigError> {
    from_path(settings_file)
        .map_err(|e| ConfigError::Other(format!("Failed to load settings file: {e}").into()))?;

    match protocol {
        Protocol::Eigenlayer => {
            let addresses = EigenlayerProtocolSettings {
                allocation_manager_address: env_var("ALLOCATION_MANAGER_ADDRESS")?,
                registry_coordinator_address: env_var("REGISTRY_COORDINATOR_ADDRESS")?,
                operator_state_retriever_address: env_var("OPERATOR_STATE_RETRIEVER_ADDRESS")?,
                delegation_manager_address: env_var("DELEGATION_MANAGER_ADDRESS")?,
                service_manager_address: env_var("SERVICE_MANAGER_ADDRESS")?,
                stake_registry_address: env_var("STAKE_REGISTRY_ADDRESS")?,
                strategy_manager_address: env_var("STRATEGY_MANAGER_ADDRESS")?,
                strategy_address: env_var("STRATEGY_ADDRESS")?,
                avs_directory_address: env_var("AVS_DIRECTORY_ADDRESS")?,
                rewards_coordinator_address: env_var("REWARDS_COORDINATOR_ADDRESS")?,
                permission_controller_address: env_var("PERMISSION_CONTROLLER_ADDRESS")?,
                allocation_delay: env_var_default("ALLOCATION_DELAY", 0)?,
                deposit_amount: env_var_default("DEPOSIT_AMOUNT", 5_000_000_000_000_000_000_000)?,
                stake_amount: env_var_default("STAKE_AMOUNT", 1_000_000_000_000_000_000)?,
                operator_sets: env_var_list("OPERATOR_SETS")
                    .map(|sets| sets.into_iter().map(|v| v as u32).collect())
                    .unwrap_or_else(|| vec![0]),
                staker_opt_out_window_blocks: env_var_default(
                    "STAKER_OPT_OUT_WINDOW_BLOCKS",
                    50_400,
                )?,
                metadata_url: std::env::var("METADATA_URL")
                    .unwrap_or_else(|_| "https://github.com/tangle-network/blueprint".to_string()),
            };
            Ok(ProtocolSettings::Eigenlayer(addresses))
        }
        Protocol::Tangle => {
            let blueprint_id = std::env::var("BLUEPRINT_ID")
                .map_err(|_| ConfigError::Other("Missing BLUEPRINT_ID".into()))?
                .parse()
                .map_err(|_| ConfigError::Other("Invalid BLUEPRINT_ID".into()))?;
            let service_id = std::env::var("SERVICE_ID")
                .ok()
                .and_then(|v| v.parse().ok());
            let tangle_contract = env_var("TANGLE_CONTRACT")?;
            let staking_contract = env_var("STAKING_CONTRACT")?;
            let status_registry_contract = env_var("STATUS_REGISTRY_CONTRACT")?;

            Ok(ProtocolSettings::Tangle(TangleProtocolSettings {
                blueprint_id,
                service_id,
                tangle_contract,
                staking_contract,
                status_registry_contract,
            }))
        }
        _ => Err(ConfigError::UnexpectedProtocol("Unsupported protocol")),
    }
}

fn env_var(name: &str) -> Result<Address, ConfigError> {
    let value =
        std::env::var(name).map_err(|_| ConfigError::Other(format!("Missing {name}").into()))?;
    Address::from_str(&value).map_err(|_| ConfigError::Other(format!("Invalid {name}").into()))
}

fn env_var_default<T>(name: &str, default: T) -> Result<T, ConfigError>
where
    T: FromStr + Copy,
{
    Ok(std::env::var(name)
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(default))
}

fn env_var_list(name: &str) -> Option<Vec<u64>> {
    std::env::var(name)
        .ok()
        .map(|s| s.split(',').filter_map(|v| v.trim().parse().ok()).collect())
}

const PREFERRED_SOURCE_KEY: &str = "PREFERRED_SOURCE";
const USE_VM_KEY: &str = "USE_VM";

#[derive(Clone, Copy, Debug, Default)]
pub struct RuntimePreferences {
    pub preferred_source: Option<SourceType>,
    pub use_vm: Option<bool>,
}

pub fn load_runtime_preferences() -> RuntimePreferences {
    let preferred_source = std::env::var(PREFERRED_SOURCE_KEY)
        .ok()
        .and_then(|value| parse_source_type(&value));
    let use_vm = std::env::var(USE_VM_KEY)
        .ok()
        .and_then(|value| parse_bool(&value));
    RuntimePreferences {
        preferred_source,
        use_vm,
    }
}

pub fn write_runtime_preferences(
    path: &Path,
    prefs: RuntimePreferences,
) -> Result<(), ConfigError> {
    let mut updates = Vec::new();
    if let Some(source) = prefs.preferred_source {
        updates.push((
            PREFERRED_SOURCE_KEY.to_string(),
            source_to_str(source).to_string(),
        ));
    }
    if let Some(use_vm) = prefs.use_vm {
        updates.push((USE_VM_KEY.to_string(), use_vm.to_string()));
    }

    if updates.is_empty() {
        return Ok(());
    }

    write_env_entries(path, &updates)
}

fn parse_source_type(value: &str) -> Option<SourceType> {
    match value.to_lowercase().as_str() {
        "native" => Some(SourceType::Native),
        "container" => Some(SourceType::Container),
        "wasm" => Some(SourceType::Wasm),
        _ => None,
    }
}

fn source_to_str(source: SourceType) -> &'static str {
    match source {
        SourceType::Native => "native",
        SourceType::Container => "container",
        SourceType::Wasm => "wasm",
    }
}

fn parse_bool(value: &str) -> Option<bool> {
    match value.to_lowercase().as_str() {
        "1" | "true" | "yes" => Some(true),
        "0" | "false" | "no" => Some(false),
        _ => None,
    }
}

fn write_env_entries(path: &Path, updates: &[(String, String)]) -> Result<(), ConfigError> {
    let mut lines = if path.exists() {
        let content = fs::read_to_string(path).map_err(|e| {
            ConfigError::Other(format!("Failed to read {}: {e}", path.display()).into())
        })?;
        parse_env_lines(&content)
    } else {
        Vec::new()
    };

    let mut remaining = updates.iter().cloned().collect::<BTreeMap<_, _>>();

    for line in &mut lines {
        if let EnvLine::Entry { key, value } = line {
            if let Some(new_value) = remaining.remove(key) {
                *value = new_value;
            }
        }
    }

    for (key, value) in remaining {
        lines.push(EnvLine::Entry { key, value });
    }

    let mut content = String::new();
    for line in lines {
        match line {
            EnvLine::Entry { key, value } => {
                content.push_str(&format!("{key}={value}\n"));
            }
            EnvLine::Other(raw) => {
                content.push_str(&raw);
                content.push('\n');
            }
        }
    }

    fs::write(path, content)
        .map_err(|e| ConfigError::Other(format!("Failed to write {}: {e}", path.display()).into()))
}

fn parse_env_lines(input: &str) -> Vec<EnvLine> {
    input
        .lines()
        .map(|line| {
            if let Some((key, value)) = line.split_once('=') {
                EnvLine::Entry {
                    key: key.trim().to_string(),
                    value: value.trim().to_string(),
                }
            } else {
                EnvLine::Other(line.to_string())
            }
        })
        .collect()
}

enum EnvLine {
    Entry { key: String, value: String },
    Other(String),
}