1use crate::cli::Config;
7use crate::errors::MigrationError;
8use crate::oracle::{OracleDetector, OracleReport};
9use colored::*;
10use serde::{Deserialize, Serialize};
11use std::fs;
12use std::path::Path;
13
14#[derive(Deserialize, Serialize, Debug)]
16struct Provider {
17 cluster: String,
19 wallet: String,
21}
22
23#[derive(Deserialize, Serialize, Debug)]
25struct Programs {
26 #[serde(rename = "localnet")]
28 localnet: std::collections::HashMap<String, String>,
29}
30
31#[derive(Deserialize, Serialize, Debug)]
33struct AnchorToml {
34 provider: Provider,
36 programs: Programs,
38 #[serde(flatten)]
40 extra: std::collections::HashMap<String, toml::Value>,
41}
42
43#[derive(Debug)]
45pub struct MigrationResult {
46 pub config_updated: bool,
48 pub oracle_report: Option<OracleReport>,
50 pub warnings: Vec<String>,
52 pub next_steps: Vec<String>,
54}
55
56fn map_cluster_to_soon(cluster: &str) -> &'static str {
57 match cluster.to_lowercase().as_str() {
58 "mainnet-beta" | "mainnet" => "https://rpc.mainnet.soo.network/rpc",
59 "testnet" => "https://rpc.testnet.soo.network/rpc",
60 "devnet" | _ => "https://rpc.devnet.soo.network/rpc",
61 }
62}
63
64pub fn run_migration(config: &Config) -> Result<MigrationResult, MigrationError> {
75 validate_anchor_project(&config.path)?;
76
77 let mut result = MigrationResult {
78 config_updated: false,
79 oracle_report: None,
80 warnings: Vec::new(),
81 next_steps: Vec::new(),
82 };
83
84 if config.verbose {
86 println!("{}", "Running oracle detection...".cyan());
87 }
88
89 let oracle_report = OracleDetector::scan_project(&config.path, config.verbose)?;
90 result.oracle_report = Some(oracle_report);
91
92 if config.oracle_only {
94 if config.verbose {
95 println!("{}", "Oracle-only mode: skipping Anchor.toml migration".yellow());
96 }
97 return Ok(result);
98 }
99
100 let anchor_toml_path = Path::new(&config.path).join("Anchor.toml");
102
103 let backup_path = anchor_toml_path.with_extension("toml.bak");
105 fs::copy(&anchor_toml_path, &backup_path)
106 .map_err(|e| MigrationError::BackupFailed(e.to_string()))?;
107
108 if config.verbose {
109 println!("{}", "Backup created successfully.".cyan());
110 }
111
112 let content = fs::read_to_string(&anchor_toml_path)
114 .map_err(|e| MigrationError::ReadFailed(e.to_string()))?;
115
116 let mut toml_value: toml::Value = content
118 .parse()
119 .map_err(|e: toml::de::Error| MigrationError::TomlParseError(e.to_string()))?;
120
121 let mut config_changed = false;
122
123 if let Some(provider) = toml_value.get_mut("provider") {
125 if let Some(table) = provider.as_table_mut() {
126 let cluster_value = table.get("cluster")
128 .and_then(|c| c.as_str())
129 .map(|c| c.to_string());
130
131 if let Some(cluster) = cluster_value {
132 let soon_rpc = map_cluster_to_soon(&cluster);
133 table.insert("cluster".to_string(), toml::Value::String(soon_rpc.to_string()));
134
135 if config.verbose {
136 println!("{}", format!("Updating cluster from '{}' to '{}'", cluster, soon_rpc).cyan());
137 }
138 config_changed = true;
139 }
140 }
141 }
142
143 let network_name = {
145 if let Some(provider) = toml_value.get("provider") {
146 if let Some(cluster) = provider.get("cluster").and_then(|c| c.as_str()) {
147 if cluster.contains("mainnet") {
148 "mainnet"
149 } else if cluster.contains("testnet") {
150 "testnet"
151 } else {
152 "devnet"
153 }
154 } else {
155 "devnet"
156 }
157 } else {
158 "devnet"
159 }
160 };
161
162 if let Some(programs) = toml_value.get_mut("programs") {
164 if let Some(table) = programs.as_table_mut() {
165 if let Some(localnet) = table.remove("localnet") {
166 table.insert(network_name.to_string(), localnet);
167 if config.verbose {
168 println!("{}", format!("Updated programs.localnet to programs.{}", network_name).cyan());
169 }
170 config_changed = true;
171 }
172 }
173 }
174
175 if let Some(ref oracle_report) = result.oracle_report {
177 if !oracle_report.detected_oracles.is_empty() {
178 result.warnings.push("Oracle usage detected in your project. Review the oracle migration recommendations.".to_string());
179
180 for detection in &oracle_report.detected_oracles {
182 if matches!(detection.confidence, crate::oracle::ConfidenceLevel::High) {
183 result.warnings.push(format!("{:?} oracle detected - migration required for SOON compatibility", detection.oracle_type));
184 }
185 }
186 }
187 }
188
189 result.next_steps.push("1. Update your dependencies if using oracles".to_string());
191 result.next_steps.push("2. Test your project on SOON devnet".to_string());
192 result.next_steps.push("3. Review oracle integration if detected".to_string());
193 result.next_steps.push("4. Deploy to SOON Network".to_string());
194
195 if config.verbose {
196 println!("{}", "Configuration updated successfully.".cyan());
197 }
198
199 if !config.dry_run && config_changed {
201 let toml_string = toml::to_string_pretty(&toml_value)
202 .map_err(|e| MigrationError::TomlParseError(e.to_string()))?;
203
204 fs::write(&anchor_toml_path, toml_string)
205 .map_err(|e| MigrationError::WriteFailed(e.to_string()))?;
206
207 if config.verbose {
208 println!("{}", "Anchor.toml written successfully.".cyan());
209 }
210 result.config_updated = true;
211 } else if config.dry_run {
212 if config.verbose {
213 println!("{}", "Dry run enabled. Changes not written.".yellow());
214 println!(
215 "{}",
216 toml::to_string_pretty(&toml_value)
217 .map_err(|e| MigrationError::TomlParseError(e.to_string()))?
218 .cyan()
219 );
220 }
221 } else if !config_changed {
222 if config.verbose {
223 println!("{}", "No changes needed to Anchor.toml".green());
224 }
225 }
226
227 Ok(result)
228}
229
230pub fn run_oracle_scan_only(config: &Config) -> Result<OracleReport, MigrationError> {
241 validate_anchor_project(&config.path)?;
242 OracleDetector::scan_project(&config.path, config.verbose)
243}
244
245pub fn restore_backup(path: &str) -> Result<(), MigrationError> {
256 let anchor_toml_path = Path::new(path).join("Anchor.toml");
257 let backup_path = anchor_toml_path.with_extension("toml.bak");
258
259 if !backup_path.exists() {
260 return Err(MigrationError::BackupNotFound(
261 backup_path.to_string_lossy().into_owned(),
262 ));
263 }
264
265 fs::copy(&backup_path, &anchor_toml_path)
266 .map_err(|e| MigrationError::RestoreFailed(e.to_string()))?;
267
268 if Path::new(&backup_path).exists() {
269 fs::remove_file(backup_path)
270 .map_err(|e| MigrationError::RestoreFailed(e.to_string()))?;
271 }
272
273 Ok(())
274}
275
276fn validate_anchor_project(path: &str) -> Result<(), MigrationError> {
287 let anchor_toml_path = Path::new(path).join("Anchor.toml");
288 if !anchor_toml_path.exists() {
289 return Err(MigrationError::NotAnAnchorProject(path.to_string()));
290 }
291
292 let cargo_toml_path = Path::new(path).join("Cargo.toml");
293 if !cargo_toml_path.exists() {
294 return Err(MigrationError::NotAnAnchorProject(path.to_string()));
295 }
296
297 Ok(())
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use std::fs;
304 use tempfile::TempDir;
305
306 fn create_test_anchor_project() -> TempDir {
307 let temp_dir = TempDir::new().unwrap();
308 let anchor_toml_content = r#"
309[toolchain]
310
311[features]
312resolution = true
313skip-lint = false
314
315[programs.localnet]
316migration = "EtQdsPNDckBhME3gRjcj9Z4Z9tGEYAoHjWKv7aHJgBua"
317
318[registry]
319url = "https://api.apr.dev"
320
321[provider]
322cluster = "devnet"
323wallet = "~/.config/solana/id.json"
324
325[scripts]
326test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
327"#;
328
329 let cargo_toml_content = r#"
330[package]
331name = "test"
332version = "0.1.0"
333
334[dependencies]
335anchor-lang = "0.28.0"
336"#;
337
338 fs::write(temp_dir.path().join("Anchor.toml"), anchor_toml_content).unwrap();
339 fs::write(temp_dir.path().join("Cargo.toml"), cargo_toml_content).unwrap();
340
341 temp_dir
342 }
343
344 fn create_test_anchor_project_with_oracle() -> TempDir {
345 let temp_dir = TempDir::new().unwrap();
346 let anchor_toml_content = r#"
347[toolchain]
348
349[features]
350resolution = true
351skip-lint = false
352
353[programs.localnet]
354migration = "EtQdsPNDckBhME3gRjcj9Z4Z9tGEYAoHjWKv7aHJgBua"
355
356[registry]
357url = "https://api.apr.dev"
358
359[provider]
360cluster = "devnet"
361wallet = "~/.config/solana/id.json"
362
363[scripts]
364test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
365"#;
366
367 let cargo_toml_content = r#"
368[package]
369name = "test"
370version = "0.1.0"
371
372[dependencies]
373anchor-lang = "0.28.0"
374pyth-solana-receiver-sdk = "0.2.0"
375"#;
376
377 let rust_code = r#"
378use anchor_lang::prelude::*;
379use pyth_solana_receiver_sdk::PriceUpdateV2;
380
381pub fn get_price() -> Result<()> {
382 // Get price from Pyth
383 let price = price_update.get_price_no_older_than(&clock, 60)?;
384 Ok(())
385}
386"#;
387
388 fs::write(temp_dir.path().join("Anchor.toml"), anchor_toml_content).unwrap();
389 fs::write(temp_dir.path().join("Cargo.toml"), cargo_toml_content).unwrap();
390
391 fs::create_dir_all(temp_dir.path().join("src")).unwrap();
393 fs::write(temp_dir.path().join("src").join("price.rs"), rust_code).unwrap();
394
395 temp_dir
396 }
397
398 #[test]
399 fn test_basic_migration() {
400 let test_dir = create_test_anchor_project();
401 let config = Config {
402 path: test_dir.path().to_str().unwrap().to_string(),
403 dry_run: false,
404 verbose: false,
405 restore: false,
406 show_guide: false,
407 oracle_only: false,
408 };
409
410 let result = run_migration(&config).unwrap();
411
412 assert!(result.config_updated);
414
415 let content = fs::read_to_string(test_dir.path().join("Anchor.toml")).unwrap();
417 assert!(content.contains("https://rpc.devnet.soo.network/rpc"));
418 assert!(content.contains("[programs.devnet]"));
419 }
420
421 #[test]
422 fn test_migration_with_oracle_detection() {
423 let test_dir = create_test_anchor_project_with_oracle();
424 let config = Config {
425 path: test_dir.path().to_str().unwrap().to_string(),
426 dry_run: false,
427 verbose: false,
428 restore: false,
429 show_guide: false,
430 oracle_only: false,
431 };
432
433 let result = run_migration(&config).unwrap();
434
435 assert!(result.oracle_report.is_some());
437 let oracle_report = result.oracle_report.unwrap();
438 assert!(!oracle_report.detected_oracles.is_empty());
439
440 let has_pyth = oracle_report.detected_oracles.iter()
442 .any(|d| matches!(d.oracle_type, crate::oracle::OracleType::Pyth));
443 assert!(has_pyth);
444
445 assert!(result.config_updated);
447
448 assert!(!result.warnings.is_empty());
450 }
451
452 #[test]
453 fn test_oracle_only_mode() {
454 let test_dir = create_test_anchor_project_with_oracle();
455 let config = Config {
456 path: test_dir.path().to_str().unwrap().to_string(),
457 dry_run: false,
458 verbose: false,
459 restore: false,
460 show_guide: false,
461 oracle_only: true,
462 };
463
464 let result = run_migration(&config).unwrap();
465
466 assert!(result.oracle_report.is_some());
468
469 assert!(!result.config_updated);
471
472 let content = fs::read_to_string(test_dir.path().join("Anchor.toml")).unwrap();
474 assert!(content.contains("cluster = \"devnet\""));
475 }
476
477 #[test]
478 fn test_dry_run_mode() {
479 let test_dir = create_test_anchor_project();
480 let config = Config {
481 path: test_dir.path().to_str().unwrap().to_string(),
482 dry_run: true,
483 verbose: false,
484 restore: false,
485 show_guide: false,
486 oracle_only: false,
487 };
488
489 let result = run_migration(&config).unwrap();
490
491 assert!(!result.config_updated);
493
494 let content = fs::read_to_string(test_dir.path().join("Anchor.toml")).unwrap();
496 assert!(content.contains("cluster = \"devnet\""));
497 }
498
499 #[test]
500 fn test_network_mapping() {
501 assert_eq!(map_cluster_to_soon("mainnet-beta"), "https://rpc.mainnet.soo.network/rpc");
502 assert_eq!(map_cluster_to_soon("testnet"), "https://rpc.testnet.soo.network/rpc");
503 assert_eq!(map_cluster_to_soon("devnet"), "https://rpc.devnet.soo.network/rpc");
504 assert_eq!(map_cluster_to_soon("unknown"), "https://rpc.devnet.soo.network/rpc");
506 }
507
508 #[test]
509 fn test_restore_backup() {
510 let test_dir = create_test_anchor_project();
511
512 let config = Config {
514 path: test_dir.path().to_str().unwrap().to_string(),
515 dry_run: false,
516 verbose: false,
517 restore: false,
518 show_guide: false,
519 oracle_only: false,
520 };
521 run_migration(&config).unwrap();
522
523 let restore_result = restore_backup(test_dir.path().to_str().unwrap());
525 assert!(restore_result.is_ok());
526
527 let content = fs::read_to_string(test_dir.path().join("Anchor.toml")).unwrap();
529 assert!(content.contains("cluster = \"devnet\""));
530 }
531
532 #[test]
533 fn test_invalid_path() {
534 let config = Config {
535 path: "/nonexistent/path".to_string(),
536 dry_run: false,
537 verbose: false,
538 restore: false,
539 show_guide: false,
540 oracle_only: false,
541 };
542
543 let result = run_migration(&config);
544 assert!(matches!(result, Err(MigrationError::NotAnAnchorProject(_))));
545 }
546}