Files
khadhroony-bot3/ks-wallet/tests/transfer.rs

319 lines
14 KiB
Rust

// file: ks-wallet/tests/transfer.rs
// version: 2
//! External migration and secret import/export contract tests.
use solana_signer::Signer; // rust-rules: trait-import
use zeroize::Zeroize; // rust-rules: trait-import
fn make_directory_private(path: &std::path::Path) {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
.unwrap_or_else(|error| panic!("fixture directory permissions must be set: {error}"));
}
}
fn write_private_file(path: &std::path::Path, bytes: &[u8]) {
use std::io::Write; // rust-rules: trait-import
let mut options = std::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt; // rust-rules: trait-import
options.mode(0o600);
}
let mut file = options
.open(path)
.unwrap_or_else(|error| panic!("private fixture must be created: {error}"));
file.write_all(bytes)
.unwrap_or_else(|error| panic!("private fixture must be written: {error}"));
file.sync_all()
.unwrap_or_else(|error| panic!("private fixture must be synchronized: {error}"));
}
fn password(value: &str) -> ks_wallet::WalletPassword {
return ks_wallet::WalletPassword::new(value.to_owned())
.unwrap_or_else(|error| panic!("test password must be accepted: {error}"));
}
fn alias(value: &str) -> ks_wallet::WalletAlias {
return ks_wallet::WalletAlias::parse(value)
.unwrap_or_else(|error| panic!("test alias must be accepted: {error}"));
}
#[tokio::test]
async fn solana_cli_json_import_export_roundtrip_preserves_exact_keypair() {
let directory = tempfile::tempdir()
.unwrap_or_else(|error| panic!("temporary directory must exist: {error}"));
make_directory_private(directory.path());
let manager = ks_wallet::WalletManager::new(directory.path())
.unwrap_or_else(|error| panic!("manager must be created: {error}"));
let keypair = solana_keypair::Keypair::new();
let expected_public_key = keypair.pubkey().to_string();
let mut source_keypair_bytes = keypair.to_bytes();
let mut source_json = serde_json::to_vec(source_keypair_bytes.as_slice())
.unwrap_or_else(|error| panic!("Solana JSON fixture must encode: {error}"));
let source_path = directory.path().join("solana-cli-source.json");
write_private_file(&source_path, source_json.as_slice());
source_json.zeroize();
let imported = manager
.import_file(
alias("cli-import"),
password("cli-import-password"),
&source_path,
ks_wallet::WalletTransferFormat::SolanaCliJson,
)
.await
.unwrap_or_else(|error| panic!("Solana CLI import must succeed: {error}"));
assert_eq!(imported.public_key(), expected_public_key);
imported.lock();
let export_path = directory.path().join("solana-cli-export.json");
manager
.export_file(
&alias("cli-import"),
password("cli-import-password"),
&export_path,
ks_wallet::WalletTransferFormat::SolanaCliJson,
)
.await
.unwrap_or_else(|error| panic!("Solana CLI export must succeed: {error}"));
let mut exported = std::fs::read(&export_path)
.unwrap_or_else(|error| panic!("Solana CLI export must be readable: {error}"));
let mut decoded = serde_json::from_slice::<std::vec::Vec<u8>>(exported.as_slice())
.unwrap_or_else(|error| panic!("Solana CLI export must decode: {error}"));
exported.zeroize();
assert_eq!(decoded.as_slice(), source_keypair_bytes.as_slice());
decoded.zeroize();
source_keypair_bytes.zeroize();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import
let metadata = std::fs::metadata(&export_path)
.unwrap_or_else(|error| panic!("export metadata must be readable: {error}"));
assert_eq!(metadata.permissions().mode() & 0o777, 0o600);
}
let overwrite_error = manager
.export_file(
&alias("cli-import"),
password("cli-import-password"),
&export_path,
ks_wallet::WalletTransferFormat::SolanaCliJson,
)
.await
.err()
.unwrap_or_else(|| panic!("secret export must refuse an existing destination"));
assert_eq!(overwrite_error.code(), "wallet_export_already_exists");
}
#[tokio::test]
async fn base58_private_key_adapter_roundtrips_phantom_style_keypair() {
let directory = tempfile::tempdir()
.unwrap_or_else(|error| panic!("temporary directory must exist: {error}"));
make_directory_private(directory.path());
let manager = ks_wallet::WalletManager::new(directory.path())
.unwrap_or_else(|error| panic!("manager must be created: {error}"));
let keypair = solana_keypair::Keypair::new();
let expected_public_key = keypair.pubkey().to_string();
let mut keypair_bytes = keypair.to_bytes();
let mut base58_private_key = bs58::encode(keypair_bytes.as_slice()).into_string();
let source_path = directory.path().join("phantom-private-key.txt");
write_private_file(&source_path, base58_private_key.as_bytes());
let imported = manager
.import_file(
alias("phantom-import"),
password("phantom-import-password"),
&source_path,
ks_wallet::WalletTransferFormat::SolanaPrivateKeyBase58,
)
.await
.unwrap_or_else(|error| panic!("Base58 private-key import must succeed: {error}"));
assert_eq!(imported.public_key(), expected_public_key);
imported.lock();
let export_path = directory.path().join("phantom-private-key-export.txt");
manager
.export_file(
&alias("phantom-import"),
password("phantom-import-password"),
&export_path,
ks_wallet::WalletTransferFormat::SolanaPrivateKeyBase58,
)
.await
.unwrap_or_else(|error| panic!("Base58 private-key export must succeed: {error}"));
let mut exported = std::fs::read_to_string(&export_path)
.unwrap_or_else(|error| panic!("Base58 export must be readable: {error}"));
assert_eq!(exported, base58_private_key);
exported.zeroize();
base58_private_key.zeroize();
keypair_bytes.zeroize();
}
#[tokio::test]
async fn legacy_migration_is_non_destructive_and_refuses_second_destination() {
let directory = tempfile::tempdir()
.unwrap_or_else(|error| panic!("temporary directory must exist: {error}"));
make_directory_private(directory.path());
let manager = ks_wallet::WalletManager::new(directory.path())
.unwrap_or_else(|error| panic!("manager must be created: {error}"));
let keypair = solana_keypair::Keypair::new();
let expected_public_key = keypair.pubkey().to_string();
let mut keypair_bytes = keypair.to_bytes();
let mut legacy_json = serde_json::to_vec(keypair_bytes.as_slice())
.unwrap_or_else(|error| panic!("legacy fixture must encode: {error}"));
let source_path = directory.path().join("legacy-migrate.json");
write_private_file(&source_path, legacy_json.as_slice());
let mut original_source = std::fs::read(&source_path)
.unwrap_or_else(|error| panic!("legacy fixture must be readable: {error}"));
let migrated = manager
.migrate_legacy(alias("legacy-migrate"), password("migration-password"))
.await
.unwrap_or_else(|error| panic!("legacy migration must succeed: {error}"));
assert_eq!(migrated.public_key(), expected_public_key);
migrated.lock();
let mut source_after = std::fs::read(&source_path)
.unwrap_or_else(|error| panic!("legacy source must remain readable: {error}"));
assert_eq!(source_after, original_source);
let error = manager
.migrate_legacy(alias("legacy-migrate"), password("second-migration-password"))
.await
.err()
.unwrap_or_else(|| panic!("second migration must refuse the native alias collision"));
assert_eq!(error.code(), "wallet_native_already_exists");
let mut source_after_error = std::fs::read(&source_path)
.unwrap_or_else(|read_error| panic!("legacy source must survive collision: {read_error}"));
assert_eq!(source_after_error, original_source);
source_after_error.zeroize();
source_after.zeroize();
original_source.zeroize();
legacy_json.zeroize();
keypair_bytes.zeroize();
}
#[tokio::test]
async fn import_rejects_duplicate_pubkey_and_export_requires_valid_password() {
let directory = tempfile::tempdir()
.unwrap_or_else(|error| panic!("temporary directory must exist: {error}"));
make_directory_private(directory.path());
let manager = ks_wallet::WalletManager::new(directory.path())
.unwrap_or_else(|error| panic!("manager must be created: {error}"));
let keypair = solana_keypair::Keypair::new();
let mut keypair_bytes = keypair.to_bytes();
let mut source_json = serde_json::to_vec(keypair_bytes.as_slice())
.unwrap_or_else(|error| panic!("duplicate fixture must encode: {error}"));
let source_path = directory.path().join("duplicate-source.json");
write_private_file(&source_path, source_json.as_slice());
source_json.zeroize();
let first = manager
.import_file(
alias("duplicate-a"),
password("duplicate-password"),
&source_path,
ks_wallet::WalletTransferFormat::SolanaCliJson,
)
.await
.unwrap_or_else(|error| panic!("first import must succeed: {error}"));
first.lock();
let duplicate_error = manager
.import_file(
alias("duplicate-b"),
password("unused-duplicate-password"),
&source_path,
ks_wallet::WalletTransferFormat::SolanaCliJson,
)
.await
.err()
.unwrap_or_else(|| panic!("duplicate public key must be rejected"));
assert_eq!(duplicate_error.code(), "wallet_public_key_already_exists");
let export_path = directory.path().join("wrong-password-export.json");
let password_error = manager
.export_file(
&alias("duplicate-a"),
password("wrong-export-password"),
&export_path,
ks_wallet::WalletTransferFormat::SolanaCliJson,
)
.await
.err()
.unwrap_or_else(|| panic!("wrong export password must fail"));
assert_eq!(password_error.code(), "wallet_native_authentication_failed");
assert!(!export_path.exists());
keypair_bytes.zeroize();
}
#[tokio::test]
async fn import_rejects_invalid_sources_without_creating_native_destination() {
let directory = tempfile::tempdir()
.unwrap_or_else(|error| panic!("temporary directory must exist: {error}"));
make_directory_private(directory.path());
let manager = ks_wallet::WalletManager::new(directory.path())
.unwrap_or_else(|error| panic!("manager must be created: {error}"));
let invalid_json_path = directory.path().join("invalid-source.json");
write_private_file(&invalid_json_path, b"[1,2,3]");
let json_error = manager
.import_file(
alias("invalid-json"),
password("invalid-json-password"),
&invalid_json_path,
ks_wallet::WalletTransferFormat::SolanaCliJson,
)
.await
.err()
.unwrap_or_else(|| panic!("short Solana JSON import must fail"));
assert_eq!(json_error.code(), "wallet_import_keypair_length_invalid");
assert!(!directory.path().join("invalid-json.kswallet").exists());
let invalid_base58_path = directory.path().join("invalid-base58.txt");
write_private_file(&invalid_base58_path, b"not valid base58 !!!");
let base58_error = manager
.import_file(
alias("invalid-base58"),
password("invalid-base58-password"),
&invalid_base58_path,
ks_wallet::WalletTransferFormat::SolanaPrivateKeyBase58,
)
.await
.err()
.unwrap_or_else(|| panic!("invalid Base58 import must fail"));
assert_eq!(base58_error.code(), "wallet_import_base58_invalid");
assert!(!directory.path().join("invalid-base58.kswallet").exists());
}
#[tokio::test]
async fn transfer_inspection_exposes_only_public_identity_for_supported_formats() {
let directory = tempfile::tempdir()
.unwrap_or_else(|error| panic!("temporary directory must exist: {error}"));
make_directory_private(directory.path());
let keypair = solana_keypair::Keypair::new();
let expected_public_key = keypair.pubkey().to_string();
let mut keypair_bytes = keypair.to_bytes();
let mut json = serde_json::to_vec(keypair_bytes.as_slice())
.unwrap_or_else(|error| panic!("Solana JSON fixture must encode: {error}"));
let json_path = directory.path().join("inspect.json");
write_private_file(&json_path, json.as_slice());
let json_inspection = ks_wallet::inspect_transfer_file(
&json_path,
ks_wallet::WalletTransferFormat::SolanaCliJson,
)
.await
.unwrap_or_else(|error| panic!("Solana JSON inspection must succeed: {error}"));
assert_eq!(json_inspection.public_key(), expected_public_key);
assert_eq!(json_inspection.format().code(), "solana_cli_json");
json.zeroize();
let mut base58 = bs58::encode(keypair_bytes.as_slice()).into_string();
let base58_path = directory.path().join("inspect.txt");
write_private_file(&base58_path, base58.as_bytes());
let base58_inspection = ks_wallet::inspect_transfer_file(
&base58_path,
ks_wallet::WalletTransferFormat::SolanaPrivateKeyBase58,
)
.await
.unwrap_or_else(|error| panic!("Base58 inspection must succeed: {error}"));
assert_eq!(base58_inspection.public_key(), expected_public_key);
assert_eq!(base58_inspection.format().code(), "solana_private_key_base58");
let formats = ks_wallet::WalletTransferFormat::supported();
assert_eq!(formats.len(), 2);
assert_eq!(formats[0].default_extension(), "json");
assert_eq!(formats[1].default_extension(), "txt");
base58.zeroize();
keypair_bytes.zeroize();
}