sybil fix, vanity fix, wallet selection fix

This commit is contained in:
viraladmin 2026-06-04 09:06:51 -06:00
parent 9f3f53ca34
commit 6cd01d1345
68 changed files with 1499 additions and 854 deletions

View File

@ -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<String> {
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::<Value>(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}");
}

View File

@ -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}");

View File

@ -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}");

View File

@ -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}");

View File

@ -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}");

View File

@ -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<String, String> {
// 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<u32, String> {
// 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<u32, String> {
Ok(timestamp as u32)
}
async fn load_signing_wallet() -> Option<Wallet> {
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;

View File

@ -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}");

View File

@ -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}");

View File

@ -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<String, String> {
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;

View File

@ -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}");

View File

@ -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<String, String> {
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;

View File

@ -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}");

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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<String> = 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;

View File

@ -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;

View File

@ -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;

View File

@ -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<String> {
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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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<String, String> {
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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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}");

View File

@ -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<String, String> {
// 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()) {
let lender =
match resolve_wallet_address_input(json["lender"].as_str().unwrap_or_default(), &wallet)
.await
{
Ok(address) => address,
Err(_) => {
println!("Transaction is not valid. Lender address is not a valid short 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}"

View File

@ -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<String, String> {
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,11 +92,13 @@ 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())
let sender1 =
match resolve_wallet_address_input(json["sender1"].as_str().unwrap_or_default(), &wallet)
.await
{
Ok(address) => address,
Err(_) => {
println!("sender1 wallet invalid");
Err(err) => {
println!("sender1 wallet invalid: {err}");
return;
}
};
@ -106,11 +108,13 @@ async fn main() {
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())
let sender2 =
match resolve_wallet_address_input(json["sender2"].as_str().unwrap_or_default(), &wallet)
.await
{
Ok(address) => address,
Err(_) => {
println!("sender2 wallet invalid");
Err(err) => {
println!("sender2 wallet invalid: {err}");
return;
}
};

View File

@ -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<String, String> {
// Rustyline gives CLI path prompts filesystem completion, including tab completion.
let config = Config::builder()
.completion_type(CompletionType::List)
.build();
let mut editor =
Editor::<PathHelper, DefaultHistory>::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}]: ");

View File

@ -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,

View File

@ -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,13 +74,7 @@ pub async fn set_reconnect_context(db: Db, wallet: Arc<Wallet>, map: Arc<Mutex<C
*context = Some(ReconnectContext { db, wallet, map });
}
async fn reconnect_dropped_outgoing(excluded_ip: &str) {
if !try_start_reconnect() {
warn!("[reconnect] replacement attempt already in progress, skipping duplicate request");
return;
}
async {
async fn reconnect_replacement_inner(excluded_ip: &str) {
// When an outgoing peer disappears, try to replace it with another
// active node that is not already connected and is not the failed IP.
let context = {
@ -124,6 +123,60 @@ async fn reconnect_dropped_outgoing(excluded_ip: &str) {
warn!("[reconnect] bootstrap recovery failed: {err}");
}
}
async fn retry_dropped_outgoing(ip: String, port: u16) {
if !try_start_reconnect() {
warn!("[reconnect] reconnect attempt already in progress, skipping duplicate request");
return;
}
async {
let context = {
let guard = RECONNECT_CONTEXT.lock().await;
guard.clone()
};
let Some(context) = context else {
warn!("[reconnect] no reconnect context configured");
return;
};
let addr_string = format!("{ip}:{port}");
for attempt in 1..=3 {
sleep(Duration::from_secs(30)).await;
let socket_addr = match addr_string.parse() {
Ok(addr) => addr,
Err(err) => {
warn!("[reconnect] invalid dropped peer address {addr_string}: {err}");
break;
}
};
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,
};
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;
finish_reconnect();
@ -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<Vec<u8>> {
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<Mutex<TcpStream>>) -> Option<String> {
@ -533,6 +615,57 @@ impl Connection {
}
}
fn spawn_monitor_update(ip: String, action: u8, peer_wallet_bytes: Vec<u8>, 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<RwLock<Option<Connection>>> = Arc::new(RwLock::new(None));
}

View File

@ -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())
}
}

View File

@ -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())
}
}

View File

@ -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,
}
}
}

View File

@ -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.

View File

@ -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<HashMap<String, NodeInfo>> = Mutex::new(HashMap::new());
static ref PING_MONITORS: Mutex<HashMap<String, String>> = 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<String>,
}
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;

View File

@ -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<Wallet>,
) -> 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<Mutex<Command>>,
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(&timestamp_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<String, NodeInfo>,
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<String> = 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<String>) {
let mut address_map = ADDRESS_MAP.lock().await;
if let Some(node) = address_map.get_mut(address) {
node.monitoring = monitors;
}
}
}

View File

@ -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<u8> = Vec::with_capacity(map.len() * NODE_RECORD_BYTES);
let mut data: Vec<u8> = 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)
}

View File

@ -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<Mutex<Command>>,
pub edit: SignedNodeEdit,
pub edit: SignedMonitorEdit,
pub remote_ip: String,
pub db: Db,
pub wallet: Arc<Wallet>,

View File

@ -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
};

View File

@ -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;

View File

@ -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;

View File

@ -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<Mutex<TcpStream>>,
db: &Db,
wallet: Arc<Wallet>,
map: Arc<Mutex<Command>>,
) -> 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))
}

View File

@ -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<Mutex<TcpStream>>,
db: &Db,
wallet: Arc<Wallet>,
map: Arc<Mutex<Command>>,
) -> 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))
}

View File

@ -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(),
),
}
}

View File

@ -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,

View File

@ -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<Vec<u8>, 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)

View File

@ -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,

View File

@ -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;

View File

@ -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<String, String> {
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<String, String> {
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<String> = 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()))
}

View File

@ -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<Mutex<Command>>,
db: &Db,
wallet: Arc<Wallet>,
) {
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<u8> = 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<u8> = 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;
}
}
}

View File

@ -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())
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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();

View File

@ -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 {