From 12916c0b1168e58030ed9c198319f7424671435d Mon Sep 17 00:00:00 2001 From: viraladmin <00purple@gmail.com> Date: Mon, 29 Jun 2026 10:05:30 -0600 Subject: [PATCH] cli tool fixes --- docs/CLI_TOOLS.md | 24 ++- src/bin/create_burn_tx.rs | 2 +- src/bin/create_collateral_claim_tx.rs | 2 +- src/bin/create_issue_token_tx.rs | 2 +- src/bin/create_loan_payment_tx.rs | 2 +- src/bin/create_loan_tx.rs | 2 +- src/bin/create_marketing_tx.rs | 2 +- src/bin/create_nft_tx.rs | 2 +- src/bin/create_swap_tx.rs | 2 +- src/bin/create_tokens_tx.rs | 2 +- src/bin/create_transfer_tx.rs | 2 +- src/bin/create_vanity_tx.rs | 2 +- src/bin/lookup_block_by_hash.rs | 7 +- src/bin/lookup_block_by_height.rs | 7 +- src/bin/lookup_contract_by_address.rs | 6 +- src/bin/lookup_contract_by_hash.rs | 6 +- src/bin/lookup_difficulty.rs | 6 +- src/bin/lookup_height.rs | 6 +- src/bin/lookup_largest_txfee.rs | 19 +- src/bin/lookup_loan_by_address.rs | 108 +++++++++++ src/bin/lookup_loan_by_hash.rs | 92 +++++++++ src/bin/lookup_local_balance.rs | 9 +- src/bin/lookup_mempool_tx_by_address.rs | 22 ++- src/bin/lookup_mempool_tx_by_signature.rs | 7 +- src/bin/lookup_mempool_tx_count.rs | 6 +- src/bin/lookup_node_time.rs | 11 +- src/bin/lookup_remote_balance.rs | 21 +- src/bin/lookup_torrent.rs | 7 +- src/bin/lookup_transaction.rs | 14 +- src/bin/validate_torrent_and_block_headers.rs | 40 ++-- src/bin/verify_address.rs | 100 +--------- src/bin/verify_message.rs | 180 ++++-------------- src/bin/verify_sign_loan_tx.rs | 2 +- src/bin/verify_sign_swap_tx.rs | 2 +- .../connections/sending_request.rs | 7 + src/standalone_tools/loan_lookup_json.rs | 111 +++++++++++ src/standalone_tools/mod.rs | 2 + .../wallet_registry_lookup.rs | 55 ++++++ 38 files changed, 602 insertions(+), 297 deletions(-) create mode 100644 src/bin/lookup_loan_by_address.rs create mode 100644 src/bin/lookup_loan_by_hash.rs create mode 100644 src/standalone_tools/loan_lookup_json.rs create mode 100644 src/standalone_tools/wallet_registry_lookup.rs diff --git a/docs/CLI_TOOLS.md b/docs/CLI_TOOLS.md index 853497b..96bbefc 100644 --- a/docs/CLI_TOOLS.md +++ b/docs/CLI_TOOLS.md @@ -847,7 +847,7 @@ Expected reply: ] ``` -### lookup_contract_by_hash +### lookup_loan_by_hash Looks up a loan contract by contract hash. @@ -860,16 +860,20 @@ Requires: Usage: ```text -lookup_contract_by_hash +lookup_loan_by_hash ``` Expected reply: -```text - +```json +{ + "contract": "", + "status": "active", + "payments": [] +} ``` -### lookup_contract_by_address +### lookup_loan_by_address Looks up loan contracts by wallet address. @@ -882,13 +886,17 @@ Requires: Usage: ```text -lookup_contract_by_address +lookup_loan_by_address ``` Expected reply: -```text - +```json +{ + "address": "", + "contracts": 1, + "loans": [] +} ``` ### lookup_remote_balance diff --git a/src/bin/create_burn_tx.rs b/src/bin/create_burn_tx.rs index 39d4b34..da14bef 100644 --- a/src/bin/create_burn_tx.rs +++ b/src/bin/create_burn_tx.rs @@ -139,5 +139,5 @@ async fn main() { return; } - println!("transaction: {output_str}"); + println!("{output_str}"); } diff --git a/src/bin/create_collateral_claim_tx.rs b/src/bin/create_collateral_claim_tx.rs index 86a0889..e71b45f 100644 --- a/src/bin/create_collateral_claim_tx.rs +++ b/src/bin/create_collateral_claim_tx.rs @@ -115,5 +115,5 @@ async fn main() { return; } - println!("transaction: {output_str}"); + println!("{output_str}"); } diff --git a/src/bin/create_issue_token_tx.rs b/src/bin/create_issue_token_tx.rs index f7de16f..7ac2db8 100644 --- a/src/bin/create_issue_token_tx.rs +++ b/src/bin/create_issue_token_tx.rs @@ -123,5 +123,5 @@ async fn main() { return; } - println!("transaction: {output_str}"); + println!("{output_str}"); } diff --git a/src/bin/create_loan_payment_tx.rs b/src/bin/create_loan_payment_tx.rs index 0504f19..dcf6b02 100644 --- a/src/bin/create_loan_payment_tx.rs +++ b/src/bin/create_loan_payment_tx.rs @@ -137,5 +137,5 @@ async fn main() { return; } - println!("transaction: {output_str}"); + println!("{output_str}"); } diff --git a/src/bin/create_loan_tx.rs b/src/bin/create_loan_tx.rs index 8347e87..132a8bf 100644 --- a/src/bin/create_loan_tx.rs +++ b/src/bin/create_loan_tx.rs @@ -256,5 +256,5 @@ async fn main() { return; } - println!("transaction: {output_str}"); + println!("{output_str}"); } diff --git a/src/bin/create_marketing_tx.rs b/src/bin/create_marketing_tx.rs index c62b559..e1b1820 100644 --- a/src/bin/create_marketing_tx.rs +++ b/src/bin/create_marketing_tx.rs @@ -174,5 +174,5 @@ async fn main() { return; } - println!("transaction: {output_str}"); + println!("{output_str}"); } diff --git a/src/bin/create_nft_tx.rs b/src/bin/create_nft_tx.rs index 63cc8b5..dd10112 100644 --- a/src/bin/create_nft_tx.rs +++ b/src/bin/create_nft_tx.rs @@ -156,5 +156,5 @@ async fn main() { return; } - println!("transaction: {output_str}"); + println!("{output_str}"); } diff --git a/src/bin/create_swap_tx.rs b/src/bin/create_swap_tx.rs index beaf3a4..3ebba27 100644 --- a/src/bin/create_swap_tx.rs +++ b/src/bin/create_swap_tx.rs @@ -237,5 +237,5 @@ async fn main() { return; } - println!("transaction: {output_str}"); + println!("{output_str}"); } diff --git a/src/bin/create_tokens_tx.rs b/src/bin/create_tokens_tx.rs index fc5f059..f5cdaf8 100644 --- a/src/bin/create_tokens_tx.rs +++ b/src/bin/create_tokens_tx.rs @@ -143,5 +143,5 @@ async fn main() { return; } - println!("transaction: {output_str}"); + println!("{output_str}"); } diff --git a/src/bin/create_transfer_tx.rs b/src/bin/create_transfer_tx.rs index fdeda93..736c663 100644 --- a/src/bin/create_transfer_tx.rs +++ b/src/bin/create_transfer_tx.rs @@ -246,5 +246,5 @@ async fn main() { return; } - println!("transaction: {output_str}"); + println!("{output_str}"); } diff --git a/src/bin/create_vanity_tx.rs b/src/bin/create_vanity_tx.rs index cb8983d..242bd06 100644 --- a/src/bin/create_vanity_tx.rs +++ b/src/bin/create_vanity_tx.rs @@ -169,5 +169,5 @@ async fn main() { return; } - println!("transaction: {output_str}"); + println!("{output_str}"); } diff --git a/src/bin/lookup_block_by_hash.rs b/src/bin/lookup_block_by_hash.rs index d8f2f07..6853a88 100644 --- a/src/bin/lookup_block_by_hash.rs +++ b/src/bin/lookup_block_by_hash.rs @@ -1,6 +1,7 @@ use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; +use blockchain::json; use blockchain::records::memory::response_channels::generate_uid; use blockchain::records::unpack_block::load_by_binary_data::load_block_from_binary; use blockchain::standalone_tools::connections::handshake; @@ -72,7 +73,11 @@ async fn main() { println!("{trimmed}"); connected = true; } else if !response.is_empty() { - println!("{}", hex::encode(response)); + println!( + "{}", + serde_json::to_string_pretty(&json!({ "hex": hex::encode(response) })) + .unwrap() + ); connected = true; } } diff --git a/src/bin/lookup_block_by_height.rs b/src/bin/lookup_block_by_height.rs index 6085928..b07e0e2 100644 --- a/src/bin/lookup_block_by_height.rs +++ b/src/bin/lookup_block_by_height.rs @@ -1,6 +1,7 @@ use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; +use blockchain::json; use blockchain::records::memory::response_channels::generate_uid; use blockchain::records::unpack_block::load_by_binary_data::load_block_from_binary; use blockchain::standalone_tools::connections::handshake; @@ -76,7 +77,11 @@ async fn main() { println!("{trimmed}"); connected = true; } else if !response.is_empty() { - println!("{}", hex::encode(response)); + println!( + "{}", + serde_json::to_string_pretty(&json!({ "hex": hex::encode(response) })) + .unwrap() + ); connected = true; } } diff --git a/src/bin/lookup_contract_by_address.rs b/src/bin/lookup_contract_by_address.rs index 1ebef88..ee8453e 100644 --- a/src/bin/lookup_contract_by_address.rs +++ b/src/bin/lookup_contract_by_address.rs @@ -2,6 +2,7 @@ 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::json; use blockchain::records::memory::response_channels::generate_uid; use blockchain::standalone_tools::connections::handshake; use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input; @@ -79,7 +80,10 @@ async fn main() { println!("{trimmed}"); connected = true; } else if !response.is_empty() { - println!("{}", encode(response)); + println!( + "{}", + serde_json::to_string_pretty(&json!({ "hex": encode(response) })).unwrap() + ); connected = true; } } diff --git a/src/bin/lookup_contract_by_hash.rs b/src/bin/lookup_contract_by_hash.rs index d756834..6464638 100644 --- a/src/bin/lookup_contract_by_hash.rs +++ b/src/bin/lookup_contract_by_hash.rs @@ -2,6 +2,7 @@ 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::json; use blockchain::records::memory::response_channels::generate_uid; use blockchain::standalone_tools::connections::handshake; @@ -64,7 +65,10 @@ async fn main() { println!("{trimmed}"); connected = true; } else if !response.is_empty() { - println!("{}", encode(response)); + println!( + "{}", + serde_json::to_string_pretty(&json!({ "hex": encode(response) })).unwrap() + ); connected = true; } } diff --git a/src/bin/lookup_difficulty.rs b/src/bin/lookup_difficulty.rs index 4bee6bf..17c3b54 100644 --- a/src/bin/lookup_difficulty.rs +++ b/src/bin/lookup_difficulty.rs @@ -1,6 +1,7 @@ use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; +use blockchain::json; use blockchain::records::memory::response_channels::generate_uid; use blockchain::standalone_tools::connections::handshake; @@ -45,7 +46,10 @@ async fn main() { // The difficulty reply carries 4 bytes of header plus an 8-byte u64 difficulty. if response.len() == 12 { let difficulty = u64::from_le_bytes(response[4..12].try_into().unwrap()); - println!("{difficulty}"); + println!( + "{}", + serde_json::to_string_pretty(&json!({ "difficulty": difficulty })).unwrap() + ); connected = true; } else { let response_text = String::from_utf8_lossy(&response); diff --git a/src/bin/lookup_height.rs b/src/bin/lookup_height.rs index 1118c84..3a89335 100644 --- a/src/bin/lookup_height.rs +++ b/src/bin/lookup_height.rs @@ -1,6 +1,7 @@ use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; +use blockchain::json; use blockchain::records::memory::response_channels::generate_uid; use blockchain::standalone_tools::connections::handshake; @@ -53,7 +54,10 @@ async fn main() { Ok(response) => { if response.len() == 4 { let height = u32::from_le_bytes(response[..4].try_into().unwrap()); - println!("{height}"); + println!( + "{}", + serde_json::to_string_pretty(&json!({ "height": height })).unwrap() + ); connected = true; } else { let response_text = String::from_utf8_lossy(&response); diff --git a/src/bin/lookup_largest_txfee.rs b/src/bin/lookup_largest_txfee.rs index db07226..668635f 100644 --- a/src/bin/lookup_largest_txfee.rs +++ b/src/bin/lookup_largest_txfee.rs @@ -1,6 +1,7 @@ use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; +use blockchain::json; use blockchain::records::memory::response_channels::generate_uid; use blockchain::standalone_tools::connections::handshake; @@ -58,15 +59,21 @@ async fn main() { match result { Ok(response) => { if response.is_empty() { - println!("0"); + println!( + "{}", + serde_json::to_string_pretty(&json!({ "largest_tx_fee": "0.00000000" })) + .unwrap() + ); connected = true; } else if response.len() == 8 { let fee = u64::from_le_bytes(response[0..8].try_into().unwrap()); - if fee == 0 { - println!("0"); - } else { - println!("{}", format_balance(fee)); - } + println!( + "{}", + serde_json::to_string_pretty( + &json!({ "largest_tx_fee": format_balance(fee) }) + ) + .unwrap() + ); connected = true; } else { let response_text = String::from_utf8_lossy(&response); diff --git a/src/bin/lookup_loan_by_address.rs b/src/bin/lookup_loan_by_address.rs new file mode 100644 index 0000000..cce0396 --- /dev/null +++ b/src/bin/lookup_loan_by_address.rs @@ -0,0 +1,108 @@ +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::json; +use blockchain::records::memory::response_channels::generate_uid; +use blockchain::standalone_tools::connections::handshake; +use blockchain::standalone_tools::loan_lookup_json::parse_loan_lookup_by_address; +use blockchain::standalone_tools::vanity_resolver::resolve_wallet_address_input; +use blockchain::wallets::structures::Wallet; + +#[tokio::main] +async fn main() { + // Command 37 asks a peer for contract records tied to a wallet address. + let hashmap_key = generate_uid(); + let rpc_command = 37; + + // The address can be a long, short, or vanity address; request encoding normalizes it later. + let args: Vec = env::args().collect(); + if args.len() != 2 { + println!("Usage: ./lookup_loan_by_address "); + return; + } + + let wallet_address = match args[1].parse::() { + Ok(address) => address, + Err(_) => { + println!("Please enter a wallet address"); + 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; + let mut connected = false; + + for conn in connections { + if connected { + break; + } + + let socket_address = conn.parse().expect("Failed to parse the socket address"); + let result = handshake::connect_and_handshake( + socket_address, + wallet_address.clone(), + rpc_command, + handshake::HandshakeWallet::WalletParts { + public_key: wallet.saved.public_key.clone(), + private_key: wallet.saved.private_key.clone(), + }, + hashmap_key, + ) + .await; + + match result { + Ok(response) => { + // Loan lookups return a key/value text summary; print successful data as JSON. + let response_text = String::from_utf8_lossy(&response); + let trimmed = response_text.trim(); + if !trimmed.is_empty() { + if trimmed.starts_with("error:") { + println!("{trimmed}"); + } else { + println!( + "{}", + serde_json::to_string_pretty(&parse_loan_lookup_by_address(trimmed)) + .unwrap() + ); + } + connected = true; + } else if !response.is_empty() { + println!( + "{}", + serde_json::to_string_pretty(&json!({ "hex": encode(response) })).unwrap() + ); + connected = true; + } + } + Err(_) => { + connected = false; + } + } + } + + if !connected { + eprintln!("failed to connect"); + } +} diff --git a/src/bin/lookup_loan_by_hash.rs b/src/bin/lookup_loan_by_hash.rs new file mode 100644 index 0000000..4f32e1d --- /dev/null +++ b/src/bin/lookup_loan_by_hash.rs @@ -0,0 +1,92 @@ +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::json; +use blockchain::records::memory::response_channels::generate_uid; +use blockchain::standalone_tools::connections::handshake; +use blockchain::standalone_tools::loan_lookup_json::parse_loan_summary; + +#[tokio::main] +async fn main() { + // Command 33 asks a peer for one loan contract by its 32-byte hash. + let hashmap_key = generate_uid(); + let rpc_command = 33; + + // The hash is passed as text here and encoded into binary request bytes later. + let args: Vec = env::args().collect(); + if args.len() != 2 { + println!("Usage: ./lookup_loan_by_hash "); + return; + } + + let contract_hash = match args[1].parse::() { + Ok(hash) => hash, + Err(_) => { + println!("Please enter a contract hash"); + 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; + + // Try each configured peer until one returns a response. + let connections = get_connections().await; + let mut connected = false; + + for conn in connections { + if connected { + break; + } + + let socket_address = conn.parse().expect("Failed to parse the socket address"); + let result = handshake::connect_and_handshake( + socket_address, + contract_hash.clone(), + rpc_command, + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.clone(), + wallet_path: wallet_path.clone(), + }, + hashmap_key, + ) + .await; + + match result { + Ok(response) => { + // Loan lookups return a key/value text summary; print successful data as JSON. + let response_text = String::from_utf8_lossy(&response); + let trimmed = response_text.trim(); + if !trimmed.is_empty() { + if trimmed.starts_with("error:") { + println!("{trimmed}"); + } else { + println!( + "{}", + serde_json::to_string_pretty(&parse_loan_summary(trimmed)).unwrap() + ); + } + connected = true; + } else if !response.is_empty() { + println!( + "{}", + serde_json::to_string_pretty(&json!({ "hex": encode(response) })).unwrap() + ); + connected = true; + } + } + Err(_) => { + connected = false; + } + } + } + + if !connected { + eprintln!("failed to connect"); + } +} diff --git a/src/bin/lookup_local_balance.rs b/src/bin/lookup_local_balance.rs index 843bee7..491e165 100644 --- a/src/bin/lookup_local_balance.rs +++ b/src/bin/lookup_local_balance.rs @@ -1,5 +1,6 @@ use blockchain::env; use blockchain::fs; +use blockchain::json; use blockchain::tilde; use rustyline::completion::FilenameCompleter; use rustyline::error::ReadlineError; @@ -85,5 +86,11 @@ async fn main() { buffer.copy_from_slice(&bytes[..8]); let value = u64::from_le_bytes(buffer); - println!("{}", format_balance(value)); + println!( + "{}", + serde_json::to_string_pretty(&json!({ + "balance": format_balance(value) + })) + .unwrap() + ); } diff --git a/src/bin/lookup_mempool_tx_by_address.rs b/src/bin/lookup_mempool_tx_by_address.rs index 19b2e16..98268ba 100644 --- a/src/bin/lookup_mempool_tx_by_address.rs +++ b/src/bin/lookup_mempool_tx_by_address.rs @@ -17,6 +17,7 @@ use blockchain::common::types::{ ISSUE_TOKEN_TYPE, LENDER_TYPE, MARKETING_TYPE, SWAP_TYPE, TRANSFER_TYPE, VANITY_ADDRESS_TYPE, }; use blockchain::env; +use blockchain::json; use blockchain::records::memory::response_channels::generate_uid; use blockchain::rpc::command_maps; use blockchain::standalone_tools::connections::handshake; @@ -154,12 +155,23 @@ async fn main() { match result { Ok(response) => { if response.is_empty() { - println!("[]"); + println!( + "{}", + serde_json::to_string_pretty(&json!({ "transactions": [] })).unwrap() + ); connected = true; } else if let Some(transactions) = decode_mempool_transactions(&response).await { + let mut output = Vec::new(); for transaction in transactions { - println!("{transaction}"); + match serde_json::from_str::(&transaction) { + Ok(value) => output.push(value), + Err(_) => output.push(json!(transaction)), + } } + println!( + "{}", + serde_json::to_string_pretty(&json!({ "transactions": output })).unwrap() + ); connected = true; } else { // Text errors are printed directly; unknown binary is shown as hex for inspection. @@ -168,7 +180,11 @@ async fn main() { if !trimmed.is_empty() { println!("{trimmed}"); } else { - println!("{}", hex::encode(response)); + println!( + "{}", + serde_json::to_string_pretty(&json!({ "hex": hex::encode(response) })) + .unwrap() + ); } connected = true; } diff --git a/src/bin/lookup_mempool_tx_by_signature.rs b/src/bin/lookup_mempool_tx_by_signature.rs index 5096a2e..fe32f40 100644 --- a/src/bin/lookup_mempool_tx_by_signature.rs +++ b/src/bin/lookup_mempool_tx_by_signature.rs @@ -17,6 +17,7 @@ use blockchain::common::types::{ ISSUE_TOKEN_TYPE, LENDER_TYPE, MARKETING_TYPE, SWAP_TYPE, TRANSFER_TYPE, VANITY_ADDRESS_TYPE, }; use blockchain::env; +use blockchain::json; use blockchain::records::memory::response_channels::generate_uid; use blockchain::standalone_tools::connections::handshake; use blockchain::to_string_pretty; @@ -132,7 +133,11 @@ async fn main() { if !trimmed.is_empty() { println!("{trimmed}"); } else { - println!("{}", hex::encode(response)); + println!( + "{}", + serde_json::to_string_pretty(&json!({ "hex": hex::encode(response) })) + .unwrap() + ); } connected = true; } diff --git a/src/bin/lookup_mempool_tx_count.rs b/src/bin/lookup_mempool_tx_count.rs index 46e4fa8..5913569 100644 --- a/src/bin/lookup_mempool_tx_count.rs +++ b/src/bin/lookup_mempool_tx_count.rs @@ -1,6 +1,7 @@ use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; +use blockchain::json; use blockchain::records::memory::response_channels::generate_uid; use blockchain::standalone_tools::connections::handshake; @@ -49,7 +50,10 @@ async fn main() { Ok(response) if response.len() >= 4 => { // The mempool count command returns a 4-byte little-endian integer. let count = u32::from_le_bytes(response[0..4].try_into().unwrap()); - println!("{count}"); + println!( + "{}", + serde_json::to_string_pretty(&json!({ "mempool_tx_count": count })).unwrap() + ); connected = true; } Ok(response) => { diff --git a/src/bin/lookup_node_time.rs b/src/bin/lookup_node_time.rs index bb6f793..78074e6 100644 --- a/src/bin/lookup_node_time.rs +++ b/src/bin/lookup_node_time.rs @@ -1,6 +1,7 @@ use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; +use blockchain::json; use blockchain::records::memory::response_channels::generate_uid; use blockchain::standalone_tools::connections::handshake; use blockchain::{TimeZone, Utc}; @@ -54,8 +55,14 @@ async fn main() { .single() .map(|datetime| datetime.format("%H:%M:%S UTC").to_string()) .unwrap_or_else(|| "invalid UTC timestamp".to_string()); - println!("timestamp: {timestamp}"); - println!("time: {time}"); + println!( + "{}", + serde_json::to_string_pretty(&json!({ + "timestamp": timestamp, + "time": time + })) + .unwrap() + ); connected = true; } Ok(response) => { diff --git a/src/bin/lookup_remote_balance.rs b/src/bin/lookup_remote_balance.rs index 4e366e0..dcdb117 100644 --- a/src/bin/lookup_remote_balance.rs +++ b/src/bin/lookup_remote_balance.rs @@ -2,6 +2,7 @@ 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::json; use blockchain::read_to_string; use blockchain::records::memory::response_channels::generate_uid; use blockchain::standalone_tools::connections::handshake; @@ -157,23 +158,31 @@ async fn main() { Ok(response) => { if response.is_empty() { connected = true; - println!("[]"); + println!( + "{}", + serde_json::to_string_pretty(&json!({ "balances": [] })).unwrap() + ); break; } // Balance rows are 15 bytes asset name, 4 bytes NFT series, and 8 bytes balance. if response.len() % 27 == 0 { + let mut balances = Vec::new(); for entry in response.chunks_exact(27) { let name_bytes = &entry[..15]; let nft_series = u32::from_le_bytes(entry[15..19].try_into().unwrap()); let balance = u64::from_le_bytes(entry[19..27].try_into().unwrap()); let asset_name = String::from_utf8_lossy(name_bytes).trim_end().to_string(); let formatted_balance = format_balance(balance); - if nft_series > 0 { - println!("{asset_name}:{nft_series} = {formatted_balance}"); - } else { - println!("{asset_name} = {formatted_balance}"); - } + balances.push(json!({ + "asset": asset_name, + "nft_series": nft_series, + "balance": formatted_balance + })); } + println!( + "{}", + serde_json::to_string_pretty(&json!({ "balances": balances })).unwrap() + ); connected = true; } else { let response_text = String::from_utf8_lossy(&response); diff --git a/src/bin/lookup_torrent.rs b/src/bin/lookup_torrent.rs index a4dc90f..b18fead 100644 --- a/src/bin/lookup_torrent.rs +++ b/src/bin/lookup_torrent.rs @@ -1,6 +1,7 @@ use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_startup::get_connections; use blockchain::env; +use blockchain::json; use blockchain::records::memory::response_channels::generate_uid; use blockchain::standalone_tools::connections::handshake; use blockchain::to_string_pretty; @@ -79,7 +80,11 @@ async fn main() { println!("{trimmed}"); connected = true; } else if !response.is_empty() { - println!("{}", hex::encode(response)); + println!( + "{}", + serde_json::to_string_pretty(&json!({ "hex": hex::encode(response) })) + .unwrap() + ); connected = true; } } diff --git a/src/bin/lookup_transaction.rs b/src/bin/lookup_transaction.rs index 63a6ccb..9d1d989 100644 --- a/src/bin/lookup_transaction.rs +++ b/src/bin/lookup_transaction.rs @@ -17,6 +17,7 @@ use blockchain::common::types::{ MARKETING_TYPE, REWARDS_TYPE, SWAP_TYPE, TRANSFER_TYPE, VANITY_ADDRESS_TYPE, }; use blockchain::env; +use blockchain::json; use blockchain::records::memory::response_channels::generate_uid; use blockchain::standalone_tools::connections::handshake; use blockchain::to_string_pretty; @@ -89,7 +90,11 @@ async fn main() { println!("{trimmed}"); connected = true; } else if !response.is_empty() { - println!("{}", hex::encode(response)); + println!( + "{}", + serde_json::to_string_pretty(&json!({ "hex": hex::encode(response) })) + .unwrap() + ); connected = true; } } @@ -163,5 +168,10 @@ async fn decode_transaction_to_json(response: &[u8]) -> Option { _ => return None, }; - Some(format!("block: {block_number}\n{json}")) + let transaction = serde_json::from_str::(&json).ok()?; + serde_json::to_string_pretty(&json!({ + "block": block_number, + "transaction": transaction + })) + .ok() } diff --git a/src/bin/validate_torrent_and_block_headers.rs b/src/bin/validate_torrent_and_block_headers.rs index 321054e..aa58457 100644 --- a/src/bin/validate_torrent_and_block_headers.rs +++ b/src/bin/validate_torrent_and_block_headers.rs @@ -1,10 +1,11 @@ use blockchain::common::binary_conversions::hex_to_u64; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_wallet_path}; use blockchain::common::network_paths_and_settings::block_extension_and_paths; -use blockchain::common::skein::{skein_128_hash_bytes, skein_256_hash_data}; +use blockchain::common::skein::skein_128_hash_bytes; use blockchain::encode; use blockchain::env; use blockchain::records::unpack_block::unpack_header::load_block_header; -use blockchain::records::wallet_registry::resolve_pubkey_from_short_address; +use blockchain::standalone_tools::wallet_registry_lookup::lookup_pubkey_from_live_registry; use blockchain::torrent::structs::Torrent; use blockchain::wallets::structures::Wallet; use blockchain::{AsyncReadExt, File}; @@ -15,8 +16,8 @@ use std::process; async fn main() { // Validate that a local block file, block header, and torrent metadata agree. let args: Vec = env::args().collect(); - if args.len() != 2 { - eprintln!("Usage: {} ", args[0]); + if !(2..=3).contains(&args.len()) { + eprintln!("Usage: {} [miner_public_key_hex]", args[0]); process::exit(1); } @@ -55,12 +56,25 @@ async fn main() { let header = load_block_header(block_number).await.unwrap(); let block_hash = header.hash().await; let block_difficulty = hex_to_u64(&block_hash).await.unwrap(); - let miner_pubkey = resolve_pubkey_from_short_address( - &blockchain::startup::initialize_startup::open_chain_state().await, - &header.unmined_block.miner, - ) - .unwrap_or(None) - .unwrap_or_default(); + let miner_pubkey = if let Some(public_key_hex) = args.get(2) { + Wallet::normalize_saved_public_key_bytes(public_key_hex).unwrap_or_else(|| { + eprintln!("Error: supplied miner public key is invalid."); + process::exit(1); + }) + } else { + 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; + lookup_pubkey_from_live_registry(&header.unmined_block.miner, &wallet_path, &encryption_key) + .await + .unwrap_or_else(|err| { + eprintln!("Error: {err}"); + process::exit(1); + }) + }; let miner_pubkey_hex = encode(&miner_pubkey); // Load the raw block bytes for file-size, info-hash, and piece-hash checks. @@ -73,9 +87,9 @@ async fn main() { let info_hash_computed = skein_128_hash_bytes(&block_data); - // Rebuild the unmined-block hash used by the miner proof signature. - let unmined_json = serde_json::to_string(&header.unmined_block).unwrap(); - let unmined_hash = skein_256_hash_data(&unmined_json); + // Rebuild the unmined-block hash using the same canonical path as mining + // and live block verification. + let unmined_hash = header.unmined_block.hash().await; let signature_ok = Wallet::verify_transaction_with_public_key(&unmined_hash, &header.proof, &miner_pubkey_hex) .await; diff --git a/src/bin/verify_address.rs b/src/bin/verify_address.rs index 1193436..d533667 100644 --- a/src/bin/verify_address.rs +++ b/src/bin/verify_address.rs @@ -1,105 +1,25 @@ +use blockchain::common::cli_prompts::prompt_visible; use blockchain::env; -use blockchain::from_str; -use blockchain::read_to_string; -use blockchain::tilde; use blockchain::wallets::structures::Wallet; -use blockchain::Value; -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; - -async fn read_address_file(path: &str) -> Result { - // Allow ~ in paths and then extract either a raw address or an address from wallet JSON. - let expanded_path = tilde(path).to_string(); - read_to_string(&expanded_path) - .await - .map_err(|e| format!("Failed to read {expanded_path}: {e}")) - .and_then(|contents| extract_address(&contents)) -} - -fn extract_address(contents: &str) -> Result { - // Address files may be a plain address string or a saved wallet JSON object. - let trimmed = contents.trim(); - if trimmed.is_empty() { - return Err("Address file is empty".to_string()); - } - - if trimmed.starts_with('{') { - // Wallet JSON address validation uses the canonical short address. - let value: Value = - from_str(trimmed).map_err(|e| format!("Failed to parse wallet JSON: {e}"))?; - let address = value - .get("short_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()); - } - - Ok(trimmed.to_string()) -} - -#[derive(RustyHelper, Completer, RustyHinter, RustyHighlighter, RustyValidator)] -struct PathHelper { - #[rustyline(Completer)] - completer: FilenameCompleter, -} - -fn prompt_for_path(prompt: &str) -> Result { - // Rustyline gives the interactive prompt filesystem completion. - let config = Config::builder() - .completion_type(CompletionType::List) - .build(); - let mut editor = - Editor::::with_config(config).map_err(|e| e.to_string())?; - editor.set_helper(Some(PathHelper { - completer: FilenameCompleter::new(), - })); - - match editor.readline(prompt) { - Ok(line) => Ok(line.trim().to_string()), - Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => { - Err("Input cancelled".to_string()) - } - Err(err) => Err(format!("Failed to read address file path: {err}")), - } -} #[tokio::main] async fn main() { - // Accept the address file path as an arg or prompt interactively. + // Accept a directly pasted address as an arg or prompt interactively. let args: Vec = env::args().collect(); if args.len() > 1 && args.len() != 2 { - println!("Usage: ./verify_address "); + println!("Usage: ./verify_address "); return; } - let address_path = if args.len() == 2 { - args[1].clone() + let address = if args.len() == 2 { + args[1].trim().to_string() } else { - match prompt_for_path("Please enter the path to the file containing the wallet address: ") { - Ok(path) => path, - Err(err) => { - eprintln!("{err}"); - return; - } - } + prompt_visible("Please enter the wallet address: ") + .await + .trim() + .to_string() }; - let address = match read_address_file(&address_path).await { - Ok(address) => address, - Err(err) => { - println!("{err}"); - return; - } - }; - - let message_hash = Wallet::short_address_validation(address.trim()); - - println!("{message_hash}"); + println!("{}", Wallet::short_address_validation(&address)); } diff --git a/src/bin/verify_message.rs b/src/bin/verify_message.rs index d1db985..208ef41 100644 --- a/src/bin/verify_message.rs +++ b/src/bin/verify_message.rs @@ -1,97 +1,17 @@ -use blockchain::common::cli_prompts::prompt_visible; -use blockchain::common::network_paths_and_settings::block_extension_and_paths; +use blockchain::common::cli_prompts::{prompt_hidden_nonempty, prompt_visible, prompt_wallet_path}; use blockchain::common::skein::skein_256_hash_data; use blockchain::env; -use blockchain::from_str; -use blockchain::read_to_string; -use blockchain::records::wallet_registry::resolve_pubkey_from_short_address; -use blockchain::sled; -use blockchain::tilde; +use blockchain::standalone_tools::wallet_registry_lookup::lookup_pubkey_from_live_registry; use blockchain::wallets::structures::Wallet; -use blockchain::Value; -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; - -async fn read_input_file(path: &str) -> Result { - // Signature input files are plain text and are trimmed before verification. - let expanded_path = tilde(path).to_string(); - read_to_string(&expanded_path) - .await - .map(|s| s.trim().to_string()) - .map_err(|e| format!("Failed to read {expanded_path}: {e}")) -} - -async fn read_address_file(path: &str) -> Result { - // Allow ~ in paths and then extract either a raw address or an address from wallet JSON. - let expanded_path = tilde(path).to_string(); - read_to_string(&expanded_path) - .await - .map_err(|e| format!("Failed to read {expanded_path}: {e}")) - .and_then(|contents| extract_address(&contents)) -} - -fn extract_address(contents: &str) -> Result { - // Address files may be a plain address string or a saved wallet JSON object. - let trimmed = contents.trim(); - if trimmed.is_empty() { - return Err("Address file is empty".to_string()); - } - - if trimmed.starts_with('{') { - // Signature verification uses the canonical short address. - let value: Value = - from_str(trimmed).map_err(|e| format!("Failed to parse wallet JSON: {e}"))?; - let address = value - .get("short_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()); - } - - Ok(trimmed.to_string()) -} - -#[derive(RustyHelper, Completer, RustyHinter, RustyHighlighter, RustyValidator)] -struct PathHelper { - #[rustyline(Completer)] - completer: FilenameCompleter, -} - -fn prompt_for_path(prompt: &str) -> Result { - // Rustyline gives the interactive prompt filesystem completion. - let config = Config::builder() - .completion_type(CompletionType::List) - .build(); - let mut editor = - Editor::::with_config(config).map_err(|e| e.to_string())?; - editor.set_helper(Some(PathHelper { - completer: FilenameCompleter::new(), - })); - - match editor.readline(prompt) { - Ok(line) => Ok(line.trim().to_string()), - Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => { - Err("Input cancelled".to_string()) - } - Err(err) => Err(format!("Failed to read file path: {err}")), - } -} #[tokio::main] async fn main() { - // Accept all verification inputs as args or prompt interactively. + // Accept direct pasted values. Verification needs a live peer registry + // lookup because the local sled database may already be locked by a node. let args: Vec = env::args().collect(); - let address_path: String; - let signature_path: String; if args.len() > 1 && args.len() != 4 { - println!("Usage: ./verify_message \"\" "); + println!("Usage: ./verify_message \"\" "); return; } @@ -101,74 +21,46 @@ async fn main() { prompt_visible("Please enter the message to verify: ").await }; - if args.len() == 4 { - address_path = args[2].clone(); - signature_path = args[3].clone(); + let address = if args.len() == 4 { + args[2].trim().to_string() } else { - address_path = match prompt_for_path( - "Please enter the path to the file containing the wallet address: ", - ) { - Ok(path) => path, + prompt_visible("Please enter the wallet address: ") + .await + .trim() + .to_string() + }; + + let signature = if args.len() == 4 { + args[3].trim().to_string() + } else { + prompt_visible("Please enter the signature: ") + .await + .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 pubkey = + match lookup_pubkey_from_live_registry(&address, &wallet_path, &encryption_key).await { + Ok(pubkey) => pubkey, Err(err) => { eprintln!("{err}"); return; } }; - signature_path = - match prompt_for_path("Please enter the path to the file containing the signature: ") { - Ok(path) => path, - Err(err) => { - eprintln!("{err}"); - return; - } - }; - } - - let address = match read_address_file(&address_path).await { - Ok(address) => address, - Err(err) => { - println!("{err}"); - return; - } - }; - let signature = match read_input_file(&signature_path).await { - Ok(signature) => signature, - Err(err) => { - println!("{err}"); - return; - } - }; - - // Hash the message exactly as sign_message does before verifying the wallet signature. + // Hash the message exactly as sign_message does before verifying the detached signature. let message_hash = skein_256_hash_data(message.as_str()); - let ( - _network_name, - _padded_base_coin, - _suffix, - _torrent_path, - _wallet_path, - _block_path, - db_path, - _balance_path, - _log_path, - ) = block_extension_and_paths(); - let db = match sled::open(&db_path) { - Ok(db) => db, - Err(err) => { - eprintln!("Failed to open wallet registry database: {err}"); - return; - } - }; - let signature = match resolve_pubkey_from_short_address(&db, address.trim()) { - Ok(Some(pubkey)) => { - Wallet::verify_transaction_with_public_key_bytes(&message_hash, &signature, &pubkey) - .await - } - _ => false, - }; + let valid = + Wallet::verify_transaction_with_public_key_bytes(&message_hash, &signature, &pubkey).await; - if signature { + if valid { println!("valid signature"); } else { println!("invalid signature"); diff --git a/src/bin/verify_sign_loan_tx.rs b/src/bin/verify_sign_loan_tx.rs index b92393c..f908d91 100644 --- a/src/bin/verify_sign_loan_tx.rs +++ b/src/bin/verify_sign_loan_tx.rs @@ -279,5 +279,5 @@ async fn main() { return; } - println!("Transaction: {output_str}"); + println!("{output_str}"); } diff --git a/src/bin/verify_sign_swap_tx.rs b/src/bin/verify_sign_swap_tx.rs index 67f5e1d..09855aa 100644 --- a/src/bin/verify_sign_swap_tx.rs +++ b/src/bin/verify_sign_swap_tx.rs @@ -250,5 +250,5 @@ async fn main() { return; } - println!("Transaction: {output_str}"); + println!("{output_str}"); } diff --git a/src/standalone_tools/connections/sending_request.rs b/src/standalone_tools/connections/sending_request.rs index 6d1d4b6..e7b832e 100644 --- a/src/standalone_tools/connections/sending_request.rs +++ b/src/standalone_tools/connections/sending_request.rs @@ -11,6 +11,7 @@ use crate::rpc::command_maps::{ RPC_TIME, RPC_TOKEN_CATALOG, RPC_TOKEN_DETAILS, RPC_TOKEN_LIST, RPC_TORRENT_BY_HEIGHT, RPC_TOTAL_CONFIRMED_TX, RPC_TRANSACTION_BY_TXID, RPC_UNBLOCK_IP, RPC_VALIDATE_MESSAGE, RPC_VANITY_LOOKUP, RPC_VANITY_OWNER_LOOKUP, RPC_WALLET_REGISTRATION_STATUS, + RPC_WALLET_REGISTRY_SYNC, }; use crate::standalone_tools::transaction_creator::create_transaction_request; use crate::timeout; @@ -486,6 +487,12 @@ async fn build_request_bytes( bin_msg.extend_from_slice(&public_key_bytes); bin_msg.extend_from_slice(&signature_bytes); } + // Request the deterministic wallet registry snapshot. + 39 => { + let command_number: u8 = RPC_WALLET_REGISTRY_SYNC; + bin_msg.extend_from_slice(&command_number.to_le_bytes()); + bin_msg.extend_from_slice(&hashmap_key); + } // Lookup remote balance by long, short, or vanity address. 40 => { let command_number: u8 = RPC_VANITY_LOOKUP; diff --git a/src/standalone_tools/loan_lookup_json.rs b/src/standalone_tools/loan_lookup_json.rs new file mode 100644 index 0000000..f648407 --- /dev/null +++ b/src/standalone_tools/loan_lookup_json.rs @@ -0,0 +1,111 @@ +use crate::{json, Map, Value}; + +fn scalar_value(value: &str) -> Value { + let trimmed = value.trim(); + if trimmed.is_empty() { + return Value::String(String::new()); + } + + if trimmed.chars().all(|ch| ch.is_ascii_digit()) { + if let Ok(number) = trimmed.parse::() { + return json!(number); + } + } + + if trimmed.contains('.') + && trimmed + .chars() + .all(|ch| ch.is_ascii_digit() || ch == '.' || ch == '-') + { + if let Ok(number) = trimmed.parse::() { + return json!(number); + } + } + + Value::String(trimmed.to_string()) +} + +fn insert_payment_field(payments: &mut Vec>, key: &str, value: &str) -> bool { + let Some(rest) = key.strip_prefix("payment_") else { + return false; + }; + let Some((index_text, field)) = rest.split_once('_') else { + return false; + }; + let Ok(index) = index_text.parse::() else { + return false; + }; + if index == 0 { + return false; + } + + while payments.len() < index { + payments.push(Map::new()); + } + payments[index - 1].insert(field.to_string(), scalar_value(value)); + true +} + +pub fn parse_loan_summary(text: &str) -> Value { + let mut loan = Map::new(); + let mut payments = Vec::>::new(); + + for line in text.lines().map(str::trim).filter(|line| !line.is_empty()) { + let Some((key, value)) = line.split_once('=') else { + continue; + }; + if insert_payment_field(&mut payments, key.trim(), value) { + continue; + } + loan.insert(key.trim().to_string(), scalar_value(value)); + } + + loan.insert( + "payments".to_string(), + Value::Array(payments.into_iter().map(Value::Object).collect()), + ); + Value::Object(loan) +} + +pub fn parse_loan_lookup_by_address(text: &str) -> Value { + let mut address = String::new(); + let mut declared_contracts = None; + let mut contracts = Vec::::new(); + let mut current_contract = Vec::::new(); + + for line in text.lines().map(str::trim) { + if line.is_empty() { + continue; + } + + if let Some(value) = line.strip_prefix("address=") { + address = value.trim().to_string(); + continue; + } + + if let Some(value) = line.strip_prefix("contracts=") { + declared_contracts = value.trim().parse::().ok(); + continue; + } + + if line.starts_with("contract_") && line.ends_with(':') { + if !current_contract.is_empty() { + contracts.push(parse_loan_summary(¤t_contract.join("\n"))); + current_contract.clear(); + } + continue; + } + + current_contract.push(line.to_string()); + } + + if !current_contract.is_empty() { + contracts.push(parse_loan_summary(¤t_contract.join("\n"))); + } + + json!({ + "address": address, + "contracts": declared_contracts.unwrap_or(contracts.len() as u64), + "loans": contracts + }) +} diff --git a/src/standalone_tools/mod.rs b/src/standalone_tools/mod.rs index a0f0362..83c54d3 100644 --- a/src/standalone_tools/mod.rs +++ b/src/standalone_tools/mod.rs @@ -1,4 +1,6 @@ // The standalone_tools module holds utility helpers that mirror node serialization and connection behavior. pub mod connections; +pub mod loan_lookup_json; pub mod transaction_creator; pub mod vanity_resolver; +pub mod wallet_registry_lookup; diff --git a/src/standalone_tools/wallet_registry_lookup.rs b/src/standalone_tools/wallet_registry_lookup.rs new file mode 100644 index 0000000..13237c7 --- /dev/null +++ b/src/standalone_tools/wallet_registry_lookup.rs @@ -0,0 +1,55 @@ +use crate::common::network_startup::get_connections; +use crate::records::memory::response_channels::generate_uid; +use crate::rpc::commands::wallet_registry_sync::WALLET_REGISTRY_RECORD_BYTES; +use crate::standalone_tools::connections::handshake; +use crate::wallets::structures::Wallet; + +pub async fn lookup_pubkey_from_live_registry( + address: &str, + wallet_path: &str, + encryption_key: &str, +) -> Result, String> { + let short_address = Wallet::normalize_to_short_address(address) + .ok_or_else(|| format!("Invalid wallet address: {address}"))?; + let short_address_bytes = Wallet::short_address_to_bytes(&short_address) + .ok_or_else(|| format!("Invalid short wallet address: {short_address}"))?; + + for conn in get_connections().await { + let Ok(socket_address) = conn.parse() else { + continue; + }; + + let result = handshake::connect_and_handshake( + socket_address, + String::new(), + 39, + handshake::HandshakeWallet::WalletKey { + encryption_key: encryption_key.to_string(), + wallet_path: wallet_path.to_string(), + }, + generate_uid(), + ) + .await; + + let Ok(response) = result else { + continue; + }; + + for chunk in response.chunks_exact(WALLET_REGISTRY_RECORD_BYTES) { + let registered_address = &chunk[..Wallet::SHORT_ADDRESS_BYTES_LENGTH]; + if registered_address == short_address_bytes.as_slice() { + let public_key = chunk[Wallet::SHORT_ADDRESS_BYTES_LENGTH..].to_vec(); + if Wallet::has_valid_public_key_bytes(&public_key) { + return Ok(public_key); + } + return Err(format!( + "Registry returned an invalid public key for {short_address}" + )); + } + } + } + + Err(format!( + "Could not find registered public key for {short_address} from any configured peer." + )) +}