cli tool fixes

This commit is contained in:
viraladmin 2026-06-29 10:05:30 -06:00
parent 4fad309fc2
commit 12916c0b11
38 changed files with 602 additions and 297 deletions

View File

@ -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 <contract_hash>
lookup_loan_by_hash <loan_hash>
```
Expected reply:
```text
<contract data or peer error>
```json
{
"contract": "<loan hash>",
"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 <wallet_address>
lookup_loan_by_address <wallet_address>
```
Expected reply:
```text
<contract data or peer error>
```json
{
"address": "<wallet address>",
"contracts": 1,
"loans": []
}
```
### lookup_remote_balance

View File

@ -139,5 +139,5 @@ async fn main() {
return;
}
println!("transaction: {output_str}");
println!("{output_str}");
}

View File

@ -115,5 +115,5 @@ async fn main() {
return;
}
println!("transaction: {output_str}");
println!("{output_str}");
}

View File

@ -123,5 +123,5 @@ async fn main() {
return;
}
println!("transaction: {output_str}");
println!("{output_str}");
}

View File

@ -137,5 +137,5 @@ async fn main() {
return;
}
println!("transaction: {output_str}");
println!("{output_str}");
}

View File

@ -256,5 +256,5 @@ async fn main() {
return;
}
println!("transaction: {output_str}");
println!("{output_str}");
}

View File

@ -174,5 +174,5 @@ async fn main() {
return;
}
println!("transaction: {output_str}");
println!("{output_str}");
}

View File

@ -156,5 +156,5 @@ async fn main() {
return;
}
println!("transaction: {output_str}");
println!("{output_str}");
}

View File

@ -237,5 +237,5 @@ async fn main() {
return;
}
println!("transaction: {output_str}");
println!("{output_str}");
}

View File

@ -143,5 +143,5 @@ async fn main() {
return;
}
println!("transaction: {output_str}");
println!("{output_str}");
}

View File

@ -246,5 +246,5 @@ async fn main() {
return;
}
println!("transaction: {output_str}");
println!("{output_str}");
}

View File

@ -169,5 +169,5 @@ async fn main() {
return;
}
println!("transaction: {output_str}");
println!("{output_str}");
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String> = env::args().collect();
if args.len() != 2 {
println!("Usage: ./lookup_loan_by_address <wallet_address>");
return;
}
let wallet_address = match args[1].parse::<String>() {
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");
}
}

View File

@ -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<String> = env::args().collect();
if args.len() != 2 {
println!("Usage: ./lookup_loan_by_hash <loan_hash>");
return;
}
let contract_hash = match args[1].parse::<String>() {
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");
}
}

View File

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

View File

@ -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::<serde_json::Value>(&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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String> {
_ => return None,
};
Some(format!("block: {block_number}\n{json}"))
let transaction = serde_json::from_str::<serde_json::Value>(&json).ok()?;
serde_json::to_string_pretty(&json!({
"block": block_number,
"transaction": transaction
}))
.ok()
}

View File

@ -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<String> = env::args().collect();
if args.len() != 2 {
eprintln!("Usage: {} <block_number>", args[0]);
if !(2..=3).contains(&args.len()) {
eprintln!("Usage: {} <block_number> [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;

View File

@ -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<String, String> {
// 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<String, String> {
// 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<String, String> {
// Rustyline gives the interactive prompt filesystem 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(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<String> = env::args().collect();
if args.len() > 1 && args.len() != 2 {
println!("Usage: ./verify_address <address_file>");
println!("Usage: ./verify_address <wallet_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));
}

View File

@ -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<String, String> {
// 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<String, String> {
// 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<String, String> {
// 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<String, String> {
// Rustyline gives the interactive prompt filesystem 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(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<String> = env::args().collect();
let address_path: String;
let signature_path: String;
if args.len() > 1 && args.len() != 4 {
println!("Usage: ./verify_message \"<message to verify>\" <address_file> <signature_file>");
println!("Usage: ./verify_message \"<message to verify>\" <wallet_address> <signature>");
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");

View File

@ -279,5 +279,5 @@ async fn main() {
return;
}
println!("Transaction: {output_str}");
println!("{output_str}");
}

View File

@ -250,5 +250,5 @@ async fn main() {
return;
}
println!("Transaction: {output_str}");
println!("{output_str}");
}

View File

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

View File

@ -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::<u64>() {
return json!(number);
}
}
if trimmed.contains('.')
&& trimmed
.chars()
.all(|ch| ch.is_ascii_digit() || ch == '.' || ch == '-')
{
if let Ok(number) = trimmed.parse::<f64>() {
return json!(number);
}
}
Value::String(trimmed.to_string())
}
fn insert_payment_field(payments: &mut Vec<Map<String, Value>>, 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::<usize>() 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::<Map<String, Value>>::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::<Value>::new();
let mut current_contract = Vec::<String>::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::<u64>().ok();
continue;
}
if line.starts_with("contract_") && line.ends_with(':') {
if !current_contract.is_empty() {
contracts.push(parse_loan_summary(&current_contract.join("\n")));
current_contract.clear();
}
continue;
}
current_contract.push(line.to_string());
}
if !current_contract.is_empty() {
contracts.push(parse_loan_summary(&current_contract.join("\n")));
}
json!({
"address": address,
"contracts": declared_contracts.unwrap_or(contracts.len() as u64),
"loans": contracts
})
}

View File

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

View File

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