use crate::cli::{Config, Network};
use crate::errors::MigrationError;
use colored::*;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
#[derive(Deserialize, Serialize, Debug)]
struct Provider {
cluster: String,
wallet: String,
}
#[derive(Deserialize, Serialize, Debug)]
struct Programs {
#[serde(rename = "localnet")]
localnet: std::collections::HashMap<String, String>,
}
#[derive(Deserialize, Serialize, Debug)]
struct AnchorToml {
provider: Provider,
programs: Programs,
#[serde(flatten)]
extra: std::collections::HashMap<String, toml::Value>,
}
fn get_network_rpc_url(network: Network) -> &'static str {
match network {
Network::TestNet => "https://api.testnet.sonic.game",
Network::MainnetAlpha => "https://api.mainnet-alpha.sonic.game",
}
}
fn map_cluster_to_soon(cluster: &str, network: Option<Network>) -> String {
let network = network.unwrap_or(Network::TestNet);
get_network_rpc_url(network).to_string()
}
fn get_programs_section_name(network: Network) -> &'static str {
match network {
Network::TestNet => "testnet",
Network::MainnetAlpha => "mainnet",
}
}
pub fn run_migration(config: &Config) -> Result<(), MigrationError> {
validate_anchor_project(&config.path)?;
let anchor_toml_path = Path::new(&config.path).join("Anchor.toml");
let backup_path = anchor_toml_path.with_extension("toml.bak");
fs::copy(&anchor_toml_path, &backup_path)
.map_err(|e| MigrationError::BackupFailed(e.to_string()))?;
if config.verbose {
println!("{}", "Backup created successfully.".cyan());
}
let content = fs::read_to_string(&anchor_toml_path)
.map_err(|e| MigrationError::ReadFailed(e.to_string()))?;
let mut toml_value: toml::Value = content
.parse()
.map_err(|e: toml::de::Error| MigrationError::TomlParseError(e.to_string()))?;
if let Some(provider) = toml_value.get_mut("provider") {
if let Some(table) = provider.as_table_mut() {
let cluster_value = table.get("cluster")
.and_then(|c| c.as_str())
.map(|c| c.to_string());
if let Some(cluster) = cluster_value {
let soon_rpc = map_cluster_to_soon(&cluster, config.network);
table.insert("cluster".to_string(), toml::Value::String(soon_rpc.to_string()));
if config.verbose {
println!("{}", format!("Updating cluster from '{}' to '{}'", cluster, soon_rpc).cyan());
}
}
}
}
let programs_section_name = config.network.map_or("testnet", get_programs_section_name);
if let Some(programs) = toml_value.get_mut("programs") {
if let Some(table) = programs.as_table_mut() {
if let Some(localnet) = table.remove("localnet") {
table.insert(programs_section_name.to_string(), localnet);
if config.verbose {
println!("{}", format!("Updated programs.localnet to programs.{}", programs_section_name).cyan());
}
}
}
}
if config.verbose {
println!("{}", "Configuration updated successfully.".cyan());
}
if !config.dry_run {
let toml_string = toml::to_string_pretty(&toml_value)
.map_err(|e| MigrationError::TomlParseError(e.to_string()))?;
fs::write(&anchor_toml_path, toml_string)
.map_err(|e| MigrationError::WriteFailed(e.to_string()))?;
if config.verbose {
println!("{}", "Anchor.toml written successfully.".cyan());
}
} else {
println!("{}", "Dry run enabled. Changes not written.".yellow());
println!(
"{}",
toml::to_string_pretty(&toml_value)
.map_err(|e| MigrationError::TomlParseError(e.to_string()))?
.cyan()
);
}
Ok(())
}
pub fn restore_backup(path: &str) -> Result<(), MigrationError> {
let anchor_toml_path = Path::new(path).join("Anchor.toml");
let backup_path = anchor_toml_path.with_extension("toml.bak");
if !backup_path.exists() {
return Err(MigrationError::BackupNotFound(
backup_path.to_string_lossy().into_owned(),
));
}
fs::copy(&backup_path, &anchor_toml_path)
.map_err(|e| MigrationError::RestoreFailed(e.to_string()))?;
if Path::new(&backup_path).exists() {
fs::remove_file(backup_path)
.map_err(|e| MigrationError::RestoreFailed(e.to_string()))?;
}
Ok(())
}
fn validate_anchor_project(path: &str) -> Result<(), MigrationError> {
let anchor_toml_path = Path::new(path).join("Anchor.toml");
if !anchor_toml_path.exists() {
return Err(MigrationError::NotAnAnchorProject(path.to_string()));
}
let cargo_toml_path = Path::new(path).join("Cargo.toml");
if !cargo_toml_path.exists() {
return Err(MigrationError::NotAnAnchorProject(path.to_string()));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn create_test_anchor_project() -> TempDir {
let temp_dir = TempDir::new().unwrap();
let anchor_toml_content = r#"
[toolchain]
[features]
resolution = true
skip-lint = false
[programs.localnet]
migration = "EtQdsPNDckBhME3gRjcj9Z4Z9tGEYAoHjWKv7aHJgBua"
[registry]
url = "https://api.apr.dev"
[provider]
cluster = "Localnet"
wallet = "~/.config/solana/id.json"
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
"#;
fs::write(temp_dir.path().join("Anchor.toml"), anchor_toml_content).unwrap();
fs::write(
temp_dir.path().join("Cargo.toml"),
"[package]\nname = \"test\"\nversion = \"0.1.0\"\n",
)
.unwrap();
temp_dir
}
#[test]
fn test_migration_dry_run() {
let test_dir = create_test_anchor_project();
let config = Config {
path: test_dir.path().to_str().unwrap().to_string(),
dry_run: true,
verbose: false,
restore: false,
network: None,
list_networks: false,
};
let result = run_migration(&config);
assert!(result.is_ok());
let content = fs::read_to_string(test_dir.path().join("Anchor.toml")).unwrap();
assert!(content.contains("cluster = \"Localnet\""));
}
#[test]
fn test_migration_actual() {
let test_dir = create_test_anchor_project();
let config = Config {
path: test_dir.path().to_str().unwrap().to_string(),
dry_run: false,
verbose: false,
restore: false,
network: None,
list_networks: false,
};
let result = run_migration(&config);
assert!(result.is_ok());
let content = fs::read_to_string(test_dir.path().join("Anchor.toml")).unwrap();
assert!(content.contains("https://api.testnet.sonic.game"));
assert!(content.contains("[programs.testnet]"));
assert!(Path::new(&test_dir.path().join("Anchor.toml.bak")).exists());
}
#[test]
fn test_migration_mainnet() {
let test_dir = create_test_anchor_project();
let config = Config {
path: test_dir.path().to_str().unwrap().to_string(),
dry_run: false,
verbose: false,
restore: false,
network: Some(Network::MainnetAlpha),
list_networks: false,
};
let result = run_migration(&config);
assert!(result.is_ok());
let content = fs::read_to_string(test_dir.path().join("Anchor.toml")).unwrap();
assert!(content.contains("https://api.mainnet-alpha.sonic.game"));
assert!(content.contains("[programs.mainnet]"));
}
#[test]
fn test_restore_backup() {
let test_dir = create_test_anchor_project();
let config = Config {
path: test_dir.path().to_str().unwrap().to_string(),
dry_run: false,
verbose: false,
restore: false,
network: None,
list_networks: false,
};
run_migration(&config).unwrap();
let restore_result = restore_backup(test_dir.path().to_str().unwrap());
assert!(restore_result.is_ok());
let content = fs::read_to_string(test_dir.path().join("Anchor.toml")).unwrap();
assert!(content.contains("cluster = \"Localnet\""));
}
#[test]
fn test_invalid_path() {
let config = Config {
path: "/nonexistent/path".to_string(),
dry_run: false,
verbose: false,
restore: false,
network: None,
list_networks: false,
};
let result = run_migration(&config);
assert!(matches!(result, Err(MigrationError::NotAnAnchorProject(_))));
}
}