diff --git a/Cargo.toml b/Cargo.toml index f4d01ca..75b2c10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ path = "src/main.rs" zewif = { path = "../zewif" } zewif-zcashd = { path = "../zewif-zcashd" } zewif-zingo = { path = "../zewif-zingo" } +zewif-zwl = { path = "../zewif-zwl" } anyhow = "1.0.95" hex = "0.4.3" diff --git a/src/lib.rs b/src/lib.rs index 5f7d4ec..84ebf24 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ -pub mod zcashd_cmd; -pub mod zingo_cmd; pub mod exec; pub mod file_args; +pub mod zcashd_cmd; +pub mod zingo_cmd; +pub mod zwl_cmd; diff --git a/src/main.rs b/src/main.rs index eff2388..35983c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,7 @@ - mod styles; use clap::{Parser as ClapParser, Subcommand}; -use zmigrate::{exec::Exec, zcashd_cmd, zingo_cmd}; +use zmigrate::{exec::Exec, zcashd_cmd, zingo_cmd, zwl_cmd}; /// A tool for migrating Zcash wallets #[derive(Debug, clap::Parser)] @@ -20,6 +19,7 @@ struct Cli { enum MainCommands { Zcashd(zcashd_cmd::CommandArgs), Zingo(zingo_cmd::CommandArgs), + Zwl(zwl_cmd::CommandArgs), } #[doc(hidden)] @@ -42,6 +42,7 @@ fn inner_main() -> anyhow::Result<()> { let output = match cli.command { MainCommands::Zcashd(args) => args.exec(), MainCommands::Zingo(args) => args.exec(), + MainCommands::Zwl(args) => args.exec(), }; let output = output?; if !output.is_empty() { diff --git a/src/zwl_cmd.rs b/src/zwl_cmd.rs new file mode 100644 index 0000000..b1e3450 --- /dev/null +++ b/src/zwl_cmd.rs @@ -0,0 +1,42 @@ +use std::path::{Path, PathBuf}; + +use anyhow::Result; +use clap::Args; +use zewif_zwl::ZwlParser; + +use crate::file_args::{FileArgs, FileArgsLike}; + +/// Process a zecwallet wallet file +#[derive(Debug, Args)] +#[group(skip)] +pub struct CommandArgs { + #[command(flatten)] + file_args: FileArgs, +} + +impl FileArgsLike for CommandArgs { + fn file(&self) -> &PathBuf { + &self.file_args.file + } +} + +impl crate::exec::Exec for CommandArgs { + fn exec(&self) -> Result { + let file = self.file(); + dump_wallet(file) + } +} + +pub fn dump_wallet(file: &Path) -> Result { + let file_data = std::fs::read(file)?.into(); + let mut parser = ZwlParser::new(&file_data); + let wallet = parser.parse()?; + let mut dump = format!("{:#?}", wallet); + let remaining = wallet.remaining(); + if remaining == 0 { + dump.push_str("\n---\nāœ… Success"); + } else { + dump.push_str(&format!("\n---\nšŸ›‘ Unparsed bytes: {}", remaining)) + } + Ok(dump) +} diff --git a/tests/fixtures/zwl/mainnet/zecwallet-light-wallet-test.dat b/tests/fixtures/zwl/mainnet/zecwallet-light-wallet-test.dat new file mode 100644 index 0000000..0a93f56 Binary files /dev/null and b/tests/fixtures/zwl/mainnet/zecwallet-light-wallet-test.dat differ diff --git a/tests/fixtures/zwl/mainnet/zecwallet-light-wallet.dat b/tests/fixtures/zwl/mainnet/zecwallet-light-wallet.dat new file mode 100644 index 0000000..3177c38 Binary files /dev/null and b/tests/fixtures/zwl/mainnet/zecwallet-light-wallet.dat differ diff --git a/tests/fixtures/zwl/mainnet/zwl-encrypted.dat b/tests/fixtures/zwl/mainnet/zwl-encrypted.dat new file mode 100644 index 0000000..3b3c94e Binary files /dev/null and b/tests/fixtures/zwl/mainnet/zwl-encrypted.dat differ diff --git a/tests/fixtures/zwl/mainnet/zwl-real.dat b/tests/fixtures/zwl/mainnet/zwl-real.dat new file mode 100644 index 0000000..738e8c3 Binary files /dev/null and b/tests/fixtures/zwl/mainnet/zwl-real.dat differ diff --git a/tests/test_dump.rs b/tests/test_dump.rs index 762067e..21e6219 100644 --- a/tests/test_dump.rs +++ b/tests/test_dump.rs @@ -1,8 +1,8 @@ use anyhow::{Result, bail}; -use zmigrate::{zcashd_cmd, zingo_cmd}; +use zmigrate::{zcashd_cmd, zingo_cmd, zwl_cmd}; -use std::fmt::Write; use regex::Regex; +use std::fmt::Write; // Import shared test utilities mod test_utils; @@ -14,6 +14,8 @@ fn dump_wallet(path_elements: &[&str]) -> Result { zcashd_cmd::dump_wallet(&path) } else if path_elements[0] == "zingo" { zingo_cmd::dump_wallet(&path) + } else if path_elements[0] == "zwl" { + zwl_cmd::dump_wallet(&path) } else { bail!("Unknown command: {}", path_elements[0]); } @@ -44,12 +46,20 @@ fn test_migration_quality(path_elements: &[&str]) -> Result { // Check address preservation let zcashd_address_count = zcashd_section.matches("Address").count(); let zewif_address_count = zewif_section.matches("Address").count(); - writeln!(report, "- Addresses: {}/{} preserved", zewif_address_count, zcashd_address_count)?; + writeln!( + report, + "- Addresses: {}/{} preserved", + zewif_address_count, zcashd_address_count + )?; // Check transaction preservation let zcashd_tx_count = zcashd_section.matches("TxId").count(); let zewif_tx_count = zewif_section.matches("TxId").count(); - writeln!(report, "- Transactions: {}/{} preserved", zewif_tx_count, zcashd_tx_count)?; + writeln!( + report, + "- Transactions: {}/{} preserved", + zewif_tx_count, zcashd_tx_count + )?; // Check position information let zero_positions_count = zewif_section.matches("Position(0)").count(); @@ -58,8 +68,11 @@ fn test_migration_quality(path_elements: &[&str]) -> Result { if total_positions > 0 { let preservation_rate = (nonzero_positions_count as f64 / total_positions as f64) * 100.0; - writeln!(report, "- Positions: {}/{} preserved ({:.1}%)", - nonzero_positions_count, total_positions, preservation_rate)?; + writeln!( + report, + "- Positions: {}/{} preserved ({:.1}%)", + nonzero_positions_count, total_positions, preservation_rate + )?; } else { writeln!(report, "- Positions: No position data found")?; } @@ -91,12 +104,34 @@ fn test_migration_quality(path_elements: &[&str]) -> Result { // Check for account handling let zcashd_accounts = count_pattern(zcashd_section, r"Account\s*\{"); let zewif_accounts = count_pattern(zewif_section, r"Account\s*\{"); - writeln!(report, "- Accounts: {}/{} preserved", zewif_accounts, zcashd_accounts)?; + writeln!( + report, + "- Accounts: {}/{} preserved", + zewif_accounts, zcashd_accounts + )?; // Check specific ZCash features - check_feature_presence(&mut report, zcashd_section, zewif_section, "Unified", "Unified Address Support")?; - check_feature_presence(&mut report, zcashd_section, zewif_section, "SeedMaterial", "Seed Material")?; - check_feature_presence(&mut report, zcashd_section, zewif_section, "Network", "Network Information")?; + check_feature_presence( + &mut report, + zcashd_section, + zewif_section, + "Unified", + "Unified Address Support", + )?; + check_feature_presence( + &mut report, + zcashd_section, + zewif_section, + "SeedMaterial", + "Seed Material", + )?; + check_feature_presence( + &mut report, + zcashd_section, + zewif_section, + "Network", + "Network Information", + )?; Ok(report) } @@ -123,7 +158,13 @@ fn count_pattern(text: &str, pattern: &str) -> usize { re.find_iter(text).count() } -fn check_feature_presence(report: &mut String, source: &str, dest: &str, key_word: &str, feature_name: &str) -> Result<()> { +fn check_feature_presence( + report: &mut String, + source: &str, + dest: &str, + key_word: &str, + feature_name: &str, +) -> Result<()> { let in_source = source.contains(key_word); let in_dest = dest.contains(key_word); @@ -138,14 +179,22 @@ fn check_feature_presence(report: &mut String, source: &str, dest: &str, key_wor Ok(()) } -fn report_key_preservation(report: &mut String, source: &str, dest: &str, key_type: &str) -> Result<()> { +fn report_key_preservation( + report: &mut String, + source: &str, + dest: &str, + key_type: &str, +) -> Result<()> { let source_count = source.matches(key_type).count(); let dest_count = dest.matches(key_type).count(); if source_count > 0 { let preservation_rate = (dest_count as f64 / source_count as f64) * 100.0; - writeln!(report, " * {} keys: {}/{} preserved ({:.1}%)", - key_type, dest_count, source_count, preservation_rate)?; + writeln!( + report, + " * {} keys: {}/{} preserved ({:.1}%)", + key_type, dest_count, source_count, preservation_rate + )?; } else { writeln!(report, " * {} keys: None found in source", key_type)?; } @@ -160,17 +209,14 @@ fn test_zcashd() { vec!["zcashd", "golden-v5.6.0", "node1_wallet.dat"], vec!["zcashd", "golden-v5.6.0", "node2_wallet.dat"], vec!["zcashd", "golden-v5.6.0", "node3_wallet.dat"], - vec!["zcashd", "tarnished-v5.6.0", "node0_wallet.dat"], vec!["zcashd", "tarnished-v5.6.0", "node1_wallet.dat"], vec!["zcashd", "tarnished-v5.6.0", "node2_wallet.dat"], vec!["zcashd", "tarnished-v5.6.0", "node3_wallet.dat"], - vec!["zcashd", "sprout", "node0_wallet.dat"], vec!["zcashd", "sprout", "node1_wallet.dat"], vec!["zcashd", "sprout", "node2_wallet.dat"], vec!["zcashd", "sprout", "node3_wallet.dat"], - vec!["zcashd", "wallet0.dat"], vec!["zcashd", "wallet1.dat"], vec!["zcashd", "wallet2.dat"], @@ -192,13 +238,10 @@ fn test_migration_quality_report() { // Golden reference wallets (expected to be fully working) vec!["zcashd", "golden-v5.6.0", "node0_wallet.dat"], vec!["zcashd", "golden-v5.6.0", "node2_wallet.dat"], // May have more shielded data - // Tarnished wallets (may have issues) vec!["zcashd", "tarnished-v5.6.0", "node0_wallet.dat"], - // Sprout wallets (older format) vec!["zcashd", "sprout", "node0_wallet.dat"], - // Standard wallets vec!["zcashd", "wallet0.dat"], // Test standard wallet vec!["zcashd", "wallet5.dat"], // Test wallet likely with Orchard data @@ -207,8 +250,18 @@ fn test_migration_quality_report() { // Create a summary table of all wallet reports let mut summary = String::new(); writeln!(summary, "=== MIGRATION QUALITY SUMMARY ===").unwrap(); - writeln!(summary, "{:<40} | {:<15} | {:<15} | {:<15}", "Wallet", "Addresses", "Transactions", "Positions").unwrap(); - writeln!(summary, "{:-<40}-+-{:-<15}-+-{:-<15}-+-{:-<15}", "", "", "", "").unwrap(); + writeln!( + summary, + "{:<40} | {:<15} | {:<15} | {:<15}", + "Wallet", "Addresses", "Transactions", "Positions" + ) + .unwrap(); + writeln!( + summary, + "{:-<40}-+-{:-<15}-+-{:-<15}-+-{:-<15}", + "", "", "", "" + ) + .unwrap(); // Process each wallet and collect stats for path in &test_paths { @@ -233,8 +286,12 @@ fn test_migration_quality_report() { "N/A".to_string() }; - writeln!(summary, "{:<40} | {:<15} | {:<15} | {:<15}", - wallet_name, addr_stats, tx_stats, pos_stats).unwrap(); + writeln!( + summary, + "{:<40} | {:<15} | {:<15} | {:<15}", + wallet_name, addr_stats, tx_stats, pos_stats + ) + .unwrap(); } // Print the summary table @@ -254,15 +311,21 @@ fn extract_stat(report: &str, label: &str) -> String { #[test] fn test_zingo() { let paths = vec![ - vec!["zingo", "mainnet", "hhcclaltpcckcsslpcnetblr-gf0aaf9347.dat"], + vec![ + "zingo", + "mainnet", + "hhcclaltpcckcsslpcnetblr-gf0aaf9347.dat", + ], vec!["zingo", "mainnet", "hhcclaltpcckcsslpcnetblr-latest.dat"], // vec!["zingo", "mainnet", "vtfcorfbcbpctcfupmegmwbp-v28.dat"], // long - vec!["zingo", "regtest", "hmvasmuvwmssvichcarbpoct-v27.dat"], vec!["zingo", "regtest", "aadaalacaadaalacaadaalac-orch-only.dat"], - vec!["zingo", "regtest", "aadaalacaadaalacaadaalac-orch-and-sapling.dat"], + vec![ + "zingo", + "regtest", + "aadaalacaadaalacaadaalac-orch-and-sapling.dat", + ], vec!["zingo", "regtest", "aaaaaaaaaaaaaaaaaaaaaaaa-v26.dat"], - vec!["zingo", "testnet", "cbbhrwiilgbrababsshsmtpr-latest.dat"], vec!["zingo", "testnet", "G93738061a.dat"], vec!["zingo", "testnet", "Gab72a38b.dat"], @@ -276,3 +339,15 @@ fn test_zingo() { test_dump(path); } } + +#[test] +fn test_zwl() { + let paths = vec![ + vec!["zwl", "mainnet", "zecwallet-light-wallet-test.dat"], + vec!["zwl", "mainnet", "zecwallet-light-wallet.dat"], + vec!["zwl", "mainnet", "zwl-real.dat"], + ]; + for path in &paths { + test_dump(path); + } +} diff --git a/tests/test_wallet_decryption.rs b/tests/test_wallet_decryption.rs new file mode 100644 index 0000000..425c768 --- /dev/null +++ b/tests/test_wallet_decryption.rs @@ -0,0 +1,50 @@ +use std::path::Path; + +use anyhow::{Ok, Result}; +use zewif::Data; + +// Import shared test utilities +mod test_utils; +use test_utils::fixtures_path; +use zewif_zwl::ZwlParser; + +/// Attempts to decrypt a zecwallet wallet file with the given password, and compares the seed phrase +/// to the expected phrase. +fn test_parser_encryption(wallet_path: &[&str], expected_phrase: &str, password: &str) { + let file_path = fixtures_path(wallet_path); + + let file_data = Data::from_vec( + std::fs::read(Path::new(&file_path)) + .expect(&format!("Failed to read wallet file: {:?}", wallet_path)), + ); + let mut parser = ZwlParser::new(&file_data); + let wallet = parser.parse(); + + let mut real_wallet = wallet.unwrap(); + + let phrase = real_wallet + .keys + .get_phrase(String::from(password)) + .unwrap() + .into_phrase(); + + assert_eq!(phrase, expected_phrase, "Expected phrase does not match"); + + real_wallet + .keys + .unlock_wallet(password.to_string()) + .unwrap(); +} + +#[test] +fn test_zwl_decryption() -> Result<()> { + let paths = vec![vec!["zwl", "mainnet", "zwl-encrypted.dat"]]; + for path in &paths { + test_parser_encryption( + path, + "basket decorate ivory office buddy embark country office trophy speak cupboard mixture crazy agent lemon permit build situate omit spider bridge panda rather chuckle", + "hello world", + ); + } + Ok(()) +} diff --git a/tests/test_witness_data.rs b/tests/test_witness_data.rs index 5294675..6db26ff 100644 --- a/tests/test_witness_data.rs +++ b/tests/test_witness_data.rs @@ -1,6 +1,6 @@ use anyhow::Result; -use zmigrate::{zcashd_cmd, zingo_cmd}; use regex::Regex; +use zmigrate::{zcashd_cmd, zingo_cmd, zwl_cmd}; // Import shared test utilities mod test_utils; @@ -12,6 +12,8 @@ fn dump_wallet(path_elements: &[&str]) -> Result { zcashd_cmd::dump_wallet(&path) } else if path_elements[0] == "zingo" { zingo_cmd::dump_wallet(&path) + } else if path_elements[0] == "zwl" { + zwl_cmd::dump_wallet(&path) } else { Err(anyhow::anyhow!("Unknown command: {}", path_elements[0])) } @@ -25,19 +27,17 @@ fn test_witness_data_migration() { // Golden reference wallets (expected to be fully working) vec!["zcashd", "golden-v5.6.0", "node0_wallet.dat"], vec!["zcashd", "golden-v5.6.0", "node2_wallet.dat"], - // Tarnished wallets (may have issues) vec!["zcashd", "tarnished-v5.6.0", "node0_wallet.dat"], - - // Standard wallets + // Standard wallets vec!["zcashd", "wallet0.dat"], vec!["zcashd", "wallet5.dat"], ]; // Process each wallet and check witness data migration for path in &test_paths { - let output = dump_wallet(path) - .unwrap_or_else(|e| panic!("Error dumping wallet {:?}: {}", path, e)); + let output = + dump_wallet(path).unwrap_or_else(|e| panic!("Error dumping wallet {:?}: {}", path, e)); // Split the output into ZcashdWallet and ZewifTop sections let sections: Vec<&str> = output.split("---").collect(); @@ -55,21 +55,24 @@ fn test_witness_data_migration() { println!("\nWitness Data & Memo Migration for {:?}:", path); println!("- Source has witness data: {}", has_witness_in_source); println!("- Destination witness entries: {}", witness_count_in_dest); - + // Check for memo field entries in the output let memo_count_in_dest = count_memo_entries(zewif_section); println!("- Destination memo entries: {}", memo_count_in_dest); // Note: Transaction time is noted in the code but not yet stored // This will be implemented in the "Extract Transaction Metadata" subtask - + // We don't want to strictly assert witness data exists because some wallets // may legitimately not have any. Instead, we just log the information. - + // But we can check that memo field support is working by verifying that we have some memo entries // Only assert if we have sapling outputs, which should have memo fields if zewif_section.contains("SaplingOutputDescription") { - assert!(memo_count_in_dest > 0, "Memo fields should be present in Sapling outputs"); + assert!( + memo_count_in_dest > 0, + "Memo fields should be present in Sapling outputs" + ); } } } @@ -78,8 +81,8 @@ fn test_witness_data_migration() { fn has_witness_data(wallet_section: &str) -> bool { // Look for evidence of witness data in the wallet // This could be in either witnesses field or witness fields - wallet_section.contains("witnesses: [") || - (wallet_section.contains("witness:") && !wallet_section.contains("witness: None")) + wallet_section.contains("witnesses: [") + || (wallet_section.contains("witness:") && !wallet_section.contains("witness: None")) } /// Count witness entries in the destination ZeWIF format @@ -96,4 +99,4 @@ fn count_memo_entries(zewif_section: &str) -> usize { let memo_pattern = r"memo:\s*Some\("; let re = Regex::new(memo_pattern).unwrap(); re.find_iter(zewif_section).count() -} \ No newline at end of file +}