v0.5.2-pre.005
This commit is contained in:
279
ks-wallet/tests/transfer.rs
Normal file
279
ks-wallet/tests/transfer.rs
Normal file
@@ -0,0 +1,279 @@
|
||||
// file: ks-wallet/tests/transfer.rs
|
||||
// version: 1
|
||||
|
||||
//! 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());
|
||||
}
|
||||
Reference in New Issue
Block a user