233 lines
12 KiB
Rust
233 lines
12 KiB
Rust
// file: crates/ksp-wallet-lib/unit_tests/transfer.rs
|
|
// version: 3
|
|
|
|
#[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<u8> {
|
|
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 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<u8> = 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);
|
|
}
|
|
}
|