// file: crates/ksp-wallet-lib/unit_tests/transfer.rs // version: 4 #[cfg(unix)] use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import use zeroize::Zeroize; // rust-rules: trait-import fn runtime() -> tokio::runtime::Runtime { return tokio::runtime::Builder::new_current_thread().build().expect("Wallet transfer test runtime must build"); } fn test_keypair() -> solana_keypair::Keypair { return solana_keypair::Keypair::new_from_array([7_u8; 32]); } fn cli_json(keypair: &solana_keypair::Keypair) -> std::vec::Vec { let mut bytes = keypair.to_bytes(); let encoded = serde_json::to_vec(bytes.as_slice()).expect("test keypair must encode as Solana CLI JSON"); bytes.zeroize(); return encoded; } #[test] fn supported_transfer_formats_have_stable_codes_labels_and_extensions() { let formats = crate::WalletTransferFormat::supported(); assert_eq!(formats, [crate::WalletTransferFormat::SolanaCliJson, crate::WalletTransferFormat::SolanaKeypairBase58]); assert_eq!(formats[0].code(), "solana_cli_json"); assert_eq!(formats[0].default_extension(), "json"); assert!(formats[0].label().contains("64 bytes")); assert_eq!(formats[1].code(), "solana_keypair_base58"); assert_eq!(formats[1].default_extension(), "txt"); assert!(formats[1].label().contains("64 bytes")); } #[test] fn in_memory_inspection_accepts_cli_json_and_canonical_base58_without_exposing_secret() { let keypair = test_keypair(); let mut json = cli_json(&keypair); let mut base58 = keypair.to_base58_string(); let json_inspection = crate::inspect_wallet_transfer(json.as_slice(), crate::WalletTransferFormat::SolanaCliJson).expect("Solana CLI JSON inspection must succeed"); let base58_inspection = crate::inspect_wallet_transfer(base58.as_bytes(), crate::WalletTransferFormat::SolanaKeypairBase58).expect("canonical Base58 inspection must succeed"); assert_eq!(json_inspection.pubkey(), base58_inspection.pubkey()); assert_eq!(json_inspection.format(), crate::WalletTransferFormat::SolanaCliJson); assert_eq!(base58_inspection.format(), crate::WalletTransferFormat::SolanaKeypairBase58); assert!(!format!("{json_inspection:?}").contains(base58.as_str())); json.zeroize(); base58.zeroize(); } #[test] fn external_seed7_transfer_canary_matches_known_solana_keypair_encodings() { let expected_keypair_bytes = [ 7_u8, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 234, 74, 108, 99, 226, 156, 82, 10, 190, 245, 80, 123, 19, 46, 197, 249, 149, 71, 118, 174, 190, 190, 123, 146, 66, 30, 234, 105, 20, 70, 210, 44, ]; let expected_pubkey = "GmaDrppBC7P5ARKV8g3djiwP89vz1jLK23V2GBjuAEGB"; let expected_base58 = "99eUso3aSbE9tqGSTXzo3TLfKb9RkMTURrHKQ1K7Zh3StnzFNUx8FKCPPPPpR479qsw5zv2WNBKmgiz7WqgAJfM"; let keypair = test_keypair(); let mut actual_keypair_bytes = keypair.to_bytes(); assert_eq!(actual_keypair_bytes, expected_keypair_bytes); let mut actual_base58 = keypair.to_base58_string(); assert_eq!(actual_base58, expected_base58); let mut json = serde_json::to_vec(expected_keypair_bytes.as_slice()).expect("external keypair canary must encode as JSON"); let json_inspection = crate::inspect_wallet_transfer(json.as_slice(), crate::WalletTransferFormat::SolanaCliJson).expect("external Solana CLI JSON canary must inspect"); let base58_inspection = crate::inspect_wallet_transfer(expected_base58.as_bytes(), crate::WalletTransferFormat::SolanaKeypairBase58) .expect("external Base58 canary must inspect"); assert_eq!(json_inspection.pubkey().to_string(), expected_pubkey); assert_eq!(base58_inspection.pubkey().to_string(), expected_pubkey); json.zeroize(); actual_base58.zeroize(); actual_keypair_bytes.zeroize(); } #[test] fn transfer_decoders_reject_short_inconsistent_or_noncanonical_sources() { let short_json = b"[1,2,3]"; assert_eq!( crate::inspect_wallet_transfer(short_json, crate::WalletTransferFormat::SolanaCliJson).expect_err("short Solana CLI JSON must fail").code(), crate::ERROR_CODE_KEY_MATERIAL_INVALID ); let keypair = test_keypair(); let mut bytes = keypair.to_bytes(); bytes[63] ^= 0x01; let mut inconsistent_json = serde_json::to_vec(bytes.as_slice()).expect("inconsistent fixture must encode"); assert_eq!( crate::inspect_wallet_transfer(inconsistent_json.as_slice(), crate::WalletTransferFormat::SolanaCliJson) .expect_err("inconsistent keypair bytes must fail") .code(), crate::ERROR_CODE_KEY_MATERIAL_INVALID ); bytes.zeroize(); inconsistent_json.zeroize(); assert_eq!( crate::inspect_wallet_transfer(b"not valid base58 !!!", crate::WalletTransferFormat::SolanaKeypairBase58) .expect_err("invalid Base58 must fail") .code(), crate::ERROR_CODE_KEY_MATERIAL_INVALID ); let mut base58 = keypair.to_base58_string(); let surrounded = format!(" {base58}"); assert_eq!( crate::inspect_wallet_transfer(surrounded.as_bytes(), crate::WalletTransferFormat::SolanaKeypairBase58) .expect_err("surrounding whitespace must fail") .code(), crate::ERROR_CODE_KEY_MATERIAL_INVALID ); base58.zeroize(); } #[test] fn cli_json_import_creates_new_no_clobber_wallet_with_imported_identity_and_metadata() { let directory = tempfile::tempdir().expect("Wallet transfer test directory must be creatable"); let destination = directory.path().join("imported.kspwallet"); let keypair = test_keypair(); let mut source = cli_json(&keypair); let expected = crate::inspect_wallet_transfer(source.as_slice(), crate::WalletTransferFormat::SolanaCliJson).expect("source inspection must succeed"); let runtime = runtime(); let owner_password = crate::OwnerPassword::new(std::string::String::from("pre008-owner-password")); let owner = runtime .block_on(crate::import_wallet_transfer_v1( destination.as_path(), source.as_slice(), crate::WalletTransferFormat::SolanaCliJson, owner_password, std::option::Option::None, crate::WalletCreateMetadataV1::new(std::option::Option::Some(std::string::String::from("imported-wallet")), std::vec::Vec::new()), )) .expect("Solana CLI JSON import must succeed"); assert_eq!(owner.pubkey(), expected.pubkey()); assert_eq!(owner.alias(), std::option::Option::Some("imported-wallet")); let second_password = crate::OwnerPassword::new(std::string::String::from("pre008-second-owner-password")); let second = runtime.block_on(crate::import_wallet_transfer_v1( destination.as_path(), source.as_slice(), crate::WalletTransferFormat::SolanaCliJson, second_password, std::option::Option::None, crate::WalletCreateMetadataV1::default(), )); assert_eq!(second.expect_err("import must never overwrite an existing native Wallet").code(), crate::ERROR_CODE_DESTINATION_EXISTS); source.zeroize(); } #[test] fn default_transfer_import_creates_v2_without_removing_explicit_v1_import() { let directory = tempfile::tempdir().expect("Wallet transfer test directory must be creatable"); let default_destination = directory.path().join("default-v2.kspwallet"); let explicit_v1_destination = directory.path().join("explicit-v1.kspwallet"); let keypair = test_keypair(); let mut source = cli_json(&keypair); let runtime = runtime(); let default_owner = runtime .block_on(crate::import_wallet_transfer( default_destination.as_path(), source.as_slice(), crate::WalletTransferFormat::SolanaCliJson, crate::OwnerPassword::new(std::string::String::from("pre016-default-import-owner")), std::option::Option::None, crate::WalletCreateMetadata::default(), )) .expect("default transfer import must succeed"); assert_eq!(default_owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2); let default_bytes = std::fs::read(default_destination).expect("default imported Wallet must be readable"); assert_eq!(crate::detect_wallet_format(default_bytes.as_slice()).expect("default imported Wallet must detect"), crate::WalletFormat::V2); let explicit_owner = runtime .block_on(crate::import_wallet_transfer_v1( explicit_v1_destination.as_path(), source.as_slice(), crate::WalletTransferFormat::SolanaCliJson, crate::OwnerPassword::new(std::string::String::from("pre016-explicit-v1-import-owner")), std::option::Option::None, crate::WalletCreateMetadataV1::default(), )) .expect("explicit V1 transfer import must remain available"); assert_eq!(explicit_owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V1); source.zeroize(); } #[test] fn transfer_file_import_is_non_destructive_and_bounded() { let directory = tempfile::tempdir().expect("Wallet transfer test directory must be creatable"); let source_path = directory.path().join("source.json"); let destination = directory.path().join("imported.kspwallet"); let keypair = test_keypair(); let mut source = cli_json(&keypair); std::fs::write(source_path.as_path(), source.as_slice()).expect("transfer fixture must be writable"); let original = std::fs::read(source_path.as_path()).expect("transfer fixture must be readable"); let runtime = runtime(); let owner_password = crate::OwnerPassword::new(std::string::String::from("pre008-file-owner-password")); runtime .block_on(crate::import_wallet_transfer_file_v1( destination.as_path(), source_path.as_path(), crate::WalletTransferFormat::SolanaCliJson, owner_password, std::option::Option::None, crate::WalletCreateMetadataV1::default(), )) .expect("transfer-file import must succeed"); assert_eq!(std::fs::read(source_path.as_path()).expect("source must survive import"), original); let oversized_path = directory.path().join("oversized.json"); std::fs::write(oversized_path.as_path(), std::vec![b'1'; crate::KSPWALLET_TRANSFER_MAX_SOLANA_CLI_JSON_BYTES + 1]) .expect("oversized transfer fixture must be writable"); let error = runtime .block_on(crate::inspect_wallet_transfer_file(oversized_path.as_path(), crate::WalletTransferFormat::SolanaCliJson)) .expect_err("oversized transfer source must fail before parse"); assert_eq!(error.code(), crate::ERROR_CODE_KEY_MATERIAL_INVALID); source.zeroize(); } #[test] fn owner_exports_cli_json_and_base58_that_roundtrip_to_the_same_imported_keypair() { let directory = tempfile::tempdir().expect("Wallet transfer test directory must be creatable"); let destination = directory.path().join("imported.kspwallet"); let keypair = test_keypair(); let mut source_base58 = keypair.to_base58_string(); let mut expected_bytes = keypair.to_bytes(); let runtime = runtime(); let owner_password = crate::OwnerPassword::new(std::string::String::from("pre008-export-owner-password")); let owner = runtime .block_on(crate::import_wallet_transfer_v1( destination.as_path(), source_base58.as_bytes(), crate::WalletTransferFormat::SolanaKeypairBase58, owner_password, std::option::Option::None, crate::WalletCreateMetadataV1::default(), )) .expect("Base58 import must succeed"); let mut exported_json = owner.export_transfer(crate::WalletTransferFormat::SolanaCliJson).expect("OWNER JSON export must succeed"); let mut decoded_json: std::vec::Vec = serde_json::from_slice(exported_json.as_slice()).expect("OWNER JSON export must be valid JSON"); assert_eq!(decoded_json.as_slice(), expected_bytes.as_slice()); let mut exported_base58 = owner.export_transfer(crate::WalletTransferFormat::SolanaKeypairBase58).expect("OWNER Base58 export must succeed"); assert_eq!(exported_base58.as_slice(), source_base58.as_bytes()); exported_json.zeroize(); decoded_json.zeroize(); exported_base58.zeroize(); source_base58.zeroize(); expected_bytes.zeroize(); } #[test] fn owner_transfer_file_export_is_no_clobber() { let directory = tempfile::tempdir().expect("Wallet transfer test directory must be creatable"); let wallet_path = directory.path().join("wallet.kspwallet"); let export_path = directory.path().join("keypair.json"); let runtime = runtime(); let owner_password = crate::OwnerPassword::new(std::string::String::from("pre008-file-export-password")); let owner = runtime .block_on(crate::create_wallet_file_v1(wallet_path.as_path(), owner_password, std::option::Option::None, crate::WalletCreateMetadataV1::default())) .expect("native Wallet creation must succeed"); runtime .block_on(owner.export_transfer_file(export_path.as_path(), crate::WalletTransferFormat::SolanaCliJson)) .expect("first OWNER transfer-file export must succeed"); let first = std::fs::read(export_path.as_path()).expect("exported transfer file must be readable"); let second = runtime.block_on(owner.export_transfer_file(export_path.as_path(), crate::WalletTransferFormat::SolanaCliJson)); assert_eq!(second.expect_err("OWNER transfer-file export must not overwrite").code(), crate::ERROR_CODE_DESTINATION_EXISTS); assert_eq!(std::fs::read(export_path.as_path()).expect("first export must remain intact"), first); #[cfg(unix)] { let mode = std::fs::metadata(export_path.as_path()).expect("export metadata must be readable").permissions().mode() & 0o777; assert_eq!(mode, 0o600); } }