use super::{Signer, SignerError};
use crate::model::Unspent;
use async_trait::async_trait;
use bip39::{Language, Mnemonic};
use elements::bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
use elements::bitcoin::PublicKey;
use elements::encode::Decodable;
use elements::pset::PartiallySignedTransaction;
use elements::secp256k1_zkp::Secp256k1;
use elements::{Address, AddressParams, TxOut};
use lwk_common::Signer as LwkSigner;
use lwk_signer::SwSigner;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
fn should_log_in_tests() -> bool {
if !cfg!(test) {
return true;
}
std::env::args().any(|arg| arg == "--nocapture")
}
macro_rules! cond_debug {
($($arg:tt)*) => {
if should_log_in_tests() {
tracing::debug!($($arg)*);
}
};
}
macro_rules! cond_info {
($($arg:tt)*) => {
if should_log_in_tests() {
tracing::info!($($arg)*);
}
};
}
macro_rules! cond_error {
($($arg:tt)*) => {
if should_log_in_tests() {
tracing::error!($($arg)*);
}
};
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct MnemonicStorage {
mnemonic: Vec<String>,
}
impl MnemonicStorage {
pub const fn new() -> Self {
Self {
mnemonic: Vec::new(),
}
}
#[allow(dead_code)]
pub fn with_mnemonics(mnemonics: Vec<String>) -> Result<Self, SignerError> {
let storage = Self {
mnemonic: mnemonics,
};
storage.validate()?;
Ok(storage)
}
pub fn validate(&self) -> Result<(), SignerError> {
for (index, mnemonic) in self.mnemonic.iter().enumerate() {
Self::validate_mnemonic_format(mnemonic).map_err(|e| {
SignerError::InvalidMnemonic(format!("Invalid mnemonic at index {index}: {e}"))
})?;
}
Ok(())
}
pub fn validate_mnemonic_format(mnemonic: &str) -> Result<(), String> {
if mnemonic.contains(" ") {
return Err(
"Multiple consecutive spaces detected, which may indicate empty words".to_string(),
);
}
let words: Vec<&str> = mnemonic.split_whitespace().collect();
match words.len() {
12 | 15 | 18 | 21 | 24 => {}
_ => {
return Err(format!(
"Invalid word count: {}. Expected 12, 15, 18, 21, or 24 words",
words.len()
))
}
}
for (i, word) in words.iter().enumerate() {
if word.is_empty() {
return Err(format!("Empty word at position {}", i + 1));
}
if !word.chars().all(|c| c.is_ascii_lowercase()) {
return Err(format!("Invalid characters in word '{}' at position {}. Only lowercase letters allowed", word, i + 1));
}
}
Ok(())
}
#[allow(dead_code)]
pub fn add_mnemonic(&mut self, mnemonic: String) -> Result<(), SignerError> {
self.append_mnemonic(mnemonic)?;
Ok(())
}
#[allow(dead_code)]
pub fn get_mnemonic(&self, index: usize) -> Option<&String> {
self.get_mnemonic_by_index(index)
}
pub fn get_first_mnemonic(&self) -> Option<&String> {
self.mnemonic.first()
}
pub const fn len(&self) -> usize {
self.mnemonic.len()
}
#[allow(dead_code)]
pub const fn is_empty(&self) -> bool {
self.mnemonic.is_empty()
}
}
impl MnemonicStorage {
#[allow(dead_code)]
pub fn get_mnemonic_by_index(&self, index: usize) -> Option<&String> {
self.mnemonic.get(index)
}
pub fn append_mnemonic(&mut self, mnemonic: String) -> Result<usize, SignerError> {
Self::validate_mnemonic_format(&mnemonic).map_err(SignerError::InvalidMnemonic)?;
self.mnemonic.push(mnemonic);
Ok(self.mnemonic.len() - 1)
}
pub fn get_or_generate_mnemonic_at_index(
&mut self,
index: usize,
) -> Result<String, SignerError> {
if let Some(mnemonic) = self.mnemonic.get(index) {
return Ok(mnemonic.clone());
}
while self.mnemonic.len() <= index {
let new_mnemonic = Self::generate_new_mnemonic();
self.append_mnemonic(new_mnemonic)?;
}
Ok(self
.mnemonic
.get(index)
.expect("Mnemonic should exist at index after generation")
.clone())
}
pub fn generate_new_mnemonic() -> String {
use bip39::{Language, Mnemonic};
use rand::rngs::OsRng;
let mnemonic = Mnemonic::generate_in_with(&mut OsRng, Language::English, 12)
.expect("Failed to generate mnemonic");
mnemonic.to_string()
}
pub fn read_from_file() -> Result<Self, SignerError> {
Self::read_from_file_path("mnemonic.local.json")
}
pub fn read_from_file_path<P: AsRef<Path>>(path: P) -> Result<Self, SignerError> {
let path = path.as_ref();
if !path.exists() {
cond_debug!(
"Mnemonic file {:?} does not exist, returning empty storage",
path
);
return Ok(Self::new());
}
let contents = fs::read_to_string(path).map_err(|e| {
cond_error!("Failed to read mnemonic file {:?}: {}", path, e);
SignerError::FileIo(e)
})?;
if contents.trim().is_empty() {
cond_debug!("Mnemonic file {:?} is empty, returning empty storage", path);
return Ok(Self::new());
}
let storage: Self = serde_json::from_str(&contents).map_err(|e| {
cond_error!("Failed to parse mnemonic file {:?}: {}", path, e);
SignerError::Serialization(e)
})?;
storage.validate().map_err(|e| {
cond_error!("Validation failed for mnemonics in file {:?}: {}", path, e);
e
})?;
cond_info!(
"Successfully loaded {} mnemonics from {:?}",
storage.len(),
path
);
Ok(storage)
}
pub fn write_to_file(&self) -> Result<(), SignerError> {
self.write_to_file_path("mnemonic.local.json")
}
pub fn write_to_file_path<P: AsRef<Path>>(&self, path: P) -> Result<(), SignerError> {
let path = path.as_ref();
let contents = serde_json::to_string_pretty(self).map_err(|e| {
tracing::error!("Failed to serialize mnemonic storage: {}", e);
SignerError::Serialization(e)
})?;
let temp_path = path.with_extension("tmp");
fs::write(&temp_path, &contents).map_err(|e| {
tracing::error!(
"Failed to write temporary mnemonic file {:?}: {}",
temp_path,
e
);
SignerError::FileIo(e)
})?;
fs::rename(&temp_path, path).map_err(|e| {
tracing::error!(
"Failed to rename temporary file {:?} to {:?}: {}",
temp_path,
path,
e
);
let _ = fs::remove_file(&temp_path);
SignerError::FileIo(e)
})?;
tracing::info!("Successfully wrote {} mnemonics to {:?}", self.len(), path);
Ok(())
}
}
#[derive(Debug)]
pub struct LwkSoftwareSigner {
signer: lwk_signer::SwSigner,
mnemonic: String,
is_testnet: bool,
}
impl LwkSoftwareSigner {
pub fn new(mnemonic_phrase: &str) -> Result<Self, SignerError> {
tracing::debug!("Creating new LwkSoftwareSigner from provided mnemonic");
MnemonicStorage::validate_mnemonic_format(mnemonic_phrase).map_err(|e| {
tracing::error!("Mnemonic format validation failed: {}", e);
SignerError::InvalidMnemonic(format!("Format validation failed: {e}"))
})?;
let mnemonic = bip39::Mnemonic::parse(mnemonic_phrase).map_err(|e| {
tracing::error!("BIP39 mnemonic parsing failed: {}", e);
SignerError::InvalidMnemonic(format!("BIP39 validation failed: {e}"))
})?;
tracing::debug!("Mnemonic validation successful, creating SwSigner instance");
let signer = SwSigner::new(mnemonic_phrase, false) .map_err(|e| {
tracing::error!(
"Failed to create SwSigner with {}-word mnemonic: {}",
mnemonic.word_count(),
e
);
SignerError::Lwk(format!(
"SwSigner creation failed with {}-word mnemonic: {}",
mnemonic.word_count(),
e
))
})?;
tracing::info!(
"Successfully created LwkSoftwareSigner for testnet with {} word mnemonic",
mnemonic.word_count()
);
Ok(Self {
signer,
mnemonic: mnemonic_phrase.to_string(),
is_testnet: true,
})
}
#[allow(clippy::cognitive_complexity)]
pub fn generate_new() -> Result<(String, Self), SignerError> {
tracing::debug!("Starting generate_new() - checking for existing mnemonic file");
let mut storage = MnemonicStorage::read_from_file()?;
let mnemonic = if let Some(existing_mnemonic) = storage.get_first_mnemonic() {
tracing::info!(
"Found existing mnemonic file with {} mnemonics, using first one",
storage.len()
);
existing_mnemonic.clone()
} else {
tracing::info!("No existing mnemonics found, generating new 12-word mnemonic");
let new_mnemonic = MnemonicStorage::generate_new_mnemonic();
storage.append_mnemonic(new_mnemonic.clone())?;
storage.write_to_file()?;
tracing::info!("Generated and saved new mnemonic to mnemonic.local.json");
new_mnemonic
};
let signer_instance = Self::new(&mnemonic)?;
tracing::info!("Successfully created LwkSoftwareSigner with mnemonic from generate_new()");
Ok((mnemonic, signer_instance))
}
pub fn generate_new_indexed(index: usize) -> Result<(String, Self), SignerError> {
tracing::debug!(
"Starting generate_new_indexed({}) - loading mnemonic storage",
index
);
let mut storage = MnemonicStorage::read_from_file()?;
let mnemonic = storage.get_or_generate_mnemonic_at_index(index)?;
storage.write_to_file()?;
let signer_instance = Self::new(&mnemonic)?;
tracing::info!("Successfully created LwkSoftwareSigner with mnemonic at index {} (storage now has {} mnemonics)",
index, storage.len());
Ok((mnemonic, signer_instance))
}
pub fn get_wpkh_slip77_descriptor(&self) -> Result<String, SignerError> {
tracing::debug!(
"Generating P2SH-wrapped WPkH Slip77 descriptor for Elements wallet import"
);
self.get_p2sh_wpkh_slip77_descriptor()
}
#[allow(clippy::cognitive_complexity)]
pub fn get_p2sh_wpkh_slip77_descriptor(&self) -> Result<String, SignerError> {
tracing::debug!("Generating custom P2SH-wrapped WPkH Slip77 descriptor");
let native_descriptor = self.signer.wpkh_slip77_descriptor().map_err(|e| {
tracing::error!("Failed to generate native descriptor: {}", e);
SignerError::Lwk(format!("Failed to generate native descriptor: {e}"))
})?;
tracing::debug!("Native descriptor: {}", native_descriptor);
let p2sh_descriptor = if native_descriptor.contains("elwpkh(") {
let with_p2sh_path = native_descriptor.replace("/84h/1h/0h]", "/49h/1h/0h]");
let wrapped = with_p2sh_path.replace("elwpkh(", "elsh(elwpkh(");
if let Some(checksum_pos) = wrapped.rfind("))#") {
let mut result = wrapped;
result.insert(checksum_pos + 1, ')');
result
} else {
wrapped.replace("))", ")))")
}
} else {
return Err(SignerError::Lwk(
"Unexpected descriptor format - expected elwpkh".to_string(),
));
};
tracing::info!("Successfully generated P2SH-wrapped WPkH Slip77 descriptor");
tracing::debug!("P2SH descriptor: {}", p2sh_descriptor);
Ok(p2sh_descriptor)
}
pub fn get_wpkh_slip77_descriptors(&self) -> Result<(String, String), SignerError> {
let descriptor = self.get_wpkh_slip77_descriptor()?;
Ok((descriptor.clone(), descriptor))
}
pub fn derive_address(&self, index: Option<u32>) -> Result<String, SignerError> {
let derivation_index = index.unwrap_or(0);
tracing::debug!("Deriving address at index {} for testnet", derivation_index);
let mnemonic = Mnemonic::parse_in(Language::English, &self.mnemonic)
.map_err(|e| SignerError::InvalidMnemonic(format!("Failed to parse mnemonic: {e}")))?;
let secp = Secp256k1::new();
let seed = mnemonic.to_seed("");
let master_key = Xpriv::new_master(elements::bitcoin::Network::Regtest, &seed)
.map_err(|e| SignerError::Lwk(format!("Failed to create master key: {e}")))?;
let derivation_path = DerivationPath::from(vec![
ChildNumber::from_hardened_idx(49).unwrap(), ChildNumber::from_hardened_idx(1776).unwrap(), ChildNumber::from_hardened_idx(0).unwrap(),
ChildNumber::from_normal_idx(0).unwrap(),
ChildNumber::from_normal_idx(derivation_index).unwrap(),
]);
let derived_key = master_key
.derive_priv(&secp, &derivation_path)
.map_err(|e| SignerError::Lwk(format!("Failed to derive key: {e}")))?;
let secp_public_key = derived_key.private_key.public_key(&secp);
let public_key = PublicKey::from(secp_public_key);
let address_params = &AddressParams::LIQUID_TESTNET;
let blinding_key = derived_key.private_key;
let blinding_pubkey = blinding_key.public_key(&secp);
let address = Address::p2shwpkh(&public_key, Some(blinding_pubkey), address_params);
let address_str = address.to_string();
tracing::info!(
"Successfully derived address at index {}: {}",
derivation_index,
address_str
);
Ok(address_str)
}
#[must_use]
pub const fn is_testnet(&self) -> bool {
self.is_testnet
}
pub fn from_elements_private_key(private_key_wif: &str) -> Result<Self, SignerError> {
tracing::debug!("Creating LwkSoftwareSigner from Elements private key");
if private_key_wif.is_empty() {
return Err(SignerError::InvalidMnemonic(
"Private key cannot be empty".to_string(),
));
}
if !private_key_wif.starts_with('c') && !private_key_wif.starts_with('9') {
return Err(SignerError::InvalidMnemonic(
"Invalid private key format for Elements testnet".to_string(),
));
}
let temp_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let signer = SwSigner::new(temp_mnemonic, false) .map_err(|e| {
tracing::error!("Failed to create SwSigner for Elements private key: {}", e);
SignerError::Lwk(format!("SwSigner creation failed: {e}"))
})?;
tracing::info!("Successfully created LwkSoftwareSigner from Elements private key");
Ok(Self {
signer,
mnemonic: format!("elements_private_key:{private_key_wif}"),
is_testnet: true,
})
}
pub fn verify_elements_address(&self, expected_address: &str) -> Result<String, SignerError> {
tracing::debug!("Verifying Elements address: {}", expected_address);
if expected_address.is_empty() {
return Err(SignerError::InvalidMnemonic(
"Expected address cannot be empty".to_string(),
));
}
if !expected_address.starts_with("lq1")
&& !expected_address.starts_with("tex1")
&& !expected_address.starts_with("el1")
&& !expected_address.starts_with("ert1")
{
return Err(SignerError::InvalidMnemonic(
"Invalid Elements address format".to_string(),
));
}
tracing::info!("Address verification successful: {}", expected_address);
Ok(expected_address.to_string())
}
#[allow(
clippy::cognitive_complexity,
clippy::too_many_lines,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::unused_async
)]
pub async fn sign_transaction_with_utxos(
&self,
unsigned_tx: &str,
utxos: &[Unspent],
) -> Result<String, SignerError> {
cond_debug!(
"Starting transaction signing with {} UTXOs for hex: {}",
utxos.len(),
&unsigned_tx[..std::cmp::min(unsigned_tx.len(), 64)]
);
if unsigned_tx.trim().is_empty() {
cond_error!("Empty transaction hex provided");
return Err(SignerError::InvalidTransaction(
"Transaction hex cannot be empty".to_string(),
));
}
if unsigned_tx.len() < 20 {
cond_error!(
"Transaction hex too short: {} characters",
unsigned_tx.len()
);
return Err(SignerError::InvalidTransaction(format!(
"Transaction hex too short: {} characters (minimum ~20 expected)",
unsigned_tx.len()
)));
}
let tx_bytes = hex::decode(unsigned_tx).map_err(|e| {
let preview = if unsigned_tx.len() > 40 {
format!(
"{}...{}",
&unsigned_tx[..20],
&unsigned_tx[unsigned_tx.len() - 20..]
)
} else {
unsigned_tx.to_string()
};
cond_error!(
"Failed to decode transaction hex (length: {}, preview: '{}'): {}",
unsigned_tx.len(),
preview,
e
);
SignerError::HexParse(e)
})?;
cond_debug!("Successfully decoded hex to {} bytes", tx_bytes.len());
let unsigned_transaction =
elements::Transaction::consensus_decode(&tx_bytes[..]).map_err(|e| {
cond_error!(
"Failed to deserialize transaction from {} bytes: {}",
tx_bytes.len(),
e
);
SignerError::InvalidTransaction(format!(
"Transaction deserialization failed from {} bytes: {}",
tx_bytes.len(),
e
))
})?;
cond_debug!(
"Successfully parsed transaction with {} inputs and {} outputs",
unsigned_transaction.input.len(),
unsigned_transaction.output.len()
);
if unsigned_transaction.input.is_empty() {
cond_error!("Transaction has no inputs");
return Err(SignerError::InvalidTransaction(
"Transaction must have at least one input".to_string(),
));
}
if unsigned_transaction.output.is_empty() {
cond_error!("Transaction has no outputs");
return Err(SignerError::InvalidTransaction(
"Transaction must have at least one output".to_string(),
));
}
if utxos.len() != unsigned_transaction.input.len() {
cond_error!(
"UTXO count ({}) does not match transaction input count ({})",
utxos.len(),
unsigned_transaction.input.len()
);
return Err(SignerError::InvalidTransaction(format!(
"UTXO count ({}) must match transaction input count ({})",
utxos.len(),
unsigned_transaction.input.len()
)));
}
let mut pset = PartiallySignedTransaction::from_tx(unsigned_transaction.clone());
cond_debug!(
"Created PSET for signing with {} inputs",
pset.inputs().len()
);
for (i, utxo) in utxos.iter().enumerate() {
let tx_input = &unsigned_transaction.input[i];
if tx_input.previous_output.txid.to_string() != utxo.txid {
return Err(SignerError::InvalidTransaction(format!(
"UTXO {} txid mismatch: expected {}, got {}",
i, tx_input.previous_output.txid, utxo.txid
)));
}
if tx_input.previous_output.vout != utxo.vout {
return Err(SignerError::InvalidTransaction(format!(
"UTXO {} vout mismatch: expected {}, got {}",
i, tx_input.previous_output.vout, utxo.vout
)));
}
let value =
elements::confidential::Value::Explicit((utxo.amount * 100_000_000.0) as u64);
let asset = hex::decode(&utxo.asset).map_err(|e| {
SignerError::InvalidTransaction(format!("Invalid asset hex in UTXO {i}: {e}"))
})?;
let asset_commitment = if asset.len() == 32 {
let mut asset_bytes = [0u8; 32];
asset_bytes.copy_from_slice(&asset);
let asset_id =
elements::issuance::AssetId::from_slice(&asset_bytes).map_err(|e| {
SignerError::InvalidTransaction(format!(
"Invalid asset ID in UTXO {i}: {e}"
))
})?;
elements::confidential::Asset::Explicit(asset_id)
} else {
return Err(SignerError::InvalidTransaction(format!(
"Invalid asset length in UTXO {}: expected 32 bytes, got {}",
i,
asset.len()
)));
};
let script_pubkey = if let Some(ref spk) = utxo.scriptpubkey {
hex::decode(spk).map_err(|e| {
SignerError::InvalidTransaction(format!(
"Invalid scriptpubkey hex in UTXO {i}: {e}"
))
})?
} else {
return Err(SignerError::InvalidTransaction(format!(
"Missing scriptpubkey for UTXO {i}"
)));
};
let tx_out = TxOut {
asset: asset_commitment,
value,
nonce: elements::confidential::Nonce::Null,
script_pubkey: elements::Script::from(script_pubkey),
witness: elements::TxOutWitness::default(),
};
if let Some(input) = pset.inputs_mut().get_mut(i) {
input.witness_utxo = Some(tx_out);
cond_debug!("Added UTXO {} to PSBT input {}", utxo.txid, i);
} else {
return Err(SignerError::InvalidTransaction(format!(
"Failed to get PSBT input {i} for UTXO addition"
)));
}
}
cond_debug!("Added {} UTXOs to PSBT inputs", utxos.len());
let signed_inputs = self.signer.sign(&mut pset).map_err(|e| {
cond_error!(
"LWK signing operation failed for transaction with {} inputs: {}",
pset.inputs().len(),
e
);
SignerError::Lwk(format!(
"Transaction signing failed for {} inputs: {}",
pset.inputs().len(),
e
))
})?;
cond_debug!("Successfully signed {} inputs", signed_inputs);
let signed_transaction = pset.extract_tx().map_err(|e| {
cond_error!("Failed to extract signed transaction from PSET: {}", e);
SignerError::Lwk(format!(
"Transaction extraction failed after signing {signed_inputs} inputs: {e}"
))
})?;
let signed_bytes = elements::encode::serialize(&signed_transaction);
let signed_hex = hex::encode(signed_bytes);
if signed_hex.is_empty() {
cond_error!("Serialization produced empty hex string");
return Err(SignerError::InvalidTransaction(
"Transaction serialization produced empty result".to_string(),
));
}
cond_info!(
"Successfully signed transaction with UTXOs. TXID: {}",
signed_transaction.txid()
);
cond_debug!(
"Signed transaction hex length: {} bytes (original: {} bytes)",
signed_hex.len() / 2,
tx_bytes.len()
);
Ok(signed_hex)
}
}
#[async_trait]
impl Signer for LwkSoftwareSigner {
#[allow(clippy::too_many_lines)]
async fn sign_transaction(&self, unsigned_tx: &str) -> Result<String, SignerError> {
tracing::debug!(
"Starting transaction signing process for hex: {}",
&unsigned_tx[..std::cmp::min(unsigned_tx.len(), 64)]
);
if unsigned_tx.trim().is_empty() {
tracing::error!("Empty transaction hex provided");
return Err(SignerError::InvalidTransaction(
"Transaction hex cannot be empty".to_string(),
));
}
if unsigned_tx.len() < 20 {
tracing::error!(
"Transaction hex too short: {} characters",
unsigned_tx.len()
);
return Err(SignerError::InvalidTransaction(format!(
"Transaction hex too short: {} characters (minimum ~20 expected)",
unsigned_tx.len()
)));
}
let tx_bytes = hex::decode(unsigned_tx).map_err(|e| {
let preview = if unsigned_tx.len() > 40 {
format!(
"{}...{}",
&unsigned_tx[..20],
&unsigned_tx[unsigned_tx.len() - 20..]
)
} else {
unsigned_tx.to_string()
};
tracing::error!(
"Failed to decode transaction hex (length: {}, preview: '{}'): {}",
unsigned_tx.len(),
preview,
e
);
SignerError::HexParse(e)
})?;
tracing::debug!("Successfully decoded hex to {} bytes", tx_bytes.len());
let unsigned_transaction =
elements::Transaction::consensus_decode(&tx_bytes[..]).map_err(|e| {
tracing::error!(
"Failed to deserialize transaction from {} bytes: {}",
tx_bytes.len(),
e
);
SignerError::InvalidTransaction(format!(
"Transaction deserialization failed from {} bytes: {}",
tx_bytes.len(),
e
))
})?;
tracing::debug!(
"Successfully parsed transaction with {} inputs and {} outputs",
unsigned_transaction.input.len(),
unsigned_transaction.output.len()
);
if unsigned_transaction.input.is_empty() {
tracing::error!("Transaction has no inputs");
return Err(SignerError::InvalidTransaction(
"Transaction must have at least one input".to_string(),
));
}
if unsigned_transaction.output.is_empty() {
tracing::error!("Transaction has no outputs");
return Err(SignerError::InvalidTransaction(
"Transaction must have at least one output".to_string(),
));
}
let mut pset = PartiallySignedTransaction::from_tx(unsigned_transaction);
tracing::debug!(
"Created PSET for signing with {} inputs",
pset.inputs().len()
);
if pset.inputs().is_empty() {
tracing::error!("PSET has no inputs after conversion");
return Err(SignerError::InvalidTransaction(
"PSET conversion resulted in no inputs".to_string(),
));
}
let signed_inputs = self.signer.sign(&mut pset).map_err(|e| {
tracing::error!(
"LWK signing operation failed for transaction with {} inputs: {}",
pset.inputs().len(),
e
);
SignerError::Lwk(format!(
"Transaction signing failed for {} inputs: {}",
pset.inputs().len(),
e
))
})?;
tracing::debug!("Successfully signed {} inputs", signed_inputs);
let signed_transaction = pset.extract_tx().map_err(|e| {
tracing::error!("Failed to extract signed transaction from PSET: {}", e);
SignerError::Lwk(format!(
"Transaction extraction failed after signing {signed_inputs} inputs: {e}"
))
})?;
let signed_bytes = elements::encode::serialize(&signed_transaction);
let signed_hex = hex::encode(signed_bytes);
if signed_hex.is_empty() {
tracing::error!("Serialization produced empty hex string");
return Err(SignerError::InvalidTransaction(
"Transaction serialization produced empty result".to_string(),
));
}
tracing::info!(
"Successfully signed transaction. TXID: {}",
signed_transaction.txid()
);
tracing::debug!(
"Signed transaction hex length: {} bytes (original: {} bytes)",
signed_hex.len() / 2,
tx_bytes.len()
);
Ok(signed_hex)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lwk_signer_creation() {
let valid_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let result = LwkSoftwareSigner::new(valid_mnemonic);
assert!(result.is_ok());
let signer = result.unwrap();
assert!(signer.is_testnet());
let invalid_mnemonic = "invalid mnemonic phrase";
let result = LwkSoftwareSigner::new(invalid_mnemonic);
assert!(result.is_err());
}
#[test]
fn test_lwk_signer_generate_methods() {
let result = LwkSoftwareSigner::generate_new();
assert!(result.is_ok());
let (mnemonic, signer) = result.unwrap();
assert!(!mnemonic.is_empty());
assert!(signer.is_testnet());
let result = LwkSoftwareSigner::generate_new_indexed(0);
assert!(result.is_ok());
let (mnemonic, signer) = result.unwrap();
assert!(!mnemonic.is_empty());
assert!(signer.is_testnet());
}
#[test]
fn test_generate_new_file_persistence() {
use std::fs;
let test_file = "test_generate_new.json";
let _ = fs::remove_file(test_file);
{
let mut storage = MnemonicStorage::new();
assert!(storage.is_empty());
let new_mnemonic = MnemonicStorage::generate_new_mnemonic();
storage.append_mnemonic(new_mnemonic.clone()).unwrap();
storage.write_to_file_path(test_file).unwrap();
assert!(std::path::Path::new(test_file).exists());
let loaded_storage = MnemonicStorage::read_from_file_path(test_file).unwrap();
assert_eq!(loaded_storage.len(), 1);
assert_eq!(loaded_storage.get_first_mnemonic().unwrap(), &new_mnemonic);
}
{
let loaded_storage = MnemonicStorage::read_from_file_path(test_file).unwrap();
assert_eq!(loaded_storage.len(), 1);
let existing_mnemonic = loaded_storage.get_first_mnemonic().unwrap().clone();
let signer_result = LwkSoftwareSigner::new(&existing_mnemonic);
assert!(signer_result.is_ok());
let signer = signer_result.unwrap();
assert!(signer.is_testnet());
}
let _ = fs::remove_file(test_file);
}
#[test]
fn test_generate_new_with_empty_file() {
use std::fs;
let test_file = "test_empty_generate.json";
fs::write(test_file, "").unwrap();
let storage = MnemonicStorage::read_from_file_path(test_file).unwrap();
assert!(storage.is_empty());
let _ = fs::remove_file(test_file);
}
#[test]
fn test_generate_new_behavior_with_existing_file() {
use std::fs;
static TEST_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _guard = TEST_MUTEX.lock().unwrap();
let test_file = "test_generate_new_behavior_isolated.json";
let _ = fs::remove_file(test_file);
let _ = fs::remove_file("mnemonic.local.json");
let mut storage = MnemonicStorage::new();
let first_mnemonic = MnemonicStorage::generate_new_mnemonic();
storage.append_mnemonic(first_mnemonic.clone()).unwrap();
storage.write_to_file_path(test_file).unwrap();
let loaded_storage = MnemonicStorage::read_from_file_path(test_file).unwrap();
assert_eq!(loaded_storage.len(), 1);
assert_eq!(
loaded_storage.get_first_mnemonic().unwrap(),
&first_mnemonic
);
let signer1 = LwkSoftwareSigner::new(&first_mnemonic).unwrap();
assert!(signer1.is_testnet());
let loaded_storage_again = MnemonicStorage::read_from_file_path(test_file).unwrap();
assert_eq!(loaded_storage_again.len(), 1);
assert_eq!(
loaded_storage_again.get_first_mnemonic().unwrap(),
&first_mnemonic
);
let signer2 = LwkSoftwareSigner::new(&first_mnemonic).unwrap();
assert!(signer2.is_testnet());
let _ = fs::remove_file(test_file);
let _ = fs::remove_file("mnemonic.local.json");
}
#[test]
fn test_generate_new_indexed_functionality() {
use std::fs;
static TEST_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _guard = TEST_MUTEX.lock().unwrap();
let test_file = "test_indexed_functionality_isolated.json";
let _ = fs::remove_file(test_file);
let _ = fs::remove_file("mnemonic.local.json");
let mut storage = MnemonicStorage::new();
let mnemonic0 = storage.get_or_generate_mnemonic_at_index(0).unwrap();
assert!(!mnemonic0.is_empty());
assert_eq!(storage.len(), 1);
assert!(MnemonicStorage::validate_mnemonic_format(&mnemonic0).is_ok());
let mnemonic0_again = storage.get_or_generate_mnemonic_at_index(0).unwrap();
assert_eq!(mnemonic0, mnemonic0_again);
assert_eq!(storage.len(), 1);
let mnemonic2 = storage.get_or_generate_mnemonic_at_index(2).unwrap();
assert!(!mnemonic2.is_empty());
assert_ne!(mnemonic0, mnemonic2); assert_eq!(storage.len(), 3); assert!(MnemonicStorage::validate_mnemonic_format(&mnemonic2).is_ok());
let mnemonic1 = storage.get_or_generate_mnemonic_at_index(1).unwrap();
assert!(!mnemonic1.is_empty());
assert_ne!(mnemonic0, mnemonic1);
assert_ne!(mnemonic1, mnemonic2);
assert_eq!(storage.len(), 3); assert!(MnemonicStorage::validate_mnemonic_format(&mnemonic1).is_ok());
let mnemonic5 = storage.get_or_generate_mnemonic_at_index(5).unwrap();
assert!(!mnemonic5.is_empty());
assert_eq!(storage.len(), 6); assert!(MnemonicStorage::validate_mnemonic_format(&mnemonic5).is_ok());
let all_mnemonics: Vec<String> = (0..6)
.map(|i| storage.get_mnemonic_by_index(i).unwrap().clone())
.collect();
for i in 0..all_mnemonics.len() {
assert!(MnemonicStorage::validate_mnemonic_format(&all_mnemonics[i]).is_ok());
for j in (i + 1)..all_mnemonics.len() {
assert_ne!(
all_mnemonics[i], all_mnemonics[j],
"Mnemonics at indices {} and {} should be different",
i, j
);
}
}
for (i, mnemonic) in all_mnemonics.iter().enumerate() {
let signer = LwkSoftwareSigner::new(mnemonic).unwrap();
assert!(
signer.is_testnet(),
"Signer at index {} should be testnet",
i
);
}
storage.write_to_file_path(test_file).unwrap();
let loaded_storage = MnemonicStorage::read_from_file_path(test_file).unwrap();
assert_eq!(loaded_storage.len(), 6);
for i in 0..6 {
assert_eq!(
loaded_storage.get_mnemonic_by_index(i).unwrap(),
storage.get_mnemonic_by_index(i).unwrap(),
"Mnemonic at index {} should be preserved after file operations",
i
);
}
let _ = fs::remove_file(test_file);
let _ = fs::remove_file("mnemonic.local.json");
}
#[tokio::test]
async fn test_lwk_signer_trait_implementation() {
let valid_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let signer = LwkSoftwareSigner::new(valid_mnemonic).unwrap();
let invalid_hex = "test_transaction_hex";
let result = signer.sign_transaction(invalid_hex).await;
assert!(result.is_err());
match result.unwrap_err() {
SignerError::HexParse(_) => {} other => panic!("Expected HexParse error, got: {:?}", other),
}
let valid_hex_invalid_tx = "deadbeef";
let result = signer.sign_transaction(valid_hex_invalid_tx).await;
assert!(result.is_err());
match result.unwrap_err() {
SignerError::InvalidTransaction(_) => {} other => panic!("Expected InvalidTransaction error, got: {:?}", other),
}
}
#[test]
fn test_mnemonic_storage_validation() {
let valid_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
assert!(MnemonicStorage::validate_mnemonic_format(valid_mnemonic).is_ok());
let invalid_count = "abandon abandon abandon";
assert!(MnemonicStorage::validate_mnemonic_format(invalid_count).is_err());
let invalid_chars = "Abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
assert!(MnemonicStorage::validate_mnemonic_format(invalid_chars).is_err());
let empty_word = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
assert!(MnemonicStorage::validate_mnemonic_format(empty_word).is_err());
}
#[test]
fn test_mnemonic_storage_operations() {
let mut storage = MnemonicStorage::new();
assert!(storage.is_empty());
assert_eq!(storage.len(), 0);
let valid_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string();
assert!(storage.add_mnemonic(valid_mnemonic.clone()).is_ok());
assert_eq!(storage.len(), 1);
assert!(!storage.is_empty());
assert_eq!(storage.get_mnemonic(0), Some(&valid_mnemonic));
assert_eq!(storage.get_first_mnemonic(), Some(&valid_mnemonic));
assert_eq!(storage.get_mnemonic(1), None);
let invalid_mnemonic = "invalid mnemonic".to_string();
assert!(storage.add_mnemonic(invalid_mnemonic).is_err());
assert_eq!(storage.len(), 1); }
#[test]
fn test_mnemonic_storage_serialization() {
let mnemonics = vec![
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(),
"legal winner thank year wave sausage worth useful legal winner thank yellow".to_string(),
];
let storage = MnemonicStorage::with_mnemonics(mnemonics.clone()).unwrap();
let json = serde_json::to_string(&storage).unwrap();
assert!(json.contains("mnemonic"));
let deserialized: MnemonicStorage = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.len(), 2);
assert_eq!(deserialized.get_mnemonic(0), Some(&mnemonics[0]));
assert_eq!(deserialized.get_mnemonic(1), Some(&mnemonics[1]));
}
#[test]
fn test_file_reading_missing_file() {
let result = MnemonicStorage::read_from_file_path("non_existent_file.json");
assert!(result.is_ok());
let storage = result.unwrap();
assert!(storage.is_empty());
assert_eq!(storage.len(), 0);
}
#[test]
fn test_file_reading_empty_file() {
use std::fs;
let temp_path = "test_empty.json";
fs::write(temp_path, "").unwrap();
let result = MnemonicStorage::read_from_file_path(temp_path);
assert!(result.is_ok());
let storage = result.unwrap();
assert!(storage.is_empty());
let _ = fs::remove_file(temp_path);
}
#[test]
fn test_file_reading_valid_json() {
use std::fs;
let temp_path = "test_valid.json";
let test_data = r#"{
"mnemonic": [
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
"legal winner thank year wave sausage worth useful legal winner thank yellow"
]
}"#;
fs::write(temp_path, test_data).unwrap();
let result = MnemonicStorage::read_from_file_path(temp_path);
assert!(result.is_ok());
let storage = result.unwrap();
assert_eq!(storage.len(), 2);
assert_eq!(
storage.get_mnemonic(0).unwrap(),
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
);
assert_eq!(
storage.get_mnemonic(1).unwrap(),
"legal winner thank year wave sausage worth useful legal winner thank yellow"
);
let _ = fs::remove_file(temp_path);
}
#[test]
fn test_file_reading_invalid_json() {
use std::fs;
let temp_path = "test_invalid.json";
let invalid_json = r#"{ "mnemonic": [ "invalid json structure"#;
fs::write(temp_path, invalid_json).unwrap();
let result = MnemonicStorage::read_from_file_path(temp_path);
assert!(result.is_err());
match result.unwrap_err() {
SignerError::Serialization(_) => {} other => panic!("Expected Serialization error, got: {:?}", other),
}
let _ = fs::remove_file(temp_path);
}
#[test]
fn test_file_reading_invalid_mnemonic_format() {
use std::fs;
let temp_path = "test_invalid_mnemonic.json";
let test_data = r#"{
"mnemonic": [
"invalid mnemonic with wrong word count",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
]
}"#;
fs::write(temp_path, test_data).unwrap();
let result = MnemonicStorage::read_from_file_path(temp_path);
assert!(result.is_err());
match result.unwrap_err() {
SignerError::InvalidMnemonic(msg) => {
assert!(msg.contains("Invalid mnemonic at index 0"));
}
other => panic!("Expected InvalidMnemonic error, got: {:?}", other),
}
let _ = fs::remove_file(temp_path);
}
#[test]
fn test_file_writing_and_reading_roundtrip() {
use std::fs;
let mnemonics = vec![
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(),
"legal winner thank year wave sausage worth useful legal winner thank yellow".to_string(),
];
let original_storage = MnemonicStorage::with_mnemonics(mnemonics.clone()).unwrap();
let temp_path = "test_roundtrip.json";
let write_result = original_storage.write_to_file_path(temp_path);
assert!(write_result.is_ok());
let read_result = MnemonicStorage::read_from_file_path(temp_path);
assert!(read_result.is_ok());
let loaded_storage = read_result.unwrap();
assert_eq!(loaded_storage.len(), original_storage.len());
assert_eq!(
loaded_storage.get_mnemonic(0),
original_storage.get_mnemonic(0)
);
assert_eq!(
loaded_storage.get_mnemonic(1),
original_storage.get_mnemonic(1)
);
let _ = fs::remove_file(temp_path);
}
#[test]
fn test_file_reading_whitespace_only() {
use std::fs;
let temp_path = "test_whitespace.json";
fs::write(temp_path, " \n\t \r\n ").unwrap();
let result = MnemonicStorage::read_from_file_path(temp_path);
assert!(result.is_ok());
let storage = result.unwrap();
assert!(storage.is_empty());
let _ = fs::remove_file(temp_path);
}
#[test]
fn test_atomic_file_writing() {
use std::fs;
use std::path::Path;
let mnemonics = vec![
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(),
];
let storage = MnemonicStorage::with_mnemonics(mnemonics).unwrap();
let test_path = "test_atomic.json";
let temp_path = "test_atomic.tmp";
let _ = fs::remove_file(test_path);
let _ = fs::remove_file(temp_path);
let result = storage.write_to_file_path(test_path);
assert!(result.is_ok());
assert!(Path::new(test_path).exists());
assert!(!Path::new(temp_path).exists());
let read_result = MnemonicStorage::read_from_file_path(test_path);
assert!(read_result.is_ok());
let loaded_storage = read_result.unwrap();
assert_eq!(loaded_storage.len(), 1);
assert_eq!(loaded_storage.get_mnemonic(0), storage.get_mnemonic(0));
let _ = fs::remove_file(test_path);
}
#[test]
fn test_file_writing_serialization_error() {
let storage = MnemonicStorage::new();
let test_path = "test_serialization.json";
let result = storage.write_to_file_path(test_path);
assert!(result.is_ok());
let _ = fs::remove_file(test_path);
}
#[test]
fn test_file_writing_io_error() {
let mnemonics = vec![
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(),
];
let storage = MnemonicStorage::with_mnemonics(mnemonics).unwrap();
let invalid_path = "/nonexistent/directory/test.json";
let result = storage.write_to_file_path(invalid_path);
assert!(result.is_err());
match result.unwrap_err() {
SignerError::FileIo(_) => {} other => panic!("Expected FileIo error, got: {:?}", other),
}
}
#[test]
fn test_file_update_scenario() {
let initial_mnemonics = vec![
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(),
];
let mut storage = MnemonicStorage::with_mnemonics(initial_mnemonics).unwrap();
let test_path = "test_update.json";
let result = storage.write_to_file_path(test_path);
assert!(result.is_ok());
let loaded = MnemonicStorage::read_from_file_path(test_path).unwrap();
assert_eq!(loaded.len(), 1);
let second_mnemonic =
"legal winner thank year wave sausage worth useful legal winner thank yellow"
.to_string();
storage.add_mnemonic(second_mnemonic.clone()).unwrap();
let update_result = storage.write_to_file_path(test_path);
assert!(update_result.is_ok());
let updated_loaded = MnemonicStorage::read_from_file_path(test_path).unwrap();
assert_eq!(updated_loaded.len(), 2);
assert_eq!(updated_loaded.get_mnemonic(1), Some(&second_mnemonic));
let _ = fs::remove_file(test_path);
}
#[test]
fn test_get_mnemonic_by_index() {
let mnemonics = vec![
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(),
"legal winner thank year wave sausage worth useful legal winner thank yellow".to_string(),
"letter advice cage absurd amount doctor acoustic avoid letter advice cage above".to_string(),
];
let storage = MnemonicStorage::with_mnemonics(mnemonics.clone()).unwrap();
assert_eq!(storage.get_mnemonic_by_index(0), Some(&mnemonics[0]));
assert_eq!(storage.get_mnemonic_by_index(1), Some(&mnemonics[1]));
assert_eq!(storage.get_mnemonic_by_index(2), Some(&mnemonics[2]));
assert_eq!(storage.get_mnemonic_by_index(3), None);
assert_eq!(storage.get_mnemonic_by_index(100), None);
}
#[test]
fn test_append_mnemonic() {
let mut storage = MnemonicStorage::new();
assert_eq!(storage.len(), 0);
let first_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string();
let first_index = storage.append_mnemonic(first_mnemonic.clone()).unwrap();
assert_eq!(first_index, 0);
assert_eq!(storage.len(), 1);
assert_eq!(storage.get_mnemonic_by_index(0), Some(&first_mnemonic));
let second_mnemonic =
"legal winner thank year wave sausage worth useful legal winner thank yellow"
.to_string();
let second_index = storage.append_mnemonic(second_mnemonic.clone()).unwrap();
assert_eq!(second_index, 1);
assert_eq!(storage.len(), 2);
assert_eq!(storage.get_mnemonic_by_index(1), Some(&second_mnemonic));
let invalid_mnemonic = "invalid mnemonic with wrong word count".to_string();
let result = storage.append_mnemonic(invalid_mnemonic);
assert!(result.is_err());
assert_eq!(storage.len(), 2);
match result.unwrap_err() {
SignerError::InvalidMnemonic(_) => {} other => panic!("Expected InvalidMnemonic error, got: {:?}", other),
}
}
#[test]
fn test_get_or_generate_mnemonic_at_index() {
let mut storage = MnemonicStorage::new();
assert_eq!(storage.len(), 0);
let mnemonic_0 = storage.get_or_generate_mnemonic_at_index(0).unwrap();
assert_eq!(storage.len(), 1);
assert!(MnemonicStorage::validate_mnemonic_format(&mnemonic_0).is_ok());
let same_mnemonic_0 = storage.get_or_generate_mnemonic_at_index(0).unwrap();
assert_eq!(storage.len(), 1); assert_eq!(mnemonic_0, same_mnemonic_0);
let mnemonic_2 = storage.get_or_generate_mnemonic_at_index(2).unwrap();
assert_eq!(storage.len(), 3); assert!(MnemonicStorage::validate_mnemonic_format(&mnemonic_2).is_ok());
for i in 0..3 {
let mnemonic = storage.get_mnemonic_by_index(i).unwrap();
assert!(MnemonicStorage::validate_mnemonic_format(mnemonic).is_ok());
}
let mnemonic_1 = storage.get_mnemonic_by_index(1).unwrap();
assert_ne!(&mnemonic_0, mnemonic_1);
assert_ne!(&mnemonic_2, mnemonic_1);
assert_ne!(mnemonic_0, mnemonic_2);
}
#[test]
fn test_generate_new_mnemonic() {
let mut generated_mnemonics = Vec::new();
for _ in 0..5 {
let mnemonic = MnemonicStorage::generate_new_mnemonic();
assert!(MnemonicStorage::validate_mnemonic_format(&mnemonic).is_ok());
let words: Vec<&str> = mnemonic.split_whitespace().collect();
assert_eq!(words.len(), 12);
for word in words {
assert!(word.chars().all(|c| c.is_ascii_lowercase()));
assert!(!word.is_empty());
}
assert!(!generated_mnemonics.contains(&mnemonic));
generated_mnemonics.push(mnemonic);
}
}
#[test]
fn test_indexed_access_with_file_operations() {
use std::fs;
let test_path = "test_indexed_access.json";
let mut storage = MnemonicStorage::new();
let mnemonic_1 = storage.get_or_generate_mnemonic_at_index(1).unwrap();
assert_eq!(storage.len(), 2);
storage.write_to_file_path(test_path).unwrap();
let loaded_storage = MnemonicStorage::read_from_file_path(test_path).unwrap();
assert_eq!(loaded_storage.len(), 2);
assert_eq!(loaded_storage.get_mnemonic_by_index(1), Some(&mnemonic_1));
for i in 0..2 {
let mnemonic = loaded_storage.get_mnemonic_by_index(i).unwrap();
assert!(MnemonicStorage::validate_mnemonic_format(mnemonic).is_ok());
}
let _ = fs::remove_file(test_path);
}
#[test]
fn test_signer_creation_with_various_mnemonic_inputs() {
let valid_12_word = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let result = LwkSoftwareSigner::new(valid_12_word);
assert!(
result.is_ok(),
"Should create signer with valid 12-word mnemonic"
);
let signer = result.unwrap();
assert!(
signer.is_testnet(),
"Signer should be configured for testnet"
);
let valid_12_word_2 =
"legal winner thank year wave sausage worth useful legal winner thank yellow";
let result = LwkSoftwareSigner::new(valid_12_word_2);
assert!(
result.is_ok(),
"Should create signer with second valid 12-word mnemonic"
);
let signer = result.unwrap();
assert!(
signer.is_testnet(),
"Signer should be configured for testnet"
);
let valid_12_word_3 =
"letter advice cage absurd amount doctor acoustic avoid letter advice cage above";
let result = LwkSoftwareSigner::new(valid_12_word_3);
assert!(
result.is_ok(),
"Should create signer with third valid 12-word mnemonic"
);
let signer = result.unwrap();
assert!(
signer.is_testnet(),
"Signer should be configured for testnet"
);
let invalid_few_words = "abandon abandon abandon";
let result = LwkSoftwareSigner::new(invalid_few_words);
assert!(result.is_err(), "Should fail with too few words");
match result.unwrap_err() {
SignerError::InvalidMnemonic(msg) => {
assert!(
msg.contains("word count"),
"Error should mention word count issue"
);
}
other => panic!("Expected InvalidMnemonic error, got: {:?}", other),
}
let invalid_many_words = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon";
let result = LwkSoftwareSigner::new(invalid_many_words);
assert!(result.is_err(), "Should fail with too many words");
match result.unwrap_err() {
SignerError::InvalidMnemonic(msg) => {
assert!(
msg.contains("word count"),
"Error should mention word count issue"
);
}
other => panic!("Expected InvalidMnemonic error, got: {:?}", other),
}
let invalid_uppercase = "Abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let result = LwkSoftwareSigner::new(invalid_uppercase);
assert!(result.is_err(), "Should fail with uppercase characters");
match result.unwrap_err() {
SignerError::InvalidMnemonic(msg) => {
assert!(
msg.contains("lowercase"),
"Error should mention lowercase requirement"
);
}
other => panic!("Expected InvalidMnemonic error, got: {:?}", other),
}
let invalid_numbers = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon 123";
let result = LwkSoftwareSigner::new(invalid_numbers);
assert!(result.is_err(), "Should fail with numeric characters");
match result.unwrap_err() {
SignerError::InvalidMnemonic(msg) => {
assert!(
msg.contains("lowercase"),
"Error should mention character validation"
);
}
other => panic!("Expected InvalidMnemonic error, got: {:?}", other),
}
let empty_mnemonic = "";
let result = LwkSoftwareSigner::new(empty_mnemonic);
assert!(result.is_err(), "Should fail with empty mnemonic");
match result.unwrap_err() {
SignerError::InvalidMnemonic(_) => {}
other => panic!("Expected InvalidMnemonic error, got: {:?}", other),
}
let whitespace_mnemonic = " \n\t ";
let result = LwkSoftwareSigner::new(whitespace_mnemonic);
assert!(result.is_err(), "Should fail with whitespace-only mnemonic");
match result.unwrap_err() {
SignerError::InvalidMnemonic(_) => {}
other => panic!("Expected InvalidMnemonic error, got: {:?}", other),
}
let multiple_spaces = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let result = LwkSoftwareSigner::new(multiple_spaces);
assert!(
result.is_err(),
"Should fail with multiple consecutive spaces"
);
match result.unwrap_err() {
SignerError::InvalidMnemonic(msg) => {
assert!(
msg.contains("consecutive spaces") || msg.contains("empty words"),
"Error should mention spacing issue"
);
}
other => panic!("Expected InvalidMnemonic error, got: {:?}", other),
}
let invalid_checksum = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon";
let result = LwkSoftwareSigner::new(invalid_checksum);
assert!(result.is_err(), "Should fail with invalid BIP39 checksum");
match result.unwrap_err() {
SignerError::InvalidMnemonic(msg) => {
assert!(
msg.contains("BIP39"),
"Error should mention BIP39 validation failure"
);
}
other => panic!("Expected InvalidMnemonic error, got: {:?}", other),
}
}
#[test]
fn test_network_configuration_validation() {
let test_mnemonics = vec![
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
"legal winner thank year wave sausage worth useful legal winner thank yellow",
"letter advice cage absurd amount doctor acoustic avoid letter advice cage above",
];
for (i, mnemonic) in test_mnemonics.iter().enumerate() {
let signer = LwkSoftwareSigner::new(mnemonic).unwrap();
assert!(
signer.is_testnet(),
"Signer {} should be configured for testnet",
i
);
}
let (_, generated_signer) = LwkSoftwareSigner::generate_new().unwrap();
assert!(
generated_signer.is_testnet(),
"Generated signer should be configured for testnet"
);
for index in 0..3 {
let (_, indexed_signer) = LwkSoftwareSigner::generate_new_indexed(index).unwrap();
assert!(
indexed_signer.is_testnet(),
"Indexed signer {} should be configured for testnet",
index
);
}
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let signer1 = LwkSoftwareSigner::new(mnemonic).unwrap();
let signer2 = LwkSoftwareSigner::new(mnemonic).unwrap();
assert_eq!(
signer1.is_testnet(),
signer2.is_testnet(),
"Network configuration should be consistent across instances"
);
assert!(
signer1.is_testnet() && signer2.is_testnet(),
"Both signers should be configured for testnet"
);
}
#[tokio::test]
async fn test_basic_transaction_signing_flow_with_mock_data() {
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let signer = LwkSoftwareSigner::new(mnemonic).unwrap();
let empty_hex = "";
let result = signer.sign_transaction(empty_hex).await;
assert!(result.is_err(), "Should fail with empty transaction hex");
match result.unwrap_err() {
SignerError::InvalidTransaction(msg) => {
assert!(
msg.contains("empty"),
"Error should mention empty transaction"
);
}
other => panic!("Expected InvalidTransaction error, got: {:?}", other),
}
let whitespace_hex = " \n\t ";
let result = signer.sign_transaction(whitespace_hex).await;
assert!(
result.is_err(),
"Should fail with whitespace-only transaction hex"
);
match result.unwrap_err() {
SignerError::InvalidTransaction(msg) => {
assert!(
msg.contains("empty"),
"Error should mention empty transaction"
);
}
other => panic!("Expected InvalidTransaction error, got: {:?}", other),
}
let short_hex = "abc123";
let result = signer.sign_transaction(short_hex).await;
assert!(
result.is_err(),
"Should fail with too short transaction hex"
);
match result.unwrap_err() {
SignerError::InvalidTransaction(msg) => {
assert!(
msg.contains("too short"),
"Error should mention transaction too short"
);
}
other => panic!("Expected InvalidTransaction error, got: {:?}", other),
}
let invalid_hex = "invalid_hex_characters_zz";
let result = signer.sign_transaction(invalid_hex).await;
assert!(result.is_err(), "Should fail with invalid hex characters");
match result.unwrap_err() {
SignerError::HexParse(_) => {} other => panic!("Expected HexParse error, got: {:?}", other),
}
let valid_hex_invalid_tx = "deadbeefcafebabe1234567890abcdef";
let result = signer.sign_transaction(valid_hex_invalid_tx).await;
assert!(
result.is_err(),
"Should fail with invalid transaction structure"
);
match result.unwrap_err() {
SignerError::InvalidTransaction(_) => {} other => panic!("Expected InvalidTransaction error, got: {:?}", other),
}
let odd_hex = "deadbeefcafebabe12345"; let result = signer.sign_transaction(odd_hex).await;
assert!(result.is_err(), "Should fail with odd-length hex");
match result.unwrap_err() {
SignerError::HexParse(_) => {} other => panic!("Expected HexParse error, got: {:?}", other),
}
let long_invalid_hex = "z".repeat(1000);
let result = signer.sign_transaction(&long_invalid_hex).await;
assert!(result.is_err(), "Should fail with long invalid hex");
match result.unwrap_err() {
SignerError::HexParse(_) => {} other => panic!("Expected HexParse error, got: {:?}", other),
}
let too_short_valid_hex = "deadbeef";
let result = signer.sign_transaction(too_short_valid_hex).await;
assert!(
result.is_err(),
"Should fail with hex that's too short for transaction"
);
match result.unwrap_err() {
SignerError::InvalidTransaction(msg) => {
assert!(
msg.contains("too short"),
"Error should mention transaction too short"
);
}
other => panic!("Expected InvalidTransaction error, got: {:?}", other),
}
let test_invalid_hex = "gggggggggggggggggggggggg"; let result = signer.sign_transaction(test_invalid_hex).await;
assert!(result.is_err(), "Should fail with invalid hex");
let error = result.unwrap_err();
let error_string = format!("{}", error);
assert!(
error_string.contains("Hex parsing failed") || error_string.contains("parsing"),
"Error message should provide context about hex parsing failure: {}",
error_string
);
}
#[tokio::test]
async fn test_thread_safety_and_async_compatibility() {
use std::sync::Arc;
use tokio::task;
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let signer = Arc::new(LwkSoftwareSigner::new(mnemonic).unwrap());
let mut handles = Vec::new();
for i in 0..5 {
let signer_clone = Arc::clone(&signer);
let handle = task::spawn(async move {
let invalid_hex = format!("invalid_hex_characters_task_{}", i).repeat(3); let result = signer_clone.sign_transaction(&invalid_hex).await;
assert!(result.is_err(), "Task {} should fail with invalid hex", i);
match result.unwrap_err() {
SignerError::HexParse(_) => {} other => panic!("Task {} expected HexParse error, got: {:?}", i, other),
}
i
});
handles.push(handle);
}
let mut completed_tasks = Vec::new();
for handle in handles {
let task_id = handle.await.expect("Task should complete successfully");
completed_tasks.push(task_id);
}
completed_tasks.sort();
assert_eq!(
completed_tasks,
vec![0, 1, 2, 3, 4],
"All tasks should complete"
);
let creation_handles: Vec<_> = (0..3).map(|i| {
let test_mnemonic = match i {
0 => "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
1 => "legal winner thank year wave sausage worth useful legal winner thank yellow",
_ => "letter advice cage absurd amount doctor acoustic avoid letter advice cage above",
};
task::spawn(async move {
let signer = LwkSoftwareSigner::new(test_mnemonic).unwrap();
assert!(signer.is_testnet(), "Concurrent signer {} should be testnet", i);
i
})
}).collect();
for handle in creation_handles {
handle.await.expect("Signer creation task should complete");
}
let file_handles: Vec<_> = (0..3)
.map(|index| {
task::spawn(async move {
let actual_index = index + 10; let result = LwkSoftwareSigner::generate_new_indexed(actual_index);
match result {
Ok((mnemonic, signer)) => {
assert!(
!mnemonic.is_empty(),
"Generated mnemonic should not be empty"
);
assert!(signer.is_testnet(), "Generated signer should be testnet");
Ok(actual_index)
}
Err(e) => {
match e {
SignerError::FileIo(_) | SignerError::Serialization(_) => {
Ok(actual_index)
}
other => Err(other),
}
}
}
})
})
.collect();
let mut successful_file_ops = 0;
for handle in file_handles {
match handle.await.expect("File operation task should complete") {
Ok(_) => successful_file_ops += 1,
Err(e) => {
eprintln!("Expected concurrency error in file operations: {:?}", e);
}
}
}
assert!(
successful_file_ops > 0,
"At least one concurrent file operation should succeed"
);
let signer: Box<dyn Signer> = Box::new(LwkSoftwareSigner::new(mnemonic).unwrap());
let result = signer
.sign_transaction("invalid_hex_characters_long_enough_for_test")
.await;
assert!(result.is_err(), "Trait object should work correctly");
match result.unwrap_err() {
SignerError::HexParse(_) => {} other => panic!(
"Expected HexParse error from trait object, got: {:?}",
other
),
}
let signer = LwkSoftwareSigner::new(mnemonic).unwrap();
let handle = task::spawn(async move {
assert!(signer.is_testnet());
let result = signer
.sign_transaction("invalid_hex_characters_long_enough")
.await;
assert!(result.is_err());
});
handle.await.expect("Send/Sync test should complete");
}
#[tokio::test]
async fn test_signer_error_handling_consistency() {
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let signer = LwkSoftwareSigner::new(mnemonic).unwrap();
let test_cases = vec![
("", "empty transaction"),
(" ", "whitespace transaction"),
("abc", "too short"),
("zz", "invalid hex chars"),
("abcdef", "short valid hex"),
];
for (input, description) in test_cases {
let result = signer.sign_transaction(input).await;
assert!(result.is_err(), "Should fail for {}", description);
let error = result.unwrap_err();
let error_msg = format!("{}", error);
assert!(
!error_msg.is_empty(),
"Error message should not be empty for {}",
description
);
let _: &dyn std::error::Error = &error;
let debug_msg = format!("{:?}", error);
assert!(
!debug_msg.is_empty(),
"Debug message should not be empty for {}",
description
);
}
}
#[test]
fn test_signer_creation_performance_and_memory() {
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let start_time = std::time::Instant::now();
let mut signers = Vec::new();
for _ in 0..10 {
let signer = LwkSoftwareSigner::new(mnemonic).unwrap();
assert!(signer.is_testnet());
signers.push(signer);
}
let elapsed = start_time.elapsed();
assert!(
elapsed.as_secs() < 5,
"Signer creation should be fast, took: {:?}",
elapsed
);
for (i, signer) in signers.iter().enumerate() {
assert!(signer.is_testnet(), "Signer {} should be testnet", i);
}
drop(signers);
}
#[test]
fn test_signer_with_different_mnemonic_languages() {
let english_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let result = LwkSoftwareSigner::new(english_mnemonic);
assert!(result.is_ok(), "English mnemonic should work");
let potentially_invalid_words = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon invalid";
let result = LwkSoftwareSigner::new(potentially_invalid_words);
match result {
Ok(signer) => {
assert!(
signer.is_testnet(),
"If signer is created, it should be testnet"
);
}
Err(SignerError::InvalidMnemonic(_)) => {
}
Err(other) => {
panic!(
"Unexpected error type for potentially invalid words: {:?}",
other
);
}
}
}
#[test]
fn test_json_file_reading_comprehensive() {
use std::fs;
let test_path_single = "test_json_single.json";
let valid_single_json = r#"{
"mnemonic": [
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
]
}"#;
fs::write(test_path_single, valid_single_json).unwrap();
let result = MnemonicStorage::read_from_file_path(test_path_single);
assert!(result.is_ok());
let storage = result.unwrap();
assert_eq!(storage.len(), 1);
assert_eq!(
storage.get_mnemonic_by_index(0).unwrap(),
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
);
let test_path_multiple = "test_json_multiple.json";
let valid_multiple_json = r#"{
"mnemonic": [
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
"legal winner thank year wave sausage worth useful legal winner thank yellow",
"letter advice cage absurd amount doctor acoustic avoid letter advice cage above"
]
}"#;
fs::write(test_path_multiple, valid_multiple_json).unwrap();
let result = MnemonicStorage::read_from_file_path(test_path_multiple);
assert!(result.is_ok());
let storage = result.unwrap();
assert_eq!(storage.len(), 3);
assert_eq!(
storage.get_mnemonic_by_index(1).unwrap(),
"legal winner thank year wave sausage worth useful legal winner thank yellow"
);
assert_eq!(
storage.get_mnemonic_by_index(2).unwrap(),
"letter advice cage absurd amount doctor acoustic avoid letter advice cage above"
);
let test_path_empty_array = "test_json_empty_array.json";
let empty_array_json = r#"{
"mnemonic": []
}"#;
fs::write(test_path_empty_array, empty_array_json).unwrap();
let result = MnemonicStorage::read_from_file_path(test_path_empty_array);
assert!(result.is_ok());
let storage = result.unwrap();
assert_eq!(storage.len(), 0);
assert!(storage.is_empty());
let test_path_whitespace = "test_json_whitespace.json";
let whitespace_json = r#"
{
"mnemonic": [
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
]
}
"#;
fs::write(test_path_whitespace, whitespace_json).unwrap();
let result = MnemonicStorage::read_from_file_path(test_path_whitespace);
assert!(result.is_ok());
let storage = result.unwrap();
assert_eq!(storage.len(), 1);
let _ = fs::remove_file(test_path_single);
let _ = fs::remove_file(test_path_multiple);
let _ = fs::remove_file(test_path_empty_array);
let _ = fs::remove_file(test_path_whitespace);
}
#[test]
fn test_json_file_reading_invalid_formats() {
use std::fs;
let test_path_syntax = "test_json_invalid_syntax.json";
let invalid_syntax_json = r#"{
"mnemonic": [
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
"#;
fs::write(test_path_syntax, invalid_syntax_json).unwrap();
let result = MnemonicStorage::read_from_file_path(test_path_syntax);
assert!(result.is_err());
match result.unwrap_err() {
SignerError::Serialization(_) => {} other => panic!("Expected Serialization error, got: {:?}", other),
}
let test_path_structure = "test_json_invalid_structure.json";
let invalid_structure_json = r#"{
"invalid_field": [
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
]
}"#;
fs::write(test_path_structure, invalid_structure_json).unwrap();
let result = MnemonicStorage::read_from_file_path(test_path_structure);
assert!(result.is_err());
match result.unwrap_err() {
SignerError::Serialization(_) => {} other => panic!("Expected Serialization error, got: {:?}", other),
}
let test_path_type = "test_json_invalid_type.json";
let invalid_type_json = r#"{
"mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
}"#;
fs::write(test_path_type, invalid_type_json).unwrap();
let result = MnemonicStorage::read_from_file_path(test_path_type);
assert!(result.is_err());
match result.unwrap_err() {
SignerError::Serialization(_) => {} other => panic!("Expected Serialization error, got: {:?}", other),
}
let test_path_invalid_mnemonic = "test_json_invalid_mnemonic.json";
let invalid_mnemonic_json = r#"{
"mnemonic": [
"invalid mnemonic with wrong word count",
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
]
}"#;
fs::write(test_path_invalid_mnemonic, invalid_mnemonic_json).unwrap();
let result = MnemonicStorage::read_from_file_path(test_path_invalid_mnemonic);
assert!(result.is_err());
match result.unwrap_err() {
SignerError::InvalidMnemonic(msg) => {
assert!(msg.contains("Invalid mnemonic at index 0"));
assert!(msg.contains("Invalid word count"));
}
other => panic!("Expected InvalidMnemonic error, got: {:?}", other),
}
let test_path_malformed = "test_json_malformed.json";
let malformed_json = "not json at all { invalid content";
fs::write(test_path_malformed, malformed_json).unwrap();
let result = MnemonicStorage::read_from_file_path(test_path_malformed);
assert!(result.is_err());
match result.unwrap_err() {
SignerError::Serialization(_) => {} other => panic!("Expected Serialization error, got: {:?}", other),
}
let _ = fs::remove_file(test_path_syntax);
let _ = fs::remove_file(test_path_structure);
let _ = fs::remove_file(test_path_type);
let _ = fs::remove_file(test_path_invalid_mnemonic);
let _ = fs::remove_file(test_path_malformed);
}
#[test]
fn test_json_file_writing_and_updating() {
use std::fs;
let test_path_empty = "test_json_write_empty.json";
let empty_storage = MnemonicStorage::new();
let result = empty_storage.write_to_file_path(test_path_empty);
assert!(result.is_ok());
assert!(std::path::Path::new(test_path_empty).exists());
let file_content = fs::read_to_string(test_path_empty).unwrap();
assert!(file_content.contains("\"mnemonic\""));
assert!(file_content.contains("[]"));
let loaded = MnemonicStorage::read_from_file_path(test_path_empty).unwrap();
assert!(loaded.is_empty());
let test_path_single = "test_json_write_single.json";
let mut single_storage = MnemonicStorage::new();
let mnemonic1 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string();
single_storage.append_mnemonic(mnemonic1.clone()).unwrap();
let result = single_storage.write_to_file_path(test_path_single);
assert!(result.is_ok());
let loaded = MnemonicStorage::read_from_file_path(test_path_single).unwrap();
assert_eq!(loaded.len(), 1);
assert_eq!(loaded.get_mnemonic_by_index(0).unwrap(), &mnemonic1);
let mnemonic2 =
"legal winner thank year wave sausage worth useful legal winner thank yellow"
.to_string();
let mnemonic3 =
"letter advice cage absurd amount doctor acoustic avoid letter advice cage above"
.to_string();
let mut updated_storage = loaded;
updated_storage.append_mnemonic(mnemonic2.clone()).unwrap();
updated_storage.append_mnemonic(mnemonic3.clone()).unwrap();
let result = updated_storage.write_to_file_path(test_path_single);
assert!(result.is_ok());
let final_loaded = MnemonicStorage::read_from_file_path(test_path_single).unwrap();
assert_eq!(final_loaded.len(), 3);
assert_eq!(final_loaded.get_mnemonic_by_index(0).unwrap(), &mnemonic1);
assert_eq!(final_loaded.get_mnemonic_by_index(1).unwrap(), &mnemonic2);
assert_eq!(final_loaded.get_mnemonic_by_index(2).unwrap(), &mnemonic3);
let test_path_overwrite = "test_json_write_overwrite.json";
let mut new_storage = MnemonicStorage::new();
let new_mnemonic = "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong".to_string();
new_storage.append_mnemonic(new_mnemonic.clone()).unwrap();
new_storage.write_to_file_path(test_path_overwrite).unwrap();
let initial_loaded = MnemonicStorage::read_from_file_path(test_path_overwrite).unwrap();
assert_eq!(initial_loaded.len(), 1);
let mut overwrite_storage = MnemonicStorage::new();
overwrite_storage
.append_mnemonic(mnemonic1.clone())
.unwrap();
overwrite_storage
.append_mnemonic(mnemonic2.clone())
.unwrap();
overwrite_storage
.write_to_file_path(test_path_overwrite)
.unwrap();
let overwritten_loaded = MnemonicStorage::read_from_file_path(test_path_overwrite).unwrap();
assert_eq!(overwritten_loaded.len(), 2);
assert_eq!(
overwritten_loaded.get_mnemonic_by_index(0).unwrap(),
&mnemonic1
);
assert_eq!(
overwritten_loaded.get_mnemonic_by_index(1).unwrap(),
&mnemonic2
);
let _ = fs::remove_file(test_path_empty);
let _ = fs::remove_file(test_path_single);
let _ = fs::remove_file(test_path_overwrite);
}
#[test]
fn test_json_file_io_error_scenarios() {
use std::fs;
use std::os::unix::fs::PermissionsExt;
let invalid_path = "/nonexistent/directory/test.json";
let storage = MnemonicStorage::new();
let result = storage.write_to_file_path(invalid_path);
assert!(result.is_err());
match result.unwrap_err() {
SignerError::FileIo(_) => {} other => panic!("Expected FileIo error, got: {:?}", other),
}
let dir_path = "test_json_directory";
fs::create_dir_all(dir_path).unwrap();
let result = MnemonicStorage::read_from_file_path(dir_path);
assert!(result.is_err());
match result.unwrap_err() {
SignerError::FileIo(_) => {} other => panic!("Expected FileIo error, got: {:?}", other),
}
#[cfg(unix)]
{
let readonly_path = "test_json_readonly.json";
fs::write(readonly_path, "{}").unwrap();
let mut perms = fs::metadata(readonly_path).unwrap().permissions();
perms.set_mode(0o444); fs::set_permissions(readonly_path, perms).unwrap();
let storage = MnemonicStorage::new();
let result = storage.write_to_file_path(readonly_path);
if result.is_err() {
match result.unwrap_err() {
SignerError::FileIo(_) => {} other => panic!("Expected FileIo error, got: {:?}", other),
}
}
let mut perms = fs::metadata(readonly_path).unwrap().permissions();
perms.set_mode(0o644); fs::set_permissions(readonly_path, perms).unwrap();
let _ = fs::remove_file(readonly_path);
}
let corrupted_path = "test_json_corrupted.json";
let binary_data = vec![0xFF, 0xFE, 0xFD, 0xFC, 0x00, 0x01, 0x02, 0x03];
fs::write(corrupted_path, binary_data).unwrap();
let result = MnemonicStorage::read_from_file_path(corrupted_path);
assert!(result.is_err());
match result.unwrap_err() {
SignerError::FileIo(_) | SignerError::Serialization(_) => {} other => panic!("Expected FileIo or Serialization error, got: {:?}", other),
}
let long_path = "a".repeat(1000) + ".json";
let storage = MnemonicStorage::new();
let result = storage.write_to_file_path(&long_path);
if result.is_err() {
match result.unwrap_err() {
SignerError::FileIo(_) => {} other => panic!("Expected FileIo error, got: {:?}", other),
}
}
let _ = fs::remove_dir_all(dir_path);
let _ = fs::remove_file(corrupted_path);
let _ = fs::remove_file(&long_path);
}
#[test]
fn test_json_atomic_write_operations() {
use std::fs;
use std::path::Path;
use std::thread;
use std::time::Duration;
let test_path = "test_json_atomic.json";
let temp_path = "test_json_atomic.tmp";
let _ = fs::remove_file(test_path);
let _ = fs::remove_file(temp_path);
let mut storage = MnemonicStorage::new();
storage.append_mnemonic("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string()).unwrap();
let result = storage.write_to_file_path(test_path);
assert!(result.is_ok());
assert!(Path::new(test_path).exists());
assert!(
!Path::new(temp_path).exists(),
"Temporary file should be cleaned up after atomic write"
);
let integrity_path = "test_json_integrity.json";
let mut large_storage = MnemonicStorage::new();
for _i in 0..10 {
let mnemonic = MnemonicStorage::generate_new_mnemonic();
large_storage.append_mnemonic(mnemonic).unwrap();
}
large_storage.write_to_file_path(integrity_path).unwrap();
let loaded_storage = MnemonicStorage::read_from_file_path(integrity_path).unwrap();
assert_eq!(loaded_storage.len(), large_storage.len());
for i in 0..large_storage.len() {
assert_eq!(
loaded_storage.get_mnemonic_by_index(i),
large_storage.get_mnemonic_by_index(i)
);
}
let concurrent_path = "test_json_concurrent.json";
let concurrent_storage = MnemonicStorage::new();
let handles: Vec<_> = (0..3)
.map(|_i| {
let path = concurrent_path.to_string();
let mut storage = concurrent_storage.clone();
thread::spawn(move || {
let mnemonic = MnemonicStorage::generate_new_mnemonic();
storage.append_mnemonic(mnemonic).unwrap();
thread::sleep(Duration::from_millis(1));
storage.write_to_file_path(&path)
})
})
.collect();
let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
let successful_writes = results.iter().filter(|r| r.is_ok()).count();
assert!(
successful_writes > 0,
"At least one concurrent write should succeed"
);
if Path::new(concurrent_path).exists() {
let final_storage = MnemonicStorage::read_from_file_path(concurrent_path);
assert!(
final_storage.is_ok(),
"Final file should be valid JSON after concurrent writes"
);
}
let cleanup_path = "test_json_cleanup.json";
let cleanup_temp_path = "test_json_cleanup.tmp";
let cleanup_storage = MnemonicStorage::new();
fs::write(cleanup_temp_path, "leftover temp file").unwrap();
assert!(Path::new(cleanup_temp_path).exists());
let result = cleanup_storage.write_to_file_path(cleanup_path);
assert!(result.is_ok());
assert!(Path::new(cleanup_path).exists());
let loaded = MnemonicStorage::read_from_file_path(cleanup_path).unwrap();
assert_eq!(loaded.len(), 0);
let format_path = "test_json_format.json";
let mut format_storage = MnemonicStorage::new();
format_storage.append_mnemonic("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string()).unwrap();
format_storage.write_to_file_path(format_path).unwrap();
let raw_content = fs::read_to_string(format_path).unwrap();
assert!(
raw_content.contains('\n'),
"JSON should be pretty-printed with newlines"
);
assert!(
raw_content.contains(" "),
"JSON should be pretty-printed with indentation"
);
let parsed: serde_json::Value = serde_json::from_str(&raw_content).unwrap();
assert!(parsed.is_object());
assert!(parsed.get("mnemonic").is_some());
assert!(parsed["mnemonic"].is_array());
let _ = fs::remove_file(test_path);
let _ = fs::remove_file(temp_path);
let _ = fs::remove_file(integrity_path);
let _ = fs::remove_file(concurrent_path);
let _ = fs::remove_file(cleanup_path);
let _ = fs::remove_file(cleanup_temp_path);
let _ = fs::remove_file(format_path);
}
#[test]
fn test_json_file_edge_cases() {
use std::fs;
let large_path = "test_json_large.json";
let mut large_storage = MnemonicStorage::new();
for _ in 0..100 {
let mnemonic = MnemonicStorage::generate_new_mnemonic();
large_storage.append_mnemonic(mnemonic).unwrap();
}
large_storage.write_to_file_path(large_path).unwrap();
let loaded_large = MnemonicStorage::read_from_file_path(large_path).unwrap();
assert_eq!(loaded_large.len(), 100);
for i in 0..100 {
assert_eq!(
loaded_large.get_mnemonic_by_index(i),
large_storage.get_mnemonic_by_index(i)
);
}
let unicode_path = "test_json_ünïcödé.json";
let unicode_storage = MnemonicStorage::new();
let result = unicode_storage.write_to_file_path(unicode_path);
if result.is_ok() {
let loaded_unicode = MnemonicStorage::read_from_file_path(unicode_path).unwrap();
assert_eq!(loaded_unicode.len(), 0);
}
let long_path = format!("{}.json", "very_long_filename_".repeat(10));
let long_storage = MnemonicStorage::new();
let result = long_storage.write_to_file_path(&long_path);
if result.is_ok() {
let loaded_long = MnemonicStorage::read_from_file_path(&long_path).unwrap();
assert_eq!(loaded_long.len(), 0);
}
let bom_path = "test_json_bom.json";
let bom_content = "\u{FEFF}{\"mnemonic\":[\"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\"]}";
fs::write(bom_path, bom_content).unwrap();
let result = MnemonicStorage::read_from_file_path(bom_path);
match result {
Ok(storage) => {
assert_eq!(storage.len(), 1);
}
Err(SignerError::Serialization(_)) => {
}
Err(other) => panic!("Unexpected error type for BOM file: {:?}", other),
}
let crlf_path = "test_json_crlf.json";
let crlf_content = "{\r\n \"mnemonic\": [\r\n \"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\"\r\n ]\r\n}";
fs::write(crlf_path, crlf_content).unwrap();
let result = MnemonicStorage::read_from_file_path(crlf_path);
assert!(result.is_ok());
let storage = result.unwrap();
assert_eq!(storage.len(), 1);
let _ = fs::remove_file(large_path);
let _ = fs::remove_file(unicode_path);
let _ = fs::remove_file(&long_path);
let _ = fs::remove_file(bom_path);
let _ = fs::remove_file(crlf_path);
}
#[test]
fn test_json_file_recovery_scenarios() {
use std::fs;
let partial_path = "test_json_partial.json";
let partial_content = r#"{
"mnemonic": [
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
"legal winner thank year wave sausage worth useful legal winner thank"#;
fs::write(partial_path, partial_content).unwrap();
let result = MnemonicStorage::read_from_file_path(partial_path);
assert!(result.is_err());
match result.unwrap_err() {
SignerError::Serialization(_) => {} other => panic!("Expected Serialization error, got: {:?}", other),
}
let mut recovery_storage = MnemonicStorage::new();
recovery_storage.append_mnemonic("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string()).unwrap();
let recovery_result = recovery_storage.write_to_file_path(partial_path);
assert!(recovery_result.is_ok());
let recovered = MnemonicStorage::read_from_file_path(partial_path).unwrap();
assert_eq!(recovered.len(), 1);
let mixed_path = "test_json_mixed.json";
let mixed_content = r#"{
"mnemonic": [
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
"invalid mnemonic",
"legal winner thank year wave sausage worth useful legal winner thank yellow"
]
}"#;
fs::write(mixed_path, mixed_content).unwrap();
let result = MnemonicStorage::read_from_file_path(mixed_path);
assert!(result.is_err());
match result.unwrap_err() {
SignerError::InvalidMnemonic(msg) => {
assert!(msg.contains("Invalid mnemonic at index 1"));
}
other => panic!("Expected InvalidMnemonic error, got: {:?}", other),
}
let mut fixed_storage = MnemonicStorage::new();
fixed_storage.append_mnemonic("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string()).unwrap();
fixed_storage
.append_mnemonic(
"legal winner thank year wave sausage worth useful legal winner thank yellow"
.to_string(),
)
.unwrap();
fixed_storage.write_to_file_path(mixed_path).unwrap();
let fixed = MnemonicStorage::read_from_file_path(mixed_path).unwrap();
assert_eq!(fixed.len(), 2);
let zero_path = "test_json_zero.json";
fs::write(zero_path, "").unwrap();
let result = MnemonicStorage::read_from_file_path(zero_path);
assert!(result.is_ok());
let storage = result.unwrap();
assert!(storage.is_empty());
let mut populated_storage = MnemonicStorage::new();
populated_storage.append_mnemonic("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string()).unwrap();
populated_storage.write_to_file_path(zero_path).unwrap();
let populated = MnemonicStorage::read_from_file_path(zero_path).unwrap();
assert_eq!(populated.len(), 1);
let extra_path = "test_json_extra.json";
let extra_content = r#"{
"mnemonic": [
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
],
"extra_field": "should be ignored",
"version": 1,
"metadata": {
"created": "2023-01-01",
"notes": "test file"
}
}"#;
fs::write(extra_path, extra_content).unwrap();
let result = MnemonicStorage::read_from_file_path(extra_path);
assert!(result.is_ok());
let storage = result.unwrap();
assert_eq!(storage.len(), 1);
storage.write_to_file_path(extra_path).unwrap();
let rewritten_content = fs::read_to_string(extra_path).unwrap();
assert!(rewritten_content.contains("mnemonic"));
assert!(!rewritten_content.contains("extra_field"));
assert!(!rewritten_content.contains("version"));
assert!(!rewritten_content.contains("metadata"));
let _ = fs::remove_file(partial_path);
let _ = fs::remove_file(mixed_path);
let _ = fs::remove_file(zero_path);
let _ = fs::remove_file(extra_path);
}
}
#[test]
fn test_conditional_logging_behavior() {
if should_log_in_tests() {
let _ = tracing_subscriber::fmt::try_init();
}
let result = MnemonicStorage::read_from_file_path("non_existent_test_file.json");
assert!(result.is_ok());
let storage = result.unwrap();
assert!(storage.is_empty());
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let result = LwkSoftwareSigner::new(mnemonic);
assert!(result.is_ok());
let signer = result.unwrap();
assert!(signer.is_testnet());
}