diff --git a/src/bin/average_block_time_checker.rs b/src/bin/average_block_time_checker.rs index ba2cdf5..9f471fe 100644 --- a/src/bin/average_block_time_checker.rs +++ b/src/bin/average_block_time_checker.rs @@ -1,94 +1,94 @@ -use blockchain::common::network_paths_and_settings::block_extension_and_paths; -use blockchain::env; -use blockchain::fs; -use blockchain::Duration; -use blockchain::Error; - -fn calculate_average_time_between_timestamps( - start_block: u32, - stop_block: u32, - directory: &str, -) -> Result, Box> { - let mut timestamps: Vec = Vec::new(); - - // Iterate over all files in the directory - for entry in fs::read_dir(directory)? { - let entry = entry?; - let path = entry.path(); - - let ( - _network_name, - _padded_base_coin, - extension, - _torrentpath, - _wallet_path, - _blockpath, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - if path.is_file() && path.extension().is_some_and(|ext| *ext == *extension) { - if let Some(file_name) = path.file_stem() { - if let Some(file_name_str) = file_name.to_str() { - if let Ok(block_number) = file_name_str.parse::() { - if block_number >= start_block && block_number <= stop_block { - let file_content = fs::read(&path)?; - if file_content.len() >= 4 { - let timestamp_bytes = &file_content[0..4]; - let timestamp = u32::from_le_bytes(timestamp_bytes.try_into()?); - timestamps.push(timestamp); - } - } - } - } - } - } - } - - // Sort the timestamps - timestamps.sort(); - - // Calculate the differences between consecutive timestamps - let mut time_differences: Vec = Vec::new(); - for i in 1..timestamps.len() { - let time_difference = timestamps[i] - timestamps[i - 1]; - time_differences.push(Duration::from_secs(time_difference as u64)); - } - - // Calculate the average time difference - if !time_differences.is_empty() { - let total_duration: Duration = time_differences.iter().sum(); - let average_time = total_duration / (time_differences.len() as u32); - Ok(Some(average_time)) - } else { - Ok(None) - } -} - -fn main() -> Result<(), Box> { - // Collect command-line arguments - let args: Vec = env::args().collect(); - - if args.len() != 4 { - println!("Usage: {} ", args[0]); - return Ok(()); - } - - let start_block: u32 = args[1].parse()?; - let stop_block: u32 = args[2].parse()?; - let directory = &args[3]; - - match calculate_average_time_between_timestamps(start_block, stop_block, directory)? { - Some(average_time) => { - println!( - "The average time between timestamps is {:.2} seconds.", - average_time.as_secs_f64() - ); - } - None => { - println!("Not enough data to calculate an average time difference."); - } - } - - Ok(()) -} +use blockchain::common::network_paths_and_settings::block_extension_and_paths; +use blockchain::env; +use blockchain::fs; +use blockchain::Duration; +use blockchain::Error; + +fn calculate_average_time_between_timestamps( + start_block: u32, + stop_block: u32, + directory: &str, +) -> Result, Box> { + let mut timestamps: Vec = Vec::new(); + + // Iterate over all files in the directory + for entry in fs::read_dir(directory)? { + let entry = entry?; + let path = entry.path(); + + let ( + _network_name, + _padded_base_coin, + extension, + _torrentpath, + _wallet_path, + _blockpath, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + if path.is_file() && path.extension().is_some_and(|ext| *ext == *extension) { + if let Some(file_name) = path.file_stem() { + if let Some(file_name_str) = file_name.to_str() { + if let Ok(block_number) = file_name_str.parse::() { + if block_number >= start_block && block_number <= stop_block { + let file_content = fs::read(&path)?; + if file_content.len() >= 4 { + let timestamp_bytes = &file_content[0..4]; + let timestamp = u32::from_le_bytes(timestamp_bytes.try_into()?); + timestamps.push(timestamp); + } + } + } + } + } + } + } + + // Sort the timestamps + timestamps.sort(); + + // Calculate the differences between consecutive timestamps + let mut time_differences: Vec = Vec::new(); + for i in 1..timestamps.len() { + let time_difference = timestamps[i] - timestamps[i - 1]; + time_differences.push(Duration::from_secs(time_difference as u64)); + } + + // Calculate the average time difference + if !time_differences.is_empty() { + let total_duration: Duration = time_differences.iter().sum(); + let average_time = total_duration / (time_differences.len() as u32); + Ok(Some(average_time)) + } else { + Ok(None) + } +} + +fn main() -> Result<(), Box> { + // Collect command-line arguments + let args: Vec = env::args().collect(); + + if args.len() != 4 { + println!("Usage: {} ", args[0]); + return Ok(()); + } + + let start_block: u32 = args[1].parse()?; + let stop_block: u32 = args[2].parse()?; + let directory = &args[3]; + + match calculate_average_time_between_timestamps(start_block, stop_block, directory)? { + Some(average_time) => { + println!( + "The average time between timestamps is {:.2} seconds.", + average_time.as_secs_f64() + ); + } + None => { + println!("Not enough data to calculate an average time difference."); + } + } + + Ok(()) +} diff --git a/src/bin/broadcast_transaction.rs b/src/bin/broadcast_transaction.rs index e3a2fbc..27e50b4 100644 --- a/src/bin/broadcast_transaction.rs +++ b/src/bin/broadcast_transaction.rs @@ -1,11 +1,11 @@ -use blockchain::common::cli_prompts::prompt_hidden_nonempty; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; use blockchain::from_slice; use blockchain::read; use blockchain::records::memory::response_channels::generate_uid; use blockchain::standalone_tools::connections::handshake; -use blockchain::wallets::structures::{SavedWallet, Wallet}; +use blockchain::wallets::structures::SavedWallet; use blockchain::Path; use serde_json::Value; @@ -19,7 +19,7 @@ fn display_vanity_address(short_address: &str) -> Option { Some(format!("{trimmed_payload}.{network_suffix}")) } -async fn persist_local_vanity_address(encryption_key: &str, tx_json: &str) { +async fn persist_local_vanity_address(encryption_key: &str, tx_json: &str, wallet_path: &str) { // Only successful vanity transactions update the local saved wallet display field. let Ok(value) = serde_json::from_str::(tx_json) else { return; @@ -41,7 +41,6 @@ async fn persist_local_vanity_address(encryption_key: &str, tx_json: &str) { }; // Read the saved wallet and only update it if it matches the vanity transaction sender. - let wallet_path = Wallet::get_wallet_path().await; let wallet_bytes = match read(Path::new(&wallet_path)).await { Ok(bytes) => bytes, Err(_) => return, @@ -63,7 +62,7 @@ async fn persist_local_vanity_address(encryption_key: &str, tx_json: &str) { }; saved_wallet.vanity_address = Some(display_vanity); - saved_wallet.save_the_wallet(Path::new(&wallet_path)).await; + saved_wallet.save_the_wallet(Path::new(wallet_path)).await; } #[tokio::main] @@ -81,6 +80,7 @@ async fn main() { } }; + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", "Wallet key cannot be empty. Please try again.", @@ -118,7 +118,10 @@ async fn main() { socket_address, json.clone(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.clone(), + wallet_path: wallet_path.clone(), + }, hashmap_key, ) .await; @@ -130,7 +133,7 @@ async fn main() { println!("{trimmed}"); // When a vanity tx broadcasts successfully, keep the local wallet display in sync. if trimmed == "successful_broadcast: true" { - persist_local_vanity_address(&encryption_key, &json).await; + persist_local_vanity_address(&encryption_key, &json, &wallet_path).await; if let Some(hash) = &txid { println!("{hash}"); } diff --git a/src/bin/create_burn_tx.rs b/src/bin/create_burn_tx.rs index 2e2a3dd..f4f162d 100644 --- a/src/bin/create_burn_tx.rs +++ b/src/bin/create_burn_tx.rs @@ -1,6 +1,7 @@ use blockchain::blocks::burn::{BurnTransaction, UnsignedBurnTransaction}; -use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible}; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path}; use blockchain::common::network_paths_and_settings::block_extension_and_paths; + use blockchain::common::types::BURN_FEE; use blockchain::json; use blockchain::wallets::structures::Wallet; @@ -63,6 +64,7 @@ async fn main() { .parse() .expect("Please enter a valid fee."); let txfee = (txfee_f64 * 100_000_000.0).round() as u64; + let wallet_path = prompt_wallet_path().await; let decryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -71,7 +73,7 @@ async fn main() { .await; // Load the wallet that owns the asset being burned. - let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await { + let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await { Ok(wallet) => wallet, Err(err) => { eprintln!("Wallet decryption failed: {err}"); diff --git a/src/bin/create_collateral_claim_tx.rs b/src/bin/create_collateral_claim_tx.rs index 6dad2d0..86a0889 100644 --- a/src/bin/create_collateral_claim_tx.rs +++ b/src/bin/create_collateral_claim_tx.rs @@ -1,8 +1,9 @@ use blockchain::blocks::collateral::{ CollateralClaimTransaction, UnsignedCollateralClaimTransaction, }; -use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible}; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path}; use blockchain::common::types::COLLATERAL_FEE; + use blockchain::json; use blockchain::wallets::structures::Wallet; use blockchain::File; @@ -35,6 +36,7 @@ async fn main() { .parse() .expect("Please enter a valid fee"); let txfee = (txfee_f64 * 100_000_000.0).round() as u64; + let wallet_path = prompt_wallet_path().await; let decryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -43,7 +45,7 @@ async fn main() { .await; // Load the wallet so the transaction can use the saved short address and private key. - let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await { + let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await { Ok(wallet) => wallet, Err(err) => { eprintln!("Wallet decryption failed: {err}"); diff --git a/src/bin/create_issue_token_tx.rs b/src/bin/create_issue_token_tx.rs index 4c09794..553c455 100644 --- a/src/bin/create_issue_token_tx.rs +++ b/src/bin/create_issue_token_tx.rs @@ -1,6 +1,7 @@ use blockchain::blocks::issue_token::{IssueTokenTransaction, UnsignedIssueTokenTransaction}; -use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible}; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path}; use blockchain::common::types::ISSUE_TOKEN_FEE; + use blockchain::json; use blockchain::wallets::structures::Wallet; use blockchain::File; @@ -49,6 +50,7 @@ async fn main() { .parse() .expect("Please enter a valid fee."); let txfee = (txfee_f64 * 100_000_000.0).round() as u64; + let wallet_path = prompt_wallet_path().await; let decryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -57,7 +59,7 @@ async fn main() { .await; // Load the creator wallet that signs the additional token issuance. - let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await { + let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await { Ok(wallet) => wallet, Err(err) => { eprintln!("Wallet decryption failed: {err}"); diff --git a/src/bin/create_loan_payment_tx.rs b/src/bin/create_loan_payment_tx.rs index a537878..0504f19 100644 --- a/src/bin/create_loan_payment_tx.rs +++ b/src/bin/create_loan_payment_tx.rs @@ -1,8 +1,9 @@ use blockchain::blocks::loan_payment::{ ContractPaymentTransaction, UnsignedContractPaymentTransaction, }; -use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible}; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path}; use blockchain::common::types::BORROWER_FEE; + use blockchain::json; use blockchain::wallets::structures::Wallet; use blockchain::File; @@ -53,6 +54,7 @@ async fn main() { .parse() .expect("Please enter a valid fee"); let txfee = (txfee_f64 * 100_000_000.0).round() as u64; + let wallet_path = prompt_wallet_path().await; let decryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -61,7 +63,7 @@ async fn main() { .await; // Load the wallet so the transaction can use the saved short address and private key. - let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await { + let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await { Ok(wallet) => wallet, Err(err) => { eprintln!("Wallet decryption failed: {err}"); diff --git a/src/bin/create_loan_tx.rs b/src/bin/create_loan_tx.rs index f1616ec..2525f5e 100644 --- a/src/bin/create_loan_tx.rs +++ b/src/bin/create_loan_tx.rs @@ -1,8 +1,9 @@ use blockchain::blocks::loans::UnsignedLoanContractTransaction; -use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible}; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path}; use blockchain::common::types::LENDER_FEE; + use blockchain::json; -use blockchain::records::wallet_registry::resolve_local_input_short_address; +use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input; use blockchain::wallets::structures::Wallet; use blockchain::File; use blockchain::{create_dir_all, AsyncWriteExt}; @@ -20,11 +21,6 @@ fn display_fee(value: u64) -> f64 { value as f64 / 100_000_000.0 } -fn normalize_short_address_input(address: &str) -> Result { - // Accept local vanity/short input and resolve it into the real short address. - resolve_local_input_short_address(address.trim()) -} - fn parse_start_date(input: &str) -> Result { // Loan start dates are entered as calendar dates and stored as local midnight timestamps. let date = NaiveDate::parse_from_str(input.trim(), "%Y-%m-%d") @@ -43,6 +39,23 @@ fn parse_start_date(input: &str) -> Result { Ok(timestamp as u32) } +async fn load_signing_wallet() -> Option { + let wallet_path = prompt_wallet_path().await; + let decryption_key = prompt_hidden_nonempty( + "What is your wallet decryption key? ", + "Wallet key cannot be empty. Please try again.", + ) + .await; + + match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await { + Ok(wallet) => Some(wallet), + Err(err) => { + eprintln!("Wallet decryption failed: {err}"); + None + } + } +} + #[tokio::main] async fn main() { // Loan contracts use transaction type 7 and are first signed by the lender. @@ -130,12 +143,18 @@ async fn main() { } }; + // Load the lender wallet before resolving vanity inputs; remote vanity lookup needs a signed handshake. + let wallet = match load_signing_wallet().await { + Some(wallet) => wallet, + None => return, + }; + // Resolve the borrower input before writing it into the loan offer. let borrower = prompt_visible("What is the wallet address of the borrower? ").await; - let borrower = match normalize_short_address_input(borrower.trim()) { + let borrower = match resolve_wallet_address_input(borrower.trim(), &wallet).await { Ok(address) => address, - Err(_) => { - println!("borrower wallet invalid"); + Err(err) => { + println!("borrower wallet invalid: {err}"); return; } }; @@ -151,20 +170,6 @@ async fn main() { .expect("Please enter a valid fee"); let txfee = (txfee_f64 * 100_000_000.0).round() as u64; - // Load the lender wallet that creates the first loan-contract signature. - let decryption_key = prompt_hidden_nonempty( - "What is your wallet decryption key? ", - "Wallet key cannot be empty. Please try again.", - ) - .await; - - let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await { - Ok(wallet) => wallet, - Err(err) => { - eprintln!("Wallet decryption failed: {err}"); - return; - } - }; let private_key = &wallet.saved.private_key; let lender = &wallet.saved.short_address; diff --git a/src/bin/create_marketing_tx.rs b/src/bin/create_marketing_tx.rs index d6f501c..c62b559 100644 --- a/src/bin/create_marketing_tx.rs +++ b/src/bin/create_marketing_tx.rs @@ -1,6 +1,7 @@ use blockchain::blocks::marketing::{MarketingTransaction, UnsignedMarketingTransaction}; -use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible}; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path}; use blockchain::common::types::MARKETING_FEE; + use blockchain::json; use blockchain::wallets::structures::Wallet; use blockchain::File; @@ -75,6 +76,7 @@ async fn main() { .parse() .expect("Please enter a valid value."); let txfee = ((txfee_f32 as f64) * (100000000_f64)).round() as u64; + let wallet_path = prompt_wallet_path().await; let decryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -82,7 +84,7 @@ async fn main() { ) .await; - let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await { + let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await { Ok(wallet) => wallet, Err(err) => { eprintln!("Wallet decryption failed: {err}"); diff --git a/src/bin/create_nft_tx.rs b/src/bin/create_nft_tx.rs index c3ccd56..9abc966 100644 --- a/src/bin/create_nft_tx.rs +++ b/src/bin/create_nft_tx.rs @@ -1,6 +1,7 @@ use blockchain::blocks::nft::{CreateNftTransaction, UnsignedCreateNftTransaction}; -use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible}; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path}; use blockchain::common::types::CREATE_NFT_FEE; + use blockchain::json; use blockchain::wallets::structures::Wallet; use blockchain::File; @@ -60,6 +61,7 @@ async fn main() { .parse() .expect("Please enter a valid value."); let txfee = ((txfee_f32 as f64) * (100000000_f64)).round() as u64; + let wallet_path = prompt_wallet_path().await; let decryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -67,7 +69,7 @@ async fn main() { ) .await; - let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await { + let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await { Ok(wallet) => wallet, Err(err) => { eprintln!("Wallet decryption failed: {err}"); diff --git a/src/bin/create_swap_tx.rs b/src/bin/create_swap_tx.rs index 5fa4412..f36d9a0 100644 --- a/src/bin/create_swap_tx.rs +++ b/src/bin/create_swap_tx.rs @@ -1,8 +1,9 @@ use blockchain::blocks::swap::UnsignedSwapTransaction; -use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible}; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path}; use blockchain::common::types::SWAP_FEE; + use blockchain::json; -use blockchain::records::wallet_registry::resolve_local_input_short_address; +use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input; use blockchain::wallets::structures::Wallet; use blockchain::Duration; use blockchain::File; @@ -20,10 +21,6 @@ fn display_fee(value: u64) -> f64 { value as f64 / 100_000_000.0 } -fn normalize_short_address_input(address: &str) -> Result { - resolve_local_input_short_address(address.trim()) -} - #[tokio::main] async fn main() { // set type and timestampe @@ -75,14 +72,30 @@ async fn main() { .expect("Please enter a valid age"); let value2 = (value2_f64 * (100000000_f64)).round() as u64; + let wallet_path = prompt_wallet_path().await; + + let decryption_key = prompt_hidden_nonempty( + "What is your wallet decryption key? ", + "Wallet key cannot be empty. Please try again.", + ) + .await; + + let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await { + Ok(wallet) => wallet, + Err(err) => { + eprintln!("Wallet decryption failed: {err}"); + return; + } + }; + // get user input swapper address let sender2 = prompt_visible("Please enter the wallet address of the account you are swapping with: ") .await; - let sender2 = match normalize_short_address_input(&sender2) { + let sender2 = match resolve_wallet_address_input(&sender2, &wallet).await { Ok(address) => address, - Err(_) => { - println!("reciver wallet is not valid"); + Err(err) => { + println!("receiver wallet is not valid: {err}"); return; } }; @@ -135,20 +148,6 @@ async fn main() { let expiration_time: u32 = expiration_duration.as_secs() as u32; let expires = timestamp + expiration_time; - let decryption_key = prompt_hidden_nonempty( - "What is your wallet decryption key? ", - "Wallet key cannot be empty. Please try again.", - ) - .await; - - let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await { - Ok(wallet) => wallet, - Err(err) => { - eprintln!("Wallet decryption failed: {err}"); - return; - } - }; - let private_key = &wallet.saved.private_key; let address = &wallet.saved.short_address; diff --git a/src/bin/create_tokens_tx.rs b/src/bin/create_tokens_tx.rs index ee52930..8bcc177 100644 --- a/src/bin/create_tokens_tx.rs +++ b/src/bin/create_tokens_tx.rs @@ -1,6 +1,7 @@ use blockchain::blocks::token::{CreateTokenTransaction, UnsignedCreateTokenTransaction}; -use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible}; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path}; use blockchain::common::types::CREATE_TOKEN_FEE; + use blockchain::json; use blockchain::wallets::structures::Wallet; use blockchain::File; @@ -61,6 +62,7 @@ async fn main() { .parse() .expect("Please enter a valid value."); let txfee = ((txfee_f32 as f64) * (100000000_f64)).round() as u64; + let wallet_path = prompt_wallet_path().await; let decryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -68,7 +70,7 @@ async fn main() { ) .await; - let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await { + let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await { Ok(wallet) => wallet, Err(err) => { eprintln!("Wallet decryption failed: {err}"); diff --git a/src/bin/create_transfer_tx.rs b/src/bin/create_transfer_tx.rs index 054b83c..73b3f0b 100644 --- a/src/bin/create_transfer_tx.rs +++ b/src/bin/create_transfer_tx.rs @@ -1,10 +1,11 @@ use blockchain::blocks::transfer::{TransferTransaction, UnsignedTransferTransaction}; -use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible}; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path}; use blockchain::common::network_paths_and_settings::block_extension_and_paths; + use blockchain::common::types::{NON_BASE_TRANSFER_MIN_FEE, TRANSFER_FEE}; use blockchain::env; use blockchain::json; -use blockchain::records::wallet_registry::resolve_local_input_short_address; +use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input; use blockchain::wallets::structures::Wallet; use blockchain::File; use blockchain::Utc; @@ -21,10 +22,6 @@ fn display_fee(value: u64) -> f64 { value as f64 / 100_000_000.0 } -fn normalize_short_address_input(address: &str) -> Result { - resolve_local_input_short_address(address.trim()) -} - #[tokio::main] async fn main() { let minimum_transfer_fee_percent = TRANSFER_FEE * 100.0; @@ -60,12 +57,28 @@ async fn main() { .expect("Please enter a valid amount."); let value = ((value_f32 as f64) * 100_000_000.0).round() as u64; + let wallet_path = prompt_wallet_path().await; + + let decryption_key = prompt_hidden_nonempty( + "What is your wallet decryption key? ", + "Wallet key cannot be empty. Please try again.", + ) + .await; + + let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await { + Ok(wallet) => wallet, + Err(err) => { + eprintln!("Wallet decryption failed: {err}"); + return; + } + }; + let receiver_input = if args.len() > 3 { args[3].clone() } else { prompt_visible("Please enter the receiver wallet address: ").await }; - let receiver = match normalize_short_address_input(&receiver_input) { + let receiver = match resolve_wallet_address_input(&receiver_input, &wallet).await { Ok(address) => address, Err(err) => { let trimmed_receiver = receiver_input.trim(); @@ -146,20 +159,6 @@ async fn main() { .parse() .expect("Please enter a valid NFT series number."); - let decryption_key = prompt_hidden_nonempty( - "What is your wallet decryption key? ", - "Wallet key cannot be empty. Please try again.", - ) - .await; - - let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await { - Ok(wallet) => wallet, - Err(err) => { - eprintln!("Wallet decryption failed: {err}"); - return; - } - }; - let private_key = &wallet.saved.private_key; let short_address = &wallet.saved.short_address; diff --git a/src/bin/create_vanity_tx.rs b/src/bin/create_vanity_tx.rs index 862f795..cb8983d 100644 --- a/src/bin/create_vanity_tx.rs +++ b/src/bin/create_vanity_tx.rs @@ -1,6 +1,7 @@ use blockchain::blocks::vanity::{UnsignedVanityAddressTransaction, VanityAddressTransaction}; -use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible}; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path}; use blockchain::common::types::{VANITY_ADDRESS_FEE, VANITY_ADDRESS_TYPE}; + use blockchain::env; use blockchain::json; use blockchain::wallets::structures::Wallet; @@ -70,6 +71,7 @@ async fn main() { .parse() .expect("Please enter a valid fee."); let txfee = ((txfee_f32 as f64) * 100_000_000.0).round() as u64; + let wallet_path = prompt_wallet_path().await; let decryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -78,7 +80,7 @@ async fn main() { .await; // Load the wallet that will own and sign the vanity registration. - let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await { + let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await { Ok(wallet) => wallet, Err(err) => { eprintln!("Wallet decryption failed: {err}"); diff --git a/src/bin/lookup_block_by_hash.rs b/src/bin/lookup_block_by_hash.rs index 2416448..d8f2f07 100644 --- a/src/bin/lookup_block_by_hash.rs +++ b/src/bin/lookup_block_by_hash.rs @@ -1,4 +1,4 @@ -use blockchain::common::cli_prompts::prompt_hidden_nonempty; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; use blockchain::records::memory::response_channels::generate_uid; @@ -24,6 +24,7 @@ async fn main() { println!("Please enter a valid 64-character block hash"); return; } + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -44,7 +45,10 @@ async fn main() { socket_address, block_hash.clone(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.clone(), + wallet_path: wallet_path.clone(), + }, hashmap_key, ) .await; diff --git a/src/bin/lookup_block_by_height.rs b/src/bin/lookup_block_by_height.rs index 6c55563..6085928 100644 --- a/src/bin/lookup_block_by_height.rs +++ b/src/bin/lookup_block_by_height.rs @@ -1,4 +1,4 @@ -use blockchain::common::cli_prompts::prompt_hidden_nonempty; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; use blockchain::records::memory::response_channels::generate_uid; @@ -26,6 +26,7 @@ async fn main() { return; } }; + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -48,7 +49,10 @@ async fn main() { socket_address, payload.clone(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.clone(), + wallet_path: wallet_path.clone(), + }, hashmap_key, ) .await; diff --git a/src/bin/lookup_contract_by_address.rs b/src/bin/lookup_contract_by_address.rs index 88fc526..a3e3943 100644 --- a/src/bin/lookup_contract_by_address.rs +++ b/src/bin/lookup_contract_by_address.rs @@ -1,9 +1,11 @@ -use blockchain::common::cli_prompts::prompt_hidden_nonempty; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::encode; use blockchain::env; use blockchain::records::memory::response_channels::generate_uid; use blockchain::standalone_tools::connections::handshake; +use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input; +use blockchain::wallets::structures::Wallet; #[tokio::main] async fn main() { @@ -25,12 +27,26 @@ async fn main() { return; } }; - + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", "Wallet key cannot be empty. Please try again.", ) .await; + let wallet = match Wallet::try_obtain_wallet(encryption_key, Some(&wallet_path)).await { + Ok(wallet) => wallet, + Err(err) => { + eprintln!("Wallet decryption failed: {err}"); + return; + } + }; + let wallet_address = match resolve_wallet_address_input(&wallet_address, &wallet).await { + Ok(address) => address, + Err(err) => { + eprintln!("wallet address is not valid: {err}"); + return; + } + }; // Try each configured peer until one returns a response. let connections = get_connections().await; @@ -46,7 +62,10 @@ async fn main() { socket_address, wallet_address.clone(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletParts { + long_address: wallet.saved.long_address.clone(), + private_key: wallet.saved.private_key.clone(), + }, hashmap_key, ) .await; diff --git a/src/bin/lookup_contract_by_hash.rs b/src/bin/lookup_contract_by_hash.rs index 8966043..d756834 100644 --- a/src/bin/lookup_contract_by_hash.rs +++ b/src/bin/lookup_contract_by_hash.rs @@ -1,4 +1,4 @@ -use blockchain::common::cli_prompts::prompt_hidden_nonempty; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::encode; use blockchain::env; @@ -25,6 +25,7 @@ async fn main() { return; } }; + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -46,7 +47,10 @@ async fn main() { socket_address, contract_hash.clone(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.clone(), + wallet_path: wallet_path.clone(), + }, hashmap_key, ) .await; diff --git a/src/bin/lookup_difficulty.rs b/src/bin/lookup_difficulty.rs index fdf7285..4bee6bf 100644 --- a/src/bin/lookup_difficulty.rs +++ b/src/bin/lookup_difficulty.rs @@ -1,4 +1,4 @@ -use blockchain::common::cli_prompts::prompt_hidden_nonempty; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; use blockchain::records::memory::response_channels::generate_uid; @@ -10,6 +10,7 @@ async fn main() { let hashmap_key = generate_uid(); let _args: Vec = env::args().collect(); + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", "Wallet key cannot be empty. Please try again.", @@ -32,7 +33,10 @@ async fn main() { socket_address, json.clone(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.clone(), + wallet_path: wallet_path.clone(), + }, hashmap_key, ) .await; diff --git a/src/bin/lookup_height.rs b/src/bin/lookup_height.rs index 6dbc31c..1118c84 100644 --- a/src/bin/lookup_height.rs +++ b/src/bin/lookup_height.rs @@ -1,4 +1,4 @@ -use blockchain::common::cli_prompts::prompt_hidden_nonempty; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; use blockchain::records::memory::response_channels::generate_uid; @@ -16,6 +16,7 @@ async fn main() { println!("Usage: ./request_height"); return; } + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -40,7 +41,10 @@ async fn main() { socket_address, json.clone(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.clone(), + wallet_path: wallet_path.clone(), + }, hashmap_key, ) .await; diff --git a/src/bin/lookup_largest_txfee.rs b/src/bin/lookup_largest_txfee.rs index 2f8b6b5..db07226 100644 --- a/src/bin/lookup_largest_txfee.rs +++ b/src/bin/lookup_largest_txfee.rs @@ -1,4 +1,4 @@ -use blockchain::common::cli_prompts::prompt_hidden_nonempty; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; use blockchain::records::memory::response_channels::generate_uid; @@ -22,6 +22,7 @@ async fn main() { println!("Usage: ./large_tx_fee"); return; } + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -46,7 +47,10 @@ async fn main() { socket_address, json.clone(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.clone(), + wallet_path: wallet_path.clone(), + }, hashmap_key, ) .await; diff --git a/src/bin/lookup_mempool_tx_by_address.rs b/src/bin/lookup_mempool_tx_by_address.rs index c78ef5c..4cbde7b 100644 --- a/src/bin/lookup_mempool_tx_by_address.rs +++ b/src/bin/lookup_mempool_tx_by_address.rs @@ -9,7 +9,8 @@ use blockchain::blocks::swap::SwapTransaction; use blockchain::blocks::token::CreateTokenTransaction; use blockchain::blocks::transfer::TransferTransaction; use blockchain::blocks::vanity::VanityAddressTransaction; -use blockchain::common::cli_prompts::prompt_hidden_nonempty; + +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::common::types::{ BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE, @@ -19,7 +20,9 @@ use blockchain::env; use blockchain::records::memory::response_channels::generate_uid; use blockchain::rpc::command_maps; use blockchain::standalone_tools::connections::handshake; +use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input; use blockchain::to_string_pretty; +use blockchain::wallets::structures::Wallet; async fn decode_one_transaction(tx_bytes: &[u8]) -> Option { let txtype = *tx_bytes.first()?; @@ -106,12 +109,26 @@ async fn main() { return; } let address = args[1].trim().to_string(); - + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", "Wallet key cannot be empty. Please try again.", ) .await; + let wallet = match Wallet::try_obtain_wallet(encryption_key, Some(&wallet_path)).await { + Ok(wallet) => wallet, + Err(err) => { + eprintln!("Wallet decryption failed: {err}"); + return; + } + }; + let address = match resolve_wallet_address_input(&address, &wallet).await { + Ok(address) => address, + Err(err) => { + eprintln!("wallet address is not valid: {err}"); + return; + } + }; let connections = get_connections().await; let mut connected = false; @@ -126,7 +143,10 @@ async fn main() { socket_address, address.clone(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletParts { + long_address: wallet.saved.long_address.clone(), + private_key: wallet.saved.private_key.clone(), + }, generate_uid(), ) .await; diff --git a/src/bin/lookup_mempool_tx_by_signature.rs b/src/bin/lookup_mempool_tx_by_signature.rs index 72f2b6b..5096a2e 100644 --- a/src/bin/lookup_mempool_tx_by_signature.rs +++ b/src/bin/lookup_mempool_tx_by_signature.rs @@ -9,7 +9,8 @@ use blockchain::blocks::swap::SwapTransaction; use blockchain::blocks::token::CreateTokenTransaction; use blockchain::blocks::transfer::TransferTransaction; use blockchain::blocks::vanity::VanityAddressTransaction; -use blockchain::common::cli_prompts::prompt_hidden_nonempty; + +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::common::types::{ BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE, @@ -87,6 +88,7 @@ async fn main() { return; } let signature = args[1].trim().to_string(); + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -107,7 +109,10 @@ async fn main() { socket_address, signature.clone(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.clone(), + wallet_path: wallet_path.clone(), + }, generate_uid(), ) .await; diff --git a/src/bin/lookup_mempool_tx_count.rs b/src/bin/lookup_mempool_tx_count.rs index ce220ce..46e4fa8 100644 --- a/src/bin/lookup_mempool_tx_count.rs +++ b/src/bin/lookup_mempool_tx_count.rs @@ -1,4 +1,4 @@ -use blockchain::common::cli_prompts::prompt_hidden_nonempty; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; use blockchain::records::memory::response_channels::generate_uid; @@ -15,6 +15,7 @@ async fn main() { println!("Usage: ./lookup_mempool_tx_count"); return; } + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -36,7 +37,10 @@ async fn main() { socket_address, "".to_string(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.clone(), + wallet_path: wallet_path.clone(), + }, generate_uid(), ) .await; diff --git a/src/bin/lookup_network_info.rs b/src/bin/lookup_network_info.rs index 10d6e80..35205ef 100644 --- a/src/bin/lookup_network_info.rs +++ b/src/bin/lookup_network_info.rs @@ -1,4 +1,4 @@ -use blockchain::common::cli_prompts::prompt_hidden_nonempty; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; use blockchain::json; @@ -86,6 +86,7 @@ async fn main() { println!("Usage: ./lookup_network_info"); return; } + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -108,7 +109,10 @@ async fn main() { socket_address, "".to_string(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.clone(), + wallet_path: wallet_path.clone(), + }, hashmap_key, ) .await; diff --git a/src/bin/lookup_nft.rs b/src/bin/lookup_nft.rs index 7542f02..f2687ee 100644 --- a/src/bin/lookup_nft.rs +++ b/src/bin/lookup_nft.rs @@ -1,5 +1,6 @@ use blockchain::common::binary_conversions::binary_to_string; -use blockchain::common::cli_prompts::prompt_hidden_nonempty; + +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::encode; use blockchain::env; @@ -181,6 +182,7 @@ async fn main() { let nft_name = args[1].clone(); let item_number = args[2].clone(); + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", "Wallet key cannot be empty. Please try again.", @@ -203,7 +205,10 @@ async fn main() { socket_address, payload.clone(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.clone(), + wallet_path: wallet_path.clone(), + }, hashmap_key, ) .await; diff --git a/src/bin/lookup_nft_list.rs b/src/bin/lookup_nft_list.rs index 42ef114..fea10e3 100644 --- a/src/bin/lookup_nft_list.rs +++ b/src/bin/lookup_nft_list.rs @@ -1,5 +1,6 @@ use blockchain::common::binary_conversions::binary_to_string; -use blockchain::common::cli_prompts::prompt_hidden_nonempty; + +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; use blockchain::json; @@ -65,6 +66,7 @@ async fn main() { println!("Usage: ./nft_list"); return; } + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -85,7 +87,10 @@ async fn main() { socket_address, "".to_string(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.clone(), + wallet_path: wallet_path.clone(), + }, hashmap_key, ) .await; diff --git a/src/bin/lookup_node_time.rs b/src/bin/lookup_node_time.rs index f7640f2..bb6f793 100644 --- a/src/bin/lookup_node_time.rs +++ b/src/bin/lookup_node_time.rs @@ -1,4 +1,4 @@ -use blockchain::common::cli_prompts::prompt_hidden_nonempty; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; use blockchain::records::memory::response_channels::generate_uid; @@ -16,6 +16,7 @@ async fn main() { println!("Usage: ./lookup_node_time"); return; } + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -37,7 +38,10 @@ async fn main() { socket_address, "".to_string(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.clone(), + wallet_path: wallet_path.clone(), + }, generate_uid(), ) .await; diff --git a/src/bin/lookup_remote_balance.rs b/src/bin/lookup_remote_balance.rs index dc7b582..cd2819c 100644 --- a/src/bin/lookup_remote_balance.rs +++ b/src/bin/lookup_remote_balance.rs @@ -1,11 +1,13 @@ -use blockchain::common::cli_prompts::prompt_hidden_nonempty; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; use blockchain::from_str; use blockchain::read_to_string; use blockchain::records::memory::response_channels::generate_uid; use blockchain::standalone_tools::connections::handshake; +use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input; use blockchain::tilde; +use blockchain::wallets::structures::Wallet; use blockchain::Value; use rustyline::completion::FilenameCompleter; use rustyline::error::ReadlineError; @@ -37,6 +39,7 @@ fn extract_address(contents: &str) -> Result { let address = value .get("short_address") .and_then(|v| v.as_str()) + .or_else(|| value.get("vanity_address").and_then(|v| v.as_str())) .or_else(|| value.get("long_address").and_then(|v| v.as_str())) .ok_or_else(|| "Wallet JSON does not contain a usable address field".to_string())?; return Ok(address.trim().to_string()); @@ -110,12 +113,26 @@ async fn main() { return; } }; - + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", "Wallet key cannot be empty. Please try again.", ) .await; + let wallet = match Wallet::try_obtain_wallet(encryption_key, Some(&wallet_path)).await { + Ok(wallet) => wallet, + Err(err) => { + eprintln!("Wallet decryption failed: {err}"); + return; + } + }; + let wallet_address = match resolve_wallet_address_input(&wallet_address, &wallet).await { + Ok(address) => address, + Err(err) => { + eprintln!("wallet address is not valid: {err}"); + return; + } + }; let json = wallet_address; // Try each configured peer until one returns a parsable balance response or text error. @@ -131,7 +148,10 @@ async fn main() { socket_address, json.clone(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletParts { + long_address: wallet.saved.long_address.clone(), + private_key: wallet.saved.private_key.clone(), + }, hashmap_key, ) .await; diff --git a/src/bin/lookup_token.rs b/src/bin/lookup_token.rs index 86e09fe..d11d088 100644 --- a/src/bin/lookup_token.rs +++ b/src/bin/lookup_token.rs @@ -1,5 +1,6 @@ use blockchain::common::binary_conversions::binary_to_string; -use blockchain::common::cli_prompts::prompt_hidden_nonempty; + +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; use blockchain::json; @@ -36,6 +37,7 @@ async fn main() { } let token_name = args[1].clone(); + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", "Wallet key cannot be empty. Please try again.", @@ -56,7 +58,10 @@ async fn main() { socket_address, token_name.clone(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.clone(), + wallet_path: wallet_path.clone(), + }, hashmap_key, ) .await; diff --git a/src/bin/lookup_token_list.rs b/src/bin/lookup_token_list.rs index 0214379..ae45437 100644 --- a/src/bin/lookup_token_list.rs +++ b/src/bin/lookup_token_list.rs @@ -1,5 +1,6 @@ use blockchain::common::binary_conversions::binary_to_string; -use blockchain::common::cli_prompts::prompt_hidden_nonempty; + +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; use blockchain::json; @@ -55,6 +56,7 @@ async fn main() { println!("Usage: ./token_list"); return; } + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -75,7 +77,10 @@ async fn main() { socket_address, "".to_string(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.clone(), + wallet_path: wallet_path.clone(), + }, hashmap_key, ) .await; diff --git a/src/bin/lookup_torrent.rs b/src/bin/lookup_torrent.rs index a70665e..a4dc90f 100644 --- a/src/bin/lookup_torrent.rs +++ b/src/bin/lookup_torrent.rs @@ -1,4 +1,4 @@ -use blockchain::common::cli_prompts::prompt_hidden_nonempty; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; use blockchain::records::memory::response_channels::generate_uid; @@ -32,6 +32,7 @@ async fn main() { }; // Extract the encryption key from the command-line arguments + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", "Wallet key cannot be empty. Please try again.", @@ -52,7 +53,10 @@ async fn main() { socket_address, json.clone(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.clone(), + wallet_path: wallet_path.clone(), + }, hashmap_key, ) .await; diff --git a/src/bin/lookup_total_transactions.rs b/src/bin/lookup_total_transactions.rs index 859aeaa..4991bc8 100644 --- a/src/bin/lookup_total_transactions.rs +++ b/src/bin/lookup_total_transactions.rs @@ -1,4 +1,4 @@ -use blockchain::common::cli_prompts::prompt_hidden_nonempty; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::common::types::{ BORROWER_TYPE, BURN_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE, @@ -20,6 +20,7 @@ async fn main() { println!("Usage: ./total_transactions"); return; } + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -46,7 +47,10 @@ async fn main() { socket_address, json.clone(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.clone(), + wallet_path: wallet_path.clone(), + }, hashmap_key, ) .await; diff --git a/src/bin/lookup_transaction.rs b/src/bin/lookup_transaction.rs index b156e6b..63a6ccb 100644 --- a/src/bin/lookup_transaction.rs +++ b/src/bin/lookup_transaction.rs @@ -9,7 +9,8 @@ use blockchain::blocks::swap::SwapTransaction; use blockchain::blocks::token::CreateTokenTransaction; use blockchain::blocks::transfer::TransferTransaction; use blockchain::blocks::vanity::VanityAddressTransaction; -use blockchain::common::cli_prompts::prompt_hidden_nonempty; + +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::common::types::{ BORROWER_TYPE, COLLATERAL_TYPE, CREATE_NFT_TYPE, CREATE_TOKEN_TYPE, GENESIS_TYPE, LENDER_TYPE, @@ -46,6 +47,7 @@ async fn main() { }; // Extract the encryption ley from the command-line argument + let wallet_path = prompt_wallet_path().await; let encryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", "Wallet key cannot be empty. Please try again.", @@ -68,7 +70,10 @@ async fn main() { socket_address, json.clone(), rpc_command, - handshake::HandshakeWallet::WalletKey(encryption_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.clone(), + wallet_path: wallet_path.clone(), + }, hashmap_key, ) .await; diff --git a/src/bin/register_wallet.rs b/src/bin/register_wallet.rs index ff0dc03..d4a0f0e 100644 --- a/src/bin/register_wallet.rs +++ b/src/bin/register_wallet.rs @@ -1,4 +1,4 @@ -use blockchain::common::cli_prompts::prompt_hidden_nonempty; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::common::skein::skein_256_hash_bytes; use blockchain::env; @@ -18,6 +18,7 @@ async fn main() { println!("Usage: ./register_wallet"); return; } + let wallet_path = prompt_wallet_path().await; let decryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -26,7 +27,7 @@ async fn main() { .await; // Load the wallet so both address forms and the signing key come from the same saved file. - let wallet = match Wallet::try_obtain_wallet(decryption_key.clone(), None).await { + let wallet = match Wallet::try_obtain_wallet(decryption_key.clone(), Some(&wallet_path)).await { Ok(wallet) => wallet, Err(err) => { eprintln!("Wallet decryption failed: {err}"); @@ -73,7 +74,10 @@ async fn main() { socket_address, json.clone(), rpc_command, - handshake::HandshakeWallet::WalletKey(decryption_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: decryption_key.clone(), + wallet_path: wallet_path.clone(), + }, hashmap_key, ) .await; diff --git a/src/bin/server_owner_block_ip.rs b/src/bin/server_owner_block_ip.rs index 85d04e6..a399418 100644 --- a/src/bin/server_owner_block_ip.rs +++ b/src/bin/server_owner_block_ip.rs @@ -1,4 +1,4 @@ -use blockchain::common::cli_prompts::prompt_hidden_nonempty; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; use blockchain::records::memory::response_channels::generate_uid; @@ -17,6 +17,7 @@ async fn main() { return; } let ip = args[1].trim().to_string(); + let wallet_path = prompt_wallet_path().await; let wallet_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -25,7 +26,7 @@ async fn main() { .await; // Server-side verification expects a signature over the exact IP string. - let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await { + let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), Some(&wallet_path)).await { Ok(wallet) => wallet, Err(err) => { eprintln!("Wallet decryption failed: {err}"); @@ -48,7 +49,10 @@ async fn main() { socket_address, payload.clone(), rpc_command, - handshake::HandshakeWallet::WalletKey(wallet_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: wallet_key.clone(), + wallet_path: wallet_path.clone(), + }, generate_uid(), ) .await; diff --git a/src/bin/server_owner_unblock_ip.rs b/src/bin/server_owner_unblock_ip.rs index 152adb6..020f162 100644 --- a/src/bin/server_owner_unblock_ip.rs +++ b/src/bin/server_owner_unblock_ip.rs @@ -1,4 +1,4 @@ -use blockchain::common::cli_prompts::prompt_hidden_nonempty; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; use blockchain::records::memory::response_channels::generate_uid; @@ -17,6 +17,7 @@ async fn main() { return; } let ip = args[1].trim().to_string(); + let wallet_path = prompt_wallet_path().await; let wallet_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -25,7 +26,7 @@ async fn main() { .await; // Server-side verification expects a signature over the exact IP string. - let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), None).await { + let wallet = match Wallet::try_obtain_wallet(wallet_key.clone(), Some(&wallet_path)).await { Ok(wallet) => wallet, Err(err) => { eprintln!("Wallet decryption failed: {err}"); @@ -48,7 +49,10 @@ async fn main() { socket_address, payload.clone(), rpc_command, - handshake::HandshakeWallet::WalletKey(wallet_key.clone()), + handshake::HandshakeWallet::WalletKey { + encryption_key: wallet_key.clone(), + wallet_path: wallet_path.clone(), + }, generate_uid(), ) .await; diff --git a/src/bin/sign_message.rs b/src/bin/sign_message.rs index 6cb677f..3df84e7 100644 --- a/src/bin/sign_message.rs +++ b/src/bin/sign_message.rs @@ -1,4 +1,4 @@ -use blockchain::common::cli_prompts::prompt_hidden_nonempty; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::skein::skein_256_hash_data; use blockchain::env; use blockchain::wallets::structures::Wallet; @@ -20,6 +20,7 @@ async fn main() { return; } }; + let wallet_path = prompt_wallet_path().await; let decryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", @@ -28,7 +29,7 @@ async fn main() { .await; // Load the wallet whose private key will create the detached signature. - let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await { + let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await { Ok(wallet) => wallet, Err(err) => { eprintln!("Wallet decryption failed: {err}"); diff --git a/src/bin/unpack_torrent.rs b/src/bin/unpack_torrent.rs index e624ed6..571318a 100644 --- a/src/bin/unpack_torrent.rs +++ b/src/bin/unpack_torrent.rs @@ -1,56 +1,56 @@ -use blockchain::common::network_paths_and_settings::block_extension_and_paths; -use blockchain::env; -use blockchain::exit; -use blockchain::io; -use blockchain::to_string_pretty; -use blockchain::torrent::structs::Torrent; -use blockchain::AsyncReadExt; -use blockchain::File; -use blockchain::Path; - -#[tokio::main] -async fn main() -> io::Result<()> { - // This utility loads one local torrent file by block number and prints the decoded struct. - let args: Vec = env::args().collect(); - if args.len() < 2 { - eprintln!("Usage: {} ", args[0]); - exit(1); - } - - let block_number = &args[1]; - let ( - _network_name, - _padded_base_coin, - _block_ext, - torrent_path, - _wallet_path, - _block_path, - _db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - // Torrent files are scoped by the active network path returned from settings. - let torrent_filename = format!("{torrent_path}/{block_number}.torrent"); - - if !Path::new(&torrent_filename).exists() { - eprintln!("Torrent file not found: {torrent_filename}"); - return Ok(()); - } - - let mut torrent_file = File::open(&torrent_filename).await?; - let mut torrent_contents = Vec::new(); - torrent_file.read_to_end(&mut torrent_contents).await?; - - // The torrent parser validates the binary layout before the result is printed as JSON. - match Torrent::from_bytes(&torrent_contents).await { - Ok(torrent) => { - let json_pretty = to_string_pretty(&torrent)?; - println!("{json_pretty}"); - } - Err(e) => { - eprintln!("Failed to parse torrent: {e}"); - } - } - - Ok(()) -} +use blockchain::common::network_paths_and_settings::block_extension_and_paths; +use blockchain::env; +use blockchain::exit; +use blockchain::io; +use blockchain::to_string_pretty; +use blockchain::torrent::structs::Torrent; +use blockchain::AsyncReadExt; +use blockchain::File; +use blockchain::Path; + +#[tokio::main] +async fn main() -> io::Result<()> { + // This utility loads one local torrent file by block number and prints the decoded struct. + let args: Vec = env::args().collect(); + if args.len() < 2 { + eprintln!("Usage: {} ", args[0]); + exit(1); + } + + let block_number = &args[1]; + let ( + _network_name, + _padded_base_coin, + _block_ext, + torrent_path, + _wallet_path, + _block_path, + _db_path, + _balance_path, + _log_path, + ) = block_extension_and_paths(); + // Torrent files are scoped by the active network path returned from settings. + let torrent_filename = format!("{torrent_path}/{block_number}.torrent"); + + if !Path::new(&torrent_filename).exists() { + eprintln!("Torrent file not found: {torrent_filename}"); + return Ok(()); + } + + let mut torrent_file = File::open(&torrent_filename).await?; + let mut torrent_contents = Vec::new(); + torrent_file.read_to_end(&mut torrent_contents).await?; + + // The torrent parser validates the binary layout before the result is printed as JSON. + match Torrent::from_bytes(&torrent_contents).await { + Ok(torrent) => { + let json_pretty = to_string_pretty(&torrent)?; + println!("{json_pretty}"); + } + Err(e) => { + eprintln!("Failed to parse torrent: {e}"); + } + } + + Ok(()) +} diff --git a/src/bin/verify_sign_loan_tx.rs b/src/bin/verify_sign_loan_tx.rs index 739ffe1..b92393c 100644 --- a/src/bin/verify_sign_loan_tx.rs +++ b/src/bin/verify_sign_loan_tx.rs @@ -1,11 +1,14 @@ use blockchain::blocks::loans::UnsignedLoanContractTransaction; -use blockchain::common::cli_prompts::{ask_yes_no_question, prompt_hidden_nonempty}; +use blockchain::common::cli_prompts::{ + ask_yes_no_question, prompt_hidden_nonempty, prompt_wallet_path, +}; use blockchain::env; + use blockchain::from_str; use blockchain::fs; use blockchain::json; use blockchain::read_to_string; -use blockchain::records::wallet_registry::resolve_local_input_short_address; +use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input; use blockchain::to_string_pretty; use blockchain::wallets::structures::Wallet; use blockchain::Value; @@ -34,11 +37,6 @@ fn display_start_date(timestamp: u32) -> String { } } -fn normalize_short_address_input(address: &str) -> Result { - // Accept local vanity/short input and resolve it into the real short address. - resolve_local_input_short_address(address.trim()) -} - #[tokio::main] async fn main() { // Borrowers use this tool to review a lender-signed loan and add signature2. @@ -67,27 +65,48 @@ async fn main() { } }; + // Load the borrower wallet before resolving vanity inputs; remote vanity lookup needs a signed handshake. + let wallet_path = prompt_wallet_path().await; + let decryption_key = prompt_hidden_nonempty( + "What is your wallet decryption key? ", + "Wallet key cannot be empty. Please try again.", + ) + .await; + + let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await { + Ok(wallet) => wallet, + Err(err) => { + eprintln!("Wallet decryption failed: {err}"); + return; + } + }; + let private_key = &wallet.saved.private_key; + let address = &wallet.saved.short_address; + // Extract every field that participates in the hash so the borrower signs the same bytes. let txtype = 7; let timestamp = json["timestamp"].as_u64().unwrap_or_default() as u32; let loan_coin = json["loan_coin"].as_str().unwrap_or_default().to_string(); let loan_amount = json["loan_amount"].as_u64().unwrap_or_default(); - let lender = match normalize_short_address_input(json["lender"].as_str().unwrap_or_default()) { - Ok(address) => address, - Err(_) => { - println!("Transaction is not valid. Lender address is not a valid short address."); - return; - } - }; + let lender = + match resolve_wallet_address_input(json["lender"].as_str().unwrap_or_default(), &wallet) + .await + { + Ok(address) => address, + Err(err) => { + println!("Transaction is not valid. Lender address is not valid: {err}"); + return; + } + }; let collateral = json["collateral"].as_str().unwrap_or_default().to_string(); let collateral_amount = json["collateral_amount"].as_u64().unwrap_or_default(); let borrower = - match normalize_short_address_input(json["borrower"].as_str().unwrap_or_default()) { + match resolve_wallet_address_input(json["borrower"].as_str().unwrap_or_default(), &wallet) + .await + { Ok(address) => address, - Err(_) => { - println!( - "Transaction is not valid. Borrower address is not a valid short address." - ); + Err(err) => { + println!("Transaction is not valid. Borrower address is not valid: {err}"); return; } }; @@ -176,23 +195,6 @@ async fn main() { return; } - // Load the borrower wallet and ensure it matches the borrower address in the offer. - let decryption_key = prompt_hidden_nonempty( - "What is your wallet decryption key? ", - "Wallet key cannot be empty. Please try again.", - ) - .await; - - let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await { - Ok(wallet) => wallet, - Err(err) => { - eprintln!("Wallet decryption failed: {err}"); - return; - } - }; - let private_key = &wallet.saved.private_key; - let address = &wallet.saved.short_address; - if borrower.trim() != address.trim() { println!( "Transaction is not valid for your wallet address. Expected {borrower} found {address}" diff --git a/src/bin/verify_sign_swap_tx.rs b/src/bin/verify_sign_swap_tx.rs index 7a78245..f15d254 100644 --- a/src/bin/verify_sign_swap_tx.rs +++ b/src/bin/verify_sign_swap_tx.rs @@ -1,11 +1,14 @@ use blockchain::blocks::swap::UnsignedSwapTransaction; -use blockchain::common::cli_prompts::{ask_yes_no_question, prompt_hidden_nonempty}; +use blockchain::common::cli_prompts::{ + ask_yes_no_question, prompt_hidden_nonempty, prompt_wallet_path, +}; use blockchain::common::network_paths_and_settings::block_extension_and_paths; + use blockchain::env; use blockchain::fs; use blockchain::json; use blockchain::read_to_string; -use blockchain::records::wallet_registry::resolve_local_input_short_address; +use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input; use blockchain::wallets::structures::Wallet; use blockchain::Value; @@ -16,10 +19,6 @@ fn pad_to_width(input: &str, width: usize) -> String { result } -fn normalize_short_address_input(address: &str) -> Result { - resolve_local_input_short_address(address.trim()) -} - #[tokio::main] async fn main() { // Get the filename from the command line arguments @@ -29,13 +28,14 @@ async fn main() { return; } let filename = &args[1]; + let wallet_path = prompt_wallet_path().await; let decryption_key = prompt_hidden_nonempty( "What is your wallet decryption key? ", "Wallet key cannot be empty. Please try again.", ) .await; - let wallet = match Wallet::try_obtain_wallet(decryption_key, None).await { + let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await { Ok(wallet) => wallet, Err(err) => { eprintln!("Wallet decryption failed: {err}"); @@ -92,28 +92,32 @@ async fn main() { let txfee1_value: u64 = json["txfee1"].as_u64().unwrap_or_default(); let tip1_value: u64 = json["tip1"].as_u64().unwrap_or_default(); - let sender1 = match normalize_short_address_input(json["sender1"].as_str().unwrap_or_default()) - { - Ok(address) => address, - Err(_) => { - println!("sender1 wallet invalid"); - return; - } - }; + let sender1 = + match resolve_wallet_address_input(json["sender1"].as_str().unwrap_or_default(), &wallet) + .await + { + Ok(address) => address, + Err(err) => { + println!("sender1 wallet invalid: {err}"); + return; + } + }; let txfee2_value: u64 = json["txfee2"].as_u64().unwrap_or_default(); let txfee2 = txfee2_value as f64 / 100000000.0; let tip2_value: u64 = json["tip2"].as_u64().unwrap_or_default(); let tip2 = tip2_value as f64 / 100000000.0; - let sender2 = match normalize_short_address_input(json["sender2"].as_str().unwrap_or_default()) - { - Ok(address) => address, - Err(_) => { - println!("sender2 wallet invalid"); - return; - } - }; + let sender2 = + match resolve_wallet_address_input(json["sender2"].as_str().unwrap_or_default(), &wallet) + .await + { + Ok(address) => address, + Err(err) => { + println!("sender2 wallet invalid: {err}"); + return; + } + }; // ensure wallet and sender2 match if sender2 != address.trim() { diff --git a/src/common/cli_prompts.rs b/src/common/cli_prompts.rs index 1389eb3..4f55b62 100644 --- a/src/common/cli_prompts.rs +++ b/src/common/cli_prompts.rs @@ -1,8 +1,52 @@ use crate::read_password; use crate::stdout; use crate::AsyncWriteExt; +use rustyline::completion::FilenameCompleter; +use rustyline::error::ReadlineError; +use rustyline::{history::DefaultHistory, CompletionType, Config, Editor}; +use rustyline_derive::Completer; +use rustyline_derive::Helper as RustyHelper; +use rustyline_derive::Highlighter as RustyHighlighter; +use rustyline_derive::Hinter as RustyHinter; +use rustyline_derive::Validator as RustyValidator; use tokio::io::{stdin, AsyncBufReadExt, BufReader}; +#[derive(RustyHelper, Completer, RustyHighlighter, RustyHinter, RustyValidator)] +struct PathHelper { + #[rustyline(Completer)] + completer: FilenameCompleter, +} + +pub fn prompt_path(prompt: &str) -> Result { + // Rustyline gives CLI path prompts filesystem completion, including tab completion. + let config = Config::builder() + .completion_type(CompletionType::List) + .build(); + let mut editor = + Editor::::with_config(config).map_err(|e| e.to_string())?; + editor.set_helper(Some(PathHelper { + completer: FilenameCompleter::new(), + })); + + match editor.readline(prompt) { + Ok(value) => Ok(value.trim().to_string()), + Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => { + Err("Input cancelled".to_string()) + } + Err(err) => Err(err.to_string()), + } +} + +pub async fn prompt_wallet_path() -> String { + loop { + match prompt_path("Please enter the path to your wallet file: ") { + Ok(path) if !path.trim().is_empty() => return path, + Ok(_) => println!("Wallet path cannot be empty. Please try again."), + Err(err) => println!("Failed to read wallet path: {err}"), + } + } +} + pub async fn prompt_visible_with_default(prompt: &str, default: &str) -> String { // Show the default in brackets so pressing enter keeps the existing value. let full_prompt = format!("{prompt} [{default}]: "); diff --git a/src/miner/genesis.rs b/src/miner/genesis.rs index 7ede38e..38cb69d 100644 --- a/src/miner/genesis.rs +++ b/src/miner/genesis.rs @@ -114,7 +114,7 @@ async fn create_genesis_block( // Genesis uses the fixed parent hash and launch difficulty. let timestamp = Utc::now().timestamp() as u32; - let next_block_difficulty = 3000000000000000_u64; + let next_block_difficulty = 1200000000000000_u64; let block_struct = UnminedBlock::new( timestamp, miner, diff --git a/src/records/memory/connections.rs b/src/records/memory/connections.rs index 1aa33ce..513d4e3 100644 --- a/src/records/memory/connections.rs +++ b/src/records/memory/connections.rs @@ -2,9 +2,14 @@ use crate::common::binary_conversions::{binary_to_ip, ip_to_binary}; use crate::lazy_static; use crate::log::{info, warn}; use crate::records::memory::enums::{ClientType, ConnectionType}; +use crate::records::memory::network_mapping::monitor::{MONITOR_ACTION_ADD, MONITOR_ACTION_REMOVE}; +use crate::records::memory::network_mapping::structs::{MonitorAddressParams, SignedMonitorEdit}; +use crate::records::memory::network_mapping::NodeInfo; use crate::records::memory::response_channels::{delete_entry, reserve_entry, Command}; use crate::records::memory::structs::{Connection, StoreConnectionParams}; +use crate::rpc::client::handshake::connect_and_handshake; use crate::rpc::client::handshake_processing::{bootstrap_peer_discovery, BootstrapParams}; +use crate::rpc::client::structs::Connect; use crate::rpc::command_maps::RPC_BLOCK_HEIGHT; use crate::rpc::responses::RpcResponse; use crate::sled::Db; @@ -69,15 +74,63 @@ pub async fn set_reconnect_context(db: Db, wallet: Arc, map: Arc addr, + Err(err) => { + warn!("[reconnect] invalid dropped peer address {addr_string}: {err}"); + break; + } + }; - let bootstrap_params = BootstrapParams { - stream, - connections_key, - wallet: context.wallet, - db: context.db, - map: context.map, - first: false, - }; + let connect = Connect { + addr: socket_addr, + node_ip: addr_string.clone(), + wallet: context.wallet.clone(), + db: context.db.clone(), + map: context.map.clone(), + first: false, + }; - if let Err(err) = bootstrap_peer_discovery(bootstrap_params).await { - warn!("[reconnect] bootstrap recovery failed: {err}"); + match connect_and_handshake(connect).await { + Ok(()) => { + info!("[reconnect] reconnected dropped peer {addr_string} on attempt {attempt}"); + return; + } + Err(err) => { + warn!( + "[reconnect] failed to reconnect dropped peer {addr_string} on attempt {attempt}/3: {err}" + ); + } + } } + + reconnect_replacement_inner(&ip).await; } .await; @@ -207,11 +260,12 @@ impl Connection { port, stream.clone(), client_type.as_bytes(), - address, + address.clone(), ); self.connection_map.insert(connection_key, connection_info); if client_type == ClientType::Miner { + spawn_monitor_update(ip.clone(), MONITOR_ACTION_ADD, address.clone(), port); Connection::client_checkup(stream, connection_type, ip, port, command_map); } true @@ -232,6 +286,14 @@ impl Connection { }; let removed = self.connection_map.remove(&connection_key); if let Some(connection_info) = removed.as_ref() { + if ClientType::from_bytes(&connection_info.client_type) == Some(ClientType::Miner) { + spawn_monitor_update( + ip.clone(), + MONITOR_ACTION_REMOVE, + connection_info.wallet_address.clone(), + port, + ); + } let stream = Arc::clone(&connection_info.stream); tokio::spawn(async move { let mut stream_guard = stream.lock().await; @@ -344,7 +406,7 @@ impl Connection { } drop(guard); if connection_type == ConnectionType::Outgoing { - reconnect_dropped_outgoing(&ip).await; + retry_dropped_outgoing(ip.clone(), port).await; } break; } @@ -434,6 +496,26 @@ impl Connection { }) } + pub async fn get_wallet_for_connection_key(key: &str) -> Option> { + let (ip, port) = split_ip_port_key(key)?; + let lock = CONNECTIONS.read().await; + let conn = lock.as_ref()?; + + let ip_bytes = ip_to_binary(&ip); + conn.connection_map + .iter() + .find_map(|(connection_key, info)| { + if connection_key.ip == ip_bytes + && connection_key.port == port + && ClientType::from_bytes(&info.client_type) == Some(ClientType::Miner) + { + Some(info.wallet_address.clone()) + } else { + None + } + }) + } + // Build the serialized connection key for a live stream when only // the stream handle is known. pub fn connection_key_for_stream(&self, stream: &Arc>) -> Option { @@ -533,6 +615,57 @@ impl Connection { } } +fn spawn_monitor_update(ip: String, action: u8, peer_wallet_bytes: Vec, port: u16) { + tokio::spawn(async move { + let context = { + let guard = RECONNECT_CONTEXT.lock().await; + guard.clone() + }; + let Some(context) = context else { + return; + }; + let peer_long = Wallet::bytes_to_long_address(peer_wallet_bytes); + let Some(monitored_address) = Wallet::normalize_to_short_address(&peer_long) else { + return; + }; + let monitoring_address = context.wallet.saved.short_address.clone(); + if monitored_address == monitoring_address { + return; + } + let timestamp = crate::Utc::now().timestamp_millis() as u64; + let signature = NodeInfo::monitor_signature( + action, + &monitored_address, + &monitoring_address, + &ip, + timestamp, + &context.wallet, + ) + .await; + let edit = SignedMonitorEdit { + action, + monitored_address, + monitoring_address, + target_ip: ip.clone(), + modified_timestamp: timestamp, + modified_signature: signature, + }; + let params = MonitorAddressParams { + map: context.map.clone(), + edit, + remote_ip: String::new(), + db: context.db.clone(), + wallet: context.wallet.clone(), + connections_key: format!("{ip}:{port}"), + }; + let _ = if action == MONITOR_ACTION_ADD { + NodeInfo::add_monitor(params).await + } else { + NodeInfo::remove_monitor(params).await + }; + }); +} + lazy_static! { pub static ref CONNECTIONS: Arc>> = Arc::new(RwLock::new(None)); } diff --git a/src/records/memory/network_mapping/add.rs b/src/records/memory/network_mapping/add.rs index 0c5c0ba..fcbe725 100644 --- a/src/records/memory/network_mapping/add.rs +++ b/src/records/memory/network_mapping/add.rs @@ -82,8 +82,6 @@ impl NodeInfo { connections_key, } = params; let current_timestamp = Utc::now().timestamp_millis() as u64; - let direct_peer_announcement = !remote_ip.is_empty() && edit.ip == remote_ip; - if !is_public_network_address(&edit.ip) { return RpcResponse::Binary(b"Error: Invalid network address".to_vec()); } @@ -129,7 +127,7 @@ impl NodeInfo { let valid_added_by = signer_node .map(|node| { (current_timestamp - node.added_timestamp) >= 3600 - && node.deleted_by.is_empty() + && node.deleted_timestamp == 0 }) .unwrap_or(false); if !valid_added_by { @@ -162,15 +160,14 @@ impl NodeInfo { } // Existing deleted entries can be revived in place when the same - // address/IP pair is re-announced, otherwise the older record is - // discarded and replaced. + // address/IP pair is re-announced. Different active wallets on the + // same IP are still rejected, but deleted records remain in place + // for historical validation. if let Some(existing_node) = address_map.get_mut(&edit.address) { - if !existing_node.deleted_by.is_empty() { + if existing_node.deleted_timestamp > 0 { if existing_node.ip == edit.ip { - existing_node.deleted_by = "".to_string(); existing_node.deleted_timestamp = 0_u64; existing_node.deleted_block = 0_u32; - existing_node.deleted_signature = "".to_string(); return RpcResponse::Binary(b"Success".to_vec()); } else { address_map.remove(&edit.address); @@ -190,9 +187,7 @@ impl NodeInfo { } if let Some(existing_node) = address_map.values_mut().find(|node| node.ip == edit.ip) { - if !existing_node.deleted_by.is_empty() { - address_map.retain(|_, node| node.ip != edit.ip); - } else if edit.ip != GENESIS_IP { + if existing_node.deleted_timestamp == 0 && edit.ip != GENESIS_IP { penalize_duplicate_ip = true; } } @@ -236,19 +231,6 @@ impl NodeInfo { ) .await; - if direct_peer_announcement { - // Only direct self-announcements get a ping monitor. Imported or - // rebroadcast map records are not guaranteed to have a live stream. - Self::ping(PingMonitorParams { - map, - edit, - remote_ip, - db, - wallet, - connections_key, - }); - } - RpcResponse::Binary(b"Success".to_vec()) } } diff --git a/src/records/memory/network_mapping/delete.rs b/src/records/memory/network_mapping/delete.rs index c887fb6..d2f8354 100644 --- a/src/records/memory/network_mapping/delete.rs +++ b/src/records/memory/network_mapping/delete.rs @@ -1,292 +1,8 @@ use super::*; impl NodeInfo { - pub fn ping(params: PingMonitorParams) { - tokio::spawn(async move { - let PingMonitorParams { - map, - edit, - remote_ip, - db, - wallet, - connections_key, - } = params; - let task_addr = edit.address.clone(); - let task_ip = edit.ip.clone(); - let task_wallet = edit.modified_by.clone(); - let task_signature = edit.modified_signature.clone(); - let added_timestamp = edit.modified_timestamp; - - let task_remote_ip = remote_ip; - let task_signing_wallet = wallet; - let task_connections_key = connections_key; - let task_db = db.clone(); - - { - let mut monitors = PING_MONITORS.lock().await; - // A matching signature means this exact monitor is already active - // for the node, so do not start a duplicate ping task. - if monitors - .get(&task_addr) - .map(|current| current == &task_signature) - .unwrap_or(false) - { - return; - } - monitors.insert(task_addr.clone(), task_signature.clone()); - } - - let mut failures = 0; - - // Periodically ping the node and remove it after repeated - // failures, then recursively reattach any inherited children. - loop { - sleep(Duration::from_secs(120)).await; - - { - let monitors = PING_MONITORS.lock().await; - // Stop this task if another monitor replaced it. - if !monitors - .get(&task_addr) - .map(|current| current == &task_signature) - .unwrap_or(false) - { - break; - } - } - - { - let map_lock = ADDRESS_MAP.lock().await; - if let Some(node) = map_lock.get(&task_addr) { - // Stop monitoring stale node data after the map entry - // has been replaced by a newer edit. - if node.added_by != task_wallet || node.added_signature != task_signature { - break; - } - } else { - break; - } - } - - if let Some(unlocked_stream) = - Connection::get_stream_from_memory(&task_connections_key).await - { - let command = RPC_BLOCK_HEIGHT; - let (ping_key, _ping_tx, ping_rx) = reserve_entry(map.clone()).await; - - // Liveness uses a normal block-height RPC and waits for the - // reserved reply channel to receive any response. - let mut message = vec![command]; - message.extend_from_slice(&ping_key); - RpcResponse::send_raw(&unlocked_stream, Some(&task_connections_key), &message) - .await; - - let response = timeout(Duration::from_secs(10), async { - let mut rx = ping_rx.lock().await; - rx.recv().await - }) - .await; - - match response { - Ok(Some(_buffer)) => { - failures = 0; - } - _ => { - failures += 1; - warn!("[network_map] ping failure: address={task_addr} ip={task_ip} failures={failures}"); - if failures >= 3 { - warn!("[network_map] deleting node after ping failures: address={task_addr} ip={task_ip} responsible_by={task_wallet}"); - let _ = Self::delete_address(DeleteAddressParams { - map: map.clone(), - edit: SignedNodeEdit { - address: task_addr.clone(), - ip: task_ip.clone(), - modified_by: task_wallet.clone(), - modified_timestamp: added_timestamp, - modified_signature: task_signature.clone(), - }, - remote_ip: task_remote_ip.clone(), - db: task_db.clone(), - wallet: task_signing_wallet.clone(), - connections_key: task_connections_key.clone(), - }) - .await; - - break; - } - } - } - } else { - // The direct socket disappeared before this monitor fired. - // Connection cleanup is owned by the connection manager, so - // stop this map monitor without marking the node deleted. - break; - } - } - - Self::release_ping_monitor(&task_addr, &task_signature).await; - }); - } - pub async fn delete_address(params: DeleteAddressParams) -> RpcResponse { - let DeleteAddressParams { - map, - mut edit, - remote_ip, - db, - wallet, - connections_key, - } = params; - let current_timestamp = Utc::now().timestamp_millis() as u64; - - // Locally initiated deletions are re-signed with fresh metadata - // before they are applied and broadcast. - if remote_ip.is_empty() { - edit.modified_timestamp = current_timestamp; - edit.modified_by = wallet.saved.long_address.clone(); - edit.modified_signature = - Self::added_signature(&edit.address, &edit.ip, current_timestamp, &wallet).await; - } - - let data = format!( - "{}{}{}{}", - edit.address, edit.ip, edit.modified_by, edit.modified_timestamp - ); - let hashed_data = skein_256_hash_data(&data); - - if !Wallet::verify_transaction(&hashed_data, &edit.modified_signature, &edit.modified_by) - .await - { - return RpcResponse::Binary(b"Error: Could not validate signature".to_vec()); - } - - { - let mut address_map = ADDRESS_MAP.lock().await; - - if get_height(&db) > 10_000 { - // Mature chains only allow established miners to remove nodes. - let signer_key = Wallet::normalize_to_short_address(&edit.modified_by) - .unwrap_or_else(|| edit.modified_by.clone()); - let signer_node = address_map.get(&signer_key); - let valid_added_by = signer_node - .map(|node| { - (current_timestamp - node.added_timestamp) >= 3600 - && node.deleted_by.is_empty() - }) - .unwrap_or(false); - - if !valid_added_by { - return RpcResponse::Binary( - b"Error: Address must exist for 60m and not be deleted".to_vec(), - ); - } - - let mined_count = signer_node.map(|node| node.blocks_mined).unwrap_or(0); - if mined_count < 100 { - return RpcResponse::Binary( - b"Error: Must mine 100 blocks to remove nodes".to_vec(), - ); - } - } - - let deleted_count_last_hour = address_map - .values() - .filter(|node| { - node.deleted_by == edit.modified_by - && (current_timestamp - node.deleted_timestamp) <= 3600 - }) - .count(); - - // Rate limit delete events per signer to prevent churn in the - // shared network map. - if deleted_count_last_hour >= 10 { - return RpcResponse::Binary(b"Error: Max 10 deletions in 60m".to_vec()); - } - - if let Some(existing_node) = address_map.get_mut(&edit.address) { - if !existing_node.deleted_by.is_empty() { - return RpcResponse::Binary( - b"Error: This address has already been deleted".to_vec(), - ); - } - - // Deletion is recorded as metadata rather than immediately - // removing the node, preserving historical validation context. - existing_node.deleted_by = edit.modified_by.clone(); - existing_node.deleted_timestamp = current_timestamp; - existing_node.deleted_block = get_height(&db) + 1; - existing_node.deleted_signature = edit.modified_signature.clone(); - info!( - "[network_map] node marked deleted: address={} ip={} deleted_by={} timestamp={} deleted_block={}", - edit.address, - edit.ip, - edit.modified_by, - current_timestamp, - existing_node.deleted_block - ); - } else { - return RpcResponse::Binary(b"Error: Address not found".to_vec()); - } - } - - // Stop any ping task owned by the deleted node record. - Self::release_ping_monitor(&edit.address, &edit.modified_signature).await; - - // Deletions propagate to peers and also tear down any live - // outgoing connection so bootstrap can recover a replacement. - Self::broadcast_node( - map.clone(), - &edit, - &remote_ip, - NodeEditType::Delete, - &connections_key, - ) - .await; - - if let Some(port) = CONNECTIONS - .read() - .await - .as_ref() - .and_then(|conn| conn.find_outgoing_port(&edit.ip)) - { - let mut writer = CONNECTIONS.write().await; - if let Some(conn) = writer.as_mut() { - // Drop the live outgoing socket after marking the node deleted. - conn.drop_connection(ConnectionType::Outgoing, edit.ip.clone(), port); - info!( - "[connection_manager] dropped dead outgoing connection: {}:{}", - edit.ip, port - ); - } - drop(writer); - - let live_connection = { - let guard = CONNECTIONS.read().await; - guard.as_ref().and_then(|conn| { - conn.get_all_streams().first().and_then(|stream| { - conn.connection_key_for_stream(stream) - .map(|key| (key, Arc::clone(stream))) - }) - }) - }; - - if let Some((connections_key, new_stream)) = live_connection { - // Use any remaining live stream to refresh peer discovery and - // replace the removed outgoing connection. - let bootstrap_params = BootstrapParams { - stream: new_stream, - connections_key, - wallet: wallet.clone(), - db: db.clone(), - map: map.clone(), - first: false, - }; - spawn_reconnect_bootstrap(bootstrap_params); - } else { - warn!("[reconnect] No live stream found to bootstrap from"); - } - } - - RpcResponse::Binary(b"Success: Node marked as deleted".to_vec()) + let _ = params; + RpcResponse::Binary(b"Error: explicit node deletion is disabled".to_vec()) } } diff --git a/src/records/memory/network_mapping/enums.rs b/src/records/memory/network_mapping/enums.rs index 9c0a13b..aa4cf67 100644 --- a/src/records/memory/network_mapping/enums.rs +++ b/src/records/memory/network_mapping/enums.rs @@ -1,10 +1,13 @@ -use crate::rpc::command_maps::{RPC_ADD_NETWORK_NODE, RPC_DELETE_NETWORK_NODE}; +use crate::rpc::command_maps::{ + RPC_ADD_NETWORK_NODE, RPC_NETWORK_MONITOR_ADD, RPC_NETWORK_MONITOR_REMOVE, +}; // NodeEditType keeps the network membership update type explicit at the call sites. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NodeEditType { Add, - Delete, + MonitorAdd, + MonitorRemove, } impl NodeEditType { @@ -12,7 +15,8 @@ impl NodeEditType { pub fn message_type(self) -> u8 { match self { NodeEditType::Add => RPC_ADD_NETWORK_NODE, - NodeEditType::Delete => RPC_DELETE_NETWORK_NODE, + NodeEditType::MonitorAdd => RPC_NETWORK_MONITOR_ADD, + NodeEditType::MonitorRemove => RPC_NETWORK_MONITOR_REMOVE, } } } diff --git a/src/records/memory/network_mapping/mined_counts.rs b/src/records/memory/network_mapping/mined_counts.rs index 5702c65..bef8b04 100644 --- a/src/records/memory/network_mapping/mined_counts.rs +++ b/src/records/memory/network_mapping/mined_counts.rs @@ -1,4 +1,5 @@ use super::*; +use crate::records::unpack_block::unpack_header::load_block_header; impl NodeInfo { pub async fn increment_mined(address: &str) { @@ -39,6 +40,18 @@ impl NodeInfo { } } + pub async fn set_deleted_metadata_from_mapping( + address: &str, + deleted_timestamp: u64, + deleted_block: u32, + ) { + let mut map = ADDRESS_MAP.lock().await; + if let Some(node_info) = map.get_mut(address) { + node_info.deleted_timestamp = deleted_timestamp; + node_info.deleted_block = deleted_block; + } + } + pub async fn rebuild_mined_counts_from_chain(db: &Db) -> Result<(), String> { // Recompute node mined counts directly from saved block headers // so startup and recovery can rebuild memory-only state. diff --git a/src/records/memory/network_mapping/mod.rs b/src/records/memory/network_mapping/mod.rs index 4ac28e9..c55ed21 100644 --- a/src/records/memory/network_mapping/mod.rs +++ b/src/records/memory/network_mapping/mod.rs @@ -8,24 +8,17 @@ use crate::log::{error, info, warn}; use crate::records::block_height::get_block_height::get_height; use crate::records::ip_score::enums::InfractionType; use crate::records::ip_score::score::update_ip_score; -use crate::records::memory::connections::{spawn_reconnect_bootstrap, CONNECTIONS}; -use crate::records::memory::enums::ConnectionType; +use crate::records::memory::connections::CONNECTIONS; use crate::records::memory::network_mapping::enums::NodeEditType; use crate::records::memory::network_mapping::structs::{ - AddAddressParams, DeleteAddressParams, PingMonitorParams, SignedNodeEdit, NODE_RECORD_BYTES, + AddAddressParams, DeleteAddressParams, MonitorAddressParams, SignedMonitorEdit, SignedNodeEdit, + NODE_RECORD_FIXED_BYTES, }; use crate::records::memory::response_channels::{reserve_entry, Command}; -use crate::records::memory::structs::Connection; -use crate::records::unpack_block::unpack_header::load_block_header; -use crate::rpc::client::handshake_processing::BootstrapParams; -use crate::rpc::command_maps::RPC_BLOCK_HEIGHT; use crate::rpc::responses::RpcResponse; use crate::sled::Db; -use crate::sleep; -use crate::timeout; use crate::wallets::structures::Wallet; use crate::Arc; -use crate::Duration; use crate::HashMap; use crate::Mutex; use crate::TcpStream; @@ -33,7 +26,6 @@ use crate::Utc; lazy_static! { static ref ADDRESS_MAP: Mutex> = Mutex::new(HashMap::new()); - static ref PING_MONITORS: Mutex> = Mutex::new(HashMap::new()); } #[derive(Debug)] @@ -43,24 +35,12 @@ pub struct NodeInfo { added_by: String, added_timestamp: u64, added_signature: String, - deleted_by: String, deleted_timestamp: u64, deleted_block: u32, - deleted_signature: String, + monitoring: Vec, } impl NodeInfo { - async fn release_ping_monitor(address: &str, signature: &str) { - let mut monitors = PING_MONITORS.lock().await; - if monitors - .get(address) - .map(|current| current == signature) - .unwrap_or(false) - { - monitors.remove(address); - } - } - fn new( ip: String, blocks_mined: u8, @@ -74,10 +54,9 @@ impl NodeInfo { added_by, added_timestamp, added_signature, - deleted_by: "".to_string(), deleted_timestamp: 0_u64, deleted_block: 0_u32, - deleted_signature: "".to_string(), + monitoring: Vec::new(), } } } @@ -86,5 +65,6 @@ mod add; mod delete; pub mod enums; mod mined_counts; +pub(crate) mod monitor; mod queries; pub mod structs; diff --git a/src/records/memory/network_mapping/monitor.rs b/src/records/memory/network_mapping/monitor.rs new file mode 100644 index 0000000..48f00a4 --- /dev/null +++ b/src/records/memory/network_mapping/monitor.rs @@ -0,0 +1,237 @@ +use super::*; +use crate::records::wallet_registry::resolve_pubkey_from_short_address; + +pub const MONITOR_ACTION_ADD: u8 = 1; +pub const MONITOR_ACTION_REMOVE: u8 = 2; + +impl NodeInfo { + pub async fn monitor_signature( + action: u8, + monitored_address: &str, + monitoring_address: &str, + target_ip: &str, + current_timestamp: u64, + wallet: &Arc, + ) -> String { + let private_key = &wallet.saved.private_key; + let data = format!( + "{action}{monitored_address}{monitoring_address}{target_ip}{current_timestamp}" + ); + let hashed_data = skein_256_hash_data(&data); + Wallet::sign_transaction(&hashed_data, private_key).await + } + + pub async fn broadcast_monitor( + map: Arc>, + edit: &SignedMonitorEdit, + remote_ip: &str, + edittype: NodeEditType, + connections_key: &str, + ) { + let monitored_bytes = match Wallet::short_address_to_bytes(&edit.monitored_address) { + Some(bytes) => bytes, + None => return, + }; + let monitoring_bytes = match Wallet::short_address_to_bytes(&edit.monitoring_address) { + Some(bytes) => bytes, + None => return, + }; + let target_ip_bytes = ip_to_binary(&edit.target_ip); + let timestamp_bytes = edit.modified_timestamp.to_le_bytes(); + let signature_bytes = match decode(&edit.modified_signature) { + Ok(bytes) => bytes, + Err(_) => return, + }; + + let connections_lock = CONNECTIONS.read().await; + let streams = connections_lock + .as_ref() + .map(|connection| connection.get_all_streams()); + + if let Some(streams) = streams { + for unlocked_stream in streams { + let peer_addr = { + let stream = unlocked_stream.lock().await; + stream.peer_addr() + }; + let Ok(addr) = peer_addr else { + continue; + }; + if !remote_ip.is_empty() && addr.ip().to_string() == remote_ip { + continue; + } + + let (hashmap_key, _hashmap_tx, hashmap_rx) = reserve_entry(map.clone()).await; + let mut message = Vec::new(); + message.push(edittype.message_type()); + message.extend_from_slice(&hashmap_key); + message.push(edit.action); + message.extend_from_slice(&monitored_bytes); + message.extend_from_slice(&monitoring_bytes); + message.extend_from_slice(&target_ip_bytes); + message.extend_from_slice(×tamp_bytes); + message.extend_from_slice(&signature_bytes); + RpcResponse::send_raw(&unlocked_stream, Some(connections_key), &message).await; + let mut rx = hashmap_rx.lock().await; + let _ = rx.recv().await; + } + } + } + + fn mark_deleted_and_cascade( + address_map: &mut HashMap, + deleted_address: &str, + current_timestamp: u64, + deleted_block: u32, + ) { + let mut stack = vec![deleted_address.to_string()]; + + while let Some(address) = stack.pop() { + let should_cascade = match address_map.get_mut(&address) { + Some(node) if node.deleted_timestamp == 0 && node.monitoring.is_empty() => { + node.deleted_timestamp = current_timestamp; + node.deleted_block = deleted_block; + true + } + _ => false, + }; + + if !should_cascade { + continue; + } + + info!( + "[network_map] node marked deleted: address={} timestamp={} deleted_block={}", + address, current_timestamp, deleted_block + ); + + let targets: Vec = address_map + .iter_mut() + .filter_map(|(target, node)| { + let before = node.monitoring.len(); + node.monitoring.retain(|monitor| monitor != &address); + if before != node.monitoring.len() && node.monitoring.is_empty() { + Some(target.clone()) + } else { + None + } + }) + .collect(); + + for target in targets { + stack.push(target); + } + } + } + + async fn verify_monitor_edit(edit: &SignedMonitorEdit, db: &Db) -> bool { + let Ok(Some(pubkey)) = resolve_pubkey_from_short_address(db, &edit.monitoring_address) + else { + return false; + }; + let signer = Wallet::bytes_to_long_address(pubkey); + let data = format!( + "{}{}{}{}{}", + edit.action, + edit.monitored_address, + edit.monitoring_address, + edit.target_ip, + edit.modified_timestamp + ); + let hashed_data = skein_256_hash_data(&data); + Wallet::verify_transaction(&hashed_data, &edit.modified_signature, &signer).await + } + + pub async fn add_monitor(params: MonitorAddressParams) -> RpcResponse { + Self::apply_monitor(params, MONITOR_ACTION_ADD).await + } + + pub async fn remove_monitor(params: MonitorAddressParams) -> RpcResponse { + Self::apply_monitor(params, MONITOR_ACTION_REMOVE).await + } + + async fn apply_monitor(params: MonitorAddressParams, action: u8) -> RpcResponse { + let MonitorAddressParams { + map, + mut edit, + remote_ip, + db, + wallet, + connections_key, + } = params; + let current_timestamp = Utc::now().timestamp_millis() as u64; + + if edit.action != action { + return RpcResponse::Binary(b"Error: Invalid monitor action".to_vec()); + } + + let local_short = wallet.saved.short_address.clone(); + if remote_ip.is_empty() && edit.monitoring_address == local_short { + edit.modified_timestamp = current_timestamp; + edit.modified_signature = Self::monitor_signature( + action, + &edit.monitored_address, + &edit.monitoring_address, + &edit.target_ip, + current_timestamp, + &wallet, + ) + .await; + } + + if !Self::verify_monitor_edit(&edit, &db).await { + return RpcResponse::Binary(b"Error: Could not validate monitor signature".to_vec()); + } + + { + let mut address_map = ADDRESS_MAP.lock().await; + let Some(monitored) = address_map.get_mut(&edit.monitored_address) else { + return RpcResponse::Binary(b"Error: monitored address not found".to_vec()); + }; + if monitored.ip != edit.target_ip { + return RpcResponse::Binary(b"Error: monitor target IP mismatch".to_vec()); + } + + match action { + MONITOR_ACTION_ADD => { + if !monitored.monitoring.contains(&edit.monitoring_address) { + monitored.monitoring.push(edit.monitoring_address.clone()); + } + monitored.deleted_timestamp = 0; + monitored.deleted_block = 0; + } + MONITOR_ACTION_REMOVE => { + monitored + .monitoring + .retain(|monitor| monitor != &edit.monitoring_address); + if monitored.monitoring.is_empty() { + let deleted_block = get_height(&db) + 1; + Self::mark_deleted_and_cascade( + &mut address_map, + &edit.monitored_address, + current_timestamp, + deleted_block, + ); + } + } + _ => return RpcResponse::Binary(b"Error: Invalid monitor action".to_vec()), + } + } + + let broadcast_type = if action == MONITOR_ACTION_ADD { + NodeEditType::MonitorAdd + } else { + NodeEditType::MonitorRemove + }; + Self::broadcast_monitor(map, &edit, &remote_ip, broadcast_type, &connections_key).await; + + RpcResponse::Binary(b"Success".to_vec()) + } + + pub async fn set_monitors_from_mapping(address: &str, monitors: Vec) { + let mut address_map = ADDRESS_MAP.lock().await; + if let Some(node) = address_map.get_mut(address) { + node.monitoring = monitors; + } + } +} diff --git a/src/records/memory/network_mapping/queries.rs b/src/records/memory/network_mapping/queries.rs index dd38f34..b18e5c7 100644 --- a/src/records/memory/network_mapping/queries.rs +++ b/src/records/memory/network_mapping/queries.rs @@ -33,7 +33,7 @@ impl NodeInfo { map.values() // Deleted nodes stay in the map for history, but active node // lists should only return bare public IPs. - .filter(|node_info| node_info.deleted_by.is_empty()) + .filter(|node_info| node_info.deleted_timestamp == 0) .map(|node_info| node_info.ip.clone()) .collect() } @@ -55,10 +55,10 @@ impl NodeInfo { } pub async fn request_valid_nodes() -> RpcResponse { - // Serialize the in-memory node map into the fixed binary layout + // Serialize the in-memory node map into the binary layout // used by peer bootstrap and node-list synchronization. let map = ADDRESS_MAP.lock().await; - let mut data: Vec = Vec::with_capacity(map.len() * NODE_RECORD_BYTES); + let mut data: Vec = Vec::with_capacity(map.len() * NODE_RECORD_FIXED_BYTES); for (address, node_info) in map.iter() { let address_bytes = match Wallet::short_address_to_bytes(address) { @@ -70,22 +70,10 @@ impl NodeInfo { let added_by_bytes = Wallet::long_address_to_bytes(node_info.added_by.to_string()); let added_timestamp_bytes = node_info.added_timestamp.to_le_bytes(); - // Empty deletion fields serialize as zero-filled fixed-width values - // so every node record stays the same size on the wire. - let deleted_by_bytes = if node_info.deleted_by.is_empty() { - vec![0u8; Wallet::ADDRESS_BYTES_LENGTH] - } else { - Wallet::long_address_to_bytes(node_info.deleted_by.to_string()) - }; - let deleted_timestamp_bytes = node_info.deleted_timestamp.to_le_bytes(); let deleted_block_bytes = node_info.deleted_block.to_le_bytes(); - - let deleted_signature_bytes = if node_info.deleted_signature.is_empty() { - vec![0u8; Wallet::SIGNATURE_LENGTH] - } else { - decode(node_info.deleted_signature.clone()).unwrap() - }; + let monitor_count = node_info.monitoring.len().min(u16::MAX as usize) as u16; + let monitor_count_bytes = monitor_count.to_le_bytes(); let added_signature_bytes = decode(node_info.added_signature.clone()).unwrap(); @@ -97,10 +85,16 @@ impl NodeInfo { data.extend_from_slice(&added_by_bytes); data.extend_from_slice(&added_timestamp_bytes); data.extend_from_slice(&added_signature_bytes); - data.extend_from_slice(&deleted_by_bytes); data.extend_from_slice(&deleted_timestamp_bytes); data.extend_from_slice(&deleted_block_bytes); - data.extend_from_slice(&deleted_signature_bytes); + data.extend_from_slice(&monitor_count_bytes); + for monitor in node_info.monitoring.iter().take(monitor_count as usize) { + if let Some(monitor_bytes) = Wallet::short_address_to_bytes(monitor) { + data.extend_from_slice(&monitor_bytes); + } else { + data.extend_from_slice(&[0u8; Wallet::SHORT_ADDRESS_BYTES_LENGTH]); + } + } } RpcResponse::Binary(data) } diff --git a/src/records/memory/network_mapping/structs.rs b/src/records/memory/network_mapping/structs.rs index 2b756fd..cf7fff8 100644 --- a/src/records/memory/network_mapping/structs.rs +++ b/src/records/memory/network_mapping/structs.rs @@ -8,6 +8,7 @@ pub const NODE_IP_BYTES: usize = 16; pub const NODE_BLOCKS_MINED_BYTES: usize = 1; pub const NODE_TIMESTAMP_BYTES: usize = 8; pub const NODE_DELETED_BLOCK_BYTES: usize = 4; +pub const NODE_MONITOR_COUNT_BYTES: usize = 2; pub const NODE_ADDRESS_OFFSET: usize = 0; pub const NODE_IP_OFFSET: usize = NODE_ADDRESS_OFFSET + Wallet::SHORT_ADDRESS_BYTES_LENGTH; @@ -15,13 +16,11 @@ pub const NODE_BLOCKS_MINED_OFFSET: usize = NODE_IP_OFFSET + NODE_IP_BYTES; pub const NODE_ADDED_BY_OFFSET: usize = NODE_BLOCKS_MINED_OFFSET + NODE_BLOCKS_MINED_BYTES; pub const NODE_ADDED_TIMESTAMP_OFFSET: usize = NODE_ADDED_BY_OFFSET + Wallet::ADDRESS_BYTES_LENGTH; pub const NODE_ADDED_SIGNATURE_OFFSET: usize = NODE_ADDED_TIMESTAMP_OFFSET + NODE_TIMESTAMP_BYTES; -pub const NODE_DELETED_BY_OFFSET: usize = NODE_ADDED_SIGNATURE_OFFSET + Wallet::SIGNATURE_LENGTH; pub const NODE_DELETED_TIMESTAMP_OFFSET: usize = - NODE_DELETED_BY_OFFSET + Wallet::ADDRESS_BYTES_LENGTH; + NODE_ADDED_SIGNATURE_OFFSET + Wallet::SIGNATURE_LENGTH; pub const NODE_DELETED_BLOCK_OFFSET: usize = NODE_DELETED_TIMESTAMP_OFFSET + NODE_TIMESTAMP_BYTES; -pub const NODE_DELETED_SIGNATURE_OFFSET: usize = - NODE_DELETED_BLOCK_OFFSET + NODE_DELETED_BLOCK_BYTES; -pub const NODE_RECORD_BYTES: usize = NODE_DELETED_SIGNATURE_OFFSET + Wallet::SIGNATURE_LENGTH; +pub const NODE_MONITOR_COUNT_OFFSET: usize = NODE_DELETED_BLOCK_OFFSET + NODE_DELETED_BLOCK_BYTES; +pub const NODE_RECORD_FIXED_BYTES: usize = NODE_MONITOR_COUNT_OFFSET + NODE_MONITOR_COUNT_BYTES; // SignedNodeEdit carries the signed node membership payload used by add/delete updates. #[derive(Debug, Clone)] @@ -33,6 +32,16 @@ pub struct SignedNodeEdit { pub modified_signature: String, } +#[derive(Debug, Clone)] +pub struct SignedMonitorEdit { + pub action: u8, + pub monitored_address: String, + pub monitoring_address: String, + pub target_ip: String, + pub modified_timestamp: u64, + pub modified_signature: String, +} + // AddAddressParams groups the shared context needed to add a node to the network map. #[derive(Clone)] pub struct AddAddressParams { @@ -56,11 +65,10 @@ pub struct DeleteAddressParams { pub connections_key: String, } -// PingMonitorParams keeps the background liveness-monitor arguments bundled together. #[derive(Clone)] -pub struct PingMonitorParams { +pub struct MonitorAddressParams { pub map: Arc>, - pub edit: SignedNodeEdit, + pub edit: SignedMonitorEdit, pub remote_ip: String, pub db: Db, pub wallet: Arc, diff --git a/src/records/record_chain/save.rs b/src/records/record_chain/save.rs index 14c5d91..3b00a15 100644 --- a/src/records/record_chain/save.rs +++ b/src/records/record_chain/save.rs @@ -509,7 +509,7 @@ async fn block_file_context(db: &Db, mut difficulty: u64) -> (String, u32, u64) let next_number = if genesis_checkup().await { current_height + 1 } else { - difficulty = 3000000000000000_u64; + difficulty = 1200000000000000_u64; 0 }; diff --git a/src/rpc/command_maps.rs b/src/rpc/command_maps.rs index dc1e029..dd8f585 100644 --- a/src/rpc/command_maps.rs +++ b/src/rpc/command_maps.rs @@ -54,6 +54,9 @@ pub const RPC_WALLET_REGISTRY_SYNC: u8 = 39; pub const RPC_VANITY_LOOKUP: u8 = 40; pub const RPC_TORRENT_CANDIDATES: u8 = 41; pub const RPC_BLOCK_HASH_AT_HEIGHT: u8 = 42; +pub const RPC_NETWORK_MONITOR_ADD: u8 = 43; +pub const RPC_NETWORK_MONITOR_REMOVE: u8 = 44; +pub const RPC_VANITY_OWNER_LOOKUP: u8 = 45; pub const RPC_REPLY: u8 = 255; pub const MAX_RPC_REPLY_BYTES: usize = 64 * 1024 * 1024; diff --git a/src/rpc/commands/mod.rs b/src/rpc/commands/mod.rs index 383febc..0915ca5 100644 --- a/src/rpc/commands/mod.rs +++ b/src/rpc/commands/mod.rs @@ -18,6 +18,8 @@ pub mod largest_tx_fee; pub mod latest_block; pub mod memory_by_signature; pub mod network_info; +pub mod network_monitor_add; +pub mod network_monitor_remove; pub mod nft_list; pub mod nft_lookup; pub mod random_node; @@ -43,3 +45,4 @@ pub mod validate_torrent; pub mod wallet_register; pub mod wallet_registry_sync; pub mod wallet_vanity_lookup; +pub mod wallet_vanity_owner_lookup; diff --git a/src/rpc/commands/network_monitor_add.rs b/src/rpc/commands/network_monitor_add.rs new file mode 100644 index 0000000..1e66dc0 --- /dev/null +++ b/src/rpc/commands/network_monitor_add.rs @@ -0,0 +1,66 @@ +use crate::records::memory::network_mapping::structs::{MonitorAddressParams, SignedMonitorEdit}; +use crate::records::memory::network_mapping::NodeInfo; +use crate::records::memory::response_channels::Command; +use crate::rpc::read_bytes_from_stream; +use crate::rpc::responses::RpcResponse; +use crate::sled::Db; +use crate::wallets::structures::Wallet; +use crate::Arc; +use crate::Mutex; +use crate::TcpStream; + +pub async fn network_monitor_add( + connections_key: &str, + stream_locked: Arc>, + db: &Db, + wallet: Arc, + map: Arc>, +) -> Result<(u32, RpcResponse), String> { + let (uid, _) = + read_bytes_from_stream::read_uid_from_stream(connections_key, stream_locked.clone()) + .await?; + let action = + read_bytes_from_stream::read_u8_from_stream(connections_key, stream_locked.clone()).await?; + let monitored_bytes = read_bytes_from_stream::read_short_address_from_stream( + connections_key, + stream_locked.clone(), + ) + .await?; + let monitored_address = Wallet::bytes_to_short_address(&monitored_bytes) + .ok_or_else(|| "error: Invalid monitored short address bytes".to_string())?; + let monitoring_bytes = read_bytes_from_stream::read_short_address_from_stream( + connections_key, + stream_locked.clone(), + ) + .await?; + let monitoring_address = Wallet::bytes_to_short_address(&monitoring_bytes) + .ok_or_else(|| "error: Invalid monitoring short address bytes".to_string())?; + let target_ip = + read_bytes_from_stream::read_ip_from_stream(connections_key, stream_locked.clone()).await?; + let modified_timestamp = + read_bytes_from_stream::read_u64_from_stream(connections_key, stream_locked.clone()) + .await?; + let modified_signature = + read_bytes_from_stream::read_signature_from_stream(connections_key, stream_locked.clone()) + .await?; + let remote_ip = read_bytes_from_stream::read_caller_ip(stream_locked).await?; + + let result = NodeInfo::add_monitor(MonitorAddressParams { + map, + edit: SignedMonitorEdit { + action, + monitored_address, + monitoring_address, + target_ip, + modified_timestamp, + modified_signature, + }, + remote_ip, + db: db.clone(), + wallet, + connections_key: connections_key.to_string(), + }) + .await; + + Ok((uid, result)) +} diff --git a/src/rpc/commands/network_monitor_remove.rs b/src/rpc/commands/network_monitor_remove.rs new file mode 100644 index 0000000..d40339f --- /dev/null +++ b/src/rpc/commands/network_monitor_remove.rs @@ -0,0 +1,66 @@ +use crate::records::memory::network_mapping::structs::{MonitorAddressParams, SignedMonitorEdit}; +use crate::records::memory::network_mapping::NodeInfo; +use crate::records::memory::response_channels::Command; +use crate::rpc::read_bytes_from_stream; +use crate::rpc::responses::RpcResponse; +use crate::sled::Db; +use crate::wallets::structures::Wallet; +use crate::Arc; +use crate::Mutex; +use crate::TcpStream; + +pub async fn network_monitor_remove( + connections_key: &str, + stream_locked: Arc>, + db: &Db, + wallet: Arc, + map: Arc>, +) -> Result<(u32, RpcResponse), String> { + let (uid, _) = + read_bytes_from_stream::read_uid_from_stream(connections_key, stream_locked.clone()) + .await?; + let action = + read_bytes_from_stream::read_u8_from_stream(connections_key, stream_locked.clone()).await?; + let monitored_bytes = read_bytes_from_stream::read_short_address_from_stream( + connections_key, + stream_locked.clone(), + ) + .await?; + let monitored_address = Wallet::bytes_to_short_address(&monitored_bytes) + .ok_or_else(|| "error: Invalid monitored short address bytes".to_string())?; + let monitoring_bytes = read_bytes_from_stream::read_short_address_from_stream( + connections_key, + stream_locked.clone(), + ) + .await?; + let monitoring_address = Wallet::bytes_to_short_address(&monitoring_bytes) + .ok_or_else(|| "error: Invalid monitoring short address bytes".to_string())?; + let target_ip = + read_bytes_from_stream::read_ip_from_stream(connections_key, stream_locked.clone()).await?; + let modified_timestamp = + read_bytes_from_stream::read_u64_from_stream(connections_key, stream_locked.clone()) + .await?; + let modified_signature = + read_bytes_from_stream::read_signature_from_stream(connections_key, stream_locked.clone()) + .await?; + let remote_ip = read_bytes_from_stream::read_caller_ip(stream_locked).await?; + + let result = NodeInfo::remove_monitor(MonitorAddressParams { + map, + edit: SignedMonitorEdit { + action, + monitored_address, + monitoring_address, + target_ip, + modified_timestamp, + modified_signature, + }, + remote_ip, + db: db.clone(), + wallet, + connections_key: connections_key.to_string(), + }) + .await; + + Ok((uid, result)) +} diff --git a/src/rpc/commands/wallet_vanity_owner_lookup.rs b/src/rpc/commands/wallet_vanity_owner_lookup.rs new file mode 100644 index 0000000..4911a1c --- /dev/null +++ b/src/rpc/commands/wallet_vanity_owner_lookup.rs @@ -0,0 +1,13 @@ +use crate::records::wallet_registry::resolve_owner_from_vanity_address; +use crate::rpc::responses::RpcResponse; +use crate::sled::Db; + +pub async fn lookup(vanity_address: String, db: &Db) -> RpcResponse { + match resolve_owner_from_vanity_address(db, &vanity_address) { + Ok(Some(owner_short_address)) => RpcResponse::Binary(owner_short_address.into_bytes()), + Ok(None) => RpcResponse::Binary(Vec::new()), + Err(err) => RpcResponse::Binary( + format!("error: Failed to lookup vanity owner address: {err}").into_bytes(), + ), + } +} diff --git a/src/rpc/server/rpc_command_loop.rs b/src/rpc/server/rpc_command_loop.rs index 8977f0b..5b25145 100644 --- a/src/rpc/server/rpc_command_loop.rs +++ b/src/rpc/server/rpc_command_loop.rs @@ -813,6 +813,67 @@ pub async fn start_loop( .send(&stream_locked, Some(&connections_key), uid) .await; } + 43 => { + // add a monitor edge in the network map + let (uid, result) = commands::network_monitor_add::network_monitor_add( + &connections_key, + stream_locked.clone(), + &db, + wallet.clone(), + map.clone(), + ) + .await?; + result + .send(&stream_locked, Some(&connections_key), uid) + .await; + } + 44 => { + // remove a monitor edge in the network map + let (uid, result) = commands::network_monitor_remove::network_monitor_remove( + &connections_key, + stream_locked.clone(), + &db, + wallet.clone(), + map.clone(), + ) + .await?; + result + .send(&stream_locked, Some(&connections_key), uid) + .await; + } + 45 => { + // request the canonical short address that owns a registered vanity address + let (uid, _) = read_bytes_from_stream::read_uid_from_stream( + &connections_key, + stream_locked.clone(), + ) + .await?; + let vanity_bytes = read_bytes_from_stream::read_short_address_from_stream( + &connections_key, + stream_locked.clone(), + ) + .await?; + let vanity_address = + match crate::wallets::structures::Wallet::bytes_to_vanity_address(&vanity_bytes) + { + Some(address) => address, + None => { + let result = crate::rpc::responses::RpcResponse::Binary( + b"error: Invalid vanity address bytes".to_vec(), + ); + result + .send(&stream_locked, Some(&connections_key), uid) + .await; + continue; + } + }; + + let result = + commands::wallet_vanity_owner_lookup::lookup(vanity_address, &db).await; + result + .send(&stream_locked, Some(&connections_key), uid) + .await; + } 255 => { commands::route_reply::route_reply( &connections_key, diff --git a/src/standalone_tools/connections/handshake.rs b/src/standalone_tools/connections/handshake.rs index d4e2fd2..3ed6229 100644 --- a/src/standalone_tools/connections/handshake.rs +++ b/src/standalone_tools/connections/handshake.rs @@ -19,7 +19,10 @@ use crate::{AsyncReadExt, AsyncWriteExt}; pub enum HandshakeWallet { // Most standalone tools only have the wallet password and need to load the saved wallet. - WalletKey(String), + WalletKey { + encryption_key: String, + wallet_path: String, + }, // Wallet recovery tools already have the address and private key before the wallet is saved. WalletParts { long_address: String, @@ -36,8 +39,11 @@ pub async fn connect_and_handshake( ) -> Result, io::Error> { // Resolve the wallet material before opening the stream so the handshake can sign its challenge. let (long_address, private_key) = match wallet_source { - HandshakeWallet::WalletKey(encryption_key) => { - let wallet = Wallet::try_obtain_wallet(encryption_key, None) + HandshakeWallet::WalletKey { + encryption_key, + wallet_path, + } => { + let wallet = Wallet::try_obtain_wallet(encryption_key, Some(&wallet_path)) .await .map_err(io::Error::other)?; (wallet.saved.long_address, wallet.saved.private_key) diff --git a/src/standalone_tools/connections/sending_request.rs b/src/standalone_tools/connections/sending_request.rs index 4a8ef95..ff22635 100644 --- a/src/standalone_tools/connections/sending_request.rs +++ b/src/standalone_tools/connections/sending_request.rs @@ -8,7 +8,7 @@ use crate::rpc::command_maps::{ RPC_LOAN_CONTRACT, RPC_MEMPOOL_TX_BY_ADDRESS, RPC_MEMPOOL_TX_BY_SIGNATURE, RPC_MEMPOOL_TX_COUNT, RPC_NETWORK_INFO, RPC_NFT_DETAILS, RPC_NFT_LIST, RPC_REGISTER_WALLET, RPC_TIME, RPC_TOKEN_DETAILS, RPC_TOKEN_LIST, RPC_TORRENT_BY_HEIGHT, RPC_TOTAL_CONFIRMED_TX, - RPC_TRANSACTION_BY_TXID, RPC_UNBLOCK_IP, RPC_VANITY_LOOKUP, + RPC_TRANSACTION_BY_TXID, RPC_UNBLOCK_IP, RPC_VANITY_LOOKUP, RPC_VANITY_OWNER_LOOKUP, }; use crate::standalone_tools::transaction_creator::create_transaction_request; use crate::timeout; @@ -455,6 +455,18 @@ async fn build_request_bytes( bin_msg.extend_from_slice(&hashmap_key); bin_msg.extend_from_slice(&address_bytes); } + // Lookup canonical short owner address by registered vanity address. + 45 => { + let command_number: u8 = RPC_VANITY_OWNER_LOOKUP; + let address_bytes = Wallet::normalize_to_short_address(&command_input) + .and_then(|vanity| Wallet::vanity_address_to_bytes(&vanity)) + .ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "Invalid vanity address") + })?; + bin_msg.extend_from_slice(&command_number.to_le_bytes()); + bin_msg.extend_from_slice(&hashmap_key); + bin_msg.extend_from_slice(&address_bytes); + } _ => { return Err(io::Error::new( io::ErrorKind::InvalidInput, diff --git a/src/standalone_tools/mod.rs b/src/standalone_tools/mod.rs index 632a9d1..a0f0362 100644 --- a/src/standalone_tools/mod.rs +++ b/src/standalone_tools/mod.rs @@ -1,3 +1,4 @@ // The standalone_tools module holds utility helpers that mirror node serialization and connection behavior. pub mod connections; pub mod transaction_creator; +pub mod vanity_resolver; diff --git a/src/standalone_tools/vanity_resolver.rs b/src/standalone_tools/vanity_resolver.rs new file mode 100644 index 0000000..4c29a58 --- /dev/null +++ b/src/standalone_tools/vanity_resolver.rs @@ -0,0 +1,87 @@ +use crate::common::network_startup::get_connections; +use crate::io; +use crate::records::memory::response_channels::generate_uid; +use crate::rpc::command_maps::RPC_VANITY_OWNER_LOOKUP; +use crate::standalone_tools::connections::handshake::{self, HandshakeWallet}; +use crate::wallets::structures::Wallet; + +pub async fn resolve_wallet_address_input( + address: &str, + wallet: &Wallet, +) -> Result { + let normalized = Wallet::normalize_to_short_address(address.trim()) + .ok_or_else(|| "Invalid wallet address.".to_string())?; + + if Wallet::short_address_to_bytes(&normalized).is_some() { + return Ok(normalized); + } + + if Wallet::vanity_address_to_bytes(&normalized).is_none() { + return Err("Invalid wallet address.".to_string()); + } + + lookup_vanity_owner(&normalized, wallet).await +} + +async fn lookup_vanity_owner(vanity_address: &str, wallet: &Wallet) -> Result { + let connections = get_connections().await; + if connections.is_empty() { + return Err("Vanity address lookup requires a reachable node.".to_string()); + } + + let mut last_error: Option = None; + + for conn in connections { + let socket_address = match conn.parse() { + Ok(address) => address, + Err(err) => { + last_error = Some(format!("Invalid configured peer address {conn}: {err}")); + continue; + } + }; + + let result = handshake::connect_and_handshake( + socket_address, + vanity_address.to_string(), + RPC_VANITY_OWNER_LOOKUP as usize, + HandshakeWallet::WalletParts { + long_address: wallet.saved.long_address.clone(), + private_key: wallet.saved.private_key.clone(), + }, + generate_uid(), + ) + .await; + + match result { + Ok(response) if response.is_empty() => { + last_error = Some("Vanity address is not registered.".to_string()); + } + Ok(response) => { + if let Ok(text) = String::from_utf8(response.clone()) { + let trimmed = text.trim(); + if trimmed.starts_with("error:") { + last_error = Some(trimmed.to_string()); + continue; + } + if Wallet::short_address_to_bytes(trimmed).is_some() { + return Ok(trimmed.to_string()); + } + } + + if let Some(short_address) = Wallet::bytes_to_short_address(&response) { + return Ok(short_address); + } + + last_error = Some("Vanity lookup returned an invalid short address.".to_string()); + } + Err(err) if err.kind() == io::ErrorKind::TimedOut => { + last_error = Some(format!("Vanity lookup timed out on {conn}.")); + } + Err(err) => { + last_error = Some(format!("Vanity lookup failed on {conn}: {err}")); + } + } + } + + Err(last_error.unwrap_or_else(|| "Vanity address lookup failed.".to_string())) +} diff --git a/src/startup/network_broadcast.rs b/src/startup/network_broadcast.rs index dcd1821..498ded3 100644 --- a/src/startup/network_broadcast.rs +++ b/src/startup/network_broadcast.rs @@ -1,14 +1,15 @@ use crate::common::binary_conversions::{binary_to_ip, binary_to_string, ip_to_binary}; use crate::common::network_startup::get_ip_and_port; -use crate::encode; +use crate::records::memory::network_mapping::monitor::MONITOR_ACTION_ADD; use crate::records::memory::network_mapping::structs::{ - AddAddressParams, DeleteAddressParams, SignedNodeEdit, NODE_ADDED_BY_OFFSET, - NODE_ADDED_SIGNATURE_OFFSET, NODE_ADDED_TIMESTAMP_OFFSET, NODE_BLOCKS_MINED_OFFSET, - NODE_DELETED_BLOCK_OFFSET, NODE_DELETED_BY_OFFSET, NODE_DELETED_SIGNATURE_OFFSET, - NODE_DELETED_TIMESTAMP_OFFSET, NODE_IP_OFFSET, NODE_RECORD_BYTES, + AddAddressParams, MonitorAddressParams, SignedMonitorEdit, SignedNodeEdit, + NODE_ADDED_BY_OFFSET, NODE_ADDED_SIGNATURE_OFFSET, NODE_ADDED_TIMESTAMP_OFFSET, + NODE_BLOCKS_MINED_OFFSET, NODE_DELETED_BLOCK_OFFSET, NODE_DELETED_TIMESTAMP_OFFSET, + NODE_IP_OFFSET, NODE_MONITOR_COUNT_OFFSET, NODE_RECORD_FIXED_BYTES, }; use crate::records::memory::network_mapping::NodeInfo; use crate::records::memory::response_channels::{reserve_entry, Command}; +use crate::records::memory::structs::Connection; use crate::rpc::command_maps::{RPC_ADD_NETWORK_NODE, RPC_REQUEST_NODE_LIST}; use crate::rpc::responses::RpcResponse; use crate::sled::Db; @@ -18,13 +19,53 @@ use crate::Mutex; use crate::TcpStream; use crate::Utc; -fn decode_optional_signature(bytes: &[u8]) -> String { - // Network mapping records use zero-filled signature fields when no signature exists yet. - if bytes.iter().all(|&byte| byte == 0) { - String::new() - } else { - encode(bytes) +async fn record_local_monitor_for_peer( + connections_key: &str, + command_map: Arc>, + db: &Db, + wallet: Arc, +) { + let Some(peer_wallet_bytes) = Connection::get_wallet_for_connection_key(connections_key).await + else { + return; + }; + let peer_long = Wallet::bytes_to_long_address(peer_wallet_bytes); + let Some(monitored_address) = Wallet::normalize_to_short_address(&peer_long) else { + return; + }; + let monitoring_address = wallet.saved.short_address.clone(); + if monitored_address == monitoring_address { + return; } + let Some((target_ip, _)) = connections_key.rsplit_once(':') else { + return; + }; + let timestamp = Utc::now().timestamp_millis() as u64; + let signature = NodeInfo::monitor_signature( + MONITOR_ACTION_ADD, + &monitored_address, + &monitoring_address, + target_ip, + timestamp, + &wallet, + ) + .await; + let _ = NodeInfo::add_monitor(MonitorAddressParams { + map: command_map, + edit: SignedMonitorEdit { + action: MONITOR_ACTION_ADD, + monitored_address, + monitoring_address, + target_ip: target_ip.to_string(), + modified_timestamp: timestamp, + modified_signature: signature, + }, + remote_ip: String::new(), + db: db.clone(), + wallet, + connections_key: connections_key.to_string(), + }) + .await; } pub async fn announce_self_to_network( @@ -84,10 +125,11 @@ pub async fn announce_self_to_network( unlocked_stream, command_map.clone(), db, - wallet, + wallet.clone(), connections_key, ) - .await + .await; + record_local_monitor_for_peer(connections_key, command_map, db, wallet).await; } } } @@ -110,15 +152,23 @@ pub async fn get_network_mapping( message.push(message_type); message.extend_from_slice(&download_hashmap_key); - // The remote reply is a concatenated list of fixed-width node records. + // The remote reply is a concatenated list of variable-width node records. RpcResponse::send_raw(&unlocked_stream, Some(connections_key), &message).await; let mut rx = download_hashmap_rx.lock().await; - // each node record is serialized into one fixed-width payload so - // startup import can safely drain the response in-place. if let Some(mut buffer) = rx.recv().await { - while buffer.len() >= NODE_RECORD_BYTES { - let chunk: Vec = buffer.drain(0..NODE_RECORD_BYTES).collect(); + while buffer.len() >= NODE_RECORD_FIXED_BYTES { + let monitor_count = u16::from_le_bytes( + buffer[NODE_MONITOR_COUNT_OFFSET..NODE_RECORD_FIXED_BYTES] + .try_into() + .unwrap(), + ) as usize; + let record_bytes = + NODE_RECORD_FIXED_BYTES + (monitor_count * Wallet::SHORT_ADDRESS_BYTES_LENGTH); + if buffer.len() < record_bytes { + break; + } + let chunk: Vec = buffer.drain(0..record_bytes).collect(); // The first part of each record describes the advertised node address and IP. let Some(address) = Wallet::bytes_to_short_address(&chunk[0..NODE_IP_OFFSET]) else { continue; @@ -137,8 +187,17 @@ pub async fn get_network_mapping( .try_into() .unwrap(), ); - let added_signature = decode_optional_signature( - &chunk[NODE_ADDED_SIGNATURE_OFFSET..NODE_DELETED_BY_OFFSET], + let added_signature = + crate::encode(&chunk[NODE_ADDED_SIGNATURE_OFFSET..NODE_DELETED_TIMESTAMP_OFFSET]); + let deleted_timestamp = u64::from_le_bytes( + chunk[NODE_DELETED_TIMESTAMP_OFFSET..NODE_DELETED_BLOCK_OFFSET] + .try_into() + .unwrap(), + ); + let deleted_block = u32::from_le_bytes( + chunk[NODE_DELETED_BLOCK_OFFSET..NODE_MONITOR_COUNT_OFFSET] + .try_into() + .unwrap(), ); let remote_ip = ""; // Add records are imported through NodeInfo so local validation/signing rules stay central. @@ -159,44 +218,24 @@ pub async fn get_network_mapping( }) .await; - if !chunk[NODE_DELETED_BY_OFFSET..NODE_DELETED_TIMESTAMP_OFFSET] - .iter() - .all(|&byte| byte == 0) - { - // Deleted fields are optional; when present, replay the delete record locally too. - let deleted_by = Wallet::bytes_to_long_address( - chunk[NODE_DELETED_BY_OFFSET..NODE_DELETED_TIMESTAMP_OFFSET].to_vec(), - ); - let deleted_timestamp = u64::from_le_bytes( - chunk[NODE_DELETED_TIMESTAMP_OFFSET..NODE_DELETED_BLOCK_OFFSET] - .try_into() - .unwrap(), - ); - let deleted_block = u32::from_le_bytes( - chunk[NODE_DELETED_BLOCK_OFFSET..NODE_DELETED_SIGNATURE_OFFSET] - .try_into() - .unwrap(), - ); - let deleted_signature = decode_optional_signature( - &chunk[NODE_DELETED_SIGNATURE_OFFSET..NODE_RECORD_BYTES], - ); - let deleted_address = address.clone(); - let _ = NodeInfo::delete_address(DeleteAddressParams { - map: command_map.clone(), - edit: SignedNodeEdit { - address, - ip, - modified_by: deleted_by, - modified_timestamp: deleted_timestamp, - modified_signature: deleted_signature, - }, - remote_ip: remote_ip.to_string(), - db: db.clone(), - wallet: wallet.clone(), - connections_key: connections_key.to_string(), - }) + let mut monitors = Vec::with_capacity(monitor_count); + for monitor_index in 0..monitor_count { + let start = + NODE_RECORD_FIXED_BYTES + monitor_index * Wallet::SHORT_ADDRESS_BYTES_LENGTH; + let end = start + Wallet::SHORT_ADDRESS_BYTES_LENGTH; + if let Some(monitor) = Wallet::bytes_to_short_address(&chunk[start..end]) { + monitors.push(monitor); + } + } + NodeInfo::set_monitors_from_mapping(&address, monitors).await; + + if deleted_timestamp > 0 { + NodeInfo::set_deleted_metadata_from_mapping( + &address, + deleted_timestamp, + deleted_block, + ) .await; - NodeInfo::set_deleted_block_from_mapping(&deleted_address, deleted_block).await; } } } diff --git a/src/verifications/async_funcs/asset_rules.rs b/src/verifications/async_funcs/asset_rules.rs index e1bea38..8414f08 100644 --- a/src/verifications/async_funcs/asset_rules.rs +++ b/src/verifications/async_funcs/asset_rules.rs @@ -15,3 +15,11 @@ pub fn reserved_base_coins() -> [&'static str; 11] { "BCH ", ] } + +pub fn is_reserved_base_coin_ticker(asset_name: &str) -> bool { + let normalized = asset_name.trim().to_lowercase(); + + reserved_base_coins() + .iter() + .any(|reserved| normalized == reserved.trim().to_lowercase()) +} diff --git a/src/verifications/async_funcs/validate_torrent_data.rs b/src/verifications/async_funcs/validate_torrent_data.rs index 731ed14..63e3739 100644 --- a/src/verifications/async_funcs/validate_torrent_data.rs +++ b/src/verifications/async_funcs/validate_torrent_data.rs @@ -134,7 +134,7 @@ impl Torrent { } blockchain_data.unmined_block.next_block_difficulty } else { - 3000000000000000_u64 + 1200000000000000_u64 }; let torrent_difficulty = self.info.this_block_difficulty; if blockchain_difficulty == torrent_difficulty { diff --git a/src/verifications/async_funcs/verify_block.rs b/src/verifications/async_funcs/verify_block.rs index e38965f..b75c42e 100644 --- a/src/verifications/async_funcs/verify_block.rs +++ b/src/verifications/async_funcs/verify_block.rs @@ -186,7 +186,7 @@ impl Block { // compare the reduced header hash against the previous // block's recorded target difficulty threshold if !genesis_checkup().await { - let difficulty_target = 3000000000000000_u64; + let difficulty_target = 1200000000000000_u64; if hash <= difficulty_target { return Ok(()); } else { diff --git a/src/verifications/async_funcs/verify_create_nft.rs b/src/verifications/async_funcs/verify_create_nft.rs index 5f91760..9b2d0da 100644 --- a/src/verifications/async_funcs/verify_create_nft.rs +++ b/src/verifications/async_funcs/verify_create_nft.rs @@ -7,7 +7,7 @@ use crate::records::wallet_registry::{ require_canonical_registered_short_address, resolve_pubkey_from_short_address, }; use crate::sled::Db; -use crate::verifications::async_funcs::asset_rules::reserved_base_coins; +use crate::verifications::async_funcs::asset_rules::is_reserved_base_coin_ticker; use crate::verifications::async_funcs::checks::balance_check::balance_checkup; use crate::verifications::async_funcs::checks::mempool_check::memcheck; use crate::verifications::async_funcs::checks::verify_db::db_hex_verification; @@ -19,6 +19,19 @@ impl CreateNftTransaction { let signature = &self.signature; let hash = &self.unsigned_create_nft.hash().await; + // NFT names use the same fixed padded asset-name length as tokens. + let nft_name_length = self.unsigned_create_nft.nft_name.len(); + if nft_name_length != COIN_LENGTH { + return Err( + "Nft Name length must be 15 characters. Consider padding with empty spaces", + ) + .map_err(|s| s.to_string())?; + } + + if is_reserved_base_coin_ticker(&self.unsigned_create_nft.nft_name) { + return Err("NFT name is reserved for the base coin.".to_string()); + } + // Transactions already present in the mempool can short-circuit // the deeper verification path and reuse their stored signature. if memcheck(signature, hash).await { @@ -63,22 +76,6 @@ impl CreateNftTransaction { return Err("A series must contain at least 2 items").map_err(|s| s.to_string())?; } - // NFT names use the same fixed padded asset-name length as tokens. - let nft_name_length = self.unsigned_create_nft.nft_name.len(); - if nft_name_length != COIN_LENGTH { - return Err( - "Nft Name length must be 15 characters. Consider padding with empty spaces", - ) - .map_err(|s| s.to_string())?; - } - - if reserved_base_coins().iter().any(|reserved| { - self.unsigned_create_nft.nft_name.trim().to_lowercase() - == reserved.trim().to_lowercase() - }) { - return Err("NFT name is reserved for the base coin.".to_string()); - } - // Series NFTs reserve each numbered item name separately, while // standalone NFTs reserve the base name directly. let tree = db.open_tree("nfts").unwrap(); diff --git a/src/verifications/async_funcs/verify_create_token.rs b/src/verifications/async_funcs/verify_create_token.rs index 0081a16..9be9210 100644 --- a/src/verifications/async_funcs/verify_create_token.rs +++ b/src/verifications/async_funcs/verify_create_token.rs @@ -6,7 +6,7 @@ use crate::records::wallet_registry::{ require_canonical_registered_short_address, resolve_pubkey_from_short_address, }; use crate::sled::Db; -use crate::verifications::async_funcs::asset_rules::reserved_base_coins; +use crate::verifications::async_funcs::asset_rules::is_reserved_base_coin_ticker; use crate::verifications::async_funcs::checks::balance_check::balance_checkup; use crate::verifications::async_funcs::checks::mempool_check::memcheck; use crate::verifications::async_funcs::checks::verify_db::{ @@ -20,6 +20,18 @@ impl CreateTokenTransaction { // the deeper verification path and reuse their stored signature. let hash = self.unsigned_create_token.hash().await; let signature = &self.signature; + + // Token tickers use the fixed padded coin-length format. + let ticker_length = self.unsigned_create_token.ticker.len(); + if ticker_length != COIN_LENGTH { + return Err("Ticker length must be 15 characters. Consider padding with empty spaces") + .map_err(|s| s.to_string())?; + } + + if is_reserved_base_coin_ticker(&self.unsigned_create_token.ticker) { + return Err("Ticker is reserved for the base coin.".to_string()); + } + if memcheck(signature, &hash).await { return Ok(self.signature.clone()); } @@ -52,20 +64,6 @@ impl CreateTokenTransaction { return Err("Ticker already exists exist.".to_string()); } - // Token tickers use the fixed padded coin-length format. - let ticker_length = self.unsigned_create_token.ticker.len(); - if ticker_length != COIN_LENGTH { - return Err("Ticker length must be 15 characters. Consider padding with empty spaces") - .map_err(|s| s.to_string())?; - } - - if reserved_base_coins().iter().any(|reserved| { - self.unsigned_create_token.ticker.trim().to_lowercase() - == reserved.trim().to_lowercase() - }) { - return Err("Ticker is reserved for the base coin.".to_string()); - } - // Token hard-limit metadata is stored as a single byte so later // issuance checks can enforce either capped or uncapped supply. if self.unsigned_create_token.hard_limit > 1 {