use blockchain::blocks::loans::UnsignedLoanContractTransaction; use blockchain::common::cli_prompts::{ask_yes_no_question, prompt_hidden_nonempty}; 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::to_string_pretty; use blockchain::wallets::structures::Wallet; use blockchain::Value; use blockchain::{Local, TimeZone}; fn display_amount(value: u64) -> f64 { // Transaction JSON stores atomic units; prompts show whole-coin decimals. value as f64 / 100_000_000.0 } fn display_payment_period(value: &str) -> &'static str { // Loan transactions store the payment period as a compact one-letter code. match value.trim().to_lowercase().as_str() { "d" => "daily", "w" => "weekly", "m" => "monthly", _ => "unknown", } } fn display_start_date(timestamp: u32) -> String { // Show the stored timestamp as a local calendar date for borrower review. match Local.timestamp_opt(timestamp as i64, 0).single() { Some(datetime) => datetime.format("%Y-%m-%d").to_string(), None => "invalid date".to_string(), } } fn normalize_short_address_input(address: &str) -> Result { // Accept local vanity/short input and resolve it into the real short address. resolve_local_input_short_address(address.trim()) } #[tokio::main] async fn main() { // Borrowers use this tool to review a lender-signed loan and add signature2. let args: Vec = env::args().collect(); if args.len() != 2 { println!("Usage: ./validate_sign_loan_tx "); return; } let filename = &args[1]; let contents = match read_to_string(filename).await { Ok(contents) => contents, Err(_) => { println!("Error reading file: {filename}"); return; } }; let json: Result = from_str(&contents); let json = match json { Ok(json) => json, Err(_) => { println!("Error parsing JSON in file: {filename}"); return; } }; // Extract every field that participates in the hash so the borrower signs the same bytes. let txtype = 7; let timestamp = json["timestamp"].as_u64().unwrap_or_default() as u32; let loan_coin = json["loan_coin"].as_str().unwrap_or_default().to_string(); let loan_amount = json["loan_amount"].as_u64().unwrap_or_default(); let lender = match normalize_short_address_input(json["lender"].as_str().unwrap_or_default()) { Ok(address) => address, Err(_) => { println!("Transaction is not valid. Lender address is not a valid short address."); return; } }; let 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()) { Ok(address) => address, Err(_) => { println!( "Transaction is not valid. Borrower address is not a valid short address." ); return; } }; let payment_period = json["payment_period"] .as_str() .unwrap_or_default() .to_string(); let payment_number = json["payment_number"].as_u64().unwrap_or_default() as u8; let payment_amount = json["payment_amount"].as_u64().unwrap_or_default(); let grace_period = json["grace_period"].as_u64().unwrap_or_default() as u8; let max_late_value = json["max_late_value"].as_u64().unwrap_or_default(); let txfee = json["txfee"].as_u64().unwrap_or_default(); let original_hash = json["hash"].as_str().unwrap_or_default().to_string(); let signature1 = json["signature1"].as_str().unwrap_or_default().to_string(); // The borrower must explicitly confirm the human-readable loan terms before signing. let question1 = format!( "Are you expecting to receive {} {}?", display_amount(loan_amount), loan_coin.trim() ); let question2 = format!( "Are you willing to lock {} {} as collateral?", display_amount(collateral_amount), collateral.trim() ); let question3 = format!( "Do you agree that payments should be made {}?", display_payment_period(&payment_period) ); let question4 = format!( "Do you agree that this loan requires {payment_number} total payments?" ); let question5 = format!( "Do you agree that each payment should be {} {}?", display_amount(payment_amount), loan_coin.trim() ); let question6 = format!( "Do you agree that {grace_period} missed payments allows the lender to claim the collateral?" ); let question7 = format!( "Do you agree that being {} {} behind allows the lender to claim the collateral?", display_amount(max_late_value), loan_coin.trim() ); let question8 = format!( "Do you agree that this loan begins on {}?", display_start_date(timestamp) ); let question9 = format!("Do you agree that the lender wallet is {}?", lender.trim()); if !ask_yes_no_question(&question1).await { println!("Transaction is not valid"); return; } if !ask_yes_no_question(&question2).await { println!("Transaction is not valid"); return; } if !ask_yes_no_question(&question3).await { println!("Transaction is not valid"); return; } if !ask_yes_no_question(&question4).await { println!("Transaction is not valid"); return; } if !ask_yes_no_question(&question5).await { println!("Transaction is not valid"); return; } if !ask_yes_no_question(&question6).await { println!("Transaction is not valid"); return; } if !ask_yes_no_question(&question7).await { println!("Transaction is not valid"); return; } if !ask_yes_no_question(&question8).await { println!("Transaction is not valid"); return; } if !ask_yes_no_question(&question9).await { println!("Transaction is not valid"); 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}" ); return; } if !Wallet::short_address_validation(address.trim()) { println!("Your wallet short address is not valid: {address}"); return; } // Rebuild the unsigned loan and sign it with the borrower wallet. let unsigned_loan = UnsignedLoanContractTransaction::new( txtype, timestamp, &loan_coin, loan_amount, &lender, &collateral, collateral_amount, &borrower, &payment_period, payment_number, payment_amount, grace_period, max_late_value, txfee, ) .await; let (hash, signature2) = match unsigned_loan.hash_and_sign(private_key).await { Ok(value) => value, Err(err) => { println!("Signing transaction failed: {err}"); return; } }; // If the rebuilt hash differs, the JSON was changed after the lender signed it. if hash != original_hash { println!("Signing transaction failed. The included hash was incorrect."); return; } // Save the fully signed loan contract JSON for broadcast. let output = json!({ "txtype": txtype, "timestamp": timestamp, "loan_coin": loan_coin, "loan_amount": loan_amount, "lender": lender, "collateral": collateral, "collateral_amount": collateral_amount, "borrower": borrower, "payment_period": payment_period, "payment_number": payment_number, "payment_amount": payment_amount, "grace_period": grace_period, "max_late_value": max_late_value, "txfee": txfee, "hash": hash, "signature1": signature1, "signature2": signature2, }); let output_str = to_string_pretty(&output).expect("Failed to serialize JSON"); // Transaction creation tools all write into ./transactions using the transaction hash. let dir_path = "./transactions"; if let Err(e) = fs::create_dir_all(dir_path) { eprintln!("Failed to create directory: {e}"); return; } let file_path = format!( "{}/{}.json", dir_path, output["hash"].as_str().unwrap_or_default() ); if let Err(e) = fs::write(&file_path, &output_str) { eprintln!("Failed to write file: {e}"); return; } println!("Transaction: {output_str}"); }