use tempfile::NamedTempFile;
use webylib::hd::{ChainCode, HDWallet};
use webylib::webcash::{SecretWebcash, SecureString};
use webylib::{Amount, Result, Wallet};
async fn create_test_wallet() -> Result<Wallet> {
let temp_file = NamedTempFile::new().unwrap();
let wallet_path = temp_file.path().with_extension("db");
Wallet::open(&wallet_path).await
}
#[tokio::test]
async fn test_wallet_creation() {
let wallet = create_test_wallet().await.unwrap();
assert!(wallet.path().exists());
let stats = wallet.stats().await.unwrap();
assert_eq!(stats.total_webcash, 0);
assert_eq!(stats.unspent_webcash, 0);
assert_eq!(stats.spent_webcash, 0);
assert_eq!(stats.total_balance, Amount::ZERO);
}
#[tokio::test]
async fn test_derive_next_secret_increments_depth() {
let wallet = create_test_wallet().await.unwrap();
let (secret0, depth0) = wallet.derive_next_secret(ChainCode::Receive).unwrap();
assert_eq!(depth0, 0);
assert_eq!(secret0.len(), 64);
let (secret1, depth1) = wallet.derive_next_secret(ChainCode::Receive).unwrap();
assert_eq!(depth1, 1);
assert_ne!(secret0, secret1);
let (secret2, depth2) = wallet.derive_next_secret(ChainCode::Receive).unwrap();
assert_eq!(depth2, 2);
let master_hex = wallet.master_secret_hex().unwrap();
let master_bytes = hex::decode(&master_hex).unwrap();
let mut master_arr = [0u8; 32];
master_arr.copy_from_slice(&master_bytes);
let hd = HDWallet::from_master_secret(master_arr);
assert_eq!(hd.derive_secret(ChainCode::Receive, 0), secret0);
assert_eq!(hd.derive_secret(ChainCode::Receive, 1), secret1);
assert_eq!(hd.derive_secret(ChainCode::Receive, 2), secret2);
}
#[tokio::test]
async fn test_depth_uses_walletdepths_not_row_count() {
let wallet = create_test_wallet().await.unwrap();
let master_hex = wallet.master_secret_hex().unwrap();
let master_bytes = hex::decode(&master_hex).unwrap();
let mut master_arr = [0u8; 32];
master_arr.copy_from_slice(&master_bytes);
let hd = HDWallet::from_master_secret(master_arr);
for _ in 0..3 {
wallet.derive_next_secret(ChainCode::Receive).unwrap();
}
for i in 0..10u64 {
let secret = hd.derive_secret(ChainCode::Mining, i);
let wc = SecretWebcash::new(SecureString::new(secret), Amount::from_wats(100));
wallet.store_directly(wc).await.unwrap();
}
let stats = wallet.stats().await.unwrap();
assert_eq!(stats.total_webcash, 10, "10 rows in unspent_outputs");
let (secret, depth) = wallet.derive_next_secret(ChainCode::Receive).unwrap();
assert_eq!(depth, 3, "Must use walletdepths, not COUNT(*)");
assert_eq!(
secret,
hd.derive_secret(ChainCode::Receive, 3),
"Derived secret must match depth 3"
);
}
#[test]
fn test_hd_derivation_reference_vector() {
let master = [1u8; 32];
let hd = HDWallet::from_master_secret(master);
let receive_0 = hd.derive_secret(ChainCode::Receive, 0);
let receive_1 = hd.derive_secret(ChainCode::Receive, 1);
let pay_0 = hd.derive_secret(ChainCode::Pay, 0);
let change_0 = hd.derive_secret(ChainCode::Change, 0);
let mining_0 = hd.derive_secret(ChainCode::Mining, 0);
assert_eq!(receive_0.len(), 64);
assert_eq!(receive_1.len(), 64);
assert_ne!(receive_0, receive_1);
assert_ne!(receive_0, pay_0);
assert_ne!(receive_0, change_0);
assert_ne!(receive_0, mining_0);
let hd2 = HDWallet::from_master_secret([1u8; 32]);
assert_eq!(hd2.derive_secret(ChainCode::Receive, 0), receive_0);
assert_eq!(hd2.derive_secret(ChainCode::Pay, 0), pay_0);
assert_eq!(
receive_0,
hd.derive_secret(ChainCode::Receive, 0),
"RECEIVE/0 must be deterministic"
);
let tag = webylib::crypto::sha256(b"webcashwalletv1");
assert_eq!(tag.len(), 32, "tag must be 32 bytes");
}
#[tokio::test]
async fn test_chain_depth_independence() {
let wallet = create_test_wallet().await.unwrap();
let (_, rd) = wallet.derive_next_secret(ChainCode::Receive).unwrap();
assert_eq!(rd, 0);
let (_, cd) = wallet.derive_next_secret(ChainCode::Change).unwrap();
assert_eq!(cd, 0);
let (_, rd2) = wallet.derive_next_secret(ChainCode::Receive).unwrap();
assert_eq!(rd2, 1);
let (_, md) = wallet.derive_next_secret(ChainCode::Mining).unwrap();
assert_eq!(md, 0);
}
#[tokio::test]
async fn test_store_directly_and_recovery_determinism() {
let wallet = create_test_wallet().await.unwrap();
let master_hex = wallet.master_secret_hex().unwrap();
let master_bytes = hex::decode(&master_hex).unwrap();
let mut master_arr = [0u8; 32];
master_arr.copy_from_slice(&master_bytes);
let hd = HDWallet::from_master_secret(master_arr);
for depth in 0..3u64 {
let secret_hex = hd.derive_secret(ChainCode::Receive, depth);
let wc = SecretWebcash::new(
SecureString::new(secret_hex),
Amount::from_wats(1000 * (depth as i64 + 1)),
);
wallet.store_directly(wc).await.unwrap();
}
let balance = wallet.balance_amount().await.unwrap();
assert_eq!(balance, Amount::from_wats(6000));
let temp2 = NamedTempFile::new().unwrap();
let path2 = temp2.path().with_extension("db");
let wallet2 = Wallet::open_with_seed(&path2, &master_arr).await.unwrap();
let hd2_master_hex = wallet2.master_secret_hex().unwrap();
assert_eq!(hd2_master_hex, master_hex);
let master_bytes2 = hex::decode(&hd2_master_hex).unwrap();
let mut master_arr2 = [0u8; 32];
master_arr2.copy_from_slice(&master_bytes2);
let hd2 = HDWallet::from_master_secret(master_arr2);
for depth in 0..3u64 {
let s1 = hd.derive_secret(ChainCode::Receive, depth);
let s2 = hd2.derive_secret(ChainCode::Receive, depth);
assert_eq!(s1, s2, "Secret at depth {} must be identical", depth);
}
}